From ccdefb3b16c31b911f4f26b901939c012b53d712 Mon Sep 17 00:00:00 2001 From: Sebastian Willing Date: Mon, 11 Jun 2018 17:25:28 +0200 Subject: [PATCH 001/957] Documentation: * Add information about return value of NewSheet() * Minor documentation language fixes Samples: * Added sample go file for dumping a XLSX file to the console --- samples/dumpXLSX.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ sheet.go | 13 +++++++------ 2 files changed, 52 insertions(+), 6 deletions(-) create mode 100644 samples/dumpXLSX.go diff --git a/samples/dumpXLSX.go b/samples/dumpXLSX.go new file mode 100644 index 0000000000..853a337ee5 --- /dev/null +++ b/samples/dumpXLSX.go @@ -0,0 +1,45 @@ +package main + +import ( + "fmt" + "os" + + "github.com/360EntSecGroup-Skylar/excelize" +) + +func main() { + // Exit on missing filename + if len(os.Args) < 2 || os.Args[1] == "" { + fmt.Println("Syntax: dumpXLSX ") + os.Exit(1) + } + + // Open file and panic on error + fmt.Println("Reading ", os.Args[1]) + xlsx, err := excelize.OpenFile(os.Args[1]) + if err != nil { + panic(err) + } + + // Read all sheets in map + for i, sheet := range xlsx.GetSheetMap() { + //Output sheet header + fmt.Printf("----- %d. %s -----\n", i, sheet) + + // Get rows + rows := xlsx.GetRows(sheet) + // Create a row number prefix pattern long enough to fit all row numbers + prefixPattern := fmt.Sprintf("%% %dd ", len(fmt.Sprintf("%d", len(rows)))) + + // Walk through rows + for j, row := range rows { + // Output row number as prefix + fmt.Printf(prefixPattern, j) + // Output row content + for _, cell := range row { + fmt.Print(cell, "\t") + } + fmt.Println() + } + } +} diff --git a/sheet.go b/sheet.go index 9e8f4c99f8..d087df4d41 100644 --- a/sheet.go +++ b/sheet.go @@ -12,9 +12,10 @@ import ( "unicode/utf8" ) -// NewSheet provides function to create a new sheet by given worksheet name, -// when creating a new XLSX file, the default sheet will be create, when you -// create a new file. +// NewSheet provides function to create a new sheet by given worksheet name. +// When creating a new XLSX file, the default sheet will be created. +// Returns the number of sheets in the workbook (file) after appending the new +// sheet. func (f *File) NewSheet(name string) int { // Check if the worksheet already exists if f.GetSheetIndex(name) != 0 { @@ -202,8 +203,8 @@ func replaceRelationshipsNameSpaceBytes(workbookMarshal []byte) []byte { } // SetActiveSheet provides function to set default active worksheet of XLSX by -// given index. Note that active index is different with the index that got by -// function GetSheetMap, and it should be greater than 0 and less than total +// given index. Note that active index is different from the index returned by +// function GetSheetMap(). It should be greater than 0 and less than total // worksheet numbers. func (f *File) SetActiveSheet(index int) { if index < 1 { @@ -237,7 +238,7 @@ func (f *File) SetActiveSheet(index int) { } } -// GetActiveSheetIndex provides function to get active sheet of XLSX. If not +// GetActiveSheetIndex provides function to get active sheet index of the XLSX. If not // found the active sheet will be return integer 0. func (f *File) GetActiveSheetIndex() int { buffer := bytes.Buffer{} From a885bb0fb92dd7a55f27f0fd57d174121db79b0f Mon Sep 17 00:00:00 2001 From: Alex Whitney Date: Wed, 25 Jul 2018 10:40:08 -0400 Subject: [PATCH 002/957] Add failing unit tests for issue-252 --- styles_test.go | 134 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 styles_test.go diff --git a/styles_test.go b/styles_test.go new file mode 100644 index 0000000000..7a7e228531 --- /dev/null +++ b/styles_test.go @@ -0,0 +1,134 @@ +package excelize + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSetConditionalFormat(t *testing.T) { + cases := []struct { + label string + format string + 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" + }]`, + rules: []*xlsxCfRule{{ + Priority: 1, + Type: "colorScale", + ColorScale: &xlsxColorScale{ + Cfvo: []*xlsxCfvo{{ + Type: "num", + Val: -10, + }, { + Type: "num", + Val: 0, + }, { + Type: "num", + Val: 10, + }}, + Color: []*xlsxColor{{ + RGB: "FFFF0000", + }, { + RGB: "FF00FF00", + }, { + RGB: "FF0000FF", + }}, + }, + }}, + }, { + 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" + }]`, + rules: []*xlsxCfRule{{ + Priority: 1, + Type: "colorScale", + ColorScale: &xlsxColorScale{ + Cfvo: []*xlsxCfvo{{ + Type: "num", + Val: 0, + }, { + Type: "num", + Val: 50, + }, { + Type: "num", + Val: 0, + }}, + Color: []*xlsxColor{{ + RGB: "FFFF0000", + }, { + RGB: "FF00FF00", + }, { + RGB: "FF0000FF", + }}, + }, + }}, + }, { + 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" + }]`, + rules: []*xlsxCfRule{{ + Priority: 1, + Type: "colorScale", + ColorScale: &xlsxColorScale{ + Cfvo: []*xlsxCfvo{{ + Type: "num", + Val: 0, + }, { + Type: "num", + Val: 0, + }}, + Color: []*xlsxColor{{ + RGB: "FFFF0000", + }, { + RGB: "FF0000FF", + }}, + }, + }}, + }} + + for _, testCase := range cases { + xl := NewFile() + const sheet = "Sheet1" + const cellRange = "A1:A1" + + err := xl.SetConditionalFormat(sheet, cellRange, testCase.format) + if err != nil { + t.Fatalf("%s", err) + } + + xlsx := xl.workSheetReader(sheet) + cf := xlsx.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.EqualValues(t, testCase.rules, cf[0].CfRule, testCase.label) + } +} From db7a605cf8384f86dd3d3f766050a201b5f10eec Mon Sep 17 00:00:00 2001 From: Alex Whitney Date: Tue, 24 Jul 2018 16:12:26 -0400 Subject: [PATCH 003/957] Use min/mid/max value for 2 and 3 color scale conditional formatting --- styles.go | 19 ++++++++++++++++--- styles_test.go | 16 ++++++++-------- xmlWorksheet.go | 2 +- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/styles.go b/styles.go index 251e335cf6..fdbf932d0d 100644 --- a/styles.go +++ b/styles.go @@ -2674,12 +2674,25 @@ func drawCondFmtDuplicateUniqueValues(p int, ct string, format *formatConditiona // 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 { + minValue := format.MinValue + if minValue == "" { + minValue = "0" + } + maxValue := format.MaxValue + if maxValue == "" { + maxValue = "0" + } + midValue := format.MidValue + if midValue == "" { + midValue = "50" + } + c := &xlsxCfRule{ Priority: p + 1, Type: "colorScale", ColorScale: &xlsxColorScale{ Cfvo: []*xlsxCfvo{ - {Type: format.MinType}, + {Type: format.MinType, Val: minValue}, }, Color: []*xlsxColor{ {RGB: getPaletteColor(format.MinColor)}, @@ -2687,10 +2700,10 @@ func drawCondFmtColorScale(p int, ct string, format *formatConditional) *xlsxCfR }, } if validType[format.Type] == "3_color_scale" { - c.ColorScale.Cfvo = append(c.ColorScale.Cfvo, &xlsxCfvo{Type: format.MidType, Val: 50}) + c.ColorScale.Cfvo = append(c.ColorScale.Cfvo, &xlsxCfvo{Type: format.MidType, Val: midValue}) c.ColorScale.Color = append(c.ColorScale.Color, &xlsxColor{RGB: getPaletteColor(format.MidColor)}) } - c.ColorScale.Cfvo = append(c.ColorScale.Cfvo, &xlsxCfvo{Type: format.MaxType}) + 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 } diff --git a/styles_test.go b/styles_test.go index 7a7e228531..baa66f0f11 100644 --- a/styles_test.go +++ b/styles_test.go @@ -32,13 +32,13 @@ func TestSetConditionalFormat(t *testing.T) { ColorScale: &xlsxColorScale{ Cfvo: []*xlsxCfvo{{ Type: "num", - Val: -10, + Val: "-10", }, { Type: "num", - Val: 0, + Val: "0", }, { Type: "num", - Val: 10, + Val: "10", }}, Color: []*xlsxColor{{ RGB: "FFFF0000", @@ -67,13 +67,13 @@ func TestSetConditionalFormat(t *testing.T) { ColorScale: &xlsxColorScale{ Cfvo: []*xlsxCfvo{{ Type: "num", - Val: 0, + Val: "0", }, { Type: "num", - Val: 50, + Val: "50", }, { Type: "num", - Val: 0, + Val: "0", }}, Color: []*xlsxColor{{ RGB: "FFFF0000", @@ -100,10 +100,10 @@ func TestSetConditionalFormat(t *testing.T) { ColorScale: &xlsxColorScale{ Cfvo: []*xlsxCfvo{{ Type: "num", - Val: 0, + Val: "0", }, { Type: "num", - Val: 0, + Val: "0", }}, Color: []*xlsxColor{{ RGB: "FFFF0000", diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 37c0d18e0d..87d66a1cf2 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -446,7 +446,7 @@ type xlsxIconSet struct { type xlsxCfvo struct { Gte bool `xml:"gte,attr,omitempty"` Type string `xml:"type,attr,omitempty"` - Val int `xml:"val,attr"` + Val string `xml:"val,attr"` ExtLst *xlsxExtLst `xml:"extLst"` } From b11b95a69a1df3da6b53c7d2955cb13403e805f1 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 27 Jul 2018 10:11:13 +0800 Subject: [PATCH 004/957] Update issue templates Issue templates have been added. --- .github/ISSUE_TEMPLATE/bug_report.md | 44 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 44 +++++++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000000..a8c31a04b3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,44 @@ +--- +name: Bug report +about: Create a report to help us improve + +--- + + + +**Description** + + + +**Steps to reproduce the issue:** +1. +2. +3. + +**Describe the results you received:** + +**Describe the results you expected:** + +**Output of `go version`:** + +```text +(paste your output here) +``` + +**Excelize version or commit ID:** + +```text +(paste here) +``` + +**Environment details (OS, Microsoft Excel™ version, physical, etc.):** diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000000..1737cd4421 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,44 @@ +--- +name: Feature request +about: Suggest an idea for this project + +--- + + + +**Description** + + + +**Steps to reproduce the issue:** +1. +2. +3. + +**Describe the results you received:** + +**Describe the results you expected:** + +**Output of `go version`:** + +```text +(paste your output here) +``` + +**Excelize version or commit ID:** + +```text +(paste here) +``` + +**Environment details (OS, Microsoft Excel™ version, physical, etc.):** From df6b0d9ff42e9c216b2bf7a30d36bd0c09def317 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 29 Jul 2018 14:00:21 +0800 Subject: [PATCH 005/957] Create PULL_REQUEST_TEMPLATE.md --- PULL_REQUEST_TEMPLATE.md | 45 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 PULL_REQUEST_TEMPLATE.md diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000..d2ac755e96 --- /dev/null +++ b/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,45 @@ +# PR Details + + + +## Description + + + +## Related Issue + + + + + + +## Motivation and Context + + + +## How Has This Been Tested + + + + + +## Types of changes + + + +- [ ] Docs change / refactoring / dependency upgrade +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to change) + +## Checklist + + + + +- [ ] My code follows the code style of this project. +- [ ] My change requires a change to the documentation. +- [ ] I have updated the documentation accordingly. +- [ ] I have read the **CONTRIBUTING** document. +- [ ] I have added tests to cover my changes. +- [ ] All new and existing tests passed. From efe3219af0c9f8c8248bb5c6ac51e4c7a900396b Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 30 Jul 2018 10:06:22 +0800 Subject: [PATCH 006/957] Delete ISSUE_TEMPLATE.md --- .github/ISSUE_TEMPLATE.md | 38 -------------------------------------- 1 file changed, 38 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE.md diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 5d7e931c4a..0000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,38 +0,0 @@ - - -**Description** - - - -**Steps to reproduce the issue:** -1. -2. -3. - -**Describe the results you received:** - -**Describe the results you expected:** - -**Output of `go version`:** - -```text -(paste your output here) -``` - -**Excelize version or commit ID:** - -```text -(paste here) -``` - -**Environment details (OS, Microsoft Excel™ version, physical, etc.):** From 054885219000f5c94c59fdf989e7c2110a023164 Mon Sep 17 00:00:00 2001 From: rentiansheng Date: Mon, 30 Jul 2018 22:09:41 +0800 Subject: [PATCH 007/957] data validation funcation --- datavalidation.go | 196 ++++++++++++++++++++++++++++++++++++++++++++++ xmlWorksheet.go | 26 ++++-- 2 files changed, 217 insertions(+), 5 deletions(-) create mode 100644 datavalidation.go diff --git a/datavalidation.go b/datavalidation.go new file mode 100644 index 0000000000..5dae7c9df2 --- /dev/null +++ b/datavalidation.go @@ -0,0 +1,196 @@ +package excelize + +import ( + "fmt" + "strings" +) + +type DataValidationType int + +// Data validation types +const ( + _DataValidationType = iota + typeNone //inline use + DataValidationTypeCustom + DataValidationTypeDate + DataValidationTypeDecimal + typeList //inline use + DataValidationTypeTextLeng + DataValidationTypeTime + // DataValidationTypeWhole Integer + DataValidationTypeWhole +) + +const ( + // dataValidationFormulaStrLen 255 characters+ 2 quotes + dataValidationFormulaStrLen = 257 + // dataValidationFormulaStrLenErr + dataValidationFormulaStrLenErr = "data validation must be 0-255 characters" +) + +type DataValidationErrorStyle int + +// Data validation error styles +const ( + _ DataValidationErrorStyle = iota + DataValidationStyleStop + DataValidationStyleWarning + DataValidationStyleInformation +) + +// Data validation error styles +const ( + styleStop = "stop" + styleWarning = "warning" + styleInformation = "information" +) + +// DataValidationOperator operator enum +type DataValidationOperator int + +// Data validation operators +const ( + _DataValidationOperator = iota + DataValidationOperatorBetween + DataValidationOperatorEqual + DataValidationOperatorGreaterThan + DataValidationOperatorGreaterThanOrEqual + DataValidationOperatorLessThan + DataValidationOperatorLessThanOrEqual + DataValidationOperatorNotBetween + DataValidationOperatorNotEqual +) + +// NewDataValidation return data validation struct +func NewDataValidation(allowBlank bool) *DataValidation { + return &DataValidation{ + AllowBlank: convBoolToStr(allowBlank), + ShowErrorMessage: convBoolToStr(false), + ShowInputMessage: convBoolToStr(false), + } +} + +// SetError set error notice +func (dd *DataValidation) SetError(style DataValidationErrorStyle, title, msg *string) { + dd.Error = msg + dd.ErrorTitle = title + strStyle := styleStop + switch style { + case DataValidationStyleStop: + strStyle = styleStop + case DataValidationStyleWarning: + strStyle = styleWarning + case DataValidationStyleInformation: + strStyle = styleInformation + + } + dd.ShowErrorMessage = convBoolToStr(true) + dd.ErrorStyle = &strStyle +} + +// SetInput set prompt notice +func (dd *DataValidation) SetInput(title, msg *string) { + dd.ShowInputMessage = convBoolToStr(true) + dd.PromptTitle = title + dd.Prompt = msg +} + +// SetDropList data validation list +func (dd *DataValidation) SetDropList(keys []string) error { + dd.Formula1 = "\"" + strings.Join(keys, ",") + "\"" + dd.Type = convDataValidationType(typeList) + return nil +} + +// SetDropList data validation range +func (dd *DataValidation) SetRange(f1, f2 int, t DataValidationType, o DataValidationOperator) error { + formula1 := fmt.Sprintf("%d", f1) + formula2 := fmt.Sprintf("%d", f2) + if dataValidationFormulaStrLen < len(dd.Formula1) || dataValidationFormulaStrLen < len(dd.Formula2) { + return fmt.Errorf(dataValidationFormulaStrLenErr) + } + switch o { + case DataValidationOperatorBetween: + if f1 > f2 { + tmp := formula1 + formula1 = formula2 + formula2 = tmp + } + case DataValidationOperatorNotBetween: + if f1 > f2 { + tmp := formula1 + formula1 = formula2 + formula2 = tmp + } + } + + dd.Formula1 = formula1 + dd.Formula2 = formula2 + dd.Type = convDataValidationType(t) + dd.Operator = convDataValidationOperatior(o) + return nil +} + +// SetDropList data validation range +func (dd *DataValidation) SetSqref(sqref string) { + if dd.Sqref == "" { + dd.Sqref = sqref + } else { + dd.Sqref = fmt.Sprintf("%s %s", dd.Sqref, sqref) + } +} + +// convBoolToStr convert boolean to string , false to 0, true to 1 +func convBoolToStr(bl bool) string { + if bl { + return "1" + } + return "0" +} + +// convDataValidationType get excel data validation type +func convDataValidationType(t DataValidationType) string { + typeMap := map[DataValidationType]string{ + typeNone: "none", + DataValidationTypeCustom: "custom", + DataValidationTypeDate: "date", + DataValidationTypeDecimal: "decimal", + typeList: "list", + DataValidationTypeTextLeng: "textLength", + DataValidationTypeTime: "time", + DataValidationTypeWhole: "whole", + } + + return typeMap[t] + +} + +// convDataValidationOperatior get excel data validation operator +func convDataValidationOperatior(o DataValidationOperator) string { + typeMap := map[DataValidationOperator]string{ + DataValidationOperatorBetween: "between", + DataValidationOperatorEqual: "equal", + DataValidationOperatorGreaterThan: "greaterThan", + DataValidationOperatorGreaterThanOrEqual: "greaterThanOrEqual", + DataValidationOperatorLessThan: "lessThan", + DataValidationOperatorLessThanOrEqual: "lessThanOrEqual", + DataValidationOperatorNotBetween: "notBetween", + DataValidationOperatorNotEqual: "notEqual", + } + + return typeMap[o] + +} + +func (f *File) AddDataValidation(sheet string, dv *DataValidation) { + xlsx := f.workSheetReader(sheet) + if nil == xlsx.DataValidations { + xlsx.DataValidations = new(xlsxDataValidations) + } + xlsx.DataValidations.DataValidation = append(xlsx.DataValidations.DataValidation, dv) + xlsx.DataValidations.Count = len(xlsx.DataValidations.DataValidation) +} + +func (f *File) GetDataValidation(sheet, sqref string) { + +} diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 87d66a1cf2..f2ac9fb5c9 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -294,11 +294,27 @@ type xlsxMergeCells struct { // xlsxDataValidations expresses all data validation information for cells in a // sheet which have data validation features applied. type xlsxDataValidations struct { - Count int `xml:"count,attr,omitempty"` - DisablePrompts bool `xml:"disablePrompts,attr,omitempty"` - XWindow int `xml:"xWindow,attr,omitempty"` - YWindow int `xml:"yWindow,attr,omitempty"` - DataValidation string `xml:",innerxml"` + Count int `xml:"count,attr,omitempty"` + DisablePrompts bool `xml:"disablePrompts,attr,omitempty"` + XWindow int `xml:"xWindow,attr,omitempty"` + YWindow int `xml:"yWindow,attr,omitempty"` + DataValidation []*DataValidation `xml:"dataValidation,innerxml"` +} + +type DataValidation struct { + AllowBlank string `xml:"allowBlank,attr"` // allow empty + ShowInputMessage string `xml:"showInputMessage,attr"` // 1, true,0,false, select cell, Whether the input message is displayed + ShowErrorMessage string `xml:"showErrorMessage,attr"` // 1, true,0,false, input error value, Whether the error message is displayed + ErrorStyle *string `xml:"errorStyle,attr"` //error icon style, warning, infomation,stop + ErrorTitle *string `xml:"errorTitle,attr"` // error title + Operator string `xml:"operator,attr"` // + Error *string `xml:"error,attr"` // input error value, notice message + PromptTitle *string `xml:"promptTitle"` + Prompt *string `xml:"prompt,attr"` + Type string `xml:"type,attr"` //data type, none,custom,date,decimal,list, textLength,time,whole + Sqref string `xml:"sqref,attr"` //Validity of data validation rules, cell and range, eg: A1 OR A1:A20 + Formula1 string `xml:"formula1"` // data validation role + Formula2 string `xml:"formula2"` //data validation role } // xlsxC directly maps the c element in the namespace From ec37b114c3b704a84c66fcf3e135c9df88ffb24d Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 6 Aug 2018 10:21:24 +0800 Subject: [PATCH 008/957] Fixes #256 and format document. --- cell.go | 64 +++++++++++----------- chart.go | 82 ++++++++++++++-------------- col.go | 28 +++++----- comment.go | 18 +++---- date.go | 26 ++++----- excelize.go | 34 ++++++------ file.go | 10 ++-- lib.go | 21 ++++---- picture.go | 57 ++++++++++---------- rows.go | 39 +++++++------- shape.go | 10 ++-- sheet.go | 54 +++++++++---------- sheetpr.go | 4 +- styles.go | 153 +++++++++++++++++++++++++++------------------------- table.go | 29 +++++----- 15 files changed, 320 insertions(+), 309 deletions(-) diff --git a/cell.go b/cell.go index eb265cc68d..5ec5976eb8 100644 --- a/cell.go +++ b/cell.go @@ -20,7 +20,7 @@ const ( STCellFormulaTypeShared = "shared" ) -// mergeCellsParser provides function to check merged cells in worksheet by +// mergeCellsParser provides a function to check merged cells in worksheet by // given axis. func (f *File) mergeCellsParser(xlsx *xlsxWorksheet, axis string) string { axis = strings.ToUpper(axis) @@ -34,8 +34,8 @@ func (f *File) mergeCellsParser(xlsx *xlsxWorksheet, axis string) string { return axis } -// SetCellValue provides function to set value of a cell. The following shows -// the supported data types: +// SetCellValue provides a function to set value of a cell. The following +// shows the supported data types: // // int // int8 @@ -83,7 +83,7 @@ func (f *File) SetCellValue(sheet, axis string, value interface{}) { } } -// setCellIntValue provides function to set int value of a cell. +// setCellIntValue provides a function to set int value of a cell. func (f *File) setCellIntValue(sheet, axis string, value interface{}) { switch value.(type) { case int: @@ -111,7 +111,7 @@ func (f *File) setCellIntValue(sheet, axis string, value interface{}) { } } -// SetCellBool provides function to set bool type value of a cell by given +// SetCellBool provides a function to set bool type value of a cell by given // worksheet name, cell coordinates and cell value. func (f *File) SetCellBool(sheet, axis string, value bool) { xlsx := f.workSheetReader(sheet) @@ -139,10 +139,10 @@ func (f *File) SetCellBool(sheet, axis string, value bool) { } } -// GetCellValue provides function to get formatted value from cell by given -// worksheet name and axis in XLSX 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. +// GetCellValue provides a function to get formatted value from cell by given +// worksheet name and axis in XLSX 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. func (f *File) GetCellValue(sheet, axis string) string { xlsx := f.workSheetReader(sheet) axis = f.mergeCellsParser(xlsx, axis) @@ -174,9 +174,9 @@ func (f *File) GetCellValue(sheet, axis string) string { return "" } -// formattedValue provides 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. +// 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) string { if s == 0 { return v @@ -189,7 +189,7 @@ func (f *File) formattedValue(s int, v string) string { return v } -// GetCellStyle provides function to get cell style index by given worksheet +// GetCellStyle provides a function to get cell style index by given worksheet // name and cell coordinates. func (f *File) GetCellStyle(sheet, axis string) int { xlsx := f.workSheetReader(sheet) @@ -211,8 +211,8 @@ func (f *File) GetCellStyle(sheet, axis string) int { return f.prepareCellStyle(xlsx, cell, xlsx.SheetData.Row[xAxis].C[yAxis].S) } -// GetCellFormula provides function to get formula from cell by given worksheet -// name and axis in XLSX file. +// 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 { xlsx := f.workSheetReader(sheet) axis = f.mergeCellsParser(xlsx, axis) @@ -276,7 +276,7 @@ func getSharedForumula(xlsx *xlsxWorksheet, si string) string { return "" } -// SetCellFormula provides function to set cell formula by given string and +// SetCellFormula provides a function to set cell formula by given string and // worksheet name. func (f *File) SetCellFormula(sheet, axis, formula string) { xlsx := f.workSheetReader(sheet) @@ -305,10 +305,10 @@ func (f *File) SetCellFormula(sheet, axis, formula string) { } } -// SetCellHyperLink provides 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. The -// below is example for external link. +// 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. The below is example for external link. // // xlsx.SetCellHyperLink("Sheet1", "A3", "https://github.com/360EntSecGroup-Skylar/excelize", "External") // // Set underline and font color style for the cell. @@ -341,10 +341,10 @@ func (f *File) SetCellHyperLink(sheet, axis, link, linkType string) { xlsx.Hyperlinks.Hyperlink = append(xlsx.Hyperlinks.Hyperlink, hyperlink) } -// GetCellHyperLink provides function to get cell hyperlink by given worksheet -// name and axis. Boolean type value link will be ture 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 +// GetCellHyperLink provides a function to get cell hyperlink by given +// worksheet name and axis. Boolean type value link will be ture 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: // // link, target := xlsx.GetCellHyperLink("Sheet1", "H6") @@ -369,8 +369,8 @@ func (f *File) GetCellHyperLink(sheet, axis string) (bool, string) { return link, target } -// MergeCell provides function to merge cells by given coordinate area and sheet -// name. For example create a merged cell of D3:E9 on Sheet1: +// MergeCell provides a function to merge cells by given coordinate area and +// sheet name. For example create a merged cell of D3:E9 on Sheet1: // // xlsx.MergeCell("Sheet1", "D3", "E9") // @@ -429,7 +429,7 @@ func (f *File) MergeCell(sheet, hcell, vcell string) { } } -// SetCellInt provides function to set int type value of a cell by given +// 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) { xlsx := f.workSheetReader(sheet) @@ -453,7 +453,7 @@ func (f *File) SetCellInt(sheet, axis string, value int) { xlsx.SheetData.Row[xAxis].C[yAxis].V = strconv.Itoa(value) } -// prepareCellStyle provides function to prepare style index of cell in +// prepareCellStyle provides a function to prepare style index of cell in // worksheet by given column index and style index. func (f *File) prepareCellStyle(xlsx *xlsxWorksheet, col, style int) int { if xlsx.Cols != nil && style == 0 { @@ -466,8 +466,8 @@ func (f *File) prepareCellStyle(xlsx *xlsxWorksheet, col, style int) int { return style } -// SetCellStr provides function to set string type value of a cell. Total number -// of characters that a cell can contain 32767 characters. +// 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) { xlsx := f.workSheetReader(sheet) axis = f.mergeCellsParser(xlsx, axis) @@ -502,7 +502,7 @@ func (f *File) SetCellStr(sheet, axis, value string) { xlsx.SheetData.Row[xAxis].C[yAxis].V = value } -// SetCellDefault provides function to set string type value of a cell as +// 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) { xlsx := f.workSheetReader(sheet) @@ -567,7 +567,7 @@ func (f *File) SetSheetRow(sheet, axis string, slice interface{}) { } } -// checkCellInArea provides function to determine if a given coordinate is +// checkCellInArea provides a function to determine if a given coordinate is // within an area. func checkCellInArea(cell, area string) bool { cell = strings.ToUpper(cell) diff --git a/chart.go b/chart.go index 41354f1b23..8e1d7e9f9b 100644 --- a/chart.go +++ b/chart.go @@ -190,7 +190,7 @@ var ( } ) -// parseFormatChartSet provides function to parse the format settings of the +// parseFormatChartSet provides a function to parse the format settings of the // chart with default value. func parseFormatChartSet(formatSet string) (*formatChart, error) { format := formatChart{ @@ -379,7 +379,7 @@ func (f *File) AddChart(sheet, cell, format string) error { return err } -// countCharts provides function to get chart files count storage in the +// countCharts provides a function to get chart files count storage in the // folder xl/charts. func (f *File) countCharts() int { count := 0 @@ -391,7 +391,7 @@ func (f *File) countCharts() int { return count } -// prepareDrawing provides function to prepare drawing ID and XML by given +// prepareDrawing provides a function to prepare drawing ID and XML by given // drawingID, worksheet name and default drawingXML. func (f *File) prepareDrawing(xlsx *xlsxWorksheet, drawingID int, sheet, drawingXML string) (int, string) { sheetRelationshipsDrawingXML := "../drawings/drawing" + strconv.Itoa(drawingID) + ".xml" @@ -408,8 +408,8 @@ func (f *File) prepareDrawing(xlsx *xlsxWorksheet, drawingID int, sheet, drawing return drawingID, drawingXML } -// addChart provides function to create chart as xl/charts/chart%d.xml by given -// format sets. +// addChart provides a function to create chart as xl/charts/chart%d.xml by +// given format sets. func (f *File) addChart(formatSet *formatChart) { count := f.countCharts() xlsxChartSpace := xlsxChartSpace{ @@ -564,7 +564,7 @@ func (f *File) addChart(formatSet *formatChart) { f.saveFileList(media, chart) } -// drawBaseChart provides function to draw the c:plotArea element for bar, +// 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 { c := cCharts{ @@ -661,7 +661,7 @@ func (f *File) drawBaseChart(formatSet *formatChart) *cPlotArea { return charts[formatSet.Type] } -// drawDoughnutChart provides function to draw the c:plotArea element for +// drawDoughnutChart provides a function to draw the c:plotArea element for // doughnut chart by given format sets. func (f *File) drawDoughnutChart(formatSet *formatChart) *cPlotArea { return &cPlotArea{ @@ -675,8 +675,8 @@ func (f *File) drawDoughnutChart(formatSet *formatChart) *cPlotArea { } } -// drawLineChart provides function to draw the c:plotArea element for line chart -// by given format sets. +// drawLineChart provides a function to draw the c:plotArea element for line +// chart by given format sets. func (f *File) drawLineChart(formatSet *formatChart) *cPlotArea { return &cPlotArea{ LineChart: &cCharts{ @@ -701,8 +701,8 @@ func (f *File) drawLineChart(formatSet *formatChart) *cPlotArea { } } -// drawPieChart provides function to draw the c:plotArea element for pie chart -// by given format sets. +// drawPieChart provides a function to draw the c:plotArea element for pie +// chart by given format sets. func (f *File) drawPieChart(formatSet *formatChart) *cPlotArea { return &cPlotArea{ PieChart: &cCharts{ @@ -714,8 +714,8 @@ func (f *File) drawPieChart(formatSet *formatChart) *cPlotArea { } } -// drawPie3DChart provides function to draw the c:plotArea element for 3D pie -// chart by given format sets. +// 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 { return &cPlotArea{ Pie3DChart: &cCharts{ @@ -727,7 +727,7 @@ func (f *File) drawPie3DChart(formatSet *formatChart) *cPlotArea { } } -// drawRadarChart provides function to draw the c:plotArea element for radar +// drawRadarChart provides a function to draw the c:plotArea element for radar // chart by given format sets. func (f *File) drawRadarChart(formatSet *formatChart) *cPlotArea { return &cPlotArea{ @@ -750,8 +750,8 @@ func (f *File) drawRadarChart(formatSet *formatChart) *cPlotArea { } } -// drawScatterChart provides function to draw the c:plotArea element for scatter -// chart by given format sets. +// drawScatterChart provides a function to draw the c:plotArea element for +// scatter chart by given format sets. func (f *File) drawScatterChart(formatSet *formatChart) *cPlotArea { return &cPlotArea{ ScatterChart: &cCharts{ @@ -773,8 +773,8 @@ func (f *File) drawScatterChart(formatSet *formatChart) *cPlotArea { } } -// drawChartSeries provides function to draw the c:ser element by given format -// sets. +// drawChartSeries provides a function to draw the c:ser element by given +// format sets. func (f *File) drawChartSeries(formatSet *formatChart) *[]cSer { ser := []cSer{} for k := range formatSet.Series { @@ -799,7 +799,7 @@ func (f *File) drawChartSeries(formatSet *formatChart) *[]cSer { return &ser } -// drawChartSeriesSpPr provides function to draw the c:spPr element by given +// drawChartSeriesSpPr provides a function to draw the c:spPr element by given // format sets. func (f *File) drawChartSeriesSpPr(i int, formatSet *formatChart) *cSpPr { spPrScatter := &cSpPr{ @@ -821,8 +821,8 @@ func (f *File) drawChartSeriesSpPr(i int, formatSet *formatChart) *cSpPr { return chartSeriesSpPr[formatSet.Type] } -// drawChartSeriesDPt provides function to draw the c:dPt element by given data -// index and format sets. +// 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 { dpt := []*cDPt{{ IDx: &attrValInt{Val: i}, @@ -850,8 +850,8 @@ func (f *File) drawChartSeriesDPt(i int, formatSet *formatChart) []*cDPt { return chartSeriesDPt[formatSet.Type] } -// drawChartSeriesCat provides function to draw the c:cat element by given chart -// series and format sets. +// 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 { cat := &cCat{ StrRef: &cStrRef{ @@ -862,8 +862,8 @@ func (f *File) drawChartSeriesCat(v formatChartSeries, formatSet *formatChart) * return chartSeriesCat[formatSet.Type] } -// drawChartSeriesVal provides function to draw the c:val element by given chart -// series and format sets. +// 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 { val := &cVal{ NumRef: &cNumRef{ @@ -874,8 +874,8 @@ func (f *File) drawChartSeriesVal(v formatChartSeries, formatSet *formatChart) * return chartSeriesVal[formatSet.Type] } -// drawChartSeriesMarker provides function to draw the c:marker element by given -// data index and format sets. +// 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 { marker := &cMarker{ Symbol: &attrValString{Val: "circle"}, @@ -900,7 +900,7 @@ func (f *File) drawChartSeriesMarker(i int, formatSet *formatChart) *cMarker { return chartSeriesMarker[formatSet.Type] } -// drawChartSeriesXVal provides function to draw the c:xVal element by given +// 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 { cat := &cCat{ @@ -912,7 +912,7 @@ func (f *File) drawChartSeriesXVal(v formatChartSeries, formatSet *formatChart) return chartSeriesXVal[formatSet.Type] } -// drawChartSeriesYVal provides function to draw the c:yVal element by given +// 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 { val := &cVal{ @@ -924,8 +924,8 @@ func (f *File) drawChartSeriesYVal(v formatChartSeries, formatSet *formatChart) return chartSeriesYVal[formatSet.Type] } -// drawChartDLbls provides function to draw the c:dLbls element by given format -// sets. +// drawChartDLbls provides a function to draw the c:dLbls element by given +// format sets. func (f *File) drawChartDLbls(formatSet *formatChart) *cDLbls { return &cDLbls{ ShowLegendKey: &attrValBool{Val: formatSet.Legend.ShowLegendKey}, @@ -938,15 +938,15 @@ func (f *File) drawChartDLbls(formatSet *formatChart) *cDLbls { } } -// drawChartSeriesDLbls provides function to draw the c:dLbls element by given -// format sets. +// 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) chartSeriesDLbls := map[string]*cDLbls{Bar: dLbls, BarStacked: dLbls, BarPercentStacked: dLbls, Bar3DClustered: dLbls, Bar3DStacked: dLbls, Bar3DPercentStacked: dLbls, Col: dLbls, ColStacked: dLbls, ColPercentStacked: dLbls, Col3DClustered: dLbls, Col3D: dLbls, Col3DStacked: dLbls, Col3DPercentStacked: dLbls, Doughnut: dLbls, Line: dLbls, Pie: dLbls, Pie3D: dLbls, Radar: dLbls, Scatter: nil} return chartSeriesDLbls[formatSet.Type] } -// drawPlotAreaCatAx provides function to draw the c:catAx element. +// drawPlotAreaCatAx provides a function to draw the c:catAx element. func (f *File) drawPlotAreaCatAx(formatSet *formatChart) []*cAxs { min := &attrValFloat{Val: formatSet.XAxis.Minimum} max := &attrValFloat{Val: formatSet.XAxis.Maximum} @@ -985,7 +985,7 @@ func (f *File) drawPlotAreaCatAx(formatSet *formatChart) []*cAxs { } } -// drawPlotAreaValAx provides function to draw the c:valAx element. +// drawPlotAreaValAx provides a function to draw the c:valAx element. func (f *File) drawPlotAreaValAx(formatSet *formatChart) []*cAxs { min := &attrValFloat{Val: formatSet.YAxis.Minimum} max := &attrValFloat{Val: formatSet.YAxis.Maximum} @@ -1021,7 +1021,7 @@ func (f *File) drawPlotAreaValAx(formatSet *formatChart) []*cAxs { } } -// drawPlotAreaSpPr provides function to draw the c:spPr element. +// drawPlotAreaSpPr provides a function to draw the c:spPr element. func (f *File) drawPlotAreaSpPr() *cSpPr { return &cSpPr{ Ln: &aLn{ @@ -1040,7 +1040,7 @@ func (f *File) drawPlotAreaSpPr() *cSpPr { } } -// drawPlotAreaTxPr provides function to draw the c:txPr element. +// drawPlotAreaTxPr provides a function to draw the c:txPr element. func (f *File) drawPlotAreaTxPr() *cTxPr { return &cTxPr{ BodyPr: aBodyPr{ @@ -1079,8 +1079,8 @@ func (f *File) drawPlotAreaTxPr() *cTxPr { } } -// drawingParser provides function to parse drawingXML. In order to solve the -// problem that the label structure is changed after serialization and +// drawingParser provides a function to parse drawingXML. In order to solve +// the problem that the label structure is changed after serialization and // deserialization, two different structures: decodeWsDr and encodeWsDr are // defined. func (f *File) drawingParser(drawingXML string, content *xlsxWsDr) int { @@ -1107,8 +1107,8 @@ func (f *File) drawingParser(drawingXML string, content *xlsxWsDr) int { return cNvPrID } -// addDrawingChart provides function to add chart graphic frame by given sheet, -// drawingXML, cell, width, height, relationship index and format sets. +// 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) { cell = strings.ToUpper(cell) fromCol := string(strings.Map(letterOnlyMapF, cell)) diff --git a/col.go b/col.go index 05ad0cceb8..e3870ee8b8 100644 --- a/col.go +++ b/col.go @@ -121,8 +121,8 @@ func (f *File) SetColOutlineLevel(sheet, column string, level uint8) { xlsx.Cols.Col = append(xlsx.Cols.Col, col) } -// SetColWidth provides function to set the width of a single column or multiple -// columns. For example: +// SetColWidth provides a function to set the width of a single column or +// multiple columns. For example: // // xlsx := excelize.NewFile() // xlsx.SetColWidth("Sheet1", "A", "H", 20) @@ -259,8 +259,8 @@ func (f *File) positionObjectPixels(sheet string, colStart, rowStart, x1, y1, wi return colStart, rowStart, xAbs, yAbs, colEnd, rowEnd, x2, y2 } -// getColWidth provides function to get column width in pixels by given sheet -// name and column index. +// getColWidth provides a function to get column width in pixels by given +// sheet name and column index. func (f *File) getColWidth(sheet string, col int) int { xlsx := f.workSheetReader(sheet) if xlsx.Cols != nil { @@ -278,8 +278,8 @@ func (f *File) getColWidth(sheet string, col int) int { return int(defaultColWidthPixels) } -// GetColWidth provides function to get column width by given worksheet name and -// column index. +// GetColWidth provides a function to get column width by given worksheet name +// and column index. func (f *File) GetColWidth(sheet, column string) float64 { col := TitleToNumber(strings.ToUpper(column)) + 1 xlsx := f.workSheetReader(sheet) @@ -298,8 +298,8 @@ func (f *File) GetColWidth(sheet, column string) float64 { return defaultColWidthPixels } -// InsertCol provides function to insert a new column before given column index. -// For example, create a new column before column C in Sheet1: +// InsertCol provides a function to insert a new column before given column +// index. For example, create a new column before column C in Sheet1: // // xlsx.InsertCol("Sheet1", "C") // @@ -308,8 +308,8 @@ func (f *File) InsertCol(sheet, column string) { f.adjustHelper(sheet, col, -1, 1) } -// RemoveCol provides function to remove single column by given worksheet name -// and column index. For example, remove column C in Sheet1: +// RemoveCol provides a function to remove single column by given worksheet +// name and column index. For example, remove column C in Sheet1: // // xlsx.RemoveCol("Sheet1", "C") // @@ -346,10 +346,10 @@ func completeCol(xlsx *xlsxWorksheet, row, cell int) { } } -// convertColWidthToPixels provieds function to convert the width of a cell from -// user's units to pixels. Excel rounds the column width to the nearest pixel. -// If the width hasn't been set by the user we use the default value. If the -// column is hidden it has a value of zero. +// convertColWidthToPixels provieds function to convert the width of a cell +// from user's units to pixels. Excel rounds the column width to the nearest +// pixel. If the width hasn't been set by the user we use the default value. +// If the column is hidden it has a value of zero. func convertColWidthToPixels(width float64) float64 { var padding float64 = 5 var pixels float64 diff --git a/comment.go b/comment.go index ab10310682..8bf4fd31a5 100644 --- a/comment.go +++ b/comment.go @@ -8,8 +8,8 @@ import ( "strings" ) -// parseFormatCommentsSet provides function to parse the format settings of the -// comment with default value. +// parseFormatCommentsSet provides a function to parse the format settings of +// the comment with default value. func parseFormatCommentsSet(formatSet string) (*formatComment, error) { format := formatComment{ Author: "Author:", @@ -19,8 +19,8 @@ func parseFormatCommentsSet(formatSet string) (*formatComment, error) { return &format, err } -// GetComments retrieves all comments and returns a map -// of worksheet name to the worksheet comments. +// GetComments retrieves all comments and returns a map of worksheet name to +// the worksheet comments. func (f *File) GetComments() (comments map[string]*xlsxComments) { comments = map[string]*xlsxComments{} for n := range f.sheetMap { @@ -81,7 +81,7 @@ func (f *File) AddComment(sheet, cell, format string) error { return err } -// addDrawingVML provides function to create comment as +// 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) { col := string(strings.Map(letterOnlyMapF, cell)) @@ -178,8 +178,8 @@ func (f *File) addDrawingVML(commentID int, drawingVML, cell string, lineCount, f.XLSX[drawingVML] = v } -// addComment provides function to create chart as xl/comments%d.xml by given -// cell and format sets. +// 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 @@ -238,8 +238,8 @@ func (f *File) addComment(commentsXML, cell string, formatSet *formatComment) { f.saveFileList(commentsXML, v) } -// countComments provides function to get comments files count storage in the -// folder xl. +// countComments provides a function to get comments files count storage in +// the folder xl. func (f *File) countComments() int { count := 0 for k := range f.XLSX { diff --git a/date.go b/date.go index f3db0ee826..72336610db 100644 --- a/date.go +++ b/date.go @@ -8,12 +8,12 @@ import ( // timeLocationUTC defined the UTC time location. var timeLocationUTC, _ = time.LoadLocation("UTC") -// timeToUTCTime provides function to convert time to UTC time. +// timeToUTCTime provides a function to convert time to UTC time. func timeToUTCTime(t time.Time) time.Time { return time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), timeLocationUTC) } -// timeToExcelTime provides function to convert time to Excel time. +// timeToExcelTime provides a function to convert time to Excel time. func timeToExcelTime(t time.Time) float64 { // TODO in future this should probably also handle date1904 and like TimeFromExcelTime var excelTime float64 @@ -32,7 +32,7 @@ func timeToExcelTime(t time.Time) float64 { return excelTime + float64(t.UnixNano())/8.64e13 + 25569.0 } -// shiftJulianToNoon provides function to process julian date to noon. +// shiftJulianToNoon provides a function to process julian date to noon. func shiftJulianToNoon(julianDays, julianFraction float64) (float64, float64) { switch { case -0.5 < julianFraction && julianFraction < 0.5: @@ -47,7 +47,7 @@ func shiftJulianToNoon(julianDays, julianFraction float64) (float64, float64) { return julianDays, julianFraction } -// fractionOfADay provides function to return the integer values for hour, +// fractionOfADay provides a function to return the integer values for hour, // minutes, seconds and nanoseconds that comprised a given fraction of a day. // values would round to 1 us. func fractionOfADay(fraction float64) (hours, minutes, seconds, nanoseconds int) { @@ -68,7 +68,7 @@ func fractionOfADay(fraction float64) (hours, minutes, seconds, nanoseconds int) return } -// julianDateToGregorianTime provides function to convert julian date to +// julianDateToGregorianTime provides a function to convert julian date to // gregorian time. func julianDateToGregorianTime(part1, part2 float64) time.Time { part1I, part1F := math.Modf(part1) @@ -81,12 +81,12 @@ func julianDateToGregorianTime(part1, part2 float64) time.Time { return time.Date(year, time.Month(month), day, hours, minutes, seconds, nanoseconds, time.UTC) } -// By this point generations of programmers have repeated the algorithm sent to -// the editor of "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 expand an explanation here - that day is not today. +// By this point generations of programmers have repeated the algorithm sent +// to the editor of "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 expand an explanation here - that day is not today. func doTheFliegelAndVanFlandernAlgorithm(jd int) (day, month, year int) { l := jd + 68569 n := (4 * l) / 146097 @@ -101,8 +101,8 @@ func doTheFliegelAndVanFlandernAlgorithm(jd int) (day, month, year int) { return d, m, y } -// timeFromExcelTime provides function to convert an excelTime representation -// (stored as a floating point number) to a time.Time. +// timeFromExcelTime provides a function to convert an excelTime +// representation (stored as a floating point number) to a time.Time. func timeFromExcelTime(excelTime float64, date1904 bool) time.Time { const MDD int64 = 106750 // Max time.Duration Days, aprox. 290 years var date time.Time diff --git a/excelize.go b/excelize.go index 99243a7ca3..0b530ab219 100644 --- a/excelize.go +++ b/excelize.go @@ -71,7 +71,7 @@ func OpenReader(r io.Reader) (*File, error) { return f, nil } -// setDefaultTimeStyle provides function to set default numbers format for +// setDefaultTimeStyle provides a function to set default numbers format for // time.Time type cell value by given worksheet name, cell coordinates and // number format code. func (f *File) setDefaultTimeStyle(sheet, axis string, format int) { @@ -81,8 +81,8 @@ func (f *File) setDefaultTimeStyle(sheet, axis string, format int) { } } -// workSheetReader provides function to get the pointer to the structure after -// deserialization by given worksheet name. +// workSheetReader provides a function to get the pointer to the structure +// after deserialization by given worksheet name. func (f *File) workSheetReader(sheet string) *xlsxWorksheet { name, ok := f.sheetMap[trimSheetName(sheet)] if !ok { @@ -105,7 +105,7 @@ func (f *File) workSheetReader(sheet string) *xlsxWorksheet { return f.Sheet[name] } -// checkSheet provides function to fill each row element and make that is +// checkSheet provides a function to fill each row element and make that is // continuous in a worksheet of XML. func checkSheet(xlsx *xlsxWorksheet) { row := len(xlsx.SheetData.Row) @@ -133,7 +133,7 @@ func checkSheet(xlsx *xlsxWorksheet) { xlsx.SheetData = sheetData } -// replaceWorkSheetsRelationshipsNameSpaceBytes provides function to replace +// replaceWorkSheetsRelationshipsNameSpaceBytes provides a function to replace // xl/worksheets/sheet%d.xml XML tags to self-closing for compatible Microsoft // Office Excel 2007. func replaceWorkSheetsRelationshipsNameSpaceBytes(workbookMarshal []byte) []byte { @@ -182,7 +182,7 @@ func (f *File) UpdateLinkedValue() { } } -// adjustHelper provides function to adjust rows and columns dimensions, +// adjustHelper provides a function to adjust rows and columns dimensions, // hyperlinks, merged cells and auto filter when inserting or deleting rows or // columns. // @@ -204,7 +204,7 @@ func (f *File) adjustHelper(sheet string, column, row, offset int) { checkRow(xlsx) } -// adjustColDimensions provides function to update column dimensions when +// adjustColDimensions provides a function to update column dimensions when // inserting or deleting rows or columns. func (f *File) adjustColDimensions(xlsx *xlsxWorksheet, column, offset int) { for i, r := range xlsx.SheetData.Row { @@ -220,8 +220,8 @@ func (f *File) adjustColDimensions(xlsx *xlsxWorksheet, column, offset int) { } } -// adjustRowDimensions provides function to update row dimensions when inserting -// or deleting rows or columns. +// adjustRowDimensions provides a function to update row dimensions when +// inserting or deleting rows or columns. func (f *File) adjustRowDimensions(xlsx *xlsxWorksheet, rowIndex, offset int) { if rowIndex == -1 { return @@ -240,7 +240,7 @@ func (f *File) adjustRowDimensions(xlsx *xlsxWorksheet, rowIndex, offset int) { } } -// adjustHyperlinks provides function to update hyperlinks when inserting or +// adjustHyperlinks provides a function to update hyperlinks when inserting or // deleting rows or columns. func (f *File) adjustHyperlinks(sheet string, column, rowIndex, offset int) { xlsx := f.workSheetReader(sheet) @@ -280,8 +280,8 @@ func (f *File) adjustHyperlinks(sheet string, column, rowIndex, offset int) { } } -// adjustMergeCellsHelper provides function to update merged cells when inserting or -// deleting rows or columns. +// adjustMergeCellsHelper provides a function to update merged cells when +// inserting or deleting rows or columns. func (f *File) adjustMergeCellsHelper(xlsx *xlsxWorksheet, column, rowIndex, offset int) { if xlsx.MergeCells != nil { for k, v := range xlsx.MergeCells.Cells { @@ -321,8 +321,8 @@ func (f *File) adjustMergeCellsHelper(xlsx *xlsxWorksheet, column, rowIndex, off } } -// adjustMergeCells provides function to update merged cells when inserting or -// deleting rows or columns. +// adjustMergeCells provides a function to update merged cells when inserting +// or deleting rows or columns. func (f *File) adjustMergeCells(xlsx *xlsxWorksheet, column, rowIndex, offset int) { f.adjustMergeCellsHelper(xlsx, column, rowIndex, offset) @@ -342,8 +342,8 @@ func (f *File) adjustMergeCells(xlsx *xlsxWorksheet, column, rowIndex, offset in } } -// adjustAutoFilter provides function to update the auto filter when inserting -// or deleting rows or columns. +// adjustAutoFilter provides a function to update the auto filter when +// inserting or deleting rows or columns. func (f *File) adjustAutoFilter(xlsx *xlsxWorksheet, column, rowIndex, offset int) { f.adjustAutoFilterHelper(xlsx, column, rowIndex, offset) @@ -376,7 +376,7 @@ func (f *File) adjustAutoFilter(xlsx *xlsxWorksheet, column, rowIndex, offset in } } -// adjustAutoFilterHelper provides function to update the auto filter when +// adjustAutoFilterHelper provides a function to update the auto filter when // inserting or deleting rows or columns. func (f *File) adjustAutoFilterHelper(xlsx *xlsxWorksheet, column, rowIndex, offset int) { if xlsx.AutoFilter != nil { diff --git a/file.go b/file.go index 2b1f1e03f2..a45fc28352 100644 --- a/file.go +++ b/file.go @@ -8,7 +8,7 @@ import ( "os" ) -// NewFile provides function to create new file by default template. For +// NewFile provides a function to create new file by default template. For // example: // // xlsx := NewFile() @@ -40,7 +40,7 @@ func NewFile() *File { return f } -// Save provides function to override the xlsx file with origin path. +// Save provides a function to override the xlsx file with origin path. func (f *File) Save() error { if f.Path == "" { return fmt.Errorf("No path defined for file, consider File.WriteTo or File.Write") @@ -48,8 +48,8 @@ func (f *File) Save() error { return f.SaveAs(f.Path) } -// SaveAs provides function to create or update to an xlsx file at the provided -// path. +// SaveAs provides a function to create or update to an xlsx file at the +// provided path. func (f *File) SaveAs(name string) error { file, err := os.OpenFile(name, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0666) if err != nil { @@ -59,7 +59,7 @@ func (f *File) SaveAs(name string) error { return f.Write(file) } -// Write provides function to write to an io.Writer. +// Write provides a function to write to an io.Writer. func (f *File) Write(w io.Writer) error { _, err := f.WriteTo(w) return err diff --git a/lib.go b/lib.go index e1b069382a..8013efa3fa 100644 --- a/lib.go +++ b/lib.go @@ -25,7 +25,7 @@ func ReadZipReader(r *zip.Reader) (map[string][]byte, int, error) { return fileList, worksheets, nil } -// readXML provides function to read XML content as string. +// readXML provides a function to read XML content as string. func (f *File) readXML(name string) []byte { if content, ok := f.XLSX[name]; ok { return content @@ -33,8 +33,8 @@ func (f *File) readXML(name string) []byte { return []byte{} } -// saveFileList provides function to update given file content in file list of -// XLSX. +// saveFileList provides a function to update given file content in file list +// of XLSX. func (f *File) saveFileList(name string, content []byte) { newContent := make([]byte, 0, len(XMLHeader)+len(content)) newContent = append(newContent, []byte(XMLHeader)...) @@ -54,7 +54,7 @@ func readFile(file *zip.File) []byte { return buff.Bytes() } -// ToAlphaString provides function to convert integer to Excel sheet column +// ToAlphaString provides a function to convert integer to Excel sheet column // title. For example convert 36 to column title AK: // // excelize.ToAlphaString(36) @@ -72,9 +72,9 @@ func ToAlphaString(value int) string { return ans } -// TitleToNumber provides function to convert Excel sheet column title to int -// (this function doesn't do value check currently). For example convert AK -// and ak to column title 36: +// TitleToNumber provides a function to convert Excel sheet column title to +// int (this function doesn't do value check currently). For example convert +// AK and ak to column title 36: // // excelize.TitleToNumber("AK") // excelize.TitleToNumber("ak") @@ -125,8 +125,8 @@ func defaultTrue(b *bool) bool { return *b } -// axisLowerOrEqualThan returns true if axis1 <= axis2 -// axis1/axis2 can be either a column or a row axis, e.g. "A", "AAE", "42", "1", etc. +// axisLowerOrEqualThan returns true if axis1 <= axis2 axis1/axis2 can be +// either a column or a row axis, e.g. "A", "AAE", "42", "1", etc. // // For instance, the following comparisons are all true: // @@ -147,7 +147,8 @@ func axisLowerOrEqualThan(axis1, axis2 string) bool { } } -// getCellColRow returns the two parts of a cell identifier (its col and row) as strings +// getCellColRow returns the two parts of a cell identifier (its col and row) +// as strings // // For instance: // diff --git a/picture.go b/picture.go index 04d50621d1..d039ae0ac3 100644 --- a/picture.go +++ b/picture.go @@ -14,8 +14,8 @@ import ( "strings" ) -// parseFormatPictureSet provides function to parse the format settings of the -// picture with default value. +// parseFormatPictureSet provides a function to parse the format settings of +// the picture with default value. func parseFormatPictureSet(formatSet string) (*formatPicture, error) { format := formatPicture{ FPrintsWithSheet: true, @@ -116,7 +116,7 @@ func (f *File) AddPicture(sheet, cell, picture, format string) error { return err } -// addSheetRelationships provides function to add +// addSheetRelationships provides a function to add // xl/worksheets/_rels/sheet%d.xml.rels by given worksheet name, relationship // type and target. func (f *File) addSheetRelationships(sheet, relType, target, targetMode string) int { @@ -149,9 +149,9 @@ func (f *File) addSheetRelationships(sheet, relType, target, targetMode string) return rID } -// deleteSheetRelationships provides function to delete relationships in -// xl/worksheets/_rels/sheet%d.xml.rels by given worksheet name and relationship -// index. +// deleteSheetRelationships provides a function to delete relationships in +// 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)] if !ok { @@ -169,7 +169,7 @@ func (f *File) deleteSheetRelationships(sheet, rID string) { f.saveFileList(rels, output) } -// addSheetLegacyDrawing provides function to add legacy drawing element to +// addSheetLegacyDrawing provides a function to add legacy drawing element to // xl/worksheets/sheet%d.xml by given worksheet name and relationship index. func (f *File) addSheetLegacyDrawing(sheet string, rID int) { xlsx := f.workSheetReader(sheet) @@ -178,7 +178,7 @@ func (f *File) addSheetLegacyDrawing(sheet string, rID int) { } } -// addSheetDrawing provides function to add drawing element to +// addSheetDrawing provides a function to add drawing element to // xl/worksheets/sheet%d.xml by given worksheet name and relationship index. func (f *File) addSheetDrawing(sheet string, rID int) { xlsx := f.workSheetReader(sheet) @@ -187,7 +187,7 @@ func (f *File) addSheetDrawing(sheet string, rID int) { } } -// addSheetPicture provides function to add picture element to +// 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) { xlsx := f.workSheetReader(sheet) @@ -196,7 +196,7 @@ func (f *File) addSheetPicture(sheet string, rID int) { } } -// countDrawings provides function to get drawing files count storage in the +// countDrawings provides a function to get drawing files count storage in the // folder xl/drawings. func (f *File) countDrawings() int { count := 0 @@ -208,7 +208,7 @@ func (f *File) countDrawings() int { return count } -// addDrawingPicture provides function to add picture by given sheet, +// 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) { @@ -263,8 +263,8 @@ func (f *File) addDrawingPicture(sheet, drawingXML, cell, file string, width, he f.saveFileList(drawingXML, output) } -// addDrawingRelationships provides function to add image part relationships in -// the file xl/drawings/_rels/drawing%d.xml.rels by given drawing index, +// addDrawingRelationships provides a function to add image part relationships +// in the file xl/drawings/_rels/drawing%d.xml.rels by given drawing index, // relationship type and target. func (f *File) addDrawingRelationships(index int, relType, target, targetMode string) int { var rels = "xl/drawings/_rels/drawing" + strconv.Itoa(index) + ".xml.rels" @@ -292,8 +292,8 @@ func (f *File) addDrawingRelationships(index int, relType, target, targetMode st return rID } -// countMedia provides function to get media files count storage in the folder -// xl/media/image. +// countMedia provides a function to get media files count storage in the +// folder xl/media/image. func (f *File) countMedia() int { count := 0 for k := range f.XLSX { @@ -304,8 +304,8 @@ func (f *File) countMedia() int { return count } -// addMedia provides function to add picture into folder xl/media/image by given -// file name and extension name. +// addMedia provides a function to add picture into folder xl/media/image by +// given file name and extension name. func (f *File) addMedia(file, ext string) { count := f.countMedia() dat, _ := ioutil.ReadFile(file) @@ -313,8 +313,8 @@ func (f *File) addMedia(file, ext string) { f.XLSX[media] = dat } -// setContentTypePartImageExtensions provides function to set the content type -// for relationship parts and the Main Document part. +// setContentTypePartImageExtensions provides a function to set the content +// type for relationship parts and the Main Document part. func (f *File) setContentTypePartImageExtensions() { var imageTypes = map[string]bool{"jpeg": false, "png": false, "gif": false} content := f.contentTypesReader() @@ -334,7 +334,7 @@ func (f *File) setContentTypePartImageExtensions() { } } -// setContentTypePartVMLExtensions provides function to set the content type +// setContentTypePartVMLExtensions provides a function to set the content type // for relationship parts and the Main Document part. func (f *File) setContentTypePartVMLExtensions() { vml := false @@ -352,8 +352,8 @@ func (f *File) setContentTypePartVMLExtensions() { } } -// addContentTypePart provides function to add content type part relationships -// in the file [Content_Types].xml by given index. +// 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(){ "comments": f.setContentTypePartVMLExtensions, @@ -387,7 +387,7 @@ func (f *File) addContentTypePart(index int, contentType string) { }) } -// getSheetRelationshipsTargetByID provides function to get Target attribute +// getSheetRelationshipsTargetByID provides a function to get Target attribute // value in xl/worksheets/_rels/sheet%d.xml.rels by given worksheet name and // relationship index. func (f *File) getSheetRelationshipsTargetByID(sheet, rID string) string { @@ -406,9 +406,9 @@ func (f *File) getSheetRelationshipsTargetByID(sheet, rID string) string { return "" } -// GetPicture provides function to get picture base name and raw content embed -// in XLSX by given worksheet and cell name. This function returns the file name -// in XLSX and file contents as []byte data types. For example: +// GetPicture provides a function to get picture base name and raw content +// embed in XLSX by given worksheet and cell name. This function returns the +// file name in XLSX and file contents as []byte data types. For example: // // xlsx, err := excelize.OpenFile("./Book1.xlsx") // if err != nil { @@ -463,8 +463,9 @@ func (f *File) GetPicture(sheet, cell string) (string, []byte) { return "", []byte{} } -// getDrawingRelationships provides function to get drawing relationships from -// xl/drawings/_rels/drawing%s.xml.rels by given file name and relationship ID. +// getDrawingRelationships provides a function to get drawing relationships +// from xl/drawings/_rels/drawing%s.xml.rels by given file name and +// relationship ID. func (f *File) getDrawingRelationships(rels, rID string) *xlsxWorkbookRelation { _, ok := f.XLSX[rels] if !ok { diff --git a/rows.go b/rows.go index ba569ad2c6..521a945424 100644 --- a/rows.go +++ b/rows.go @@ -132,7 +132,7 @@ func (err ErrSheetNotExist) Error() string { // Rows return a rows iterator. For example: // -// rows, err := xlsx.GetRows("Sheet1") +// rows, err := xlsx.Rows("Sheet1") // for rows.Next() { // for _, colCell := range rows.Columns() { // fmt.Print(colCell, "\t") @@ -202,7 +202,7 @@ func (f *File) SetRowHeight(sheet string, row int, height float64) { xlsx.SheetData.Row[rowIdx].CustomHeight = true } -// getRowHeight provides function to get row height in pixels by given sheet +// getRowHeight provides a function to get row height in pixels by given sheet // name and row index. func (f *File) getRowHeight(sheet string, row int) int { xlsx := f.workSheetReader(sheet) @@ -215,7 +215,7 @@ func (f *File) getRowHeight(sheet string, row int) int { return int(defaultRowHeightPixels) } -// GetRowHeight provides function to get row height by given worksheet name +// GetRowHeight provides a function to get row height by given worksheet name // and row index. For example, get the height of the first row in Sheet1: // // xlsx.GetRowHeight("Sheet1", 1) @@ -231,7 +231,7 @@ func (f *File) GetRowHeight(sheet string, row int) float64 { return defaultRowHeightPixels } -// sharedStringsReader provides function to get the pointer to the structure +// sharedStringsReader provides a function to get the pointer to the structure // after deserialization of xl/sharedStrings.xml. func (f *File) sharedStringsReader() *xlsxSST { if f.SharedStrings == nil { @@ -246,8 +246,9 @@ func (f *File) sharedStringsReader() *xlsxSST { return f.SharedStrings } -// getValueFrom return a value from a column/row cell, this function is inteded -// to be used with for range on rows an argument with the xlsx opened file. +// getValueFrom return a value from a column/row cell, this function is +// inteded to be used with for range on rows an argument with the xlsx opened +// file. func (xlsx *xlsxC) getValueFrom(f *File, d *xlsxSST) (string, error) { switch xlsx.T { case "s": @@ -315,9 +316,9 @@ func (f *File) SetRowOutlineLevel(sheet string, rowIndex int, level uint8) { xlsx.SheetData.Row[rowIndex].OutlineLevel = level } -// GetRowOutlineLevel provides a function to get outline level number of a single row by given -// worksheet name and row index. For example, get outline number of row 2 in -// Sheet1: +// GetRowOutlineLevel provides a function to get outline level number of a +// single row by given worksheet name and row index. For example, get outline +// number of row 2 in Sheet1: // // xlsx.GetRowOutlineLevel("Sheet1", 2) // @@ -329,8 +330,8 @@ func (f *File) GetRowOutlineLevel(sheet string, rowIndex int) uint8 { return xlsx.SheetData.Row[rowIndex].OutlineLevel } -// RemoveRow provides function to remove single row by given worksheet name and -// row index. For example, remove row 3 in Sheet1: +// RemoveRow provides a function to remove single row by given worksheet name +// and row index. For example, remove row 3 in Sheet1: // // xlsx.RemoveRow("Sheet1", 2) // @@ -349,8 +350,8 @@ func (f *File) RemoveRow(sheet string, row int) { } } -// InsertRow provides function to insert a new row before given row index. For -// example, create a new row before row 3 in Sheet1: +// InsertRow provides a function to insert a new row before given row index. +// For example, create a new row before row 3 in Sheet1: // // xlsx.InsertRow("Sheet1", 2) // @@ -362,8 +363,8 @@ func (f *File) InsertRow(sheet string, row int) { f.adjustHelper(sheet, -1, row, 1) } -// checkRow provides function to check and fill each column element for all rows -// and make that is continuous in a worksheet of XML. For example: +// 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: // // // @@ -416,7 +417,7 @@ func checkRow(xlsx *xlsxWorksheet) { } } -// completeRow provides function to check and fill each column element for a +// completeRow provides a function to check and fill each column element for a // single row and make that is continuous in a worksheet of XML by given row // index and axis. func completeRow(xlsx *xlsxWorksheet, row, cell int) { @@ -448,9 +449,9 @@ func completeRow(xlsx *xlsxWorksheet, row, cell int) { } } -// convertRowHeightToPixels provides function to convert the height of a 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. +// convertRowHeightToPixels provides a function to convert the height of a +// 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 { diff --git a/shape.go b/shape.go index 96cedb4942..8fbfbcd394 100644 --- a/shape.go +++ b/shape.go @@ -7,7 +7,7 @@ import ( "strings" ) -// parseFormatShapeSet provides function to parse the format settings of the +// parseFormatShapeSet provides a function to parse the format settings of the // shape with default value. func parseFormatShapeSet(formatSet string) (*formatShape, error) { format := formatShape{ @@ -29,8 +29,8 @@ func parseFormatShapeSet(formatSet string) (*formatShape, error) { // AddShape provides the method to add shape in a sheet by given worksheet // index, shape format set (such as offset, scale, aspect ratio setting and -// print settings) and properties set. For example, add text box (rect shape) in -// Sheet1: +// print settings) and properties set. For example, add text box (rect shape) +// in Sheet1: // // xlsx.AddShape("Sheet1", "G6", `{"type":"rect","color":{"line":"#4286F4","fill":"#8eb9ff"},"paragraph":[{"text":"Rectangle Shape","font":{"bold":true,"italic":true,"family":"Berlin Sans FB Demi","size":36,"color":"#777777","underline":"sng"}}],"width":180,"height": 90}`) // @@ -272,7 +272,7 @@ func (f *File) AddShape(sheet, cell, format string) error { return err } -// addDrawingShape provides function to add preset geometry by given sheet, +// addDrawingShape provides a function to add preset geometry by given sheet, // drawingXMLand format sets. func (f *File) addDrawingShape(sheet, drawingXML, cell string, formatSet *formatShape) { 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} @@ -397,7 +397,7 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, formatSet *format f.saveFileList(drawingXML, output) } -// setShapeRef provides function to set color with hex model by given actual +// setShapeRef provides a function to set color with hex model by given actual // color value. func setShapeRef(color string, i int) *aRef { if color == "" { diff --git a/sheet.go b/sheet.go index 6029a299b6..011ecb8584 100644 --- a/sheet.go +++ b/sheet.go @@ -14,7 +14,7 @@ import ( "github.com/mohae/deepcopy" ) -// NewSheet provides function to create a new sheet by given worksheet name, +// NewSheet provides a function to create a new sheet by given worksheet name, // when creating a new XLSX file, the default sheet will be create, when you // create a new file. func (f *File) NewSheet(name string) int { @@ -36,7 +36,7 @@ func (f *File) NewSheet(name string) int { return f.SheetCount } -// contentTypesReader provides function to get the pointer to the +// contentTypesReader provides a function to get the pointer to the // [Content_Types].xml structure after deserialization. func (f *File) contentTypesReader() *xlsxTypes { if f.ContentTypes == nil { @@ -47,7 +47,7 @@ func (f *File) contentTypesReader() *xlsxTypes { return f.ContentTypes } -// contentTypesWriter provides function to save [Content_Types].xml after +// contentTypesWriter provides a function to save [Content_Types].xml after // serialize structure. func (f *File) contentTypesWriter() { if f.ContentTypes != nil { @@ -56,7 +56,7 @@ func (f *File) contentTypesWriter() { } } -// workbookReader provides function to get the pointer to the xl/workbook.xml +// workbookReader provides a function to get the pointer to the xl/workbook.xml // structure after deserialization. func (f *File) workbookReader() *xlsxWorkbook { if f.WorkBook == nil { @@ -67,7 +67,7 @@ func (f *File) workbookReader() *xlsxWorkbook { return f.WorkBook } -// workbookWriter provides function to save xl/workbook.xml after serialize +// workbookWriter provides a function to save xl/workbook.xml after serialize // structure. func (f *File) workbookWriter() { if f.WorkBook != nil { @@ -76,7 +76,7 @@ func (f *File) workbookWriter() { } } -// worksheetWriter provides function to save xl/worksheets/sheet%d.xml after +// worksheetWriter provides a function to save xl/worksheets/sheet%d.xml after // serialize structure. func (f *File) worksheetWriter() { for path, sheet := range f.Sheet { @@ -94,7 +94,7 @@ func (f *File) worksheetWriter() { } } -// trimCell provides function to trim blank cells which created by completeCol. +// trimCell provides a function to trim blank cells which created by completeCol. func trimCell(column []xlsxC) []xlsxC { col := make([]xlsxC, len(column)) i := 0 @@ -147,7 +147,7 @@ func (f *File) setWorkbook(name string, rid int) { }) } -// workbookRelsReader provides function to read and unmarshal workbook +// workbookRelsReader provides a function to read and unmarshal workbook // relationships of XLSX file. func (f *File) workbookRelsReader() *xlsxWorkbookRels { if f.WorkBookRels == nil { @@ -158,7 +158,7 @@ func (f *File) workbookRelsReader() *xlsxWorkbookRels { return f.WorkBookRels } -// workbookRelsWriter provides function to save xl/_rels/workbook.xml.rels after +// workbookRelsWriter provides a function to save xl/_rels/workbook.xml.rels after // serialize structure. func (f *File) workbookRelsWriter() { if f.WorkBookRels != nil { @@ -211,7 +211,7 @@ func replaceRelationshipsNameSpaceBytes(workbookMarshal []byte) []byte { return bytes.Replace(workbookMarshal, oldXmlns, newXmlns, -1) } -// SetActiveSheet provides function to set default active worksheet of XLSX by +// SetActiveSheet provides a function to set default active worksheet of XLSX by // given index. Note that active index is different with the index that got by // function GetSheetMap, and it should be greater than 0 and less than total // worksheet numbers. @@ -247,7 +247,7 @@ func (f *File) SetActiveSheet(index int) { } } -// GetActiveSheetIndex provides function to get active sheet of XLSX. If not +// GetActiveSheetIndex provides a function to get active sheet of XLSX. If not // found the active sheet will be return integer 0. func (f *File) GetActiveSheetIndex() int { buffer := bytes.Buffer{} @@ -269,7 +269,7 @@ func (f *File) GetActiveSheetIndex() int { return 0 } -// SetSheetName provides function to set the worksheet name be given old and new +// SetSheetName provides a function to set the worksheet name be given old and new // worksheet name. 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 @@ -287,7 +287,7 @@ func (f *File) SetSheetName(oldName, newName string) { } } -// GetSheetName provides function to get worksheet name of XLSX by given +// GetSheetName provides a function to get worksheet name of XLSX by given // worksheet index. If given sheet index is invalid, will return an empty // string. func (f *File) GetSheetName(index int) string { @@ -306,7 +306,7 @@ func (f *File) GetSheetName(index int) string { return "" } -// GetSheetIndex provides function to get worksheet index of XLSX by given sheet +// GetSheetIndex provides a function to get worksheet index of XLSX by given sheet // name. If given worksheet name is invalid, will return an integer type value // 0. func (f *File) GetSheetIndex(name string) int { @@ -325,7 +325,7 @@ func (f *File) GetSheetIndex(name string) int { return 0 } -// GetSheetMap provides function to get worksheet name and index map of XLSX. +// GetSheetMap provides a function to get worksheet name and index map of XLSX. // For example: // // xlsx, err := excelize.OpenFile("./Book1.xlsx") @@ -351,7 +351,7 @@ func (f *File) GetSheetMap() map[int]string { return sheetMap } -// getSheetMap provides function to get worksheet name and XML file path map of +// getSheetMap provides a function to get worksheet name and XML file path map of // XLSX. func (f *File) getSheetMap() map[string]string { maps := make(map[string]string) @@ -361,7 +361,7 @@ func (f *File) getSheetMap() map[string]string { return maps } -// SetSheetBackground provides function to set background picture by given +// SetSheetBackground provides a function to set background picture by given // worksheet name. func (f *File) SetSheetBackground(sheet, picture string) error { var err error @@ -381,7 +381,7 @@ func (f *File) SetSheetBackground(sheet, picture string) error { return err } -// DeleteSheet provides function to delete worksheet in a workbook by given +// DeleteSheet provides a function to delete worksheet in a workbook by given // 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. @@ -405,7 +405,7 @@ func (f *File) DeleteSheet(name string) { f.SetActiveSheet(len(f.GetSheetMap())) } -// deleteSheetFromWorkbookRels provides function to remove worksheet +// deleteSheetFromWorkbookRels provides a function to remove worksheet // relationships by given relationships ID in the file // xl/_rels/workbook.xml.rels. func (f *File) deleteSheetFromWorkbookRels(rID string) string { @@ -419,7 +419,7 @@ func (f *File) deleteSheetFromWorkbookRels(rID string) string { return "" } -// deleteSheetFromContentTypes provides function to remove worksheet +// deleteSheetFromContentTypes provides a function to remove worksheet // relationships by given target name in the file [Content_Types].xml. func (f *File) deleteSheetFromContentTypes(target string) { content := f.contentTypesReader() @@ -430,7 +430,7 @@ func (f *File) deleteSheetFromContentTypes(target string) { } } -// CopySheet provides function to duplicate a worksheet by gave source and +// CopySheet provides a function to duplicate a worksheet by gave source and // target worksheet index. Note that currently doesn't support duplicate // workbooks that contain tables, charts or pictures. For Example: // @@ -447,7 +447,7 @@ func (f *File) CopySheet(from, to int) error { return nil } -// copySheet provides function to duplicate a worksheet by gave source and +// copySheet provides a function to duplicate a worksheet by gave source and // target worksheet name. func (f *File) copySheet(from, to int) { sheet := f.workSheetReader("sheet" + strconv.Itoa(from)) @@ -468,7 +468,7 @@ func (f *File) copySheet(from, to int) { } } -// SetSheetVisible provides function to set worksheet visible by given worksheet +// 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 http://msdn.microsoft.com/en-us/library/office/documentformat.openxml.spreadsheet.sheetstatevalues.aspx @@ -510,14 +510,14 @@ func (f *File) SetSheetVisible(name string, visible bool) { } } -// parseFormatPanesSet provides function to parse the panes settings. +// parseFormatPanesSet provides a function to parse the panes settings. func parseFormatPanesSet(formatSet string) (*formatPanes, error) { format := formatPanes{} err := json.Unmarshal([]byte(formatSet), &format) return &format, err } -// SetPanes provides function to create and remove freeze panes and split panes +// SetPanes provides a function to create and remove freeze panes and split panes // by given worksheet name and panes format set. // // activePane defines the pane that is active. The possible values for this @@ -631,7 +631,7 @@ func (f *File) SetPanes(sheet, panes string) { xlsx.SheetViews.SheetView[len(xlsx.SheetViews.SheetView)-1].Selection = s } -// GetSheetVisible provides function to get worksheet visible by given worksheet +// GetSheetVisible provides a function to get worksheet visible by given worksheet // name. For example, get visible state of Sheet1: // // xlsx.GetSheetVisible("Sheet1") @@ -649,7 +649,7 @@ func (f *File) GetSheetVisible(name string) bool { return visible } -// trimSheetName provides function to trim invaild characters by given worksheet +// trimSheetName provides a function to trim invaild characters by given worksheet // name. func trimSheetName(name string) string { r := []rune{} diff --git a/sheetpr.go b/sheetpr.go index 7b8df54837..1601ab138c 100644 --- a/sheetpr.go +++ b/sheetpr.go @@ -98,7 +98,7 @@ func (o *AutoPageBreaks) getSheetPrOption(pr *xlsxSheetPr) { *o = AutoPageBreaks(pr.PageSetUpPr.AutoPageBreaks) } -// SetSheetPrOptions provides function to sets worksheet properties. +// SetSheetPrOptions provides a function to sets worksheet properties. // // Available options: // CodeName(string) @@ -120,7 +120,7 @@ func (f *File) SetSheetPrOptions(name string, opts ...SheetPrOption) error { return nil } -// GetSheetPrOptions provides function to gets worksheet properties. +// GetSheetPrOptions provides a function to gets worksheet properties. // // Available options: // CodeName(string) diff --git a/styles.go b/styles.go index fdbf932d0d..a288b3706f 100644 --- a/styles.go +++ b/styles.go @@ -10,8 +10,8 @@ import ( ) // Excel styles can reference number formats that are built-in, all of which -// have an id less than 164. This is a possibly incomplete list comprised of as -// many of them as I could find. +// have an id less than 164. This is a possibly incomplete list comprised of +// as many of them as I could find. var builtInNumFmt = map[int]string{ 0: "general", 1: "0", @@ -829,14 +829,15 @@ var criteriaType = map[string]string{ "continue month": "continueMonth", } -// formatToString provides function to return original string by given built-in -// number formats code and cell string. +// formatToString provides a function to return original string by given +// built-in number formats code and cell string. func formatToString(i int, v string) string { return v } -// formatToInt provides function to convert original string to integer format as -// string type by given built-in number formats code and cell 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(i int, v string) string { f, err := strconv.ParseFloat(v, 64) if err != nil { @@ -845,8 +846,9 @@ func formatToInt(i int, v string) string { return fmt.Sprintf("%d", int(f)) } -// formatToFloat provides function to convert original string to float format as -// string type by given built-in number formats code and cell 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(i int, v string) string { f, err := strconv.ParseFloat(v, 64) if err != nil { @@ -855,8 +857,8 @@ func formatToFloat(i int, v string) string { return fmt.Sprintf("%.2f", f) } -// formatToA provides function to convert original string to special format as -// string type by given built-in number formats code and cell 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(i int, v string) string { f, err := strconv.ParseFloat(v, 64) if err != nil { @@ -870,8 +872,8 @@ func formatToA(i int, v string) string { return fmt.Sprintf("%d", t) } -// formatToB provides function to convert original string to special format as -// string type by given built-in number formats code and cell 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(i int, v string) string { f, err := strconv.ParseFloat(v, 64) if err != nil { @@ -883,8 +885,8 @@ func formatToB(i int, v string) string { return fmt.Sprintf("%.2f", f) } -// formatToC provides function to convert original string to special format as -// string type by given built-in number formats code and cell 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(i int, v string) string { f, err := strconv.ParseFloat(v, 64) if err != nil { @@ -894,8 +896,8 @@ func formatToC(i int, v string) string { return fmt.Sprintf("%d%%", int(f)) } -// formatToD provides function to convert original string to special format as -// string type by given built-in number formats code and cell 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(i int, v string) string { f, err := strconv.ParseFloat(v, 64) if err != nil { @@ -905,8 +907,8 @@ func formatToD(i int, v string) string { return fmt.Sprintf("%.2f%%", f) } -// formatToE provides function to convert original string to special format as -// string type by given built-in number formats code and cell 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(i int, v string) string { f, err := strconv.ParseFloat(v, 64) if err != nil { @@ -915,17 +917,17 @@ func formatToE(i int, v string) string { return fmt.Sprintf("%.e", f) } -// parseTime provides function to returns a string parsed using time.Time. +// parseTime provides a function to returns a string parsed using time.Time. // Replace Excel placeholders with Go time placeholders. For example, replace -// yyyy with 2006. These are in a specific order, due to the fact that m is used -// in month, minute, and am/pm. It would be easier to fix that with regular -// expressions, but if it's possible to keep this simple it would be easier to -// maintain. Full-length month and days (e.g. March, Tuesday) have letters in -// them that would be replaced by other characters below (such as the 'h' in -// March, or the 'd' in Tuesday) below. First we convert them to arbitrary -// characters unused in Excel Date formats, and then at the end, turn them to -// what they should actually be. -// Based off: http://www.ozgrid.com/Excel/CustomFormats.htm +// yyyy with 2006. These are in a specific order, due to the fact that m is +// used in month, minute, and am/pm. It would be easier to fix that with +// regular expressions, but if it's possible to keep this simple it would be +// easier to maintain. Full-length month and days (e.g. March, Tuesday) have +// letters in them that would be replaced by other characters below (such as +// the 'h' in March, or the 'd' in Tuesday) below. First we convert them to +// arbitrary characters unused in Excel Date formats, and then at the end, +// turn them to what they should actually be. Based off: +// http://www.ozgrid.com/Excel/CustomFormats.htm func parseTime(i int, v string) string { f, err := strconv.ParseFloat(v, 64) if err != nil { @@ -983,7 +985,7 @@ func is12HourTime(format string) bool { return strings.Contains(format, "am/pm") || strings.Contains(format, "AM/PM") || strings.Contains(format, "a/p") || strings.Contains(format, "A/P") } -// stylesReader provides function to get the pointer to the structure after +// stylesReader provides a function to get the pointer to the structure after // deserialization of xl/styles.xml. func (f *File) stylesReader() *xlsxStyleSheet { if f.Styles == nil { @@ -994,7 +996,7 @@ func (f *File) stylesReader() *xlsxStyleSheet { return f.Styles } -// styleSheetWriter provides function to save xl/styles.xml after serialize +// styleSheetWriter provides a function to save xl/styles.xml after serialize // structure. func (f *File) styleSheetWriter() { if f.Styles != nil { @@ -1003,7 +1005,7 @@ func (f *File) styleSheetWriter() { } } -// parseFormatStyleSet provides function to parse the format settings of the +// parseFormatStyleSet provides a function to parse the format settings of the // cells and conditional formats. func parseFormatStyleSet(style string) (*formatStyle, error) { format := formatStyle{ @@ -1013,8 +1015,8 @@ func parseFormatStyleSet(style string) (*formatStyle, error) { return &format, err } -// NewStyle provides function to create style for cells by given style format. -// Note that the color field uses RGB color code. +// NewStyle provides a function to create style for cells by given style +// format. Note that the color field uses RGB color code. // // The following shows the border styles sorted by excelize index number: // @@ -1906,10 +1908,10 @@ func (f *File) NewStyle(style string) (int, error) { return cellXfsID, nil } -// NewConditionalStyle provides 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. +// 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. func (f *File) NewConditionalStyle(style string) (int, error) { s := f.stylesReader() fs, err := parseFormatStyleSet(style) @@ -1935,7 +1937,8 @@ func (f *File) NewConditionalStyle(style string) (int, error) { return s.Dxfs.Count - 1, nil } -// setFont provides function to add font style by given cell format settings. +// setFont provides a function to add font style by given cell format +// settings. func setFont(formatStyle *formatStyle) *font { fontUnderlineType := map[string]string{"single": "single", "double": "double"} if formatStyle.Font.Size < 1 { @@ -1963,8 +1966,8 @@ func setFont(formatStyle *formatStyle) *font { return &f } -// setNumFmt provides function to check if number format code in the range of -// built-in values. +// setNumFmt provides a function to check if number format code in the range +// of built-in values. func setNumFmt(style *xlsxStyleSheet, formatStyle *formatStyle) int { dp := "0." numFmtID := 164 // Default custom number format code from 164. @@ -2011,7 +2014,7 @@ func setNumFmt(style *xlsxStyleSheet, formatStyle *formatStyle) int { return formatStyle.NumFmt } -// setCustomNumFmt provides function to set custom number format code. +// setCustomNumFmt provides a function to set custom number format code. func setCustomNumFmt(style *xlsxStyleSheet, formatStyle *formatStyle) int { nf := xlsxNumFmt{FormatCode: *formatStyle.CustomNumFmt} if style.NumFmts != nil { @@ -2029,7 +2032,7 @@ func setCustomNumFmt(style *xlsxStyleSheet, formatStyle *formatStyle) int { return nf.NumFmtID } -// setLangNumFmt provides function to set number format code with language. +// setLangNumFmt provides a function to set number format code with language. func setLangNumFmt(style *xlsxStyleSheet, formatStyle *formatStyle) int { numFmts, ok := langNumFmt[formatStyle.Lang] if !ok { @@ -2056,8 +2059,8 @@ func setLangNumFmt(style *xlsxStyleSheet, formatStyle *formatStyle) int { return nf.NumFmtID } -// setFills provides function to add fill elements in the styles.xml by given -// cell format settings. +// setFills provides a function to add fill elements in the styles.xml by +// given cell format settings. func setFills(formatStyle *formatStyle, fg bool) *xlsxFill { var patterns = []string{ "none", @@ -2137,9 +2140,10 @@ func setFills(formatStyle *formatStyle, fg bool) *xlsxFill { return &fill } -// setAlignment provides function to formatting information pertaining to text -// alignment in cells. There are a variety of choices for how text is aligned -// both horizontally and vertically, as well as indentation settings, and so on. +// setAlignment provides a function to formatting information pertaining to +// text alignment in cells. There are a variety of choices for how text is +// aligned both horizontally and vertically, as well as indentation settings, +// and so on. func setAlignment(formatStyle *formatStyle) *xlsxAlignment { var alignment xlsxAlignment if formatStyle.Alignment != nil { @@ -2156,7 +2160,7 @@ func setAlignment(formatStyle *formatStyle) *xlsxAlignment { return &alignment } -// setProtection provides function to set protection properties associated +// setProtection provides a function to set protection properties associated // with the cell. func setProtection(formatStyle *formatStyle) *xlsxProtection { var protection xlsxProtection @@ -2167,7 +2171,7 @@ func setProtection(formatStyle *formatStyle) *xlsxProtection { return &protection } -// setBorders provides function to add border elements in the styles.xml by +// setBorders provides a function to add border elements in the styles.xml by // given borders format settings. func setBorders(formatStyle *formatStyle) *xlsxBorder { var styles = []string{ @@ -2219,7 +2223,7 @@ func setBorders(formatStyle *formatStyle) *xlsxBorder { return &border } -// setCellXfs provides function to set describes all of the formatting for a +// 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 { var xf xlsxXf @@ -2246,9 +2250,10 @@ func setCellXfs(style *xlsxStyleSheet, fontID, numFmtID, fillID, borderID int, a return style.CellXfs.Count - 1 } -// SetCellStyle provides function to add style attribute for cells by given +// 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. +// diagonalUp type border should be use same color in the same coordinate +// area. // // For example create a borders of cell H9 on Sheet1: // @@ -2352,9 +2357,10 @@ func (f *File) SetCellStyle(sheet, hcell, vcell string, styleID int) { } } -// SetConditionalFormat provides function to create conditional formatting rule -// for cell value. Conditional formatting is a feature of Excel which allows you -// to apply a format to a cell or a range of cells based on certain criteria. +// SetConditionalFormat provides a function to create conditional formatting +// rule for cell value. Conditional formatting is a feature of Excel which +// allows you to apply a format to a cell or a range of cells based on certain +// criteria. // // The type option is a required parameter and it has no default value. // Allowable type values and their associated parameters are: @@ -2606,9 +2612,9 @@ func (f *File) SetConditionalFormat(sheet, area, formatSet string) error { return err } -// drawCondFmtCellIs provides 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. +// 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 { c := &xlsxCfRule{ Priority: p + 1, @@ -2629,8 +2635,8 @@ func drawCondFmtCellIs(p int, ct string, format *formatConditional) *xlsxCfRule return c } -// drawCondFmtTop10 provides function to create conditional formatting rule for -// top N (default is top 10) by given priority, criteria type and format +// 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 { c := &xlsxCfRule{ @@ -2647,9 +2653,9 @@ func drawCondFmtTop10(p int, ct string, format *formatConditional) *xlsxCfRule { return c } -// drawCondFmtAboveAverage provides function to create conditional formatting -// rule for above average and below average by given priority, criteria type and -// format settings. +// 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 { return &xlsxCfRule{ Priority: p + 1, @@ -2659,7 +2665,7 @@ func drawCondFmtAboveAverage(p int, ct string, format *formatConditional) *xlsxC } } -// drawCondFmtDuplicateUniqueValues provides function to create conditional +// 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 { @@ -2670,9 +2676,9 @@ func drawCondFmtDuplicateUniqueValues(p int, ct string, format *formatConditiona } } -// drawCondFmtColorScale provides 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. +// 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 { minValue := format.MinValue if minValue == "" { @@ -2708,8 +2714,8 @@ func drawCondFmtColorScale(p int, ct string, format *formatConditional) *xlsxCfR return c } -// drawCondFmtDataBar provides function to create conditional formatting rule -// for data bar by given priority, criteria type and format settings. +// 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 { return &xlsxCfRule{ Priority: p + 1, @@ -2721,8 +2727,8 @@ func drawCondFmtDataBar(p int, ct string, format *formatConditional) *xlsxCfRule } } -// drawConfFmtExp provides function to create conditional formatting rule for -// expression by given priority, criteria type and format settings. +// drawConfFmtExp 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 { return &xlsxCfRule{ Priority: p + 1, @@ -2732,12 +2738,13 @@ func drawConfFmtExp(p int, ct string, format *formatConditional) *xlsxCfRule { } } -// getPaletteColor provides function to convert the RBG color by given string. +// 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) } -// themeReader provides function to get the pointer to the xl/theme/theme1.xml +// themeReader provides a function to get the pointer to the xl/theme/theme1.xml // structure after deserialization. func (f *File) themeReader() *xlsxTheme { var theme xlsxTheme diff --git a/table.go b/table.go index 67f9be06ac..643b156c87 100644 --- a/table.go +++ b/table.go @@ -9,7 +9,7 @@ import ( "strings" ) -// parseFormatTableSet provides function to parse the format settings of the +// parseFormatTableSet provides a function to parse the format settings of the // table with default value. func parseFormatTableSet(formatSet string) (*formatTable, error) { format := formatTable{ @@ -75,8 +75,8 @@ func (f *File) AddTable(sheet, hcell, vcell, format string) error { return err } -// countTables provides function to get table files count storage in the folder -// xl/tables. +// countTables provides a function to get table files count storage in the +// folder xl/tables. func (f *File) countTables() int { count := 0 for k := range f.XLSX { @@ -87,7 +87,7 @@ func (f *File) countTables() int { return count } -// addSheetTable provides function to add tablePart element to +// addSheetTable provides a function to add tablePart element to // xl/worksheets/sheet%d.xml by given worksheet name and relationship index. func (f *File) addSheetTable(sheet string, rID int) { xlsx := f.workSheetReader(sheet) @@ -101,8 +101,8 @@ func (f *File) addSheetTable(sheet string, rID int) { xlsx.TableParts.TableParts = append(xlsx.TableParts.TableParts, table) } -// addTable provides function to add table by given worksheet name, coordinate -// area and format set. +// addTable provides a function to add table by given worksheet name, +// coordinate area and format set. func (f *File) addTable(sheet, tableXML string, hxAxis, hyAxis, vxAxis, vyAxis, i int, formatSet *formatTable) { // Correct the minimum number of rows, the table at least two lines. if hyAxis == vyAxis { @@ -157,7 +157,7 @@ func (f *File) addTable(sheet, tableXML string, hxAxis, hyAxis, vxAxis, vyAxis, f.saveFileList(tableXML, table) } -// parseAutoFilterSet provides function to parse the settings of the auto +// parseAutoFilterSet provides a function to parse the settings of the auto // filter. func parseAutoFilterSet(formatSet string) (*formatAutoFilter, error) { format := formatAutoFilter{} @@ -264,7 +264,7 @@ func (f *File) AutoFilter(sheet, hcell, vcell, format string) error { return f.autoFilter(sheet, ref, refRange, hxAxis, formatSet) } -// autoFilter provides function to extract the tokens from the filter +// 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, hxAxis int, formatSet *formatAutoFilter) error { xlsx := f.workSheetReader(sheet) @@ -301,8 +301,8 @@ func (f *File) autoFilter(sheet, ref string, refRange, hxAxis int, formatSet *fo return nil } -// writeAutoFilter provides function to check for single or double custom filters -// as default filters and handle them accordingly. +// 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) { if len(exp) == 1 && exp[0] == 2 { // Single equality. @@ -329,7 +329,7 @@ func (f *File) writeAutoFilter(filter *xlsxAutoFilter, exp []int, tokens []strin } } -// writeCustomFilter provides function to write the element. +// writeCustomFilter provides a function to write the element. func (f *File) writeCustomFilter(filter *xlsxAutoFilter, operator int, val string) { operators := map[int]string{ 1: "lessThan", @@ -353,8 +353,9 @@ func (f *File) writeCustomFilter(filter *xlsxAutoFilter, operator int, val strin } } -// parseFilterExpression provides function to converts the tokens of a possibly -// conditional expression into 1 or 2 sub expressions for further parsing. +// parseFilterExpression provides a function to converts the tokens of a +// possibly conditional expression into 1 or 2 sub expressions for further +// parsing. // // Examples: // @@ -394,7 +395,7 @@ func (f *File) parseFilterExpression(expression string, tokens []string) ([]int, return expressions, t, nil } -// parseFilterTokens provides function to parse the 3 tokens of a filter +// parseFilterTokens provides a function to parse the 3 tokens of a filter // expression and return the operator and token. func (f *File) parseFilterTokens(expression string, tokens []string) ([]int, string, error) { operators := map[string]int{ From ce5b37a4ac93196f90cfef2aec381a9b7d153fdd Mon Sep 17 00:00:00 2001 From: Farmerx Date: Mon, 20 Aug 2018 16:53:51 +0800 Subject: [PATCH 009/957] =?UTF-8?q?#=20fix=20:=20file=20close=20=E6=B2=A1?= =?UTF-8?q?=E6=9C=89=E5=85=B3=E9=97=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- picture.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/picture.go b/picture.go index d039ae0ac3..97e72a6645 100644 --- a/picture.go +++ b/picture.go @@ -88,7 +88,11 @@ func (f *File) AddPicture(sheet, cell, picture, format string) error { if !ok { return errors.New("Unsupported image extension") } - readFile, _ := os.Open(picture) + readFile, err := os.Open(picture) + if err!=nil{ + return err + } + defer readFile.Close() image, _, _ := image.DecodeConfig(readFile) _, file := filepath.Split(picture) formatSet, err := parseFormatPictureSet(format) From 24a8d64f939afb5c15b04e552b3d3b7046daa851 Mon Sep 17 00:00:00 2001 From: rentiansheng Date: Sat, 1 Sep 2018 19:38:30 +0800 Subject: [PATCH 010/957] add datavalidation test and fixed struct bug issue #240 --- datavalidation.go | 32 ++++++++++++++------------------ datavalidation_test.go | 32 ++++++++++++++++++++++++++++++++ xmlWorksheet.go | 4 ++-- 3 files changed, 48 insertions(+), 20 deletions(-) create mode 100644 datavalidation_test.go diff --git a/datavalidation.go b/datavalidation.go index 5dae7c9df2..f1db7329dd 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -33,9 +33,9 @@ type DataValidationErrorStyle int // Data validation error styles const ( _ DataValidationErrorStyle = iota - DataValidationStyleStop - DataValidationStyleWarning - DataValidationStyleInformation + DataValidationErrorStyleStop + DataValidationErrorStyleWarning + DataValidationErrorStyleInformation ) // Data validation error styles @@ -71,16 +71,16 @@ 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 (dd *DataValidation) SetError(style DataValidationErrorStyle, title, msg string) { + dd.Error = &msg + dd.ErrorTitle = &title strStyle := styleStop switch style { - case DataValidationStyleStop: + case DataValidationErrorStyleStop: strStyle = styleStop - case DataValidationStyleWarning: + case DataValidationErrorStyleWarning: strStyle = styleWarning - case DataValidationStyleInformation: + case DataValidationErrorStyleInformation: strStyle = styleInformation } @@ -89,10 +89,10 @@ func (dd *DataValidation) SetError(style DataValidationErrorStyle, title, msg *s } // SetInput set prompt notice -func (dd *DataValidation) SetInput(title, msg *string) { +func (dd *DataValidation) SetInput(title, msg string) { dd.ShowInputMessage = convBoolToStr(true) - dd.PromptTitle = title - dd.Prompt = msg + dd.PromptTitle = &title + dd.Prompt = &msg } // SetDropList data validation list @@ -109,7 +109,7 @@ func (dd *DataValidation) SetRange(f1, f2 int, t DataValidationType, o DataValid if dataValidationFormulaStrLen < len(dd.Formula1) || dataValidationFormulaStrLen < len(dd.Formula2) { return fmt.Errorf(dataValidationFormulaStrLenErr) } - switch o { + /*switch o { case DataValidationOperatorBetween: if f1 > f2 { tmp := formula1 @@ -122,7 +122,7 @@ func (dd *DataValidation) SetRange(f1, f2 int, t DataValidationType, o DataValid formula1 = formula2 formula2 = tmp } - } + }*/ dd.Formula1 = formula1 dd.Formula2 = formula2 @@ -190,7 +190,3 @@ func (f *File) AddDataValidation(sheet string, dv *DataValidation) { xlsx.DataValidations.DataValidation = append(xlsx.DataValidations.DataValidation, dv) xlsx.DataValidations.Count = len(xlsx.DataValidations.DataValidation) } - -func (f *File) GetDataValidation(sheet, sqref string) { - -} diff --git a/datavalidation_test.go b/datavalidation_test.go new file mode 100644 index 0000000000..718131fede --- /dev/null +++ b/datavalidation_test.go @@ -0,0 +1,32 @@ +package excelize + +import ( + "testing" +) + +func TestDataValidation(t *testing.T) { + xlsx := NewFile() + + dvRange := NewDataValidation(true) + dvRange.Sqref = "A1:B2" + dvRange.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorBetween) + dvRange.SetError(DataValidationErrorStyleStop, "error title", "error body") + xlsx.AddDataValidation("Sheet1", dvRange) + + dvRange = NewDataValidation(true) + dvRange.Sqref = "A3:B4" + dvRange.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorGreaterThan) + dvRange.SetInput("input title", "input body") + xlsx.AddDataValidation("Sheet1", dvRange) + + dvRange = NewDataValidation(true) + dvRange.Sqref = "A5:B6" + dvRange.SetDropList([]string{"1", "2", "3"}) + xlsx.AddDataValidation("Sheet1", dvRange) + + // Test write file to given path. + err := xlsx.SaveAs("./test/Bookdatavalition.xlsx") + if err != nil { + t.Error(err) + } +} diff --git a/xmlWorksheet.go b/xmlWorksheet.go index f2ac9fb5c9..25e3904263 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -18,7 +18,7 @@ type xlsxWorksheet struct { MergeCells *xlsxMergeCells `xml:"mergeCells"` PhoneticPr *xlsxPhoneticPr `xml:"phoneticPr"` ConditionalFormatting []*xlsxConditionalFormatting `xml:"conditionalFormatting"` - DataValidations *xlsxDataValidations `xml:"dataValidations"` + DataValidations *xlsxDataValidations `xml:"dataValidations,omitempty"` Hyperlinks *xlsxHyperlinks `xml:"hyperlinks"` PrintOptions *xlsxPrintOptions `xml:"printOptions"` PageMargins *xlsxPageMargins `xml:"pageMargins"` @@ -298,7 +298,7 @@ type xlsxDataValidations struct { DisablePrompts bool `xml:"disablePrompts,attr,omitempty"` XWindow int `xml:"xWindow,attr,omitempty"` YWindow int `xml:"yWindow,attr,omitempty"` - DataValidation []*DataValidation `xml:"dataValidation,innerxml"` + DataValidation []*DataValidation `xml:"dataValidation"` } type DataValidation struct { From 562ba3d234489796b94aca01eda88aea7b0c5cbf Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 1 Sep 2018 23:32:44 +0800 Subject: [PATCH 011/957] Add function doc and fix golint error --- datavalidation.go | 95 ++++++++++++++++++++++++++--------------------- 1 file changed, 53 insertions(+), 42 deletions(-) diff --git a/datavalidation.go b/datavalidation.go index f1db7329dd..62d2cf84fd 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -5,16 +5,17 @@ import ( "strings" ) +// DataValidationType defined the type of data validation. type DataValidationType int -// Data validation types +// Data validation types. const ( _DataValidationType = iota - typeNone //inline use + typeNone // inline use DataValidationTypeCustom DataValidationTypeDate DataValidationTypeDecimal - typeList //inline use + typeList // inline use DataValidationTypeTextLeng DataValidationTypeTime // DataValidationTypeWhole Integer @@ -28,9 +29,10 @@ const ( dataValidationFormulaStrLenErr = "data validation must be 0-255 characters" ) +// DataValidationErrorStyle defined the style of data validation error alert. type DataValidationErrorStyle int -// Data validation error styles +// Data validation error styles. const ( _ DataValidationErrorStyle = iota DataValidationErrorStyleStop @@ -38,17 +40,17 @@ const ( DataValidationErrorStyleInformation ) -// Data validation error styles +// Data validation error styles. const ( styleStop = "stop" styleWarning = "warning" styleInformation = "information" ) -// DataValidationOperator operator enum +// DataValidationOperator operator enum. type DataValidationOperator int -// Data validation operators +// Data validation operators. const ( _DataValidationOperator = iota DataValidationOperatorBetween @@ -61,16 +63,16 @@ const ( DataValidationOperatorNotEqual ) -// NewDataValidation return data validation struct +// NewDataValidation return data validation struct. func NewDataValidation(allowBlank bool) *DataValidation { return &DataValidation{ - AllowBlank: convBoolToStr(allowBlank), - ShowErrorMessage: convBoolToStr(false), - ShowInputMessage: convBoolToStr(false), + AllowBlank: allowBlank, + ShowErrorMessage: false, + ShowInputMessage: false, } } -// SetError set error notice +// SetError set error notice. func (dd *DataValidation) SetError(style DataValidationErrorStyle, title, msg string) { dd.Error = &msg dd.ErrorTitle = &title @@ -84,45 +86,31 @@ func (dd *DataValidation) SetError(style DataValidationErrorStyle, title, msg st strStyle = styleInformation } - dd.ShowErrorMessage = convBoolToStr(true) + dd.ShowErrorMessage = true dd.ErrorStyle = &strStyle } -// SetInput set prompt notice +// SetInput set prompt notice. func (dd *DataValidation) SetInput(title, msg string) { - dd.ShowInputMessage = convBoolToStr(true) + dd.ShowInputMessage = true dd.PromptTitle = &title dd.Prompt = &msg } -// SetDropList data validation list +// SetDropList data validation list. func (dd *DataValidation) SetDropList(keys []string) error { dd.Formula1 = "\"" + strings.Join(keys, ",") + "\"" dd.Type = convDataValidationType(typeList) return nil } -// SetDropList data validation range +// SetRange provides function to set data validation range in drop list. func (dd *DataValidation) SetRange(f1, f2 int, t DataValidationType, o DataValidationOperator) error { formula1 := fmt.Sprintf("%d", f1) formula2 := fmt.Sprintf("%d", f2) if dataValidationFormulaStrLen < len(dd.Formula1) || dataValidationFormulaStrLen < len(dd.Formula2) { return fmt.Errorf(dataValidationFormulaStrLenErr) } - /*switch o { - case DataValidationOperatorBetween: - if f1 > f2 { - tmp := formula1 - formula1 = formula2 - formula2 = tmp - } - case DataValidationOperatorNotBetween: - if f1 > f2 { - tmp := formula1 - formula1 = formula2 - formula2 = tmp - } - }*/ dd.Formula1 = formula1 dd.Formula2 = formula2 @@ -131,7 +119,7 @@ func (dd *DataValidation) SetRange(f1, f2 int, t DataValidationType, o DataValid return nil } -// SetDropList data validation range +// SetSqref provides function to set data validation range in drop list. func (dd *DataValidation) SetSqref(sqref string) { if dd.Sqref == "" { dd.Sqref = sqref @@ -140,15 +128,7 @@ func (dd *DataValidation) SetSqref(sqref string) { } } -// convBoolToStr convert boolean to string , false to 0, true to 1 -func convBoolToStr(bl bool) string { - if bl { - return "1" - } - return "0" -} - -// convDataValidationType get excel data validation type +// convDataValidationType get excel data validation type. func convDataValidationType(t DataValidationType) string { typeMap := map[DataValidationType]string{ typeNone: "none", @@ -165,7 +145,7 @@ func convDataValidationType(t DataValidationType) string { } -// convDataValidationOperatior get excel data validation operator +// convDataValidationOperatior get excel data validation operator. func convDataValidationOperatior(o DataValidationOperator) string { typeMap := map[DataValidationOperator]string{ DataValidationOperatorBetween: "between", @@ -182,6 +162,37 @@ func convDataValidationOperatior(o DataValidationOperator) string { } +// AddDataValidation provides set data validation on a range of the worksheet +// by given data validation object and worksheet name. The data validation +// object can be created by NewDataValidation function. +// +// Example 1, set data validation on Sheet1!A1:B2 with validation criteria +// settings, show error alert after invalid data is entered whth "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") +// xlsx.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") +// xlsx.AddDataValidation("Sheet1", dvRange) +// +// Example 4, set data validation on Sheet1!A5:B6 with validation criteria +// settings, create in-cell dropdown by allow list source: +// +// dvRange = excelize.NewDataValidation(true) +// dvRange.Sqref = "A5:B6" +// dvRange.SetDropList([]string{"1", "2", "3"}) +// xlsx.AddDataValidation("Sheet1", dvRange) +// func (f *File) AddDataValidation(sheet string, dv *DataValidation) { xlsx := f.workSheetReader(sheet) if nil == xlsx.DataValidations { From ba459dc659720d7504e5eb6f5bda9081a452a509 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 1 Sep 2018 23:36:57 +0800 Subject: [PATCH 012/957] DataValidation struct changed Change `allowBlank`, `ShowErrorMessage` and `ShowInputMessage` type as boolean, add new field `ShowDropDown`, change fields orders follow as ECMA-376-1:2016 18.3.1.32. --- xmlWorksheet.go | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 25e3904263..7cf4994e82 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -301,20 +301,23 @@ type xlsxDataValidations struct { DataValidation []*DataValidation `xml:"dataValidation"` } +// DataValidation directly maps the a single item of data validation defined +// on a range of the worksheet. type DataValidation struct { - AllowBlank string `xml:"allowBlank,attr"` // allow empty - ShowInputMessage string `xml:"showInputMessage,attr"` // 1, true,0,false, select cell, Whether the input message is displayed - ShowErrorMessage string `xml:"showErrorMessage,attr"` // 1, true,0,false, input error value, Whether the error message is displayed - ErrorStyle *string `xml:"errorStyle,attr"` //error icon style, warning, infomation,stop - ErrorTitle *string `xml:"errorTitle,attr"` // error title - Operator string `xml:"operator,attr"` // - Error *string `xml:"error,attr"` // input error value, notice message - PromptTitle *string `xml:"promptTitle"` + AllowBlank bool `xml:"allowBlank,attr"` + Error *string `xml:"error,attr"` + ErrorStyle *string `xml:"errorStyle,attr"` + ErrorTitle *string `xml:"errorTitle,attr"` + Operator string `xml:"operator,attr"` Prompt *string `xml:"prompt,attr"` - Type string `xml:"type,attr"` //data type, none,custom,date,decimal,list, textLength,time,whole - Sqref string `xml:"sqref,attr"` //Validity of data validation rules, cell and range, eg: A1 OR A1:A20 - Formula1 string `xml:"formula1"` // data validation role - Formula2 string `xml:"formula2"` //data validation role + PromptTitle *string `xml:"promptTitle"` + ShowDropDown bool `xml:"showDropDown,attr"` + ShowErrorMessage bool `xml:"showErrorMessage,attr"` + ShowInputMessage bool `xml:"showInputMessage,attr"` + Sqref string `xml:"sqref,attr"` + Type string `xml:"type,attr"` + Formula1 string `xml:"formula1"` + Formula2 string `xml:"formula2"` } // xlsxC directly maps the c element in the namespace From 2da107d3b20a5561d311466b7b2cb91170885f9f Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 2 Sep 2018 01:44:32 +0800 Subject: [PATCH 013/957] Fix typo. --- datavalidation.go | 6 +++--- datavalidation_test.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/datavalidation.go b/datavalidation.go index 62d2cf84fd..914e877681 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -167,11 +167,11 @@ func convDataValidationOperatior(o DataValidationOperator) string { // object can be created by NewDataValidation function. // // Example 1, set data validation on Sheet1!A1:B2 with validation criteria -// settings, show error alert after invalid data is entered whth "Stop" style +// 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.Sqref = "A1:B2" // dvRange.SetRange(10, 20, excelize.DataValidationTypeWhole, excelize.DataValidationOperatorBetween) // dvRange.SetError(excelize.DataValidationErrorStyleStop, "error title", "error body") // xlsx.AddDataValidation("Sheet1", dvRange) @@ -185,7 +185,7 @@ func convDataValidationOperatior(o DataValidationOperator) string { // dvRange.SetInput("input title", "input body") // xlsx.AddDataValidation("Sheet1", dvRange) // -// Example 4, set data validation on Sheet1!A5:B6 with validation criteria +// Example 3, set data validation on Sheet1!A5:B6 with validation criteria // settings, create in-cell dropdown by allow list source: // // dvRange = excelize.NewDataValidation(true) diff --git a/datavalidation_test.go b/datavalidation_test.go index 718131fede..32f90598a3 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -25,7 +25,7 @@ func TestDataValidation(t *testing.T) { xlsx.AddDataValidation("Sheet1", dvRange) // Test write file to given path. - err := xlsx.SaveAs("./test/Bookdatavalition.xlsx") + err := xlsx.SaveAs("./test/Book_data_validation.xlsx") if err != nil { t.Error(err) } From 93cbafb0e2ff5df0236d543650712cd175cd789d Mon Sep 17 00:00:00 2001 From: rentiansheng Date: Tue, 4 Sep 2018 13:40:53 +0800 Subject: [PATCH 014/957] data validation drop-down list use sqref cell issue #268 --- datavalidation.go | 12 ++++++++++++ datavalidation_test.go | 8 ++++++++ 2 files changed, 20 insertions(+) diff --git a/datavalidation.go b/datavalidation.go index 914e877681..010615c460 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -119,6 +119,18 @@ func (dd *DataValidation) SetRange(f1, f2 int, t DataValidationType, o DataValid return nil } +// SetSqrefDropList data validation list with current sheet cell rang +func (dd *DataValidation) SetSqrefDropList(sqref string, isCurrentSheet bool) error { + if isCurrentSheet { + dd.Formula1 = sqref + dd.Type = convDataValidationType(typeList) + return nil + } + + //isCurrentSheet = false Cross-sheet sqref cell use extLst xml node unrealized + return fmt.Errorf("Cross-sheet sqref cell are not supported") +} + // SetSqref provides function to set data validation range in drop list. func (dd *DataValidation) SetSqref(sqref string) { if dd.Sqref == "" { diff --git a/datavalidation_test.go b/datavalidation_test.go index 32f90598a3..f3db81c6c0 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -24,6 +24,14 @@ func TestDataValidation(t *testing.T) { dvRange.SetDropList([]string{"1", "2", "3"}) xlsx.AddDataValidation("Sheet1", dvRange) + xlsx.SetCellStr("Sheet1", "E1", "E1") + xlsx.SetCellStr("Sheet1", "E2", "E2") + xlsx.SetCellStr("Sheet1", "E3", "E3") + dvRange = NewDataValidation(true) + dvRange.Sqref = "A7:B8" + dvRange.SetSqrefDropList("$E$1:$E$3", true) + xlsx.AddDataValidation("Sheet1", dvRange) + // Test write file to given path. err := xlsx.SaveAs("./test/Book_data_validation.xlsx") if err != nil { From b4a6e61ec34d4a0db1110907cc969f0d7d38991a Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 12 Sep 2018 15:47:56 +0800 Subject: [PATCH 015/957] Fix golint errors under confidence 0.1 --- cell.go | 8 ++++++ chart.go | 8 ++++++ col.go | 8 ++++++ comment.go | 8 ++++++ datavalidation.go | 10 ++++++- datavalidation_test.go | 8 ++++++ date.go | 8 ++++++ excelize.go | 8 ++++++ file.go | 10 ++++++- hsl.go | 62 ++++++++++++++++++++-------------------- lib.go | 8 ++++++ picture.go | 12 ++++++-- rows.go | 8 ++++++ shape.go | 8 ++++++ sheet.go | 12 ++++++-- sheetpr.go | 8 ++++++ sheetview.go | 8 ++++++ styles.go | 64 ++++++++++++++++++++++++------------------ table.go | 16 ++++++++--- templates.go | 10 ++++++- vmlDrawing.go | 8 ++++++ xmlChart.go | 8 ++++++ xmlComments.go | 8 ++++++ xmlContentTypes.go | 8 ++++++ xmlDecodeDrawing.go | 8 ++++++ xmlDrawing.go | 8 ++++++ xmlSharedStrings.go | 8 ++++++ xmlStyles.go | 8 ++++++ xmlTable.go | 8 ++++++ xmlTheme.go | 8 ++++++ xmlWorkbook.go | 8 ++++++ xmlWorksheet.go | 8 ++++++ 32 files changed, 319 insertions(+), 69 deletions(-) diff --git a/cell.go b/cell.go index 5ec5976eb8..e6755d6663 100644 --- a/cell.go +++ b/cell.go @@ -1,3 +1,11 @@ +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. +// +// Copyright 2016 - 2018 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. package excelize import ( diff --git a/chart.go b/chart.go index 8e1d7e9f9b..204768497b 100644 --- a/chart.go +++ b/chart.go @@ -1,3 +1,11 @@ +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. +// +// Copyright 2016 - 2018 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. package excelize import ( diff --git a/col.go b/col.go index e3870ee8b8..cddb09f45a 100644 --- a/col.go +++ b/col.go @@ -1,3 +1,11 @@ +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. +// +// Copyright 2016 - 2018 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. package excelize import ( diff --git a/comment.go b/comment.go index 8bf4fd31a5..32ed02f2fd 100644 --- a/comment.go +++ b/comment.go @@ -1,3 +1,11 @@ +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. +// +// Copyright 2016 - 2018 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. package excelize import ( diff --git a/datavalidation.go b/datavalidation.go index 010615c460..be3caabe92 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -1,3 +1,11 @@ +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. +// +// Copyright 2016 - 2018 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. package excelize import ( @@ -128,7 +136,7 @@ func (dd *DataValidation) SetSqrefDropList(sqref string, isCurrentSheet bool) er } //isCurrentSheet = false Cross-sheet sqref cell use extLst xml node unrealized - return fmt.Errorf("Cross-sheet sqref cell are not supported") + return fmt.Errorf("cross-sheet sqref cell are not supported") } // SetSqref provides function to set data validation range in drop list. diff --git a/datavalidation_test.go b/datavalidation_test.go index f3db81c6c0..8134b721a5 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -1,3 +1,11 @@ +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. +// +// Copyright 2016 - 2018 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. package excelize import ( diff --git a/date.go b/date.go index 72336610db..56d6ad7ae7 100644 --- a/date.go +++ b/date.go @@ -1,3 +1,11 @@ +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. +// +// Copyright 2016 - 2018 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. package excelize import ( diff --git a/excelize.go b/excelize.go index 0b530ab219..915955c8fb 100644 --- a/excelize.go +++ b/excelize.go @@ -1,3 +1,11 @@ +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. +// +// Copyright 2016 - 2018 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. package excelize import ( diff --git a/file.go b/file.go index a45fc28352..76ef98687a 100644 --- a/file.go +++ b/file.go @@ -1,3 +1,11 @@ +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. +// +// Copyright 2016 - 2018 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. package excelize import ( @@ -43,7 +51,7 @@ func NewFile() *File { // Save provides a function to override the xlsx file with origin path. func (f *File) Save() error { if f.Path == "" { - return fmt.Errorf("No path defined for file, consider File.WriteTo or File.Write") + return fmt.Errorf("no path defined for file, consider File.WriteTo or File.Write") } return f.SaveAs(f.Path) } diff --git a/hsl.go b/hsl.go index bd868b12a7..fd10b5dae8 100644 --- a/hsl.go +++ b/hsl.go @@ -1,33 +1,35 @@ -/* -Copyright (c) 2012 Rodrigo Moraes. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ - +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. +// +// Copyright (c) 2012 Rodrigo Moraes. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. package excelize import ( diff --git a/lib.go b/lib.go index 8013efa3fa..5be9b16213 100644 --- a/lib.go +++ b/lib.go @@ -1,3 +1,11 @@ +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. +// +// Copyright 2016 - 2018 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. package excelize import ( diff --git a/picture.go b/picture.go index 97e72a6645..decc37db4e 100644 --- a/picture.go +++ b/picture.go @@ -1,3 +1,11 @@ +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. +// +// Copyright 2016 - 2018 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. package excelize import ( @@ -86,10 +94,10 @@ func (f *File) AddPicture(sheet, cell, picture, format string) error { } ext, ok := supportImageTypes[path.Ext(picture)] if !ok { - return errors.New("Unsupported image extension") + return errors.New("unsupported image extension") } readFile, err := os.Open(picture) - if err!=nil{ + if err != nil { return err } defer readFile.Close() diff --git a/rows.go b/rows.go index 521a945424..8e1e0c3bb3 100644 --- a/rows.go +++ b/rows.go @@ -1,3 +1,11 @@ +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. +// +// Copyright 2016 - 2018 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. package excelize import ( diff --git a/shape.go b/shape.go index 8fbfbcd394..42aefcf99c 100644 --- a/shape.go +++ b/shape.go @@ -1,3 +1,11 @@ +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. +// +// Copyright 2016 - 2018 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. package excelize import ( diff --git a/sheet.go b/sheet.go index 011ecb8584..b5c3e6cb0d 100644 --- a/sheet.go +++ b/sheet.go @@ -1,3 +1,11 @@ +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. +// +// Copyright 2016 - 2018 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. package excelize import ( @@ -371,7 +379,7 @@ func (f *File) SetSheetBackground(sheet, picture string) error { } ext, ok := supportImageTypes[path.Ext(picture)] if !ok { - return errors.New("Unsupported image extension") + return errors.New("unsupported image extension") } pictureID := f.countMedia() + 1 rID := f.addSheetRelationships(sheet, SourceRelationshipImage, "../media/image"+strconv.Itoa(pictureID)+ext, "") @@ -441,7 +449,7 @@ func (f *File) deleteSheetFromContentTypes(target string) { // func (f *File) CopySheet(from, to int) error { if from < 1 || to < 1 || from == to || f.GetSheetName(from) == "" || f.GetSheetName(to) == "" { - return errors.New("Invalid worksheet index") + return errors.New("invalid worksheet index") } f.copySheet(from, to) return nil diff --git a/sheetpr.go b/sheetpr.go index 1601ab138c..a010130a38 100644 --- a/sheetpr.go +++ b/sheetpr.go @@ -1,3 +1,11 @@ +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. +// +// Copyright 2016 - 2018 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. package excelize // SheetPrOption is an option of a view of a worksheet. See SetSheetPrOptions(). diff --git a/sheetview.go b/sheetview.go index 679e915391..5e756ead27 100644 --- a/sheetview.go +++ b/sheetview.go @@ -1,3 +1,11 @@ +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. +// +// Copyright 2016 - 2018 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. package excelize import "fmt" diff --git a/styles.go b/styles.go index a288b3706f..80f347ecbd 100644 --- a/styles.go +++ b/styles.go @@ -1,3 +1,11 @@ +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. +// +// Copyright 2016 - 2018 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. package excelize import ( @@ -798,35 +806,35 @@ var validType = map[string]string{ // criteriaType defined the list of valid criteria types. var criteriaType = map[string]string{ - "between": "between", - "not between": "notBetween", - "equal to": "equal", - "=": "equal", - "==": "equal", - "not equal to": "notEqual", - "!=": "notEqual", - "<>": "notEqual", - "greater than": "greaterThan", - ">": "greaterThan", - "less than": "lessThan", - "<": "lessThan", + "between": "between", + "not between": "notBetween", + "equal to": "equal", + "=": "equal", + "==": "equal", + "not equal to": "notEqual", + "!=": "notEqual", + "<>": "notEqual", + "greater than": "greaterThan", + ">": "greaterThan", + "less than": "lessThan", + "<": "lessThan", "greater than or equal to": "greaterThanOrEqual", - ">=": "greaterThanOrEqual", - "less than or equal to": "lessThanOrEqual", - "<=": "lessThanOrEqual", - "containing": "containsText", - "not containing": "notContains", - "begins with": "beginsWith", - "ends with": "endsWith", - "yesterday": "yesterday", - "today": "today", - "last 7 days": "last7Days", - "last week": "lastWeek", - "this week": "thisWeek", - "continue week": "continueWeek", - "last month": "lastMonth", - "this month": "thisMonth", - "continue month": "continueMonth", + ">=": "greaterThanOrEqual", + "less than or equal to": "lessThanOrEqual", + "<=": "lessThanOrEqual", + "containing": "containsText", + "not containing": "notContains", + "begins with": "beginsWith", + "ends with": "endsWith", + "yesterday": "yesterday", + "today": "today", + "last 7 days": "last7Days", + "last week": "lastWeek", + "this week": "thisWeek", + "continue week": "continueWeek", + "last month": "lastMonth", + "this month": "thisMonth", + "continue month": "continueMonth", } // formatToString provides a function to return original string by given diff --git a/table.go b/table.go index 643b156c87..fcc4d3c327 100644 --- a/table.go +++ b/table.go @@ -1,3 +1,11 @@ +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. +// +// Copyright 2016 - 2018 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. package excelize import ( @@ -282,7 +290,7 @@ func (f *File) autoFilter(sheet, ref string, refRange, hxAxis int, formatSet *fo col := TitleToNumber(formatSet.Column) offset := col - hxAxis if offset < 0 || offset > refRange { - return fmt.Errorf("Incorrect index of column '%s'", formatSet.Column) + return fmt.Errorf("incorrect index of column '%s'", formatSet.Column) } filter.FilterColumn = &xlsxFilterColumn{ ColID: offset, @@ -290,7 +298,7 @@ func (f *File) autoFilter(sheet, ref string, refRange, hxAxis int, formatSet *fo re := regexp.MustCompile(`"(?:[^"]|"")*"|\S+`) token := re.FindAllString(formatSet.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'", formatSet.Expression) } expressions, tokens, err := f.parseFilterExpression(formatSet.Expression, token) if err != nil { @@ -415,7 +423,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{}, "", fmt.Errorf("unknown operator: %s", tokens[1]) } token := tokens[2] // Special handling for Blanks/NonBlanks. @@ -423,7 +431,7 @@ func (f *File) parseFilterTokens(expression string, tokens []string) ([]int, str if re { // Only allow Equals or NotEqual in this context. if operator != 2 && operator != 5 { - return []int{operator}, token, fmt.Errorf("The operator '%s' in expression '%s' is not valid in relation to Blanks/NonBlanks'", tokens[1], expression) + return []int{operator}, token, fmt.Errorf("the operator '%s' in expression '%s' is not valid in relation to Blanks/NonBlanks'", tokens[1], expression) } token = strings.ToLower(token) // The operator should always be 2 (=) to flag a "simple" equality in diff --git a/templates.go b/templates.go index ef6058cbc6..a619dfc286 100644 --- a/templates.go +++ b/templates.go @@ -1,6 +1,14 @@ +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. +// +// Copyright 2016 - 2018 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. +// // This file contains default templates for XML files we don't yet populated // based on content. - package excelize // XMLHeader define an XML declaration can also contain a standalone declaration. diff --git a/vmlDrawing.go b/vmlDrawing.go index 307186a95e..f9323721c4 100644 --- a/vmlDrawing.go +++ b/vmlDrawing.go @@ -1,3 +1,11 @@ +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. +// +// Copyright 2016 - 2018 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. package excelize import "encoding/xml" diff --git a/xmlChart.go b/xmlChart.go index a263334307..8a2cfd8018 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -1,3 +1,11 @@ +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. +// +// Copyright 2016 - 2018 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. package excelize import "encoding/xml" diff --git a/xmlComments.go b/xmlComments.go index fadc9b3873..08d85c1cb0 100644 --- a/xmlComments.go +++ b/xmlComments.go @@ -1,3 +1,11 @@ +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. +// +// Copyright 2016 - 2018 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. package excelize import "encoding/xml" diff --git a/xmlContentTypes.go b/xmlContentTypes.go index 121c6846bf..6c31bffa67 100644 --- a/xmlContentTypes.go +++ b/xmlContentTypes.go @@ -1,3 +1,11 @@ +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. +// +// Copyright 2016 - 2018 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. package excelize import "encoding/xml" diff --git a/xmlDecodeDrawing.go b/xmlDecodeDrawing.go index fff6b9debf..c85c490d3d 100644 --- a/xmlDecodeDrawing.go +++ b/xmlDecodeDrawing.go @@ -1,3 +1,11 @@ +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. +// +// Copyright 2016 - 2018 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. package excelize import "encoding/xml" diff --git a/xmlDrawing.go b/xmlDrawing.go index beb6bc94a4..ee30a4ddf1 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -1,3 +1,11 @@ +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. +// +// Copyright 2016 - 2018 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. package excelize import "encoding/xml" diff --git a/xmlSharedStrings.go b/xmlSharedStrings.go index c8b54a013a..94959c65dc 100644 --- a/xmlSharedStrings.go +++ b/xmlSharedStrings.go @@ -1,3 +1,11 @@ +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. +// +// Copyright 2016 - 2018 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. package excelize import "encoding/xml" diff --git a/xmlStyles.go b/xmlStyles.go index 05ff22bc4d..3024c1e2b3 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -1,3 +1,11 @@ +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. +// +// Copyright 2016 - 2018 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. package excelize import "encoding/xml" diff --git a/xmlTable.go b/xmlTable.go index b238350267..53a5af432a 100644 --- a/xmlTable.go +++ b/xmlTable.go @@ -1,3 +1,11 @@ +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. +// +// Copyright 2016 - 2018 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. package excelize import "encoding/xml" diff --git a/xmlTheme.go b/xmlTheme.go index d2ab343ceb..e72e53b98a 100644 --- a/xmlTheme.go +++ b/xmlTheme.go @@ -1,3 +1,11 @@ +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. +// +// Copyright 2016 - 2018 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. package excelize import "encoding/xml" diff --git a/xmlWorkbook.go b/xmlWorkbook.go index 816d5a4617..d194bac46d 100644 --- a/xmlWorkbook.go +++ b/xmlWorkbook.go @@ -1,3 +1,11 @@ +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. +// +// Copyright 2016 - 2018 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. package excelize import "encoding/xml" diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 7cf4994e82..e298329d81 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -1,3 +1,11 @@ +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. +// +// Copyright 2016 - 2018 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. package excelize import "encoding/xml" From 4f47737d64fc9d9108675cbc1e73ae93c2d723a9 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 13 Sep 2018 10:38:01 +0800 Subject: [PATCH 016/957] Complete unit testing case for data validation --- .travis.yml | 1 + datavalidation.go | 17 +++++++++++++---- datavalidation_test.go | 14 ++++++++++++-- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 26ca7b299c..c2f0f90590 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,7 @@ go: - 1.8.x - 1.9.x - 1.10.x + - 1.11.x os: - linux diff --git a/datavalidation.go b/datavalidation.go index be3caabe92..c0f006f35f 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -127,15 +127,24 @@ func (dd *DataValidation) SetRange(f1, f2 int, t DataValidationType, o DataValid return nil } -// SetSqrefDropList data validation list with current sheet cell rang +// SetSqrefDropList provides set data validation on a range with source +// reference range of the worksheet by given data validation object and +// worksheet name. The data validation object can be created by +// NewDataValidation function. For example, set data validation on +// 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("E1:E3", true) +// xlsx.AddDataValidation("Sheet1", dvRange) +// func (dd *DataValidation) SetSqrefDropList(sqref string, isCurrentSheet bool) error { if isCurrentSheet { dd.Formula1 = sqref dd.Type = convDataValidationType(typeList) return nil } - - //isCurrentSheet = false Cross-sheet sqref cell use extLst xml node unrealized return fmt.Errorf("cross-sheet sqref cell are not supported") } @@ -206,7 +215,7 @@ func convDataValidationOperatior(o DataValidationOperator) string { // xlsx.AddDataValidation("Sheet1", dvRange) // // Example 3, set data validation on Sheet1!A5:B6 with validation criteria -// settings, create in-cell dropdown by allow list source: +// settings, create in-cell dropdown by allowing list source: // // dvRange = excelize.NewDataValidation(true) // dvRange.Sqref = "A5:B6" diff --git a/datavalidation_test.go b/datavalidation_test.go index 8134b721a5..98993342d8 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -19,6 +19,8 @@ func TestDataValidation(t *testing.T) { dvRange.Sqref = "A1:B2" 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") xlsx.AddDataValidation("Sheet1", dvRange) dvRange = NewDataValidation(true) @@ -36,12 +38,20 @@ func TestDataValidation(t *testing.T) { xlsx.SetCellStr("Sheet1", "E2", "E2") xlsx.SetCellStr("Sheet1", "E3", "E3") dvRange = NewDataValidation(true) - dvRange.Sqref = "A7:B8" + dvRange.SetSqref("A7:B8") + dvRange.SetSqref("A7:B8") dvRange.SetSqrefDropList("$E$1:$E$3", true) + err := dvRange.SetSqrefDropList("$E$1:$E$3", false) + t.Log(err) xlsx.AddDataValidation("Sheet1", dvRange) + dvRange = NewDataValidation(true) + dvRange.SetDropList(make([]string, 258)) + err = dvRange.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorGreaterThan) + t.Log(err) + // Test write file to given path. - err := xlsx.SaveAs("./test/Book_data_validation.xlsx") + err = xlsx.SaveAs("./test/Book_data_validation.xlsx") if err != nil { t.Error(err) } From 6ced438f39030e8a9a521548d4112dd002dc2ebe Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 14 Sep 2018 00:24:49 +0800 Subject: [PATCH 017/957] New function `AddPictureFromBytes()` has been added, this resolve #259 and close #271. --- comment.go | 19 +++++++++++-- datavalidation_test.go | 4 +-- date_test.go | 54 +++++++++++++++++------------------ excelize_test.go | 25 ++++++++++++++++- picture.go | 64 +++++++++++++++++++++++++++++++++--------- sheet.go | 6 ++-- xmlComments.go | 8 ++++++ 7 files changed, 131 insertions(+), 49 deletions(-) diff --git a/comment.go b/comment.go index 32ed02f2fd..94aad6c198 100644 --- a/comment.go +++ b/comment.go @@ -29,8 +29,8 @@ func parseFormatCommentsSet(formatSet string) (*formatComment, error) { // GetComments retrieves all comments and returns a map of worksheet name to // the worksheet comments. -func (f *File) GetComments() (comments map[string]*xlsxComments) { - comments = map[string]*xlsxComments{} +func (f *File) GetComments() (comments map[string][]Comment) { + comments = map[string][]Comment{} for n := range f.sheetMap { commentID := f.GetSheetIndex(n) commentsXML := "xl/comments" + strconv.Itoa(commentID) + ".xml" @@ -38,7 +38,20 @@ func (f *File) GetComments() (comments map[string]*xlsxComments) { if ok { d := xlsxComments{} xml.Unmarshal([]byte(c), &d) - comments[n] = &d + sheetComments := []Comment{} + for _, comment := range d.CommentList.Comment { + sheetComment := Comment{} + if comment.AuthorID < len(d.Authors) { + sheetComment.Author = d.Authors[comment.AuthorID].Author + } + sheetComment.Ref = comment.Ref + sheetComment.AuthorID = comment.AuthorID + for _, text := range comment.Text.R { + sheetComment.Text += text.T + } + sheetComments = append(sheetComments, sheetComment) + } + comments[n] = sheetComments } } return diff --git a/datavalidation_test.go b/datavalidation_test.go index 98993342d8..0f50f2906d 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -8,9 +8,7 @@ // the LICENSE file. package excelize -import ( - "testing" -) +import "testing" func TestDataValidation(t *testing.T) { xlsx := NewFile() diff --git a/date_test.go b/date_test.go index bf071e0884..06421b8b1f 100644 --- a/date_test.go +++ b/date_test.go @@ -1,42 +1,42 @@ package excelize import ( - "testing" - "time" + "testing" + "time" ) type dateTest struct { - ExcelValue float64 - GoValue time.Time + ExcelValue float64 + GoValue time.Time } func TestTimeToExcelTime(t *testing.T) { - trueExpectedInputList := []dateTest { - {0.0, time.Date(1899, 12, 30, 0, 0, 0, 0, time.UTC)}, - {25569.0, time.Unix(0, 0)}, - {43269.0, time.Date(2018, 6, 18, 0, 0, 0, 0, time.UTC)}, - {401769.0, time.Date(3000, 1, 1, 0, 0, 0, 0, time.UTC)}, - } + trueExpectedInputList := []dateTest{ + {0.0, time.Date(1899, 12, 30, 0, 0, 0, 0, time.UTC)}, + {25569.0, time.Unix(0, 0)}, + {43269.0, time.Date(2018, 6, 18, 0, 0, 0, 0, time.UTC)}, + {401769.0, time.Date(3000, 1, 1, 0, 0, 0, 0, time.UTC)}, + } - for _, test := range trueExpectedInputList { - if test.ExcelValue != timeToExcelTime(test.GoValue) { - t.Fatalf("Expected %v from %v = true, got %v\n", test.ExcelValue, test.GoValue, timeToExcelTime(test.GoValue)) - } - } + for _, test := range trueExpectedInputList { + if test.ExcelValue != timeToExcelTime(test.GoValue) { + t.Fatalf("Expected %v from %v = true, got %v\n", test.ExcelValue, test.GoValue, timeToExcelTime(test.GoValue)) + } + } } func TestTimeFromExcelTime(t *testing.T) { - trueExpectedInputList := []dateTest { - {0.0, time.Date(1899, 12, 30, 0, 0, 0, 0, time.UTC)}, - {60.0, time.Date(1900, 2, 28, 0, 0, 0, 0, time.UTC)}, - {61.0, time.Date(1900, 3, 1, 0, 0, 0, 0, time.UTC)}, - {41275.0, time.Date(2013, 1, 1, 0, 0, 0, 0, time.UTC)}, - {401769.0, time.Date(3000, 1, 1, 0, 0, 0, 0, time.UTC)}, - } + trueExpectedInputList := []dateTest{ + {0.0, time.Date(1899, 12, 30, 0, 0, 0, 0, time.UTC)}, + {60.0, time.Date(1900, 2, 28, 0, 0, 0, 0, time.UTC)}, + {61.0, time.Date(1900, 3, 1, 0, 0, 0, 0, time.UTC)}, + {41275.0, time.Date(2013, 1, 1, 0, 0, 0, 0, time.UTC)}, + {401769.0, time.Date(3000, 1, 1, 0, 0, 0, 0, time.UTC)}, + } - for _, test := range trueExpectedInputList { - if test.GoValue != timeFromExcelTime(test.ExcelValue, false) { - t.Fatalf("Expected %v from %v = true, got %v\n", test.GoValue, test.ExcelValue, timeFromExcelTime(test.ExcelValue, false)) - } - } + for _, test := range trueExpectedInputList { + if test.GoValue != timeFromExcelTime(test.ExcelValue, false) { + t.Fatalf("Expected %v from %v = true, got %v\n", test.GoValue, test.ExcelValue, timeFromExcelTime(test.ExcelValue, false)) + } + } } diff --git a/excelize_test.go b/excelize_test.go index c9a87d073d..9f738f331d 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -162,6 +162,24 @@ func TestAddPicture(t *testing.T) { if err != nil { t.Log(err) } + err = xlsx.AddPictureFromBytes("Sheet1", "G21", "", "Excel Logo", "jpg", make([]byte, 1)) + if err != nil { + t.Log(err) + } + // Test add picture to worksheet with invalid file data. + err = xlsx.AddPictureFromBytes("Sheet1", "G21", "", "Excel Logo", ".jpg", make([]byte, 1)) + if err != nil { + t.Log(err) + } + file, err := ioutil.ReadFile("./test/images/excel.jpg") + if err != nil { + t.Error(err) + } + // Test add picture to worksheet from bytes. + err = xlsx.AddPictureFromBytes("Sheet1", "Q1", "", "Excel Logo", ".jpg", file) + if err != nil { + t.Log(err) + } // Test write file to given path. err = xlsx.SaveAs("./test/Book2.xlsx") if err != nil { @@ -211,8 +229,13 @@ func TestNewFile(t *testing.T) { if err != nil { t.Error(err) } - // Test add picture to worksheet with invalid formatset + // Test add picture to worksheet without formatset. err = xlsx.AddPicture("Sheet1", "C2", "./test/images/excel.png", "") + if err != nil { + t.Error(err) + } + // Test add picture to worksheet with invalid formatset. + err = xlsx.AddPicture("Sheet1", "C2", "./test/images/excel.png", `{`) if err != nil { t.Log(err) } diff --git a/picture.go b/picture.go index decc37db4e..e4734985fa 100644 --- a/picture.go +++ b/picture.go @@ -86,8 +86,6 @@ func parseFormatPictureSet(formatSet string) (*formatPicture, error) { // positioning is move and size with cells. func (f *File) AddPicture(sheet, cell, picture, format string) error { var err error - var drawingHyperlinkRID int - var hyperlinkType string // Check picture exists first. if _, err = os.Stat(picture); os.IsNotExist(err) { return err @@ -96,14 +94,55 @@ func (f *File) AddPicture(sheet, cell, picture, format string) error { if !ok { return errors.New("unsupported image extension") } - readFile, err := os.Open(picture) + file, _ := ioutil.ReadFile(picture) + _, name := filepath.Split(picture) + return f.AddPictureFromBytes(sheet, cell, format, name, ext, file) +} + +// 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: +// +// package main +// +// import ( +// "fmt" +// _ "image/jpeg" +// "io/ioutil" +// +// "github.com/360EntSecGroup-Skylar/excelize" +// ) +// +// func main() { +// xlsx := excelize.NewFile() +// +// file, err := ioutil.ReadFile("./image1.jpg") +// if err != nil { +// fmt.Println(err) +// } +// err = xlsx.AddPictureFromBytes("Sheet1", "A2", "", "Excel Logo", ".jpg", file) +// if err != nil { +// fmt.Println(err) +// } +// err = xlsx.SaveAs("./Book1.xlsx") +// if err != nil { +// fmt.Println(err) +// } +// } +// +func (f *File) AddPictureFromBytes(sheet, cell, format, name, extension string, file []byte) error { + var err error + var drawingHyperlinkRID int + var hyperlinkType string + ext, ok := supportImageTypes[extension] + if !ok { + return errors.New("unsupported image extension") + } + formatSet, err := parseFormatPictureSet(format) if err != nil { return err } - defer readFile.Close() - image, _, _ := image.DecodeConfig(readFile) - _, file := filepath.Split(picture) - formatSet, err := parseFormatPictureSet(format) + image, _, err := image.DecodeConfig(bytes.NewReader(file)) if err != nil { return err } @@ -122,8 +161,8 @@ func (f *File) AddPicture(sheet, cell, picture, format string) error { } drawingHyperlinkRID = f.addDrawingRelationships(drawingID, SourceRelationshipHyperLink, formatSet.Hyperlink, hyperlinkType) } - f.addDrawingPicture(sheet, drawingXML, cell, file, image.Width, image.Height, drawingRID, drawingHyperlinkRID, formatSet) - f.addMedia(picture, ext) + f.addDrawingPicture(sheet, drawingXML, cell, name, image.Width, image.Height, drawingRID, drawingHyperlinkRID, formatSet) + f.addMedia(file, ext) f.addContentTypePart(drawingID, "drawings") return err } @@ -317,12 +356,11 @@ func (f *File) countMedia() int { } // addMedia provides a function to add picture into folder xl/media/image by -// given file name and extension name. -func (f *File) addMedia(file, ext string) { +// given file and extension name. +func (f *File) addMedia(file []byte, ext string) { count := f.countMedia() - dat, _ := ioutil.ReadFile(file) media := "xl/media/image" + strconv.Itoa(count+1) + ext - f.XLSX[media] = dat + f.XLSX[media] = file } // setContentTypePartImageExtensions provides a function to set the content diff --git a/sheet.go b/sheet.go index b5c3e6cb0d..ef2b6f4c40 100644 --- a/sheet.go +++ b/sheet.go @@ -13,6 +13,7 @@ import ( "encoding/json" "encoding/xml" "errors" + "io/ioutil" "os" "path" "strconv" @@ -370,7 +371,7 @@ func (f *File) getSheetMap() map[string]string { } // SetSheetBackground provides a function to set background picture by given -// worksheet name. +// worksheet name and file path. func (f *File) SetSheetBackground(sheet, picture string) error { var err error // Check picture exists first. @@ -384,7 +385,8 @@ func (f *File) SetSheetBackground(sheet, picture string) error { pictureID := f.countMedia() + 1 rID := f.addSheetRelationships(sheet, SourceRelationshipImage, "../media/image"+strconv.Itoa(pictureID)+ext, "") f.addSheetPicture(sheet, rID) - f.addMedia(picture, ext) + file, _ := ioutil.ReadFile(picture) + f.addMedia(file, ext) f.setContentTypePartImageExtensions() return err } diff --git a/xmlComments.go b/xmlComments.go index 08d85c1cb0..1607506cae 100644 --- a/xmlComments.go +++ b/xmlComments.go @@ -61,3 +61,11 @@ type formatComment 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"` +} From 2f146c923ccd19c5ecc1f732b5b09d591fb935a3 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 14 Sep 2018 00:35:47 +0800 Subject: [PATCH 018/957] Comments style changed. --- cell.go | 18 +++++++----- chart.go | 18 +++++++----- col.go | 18 +++++++----- comment.go | 18 +++++++----- datavalidation.go | 18 +++++++----- datavalidation_test.go | 18 +++++++----- date.go | 18 +++++++----- excelize.go | 18 +++++++----- file.go | 18 +++++++----- hsl.go | 66 ++++++++++++++++++++++-------------------- lib.go | 18 +++++++----- rows.go | 18 +++++++----- shape.go | 18 +++++++----- sheet.go | 18 +++++++----- sheetpr.go | 18 +++++++----- sheetview.go | 18 +++++++----- styles.go | 18 +++++++----- table.go | 18 +++++++----- templates.go | 24 ++++++++------- vmlDrawing.go | 18 +++++++----- xmlChart.go | 18 +++++++----- xmlComments.go | 18 +++++++----- xmlContentTypes.go | 18 +++++++----- xmlDecodeDrawing.go | 18 +++++++----- xmlDrawing.go | 18 +++++++----- xmlSharedStrings.go | 18 +++++++----- xmlStyles.go | 18 +++++++----- xmlTable.go | 18 +++++++----- xmlTheme.go | 18 +++++++----- xmlWorkbook.go | 18 +++++++----- xmlWorksheet.go | 18 +++++++----- 31 files changed, 337 insertions(+), 275 deletions(-) diff --git a/cell.go b/cell.go index e6755d6663..69f8697193 100644 --- a/cell.go +++ b/cell.go @@ -1,11 +1,13 @@ -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. -// -// Copyright 2016 - 2018 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. +/* +Package excelize providing a set of functions that allow you to write to +and read from XLSX files. Support reads and writes XLSX file generated by +Microsoft Excel™ 2007 and later. Support save file without losing original +charts of XLSX. This library needs Go version 1.8 or later. + +Copyright 2016 - 2018 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. +*/ package excelize import ( diff --git a/chart.go b/chart.go index 204768497b..4b73c3b75c 100644 --- a/chart.go +++ b/chart.go @@ -1,11 +1,13 @@ -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. -// -// Copyright 2016 - 2018 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. +/* +Package excelize providing a set of functions that allow you to write to +and read from XLSX files. Support reads and writes XLSX file generated by +Microsoft Excel™ 2007 and later. Support save file without losing original +charts of XLSX. This library needs Go version 1.8 or later. + +Copyright 2016 - 2018 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. +*/ package excelize import ( diff --git a/col.go b/col.go index cddb09f45a..a2d7e69889 100644 --- a/col.go +++ b/col.go @@ -1,11 +1,13 @@ -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. -// -// Copyright 2016 - 2018 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. +/* +Package excelize providing a set of functions that allow you to write to +and read from XLSX files. Support reads and writes XLSX file generated by +Microsoft Excel™ 2007 and later. Support save file without losing original +charts of XLSX. This library needs Go version 1.8 or later. + +Copyright 2016 - 2018 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. +*/ package excelize import ( diff --git a/comment.go b/comment.go index 94aad6c198..241261cb41 100644 --- a/comment.go +++ b/comment.go @@ -1,11 +1,13 @@ -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. -// -// Copyright 2016 - 2018 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. +/* +Package excelize providing a set of functions that allow you to write to +and read from XLSX files. Support reads and writes XLSX file generated by +Microsoft Excel™ 2007 and later. Support save file without losing original +charts of XLSX. This library needs Go version 1.8 or later. + +Copyright 2016 - 2018 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. +*/ package excelize import ( diff --git a/datavalidation.go b/datavalidation.go index c0f006f35f..66485569d9 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -1,11 +1,13 @@ -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. -// -// Copyright 2016 - 2018 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. +/* +Package excelize providing a set of functions that allow you to write to +and read from XLSX files. Support reads and writes XLSX file generated by +Microsoft Excel™ 2007 and later. Support save file without losing original +charts of XLSX. This library needs Go version 1.8 or later. + +Copyright 2016 - 2018 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. +*/ package excelize import ( diff --git a/datavalidation_test.go b/datavalidation_test.go index 0f50f2906d..7d10024c1c 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -1,11 +1,13 @@ -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. -// -// Copyright 2016 - 2018 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. +/* +Package excelize providing a set of functions that allow you to write to +and read from XLSX files. Support reads and writes XLSX file generated by +Microsoft Excel™ 2007 and later. Support save file without losing original +charts of XLSX. This library needs Go version 1.8 or later. + +Copyright 2016 - 2018 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. +*/ package excelize import "testing" diff --git a/date.go b/date.go index 56d6ad7ae7..4ef055985b 100644 --- a/date.go +++ b/date.go @@ -1,11 +1,13 @@ -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. -// -// Copyright 2016 - 2018 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. +/* +Package excelize providing a set of functions that allow you to write to +and read from XLSX files. Support reads and writes XLSX file generated by +Microsoft Excel™ 2007 and later. Support save file without losing original +charts of XLSX. This library needs Go version 1.8 or later. + +Copyright 2016 - 2018 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. +*/ package excelize import ( diff --git a/excelize.go b/excelize.go index 915955c8fb..4f45784592 100644 --- a/excelize.go +++ b/excelize.go @@ -1,11 +1,13 @@ -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. -// -// Copyright 2016 - 2018 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. +/* +Package excelize providing a set of functions that allow you to write to +and read from XLSX files. Support reads and writes XLSX file generated by +Microsoft Excel™ 2007 and later. Support save file without losing original +charts of XLSX. This library needs Go version 1.8 or later. + +Copyright 2016 - 2018 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. +*/ package excelize import ( diff --git a/file.go b/file.go index 76ef98687a..350f753949 100644 --- a/file.go +++ b/file.go @@ -1,11 +1,13 @@ -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. -// -// Copyright 2016 - 2018 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. +/* +Package excelize providing a set of functions that allow you to write to +and read from XLSX files. Support reads and writes XLSX file generated by +Microsoft Excel™ 2007 and later. Support save file without losing original +charts of XLSX. This library needs Go version 1.8 or later. + +Copyright 2016 - 2018 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. +*/ package excelize import ( diff --git a/hsl.go b/hsl.go index fd10b5dae8..d8fc87dec8 100644 --- a/hsl.go +++ b/hsl.go @@ -1,35 +1,37 @@ -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. -// -// Copyright (c) 2012 Rodrigo Moraes. All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// * Neither the name of Google Inc. nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +/* +Package excelize providing a set of functions that allow you to write to +and read from XLSX files. Support reads and writes XLSX file generated by +Microsoft Excel™ 2007 and later. Support save file without losing original +charts of XLSX. This library needs Go version 1.8 or later. + +Copyright (c) 2012 Rodrigo Moraes. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ package excelize import ( diff --git a/lib.go b/lib.go index 5be9b16213..c4cd95b740 100644 --- a/lib.go +++ b/lib.go @@ -1,11 +1,13 @@ -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. -// -// Copyright 2016 - 2018 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. +/* +Package excelize providing a set of functions that allow you to write to +and read from XLSX files. Support reads and writes XLSX file generated by +Microsoft Excel™ 2007 and later. Support save file without losing original +charts of XLSX. This library needs Go version 1.8 or later. + +Copyright 2016 - 2018 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. +*/ package excelize import ( diff --git a/rows.go b/rows.go index 8e1e0c3bb3..aa8b2a666d 100644 --- a/rows.go +++ b/rows.go @@ -1,11 +1,13 @@ -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. -// -// Copyright 2016 - 2018 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. +/* +Package excelize providing a set of functions that allow you to write to +and read from XLSX files. Support reads and writes XLSX file generated by +Microsoft Excel™ 2007 and later. Support save file without losing original +charts of XLSX. This library needs Go version 1.8 or later. + +Copyright 2016 - 2018 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. +*/ package excelize import ( diff --git a/shape.go b/shape.go index 42aefcf99c..12cb725086 100644 --- a/shape.go +++ b/shape.go @@ -1,11 +1,13 @@ -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. -// -// Copyright 2016 - 2018 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. +/* +Package excelize providing a set of functions that allow you to write to +and read from XLSX files. Support reads and writes XLSX file generated by +Microsoft Excel™ 2007 and later. Support save file without losing original +charts of XLSX. This library needs Go version 1.8 or later. + +Copyright 2016 - 2018 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. +*/ package excelize import ( diff --git a/sheet.go b/sheet.go index ef2b6f4c40..a8d5ed7a5e 100644 --- a/sheet.go +++ b/sheet.go @@ -1,11 +1,13 @@ -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. -// -// Copyright 2016 - 2018 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. +/* +Package excelize providing a set of functions that allow you to write to +and read from XLSX files. Support reads and writes XLSX file generated by +Microsoft Excel™ 2007 and later. Support save file without losing original +charts of XLSX. This library needs Go version 1.8 or later. + +Copyright 2016 - 2018 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. +*/ package excelize import ( diff --git a/sheetpr.go b/sheetpr.go index a010130a38..4216cb9128 100644 --- a/sheetpr.go +++ b/sheetpr.go @@ -1,11 +1,13 @@ -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. -// -// Copyright 2016 - 2018 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. +/* +Package excelize providing a set of functions that allow you to write to +and read from XLSX files. Support reads and writes XLSX file generated by +Microsoft Excel™ 2007 and later. Support save file without losing original +charts of XLSX. This library needs Go version 1.8 or later. + +Copyright 2016 - 2018 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. +*/ package excelize // SheetPrOption is an option of a view of a worksheet. See SetSheetPrOptions(). diff --git a/sheetview.go b/sheetview.go index 5e756ead27..62b35a1b44 100644 --- a/sheetview.go +++ b/sheetview.go @@ -1,11 +1,13 @@ -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. -// -// Copyright 2016 - 2018 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. +/* +Package excelize providing a set of functions that allow you to write to +and read from XLSX files. Support reads and writes XLSX file generated by +Microsoft Excel™ 2007 and later. Support save file without losing original +charts of XLSX. This library needs Go version 1.8 or later. + +Copyright 2016 - 2018 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. +*/ package excelize import "fmt" diff --git a/styles.go b/styles.go index 80f347ecbd..ba5af3d5f4 100644 --- a/styles.go +++ b/styles.go @@ -1,11 +1,13 @@ -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. -// -// Copyright 2016 - 2018 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. +/* +Package excelize providing a set of functions that allow you to write to +and read from XLSX files. Support reads and writes XLSX file generated by +Microsoft Excel™ 2007 and later. Support save file without losing original +charts of XLSX. This library needs Go version 1.8 or later. + +Copyright 2016 - 2018 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. +*/ package excelize import ( diff --git a/table.go b/table.go index fcc4d3c327..a7b71edd3a 100644 --- a/table.go +++ b/table.go @@ -1,11 +1,13 @@ -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. -// -// Copyright 2016 - 2018 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. +/* +Package excelize providing a set of functions that allow you to write to +and read from XLSX files. Support reads and writes XLSX file generated by +Microsoft Excel™ 2007 and later. Support save file without losing original +charts of XLSX. This library needs Go version 1.8 or later. + +Copyright 2016 - 2018 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. +*/ package excelize import ( diff --git a/templates.go b/templates.go index a619dfc286..5f62f46a23 100644 --- a/templates.go +++ b/templates.go @@ -1,14 +1,16 @@ -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. -// -// Copyright 2016 - 2018 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. -// -// This file contains default templates for XML files we don't yet populated -// based on content. +/* +Package excelize providing a set of functions that allow you to write to +and read from XLSX files. Support reads and writes XLSX file generated by +Microsoft Excel™ 2007 and later. Support save file without losing original +charts of XLSX. This library needs Go version 1.8 or later. + +Copyright 2016 - 2018 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. + +This file contains default templates for XML files we don't yet populated +based on content. +*/ package excelize // XMLHeader define an XML declaration can also contain a standalone declaration. diff --git a/vmlDrawing.go b/vmlDrawing.go index f9323721c4..768ca2f503 100644 --- a/vmlDrawing.go +++ b/vmlDrawing.go @@ -1,11 +1,13 @@ -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. -// -// Copyright 2016 - 2018 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. +/* +Package excelize providing a set of functions that allow you to write to +and read from XLSX files. Support reads and writes XLSX file generated by +Microsoft Excel™ 2007 and later. Support save file without losing original +charts of XLSX. This library needs Go version 1.8 or later. + +Copyright 2016 - 2018 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. +*/ package excelize import "encoding/xml" diff --git a/xmlChart.go b/xmlChart.go index 8a2cfd8018..72ad4cee44 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -1,11 +1,13 @@ -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. -// -// Copyright 2016 - 2018 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. +/* +Package excelize providing a set of functions that allow you to write to +and read from XLSX files. Support reads and writes XLSX file generated by +Microsoft Excel™ 2007 and later. Support save file without losing original +charts of XLSX. This library needs Go version 1.8 or later. + +Copyright 2016 - 2018 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. +*/ package excelize import "encoding/xml" diff --git a/xmlComments.go b/xmlComments.go index 1607506cae..f16128d3d7 100644 --- a/xmlComments.go +++ b/xmlComments.go @@ -1,11 +1,13 @@ -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. -// -// Copyright 2016 - 2018 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. +/* +Package excelize providing a set of functions that allow you to write to +and read from XLSX files. Support reads and writes XLSX file generated by +Microsoft Excel™ 2007 and later. Support save file without losing original +charts of XLSX. This library needs Go version 1.8 or later. + +Copyright 2016 - 2018 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. +*/ package excelize import "encoding/xml" diff --git a/xmlContentTypes.go b/xmlContentTypes.go index 6c31bffa67..6abd0e67be 100644 --- a/xmlContentTypes.go +++ b/xmlContentTypes.go @@ -1,11 +1,13 @@ -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. -// -// Copyright 2016 - 2018 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. +/* +Package excelize providing a set of functions that allow you to write to +and read from XLSX files. Support reads and writes XLSX file generated by +Microsoft Excel™ 2007 and later. Support save file without losing original +charts of XLSX. This library needs Go version 1.8 or later. + +Copyright 2016 - 2018 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. +*/ package excelize import "encoding/xml" diff --git a/xmlDecodeDrawing.go b/xmlDecodeDrawing.go index c85c490d3d..c66176c725 100644 --- a/xmlDecodeDrawing.go +++ b/xmlDecodeDrawing.go @@ -1,11 +1,13 @@ -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. -// -// Copyright 2016 - 2018 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. +/* +Package excelize providing a set of functions that allow you to write to +and read from XLSX files. Support reads and writes XLSX file generated by +Microsoft Excel™ 2007 and later. Support save file without losing original +charts of XLSX. This library needs Go version 1.8 or later. + +Copyright 2016 - 2018 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. +*/ package excelize import "encoding/xml" diff --git a/xmlDrawing.go b/xmlDrawing.go index ee30a4ddf1..633856a2d2 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -1,11 +1,13 @@ -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. -// -// Copyright 2016 - 2018 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. +/* +Package excelize providing a set of functions that allow you to write to +and read from XLSX files. Support reads and writes XLSX file generated by +Microsoft Excel™ 2007 and later. Support save file without losing original +charts of XLSX. This library needs Go version 1.8 or later. + +Copyright 2016 - 2018 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. +*/ package excelize import "encoding/xml" diff --git a/xmlSharedStrings.go b/xmlSharedStrings.go index 94959c65dc..1f31f99ec9 100644 --- a/xmlSharedStrings.go +++ b/xmlSharedStrings.go @@ -1,11 +1,13 @@ -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. -// -// Copyright 2016 - 2018 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. +/* +Package excelize providing a set of functions that allow you to write to +and read from XLSX files. Support reads and writes XLSX file generated by +Microsoft Excel™ 2007 and later. Support save file without losing original +charts of XLSX. This library needs Go version 1.8 or later. + +Copyright 2016 - 2018 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. +*/ package excelize import "encoding/xml" diff --git a/xmlStyles.go b/xmlStyles.go index 3024c1e2b3..5b3c9e0151 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -1,11 +1,13 @@ -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. -// -// Copyright 2016 - 2018 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. +/* +Package excelize providing a set of functions that allow you to write to +and read from XLSX files. Support reads and writes XLSX file generated by +Microsoft Excel™ 2007 and later. Support save file without losing original +charts of XLSX. This library needs Go version 1.8 or later. + +Copyright 2016 - 2018 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. +*/ package excelize import "encoding/xml" diff --git a/xmlTable.go b/xmlTable.go index 53a5af432a..3949d49446 100644 --- a/xmlTable.go +++ b/xmlTable.go @@ -1,11 +1,13 @@ -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. -// -// Copyright 2016 - 2018 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. +/* +Package excelize providing a set of functions that allow you to write to +and read from XLSX files. Support reads and writes XLSX file generated by +Microsoft Excel™ 2007 and later. Support save file without losing original +charts of XLSX. This library needs Go version 1.8 or later. + +Copyright 2016 - 2018 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. +*/ package excelize import "encoding/xml" diff --git a/xmlTheme.go b/xmlTheme.go index e72e53b98a..b9dd2bbcc3 100644 --- a/xmlTheme.go +++ b/xmlTheme.go @@ -1,11 +1,13 @@ -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. -// -// Copyright 2016 - 2018 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. +/* +Package excelize providing a set of functions that allow you to write to +and read from XLSX files. Support reads and writes XLSX file generated by +Microsoft Excel™ 2007 and later. Support save file without losing original +charts of XLSX. This library needs Go version 1.8 or later. + +Copyright 2016 - 2018 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. +*/ package excelize import "encoding/xml" diff --git a/xmlWorkbook.go b/xmlWorkbook.go index d194bac46d..ca17d0356b 100644 --- a/xmlWorkbook.go +++ b/xmlWorkbook.go @@ -1,11 +1,13 @@ -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. -// -// Copyright 2016 - 2018 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. +/* +Package excelize providing a set of functions that allow you to write to +and read from XLSX files. Support reads and writes XLSX file generated by +Microsoft Excel™ 2007 and later. Support save file without losing original +charts of XLSX. This library needs Go version 1.8 or later. + +Copyright 2016 - 2018 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. +*/ package excelize import "encoding/xml" diff --git a/xmlWorksheet.go b/xmlWorksheet.go index e298329d81..c481a6d09b 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -1,11 +1,13 @@ -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. -// -// Copyright 2016 - 2018 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. +/* +Package excelize providing a set of functions that allow you to write to +and read from XLSX files. Support reads and writes XLSX file generated by +Microsoft Excel™ 2007 and later. Support save file without losing original +charts of XLSX. This library needs Go version 1.8 or later. + +Copyright 2016 - 2018 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. +*/ package excelize import "encoding/xml" From 13a9769cc5bde486c52d8e45661ff8108cd786ae Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 14 Sep 2018 00:44:23 +0800 Subject: [PATCH 019/957] Comments style changed. --- cell.go | 18 +++++------- chart.go | 18 +++++------- col.go | 18 +++++------- comment.go | 18 +++++------- datavalidation.go | 18 +++++------- datavalidation_test.go | 18 +++++------- date.go | 18 +++++------- excelize.go | 18 +++++------- file.go | 18 +++++------- hsl.go | 66 ++++++++++++++++++++---------------------- lib.go | 18 +++++------- picture.go | 8 ++--- rows.go | 18 +++++------- shape.go | 18 +++++------- sheet.go | 18 +++++------- sheetpr.go | 18 +++++------- sheetview.go | 18 +++++------- styles.go | 18 +++++------- table.go | 18 +++++------- templates.go | 24 +++++++-------- vmlDrawing.go | 18 +++++------- xmlChart.go | 18 +++++------- xmlComments.go | 18 +++++------- xmlContentTypes.go | 18 +++++------- xmlDecodeDrawing.go | 18 +++++------- xmlDrawing.go | 18 +++++------- xmlSharedStrings.go | 18 +++++------- xmlStyles.go | 18 +++++------- xmlTable.go | 18 +++++------- xmlTheme.go | 18 +++++------- xmlWorkbook.go | 18 +++++------- xmlWorksheet.go | 18 +++++------- 32 files changed, 279 insertions(+), 341 deletions(-) diff --git a/cell.go b/cell.go index 69f8697193..e1bb91dc35 100644 --- a/cell.go +++ b/cell.go @@ -1,13 +1,11 @@ -/* -Package excelize providing a set of functions that allow you to write to -and read from XLSX files. Support reads and writes XLSX file generated by -Microsoft Excel™ 2007 and later. Support save file without losing original -charts of XLSX. This library needs Go version 1.8 or later. - -Copyright 2016 - 2018 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. -*/ +// Copyright 2016 - 2018 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. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. package excelize import ( diff --git a/chart.go b/chart.go index 4b73c3b75c..a84e0f6603 100644 --- a/chart.go +++ b/chart.go @@ -1,13 +1,11 @@ -/* -Package excelize providing a set of functions that allow you to write to -and read from XLSX files. Support reads and writes XLSX file generated by -Microsoft Excel™ 2007 and later. Support save file without losing original -charts of XLSX. This library needs Go version 1.8 or later. - -Copyright 2016 - 2018 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. -*/ +// Copyright 2016 - 2018 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. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. package excelize import ( diff --git a/col.go b/col.go index a2d7e69889..c7ab9e32aa 100644 --- a/col.go +++ b/col.go @@ -1,13 +1,11 @@ -/* -Package excelize providing a set of functions that allow you to write to -and read from XLSX files. Support reads and writes XLSX file generated by -Microsoft Excel™ 2007 and later. Support save file without losing original -charts of XLSX. This library needs Go version 1.8 or later. - -Copyright 2016 - 2018 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. -*/ +// Copyright 2016 - 2018 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. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. package excelize import ( diff --git a/comment.go b/comment.go index 241261cb41..c87e08c851 100644 --- a/comment.go +++ b/comment.go @@ -1,13 +1,11 @@ -/* -Package excelize providing a set of functions that allow you to write to -and read from XLSX files. Support reads and writes XLSX file generated by -Microsoft Excel™ 2007 and later. Support save file without losing original -charts of XLSX. This library needs Go version 1.8 or later. - -Copyright 2016 - 2018 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. -*/ +// Copyright 2016 - 2018 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. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. package excelize import ( diff --git a/datavalidation.go b/datavalidation.go index 66485569d9..69f67b11f0 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -1,13 +1,11 @@ -/* -Package excelize providing a set of functions that allow you to write to -and read from XLSX files. Support reads and writes XLSX file generated by -Microsoft Excel™ 2007 and later. Support save file without losing original -charts of XLSX. This library needs Go version 1.8 or later. - -Copyright 2016 - 2018 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. -*/ +// Copyright 2016 - 2018 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. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. package excelize import ( diff --git a/datavalidation_test.go b/datavalidation_test.go index 7d10024c1c..b9c51ad461 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -1,13 +1,11 @@ -/* -Package excelize providing a set of functions that allow you to write to -and read from XLSX files. Support reads and writes XLSX file generated by -Microsoft Excel™ 2007 and later. Support save file without losing original -charts of XLSX. This library needs Go version 1.8 or later. - -Copyright 2016 - 2018 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. -*/ +// Copyright 2016 - 2018 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. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. package excelize import "testing" diff --git a/date.go b/date.go index 4ef055985b..c67c3a1ea9 100644 --- a/date.go +++ b/date.go @@ -1,13 +1,11 @@ -/* -Package excelize providing a set of functions that allow you to write to -and read from XLSX files. Support reads and writes XLSX file generated by -Microsoft Excel™ 2007 and later. Support save file without losing original -charts of XLSX. This library needs Go version 1.8 or later. - -Copyright 2016 - 2018 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. -*/ +// Copyright 2016 - 2018 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. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. package excelize import ( diff --git a/excelize.go b/excelize.go index 4f45784592..93596d8a6d 100644 --- a/excelize.go +++ b/excelize.go @@ -1,13 +1,11 @@ -/* -Package excelize providing a set of functions that allow you to write to -and read from XLSX files. Support reads and writes XLSX file generated by -Microsoft Excel™ 2007 and later. Support save file without losing original -charts of XLSX. This library needs Go version 1.8 or later. - -Copyright 2016 - 2018 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. -*/ +// Copyright 2016 - 2018 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. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. package excelize import ( diff --git a/file.go b/file.go index 350f753949..582228f6c5 100644 --- a/file.go +++ b/file.go @@ -1,13 +1,11 @@ -/* -Package excelize providing a set of functions that allow you to write to -and read from XLSX files. Support reads and writes XLSX file generated by -Microsoft Excel™ 2007 and later. Support save file without losing original -charts of XLSX. This library needs Go version 1.8 or later. - -Copyright 2016 - 2018 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. -*/ +// Copyright 2016 - 2018 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. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. package excelize import ( diff --git a/hsl.go b/hsl.go index d8fc87dec8..77946ac3df 100644 --- a/hsl.go +++ b/hsl.go @@ -1,37 +1,35 @@ -/* -Package excelize providing a set of functions that allow you to write to -and read from XLSX files. Support reads and writes XLSX file generated by -Microsoft Excel™ 2007 and later. Support save file without losing original -charts of XLSX. This library needs Go version 1.8 or later. - -Copyright (c) 2012 Rodrigo Moraes. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ +// Copyright (c) 2012 Rodrigo Moraes. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. package excelize import ( diff --git a/lib.go b/lib.go index c4cd95b740..14930395e6 100644 --- a/lib.go +++ b/lib.go @@ -1,13 +1,11 @@ -/* -Package excelize providing a set of functions that allow you to write to -and read from XLSX files. Support reads and writes XLSX file generated by -Microsoft Excel™ 2007 and later. Support save file without losing original -charts of XLSX. This library needs Go version 1.8 or later. - -Copyright 2016 - 2018 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. -*/ +// Copyright 2016 - 2018 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. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. package excelize import ( diff --git a/picture.go b/picture.go index e4734985fa..16b428f1d3 100644 --- a/picture.go +++ b/picture.go @@ -1,11 +1,11 @@ +// Copyright 2016 - 2018 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. +// // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original // charts of XLSX. This library needs Go version 1.8 or later. -// -// Copyright 2016 - 2018 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. package excelize import ( diff --git a/rows.go b/rows.go index aa8b2a666d..84dc6d8f11 100644 --- a/rows.go +++ b/rows.go @@ -1,13 +1,11 @@ -/* -Package excelize providing a set of functions that allow you to write to -and read from XLSX files. Support reads and writes XLSX file generated by -Microsoft Excel™ 2007 and later. Support save file without losing original -charts of XLSX. This library needs Go version 1.8 or later. - -Copyright 2016 - 2018 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. -*/ +// Copyright 2016 - 2018 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. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. package excelize import ( diff --git a/shape.go b/shape.go index 12cb725086..dcdf6d9b48 100644 --- a/shape.go +++ b/shape.go @@ -1,13 +1,11 @@ -/* -Package excelize providing a set of functions that allow you to write to -and read from XLSX files. Support reads and writes XLSX file generated by -Microsoft Excel™ 2007 and later. Support save file without losing original -charts of XLSX. This library needs Go version 1.8 or later. - -Copyright 2016 - 2018 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. -*/ +// Copyright 2016 - 2018 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. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. package excelize import ( diff --git a/sheet.go b/sheet.go index a8d5ed7a5e..97bb9b2cb4 100644 --- a/sheet.go +++ b/sheet.go @@ -1,13 +1,11 @@ -/* -Package excelize providing a set of functions that allow you to write to -and read from XLSX files. Support reads and writes XLSX file generated by -Microsoft Excel™ 2007 and later. Support save file without losing original -charts of XLSX. This library needs Go version 1.8 or later. - -Copyright 2016 - 2018 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. -*/ +// Copyright 2016 - 2018 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. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. package excelize import ( diff --git a/sheetpr.go b/sheetpr.go index 4216cb9128..6f77f6f897 100644 --- a/sheetpr.go +++ b/sheetpr.go @@ -1,13 +1,11 @@ -/* -Package excelize providing a set of functions that allow you to write to -and read from XLSX files. Support reads and writes XLSX file generated by -Microsoft Excel™ 2007 and later. Support save file without losing original -charts of XLSX. This library needs Go version 1.8 or later. - -Copyright 2016 - 2018 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. -*/ +// Copyright 2016 - 2018 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. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. package excelize // SheetPrOption is an option of a view of a worksheet. See SetSheetPrOptions(). diff --git a/sheetview.go b/sheetview.go index 62b35a1b44..b3ef4779d4 100644 --- a/sheetview.go +++ b/sheetview.go @@ -1,13 +1,11 @@ -/* -Package excelize providing a set of functions that allow you to write to -and read from XLSX files. Support reads and writes XLSX file generated by -Microsoft Excel™ 2007 and later. Support save file without losing original -charts of XLSX. This library needs Go version 1.8 or later. - -Copyright 2016 - 2018 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. -*/ +// Copyright 2016 - 2018 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. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. package excelize import "fmt" diff --git a/styles.go b/styles.go index ba5af3d5f4..e9da3b3188 100644 --- a/styles.go +++ b/styles.go @@ -1,13 +1,11 @@ -/* -Package excelize providing a set of functions that allow you to write to -and read from XLSX files. Support reads and writes XLSX file generated by -Microsoft Excel™ 2007 and later. Support save file without losing original -charts of XLSX. This library needs Go version 1.8 or later. - -Copyright 2016 - 2018 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. -*/ +// Copyright 2016 - 2018 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. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. package excelize import ( diff --git a/table.go b/table.go index a7b71edd3a..e5d8785bd6 100644 --- a/table.go +++ b/table.go @@ -1,13 +1,11 @@ -/* -Package excelize providing a set of functions that allow you to write to -and read from XLSX files. Support reads and writes XLSX file generated by -Microsoft Excel™ 2007 and later. Support save file without losing original -charts of XLSX. This library needs Go version 1.8 or later. - -Copyright 2016 - 2018 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. -*/ +// Copyright 2016 - 2018 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. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. package excelize import ( diff --git a/templates.go b/templates.go index 5f62f46a23..f648fc85a7 100644 --- a/templates.go +++ b/templates.go @@ -1,16 +1,14 @@ -/* -Package excelize providing a set of functions that allow you to write to -and read from XLSX files. Support reads and writes XLSX file generated by -Microsoft Excel™ 2007 and later. Support save file without losing original -charts of XLSX. This library needs Go version 1.8 or later. - -Copyright 2016 - 2018 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. - -This file contains default templates for XML files we don't yet populated -based on content. -*/ +// Copyright 2016 - 2018 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. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. +// +// This file contains default templates for XML files we don't yet populated +// based on content. package excelize // XMLHeader define an XML declaration can also contain a standalone declaration. diff --git a/vmlDrawing.go b/vmlDrawing.go index 768ca2f503..305a6896b2 100644 --- a/vmlDrawing.go +++ b/vmlDrawing.go @@ -1,13 +1,11 @@ -/* -Package excelize providing a set of functions that allow you to write to -and read from XLSX files. Support reads and writes XLSX file generated by -Microsoft Excel™ 2007 and later. Support save file without losing original -charts of XLSX. This library needs Go version 1.8 or later. - -Copyright 2016 - 2018 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. -*/ +// Copyright 2016 - 2018 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. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. package excelize import "encoding/xml" diff --git a/xmlChart.go b/xmlChart.go index 72ad4cee44..9dcec1ec30 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -1,13 +1,11 @@ -/* -Package excelize providing a set of functions that allow you to write to -and read from XLSX files. Support reads and writes XLSX file generated by -Microsoft Excel™ 2007 and later. Support save file without losing original -charts of XLSX. This library needs Go version 1.8 or later. - -Copyright 2016 - 2018 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. -*/ +// Copyright 2016 - 2018 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. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. package excelize import "encoding/xml" diff --git a/xmlComments.go b/xmlComments.go index f16128d3d7..b3cb575b80 100644 --- a/xmlComments.go +++ b/xmlComments.go @@ -1,13 +1,11 @@ -/* -Package excelize providing a set of functions that allow you to write to -and read from XLSX files. Support reads and writes XLSX file generated by -Microsoft Excel™ 2007 and later. Support save file without losing original -charts of XLSX. This library needs Go version 1.8 or later. - -Copyright 2016 - 2018 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. -*/ +// Copyright 2016 - 2018 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. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. package excelize import "encoding/xml" diff --git a/xmlContentTypes.go b/xmlContentTypes.go index 6abd0e67be..efbca78cc7 100644 --- a/xmlContentTypes.go +++ b/xmlContentTypes.go @@ -1,13 +1,11 @@ -/* -Package excelize providing a set of functions that allow you to write to -and read from XLSX files. Support reads and writes XLSX file generated by -Microsoft Excel™ 2007 and later. Support save file without losing original -charts of XLSX. This library needs Go version 1.8 or later. - -Copyright 2016 - 2018 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. -*/ +// Copyright 2016 - 2018 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. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. package excelize import "encoding/xml" diff --git a/xmlDecodeDrawing.go b/xmlDecodeDrawing.go index c66176c725..80511403bd 100644 --- a/xmlDecodeDrawing.go +++ b/xmlDecodeDrawing.go @@ -1,13 +1,11 @@ -/* -Package excelize providing a set of functions that allow you to write to -and read from XLSX files. Support reads and writes XLSX file generated by -Microsoft Excel™ 2007 and later. Support save file without losing original -charts of XLSX. This library needs Go version 1.8 or later. - -Copyright 2016 - 2018 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. -*/ +// Copyright 2016 - 2018 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. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. package excelize import "encoding/xml" diff --git a/xmlDrawing.go b/xmlDrawing.go index 633856a2d2..e71932b420 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -1,13 +1,11 @@ -/* -Package excelize providing a set of functions that allow you to write to -and read from XLSX files. Support reads and writes XLSX file generated by -Microsoft Excel™ 2007 and later. Support save file without losing original -charts of XLSX. This library needs Go version 1.8 or later. - -Copyright 2016 - 2018 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. -*/ +// Copyright 2016 - 2018 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. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. package excelize import "encoding/xml" diff --git a/xmlSharedStrings.go b/xmlSharedStrings.go index 1f31f99ec9..9fc857992a 100644 --- a/xmlSharedStrings.go +++ b/xmlSharedStrings.go @@ -1,13 +1,11 @@ -/* -Package excelize providing a set of functions that allow you to write to -and read from XLSX files. Support reads and writes XLSX file generated by -Microsoft Excel™ 2007 and later. Support save file without losing original -charts of XLSX. This library needs Go version 1.8 or later. - -Copyright 2016 - 2018 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. -*/ +// Copyright 2016 - 2018 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. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. package excelize import "encoding/xml" diff --git a/xmlStyles.go b/xmlStyles.go index 5b3c9e0151..67785ed01f 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -1,13 +1,11 @@ -/* -Package excelize providing a set of functions that allow you to write to -and read from XLSX files. Support reads and writes XLSX file generated by -Microsoft Excel™ 2007 and later. Support save file without losing original -charts of XLSX. This library needs Go version 1.8 or later. - -Copyright 2016 - 2018 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. -*/ +// Copyright 2016 - 2018 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. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. package excelize import "encoding/xml" diff --git a/xmlTable.go b/xmlTable.go index 3949d49446..a5d867331d 100644 --- a/xmlTable.go +++ b/xmlTable.go @@ -1,13 +1,11 @@ -/* -Package excelize providing a set of functions that allow you to write to -and read from XLSX files. Support reads and writes XLSX file generated by -Microsoft Excel™ 2007 and later. Support save file without losing original -charts of XLSX. This library needs Go version 1.8 or later. - -Copyright 2016 - 2018 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. -*/ +// Copyright 2016 - 2018 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. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. package excelize import "encoding/xml" diff --git a/xmlTheme.go b/xmlTheme.go index b9dd2bbcc3..146ca3a9e3 100644 --- a/xmlTheme.go +++ b/xmlTheme.go @@ -1,13 +1,11 @@ -/* -Package excelize providing a set of functions that allow you to write to -and read from XLSX files. Support reads and writes XLSX file generated by -Microsoft Excel™ 2007 and later. Support save file without losing original -charts of XLSX. This library needs Go version 1.8 or later. - -Copyright 2016 - 2018 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. -*/ +// Copyright 2016 - 2018 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. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. package excelize import "encoding/xml" diff --git a/xmlWorkbook.go b/xmlWorkbook.go index ca17d0356b..2e3ab4705e 100644 --- a/xmlWorkbook.go +++ b/xmlWorkbook.go @@ -1,13 +1,11 @@ -/* -Package excelize providing a set of functions that allow you to write to -and read from XLSX files. Support reads and writes XLSX file generated by -Microsoft Excel™ 2007 and later. Support save file without losing original -charts of XLSX. This library needs Go version 1.8 or later. - -Copyright 2016 - 2018 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. -*/ +// Copyright 2016 - 2018 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. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. package excelize import "encoding/xml" diff --git a/xmlWorksheet.go b/xmlWorksheet.go index c481a6d09b..8868cecfcf 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -1,13 +1,11 @@ -/* -Package excelize providing a set of functions that allow you to write to -and read from XLSX files. Support reads and writes XLSX file generated by -Microsoft Excel™ 2007 and later. Support save file without losing original -charts of XLSX. This library needs Go version 1.8 or later. - -Copyright 2016 - 2018 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. -*/ +// Copyright 2016 - 2018 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. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. package excelize import "encoding/xml" From 3e004d900b103379c2d62657a3070de4a2e8585a Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 14 Sep 2018 00:58:48 +0800 Subject: [PATCH 020/957] Comments style changed. --- cell.go | 1 + chart.go | 1 + col.go | 1 + comment.go | 1 + datavalidation.go | 1 + datavalidation_test.go | 1 + date.go | 1 + excelize.go | 2 ++ file.go | 1 + hsl.go | 6 +----- lib.go | 1 + picture.go | 1 + rows.go | 1 + shape.go | 1 + sheet.go | 1 + sheetpr.go | 1 + sheetview.go | 1 + styles.go | 1 + table.go | 1 + templates.go | 1 + vmlDrawing.go | 1 + xmlChart.go | 1 + xmlComments.go | 1 + xmlContentTypes.go | 1 + xmlDecodeDrawing.go | 1 + xmlDrawing.go | 1 + xmlSharedStrings.go | 1 + xmlStyles.go | 1 + xmlTable.go | 1 + xmlTheme.go | 1 + xmlWorkbook.go | 1 + xmlWorksheet.go | 1 + 32 files changed, 33 insertions(+), 5 deletions(-) diff --git a/cell.go b/cell.go index e1bb91dc35..1277a18073 100644 --- a/cell.go +++ b/cell.go @@ -6,6 +6,7 @@ // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original // charts of XLSX. This library needs Go version 1.8 or later. + package excelize import ( diff --git a/chart.go b/chart.go index a84e0f6603..5353a3297c 100644 --- a/chart.go +++ b/chart.go @@ -6,6 +6,7 @@ // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original // charts of XLSX. This library needs Go version 1.8 or later. + package excelize import ( diff --git a/col.go b/col.go index c7ab9e32aa..32cda12a64 100644 --- a/col.go +++ b/col.go @@ -6,6 +6,7 @@ // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original // charts of XLSX. This library needs Go version 1.8 or later. + package excelize import ( diff --git a/comment.go b/comment.go index c87e08c851..2bfd785da6 100644 --- a/comment.go +++ b/comment.go @@ -6,6 +6,7 @@ // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original // charts of XLSX. This library needs Go version 1.8 or later. + package excelize import ( diff --git a/datavalidation.go b/datavalidation.go index 69f67b11f0..5ebd61f97c 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -6,6 +6,7 @@ // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original // charts of XLSX. This library needs Go version 1.8 or later. + package excelize import ( diff --git a/datavalidation_test.go b/datavalidation_test.go index b9c51ad461..39dd2294ae 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -6,6 +6,7 @@ // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original // charts of XLSX. This library needs Go version 1.8 or later. + package excelize import "testing" diff --git a/date.go b/date.go index c67c3a1ea9..45f3040cf5 100644 --- a/date.go +++ b/date.go @@ -6,6 +6,7 @@ // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original // charts of XLSX. This library needs Go version 1.8 or later. + package excelize import ( diff --git a/excelize.go b/excelize.go index 93596d8a6d..d1f0b7f1c1 100644 --- a/excelize.go +++ b/excelize.go @@ -6,6 +6,8 @@ // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original // charts of XLSX. This library needs Go version 1.8 or later. +// +// See https://xuri.me/excelize for more information about this package. package excelize import ( diff --git a/file.go b/file.go index 582228f6c5..5bfed392f6 100644 --- a/file.go +++ b/file.go @@ -6,6 +6,7 @@ // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original // charts of XLSX. This library needs Go version 1.8 or later. + package excelize import ( diff --git a/hsl.go b/hsl.go index 77946ac3df..c30c165a5b 100644 --- a/hsl.go +++ b/hsl.go @@ -25,11 +25,7 @@ // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -// -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. + package excelize import ( diff --git a/lib.go b/lib.go index 14930395e6..865ee29657 100644 --- a/lib.go +++ b/lib.go @@ -6,6 +6,7 @@ // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original // charts of XLSX. This library needs Go version 1.8 or later. + package excelize import ( diff --git a/picture.go b/picture.go index 16b428f1d3..8785aaf53f 100644 --- a/picture.go +++ b/picture.go @@ -6,6 +6,7 @@ // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original // charts of XLSX. This library needs Go version 1.8 or later. + package excelize import ( diff --git a/rows.go b/rows.go index 84dc6d8f11..5c384c8f09 100644 --- a/rows.go +++ b/rows.go @@ -6,6 +6,7 @@ // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original // charts of XLSX. This library needs Go version 1.8 or later. + package excelize import ( diff --git a/shape.go b/shape.go index dcdf6d9b48..ad87712e82 100644 --- a/shape.go +++ b/shape.go @@ -6,6 +6,7 @@ // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original // charts of XLSX. This library needs Go version 1.8 or later. + package excelize import ( diff --git a/sheet.go b/sheet.go index 97bb9b2cb4..b615ae5e14 100644 --- a/sheet.go +++ b/sheet.go @@ -6,6 +6,7 @@ // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original // charts of XLSX. This library needs Go version 1.8 or later. + package excelize import ( diff --git a/sheetpr.go b/sheetpr.go index 6f77f6f897..e38b64e25c 100644 --- a/sheetpr.go +++ b/sheetpr.go @@ -6,6 +6,7 @@ // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original // charts of XLSX. This library needs Go version 1.8 or later. + package excelize // SheetPrOption is an option of a view of a worksheet. See SetSheetPrOptions(). diff --git a/sheetview.go b/sheetview.go index b3ef4779d4..e76325c558 100644 --- a/sheetview.go +++ b/sheetview.go @@ -6,6 +6,7 @@ // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original // charts of XLSX. This library needs Go version 1.8 or later. + package excelize import "fmt" diff --git a/styles.go b/styles.go index e9da3b3188..513fc9b3a9 100644 --- a/styles.go +++ b/styles.go @@ -6,6 +6,7 @@ // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original // charts of XLSX. This library needs Go version 1.8 or later. + package excelize import ( diff --git a/table.go b/table.go index e5d8785bd6..02c89fa561 100644 --- a/table.go +++ b/table.go @@ -6,6 +6,7 @@ // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original // charts of XLSX. This library needs Go version 1.8 or later. + package excelize import ( diff --git a/templates.go b/templates.go index f648fc85a7..1d0655dc93 100644 --- a/templates.go +++ b/templates.go @@ -9,6 +9,7 @@ // // This file contains default templates for XML files we don't yet populated // based on content. + package excelize // XMLHeader define an XML declaration can also contain a standalone declaration. diff --git a/vmlDrawing.go b/vmlDrawing.go index 305a6896b2..c17dde7887 100644 --- a/vmlDrawing.go +++ b/vmlDrawing.go @@ -6,6 +6,7 @@ // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original // charts of XLSX. This library needs Go version 1.8 or later. + package excelize import "encoding/xml" diff --git a/xmlChart.go b/xmlChart.go index 9dcec1ec30..78218a01d4 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -6,6 +6,7 @@ // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original // charts of XLSX. This library needs Go version 1.8 or later. + package excelize import "encoding/xml" diff --git a/xmlComments.go b/xmlComments.go index b3cb575b80..9075c8873a 100644 --- a/xmlComments.go +++ b/xmlComments.go @@ -6,6 +6,7 @@ // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original // charts of XLSX. This library needs Go version 1.8 or later. + package excelize import "encoding/xml" diff --git a/xmlContentTypes.go b/xmlContentTypes.go index efbca78cc7..8d09d515ef 100644 --- a/xmlContentTypes.go +++ b/xmlContentTypes.go @@ -6,6 +6,7 @@ // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original // charts of XLSX. This library needs Go version 1.8 or later. + package excelize import "encoding/xml" diff --git a/xmlDecodeDrawing.go b/xmlDecodeDrawing.go index 80511403bd..d21c3f01df 100644 --- a/xmlDecodeDrawing.go +++ b/xmlDecodeDrawing.go @@ -6,6 +6,7 @@ // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original // charts of XLSX. This library needs Go version 1.8 or later. + package excelize import "encoding/xml" diff --git a/xmlDrawing.go b/xmlDrawing.go index e71932b420..6ba7d31d1f 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -6,6 +6,7 @@ // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original // charts of XLSX. This library needs Go version 1.8 or later. + package excelize import "encoding/xml" diff --git a/xmlSharedStrings.go b/xmlSharedStrings.go index 9fc857992a..782ed61ad5 100644 --- a/xmlSharedStrings.go +++ b/xmlSharedStrings.go @@ -6,6 +6,7 @@ // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original // charts of XLSX. This library needs Go version 1.8 or later. + package excelize import "encoding/xml" diff --git a/xmlStyles.go b/xmlStyles.go index 67785ed01f..7ba4379148 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -6,6 +6,7 @@ // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original // charts of XLSX. This library needs Go version 1.8 or later. + package excelize import "encoding/xml" diff --git a/xmlTable.go b/xmlTable.go index a5d867331d..7e155e6d72 100644 --- a/xmlTable.go +++ b/xmlTable.go @@ -6,6 +6,7 @@ // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original // charts of XLSX. This library needs Go version 1.8 or later. + package excelize import "encoding/xml" diff --git a/xmlTheme.go b/xmlTheme.go index 146ca3a9e3..b4140b6acd 100644 --- a/xmlTheme.go +++ b/xmlTheme.go @@ -6,6 +6,7 @@ // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original // charts of XLSX. This library needs Go version 1.8 or later. + package excelize import "encoding/xml" diff --git a/xmlWorkbook.go b/xmlWorkbook.go index 2e3ab4705e..f00a0b8584 100644 --- a/xmlWorkbook.go +++ b/xmlWorkbook.go @@ -6,6 +6,7 @@ // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original // charts of XLSX. This library needs Go version 1.8 or later. + package excelize import "encoding/xml" diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 8868cecfcf..072ecce6da 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -6,6 +6,7 @@ // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original // charts of XLSX. This library needs Go version 1.8 or later. + package excelize import "encoding/xml" From 250946568ca1e5a69c07f19dff4d1d3a2264e31d Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 27 Sep 2018 23:36:27 +0800 Subject: [PATCH 021/957] Remove independent sample file --- samples/dumpXLSX.go | 45 --------------------------------------------- 1 file changed, 45 deletions(-) delete mode 100644 samples/dumpXLSX.go diff --git a/samples/dumpXLSX.go b/samples/dumpXLSX.go deleted file mode 100644 index 853a337ee5..0000000000 --- a/samples/dumpXLSX.go +++ /dev/null @@ -1,45 +0,0 @@ -package main - -import ( - "fmt" - "os" - - "github.com/360EntSecGroup-Skylar/excelize" -) - -func main() { - // Exit on missing filename - if len(os.Args) < 2 || os.Args[1] == "" { - fmt.Println("Syntax: dumpXLSX ") - os.Exit(1) - } - - // Open file and panic on error - fmt.Println("Reading ", os.Args[1]) - xlsx, err := excelize.OpenFile(os.Args[1]) - if err != nil { - panic(err) - } - - // Read all sheets in map - for i, sheet := range xlsx.GetSheetMap() { - //Output sheet header - fmt.Printf("----- %d. %s -----\n", i, sheet) - - // Get rows - rows := xlsx.GetRows(sheet) - // Create a row number prefix pattern long enough to fit all row numbers - prefixPattern := fmt.Sprintf("%% %dd ", len(fmt.Sprintf("%d", len(rows)))) - - // Walk through rows - for j, row := range rows { - // Output row number as prefix - fmt.Printf(prefixPattern, j) - // Output row content - for _, cell := range row { - fmt.Print(cell, "\t") - } - fmt.Println() - } - } -} From 2be4d45c6270d2b8c3e80d8bd6a10cba71b5052a Mon Sep 17 00:00:00 2001 From: lizheao Date: Mon, 8 Oct 2018 22:17:33 +0800 Subject: [PATCH 022/957] feat: add new function and refactor writeto action * add new exported function to get Excel file content buffer * refactor the WriteTo function --- excelize_test.go | 4 ++++ file.go | 20 ++++++++++++-------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/excelize_test.go b/excelize_test.go index 9f738f331d..b24e45bda3 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -135,6 +135,10 @@ func TestOpenFile(t *testing.T) { if err != nil { t.Log(err) } + _, err = xlsx.WriteToBuffer() + if err != nil { + t.Error(err) + } } func TestAddPicture(t *testing.T) { diff --git a/file.go b/file.go index 5bfed392f6..1f69005486 100644 --- a/file.go +++ b/file.go @@ -76,6 +76,15 @@ func (f *File) Write(w io.Writer) error { // WriteTo implements io.WriterTo to write the file. func (f *File) WriteTo(w io.Writer) (int64, error) { + buf, err := f.WriteToBuffer() + if err != nil { + return 0, err + } + return buf.WriteTo(w) +} + +// WriteToBuffer provides a function to get bytes.Buffer from the saved file. +func (f *File) WriteToBuffer() (*bytes.Buffer, error) { buf := new(bytes.Buffer) zw := zip.NewWriter(buf) f.contentTypesWriter() @@ -86,17 +95,12 @@ func (f *File) WriteTo(w io.Writer) (int64, error) { for path, content := range f.XLSX { fi, err := zw.Create(path) if err != nil { - return 0, err + return buf, err } _, err = fi.Write(content) if err != nil { - return 0, err + return buf, err } } - err := zw.Close() - if err != nil { - return 0, err - } - - return buf.WriteTo(w) + return buf, zw.Close() } From 1c45425f12f38012b975c36f4d17bd1cec3c0aba Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 17 Oct 2018 00:28:31 +0800 Subject: [PATCH 023/957] resolve #276, add OfficeOpenXML-XMLSchema-Strict mode support --- chart.go | 2 +- excelize.go | 2 +- lib.go | 16 ++++++++++++++++ picture.go | 12 ++++++------ sheet.go | 8 ++++---- styles.go | 4 ++-- test/Book1.xlsx | Bin 23099 -> 33070 bytes xmlDrawing.go | 41 +++++++++++++++++++++++------------------ 8 files changed, 53 insertions(+), 32 deletions(-) diff --git a/chart.go b/chart.go index 5353a3297c..edfcab7f31 100644 --- a/chart.go +++ b/chart.go @@ -1097,7 +1097,7 @@ func (f *File) drawingParser(drawingXML string, content *xlsxWsDr) int { _, ok := f.XLSX[drawingXML] if ok { // Append Model decodeWsDr := decodeWsDr{} - _ = xml.Unmarshal([]byte(f.readXML(drawingXML)), &decodeWsDr) + _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML(drawingXML)), &decodeWsDr) content.R = decodeWsDr.R cNvPrID = len(decodeWsDr.OneCellAnchor) + len(decodeWsDr.TwoCellAnchor) + 1 for _, v := range decodeWsDr.OneCellAnchor { diff --git a/excelize.go b/excelize.go index d1f0b7f1c1..36a6d8a968 100644 --- a/excelize.go +++ b/excelize.go @@ -100,7 +100,7 @@ func (f *File) workSheetReader(sheet string) *xlsxWorksheet { } if f.Sheet[name] == nil { var xlsx xlsxWorksheet - _ = xml.Unmarshal(f.readXML(name), &xlsx) + _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML(name)), &xlsx) if f.checked == nil { f.checked = make(map[string]bool) } diff --git a/lib.go b/lib.go index 865ee29657..8e63da9f8f 100644 --- a/lib.go +++ b/lib.go @@ -183,3 +183,19 @@ func parseFormatSet(formatSet string) []byte { } return []byte("{}") } + +// namespaceStrictToTransitional provides a method to convert Strict and +// Transitional namespaces. +func namespaceStrictToTransitional(content []byte) []byte { + var namespaceTranslationDic = map[string]string{ + StrictSourceRelationship: SourceRelationship, + StrictSourceRelationshipChart: SourceRelationshipChart, + StrictSourceRelationshipComments: SourceRelationshipComments, + StrictSourceRelationshipImage: SourceRelationshipImage, + StrictNameSpaceSpreadSheet: NameSpaceSpreadSheet, + } + for s, n := range namespaceTranslationDic { + content = bytes.Replace(content, []byte(s), []byte(n), -1) + } + return content +} diff --git a/picture.go b/picture.go index 8785aaf53f..9efd875a98 100644 --- a/picture.go +++ b/picture.go @@ -185,7 +185,7 @@ func (f *File) addSheetRelationships(sheet, relType, target, targetMode string) _, ok = f.XLSX[rels] if ok { ID.Reset() - _ = xml.Unmarshal([]byte(f.readXML(rels)), &sheetRels) + _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML(rels)), &sheetRels) rID = len(sheetRels.Relationships) + 1 ID.WriteString("rId") ID.WriteString(strconv.Itoa(rID)) @@ -211,7 +211,7 @@ func (f *File) deleteSheetRelationships(sheet, rID string) { } var rels = "xl/worksheets/_rels/" + strings.TrimPrefix(name, "xl/worksheets/") + ".rels" var sheetRels xlsxWorkbookRels - _ = xml.Unmarshal([]byte(f.readXML(rels)), &sheetRels) + _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML(rels)), &sheetRels) for k, v := range sheetRels.Relationships { if v.ID == rID { sheetRels.Relationships = append(sheetRels.Relationships[:k], sheetRels.Relationships[k+1:]...) @@ -328,7 +328,7 @@ func (f *File) addDrawingRelationships(index int, relType, target, targetMode st _, ok := f.XLSX[rels] if ok { ID.Reset() - _ = xml.Unmarshal([]byte(f.readXML(rels)), &drawingRels) + _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML(rels)), &drawingRels) rID = len(drawingRels.Relationships) + 1 ID.WriteString("rId") ID.WriteString(strconv.Itoa(rID)) @@ -448,7 +448,7 @@ func (f *File) getSheetRelationshipsTargetByID(sheet, rID string) string { } var rels = "xl/worksheets/_rels/" + strings.TrimPrefix(name, "xl/worksheets/") + ".rels" var sheetRels xlsxWorkbookRels - _ = xml.Unmarshal([]byte(f.readXML(rels)), &sheetRels) + _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML(rels)), &sheetRels) for _, v := range sheetRels.Relationships { if v.ID == rID { return v.Target @@ -488,7 +488,7 @@ func (f *File) GetPicture(sheet, cell string) (string, []byte) { return "", nil } decodeWsDr := decodeWsDr{} - _ = xml.Unmarshal([]byte(f.readXML(drawingXML)), &decodeWsDr) + _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML(drawingXML)), &decodeWsDr) cell = strings.ToUpper(cell) fromCol := string(strings.Map(letterOnlyMapF, cell)) @@ -523,7 +523,7 @@ func (f *File) getDrawingRelationships(rels, rID string) *xlsxWorkbookRelation { return nil } var drawingRels xlsxWorkbookRels - _ = xml.Unmarshal([]byte(f.readXML(rels)), &drawingRels) + _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML(rels)), &drawingRels) for _, v := range drawingRels.Relationships { if v.ID == rID { return &v diff --git a/sheet.go b/sheet.go index 2344218c40..7b97d3e841 100644 --- a/sheet.go +++ b/sheet.go @@ -51,7 +51,7 @@ func (f *File) NewSheet(name string) int { func (f *File) contentTypesReader() *xlsxTypes { if f.ContentTypes == nil { var content xlsxTypes - _ = xml.Unmarshal([]byte(f.readXML("[Content_Types].xml")), &content) + _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML("[Content_Types].xml")), &content) f.ContentTypes = &content } return f.ContentTypes @@ -71,7 +71,7 @@ func (f *File) contentTypesWriter() { func (f *File) workbookReader() *xlsxWorkbook { if f.WorkBook == nil { var content xlsxWorkbook - _ = xml.Unmarshal([]byte(f.readXML("xl/workbook.xml")), &content) + _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML("xl/workbook.xml")), &content) f.WorkBook = &content } return f.WorkBook @@ -162,7 +162,7 @@ func (f *File) setWorkbook(name string, rid int) { func (f *File) workbookRelsReader() *xlsxWorkbookRels { if f.WorkBookRels == nil { var content xlsxWorkbookRels - _ = xml.Unmarshal([]byte(f.readXML("xl/_rels/workbook.xml.rels")), &content) + _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML("xl/_rels/workbook.xml.rels")), &content) f.WorkBookRels = &content } return f.WorkBookRels @@ -267,7 +267,7 @@ func (f *File) GetActiveSheetIndex() int { buffer.WriteString("xl/worksheets/sheet") buffer.WriteString(strings.TrimPrefix(v.ID, "rId")) buffer.WriteString(".xml") - _ = xml.Unmarshal([]byte(f.readXML(buffer.String())), &xlsx) + _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML(buffer.String())), &xlsx) for _, sheetView := range xlsx.SheetViews.SheetView { if sheetView.TabSelected { ID, _ := strconv.Atoi(strings.TrimPrefix(v.ID, "rId")) diff --git a/styles.go b/styles.go index 513fc9b3a9..f923787f4d 100644 --- a/styles.go +++ b/styles.go @@ -999,7 +999,7 @@ func is12HourTime(format string) bool { func (f *File) stylesReader() *xlsxStyleSheet { if f.Styles == nil { var styleSheet xlsxStyleSheet - _ = xml.Unmarshal([]byte(f.readXML("xl/styles.xml")), &styleSheet) + _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML("xl/styles.xml")), &styleSheet) f.Styles = &styleSheet } return f.Styles @@ -2757,7 +2757,7 @@ func getPaletteColor(color string) string { // structure after deserialization. func (f *File) themeReader() *xlsxTheme { var theme xlsxTheme - _ = xml.Unmarshal([]byte(f.readXML("xl/theme/theme1.xml")), &theme) + _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML("xl/theme/theme1.xml")), &theme) return &theme } diff --git a/test/Book1.xlsx b/test/Book1.xlsx index 84c43d15f284fcd570060fecc9c55cea5c1a9852..7d9886ef6720ebe744d11a2c4a1ac239f64fb87a 100755 GIT binary patch literal 33070 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

md2+2u7)%IvjR5RP_i!f^`Dghpi{sTy!}T!Uv~L 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 diff --git a/xmlDrawing.go b/xmlDrawing.go index 6ba7d31d1f..7356cb5921 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -13,24 +13,29 @@ import "encoding/xml" // Source relationship and namespace. const ( - SourceRelationship = "http://schemas.openxmlformats.org/officeDocument/2006/relationships" - 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" - SourceRelationshipChart201506 = "http://schemas.microsoft.com/office/drawing/2015/06/chart" - SourceRelationshipChart20070802 = "http://schemas.microsoft.com/office/drawing/2007/8/2/chart" - SourceRelationshipChart2014 = "http://schemas.microsoft.com/office/drawing/2014/chart" - SourceRelationshipCompatibility = "http://schemas.openxmlformats.org/markup-compatibility/2006" - NameSpaceDrawingML = "http://schemas.openxmlformats.org/drawingml/2006/main" - NameSpaceDrawingMLChart = "http://schemas.openxmlformats.org/drawingml/2006/chart" - NameSpaceDrawingMLSpreadSheet = "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" - NameSpaceSpreadSheet = "http://schemas.openxmlformats.org/spreadsheetml/2006/main" - NameSpaceXML = "http://www.w3.org/XML/1998/namespace" + SourceRelationship = "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + 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" + SourceRelationshipChart201506 = "http://schemas.microsoft.com/office/drawing/2015/06/chart" + SourceRelationshipChart20070802 = "http://schemas.microsoft.com/office/drawing/2007/8/2/chart" + SourceRelationshipChart2014 = "http://schemas.microsoft.com/office/drawing/2014/chart" + SourceRelationshipCompatibility = "http://schemas.openxmlformats.org/markup-compatibility/2006" + NameSpaceDrawingML = "http://schemas.openxmlformats.org/drawingml/2006/main" + NameSpaceDrawingMLChart = "http://schemas.openxmlformats.org/drawingml/2006/chart" + NameSpaceDrawingMLSpreadSheet = "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" + NameSpaceSpreadSheet = "http://schemas.openxmlformats.org/spreadsheetml/2006/main" + NameSpaceXML = "http://www.w3.org/XML/1998/namespace" + 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" + StrictNameSpaceSpreadSheet = "http://purl.oclc.org/ooxml/spreadsheetml/main" ) var supportImageTypes = map[string]string{".gif": ".gif", ".jpg": ".jpeg", ".jpeg": ".jpeg", ".png": ".png"} From 90bdd3632f16244583525e580fa3edd42361db68 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 18 Oct 2018 10:23:08 +0800 Subject: [PATCH 024/957] Fix the issue caused by missing tradition to strict conversion for `sharedStringsReader()`, relate issue #276 --- comment.go | 4 ++-- excelize_test.go | 4 ---- rows.go | 2 +- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/comment.go b/comment.go index 2bfd785da6..9031aad494 100644 --- a/comment.go +++ b/comment.go @@ -182,7 +182,7 @@ func (f *File) addDrawingVML(commentID int, drawingVML, cell string, lineCount, c, ok := f.XLSX[drawingVML] if ok { d := decodeVmlDrawing{} - _ = xml.Unmarshal([]byte(c), &d) + _ = xml.Unmarshal(namespaceStrictToTransitional(c), &d) for _, v := range d.Shape { s := xlsxShape{ ID: "_x0000_s1025", @@ -252,7 +252,7 @@ func (f *File) addComment(commentsXML, cell string, formatSet *formatComment) { c, ok := f.XLSX[commentsXML] if ok { d := xlsxComments{} - _ = xml.Unmarshal([]byte(c), &d) + _ = xml.Unmarshal(namespaceStrictToTransitional(c), &d) comments.CommentList.Comment = append(comments.CommentList.Comment, d.CommentList.Comment...) } comments.CommentList.Comment = append(comments.CommentList.Comment, cmt) diff --git a/excelize_test.go b/excelize_test.go index b24e45bda3..9f738f331d 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -135,10 +135,6 @@ func TestOpenFile(t *testing.T) { if err != nil { t.Log(err) } - _, err = xlsx.WriteToBuffer() - if err != nil { - t.Error(err) - } } func TestAddPicture(t *testing.T) { diff --git a/rows.go b/rows.go index 5c384c8f09..b6336331df 100644 --- a/rows.go +++ b/rows.go @@ -249,7 +249,7 @@ func (f *File) sharedStringsReader() *xlsxSST { if len(ss) == 0 { ss = f.readXML("xl/SharedStrings.xml") } - _ = xml.Unmarshal([]byte(ss), &sharedStrings) + _ = xml.Unmarshal(namespaceStrictToTransitional(ss), &sharedStrings) f.SharedStrings = &sharedStrings } return f.SharedStrings From e2e58a3a441a169ff8e9e5e65caf545a15026c37 Mon Sep 17 00:00:00 2001 From: peng <414326615@qq.com> Date: Sat, 27 Oct 2018 14:19:54 +0800 Subject: [PATCH 025/957] New function: `SearchSheet()`, relate issue #277 --- sheet.go | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/sheet.go b/sheet.go index 7b97d3e841..522d112841 100644 --- a/sheet.go +++ b/sheet.go @@ -14,6 +14,7 @@ import ( "encoding/json" "encoding/xml" "errors" + "fmt" "io/ioutil" "os" "path" @@ -660,6 +661,56 @@ func (f *File) GetSheetVisible(name string) bool { return visible } +// SearchSheet provides a function to get coordinates by given worksheet name +// and cell value. This function only supports exact match of strings and +// numbers, doesn't support 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. For example, +// search the coordinates of the value of "100" on Sheet1: +// +// xlsx.SearchSheet("Sheet1", "100") +// +func (f *File) SearchSheet(sheet, value string) []string { + xlsx := f.workSheetReader(sheet) + result := []string{} + name, ok := f.sheetMap[trimSheetName(sheet)] + if !ok { + return result + } + if xlsx != nil { + output, _ := xml.Marshal(f.Sheet[name]) + f.saveFileList(name, replaceWorkSheetsRelationshipsNameSpaceBytes(output)) + } + xml.NewDecoder(bytes.NewReader(f.readXML(name))) + d := f.sharedStringsReader() + var inElement string + var r xlsxRow + decoder := xml.NewDecoder(bytes.NewReader(f.readXML(name))) + for { + token, _ := decoder.Token() + if token == nil { + break + } + switch startElement := token.(type) { + case xml.StartElement: + inElement = startElement.Name.Local + if inElement == "row" { + r = xlsxRow{} + _ = decoder.DecodeElement(&r, &startElement) + for _, colCell := range r.C { + val, _ := colCell.getValueFrom(f, d) + if val != value { + continue + } + result = append(result, fmt.Sprintf("%s%d", strings.Map(letterOnlyMapF, colCell.R), r.R)) + } + } + default: + } + } + return result +} + // trimSheetName provides a function to trim invaild characters by given worksheet // name. func trimSheetName(name string) string { From 75edf1ac7d05854f36a518be0aecb397908adb76 Mon Sep 17 00:00:00 2001 From: covv Date: Sat, 27 Oct 2018 22:54:17 +0800 Subject: [PATCH 026/957] Add testing case for the function `SearchSheet()`. --- excelize_test.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/excelize_test.go b/excelize_test.go index 9f738f331d..d021b55ce9 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -1195,6 +1195,19 @@ func TestHSL(t *testing.T) { t.Log(RGBToHSL(250, 50, 100)) } +func TestSearchSheet(t *testing.T) { + xlsx, err := OpenFile("./test/SharedStrings.xlsx") + if err != nil { + t.Error(err) + return + } + // Test search in a not exists worksheet. + t.Log(xlsx.SearchSheet("Sheet4", "")) + // Test search a not exists value. + t.Log(xlsx.SearchSheet("Sheet1", "X")) + t.Log(xlsx.SearchSheet("Sheet1", "A")) +} + func trimSliceSpace(s []string) []string { for { if len(s) > 0 && s[len(s)-1] == "" { From 0ef2324f0bdd2fb524b0e19973c2b1a093a5afc5 Mon Sep 17 00:00:00 2001 From: tvso Date: Sat, 27 Oct 2018 23:19:59 +0800 Subject: [PATCH 027/957] fix unknown option in JSON struct tag --- xmlChart.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/xmlChart.go b/xmlChart.go index 78218a01d4..3271cbbbf6 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -253,7 +253,7 @@ type aLn struct { Algn string `xml:"algn,attr,omitempty"` Cap string `xml:"cap,attr,omitempty"` Cmpd string `xml:"cmpd,attr,omitempty"` - W int `xml:"w,attr,omitempty" ` + W int `xml:"w,attr,omitempty"` NoFill string `xml:"a:noFill,omitempty"` Round string `xml:"a:round,omitempty"` SolidFill *aSolidFill `xml:"a:solidFill"` @@ -591,7 +591,7 @@ type formatChartSeries struct { } `json:"line"` Marker struct { Type string `json:"type"` - Size int `json:"size,"` + Size int `json:"size"` Width float64 `json:"width"` Border struct { Color string `json:"color"` From 4dbc78ce0a0d40be189b4c77207cdbb598846c73 Mon Sep 17 00:00:00 2001 From: HcySunYang Date: Fri, 2 Nov 2018 23:08:31 +0800 Subject: [PATCH 028/957] resolve #273 new feature: protect sheet support new feature: protect sheet support, relate issue #273 --- excelize_test.go | 13 ++++++++++ lib.go | 28 ++++++++++++++++++++++ sheet.go | 41 +++++++++++++++++++++++++++++++ test/Book1.xlsx | Bin 33070 -> 23099 bytes xmlWorksheet.go | 61 +++++++++++++++++++++++++++++++---------------- 5 files changed, 123 insertions(+), 20 deletions(-) mode change 100644 => 100755 excelize_test.go mode change 100755 => 100644 test/Book1.xlsx diff --git a/excelize_test.go b/excelize_test.go old mode 100644 new mode 100755 index d021b55ce9..f7a70d9165 --- a/excelize_test.go +++ b/excelize_test.go @@ -1208,6 +1208,19 @@ func TestSearchSheet(t *testing.T) { t.Log(xlsx.SearchSheet("Sheet1", "A")) } +func TestProtectSheet(t *testing.T) { + xlsx := NewFile() + xlsx.ProtectSheet("Sheet1", nil) + xlsx.ProtectSheet("Sheet1", &FormatSheetProtection{ + Password: "password", + EditScenarios: false, + }) + err := xlsx.SaveAs("./test/Book_protect_sheet.xlsx") + if err != nil { + t.Error(err) + } +} + func trimSliceSpace(s []string) []string { for { if len(s) > 0 && s[len(s)-1] == "" { diff --git a/lib.go b/lib.go index 8e63da9f8f..cf43dc9402 100644 --- a/lib.go +++ b/lib.go @@ -15,6 +15,8 @@ import ( "io" "log" "math" + "strconv" + "strings" "unicode" ) @@ -199,3 +201,29 @@ func namespaceStrictToTransitional(content []byte) []byte { } return content } + +// genSheetPasswd provides a method to generate password for worksheet +// protection by given plaintext. When an Excel sheet is being protected with +// a password, a 16-bit (two byte) long hash is generated. To verify a +// password, it is compared to the hash. Obviously, if the input data volume +// is great, numerous passwords will match the same hash. Here is the +// algorithm to create the hash value: +// +// take the ASCII values of all characters shift left the first character 1 bit, the second 2 bits and so on (use only the lower 15 bits and rotate all higher bits, the highest bit of the 16-bit value is always 0 [signed short]) +// XOR all these values +// XOR the count of characters +// XOR the constant 0xCE4B +func genSheetPasswd(plaintext string) string { + var password int64 = 0x0000 + var charPos uint = 1 + for _, v := range plaintext { + value := int64(v) << charPos + charPos++ + rotatedBits := value >> 15 // rotated bits beyond bit 15 + value &= 0x7fff // first 15 bits + password ^= (value | rotatedBits) + } + password ^= int64(len(plaintext)) + password ^= 0xCE4B + return strings.ToUpper(strconv.FormatInt(password, 16)) +} diff --git a/sheet.go b/sheet.go index 522d112841..8ddb8c9a18 100644 --- a/sheet.go +++ b/sheet.go @@ -711,6 +711,47 @@ func (f *File) SearchSheet(sheet, value string) []string { return result } +// ProtectSheet provides a function to prevent other users from accidentally +// or deliberately changing, moving, or deleting data in a worksheet. For +// example protect Sheet1 with protection settings: +// +// xlsx.ProtectSheet("Sheet1", &excelize.FormatSheetProtection{ +// Password: "password", +// EditScenarios: false, +// }) +// +func (f *File) ProtectSheet(sheet string, settings *FormatSheetProtection) { + xlsx := f.workSheetReader(sheet) + if settings == nil { + settings = &FormatSheetProtection{ + EditObjects: true, + EditScenarios: true, + SelectLockedCells: true, + } + } + xlsx.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, + Sheet: true, + Sort: settings.Sort, + } + if settings.Password != "" { + xlsx.SheetProtection.Password = genSheetPasswd(settings.Password) + } +} + // trimSheetName provides a function to trim invaild characters by given worksheet // name. func trimSheetName(name string) string { diff --git a/test/Book1.xlsx b/test/Book1.xlsx old mode 100755 new mode 100644 index 7d9886ef6720ebe744d11a2c4a1ac239f64fb87a..84c43d15f284fcd570060fecc9c55cea5c1a9852 GIT binary patch literal 23099 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 literal 33070 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

md2+2u7)%IvjR5RP_i!f^`Dghpi{sTy!}T!Uv~L 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 F{} Date: Sun, 4 Nov 2018 23:14:43 +0800 Subject: [PATCH 029/957] New function `UnprotectSheet()` has been added --- excelize_test.go | 14 +++++++++++++- sheet.go | 8 +++++++- 2 files changed, 20 insertions(+), 2 deletions(-) mode change 100755 => 100644 excelize_test.go diff --git a/excelize_test.go b/excelize_test.go old mode 100755 new mode 100644 index f7a70d9165..3860e175e1 --- a/excelize_test.go +++ b/excelize_test.go @@ -128,7 +128,7 @@ func TestOpenFile(t *testing.T) { } err = xlsx.Save() if err != nil { - t.Log(err) + t.Error(err) } // Test write file to not exist directory. err = xlsx.SaveAs("") @@ -1221,6 +1221,18 @@ func TestProtectSheet(t *testing.T) { } } +func TestUnprotectSheet(t *testing.T) { + xlsx, err := OpenFile("./test/Book1.xlsx") + if err != nil { + t.Error(err) + } + xlsx.UnprotectSheet("Sheet1") + err = xlsx.Save() + if err != nil { + t.Error(err) + } +} + func trimSliceSpace(s []string) []string { for { if len(s) > 0 && s[len(s)-1] == "" { diff --git a/sheet.go b/sheet.go index 8ddb8c9a18..9861d20ffe 100644 --- a/sheet.go +++ b/sheet.go @@ -713,7 +713,7 @@ func (f *File) SearchSheet(sheet, value string) []string { // ProtectSheet provides a function to prevent other users from accidentally // or deliberately changing, moving, or deleting data in a worksheet. For -// example protect Sheet1 with protection settings: +// example, protect Sheet1 with protection settings: // // xlsx.ProtectSheet("Sheet1", &excelize.FormatSheetProtection{ // Password: "password", @@ -752,6 +752,12 @@ func (f *File) ProtectSheet(sheet string, settings *FormatSheetProtection) { } } +// UnprotectSheet provides a function to unprotect an Excel worksheet. +func (f *File) UnprotectSheet(sheet string) { + xlsx := f.workSheetReader(sheet) + xlsx.SheetProtection = nil +} + // trimSheetName provides a function to trim invaild characters by given worksheet // name. func trimSheetName(name string) string { From ef334ee658e6d4d7279e3a12e292d8c8f3800f77 Mon Sep 17 00:00:00 2001 From: peiqi Date: Thu, 8 Nov 2018 11:43:29 +0800 Subject: [PATCH 030/957] fix issue #290 --- cell.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cell.go b/cell.go index 1277a18073..aa4067f739 100644 --- a/cell.go +++ b/cell.go @@ -244,12 +244,13 @@ func (f *File) GetCellFormula(sheet, axis string) string { if xlsx.SheetData.Row[k].R == row { for i := range xlsx.SheetData.Row[k].C { if axis == xlsx.SheetData.Row[k].C[i].R { + if xlsx.SheetData.Row[k].C[i].F == nil { + continue + } if xlsx.SheetData.Row[k].C[i].F.T == STCellFormulaTypeShared { return getSharedForumula(xlsx, xlsx.SheetData.Row[k].C[i].F.Si) } - if xlsx.SheetData.Row[k].C[i].F != nil { - return xlsx.SheetData.Row[k].C[i].F.Content - } + return xlsx.SheetData.Row[k].C[i].F.Content } } } From 1bb59f75ea71d88a20802f97da7158eb43d082dd Mon Sep 17 00:00:00 2001 From: taomin597715379 <597715379@qq.com> Date: Sat, 24 Nov 2018 21:27:29 +0800 Subject: [PATCH 031/957] resolve #297, fix GetSheetMap() failed Change-Id: I585a4a017867b89bd39cb6e711467a46eaa757be --- sheet.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) mode change 100644 => 100755 sheet.go diff --git a/sheet.go b/sheet.go old mode 100644 new mode 100755 index 9861d20ffe..3021946c07 --- a/sheet.go +++ b/sheet.go @@ -353,8 +353,9 @@ func (f *File) GetSheetMap() map[int]string { sheetMap := map[int]string{} for _, v := range content.Sheets.Sheet { for _, rel := range rels.Relationships { - if rel.ID == v.ID { - rID, _ := strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(rel.Target, "worksheets/sheet"), ".xml")) + relStr := strings.SplitN(rel.Target, "worksheets/sheet", 2) + if rel.ID == v.ID && len(relStr) == 2 { + rID, _ := strconv.Atoi(strings.TrimSuffix(relStr[1], ".xml")) sheetMap[rID] = v.Name } } From b89f75c8968e07f41cb7e44a4bbad493cd9e051a Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 5 Dec 2018 00:27:19 +0800 Subject: [PATCH 032/957] Add new logo for excelize --- README.md | 20 +++++++++++--------- README_zh.md | 20 +++++++++++--------- excelize.go | 2 +- excelize.png | Bin 54188 -> 62974 bytes excelize_test.go | 6 +++--- logo.png | Bin 4208 -> 5207 bytes sheet.go | 0 test/images/excel.gif | Bin 4952 -> 7221 bytes test/images/excel.jpg | Bin 3960 -> 5376 bytes test/images/excel.png | Bin 8991 -> 13233 bytes 10 files changed, 26 insertions(+), 22 deletions(-) mode change 100755 => 100644 sheet.go diff --git a/README.md b/README.md index 61ac2d6bee..e0fbc27bce 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,15 @@ -![Excelize](./excelize.png "Excelize") +

Excelize logo

-# Excelize +

+ Build Status + Code Coverage + Go Report Card + GoDoc + Licenses + Donate +

-[![Build Status](https://travis-ci.org/360EntSecGroup-Skylar/excelize.svg?branch=master)](https://travis-ci.org/360EntSecGroup-Skylar/excelize) -[![Code Coverage](https://codecov.io/gh/360EntSecGroup-Skylar/excelize/branch/master/graph/badge.svg)](https://codecov.io/gh/360EntSecGroup-Skylar/excelize) -[![Go Report Card](https://goreportcard.com/badge/github.com/360EntSecGroup-Skylar/excelize)](https://goreportcard.com/report/github.com/360EntSecGroup-Skylar/excelize) -[![GoDoc](https://godoc.org/github.com/360EntSecGroup-Skylar/excelize?status.svg)](https://godoc.org/github.com/360EntSecGroup-Skylar/excelize) -[![Licenses](https://img.shields.io/badge/license-bsd-orange.svg)](https://opensource.org/licenses/BSD-3-Clause) -[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.me/xuri) +# Excelize ## Introduction @@ -88,7 +90,7 @@ func main() { With Excelize chart generation and management is as easy as a few lines of code. You can build charts based off data in your worksheet or generate charts without any data in your worksheet at all. -![Excelize](./test/images/chart.png "Excelize") +

Excelize

```go package main diff --git a/README_zh.md b/README_zh.md index 49680d1c8c..6be0f923ed 100644 --- a/README_zh.md +++ b/README_zh.md @@ -1,13 +1,15 @@ -![Excelize](./excelize.png "Excelize") +

Excelize logo

-# Excelize +

+ Build Status + Code Coverage + Go Report Card + GoDoc + Licenses + Donate +

-[![Build Status](https://travis-ci.org/360EntSecGroup-Skylar/excelize.svg?branch=master)](https://travis-ci.org/360EntSecGroup-Skylar/excelize) -[![Code Coverage](https://codecov.io/gh/360EntSecGroup-Skylar/excelize/branch/master/graph/badge.svg)](https://codecov.io/gh/360EntSecGroup-Skylar/excelize) -[![Go Report Card](https://goreportcard.com/badge/github.com/360EntSecGroup-Skylar/excelize)](https://goreportcard.com/report/github.com/360EntSecGroup-Skylar/excelize) -[![GoDoc](https://godoc.org/github.com/360EntSecGroup-Skylar/excelize?status.svg)](https://godoc.org/github.com/360EntSecGroup-Skylar/excelize) -[![Licenses](https://img.shields.io/badge/license-bsd-orange.svg)](https://opensource.org/licenses/BSD-3-Clause) -[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.me/xuri) +# Excelize ## 简介 @@ -88,7 +90,7 @@ func main() { 使用 Excelize 生成图表十分简单,仅需几行代码。您可以根据工作表中的已有数据构建图表,或向工作表中添加数据并创建图表。 -![Excelize](./test/images/chart.png "Excelize") +

Excelize

```go package main diff --git a/excelize.go b/excelize.go index 36a6d8a968..309a8d867e 100644 --- a/excelize.go +++ b/excelize.go @@ -1,7 +1,7 @@ // Copyright 2016 - 2018 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. -// + // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original diff --git a/excelize.png b/excelize.png index 9f220b514f2d14eb9a5a39c833468b13c64621d6..5d0766ba13ab1bfc3584eaec2e9d6fc207a9d9b6 100644 GIT binary patch literal 62974 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 aD@5Cz6BvIU`f{SHkFK_nR*lB<@c#qR7Ws_; literal 54188 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 XlTV~&pgzcBL<^yOF4LM12^{@Dx20== delta 4946 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 Qe9EYt%BsA|1`!YdI|2FnqW}N^ diff --git a/test/images/excel.jpg b/test/images/excel.jpg index 49edfefc90d80c1e47b07792e9246ec7813c16b9..ed1ee1d09222100f28baf39e71691a72dc42a416 100644 GIT binary patch literal 5376 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 literal 3960 zcmZ9P2|QF^|Hto`8OspH5{WQ~@grGMmh5Etp|O^klDd+zJ?Ifs*na{#BYfsp|K0s#Ps zegTKmKs3Mzfk2=TMkth#g^_- zpNE~Do#$7_&IAAb82nFQ`zQSGtAFytE&$F9L;!tY5FB8DgTQdm;Y&b}4#)ro{bK!B zgD`@jOw1sL!zqCI&-gb(Ad%I33jo_*B2IYzz^~sDt_4!(_v*!6N!Tr>l^05Y!A6+7 z0**H79=s^^O~1{JKK=RRjstPzb?Y>t)6!W9xUk{%OrJXUqB zo~>Oii(o#GavIE(ZFn0btceDIc+-mU#>5tpFhPqTpYN z^W)FNH=h7=iM9C&ll*&tWZumO#P(Og0QbOZV2!D3DZrFP!c}LVmjcYySw|MjA{c_^ z{8s?LRVC-W$G?{PcjTah2+$Y$zpDU%zz`_opCW*Nqi_@?Y5}|-2l406pO^z6L0m4u z=W79`G-1oPs}X>D%=Z3mR82|1aT)qp{??arv(HO2Zh(4XjbLTlYR*pq07MOI%l5Nt zesAzI0z_T8%;WJsK#d46WooOQ@faNG0K`aodPBO+0Fa>QQm!#O2>_zPTK{3CbD}uu zTnu0^n2m!GbmZ3*1Dz9qGQzo-xJ422+C03c6%8yRC_flZ%T#Ky6Y>jPGW6G7LP2ojmD9ex3imBe_eP(GO$U=!{r(Pz zn!e(H8Z2$p-YJw53ypb-jLB%XKyu^73)~g?)QTd>0QUVeT&GqQ9Xd9@KsCGCvgDq(>{xD9mnMFZ z-5=XWUhnzj-1wu?=B4&cUzkXQVIKqqhx3+^a@C#5So?{Aucv~mw5K@I&-2VM>7Uis zeRw^wbYMJP99`B4-5iH)-C`+0VeVhBkW-+Ut#S(`;ZQwim8^KJta8>a)AIZ$Ryh*t zB+52o@y+%w=;<)Z=Tyk8!AHKs(|Tefm6d@L>#Rb~+4cB5YQcI6?Xykynk{P841H_rx4_^!Ved;NlHTOSazS-bcFZAUnJfFlK6@M0! z|M%CHb3a{BLX16wSmik5_fmHeB4fIno^By37(wNo+#Kp|rctly1PFxohqEzdc8eVe z4m0&?ajo;Swe5d`ADo_dcfNmZ3;xuoBJ+&S+@w^f<71p>_jZyot++QO^P-joZe6vy z(ojifCMC#yCf06mv8RY6r(>NTkT3M@E_S?Z9d4smEPp}^rE4jenic&3=@&Mb-ZTX0 zjKmMN#Xb9B^H#3ajII6J%$L_8-Khp*DGSeEV6}O0pjj5&-c`oV?3Km4KfqAckR-E1 z0RNa(fvm`?lj+qGQ_UI|l5<3aSvl?lf|^{+!E1_ zlq@cMoegse8Yb!WrQF#V>R52jlT2&3er!YW@@wF8elCPHOgei!5aCiH5j~6DYl|k! z5ig#IZ&=N45q+T(EaT*lx6-zC>BqcO+5cK5y6J`q9Xb1`wR+%o|I(=%71jC5+)(i< zohK3wKZUQ#A3q&jS7CR( zo$t0*sIfd&&;_G$2HQ3-<+NeuFF9W0awjcVqI6j z=j3#Qzf#?z=TBwDZB-Y}Jweb+Jky89cb+X#4yMSCdT$`jN~W2{QfuBh71AynH69iB zv1>aV4-QO1vldtVYZMj3V<(mqg(Utr8!e|Slv>y?@YV;SwCQl`-c3ZS{vQAC$8usmG>{rhYUeS?iSPcZV zj#|w;mS}9}P;E#^APNK?a5cxI){L{xVmr01l z!$aVQ4P(b(r$eBvN(GCw&WW!#6C(S3BNg7F|p|@qj(Npi+`C@59 zq)ypna1HW^`%XI)Lo!(JrY=9eoP^~kESzxaMaNMLc(V|-dRnR^%M>IZuLZSyjF|7u zR$E|Gzjr}WCr4{pfgHz;O>jrhLPpsd9{a?5UrO1UnT1HsJ@qQFjM9k|i=J|tjw-Ag z7|kgdE7HIo0_LU;v9)_iHqWm<61;?Dq4C)b*);kqm@!;AHbUN(A`*Dl?AVR+JaBk9 z#Y;clH;}njNpaP3dVyjYdIoXdy)J#Ibw=n0i~bUdQ#(fi$~ zDM8rssRuznHzO;ku*Roq_Fa2BMFaO;b{k#LG}1xJCk*pJ=s#bK8!WoQEN$ema{K*h z9z`xnS6|xcnKRzJQ2&zRgbr)9t&lUXqWckcChGO(^9@7Unm%Z=u1gsks$;Zzu70e# zrTe_Dmw#RwK~gHS^J+ULU}(H};}GC2SSRLBOHO@BVpg1@m^cg0Hsu|)d#GTBAUq)1 ztgZWgAELI9u_6~3wW5%4B{#$aYis>L3ALkEx8S))xaANCZZkyW$M{+MGXrL@+#omO zUzoeDXtDc34@IaVO?q zsqz6eKlAXhC|!AV%wW)f&#KtJc1~sYV$%UE*R5R}aV%{%rDUkn>(00yF-j_HRFL6Q zgQ2X(Wtmbj*+3*ZtAmF~vWMBwYM@9cH=c6pg7X^+{I)+WAGw%TjJlky6D3d~*I|#` zDn+ltMbnSVlbv6|;oa`F44C$1soZ^~e46z@sUro+H7!lud&lov#@lQeMWU9-)(@K& zG9$D9dJm+%YjB%XUGVjIf5I>N)x=4GOo6|x6-ehr>W{k_K9$-e{7k8x$+-3$FY+cN zveOWC`KD#xY>*F2vvwD-k)Kz49W8W#>~AC;lWh)ZL2*VrADwL_7q*fElS2C%TlmlW zn%sCQgfW$gOfHbM)6udVom-Y6%-4gN?UN%eT;2Bna2jH#mn_MC&!YbZuQ(~MTG=Zo zeofi8Lp#Q`X=nnP=%k%y zkQWf^ix+sC6*c<7hP%T^JtUxoB7-8{%ATuJcFL(wFx%35R~GqlrZKNak>)%#5|Zo* zzc#s(7>%ytrrA~39rueZ>pnSrKiirIFYhk|7SgQT#PTeUsGLOQCz_HP%_e;oP4>f5_DS6z6xDlsO4%hv7MZ~TZ z|5ig6s|oC;_zIN${pvWlzs;t_(rLel^K-_{F(HPq(X^!(s)P$3tJ8`vHS!JDq(q`v zT>|fp1VuEv#)V4WE03vH-+L#T z^?`6^0WDRK>C?w_^g%4qB9_F*ak`(NrpqxGUM>^A(3T-H<2m0hNg;>~P`uiODLiaF z%~V(sJA7kwpgF?tnppcsjH*ZxpVZmdgu2dIp`)7m#+pet*j&s2ByHR83koiN^L*%- zot>d-G_Oo)O6!%T<>I2(O>Navhd^;8?KNbRlN8x`@9QzX1nsx~nd}5>Bd{A>p{yz>925$fW diff --git a/test/images/excel.png b/test/images/excel.png index 52c36db8486680f0ea702174d6688b0f2ebc1235..3e4760b909051b0fcb3477a73ec89c983fdc293d 100644 GIT binary patch literal 13233 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 literal 8991 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 FV1mJ!OnU$T From 81b43da7b6b5b34f408f0d785a6c798ddd3b6124 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 5 Dec 2018 00:27:19 +0800 Subject: [PATCH 033/957] Fix comments according to best practices by effective go --- col.go | 2 +- date.go | 2 +- sheet.go | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/col.go b/col.go index 32cda12a64..8401a99290 100644 --- a/col.go +++ b/col.go @@ -337,7 +337,7 @@ func (f *File) RemoveCol(sheet, column string) { f.adjustHelper(sheet, col, -1, -1) } -// Completion column element tags of XML in a sheet. +// completeCol; Completion column element tags of XML in a sheet. func completeCol(xlsx *xlsxWorksheet, row, cell int) { buffer := bytes.Buffer{} for r := range xlsx.SheetData.Row { diff --git a/date.go b/date.go index 45f3040cf5..f52ec5945c 100644 --- a/date.go +++ b/date.go @@ -90,7 +90,7 @@ func julianDateToGregorianTime(part1, part2 float64) time.Time { return time.Date(year, time.Month(month), day, hours, minutes, seconds, nanoseconds, time.UTC) } -// By this point generations of programmers have repeated the algorithm sent +// doTheFliegelAndVanFlandernAlgorithm; By this point generations of programmers have repeated the algorithm sent // to the editor of "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 diff --git a/sheet.go b/sheet.go index 3021946c07..dbcad2b07d 100644 --- a/sheet.go +++ b/sheet.go @@ -118,7 +118,7 @@ func trimCell(column []xlsxC) []xlsxC { return col[0:i] } -// Read and update property of contents type of XLSX. +// setContentTypes; Read and update property of contents type of XLSX. func (f *File) setContentTypes(index int) { content := f.contentTypesReader() content.Overrides = append(content.Overrides, xlsxOverride{ @@ -127,7 +127,7 @@ func (f *File) setContentTypes(index int) { }) } -// Update sheet property by given index. +// setSheet; Update sheet property by given index. func (f *File) setSheet(index int, name string) { var xlsx xlsxWorksheet xlsx.Dimension.Ref = "A1" @@ -209,7 +209,7 @@ func (f *File) setAppXML() { f.saveFileList("docProps/app.xml", []byte(templateDocpropsApp)) } -// Some tools that read XLSX files have very strict requirements about the +// replaceRelationshipsNameSpaceBytes; Some tools that read XLSX files have very strict requirements about the // structure of the input XML. In particular both Numbers on the Mac and SAS // dislike inline XML namespace declarations, or namespace prefixes that don't // match the ones that Excel itself uses. This is a problem because the Go XML From 252d31b3c6adbfddbb7dcb6732ddad7ca2c87302 Mon Sep 17 00:00:00 2001 From: wcsiu Date: Tue, 11 Dec 2018 16:09:23 +0800 Subject: [PATCH 034/957] migrate to go module --- go.mod | 1 + 1 file changed, 1 insertion(+) create mode 100644 go.mod diff --git a/go.mod b/go.mod new file mode 100644 index 0000000000..05363712bc --- /dev/null +++ b/go.mod @@ -0,0 +1 @@ +module github.com/360EntSecGroup-Skylar/excelize From faa7285a4f5db13e3629360562abc8a459bad0c2 Mon Sep 17 00:00:00 2001 From: Harris Date: Thu, 13 Dec 2018 13:01:36 -0600 Subject: [PATCH 035/957] Add support to flip outline summaries This adds outlinePr support, with the summaryBelow attribute which defaults to true. Closes #304 Signed-off-by: Michael Harris --- sheetpr.go | 20 ++++++++++++++++++++ sheetpr_test.go | 8 ++++++++ xmlWorksheet.go | 7 +++++++ 3 files changed, 35 insertions(+) diff --git a/sheetpr.go b/sheetpr.go index e38b64e25c..57eebd44d6 100644 --- a/sheetpr.go +++ b/sheetpr.go @@ -31,8 +31,26 @@ type ( FitToPage bool // AutoPageBreaks is a SheetPrOption AutoPageBreaks bool + // OutlineSummaryBelow is an outlinePr, within SheetPr option + OutlineSummaryBelow bool ) +func (o OutlineSummaryBelow) setSheetPrOption(pr *xlsxSheetPr) { + if pr.OutlinePr == nil { + pr.OutlinePr = new(xlsxOutlinePr) + } + pr.OutlinePr.SummaryBelow = bool(o) +} + +func (o *OutlineSummaryBelow) getSheetPrOption(pr *xlsxSheetPr) { + // Excel default: true + if pr == nil || pr.OutlinePr == nil { + *o = true + return + } + *o = OutlineSummaryBelow(defaultTrue(&pr.OutlinePr.SummaryBelow)) +} + func (o CodeName) setSheetPrOption(pr *xlsxSheetPr) { pr.CodeName = string(o) } @@ -115,6 +133,7 @@ func (o *AutoPageBreaks) getSheetPrOption(pr *xlsxSheetPr) { // Published(bool) // FitToPage(bool) // AutoPageBreaks(bool) +// OutlineSummaryBelow(bool) func (f *File) SetSheetPrOptions(name string, opts ...SheetPrOption) error { sheet := f.workSheetReader(name) pr := sheet.SheetPr @@ -137,6 +156,7 @@ func (f *File) SetSheetPrOptions(name string, opts ...SheetPrOption) error { // Published(bool) // FitToPage(bool) // AutoPageBreaks(bool) +// OutlineSummaryBelow(bool) func (f *File) GetSheetPrOptions(name string, opts ...SheetPrOptionPtr) error { sheet := f.workSheetReader(name) pr := sheet.SheetPr diff --git a/sheetpr_test.go b/sheetpr_test.go index e7e7482e0e..d9f50590fd 100644 --- a/sheetpr_test.go +++ b/sheetpr_test.go @@ -15,6 +15,7 @@ var _ = []excelize.SheetPrOption{ excelize.Published(false), excelize.FitToPage(true), excelize.AutoPageBreaks(true), + excelize.OutlineSummaryBelow(true), } var _ = []excelize.SheetPrOptionPtr{ @@ -23,6 +24,7 @@ var _ = []excelize.SheetPrOptionPtr{ (*excelize.Published)(nil), (*excelize.FitToPage)(nil), (*excelize.AutoPageBreaks)(nil), + (*excelize.OutlineSummaryBelow)(nil), } func ExampleFile_SetSheetPrOptions() { @@ -35,6 +37,7 @@ func ExampleFile_SetSheetPrOptions() { excelize.Published(false), excelize.FitToPage(true), excelize.AutoPageBreaks(true), + excelize.OutlineSummaryBelow(false), ); err != nil { panic(err) } @@ -51,6 +54,7 @@ func ExampleFile_GetSheetPrOptions() { published excelize.Published fitToPage excelize.FitToPage autoPageBreaks excelize.AutoPageBreaks + outlineSummaryBelow excelize.OutlineSummaryBelow ) if err := xl.GetSheetPrOptions(sheet, @@ -59,6 +63,7 @@ func ExampleFile_GetSheetPrOptions() { &published, &fitToPage, &autoPageBreaks, + &outlineSummaryBelow, ); err != nil { panic(err) } @@ -68,6 +73,7 @@ func ExampleFile_GetSheetPrOptions() { fmt.Println("- published:", published) fmt.Println("- fitToPage:", fitToPage) fmt.Println("- autoPageBreaks:", autoPageBreaks) + fmt.Println("- outlineSummaryBelow:", outlineSummaryBelow) // Output: // Defaults: // - codeName: "" @@ -75,6 +81,7 @@ func ExampleFile_GetSheetPrOptions() { // - published: true // - fitToPage: false // - autoPageBreaks: false + // - outlineSummaryBelow: true } func TestSheetPrOptions(t *testing.T) { @@ -88,6 +95,7 @@ func TestSheetPrOptions(t *testing.T) { {new(excelize.Published), excelize.Published(false)}, {new(excelize.FitToPage), excelize.FitToPage(true)}, {new(excelize.AutoPageBreaks), excelize.AutoPageBreaks(true)}, + {new(excelize.OutlineSummaryBelow), excelize.OutlineSummaryBelow(false)}, } { opt := test.nonDefault t.Logf("option %T", opt) diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 42f8ddb7a5..d35b40e0f8 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -211,6 +211,13 @@ type xlsxSheetPr struct { TransitionEntry bool `xml:"transitionEntry,attr,omitempty"` TabColor *xlsxTabColor `xml:"tabColor,omitempty"` PageSetUpPr *xlsxPageSetUpPr `xml:"pageSetUpPr,omitempty"` + OutlinePr *xlsxOutlinePr `xml:"outlinePr,omitempty"` +} + +// xlsxOutlinePr maps to the outlinePr element +// SummaryBelow allows you to adjust the direction of grouper controls +type xlsxOutlinePr struct { + SummaryBelow bool `xml:"summaryBelow,attr"` } // xlsxPageSetUpPr directly maps the pageSetupPr element in the namespace From e728ff14981bda19a18a830e416290cecc5a269e Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 15 Dec 2018 00:08:55 +0800 Subject: [PATCH 036/957] Fixes #308, refactor `NewSheet()`, `DeleteSheet()`, `SetActiveSheet()` and `GetActiveSheetIndex()` --- col.go | 3 +- date.go | 13 ++++--- logo.png | Bin 5207 -> 7085 bytes sheet.go | 95 +++++++++++++++++++++++------------------------- test/Book1.xlsx | Bin 23099 -> 23153 bytes xmlWorkbook.go | 2 +- 6 files changed, 55 insertions(+), 58 deletions(-) diff --git a/col.go b/col.go index 8401a99290..05405bd16e 100644 --- a/col.go +++ b/col.go @@ -337,7 +337,8 @@ func (f *File) RemoveCol(sheet, column string) { f.adjustHelper(sheet, col, -1, -1) } -// completeCol; Completion column element tags of XML in a sheet. +// completeCol provieds function to completion column element tags of XML in a +// sheet. func completeCol(xlsx *xlsxWorksheet, row, cell int) { buffer := bytes.Buffer{} for r := range xlsx.SheetData.Row { diff --git a/date.go b/date.go index f52ec5945c..a0b8cebe8a 100644 --- a/date.go +++ b/date.go @@ -90,12 +90,13 @@ func julianDateToGregorianTime(part1, part2 float64) time.Time { return time.Date(year, time.Month(month), day, hours, minutes, seconds, nanoseconds, time.UTC) } -// doTheFliegelAndVanFlandernAlgorithm; By this point generations of programmers have repeated the algorithm sent -// to the editor of "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 expand an explanation here - that day is not today. +// doTheFliegelAndVanFlandernAlgorithm; By this point generations of +// programmers have repeated the algorithm sent to the editor of +// "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 +// expand an explanation here - that day is not today. func doTheFliegelAndVanFlandernAlgorithm(jd int) (day, month, year int) { l := jd + 68569 n := (4 * l) / 146097 diff --git a/logo.png b/logo.png index df9059d18ad8a44e31eb72653af98547a6258ad5..32235b8aeb985020036bb3f29e1aa9a3a4c39aa5 100644 GIT binary patch literal 7085 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 z sheetID { + sheetID = v.SheetID + } + } + sheetID++ // Update docProps/app.xml f.setAppXML() // Update [Content_Types].xml - f.setContentTypes(f.SheetCount) + f.setContentTypes(sheetID) // Create new sheet /xl/worksheets/sheet%d.xml - f.setSheet(f.SheetCount, name) + f.setSheet(sheetID, name) // Update xl/_rels/workbook.xml.rels - rID := f.addXlsxWorkbookRels(f.SheetCount) + rID := f.addXlsxWorkbookRels(sheetID) // Update xl/workbook.xml - f.setWorkbook(name, rID) - return f.SheetCount + f.setWorkbook(name, sheetID, rID) + return sheetID } // contentTypesReader provides a function to get the pointer to the @@ -118,7 +127,8 @@ func trimCell(column []xlsxC) []xlsxC { return col[0:i] } -// setContentTypes; Read and update property of contents type of XLSX. +// setContentTypes provides a function to read and update property of contents +// type of XLSX. func (f *File) setContentTypes(index int) { content := f.contentTypesReader() content.Overrides = append(content.Overrides, xlsxOverride{ @@ -127,7 +137,7 @@ func (f *File) setContentTypes(index int) { }) } -// setSheet; Update sheet property by given index. +// setSheet provides a function to update sheet property by given index. func (f *File) setSheet(index int, name string) { var xlsx xlsxWorksheet xlsx.Dimension.Ref = "A1" @@ -141,19 +151,11 @@ func (f *File) setSheet(index int, name string) { // setWorkbook update workbook property of XLSX. Maximum 31 characters are // allowed in sheet title. -func (f *File) setWorkbook(name string, rid int) { +func (f *File) setWorkbook(name string, sheetID, rid int) { content := f.workbookReader() - rID := 0 - for _, v := range content.Sheets.Sheet { - t, _ := strconv.Atoi(v.SheetID) - if t > rID { - rID = t - } - } - rID++ content.Sheets.Sheet = append(content.Sheets.Sheet, xlsxSheet{ Name: trimSheetName(name), - SheetID: strconv.Itoa(rID), + SheetID: sheetID, ID: "rId" + strconv.Itoa(rid), }) } @@ -209,13 +211,13 @@ func (f *File) setAppXML() { f.saveFileList("docProps/app.xml", []byte(templateDocpropsApp)) } -// replaceRelationshipsNameSpaceBytes; Some tools that read XLSX files have very strict requirements about the -// structure of the input XML. In particular both Numbers on the Mac and SAS -// dislike inline XML namespace declarations, or namespace prefixes that don't -// match the ones that Excel itself uses. This is a problem because the Go XML -// library doesn't multiple namespace declarations in a single element of a -// document. This function is a horrible hack to fix that after the XML -// marshalling is completed. +// replaceRelationshipsNameSpaceBytes; Some tools that read XLSX files have +// very strict requirements about the structure of the input XML. In +// particular both Numbers on the Mac and SAS dislike inline XML namespace +// declarations, or namespace prefixes that don't match the ones that Excel +// itself uses. This is a problem because the Go XML library doesn't multiple +// namespace declarations in a single element of a document. This function is +// a horrible hack to fix that after the XML marshalling is completed. func replaceRelationshipsNameSpaceBytes(workbookMarshal []byte) []byte { oldXmlns := []byte(``) newXmlns := []byte(``) @@ -230,18 +232,23 @@ func (f *File) SetActiveSheet(index int) { if index < 1 { index = 1 } - index-- - content := f.workbookReader() - if len(content.BookViews.WorkBookView) > 0 { - content.BookViews.WorkBookView[0].ActiveTab = index - } else { - content.BookViews.WorkBookView = append(content.BookViews.WorkBookView, xlsxWorkBookView{ - ActiveTab: index, - }) + wb := f.workbookReader() + for activeTab, sheet := range wb.Sheets.Sheet { + if sheet.SheetID == index { + if len(wb.BookViews.WorkBookView) > 0 { + wb.BookViews.WorkBookView[0].ActiveTab = activeTab + } else { + wb.BookViews.WorkBookView = append(wb.BookViews.WorkBookView, xlsxWorkBookView{ + ActiveTab: activeTab, + }) + } + } } - index++ for idx, name := range f.GetSheetMap() { xlsx := f.workSheetReader(name) + if len(xlsx.SheetViews.SheetView) > 0 { + xlsx.SheetViews.SheetView[0].TabSelected = false + } if index == idx { if len(xlsx.SheetViews.SheetView) > 0 { xlsx.SheetViews.SheetView[0].TabSelected = true @@ -250,10 +257,6 @@ func (f *File) SetActiveSheet(index int) { TabSelected: true, }) } - } else { - if len(xlsx.SheetViews.SheetView) > 0 { - xlsx.SheetViews.SheetView[0].TabSelected = false - } } } } @@ -261,21 +264,13 @@ func (f *File) SetActiveSheet(index int) { // GetActiveSheetIndex provides a function to get active sheet index of the // XLSX. If not found the active sheet will be return integer 0. func (f *File) GetActiveSheetIndex() int { - buffer := bytes.Buffer{} - content := f.workbookReader() - for _, v := range content.Sheets.Sheet { - xlsx := xlsxWorksheet{} - buffer.WriteString("xl/worksheets/sheet") - buffer.WriteString(strings.TrimPrefix(v.ID, "rId")) - buffer.WriteString(".xml") - _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML(buffer.String())), &xlsx) + for idx, name := range f.GetSheetMap() { + xlsx := f.workSheetReader(name) for _, sheetView := range xlsx.SheetViews.SheetView { if sheetView.TabSelected { - ID, _ := strconv.Atoi(strings.TrimPrefix(v.ID, "rId")) - return ID + return idx } } - buffer.Reset() } return 0 } @@ -404,8 +399,8 @@ func (f *File) DeleteSheet(name string) { for k, v := range content.Sheets.Sheet { if v.Name == trimSheetName(name) && len(content.Sheets.Sheet) > 1 { content.Sheets.Sheet = append(content.Sheets.Sheet[:k], content.Sheets.Sheet[k+1:]...) - sheet := "xl/worksheets/sheet" + strings.TrimPrefix(v.ID, "rId") + ".xml" - rels := "xl/worksheets/_rels/sheet" + strings.TrimPrefix(v.ID, "rId") + ".xml.rels" + sheet := "xl/worksheets/sheet" + strconv.Itoa(v.SheetID) + ".xml" + rels := "xl/worksheets/_rels/sheet" + strconv.Itoa(v.SheetID) + ".xml.rels" target := f.deleteSheetFromWorkbookRels(v.ID) f.deleteSheetFromContentTypes(target) delete(f.sheetMap, name) diff --git a/test/Book1.xlsx b/test/Book1.xlsx index 84c43d15f284fcd570060fecc9c55cea5c1a9852..2ef11210d6cfb5bda5194c91342ae2f750efbe7d 100644 GIT binary patch literal 23153 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 literal 23099 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 diff --git a/xmlWorkbook.go b/xmlWorkbook.go index f00a0b8584..65720337ae 100644 --- a/xmlWorkbook.go +++ b/xmlWorkbook.go @@ -151,7 +151,7 @@ type xlsxSheets struct { // not checked it for completeness - it does as much as I need. type xlsxSheet struct { Name string `xml:"name,attr,omitempty"` - SheetID string `xml:"sheetId,attr,omitempty"` + SheetID int `xml:"sheetId,attr,omitempty"` ID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr,omitempty"` State string `xml:"state,attr,omitempty"` } From 90221bd98fab0ce85a177d955d79a964bb4b66b2 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 18 Dec 2018 21:50:07 +0800 Subject: [PATCH 037/957] Fixes #310, support set and get TopLeftCell properties of sheet view options --- sheetview.go | 10 ++++++++++ sheetview_test.go | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/sheetview.go b/sheetview.go index e76325c558..37a0c393c8 100644 --- a/sheetview.go +++ b/sheetview.go @@ -35,6 +35,8 @@ type ( ShowRowColHeaders bool // ZoomScale is a SheetViewOption. ZoomScale float64 + // TopLeftCell is a SheetViewOption. + TopLeftCell string /* TODO // ShowWhiteSpace is a SheetViewOption. ShowWhiteSpace bool @@ -47,6 +49,14 @@ type ( // Defaults for each option are described in XML schema for CT_SheetView +func (o TopLeftCell) setSheetViewOption(view *xlsxSheetView) { + view.TopLeftCell = string(o) +} + +func (o *TopLeftCell) getSheetViewOption(view *xlsxSheetView) { + *o = TopLeftCell(string(view.TopLeftCell)) +} + func (o DefaultGridColor) setSheetViewOption(view *xlsxSheetView) { view.DefaultGridColor = boolPtr(bool(o)) } diff --git a/sheetview_test.go b/sheetview_test.go index c58090675f..ee81d5b55c 100644 --- a/sheetview_test.go +++ b/sheetview_test.go @@ -13,12 +13,14 @@ var _ = []excelize.SheetViewOption{ excelize.ShowFormulas(false), excelize.ShowGridLines(true), excelize.ShowRowColHeaders(true), + excelize.TopLeftCell("B2"), // SheetViewOptionPtr are also SheetViewOption new(excelize.DefaultGridColor), new(excelize.RightToLeft), new(excelize.ShowFormulas), new(excelize.ShowGridLines), new(excelize.ShowRowColHeaders), + new(excelize.TopLeftCell), } var _ = []excelize.SheetViewOptionPtr{ @@ -27,6 +29,7 @@ var _ = []excelize.SheetViewOptionPtr{ (*excelize.ShowFormulas)(nil), (*excelize.ShowGridLines)(nil), (*excelize.ShowRowColHeaders)(nil), + (*excelize.TopLeftCell)(nil), } func ExampleFile_SetSheetViewOptions() { @@ -40,6 +43,7 @@ func ExampleFile_SetSheetViewOptions() { excelize.ShowGridLines(true), excelize.ShowRowColHeaders(true), excelize.ZoomScale(80), + excelize.TopLeftCell("C3"), ); err != nil { panic(err) } @@ -91,6 +95,7 @@ func ExampleFile_GetSheetViewOptions() { showGridLines excelize.ShowGridLines showRowColHeaders excelize.ShowRowColHeaders zoomScale excelize.ZoomScale + topLeftCell excelize.TopLeftCell ) if err := xl.GetSheetViewOptions(sheet, 0, @@ -100,6 +105,7 @@ func ExampleFile_GetSheetViewOptions() { &showGridLines, &showRowColHeaders, &zoomScale, + &topLeftCell, ); err != nil { panic(err) } @@ -111,6 +117,15 @@ func ExampleFile_GetSheetViewOptions() { fmt.Println("- showGridLines:", showGridLines) fmt.Println("- showRowColHeaders:", showRowColHeaders) fmt.Println("- zoomScale:", zoomScale) + fmt.Println("- topLeftCell:", `"`+topLeftCell+`"`) + + if err := xl.SetSheetViewOptions(sheet, 0, excelize.TopLeftCell("B2")); err != nil { + panic(err) + } + + if err := xl.GetSheetViewOptions(sheet, 0, &topLeftCell); err != nil { + panic(err) + } if err := xl.SetSheetViewOptions(sheet, 0, excelize.ShowGridLines(false)); err != nil { panic(err) @@ -122,6 +137,7 @@ func ExampleFile_GetSheetViewOptions() { fmt.Println("After change:") fmt.Println("- showGridLines:", showGridLines) + fmt.Println("- topLeftCell:", topLeftCell) // Output: // Default: @@ -131,8 +147,10 @@ func ExampleFile_GetSheetViewOptions() { // - showGridLines: true // - showRowColHeaders: true // - zoomScale: 0 + // - topLeftCell: "" // After change: // - showGridLines: false + // - topLeftCell: B2 } func TestSheetViewOptionsErrors(t *testing.T) { From 3012df08eb81756704a8e3f47fff59d0a6bf693b Mon Sep 17 00:00:00 2001 From: sairoutine Date: Wed, 19 Dec 2018 20:54:38 +0900 Subject: [PATCH 038/957] Add GetMergeCells --- excelize.go | 40 +++++++++++++++++++++++++++++++++ excelize_test.go | 53 ++++++++++++++++++++++++++++++++++++++++++++ test/MergeCell.xlsx | Bin 0 -> 8583 bytes 3 files changed, 93 insertions(+) create mode 100644 test/MergeCell.xlsx diff --git a/excelize.go b/excelize.go index 0b530ab219..013f35798d 100644 --- a/excelize.go +++ b/excelize.go @@ -401,3 +401,43 @@ func (f *File) adjustAutoFilterHelper(xlsx *xlsxWorksheet, column, rowIndex, off } } } + +// GetMergeCells provides a function to get all merged cells from a worksheet currently. +func (f *File) GetMergeCells(sheet string) []MergeCell { + mergeCells := []MergeCell{} + + xlsx := f.workSheetReader(sheet) + if xlsx.MergeCells != nil { + for i := 0; i < len(xlsx.MergeCells.Cells); i++ { + ref := xlsx.MergeCells.Cells[i].Ref + axis := strings.Split(ref, ":")[0] + mergeCells = append(mergeCells, []string{ref, f.GetCellValue(sheet, axis)}) + } + } + + return mergeCells +} + +// MergeCell define a merged cell data. +// It consists of the following structure. +// example: []string{"D4:E10", "cell value"} +type MergeCell []string + +// GetCellValue returns merged cell value. +func (m *MergeCell) GetCellValue() string { + return (*m)[1] +} + +// GetStartAxis returns the merge start axis. +// example: "C2" +func (m *MergeCell) GetStartAxis() string { + axis := strings.Split((*m)[0], ":") + return axis[0] +} + +// GetEndAxis returns the merge end axis. +// example: "D4" +func (m *MergeCell) GetEndAxis() string { + axis := strings.Split((*m)[0], ":") + return axis[1] +} diff --git a/excelize_test.go b/excelize_test.go index c9a87d073d..f55ac01b61 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -351,6 +351,59 @@ func TestMergeCell(t *testing.T) { } } +func TestGetMergeCells(t *testing.T) { + wants := []struct { + value string + start string + end string + }{ + { + value: "A1", + start: "A1", + end: "B1", + }, + { + value: "A2", + start: "A2", + end: "A3", + }, + { + value: "A4", + start: "A4", + end: "B5", + }, + { + value: "A7", + start: "A7", + end: "C10", + }, + } + + xlsx, err := OpenFile("./test/MergeCell.xlsx") + if err != nil { + t.Error(err) + } + + mergeCells := xlsx.GetMergeCells("Sheet1") + if len(mergeCells) != len(wants) { + t.Fatalf("Expected count of merge cells %d, but got %d\n", len(wants), len(mergeCells)) + } + + for i, m := range mergeCells { + if wants[i].value != m.GetCellValue() { + t.Fatalf("Expected merged cell value %s, but got %s\n", wants[i].value, m.GetCellValue()) + } + + if wants[i].start != m.GetStartAxis() { + t.Fatalf("Expected merged cell value %s, but got %s\n", wants[i].start, m.GetStartAxis()) + } + + if wants[i].end != m.GetEndAxis() { + t.Fatalf("Expected merged cell value %s, but got %s\n", wants[i].end, m.GetEndAxis()) + } + } +} + func TestSetCellStyleAlignment(t *testing.T) { xlsx, err := OpenFile("./test/Book2.xlsx") if err != nil { diff --git a/test/MergeCell.xlsx b/test/MergeCell.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..d4dad181d620ae074eab39329943b700e795ea06 GIT binary patch literal 8583 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 Date: Fri, 21 Dec 2018 14:20:22 +1300 Subject: [PATCH 039/957] CodeLingo setup Signed-off-by: CodeLingo Bot --- CONTRIBUTING.md | 89 +++++++++ CONTRIBUTING_TEMPLATE.md | 384 +++++++++++++++++++++++++++++++++++++++ codelingo.yaml | 3 + 3 files changed, 476 insertions(+) create mode 100644 CONTRIBUTING_TEMPLATE.md create mode 100644 codelingo.yaml diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5239a94704..f4382ec03a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,3 +1,4 @@ + # Contributing to excelize Want to hack on excelize? Awesome! This page contains information about reporting issues as well as some tips and @@ -373,3 +374,91 @@ 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 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). + + +### Package Comment +Every package should have a package comment, a block comment preceding the package clause. +For multi-file packages, the package comment only needs to be present in one file, and any one will do. +The package comment should introduce the package and provide information relevant to the package as a +whole. It will appear first on the godoc page and should set up the detailed documentation that follows. + + +### Single Method Interface Name +By convention, one-method interfaces are named by the method name plus an -er suffix +or similar modification to construct an agent noun: Reader, Writer, Formatter, CloseNotifier etc. + +There are a number of such names and it's productive to honor them and the function names they capture. +Read, Write, Close, Flush, String and so on have canonical signatures and meanings. To avoid confusion, +don't give your method one of those names unless it has the same signature and meaning. Conversely, +if your type implements a method with the same meaning as a method on a well-known type, give it the +same name and signature; call your string-converter method String not ToString. + + +### Avoid Annotations in Comments +Comments do not need extra formatting such as banners of stars. The generated output +may not even be presented in a fixed-width font, so don't depend on spacing for alignment—godoc, +like gofmt, takes care of that. The comments are uninterpreted plain text, so HTML and other +annotations such as _this_ will reproduce verbatim and should not be used. One adjustment godoc +does do is to display indented text in a fixed-width font, suitable for program snippets. +The package comment for the fmt package uses this to good effect. + + +### Comment First Word as Subject +Doc comments work best as complete sentences, which allow a wide variety of automated presentations. +The first sentence should be a one-sentence summary that starts with the name being declared. + + +### Good Package Name +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 +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 +collisions a priori. The package name is only the default name for +imports; it need not be unique across all source code, and in the +rare case of a collision the importing package can choose a different +name to use locally. In any case, confusion is rare because the file +name in the import determines just which package is being used. + + +### Avoid Renaming Imports +Avoid renaming imports except to avoid a name collision; good package names +should not require renaming. In the event of collision, prefer to rename the +most local or project-specific import. + + +### Context as First Argument +Values of the context.Context type carry security credentials, tracing information, +deadlines, and cancellation signals across API and process boundaries. Go programs +pass Contexts explicitly along the entire function call chain from incoming RPCs +and HTTP requests to outgoing requests. + +Most functions that use a Context should accept it as their first parameter. + + +### Do Not Discard Errors +Do not discard errors using _ variables. If a function returns an error, +check it to make sure the function succeeded. Handle the error, return it, or, +in truly exceptional situations, panic. + + +### Go Error Format +Error strings should not be capitalized (unless beginning with proper nouns +or acronyms) or end with punctuation, since they are usually printed following +other context. That is, use fmt.Errorf("something bad") not fmt.Errorf("Something bad"), +so that log.Printf("Reading %s: %v", filename, err) formats without a spurious +capital letter mid-message. This does not apply to logging, which is implicitly +line-oriented and not combined inside other messages. + + +### Use Crypto Rand +Do not use package math/rand to generate keys, even +throwaway ones. Unseeded, the generator is completely predictable. +Seeded with time.Nanoseconds(), there are just a few bits of entropy. +Instead, use crypto/rand's Reader, and if you need text, print to +hexadecimal or base64 + diff --git a/CONTRIBUTING_TEMPLATE.md b/CONTRIBUTING_TEMPLATE.md new file mode 100644 index 0000000000..389f2433a2 --- /dev/null +++ b/CONTRIBUTING_TEMPLATE.md @@ -0,0 +1,384 @@ + +# Contributing to excelize + +Want to hack on excelize? Awesome! This page contains information about reporting issues as well as some tips and +guidelines useful to experienced open source contributors. Finally, make sure +you read our [community guidelines](#community-guidelines) before you +start participating. + +## Topics + +* [Reporting Security Issues](#reporting-security-issues) +* [Design and Cleanup Proposals](#design-and-cleanup-proposals) +* [Reporting Issues](#reporting-other-issues) +* [Quick Contribution Tips and Guidelines](#quick-contribution-tips-and-guidelines) +* [Community Guidelines](#community-guidelines) + +## Reporting security issues + +The excelize maintainers take security seriously. If you discover a security +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. +We currently do not offer a paid security bounty program, but are not +ruling it out in the future. + +## Reporting other issues + +A great way to contribute to the project is to send a detailed report when you +encounter an issue. We always appreciate a well-written, thorough bug report, +and will thank you for it! + +Check that [our issue database](https://github.com/360EntSecGroup-Skylar/excelize/issues) +doesn't already include that problem or suggestion before submitting an issue. +If you find a match, you can use the "subscribe" button to get notified on +updates. Do *not* leave random "+1" or "I have this too" comments, as they +only clutter the discussion, and don't help resolving it. However, if you +have ways to reproduce the issue or have additional information that may help +resolving the issue, please leave a comment. + +When reporting issues, always include the output of `go env`. + +Also include the steps required to reproduce the problem if possible and +applicable. This information will help us review and fix your issue faster. +When sending lengthy log-files, consider posting them as a gist [https://gist.github.com](https://gist.github.com). +Don't forget to remove sensitive data from your logfiles before posting (you can +replace those parts with "REDACTED"). + +## Quick contribution tips and guidelines + +This section gives the experienced contributor some tips and guidelines. + +### Pull requests are always welcome + +Not sure if that typo is worth a pull request? Found a bug and know how to fix +it? Do it! We will appreciate it. Any significant improvement should be +documented as [a GitHub issue](https://github.com/360EntSecGroup-Skylar/excelize/issues) before +anybody starts working on it. + +We are always thrilled to receive pull requests. We do our best to process them +quickly. If your pull request is not accepted on the first try, +don't get discouraged! + +### Design and cleanup proposals + +You can propose new designs for existing excelize features. You can also design +entirely new features. We really appreciate contributors who want to refactor or +otherwise cleanup our project. + +We try hard to keep excelize lean and focused. Excelize can't do everything for +everybody. This means that we might decide against incorporating a new feature. +However, there might be a way to implement that feature *on top of* excelize. + +### Conventions + +Fork the repository and make changes on your fork in a feature branch: + +* If it's a bug fix branch, name it XXXX-something where XXXX is the number of + the issue. +* If it's a feature branch, create an enhancement issue to announce + your intentions, and name it XXXX-something where XXXX is the number of the + issue. + +Submit unit tests for your changes. Go has a great test framework built in; use +it! Take a look at existing tests for inspiration. Run the full test on your branch before +submitting a pull request. + +Update the documentation when creating or modifying features. Test your +documentation changes for clarity, concision, and correctness, as well as a +clean documentation build. + +Write clean code. Universally formatted code promotes ease of writing, reading, +and maintenance. Always run `gofmt -s -w file.go` on each changed file before +committing your changes. Most editors have plug-ins that do this automatically. + +Pull request descriptions should be as clear as possible and include a reference +to all the issues that they address. + +### Successful Changes + +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 +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 +solutions. + +Typically, the best methods of accomplishing this are to submit an issue, +stating the problem. This issue can include a problem statement and a +checklist with requirements. If solutions are proposed, alternatives should be +listed and eliminated. Even if the criteria for elimination of a solution is +frivolous, say so. + +Larger changes typically work best with design documents. These are focused on +providing context to the design at the time the feature was conceived and can +inform future documentation contributions. + +### Commit Messages + +Commit messages must start with a capitalized and short summary +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 +required. They should tell the story of the change and provide readers +understanding of what led to it. + +In practice, the best approach to maintaining a nice commit message is to +leverage a `git add -p` and `git commit --amend` to formulate a solid +changeset. This allows one to piece together a change, as information becomes +available. + +If you squash a series of commits, don't just submit that. Re-write the commit +message, as if the series of commits was a single stroke of brilliance. + +That said, there is no requirement to have a single commit for a PR, as long as +each commit tells the story. For example, if there is a feature that requires a +package, it might make sense to have the package in a separate commit then have +a subsequent commit that uses it. + +Remember, you're telling part of the story with the commit message. Don't make +your chapter weird. + +### Review + +Code review comments may be added to your pull request. Discuss, then make the +suggested modifications and push additional commits to your feature branch. Post +a comment after pushing. New commits show up in the pull request automatically, +but the reviewers are notified only when you comment. + +Pull requests must be cleanly rebased on top of master without multiple branches +mixed into the PR. + +**Git tip**: If your PR no longer merges cleanly, use `rebase master` in your +feature branch to update your pull request rather than `merge master`. + +Before you make a pull request, squash your commits into logical units of work +using `git rebase -i` and `git push -f`. A logical unit of work is a consistent +set of patches that should be reviewed together: for example, upgrading the +version of a vendored dependency and taking advantage of its now available new +feature constitute two separate units of work. Implementing a new function and +calling it in another file constitute a single logical unit of work. The very +high majority of submissions should have a single commit, so if in doubt: squash +down to one. + +After every commit, make sure the test passes. Include documentation +changes in the same pull request so that a revert would remove all traces of +the feature or fix. + +Include an issue reference like `Closes #XXXX` or `Fixes #XXXX` in commits that +close an issue. Including references automatically closes the issue on a merge. + +Please see the [Coding Style](#coding-style) for further guidelines. + +### Merge approval + +The excelize maintainers use LGTM (Looks Good To Me) in comments on the code review to +indicate acceptance. + +### Sign your work + +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/)): + +```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: + +(a) The contribution was created in whole or in part by me and I + have the right to submit it under the open source license + indicated in the file; or + +(b) The contribution is based upon previous work that, to the best + of my knowledge, is covered under an appropriate open source + license and I have the right under that license to submit that + work with modifications, whether created in whole or in part + by me, under the same open source license (unless I am + permitted to submit under a different license), as indicated + in the file; or + +(c) The contribution was provided directly to me by some other + person who certified (a), (b) or (c) and I have not modified + it. + +(d) I understand and agree that this project and the contribution + are public and that a record of the contribution (including all + personal information I submit with it, including my sign-off) is + maintained indefinitely and may be redistributed consistent with + this project or the open source license(s) involved. +``` + +Then you just add a line to every git commit message: + + Signed-off-by: Ri Xu https://xuri.me + +Use your real name (sorry, no pseudonyms or anonymous contributions.) + +If you set your `user.name` and `user.email` git configs, you can sign your +commit automatically with `git commit -s`. + +### How can I become a maintainer + +First, all maintainers have 3 things + +* They share responsibility in the project's success. +* They have made a long-term, recurring time investment to improve the project. +* They spend that time doing whatever needs to be done, not necessarily what + is the most interesting or fun. + +Maintainers are often under-appreciated, because their work is harder to appreciate. +It's easy to appreciate a really cool and technically advanced feature. It's harder +to appreciate the absence of bugs, the slow but steady improvement in stability, +or the reliability of a release process. But those things distinguish a good +project from a great one. + +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. + +## Community guidelines + +We want to keep the community awesome, growing and collaborative. We need +your help to keep it that way. To help with this we've come up with some general +guidelines for the community as a whole: + +* Be nice: Be courteous, respectful and polite to fellow community members: + no regional, racial, gender, or other abuse will be tolerated. We like + nice people way better than mean ones! + +* Encourage diversity and participation: Make everyone in our community feel + welcome, regardless of their background and the extent of their + contributions, and do everything possible to encourage participation in + our community. + +* Keep it legal: Basically, don't get us in trouble. Share only content that + you own, do not share private or sensitive information, and don't break + the law. + +* Stay on topic: Make sure that you are posting to the correct channel and + avoid off-topic discussions. Remember when you update an issue or respond + to an email you are potentially sending to a large number of people. Please + consider this before you update. Also remember that nobody likes spam. + +* Don't send email to the maintainers: There's no need to send email to the + maintainers to ask them to investigate an issue or to take a look at a + pull request. Instead of sending an email, GitHub mentions should be + used to ping maintainers to review a pull request, a proposal or an + issue. + +### Guideline violations — 3 strikes method + +The point of this section is not to find opportunities to punish people, but we +do need a fair way to deal with people who are making our community suck. + +1. First occurrence: We'll give you a friendly, but public reminder that the + behavior is inappropriate according to our guidelines. + +2. Second occurrence: We will send you a private message with a warning that + any additional violations will result in removal from the community. + +3. Third occurrence: Depending on the violation, we may need to delete or ban + your account. + +**Notes:** + +* Obvious spammers are banned on first occurrence. If we don't do this, we'll + have spam all over the place. + +* Violations are forgiven after 6 months of good behavior, and we won't hold a + grudge. + +* People who commit minor infractions will get some education, rather than + hammering them in the 3 strikes process. + +* The rules apply equally to everyone in the community, no matter how much + you've contributed. + +* Extreme violations of a threatening, abusive, destructive or illegal nature + will be addressed immediately and are not subject to 3 strikes or forgiveness. + +* Contact [xuri.me](https://xuri.me) to report abuse or appeal violations. In the case of + appeals, we know that mistakes happen, and we'll work with you to come up with a + fair solution if there has been a misunderstanding. + +## Coding Style + +Unless explicitly stated, we follow all coding guidelines from the Go +community. While some of these standards may seem arbitrary, they somehow seem +to result in a solid, consistent codebase. + +It is possible that the code base does not currently comply with these +guidelines. We are not looking for a massive PR that fixes this, since that +goes against the spirit of the guidelines. All new contributions should make a +best effort to clean up and make the code base better than they left it. +Obviously, apply your best judgement. Remember, the goal here is to make the +code base easier for humans to navigate and understand. Always keep that in +mind when nudging others to comply. + +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). +3. All code should follow the guidelines covered in [Effective + Go](http://golang.org/doc/effective_go.html) 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 + expectations, caveats and anything else that may be important. If a type + gets exported, having the comments already there will ensure it's ready. +6. Variable name length should be proportional to its context and no longer. + `noCommaALongVariableNameLikeThisIsNotMoreClearWhenASimpleCommentWouldDo`. + In practice, short methods will have short variable names and globals will + have longer names. +7. No underscores in package names. If you need a compound name, step back, + and re-examine why you need a compound name. If you still think you need a + compound name, lose the underscore. +8. No utils or helpers packages. If a function is not general enough to + warrant its own package, it has not been written generally enough to be a + part of a util package. Just leave it unexported and well-documented. +9. All tests should run with `go test` and outside tooling should not be + required. No, we don't need another unit testing framework. Assertion + packages are acceptable if they provide _real_ incremental value. +10. Even though we call these "rules" above, they are actually just + 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 +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). + +{{range .}} +### {{.title}} +{{.body}} +{{end}} \ No newline at end of file diff --git a/codelingo.yaml b/codelingo.yaml new file mode 100644 index 0000000000..dfe344b471 --- /dev/null +++ b/codelingo.yaml @@ -0,0 +1,3 @@ +tenets: + - import: codelingo/effective-go + - import: codelingo/code-review-comments From b04107c4a374dfc0a625d1513b0e5cb1d907260b Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 23 Dec 2018 00:07:47 +0800 Subject: [PATCH 040/957] Resolve #311, create 2D/3D area, stacked area, 100% stacked area chart support --- chart.go | 473 +++++++++++++++++++++++++++++------------------ excelize_test.go | 7 + xmlChart.go | 2 + 3 files changed, 303 insertions(+), 179 deletions(-) diff --git a/chart.go b/chart.go index edfcab7f31..fb7b60d901 100644 --- a/chart.go +++ b/chart.go @@ -18,112 +18,142 @@ import ( // This section defines the currently supported chart types. const ( - Bar = "bar" - BarStacked = "barStacked" - BarPercentStacked = "barPercentStacked" - Bar3DClustered = "bar3DClustered" - Bar3DStacked = "bar3DStacked" - Bar3DPercentStacked = "bar3DPercentStacked" - Col = "col" - ColStacked = "colStacked" - ColPercentStacked = "colPercentStacked" - Col3DClustered = "col3DClustered" - Col3D = "col3D" - Col3DStacked = "col3DStacked" - Col3DPercentStacked = "col3DPercentStacked" - Doughnut = "doughnut" - Line = "line" - Pie = "pie" - Pie3D = "pie3D" - Radar = "radar" - Scatter = "scatter" + Area = "area" + AreaStacked = "areaStacked" + AreaPercentStacked = "areaPercentStacked" + Area3D = "area3D" + Area3DStacked = "area3DStacked" + Area3DPercentStacked = "area3DPercentStacked" + Bar = "bar" + BarStacked = "barStacked" + BarPercentStacked = "barPercentStacked" + Bar3DClustered = "bar3DClustered" + Bar3DStacked = "bar3DStacked" + Bar3DPercentStacked = "bar3DPercentStacked" + Col = "col" + ColStacked = "colStacked" + ColPercentStacked = "colPercentStacked" + Col3DClustered = "col3DClustered" + Col3D = "col3D" + Col3DStacked = "col3DStacked" + Col3DPercentStacked = "col3DPercentStacked" + Doughnut = "doughnut" + Line = "line" + Pie = "pie" + Pie3D = "pie3D" + Radar = "radar" + Scatter = "scatter" ) // This section defines the default value of chart properties. var ( chartView3DRotX = map[string]int{ - Bar: 0, - BarStacked: 0, - BarPercentStacked: 0, - Bar3DClustered: 15, - Bar3DStacked: 15, - Bar3DPercentStacked: 15, - Col: 0, - ColStacked: 0, - ColPercentStacked: 0, - Col3DClustered: 15, - Col3D: 15, - Col3DStacked: 15, - Col3DPercentStacked: 15, - Doughnut: 0, - Line: 0, - Pie: 0, - Pie3D: 30, - Radar: 0, - Scatter: 0, + Area: 0, + AreaStacked: 0, + AreaPercentStacked: 0, + Area3D: 15, + Area3DStacked: 15, + Area3DPercentStacked: 15, + Bar: 0, + BarStacked: 0, + BarPercentStacked: 0, + Bar3DClustered: 15, + Bar3DStacked: 15, + Bar3DPercentStacked: 15, + Col: 0, + ColStacked: 0, + ColPercentStacked: 0, + Col3DClustered: 15, + Col3D: 15, + Col3DStacked: 15, + Col3DPercentStacked: 15, + Doughnut: 0, + Line: 0, + Pie: 0, + Pie3D: 30, + Radar: 0, + Scatter: 0, } chartView3DRotY = map[string]int{ - Bar: 0, - BarStacked: 0, - BarPercentStacked: 0, - Bar3DClustered: 20, - Bar3DStacked: 20, - Bar3DPercentStacked: 20, - Col: 0, - ColStacked: 0, - ColPercentStacked: 0, - Col3DClustered: 20, - Col3D: 20, - Col3DStacked: 20, - Col3DPercentStacked: 20, - Doughnut: 0, - Line: 0, - Pie: 0, - Pie3D: 0, - Radar: 0, - Scatter: 0, + Area: 0, + AreaStacked: 0, + AreaPercentStacked: 0, + Area3D: 20, + Area3DStacked: 20, + Area3DPercentStacked: 20, + Bar: 0, + BarStacked: 0, + BarPercentStacked: 0, + Bar3DClustered: 20, + Bar3DStacked: 20, + Bar3DPercentStacked: 20, + Col: 0, + ColStacked: 0, + ColPercentStacked: 0, + Col3DClustered: 20, + Col3D: 20, + Col3DStacked: 20, + Col3DPercentStacked: 20, + Doughnut: 0, + Line: 0, + Pie: 0, + Pie3D: 0, + Radar: 0, + Scatter: 0, } chartView3DDepthPercent = map[string]int{ - Bar: 100, - BarStacked: 100, - BarPercentStacked: 100, - Bar3DClustered: 100, - Bar3DStacked: 100, - Bar3DPercentStacked: 100, - Col: 100, - ColStacked: 100, - ColPercentStacked: 100, - Col3DClustered: 100, - Col3D: 100, - Col3DStacked: 100, - Col3DPercentStacked: 100, - Doughnut: 100, - Line: 100, - Pie: 100, - Pie3D: 100, - Radar: 100, - Scatter: 100, + Area: 100, + AreaStacked: 100, + AreaPercentStacked: 100, + Area3D: 100, + Area3DStacked: 100, + Area3DPercentStacked: 100, + Bar: 100, + BarStacked: 100, + BarPercentStacked: 100, + Bar3DClustered: 100, + Bar3DStacked: 100, + Bar3DPercentStacked: 100, + Col: 100, + ColStacked: 100, + ColPercentStacked: 100, + Col3DClustered: 100, + Col3D: 100, + Col3DStacked: 100, + Col3DPercentStacked: 100, + Doughnut: 100, + Line: 100, + Pie: 100, + Pie3D: 100, + Radar: 100, + Scatter: 100, } chartView3DRAngAx = map[string]int{ - Bar: 0, - BarStacked: 0, - BarPercentStacked: 0, - Bar3DClustered: 1, - Bar3DStacked: 1, - Bar3DPercentStacked: 1, - Col: 0, - ColStacked: 0, - ColPercentStacked: 0, - Col3DClustered: 1, - Col3D: 1, - Col3DStacked: 1, - Col3DPercentStacked: 1, - Doughnut: 0, - Line: 0, - Pie: 0, - Pie3D: 0, - Radar: 0, - Scatter: 0, + Area: 0, + AreaStacked: 0, + AreaPercentStacked: 0, + Area3D: 1, + Area3DStacked: 1, + Area3DPercentStacked: 1, + Bar: 0, + BarStacked: 0, + BarPercentStacked: 0, + Bar3DClustered: 1, + Bar3DStacked: 1, + Bar3DPercentStacked: 1, + Col: 0, + ColStacked: 0, + ColPercentStacked: 0, + Col3DClustered: 1, + Col3D: 1, + Col3DStacked: 1, + Col3DPercentStacked: 1, + Doughnut: 0, + Line: 0, + Pie: 0, + Pie3D: 0, + Radar: 0, + Scatter: 0, } chartLegendPosition = map[string]string{ "bottom": "b", @@ -133,41 +163,80 @@ var ( "top_right": "tr", } chartValAxNumFmtFormatCode = map[string]string{ - Bar: "General", - BarStacked: "General", - BarPercentStacked: "0%", - Bar3DClustered: "General", - Bar3DStacked: "General", - Bar3DPercentStacked: "0%", - Col: "General", - ColStacked: "General", - ColPercentStacked: "0%", - Col3DClustered: "General", - Col3D: "General", - Col3DStacked: "General", - Col3DPercentStacked: "0%", - Doughnut: "General", - Line: "General", - Pie: "General", - Pie3D: "General", - Radar: "General", - Scatter: "General", + Area: "General", + AreaStacked: "General", + AreaPercentStacked: "0%", + Area3D: "General", + Area3DStacked: "General", + Area3DPercentStacked: "0%", + Bar: "General", + BarStacked: "General", + BarPercentStacked: "0%", + Bar3DClustered: "General", + Bar3DStacked: "General", + Bar3DPercentStacked: "0%", + Col: "General", + ColStacked: "General", + ColPercentStacked: "0%", + Col3DClustered: "General", + Col3D: "General", + Col3DStacked: "General", + Col3DPercentStacked: "0%", + Doughnut: "General", + Line: "General", + Pie: "General", + Pie3D: "General", + Radar: "General", + Scatter: "General", + } + chartValAxCrossBetween = map[string]string{ + Area: "midCat", + AreaStacked: "midCat", + AreaPercentStacked: "midCat", + Area3D: "midCat", + Area3DStacked: "midCat", + Area3DPercentStacked: "midCat", + Bar: "between", + BarStacked: "between", + BarPercentStacked: "between", + Bar3DClustered: "between", + Bar3DStacked: "between", + Bar3DPercentStacked: "between", + Col: "between", + ColStacked: "between", + ColPercentStacked: "between", + Col3DClustered: "between", + Col3D: "between", + Col3DStacked: "between", + Col3DPercentStacked: "between", + Doughnut: "between", + Line: "between", + Pie: "between", + Pie3D: "between", + Radar: "between", + Scatter: "between", } plotAreaChartGrouping = map[string]string{ - Bar: "clustered", - BarStacked: "stacked", - BarPercentStacked: "percentStacked", - Bar3DClustered: "clustered", - Bar3DStacked: "stacked", - Bar3DPercentStacked: "percentStacked", - Col: "clustered", - ColStacked: "stacked", - ColPercentStacked: "percentStacked", - Col3DClustered: "clustered", - Col3D: "standard", - Col3DStacked: "stacked", - Col3DPercentStacked: "percentStacked", - Line: "standard", + Area: "standard", + AreaStacked: "stacked", + AreaPercentStacked: "percentStacked", + Area3D: "standard", + Area3DStacked: "stacked", + Area3DPercentStacked: "percentStacked", + Bar: "clustered", + BarStacked: "stacked", + BarPercentStacked: "percentStacked", + Bar3DClustered: "clustered", + Bar3DStacked: "stacked", + Bar3DPercentStacked: "percentStacked", + Col: "clustered", + ColStacked: "stacked", + ColPercentStacked: "percentStacked", + Col3DClustered: "clustered", + Col3D: "standard", + Col3DStacked: "stacked", + Col3DPercentStacked: "percentStacked", + Line: "standard", } plotAreaChartBarDir = map[string]string{ Bar: "bar", @@ -262,27 +331,33 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // // The following shows the type of chart supported by excelize: // -// Type | 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 -// 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 -// doughnut | doughnut chart -// line | line chart -// pie | pie chart -// pie3D | 3D pie chart -// radar | radar chart -// scatter | scatter 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 +// 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 +// doughnut | doughnut chart +// line | line chart +// pie | pie chart +// pie3D | 3D pie chart +// radar | radar chart +// scatter | scatter chart // // In Excel a chart series is a collection of information that defines which data is plotted such as values, axis labels and formatting. // @@ -546,25 +621,31 @@ func (f *File) addChart(formatSet *formatChart) { }, } plotAreaFunc := map[string]func(*formatChart) *cPlotArea{ - Bar: f.drawBaseChart, - BarStacked: f.drawBaseChart, - BarPercentStacked: f.drawBaseChart, - Bar3DClustered: f.drawBaseChart, - Bar3DStacked: f.drawBaseChart, - Bar3DPercentStacked: f.drawBaseChart, - Col: f.drawBaseChart, - ColStacked: f.drawBaseChart, - ColPercentStacked: f.drawBaseChart, - Col3DClustered: f.drawBaseChart, - Col3D: f.drawBaseChart, - Col3DStacked: f.drawBaseChart, - Col3DPercentStacked: f.drawBaseChart, - Doughnut: f.drawDoughnutChart, - Line: f.drawLineChart, - Pie3D: f.drawPie3DChart, - Pie: f.drawPieChart, - Radar: f.drawRadarChart, - Scatter: f.drawScatterChart, + Area: f.drawBaseChart, + AreaStacked: f.drawBaseChart, + AreaPercentStacked: f.drawBaseChart, + Area3D: f.drawBaseChart, + Area3DStacked: f.drawBaseChart, + Area3DPercentStacked: f.drawBaseChart, + Bar: f.drawBaseChart, + BarStacked: f.drawBaseChart, + BarPercentStacked: f.drawBaseChart, + Bar3DClustered: f.drawBaseChart, + Bar3DStacked: f.drawBaseChart, + Bar3DPercentStacked: f.drawBaseChart, + Col: f.drawBaseChart, + ColStacked: f.drawBaseChart, + ColPercentStacked: f.drawBaseChart, + Col3DClustered: f.drawBaseChart, + Col3D: f.drawBaseChart, + Col3DStacked: f.drawBaseChart, + Col3DPercentStacked: f.drawBaseChart, + Doughnut: f.drawDoughnutChart, + Line: f.drawLineChart, + Pie3D: f.drawPie3DChart, + Pie: f.drawPieChart, + Radar: f.drawRadarChart, + Scatter: f.drawScatterChart, } xlsxChartSpace.Chart.PlotArea = plotAreaFunc[formatSet.Type](formatSet) @@ -593,14 +674,48 @@ func (f *File) drawBaseChart(formatSet *formatChart) *cPlotArea { {Val: 753999904}, }, } - c.BarDir.Val = plotAreaChartBarDir[formatSet.Type] + var ok bool + c.BarDir.Val, ok = plotAreaChartBarDir[formatSet.Type] + if !ok { + c.BarDir = nil + } c.Grouping.Val = plotAreaChartGrouping[formatSet.Type] - if formatSet.Type == "colStacked" || formatSet.Type == "barStacked" || formatSet.Type == "barPercentStacked" || formatSet.Type == "colPercentStacked" { + if formatSet.Type == "colStacked" || formatSet.Type == "barStacked" || formatSet.Type == "barPercentStacked" || formatSet.Type == "colPercentStacked" || formatSet.Type == "areaPercentStacked" { c.Overlap = &attrValInt{Val: 100} } catAx := f.drawPlotAreaCatAx(formatSet) valAx := f.drawPlotAreaValAx(formatSet) charts := map[string]*cPlotArea{ + "area": { + AreaChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "areaStacked": { + AreaChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "areaPercentStacked": { + AreaChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "area3D": { + Area3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "area3DStacked": { + Area3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "area3DPercentStacked": { + Area3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, "bar": { BarChart: &c, CatAx: catAx, @@ -826,7 +941,7 @@ func (f *File) drawChartSeriesSpPr(i int, formatSet *formatChart) *cSpPr { }, }, } - chartSeriesSpPr := map[string]*cSpPr{Bar: nil, BarStacked: nil, BarPercentStacked: nil, Bar3DClustered: nil, Bar3DStacked: nil, Bar3DPercentStacked: nil, Col: nil, ColStacked: nil, ColPercentStacked: nil, Col3DClustered: nil, Col3D: nil, Col3DStacked: nil, Col3DPercentStacked: nil, Doughnut: nil, Line: spPrLine, Pie: nil, Pie3D: nil, Radar: nil, Scatter: spPrScatter} + chartSeriesSpPr := map[string]*cSpPr{Area: nil, AreaStacked: nil, AreaPercentStacked: nil, Area3D: nil, Area3DStacked: nil, Area3DPercentStacked: nil, Bar: nil, BarStacked: nil, BarPercentStacked: nil, Bar3DClustered: nil, Bar3DStacked: nil, Bar3DPercentStacked: nil, Col: nil, ColStacked: nil, ColPercentStacked: nil, Col3DClustered: nil, Col3D: nil, Col3DStacked: nil, Col3DPercentStacked: nil, Doughnut: nil, Line: spPrLine, Pie: nil, Pie3D: nil, Radar: nil, Scatter: spPrScatter} return chartSeriesSpPr[formatSet.Type] } @@ -855,7 +970,7 @@ func (f *File) drawChartSeriesDPt(i int, formatSet *formatChart) []*cDPt { }, }, }} - chartSeriesDPt := map[string][]*cDPt{Bar: nil, BarStacked: nil, BarPercentStacked: nil, Bar3DClustered: nil, Bar3DStacked: nil, Bar3DPercentStacked: nil, Col: nil, ColStacked: nil, ColPercentStacked: nil, Col3DClustered: nil, Col3D: nil, Col3DStacked: nil, Col3DPercentStacked: nil, Doughnut: nil, Line: nil, Pie: dpt, Pie3D: dpt, Radar: nil, Scatter: nil} + chartSeriesDPt := map[string][]*cDPt{Area: nil, AreaStacked: nil, AreaPercentStacked: nil, Area3D: nil, Area3DStacked: nil, Area3DPercentStacked: nil, Bar: nil, BarStacked: nil, BarPercentStacked: nil, Bar3DClustered: nil, Bar3DStacked: nil, Bar3DPercentStacked: nil, Col: nil, ColStacked: nil, ColPercentStacked: nil, Col3DClustered: nil, Col3D: nil, Col3DStacked: nil, Col3DPercentStacked: nil, Doughnut: nil, Line: nil, Pie: dpt, Pie3D: dpt, Radar: nil, Scatter: nil} return chartSeriesDPt[formatSet.Type] } @@ -867,7 +982,7 @@ func (f *File) drawChartSeriesCat(v formatChartSeries, formatSet *formatChart) * F: v.Categories, }, } - chartSeriesCat := map[string]*cCat{Bar: cat, BarStacked: cat, BarPercentStacked: cat, Bar3DClustered: cat, Bar3DStacked: cat, Bar3DPercentStacked: cat, Col: cat, ColStacked: cat, ColPercentStacked: cat, Col3DClustered: cat, Col3D: cat, Col3DStacked: cat, Col3DPercentStacked: cat, Doughnut: cat, Line: cat, Pie: cat, Pie3D: cat, Radar: cat, Scatter: nil} + chartSeriesCat := map[string]*cCat{Area: cat, AreaStacked: cat, AreaPercentStacked: cat, Area3D: cat, Area3DStacked: cat, Area3DPercentStacked: cat, Bar: cat, BarStacked: cat, BarPercentStacked: cat, Bar3DClustered: cat, Bar3DStacked: cat, Bar3DPercentStacked: cat, Col: cat, ColStacked: cat, ColPercentStacked: cat, Col3DClustered: cat, Col3D: cat, Col3DStacked: cat, Col3DPercentStacked: cat, Doughnut: cat, Line: cat, Pie: cat, Pie3D: cat, Radar: cat, Scatter: nil} return chartSeriesCat[formatSet.Type] } @@ -879,7 +994,7 @@ func (f *File) drawChartSeriesVal(v formatChartSeries, formatSet *formatChart) * F: v.Values, }, } - chartSeriesVal := map[string]*cVal{Bar: val, BarStacked: val, BarPercentStacked: val, Bar3DClustered: val, Bar3DStacked: val, Bar3DPercentStacked: val, Col: val, ColStacked: val, ColPercentStacked: val, Col3DClustered: val, Col3D: val, Col3DStacked: val, Col3DPercentStacked: val, Doughnut: val, Line: val, Pie: val, Pie3D: val, Radar: val, Scatter: nil} + chartSeriesVal := map[string]*cVal{Area: val, AreaStacked: val, AreaPercentStacked: val, Area3D: val, Area3DStacked: val, Area3DPercentStacked: val, Bar: val, BarStacked: val, BarPercentStacked: val, Bar3DClustered: val, Bar3DStacked: val, Bar3DPercentStacked: val, Col: val, ColStacked: val, ColPercentStacked: val, Col3DClustered: val, Col3D: val, Col3DStacked: val, Col3DPercentStacked: val, Doughnut: val, Line: val, Pie: val, Pie3D: val, Radar: val, Scatter: nil} return chartSeriesVal[formatSet.Type] } @@ -905,7 +1020,7 @@ func (f *File) drawChartSeriesMarker(i int, formatSet *formatChart) *cMarker { }, }, } - chartSeriesMarker := map[string]*cMarker{Bar: nil, BarStacked: nil, BarPercentStacked: nil, Bar3DClustered: nil, Bar3DStacked: nil, Bar3DPercentStacked: nil, Col: nil, ColStacked: nil, ColPercentStacked: nil, Col3DClustered: nil, Col3D: nil, Col3DStacked: nil, Col3DPercentStacked: nil, Doughnut: nil, Line: nil, Pie: nil, Pie3D: nil, Radar: nil, Scatter: marker} + chartSeriesMarker := map[string]*cMarker{Area: nil, AreaStacked: nil, AreaPercentStacked: nil, Area3D: nil, Area3DStacked: nil, Area3DPercentStacked: nil, Bar: nil, BarStacked: nil, BarPercentStacked: nil, Bar3DClustered: nil, Bar3DStacked: nil, Bar3DPercentStacked: nil, Col: nil, ColStacked: nil, ColPercentStacked: nil, Col3DClustered: nil, Col3D: nil, Col3DStacked: nil, Col3DPercentStacked: nil, Doughnut: nil, Line: nil, Pie: nil, Pie3D: nil, Radar: nil, Scatter: marker} return chartSeriesMarker[formatSet.Type] } @@ -917,7 +1032,7 @@ func (f *File) drawChartSeriesXVal(v formatChartSeries, formatSet *formatChart) F: v.Categories, }, } - chartSeriesXVal := map[string]*cCat{Bar: nil, BarStacked: nil, BarPercentStacked: nil, Bar3DClustered: nil, Bar3DStacked: nil, Bar3DPercentStacked: nil, Col: nil, ColStacked: nil, ColPercentStacked: nil, Col3DClustered: nil, Col3D: nil, Col3DStacked: nil, Col3DPercentStacked: nil, Doughnut: nil, Line: nil, Pie: nil, Pie3D: nil, Radar: nil, Scatter: cat} + chartSeriesXVal := map[string]*cCat{Area: nil, AreaStacked: nil, AreaPercentStacked: nil, Area3D: nil, Area3DStacked: nil, Area3DPercentStacked: nil, Bar: nil, BarStacked: nil, BarPercentStacked: nil, Bar3DClustered: nil, Bar3DStacked: nil, Bar3DPercentStacked: nil, Col: nil, ColStacked: nil, ColPercentStacked: nil, Col3DClustered: nil, Col3D: nil, Col3DStacked: nil, Col3DPercentStacked: nil, Doughnut: nil, Line: nil, Pie: nil, Pie3D: nil, Radar: nil, Scatter: cat} return chartSeriesXVal[formatSet.Type] } @@ -929,7 +1044,7 @@ func (f *File) drawChartSeriesYVal(v formatChartSeries, formatSet *formatChart) F: v.Values, }, } - chartSeriesYVal := map[string]*cVal{Bar: nil, BarStacked: nil, BarPercentStacked: nil, Bar3DClustered: nil, Bar3DStacked: nil, Bar3DPercentStacked: nil, Col: nil, ColStacked: nil, ColPercentStacked: nil, Col3DClustered: nil, Col3D: nil, Col3DStacked: nil, Col3DPercentStacked: nil, Doughnut: nil, Line: nil, Pie: nil, Pie3D: nil, Radar: nil, Scatter: val} + chartSeriesYVal := map[string]*cVal{Area: nil, AreaStacked: nil, AreaPercentStacked: nil, Area3D: nil, Area3DStacked: nil, Area3DPercentStacked: nil, Bar: nil, BarStacked: nil, BarPercentStacked: nil, Bar3DClustered: nil, Bar3DStacked: nil, Bar3DPercentStacked: nil, Col: nil, ColStacked: nil, ColPercentStacked: nil, Col3DClustered: nil, Col3D: nil, Col3DStacked: nil, Col3DPercentStacked: nil, Doughnut: nil, Line: nil, Pie: nil, Pie3D: nil, Radar: nil, Scatter: val} return chartSeriesYVal[formatSet.Type] } @@ -951,7 +1066,7 @@ func (f *File) drawChartDLbls(formatSet *formatChart) *cDLbls { // given format sets. func (f *File) drawChartSeriesDLbls(formatSet *formatChart) *cDLbls { dLbls := f.drawChartDLbls(formatSet) - chartSeriesDLbls := map[string]*cDLbls{Bar: dLbls, BarStacked: dLbls, BarPercentStacked: dLbls, Bar3DClustered: dLbls, Bar3DStacked: dLbls, Bar3DPercentStacked: dLbls, Col: dLbls, ColStacked: dLbls, ColPercentStacked: dLbls, Col3DClustered: dLbls, Col3D: dLbls, Col3DStacked: dLbls, Col3DPercentStacked: dLbls, Doughnut: dLbls, Line: dLbls, Pie: dLbls, Pie3D: dLbls, Radar: dLbls, Scatter: nil} + chartSeriesDLbls := map[string]*cDLbls{Area: dLbls, AreaStacked: dLbls, AreaPercentStacked: dLbls, Area3D: dLbls, Area3DStacked: dLbls, Area3DPercentStacked: dLbls, Bar: dLbls, BarStacked: dLbls, BarPercentStacked: dLbls, Bar3DClustered: dLbls, Bar3DStacked: dLbls, Bar3DPercentStacked: dLbls, Col: dLbls, ColStacked: dLbls, ColPercentStacked: dLbls, Col3DClustered: dLbls, Col3D: dLbls, Col3DStacked: dLbls, Col3DPercentStacked: dLbls, Doughnut: dLbls, Line: dLbls, Pie: dLbls, Pie3D: dLbls, Radar: dLbls, Scatter: nil} return chartSeriesDLbls[formatSet.Type] } @@ -1025,7 +1140,7 @@ func (f *File) drawPlotAreaValAx(formatSet *formatChart) []*cAxs { TxPr: f.drawPlotAreaTxPr(), CrossAx: &attrValInt{Val: 754001152}, Crosses: &attrValString{Val: "autoZero"}, - CrossBetween: &attrValString{Val: "between"}, + CrossBetween: &attrValString{Val: chartValAxCrossBetween[formatSet.Type]}, }, } } diff --git a/excelize_test.go b/excelize_test.go index 6eb36926e8..d082a5c354 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -959,6 +959,13 @@ func TestAddChart(t *testing.T) { xlsx.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"}],"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":"Fruit 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"}`) xlsx.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"}],"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":"Fruit 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}}`) xlsx.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"}],"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":"Fruit 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}}`) + // area series charts + xlsx.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"}],"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":"Fruit 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"}`) + xlsx.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"}],"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":"Fruit 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"}`) + xlsx.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"}],"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":"Fruit 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"}`) + xlsx.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"}],"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":"Fruit 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"}`) + xlsx.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"}],"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":"Fruit 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"}`) + xlsx.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"}],"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":"Fruit 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"}`) // Save xlsx file by the given path. err = xlsx.SaveAs("./test/Book_addchart.xlsx") if err != nil { diff --git a/xmlChart.go b/xmlChart.go index 3271cbbbf6..2f9b8d96d7 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -301,6 +301,8 @@ type cView3D struct { // plot area of the chart. type cPlotArea struct { Layout *string `xml:"c:layout"` + AreaChart *cCharts `xml:"c:areaChart"` + Area3DChart *cCharts `xml:"c:area3DChart"` BarChart *cCharts `xml:"c:barChart"` Bar3DChart *cCharts `xml:"c:bar3DChart"` DoughnutChart *cCharts `xml:"c:doughnutChart"` From 9b8baf75ad7613dba24635b4c66791e404c6b7d5 Mon Sep 17 00:00:00 2001 From: r-uchino <46125593+r-uchino@users.noreply.github.com> Date: Wed, 26 Dec 2018 14:30:59 +0900 Subject: [PATCH 041/957] Add RegSearchSheet (#316) --- excelize_test.go | 13 ++++++++++++ sheet.go | 52 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/excelize_test.go b/excelize_test.go index d082a5c354..199505834f 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -1268,6 +1268,19 @@ func TestSearchSheet(t *testing.T) { t.Log(xlsx.SearchSheet("Sheet1", "A")) } +func TestRegSearchSheet(t *testing.T) { + xlsx, err := OpenFile("./test/Book1.xlsx") + if err != nil { + t.Error(err) + return + } + t.Log(xlsx.SearchSheet("Sheet1", "[0-9]")) + // Test search in a not exists worksheet. + t.Log(xlsx.SearchSheet("Sheet4", "")) + // Test search a not exists value. + t.Log(xlsx.SearchSheet("Sheet1", "")) +} + func TestProtectSheet(t *testing.T) { xlsx := NewFile() xlsx.ProtectSheet("Sheet1", nil) diff --git a/sheet.go b/sheet.go index f54656141d..cf05fe99a1 100644 --- a/sheet.go +++ b/sheet.go @@ -18,6 +18,7 @@ import ( "io/ioutil" "os" "path" + "regexp" "strconv" "strings" "unicode/utf8" @@ -707,6 +708,57 @@ func (f *File) SearchSheet(sheet, value string) []string { return result } +// RegSearchSheet provides the ability to retrieve coordinates +// with the given worksheet name and regular expression +// For a merged cell, get the coordinates +// of the upper left corner of the merge area. +// :example) +// Search the coordinates where the numerical value in the range of "0-9" of Sheet 1 is described: +// +// xlsx.RegSearchSheet("Sheet1", "[0-9]") +// +func (f *File) RegSearchSheet(sheet, value string) []string { + xlsx := f.workSheetReader(sheet) + result := []string{} + name, ok := f.sheetMap[trimSheetName(sheet)] + if !ok { + return result + } + if xlsx != nil { + output, _ := xml.Marshal(f.Sheet[name]) + f.saveFileList(name, replaceWorkSheetsRelationshipsNameSpaceBytes(output)) + } + xml.NewDecoder(bytes.NewReader(f.readXML(name))) + d := f.sharedStringsReader() + var inElement string + var r xlsxRow + decoder := xml.NewDecoder(bytes.NewReader(f.readXML(name))) + for { + token, _ := decoder.Token() + if token == nil { + break + } + switch startElement := token.(type) { + case xml.StartElement: + inElement = startElement.Name.Local + if inElement == "row" { + r = xlsxRow{} + _ = decoder.DecodeElement(&r, &startElement) + for _, colCell := range r.C { + val, _ := colCell.getValueFrom(f, d) + regex := regexp.MustCompile(value) + if !regex.MatchString(val) { + continue + } + result = append(result, fmt.Sprintf("%s%d", strings.Map(letterOnlyMapF, colCell.R), r.R)) + } + } + default: + } + } + return result +} + // ProtectSheet provides a function to prevent other users from accidentally // or deliberately changing, moving, or deleting data in a worksheet. For // example, protect Sheet1 with protection settings: From 7b7ca99f5d570c30f7eee92c38c5e632b7815239 Mon Sep 17 00:00:00 2001 From: Veniamin Albaev Date: Wed, 26 Dec 2018 08:33:40 +0300 Subject: [PATCH 042/957] Duplicate row (#317) * go mod tidy applied * File.DuplicateRow() method added --- excelize.go | 17 ++++++------ excelize_test.go | 67 ++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 7 +++++ go.sum | 8 ++++++ rows.go | 42 +++++++++++++++++++++++++++++- 5 files changed, 132 insertions(+), 9 deletions(-) create mode 100644 go.sum diff --git a/excelize.go b/excelize.go index 4b4aa32184..b162b79910 100644 --- a/excelize.go +++ b/excelize.go @@ -238,18 +238,19 @@ func (f *File) adjustRowDimensions(xlsx *xlsxWorksheet, rowIndex, offset int) { } for i, r := range xlsx.SheetData.Row { if r.R >= rowIndex { - xlsx.SheetData.Row[i].R += offset - for k, v := range xlsx.SheetData.Row[i].C { - axis := v.R - col := string(strings.Map(letterOnlyMapF, axis)) - row, _ := strconv.Atoi(strings.Map(intOnlyMapF, axis)) - xAxis := row + offset - xlsx.SheetData.Row[i].C[k].R = col + strconv.Itoa(xAxis) - } + f.ajustSingleRowDimensions(&xlsx.SheetData.Row[i], offset) } } } +func (f *File) ajustSingleRowDimensions(r *xlsxRow, offset int) { + r.R += offset + for i, col := range r.C { + row, _ := strconv.Atoi(strings.Map(intOnlyMapF, col.R)) + r.C[i].R = string(strings.Map(letterOnlyMapF, col.R)) + strconv.Itoa(row+offset) + } +} + // adjustHyperlinks provides a function to update hyperlinks when inserting or // deleting rows or columns. func (f *File) adjustHyperlinks(sheet string, column, rowIndex, offset int) { diff --git a/excelize_test.go b/excelize_test.go index 199505834f..8c19a3e9d4 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -12,6 +12,8 @@ import ( "strings" "testing" "time" + + "github.com/stretchr/testify/assert" ) func TestOpenFile(t *testing.T) { @@ -1029,6 +1031,71 @@ func TestInsertRow(t *testing.T) { } } +func TestDuplicateRow(t *testing.T) { + const ( + file = "./test/Book_DuplicateRow_%s.xlsx" + sheet = "Sheet1" + a1 = "A1" + b1 = "B1" + a2 = "A2" + b2 = "B2" + a3 = "A3" + b3 = "B3" + a4 = "A4" + b4 = "B4" + a1Value = "A1 value" + a2Value = "A2 value" + a3Value = "A3 value" + bnValue = "Bn value" + ) + xlsx := NewFile() + xlsx.SetCellStr(sheet, a1, a1Value) + xlsx.SetCellStr(sheet, b1, bnValue) + + t.Run("FromSingleRow", func(t *testing.T) { + xlsx.DuplicateRow(sheet, 1) + xlsx.DuplicateRow(sheet, 2) + + if assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(file, "SignleRow"))) { + assert.Equal(t, a1Value, xlsx.GetCellValue(sheet, a1)) + assert.Equal(t, a1Value, xlsx.GetCellValue(sheet, a2)) + assert.Equal(t, a1Value, xlsx.GetCellValue(sheet, a3)) + assert.Equal(t, bnValue, xlsx.GetCellValue(sheet, b1)) + assert.Equal(t, bnValue, xlsx.GetCellValue(sheet, b2)) + assert.Equal(t, bnValue, xlsx.GetCellValue(sheet, b3)) + } + }) + + t.Run("UpdateDuplicatedRows", func(t *testing.T) { + xlsx.SetCellStr(sheet, a2, a2Value) + xlsx.SetCellStr(sheet, a3, a3Value) + + if assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(file, "Updated"))) { + assert.Equal(t, a1Value, xlsx.GetCellValue(sheet, a1)) + assert.Equal(t, a2Value, xlsx.GetCellValue(sheet, a2)) + assert.Equal(t, a3Value, xlsx.GetCellValue(sheet, a3)) + assert.Equal(t, bnValue, xlsx.GetCellValue(sheet, b1)) + assert.Equal(t, bnValue, xlsx.GetCellValue(sheet, b2)) + assert.Equal(t, bnValue, xlsx.GetCellValue(sheet, b3)) + } + }) + + t.Run("FromFirstOfMultipleRows", func(t *testing.T) { + xlsx.DuplicateRow(sheet, 1) + + if assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(file, "FirstOfMultipleRows"))) { + assert.Equal(t, a1Value, xlsx.GetCellValue(sheet, a1)) + assert.Equal(t, a1Value, xlsx.GetCellValue(sheet, a2)) + assert.Equal(t, a2Value, xlsx.GetCellValue(sheet, a3)) + assert.Equal(t, a3Value, xlsx.GetCellValue(sheet, a4)) + assert.Equal(t, bnValue, xlsx.GetCellValue(sheet, b1)) + assert.Equal(t, bnValue, xlsx.GetCellValue(sheet, b2)) + assert.Equal(t, bnValue, xlsx.GetCellValue(sheet, b3)) + assert.Equal(t, bnValue, xlsx.GetCellValue(sheet, b4)) + } + }) +} + func TestSetPane(t *testing.T) { xlsx := NewFile() xlsx.SetPanes("Sheet1", `{"freeze":false,"split":false}`) diff --git a/go.mod b/go.mod index 05363712bc..8db2fe6c2f 100644 --- a/go.mod +++ b/go.mod @@ -1 +1,8 @@ module github.com/360EntSecGroup-Skylar/excelize + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/testify v1.2.2 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000000..ca5f759f66 --- /dev/null +++ b/go.sum @@ -0,0 +1,8 @@ +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/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= diff --git a/rows.go b/rows.go index b6336331df..def150d1df 100644 --- a/rows.go +++ b/rows.go @@ -359,7 +359,7 @@ func (f *File) RemoveRow(sheet string, row int) { } } -// InsertRow provides a function to insert a new row before given row index. +// InsertRow provides a function to insert a new row after given row index. // For example, create a new row before row 3 in Sheet1: // // xlsx.InsertRow("Sheet1", 2) @@ -372,6 +372,46 @@ func (f *File) InsertRow(sheet string, row int) { f.adjustHelper(sheet, -1, row, 1) } +// DuplicateRow inserts a copy of specified row below specified +// +// xlsx.DuplicateRow("Sheet1", 2) +// +func (f *File) DuplicateRow(sheet string, row int) { + if row < 0 { + return + } + row2 := row + 1 + f.adjustHelper(sheet, -1, row2, 1) + + xlsx := f.workSheetReader(sheet) + idx := -1 + idx2 := -1 + + for i, r := range xlsx.SheetData.Row { + if r.R == row { + idx = i + } else if r.R == row2 { + idx2 = i + } + if idx != -1 && idx2 != -1 { + break + } + } + + if idx == -1 || (idx2 == -1 && len(xlsx.SheetData.Row) >= row2) { + return + } + rowData := xlsx.SheetData.Row[idx] + cols := make([]xlsxC, 0, len(rowData.C)) + rowData.C = append(cols, rowData.C...) + f.ajustSingleRowDimensions(&rowData, 1) + if idx2 != -1 { + xlsx.SheetData.Row[idx2] = rowData + } else { + xlsx.SheetData.Row = append(xlsx.SheetData.Row, rowData) + } +} + // 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: // From 9a6f66a996eb83f16da13416c5fca361afe575b0 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 26 Dec 2018 14:48:14 +0800 Subject: [PATCH 043/957] New feature: the function `SearchSheet` now support regular expression, relate pull request #316 --- excelize.go | 2 ++ excelize_test.go | 17 +++------- sheet.go | 82 ++++++++++++++---------------------------------- 3 files changed, 30 insertions(+), 71 deletions(-) diff --git a/excelize.go b/excelize.go index b162b79910..35ff75a34b 100644 --- a/excelize.go +++ b/excelize.go @@ -243,6 +243,8 @@ func (f *File) adjustRowDimensions(xlsx *xlsxWorksheet, rowIndex, offset int) { } } +// ajustSingleRowDimensions provides a function to ajust single row +// dimensions. func (f *File) ajustSingleRowDimensions(r *xlsxRow, offset int) { r.R += offset for i, col := range r.C { diff --git a/excelize_test.go b/excelize_test.go index 8c19a3e9d4..b1eb03a860 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -1053,6 +1053,7 @@ func TestDuplicateRow(t *testing.T) { xlsx.SetCellStr(sheet, b1, bnValue) t.Run("FromSingleRow", func(t *testing.T) { + xlsx.DuplicateRow(sheet, -1) xlsx.DuplicateRow(sheet, 1) xlsx.DuplicateRow(sheet, 2) @@ -1333,19 +1334,9 @@ func TestSearchSheet(t *testing.T) { // Test search a not exists value. t.Log(xlsx.SearchSheet("Sheet1", "X")) t.Log(xlsx.SearchSheet("Sheet1", "A")) -} - -func TestRegSearchSheet(t *testing.T) { - xlsx, err := OpenFile("./test/Book1.xlsx") - if err != nil { - t.Error(err) - return - } - t.Log(xlsx.SearchSheet("Sheet1", "[0-9]")) - // Test search in a not exists worksheet. - t.Log(xlsx.SearchSheet("Sheet4", "")) - // Test search a not exists value. - t.Log(xlsx.SearchSheet("Sheet1", "")) + // Test search the coordinates where the numerical value in the range of + // "0-9" of Sheet1 is described by regular expression: + t.Log(xlsx.SearchSheet("Sheet1", "[0-9]", true)) } func TestProtectSheet(t *testing.T) { diff --git a/sheet.go b/sheet.go index cf05fe99a1..1b0fe76368 100644 --- a/sheet.go +++ b/sheet.go @@ -658,66 +658,26 @@ func (f *File) GetSheetVisible(name string) bool { return visible } -// SearchSheet provides a function to get coordinates by given worksheet name -// and cell value. This function only supports exact match of strings and -// numbers, doesn't support 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. For example, -// search the coordinates of the value of "100" on Sheet1: +// SearchSheet provides a function to get coordinates 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. +// +// An example of search the coordinates of the value of "100" on Sheet1: // // xlsx.SearchSheet("Sheet1", "100") // -func (f *File) SearchSheet(sheet, value string) []string { - xlsx := f.workSheetReader(sheet) - result := []string{} - name, ok := f.sheetMap[trimSheetName(sheet)] - if !ok { - return result - } - if xlsx != nil { - output, _ := xml.Marshal(f.Sheet[name]) - f.saveFileList(name, replaceWorkSheetsRelationshipsNameSpaceBytes(output)) - } - xml.NewDecoder(bytes.NewReader(f.readXML(name))) - d := f.sharedStringsReader() - var inElement string - var r xlsxRow - decoder := xml.NewDecoder(bytes.NewReader(f.readXML(name))) - for { - token, _ := decoder.Token() - if token == nil { - break - } - switch startElement := token.(type) { - case xml.StartElement: - inElement = startElement.Name.Local - if inElement == "row" { - r = xlsxRow{} - _ = decoder.DecodeElement(&r, &startElement) - for _, colCell := range r.C { - val, _ := colCell.getValueFrom(f, d) - if val != value { - continue - } - result = append(result, fmt.Sprintf("%s%d", strings.Map(letterOnlyMapF, colCell.R), r.R)) - } - } - default: - } - } - return result -} - -// RegSearchSheet provides the ability to retrieve coordinates -// with the given worksheet name and regular expression -// For a merged cell, get the coordinates -// of the upper left corner of the merge area. -// :example) -// Search the coordinates where the numerical value in the range of "0-9" of Sheet 1 is described: +// An example of search the coordinates where the numerical value in the range +// of "0-9" of Sheet1 is described: // -// xlsx.RegSearchSheet("Sheet1", "[0-9]") +// xlsx.SearchSheet("Sheet1", "[0-9]", true) // -func (f *File) RegSearchSheet(sheet, value string) []string { +func (f *File) SearchSheet(sheet, value string, reg ...bool) []string { + var regSearch bool + for _, r := range reg { + regSearch = r + } xlsx := f.workSheetReader(sheet) result := []string{} name, ok := f.sheetMap[trimSheetName(sheet)] @@ -746,9 +706,15 @@ func (f *File) RegSearchSheet(sheet, value string) []string { _ = decoder.DecodeElement(&r, &startElement) for _, colCell := range r.C { val, _ := colCell.getValueFrom(f, d) - regex := regexp.MustCompile(value) - if !regex.MatchString(val) { - continue + if regSearch { + regex := regexp.MustCompile(value) + if !regex.MatchString(val) { + continue + } + } else { + if val != value { + continue + } } result = append(result, fmt.Sprintf("%s%d", strings.Map(letterOnlyMapF, colCell.R), r.R)) } From 35426ed5dc5196374332498c82b4d480b05e5ac5 Mon Sep 17 00:00:00 2001 From: Veniamin Albaev Date: Thu, 27 Dec 2018 13:51:44 +0300 Subject: [PATCH 044/957] Tests refactoring Primary motivation: Avoid statefull tests with not ignorable git file tree changes. Multiple tests reads and overwrites signle file for won needs. Multiple tests reads and overwrites file under version control. Secondary motivation: Minimal tests logic aligment, separate error expectation and not error expectation tests. Introduce sub-test over test data sets and so far. This commit is not ideal but necessary (IMHO) --- .gitignore | 1 + cell_test.go | 20 +- chart_test.go | 40 +- datavalidation_test.go | 42 +- date_test.go | 19 +- excelize_test.go | 1099 +++++++++++-------- go.mod | 2 +- go.sum | 4 +- lib_test.go | 57 +- sheetpr_test.go | 133 +-- sheetview_test.go | 35 +- test/{badWorkbook.xlsx => BadWorkbook.xlsx} | Bin 12 files changed, 810 insertions(+), 642 deletions(-) create mode 100644 .gitignore rename test/{badWorkbook.xlsx => BadWorkbook.xlsx} (100%) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..ef972f604b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +test/Test*.xlsx diff --git a/cell_test.go b/cell_test.go index 4f9accddec..cb3d80e1db 100644 --- a/cell_test.go +++ b/cell_test.go @@ -1,6 +1,10 @@ package excelize -import "testing" +import ( + "testing" + + "github.com/stretchr/testify/assert" +) func TestCheckCellInArea(t *testing.T) { expectedTrueCellInAreaList := [][2]string{ @@ -14,11 +18,8 @@ func TestCheckCellInArea(t *testing.T) { cell := expectedTrueCellInArea[0] area := expectedTrueCellInArea[1] - cellInArea := checkCellInArea(cell, area) - - if !cellInArea { - t.Fatalf("Expected cell %v to be in area %v, got false\n", cell, area) - } + assert.True(t, checkCellInArea(cell, area), + "Expected cell %v to be in area %v, got false\n", cell, area) } expectedFalseCellInAreaList := [][2]string{ @@ -31,10 +32,7 @@ func TestCheckCellInArea(t *testing.T) { cell := expectedFalseCellInArea[0] area := expectedFalseCellInArea[1] - cellInArea := checkCellInArea(cell, area) - - if cellInArea { - t.Fatalf("Expected cell %v not to be inside of area %v, but got true\n", cell, area) - } + assert.False(t, checkCellInArea(cell, area), + "Expected cell %v not to be inside of area %v, but got true\n", cell, area) } } diff --git a/chart_test.go b/chart_test.go index d35c27248f..f3d7bdf27b 100644 --- a/chart_test.go +++ b/chart_test.go @@ -4,6 +4,8 @@ import ( "bytes" "encoding/xml" "testing" + + "github.com/stretchr/testify/assert" ) func TestChartSize(t *testing.T) { @@ -22,18 +24,18 @@ func TestChartSize(t *testing.T) { xlsx.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":"Fruit 3D Clustered Column Chart"}}`) // Save xlsx file by the given path. err := xlsx.Write(&buffer) - if err != nil { - t.Fatal(err) + if !assert.NoError(t, err) { + t.FailNow() } newFile, err := OpenReader(&buffer) - if err != nil { - t.Fatal(err) + if !assert.NoError(t, err) { + t.FailNow() } chartsNum := newFile.countCharts() - if chartsNum != 1 { - t.Fatalf("Expected 1 chart, actual %d", chartsNum) + if !assert.Equal(t, 1, chartsNum, "Expected 1 chart, actual %d", chartsNum) { + t.FailNow() } var ( @@ -42,25 +44,27 @@ func TestChartSize(t *testing.T) { ) content, ok := newFile.XLSX["xl/drawings/drawing1.xml"] - if !ok { - t.Fatal("Can't open the chart") - } + assert.True(t, ok, "Can't open the chart") err = xml.Unmarshal([]byte(content), &workdir) - if err != nil { - t.Fatal(err) + if !assert.NoError(t, err) { + t.FailNow() } err = xml.Unmarshal([]byte(""+workdir.TwoCellAnchor[0].Content+""), &anchor) - if err != nil { - t.Fatal(err) + if !assert.NoError(t, err) { + t.FailNow() } - if anchor.From.Col != 4 || anchor.From.Row != 3 { - t.Fatalf("From: Expected column 4, row 3, actual column %d, row %d", anchor.From.Col, anchor.From.Row) - } - if anchor.To.Col != 14 || anchor.To.Row != 27 { - t.Fatalf("To: Expected column 14, row 27, actual column %d, row %d", anchor.To.Col, anchor.To.Row) + if !assert.Equal(t, 4, anchor.From.Col, "Expected 'from' column 4") || + !assert.Equal(t, 3, anchor.From.Row, "Expected 'from' row 3") { + + t.FailNow() } + if !assert.Equal(t, 14, anchor.To.Col, "Expected 'to' column 14") || + !assert.Equal(t, 27, anchor.To.Row, "Expected 'to' row 27") { + + t.FailNow() + } } diff --git a/datavalidation_test.go b/datavalidation_test.go index 39dd2294ae..e0d4a00e76 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -9,9 +9,15 @@ package excelize -import "testing" +import ( + "testing" + + "github.com/stretchr/testify/assert" +) func TestDataValidation(t *testing.T) { + const resultFile = "./test/TestDataValidation.xlsx" + xlsx := NewFile() dvRange := NewDataValidation(true) @@ -21,37 +27,57 @@ func TestDataValidation(t *testing.T) { dvRange.SetError(DataValidationErrorStyleWarning, "error title", "error body") dvRange.SetError(DataValidationErrorStyleInformation, "error title", "error body") xlsx.AddDataValidation("Sheet1", dvRange) + if !assert.NoError(t, xlsx.SaveAs(resultFile)) { + t.FailNow() + } dvRange = NewDataValidation(true) dvRange.Sqref = "A3:B4" dvRange.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorGreaterThan) dvRange.SetInput("input title", "input body") xlsx.AddDataValidation("Sheet1", dvRange) + if !assert.NoError(t, xlsx.SaveAs(resultFile)) { + t.FailNow() + } dvRange = NewDataValidation(true) dvRange.Sqref = "A5:B6" dvRange.SetDropList([]string{"1", "2", "3"}) xlsx.AddDataValidation("Sheet1", dvRange) + if !assert.NoError(t, xlsx.SaveAs(resultFile)) { + t.FailNow() + } +} +func TestDataValidationError(t *testing.T) { + const resultFile = "./test/TestDataValidationError.xlsx" + + xlsx := NewFile() xlsx.SetCellStr("Sheet1", "E1", "E1") xlsx.SetCellStr("Sheet1", "E2", "E2") xlsx.SetCellStr("Sheet1", "E3", "E3") - dvRange = NewDataValidation(true) + + dvRange := NewDataValidation(true) dvRange.SetSqref("A7:B8") dvRange.SetSqref("A7:B8") dvRange.SetSqrefDropList("$E$1:$E$3", true) + err := dvRange.SetSqrefDropList("$E$1:$E$3", false) - t.Log(err) + assert.EqualError(t, err, "cross-sheet sqref cell are not supported") + xlsx.AddDataValidation("Sheet1", dvRange) + if !assert.NoError(t, xlsx.SaveAs(resultFile)) { + t.FailNow() + } dvRange = NewDataValidation(true) dvRange.SetDropList(make([]string, 258)) + err = dvRange.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorGreaterThan) - t.Log(err) + assert.EqualError(t, err, "data validation must be 0-255 characters") - // Test write file to given path. - err = xlsx.SaveAs("./test/Book_data_validation.xlsx") - if err != nil { - t.Error(err) + xlsx.AddDataValidation("Sheet1", dvRange) + if !assert.NoError(t, xlsx.SaveAs(resultFile)) { + t.FailNow() } } diff --git a/date_test.go b/date_test.go index 06421b8b1f..a1bfbf48a7 100644 --- a/date_test.go +++ b/date_test.go @@ -1,8 +1,11 @@ package excelize import ( + "fmt" "testing" "time" + + "github.com/stretchr/testify/assert" ) type dateTest struct { @@ -18,10 +21,10 @@ func TestTimeToExcelTime(t *testing.T) { {401769.0, time.Date(3000, 1, 1, 0, 0, 0, 0, time.UTC)}, } - for _, test := range trueExpectedInputList { - if test.ExcelValue != timeToExcelTime(test.GoValue) { - t.Fatalf("Expected %v from %v = true, got %v\n", test.ExcelValue, test.GoValue, timeToExcelTime(test.GoValue)) - } + for i, test := range trueExpectedInputList { + t.Run(fmt.Sprintf("TestData%d", i+1), func(t *testing.T) { + assert.Equal(t, test.ExcelValue, timeToExcelTime(test.GoValue)) + }) } } @@ -34,9 +37,9 @@ func TestTimeFromExcelTime(t *testing.T) { {401769.0, time.Date(3000, 1, 1, 0, 0, 0, 0, time.UTC)}, } - for _, test := range trueExpectedInputList { - if test.GoValue != timeFromExcelTime(test.ExcelValue, false) { - t.Fatalf("Expected %v from %v = true, got %v\n", test.GoValue, test.ExcelValue, timeFromExcelTime(test.ExcelValue, false)) - } + for i, test := range trueExpectedInputList { + t.Run(fmt.Sprintf("TestData%d", i+1), func(t *testing.T) { + assert.Equal(t, test.GoValue, timeFromExcelTime(test.ExcelValue, false)) + }) } } diff --git a/excelize_test.go b/excelize_test.go index b1eb03a860..1411371e45 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -7,7 +7,7 @@ import ( _ "image/jpeg" _ "image/png" "io/ioutil" - "reflect" + "os" "strconv" "strings" "testing" @@ -19,9 +19,10 @@ import ( func TestOpenFile(t *testing.T) { // Test update a XLSX file. xlsx, err := OpenFile("./test/Book1.xlsx") - if err != nil { - t.Error(err) + if !assert.NoError(t, err) { + t.FailNow() } + // Test get all the rows in a not exists worksheet. xlsx.GetRows("Sheet4") // Test get all the rows in a worksheet. @@ -104,10 +105,7 @@ func TestOpenFile(t *testing.T) { } for _, test := range booltest { xlsx.SetCellValue("Sheet2", "F16", test.value) - value := xlsx.GetCellValue("Sheet2", "F16") - if value != test.expected { - t.Errorf(`Expecting result of xlsx.SetCellValue("Sheet2", "F16", %v) to be %v (false), got: %s `, test.value, test.expected, value) - } + assert.Equal(t, test.expected, xlsx.GetCellValue("Sheet2", "F16")) } xlsx.SetCellValue("Sheet2", "G2", nil) xlsx.SetCellValue("Sheet2", "G4", time.Now()) @@ -128,93 +126,106 @@ func TestOpenFile(t *testing.T) { for i := 1; i <= 300; i++ { xlsx.SetCellStr("Sheet3", "c"+strconv.Itoa(i), strconv.Itoa(i)) } - err = xlsx.Save() - if err != nil { - t.Error(err) - } - // Test write file to not exist directory. - err = xlsx.SaveAs("") - if err != nil { - t.Log(err) + assert.NoError(t, xlsx.SaveAs("./test/TestOpenFile.xlsx")) +} + +func TestSaveAsWrongPath(t *testing.T) { + xlsx, err := OpenFile("./test/Book1.xlsx") + if assert.NoError(t, err) { + // Test write file to not exist directory. + err = xlsx.SaveAs("") + if assert.Error(t, err) { + assert.True(t, os.IsNotExist(err), "Error: %v: Expected os.IsNotExists(err) == true", err) + } } } func TestAddPicture(t *testing.T) { xlsx, err := OpenFile("./test/Book1.xlsx") - if err != nil { - t.Error(err) + if !assert.NoError(t, err) { + t.FailNow() } + // Test add picture to worksheet with offset and location hyperlink. - err = xlsx.AddPicture("Sheet2", "I9", "./test/images/excel.jpg", `{"x_offset": 140, "y_offset": 120, "hyperlink": "#Sheet2!D8", "hyperlink_type": "Location"}`) - if err != nil { - t.Error(err) + err = xlsx.AddPicture("Sheet2", "I9", "./test/images/excel.jpg", + `{"x_offset": 140, "y_offset": 120, "hyperlink": "#Sheet2!D8", "hyperlink_type": "Location"}`) + if !assert.NoError(t, err) { + t.FailNow() } + // Test add picture to worksheet with offset, external hyperlink and positioning. - err = xlsx.AddPicture("Sheet1", "F21", "./test/images/excel.png", `{"x_offset": 10, "y_offset": 10, "hyperlink": "https://github.com/360EntSecGroup-Skylar/excelize", "hyperlink_type": "External", "positioning": "oneCell"}`) - if err != nil { - t.Error(err) + err = xlsx.AddPicture("Sheet1", "F21", "./test/images/excel.png", + `{"x_offset": 10, "y_offset": 10, "hyperlink": "https://github.com/360EntSecGroup-Skylar/excelize", "hyperlink_type": "External", "positioning": "oneCell"}`) + if !assert.NoError(t, err) { + t.FailNow() } + + file, err := ioutil.ReadFile("./test/images/excel.jpg") + if !assert.NoError(t, err) { + t.FailNow() + } + + // Test add picture to worksheet from bytes. + err = xlsx.AddPictureFromBytes("Sheet1", "Q1", "", "Excel Logo", ".jpg", file) + assert.NoError(t, err) + + // Test write file to given path. + err = xlsx.SaveAs("./test/TestAddPicture.xlsx") + assert.NoError(t, err) +} + +func TestAddPictureErrors(t *testing.T) { + xlsx, err := OpenFile("./test/Book1.xlsx") + if !assert.NoError(t, err) { + t.FailNow() + } + // Test add picture to worksheet with invalid file path. - err = xlsx.AddPicture("Sheet1", "G21", "./test/images/excel.icon", "") - if err != nil { - t.Log(err) + err = xlsx.AddPicture("Sheet1", "G21", "./test/not_exists_dir/not_exists.icon", "") + if assert.Error(t, err) { + assert.True(t, os.IsNotExist(err), "Expected os.IsNotExist(err) == true") } + // Test add picture to worksheet with unsupport file type. err = xlsx.AddPicture("Sheet1", "G21", "./test/Book1.xlsx", "") - if err != nil { - t.Log(err) - } + assert.EqualError(t, err, "unsupported image extension") + err = xlsx.AddPictureFromBytes("Sheet1", "G21", "", "Excel Logo", "jpg", make([]byte, 1)) - if err != nil { - t.Log(err) - } + assert.EqualError(t, err, "unsupported image extension") + // Test add picture to worksheet with invalid file data. err = xlsx.AddPictureFromBytes("Sheet1", "G21", "", "Excel Logo", ".jpg", make([]byte, 1)) - if err != nil { - t.Log(err) - } - file, err := ioutil.ReadFile("./test/images/excel.jpg") - if err != nil { - t.Error(err) - } - // Test add picture to worksheet from bytes. - err = xlsx.AddPictureFromBytes("Sheet1", "Q1", "", "Excel Logo", ".jpg", file) - if err != nil { - t.Log(err) - } - // Test write file to given path. - err = xlsx.SaveAs("./test/Book2.xlsx") - if err != nil { - t.Error(err) - } + assert.EqualError(t, err, "image: unknown format") } func TestBrokenFile(t *testing.T) { // Test write file with broken file struct. xlsx := File{} - err := xlsx.Save() - if err != nil { - t.Log(err) - } - // Test write file with broken file struct with given path. - err = xlsx.SaveAs("./test/Book3.xlsx") - if err != nil { - t.Log(err) - } - // Test set active sheet without BookViews and Sheets maps in xl/workbook.xml. - f3, err := OpenFile("./test/badWorkbook.xlsx") - f3.GetActiveSheetIndex() - f3.SetActiveSheet(2) - if err != nil { - t.Log(err) - } + t.Run("SaveWithoutName", func(t *testing.T) { + assert.EqualError(t, xlsx.Save(), "no path defined for file, consider File.WriteTo or File.Write") + }) - // Test open a XLSX file with given illegal path. - _, err = OpenFile("./test/Book.xlsx") - if err != nil { - t.Log(err) - } + t.Run("SaveAsEmptyStruct", func(t *testing.T) { + // Test write file with broken file struct with given path. + assert.NoError(t, xlsx.SaveAs("./test/TestBrokenFile.SaveAsEmptyStruct.xlsx")) + }) + + t.Run("OpenBadWorkbook", func(t *testing.T) { + // Test set active sheet without BookViews and Sheets maps in xl/workbook.xml. + f3, err := OpenFile("./test/BadWorkbook.xlsx") + f3.GetActiveSheetIndex() + f3.SetActiveSheet(2) + assert.NoError(t, err) + }) + + t.Run("OpenNotExistsFile", func(t *testing.T) { + // Test open a XLSX file with given illegal path. + _, err := OpenFile("./test/NotExistsFile.xlsx") + if assert.Error(t, err) { + assert.True(t, os.IsNotExist(err), "Expected os.IsNotExists(err) == true") + } + }) } func TestNewFile(t *testing.T) { @@ -226,25 +237,26 @@ func TestNewFile(t *testing.T) { xlsx.SetCellInt("XLSXSheet2", "A23", 56) xlsx.SetCellStr("Sheet1", "B20", "42") xlsx.SetActiveSheet(0) + // Test add picture to sheet with scaling and positioning. err := xlsx.AddPicture("Sheet1", "H2", "./test/images/excel.gif", `{"x_scale": 0.5, "y_scale": 0.5, "positioning": "absolute"}`) - if err != nil { - t.Error(err) + if !assert.NoError(t, err) { + t.FailNow() } + // Test add picture to worksheet without formatset. err = xlsx.AddPicture("Sheet1", "C2", "./test/images/excel.png", "") - if err != nil { - t.Error(err) + if !assert.NoError(t, err) { + t.FailNow() } + // Test add picture to worksheet with invalid formatset. err = xlsx.AddPicture("Sheet1", "C2", "./test/images/excel.png", `{`) - if err != nil { - t.Log(err) - } - err = xlsx.SaveAs("./test/Book3.xlsx") - if err != nil { - t.Error(err) + if !assert.Error(t, err) { + t.FailNow() } + + assert.NoError(t, xlsx.SaveAs("./test/TestNewFile.xlsx")) } func TestColWidth(t *testing.T) { @@ -253,7 +265,7 @@ func TestColWidth(t *testing.T) { xlsx.SetColWidth("Sheet1", "A", "B", 12) xlsx.GetColWidth("Sheet1", "A") xlsx.GetColWidth("Sheet1", "C") - err := xlsx.SaveAs("./test/Book4.xlsx") + err := xlsx.SaveAs("./test/TestColWidth.xlsx") if err != nil { t.Error(err) } @@ -266,9 +278,9 @@ func TestRowHeight(t *testing.T) { xlsx.SetRowHeight("Sheet1", 4, 90) t.Log(xlsx.GetRowHeight("Sheet1", 1)) t.Log(xlsx.GetRowHeight("Sheet1", 0)) - err := xlsx.SaveAs("./test/Book5.xlsx") - if err != nil { - t.Error(err) + err := xlsx.SaveAs("./test/TestRowHeight.xlsx") + if !assert.NoError(t, err) { + t.FailNow() } convertColWidthToPixels(0) } @@ -286,17 +298,15 @@ func TestSetCellHyperLink(t *testing.T) { xlsx.SetCellHyperLink("Sheet2", "D6", "Sheet1!D8", "Location") xlsx.SetCellHyperLink("Sheet2", "C3", "Sheet1!D8", "") xlsx.SetCellHyperLink("Sheet2", "", "Sheet1!D60", "Location") - err = xlsx.Save() - if err != nil { - t.Error(err) - } + assert.NoError(t, xlsx.SaveAs("./test/TestSetCellHyperLink.xlsx")) } func TestGetCellHyperLink(t *testing.T) { xlsx, err := OpenFile("./test/Book1.xlsx") - if err != nil { - t.Error(err) + if !assert.NoError(t, err) { + t.FailNow() } + link, target := xlsx.GetCellHyperLink("Sheet1", "") t.Log(link, target) link, target = xlsx.GetCellHyperLink("Sheet1", "B19") @@ -309,51 +319,58 @@ func TestGetCellHyperLink(t *testing.T) { func TestSetCellFormula(t *testing.T) { xlsx, err := OpenFile("./test/Book1.xlsx") - if err != nil { - t.Error(err) + if !assert.NoError(t, err) { + t.FailNow() } + xlsx.SetCellFormula("Sheet1", "B19", "SUM(Sheet2!D2,Sheet2!D11)") xlsx.SetCellFormula("Sheet1", "C19", "SUM(Sheet2!D2,Sheet2!D9)") // Test set cell formula with illegal rows number. xlsx.SetCellFormula("Sheet1", "C", "SUM(Sheet2!D2,Sheet2!D9)") - err = xlsx.Save() - if err != nil { - t.Error(err) - } + + assert.NoError(t, xlsx.SaveAs("./test/TestSetCellFormula.xlsx")) } func TestSetSheetBackground(t *testing.T) { xlsx, err := OpenFile("./test/Book1.xlsx") - if err != nil { - t.Error(err) - } - err = xlsx.SetSheetBackground("Sheet2", "./test/images/background.png") - if err != nil { - t.Log(err) - } - err = xlsx.SetSheetBackground("Sheet2", "./test/Book1.xlsx") - if err != nil { - t.Log(err) + if !assert.NoError(t, err) { + t.FailNow() } + err = xlsx.SetSheetBackground("Sheet2", "./test/images/background.jpg") - if err != nil { - t.Error(err) + if !assert.NoError(t, err) { + t.FailNow() } + err = xlsx.SetSheetBackground("Sheet2", "./test/images/background.jpg") - if err != nil { - t.Error(err) + if !assert.NoError(t, err) { + t.FailNow() } - err = xlsx.Save() - if err != nil { - t.Error(err) + + assert.NoError(t, xlsx.SaveAs("./test/TestSetSheetBackground.xlsx")) +} + +func TestSetSheetBackgroundErrors(t *testing.T) { + xlsx, err := OpenFile("./test/Book1.xlsx") + if !assert.NoError(t, err) { + t.FailNow() } + + err = xlsx.SetSheetBackground("Sheet2", "./test/not_exists/not_exists.png") + if assert.Error(t, err) { + assert.True(t, os.IsNotExist(err), "Expected os.IsNotExists(err) == true") + } + + err = xlsx.SetSheetBackground("Sheet2", "./test/Book1.xlsx") + assert.EqualError(t, err, "unsupported image extension") } func TestMergeCell(t *testing.T) { xlsx, err := OpenFile("./test/Book1.xlsx") - if err != nil { - t.Error(err) + if !assert.NoError(t, err) { + t.FailNow() } + xlsx.MergeCell("Sheet1", "D9", "D9") xlsx.MergeCell("Sheet1", "D9", "E9") xlsx.MergeCell("Sheet1", "H14", "G13") @@ -370,10 +387,8 @@ func TestMergeCell(t *testing.T) { xlsx.GetCellValue("Sheet1", "H11") xlsx.GetCellValue("Sheet2", "A6") // Merged cell ref is single coordinate. xlsx.GetCellFormula("Sheet1", "G12") - err = xlsx.Save() - if err != nil { - t.Error(err) - } + + assert.NoError(t, xlsx.SaveAs("./test/TestMergeCell.xlsx")) } func TestGetMergeCells(t *testing.T) { @@ -405,8 +420,8 @@ func TestGetMergeCells(t *testing.T) { } xlsx, err := OpenFile("./test/MergeCell.xlsx") - if err != nil { - t.Error(err) + if !assert.NoError(t, err) { + t.FailNow() } mergeCells := xlsx.GetMergeCells("Sheet1") @@ -430,84 +445,90 @@ func TestGetMergeCells(t *testing.T) { } func TestSetCellStyleAlignment(t *testing.T) { - xlsx, err := OpenFile("./test/Book2.xlsx") - if err != nil { - t.Error(err) + xlsx, err := prepareTestBook1() + if !assert.NoError(t, err) { + t.FailNow() } + var style int style, err = xlsx.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 err != nil { - t.Error(err) + if !assert.NoError(t, err) { + t.FailNow() } + xlsx.SetCellStyle("Sheet1", "A22", "A22", style) // Test set cell style with given illegal rows number. xlsx.SetCellStyle("Sheet1", "A", "A22", style) xlsx.SetCellStyle("Sheet1", "A22", "A", style) // Test get cell style with given illegal rows number. xlsx.GetCellStyle("Sheet1", "A") - err = xlsx.Save() - if err != nil { - t.Error(err) - } + + assert.NoError(t, xlsx.SaveAs("./test/TestSetCellStyleAlignment.xlsx")) } func TestSetCellStyleBorder(t *testing.T) { - xlsx, err := OpenFile("./test/Book2.xlsx") - if err != nil { - t.Error(err) - } - var style int - // Test set border with invalid style parameter. - style, err = xlsx.NewStyle("") - if err != nil { - t.Log(err) + xlsx, err := prepareTestBook1() + if !assert.NoError(t, err) { + t.FailNow() } - xlsx.SetCellStyle("Sheet1", "J21", "L25", style) - // Test set border with invalid style index number. - style, err = xlsx.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 err != nil { - t.Log(err) - } - xlsx.SetCellStyle("Sheet1", "J21", "L25", style) + var style int // Test set border on overlapping area with vertical variants shading styles gradient fill. style, err = xlsx.NewStyle(`{"border":[{"type":"left","color":"0000FF","style":2},{"type":"top","color":"00FF00","style":12},{"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 err != nil { - t.Log(err) + if !assert.NoError(t, err) { + t.FailNow() } xlsx.SetCellStyle("Sheet1", "J21", "L25", style) style, err = xlsx.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 err != nil { - t.Log(err) + if !assert.NoError(t, err) { + t.FailNow() } xlsx.SetCellStyle("Sheet1", "M28", "K24", style) style, err = xlsx.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 err != nil { - t.Log(err) + if !assert.NoError(t, err) { + t.FailNow() } xlsx.SetCellStyle("Sheet1", "M28", "K24", style) // Test set border and solid style pattern fill for a single cell. style, err = xlsx.NewStyle(`{"border":[{"type":"left","color":"0000FF","style":8},{"type":"top","color":"00FF00","style":9},{"type":"bottom","color":"FFFF00","style":10},{"type":"right","color":"FF0000","style":11},{"type":"diagonalDown","color":"A020F0","style":12},{"type":"diagonalUp","color":"A020F0","style":13}],"fill":{"type":"pattern","color":["#E0EBF5"],"pattern":1}}`) - if err != nil { - t.Log(err) + if !assert.NoError(t, err) { + t.FailNow() } xlsx.SetCellStyle("Sheet1", "O22", "O22", style) - err = xlsx.Save() - if err != nil { - t.Error(err) + + assert.NoError(t, xlsx.SaveAs("./test/TestSetCellStyleBorder.xlsx")) +} + +func TestSetCellStyleBorderErrors(t *testing.T) { + xlsx, err := prepareTestBook1() + if !assert.NoError(t, err) { + t.FailNow() + } + + // Set border with invalid style parameter. + _, err = xlsx.NewStyle("") + if !assert.EqualError(t, err, "unexpected end of JSON input") { + t.FailNow() + } + + // Set border with invalid style index number. + _, err = xlsx.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() } } func TestSetCellStyleNumberFormat(t *testing.T) { - xlsx, err := OpenFile("./test/Book2.xlsx") - if err != nil { - t.Error(err) + xlsx, err := prepareTestBook1() + if !assert.NoError(t, err) { + t.FailNow() } + // 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} @@ -523,8 +544,8 @@ func TestSetCellStyleNumberFormat(t *testing.T) { xlsx.SetCellValue("Sheet2", c, val) } style, err := xlsx.NewStyle(`{"fill":{"type":"gradient","color":["#FFFFFF","#E0EBF5"],"shading":5},"number_format": ` + strconv.Itoa(d) + `}`) - if err != nil { - t.Log(err) + if !assert.NoError(t, err) { + t.FailNow() } xlsx.SetCellStyle("Sheet2", c, c, style) t.Log(xlsx.GetCellValue("Sheet2", c)) @@ -532,72 +553,74 @@ func TestSetCellStyleNumberFormat(t *testing.T) { } var style int style, err = xlsx.NewStyle(`{"number_format":-1}`) - if err != nil { - t.Log(err) + if !assert.NoError(t, err) { + t.FailNow() } xlsx.SetCellStyle("Sheet2", "L33", "L33", style) - err = xlsx.Save() - if err != nil { - t.Error(err) - } + + assert.NoError(t, xlsx.SaveAs("./test/TestSetCellStyleNumberFormat.xlsx")) } func TestSetCellStyleCurrencyNumberFormat(t *testing.T) { - xlsx, err := OpenFile("./test/Book3.xlsx") - if err != nil { - t.Error(err) - } - xlsx.SetCellValue("Sheet1", "A1", 56) - xlsx.SetCellValue("Sheet1", "A2", -32.3) - var style int - style, err = xlsx.NewStyle(`{"number_format": 188, "decimal_places": -1}`) - if err != nil { - t.Log(err) - } - xlsx.SetCellStyle("Sheet1", "A1", "A1", style) - style, err = xlsx.NewStyle(`{"number_format": 188, "decimal_places": 31, "negred": true}`) - if err != nil { - t.Log(err) - } - xlsx.SetCellStyle("Sheet1", "A2", "A2", style) + t.Run("TestBook3", func(t *testing.T) { + xlsx, err := prepareTestBook3() + if !assert.NoError(t, err) { + t.FailNow() + } - err = xlsx.Save() - if err != nil { - t.Log(err) - } + xlsx.SetCellValue("Sheet1", "A1", 56) + xlsx.SetCellValue("Sheet1", "A2", -32.3) + var style int + style, err = xlsx.NewStyle(`{"number_format": 188, "decimal_places": -1}`) + if !assert.NoError(t, err) { + t.FailNow() + } - xlsx, err = OpenFile("./test/Book4.xlsx") - if err != nil { - t.Log(err) - } - xlsx.SetCellValue("Sheet1", "A1", 42920.5) - xlsx.SetCellValue("Sheet1", "A2", 42920.5) + xlsx.SetCellStyle("Sheet1", "A1", "A1", style) + style, err = xlsx.NewStyle(`{"number_format": 188, "decimal_places": 31, "negred": true}`) + if !assert.NoError(t, err) { + t.FailNow() + } - _, err = xlsx.NewStyle(`{"number_format": 26, "lang": "zh-tw"}`) - if err != nil { - t.Log(err) - } - style, err = xlsx.NewStyle(`{"number_format": 27}`) - if err != nil { - t.Log(err) - } - xlsx.SetCellStyle("Sheet1", "A1", "A1", style) - style, err = xlsx.NewStyle(`{"number_format": 31, "lang": "ko-kr"}`) - if err != nil { - t.Log(err) - } - xlsx.SetCellStyle("Sheet1", "A2", "A2", style) + xlsx.SetCellStyle("Sheet1", "A2", "A2", style) - style, err = xlsx.NewStyle(`{"number_format": 71, "lang": "th-th"}`) - if err != nil { - t.Log(err) - } - xlsx.SetCellStyle("Sheet1", "A2", "A2", style) + assert.NoError(t, xlsx.SaveAs("./test/TestSetCellStyleCurrencyNumberFormat.TestBook3.xlsx")) + }) - err = xlsx.Save() - if err != nil { - t.Error(err) - } + t.Run("TestBook4", func(t *testing.T) { + xlsx, err := prepareTestBook4() + if !assert.NoError(t, err) { + t.FailNow() + } + xlsx.SetCellValue("Sheet1", "A1", 42920.5) + xlsx.SetCellValue("Sheet1", "A2", 42920.5) + + _, err = xlsx.NewStyle(`{"number_format": 26, "lang": "zh-tw"}`) + if !assert.NoError(t, err) { + t.FailNow() + } + + style, err := xlsx.NewStyle(`{"number_format": 27}`) + if !assert.NoError(t, err) { + t.FailNow() + } + + xlsx.SetCellStyle("Sheet1", "A1", "A1", style) + style, err = xlsx.NewStyle(`{"number_format": 31, "lang": "ko-kr"}`) + if !assert.NoError(t, err) { + t.FailNow() + } + + xlsx.SetCellStyle("Sheet1", "A2", "A2", style) + + style, err = xlsx.NewStyle(`{"number_format": 71, "lang": "th-th"}`) + if !assert.NoError(t, err) { + t.FailNow() + } + xlsx.SetCellStyle("Sheet1", "A2", "A2", style) + + assert.NoError(t, xlsx.SaveAs("./test/TestSetCellStyleCurrencyNumberFormat.TestBook4.xlsx")) + }) } func TestSetCellStyleCustomNumberFormat(t *testing.T) { @@ -614,148 +637,152 @@ func TestSetCellStyleCustomNumberFormat(t *testing.T) { t.Log(err) } xlsx.SetCellStyle("Sheet1", "A2", "A2", style) - err = xlsx.SaveAs("./test/Book_custom_number_format.xlsx") - if err != nil { - t.Error(err) - } + + assert.NoError(t, xlsx.SaveAs("./test/TestSetCellStyleCustomNumberFormat.xlsx")) } func TestSetCellStyleFill(t *testing.T) { - xlsx, err := OpenFile("./test/Book2.xlsx") - if err != nil { - t.Error(err) + xlsx, err := prepareTestBook1() + if !assert.NoError(t, err) { + t.FailNow() } + var style int // Test set fill for cell with invalid parameter. style, err = xlsx.NewStyle(`{"fill":{"type":"gradient","color":["#FFFFFF","#E0EBF5"],"shading":6}}`) - if err != nil { - t.Log(err) + if !assert.NoError(t, err) { + t.FailNow() } xlsx.SetCellStyle("Sheet1", "O23", "O23", style) style, err = xlsx.NewStyle(`{"fill":{"type":"gradient","color":["#FFFFFF"],"shading":1}}`) - if err != nil { - t.Log(err) + if !assert.NoError(t, err) { + t.FailNow() } xlsx.SetCellStyle("Sheet1", "O23", "O23", style) style, err = xlsx.NewStyle(`{"fill":{"type":"pattern","color":[],"pattern":1}}`) - if err != nil { - t.Log(err) + if !assert.NoError(t, err) { + t.FailNow() } xlsx.SetCellStyle("Sheet1", "O23", "O23", style) style, err = xlsx.NewStyle(`{"fill":{"type":"pattern","color":["#E0EBF5"],"pattern":19}}`) - if err != nil { - t.Log(err) + if !assert.NoError(t, err) { + t.FailNow() } xlsx.SetCellStyle("Sheet1", "O23", "O23", style) - err = xlsx.Save() - if err != nil { - t.Error(err) - } + assert.NoError(t, xlsx.SaveAs("./test/TestSetCellStyleFill.xlsx")) } func TestSetCellStyleFont(t *testing.T) { - xlsx, err := OpenFile("./test/Book2.xlsx") - if err != nil { - t.Error(err) + xlsx, err := prepareTestBook1() + if !assert.NoError(t, err) { + t.FailNow() } + var style int style, err = xlsx.NewStyle(`{"font":{"bold":true,"italic":true,"family":"Berlin Sans FB Demi","size":36,"color":"#777777","underline":"single"}}`) - if err != nil { - t.Log(err) + if !assert.NoError(t, err) { + t.FailNow() } + xlsx.SetCellStyle("Sheet2", "A1", "A1", style) style, err = xlsx.NewStyle(`{"font":{"italic":true,"underline":"double"}}`) - if err != nil { - t.Log(err) + if !assert.NoError(t, err) { + t.FailNow() } + xlsx.SetCellStyle("Sheet2", "A2", "A2", style) style, err = xlsx.NewStyle(`{"font":{"bold":true}}`) - if err != nil { - t.Log(err) + if !assert.NoError(t, err) { + t.FailNow() } + xlsx.SetCellStyle("Sheet2", "A3", "A3", style) style, err = xlsx.NewStyle(`{"font":{"bold":true,"family":"","size":0,"color":"","underline":""}}`) - if err != nil { - t.Log(err) + if !assert.NoError(t, err) { + t.FailNow() } + xlsx.SetCellStyle("Sheet2", "A4", "A4", style) style, err = xlsx.NewStyle(`{"font":{"color":"#777777"}}`) - if err != nil { - t.Log(err) + if !assert.NoError(t, err) { + t.FailNow() } + xlsx.SetCellStyle("Sheet2", "A5", "A5", style) - err = xlsx.Save() - if err != nil { - t.Error(err) - } + + assert.NoError(t, xlsx.SaveAs("./test/TestSetCellStyleFont.xlsx")) } func TestSetCellStyleProtection(t *testing.T) { - xlsx, err := OpenFile("./test/Book2.xlsx") - if err != nil { - t.Error(err) + xlsx, err := prepareTestBook1() + if !assert.NoError(t, err) { + t.FailNow() } + var style int style, err = xlsx.NewStyle(`{"protection":{"hidden":true, "locked":true}}`) - if err != nil { - t.Log(err) + if !assert.NoError(t, err) { + t.FailNow() } + xlsx.SetCellStyle("Sheet2", "A6", "A6", style) - err = xlsx.Save() - if err != nil { - t.Error(err) + err = xlsx.SaveAs("./test/TestSetCellStyleProtection.xlsx") + if !assert.NoError(t, err) { + t.FailNow() } } -func TestSetDeleteSheet(t *testing.T) { - xlsx, err := OpenFile("./test/Book3.xlsx") - if err != nil { - t.Error(err) - } - xlsx.DeleteSheet("XLSXSheet3") - err = xlsx.Save() - if err != nil { - t.Error(err) - } - xlsx, err = OpenFile("./test/Book4.xlsx") - if err != nil { - t.Error(err) - } - xlsx.DeleteSheet("Sheet1") - xlsx.AddComment("Sheet1", "A1", "") - xlsx.AddComment("Sheet1", "A1", `{"author":"Excelize: ","text":"This is a comment."}`) - err = xlsx.SaveAs("./test/Book_delete_sheet.xlsx") - if err != nil { - t.Error(err) - } +func TestSetDeleteSheet(t *testing.T) { + t.Run("TestBook3", func(t *testing.T) { + xlsx, err := prepareTestBook3() + if !assert.NoError(t, err) { + t.FailNow() + } + + xlsx.DeleteSheet("XLSXSheet3") + assert.NoError(t, xlsx.SaveAs("./test/TestSetDeleteSheet.TestBook3.xlsx")) + }) + + t.Run("TestBook4", func(t *testing.T) { + xlsx, err := prepareTestBook4() + if !assert.NoError(t, err) { + t.FailNow() + } + xlsx.DeleteSheet("Sheet1") + xlsx.AddComment("Sheet1", "A1", "") + xlsx.AddComment("Sheet1", "A1", `{"author":"Excelize: ","text":"This is a comment."}`) + assert.NoError(t, xlsx.SaveAs("./test/TestSetDeleteSheet.TestBook4.xlsx")) + }) } func TestGetPicture(t *testing.T) { - xlsx, err := OpenFile("./test/Book2.xlsx") - if err != nil { - t.Log(err) + xlsx, err := prepareTestBook1() + if !assert.NoError(t, err) { + t.FailNow() } + file, raw := xlsx.GetPicture("Sheet1", "F21") if file == "" { err = ioutil.WriteFile(file, raw, 0644) - if err != nil { - t.Log(err) + if !assert.NoError(t, err) { + t.FailNow() } } + // Try to get picture from a worksheet that doesn't contain any images. file, raw = xlsx.GetPicture("Sheet3", "I9") if file != "" { err = ioutil.WriteFile(file, raw, 0644) - if err != nil { - t.Log(err) + if !assert.NoError(t, err) { + t.FailNow() } } // Try to get picture from a cell that doesn't contain an image. @@ -768,142 +795,149 @@ func TestGetPicture(t *testing.T) { } func TestSheetVisibility(t *testing.T) { - xlsx, err := OpenFile("./test/Book2.xlsx") - if err != nil { - t.Error(err) + xlsx, err := prepareTestBook1() + if !assert.NoError(t, err) { + t.FailNow() } + xlsx.SetSheetVisible("Sheet2", false) xlsx.SetSheetVisible("Sheet1", false) xlsx.SetSheetVisible("Sheet1", true) xlsx.GetSheetVisible("Sheet1") - err = xlsx.Save() - if err != nil { - t.Error(err) - } + + assert.NoError(t, xlsx.SaveAs("./test/TestSheetVisibility.xlsx")) } func TestRowVisibility(t *testing.T) { - xlsx, err := OpenFile("./test/Book2.xlsx") - if err != nil { - t.Error(err) + xlsx, err := prepareTestBook1() + if !assert.NoError(t, err) { + t.FailNow() } + xlsx.SetRowVisible("Sheet3", 2, false) xlsx.SetRowVisible("Sheet3", 2, true) xlsx.GetRowVisible("Sheet3", 2) - err = xlsx.Save() - if err != nil { - t.Error(err) - } + + assert.NoError(t, xlsx.SaveAs("./test/TestRowVisibility.xlsx")) } func TestColumnVisibility(t *testing.T) { - xlsx, err := OpenFile("./test/Book2.xlsx") - if err != nil { - t.Error(err) - } - xlsx.SetColVisible("Sheet1", "F", false) - xlsx.SetColVisible("Sheet1", "F", true) - xlsx.GetColVisible("Sheet1", "F") - xlsx.SetColVisible("Sheet3", "E", false) - err = xlsx.Save() - if err != nil { - t.Error(err) - } - xlsx, err = OpenFile("./test/Book3.xlsx") - if err != nil { - t.Error(err) - } - xlsx.GetColVisible("Sheet1", "B") + t.Run("TestBook1", func(t *testing.T) { + xlsx, err := prepareTestBook1() + if !assert.NoError(t, err) { + t.FailNow() + } + + xlsx.SetColVisible("Sheet1", "F", false) + xlsx.SetColVisible("Sheet1", "F", true) + xlsx.GetColVisible("Sheet1", "F") + xlsx.SetColVisible("Sheet3", "E", false) + assert.NoError(t, xlsx.SaveAs("./test/TestColumnVisibility.xlsx")) + }) + + t.Run("TestBook3", func(t *testing.T) { + xlsx, err := prepareTestBook3() + if !assert.NoError(t, err) { + t.FailNow() + } + xlsx.GetColVisible("Sheet1", "B") + }) } func TestCopySheet(t *testing.T) { - xlsx, err := OpenFile("./test/Book2.xlsx") - if err != nil { - t.Error(err) - } - err = xlsx.CopySheet(0, -1) - if err != nil { - t.Log(err) + xlsx, err := prepareTestBook1() + if !assert.NoError(t, err) { + t.FailNow() } + idx := xlsx.NewSheet("CopySheet") err = xlsx.CopySheet(1, idx) - if err != nil { - t.Log(err) + if !assert.NoError(t, err) { + t.FailNow() } + xlsx.SetCellValue("Sheet4", "F1", "Hello") - if xlsx.GetCellValue("Sheet1", "F1") == "Hello" { - t.Error("Invalid value \"Hello\" in Sheet1") + assert.NotEqual(t, "Hello", xlsx.GetCellValue("Sheet1", "F1")) + + assert.NoError(t, xlsx.SaveAs("./test/TestCopySheet.xlsx")) +} + +func TestCopySheetError(t *testing.T) { + xlsx, err := prepareTestBook1() + if !assert.NoError(t, err) { + t.FailNow() } - err = xlsx.Save() - if err != nil { - t.Error(err) + + err = xlsx.CopySheet(0, -1) + if !assert.EqualError(t, err, "invalid worksheet index") { + t.FailNow() } + + assert.NoError(t, xlsx.SaveAs("./test/TestCopySheetError.xlsx")) } func TestAddTable(t *testing.T) { - xlsx, err := OpenFile("./test/Book2.xlsx") - if err != nil { - t.Error(err) + xlsx, err := prepareTestBook1() + if !assert.NoError(t, err) { + t.FailNow() } + err = xlsx.AddTable("Sheet1", "B26", "A21", `{}`) - if err != nil { - t.Error(err) + if !assert.NoError(t, err) { + t.FailNow() } + err = xlsx.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 err != nil { - t.Error(err) + if !assert.NoError(t, err) { + t.FailNow() } + err = xlsx.AddTable("Sheet2", "F1", "F1", `{"table_style":"TableStyleMedium8"}`) - if err != nil { - t.Error(err) - } - err = xlsx.Save() - if err != nil { - t.Error(err) + if !assert.NoError(t, err) { + t.FailNow() } + + assert.NoError(t, xlsx.SaveAs("./test/TestAddTable.xlsx")) } func TestAddShape(t *testing.T) { - xlsx, err := OpenFile("./test/Book2.xlsx") - if err != nil { - t.Error(err) + xlsx, err := prepareTestBook1() + if !assert.NoError(t, err) { + t.FailNow() } + xlsx.AddShape("Sheet1", "A30", `{"type":"rect","paragraph":[{"text":"Rectangle","font":{"color":"CD5C5C"}},{"text":"Shape","font":{"bold":true,"color":"2980B9"}}]}`) xlsx.AddShape("Sheet1", "B30", `{"type":"rect","paragraph":[{"text":"Rectangle"},{}]}`) xlsx.AddShape("Sheet1", "C30", `{"type":"rect","paragraph":[]}`) xlsx.AddShape("Sheet3", "H1", `{"type":"ellipseRibbon", "color":{"line":"#4286f4","fill":"#8eb9ff"}, "paragraph":[{"font":{"bold":true,"italic":true,"family":"Berlin Sans FB Demi","size":36,"color":"#777777","underline":"single"}}], "height": 90}`) xlsx.AddShape("Sheet3", "H1", "") - err = xlsx.Save() - if err != nil { - t.Error(err) - } + + assert.NoError(t, xlsx.SaveAs("./test/TestAddShape.xlsx")) } func TestAddComments(t *testing.T) { - xlsx, err := OpenFile("./test/Book2.xlsx") - if err != nil { - t.Error(err) + xlsx, err := prepareTestBook1() + if !assert.NoError(t, err) { + t.FailNow() } + s := strings.Repeat("c", 32768) xlsx.AddComment("Sheet1", "A30", `{"author":"`+s+`","text":"`+s+`"}`) xlsx.AddComment("Sheet2", "B7", `{"author":"Excelize: ","text":"This is a comment."}`) - err = xlsx.Save() - if err != nil { - t.Error(err) - } - allComments := xlsx.GetComments() - if len(allComments) != 2 { - t.Error("Expected 2 comment entry elements.") - } + if assert.NoError(t, xlsx.SaveAs("./test/TestAddComments.xlsx")) { + assert.Len(t, xlsx.GetComments(), 2) + } } func TestAutoFilter(t *testing.T) { - xlsx, err := OpenFile("./test/Book2.xlsx") - if err != nil { - t.Error(err) + xlsx, err := prepareTestBook1() + if !assert.NoError(t, err) { + t.FailNow() } - formats := []string{``, + + formats := []string{ + ``, `{"column":"B","expression":"x != blanks"}`, `{"column":"B","expression":"x == blanks"}`, `{"column":"B","expression":"x != nonblanks"}`, @@ -911,6 +945,26 @@ func TestAutoFilter(t *testing.T) { `{"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 { + t.Run(fmt.Sprintf("Expression%d", i+1), func(t *testing.T) { + err = xlsx.AutoFilter("Sheet3", "D4", "B1", format) + if assert.NoError(t, err) { + assert.NoError(t, xlsx.SaveAs(fmt.Sprintf("./test/TestAutoFilter%d.xlsx", i+1))) + } + }) + } + +} + +func TestAutoFilterError(t *testing.T) { + xlsx, 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"}`, @@ -918,21 +972,22 @@ func TestAutoFilter(t *testing.T) { `{"column":"B","expression":"x -- y"}`, `{"column":"A","expression":"x -- y"}`, } - for _, format := range formats { - err = xlsx.AutoFilter("Sheet3", "D4", "B1", format) - t.Log(err) - } - err = xlsx.Save() - if err != nil { - t.Error(err) + for i, format := range formats { + t.Run(fmt.Sprintf("Expression%d", i+1), func(t *testing.T) { + err = xlsx.AutoFilter("Sheet3", "D4", "B1", format) + if assert.Error(t, err) { + assert.NoError(t, xlsx.SaveAs(fmt.Sprintf("./test/TestAutoFilterError%d.xlsx", i+1))) + } + }) } } func TestAddChart(t *testing.T) { xlsx, err := OpenFile("./test/Book1.xlsx") - if err != nil { - t.Error(err) + if !assert.NoError(t, err) { + t.FailNow() } + categories := map[string]string{"A30": "Small", "A31": "Normal", "A32": "Large", "B29": "Apple", "C29": "Orange", "D29": "Pear"} values := map[string]int{"B30": 2, "C30": 3, "D30": 3, "B31": 5, "C31": 2, "D31": 4, "B32": 6, "C32": 7, "D32": 8} for k, v := range categories { @@ -968,11 +1023,8 @@ func TestAddChart(t *testing.T) { xlsx.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"}],"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":"Fruit 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"}`) xlsx.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"}],"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":"Fruit 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"}`) xlsx.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"}],"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":"Fruit 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"}`) - // Save xlsx file by the given path. - err = xlsx.SaveAs("./test/Book_addchart.xlsx") - if err != nil { - t.Error(err) - } + + assert.NoError(t, xlsx.SaveAs("./test/TestAddChart.xlsx")) } func TestInsertCol(t *testing.T) { @@ -986,12 +1038,13 @@ func TestInsertCol(t *testing.T) { xlsx.SetCellHyperLink("Sheet1", "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External") xlsx.MergeCell("Sheet1", "A1", "C3") err := xlsx.AutoFilter("Sheet1", "A2", "B2", `{"column":"B","expression":"x != blanks"}`) - t.Log(err) - xlsx.InsertCol("Sheet1", "A") - err = xlsx.SaveAs("./test/Book_insertcol.xlsx") - if err != nil { - t.Error(err) + if !assert.NoError(t, err) { + t.FailNow() } + + xlsx.InsertCol("Sheet1", "A") + + assert.NoError(t, xlsx.SaveAs("./test/TestInsertCol.xlsx")) } func TestRemoveCol(t *testing.T) { @@ -1008,10 +1061,8 @@ func TestRemoveCol(t *testing.T) { xlsx.MergeCell("Sheet1", "A2", "B2") xlsx.RemoveCol("Sheet1", "A") xlsx.RemoveCol("Sheet1", "A") - err := xlsx.SaveAs("./test/Book_removecol.xlsx") - if err != nil { - t.Error(err) - } + + assert.NoError(t, xlsx.SaveAs("./test/TestRemoveCol.xlsx")) } func TestInsertRow(t *testing.T) { @@ -1025,15 +1076,14 @@ func TestInsertRow(t *testing.T) { xlsx.SetCellHyperLink("Sheet1", "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External") xlsx.InsertRow("Sheet1", -1) xlsx.InsertRow("Sheet1", 4) - err := xlsx.SaveAs("./test/Book_insertrow.xlsx") - if err != nil { - t.Error(err) - } + + assert.NoError(t, xlsx.SaveAs("./test/TestInsertRow.xlsx")) } func TestDuplicateRow(t *testing.T) { const ( - file = "./test/Book_DuplicateRow_%s.xlsx" + file = "./test/TestDuplicateRow" + + ".%s.xlsx" sheet = "Sheet1" a1 = "A1" b1 = "B1" @@ -1053,11 +1103,10 @@ func TestDuplicateRow(t *testing.T) { xlsx.SetCellStr(sheet, b1, bnValue) t.Run("FromSingleRow", func(t *testing.T) { - xlsx.DuplicateRow(sheet, -1) xlsx.DuplicateRow(sheet, 1) xlsx.DuplicateRow(sheet, 2) - if assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(file, "SignleRow"))) { + if assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(file, "TestDuplicateRow.FromSingleRow"))) { assert.Equal(t, a1Value, xlsx.GetCellValue(sheet, a1)) assert.Equal(t, a1Value, xlsx.GetCellValue(sheet, a2)) assert.Equal(t, a1Value, xlsx.GetCellValue(sheet, a3)) @@ -1071,7 +1120,7 @@ func TestDuplicateRow(t *testing.T) { xlsx.SetCellStr(sheet, a2, a2Value) xlsx.SetCellStr(sheet, a3, a3Value) - if assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(file, "Updated"))) { + if assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(file, "TestDuplicateRow.UpdateDuplicatedRows"))) { assert.Equal(t, a1Value, xlsx.GetCellValue(sheet, a1)) assert.Equal(t, a2Value, xlsx.GetCellValue(sheet, a2)) assert.Equal(t, a3Value, xlsx.GetCellValue(sheet, a3)) @@ -1084,7 +1133,7 @@ func TestDuplicateRow(t *testing.T) { t.Run("FromFirstOfMultipleRows", func(t *testing.T) { xlsx.DuplicateRow(sheet, 1) - if assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(file, "FirstOfMultipleRows"))) { + if assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(file, "TestDuplicateRow.FromFirstOfMultipleRows"))) { assert.Equal(t, a1Value, xlsx.GetCellValue(sheet, a1)) assert.Equal(t, a1Value, xlsx.GetCellValue(sheet, a2)) assert.Equal(t, a2Value, xlsx.GetCellValue(sheet, a3)) @@ -1095,6 +1144,17 @@ func TestDuplicateRow(t *testing.T) { assert.Equal(t, bnValue, xlsx.GetCellValue(sheet, b4)) } }) + + t.Run("ZeroAndNegativeRowNum", func(t *testing.T) { + xlsx.DuplicateRow(sheet, -1) + xlsx.DuplicateRow(sheet, 0) + if assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(file, "TestDuplicateRow.ZeroAndNegativeRowNum"))) { + assert.Equal(t, "", xlsx.GetCellValue(sheet, a1)) + assert.Equal(t, "", xlsx.GetCellValue(sheet, b1)) + assert.Equal(t, a1Value, xlsx.GetCellValue(sheet, a2)) + assert.Equal(t, bnValue, xlsx.GetCellValue(sheet, b2)) + } + }) } func TestSetPane(t *testing.T) { @@ -1107,10 +1167,8 @@ func TestSetPane(t *testing.T) { xlsx.NewSheet("Panes 4") xlsx.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"}]}`) xlsx.SetPanes("Panes 4", "") - err := xlsx.SaveAs("./test/Book_set_panes.xlsx") - if err != nil { - t.Error(err) - } + + assert.NoError(t, xlsx.SaveAs("./test/TestSetPane.xlsx")) } func TestRemoveRow(t *testing.T) { @@ -1127,15 +1185,17 @@ func TestRemoveRow(t *testing.T) { xlsx.MergeCell("Sheet1", "B3", "B5") xlsx.RemoveRow("Sheet1", 2) xlsx.RemoveRow("Sheet1", 4) + err := xlsx.AutoFilter("Sheet1", "A2", "A2", `{"column":"A","expression":"x != blanks"}`) - t.Log(err) + if !assert.NoError(t, err) { + t.FailNow() + } + xlsx.RemoveRow("Sheet1", 0) xlsx.RemoveRow("Sheet1", 1) xlsx.RemoveRow("Sheet1", 0) - err = xlsx.SaveAs("./test/Book_removerow.xlsx") - if err != nil { - t.Error(err) - } + + assert.NoError(t, xlsx.SaveAs("./test/TestRemoveRow.xlsx")) } func TestConditionalFormat(t *testing.T) { @@ -1149,13 +1209,22 @@ func TestConditionalFormat(t *testing.T) { var err error // Rose format for bad conditional. format1, err = xlsx.NewConditionalStyle(`{"font":{"color":"#9A0511"},"fill":{"type":"pattern","color":["#FEC7CE"],"pattern":1}}`) - t.Log(err) + if !assert.NoError(t, err) { + t.FailNow() + } + // Light yellow format for neutral conditional. format2, err = xlsx.NewConditionalStyle(`{"fill":{"type":"pattern","color":["#FEEAA0"],"pattern":1}}`) - t.Log(err) + if !assert.NoError(t, err) { + t.FailNow() + } + // Light green format for good conditional. format3, err = xlsx.NewConditionalStyle(`{"font":{"color":"#09600B"},"fill":{"type":"pattern","color":["#C7EECF"],"pattern":1}}`) - t.Log(err) + if !assert.NoError(t, err) { + t.FailNow() + } + // Color scales: 2 color. xlsx.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. @@ -1182,71 +1251,82 @@ func TestConditionalFormat(t *testing.T) { xlsx.SetConditionalFormat("Sheet1", "L1:L10", fmt.Sprintf(`[{"type":"formula", "criteria":"L2<3", "format":%d}]`, format1)) // Test set invalid format set in conditional format xlsx.SetConditionalFormat("Sheet1", "L1:L10", "") - err = xlsx.SaveAs("./test/Book_conditional_format.xlsx") - if err != nil { - t.Log(err) + + err = xlsx.SaveAs("./test/TestConditionalFormat.xlsx") + if !assert.NoError(t, err) { + t.FailNow() } - // Set conditional format with illegal JSON string. - _, err = xlsx.NewConditionalStyle("") - t.Log(err) // Set conditional format with illegal valid type. xlsx.SetConditionalFormat("Sheet1", "K1:K10", `[{"type":"", "criteria":"=", "min_type":"min","max_type":"max","bar_color":"#638EC6"}]`) // Set conditional format with illegal criteria type. xlsx.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. + + // Set conditional format with file without dxfs element shold not return error. xlsx, err = OpenFile("./test/Book1.xlsx") - t.Log(err) + if !assert.NoError(t, err) { + t.FailNow() + } + _, err = xlsx.NewConditionalStyle(`{"font":{"color":"#9A0511"},"fill":{"type":"pattern","color":["#FEC7CE"],"pattern":1}}`) - t.Log(err) + if !assert.NoError(t, err) { + t.FailNow() + } } -func TestTitleToNumber(t *testing.T) { - if TitleToNumber("AK") != 36 { - t.Error("Conver title to number failed") +func TestConditionalFormatError(t *testing.T) { + xlsx := NewFile() + for j := 1; j <= 10; j++ { + for i := 0; i <= 15; i++ { + xlsx.SetCellInt("Sheet1", ToAlphaString(i)+strconv.Itoa(j), j) + } } - if TitleToNumber("ak") != 36 { - t.Error("Conver title to number failed") + + // Set conditional format with illegal JSON string should return error + _, err := xlsx.NewConditionalStyle("") + if !assert.EqualError(t, err, "unexpected end of JSON input") { + t.FailNow() } } +func TestTitleToNumber(t *testing.T) { + assert.Equal(t, 36, TitleToNumber("AK")) + assert.Equal(t, 36, TitleToNumber("ak")) +} + func TestSharedStrings(t *testing.T) { xlsx, err := OpenFile("./test/SharedStrings.xlsx") - if err != nil { - t.Error(err) - return + if !assert.NoError(t, err) { + t.FailNow() } xlsx.GetRows("Sheet1") } func TestSetSheetRow(t *testing.T) { xlsx, err := OpenFile("./test/Book1.xlsx") - if err != nil { - t.Error(err) - return + if !assert.NoError(t, err) { + t.FailNow() } + xlsx.SetSheetRow("Sheet1", "B27", &[]interface{}{"cell", nil, int32(42), float64(42), time.Now()}) xlsx.SetSheetRow("Sheet1", "", &[]interface{}{"cell", nil, 2}) xlsx.SetSheetRow("Sheet1", "B27", []interface{}{}) xlsx.SetSheetRow("Sheet1", "B27", &xlsx) - err = xlsx.Save() - if err != nil { - t.Error(err) - return - } + + assert.NoError(t, xlsx.SaveAs("./test/TestSetSheetRow.xlsx")) } func TestRows(t *testing.T) { xlsx, err := OpenFile("./test/Book1.xlsx") - if err != nil { - t.Error(err) - return + if !assert.NoError(t, err) { + t.FailNow() } + rows, err := xlsx.Rows("Sheet2") - if err != nil { - t.Error(err) - return + if !assert.NoError(t, err) { + t.FailNow() } + rowStrs := make([][]string, 0) var i = 0 for rows.Next() { @@ -1254,25 +1334,30 @@ func TestRows(t *testing.T) { columns := rows.Columns() rowStrs = append(rowStrs, columns) } - if rows.Error() != nil { - t.Error(rows.Error()) - return + + if !assert.NoError(t, rows.Error()) { + t.FailNow() } + dstRows := xlsx.GetRows("Sheet2") - if len(dstRows) != len(rowStrs) { - t.Error("values not equal") - return + if !assert.Equal(t, len(rowStrs), len(dstRows)) { + t.FailNow() } + for i := 0; i < len(rowStrs); i++ { - if !reflect.DeepEqual(trimSliceSpace(dstRows[i]), trimSliceSpace(rowStrs[i])) { - t.Error("values not equal") - return + if !assert.Equal(t, trimSliceSpace(dstRows[i]), trimSliceSpace(rowStrs[i])) { + t.FailNow() } } - rows, err = xlsx.Rows("SheetN") - if err != nil { - t.Log(err) +} + +func TestRowsError(t *testing.T) { + xlsx, err := OpenFile("./test/Book1.xlsx") + if !assert.NoError(t, err) { + t.FailNow() } + _, err = xlsx.Rows("SheetN") + assert.EqualError(t, err, "Sheet SheetN is not exist") } func TestOutlineLevel(t *testing.T) { @@ -1285,16 +1370,16 @@ func TestOutlineLevel(t *testing.T) { xlsx.SetColOutlineLevel("Sheet2", "B", 2) xlsx.SetRowOutlineLevel("Sheet1", 2, 1) xlsx.GetRowOutlineLevel("Sheet1", 2) - err := xlsx.SaveAs("./test/Book_outline_level.xlsx") - if err != nil { - t.Error(err) - return + err := xlsx.SaveAs("./test/TestOutlineLevel.xlsx") + if !assert.NoError(t, err) { + t.FailNow() } + xlsx, err = OpenFile("./test/Book1.xlsx") - if err != nil { - t.Error(err) - return + if !assert.NoError(t, err) { + t.FailNow() } + xlsx.SetColOutlineLevel("Sheet2", "B", 2) } @@ -1325,10 +1410,10 @@ func TestHSL(t *testing.T) { func TestSearchSheet(t *testing.T) { xlsx, err := OpenFile("./test/SharedStrings.xlsx") - if err != nil { - t.Error(err) - return + if !assert.NoError(t, err) { + t.FailNow() } + // Test search in a not exists worksheet. t.Log(xlsx.SearchSheet("Sheet4", "")) // Test search a not exists value. @@ -1346,22 +1431,18 @@ func TestProtectSheet(t *testing.T) { Password: "password", EditScenarios: false, }) - err := xlsx.SaveAs("./test/Book_protect_sheet.xlsx") - if err != nil { - t.Error(err) - } + + assert.NoError(t, xlsx.SaveAs("./test/TestProtectSheet.xlsx")) } func TestUnprotectSheet(t *testing.T) { xlsx, err := OpenFile("./test/Book1.xlsx") - if err != nil { - t.Error(err) + if !assert.NoError(t, err) { + t.FailNow() } + xlsx.UnprotectSheet("Sheet1") - err = xlsx.Save() - if err != nil { - t.Error(err) - } + assert.NoError(t, xlsx.SaveAs("./test/TestUnprotectSheet.xlsx")) } func trimSliceSpace(s []string) []string { @@ -1374,3 +1455,67 @@ func trimSliceSpace(s []string) []string { } return s } + +func prepareTestBook1() (*File, error) { + xlsx, err := OpenFile("./test/Book1.xlsx") + if err != nil { + return nil, err + } + + err = xlsx.AddPicture("Sheet2", "I9", "./test/images/excel.jpg", + `{"x_offset": 140, "y_offset": 120, "hyperlink": "#Sheet2!D8", "hyperlink_type": "Location"}`) + if err != nil { + return nil, err + } + + // Test add picture to worksheet with offset, external hyperlink and positioning. + err = xlsx.AddPicture("Sheet1", "F21", "./test/images/excel.png", + `{"x_offset": 10, "y_offset": 10, "hyperlink": "https://github.com/360EntSecGroup-Skylar/excelize", "hyperlink_type": "External", "positioning": "oneCell"}`) + if err != nil { + return nil, err + } + + file, err := ioutil.ReadFile("./test/images/excel.jpg") + if err != nil { + return nil, err + } + + err = xlsx.AddPictureFromBytes("Sheet1", "Q1", "", "Excel Logo", ".jpg", file) + if err != nil { + return nil, err + } + + return xlsx, nil +} + +func prepareTestBook3() (*File, error) { + xlsx := NewFile() + xlsx.NewSheet("Sheet1") + xlsx.NewSheet("XLSXSheet2") + xlsx.NewSheet("XLSXSheet3") + xlsx.SetCellInt("XLSXSheet2", "A23", 56) + xlsx.SetCellStr("Sheet1", "B20", "42") + xlsx.SetActiveSheet(0) + + err := xlsx.AddPicture("Sheet1", "H2", "./test/images/excel.gif", `{"x_scale": 0.5, "y_scale": 0.5, "positioning": "absolute"}`) + if err != nil { + return nil, err + } + + err = xlsx.AddPicture("Sheet1", "C2", "./test/images/excel.png", "") + if err != nil { + return nil, err + } + + return xlsx, nil +} + +func prepareTestBook4() (*File, error) { + xlsx := NewFile() + xlsx.SetColWidth("Sheet1", "B", "A", 12) + xlsx.SetColWidth("Sheet1", "A", "B", 12) + xlsx.GetColWidth("Sheet1", "A") + xlsx.GetColWidth("Sheet1", "C") + + return xlsx, nil +} diff --git a/go.mod b/go.mod index 8db2fe6c2f..4ecf6ee975 100644 --- a/go.mod +++ b/go.mod @@ -4,5 +4,5 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/stretchr/testify v1.2.2 + github.com/stretchr/testify v1.2.3-0.20181224173747-660f15d67dbb ) diff --git a/go.sum b/go.sum index ca5f759f66..8dce13c35a 100644 --- a/go.sum +++ b/go.sum @@ -4,5 +4,5 @@ github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9 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/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.2.3-0.20181224173747-660f15d67dbb h1:cRItZejS4Ok67vfCdrbGIaqk86wmtQNOjVD7jSyS2aw= +github.com/stretchr/testify v1.2.3-0.20181224173747-660f15d67dbb/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= diff --git a/lib_test.go b/lib_test.go index c668fc8d0f..ef0d8f5b31 100644 --- a/lib_test.go +++ b/lib_test.go @@ -1,8 +1,13 @@ package excelize -import "testing" +import ( + "fmt" + "testing" -func TestAxisLowerOrEqualThan(t *testing.T) { + "github.com/stretchr/testify/assert" +) + +func TestAxisLowerOrEqualThanIsTrue(t *testing.T) { trueExpectedInputList := [][2]string{ {"A", "B"}, {"A", "AA"}, @@ -12,13 +17,14 @@ func TestAxisLowerOrEqualThan(t *testing.T) { {"2", "11"}, } - for _, trueExpectedInput := range trueExpectedInputList { - isLowerOrEqual := axisLowerOrEqualThan(trueExpectedInput[0], trueExpectedInput[1]) - if !isLowerOrEqual { - t.Fatalf("Expected %v <= %v = true, got false\n", trueExpectedInput[0], trueExpectedInput[1]) - } + for i, trueExpectedInput := range trueExpectedInputList { + t.Run(fmt.Sprintf("TestData%d", i), func(t *testing.T) { + assert.True(t, axisLowerOrEqualThan(trueExpectedInput[0], trueExpectedInput[1])) + }) } +} +func TestAxisLowerOrEqualThanIsFalse(t *testing.T) { falseExpectedInputList := [][2]string{ {"B", "A"}, {"AA", "A"}, @@ -28,32 +34,27 @@ func TestAxisLowerOrEqualThan(t *testing.T) { {"11", "2"}, } - for _, falseExpectedInput := range falseExpectedInputList { - isLowerOrEqual := axisLowerOrEqualThan(falseExpectedInput[0], falseExpectedInput[1]) - if isLowerOrEqual { - t.Fatalf("Expected %v <= %v = false, got true\n", falseExpectedInput[0], falseExpectedInput[1]) - } + for i, falseExpectedInput := range falseExpectedInputList { + t.Run(fmt.Sprintf("TestData%d", i), func(t *testing.T) { + assert.False(t, axisLowerOrEqualThan(falseExpectedInput[0], falseExpectedInput[1])) + }) } } func TestGetCellColRow(t *testing.T) { - cellExpectedColRowList := map[string][2]string{ - "C220": {"C", "220"}, - "aaef42": {"aaef", "42"}, - "bonjour": {"bonjour", ""}, - "59": {"", "59"}, - "": {"", ""}, + cellExpectedColRowList := [][3]string{ + {"C220", "C", "220"}, + {"aaef42", "aaef", "42"}, + {"bonjour", "bonjour", ""}, + {"59", "", "59"}, + {"", "", ""}, } - for cell, expectedColRow := range cellExpectedColRowList { - col, row := getCellColRow(cell) - - if col != expectedColRow[0] { - t.Fatalf("Expected cell %v to return col %v, got col %v\n", cell, expectedColRow[0], col) - } - - if row != expectedColRow[1] { - t.Fatalf("Expected cell %v to return row %v, got row %v\n", cell, expectedColRow[1], row) - } + for i, test := range cellExpectedColRowList { + t.Run(fmt.Sprintf("TestData%d", i), func(t *testing.T) { + col, row := getCellColRow(test[0]) + assert.Equal(t, test[1], col, "Unexpected col") + assert.Equal(t, test[2], row, "Unexpected row") + }) } } diff --git a/sheetpr_test.go b/sheetpr_test.go index d9f50590fd..22dbd42437 100644 --- a/sheetpr_test.go +++ b/sheetpr_test.go @@ -2,11 +2,12 @@ package excelize_test import ( "fmt" - "reflect" "testing" - "github.com/360EntSecGroup-Skylar/excelize" "github.com/mohae/deepcopy" + "github.com/stretchr/testify/assert" + + "github.com/360EntSecGroup-Skylar/excelize" ) var _ = []excelize.SheetPrOption{ @@ -86,7 +87,8 @@ func ExampleFile_GetSheetPrOptions() { func TestSheetPrOptions(t *testing.T) { const sheet = "Sheet1" - for _, test := range []struct { + + testData := []struct { container excelize.SheetPrOptionPtr nonDefault excelize.SheetPrOption }{ @@ -96,66 +98,69 @@ func TestSheetPrOptions(t *testing.T) { {new(excelize.FitToPage), excelize.FitToPage(true)}, {new(excelize.AutoPageBreaks), excelize.AutoPageBreaks(true)}, {new(excelize.OutlineSummaryBelow), excelize.OutlineSummaryBelow(false)}, - } { - opt := test.nonDefault - t.Logf("option %T", opt) - - def := deepcopy.Copy(test.container).(excelize.SheetPrOptionPtr) - val1 := deepcopy.Copy(def).(excelize.SheetPrOptionPtr) - val2 := deepcopy.Copy(def).(excelize.SheetPrOptionPtr) - - xl := excelize.NewFile() - // Get the default value - if err := xl.GetSheetPrOptions(sheet, def); err != nil { - t.Fatalf("%T: %s", opt, err) - } - // Get again and check - if err := xl.GetSheetPrOptions(sheet, val1); err != nil { - t.Fatalf("%T: %s", opt, err) - } - if !reflect.DeepEqual(val1, def) { - t.Fatalf("%T: value should not have changed", opt) - } - // Set the same value - if err := xl.SetSheetPrOptions(sheet, val1); err != nil { - t.Fatalf("%T: %s", opt, err) - } - // Get again and check - if err := xl.GetSheetPrOptions(sheet, val1); err != nil { - t.Fatalf("%T: %s", opt, err) - } - if !reflect.DeepEqual(val1, def) { - t.Fatalf("%T: value should not have changed", opt) - } - - // Set a different value - if err := xl.SetSheetPrOptions(sheet, test.nonDefault); err != nil { - t.Fatalf("%T: %s", opt, err) - } - if err := xl.GetSheetPrOptions(sheet, val1); err != nil { - t.Fatalf("%T: %s", opt, err) - } - // Get again and compare - if err := xl.GetSheetPrOptions(sheet, val2); err != nil { - t.Fatalf("%T: %s", opt, err) - } - if !reflect.DeepEqual(val2, val1) { - t.Fatalf("%T: value should not have changed", opt) - } - // Value should not be the same as the default - if reflect.DeepEqual(val1, def) { - t.Fatalf("%T: value should have changed from default", opt) - } - - // Restore the default value - if err := xl.SetSheetPrOptions(sheet, def); err != nil { - t.Fatalf("%T: %s", opt, err) - } - if err := xl.GetSheetPrOptions(sheet, val1); err != nil { - t.Fatalf("%T: %s", opt, err) - } - if !reflect.DeepEqual(val1, def) { - t.Fatalf("%T: value should now be the same as default", opt) - } + } + + for i, test := range testData { + t.Run(fmt.Sprintf("TestData%d", i), func(t *testing.T) { + + opt := test.nonDefault + t.Logf("option %T", opt) + + def := deepcopy.Copy(test.container).(excelize.SheetPrOptionPtr) + val1 := deepcopy.Copy(def).(excelize.SheetPrOptionPtr) + val2 := deepcopy.Copy(def).(excelize.SheetPrOptionPtr) + + xl := excelize.NewFile() + // Get the default value + if !assert.NoError(t, xl.GetSheetPrOptions(sheet, def), opt) { + t.FailNow() + } + // Get again and check + if !assert.NoError(t, xl.GetSheetPrOptions(sheet, val1), opt) { + t.FailNow() + } + if !assert.Equal(t, val1, def, opt) { + t.FailNow() + } + // Set the same value + if !assert.NoError(t, xl.SetSheetPrOptions(sheet, val1), opt) { + t.FailNow() + } + // Get again and check + if !assert.NoError(t, xl.GetSheetPrOptions(sheet, val1), opt) { + t.FailNow() + } + if !assert.Equal(t, val1, def, "%T: value should not have changed", opt) { + t.FailNow() + } + // Set a different value + if !assert.NoError(t, xl.SetSheetPrOptions(sheet, test.nonDefault), opt) { + t.FailNow() + } + if !assert.NoError(t, xl.GetSheetPrOptions(sheet, val1), opt) { + t.FailNow() + } + // Get again and compare + if !assert.NoError(t, xl.GetSheetPrOptions(sheet, val2), opt) { + t.FailNow() + } + if !assert.Equal(t, val1, val2, "%T: value should not have changed", opt) { + 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) { + t.FailNow() + } + // Restore the default value + if !assert.NoError(t, xl.SetSheetPrOptions(sheet, def), opt) { + t.FailNow() + } + if !assert.NoError(t, xl.GetSheetPrOptions(sheet, val1), opt) { + t.FailNow() + } + if !assert.Equal(t, def, val1) { + t.FailNow() + } + }) } } diff --git a/sheetview_test.go b/sheetview_test.go index ee81d5b55c..b565a12221 100644 --- a/sheetview_test.go +++ b/sheetview_test.go @@ -4,6 +4,8 @@ import ( "fmt" "testing" + "github.com/stretchr/testify/assert" + "github.com/360EntSecGroup-Skylar/excelize" ) @@ -157,29 +159,12 @@ func TestSheetViewOptionsErrors(t *testing.T) { xl := excelize.NewFile() const sheet = "Sheet1" - if err := xl.GetSheetViewOptions(sheet, 0); err != nil { - t.Errorf("Unexpected error: %s", err) - } - if err := xl.GetSheetViewOptions(sheet, -1); err != nil { - t.Errorf("Unexpected error: %s", err) - } - if err := xl.GetSheetViewOptions(sheet, 1); err == nil { - t.Error("Error expected but got nil") - } - if err := xl.GetSheetViewOptions(sheet, -2); err == nil { - t.Error("Error expected but got nil") - } - - if err := xl.SetSheetViewOptions(sheet, 0); err != nil { - t.Errorf("Unexpected error: %s", err) - } - if err := xl.SetSheetViewOptions(sheet, -1); err != nil { - t.Errorf("Unexpected error: %s", err) - } - if err := xl.SetSheetViewOptions(sheet, 1); err == nil { - t.Error("Error expected but got nil") - } - if err := xl.SetSheetViewOptions(sheet, -2); err == nil { - t.Error("Error expected but got nil") - } + assert.NoError(t, xl.GetSheetViewOptions(sheet, 0)) + assert.NoError(t, xl.GetSheetViewOptions(sheet, -1)) + assert.Error(t, xl.GetSheetViewOptions(sheet, 1)) + assert.Error(t, xl.GetSheetViewOptions(sheet, -2)) + assert.NoError(t, xl.SetSheetViewOptions(sheet, 0)) + assert.NoError(t, xl.SetSheetViewOptions(sheet, -1)) + assert.Error(t, xl.SetSheetViewOptions(sheet, 1)) + assert.Error(t, xl.SetSheetViewOptions(sheet, -2)) } diff --git a/test/badWorkbook.xlsx b/test/BadWorkbook.xlsx similarity index 100% rename from test/badWorkbook.xlsx rename to test/BadWorkbook.xlsx From 46179eac457dee67b40dde1070affe3bf787bd9c Mon Sep 17 00:00:00 2001 From: fossabot Date: Sun, 30 Dec 2018 06:41:08 -0800 Subject: [PATCH 045/957] Add license scan report and status Signed-off-by: fossabot --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e0fbc27bce..9d787d39f5 100644 --- a/README.md +++ b/README.md @@ -3,12 +3,16 @@

Build Status Code Coverage + Go Report Card GoDoc Licenses Donate

+ +[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2F360EntSecGroup-Skylar%2Fexcelize.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2F360EntSecGroup-Skylar%2Fexcelize?ref=badge_large) + # Excelize ## Introduction @@ -174,4 +178,4 @@ Some struct of XML originally by [tealeg/xlsx](https://github.com/tealeg/xlsx). ## Licenses -This program is under the terms of the BSD 3-Clause License. See [https://opensource.org/licenses/BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause). +This program is under the terms of the BSD 3-Clause License. See [https://opensource.org/licenses/BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause). \ No newline at end of file From fabd9d013f7d18cfc77e374a603a26995fdf091d Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 1 Jan 2019 13:20:14 +0800 Subject: [PATCH 046/957] README updated --- LICENSE | 2 +- README.md | 10 ++++------ README_zh.md | 4 +++- cell.go | 2 +- chart.go | 2 +- col.go | 2 +- comment.go | 2 +- datavalidation.go | 2 +- datavalidation_test.go | 2 +- date.go | 2 +- excelize.go | 2 +- excelize_test.go | 17 +++++++++++++++-- file.go | 2 +- lib.go | 2 +- picture.go | 2 +- rows.go | 2 +- shape.go | 2 +- sheet.go | 2 +- sheetpr.go | 2 +- sheetview.go | 2 +- styles.go | 2 +- table.go | 2 +- templates.go | 2 +- vmlDrawing.go | 2 +- xmlChart.go | 2 +- xmlComments.go | 2 +- xmlContentTypes.go | 2 +- xmlDecodeDrawing.go | 2 +- xmlDrawing.go | 2 +- xmlSharedStrings.go | 2 +- xmlStyles.go | 2 +- xmlTable.go | 2 +- xmlTheme.go | 2 +- xmlWorkbook.go | 2 +- xmlWorksheet.go | 2 +- 35 files changed, 54 insertions(+), 41 deletions(-) diff --git a/LICENSE b/LICENSE index 4ca04b8f6a..1ece21dec2 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 2016 - 2018 360 Enterprise Security Group, Endpoint Security, +Copyright (c) 2016 - 2019 360 Enterprise Security Group, Endpoint Security, inc. All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/README.md b/README.md index 9d787d39f5..56f79e0a0f 100644 --- a/README.md +++ b/README.md @@ -3,21 +3,17 @@

Build Status Code Coverage - Go Report Card GoDoc Licenses Donate

- -[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2F360EntSecGroup-Skylar%2Fexcelize.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2F360EntSecGroup-Skylar%2Fexcelize?ref=badge_large) - # Excelize ## Introduction -Excelize is a library written in pure Go and providing a set of functions that allow you to write to and read from XLSX files. Support reads and writes XLSX file generated by Microsoft Excel™ 2007 and later. Support save file without losing original charts of XLSX. This library needs Go version 1.8 or later. The full API docs can be seen using go's built-in documentation tool, or online at [godoc.org](https://godoc.org/github.com/360EntSecGroup-Skylar/excelize) and [docs reference](https://xuri.me/excelize/). +Excelize is a library written in pure Go and providing a set of functions that allow you to write to and read from XLSX files. Support reads and writes XLSX file generated by Microsoft Excel™ 2007 and later. Support save file without losing original charts of XLSX. This library needs Go version 1.8 or later. The full API docs can be seen using go's built-in documentation tool, or online at [godoc.org](https://godoc.org/github.com/360EntSecGroup-Skylar/excelize) and [docs reference](https://xuri.me/excelize/). ## Basic Usage @@ -178,4 +174,6 @@ Some struct of XML originally by [tealeg/xlsx](https://github.com/tealeg/xlsx). ## Licenses -This program is under the terms of the BSD 3-Clause License. See [https://opensource.org/licenses/BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause). \ No newline at end of file +This program is under the terms of the BSD 3-Clause License. See [https://opensource.org/licenses/BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause). + +[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2F360EntSecGroup-Skylar%2Fexcelize.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2F360EntSecGroup-Skylar%2Fexcelize?ref=badge_large) diff --git a/README_zh.md b/README_zh.md index 6be0f923ed..6fbbb00b17 100644 --- a/README_zh.md +++ b/README_zh.md @@ -13,7 +13,7 @@ ## 简介 -Excelize 是 Go 语言编写的用于操作 Office Excel 文档类库,基于 ECMA-376 Office OpenXML 标准。可以使用它来读取、写入由 Microsoft Excel™ 2007 及以上版本创建的 XLSX 文档。相比较其他的开源类库,Excelize 支持写入原本带有图片(表)、透视表和切片器等复杂样式的文档,还支持向 Excel 文档中插入图片与图表,并且在保存后不会丢失文档原有样式,可以应用于各类报表系统中。使用本类库要求使用的 Go 语言为 1.8 或更高版本,完整的 API 使用文档请访问 [godoc.org](https://godoc.org/github.com/360EntSecGroup-Skylar/excelize) 或查看 [参考文档](https://xuri.me/excelize/)。 +Excelize 是 Go 语言编写的用于操作 Office Excel 文档类库,基于 ECMA-376 Office OpenXML 标准。可以使用它来读取、写入由 Microsoft Excel™ 2007 及以上版本创建的 XLSX 文档。相比较其他的开源类库,Excelize 支持写入原本带有图片(表)、透视表和切片器等复杂样式的文档,还支持向 Excel 文档中插入图片与图表,并且在保存后不会丢失文档原有样式,可以应用于各类报表系统中。使用本类库要求使用的 Go 语言为 1.8 或更高版本,完整的 API 使用文档请访问 [godoc.org](https://godoc.org/github.com/360EntSecGroup-Skylar/excelize) 或查看 [参考文档](https://xuri.me/excelize/)。 ## 快速上手 @@ -175,3 +175,5 @@ func main() { ## 开源许可 本项目遵循 BSD 3-Clause 开源许可协议,访问 [https://opensource.org/licenses/BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) 查看许可协议文件。 + +[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2F360EntSecGroup-Skylar%2Fexcelize.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2F360EntSecGroup-Skylar%2Fexcelize?ref=badge_large) diff --git a/cell.go b/cell.go index aa4067f739..afe8635f8c 100644 --- a/cell.go +++ b/cell.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2018 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2019 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. // diff --git a/chart.go b/chart.go index fb7b60d901..77a012543e 100644 --- a/chart.go +++ b/chart.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2018 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2019 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. // diff --git a/col.go b/col.go index 05405bd16e..af2c321347 100644 --- a/col.go +++ b/col.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2018 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2019 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. // diff --git a/comment.go b/comment.go index 9031aad494..07f70b5088 100644 --- a/comment.go +++ b/comment.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2018 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2019 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. // diff --git a/datavalidation.go b/datavalidation.go index 5ebd61f97c..0a95251b82 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2018 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2019 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. // diff --git a/datavalidation_test.go b/datavalidation_test.go index e0d4a00e76..82bc42f23f 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2018 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2019 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. // diff --git a/date.go b/date.go index a0b8cebe8a..7dc5ef811e 100644 --- a/date.go +++ b/date.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2018 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2019 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. // diff --git a/excelize.go b/excelize.go index 35ff75a34b..60480c8d84 100644 --- a/excelize.go +++ b/excelize.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2018 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2019 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. diff --git a/excelize_test.go b/excelize_test.go index 1411371e45..1b6997ae52 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -129,6 +129,19 @@ func TestOpenFile(t *testing.T) { assert.NoError(t, xlsx.SaveAs("./test/TestOpenFile.xlsx")) } +func TestSaveFile(t *testing.T) { + xlsx, err := OpenFile("./test/Book1.xlsx") + if !assert.NoError(t, err) { + t.FailNow() + } + assert.NoError(t, xlsx.SaveAs("./test/TestSaveFile.xlsx")) + xlsx, err = OpenFile("./test/TestSaveFile.xlsx") + if !assert.NoError(t, err) { + t.FailNow() + } + assert.NoError(t, xlsx.Save()) +} + func TestSaveAsWrongPath(t *testing.T) { xlsx, err := OpenFile("./test/Book1.xlsx") if assert.NoError(t, err) { @@ -309,7 +322,7 @@ func TestGetCellHyperLink(t *testing.T) { link, target := xlsx.GetCellHyperLink("Sheet1", "") t.Log(link, target) - link, target = xlsx.GetCellHyperLink("Sheet1", "B19") + link, target = xlsx.GetCellHyperLink("Sheet1", "A22") t.Log(link, target) link, target = xlsx.GetCellHyperLink("Sheet2", "D6") t.Log(link, target) @@ -1082,7 +1095,7 @@ func TestInsertRow(t *testing.T) { func TestDuplicateRow(t *testing.T) { const ( - file = "./test/TestDuplicateRow" + + file = "./test/TestDuplicateRow" + ".%s.xlsx" sheet = "Sheet1" a1 = "A1" diff --git a/file.go b/file.go index 1f69005486..3e49803306 100644 --- a/file.go +++ b/file.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2018 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2019 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. // diff --git a/lib.go b/lib.go index cf43dc9402..99c513edbf 100644 --- a/lib.go +++ b/lib.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2018 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2019 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. // diff --git a/picture.go b/picture.go index 9efd875a98..763d89a6c7 100644 --- a/picture.go +++ b/picture.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2018 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2019 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. // diff --git a/rows.go b/rows.go index def150d1df..cebedfa491 100644 --- a/rows.go +++ b/rows.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2018 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2019 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. // diff --git a/shape.go b/shape.go index ad87712e82..e2281a23ce 100644 --- a/shape.go +++ b/shape.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2018 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2019 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. // diff --git a/sheet.go b/sheet.go index 1b0fe76368..b03492c89b 100644 --- a/sheet.go +++ b/sheet.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2018 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2019 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. // diff --git a/sheetpr.go b/sheetpr.go index 57eebd44d6..4497e7a837 100644 --- a/sheetpr.go +++ b/sheetpr.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2018 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2019 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. // diff --git a/sheetview.go b/sheetview.go index 37a0c393c8..6b191e92f5 100644 --- a/sheetview.go +++ b/sheetview.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2018 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2019 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. // diff --git a/styles.go b/styles.go index f923787f4d..d075266693 100644 --- a/styles.go +++ b/styles.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2018 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2019 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. // diff --git a/table.go b/table.go index 02c89fa561..7c7e061921 100644 --- a/table.go +++ b/table.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2018 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2019 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. // diff --git a/templates.go b/templates.go index 1d0655dc93..17fc8d4011 100644 --- a/templates.go +++ b/templates.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2018 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2019 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. // diff --git a/vmlDrawing.go b/vmlDrawing.go index c17dde7887..8b1d00fe96 100644 --- a/vmlDrawing.go +++ b/vmlDrawing.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2018 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2019 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. // diff --git a/xmlChart.go b/xmlChart.go index 2f9b8d96d7..163812d542 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2018 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2019 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. // diff --git a/xmlComments.go b/xmlComments.go index 9075c8873a..5ffbecf740 100644 --- a/xmlComments.go +++ b/xmlComments.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2018 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2019 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. // diff --git a/xmlContentTypes.go b/xmlContentTypes.go index 8d09d515ef..e99b0b32d5 100644 --- a/xmlContentTypes.go +++ b/xmlContentTypes.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2018 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2019 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. // diff --git a/xmlDecodeDrawing.go b/xmlDecodeDrawing.go index d21c3f01df..eead575265 100644 --- a/xmlDecodeDrawing.go +++ b/xmlDecodeDrawing.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2018 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2019 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. // diff --git a/xmlDrawing.go b/xmlDrawing.go index 7356cb5921..89496c413f 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2018 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2019 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. // diff --git a/xmlSharedStrings.go b/xmlSharedStrings.go index 782ed61ad5..3fcf3d5bb7 100644 --- a/xmlSharedStrings.go +++ b/xmlSharedStrings.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2018 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2019 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. // diff --git a/xmlStyles.go b/xmlStyles.go index 7ba4379148..fc53f7756c 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2018 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2019 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. // diff --git a/xmlTable.go b/xmlTable.go index 7e155e6d72..6d27dc9d44 100644 --- a/xmlTable.go +++ b/xmlTable.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2018 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2019 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. // diff --git a/xmlTheme.go b/xmlTheme.go index b4140b6acd..01d0054c27 100644 --- a/xmlTheme.go +++ b/xmlTheme.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2018 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2019 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. // diff --git a/xmlWorkbook.go b/xmlWorkbook.go index 65720337ae..ad66f4295c 100644 --- a/xmlWorkbook.go +++ b/xmlWorkbook.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2018 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2019 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. // diff --git a/xmlWorksheet.go b/xmlWorksheet.go index d35b40e0f8..b3e887705d 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2018 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2019 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. // From dea7ba0ec43a4c29a6642d02b6edc73b8b0369f0 Mon Sep 17 00:00:00 2001 From: Rafael Barros Date: Tue, 1 Jan 2019 08:18:42 -0200 Subject: [PATCH 047/957] Fixes #195: Make GetRows return value avoid empty cell * #195: proposed resolution to the issue * Make GetRows return value avoid empty cell * Update test file to fix broken testing. --- rows.go | 19 +++++++++---------- test/Book1.xlsx | Bin 23153 -> 20899 bytes 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/rows.go b/rows.go index cebedfa491..d3fe6e0994 100644 --- a/rows.go +++ b/rows.go @@ -31,10 +31,9 @@ import ( // func (f *File) GetRows(sheet string) [][]string { xlsx := f.workSheetReader(sheet) - rows := [][]string{} name, ok := f.sheetMap[trimSheetName(sheet)] if !ok { - return rows + return [][]string{} } if xlsx != nil { output, _ := xml.Marshal(f.Sheet[name]) @@ -44,15 +43,12 @@ func (f *File) GetRows(sheet string) [][]string { d := f.sharedStringsReader() var inElement string var r xlsxRow - var row []string tr, tc := f.getTotalRowsCols(name) - for i := 0; i < tr; i++ { - row = []string{} - for j := 0; j <= tc; j++ { - row = append(row, "") - } - rows = append(rows, row) + rows := make([][]string, tr) + for i := range rows { + rows[i] = make([]string, tc+1) } + var row int decoder := xml.NewDecoder(bytes.NewReader(f.readXML(name))) for { token, _ := decoder.Token() @@ -70,12 +66,15 @@ func (f *File) GetRows(sheet string) [][]string { c := TitleToNumber(strings.Map(letterOnlyMapF, colCell.R)) val, _ := colCell.getValueFrom(f, d) rows[cr][c] = val + if val != "" { + row = r.R + } } } default: } } - return rows + return rows[:row] } // Rows defines an iterator to a sheet diff --git a/test/Book1.xlsx b/test/Book1.xlsx index 2ef11210d6cfb5bda5194c91342ae2f750efbe7d..78431dceaa3c5bd2ee952b1bbbf8482fdc5cd39f 100644 GIT binary patch literal 20899 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 YNcrc^;|E4?E-pFXMF-xM;aH#k2HIYoL;wH) literal 23153 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 From 969f4e2399d17386c1beda389096a737873110ff Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 5 Jan 2019 10:55:43 +0800 Subject: [PATCH 048/957] Resolve #329, add copyright agreement statement on README and LICENSE --- LICENSE | 7 ++++--- README.md | 10 ++++++---- README_zh.md | 10 ++++++---- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/LICENSE b/LICENSE index 1ece21dec2..1962b4a393 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,8 @@ BSD 3-Clause License -Copyright (c) 2016 - 2019 360 Enterprise Security Group, Endpoint Security, -inc. All rights reserved. +Copyright (c) 2016-2019, 360 Enterprise Security Group, Endpoint Security, Inc. +Copyright (c) 2011-2017, Geoffrey J. Teale (complying with the tealeg/xlsx license) +All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -13,7 +14,7 @@ modification, are permitted provided that the following conditions are met: this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -* Neither the name of Excelize nor the names of its +* Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. diff --git a/README.md b/README.md index 56f79e0a0f..52cf367057 100644 --- a/README.md +++ b/README.md @@ -168,12 +168,14 @@ func main() { Contributions are welcome! Open a pull request to fix a bug, or open an issue to discuss a new feature or change. XML is compliant with [part 1 of the 5th edition of the ECMA-376 Standard for Office Open XML](http://www.ecma-international.org/publications/standards/Ecma-376.htm). -## Credits - -Some struct of XML originally by [tealeg/xlsx](https://github.com/tealeg/xlsx). - ## Licenses This program is under the terms of the BSD 3-Clause License. See [https://opensource.org/licenses/BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause). +The Excel logo is a trademark of [Microsoft Corporation](https://aka.ms/trademarks-usage). This artwork is an adaptation. + +Some struct of XML originally by [tealeg/xlsx](https://github.com/tealeg/xlsx). Licensed under the [BSD 3-Clause License](https://github.com/tealeg/xlsx/blob/master/LICENSE). + +gopher.{ai,svg,png} was created by [Takuya Ueda](https://twitter.com/tenntenn). Licensed under the [Creative Commons 3.0 Attributions license](http://creativecommons.org/licenses/by/3.0/). + [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2F360EntSecGroup-Skylar%2Fexcelize.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2F360EntSecGroup-Skylar%2Fexcelize?ref=badge_large) diff --git a/README_zh.md b/README_zh.md index 6fbbb00b17..f4e893be9d 100644 --- a/README_zh.md +++ b/README_zh.md @@ -168,12 +168,14 @@ func main() { 欢迎您为此项目贡献代码,提出建议或问题、修复 Bug 以及参与讨论对新功能的想法。 XML 符合标准: [part 1 of the 5th edition of the ECMA-376 Standard for Office Open XML](http://www.ecma-international.org/publications/standards/Ecma-376.htm)。 -## 致谢 - -本类库中部分 XML 结构体的定义参考了开源项目:[tealeg/xlsx](https://github.com/tealeg/xlsx). - ## 开源许可 本项目遵循 BSD 3-Clause 开源许可协议,访问 [https://opensource.org/licenses/BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause) 查看许可协议文件。 +Excel 徽标是 [Microsoft Corporation](https://aka.ms/trademarks-usage) 的商标,项目的图片是一种改编。 + +本类库中部分 XML 结构体的定义参考了开源项目:[tealeg/xlsx](https://github.com/tealeg/xlsx),遵循 [BSD 3-Clause License](https://github.com/tealeg/xlsx/blob/master/LICENSE) 开源许可协议。 + +gopher.{ai,svg,png} 由 [Takuya Ueda](https://twitter.com/tenntenn) 创作,遵循 [Creative Commons 3.0 Attributions license](http://creativecommons.org/licenses/by/3.0/) 创作共用授权条款。 + [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2F360EntSecGroup-Skylar%2Fexcelize.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2F360EntSecGroup-Skylar%2Fexcelize?ref=badge_large) From 034de7acd88e848e051b9ee809767e63476cd88e Mon Sep 17 00:00:00 2001 From: zhangleijlu Date: Sun, 6 Jan 2019 14:12:31 +0800 Subject: [PATCH 049/957] Resolve #318, add new functions and --- CONTRIBUTING.md | 89 +++++---- CONTRIBUTING_TEMPLATE.md | 384 --------------------------------------- excelize.go | 2 +- sheet.go | 70 +++++++ sheet_test.go | 114 ++++++++++++ sheetpr.go | 19 ++ 6 files changed, 248 insertions(+), 430 deletions(-) delete mode 100644 CONTRIBUTING_TEMPLATE.md create mode 100644 sheet_test.go diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f4382ec03a..afb7d4eef9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,3 @@ - # Contributing to excelize Want to hack on excelize? Awesome! This page contains information about reporting issues as well as some tips and @@ -376,89 +375,89 @@ reading through [Effective Go](https://golang.org/doc/effective_go.html). 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://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). ### Package Comment -Every package should have a package comment, a block comment preceding the package clause. -For multi-file packages, the package comment only needs to be present in one file, and any one will do. -The package comment should introduce the package and provide information relevant to the package as a -whole. It will appear first on the godoc page and should set up the detailed documentation that follows. +Every package should have a package comment, a block comment preceding the package clause. +For multi-file packages, the package comment only needs to be present in one file, and any one will do. +The package comment should introduce the package and provide information relevant to the package as a +whole. It will appear first on the godoc page and should set up the detailed documentation that follows. ### Single Method Interface Name -By convention, one-method interfaces are named by the method name plus an -er suffix + +By convention, one-method interfaces are named by the method name plus an -er suffix or similar modification to construct an agent noun: Reader, Writer, Formatter, CloseNotifier etc. -There are a number of such names and it's productive to honor them and the function names they capture. -Read, Write, Close, Flush, String and so on have canonical signatures and meanings. To avoid confusion, -don't give your method one of those names unless it has the same signature and meaning. Conversely, -if your type implements a method with the same meaning as a method on a well-known type, give it the +There are a number of such names and it's productive to honor them and the function names they capture. +Read, Write, Close, Flush, String and so on have canonical signatures and meanings. To avoid confusion, +don't give your method one of those names unless it has the same signature and meaning. Conversely, +if your type implements a method with the same meaning as a method on a well-known type, give it the same name and signature; call your string-converter method String not ToString. - ### Avoid Annotations in Comments + Comments do not need extra formatting such as banners of stars. The generated output -may not even be presented in a fixed-width font, so don't depend on spacing for alignment—godoc, -like gofmt, takes care of that. The comments are uninterpreted plain text, so HTML and other -annotations such as _this_ will reproduce verbatim and should not be used. One adjustment godoc -does do is to display indented text in a fixed-width font, suitable for program snippets. +may not even be presented in a fixed-width font, so don't depend on spacing for alignment—godoc, +like gofmt, takes care of that. The comments are uninterpreted plain text, so HTML and other +annotations such as _this_ will reproduce verbatim and should not be used. One adjustment godoc +does do is to display indented text in a fixed-width font, suitable for program snippets. The package comment for the fmt package uses this to good effect. - ### Comment First Word as Subject + Doc comments work best as complete sentences, which allow a wide variety of automated presentations. The first sentence should be a one-sentence summary that starts with the name being declared. - ### Good Package Name -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 -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 -collisions a priori. The package name is only the default name for -imports; it need not be unique across all source code, and in the -rare case of a collision the importing package can choose a different -name to use locally. In any case, confusion is rare because the file -name in the import determines just which package is being used. +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 +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 +collisions a priori. The package name is only the default name for +imports; it need not be unique across all source code, and in the +rare case of a collision the importing package can choose a different +name to use locally. In any case, confusion is rare because the file +name in the import determines just which package is being used. ### Avoid Renaming Imports + Avoid renaming imports except to avoid a name collision; good package names should not require renaming. In the event of collision, prefer to rename the most local or project-specific import. - ### Context as First Argument -Values of the context.Context type carry security credentials, tracing information, -deadlines, and cancellation signals across API and process boundaries. Go programs -pass Contexts explicitly along the entire function call chain from incoming RPCs + +Values of the context.Context type carry security credentials, tracing information, +deadlines, and cancellation signals across API and process boundaries. Go programs +pass Contexts explicitly along the entire function call chain from incoming RPCs and HTTP requests to outgoing requests. Most functions that use a Context should accept it as their first parameter. - ### Do Not Discard Errors -Do not discard errors using _ variables. If a function returns an error, -check it to make sure the function succeeded. Handle the error, return it, or, -in truly exceptional situations, panic. +Do not discard errors using _ variables. If a function returns an error, +check it to make sure the function succeeded. Handle the error, return it, or, +in truly exceptional situations, panic. ### Go Error Format -Error strings should not be capitalized (unless beginning with proper nouns + +Error strings should not be capitalized (unless beginning with proper nouns or acronyms) or end with punctuation, since they are usually printed following other context. That is, use fmt.Errorf("something bad") not fmt.Errorf("Something bad"), -so that log.Printf("Reading %s: %v", filename, err) formats without a spurious +so that log.Printf("Reading %s: %v", filename, err) formats without a spurious capital letter mid-message. This does not apply to logging, which is implicitly line-oriented and not combined inside other messages. - ### Use Crypto Rand -Do not use package math/rand to generate keys, even -throwaway ones. Unseeded, the generator is completely predictable. -Seeded with time.Nanoseconds(), there are just a few bits of entropy. -Instead, use crypto/rand's Reader, and if you need text, print to -hexadecimal or base64 +Do not use package math/rand to generate keys, even +throwaway ones. Unseeded, the generator is completely predictable. +Seeded with time.Nanoseconds(), there are just a few bits of entropy. +Instead, use crypto/rand's Reader, and if you need text, print to +hexadecimal or base64 diff --git a/CONTRIBUTING_TEMPLATE.md b/CONTRIBUTING_TEMPLATE.md deleted file mode 100644 index 389f2433a2..0000000000 --- a/CONTRIBUTING_TEMPLATE.md +++ /dev/null @@ -1,384 +0,0 @@ - -# Contributing to excelize - -Want to hack on excelize? Awesome! This page contains information about reporting issues as well as some tips and -guidelines useful to experienced open source contributors. Finally, make sure -you read our [community guidelines](#community-guidelines) before you -start participating. - -## Topics - -* [Reporting Security Issues](#reporting-security-issues) -* [Design and Cleanup Proposals](#design-and-cleanup-proposals) -* [Reporting Issues](#reporting-other-issues) -* [Quick Contribution Tips and Guidelines](#quick-contribution-tips-and-guidelines) -* [Community Guidelines](#community-guidelines) - -## Reporting security issues - -The excelize maintainers take security seriously. If you discover a security -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. -We currently do not offer a paid security bounty program, but are not -ruling it out in the future. - -## Reporting other issues - -A great way to contribute to the project is to send a detailed report when you -encounter an issue. We always appreciate a well-written, thorough bug report, -and will thank you for it! - -Check that [our issue database](https://github.com/360EntSecGroup-Skylar/excelize/issues) -doesn't already include that problem or suggestion before submitting an issue. -If you find a match, you can use the "subscribe" button to get notified on -updates. Do *not* leave random "+1" or "I have this too" comments, as they -only clutter the discussion, and don't help resolving it. However, if you -have ways to reproduce the issue or have additional information that may help -resolving the issue, please leave a comment. - -When reporting issues, always include the output of `go env`. - -Also include the steps required to reproduce the problem if possible and -applicable. This information will help us review and fix your issue faster. -When sending lengthy log-files, consider posting them as a gist [https://gist.github.com](https://gist.github.com). -Don't forget to remove sensitive data from your logfiles before posting (you can -replace those parts with "REDACTED"). - -## Quick contribution tips and guidelines - -This section gives the experienced contributor some tips and guidelines. - -### Pull requests are always welcome - -Not sure if that typo is worth a pull request? Found a bug and know how to fix -it? Do it! We will appreciate it. Any significant improvement should be -documented as [a GitHub issue](https://github.com/360EntSecGroup-Skylar/excelize/issues) before -anybody starts working on it. - -We are always thrilled to receive pull requests. We do our best to process them -quickly. If your pull request is not accepted on the first try, -don't get discouraged! - -### Design and cleanup proposals - -You can propose new designs for existing excelize features. You can also design -entirely new features. We really appreciate contributors who want to refactor or -otherwise cleanup our project. - -We try hard to keep excelize lean and focused. Excelize can't do everything for -everybody. This means that we might decide against incorporating a new feature. -However, there might be a way to implement that feature *on top of* excelize. - -### Conventions - -Fork the repository and make changes on your fork in a feature branch: - -* If it's a bug fix branch, name it XXXX-something where XXXX is the number of - the issue. -* If it's a feature branch, create an enhancement issue to announce - your intentions, and name it XXXX-something where XXXX is the number of the - issue. - -Submit unit tests for your changes. Go has a great test framework built in; use -it! Take a look at existing tests for inspiration. Run the full test on your branch before -submitting a pull request. - -Update the documentation when creating or modifying features. Test your -documentation changes for clarity, concision, and correctness, as well as a -clean documentation build. - -Write clean code. Universally formatted code promotes ease of writing, reading, -and maintenance. Always run `gofmt -s -w file.go` on each changed file before -committing your changes. Most editors have plug-ins that do this automatically. - -Pull request descriptions should be as clear as possible and include a reference -to all the issues that they address. - -### Successful Changes - -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 -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 -solutions. - -Typically, the best methods of accomplishing this are to submit an issue, -stating the problem. This issue can include a problem statement and a -checklist with requirements. If solutions are proposed, alternatives should be -listed and eliminated. Even if the criteria for elimination of a solution is -frivolous, say so. - -Larger changes typically work best with design documents. These are focused on -providing context to the design at the time the feature was conceived and can -inform future documentation contributions. - -### Commit Messages - -Commit messages must start with a capitalized and short summary -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 -required. They should tell the story of the change and provide readers -understanding of what led to it. - -In practice, the best approach to maintaining a nice commit message is to -leverage a `git add -p` and `git commit --amend` to formulate a solid -changeset. This allows one to piece together a change, as information becomes -available. - -If you squash a series of commits, don't just submit that. Re-write the commit -message, as if the series of commits was a single stroke of brilliance. - -That said, there is no requirement to have a single commit for a PR, as long as -each commit tells the story. For example, if there is a feature that requires a -package, it might make sense to have the package in a separate commit then have -a subsequent commit that uses it. - -Remember, you're telling part of the story with the commit message. Don't make -your chapter weird. - -### Review - -Code review comments may be added to your pull request. Discuss, then make the -suggested modifications and push additional commits to your feature branch. Post -a comment after pushing. New commits show up in the pull request automatically, -but the reviewers are notified only when you comment. - -Pull requests must be cleanly rebased on top of master without multiple branches -mixed into the PR. - -**Git tip**: If your PR no longer merges cleanly, use `rebase master` in your -feature branch to update your pull request rather than `merge master`. - -Before you make a pull request, squash your commits into logical units of work -using `git rebase -i` and `git push -f`. A logical unit of work is a consistent -set of patches that should be reviewed together: for example, upgrading the -version of a vendored dependency and taking advantage of its now available new -feature constitute two separate units of work. Implementing a new function and -calling it in another file constitute a single logical unit of work. The very -high majority of submissions should have a single commit, so if in doubt: squash -down to one. - -After every commit, make sure the test passes. Include documentation -changes in the same pull request so that a revert would remove all traces of -the feature or fix. - -Include an issue reference like `Closes #XXXX` or `Fixes #XXXX` in commits that -close an issue. Including references automatically closes the issue on a merge. - -Please see the [Coding Style](#coding-style) for further guidelines. - -### Merge approval - -The excelize maintainers use LGTM (Looks Good To Me) in comments on the code review to -indicate acceptance. - -### Sign your work - -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/)): - -```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: - -(a) The contribution was created in whole or in part by me and I - have the right to submit it under the open source license - indicated in the file; or - -(b) The contribution is based upon previous work that, to the best - of my knowledge, is covered under an appropriate open source - license and I have the right under that license to submit that - work with modifications, whether created in whole or in part - by me, under the same open source license (unless I am - permitted to submit under a different license), as indicated - in the file; or - -(c) The contribution was provided directly to me by some other - person who certified (a), (b) or (c) and I have not modified - it. - -(d) I understand and agree that this project and the contribution - are public and that a record of the contribution (including all - personal information I submit with it, including my sign-off) is - maintained indefinitely and may be redistributed consistent with - this project or the open source license(s) involved. -``` - -Then you just add a line to every git commit message: - - Signed-off-by: Ri Xu https://xuri.me - -Use your real name (sorry, no pseudonyms or anonymous contributions.) - -If you set your `user.name` and `user.email` git configs, you can sign your -commit automatically with `git commit -s`. - -### How can I become a maintainer - -First, all maintainers have 3 things - -* They share responsibility in the project's success. -* They have made a long-term, recurring time investment to improve the project. -* They spend that time doing whatever needs to be done, not necessarily what - is the most interesting or fun. - -Maintainers are often under-appreciated, because their work is harder to appreciate. -It's easy to appreciate a really cool and technically advanced feature. It's harder -to appreciate the absence of bugs, the slow but steady improvement in stability, -or the reliability of a release process. But those things distinguish a good -project from a great one. - -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. - -## Community guidelines - -We want to keep the community awesome, growing and collaborative. We need -your help to keep it that way. To help with this we've come up with some general -guidelines for the community as a whole: - -* Be nice: Be courteous, respectful and polite to fellow community members: - no regional, racial, gender, or other abuse will be tolerated. We like - nice people way better than mean ones! - -* Encourage diversity and participation: Make everyone in our community feel - welcome, regardless of their background and the extent of their - contributions, and do everything possible to encourage participation in - our community. - -* Keep it legal: Basically, don't get us in trouble. Share only content that - you own, do not share private or sensitive information, and don't break - the law. - -* Stay on topic: Make sure that you are posting to the correct channel and - avoid off-topic discussions. Remember when you update an issue or respond - to an email you are potentially sending to a large number of people. Please - consider this before you update. Also remember that nobody likes spam. - -* Don't send email to the maintainers: There's no need to send email to the - maintainers to ask them to investigate an issue or to take a look at a - pull request. Instead of sending an email, GitHub mentions should be - used to ping maintainers to review a pull request, a proposal or an - issue. - -### Guideline violations — 3 strikes method - -The point of this section is not to find opportunities to punish people, but we -do need a fair way to deal with people who are making our community suck. - -1. First occurrence: We'll give you a friendly, but public reminder that the - behavior is inappropriate according to our guidelines. - -2. Second occurrence: We will send you a private message with a warning that - any additional violations will result in removal from the community. - -3. Third occurrence: Depending on the violation, we may need to delete or ban - your account. - -**Notes:** - -* Obvious spammers are banned on first occurrence. If we don't do this, we'll - have spam all over the place. - -* Violations are forgiven after 6 months of good behavior, and we won't hold a - grudge. - -* People who commit minor infractions will get some education, rather than - hammering them in the 3 strikes process. - -* The rules apply equally to everyone in the community, no matter how much - you've contributed. - -* Extreme violations of a threatening, abusive, destructive or illegal nature - will be addressed immediately and are not subject to 3 strikes or forgiveness. - -* Contact [xuri.me](https://xuri.me) to report abuse or appeal violations. In the case of - appeals, we know that mistakes happen, and we'll work with you to come up with a - fair solution if there has been a misunderstanding. - -## Coding Style - -Unless explicitly stated, we follow all coding guidelines from the Go -community. While some of these standards may seem arbitrary, they somehow seem -to result in a solid, consistent codebase. - -It is possible that the code base does not currently comply with these -guidelines. We are not looking for a massive PR that fixes this, since that -goes against the spirit of the guidelines. All new contributions should make a -best effort to clean up and make the code base better than they left it. -Obviously, apply your best judgement. Remember, the goal here is to make the -code base easier for humans to navigate and understand. Always keep that in -mind when nudging others to comply. - -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). -3. All code should follow the guidelines covered in [Effective - Go](http://golang.org/doc/effective_go.html) 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 - expectations, caveats and anything else that may be important. If a type - gets exported, having the comments already there will ensure it's ready. -6. Variable name length should be proportional to its context and no longer. - `noCommaALongVariableNameLikeThisIsNotMoreClearWhenASimpleCommentWouldDo`. - In practice, short methods will have short variable names and globals will - have longer names. -7. No underscores in package names. If you need a compound name, step back, - and re-examine why you need a compound name. If you still think you need a - compound name, lose the underscore. -8. No utils or helpers packages. If a function is not general enough to - warrant its own package, it has not been written generally enough to be a - part of a util package. Just leave it unexported and well-documented. -9. All tests should run with `go test` and outside tooling should not be - required. No, we don't need another unit testing framework. Assertion - packages are acceptable if they provide _real_ incremental value. -10. Even though we call these "rules" above, they are actually just - 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 -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). - -{{range .}} -### {{.title}} -{{.body}} -{{end}} \ No newline at end of file diff --git a/excelize.go b/excelize.go index 60480c8d84..61f6dd64e8 100644 --- a/excelize.go +++ b/excelize.go @@ -156,7 +156,7 @@ func replaceWorkSheetsRelationshipsNameSpaceBytes(workbookMarshal []byte) []byte // 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 // 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?forum=excel +// 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. diff --git a/sheet.go b/sheet.go index b03492c89b..f21fc9b8df 100644 --- a/sheet.go +++ b/sheet.go @@ -790,3 +790,73 @@ 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) +} + +// PageLayoutOrientation defines the orientation of page layout for a +// worksheet. +type PageLayoutOrientation string + +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 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 function to sets worksheet page layout. +// +// Available options: +// PageLayoutOrientation(string) +func (f *File) SetPageLayout(sheet string, opts ...PageLayoutOption) error { + s := f.workSheetReader(sheet) + ps := s.PageSetUp + if ps == nil { + ps = new(xlsxPageSetUp) + s.PageSetUp = ps + } + + for _, opt := range opts { + opt.setPageLayout(ps) + } + return nil +} + +// GetPageLayout provides a function to gets worksheet page layout. +// +// Available options: +// PageLayoutOrientation(string) +func (f *File) GetPageLayout(sheet string, opts ...PageLayoutOptionPtr) error { + s := f.workSheetReader(sheet) + ps := s.PageSetUp + + for _, opt := range opts { + opt.getPageLayout(ps) + } + return nil +} diff --git a/sheet_test.go b/sheet_test.go new file mode 100644 index 0000000000..e4b4700f53 --- /dev/null +++ b/sheet_test.go @@ -0,0 +1,114 @@ +package excelize_test + +import ( + "fmt" + "testing" + + "github.com/360EntSecGroup-Skylar/excelize" + "github.com/mohae/deepcopy" + "github.com/stretchr/testify/assert" +) + +func ExampleFile_SetPageLayout() { + xl := excelize.NewFile() + const sheet = "Sheet1" + + if err := xl.SetPageLayout( + "Sheet1", + excelize.PageLayoutOrientation(excelize.OrientationLandscape), + ); err != nil { + panic(err) + } + // Output: +} + +func ExampleFile_GetPageLayout() { + xl := excelize.NewFile() + const sheet = "Sheet1" + var ( + orientation excelize.PageLayoutOrientation + ) + if err := xl.GetPageLayout("Sheet1", &orientation); err != nil { + panic(err) + } + fmt.Println("Defaults:") + fmt.Printf("- orientation: %q\n", orientation) + // Output: + // Defaults: + // - orientation: "portrait" +} + +func TestPageLayoutOption(t *testing.T) { + const sheet = "Sheet1" + + testData := []struct { + container excelize.PageLayoutOptionPtr + nonDefault excelize.PageLayoutOption + }{ + {new(excelize.PageLayoutOrientation), excelize.PageLayoutOrientation(excelize.OrientationLandscape)}, + } + + for i, test := range testData { + t.Run(fmt.Sprintf("TestData%d", i), func(t *testing.T) { + + opt := test.nonDefault + t.Logf("option %T", opt) + + def := deepcopy.Copy(test.container).(excelize.PageLayoutOptionPtr) + val1 := deepcopy.Copy(def).(excelize.PageLayoutOptionPtr) + val2 := deepcopy.Copy(def).(excelize.PageLayoutOptionPtr) + + xl := excelize.NewFile() + // Get the default value + if !assert.NoError(t, xl.GetPageLayout(sheet, def), opt) { + t.FailNow() + } + // Get again and check + if !assert.NoError(t, xl.GetPageLayout(sheet, val1), opt) { + t.FailNow() + } + if !assert.Equal(t, val1, def, opt) { + t.FailNow() + } + // Set the same value + if !assert.NoError(t, xl.SetPageLayout(sheet, val1), opt) { + t.FailNow() + } + // Get again and check + if !assert.NoError(t, xl.GetPageLayout(sheet, val1), opt) { + t.FailNow() + } + if !assert.Equal(t, val1, def, "%T: value should not have changed", opt) { + t.FailNow() + } + // Set a different value + if !assert.NoError(t, xl.SetPageLayout(sheet, test.nonDefault), opt) { + t.FailNow() + } + if !assert.NoError(t, xl.GetPageLayout(sheet, val1), opt) { + t.FailNow() + } + // Get again and compare + if !assert.NoError(t, xl.GetPageLayout(sheet, val2), opt) { + t.FailNow() + } + if !assert.Equal(t, val1, val2, "%T: value should not have changed", opt) { + 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) { + t.FailNow() + } + // Restore the default value + if !assert.NoError(t, xl.SetPageLayout(sheet, def), opt) { + t.FailNow() + } + if !assert.NoError(t, xl.GetPageLayout(sheet, val1), opt) { + t.FailNow() + } + if !assert.Equal(t, def, val1) { + t.FailNow() + } + }) + } +} diff --git a/sheetpr.go b/sheetpr.go index 4497e7a837..14c18da0f5 100644 --- a/sheetpr.go +++ b/sheetpr.go @@ -35,6 +35,7 @@ type ( OutlineSummaryBelow bool ) +// setSheetPrOption implements the SheetPrOption interface. func (o OutlineSummaryBelow) setSheetPrOption(pr *xlsxSheetPr) { if pr.OutlinePr == nil { pr.OutlinePr = new(xlsxOutlinePr) @@ -42,6 +43,7 @@ func (o OutlineSummaryBelow) setSheetPrOption(pr *xlsxSheetPr) { 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 { @@ -51,10 +53,14 @@ func (o *OutlineSummaryBelow) getSheetPrOption(pr *xlsxSheetPr) { *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 = "" @@ -63,10 +69,15 @@ func (o *CodeName) getSheetPrOption(pr *xlsxSheetPr) { *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 @@ -75,10 +86,14 @@ func (o *EnableFormatConditionsCalculation) getSheetPrOption(pr *xlsxSheetPr) { *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 @@ -87,6 +102,7 @@ func (o *Published) getSheetPrOption(pr *xlsxSheetPr) { *o = Published(defaultTrue(pr.Published)) } +// setSheetPrOption implements the SheetPrOption interface. func (o FitToPage) setSheetPrOption(pr *xlsxSheetPr) { if pr.PageSetUpPr == nil { if !o { @@ -97,6 +113,7 @@ func (o FitToPage) setSheetPrOption(pr *xlsxSheetPr) { 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 { @@ -106,6 +123,7 @@ func (o *FitToPage) getSheetPrOption(pr *xlsxSheetPr) { *o = FitToPage(pr.PageSetUpPr.FitToPage) } +// setSheetPrOption implements the SheetPrOption interface. func (o AutoPageBreaks) setSheetPrOption(pr *xlsxSheetPr) { if pr.PageSetUpPr == nil { if !o { @@ -116,6 +134,7 @@ func (o AutoPageBreaks) setSheetPrOption(pr *xlsxSheetPr) { 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 { From 5dd00b9a004d5a7bc867ed09387cbd88fed240e4 Mon Sep 17 00:00:00 2001 From: "Michael W. Mitton" Date: Wed, 9 Jan 2019 10:12:53 -0500 Subject: [PATCH 050/957] Do not create a blank fill if no fill is specified in the style format --- styles.go | 10 +++++++--- styles_test.go | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/styles.go b/styles.go index d075266693..7ffc8ff17d 100644 --- a/styles.go +++ b/styles.go @@ -1907,9 +1907,11 @@ func (f *File) NewStyle(style string) (int, error) { s.Borders.Border = append(s.Borders.Border, setBorders(fs)) borderID = s.Borders.Count - 1 - s.Fills.Count++ - s.Fills.Fill = append(s.Fills.Fill, setFills(fs, true)) - fillID = s.Fills.Count - 1 + if fill := setFills(fs, true); fill != nil { + s.Fills.Count++ + s.Fills.Fill = append(s.Fills.Fill, fill) + fillID = s.Fills.Count - 1 + } applyAlignment, alignment := fs.Alignment != nil, setAlignment(fs) applyProtection, protection := fs.Protection != nil, setProtection(fs) @@ -2145,6 +2147,8 @@ func setFills(formatStyle *formatStyle, fg bool) *xlsxFill { pattern.BgColor.RGB = getPaletteColor(formatStyle.Fill.Color[0]) } fill.PatternFill = &pattern + default: + return nil } return &fill } diff --git a/styles_test.go b/styles_test.go index baa66f0f11..c6fbbef0b7 100644 --- a/styles_test.go +++ b/styles_test.go @@ -6,6 +6,40 @@ import ( "github.com/stretchr/testify/assert" ) +func TestStyleFill(t *testing.T) { + cases := []struct { + label string + format string + expectFill bool + }{{ + label: "no_fill", + format: `{"alignment":{"wrap_text":true}}`, + expectFill: false, + }, { + label: "fill", + format: `{"fill":{"type":"pattern","pattern":1,"color":["#000000"]}}`, + expectFill: true, + }} + + for _, testCase := range cases { + xl := NewFile() + const sheet = "Sheet1" + + styleID, err := xl.NewStyle(testCase.format) + if err != nil { + t.Fatalf("%v", err) + } + + styles := xl.stylesReader() + style := styles.CellXfs.Xf[styleID] + if testCase.expectFill { + assert.NotEqual(t, style.FillID, 0, testCase.label) + } else { + assert.Equal(t, style.FillID, 0, testCase.label) + } + } +} + func TestSetConditionalFormat(t *testing.T) { cases := []struct { label string From 725c1a0c40971282ff924f4802a57e4c17431691 Mon Sep 17 00:00:00 2001 From: Veniamin Albaev Date: Thu, 27 Dec 2018 17:28:28 +0300 Subject: [PATCH 051/957] New feature: File.DuplicateRowTo() duplicate row to specified row position. DuplicateRowTo() is similar to DuplicateRow() but copies specified row not just after specified source row but to any other specified position below or above source row. Also I made minor modifications of tests: using filepath.Join() instead of direct unix-way paths strings to avoid possible tests fails on other OS. --- datavalidation_test.go | 5 +- excelize.go | 12 +- excelize_test.go | 524 ++++++++++++++++++++++++++++++----------- go.mod | 3 +- go.sum | 6 +- rows.go | 57 +++-- 6 files changed, 434 insertions(+), 173 deletions(-) diff --git a/datavalidation_test.go b/datavalidation_test.go index 82bc42f23f..fad50c2933 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -10,13 +10,14 @@ package excelize import ( + "path/filepath" "testing" "github.com/stretchr/testify/assert" ) func TestDataValidation(t *testing.T) { - const resultFile = "./test/TestDataValidation.xlsx" + resultFile := filepath.Join("test", "TestDataValidation.xlsx") xlsx := NewFile() @@ -50,7 +51,7 @@ func TestDataValidation(t *testing.T) { } func TestDataValidationError(t *testing.T) { - const resultFile = "./test/TestDataValidationError.xlsx" + resultFile := filepath.Join("test", "TestDataValidationError.xlsx") xlsx := NewFile() xlsx.SetCellStr("Sheet1", "E1", "E1") diff --git a/excelize.go b/excelize.go index 61f6dd64e8..32f045153b 100644 --- a/excelize.go +++ b/excelize.go @@ -238,18 +238,16 @@ func (f *File) adjustRowDimensions(xlsx *xlsxWorksheet, rowIndex, offset int) { } for i, r := range xlsx.SheetData.Row { if r.R >= rowIndex { - f.ajustSingleRowDimensions(&xlsx.SheetData.Row[i], offset) + f.ajustSingleRowDimensions(&xlsx.SheetData.Row[i], r.R+offset) } } } -// ajustSingleRowDimensions provides a function to ajust single row -// dimensions. -func (f *File) ajustSingleRowDimensions(r *xlsxRow, offset int) { - r.R += offset +// ajustSingleRowDimensions provides a function to ajust single row dimensions. +func (f *File) ajustSingleRowDimensions(r *xlsxRow, row int) { + r.R = row for i, col := range r.C { - row, _ := strconv.Atoi(strings.Map(intOnlyMapF, col.R)) - r.C[i].R = string(strings.Map(letterOnlyMapF, col.R)) + strconv.Itoa(row+offset) + r.C[i].R = string(strings.Map(letterOnlyMapF, col.R)) + strconv.Itoa(r.R) } } diff --git a/excelize_test.go b/excelize_test.go index 1b6997ae52..6a106ec115 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -8,6 +8,7 @@ import ( _ "image/png" "io/ioutil" "os" + "path/filepath" "strconv" "strings" "testing" @@ -18,7 +19,7 @@ import ( func TestOpenFile(t *testing.T) { // Test update a XLSX file. - xlsx, err := OpenFile("./test/Book1.xlsx") + xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if !assert.NoError(t, err) { t.FailNow() } @@ -126,16 +127,16 @@ func TestOpenFile(t *testing.T) { for i := 1; i <= 300; i++ { xlsx.SetCellStr("Sheet3", "c"+strconv.Itoa(i), strconv.Itoa(i)) } - assert.NoError(t, xlsx.SaveAs("./test/TestOpenFile.xlsx")) + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestOpenFile.xlsx"))) } func TestSaveFile(t *testing.T) { - xlsx, err := OpenFile("./test/Book1.xlsx") + xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if !assert.NoError(t, err) { t.FailNow() } - assert.NoError(t, xlsx.SaveAs("./test/TestSaveFile.xlsx")) - xlsx, err = OpenFile("./test/TestSaveFile.xlsx") + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSaveFile.xlsx"))) + xlsx, err = OpenFile(filepath.Join("test", "TestSaveFile.xlsx")) if !assert.NoError(t, err) { t.FailNow() } @@ -143,7 +144,7 @@ func TestSaveFile(t *testing.T) { } func TestSaveAsWrongPath(t *testing.T) { - xlsx, err := OpenFile("./test/Book1.xlsx") + xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if assert.NoError(t, err) { // Test write file to not exist directory. err = xlsx.SaveAs("") @@ -154,26 +155,26 @@ func TestSaveAsWrongPath(t *testing.T) { } func TestAddPicture(t *testing.T) { - xlsx, err := OpenFile("./test/Book1.xlsx") + xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if !assert.NoError(t, err) { t.FailNow() } // Test add picture to worksheet with offset and location hyperlink. - err = xlsx.AddPicture("Sheet2", "I9", "./test/images/excel.jpg", + err = xlsx.AddPicture("Sheet2", "I9", filepath.Join("test", "images", "excel.jpg"), `{"x_offset": 140, "y_offset": 120, "hyperlink": "#Sheet2!D8", "hyperlink_type": "Location"}`) if !assert.NoError(t, err) { t.FailNow() } // Test add picture to worksheet with offset, external hyperlink and positioning. - err = xlsx.AddPicture("Sheet1", "F21", "./test/images/excel.png", + err = xlsx.AddPicture("Sheet1", "F21", filepath.Join("test", "images", "excel.jpg"), `{"x_offset": 10, "y_offset": 10, "hyperlink": "https://github.com/360EntSecGroup-Skylar/excelize", "hyperlink_type": "External", "positioning": "oneCell"}`) if !assert.NoError(t, err) { t.FailNow() } - file, err := ioutil.ReadFile("./test/images/excel.jpg") + file, err := ioutil.ReadFile(filepath.Join("test", "images", "excel.jpg")) if !assert.NoError(t, err) { t.FailNow() } @@ -183,24 +184,24 @@ func TestAddPicture(t *testing.T) { assert.NoError(t, err) // Test write file to given path. - err = xlsx.SaveAs("./test/TestAddPicture.xlsx") + err = xlsx.SaveAs(filepath.Join("test", "TestAddPicture.xlsx")) assert.NoError(t, err) } func TestAddPictureErrors(t *testing.T) { - xlsx, err := OpenFile("./test/Book1.xlsx") + xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if !assert.NoError(t, err) { t.FailNow() } // Test add picture to worksheet with invalid file path. - err = xlsx.AddPicture("Sheet1", "G21", "./test/not_exists_dir/not_exists.icon", "") + err = xlsx.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") } // Test add picture to worksheet with unsupport file type. - err = xlsx.AddPicture("Sheet1", "G21", "./test/Book1.xlsx", "") + err = xlsx.AddPicture("Sheet1", "G21", filepath.Join("test", "Book1.xlsx"), "") assert.EqualError(t, err, "unsupported image extension") err = xlsx.AddPictureFromBytes("Sheet1", "G21", "", "Excel Logo", "jpg", make([]byte, 1)) @@ -221,12 +222,12 @@ func TestBrokenFile(t *testing.T) { t.Run("SaveAsEmptyStruct", func(t *testing.T) { // Test write file with broken file struct with given path. - assert.NoError(t, xlsx.SaveAs("./test/TestBrokenFile.SaveAsEmptyStruct.xlsx")) + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestBrokenFile.SaveAsEmptyStruct.xlsx"))) }) t.Run("OpenBadWorkbook", func(t *testing.T) { // Test set active sheet without BookViews and Sheets maps in xl/workbook.xml. - f3, err := OpenFile("./test/BadWorkbook.xlsx") + f3, err := OpenFile(filepath.Join("test", "BadWorkbook.xlsx")) f3.GetActiveSheetIndex() f3.SetActiveSheet(2) assert.NoError(t, err) @@ -234,7 +235,7 @@ func TestBrokenFile(t *testing.T) { t.Run("OpenNotExistsFile", func(t *testing.T) { // Test open a XLSX file with given illegal path. - _, err := OpenFile("./test/NotExistsFile.xlsx") + _, err := OpenFile(filepath.Join("test", "NotExistsFile.xlsx")) if assert.Error(t, err) { assert.True(t, os.IsNotExist(err), "Expected os.IsNotExists(err) == true") } @@ -252,24 +253,25 @@ func TestNewFile(t *testing.T) { xlsx.SetActiveSheet(0) // Test add picture to sheet with scaling and positioning. - err := xlsx.AddPicture("Sheet1", "H2", "./test/images/excel.gif", `{"x_scale": 0.5, "y_scale": 0.5, "positioning": "absolute"}`) + err := xlsx.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 formatset. - err = xlsx.AddPicture("Sheet1", "C2", "./test/images/excel.png", "") + err = xlsx.AddPicture("Sheet1", "C2", filepath.Join("test", "images", "excel.png"), "") if !assert.NoError(t, err) { t.FailNow() } // Test add picture to worksheet with invalid formatset. - err = xlsx.AddPicture("Sheet1", "C2", "./test/images/excel.png", `{`) + err = xlsx.AddPicture("Sheet1", "C2", filepath.Join("test", "images", "excel.png"), `{`) if !assert.Error(t, err) { t.FailNow() } - assert.NoError(t, xlsx.SaveAs("./test/TestNewFile.xlsx")) + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestNewFile.xlsx"))) } func TestColWidth(t *testing.T) { @@ -278,7 +280,7 @@ func TestColWidth(t *testing.T) { xlsx.SetColWidth("Sheet1", "A", "B", 12) xlsx.GetColWidth("Sheet1", "A") xlsx.GetColWidth("Sheet1", "C") - err := xlsx.SaveAs("./test/TestColWidth.xlsx") + err := xlsx.SaveAs(filepath.Join("test", "TestColWidth.xlsx")) if err != nil { t.Error(err) } @@ -291,7 +293,7 @@ func TestRowHeight(t *testing.T) { xlsx.SetRowHeight("Sheet1", 4, 90) t.Log(xlsx.GetRowHeight("Sheet1", 1)) t.Log(xlsx.GetRowHeight("Sheet1", 0)) - err := xlsx.SaveAs("./test/TestRowHeight.xlsx") + err := xlsx.SaveAs(filepath.Join("test", "TestRowHeight.xlsx")) if !assert.NoError(t, err) { t.FailNow() } @@ -299,7 +301,7 @@ func TestRowHeight(t *testing.T) { } func TestSetCellHyperLink(t *testing.T) { - xlsx, err := OpenFile("./test/Book1.xlsx") + xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if err != nil { t.Log(err) } @@ -311,11 +313,11 @@ func TestSetCellHyperLink(t *testing.T) { xlsx.SetCellHyperLink("Sheet2", "D6", "Sheet1!D8", "Location") xlsx.SetCellHyperLink("Sheet2", "C3", "Sheet1!D8", "") xlsx.SetCellHyperLink("Sheet2", "", "Sheet1!D60", "Location") - assert.NoError(t, xlsx.SaveAs("./test/TestSetCellHyperLink.xlsx")) + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetCellHyperLink.xlsx"))) } func TestGetCellHyperLink(t *testing.T) { - xlsx, err := OpenFile("./test/Book1.xlsx") + xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if !assert.NoError(t, err) { t.FailNow() } @@ -331,7 +333,7 @@ func TestGetCellHyperLink(t *testing.T) { } func TestSetCellFormula(t *testing.T) { - xlsx, err := OpenFile("./test/Book1.xlsx") + xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if !assert.NoError(t, err) { t.FailNow() } @@ -341,45 +343,45 @@ func TestSetCellFormula(t *testing.T) { // Test set cell formula with illegal rows number. xlsx.SetCellFormula("Sheet1", "C", "SUM(Sheet2!D2,Sheet2!D9)") - assert.NoError(t, xlsx.SaveAs("./test/TestSetCellFormula.xlsx")) + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetCellFormula.xlsx"))) } func TestSetSheetBackground(t *testing.T) { - xlsx, err := OpenFile("./test/Book1.xlsx") + xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if !assert.NoError(t, err) { t.FailNow() } - err = xlsx.SetSheetBackground("Sheet2", "./test/images/background.jpg") + err = xlsx.SetSheetBackground("Sheet2", filepath.Join("test", "images", "background.jpg")) if !assert.NoError(t, err) { t.FailNow() } - err = xlsx.SetSheetBackground("Sheet2", "./test/images/background.jpg") + err = xlsx.SetSheetBackground("Sheet2", filepath.Join("test", "images", "background.jpg")) if !assert.NoError(t, err) { t.FailNow() } - assert.NoError(t, xlsx.SaveAs("./test/TestSetSheetBackground.xlsx")) + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetSheetBackground.xlsx"))) } func TestSetSheetBackgroundErrors(t *testing.T) { - xlsx, err := OpenFile("./test/Book1.xlsx") + xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if !assert.NoError(t, err) { t.FailNow() } - err = xlsx.SetSheetBackground("Sheet2", "./test/not_exists/not_exists.png") + err = xlsx.SetSheetBackground("Sheet2", filepath.Join("test", "not_exists", "not_exists.png")) if assert.Error(t, err) { assert.True(t, os.IsNotExist(err), "Expected os.IsNotExists(err) == true") } - err = xlsx.SetSheetBackground("Sheet2", "./test/Book1.xlsx") + err = xlsx.SetSheetBackground("Sheet2", filepath.Join("test", "Book1.xlsx")) assert.EqualError(t, err, "unsupported image extension") } func TestMergeCell(t *testing.T) { - xlsx, err := OpenFile("./test/Book1.xlsx") + xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if !assert.NoError(t, err) { t.FailNow() } @@ -401,7 +403,7 @@ func TestMergeCell(t *testing.T) { xlsx.GetCellValue("Sheet2", "A6") // Merged cell ref is single coordinate. xlsx.GetCellFormula("Sheet1", "G12") - assert.NoError(t, xlsx.SaveAs("./test/TestMergeCell.xlsx")) + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestMergeCell.xlsx"))) } func TestGetMergeCells(t *testing.T) { @@ -432,7 +434,7 @@ func TestGetMergeCells(t *testing.T) { }, } - xlsx, err := OpenFile("./test/MergeCell.xlsx") + xlsx, err := OpenFile(filepath.Join("test", "MergeCell.xlsx")) if !assert.NoError(t, err) { t.FailNow() } @@ -476,7 +478,7 @@ func TestSetCellStyleAlignment(t *testing.T) { // Test get cell style with given illegal rows number. xlsx.GetCellStyle("Sheet1", "A") - assert.NoError(t, xlsx.SaveAs("./test/TestSetCellStyleAlignment.xlsx")) + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetCellStyleAlignment.xlsx"))) } func TestSetCellStyleBorder(t *testing.T) { @@ -514,7 +516,7 @@ func TestSetCellStyleBorder(t *testing.T) { xlsx.SetCellStyle("Sheet1", "O22", "O22", style) - assert.NoError(t, xlsx.SaveAs("./test/TestSetCellStyleBorder.xlsx")) + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetCellStyleBorder.xlsx"))) } func TestSetCellStyleBorderErrors(t *testing.T) { @@ -571,7 +573,7 @@ func TestSetCellStyleNumberFormat(t *testing.T) { } xlsx.SetCellStyle("Sheet2", "L33", "L33", style) - assert.NoError(t, xlsx.SaveAs("./test/TestSetCellStyleNumberFormat.xlsx")) + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetCellStyleNumberFormat.xlsx"))) } func TestSetCellStyleCurrencyNumberFormat(t *testing.T) { @@ -597,7 +599,7 @@ func TestSetCellStyleCurrencyNumberFormat(t *testing.T) { xlsx.SetCellStyle("Sheet1", "A2", "A2", style) - assert.NoError(t, xlsx.SaveAs("./test/TestSetCellStyleCurrencyNumberFormat.TestBook3.xlsx")) + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetCellStyleCurrencyNumberFormat.TestBook3.xlsx"))) }) t.Run("TestBook4", func(t *testing.T) { @@ -632,7 +634,7 @@ func TestSetCellStyleCurrencyNumberFormat(t *testing.T) { } xlsx.SetCellStyle("Sheet1", "A2", "A2", style) - assert.NoError(t, xlsx.SaveAs("./test/TestSetCellStyleCurrencyNumberFormat.TestBook4.xlsx")) + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetCellStyleCurrencyNumberFormat.TestBook4.xlsx"))) }) } @@ -651,7 +653,7 @@ func TestSetCellStyleCustomNumberFormat(t *testing.T) { } xlsx.SetCellStyle("Sheet1", "A2", "A2", style) - assert.NoError(t, xlsx.SaveAs("./test/TestSetCellStyleCustomNumberFormat.xlsx")) + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetCellStyleCustomNumberFormat.xlsx"))) } func TestSetCellStyleFill(t *testing.T) { @@ -686,7 +688,7 @@ func TestSetCellStyleFill(t *testing.T) { } xlsx.SetCellStyle("Sheet1", "O23", "O23", style) - assert.NoError(t, xlsx.SaveAs("./test/TestSetCellStyleFill.xlsx")) + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetCellStyleFill.xlsx"))) } func TestSetCellStyleFont(t *testing.T) { @@ -731,7 +733,7 @@ func TestSetCellStyleFont(t *testing.T) { xlsx.SetCellStyle("Sheet2", "A5", "A5", style) - assert.NoError(t, xlsx.SaveAs("./test/TestSetCellStyleFont.xlsx")) + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetCellStyleFont.xlsx"))) } func TestSetCellStyleProtection(t *testing.T) { @@ -747,7 +749,7 @@ func TestSetCellStyleProtection(t *testing.T) { } xlsx.SetCellStyle("Sheet2", "A6", "A6", style) - err = xlsx.SaveAs("./test/TestSetCellStyleProtection.xlsx") + err = xlsx.SaveAs(filepath.Join("test", "TestSetCellStyleProtection.xlsx")) if !assert.NoError(t, err) { t.FailNow() } @@ -761,7 +763,7 @@ func TestSetDeleteSheet(t *testing.T) { } xlsx.DeleteSheet("XLSXSheet3") - assert.NoError(t, xlsx.SaveAs("./test/TestSetDeleteSheet.TestBook3.xlsx")) + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetDeleteSheet.TestBook3.xlsx"))) }) t.Run("TestBook4", func(t *testing.T) { @@ -772,7 +774,7 @@ func TestSetDeleteSheet(t *testing.T) { xlsx.DeleteSheet("Sheet1") xlsx.AddComment("Sheet1", "A1", "") xlsx.AddComment("Sheet1", "A1", `{"author":"Excelize: ","text":"This is a comment."}`) - assert.NoError(t, xlsx.SaveAs("./test/TestSetDeleteSheet.TestBook4.xlsx")) + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetDeleteSheet.TestBook4.xlsx"))) }) } @@ -818,7 +820,7 @@ func TestSheetVisibility(t *testing.T) { xlsx.SetSheetVisible("Sheet1", true) xlsx.GetSheetVisible("Sheet1") - assert.NoError(t, xlsx.SaveAs("./test/TestSheetVisibility.xlsx")) + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSheetVisibility.xlsx"))) } func TestRowVisibility(t *testing.T) { @@ -831,7 +833,7 @@ func TestRowVisibility(t *testing.T) { xlsx.SetRowVisible("Sheet3", 2, true) xlsx.GetRowVisible("Sheet3", 2) - assert.NoError(t, xlsx.SaveAs("./test/TestRowVisibility.xlsx")) + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestRowVisibility.xlsx"))) } func TestColumnVisibility(t *testing.T) { @@ -845,7 +847,7 @@ func TestColumnVisibility(t *testing.T) { xlsx.SetColVisible("Sheet1", "F", true) xlsx.GetColVisible("Sheet1", "F") xlsx.SetColVisible("Sheet3", "E", false) - assert.NoError(t, xlsx.SaveAs("./test/TestColumnVisibility.xlsx")) + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestColumnVisibility.xlsx"))) }) t.Run("TestBook3", func(t *testing.T) { @@ -872,7 +874,7 @@ func TestCopySheet(t *testing.T) { xlsx.SetCellValue("Sheet4", "F1", "Hello") assert.NotEqual(t, "Hello", xlsx.GetCellValue("Sheet1", "F1")) - assert.NoError(t, xlsx.SaveAs("./test/TestCopySheet.xlsx")) + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestCopySheet.xlsx"))) } func TestCopySheetError(t *testing.T) { @@ -886,7 +888,7 @@ func TestCopySheetError(t *testing.T) { t.FailNow() } - assert.NoError(t, xlsx.SaveAs("./test/TestCopySheetError.xlsx")) + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestCopySheetError.xlsx"))) } func TestAddTable(t *testing.T) { @@ -910,7 +912,7 @@ func TestAddTable(t *testing.T) { t.FailNow() } - assert.NoError(t, xlsx.SaveAs("./test/TestAddTable.xlsx")) + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestAddTable.xlsx"))) } func TestAddShape(t *testing.T) { @@ -925,7 +927,7 @@ func TestAddShape(t *testing.T) { xlsx.AddShape("Sheet3", "H1", `{"type":"ellipseRibbon", "color":{"line":"#4286f4","fill":"#8eb9ff"}, "paragraph":[{"font":{"bold":true,"italic":true,"family":"Berlin Sans FB Demi","size":36,"color":"#777777","underline":"single"}}], "height": 90}`) xlsx.AddShape("Sheet3", "H1", "") - assert.NoError(t, xlsx.SaveAs("./test/TestAddShape.xlsx")) + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestAddShape.xlsx"))) } func TestAddComments(t *testing.T) { @@ -938,12 +940,14 @@ func TestAddComments(t *testing.T) { xlsx.AddComment("Sheet1", "A30", `{"author":"`+s+`","text":"`+s+`"}`) xlsx.AddComment("Sheet2", "B7", `{"author":"Excelize: ","text":"This is a comment."}`) - if assert.NoError(t, xlsx.SaveAs("./test/TestAddComments.xlsx")) { + if assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestAddComments.xlsx"))) { assert.Len(t, xlsx.GetComments(), 2) } } func TestAutoFilter(t *testing.T) { + outFile := filepath.Join("test", "TestAutoFilter%d.xlsx") + xlsx, err := prepareTestBook1() if !assert.NoError(t, err) { t.FailNow() @@ -964,7 +968,7 @@ func TestAutoFilter(t *testing.T) { t.Run(fmt.Sprintf("Expression%d", i+1), func(t *testing.T) { err = xlsx.AutoFilter("Sheet3", "D4", "B1", format) if assert.NoError(t, err) { - assert.NoError(t, xlsx.SaveAs(fmt.Sprintf("./test/TestAutoFilter%d.xlsx", i+1))) + assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, i+1))) } }) } @@ -972,6 +976,8 @@ func TestAutoFilter(t *testing.T) { } func TestAutoFilterError(t *testing.T) { + outFile := filepath.Join("test", "TestAutoFilterError%d.xlsx") + xlsx, err := prepareTestBook1() if !assert.NoError(t, err) { t.FailNow() @@ -989,14 +995,14 @@ func TestAutoFilterError(t *testing.T) { t.Run(fmt.Sprintf("Expression%d", i+1), func(t *testing.T) { err = xlsx.AutoFilter("Sheet3", "D4", "B1", format) if assert.Error(t, err) { - assert.NoError(t, xlsx.SaveAs(fmt.Sprintf("./test/TestAutoFilterError%d.xlsx", i+1))) + assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, i+1))) } }) } } func TestAddChart(t *testing.T) { - xlsx, err := OpenFile("./test/Book1.xlsx") + xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if !assert.NoError(t, err) { t.FailNow() } @@ -1037,7 +1043,7 @@ func TestAddChart(t *testing.T) { xlsx.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"}],"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":"Fruit 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"}`) xlsx.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"}],"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":"Fruit 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"}`) - assert.NoError(t, xlsx.SaveAs("./test/TestAddChart.xlsx")) + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestAddChart.xlsx"))) } func TestInsertCol(t *testing.T) { @@ -1057,7 +1063,7 @@ func TestInsertCol(t *testing.T) { xlsx.InsertCol("Sheet1", "A") - assert.NoError(t, xlsx.SaveAs("./test/TestInsertCol.xlsx")) + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestInsertCol.xlsx"))) } func TestRemoveCol(t *testing.T) { @@ -1075,7 +1081,7 @@ func TestRemoveCol(t *testing.T) { xlsx.RemoveCol("Sheet1", "A") xlsx.RemoveCol("Sheet1", "A") - assert.NoError(t, xlsx.SaveAs("./test/TestRemoveCol.xlsx")) + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestRemoveCol.xlsx"))) } func TestInsertRow(t *testing.T) { @@ -1090,84 +1096,321 @@ func TestInsertRow(t *testing.T) { xlsx.InsertRow("Sheet1", -1) xlsx.InsertRow("Sheet1", 4) - assert.NoError(t, xlsx.SaveAs("./test/TestInsertRow.xlsx")) + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestInsertRow.xlsx"))) } -func TestDuplicateRow(t *testing.T) { - const ( - file = "./test/TestDuplicateRow" + - ".%s.xlsx" - sheet = "Sheet1" - a1 = "A1" - b1 = "B1" - a2 = "A2" - b2 = "B2" - a3 = "A3" - b3 = "B3" - a4 = "A4" - b4 = "B4" - a1Value = "A1 value" - a2Value = "A2 value" - a3Value = "A3 value" - bnValue = "Bn value" - ) +// Testing internal sructure 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) { xlsx := NewFile() - xlsx.SetCellStr(sheet, a1, a1Value) - xlsx.SetCellStr(sheet, b1, bnValue) + sheet1 := xlsx.GetSheetName(1) + r := xlsx.workSheetReader(sheet1) + xlsx.InsertRow(sheet1, 0) + assert.Len(t, r.SheetData.Row, 0) + xlsx.InsertRow(sheet1, 1) + assert.Len(t, r.SheetData.Row, 0) + xlsx.InsertRow(sheet1, 99) + assert.Len(t, r.SheetData.Row, 0) + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestInsertRowInEmptyFile.xlsx"))) +} + +func TestDuplicateRow(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 { + f.SetCellStr(sheet, cell, val) + + } + return f + } t.Run("FromSingleRow", func(t *testing.T) { + xlsx := NewFile() + xlsx.SetCellStr(sheet, "A1", cells["A1"]) + xlsx.SetCellStr(sheet, "B1", cells["B1"]) + xlsx.DuplicateRow(sheet, 1) - xlsx.DuplicateRow(sheet, 2) + if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.FromSingleRow_1"))) { + t.FailNow() + } + expect := map[string]string{ + "A1": cells["A1"], "B1": cells["B1"], + "A2": cells["A1"], "B2": cells["B1"], + } + for cell, val := range expect { + if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { + t.FailNow() + } + } - if assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(file, "TestDuplicateRow.FromSingleRow"))) { - assert.Equal(t, a1Value, xlsx.GetCellValue(sheet, a1)) - assert.Equal(t, a1Value, xlsx.GetCellValue(sheet, a2)) - assert.Equal(t, a1Value, xlsx.GetCellValue(sheet, a3)) - assert.Equal(t, bnValue, xlsx.GetCellValue(sheet, b1)) - assert.Equal(t, bnValue, xlsx.GetCellValue(sheet, b2)) - assert.Equal(t, bnValue, xlsx.GetCellValue(sheet, b3)) + xlsx.DuplicateRow(sheet, 2) + if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.FromSingleRow_2"))) { + t.FailNow() + } + expect = map[string]string{ + "A1": cells["A1"], "B1": cells["B1"], + "A2": cells["A1"], "B2": cells["B1"], + "A3": cells["A1"], "B3": cells["B1"], + } + for cell, val := range expect { + if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { + t.FailNow() + } } }) t.Run("UpdateDuplicatedRows", func(t *testing.T) { - xlsx.SetCellStr(sheet, a2, a2Value) - xlsx.SetCellStr(sheet, a3, a3Value) - - if assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(file, "TestDuplicateRow.UpdateDuplicatedRows"))) { - assert.Equal(t, a1Value, xlsx.GetCellValue(sheet, a1)) - assert.Equal(t, a2Value, xlsx.GetCellValue(sheet, a2)) - assert.Equal(t, a3Value, xlsx.GetCellValue(sheet, a3)) - assert.Equal(t, bnValue, xlsx.GetCellValue(sheet, b1)) - assert.Equal(t, bnValue, xlsx.GetCellValue(sheet, b2)) - assert.Equal(t, bnValue, xlsx.GetCellValue(sheet, b3)) + xlsx := NewFile() + xlsx.SetCellStr(sheet, "A1", cells["A1"]) + xlsx.SetCellStr(sheet, "B1", cells["B1"]) + + xlsx.DuplicateRow(sheet, 1) + + xlsx.SetCellStr(sheet, "A2", cells["A2"]) + xlsx.SetCellStr(sheet, "B2", cells["B2"]) + + if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.UpdateDuplicatedRows"))) { + t.FailNow() + } + expect := map[string]string{ + "A1": cells["A1"], "B1": cells["B1"], + "A2": cells["A2"], "B2": cells["B2"], + } + for cell, val := range expect { + if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { + t.FailNow() + } } }) - t.Run("FromFirstOfMultipleRows", func(t *testing.T) { + t.Run("FirstOfMultipleRows", func(t *testing.T) { + xlsx := newFileWithDefaults() + xlsx.DuplicateRow(sheet, 1) - if assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(file, "TestDuplicateRow.FromFirstOfMultipleRows"))) { - assert.Equal(t, a1Value, xlsx.GetCellValue(sheet, a1)) - assert.Equal(t, a1Value, xlsx.GetCellValue(sheet, a2)) - assert.Equal(t, a2Value, xlsx.GetCellValue(sheet, a3)) - assert.Equal(t, a3Value, xlsx.GetCellValue(sheet, a4)) - assert.Equal(t, bnValue, xlsx.GetCellValue(sheet, b1)) - assert.Equal(t, bnValue, xlsx.GetCellValue(sheet, b2)) - assert.Equal(t, bnValue, xlsx.GetCellValue(sheet, b3)) - assert.Equal(t, bnValue, xlsx.GetCellValue(sheet, b4)) + if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.FirstOfMultipleRows"))) { + t.FailNow() + } + expect := map[string]string{ + "A1": cells["A1"], "B1": cells["B1"], + "A2": cells["A1"], "B2": cells["B1"], + "A3": cells["A2"], "B3": cells["B2"], + "A4": cells["A3"], "B4": cells["B3"], + } + for cell, val := range expect { + if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { + t.FailNow() + } } }) - t.Run("ZeroAndNegativeRowNum", func(t *testing.T) { - xlsx.DuplicateRow(sheet, -1) + t.Run("ZeroWithNoRows", func(t *testing.T) { + xlsx := NewFile() + xlsx.DuplicateRow(sheet, 0) - if assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(file, "TestDuplicateRow.ZeroAndNegativeRowNum"))) { - assert.Equal(t, "", xlsx.GetCellValue(sheet, a1)) - assert.Equal(t, "", xlsx.GetCellValue(sheet, b1)) - assert.Equal(t, a1Value, xlsx.GetCellValue(sheet, a2)) - assert.Equal(t, bnValue, xlsx.GetCellValue(sheet, b2)) + + if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.ZeroWithNoRows"))) { + t.FailNow() + } + assert.Equal(t, "", xlsx.GetCellValue(sheet, "A1")) + assert.Equal(t, "", xlsx.GetCellValue(sheet, "B1")) + assert.Equal(t, "", xlsx.GetCellValue(sheet, "A2")) + assert.Equal(t, "", xlsx.GetCellValue(sheet, "B2")) + expect := map[string]string{ + "A1": "", "B1": "", + "A2": "", "B2": "", + } + for cell, val := range expect { + if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { + t.FailNow() + } + } + }) + + t.Run("MiddleRowOfEmptyFile", func(t *testing.T) { + xlsx := NewFile() + + xlsx.DuplicateRow(sheet, 99) + + if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.MiddleRowOfEmptyFile"))) { + t.FailNow() + } + expect := map[string]string{ + "A98": "", + "A99": "", + "A100": "", + } + for cell, val := range expect { + if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { + t.FailNow() + } + } + }) + + t.Run("WithLargeOffsetToMiddleOfData", func(t *testing.T) { + xlsx := newFileWithDefaults() + + xlsx.DuplicateRowTo(sheet, 1, 3) + + if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.WithLargeOffsetToMiddleOfData"))) { + t.FailNow() + } + expect := map[string]string{ + "A1": cells["A1"], "B1": cells["B1"], + "A2": cells["A2"], "B2": cells["B2"], + "A3": cells["A1"], "B3": cells["B1"], + "A4": cells["A3"], "B4": cells["B3"], + } + for cell, val := range expect { + if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { + t.FailNow() + } + } + }) + + t.Run("WithLargeOffsetToEmptyRows", func(t *testing.T) { + xlsx := newFileWithDefaults() + + xlsx.DuplicateRowTo(sheet, 1, 7) + + if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.WithLargeOffsetToEmptyRows"))) { + t.FailNow() + } + expect := map[string]string{ + "A1": cells["A1"], "B1": cells["B1"], + "A2": cells["A2"], "B2": cells["B2"], + "A3": cells["A3"], "B3": cells["B3"], + "A7": cells["A1"], "B7": cells["B1"], + } + for cell, val := range expect { + if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { + t.FailNow() + } + } + }) + + t.Run("InsertBefore", func(t *testing.T) { + xlsx := newFileWithDefaults() + + xlsx.DuplicateRowTo(sheet, 2, 1) + + if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.InsertBefore"))) { + t.FailNow() + } + + expect := map[string]string{ + "A1": cells["A2"], "B1": cells["B2"], + "A2": cells["A1"], "B2": cells["B1"], + "A3": cells["A2"], "B3": cells["B2"], + "A4": cells["A3"], "B4": cells["B3"], + } + for cell, val := range expect { + if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { + t.FailNow() + } } }) + + t.Run("InsertBeforeWithLargeOffset", func(t *testing.T) { + xlsx := newFileWithDefaults() + + xlsx.DuplicateRowTo(sheet, 3, 1) + + if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.InsertBeforeWithLargeOffset"))) { + t.FailNow() + } + + expect := map[string]string{ + "A1": cells["A3"], "B1": cells["B3"], + "A2": cells["A1"], "B2": cells["B1"], + "A3": cells["A2"], "B3": cells["B2"], + "A4": cells["A3"], "B4": cells["B3"], + } + for cell, val := range expect { + if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell)) { + t.FailNow() + } + } + }) +} + +func TestDuplicateRowInvalidRownum(t *testing.T) { + const sheet = "Sheet1" + outFile := filepath.Join("test", "TestDuplicateRowInvalidRownum.%s.xlsx") + + cells := map[string]string{ + "A1": "A1 Value", + "A2": "A2 Value", + "A3": "A3 Value", + "B1": "B1 Value", + "B2": "B2 Value", + "B3": "B3 Value", + } + + testRows := []int{-2, -1} + + testRowPairs := []struct { + row1 int + row2 int + }{ + {-1, -1}, + {-1, 0}, + {-1, 1}, + {0, -1}, + {0, 0}, + {0, 1}, + {1, -1}, + {1, 1}, + {1, 0}, + } + + for i, row := range testRows { + name := fmt.Sprintf("TestRow_%d", i+1) + t.Run(name, func(t *testing.T) { + xlsx := NewFile() + for col, val := range cells { + xlsx.SetCellStr(sheet, col, val) + } + xlsx.DuplicateRow(sheet, row) + + for col, val := range cells { + if !assert.Equal(t, val, xlsx.GetCellValue(sheet, col)) { + t.FailNow() + } + } + assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, name))) + }) + } + + for i, pair := range testRowPairs { + name := fmt.Sprintf("TestRowPair_%d", i+1) + t.Run(name, func(t *testing.T) { + xlsx := NewFile() + for col, val := range cells { + xlsx.SetCellStr(sheet, col, val) + } + xlsx.DuplicateRowTo(sheet, pair.row1, pair.row2) + + for col, val := range cells { + if !assert.Equal(t, val, xlsx.GetCellValue(sheet, col)) { + t.FailNow() + } + } + assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, name))) + }) + } } func TestSetPane(t *testing.T) { @@ -1181,7 +1424,7 @@ func TestSetPane(t *testing.T) { xlsx.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"}]}`) xlsx.SetPanes("Panes 4", "") - assert.NoError(t, xlsx.SaveAs("./test/TestSetPane.xlsx")) + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetPane.xlsx"))) } func TestRemoveRow(t *testing.T) { @@ -1208,7 +1451,7 @@ func TestRemoveRow(t *testing.T) { xlsx.RemoveRow("Sheet1", 1) xlsx.RemoveRow("Sheet1", 0) - assert.NoError(t, xlsx.SaveAs("./test/TestRemoveRow.xlsx")) + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestRemoveRow.xlsx"))) } func TestConditionalFormat(t *testing.T) { @@ -1265,7 +1508,7 @@ func TestConditionalFormat(t *testing.T) { // Test set invalid format set in conditional format xlsx.SetConditionalFormat("Sheet1", "L1:L10", "") - err = xlsx.SaveAs("./test/TestConditionalFormat.xlsx") + err = xlsx.SaveAs(filepath.Join("test", "TestConditionalFormat.xlsx")) if !assert.NoError(t, err) { t.FailNow() } @@ -1276,7 +1519,7 @@ func TestConditionalFormat(t *testing.T) { xlsx.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 shold not return error. - xlsx, err = OpenFile("./test/Book1.xlsx") + xlsx, err = OpenFile(filepath.Join("test", "Book1.xlsx")) if !assert.NoError(t, err) { t.FailNow() } @@ -1308,7 +1551,7 @@ func TestTitleToNumber(t *testing.T) { } func TestSharedStrings(t *testing.T) { - xlsx, err := OpenFile("./test/SharedStrings.xlsx") + xlsx, err := OpenFile(filepath.Join("test", "SharedStrings.xlsx")) if !assert.NoError(t, err) { t.FailNow() } @@ -1316,7 +1559,7 @@ func TestSharedStrings(t *testing.T) { } func TestSetSheetRow(t *testing.T) { - xlsx, err := OpenFile("./test/Book1.xlsx") + xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if !assert.NoError(t, err) { t.FailNow() } @@ -1326,11 +1569,11 @@ func TestSetSheetRow(t *testing.T) { xlsx.SetSheetRow("Sheet1", "B27", []interface{}{}) xlsx.SetSheetRow("Sheet1", "B27", &xlsx) - assert.NoError(t, xlsx.SaveAs("./test/TestSetSheetRow.xlsx")) + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetSheetRow.xlsx"))) } func TestRows(t *testing.T) { - xlsx, err := OpenFile("./test/Book1.xlsx") + xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if !assert.NoError(t, err) { t.FailNow() } @@ -1365,7 +1608,7 @@ func TestRows(t *testing.T) { } func TestRowsError(t *testing.T) { - xlsx, err := OpenFile("./test/Book1.xlsx") + xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if !assert.NoError(t, err) { t.FailNow() } @@ -1383,12 +1626,12 @@ func TestOutlineLevel(t *testing.T) { xlsx.SetColOutlineLevel("Sheet2", "B", 2) xlsx.SetRowOutlineLevel("Sheet1", 2, 1) xlsx.GetRowOutlineLevel("Sheet1", 2) - err := xlsx.SaveAs("./test/TestOutlineLevel.xlsx") + err := xlsx.SaveAs(filepath.Join("test", "TestOutlineLevel.xlsx")) if !assert.NoError(t, err) { t.FailNow() } - xlsx, err = OpenFile("./test/Book1.xlsx") + xlsx, err = OpenFile(filepath.Join("test", "Book1.xlsx")) if !assert.NoError(t, err) { t.FailNow() } @@ -1422,7 +1665,7 @@ func TestHSL(t *testing.T) { } func TestSearchSheet(t *testing.T) { - xlsx, err := OpenFile("./test/SharedStrings.xlsx") + xlsx, err := OpenFile(filepath.Join("test", "SharedStrings.xlsx")) if !assert.NoError(t, err) { t.FailNow() } @@ -1445,17 +1688,17 @@ func TestProtectSheet(t *testing.T) { EditScenarios: false, }) - assert.NoError(t, xlsx.SaveAs("./test/TestProtectSheet.xlsx")) + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestProtectSheet.xlsx"))) } func TestUnprotectSheet(t *testing.T) { - xlsx, err := OpenFile("./test/Book1.xlsx") + xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if !assert.NoError(t, err) { t.FailNow() } xlsx.UnprotectSheet("Sheet1") - assert.NoError(t, xlsx.SaveAs("./test/TestUnprotectSheet.xlsx")) + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestUnprotectSheet.xlsx"))) } func trimSliceSpace(s []string) []string { @@ -1470,25 +1713,25 @@ func trimSliceSpace(s []string) []string { } func prepareTestBook1() (*File, error) { - xlsx, err := OpenFile("./test/Book1.xlsx") + xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if err != nil { return nil, err } - err = xlsx.AddPicture("Sheet2", "I9", "./test/images/excel.jpg", + err = xlsx.AddPicture("Sheet2", "I9", filepath.Join("test", "images", "excel.jpg"), `{"x_offset": 140, "y_offset": 120, "hyperlink": "#Sheet2!D8", "hyperlink_type": "Location"}`) if err != nil { return nil, err } // Test add picture to worksheet with offset, external hyperlink and positioning. - err = xlsx.AddPicture("Sheet1", "F21", "./test/images/excel.png", + err = xlsx.AddPicture("Sheet1", "F21", filepath.Join("test", "images", "excel.png"), `{"x_offset": 10, "y_offset": 10, "hyperlink": "https://github.com/360EntSecGroup-Skylar/excelize", "hyperlink_type": "External", "positioning": "oneCell"}`) if err != nil { return nil, err } - file, err := ioutil.ReadFile("./test/images/excel.jpg") + file, err := ioutil.ReadFile(filepath.Join("test", "images", "excel.jpg")) if err != nil { return nil, err } @@ -1510,12 +1753,13 @@ func prepareTestBook3() (*File, error) { xlsx.SetCellStr("Sheet1", "B20", "42") xlsx.SetActiveSheet(0) - err := xlsx.AddPicture("Sheet1", "H2", "./test/images/excel.gif", `{"x_scale": 0.5, "y_scale": 0.5, "positioning": "absolute"}`) + err := xlsx.AddPicture("Sheet1", "H2", filepath.Join("test", "images", "excel.gif"), + `{"x_scale": 0.5, "y_scale": 0.5, "positioning": "absolute"}`) if err != nil { return nil, err } - err = xlsx.AddPicture("Sheet1", "C2", "./test/images/excel.png", "") + err = xlsx.AddPicture("Sheet1", "C2", filepath.Join("test", "images", "excel.png"), "") if err != nil { return nil, err } diff --git a/go.mod b/go.mod index 4ecf6ee975..b96dbe2c5f 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,5 @@ module github.com/360EntSecGroup-Skylar/excelize require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/stretchr/testify v1.2.3-0.20181224173747-660f15d67dbb + github.com/stretchr/testify v1.3.0 ) diff --git a/go.sum b/go.sum index 8dce13c35a..106a41735b 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,10 @@ +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/stretchr/testify v1.2.3-0.20181224173747-660f15d67dbb h1:cRItZejS4Ok67vfCdrbGIaqk86wmtQNOjVD7jSyS2aw= -github.com/stretchr/testify v1.2.3-0.20181224173747-660f15d67dbb/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= diff --git a/rows.go b/rows.go index d3fe6e0994..3984216036 100644 --- a/rows.go +++ b/rows.go @@ -376,38 +376,55 @@ func (f *File) InsertRow(sheet string, row int) { // xlsx.DuplicateRow("Sheet1", 2) // func (f *File) DuplicateRow(sheet string, row int) { - if row < 0 { + f.DuplicateRowTo(sheet, row, row+1) +} + +// DuplicateRowTo inserts a copy of specified row at specified row position +// movig down exists rows aftet target position +// +// xlsx.DuplicateRowTo("Sheet1", 2, 7) +// +func (f *File) DuplicateRowTo(sheet string, row, row2 int) { + if row <= 0 || row2 <= 0 || row == row2 { return } - row2 := row + 1 - f.adjustHelper(sheet, -1, row2, 1) - xlsx := f.workSheetReader(sheet) - idx := -1 - idx2 := -1 + ws := f.workSheetReader(sheet) - for i, r := range xlsx.SheetData.Row { + var ok bool + var rowCopy xlsxRow + + for i, r := range ws.SheetData.Row { if r.R == row { - idx = i - } else if r.R == row2 { - idx2 = i - } - if idx != -1 && idx2 != -1 { + rowCopy = ws.SheetData.Row[i] + ok = true break } } + if !ok { + return + } - if idx == -1 || (idx2 == -1 && len(xlsx.SheetData.Row) >= row2) { + f.adjustHelper(sheet, -1, row2, 1) + + idx2 := -1 + for i, r := range ws.SheetData.Row { + if r.R == row2 { + idx2 = i + break + } + } + if idx2 == -1 && len(ws.SheetData.Row) >= row2 { return } - rowData := xlsx.SheetData.Row[idx] - cols := make([]xlsxC, 0, len(rowData.C)) - rowData.C = append(cols, rowData.C...) - f.ajustSingleRowDimensions(&rowData, 1) + + rowCopy.C = append(make([]xlsxC, 0, len(rowCopy.C)), rowCopy.C...) + f.ajustSingleRowDimensions(&rowCopy, row2) + if idx2 != -1 { - xlsx.SheetData.Row[idx2] = rowData + ws.SheetData.Row[idx2] = rowCopy } else { - xlsx.SheetData.Row = append(xlsx.SheetData.Row, rowData) + ws.SheetData.Row = append(ws.SheetData.Row, rowCopy) } } @@ -446,7 +463,7 @@ func checkRow(xlsx *xlsxWorksheet) { if lenCol < endCol { oldRow := xlsx.SheetData.Row[k].C xlsx.SheetData.Row[k].C = xlsx.SheetData.Row[k].C[:0] - tmp := []xlsxC{} + var tmp []xlsxC for i := 0; i < endCol; i++ { buffer.WriteString(ToAlphaString(i)) buffer.WriteString(strconv.Itoa(endRow)) From 81948d9e1ee807a1e5f9b654f2edc47b121c4b0e Mon Sep 17 00:00:00 2001 From: kkxkkxkkgh Date: Sun, 13 Jan 2019 21:58:50 +0800 Subject: [PATCH 052/957] The function SetPageLayout support set paper size --- sheet.go | 149 +++++++++++++++++++++++++++++++++++++++++++++++- sheet_test.go | 13 +++++ xmlWorksheet.go | 2 +- 3 files changed, 160 insertions(+), 4 deletions(-) diff --git a/sheet.go b/sheet.go index f21fc9b8df..cb1c08e83c 100644 --- a/sheet.go +++ b/sheet.go @@ -803,9 +803,13 @@ type PageLayoutOptionPtr interface { getPageLayout(layout *xlsxPageSetUp) } -// PageLayoutOrientation defines the orientation of page layout for a -// worksheet. -type PageLayoutOrientation string +type ( + // PageLayoutOrientation defines the orientation of page layout for a + // worksheet. + PageLayoutOrientation string + // PageLayoutPaperSize defines the paper size of the worksheet + PageLayoutPaperSize int +) const ( // OrientationPortrait indicates page layout orientation id portrait. @@ -829,10 +833,148 @@ func (o *PageLayoutOrientation) getPageLayout(ps *xlsxPageSetUp) { *o = PageLayoutOrientation(ps.Orientation) } +// setPageLayout provides a method to set the paper size for the worksheet. +func (p PageLayoutPaperSize) setPageLayout(ps *xlsxPageSetUp) { + ps.PaperSize = 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 == 0 { + *p = 1 + return + } + *p = PageLayoutPaperSize(ps.PaperSize) +} + // SetPageLayout provides a function to sets worksheet page layout. // // Available options: // PageLayoutOrientation(string) +// PageLayoutPaperSize(int) +// +// 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 double postcard (200 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 := f.workSheetReader(sheet) ps := s.PageSetUp @@ -851,6 +993,7 @@ func (f *File) SetPageLayout(sheet string, opts ...PageLayoutOption) error { // // Available options: // PageLayoutOrientation(string) +// PageLayoutPaperSize(int) func (f *File) GetPageLayout(sheet string, opts ...PageLayoutOptionPtr) error { s := f.workSheetReader(sheet) ps := s.PageSetUp diff --git a/sheet_test.go b/sheet_test.go index e4b4700f53..1307dc5aec 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -19,6 +19,12 @@ func ExampleFile_SetPageLayout() { ); err != nil { panic(err) } + if err := xl.SetPageLayout( + "Sheet1", + excelize.PageLayoutPaperSize(10), + ); err != nil { + panic(err) + } // Output: } @@ -27,15 +33,21 @@ func ExampleFile_GetPageLayout() { const sheet = "Sheet1" var ( orientation excelize.PageLayoutOrientation + paperSize excelize.PageLayoutPaperSize ) if err := xl.GetPageLayout("Sheet1", &orientation); err != nil { panic(err) } + if err := xl.GetPageLayout("Sheet1", &paperSize); err != nil { + panic(err) + } fmt.Println("Defaults:") fmt.Printf("- orientation: %q\n", orientation) + fmt.Printf("- paper size: %d\n", paperSize) // Output: // Defaults: // - orientation: "portrait" + // - paper size: 1 } func TestPageLayoutOption(t *testing.T) { @@ -46,6 +58,7 @@ func TestPageLayoutOption(t *testing.T) { nonDefault excelize.PageLayoutOption }{ {new(excelize.PageLayoutOrientation), excelize.PageLayoutOrientation(excelize.OrientationLandscape)}, + {new(excelize.PageLayoutPaperSize), excelize.PageLayoutPaperSize(10)}, } for i, test := range testData { diff --git a/xmlWorksheet.go b/xmlWorksheet.go index b3e887705d..f3323cbb71 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -90,7 +90,7 @@ type xlsxPageSetUp struct { Orientation string `xml:"orientation,attr,omitempty"` PageOrder string `xml:"pageOrder,attr,omitempty"` PaperHeight string `xml:"paperHeight,attr,omitempty"` - PaperSize string `xml:"paperSize,attr,omitempty"` + PaperSize int `xml:"paperSize,attr,omitempty"` PaperWidth string `xml:"paperWidth,attr,omitempty"` Scale int `xml:"scale,attr,omitempty"` UseFirstPageNumber bool `xml:"useFirstPageNumber,attr,omitempty"` From daf32a37f948f08cd585b14a935b49efdec9ff96 Mon Sep 17 00:00:00 2001 From: rentiansheng Date: Wed, 23 Jan 2019 22:07:11 +0800 Subject: [PATCH 053/957] fix: datavalidation list error, formula > 255 issue #339 --- datavalidation.go | 6 +++++- datavalidation_test.go | 10 +++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/datavalidation.go b/datavalidation.go index 0a95251b82..3035bb2e63 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -108,7 +108,11 @@ func (dd *DataValidation) SetInput(title, msg string) { // SetDropList data validation list. func (dd *DataValidation) SetDropList(keys []string) error { - dd.Formula1 = "\"" + strings.Join(keys, ",") + "\"" + formula := "\"" + strings.Join(keys, ",") + "\"" + if dataValidationFormulaStrLen < len(formula) { + return fmt.Errorf(dataValidationFormulaStrLenErr) + } + dd.Formula1 = formula dd.Type = convDataValidationType(typeList) return nil } diff --git a/datavalidation_test.go b/datavalidation_test.go index fad50c2933..afb659c2e8 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -72,10 +72,14 @@ func TestDataValidationError(t *testing.T) { } dvRange = NewDataValidation(true) - dvRange.SetDropList(make([]string, 258)) - - err = dvRange.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorGreaterThan) + err = dvRange.SetDropList(make([]string, 258)) + if dvRange.Formula1 != "" { + t.Errorf("data validation error. Formula1 must be empty!") + return + } assert.EqualError(t, err, "data validation must be 0-255 characters") + dvRange.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorGreaterThan) + dvRange.SetSqref("A9:B10") xlsx.AddDataValidation("Sheet1", dvRange) if !assert.NoError(t, xlsx.SaveAs(resultFile)) { From b7b925c03fa611b0157214357cc7f7b949b32992 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 29 Jan 2019 10:47:24 +0800 Subject: [PATCH 054/957] Update readme --- README.md | 2 -- README_zh.md | 2 -- test/images/chart.png | Bin 102494 -> 190484 bytes 3 files changed, 4 deletions(-) diff --git a/README.md b/README.md index 52cf367057..86d1e8a3af 100644 --- a/README.md +++ b/README.md @@ -177,5 +177,3 @@ The Excel logo is a trademark of [Microsoft Corporation](https://aka.ms/trademar Some struct of XML originally by [tealeg/xlsx](https://github.com/tealeg/xlsx). Licensed under the [BSD 3-Clause License](https://github.com/tealeg/xlsx/blob/master/LICENSE). gopher.{ai,svg,png} was created by [Takuya Ueda](https://twitter.com/tenntenn). Licensed under the [Creative Commons 3.0 Attributions license](http://creativecommons.org/licenses/by/3.0/). - -[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2F360EntSecGroup-Skylar%2Fexcelize.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2F360EntSecGroup-Skylar%2Fexcelize?ref=badge_large) diff --git a/README_zh.md b/README_zh.md index f4e893be9d..6a97c6faad 100644 --- a/README_zh.md +++ b/README_zh.md @@ -177,5 +177,3 @@ Excel 徽标是 [Microsoft Corporation](https://aka.ms/trademarks-usage) 的商 本类库中部分 XML 结构体的定义参考了开源项目:[tealeg/xlsx](https://github.com/tealeg/xlsx),遵循 [BSD 3-Clause License](https://github.com/tealeg/xlsx/blob/master/LICENSE) 开源许可协议。 gopher.{ai,svg,png} 由 [Takuya Ueda](https://twitter.com/tenntenn) 创作,遵循 [Creative Commons 3.0 Attributions license](http://creativecommons.org/licenses/by/3.0/) 创作共用授权条款。 - -[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2F360EntSecGroup-Skylar%2Fexcelize.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2F360EntSecGroup-Skylar%2Fexcelize?ref=badge_large) diff --git a/test/images/chart.png b/test/images/chart.png index 5d6e58a460a6cedd74045baced62dd913f90a983..9fcd28a3de177f2baa1943e415d24158059b90b8 100644 GIT binary patch literal 190484 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 Kt& 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 Q_~%E*ZJm!*TKnGmUpnDTZ2$lO literal 102494 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 From e780e41e0222517caa9c69030b5955ab2b458a49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Mengu=C3=A9?= Date: Sat, 2 Feb 2019 04:05:01 +0100 Subject: [PATCH 055/957] Faster TitleToNumber (#343) * TestTitleToNumber: more test cases * TitleToNumber: drop use of math.Pow() Compute using pure integers * TitleToNumber: simplify Remove unecessary casts to int --- excelize_test.go | 4 ++++ lib.go | 13 ++++++------- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/excelize_test.go b/excelize_test.go index 6a106ec115..ebbfcf7581 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -1546,8 +1546,12 @@ func TestConditionalFormatError(t *testing.T) { } func TestTitleToNumber(t *testing.T) { + assert.Equal(t, 0, TitleToNumber("A")) + assert.Equal(t, 25, TitleToNumber("Z")) + assert.Equal(t, 26, TitleToNumber("AA")) assert.Equal(t, 36, TitleToNumber("AK")) assert.Equal(t, 36, TitleToNumber("ak")) + assert.Equal(t, 51, TitleToNumber("AZ")) } func TestSharedStrings(t *testing.T) { diff --git a/lib.go b/lib.go index 99c513edbf..30a20e03a0 100644 --- a/lib.go +++ b/lib.go @@ -14,7 +14,6 @@ import ( "bytes" "io" "log" - "math" "strconv" "strings" "unicode" @@ -91,15 +90,15 @@ func ToAlphaString(value int) string { // excelize.TitleToNumber("ak") // func TitleToNumber(s string) int { - weight := 0.0 + weight := 1 sum := 0 for i := len(s) - 1; i >= 0; i-- { - ch := int(s[i]) - if int(s[i]) >= int('a') && int(s[i]) <= int('z') { - ch = int(s[i]) - 32 + ch := s[i] + if ch >= 'a' && ch <= 'z' { + ch -= 32 } - sum = sum + (ch-int('A')+1)*int(math.Pow(26, weight)) - weight++ + sum += int(ch-'A'+1) * weight + weight *= 26 } return sum - 1 } From 0072bb731043f89ce978778b9d7fdc6160e29de0 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 22 Feb 2019 22:17:38 +0800 Subject: [PATCH 056/957] resolve the issue corrupted xlsx after deleting formula of cell, reference #346 --- calcchain.go | 55 ++++++++++++++++++++++++++++++++++++++++++++ cell.go | 5 ++++ col.go | 4 ++++ excelize.go | 4 +++- excelize_test.go | 12 +++++++++- file.go | 1 + rows.go | 14 ++++++++++- sheet.go | 2 +- test/CalcChain.xlsx | Bin 0 -> 5959 bytes xmlCalcChain.go | 28 ++++++++++++++++++++++ 10 files changed, 121 insertions(+), 4 deletions(-) create mode 100644 calcchain.go create mode 100755 test/CalcChain.xlsx create mode 100644 xmlCalcChain.go diff --git a/calcchain.go b/calcchain.go new file mode 100644 index 0000000000..285a3e9ee9 --- /dev/null +++ b/calcchain.go @@ -0,0 +1,55 @@ +// Copyright 2016 - 2019 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. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. + +package excelize + +import "encoding/xml" + +// calcChainReader provides a function to get the pointer to the structure +// after deserialization of xl/calcChain.xml. +func (f *File) calcChainReader() *xlsxCalcChain { + if f.CalcChain == nil { + var c xlsxCalcChain + _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML("xl/calcChain.xml")), &c) + f.CalcChain = &c + } + return f.CalcChain +} + +// calcChainWriter provides a function to save xl/calcChain.xml after +// serialize structure. +func (f *File) calcChainWriter() { + if f.CalcChain != nil { + output, _ := xml.Marshal(f.CalcChain) + f.saveFileList("xl/calcChain.xml", output) + } +} + +// deleteCalcChain provides a function to remove cell reference on the +// calculation chain. +func (f *File) deleteCalcChain(axis string) { + calc := f.calcChainReader() + if calc != nil { + for i, c := range calc.C { + if c.R == axis { + calc.C = append(calc.C[:i], calc.C[i+1:]...) + } + } + } + if len(calc.C) == 0 { + f.CalcChain = nil + delete(f.XLSX, "xl/calcChain.xml") + content := f.contentTypesReader() + 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 afe8635f8c..3cf880a98a 100644 --- a/cell.go +++ b/cell.go @@ -305,6 +305,11 @@ func (f *File) SetCellFormula(sheet, axis, formula string) { completeRow(xlsx, rows, cell) completeCol(xlsx, rows, cell) + if formula == "" { + xlsx.SheetData.Row[xAxis].C[yAxis].F = nil + f.deleteCalcChain(axis) + return + } if xlsx.SheetData.Row[xAxis].C[yAxis].F != nil { xlsx.SheetData.Row[xAxis].C[yAxis].F.Content = formula } else { diff --git a/col.go b/col.go index af2c321347..1130c3a524 100644 --- a/col.go +++ b/col.go @@ -322,6 +322,10 @@ func (f *File) InsertCol(sheet, column string) { // // xlsx.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 +// worksheet, it will cause a file error when you open it. The excelize only +// partially updates these references currently. func (f *File) RemoveCol(sheet, column string) { xlsx := f.workSheetReader(sheet) for r := range xlsx.SheetData.Row { diff --git a/excelize.go b/excelize.go index 32f045153b..32aa431e31 100644 --- a/excelize.go +++ b/excelize.go @@ -25,6 +25,7 @@ import ( type File struct { checked map[string]bool sheetMap map[string]string + CalcChain *xlsxCalcChain ContentTypes *xlsxTypes Path string SharedStrings *xlsxSST @@ -201,7 +202,8 @@ func (f *File) UpdateLinkedValue() { // row: Index number of the row we're inserting/deleting before // offset: Number of rows/column to insert/delete negative values indicate deletion // -// TODO: adjustPageBreaks, adjustComments, adjustDataValidations, adjustProtectedCells +// TODO: adjustCalcChain, adjustPageBreaks, adjustComments, +// adjustDataValidations, adjustProtectedCells // func (f *File) adjustHelper(sheet string, column, row, offset int) { xlsx := f.workSheetReader(sheet) diff --git a/excelize_test.go b/excelize_test.go index ebbfcf7581..d621b87bfa 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -342,8 +342,18 @@ func TestSetCellFormula(t *testing.T) { xlsx.SetCellFormula("Sheet1", "C19", "SUM(Sheet2!D2,Sheet2!D9)") // Test set cell formula with illegal rows number. xlsx.SetCellFormula("Sheet1", "C", "SUM(Sheet2!D2,Sheet2!D9)") + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetCellFormula1.xlsx"))) - assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetCellFormula.xlsx"))) + xlsx, err = OpenFile(filepath.Join("test", "CalcChain.xlsx")) + if !assert.NoError(t, err) { + t.FailNow() + } + // Test remove cell formula. + xlsx.SetCellFormula("Sheet1", "A1", "") + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetCellFormula2.xlsx"))) + // Test remove all cell formula. + xlsx.SetCellFormula("Sheet1", "B1", "") + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetCellFormula3.xlsx"))) } func TestSetSheetBackground(t *testing.T) { diff --git a/file.go b/file.go index 3e49803306..66b46c59f1 100644 --- a/file.go +++ b/file.go @@ -92,6 +92,7 @@ func (f *File) WriteToBuffer() (*bytes.Buffer, error) { f.workbookRelsWriter() f.worksheetWriter() f.styleSheetWriter() + f.calcChainWriter() for path, content := range f.XLSX { fi, err := zw.Create(path) if err != nil { diff --git a/rows.go b/rows.go index 3984216036..aebc97941c 100644 --- a/rows.go +++ b/rows.go @@ -343,6 +343,10 @@ func (f *File) GetRowOutlineLevel(sheet string, rowIndex int) uint8 { // // xlsx.RemoveRow("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 +// worksheet, it will cause a file error when you open it. The excelize only +// partially updates these references currently. func (f *File) RemoveRow(sheet string, row int) { if row < 0 { return @@ -375,15 +379,23 @@ func (f *File) InsertRow(sheet string, row int) { // // xlsx.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 +// worksheet, it will cause a file error when you open it. The excelize only +// partially updates these references currently. func (f *File) DuplicateRow(sheet string, row int) { f.DuplicateRowTo(sheet, row, row+1) } // DuplicateRowTo inserts a copy of specified row at specified row position -// movig down exists rows aftet target position +// moving down exists rows after target position // // xlsx.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 +// worksheet, it will cause a file error when you open it. The excelize only +// partially updates these references currently. func (f *File) DuplicateRowTo(sheet string, row, row2 int) { if row <= 0 || row2 <= 0 || row == row2 { return diff --git a/sheet.go b/sheet.go index cb1c08e83c..efbd4667ef 100644 --- a/sheet.go +++ b/sheet.go @@ -900,7 +900,7 @@ func (p *PageLayoutPaperSize) getPageLayout(ps *xlsxPageSetUp) { // 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 double postcard (200 mm by 148 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.) diff --git a/test/CalcChain.xlsx b/test/CalcChain.xlsx new file mode 100755 index 0000000000000000000000000000000000000000..8558f82fa79c99e4d81065ec488f3195ac4db5d5 GIT binary patch literal 5959 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 literal 0 HcmV?d00001 diff --git a/xmlCalcChain.go b/xmlCalcChain.go new file mode 100644 index 0000000000..9c916bf35a --- /dev/null +++ b/xmlCalcChain.go @@ -0,0 +1,28 @@ +// Copyright 2016 - 2019 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. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. + +package excelize + +import "encoding/xml" + +// xlsxCalcChain directly maps the calcChain element. This element represents the root of the calculation chain. +type xlsxCalcChain struct { + XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main calcChain"` + C []xlsxCalcChainC `xml:"c"` +} + +// xlsxCalcChainC directly maps the c element. +type xlsxCalcChainC struct { + R string `xml:"r,attr"` + I int `xml:"i,attr"` + L bool `xml:"l,attr,omitempty"` + S bool `xml:"s,attr,omitempty"` + T bool `xml:"t,attr,omitempty"` + A bool `xml:"a,attr,omitempty"` +} From c223815cfe88e2cdfe3631860a6936e94229f779 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 23 Feb 2019 16:20:44 +0800 Subject: [PATCH 057/957] Resolve #345, get comments by target reference --- comment.go | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/comment.go b/comment.go index 07f70b5088..42e7d451ae 100644 --- a/comment.go +++ b/comment.go @@ -33,9 +33,7 @@ func parseFormatCommentsSet(formatSet string) (*formatComment, error) { func (f *File) GetComments() (comments map[string][]Comment) { comments = map[string][]Comment{} for n := range f.sheetMap { - commentID := f.GetSheetIndex(n) - commentsXML := "xl/comments" + strconv.Itoa(commentID) + ".xml" - c, ok := f.XLSX[commentsXML] + c, ok := f.XLSX["xl"+strings.TrimPrefix(f.getSheetComments(f.GetSheetIndex(n)), "..")] if ok { d := xlsxComments{} xml.Unmarshal([]byte(c), &d) @@ -58,6 +56,20 @@ func (f *File) GetComments() (comments map[string][]Comment) { return } +// getSheetComments provides the method to get the target comment reference by +// given worksheet index. +func (f *File) getSheetComments(sheetID int) string { + var rels = "xl/worksheets/_rels/sheet" + strconv.Itoa(sheetID) + ".xml.rels" + var sheetRels xlsxWorkbookRels + _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML(rels)), &sheetRels) + for _, v := range sheetRels.Relationships { + if v.Type == SourceRelationshipComments { + return v.Target + } + } + return "" +} + // AddComment provides the method to add comment in a sheet by given worksheet // index, cell and format set (such as author and text). Note that the max // author length is 255 and the max text length is 32512. For example, add a From 1427027e38d6db46d441243f00d6989c2f53e7ce Mon Sep 17 00:00:00 2001 From: BluesJhao Date: Mon, 25 Feb 2019 00:29:58 +0800 Subject: [PATCH 058/957] Resolve #235, performance optimization for add comments (#347) --- comment.go | 130 +++++++++++++++++++++++++++++++++++----------------- excelize.go | 41 ++++++++++------- file.go | 9 +++- 3 files changed, 120 insertions(+), 60 deletions(-) diff --git a/comment.go b/comment.go index 42e7d451ae..5db6312f63 100644 --- a/comment.go +++ b/comment.go @@ -122,31 +122,34 @@ func (f *File) addDrawingVML(commentID int, drawingVML, cell string, lineCount, row, _ := strconv.Atoi(strings.Map(intOnlyMapF, cell)) xAxis := row - 1 yAxis := TitleToNumber(col) - vml := vmlDrawing{ - XMLNSv: "urn:schemas-microsoft-com:vml", - XMLNSo: "urn:schemas-microsoft-com:office:office", - XMLNSx: "urn:schemas-microsoft-com:office:excel", - XMLNSmv: "http://macVmlSchemaUri", - Shapelayout: &xlsxShapelayout{ - Ext: "edit", - IDmap: &xlsxIDmap{ - Ext: "edit", - Data: commentID, - }, - }, - Shapetype: &xlsxShapetype{ - ID: "_x0000_t202", - Coordsize: "21600,21600", - Spt: 202, - Path: "m0,0l0,21600,21600,21600,21600,0xe", - Stroke: &xlsxStroke{ - Joinstyle: "miter", + vml := f.VMLDrawing[drawingVML] + if vml == nil { + vml = &vmlDrawing{ + XMLNSv: "urn:schemas-microsoft-com:vml", + XMLNSo: "urn:schemas-microsoft-com:office:office", + XMLNSx: "urn:schemas-microsoft-com:office:excel", + XMLNSmv: "http://macVmlSchemaUri", + Shapelayout: &xlsxShapelayout{ + Ext: "edit", + IDmap: &xlsxIDmap{ + Ext: "edit", + Data: commentID, + }, }, - VPath: &vPath{ - Gradientshapeok: "t", - Connecttype: "miter", + Shapetype: &xlsxShapetype{ + ID: "_x0000_t202", + Coordsize: "21600,21600", + Spt: 202, + Path: "m0,0l0,21600,21600,21600,21600,0xe", + Stroke: &xlsxStroke{ + Joinstyle: "miter", + }, + VPath: &vPath{ + Gradientshapeok: "t", + Connecttype: "miter", + }, }, - }, + } } sp := encodeShape{ Fill: &vFill{ @@ -191,10 +194,8 @@ func (f *File) addDrawingVML(commentID int, drawingVML, cell string, lineCount, Strokecolor: "#edeaa1", Val: string(s[13 : len(s)-14]), } - c, ok := f.XLSX[drawingVML] - if ok { - d := decodeVmlDrawing{} - _ = xml.Unmarshal(namespaceStrictToTransitional(c), &d) + d := f.decodeVMLDrawingReader(drawingVML) + if d != nil { for _, v := range d.Shape { s := xlsxShape{ ID: "_x0000_s1025", @@ -208,8 +209,7 @@ func (f *File) addDrawingVML(commentID int, drawingVML, cell string, lineCount, } } vml.Shape = append(vml.Shape, shape) - v, _ := xml.Marshal(vml) - f.XLSX[drawingVML] = v + f.VMLDrawing[drawingVML] = vml } // addComment provides a function to create chart as xl/comments%d.xml by @@ -223,12 +223,15 @@ func (f *File) addComment(commentsXML, cell string, formatSet *formatComment) { if len(t) > 32512 { t = t[0:32512] } - comments := xlsxComments{ - Authors: []xlsxAuthor{ - { - Author: formatSet.Author, + comments := f.commentsReader(commentsXML) + if comments == nil { + comments = &xlsxComments{ + Authors: []xlsxAuthor{ + { + Author: formatSet.Author, + }, }, - }, + } } cmt := xlsxComment{ Ref: cell, @@ -261,15 +264,8 @@ func (f *File) addComment(commentsXML, cell string, formatSet *formatComment) { }, }, } - c, ok := f.XLSX[commentsXML] - if ok { - d := xlsxComments{} - _ = xml.Unmarshal(namespaceStrictToTransitional(c), &d) - comments.CommentList.Comment = append(comments.CommentList.Comment, d.CommentList.Comment...) - } comments.CommentList.Comment = append(comments.CommentList.Comment, cmt) - v, _ := xml.Marshal(comments) - f.saveFileList(commentsXML, v) + f.Comments[commentsXML] = comments } // countComments provides a function to get comments files count storage in @@ -283,3 +279,53 @@ func (f *File) countComments() int { } return count } + +// 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 { + if f.DecodeVMLDrawing[path] == nil { + c, ok := f.XLSX[path] + if ok { + d := decodeVmlDrawing{} + _ = xml.Unmarshal(namespaceStrictToTransitional(c), &d) + f.DecodeVMLDrawing[path] = &d + } + } + return f.DecodeVMLDrawing[path] +} + +// vmlDrawingWriter provides a function to save xl/drawings/vmlDrawing%d.xml. +// after serialize structure. +func (f *File) vmlDrawingWriter() { + for path, vml := range f.VMLDrawing { + if vml != nil { + v, _ := xml.Marshal(vml) + f.XLSX[path] = v + } + } +} + +// 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 { + if f.Comments[path] == nil { + content, ok := f.XLSX[path] + if ok { + c := xlsxComments{} + _ = xml.Unmarshal(namespaceStrictToTransitional(content), &c) + f.Comments[path] = &c + } + } + return f.Comments[path] +} + +// commentsWriter provides a function to save xl/comments%d.xml after +// serialize structure. +func (f *File) commentsWriter() { + for path, c := range f.Comments { + if c != nil { + v, _ := xml.Marshal(c) + f.saveFileList(path, v) + } + } +} diff --git a/excelize.go b/excelize.go index 32aa431e31..df3cd05b45 100644 --- a/excelize.go +++ b/excelize.go @@ -23,19 +23,22 @@ import ( // File define a populated XLSX file struct. type File struct { - checked map[string]bool - sheetMap map[string]string - CalcChain *xlsxCalcChain - ContentTypes *xlsxTypes - Path string - SharedStrings *xlsxSST - Sheet map[string]*xlsxWorksheet - SheetCount int - Styles *xlsxStyleSheet - Theme *xlsxTheme - WorkBook *xlsxWorkbook - WorkBookRels *xlsxWorkbookRels - XLSX map[string][]byte + checked map[string]bool + sheetMap map[string]string + CalcChain *xlsxCalcChain + Comments map[string]*xlsxComments + ContentTypes *xlsxTypes + Path string + SharedStrings *xlsxSST + Sheet map[string]*xlsxWorksheet + SheetCount int + Styles *xlsxStyleSheet + Theme *xlsxTheme + DecodeVMLDrawing map[string]*decodeVmlDrawing + VMLDrawing map[string]*vmlDrawing + WorkBook *xlsxWorkbook + WorkBookRels *xlsxWorkbookRels + XLSX map[string][]byte } // OpenFile take the name of an XLSX file and returns a populated XLSX file @@ -71,11 +74,15 @@ func OpenReader(r io.Reader) (*File, error) { return nil, err } f := &File{ - checked: make(map[string]bool), - Sheet: make(map[string]*xlsxWorksheet), - SheetCount: sheetCount, - XLSX: file, + checked: make(map[string]bool), + Comments: make(map[string]*xlsxComments), + Sheet: make(map[string]*xlsxWorksheet), + SheetCount: sheetCount, + DecodeVMLDrawing: make(map[string]*decodeVmlDrawing), + VMLDrawing: make(map[string]*vmlDrawing), + XLSX: file, } + f.CalcChain = f.calcChainReader() f.sheetMap = f.getSheetMap() f.Styles = f.stylesReader() f.Theme = f.themeReader() diff --git a/file.go b/file.go index 66b46c59f1..b6bf57d0d1 100644 --- a/file.go +++ b/file.go @@ -39,8 +39,12 @@ func NewFile() *File { SheetCount: 1, XLSX: file, } + f.CalcChain = f.calcChainReader() + f.Comments = make(map[string]*xlsxComments) f.ContentTypes = f.contentTypesReader() f.Styles = f.stylesReader() + f.DecodeVMLDrawing = make(map[string]*decodeVmlDrawing) + f.VMLDrawing = make(map[string]*vmlDrawing) f.WorkBook = f.workbookReader() f.WorkBookRels = f.workbookRelsReader() f.Sheet["xl/worksheets/sheet1.xml"] = f.workSheetReader("Sheet1") @@ -87,12 +91,15 @@ func (f *File) WriteTo(w io.Writer) (int64, error) { func (f *File) WriteToBuffer() (*bytes.Buffer, error) { buf := new(bytes.Buffer) zw := zip.NewWriter(buf) + f.calcChainWriter() + f.commentsWriter() f.contentTypesWriter() + f.vmlDrawingWriter() f.workbookWriter() f.workbookRelsWriter() f.worksheetWriter() f.styleSheetWriter() - f.calcChainWriter() + for path, content := range f.XLSX { fi, err := zw.Create(path) if err != nil { From 1aed1d744b12885c4a88c090494175c59208e038 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 25 Feb 2019 22:14:34 +0800 Subject: [PATCH 059/957] Resolve #274, performance optimization for add images, charts and shapes --- chart.go | 52 ++++++++++++------------ comment.go | 7 +--- excelize.go | 4 ++ excelize_test.go | 18 ++++++++ file.go | 4 ++ picture.go | 104 +++++++++++++++++++++++++++++++---------------- shape.go | 9 +--- 7 files changed, 126 insertions(+), 72 deletions(-) diff --git a/chart.go b/chart.go index 77a012543e..f11fd55af6 100644 --- a/chart.go +++ b/chart.go @@ -1207,28 +1207,34 @@ func (f *File) drawPlotAreaTxPr() *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(drawingXML string, content *xlsxWsDr) int { +func (f *File) drawingParser(path string) (*xlsxWsDr, int) { cNvPrID := 1 - _, ok := f.XLSX[drawingXML] - if ok { // Append Model - decodeWsDr := decodeWsDr{} - _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML(drawingXML)), &decodeWsDr) - content.R = decodeWsDr.R - cNvPrID = len(decodeWsDr.OneCellAnchor) + len(decodeWsDr.TwoCellAnchor) + 1 - for _, v := range decodeWsDr.OneCellAnchor { - content.OneCellAnchor = append(content.OneCellAnchor, &xdrCellAnchor{ - EditAs: v.EditAs, - GraphicFrame: v.Content, - }) - } - for _, v := range decodeWsDr.TwoCellAnchor { - content.TwoCellAnchor = append(content.TwoCellAnchor, &xdrCellAnchor{ - EditAs: v.EditAs, - GraphicFrame: v.Content, - }) + if f.Drawings[path] == nil { + content := xlsxWsDr{} + content.A = NameSpaceDrawingML + content.Xdr = NameSpaceDrawingMLSpreadSheet + _, ok := f.XLSX[path] + if ok { // Append Model + decodeWsDr := decodeWsDr{} + _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML(path)), &decodeWsDr) + content.R = decodeWsDr.R + cNvPrID = len(decodeWsDr.OneCellAnchor) + len(decodeWsDr.TwoCellAnchor) + 1 + for _, v := range decodeWsDr.OneCellAnchor { + content.OneCellAnchor = append(content.OneCellAnchor, &xdrCellAnchor{ + EditAs: v.EditAs, + GraphicFrame: v.Content, + }) + } + for _, v := range decodeWsDr.TwoCellAnchor { + content.TwoCellAnchor = append(content.TwoCellAnchor, &xdrCellAnchor{ + EditAs: v.EditAs, + GraphicFrame: v.Content, + }) + } } + f.Drawings[path] = &content } - return cNvPrID + return f.Drawings[path], cNvPrID } // addDrawingChart provides a function to add chart graphic frame by given @@ -1242,10 +1248,7 @@ func (f *File) addDrawingChart(sheet, drawingXML, cell string, width, height, rI width = int(float64(width) * formatSet.XScale) height = int(float64(height) * formatSet.YScale) colStart, rowStart, _, _, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, col, row, formatSet.OffsetX, formatSet.OffsetY, width, height) - content := xlsxWsDr{} - content.A = NameSpaceDrawingML - content.Xdr = NameSpaceDrawingMLSpreadSheet - cNvPrID := f.drawingParser(drawingXML, &content) + content, cNvPrID := f.drawingParser(drawingXML) twoCellAnchor := xdrCellAnchor{} twoCellAnchor.EditAs = formatSet.Positioning from := xlsxFrom{} @@ -1286,6 +1289,5 @@ func (f *File) addDrawingChart(sheet, drawingXML, cell string, width, height, rI FPrintsWithSheet: formatSet.FPrintsWithSheet, } content.TwoCellAnchor = append(content.TwoCellAnchor, &twoCellAnchor) - output, _ := xml.Marshal(content) - f.saveFileList(drawingXML, output) + f.Drawings[drawingXML] = content } diff --git a/comment.go b/comment.go index 5db6312f63..c86e93257e 100644 --- a/comment.go +++ b/comment.go @@ -33,10 +33,7 @@ func parseFormatCommentsSet(formatSet string) (*formatComment, error) { func (f *File) GetComments() (comments map[string][]Comment) { comments = map[string][]Comment{} for n := range f.sheetMap { - c, ok := f.XLSX["xl"+strings.TrimPrefix(f.getSheetComments(f.GetSheetIndex(n)), "..")] - if ok { - d := xlsxComments{} - xml.Unmarshal([]byte(c), &d) + if d := f.commentsReader("xl" + strings.TrimPrefix(f.getSheetComments(f.GetSheetIndex(n)), "..")); d != nil { sheetComments := []Comment{} for _, comment := range d.CommentList.Comment { sheetComment := Comment{} @@ -294,7 +291,7 @@ func (f *File) decodeVMLDrawingReader(path string) *decodeVmlDrawing { return f.DecodeVMLDrawing[path] } -// vmlDrawingWriter provides a function to save xl/drawings/vmlDrawing%d.xml. +// vmlDrawingWriter provides a function to save xl/drawings/vmlDrawing%d.xml // after serialize structure. func (f *File) vmlDrawingWriter() { for path, vml := range f.VMLDrawing { diff --git a/excelize.go b/excelize.go index df3cd05b45..a2bec077e1 100644 --- a/excelize.go +++ b/excelize.go @@ -28,6 +28,8 @@ type File struct { CalcChain *xlsxCalcChain Comments map[string]*xlsxComments ContentTypes *xlsxTypes + DrawingRels map[string]*xlsxWorkbookRels + Drawings map[string]*xlsxWsDr Path string SharedStrings *xlsxSST Sheet map[string]*xlsxWorksheet @@ -76,6 +78,8 @@ func OpenReader(r io.Reader) (*File, error) { f := &File{ checked: make(map[string]bool), Comments: make(map[string]*xlsxComments), + DrawingRels: make(map[string]*xlsxWorkbookRels), + Drawings: make(map[string]*xlsxWsDr), Sheet: make(map[string]*xlsxWorksheet), SheetCount: sheetCount, DecodeVMLDrawing: make(map[string]*decodeVmlDrawing), diff --git a/excelize_test.go b/excelize_test.go index d621b87bfa..ed6f073ce8 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -817,6 +817,24 @@ func TestGetPicture(t *testing.T) { xlsx.getDrawingRelationships("", "") xlsx.getSheetRelationshipsTargetByID("", "") xlsx.deleteSheetRelationships("", "") + + // Try to get picture from a local storage file. + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestGetPicture.xlsx"))) + xlsx, err = OpenFile(filepath.Join("test", "TestGetPicture.xlsx")) + if !assert.NoError(t, err) { + t.FailNow() + } + file, raw = xlsx.GetPicture("Sheet1", "F21") + if file == "" { + err = ioutil.WriteFile(file, raw, 0644) + if !assert.NoError(t, err) { + t.FailNow() + } + } + + // Try to get picture from a local storage file that doesn't contain an image. + file, raw = xlsx.GetPicture("Sheet1", "F22") + t.Log(file, len(raw)) } func TestSheetVisibility(t *testing.T) { diff --git a/file.go b/file.go index b6bf57d0d1..8d68851e22 100644 --- a/file.go +++ b/file.go @@ -42,6 +42,8 @@ func NewFile() *File { f.CalcChain = f.calcChainReader() f.Comments = make(map[string]*xlsxComments) f.ContentTypes = f.contentTypesReader() + f.DrawingRels = make(map[string]*xlsxWorkbookRels) + f.Drawings = make(map[string]*xlsxWsDr) f.Styles = f.stylesReader() f.DecodeVMLDrawing = make(map[string]*decodeVmlDrawing) f.VMLDrawing = make(map[string]*vmlDrawing) @@ -94,6 +96,8 @@ func (f *File) WriteToBuffer() (*bytes.Buffer, error) { f.calcChainWriter() f.commentsWriter() f.contentTypesWriter() + f.drawingRelsWriter() + f.drawingsWriter() f.vmlDrawingWriter() f.workbookWriter() f.workbookRelsWriter() diff --git a/picture.go b/picture.go index 763d89a6c7..2ad9db2c3c 100644 --- a/picture.go +++ b/picture.go @@ -272,10 +272,7 @@ func (f *File) addDrawingPicture(sheet, drawingXML, cell, file string, width, he width = int(float64(width) * formatSet.XScale) height = int(float64(height) * formatSet.YScale) colStart, rowStart, _, _, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, col, row, formatSet.OffsetX, formatSet.OffsetY, width, height) - content := xlsxWsDr{} - content.A = NameSpaceDrawingML - content.Xdr = NameSpaceDrawingMLSpreadSheet - cNvPrID := f.drawingParser(drawingXML, &content) + content, cNvPrID := f.drawingParser(drawingXML) twoCellAnchor := xdrCellAnchor{} twoCellAnchor.EditAs = formatSet.Positioning from := xlsxFrom{} @@ -311,8 +308,7 @@ func (f *File) addDrawingPicture(sheet, drawingXML, cell, file string, width, he FPrintsWithSheet: formatSet.FPrintsWithSheet, } content.TwoCellAnchor = append(content.TwoCellAnchor, &twoCellAnchor) - output, _ := xml.Marshal(content) - f.saveFileList(drawingXML, output) + f.Drawings[drawingXML] = content } // addDrawingRelationships provides a function to add image part relationships @@ -320,27 +316,25 @@ func (f *File) addDrawingPicture(sheet, drawingXML, cell, file string, width, he // relationship type and target. func (f *File) addDrawingRelationships(index int, relType, target, targetMode string) int { var rels = "xl/drawings/_rels/drawing" + strconv.Itoa(index) + ".xml.rels" - var drawingRels xlsxWorkbookRels var rID = 1 var ID bytes.Buffer ID.WriteString("rId") ID.WriteString(strconv.Itoa(rID)) - _, ok := f.XLSX[rels] - if ok { - ID.Reset() - _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML(rels)), &drawingRels) - rID = len(drawingRels.Relationships) + 1 - ID.WriteString("rId") - ID.WriteString(strconv.Itoa(rID)) + drawingRels := f.drawingRelsReader(rels) + if drawingRels == nil { + drawingRels = &xlsxWorkbookRels{} } + ID.Reset() + rID = len(drawingRels.Relationships) + 1 + ID.WriteString("rId") + ID.WriteString(strconv.Itoa(rID)) drawingRels.Relationships = append(drawingRels.Relationships, xlsxWorkbookRelation{ ID: ID.String(), Type: relType, Target: target, TargetMode: targetMode, }) - output, _ := xml.Marshal(drawingRels) - f.saveFileList(rels, output) + f.DrawingRels[rels] = drawingRels return rID } @@ -482,22 +476,30 @@ func (f *File) GetPicture(sheet, cell string) (string, []byte) { } target := f.getSheetRelationshipsTargetByID(sheet, xlsx.Drawing.RID) drawingXML := strings.Replace(target, "..", "xl", -1) - - _, ok := f.XLSX[drawingXML] - if !ok { - return "", nil - } - decodeWsDr := decodeWsDr{} - _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML(drawingXML)), &decodeWsDr) - cell = strings.ToUpper(cell) fromCol := string(strings.Map(letterOnlyMapF, cell)) fromRow, _ := strconv.Atoi(strings.Map(intOnlyMapF, cell)) row := fromRow - 1 col := TitleToNumber(fromCol) - drawingRelationships := strings.Replace(strings.Replace(target, "../drawings", "xl/drawings/_rels", -1), ".xml", ".xml.rels", -1) - + wsDr, _ := f.drawingParser(drawingXML) + for _, anchor := range wsDr.TwoCellAnchor { + if anchor.From != nil && anchor.Pic != nil { + if anchor.From.Col == col && anchor.From.Row == row { + xlsxWorkbookRelation := f.getDrawingRelationships(drawingRelationships, anchor.Pic.BlipFill.Blip.Embed) + _, ok := supportImageTypes[filepath.Ext(xlsxWorkbookRelation.Target)] + if ok { + return filepath.Base(xlsxWorkbookRelation.Target), []byte(f.XLSX[strings.Replace(xlsxWorkbookRelation.Target, "..", "xl", -1)]) + } + } + } + } + _, ok := f.XLSX[drawingXML] + if !ok { + return "", nil + } + decodeWsDr := decodeWsDr{} + _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML(drawingXML)), &decodeWsDr) for _, anchor := range decodeWsDr.TwoCellAnchor { decodeTwoCellAnchor := decodeTwoCellAnchor{} _ = xml.Unmarshal([]byte(""+anchor.Content+""), &decodeTwoCellAnchor) @@ -518,16 +520,48 @@ func (f *File) GetPicture(sheet, cell string) (string, []byte) { // from xl/drawings/_rels/drawing%s.xml.rels by given file name and // relationship ID. func (f *File) getDrawingRelationships(rels, rID string) *xlsxWorkbookRelation { - _, ok := f.XLSX[rels] - if !ok { - return nil - } - var drawingRels xlsxWorkbookRels - _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML(rels)), &drawingRels) - for _, v := range drawingRels.Relationships { - if v.ID == rID { - return &v + if drawingRels := f.drawingRelsReader(rels); drawingRels != nil { + for _, v := range drawingRels.Relationships { + if v.ID == rID { + return &v + } } } return nil } + +// drawingRelsReader provides a function to get the pointer to the structure +// after deserialization of xl/drawings/_rels/drawing%d.xml.rels. +func (f *File) drawingRelsReader(rel string) *xlsxWorkbookRels { + if f.DrawingRels[rel] == nil { + _, ok := f.XLSX[rel] + if ok { + d := xlsxWorkbookRels{} + _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML(rel)), &d) + f.DrawingRels[rel] = &d + } + } + return f.DrawingRels[rel] +} + +// drawingRelsWriter provides a function to save +// xl/drawings/_rels/drawing%d.xml.rels after serialize structure. +func (f *File) drawingRelsWriter() { + for path, d := range f.DrawingRels { + if d != nil { + v, _ := xml.Marshal(d) + f.saveFileList(path, v) + } + } +} + +// drawingsWriter provides a function to save xl/drawings/drawing%d.xml after +// serialize structure. +func (f *File) drawingsWriter() { + for path, d := range f.Drawings { + if d != nil { + v, _ := xml.Marshal(d) + f.saveFileList(path, v) + } + } +} diff --git a/shape.go b/shape.go index e2281a23ce..3cf09d8aa5 100644 --- a/shape.go +++ b/shape.go @@ -11,7 +11,6 @@ package excelize import ( "encoding/json" - "encoding/xml" "strconv" "strings" ) @@ -293,10 +292,7 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, formatSet *format width := int(float64(formatSet.Width) * formatSet.Format.XScale) height := int(float64(formatSet.Height) * formatSet.Format.YScale) colStart, rowStart, _, _, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, col, row, formatSet.Format.OffsetX, formatSet.Format.OffsetY, width, height) - content := xlsxWsDr{} - content.A = NameSpaceDrawingML - content.Xdr = NameSpaceDrawingMLSpreadSheet - cNvPrID := f.drawingParser(drawingXML, &content) + content, cNvPrID := f.drawingParser(drawingXML) twoCellAnchor := xdrCellAnchor{} twoCellAnchor.EditAs = formatSet.Format.Positioning from := xlsxFrom{} @@ -402,8 +398,7 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, formatSet *format FPrintsWithSheet: formatSet.Format.FPrintsWithSheet, } content.TwoCellAnchor = append(content.TwoCellAnchor, &twoCellAnchor) - output, _ := xml.Marshal(content) - f.saveFileList(drawingXML, output) + f.Drawings[drawingXML] = content } // setShapeRef provides a function to set color with hex model by given actual From f66212da9bab1c39ab791d41881c70ae7ba00c20 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 26 Feb 2019 14:21:44 +0800 Subject: [PATCH 060/957] Resolve #106, #294 performance optimization for add hyperlink --- comment.go | 10 +++++----- excelize.go | 2 ++ file.go | 8 +++++--- picture.go | 35 ++++++++++++++++++----------------- sheet.go | 37 +++++++++++++++++++++++++++++++------ test/CalcChain.xlsx | Bin 6 files changed, 61 insertions(+), 31 deletions(-) mode change 100755 => 100644 test/CalcChain.xlsx diff --git a/comment.go b/comment.go index c86e93257e..7fb1739aaf 100644 --- a/comment.go +++ b/comment.go @@ -57,11 +57,11 @@ func (f *File) GetComments() (comments map[string][]Comment) { // given worksheet index. func (f *File) getSheetComments(sheetID int) string { var rels = "xl/worksheets/_rels/sheet" + strconv.Itoa(sheetID) + ".xml.rels" - var sheetRels xlsxWorkbookRels - _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML(rels)), &sheetRels) - for _, v := range sheetRels.Relationships { - if v.Type == SourceRelationshipComments { - return v.Target + if sheetRels := f.workSheetRelsReader(rels); sheetRels != nil { + for _, v := range sheetRels.Relationships { + if v.Type == SourceRelationshipComments { + return v.Target + } } } return "" diff --git a/excelize.go b/excelize.go index a2bec077e1..feb41cba31 100644 --- a/excelize.go +++ b/excelize.go @@ -40,6 +40,7 @@ type File struct { VMLDrawing map[string]*vmlDrawing WorkBook *xlsxWorkbook WorkBookRels *xlsxWorkbookRels + WorkSheetRels map[string]*xlsxWorkbookRels XLSX map[string][]byte } @@ -84,6 +85,7 @@ func OpenReader(r io.Reader) (*File, error) { SheetCount: sheetCount, DecodeVMLDrawing: make(map[string]*decodeVmlDrawing), VMLDrawing: make(map[string]*vmlDrawing), + WorkSheetRels: make(map[string]*xlsxWorkbookRels), XLSX: file, } f.CalcChain = f.calcChainReader() diff --git a/file.go b/file.go index 8d68851e22..2f5164f39e 100644 --- a/file.go +++ b/file.go @@ -49,6 +49,7 @@ func NewFile() *File { f.VMLDrawing = make(map[string]*vmlDrawing) f.WorkBook = f.workbookReader() f.WorkBookRels = f.workbookRelsReader() + f.WorkSheetRels = make(map[string]*xlsxWorkbookRels) f.Sheet["xl/worksheets/sheet1.xml"] = f.workSheetReader("Sheet1") f.sheetMap["Sheet1"] = "xl/worksheets/sheet1.xml" f.Theme = f.themeReader() @@ -99,9 +100,10 @@ func (f *File) WriteToBuffer() (*bytes.Buffer, error) { f.drawingRelsWriter() f.drawingsWriter() f.vmlDrawingWriter() - f.workbookWriter() - f.workbookRelsWriter() - f.worksheetWriter() + f.workBookWriter() + f.workBookRelsWriter() + f.workSheetWriter() + f.workSheetRelsWriter() f.styleSheetWriter() for path, content := range f.XLSX { diff --git a/picture.go b/picture.go index 2ad9db2c3c..131b15ce7d 100644 --- a/picture.go +++ b/picture.go @@ -177,27 +177,25 @@ func (f *File) addSheetRelationships(sheet, relType, target, targetMode string) name = strings.ToLower(sheet) + ".xml" } var rels = "xl/worksheets/_rels/" + strings.TrimPrefix(name, "xl/worksheets/") + ".rels" - var sheetRels xlsxWorkbookRels + sheetRels := f.workSheetRelsReader(rels) + if sheetRels == nil { + sheetRels = &xlsxWorkbookRels{} + } var rID = 1 var ID bytes.Buffer ID.WriteString("rId") ID.WriteString(strconv.Itoa(rID)) - _, ok = f.XLSX[rels] - if ok { - ID.Reset() - _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML(rels)), &sheetRels) - rID = len(sheetRels.Relationships) + 1 - ID.WriteString("rId") - ID.WriteString(strconv.Itoa(rID)) - } + ID.Reset() + rID = len(sheetRels.Relationships) + 1 + ID.WriteString("rId") + ID.WriteString(strconv.Itoa(rID)) sheetRels.Relationships = append(sheetRels.Relationships, xlsxWorkbookRelation{ ID: ID.String(), Type: relType, Target: target, TargetMode: targetMode, }) - output, _ := xml.Marshal(sheetRels) - f.saveFileList(rels, output) + f.WorkSheetRels[rels] = sheetRels return rID } @@ -210,15 +208,16 @@ func (f *File) deleteSheetRelationships(sheet, rID string) { name = strings.ToLower(sheet) + ".xml" } var rels = "xl/worksheets/_rels/" + strings.TrimPrefix(name, "xl/worksheets/") + ".rels" - var sheetRels xlsxWorkbookRels - _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML(rels)), &sheetRels) + sheetRels := f.workSheetRelsReader(rels) + if sheetRels == nil { + sheetRels = &xlsxWorkbookRels{} + } for k, v := range sheetRels.Relationships { if v.ID == rID { sheetRels.Relationships = append(sheetRels.Relationships[:k], sheetRels.Relationships[k+1:]...) } } - output, _ := xml.Marshal(sheetRels) - f.saveFileList(rels, output) + f.WorkSheetRels[rels] = sheetRels } // addSheetLegacyDrawing provides a function to add legacy drawing element to @@ -441,8 +440,10 @@ func (f *File) getSheetRelationshipsTargetByID(sheet, rID string) string { name = strings.ToLower(sheet) + ".xml" } var rels = "xl/worksheets/_rels/" + strings.TrimPrefix(name, "xl/worksheets/") + ".rels" - var sheetRels xlsxWorkbookRels - _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML(rels)), &sheetRels) + sheetRels := f.workSheetRelsReader(rels) + if sheetRels == nil { + sheetRels = &xlsxWorkbookRels{} + } for _, v := range sheetRels.Relationships { if v.ID == rID { return v.Target diff --git a/sheet.go b/sheet.go index efbd4667ef..26d4b4cb0d 100644 --- a/sheet.go +++ b/sheet.go @@ -88,18 +88,18 @@ func (f *File) workbookReader() *xlsxWorkbook { return f.WorkBook } -// workbookWriter provides a function to save xl/workbook.xml after serialize +// workBookWriter provides a function to save xl/workbook.xml after serialize // structure. -func (f *File) workbookWriter() { +func (f *File) workBookWriter() { if f.WorkBook != nil { output, _ := xml.Marshal(f.WorkBook) f.saveFileList("xl/workbook.xml", replaceRelationshipsNameSpaceBytes(output)) } } -// worksheetWriter provides a function to save xl/worksheets/sheet%d.xml after +// workSheetWriter provides a function to save xl/worksheets/sheet%d.xml after // serialize structure. -func (f *File) worksheetWriter() { +func (f *File) workSheetWriter() { for path, sheet := range f.Sheet { if sheet != nil { for k, v := range sheet.SheetData.Row { @@ -172,9 +172,9 @@ func (f *File) workbookRelsReader() *xlsxWorkbookRels { return f.WorkBookRels } -// workbookRelsWriter provides a function to save xl/_rels/workbook.xml.rels after +// workBookRelsWriter provides a function to save xl/_rels/workbook.xml.rels after // serialize structure. -func (f *File) workbookRelsWriter() { +func (f *File) workBookRelsWriter() { if f.WorkBookRels != nil { output, _ := xml.Marshal(f.WorkBookRels) f.saveFileList("xl/_rels/workbook.xml.rels", output) @@ -1003,3 +1003,28 @@ func (f *File) GetPageLayout(sheet string, opts ...PageLayoutOptionPtr) error { } return nil } + +// workSheetRelsReader provides a function to get the pointer to the structure +// after deserialization of xl/worksheets/_rels/sheet%d.xml.rels. +func (f *File) workSheetRelsReader(path string) *xlsxWorkbookRels { + if f.WorkSheetRels[path] == nil { + _, ok := f.XLSX[path] + if ok { + c := xlsxWorkbookRels{} + _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML(path)), &c) + f.WorkSheetRels[path] = &c + } + } + return f.WorkSheetRels[path] +} + +// workSheetRelsWriter provides a function to save +// xl/worksheets/_rels/sheet%d.xml.rels after serialize structure. +func (f *File) workSheetRelsWriter() { + for path, r := range f.WorkSheetRels { + if r != nil { + v, _ := xml.Marshal(r) + f.saveFileList(path, v) + } + } +} diff --git a/test/CalcChain.xlsx b/test/CalcChain.xlsx old mode 100755 new mode 100644 From 12c1e2481e3f9f3c3c12a938484f04b12d5dede8 Mon Sep 17 00:00:00 2001 From: Veniamin Albaev Date: Wed, 6 Mar 2019 16:40:45 +0300 Subject: [PATCH 061/957] Implement consistent row addressing by Excel row number starting with 1 (#350) * Implement consistent row addressing by Excel row number starting with 1 1. Added second versions for all row manipulation methods with zero-based row addressing. 2. Fixed methods documentation to explicitly describe which row addressing used in method. 3. Added WARNING to README.md. 4. Cosmetic change: All row test moved to file `rows_test.go`. * TravisCI: go1.12 added to tests matrix * BACKWARD INCOMPARTIBLE: Use only Excel numbering logic from 1 row * README updated --- .travis.yml | 1 + README.md | 5 + excelize_test.go | 424 --------------------------------------- rows.go | 107 +++++----- rows_test.go | 507 +++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 573 insertions(+), 471 deletions(-) create mode 100644 rows_test.go diff --git a/.travis.yml b/.travis.yml index c2f0f90590..6c061a87a4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,7 @@ go: - 1.9.x - 1.10.x - 1.11.x + - 1.12.x os: - linux diff --git a/README.md b/README.md index 86d1e8a3af..c4936b6c16 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,11 @@ Excelize is a library written in pure Go and providing a set of functions that allow you to write to and read from XLSX files. Support reads and writes XLSX file generated by Microsoft Excel™ 2007 and later. Support save file without losing original charts of XLSX. This library needs Go version 1.8 or later. The full API docs can be seen using go's built-in documentation tool, or online at [godoc.org](https://godoc.org/github.com/360EntSecGroup-Skylar/excelize) and [docs reference](https://xuri.me/excelize/). +**WARNING!** + +From version 1.5 all row manipulation methods uses Excel row numbering starting with `1` instead of zero-based numbering +which take place in some methods in eraler versions. + ## Basic Usage ### Installation diff --git a/excelize_test.go b/excelize_test.go index ed6f073ce8..abee199f5a 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -287,19 +287,6 @@ func TestColWidth(t *testing.T) { convertRowHeightToPixels(0) } -func TestRowHeight(t *testing.T) { - xlsx := NewFile() - xlsx.SetRowHeight("Sheet1", 1, 50) - xlsx.SetRowHeight("Sheet1", 4, 90) - t.Log(xlsx.GetRowHeight("Sheet1", 1)) - t.Log(xlsx.GetRowHeight("Sheet1", 0)) - err := xlsx.SaveAs(filepath.Join("test", "TestRowHeight.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } - convertColWidthToPixels(0) -} - func TestSetCellHyperLink(t *testing.T) { xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if err != nil { @@ -1112,335 +1099,6 @@ func TestRemoveCol(t *testing.T) { assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestRemoveCol.xlsx"))) } -func TestInsertRow(t *testing.T) { - xlsx := NewFile() - for j := 1; j <= 10; j++ { - for i := 0; i <= 10; i++ { - axis := ToAlphaString(i) + strconv.Itoa(j) - xlsx.SetCellStr("Sheet1", axis, axis) - } - } - xlsx.SetCellHyperLink("Sheet1", "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External") - xlsx.InsertRow("Sheet1", -1) - xlsx.InsertRow("Sheet1", 4) - - assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestInsertRow.xlsx"))) -} - -// Testing internal sructure 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) { - xlsx := NewFile() - sheet1 := xlsx.GetSheetName(1) - r := xlsx.workSheetReader(sheet1) - xlsx.InsertRow(sheet1, 0) - assert.Len(t, r.SheetData.Row, 0) - xlsx.InsertRow(sheet1, 1) - assert.Len(t, r.SheetData.Row, 0) - xlsx.InsertRow(sheet1, 99) - assert.Len(t, r.SheetData.Row, 0) - assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestInsertRowInEmptyFile.xlsx"))) -} - -func TestDuplicateRow(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 { - f.SetCellStr(sheet, cell, val) - - } - return f - } - - t.Run("FromSingleRow", func(t *testing.T) { - xlsx := NewFile() - xlsx.SetCellStr(sheet, "A1", cells["A1"]) - xlsx.SetCellStr(sheet, "B1", cells["B1"]) - - xlsx.DuplicateRow(sheet, 1) - if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.FromSingleRow_1"))) { - t.FailNow() - } - expect := map[string]string{ - "A1": cells["A1"], "B1": cells["B1"], - "A2": cells["A1"], "B2": cells["B1"], - } - for cell, val := range expect { - if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { - t.FailNow() - } - } - - xlsx.DuplicateRow(sheet, 2) - if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.FromSingleRow_2"))) { - t.FailNow() - } - expect = map[string]string{ - "A1": cells["A1"], "B1": cells["B1"], - "A2": cells["A1"], "B2": cells["B1"], - "A3": cells["A1"], "B3": cells["B1"], - } - for cell, val := range expect { - if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { - t.FailNow() - } - } - }) - - t.Run("UpdateDuplicatedRows", func(t *testing.T) { - xlsx := NewFile() - xlsx.SetCellStr(sheet, "A1", cells["A1"]) - xlsx.SetCellStr(sheet, "B1", cells["B1"]) - - xlsx.DuplicateRow(sheet, 1) - - xlsx.SetCellStr(sheet, "A2", cells["A2"]) - xlsx.SetCellStr(sheet, "B2", cells["B2"]) - - if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.UpdateDuplicatedRows"))) { - t.FailNow() - } - expect := map[string]string{ - "A1": cells["A1"], "B1": cells["B1"], - "A2": cells["A2"], "B2": cells["B2"], - } - for cell, val := range expect { - if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { - t.FailNow() - } - } - }) - - t.Run("FirstOfMultipleRows", func(t *testing.T) { - xlsx := newFileWithDefaults() - - xlsx.DuplicateRow(sheet, 1) - - if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.FirstOfMultipleRows"))) { - t.FailNow() - } - expect := map[string]string{ - "A1": cells["A1"], "B1": cells["B1"], - "A2": cells["A1"], "B2": cells["B1"], - "A3": cells["A2"], "B3": cells["B2"], - "A4": cells["A3"], "B4": cells["B3"], - } - for cell, val := range expect { - if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { - t.FailNow() - } - } - }) - - t.Run("ZeroWithNoRows", func(t *testing.T) { - xlsx := NewFile() - - xlsx.DuplicateRow(sheet, 0) - - if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.ZeroWithNoRows"))) { - t.FailNow() - } - assert.Equal(t, "", xlsx.GetCellValue(sheet, "A1")) - assert.Equal(t, "", xlsx.GetCellValue(sheet, "B1")) - assert.Equal(t, "", xlsx.GetCellValue(sheet, "A2")) - assert.Equal(t, "", xlsx.GetCellValue(sheet, "B2")) - expect := map[string]string{ - "A1": "", "B1": "", - "A2": "", "B2": "", - } - for cell, val := range expect { - if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { - t.FailNow() - } - } - }) - - t.Run("MiddleRowOfEmptyFile", func(t *testing.T) { - xlsx := NewFile() - - xlsx.DuplicateRow(sheet, 99) - - if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.MiddleRowOfEmptyFile"))) { - t.FailNow() - } - expect := map[string]string{ - "A98": "", - "A99": "", - "A100": "", - } - for cell, val := range expect { - if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { - t.FailNow() - } - } - }) - - t.Run("WithLargeOffsetToMiddleOfData", func(t *testing.T) { - xlsx := newFileWithDefaults() - - xlsx.DuplicateRowTo(sheet, 1, 3) - - if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.WithLargeOffsetToMiddleOfData"))) { - t.FailNow() - } - expect := map[string]string{ - "A1": cells["A1"], "B1": cells["B1"], - "A2": cells["A2"], "B2": cells["B2"], - "A3": cells["A1"], "B3": cells["B1"], - "A4": cells["A3"], "B4": cells["B3"], - } - for cell, val := range expect { - if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { - t.FailNow() - } - } - }) - - t.Run("WithLargeOffsetToEmptyRows", func(t *testing.T) { - xlsx := newFileWithDefaults() - - xlsx.DuplicateRowTo(sheet, 1, 7) - - if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.WithLargeOffsetToEmptyRows"))) { - t.FailNow() - } - expect := map[string]string{ - "A1": cells["A1"], "B1": cells["B1"], - "A2": cells["A2"], "B2": cells["B2"], - "A3": cells["A3"], "B3": cells["B3"], - "A7": cells["A1"], "B7": cells["B1"], - } - for cell, val := range expect { - if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { - t.FailNow() - } - } - }) - - t.Run("InsertBefore", func(t *testing.T) { - xlsx := newFileWithDefaults() - - xlsx.DuplicateRowTo(sheet, 2, 1) - - if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.InsertBefore"))) { - t.FailNow() - } - - expect := map[string]string{ - "A1": cells["A2"], "B1": cells["B2"], - "A2": cells["A1"], "B2": cells["B1"], - "A3": cells["A2"], "B3": cells["B2"], - "A4": cells["A3"], "B4": cells["B3"], - } - for cell, val := range expect { - if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { - t.FailNow() - } - } - }) - - t.Run("InsertBeforeWithLargeOffset", func(t *testing.T) { - xlsx := newFileWithDefaults() - - xlsx.DuplicateRowTo(sheet, 3, 1) - - if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.InsertBeforeWithLargeOffset"))) { - t.FailNow() - } - - expect := map[string]string{ - "A1": cells["A3"], "B1": cells["B3"], - "A2": cells["A1"], "B2": cells["B1"], - "A3": cells["A2"], "B3": cells["B2"], - "A4": cells["A3"], "B4": cells["B3"], - } - for cell, val := range expect { - if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell)) { - t.FailNow() - } - } - }) -} - -func TestDuplicateRowInvalidRownum(t *testing.T) { - const sheet = "Sheet1" - outFile := filepath.Join("test", "TestDuplicateRowInvalidRownum.%s.xlsx") - - cells := map[string]string{ - "A1": "A1 Value", - "A2": "A2 Value", - "A3": "A3 Value", - "B1": "B1 Value", - "B2": "B2 Value", - "B3": "B3 Value", - } - - testRows := []int{-2, -1} - - testRowPairs := []struct { - row1 int - row2 int - }{ - {-1, -1}, - {-1, 0}, - {-1, 1}, - {0, -1}, - {0, 0}, - {0, 1}, - {1, -1}, - {1, 1}, - {1, 0}, - } - - for i, row := range testRows { - name := fmt.Sprintf("TestRow_%d", i+1) - t.Run(name, func(t *testing.T) { - xlsx := NewFile() - for col, val := range cells { - xlsx.SetCellStr(sheet, col, val) - } - xlsx.DuplicateRow(sheet, row) - - for col, val := range cells { - if !assert.Equal(t, val, xlsx.GetCellValue(sheet, col)) { - t.FailNow() - } - } - assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, name))) - }) - } - - for i, pair := range testRowPairs { - name := fmt.Sprintf("TestRowPair_%d", i+1) - t.Run(name, func(t *testing.T) { - xlsx := NewFile() - for col, val := range cells { - xlsx.SetCellStr(sheet, col, val) - } - xlsx.DuplicateRowTo(sheet, pair.row1, pair.row2) - - for col, val := range cells { - if !assert.Equal(t, val, xlsx.GetCellValue(sheet, col)) { - t.FailNow() - } - } - assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, name))) - }) - } -} - func TestSetPane(t *testing.T) { xlsx := NewFile() xlsx.SetPanes("Sheet1", `{"freeze":false,"split":false}`) @@ -1455,33 +1113,6 @@ func TestSetPane(t *testing.T) { assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetPane.xlsx"))) } -func TestRemoveRow(t *testing.T) { - xlsx := NewFile() - for j := 1; j <= 10; j++ { - for i := 0; i <= 10; i++ { - axis := ToAlphaString(i) + strconv.Itoa(j) - xlsx.SetCellStr("Sheet1", axis, axis) - } - } - xlsx.SetCellHyperLink("Sheet1", "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External") - xlsx.RemoveRow("Sheet1", -1) - xlsx.RemoveRow("Sheet1", 4) - xlsx.MergeCell("Sheet1", "B3", "B5") - xlsx.RemoveRow("Sheet1", 2) - xlsx.RemoveRow("Sheet1", 4) - - err := xlsx.AutoFilter("Sheet1", "A2", "A2", `{"column":"A","expression":"x != blanks"}`) - if !assert.NoError(t, err) { - t.FailNow() - } - - xlsx.RemoveRow("Sheet1", 0) - xlsx.RemoveRow("Sheet1", 1) - xlsx.RemoveRow("Sheet1", 0) - - assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestRemoveRow.xlsx"))) -} - func TestConditionalFormat(t *testing.T) { xlsx := NewFile() for j := 1; j <= 10; j++ { @@ -1604,50 +1235,6 @@ func TestSetSheetRow(t *testing.T) { assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetSheetRow.xlsx"))) } -func TestRows(t *testing.T) { - xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } - - rows, err := xlsx.Rows("Sheet2") - if !assert.NoError(t, err) { - t.FailNow() - } - - rowStrs := make([][]string, 0) - var i = 0 - for rows.Next() { - i++ - columns := rows.Columns() - rowStrs = append(rowStrs, columns) - } - - if !assert.NoError(t, rows.Error()) { - t.FailNow() - } - - dstRows := xlsx.GetRows("Sheet2") - if !assert.Equal(t, len(rowStrs), len(dstRows)) { - t.FailNow() - } - - for i := 0; i < len(rowStrs); i++ { - if !assert.Equal(t, trimSliceSpace(dstRows[i]), trimSliceSpace(rowStrs[i])) { - t.FailNow() - } - } -} - -func TestRowsError(t *testing.T) { - xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } - _, err = xlsx.Rows("SheetN") - assert.EqualError(t, err, "Sheet SheetN is not exist") -} - func TestOutlineLevel(t *testing.T) { xlsx := NewFile() xlsx.NewSheet("Sheet2") @@ -1733,17 +1320,6 @@ func TestUnprotectSheet(t *testing.T) { assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestUnprotectSheet.xlsx"))) } -func trimSliceSpace(s []string) []string { - for { - if len(s) > 0 && s[len(s)-1] == "" { - s = s[:len(s)-1] - } else { - break - } - } - return s -} - func prepareTestBook1() (*File, error) { xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if err != nil { diff --git a/rows.go b/rows.go index aebc97941c..79d8e46b42 100644 --- a/rows.go +++ b/rows.go @@ -203,6 +203,9 @@ func (f *File) getTotalRowsCols(name string) (int, int) { // func (f *File) SetRowHeight(sheet string, row int, height float64) { xlsx := f.workSheetReader(sheet) + if row < 1 { + return + } cells := 0 rowIdx := row - 1 completeRow(xlsx, row, cells) @@ -230,6 +233,9 @@ func (f *File) getRowHeight(sheet string, row int) int { // func (f *File) GetRowHeight(sheet string, row int) float64 { xlsx := f.workSheetReader(sheet) + if row < 1 || row > len(xlsx.SheetData.Row) { + return defaultRowHeightPixels // it will be better to use 0, but we take care with BC + } for _, v := range xlsx.SheetData.Row { if v.R == row && v.Ht != 0 { return v.Ht @@ -279,80 +285,88 @@ func (xlsx *xlsxC) getValueFrom(f *File, d *xlsxSST) (string, error) { } } -// SetRowVisible provides a function to set visible of a single row by given -// worksheet name and row index. For example, hide row 2 in Sheet1: +// SetRowVisible2 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: // // xlsx.SetRowVisible("Sheet1", 2, false) // -func (f *File) SetRowVisible(sheet string, rowIndex int, visible bool) { +func (f *File) SetRowVisible(sheet string, row int, visible bool) { xlsx := f.workSheetReader(sheet) - rows := rowIndex + 1 + if row < 1 { + return + } cells := 0 - completeRow(xlsx, rows, cells) + completeRow(xlsx, row, cells) + rowIdx := row - 1 if visible { - xlsx.SheetData.Row[rowIndex].Hidden = false + xlsx.SheetData.Row[rowIdx].Hidden = false return } - xlsx.SheetData.Row[rowIndex].Hidden = true + xlsx.SheetData.Row[rowIdx].Hidden = true } -// GetRowVisible provides a function to get visible of a single row by given -// worksheet name and row index. For example, get visible state of row 2 in +// GetRowVisible2 provides a function to get visible of a single row by given +// worksheet name and Excel row number. +// For example, get visible state of row 2 in // Sheet1: // // xlsx.GetRowVisible("Sheet1", 2) // -func (f *File) GetRowVisible(sheet string, rowIndex int) bool { +func (f *File) GetRowVisible(sheet string, row int) bool { xlsx := f.workSheetReader(sheet) - rows := rowIndex + 1 + if row < 1 || row > len(xlsx.SheetData.Row) { + return false + } + rowIndex := row - 1 cells := 0 - completeRow(xlsx, rows, cells) + completeRow(xlsx, row, cells) return !xlsx.SheetData.Row[rowIndex].Hidden } // SetRowOutlineLevel provides a function to set outline level number of a -// single row by given worksheet name and row index. For example, outline row +// single row by given worksheet name and Excel row number. For example, outline row // 2 in Sheet1 to level 1: // // xlsx.SetRowOutlineLevel("Sheet1", 2, 1) // -func (f *File) SetRowOutlineLevel(sheet string, rowIndex int, level uint8) { +func (f *File) SetRowOutlineLevel(sheet string, row int, level uint8) { xlsx := f.workSheetReader(sheet) - rows := rowIndex + 1 + if row < 1 { + return + } cells := 0 - completeRow(xlsx, rows, cells) - xlsx.SheetData.Row[rowIndex].OutlineLevel = level + completeRow(xlsx, row, cells) + xlsx.SheetData.Row[row-1].OutlineLevel = level } // GetRowOutlineLevel provides a function to get outline level number of a -// single row by given worksheet name and row index. For example, get outline -// number of row 2 in Sheet1: +// single row by given worksheet name and Exce row number. +// For example, get outline number of row 2 in Sheet1: // // xlsx.GetRowOutlineLevel("Sheet1", 2) // -func (f *File) GetRowOutlineLevel(sheet string, rowIndex int) uint8 { +func (f *File) GetRowOutlineLevel(sheet string, row int) uint8 { xlsx := f.workSheetReader(sheet) - rows := rowIndex + 1 - cells := 0 - completeRow(xlsx, rows, cells) - return xlsx.SheetData.Row[rowIndex].OutlineLevel + if row < 1 || row > len(xlsx.SheetData.Row) { + return 0 + } + return xlsx.SheetData.Row[row-1].OutlineLevel } -// RemoveRow provides a function to remove single row by given worksheet name -// and row index. For example, remove row 3 in Sheet1: +// RemoveRow2 provides a function to remove single row by given worksheet name +// and Excel row number. For example, remove row 3 in Sheet1: // -// xlsx.RemoveRow("Sheet1", 2) +// xlsx.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 // worksheet, it will cause a file error when you open it. The excelize only // partially updates these references currently. func (f *File) RemoveRow(sheet string, row int) { - if row < 0 { + xlsx := f.workSheetReader(sheet) + if row < 1 || row > len(xlsx.SheetData.Row) { return } - xlsx := f.workSheetReader(sheet) - row++ for i, r := range xlsx.SheetData.Row { if r.R == row { xlsx.SheetData.Row = append(xlsx.SheetData.Row[:i], xlsx.SheetData.Row[i+1:]...) @@ -362,20 +376,19 @@ func (f *File) RemoveRow(sheet string, row int) { } } -// InsertRow provides a function to insert a new row after given row index. -// For example, create a new row before row 3 in Sheet1: +// InsertRow2 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: // -// xlsx.InsertRow("Sheet1", 2) +// xlsx.InsertRow("Sheet1", 3) // func (f *File) InsertRow(sheet string, row int) { - if row < 0 { + if row < 1 { return } - row++ f.adjustHelper(sheet, -1, row, 1) } -// DuplicateRow inserts a copy of specified row below specified +// DuplicateRow inserts a copy of specified row (by it Excel row number) below // // xlsx.DuplicateRow("Sheet1", 2) // @@ -387,8 +400,8 @@ func (f *File) DuplicateRow(sheet string, row int) { f.DuplicateRowTo(sheet, row, row+1) } -// DuplicateRowTo inserts a copy of specified row at specified row position -// moving down exists rows after target position +// DuplicateRowTo inserts a copy of specified row by it Excel number +// to specified row position moving down exists rows after target position // // xlsx.DuplicateRowTo("Sheet1", 2, 7) // @@ -397,18 +410,18 @@ func (f *File) DuplicateRow(sheet string, row int) { // worksheet, it will cause a file error when you open it. The excelize only // partially updates these references currently. func (f *File) DuplicateRowTo(sheet string, row, row2 int) { - if row <= 0 || row2 <= 0 || row == row2 { + xlsx := f.workSheetReader(sheet) + + if row < 1 || row > len(xlsx.SheetData.Row) || row2 < 1 || row == row2 { return } - ws := f.workSheetReader(sheet) - var ok bool var rowCopy xlsxRow - for i, r := range ws.SheetData.Row { + for i, r := range xlsx.SheetData.Row { if r.R == row { - rowCopy = ws.SheetData.Row[i] + rowCopy = xlsx.SheetData.Row[i] ok = true break } @@ -420,13 +433,13 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) { f.adjustHelper(sheet, -1, row2, 1) idx2 := -1 - for i, r := range ws.SheetData.Row { + for i, r := range xlsx.SheetData.Row { if r.R == row2 { idx2 = i break } } - if idx2 == -1 && len(ws.SheetData.Row) >= row2 { + if idx2 == -1 && len(xlsx.SheetData.Row) >= row2 { return } @@ -434,9 +447,9 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) { f.ajustSingleRowDimensions(&rowCopy, row2) if idx2 != -1 { - ws.SheetData.Row[idx2] = rowCopy + xlsx.SheetData.Row[idx2] = rowCopy } else { - ws.SheetData.Row = append(ws.SheetData.Row, rowCopy) + xlsx.SheetData.Row = append(xlsx.SheetData.Row, rowCopy) } } diff --git a/rows_test.go b/rows_test.go new file mode 100644 index 0000000000..26fcb47375 --- /dev/null +++ b/rows_test.go @@ -0,0 +1,507 @@ +package excelize + +import ( + "fmt" + "path/filepath" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRows(t *testing.T) { + xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + if !assert.NoError(t, err) { + t.FailNow() + } + + rows, err := xlsx.Rows("Sheet2") + if !assert.NoError(t, err) { + t.FailNow() + } + + rowStrs := make([][]string, 0) + var i = 0 + for rows.Next() { + i++ + columns := rows.Columns() + rowStrs = append(rowStrs, columns) + } + + if !assert.NoError(t, rows.Error()) { + t.FailNow() + } + + dstRows := xlsx.GetRows("Sheet2") + if !assert.Equal(t, len(rowStrs), len(dstRows)) { + t.FailNow() + } + + for i := 0; i < len(rowStrs); i++ { + if !assert.Equal(t, trimSliceSpace(dstRows[i]), trimSliceSpace(rowStrs[i])) { + t.FailNow() + } + } +} + +func TestRowsError(t *testing.T) { + xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + if !assert.NoError(t, err) { + t.FailNow() + } + _, err = xlsx.Rows("SheetN") + assert.EqualError(t, err, "Sheet SheetN is not exist") +} + +func TestRowHeight(t *testing.T) { + xlsx := NewFile() + sheet1 := xlsx.GetSheetName(1) + + xlsx.SetRowHeight(sheet1, 0, defaultRowHeightPixels+1.0) // should no effect + assert.Equal(t, defaultRowHeightPixels, xlsx.GetRowHeight("Sheet1", 0)) + + xlsx.SetRowHeight(sheet1, 1, 111.0) + assert.Equal(t, 111.0, xlsx.GetRowHeight(sheet1, 1)) + + xlsx.SetRowHeight(sheet1, 4, 444.0) + assert.Equal(t, 444.0, xlsx.GetRowHeight(sheet1, 4)) + + err := xlsx.SaveAs(filepath.Join("test", "TestRowHeight.xlsx")) + if !assert.NoError(t, err) { + t.FailNow() + } + + convertColWidthToPixels(0) +} + +func TestRemoveRow(t *testing.T) { + xlsx := NewFile() + sheet1 := xlsx.GetSheetName(1) + r := xlsx.workSheetReader(sheet1) + + const ( + cellCount = 10 + rowCount = 10 + ) + for j := 1; j <= cellCount; j++ { + for i := 1; i <= rowCount; i++ { + axis := ToAlphaString(i) + strconv.Itoa(j) + xlsx.SetCellStr(sheet1, axis, axis) + } + } + xlsx.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External") + + xlsx.RemoveRow(sheet1, -1) + if !assert.Len(t, r.SheetData.Row, rowCount) { + t.FailNow() + } + + xlsx.RemoveRow(sheet1, 0) + if !assert.Len(t, r.SheetData.Row, rowCount) { + t.FailNow() + } + + xlsx.RemoveRow(sheet1, 4) + if !assert.Len(t, r.SheetData.Row, rowCount-1) { + t.FailNow() + } + + xlsx.MergeCell(sheet1, "B3", "B5") + + xlsx.RemoveRow(sheet1, 2) + if !assert.Len(t, r.SheetData.Row, rowCount-2) { + t.FailNow() + } + + xlsx.RemoveRow(sheet1, 4) + if !assert.Len(t, r.SheetData.Row, rowCount-3) { + t.FailNow() + } + + err := xlsx.AutoFilter(sheet1, "A2", "A2", `{"column":"A","expression":"x != blanks"}`) + if !assert.NoError(t, err) { + t.FailNow() + } + + xlsx.RemoveRow(sheet1, 1) + if !assert.Len(t, r.SheetData.Row, rowCount-4) { + t.FailNow() + } + + xlsx.RemoveRow(sheet1, 2) + if !assert.Len(t, r.SheetData.Row, rowCount-5) { + t.FailNow() + } + + xlsx.RemoveRow(sheet1, 1) + if !assert.Len(t, r.SheetData.Row, rowCount-6) { + t.FailNow() + } + + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestRemoveRow.xlsx"))) +} + +func TestInsertRow(t *testing.T) { + xlsx := NewFile() + sheet1 := xlsx.GetSheetName(1) + r := xlsx.workSheetReader(sheet1) + + const ( + cellCount = 10 + rowCount = 10 + ) + for j := 1; j <= cellCount; j++ { + for i := 1; i < rowCount; i++ { + axis := ToAlphaString(i) + strconv.Itoa(j) + xlsx.SetCellStr(sheet1, axis, axis) + } + } + xlsx.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External") + + xlsx.InsertRow(sheet1, -1) + if !assert.Len(t, r.SheetData.Row, rowCount) { + t.FailNow() + } + + xlsx.InsertRow(sheet1, 0) + if !assert.Len(t, r.SheetData.Row, rowCount) { + t.FailNow() + } + + xlsx.InsertRow(sheet1, 1) + if !assert.Len(t, r.SheetData.Row, rowCount+1) { + t.FailNow() + } + + xlsx.InsertRow(sheet1, 4) + if !assert.Len(t, r.SheetData.Row, rowCount+2) { + t.FailNow() + } + + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestInsertRow.xlsx"))) +} + +// Testing internal sructure 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) { + xlsx := NewFile() + sheet1 := xlsx.GetSheetName(1) + r := xlsx.workSheetReader(sheet1) + xlsx.InsertRow(sheet1, 1) + assert.Len(t, r.SheetData.Row, 0) + xlsx.InsertRow(sheet1, 2) + assert.Len(t, r.SheetData.Row, 0) + xlsx.InsertRow(sheet1, 99) + assert.Len(t, r.SheetData.Row, 0) + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestInsertRowInEmptyFile.xlsx"))) +} + +func TestDuplicateRow(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 { + f.SetCellStr(sheet, cell, val) + + } + return f + } + + t.Run("FromSingleRow", func(t *testing.T) { + xlsx := NewFile() + xlsx.SetCellStr(sheet, "A1", cells["A1"]) + xlsx.SetCellStr(sheet, "B1", cells["B1"]) + + xlsx.DuplicateRow(sheet, 1) + if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.FromSingleRow_1"))) { + t.FailNow() + } + expect := map[string]string{ + "A1": cells["A1"], "B1": cells["B1"], + "A2": cells["A1"], "B2": cells["B1"], + } + for cell, val := range expect { + if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { + t.FailNow() + } + } + + xlsx.DuplicateRow(sheet, 2) + if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.FromSingleRow_2"))) { + t.FailNow() + } + expect = map[string]string{ + "A1": cells["A1"], "B1": cells["B1"], + "A2": cells["A1"], "B2": cells["B1"], + "A3": cells["A1"], "B3": cells["B1"], + } + for cell, val := range expect { + if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { + t.FailNow() + } + } + }) + + t.Run("UpdateDuplicatedRows", func(t *testing.T) { + xlsx := NewFile() + xlsx.SetCellStr(sheet, "A1", cells["A1"]) + xlsx.SetCellStr(sheet, "B1", cells["B1"]) + + xlsx.DuplicateRow(sheet, 1) + + xlsx.SetCellStr(sheet, "A2", cells["A2"]) + xlsx.SetCellStr(sheet, "B2", cells["B2"]) + + if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.UpdateDuplicatedRows"))) { + t.FailNow() + } + expect := map[string]string{ + "A1": cells["A1"], "B1": cells["B1"], + "A2": cells["A2"], "B2": cells["B2"], + } + for cell, val := range expect { + if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { + t.FailNow() + } + } + }) + + t.Run("FirstOfMultipleRows", func(t *testing.T) { + xlsx := newFileWithDefaults() + + xlsx.DuplicateRow(sheet, 1) + + if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.FirstOfMultipleRows"))) { + t.FailNow() + } + expect := map[string]string{ + "A1": cells["A1"], "B1": cells["B1"], + "A2": cells["A1"], "B2": cells["B1"], + "A3": cells["A2"], "B3": cells["B2"], + "A4": cells["A3"], "B4": cells["B3"], + } + for cell, val := range expect { + if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { + t.FailNow() + } + } + }) + + t.Run("ZeroWithNoRows", func(t *testing.T) { + xlsx := NewFile() + + xlsx.DuplicateRow(sheet, 0) + + if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.ZeroWithNoRows"))) { + t.FailNow() + } + assert.Equal(t, "", xlsx.GetCellValue(sheet, "A1")) + assert.Equal(t, "", xlsx.GetCellValue(sheet, "B1")) + assert.Equal(t, "", xlsx.GetCellValue(sheet, "A2")) + assert.Equal(t, "", xlsx.GetCellValue(sheet, "B2")) + expect := map[string]string{ + "A1": "", "B1": "", + "A2": "", "B2": "", + } + for cell, val := range expect { + if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { + t.FailNow() + } + } + }) + + t.Run("MiddleRowOfEmptyFile", func(t *testing.T) { + xlsx := NewFile() + + xlsx.DuplicateRow(sheet, 99) + + if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.MiddleRowOfEmptyFile"))) { + t.FailNow() + } + expect := map[string]string{ + "A98": "", + "A99": "", + "A100": "", + } + for cell, val := range expect { + if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { + t.FailNow() + } + } + }) + + t.Run("WithLargeOffsetToMiddleOfData", func(t *testing.T) { + xlsx := newFileWithDefaults() + + xlsx.DuplicateRowTo(sheet, 1, 3) + + if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.WithLargeOffsetToMiddleOfData"))) { + t.FailNow() + } + expect := map[string]string{ + "A1": cells["A1"], "B1": cells["B1"], + "A2": cells["A2"], "B2": cells["B2"], + "A3": cells["A1"], "B3": cells["B1"], + "A4": cells["A3"], "B4": cells["B3"], + } + for cell, val := range expect { + if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { + t.FailNow() + } + } + }) + + t.Run("WithLargeOffsetToEmptyRows", func(t *testing.T) { + xlsx := newFileWithDefaults() + + xlsx.DuplicateRowTo(sheet, 1, 7) + + if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.WithLargeOffsetToEmptyRows"))) { + t.FailNow() + } + expect := map[string]string{ + "A1": cells["A1"], "B1": cells["B1"], + "A2": cells["A2"], "B2": cells["B2"], + "A3": cells["A3"], "B3": cells["B3"], + "A7": cells["A1"], "B7": cells["B1"], + } + for cell, val := range expect { + if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { + t.FailNow() + } + } + }) + + t.Run("InsertBefore", func(t *testing.T) { + xlsx := newFileWithDefaults() + + xlsx.DuplicateRowTo(sheet, 2, 1) + + if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.InsertBefore"))) { + t.FailNow() + } + + expect := map[string]string{ + "A1": cells["A2"], "B1": cells["B2"], + "A2": cells["A1"], "B2": cells["B1"], + "A3": cells["A2"], "B3": cells["B2"], + "A4": cells["A3"], "B4": cells["B3"], + } + for cell, val := range expect { + if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { + t.FailNow() + } + } + }) + + t.Run("InsertBeforeWithLargeOffset", func(t *testing.T) { + xlsx := newFileWithDefaults() + + xlsx.DuplicateRowTo(sheet, 3, 1) + + if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.InsertBeforeWithLargeOffset"))) { + t.FailNow() + } + + expect := map[string]string{ + "A1": cells["A3"], "B1": cells["B3"], + "A2": cells["A1"], "B2": cells["B1"], + "A3": cells["A2"], "B3": cells["B2"], + "A4": cells["A3"], "B4": cells["B3"], + } + for cell, val := range expect { + if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell)) { + t.FailNow() + } + } + }) +} + +func TestDuplicateRowInvalidRownum(t *testing.T) { + const sheet = "Sheet1" + outFile := filepath.Join("test", "TestDuplicateRowInvalidRownum.%s.xlsx") + + cells := map[string]string{ + "A1": "A1 Value", + "A2": "A2 Value", + "A3": "A3 Value", + "B1": "B1 Value", + "B2": "B2 Value", + "B3": "B3 Value", + } + + testRows := []int{-2, -1} + + testRowPairs := []struct { + row1 int + row2 int + }{ + {-1, -1}, + {-1, 0}, + {-1, 1}, + {0, -1}, + {0, 0}, + {0, 1}, + {1, -1}, + {1, 1}, + {1, 0}, + } + + for i, row := range testRows { + name := fmt.Sprintf("TestRow_%d", i+1) + t.Run(name, func(t *testing.T) { + xlsx := NewFile() + for col, val := range cells { + xlsx.SetCellStr(sheet, col, val) + } + xlsx.DuplicateRow(sheet, row) + + for col, val := range cells { + if !assert.Equal(t, val, xlsx.GetCellValue(sheet, col)) { + t.FailNow() + } + } + assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, name))) + }) + } + + for i, pair := range testRowPairs { + name := fmt.Sprintf("TestRowPair_%d", i+1) + t.Run(name, func(t *testing.T) { + xlsx := NewFile() + for col, val := range cells { + xlsx.SetCellStr(sheet, col, val) + } + xlsx.DuplicateRowTo(sheet, pair.row1, pair.row2) + + for col, val := range cells { + if !assert.Equal(t, val, xlsx.GetCellValue(sheet, col)) { + t.FailNow() + } + } + assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, name))) + }) + } +} + +func trimSliceSpace(s []string) []string { + for { + if len(s) > 0 && s[len(s)-1] == "" { + s = s[:len(s)-1] + } else { + break + } + } + return s +} From 164a3e126aafd1b5652d9da60ae2ba2240d412eb Mon Sep 17 00:00:00 2001 From: Kimxu Date: Thu, 7 Mar 2019 15:13:32 +0800 Subject: [PATCH 062/957] update README and functions docs (#351) * update README and functions docs * update README and functions docs --- README.md | 4 ++-- README_zh.md | 4 ++++ rows.go | 14 +++++++------- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index c4936b6c16..faa4bf63bf 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,8 @@ Excelize is a library written in pure Go and providing a set of functions that a **WARNING!** -From version 1.5 all row manipulation methods uses Excel row numbering starting with `1` instead of zero-based numbering -which take place in some methods in eraler versions. +From version 1.5.0 all row manipulation methods uses Excel row numbering starting with `1` instead of zero-based numbering +which take place in some methods in eraler versions. ## Basic Usage diff --git a/README_zh.md b/README_zh.md index 6a97c6faad..806a2f5881 100644 --- a/README_zh.md +++ b/README_zh.md @@ -15,6 +15,10 @@ Excelize 是 Go 语言编写的用于操作 Office Excel 文档类库,基于 ECMA-376 Office OpenXML 标准。可以使用它来读取、写入由 Microsoft Excel™ 2007 及以上版本创建的 XLSX 文档。相比较其他的开源类库,Excelize 支持写入原本带有图片(表)、透视表和切片器等复杂样式的文档,还支持向 Excel 文档中插入图片与图表,并且在保存后不会丢失文档原有样式,可以应用于各类报表系统中。使用本类库要求使用的 Go 语言为 1.8 或更高版本,完整的 API 使用文档请访问 [godoc.org](https://godoc.org/github.com/360EntSecGroup-Skylar/excelize) 或查看 [参考文档](https://xuri.me/excelize/)。 +**重要提示** + +从版本 1.5.0 开始,所有行操作方法都使用从 `1` 开始的 Excel 行编号,早期版本中某些方法中的基于 `0` 的行编号将不再使用。 + ## 快速上手 ### 安装 diff --git a/rows.go b/rows.go index 79d8e46b42..307936caf6 100644 --- a/rows.go +++ b/rows.go @@ -285,7 +285,7 @@ func (xlsx *xlsxC) getValueFrom(f *File, d *xlsxSST) (string, error) { } } -// SetRowVisible2 provides a function to set visible of a single row by given +// 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: // // xlsx.SetRowVisible("Sheet1", 2, false) @@ -305,10 +305,9 @@ func (f *File) SetRowVisible(sheet string, row int, visible bool) { xlsx.SheetData.Row[rowIdx].Hidden = true } -// GetRowVisible2 provides a function to get visible of a single row by given -// worksheet name and Excel row number. -// For example, get visible state of row 2 in -// Sheet1: +// GetRowVisible provides a function to get visible of a single row by given +// worksheet name and Excel row number. For example, get visible state of row +// 2 in Sheet1: // // xlsx.GetRowVisible("Sheet1", 2) // @@ -376,8 +375,9 @@ func (f *File) RemoveRow(sheet string, row int) { } } -// InsertRow2 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: +// 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: // // xlsx.InsertRow("Sheet1", 3) // From b974df402a70364ec9b39360b5e1f6722d36a857 Mon Sep 17 00:00:00 2001 From: caozhiyi <272653256@qq.com> Date: Thu, 7 Mar 2019 16:03:31 +0800 Subject: [PATCH 063/957] update go test and function docs --- excelize_test.go | 4 ++++ rows.go | 2 +- rows_test.go | 3 +++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/excelize_test.go b/excelize_test.go index abee199f5a..47b9561c07 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -846,7 +846,9 @@ func TestRowVisibility(t *testing.T) { xlsx.SetRowVisible("Sheet3", 2, false) xlsx.SetRowVisible("Sheet3", 2, true) + xlsx.SetRowVisible("Sheet3", 0, true) xlsx.GetRowVisible("Sheet3", 2) + xlsx.GetRowVisible("Sheet3", 0) assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestRowVisibility.xlsx"))) } @@ -1244,7 +1246,9 @@ func TestOutlineLevel(t *testing.T) { xlsx.SetColWidth("Sheet2", "A", "D", 13) xlsx.SetColOutlineLevel("Sheet2", "B", 2) xlsx.SetRowOutlineLevel("Sheet1", 2, 1) + xlsx.SetRowOutlineLevel("Sheet1", 0, 1) xlsx.GetRowOutlineLevel("Sheet1", 2) + xlsx.GetRowOutlineLevel("Sheet1", 0) err := xlsx.SaveAs(filepath.Join("test", "TestOutlineLevel.xlsx")) if !assert.NoError(t, err) { t.FailNow() diff --git a/rows.go b/rows.go index 307936caf6..9575876baf 100644 --- a/rows.go +++ b/rows.go @@ -352,7 +352,7 @@ func (f *File) GetRowOutlineLevel(sheet string, row int) uint8 { return xlsx.SheetData.Row[row-1].OutlineLevel } -// RemoveRow2 provides a function to remove single row by given worksheet name +// RemoveRow provides a function to remove single row by given worksheet name // and Excel row number. For example, remove row 3 in Sheet1: // // xlsx.RemoveRow("Sheet1", 3) diff --git a/rows_test.go b/rows_test.go index 26fcb47375..b83d37761d 100644 --- a/rows_test.go +++ b/rows_test.go @@ -42,6 +42,9 @@ func TestRows(t *testing.T) { t.FailNow() } } + + r := Rows{} + r.Columns() } func TestRowsError(t *testing.T) { From dc01264562e6e88d77a28042408029770ea32df4 Mon Sep 17 00:00:00 2001 From: Veniamin Albaev Date: Tue, 19 Mar 2019 19:14:41 +0300 Subject: [PATCH 064/957] Huge refactorig for consistent col/row numbering (#356) * Huge refactorig for consistent col/row numbering Started from simply changing ToALphaString()/TitleToNumber() logic and related fixes. But have to go deeper, do fixes, after do related fixes and again and again. Major improvements: 1. Tests made stronger again (But still be weak). 2. "Empty" returns for incorrect input replaces with panic. 3. Check for correct col/row/cell naming & addressing by default. 4. Removed huge amount of duplicated code. 5. Removed ToALphaString(), TitleToNumber() and it helpers functions at all, and replaced with SplitCellName(), JoinCellName(), ColumnNameToNumber(), ColumnNumberToName(), CellNameToCoordinates(), CoordinatesToCellName(). 6. Minor fixes for internal variable naming for code readability (ex. col, row for input params, colIdx, rowIdx for slice indexes etc). * Formatting fixes --- .gitignore | 1 + adjust.go | 229 +++++++++++++++ cell.go | 706 ++++++++++++++++++++--------------------------- cell_test.go | 9 +- chart.go | 12 +- chart_test.go | 48 +++- col.go | 166 ++++++----- comment.go | 5 +- date.go | 60 ++-- date_test.go | 43 ++- errors.go | 17 ++ errors_test.go | 21 ++ excelize.go | 229 +-------------- excelize_test.go | 347 ++++++++++++----------- lib.go | 234 ++++++++++------ lib_test.go | 228 ++++++++++++--- picture.go | 35 +-- rows.go | 197 ++++++------- rows_test.go | 174 ++++++------ shape.go | 36 ++- sheet.go | 48 +++- styles.go | 50 ++-- table.go | 86 +++--- 23 files changed, 1647 insertions(+), 1334 deletions(-) create mode 100644 adjust.go create mode 100644 errors.go create mode 100644 errors_test.go diff --git a/.gitignore b/.gitignore index ef972f604b..29e0f2b7b9 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ +~$*.xlsx test/Test*.xlsx diff --git a/adjust.go b/adjust.go new file mode 100644 index 0000000000..ee2065f4d8 --- /dev/null +++ b/adjust.go @@ -0,0 +1,229 @@ +package excelize + +import ( + "strings" +) + +type adjustDirection bool + +const ( + columns adjustDirection = false + rows adjustDirection = true +) + +// adjustHelper provides a function to adjust rows and columns dimensions, +// hyperlinks, merged cells and auto filter when inserting or deleting rows or +// columns. +// +// sheet: Worksheet name that we're editing +// column: Index number of the column we're inserting/deleting before +// row: Index number of the row we're inserting/deleting before +// offset: Number of rows/column to insert/delete negative values indicate deletion +// +// TODO: adjustCalcChain, adjustPageBreaks, adjustComments, +// adjustDataValidations, adjustProtectedCells +// +func (f *File) adjustHelper(sheet string, dir adjustDirection, num, offset int) { + xlsx := f.workSheetReader(sheet) + + if dir == rows { + f.adjustRowDimensions(xlsx, num, offset) + } else { + f.adjustColDimensions(xlsx, num, offset) + } + f.adjustHyperlinks(xlsx, sheet, dir, num, offset) + f.adjustMergeCells(xlsx, dir, num, offset) + f.adjustAutoFilter(xlsx, dir, num, offset) + + checkSheet(xlsx) + checkRow(xlsx) +} + +// adjustColDimensions provides a function to update column dimensions when +// inserting or deleting rows or columns. +func (f *File) adjustColDimensions(xlsx *xlsxWorksheet, col, offset int) { + for rowIdx := range xlsx.SheetData.Row { + for colIdx, v := range xlsx.SheetData.Row[rowIdx].C { + cellCol, cellRow, _ := CellNameToCoordinates(v.R) + if col <= cellCol { + if newCol := cellCol + offset; newCol > 0 { + xlsx.SheetData.Row[rowIdx].C[colIdx].R, _ = CoordinatesToCellName(newCol, cellRow) + } + } + } + } +} + +// adjustRowDimensions provides a function to update row dimensions when +// inserting or deleting rows or columns. +func (f *File) adjustRowDimensions(xlsx *xlsxWorksheet, row, offset int) { + for i, r := range xlsx.SheetData.Row { + if newRow := r.R + offset; r.R >= row && newRow > 0 { + f.ajustSingleRowDimensions(&xlsx.SheetData.Row[i], newRow) + } + } +} + +// ajustSingleRowDimensions provides a function to ajust single row dimensions. +func (f *File) ajustSingleRowDimensions(r *xlsxRow, num int) { + r.R = num + for i, col := range r.C { + colName, _, _ := SplitCellName(col.R) + r.C[i].R, _ = JoinCellName(colName, num) + } +} + +// adjustHyperlinks provides a function to update hyperlinks when inserting or +// deleting rows or columns. +func (f *File) adjustHyperlinks(xlsx *xlsxWorksheet, sheet string, dir adjustDirection, num, offset int) { + // short path + if xlsx.Hyperlinks == nil || len(xlsx.Hyperlinks.Hyperlink) == 0 { + return + } + + // order is important + if offset < 0 { + for rowIdx, linkData := range xlsx.Hyperlinks.Hyperlink { + colNum, rowNum, _ := CellNameToCoordinates(linkData.Ref) + + if (dir == rows && num == rowNum) || (dir == columns && num == colNum) { + f.deleteSheetRelationships(sheet, linkData.RID) + if len(xlsx.Hyperlinks.Hyperlink) > 1 { + xlsx.Hyperlinks.Hyperlink = append(xlsx.Hyperlinks.Hyperlink[:rowIdx], + xlsx.Hyperlinks.Hyperlink[rowIdx+1:]...) + } else { + xlsx.Hyperlinks = nil + } + } + } + } + + if xlsx.Hyperlinks == nil { + return + } + + for i := range xlsx.Hyperlinks.Hyperlink { + link := &xlsx.Hyperlinks.Hyperlink[i] // get reference + colNum, rowNum, _ := CellNameToCoordinates(link.Ref) + + if dir == rows { + if rowNum >= num { + link.Ref, _ = CoordinatesToCellName(colNum, rowNum+offset) + } + } else { + if colNum >= num { + link.Ref, _ = CoordinatesToCellName(colNum+offset, rowNum) + } + } + } +} + +// adjustAutoFilter provides a function to update the auto filter when +// inserting or deleting rows or columns. +func (f *File) adjustAutoFilter(xlsx *xlsxWorksheet, dir adjustDirection, num, offset int) { + if xlsx.AutoFilter == nil { + return + } + + rng := strings.Split(xlsx.AutoFilter.Ref, ":") + firstCell := rng[0] + lastCell := rng[1] + + firstCol, firstRow, err := CellNameToCoordinates(firstCell) + if err != nil { + panic(err) + } + + lastCol, lastRow, err := CellNameToCoordinates(lastCell) + if err != nil { + panic(err) + } + + if (dir == rows && firstRow == num && offset < 0) || (dir == columns && firstCol == num && lastCol == num) { + xlsx.AutoFilter = nil + for rowIdx := range xlsx.SheetData.Row { + rowData := &xlsx.SheetData.Row[rowIdx] + if rowData.R > firstRow && rowData.R <= lastRow { + rowData.Hidden = false + } + } + return + } + + if dir == rows { + if firstRow >= num { + firstCell, _ = CoordinatesToCellName(firstCol, firstRow+offset) + } + if lastRow >= num { + lastCell, _ = CoordinatesToCellName(lastCol, lastRow+offset) + } + } else { + if lastCol >= num { + lastCell, _ = CoordinatesToCellName(lastCol+offset, lastRow) + } + } + + xlsx.AutoFilter.Ref = firstCell + ":" + lastCell +} + +// adjustMergeCells provides a function to update merged cells when inserting +// or deleting rows or columns. +func (f *File) adjustMergeCells(xlsx *xlsxWorksheet, dir adjustDirection, num, offset int) { + if xlsx.MergeCells == nil { + return + } + + for i, areaData := range xlsx.MergeCells.Cells { + rng := strings.Split(areaData.Ref, ":") + firstCell := rng[0] + lastCell := rng[1] + + firstCol, firstRow, err := CellNameToCoordinates(firstCell) + if err != nil { + panic(err) + } + + lastCol, lastRow, err := CellNameToCoordinates(lastCell) + if err != nil { + panic(err) + } + + adjust := func(v int) int { + if v >= num { + v += offset + if v < 1 { + return 1 + } + return v + } + return v + } + + if dir == rows { + firstRow = adjust(firstRow) + lastRow = adjust(lastRow) + } else { + firstCol = adjust(firstCol) + lastCol = adjust(lastCol) + } + + if firstCol == lastCol && firstRow == lastRow { + if len(xlsx.MergeCells.Cells) > 1 { + xlsx.MergeCells.Cells = append(xlsx.MergeCells.Cells[:i], xlsx.MergeCells.Cells[i+1:]...) + xlsx.MergeCells.Count = len(xlsx.MergeCells.Cells) + } else { + xlsx.MergeCells = nil + } + } + + if firstCell, err = CoordinatesToCellName(firstCol, firstRow); err != nil { + panic(err) + } + + if lastCell, err = CoordinatesToCellName(lastCol, lastRow); err != nil { + panic(err) + } + + areaData.Ref = firstCell + ":" + lastCell + } +} diff --git a/cell.go b/cell.go index 3cf880a98a..126b1ff59c 100644 --- a/cell.go +++ b/cell.go @@ -11,6 +11,7 @@ package excelize import ( "encoding/xml" + "errors" "fmt" "reflect" "strconv" @@ -29,18 +30,18 @@ const ( STCellFormulaTypeShared = "shared" ) -// mergeCellsParser provides a function to check merged cells in worksheet by -// given axis. -func (f *File) mergeCellsParser(xlsx *xlsxWorksheet, axis string) string { - axis = strings.ToUpper(axis) - if xlsx.MergeCells != nil { - for i := 0; i < len(xlsx.MergeCells.Cells); i++ { - if checkCellInArea(axis, xlsx.MergeCells.Cells[i].Ref) { - axis = strings.Split(xlsx.MergeCells.Cells[i].Ref, ":")[0] - } +// GetCellValue provides a function to get formatted value from cell by given +// worksheet name and axis in XLSX 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. +func (f *File) GetCellValue(sheet, axis string) string { + return f.getCellStringFunc(sheet, axis, func(x *xlsxWorksheet, c *xlsxC) (string, bool) { + val, err := c.getValueFrom(f, f.sharedStringsReader()) + if err != nil { + panic(err) // Fail fast to avoid future side effects! } - } - return axis + return val, true + }) } // SetCellValue provides a function to set value of a cell. The following @@ -68,256 +69,169 @@ func (f *File) mergeCellsParser(xlsx *xlsxWorksheet, axis string) string { // Note that default date format is m/d/yy h:mm of time.Time type value. You can // set numbers format by SetCellStyle() method. func (f *File) SetCellValue(sheet, axis string, value interface{}) { - switch t := value.(type) { - case float32: - f.SetCellDefault(sheet, axis, strconv.FormatFloat(float64(value.(float32)), 'f', -1, 32)) - case float64: - f.SetCellDefault(sheet, axis, strconv.FormatFloat(float64(value.(float64)), 'f', -1, 64)) - case string: - f.SetCellStr(sheet, axis, t) - case []byte: - f.SetCellStr(sheet, axis, string(t)) - case time.Duration: - f.SetCellDefault(sheet, axis, strconv.FormatFloat(float64(value.(time.Duration).Seconds()/86400), 'f', -1, 32)) - f.setDefaultTimeStyle(sheet, axis, 21) - case time.Time: - f.SetCellDefault(sheet, axis, strconv.FormatFloat(float64(timeToExcelTime(timeToUTCTime(value.(time.Time)))), 'f', -1, 64)) - f.setDefaultTimeStyle(sheet, axis, 22) - case nil: - f.SetCellStr(sheet, axis, "") - case bool: - f.SetCellBool(sheet, axis, bool(value.(bool))) - default: - f.setCellIntValue(sheet, axis, value) - } -} - -// setCellIntValue provides a function to set int value of a cell. -func (f *File) setCellIntValue(sheet, axis string, value interface{}) { - switch value.(type) { + switch v := value.(type) { case int: - f.SetCellInt(sheet, axis, value.(int)) + f.SetCellInt(sheet, axis, v) case int8: - f.SetCellInt(sheet, axis, int(value.(int8))) + f.SetCellInt(sheet, axis, int(v)) case int16: - f.SetCellInt(sheet, axis, int(value.(int16))) + f.SetCellInt(sheet, axis, int(v)) case int32: - f.SetCellInt(sheet, axis, int(value.(int32))) + f.SetCellInt(sheet, axis, int(v)) case int64: - f.SetCellInt(sheet, axis, int(value.(int64))) + f.SetCellInt(sheet, axis, int(v)) case uint: - f.SetCellInt(sheet, axis, int(value.(uint))) + f.SetCellInt(sheet, axis, int(v)) case uint8: - f.SetCellInt(sheet, axis, int(value.(uint8))) + f.SetCellInt(sheet, axis, int(v)) case uint16: - f.SetCellInt(sheet, axis, int(value.(uint16))) + f.SetCellInt(sheet, axis, int(v)) case uint32: - f.SetCellInt(sheet, axis, int(value.(uint32))) + f.SetCellInt(sheet, axis, int(v)) case uint64: - f.SetCellInt(sheet, axis, int(value.(uint64))) + f.SetCellInt(sheet, axis, int(v)) + case float32: + f.SetCellDefault(sheet, axis, strconv.FormatFloat(float64(v), 'f', -1, 32)) + case float64: + f.SetCellDefault(sheet, axis, strconv.FormatFloat(v, 'f', -1, 64)) + case string: + f.SetCellStr(sheet, axis, v) + case []byte: + f.SetCellStr(sheet, axis, string(v)) + case time.Duration: + f.SetCellDefault(sheet, axis, strconv.FormatFloat(v.Seconds()/86400.0, 'f', -1, 32)) + f.setDefaultTimeStyle(sheet, axis, 21) + case time.Time: + vv := timeToExcelTime(v) + if vv > 0 { + f.SetCellDefault(sheet, axis, strconv.FormatFloat(timeToExcelTime(v), 'f', -1, 64)) + f.setDefaultTimeStyle(sheet, axis, 22) + } else { + f.SetCellStr(sheet, axis, v.Format(time.RFC3339Nano)) + } + case bool: + f.SetCellBool(sheet, axis, v) + case nil: + f.SetCellStr(sheet, axis, "") default: f.SetCellStr(sheet, axis, fmt.Sprintf("%v", value)) } } -// SetCellBool provides a function to set bool type value of a cell by given +// SetCellInt provides a function to set int type value of a cell by given // worksheet name, cell coordinates and cell value. -func (f *File) SetCellBool(sheet, axis string, value bool) { +func (f *File) SetCellInt(sheet, axis string, value int) { xlsx := f.workSheetReader(sheet) - axis = f.mergeCellsParser(xlsx, axis) - col := string(strings.Map(letterOnlyMapF, axis)) - row, err := strconv.Atoi(strings.Map(intOnlyMapF, axis)) - if err != nil { - return - } - xAxis := row - 1 - yAxis := TitleToNumber(col) - - rows := xAxis + 1 - cell := yAxis + 1 - - completeRow(xlsx, rows, cell) - completeCol(xlsx, rows, cell) + cellData, col, _ := f.prepareCell(xlsx, sheet, axis) + cellData.S = f.prepareCellStyle(xlsx, col, cellData.S) + cellData.T = "" + cellData.V = strconv.Itoa(value) +} - xlsx.SheetData.Row[xAxis].C[yAxis].S = f.prepareCellStyle(xlsx, cell, xlsx.SheetData.Row[xAxis].C[yAxis].S) - xlsx.SheetData.Row[xAxis].C[yAxis].T = "b" +// 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) { + xlsx := f.workSheetReader(sheet) + cellData, col, _ := f.prepareCell(xlsx, sheet, axis) + cellData.S = f.prepareCellStyle(xlsx, col, cellData.S) + cellData.T = "b" if value { - xlsx.SheetData.Row[xAxis].C[yAxis].V = "1" + cellData.V = "1" } else { - xlsx.SheetData.Row[xAxis].C[yAxis].V = "0" + cellData.V = "0" } } -// GetCellValue provides a function to get formatted value from cell by given -// worksheet name and axis in XLSX 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. -func (f *File) GetCellValue(sheet, axis string) 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) { xlsx := f.workSheetReader(sheet) - axis = f.mergeCellsParser(xlsx, axis) - row, err := strconv.Atoi(strings.Map(intOnlyMapF, axis)) - if err != nil { - return "" - } - xAxis := row - 1 - rows := len(xlsx.SheetData.Row) - if rows > 1 { - lastRow := xlsx.SheetData.Row[rows-1].R - if lastRow >= rows { - rows = lastRow - } - } - if rows < xAxis { - return "" - } - for k := range xlsx.SheetData.Row { - if xlsx.SheetData.Row[k].R == row { - for i := range xlsx.SheetData.Row[k].C { - if axis == xlsx.SheetData.Row[k].C[i].R { - val, _ := xlsx.SheetData.Row[k].C[i].getValueFrom(f, f.sharedStringsReader()) - return val - } - } + cellData, col, _ := f.prepareCell(xlsx, sheet, axis) + + // Leading space(s) character detection. + if len(value) > 0 && value[0] == 32 { + cellData.XMLSpace = xml.Attr{ + Name: xml.Name{Space: NameSpaceXML, Local: "space"}, + Value: "preserve", } } - return "" -} -// 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) string { - if s == 0 { - return v - } - styleSheet := f.stylesReader() - ok := builtInNumFmtFunc[styleSheet.CellXfs.Xf[s].NumFmtID] - if ok != nil { - return ok(styleSheet.CellXfs.Xf[s].NumFmtID, v) - } - return v + cellData.S = f.prepareCellStyle(xlsx, col, cellData.S) + cellData.T = "str" + cellData.V = value } -// GetCellStyle provides a function to get cell style index by given worksheet -// name and cell coordinates. -func (f *File) GetCellStyle(sheet, axis string) int { +// 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) { xlsx := f.workSheetReader(sheet) - axis = f.mergeCellsParser(xlsx, axis) - col := string(strings.Map(letterOnlyMapF, axis)) - row, err := strconv.Atoi(strings.Map(intOnlyMapF, axis)) - if err != nil { - return 0 - } - xAxis := row - 1 - yAxis := TitleToNumber(col) - - rows := xAxis + 1 - cell := yAxis + 1 - - completeRow(xlsx, rows, cell) - completeCol(xlsx, rows, cell) - - return f.prepareCellStyle(xlsx, cell, xlsx.SheetData.Row[xAxis].C[yAxis].S) + cellData, col, _ := f.prepareCell(xlsx, sheet, axis) + cellData.S = f.prepareCellStyle(xlsx, col, cellData.S) + cellData.T = "" + cellData.V = value } // 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 { - xlsx := f.workSheetReader(sheet) - axis = f.mergeCellsParser(xlsx, axis) - row, err := strconv.Atoi(strings.Map(intOnlyMapF, axis)) - if err != nil { - return "" - } - xAxis := row - 1 - rows := len(xlsx.SheetData.Row) - if rows > 1 { - lastRow := xlsx.SheetData.Row[rows-1].R - if lastRow >= rows { - rows = lastRow - } - } - if rows < xAxis { - return "" - } - for k := range xlsx.SheetData.Row { - if xlsx.SheetData.Row[k].R == row { - for i := range xlsx.SheetData.Row[k].C { - if axis == xlsx.SheetData.Row[k].C[i].R { - if xlsx.SheetData.Row[k].C[i].F == nil { - continue - } - if xlsx.SheetData.Row[k].C[i].F.T == STCellFormulaTypeShared { - return getSharedForumula(xlsx, xlsx.SheetData.Row[k].C[i].F.Si) - } - return xlsx.SheetData.Row[k].C[i].F.Content - } - } + return f.getCellStringFunc(sheet, axis, func(x *xlsxWorksheet, c *xlsxC) (string, bool) { + if c.F == nil { + return "", false } - } - return "" -} - -// getSharedForumula find a cell contains the same formula as another cell, -// the "shared" value can be used for the t attribute and the si attribute can -// be used to refer to the cell containing the formula. Two formulas are -// 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 -// allow area, and always return origin shared formula. -func getSharedForumula(xlsx *xlsxWorksheet, si string) string { - for k := range xlsx.SheetData.Row { - for i := range xlsx.SheetData.Row[k].C { - if xlsx.SheetData.Row[k].C[i].F == nil { - continue - } - if xlsx.SheetData.Row[k].C[i].F.T != STCellFormulaTypeShared { - continue - } - if xlsx.SheetData.Row[k].C[i].F.Si != si { - continue - } - if xlsx.SheetData.Row[k].C[i].F.Ref != "" { - return xlsx.SheetData.Row[k].C[i].F.Content - } + if c.F.T == STCellFormulaTypeShared { + return getSharedForumula(x, c.F.Si), true } - } - return "" + return c.F.Content, true + }) } // SetCellFormula provides a function to set cell formula by given string and // worksheet name. func (f *File) SetCellFormula(sheet, axis, formula string) { xlsx := f.workSheetReader(sheet) - axis = f.mergeCellsParser(xlsx, axis) - col := string(strings.Map(letterOnlyMapF, axis)) - row, err := strconv.Atoi(strings.Map(intOnlyMapF, axis)) - if err != nil { - return - } - xAxis := row - 1 - yAxis := TitleToNumber(col) - - rows := xAxis + 1 - cell := yAxis + 1 - - completeRow(xlsx, rows, cell) - completeCol(xlsx, rows, cell) + cellData, _, _ := f.prepareCell(xlsx, sheet, axis) if formula == "" { - xlsx.SheetData.Row[xAxis].C[yAxis].F = nil + cellData.F = nil f.deleteCalcChain(axis) return } - if xlsx.SheetData.Row[xAxis].C[yAxis].F != nil { - xlsx.SheetData.Row[xAxis].C[yAxis].F.Content = formula + + if cellData.F != nil { + cellData.F.Content = formula } else { - f := xlsxF{ - Content: formula, + cellData.F = &xlsxF{Content: formula} + } +} + +// GetCellHyperLink provides a function to get cell hyperlink by given +// worksheet name and axis. Boolean type value link will be ture 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: +// +// link, target := xlsx.GetCellHyperLink("Sheet1", "H6") +// +func (f *File) GetCellHyperLink(sheet, axis string) (bool, string) { + // Check for correct cell name + if _, _, err := SplitCellName(axis); err != nil { + panic(err) // Fail fast to avoid possible future side effects + } + + xlsx := f.workSheetReader(sheet) + axis = f.mergeCellsParser(xlsx, axis) + + if xlsx.Hyperlinks != nil { + for _, link := range xlsx.Hyperlinks.Hyperlink { + if link.Ref == axis { + if link.RID != "" { + return true, f.getSheetRelationshipsTargetByID(sheet, link.RID) + } + return true, link.Location + } } - xlsx.SheetData.Row[xAxis].C[yAxis].F = &f } + return false, "" } // SetCellHyperLink provides a function to set cell hyperlink by given @@ -335,53 +249,36 @@ func (f *File) SetCellFormula(sheet, axis, formula string) { // xlsx.SetCellHyperLink("Sheet1", "A3", "Sheet1!A40", "Location") // func (f *File) SetCellHyperLink(sheet, axis, link, linkType string) { - xlsx := f.workSheetReader(sheet) - axis = f.mergeCellsParser(xlsx, axis) - linkTypes := map[string]xlsxHyperlink{ - "External": {}, - "Location": {Location: link}, + // Check for correct cell name + if _, _, err := SplitCellName(axis); err != nil { + panic(err) // Fail fast to avoid possible future side effects } - hyperlink, ok := linkTypes[linkType] - if !ok || axis == "" { - return - } - hyperlink.Ref = axis - if linkType == "External" { - rID := f.addSheetRelationships(sheet, SourceRelationshipHyperLink, link, linkType) - hyperlink.RID = "rId" + strconv.Itoa(rID) - } - if xlsx.Hyperlinks == nil { - xlsx.Hyperlinks = &xlsxHyperlinks{} - } - xlsx.Hyperlinks.Hyperlink = append(xlsx.Hyperlinks.Hyperlink, hyperlink) -} -// GetCellHyperLink provides a function to get cell hyperlink by given -// worksheet name and axis. Boolean type value link will be ture 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: -// -// link, target := xlsx.GetCellHyperLink("Sheet1", "H6") -// -func (f *File) GetCellHyperLink(sheet, axis string) (bool, string) { - var link bool - var target string xlsx := f.workSheetReader(sheet) axis = f.mergeCellsParser(xlsx, axis) - if xlsx.Hyperlinks == nil || axis == "" { - return link, target - } - for h := range xlsx.Hyperlinks.Hyperlink { - if xlsx.Hyperlinks.Hyperlink[h].Ref == axis { - link = true - target = xlsx.Hyperlinks.Hyperlink[h].Location - if xlsx.Hyperlinks.Hyperlink[h].RID != "" { - target = f.getSheetRelationshipsTargetByID(sheet, xlsx.Hyperlinks.Hyperlink[h].RID) - } + + var linkData xlsxHyperlink + + switch linkType { + case "External": + linkData = xlsxHyperlink{ + Ref: axis, } + rID := f.addSheetRelationships(sheet, SourceRelationshipHyperLink, link, linkType) + linkData.RID = "rId" + strconv.Itoa(rID) + case "Location": + linkData = xlsxHyperlink{ + Ref: axis, + Location: link, + } + default: + panic(fmt.Errorf("invalid link type %q", linkType)) + } + + if xlsx.Hyperlinks == nil { + xlsx.Hyperlinks = new(xlsxHyperlinks) } - return link, target + xlsx.Hyperlinks.Hyperlink = append(xlsx.Hyperlinks.Hyperlink, linkData) } // MergeCell provides a function to merge cells by given coordinate area and @@ -392,213 +289,214 @@ func (f *File) GetCellHyperLink(sheet, axis string) (bool, string) { // If you create a merged cell that overlaps with another existing merged cell, // those merged cells that already exist will be removed. func (f *File) MergeCell(sheet, hcell, vcell string) { - if hcell == vcell { - return + hcol, hrow, err := CellNameToCoordinates(hcell) + if err != nil { + panic(err) } - hcell = strings.ToUpper(hcell) - vcell = strings.ToUpper(vcell) - - // Coordinate conversion, convert C1:B3 to 2,0,1,2. - hcol := string(strings.Map(letterOnlyMapF, hcell)) - hrow, _ := strconv.Atoi(strings.Map(intOnlyMapF, hcell)) - hyAxis := hrow - 1 - hxAxis := TitleToNumber(hcol) + vcol, vrow, err := CellNameToCoordinates(vcell) + if err != nil { + panic(err) + } - vcol := string(strings.Map(letterOnlyMapF, vcell)) - vrow, _ := strconv.Atoi(strings.Map(intOnlyMapF, vcell)) - vyAxis := vrow - 1 - vxAxis := TitleToNumber(vcol) + if hcol == vcol && hrow == vrow { + return + } - if vxAxis < hxAxis { - hcell, vcell = vcell, hcell - vxAxis, hxAxis = hxAxis, vxAxis + if vcol < hcol { + hcol, vcol = vcol, hcol } - if vyAxis < hyAxis { - hcell, vcell = vcell, hcell - vyAxis, hyAxis = hyAxis, vyAxis + if vrow < hrow { + hrow, vrow = vrow, hrow } + hcell, _ = CoordinatesToCellName(hcol, hrow) + vcell, _ = CoordinatesToCellName(vcol, vrow) + xlsx := f.workSheetReader(sheet) if xlsx.MergeCells != nil { - mergeCell := xlsxMergeCell{} - // Correct the coordinate area, such correct C1:B3 to B1:C3. - mergeCell.Ref = ToAlphaString(hxAxis) + strconv.Itoa(hyAxis+1) + ":" + ToAlphaString(vxAxis) + strconv.Itoa(vyAxis+1) + ref := hcell + ":" + vcell + cells := make([]*xlsxMergeCell, 0, len(xlsx.MergeCells.Cells)) + // Delete the merged cells of the overlapping area. - for i := 0; i < len(xlsx.MergeCells.Cells); i++ { - if checkCellInArea(hcell, xlsx.MergeCells.Cells[i].Ref) || checkCellInArea(strings.Split(xlsx.MergeCells.Cells[i].Ref, ":")[0], mergeCell.Ref) { - xlsx.MergeCells.Cells = append(xlsx.MergeCells.Cells[:i], xlsx.MergeCells.Cells[i+1:]...) - } else if checkCellInArea(vcell, xlsx.MergeCells.Cells[i].Ref) || checkCellInArea(strings.Split(xlsx.MergeCells.Cells[i].Ref, ":")[1], mergeCell.Ref) { - xlsx.MergeCells.Cells = append(xlsx.MergeCells.Cells[:i], xlsx.MergeCells.Cells[i+1:]...) + for _, cellData := range xlsx.MergeCells.Cells { + cc := strings.Split(cellData.Ref, ":") + if len(cc) != 2 { + panic(fmt.Errorf("invalid area %q", cellData.Ref)) + } + + if !checkCellInArea(hcell, cellData.Ref) && !checkCellInArea(vcell, cellData.Ref) && + !checkCellInArea(cc[0], ref) && !checkCellInArea(cc[1], ref) { + cells = append(cells, cellData) } } - xlsx.MergeCells.Cells = append(xlsx.MergeCells.Cells, &mergeCell) + cells = append(xlsx.MergeCells.Cells, &xlsxMergeCell{Ref: ref}) + xlsx.MergeCells.Cells = cells } else { - mergeCell := xlsxMergeCell{} - // Correct the coordinate area, such correct C1:B3 to B1:C3. - mergeCell.Ref = ToAlphaString(hxAxis) + strconv.Itoa(hyAxis+1) + ":" + ToAlphaString(vxAxis) + strconv.Itoa(vyAxis+1) - mergeCells := xlsxMergeCells{} - mergeCells.Cells = append(mergeCells.Cells, &mergeCell) - xlsx.MergeCells = &mergeCells + xlsx.MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: hcell + ":" + vcell}}} } } -// 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) { - xlsx := f.workSheetReader(sheet) - axis = f.mergeCellsParser(xlsx, axis) - col := string(strings.Map(letterOnlyMapF, axis)) - row, err := strconv.Atoi(strings.Map(intOnlyMapF, axis)) +// 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: +// +// xlsx.SetSheetRow("Sheet1", "B6", &[]interface{}{"1", nil, 2}) +// +func (f *File) SetSheetRow(sheet, axis string, slice interface{}) { + col, row, err := CellNameToCoordinates(axis) if err != nil { - return + panic(err) // Fail fast to avoid future side effects! } - xAxis := row - 1 - yAxis := TitleToNumber(col) - - rows := xAxis + 1 - cell := yAxis + 1 - - completeRow(xlsx, rows, cell) - completeCol(xlsx, rows, cell) - xlsx.SheetData.Row[xAxis].C[yAxis].S = f.prepareCellStyle(xlsx, cell, xlsx.SheetData.Row[xAxis].C[yAxis].S) - xlsx.SheetData.Row[xAxis].C[yAxis].T = "" - xlsx.SheetData.Row[xAxis].C[yAxis].V = strconv.Itoa(value) -} + // Make sure 'slice' is a Ptr to Slice + v := reflect.ValueOf(slice) + if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Slice { + panic(errors.New("pointer to slice expected")) // Fail fast to avoid future side effects! + } + v = v.Elem() -// prepareCellStyle provides a function to prepare style index of cell in -// worksheet by given column index and style index. -func (f *File) prepareCellStyle(xlsx *xlsxWorksheet, col, style int) int { - if xlsx.Cols != nil && style == 0 { - for _, v := range xlsx.Cols.Col { - if v.Min <= col && col <= v.Max { - style = v.Style - } + for i := 0; i < v.Len(); i++ { + cell, err := CoordinatesToCellName(col+i, row) + // Error should never happens here. But keep ckecking to early detect regresions + // if it will be introduced in furure + if err != nil { + panic(err) // Fail fast to avoid future side effects! } + f.SetCellValue(sheet, cell, v.Index(i).Interface()) } - return style } -// 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) { - xlsx := f.workSheetReader(sheet) - axis = f.mergeCellsParser(xlsx, axis) - if len(value) > 32767 { - value = value[0:32767] - } - col := string(strings.Map(letterOnlyMapF, axis)) - row, err := strconv.Atoi(strings.Map(intOnlyMapF, axis)) +// getCellInfo does common preparation for all SetCell* methods. +func (f *File) prepareCell(xlsx *xlsxWorksheet, sheet, cell string) (*xlsxC, int, int) { + cell = f.mergeCellsParser(xlsx, cell) + + col, row, err := CellNameToCoordinates(cell) if err != nil { - return + panic(err) // Fail fast and prevent future side effects } - xAxis := row - 1 - yAxis := TitleToNumber(col) - - rows := xAxis + 1 - cell := yAxis + 1 - completeRow(xlsx, rows, cell) - completeCol(xlsx, rows, cell) + prepareSheetXML(xlsx, col, row) - // Leading space(s) character detection. - if len(value) > 0 { - if value[0] == 32 { - xlsx.SheetData.Row[xAxis].C[yAxis].XMLSpace = xml.Attr{ - Name: xml.Name{Space: NameSpaceXML, Local: "space"}, - Value: "preserve", - } - } - } - xlsx.SheetData.Row[xAxis].C[yAxis].S = f.prepareCellStyle(xlsx, cell, xlsx.SheetData.Row[xAxis].C[yAxis].S) - xlsx.SheetData.Row[xAxis].C[yAxis].T = "str" - xlsx.SheetData.Row[xAxis].C[yAxis].V = value + return &xlsx.SheetData.Row[row-1].C[col-1], col, row } -// 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) { +// 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)) string { xlsx := f.workSheetReader(sheet) axis = f.mergeCellsParser(xlsx, axis) - col := string(strings.Map(letterOnlyMapF, axis)) - row, err := strconv.Atoi(strings.Map(intOnlyMapF, axis)) + + _, row, err := CellNameToCoordinates(axis) if err != nil { - return + panic(err) // Fail fast to avoid future side effects! } - xAxis := row - 1 - yAxis := TitleToNumber(col) - rows := xAxis + 1 - cell := yAxis + 1 + lastRowNum := 0 + if l := len(xlsx.SheetData.Row); l > 0 { + lastRowNum = xlsx.SheetData.Row[l-1].R + } - completeRow(xlsx, rows, cell) - completeCol(xlsx, rows, cell) + // keep in mind: row starts from 1 + if row > lastRowNum { + return "" + } - xlsx.SheetData.Row[xAxis].C[yAxis].S = f.prepareCellStyle(xlsx, cell, xlsx.SheetData.Row[xAxis].C[yAxis].S) - xlsx.SheetData.Row[xAxis].C[yAxis].T = "" - xlsx.SheetData.Row[xAxis].C[yAxis].V = value + for rowIdx := range xlsx.SheetData.Row { + rowData := &xlsx.SheetData.Row[rowIdx] + if rowData.R != row { + continue + } + for colIdx := range rowData.C { + colData := &rowData.C[colIdx] + if axis != colData.R { + continue + } + if val, ok := fn(xlsx, colData); ok { + return val + } + } + } + return "" } -// 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: -// -// xlsx.SetSheetRow("Sheet1", "B6", &[]interface{}{"1", nil, 2}) -// -func (f *File) SetSheetRow(sheet, axis string, slice interface{}) { - xlsx := f.workSheetReader(sheet) - axis = f.mergeCellsParser(xlsx, axis) - col := string(strings.Map(letterOnlyMapF, axis)) - row, err := strconv.Atoi(strings.Map(intOnlyMapF, axis)) - if err != nil { - return - } - // Make sure 'slice' is a Ptr to Slice - v := reflect.ValueOf(slice) - if v.Kind() != reflect.Ptr { - return +// 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) string { + if s == 0 { + return v } - v = v.Elem() - if v.Kind() != reflect.Slice { - return + styleSheet := f.stylesReader() + ok := builtInNumFmtFunc[styleSheet.CellXfs.Xf[s].NumFmtID] + if ok != nil { + return ok(styleSheet.CellXfs.Xf[s].NumFmtID, v) } + return v +} - xAxis := row - 1 - yAxis := TitleToNumber(col) - - rows := xAxis + 1 - cell := yAxis + 1 - - completeRow(xlsx, rows, cell) - completeCol(xlsx, rows, cell) +// prepareCellStyle provides a function to prepare style index of cell in +// worksheet by given column index and style index. +func (f *File) prepareCellStyle(xlsx *xlsxWorksheet, col, style int) int { + if xlsx.Cols != nil && style == 0 { + for _, c := range xlsx.Cols.Col { + if c.Min <= col && col <= c.Max { + style = c.Style + } + } + } + return style +} - idx := 0 - for i := cell - 1; i < v.Len()+cell-1; i++ { - c := ToAlphaString(i) + strconv.Itoa(row) - f.SetCellValue(sheet, c, v.Index(idx).Interface()) - idx++ +// mergeCellsParser provides a function to check merged cells in worksheet by +// given axis. +func (f *File) mergeCellsParser(xlsx *xlsxWorksheet, axis string) string { + axis = strings.ToUpper(axis) + if xlsx.MergeCells != nil { + for i := 0; i < len(xlsx.MergeCells.Cells); i++ { + if checkCellInArea(axis, xlsx.MergeCells.Cells[i].Ref) { + axis = strings.Split(xlsx.MergeCells.Cells[i].Ref, ":")[0] + } + } } + return axis } // checkCellInArea provides a function to determine if a given coordinate is // within an area. func checkCellInArea(cell, area string) bool { - cell = strings.ToUpper(cell) - area = strings.ToUpper(area) + col, row, err := CellNameToCoordinates(cell) + if err != nil { + panic(err) + } - ref := strings.Split(area, ":") - if len(ref) < 2 { + rng := strings.Split(area, ":") + if len(rng) != 2 { return false } - from := ref[0] - to := ref[1] + firstCol, firtsRow, _ := CellNameToCoordinates(rng[0]) + lastCol, lastRow, _ := CellNameToCoordinates(rng[1]) - col, row := getCellColRow(cell) - fromCol, fromRow := getCellColRow(from) - toCol, toRow := getCellColRow(to) + return col >= firstCol && col <= lastCol && row >= firtsRow && row <= lastRow +} - return axisLowerOrEqualThan(fromCol, col) && axisLowerOrEqualThan(col, toCol) && axisLowerOrEqualThan(fromRow, row) && axisLowerOrEqualThan(row, toRow) +// getSharedForumula find a cell contains the same formula as another cell, +// the "shared" value can be used for the t attribute and the si attribute can +// be used to refer to the cell containing the formula. Two formulas are +// 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 +// allow area, and always return origin shared formula. +func getSharedForumula(xlsx *xlsxWorksheet, si string) string { + for _, r := range xlsx.SheetData.Row { + for _, c := range r.C { + if c.F != nil && c.F.Ref != "" && c.F.T == STCellFormulaTypeShared && c.F.Si == si { + return c.F.Content + } + } + } + return "" } diff --git a/cell_test.go b/cell_test.go index cb3d80e1db..d388c7f2f2 100644 --- a/cell_test.go +++ b/cell_test.go @@ -9,7 +9,6 @@ import ( func TestCheckCellInArea(t *testing.T) { expectedTrueCellInAreaList := [][2]string{ {"c2", "A1:AAZ32"}, - {"AA0", "Z0:AB1"}, {"B9", "A1:B9"}, {"C2", "C2:C2"}, } @@ -18,7 +17,7 @@ func TestCheckCellInArea(t *testing.T) { cell := expectedTrueCellInArea[0] area := expectedTrueCellInArea[1] - assert.True(t, checkCellInArea(cell, area), + assert.Truef(t, checkCellInArea(cell, area), "Expected cell %v to be in area %v, got false\n", cell, area) } @@ -32,7 +31,11 @@ func TestCheckCellInArea(t *testing.T) { cell := expectedFalseCellInArea[0] area := expectedFalseCellInArea[1] - assert.False(t, checkCellInArea(cell, area), + assert.Falsef(t, checkCellInArea(cell, area), "Expected cell %v not to be inside of area %v, but got true\n", cell, area) } + + assert.Panics(t, func() { + checkCellInArea("AA0", "Z0:AB1") + }) } diff --git a/chart.go b/chart.go index f11fd55af6..c31995e1e4 100644 --- a/chart.go +++ b/chart.go @@ -1240,14 +1240,14 @@ 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) { - cell = strings.ToUpper(cell) - fromCol := string(strings.Map(letterOnlyMapF, cell)) - fromRow, _ := strconv.Atoi(strings.Map(intOnlyMapF, cell)) - row := fromRow - 1 - col := TitleToNumber(fromCol) + col, row := MustCellNameToCoordinates(cell) + 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, col, row, formatSet.OffsetX, formatSet.OffsetY, width, height) + colStart, rowStart, _, _, colEnd, rowEnd, x2, y2 := + f.positionObjectPixels(sheet, colIdx, rowIdx, formatSet.OffsetX, formatSet.OffsetY, width, height) content, cNvPrID := f.drawingParser(drawingXML) twoCellAnchor := xdrCellAnchor{} twoCellAnchor.EditAs = formatSet.Positioning diff --git a/chart_test.go b/chart_test.go index f3d7bdf27b..1dfc46869e 100644 --- a/chart_test.go +++ b/chart_test.go @@ -9,19 +9,46 @@ import ( ) func TestChartSize(t *testing.T) { + xlsx := NewFile() + sheet1 := xlsx.GetSheetName(1) - var buffer bytes.Buffer + categories := map[string]string{ + "A2": "Small", + "A3": "Normal", + "A4": "Large", + "B1": "Apple", + "C1": "Orange", + "D1": "Pear", + } + for cell, v := range categories { + xlsx.SetCellValue(sheet1, cell, v) + } - 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} - xlsx := NewFile() - for k, v := range categories { - xlsx.SetCellValue("Sheet1", k, v) + values := map[string]int{ + "B2": 2, + "C2": 3, + "D2": 3, + "B3": 5, + "C3": 2, + "D3": 4, + "B4": 6, + "C4": 7, + "D4": 8, } - for k, v := range values { - xlsx.SetCellValue("Sheet1", k, v) + for cell, v := range values { + xlsx.SetCellValue(sheet1, cell, v) } - xlsx.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":"Fruit 3D Clustered Column Chart"}}`) + + xlsx.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":"Fruit 3D Clustered Column Chart"}}`) + + var ( + buffer bytes.Buffer + ) + // Save xlsx file by the given path. err := xlsx.Write(&buffer) if !assert.NoError(t, err) { @@ -51,7 +78,8 @@ func TestChartSize(t *testing.T) { t.FailNow() } - err = xml.Unmarshal([]byte(""+workdir.TwoCellAnchor[0].Content+""), &anchor) + err = xml.Unmarshal([]byte(""+ + workdir.TwoCellAnchor[0].Content+""), &anchor) if !assert.NoError(t, err) { t.FailNow() } diff --git a/col.go b/col.go index 1130c3a524..131af1e0c7 100644 --- a/col.go +++ b/col.go @@ -10,10 +10,7 @@ package excelize import ( - "bytes" "math" - "strconv" - "strings" ) // Define the default cell size and EMU unit of measurement. @@ -29,16 +26,19 @@ const ( // // xlsx.GetColVisible("Sheet1", "D") // -func (f *File) GetColVisible(sheet, column string) bool { +func (f *File) GetColVisible(sheet, col string) bool { + colNum := MustColumnNameToNumber(col) + xlsx := f.workSheetReader(sheet) - col := TitleToNumber(strings.ToUpper(column)) + 1 - visible := true if xlsx.Cols == nil { - return visible + return true } + + visible := true for c := range xlsx.Cols.Col { - if xlsx.Cols.Col[c].Min <= col && col <= xlsx.Cols.Col[c].Max { - visible = !xlsx.Cols.Col[c].Hidden + colData := &xlsx.Cols.Col[c] + if colData.Min <= colNum && colNum <= colData.Max { + visible = !colData.Hidden } } return visible @@ -49,31 +49,31 @@ func (f *File) GetColVisible(sheet, column string) bool { // // xlsx.SetColVisible("Sheet1", "D", false) // -func (f *File) SetColVisible(sheet, column string, visible bool) { - xlsx := f.workSheetReader(sheet) - c := TitleToNumber(strings.ToUpper(column)) + 1 - col := xlsxCol{ - Min: c, - Max: c, +func (f *File) SetColVisible(sheet, col string, visible bool) { + colNum := MustColumnNameToNumber(col) + colData := xlsxCol{ + Min: colNum, + Max: colNum, Hidden: !visible, CustomWidth: true, } + xlsx := f.workSheetReader(sheet) if xlsx.Cols == nil { cols := xlsxCols{} - cols.Col = append(cols.Col, col) + cols.Col = append(cols.Col, colData) xlsx.Cols = &cols return } for v := range xlsx.Cols.Col { - if xlsx.Cols.Col[v].Min <= c && c <= xlsx.Cols.Col[v].Max { - col = xlsx.Cols.Col[v] + if xlsx.Cols.Col[v].Min <= colNum && colNum <= xlsx.Cols.Col[v].Max { + colData = xlsx.Cols.Col[v] } } - col.Min = c - col.Max = c - col.Hidden = !visible - col.CustomWidth = true - xlsx.Cols.Col = append(xlsx.Cols.Col, col) + colData.Min = colNum + colData.Max = colNum + colData.Hidden = !visible + colData.CustomWidth = true + xlsx.Cols.Col = append(xlsx.Cols.Col, colData) } // GetColOutlineLevel provides a function to get outline level of a single @@ -82,16 +82,17 @@ func (f *File) SetColVisible(sheet, column string, visible bool) { // // xlsx.GetColOutlineLevel("Sheet1", "D") // -func (f *File) GetColOutlineLevel(sheet, column string) uint8 { +func (f *File) GetColOutlineLevel(sheet, col string) uint8 { + colNum := MustColumnNameToNumber(col) xlsx := f.workSheetReader(sheet) - col := TitleToNumber(strings.ToUpper(column)) + 1 level := uint8(0) if xlsx.Cols == nil { return level } for c := range xlsx.Cols.Col { - if xlsx.Cols.Col[c].Min <= col && col <= xlsx.Cols.Col[c].Max { - level = xlsx.Cols.Col[c].OutlineLevel + colData := &xlsx.Cols.Col[c] + if colData.Min <= colNum && colNum <= colData.Max { + level = colData.OutlineLevel } } return level @@ -103,31 +104,31 @@ func (f *File) GetColOutlineLevel(sheet, column string) uint8 { // // xlsx.SetColOutlineLevel("Sheet1", "D", 2) // -func (f *File) SetColOutlineLevel(sheet, column string, level uint8) { - xlsx := f.workSheetReader(sheet) - c := TitleToNumber(strings.ToUpper(column)) + 1 - col := xlsxCol{ - Min: c, - Max: c, +func (f *File) SetColOutlineLevel(sheet, col string, level uint8) { + colNum := MustColumnNameToNumber(col) + colData := xlsxCol{ + Min: colNum, + Max: colNum, OutlineLevel: level, CustomWidth: true, } + xlsx := f.workSheetReader(sheet) if xlsx.Cols == nil { cols := xlsxCols{} - cols.Col = append(cols.Col, col) + cols.Col = append(cols.Col, colData) xlsx.Cols = &cols return } for v := range xlsx.Cols.Col { - if xlsx.Cols.Col[v].Min <= c && c <= xlsx.Cols.Col[v].Max { - col = xlsx.Cols.Col[v] + if xlsx.Cols.Col[v].Min <= colNum && colNum <= xlsx.Cols.Col[v].Max { + colData = xlsx.Cols.Col[v] } } - col.Min = c - col.Max = c - col.OutlineLevel = level - col.CustomWidth = true - xlsx.Cols.Col = append(xlsx.Cols.Col, col) + colData.Min = colNum + colData.Max = colNum + colData.OutlineLevel = level + colData.CustomWidth = true + xlsx.Cols.Col = append(xlsx.Cols.Col, colData) } // SetColWidth provides a function to set the width of a single column or @@ -141,11 +142,12 @@ func (f *File) SetColOutlineLevel(sheet, column string, level uint8) { // } // func (f *File) SetColWidth(sheet, startcol, endcol string, width float64) { - min := TitleToNumber(strings.ToUpper(startcol)) + 1 - max := TitleToNumber(strings.ToUpper(endcol)) + 1 + min := MustColumnNameToNumber(startcol) + max := MustColumnNameToNumber(endcol) if min > max { min, max = max, min } + xlsx := f.workSheetReader(sheet) col := xlsxCol{ Min: min, @@ -214,38 +216,38 @@ func (f *File) SetColWidth(sheet, startcol, endcol string, width float64) { // xAbs # Absolute distance to left side of object. // yAbs # Absolute distance to top side of object. // -func (f *File) positionObjectPixels(sheet string, colStart, rowStart, x1, y1, width, height int) (int, int, int, int, int, int, int, int) { +func (f *File) positionObjectPixels(sheet string, col, row, x1, y1, width, height int) (int, int, int, int, int, int, int, int) { xAbs := 0 yAbs := 0 // Calculate the absolute x offset of the top-left vertex. - for colID := 1; colID <= colStart; colID++ { + for colID := 1; colID <= col; colID++ { xAbs += f.getColWidth(sheet, colID) } xAbs += x1 // Calculate the absolute y offset of the top-left vertex. // Store the column change to allow optimisations. - for rowID := 1; rowID <= rowStart; rowID++ { + for rowID := 1; rowID <= row; rowID++ { yAbs += f.getRowHeight(sheet, rowID) } yAbs += y1 // Adjust start column for offsets that are greater than the col width. - for x1 >= f.getColWidth(sheet, colStart) { - x1 -= f.getColWidth(sheet, colStart) - colStart++ + for x1 >= f.getColWidth(sheet, col) { + x1 -= f.getColWidth(sheet, col) + col++ } // Adjust start row for offsets that are greater than the row height. - for y1 >= f.getRowHeight(sheet, rowStart) { - y1 -= f.getRowHeight(sheet, rowStart) - rowStart++ + for y1 >= f.getRowHeight(sheet, row) { + y1 -= f.getRowHeight(sheet, row) + row++ } // Initialise end cell to the same as the start cell. - colEnd := colStart - rowEnd := rowStart + colEnd := col + rowEnd := row width += x1 height += y1 @@ -265,7 +267,7 @@ func (f *File) positionObjectPixels(sheet string, colStart, rowStart, x1, y1, wi // The end vertices are whatever is left from the width and height. x2 := width y2 := height - return colStart, rowStart, xAbs, yAbs, colEnd, rowEnd, x2, y2 + return col, row, xAbs, yAbs, colEnd, rowEnd, x2, y2 } // getColWidth provides a function to get column width in pixels by given @@ -289,13 +291,13 @@ func (f *File) getColWidth(sheet string, col int) int { // GetColWidth provides a function to get column width by given worksheet name // and column index. -func (f *File) GetColWidth(sheet, column string) float64 { - col := TitleToNumber(strings.ToUpper(column)) + 1 +func (f *File) GetColWidth(sheet, col string) float64 { + colNum := MustColumnNameToNumber(col) xlsx := f.workSheetReader(sheet) if xlsx.Cols != nil { var width float64 for _, v := range xlsx.Cols.Col { - if v.Min <= col && col <= v.Max { + if v.Min <= colNum && colNum <= v.Max { width = v.Width } } @@ -312,9 +314,12 @@ func (f *File) GetColWidth(sheet, column string) float64 { // // xlsx.InsertCol("Sheet1", "C") // -func (f *File) InsertCol(sheet, column string) { - col := TitleToNumber(strings.ToUpper(column)) - f.adjustHelper(sheet, col, -1, 1) +func (f *File) InsertCol(sheet, col string) { + num, err := ColumnNameToNumber(col) + if err != nil { + panic(err) + } + f.adjustHelper(sheet, columns, num, 1) } // RemoveCol provides a function to remove single column by given worksheet @@ -326,38 +331,23 @@ func (f *File) InsertCol(sheet, column string) { // 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) RemoveCol(sheet, column string) { - xlsx := f.workSheetReader(sheet) - for r := range xlsx.SheetData.Row { - for k, v := range xlsx.SheetData.Row[r].C { - axis := v.R - col := string(strings.Map(letterOnlyMapF, axis)) - if col == column { - xlsx.SheetData.Row[r].C = append(xlsx.SheetData.Row[r].C[:k], xlsx.SheetData.Row[r].C[k+1:]...) - } - } +func (f *File) RemoveCol(sheet, col string) { + num, err := ColumnNameToNumber(col) + if err != nil { + panic(err) // Fail fast to avoid possible future side effects! } - col := TitleToNumber(strings.ToUpper(column)) - f.adjustHelper(sheet, col, -1, -1) -} -// completeCol provieds function to completion column element tags of XML in a -// sheet. -func completeCol(xlsx *xlsxWorksheet, row, cell int) { - buffer := bytes.Buffer{} - for r := range xlsx.SheetData.Row { - if len(xlsx.SheetData.Row[r].C) < cell { - start := len(xlsx.SheetData.Row[r].C) - for iii := start; iii < cell; iii++ { - buffer.WriteString(ToAlphaString(iii)) - buffer.WriteString(strconv.Itoa(r + 1)) - xlsx.SheetData.Row[r].C = append(xlsx.SheetData.Row[r].C, xlsxC{ - R: buffer.String(), - }) - buffer.Reset() + xlsx := f.workSheetReader(sheet) + for rowIdx := range xlsx.SheetData.Row { + rowData := xlsx.SheetData.Row[rowIdx] + for colIdx, cellData := range rowData.C { + colName, _, _ := SplitCellName(cellData.R) + if colName == col { + rowData.C = append(rowData.C[:colIdx], rowData.C[colIdx+1:]...) } } } + f.adjustHelper(sheet, columns, num, -1) } // convertColWidthToPixels provieds function to convert the width of a cell diff --git a/comment.go b/comment.go index 7fb1739aaf..a94194dd61 100644 --- a/comment.go +++ b/comment.go @@ -115,10 +115,9 @@ func (f *File) AddComment(sheet, cell, format string) error { // 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) { - col := string(strings.Map(letterOnlyMapF, cell)) - row, _ := strconv.Atoi(strings.Map(intOnlyMapF, cell)) + col, row := MustCellNameToCoordinates(cell) + yAxis := col - 1 xAxis := row - 1 - yAxis := TitleToNumber(col) vml := f.VMLDrawing[drawingVML] if vml == nil { vml = &vmlDrawing{ diff --git a/date.go b/date.go index 7dc5ef811e..e550feb2bd 100644 --- a/date.go +++ b/date.go @@ -14,31 +14,53 @@ import ( "time" ) -// timeLocationUTC defined the UTC time location. -var timeLocationUTC, _ = time.LoadLocation("UTC") +const ( + dayNanoseconds = 24 * time.Hour + maxDuration = 290 * 364 * dayNanoseconds +) -// timeToUTCTime provides a function to convert time to UTC time. -func timeToUTCTime(t time.Time) time.Time { - return time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), timeLocationUTC) -} +var ( + excelMinTime1900 = time.Date(1899, time.December, 31, 0, 0, 0, 0, time.UTC) + excelBuggyPeriodStart = time.Date(1900, time.March, 1, 0, 0, 0, 0, time.UTC).Add(-time.Nanosecond) +) // timeToExcelTime provides a function to convert time to Excel time. func timeToExcelTime(t time.Time) float64 { // TODO in future this should probably also handle date1904 and like TimeFromExcelTime - var excelTime float64 - var deltaDays int64 - excelTime = 0 - deltaDays = 290 * 364 - // check if UnixNano would be out of int64 range - for t.Unix() > deltaDays*24*60*60 { - // reduce by aprox. 290 years, which is max for int64 nanoseconds - delta := time.Duration(deltaDays) * 24 * time.Hour - excelTime = excelTime + float64(deltaDays) - t = t.Add(-delta) + + // Force user to explicit convet passed value to UTC time. + // Because for example 1900-01-01 00:00:00 +0300 MSK converts to 1900-01-01 00:00:00 +0230 LMT + // probably due to daylight saving. + if t.Location() != time.UTC { + panic("only UTC time expected") + } + + if t.Before(excelMinTime1900) { + return 0.0 + } + + tt := t + diff := t.Sub(excelMinTime1900) + result := float64(0) + + for diff >= maxDuration { + result += float64(maxDuration / dayNanoseconds) + tt = tt.Add(-maxDuration) + diff = tt.Sub(excelMinTime1900) + } + + rem := diff % dayNanoseconds + result += float64(diff-rem)/float64(dayNanoseconds) + float64(rem)/float64(dayNanoseconds) + + // Excel dates after 28th February 1900 are actually one day out. + // Excel behaves as though the date 29th February 1900 existed, which it didn't. + // 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) { + result += 1.0 } - // finally add remainder of UnixNano to keep nano precision - // and 25569 which is days between 1900 and 1970 - return excelTime + float64(t.UnixNano())/8.64e13 + 25569.0 + return result } // shiftJulianToNoon provides a function to process julian date to noon. diff --git a/date_test.go b/date_test.go index a1bfbf48a7..709fb001cd 100644 --- a/date_test.go +++ b/date_test.go @@ -13,17 +13,40 @@ type dateTest struct { GoValue time.Time } +var trueExpectedDateList = []dateTest{ + {0.0000000000000000, time.Date(1899, time.December, 30, 0, 0, 0, 0, time.UTC)}, + {25569.000000000000, time.Unix(0, 0).UTC()}, + + // Expected values extracted from real xlsx file + {1.0000000000000000, time.Date(1900, time.January, 1, 0, 0, 0, 0, time.UTC)}, + {1.0000115740740740, time.Date(1900, time.January, 1, 0, 0, 1, 0, time.UTC)}, + {1.0006944444444446, time.Date(1900, time.January, 1, 0, 1, 0, 0, time.UTC)}, + {1.0416666666666667, time.Date(1900, time.January, 1, 1, 0, 0, 0, time.UTC)}, + {2.0000000000000000, time.Date(1900, time.January, 2, 0, 0, 0, 0, time.UTC)}, + {43269.000000000000, time.Date(2018, time.June, 18, 0, 0, 0, 0, time.UTC)}, + {43542.611111111109, time.Date(2019, time.March, 18, 14, 40, 0, 0, time.UTC)}, + {401769.00000000000, time.Date(3000, time.January, 1, 0, 0, 0, 0, time.UTC)}, +} + func TestTimeToExcelTime(t *testing.T) { - trueExpectedInputList := []dateTest{ - {0.0, time.Date(1899, 12, 30, 0, 0, 0, 0, time.UTC)}, - {25569.0, time.Unix(0, 0)}, - {43269.0, time.Date(2018, 6, 18, 0, 0, 0, 0, time.UTC)}, - {401769.0, time.Date(3000, 1, 1, 0, 0, 0, 0, time.UTC)}, + for i, test := range trueExpectedDateList { + t.Run(fmt.Sprintf("TestData%d", i+1), func(t *testing.T) { + assert.Equalf(t, test.ExcelValue, timeToExcelTime(test.GoValue), + "Time: %s", test.GoValue.String()) + }) } +} - for i, test := range trueExpectedInputList { +func TestTimeToExcelTime_Timezone(t *testing.T) { + msk, err := time.LoadLocation("Europe/Moscow") + if !assert.NoError(t, err) { + t.FailNow() + } + for i, test := range trueExpectedDateList { t.Run(fmt.Sprintf("TestData%d", i+1), func(t *testing.T) { - assert.Equal(t, test.ExcelValue, timeToExcelTime(test.GoValue)) + assert.Panics(t, func() { + timeToExcelTime(test.GoValue.In(msk)) + }, "Time: %s", test.GoValue.String()) }) } } @@ -43,3 +66,9 @@ func TestTimeFromExcelTime(t *testing.T) { }) } } + +func TestTimeFromExcelTime_1904(t *testing.T) { + shiftJulianToNoon(1, -0.6) + timeFromExcelTime(61, true) + timeFromExcelTime(62, true) +} diff --git a/errors.go b/errors.go new file mode 100644 index 0000000000..4dec815920 --- /dev/null +++ b/errors.go @@ -0,0 +1,17 @@ +package excelize + +import ( + "fmt" +) + +func newInvalidColumnNameError(col string) error { + return fmt.Errorf("invalid column name %q", col) +} + +func newInvalidRowNumberError(row int) error { + return fmt.Errorf("invalid row number %d", row) +} + +func newInvalidCellNameError(cell string) error { + return fmt.Errorf("invalid cell name %q", cell) +} diff --git a/errors_test.go b/errors_test.go new file mode 100644 index 0000000000..89d241c7ea --- /dev/null +++ b/errors_test.go @@ -0,0 +1,21 @@ +package excelize + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewInvalidColNameError(t *testing.T) { + assert.EqualError(t, newInvalidColumnNameError("A"), "invalid column name \"A\"") + assert.EqualError(t, newInvalidColumnNameError(""), "invalid column name \"\"") +} + +func TestNewInvalidRowNumberError(t *testing.T) { + assert.EqualError(t, newInvalidRowNumberError(0), "invalid row number 0") +} + +func TestNewInvalidCellNameError(t *testing.T) { + assert.EqualError(t, newInvalidCellNameError("A"), "invalid cell name \"A\"") + assert.EqualError(t, newInvalidCellNameError(""), "invalid cell name \"\"") +} diff --git a/excelize.go b/excelize.go index feb41cba31..7a5046043e 100644 --- a/excelize.go +++ b/excelize.go @@ -206,235 +206,16 @@ func (f *File) UpdateLinkedValue() { } } -// adjustHelper provides a function to adjust rows and columns dimensions, -// hyperlinks, merged cells and auto filter when inserting or deleting rows or -// columns. -// -// sheet: Worksheet name that we're editing -// column: Index number of the column we're inserting/deleting before -// row: Index number of the row we're inserting/deleting before -// offset: Number of rows/column to insert/delete negative values indicate deletion -// -// TODO: adjustCalcChain, adjustPageBreaks, adjustComments, -// adjustDataValidations, adjustProtectedCells -// -func (f *File) adjustHelper(sheet string, column, row, offset int) { - xlsx := f.workSheetReader(sheet) - f.adjustRowDimensions(xlsx, row, offset) - f.adjustColDimensions(xlsx, column, offset) - f.adjustHyperlinks(sheet, column, row, offset) - f.adjustMergeCells(xlsx, column, row, offset) - f.adjustAutoFilter(xlsx, column, row, offset) - checkSheet(xlsx) - checkRow(xlsx) -} - -// adjustColDimensions provides a function to update column dimensions when -// inserting or deleting rows or columns. -func (f *File) adjustColDimensions(xlsx *xlsxWorksheet, column, offset int) { - for i, r := range xlsx.SheetData.Row { - for k, v := range r.C { - axis := v.R - col := string(strings.Map(letterOnlyMapF, axis)) - row, _ := strconv.Atoi(strings.Map(intOnlyMapF, axis)) - yAxis := TitleToNumber(col) - if yAxis >= column && column != -1 { - xlsx.SheetData.Row[i].C[k].R = ToAlphaString(yAxis+offset) + strconv.Itoa(row) - } - } - } -} - -// adjustRowDimensions provides a function to update row dimensions when -// inserting or deleting rows or columns. -func (f *File) adjustRowDimensions(xlsx *xlsxWorksheet, rowIndex, offset int) { - if rowIndex == -1 { - return - } - for i, r := range xlsx.SheetData.Row { - if r.R >= rowIndex { - f.ajustSingleRowDimensions(&xlsx.SheetData.Row[i], r.R+offset) - } - } -} - -// ajustSingleRowDimensions provides a function to ajust single row dimensions. -func (f *File) ajustSingleRowDimensions(r *xlsxRow, row int) { - r.R = row - for i, col := range r.C { - r.C[i].R = string(strings.Map(letterOnlyMapF, col.R)) + strconv.Itoa(r.R) - } -} - -// adjustHyperlinks provides a function to update hyperlinks when inserting or -// deleting rows or columns. -func (f *File) adjustHyperlinks(sheet string, column, rowIndex, offset int) { +// GetMergeCells provides a function to get all merged cells from a worksheet currently. +func (f *File) GetMergeCells(sheet string) []MergeCell { xlsx := f.workSheetReader(sheet) - // order is important - if xlsx.Hyperlinks != nil && offset < 0 { - for i, v := range xlsx.Hyperlinks.Hyperlink { - axis := v.Ref - col := string(strings.Map(letterOnlyMapF, axis)) - row, _ := strconv.Atoi(strings.Map(intOnlyMapF, axis)) - yAxis := TitleToNumber(col) - if row == rowIndex || yAxis == column { - f.deleteSheetRelationships(sheet, v.RID) - if len(xlsx.Hyperlinks.Hyperlink) > 1 { - xlsx.Hyperlinks.Hyperlink = append(xlsx.Hyperlinks.Hyperlink[:i], xlsx.Hyperlinks.Hyperlink[i+1:]...) - } else { - xlsx.Hyperlinks = nil - } - } - } - } + var mergeCells []MergeCell - if xlsx.Hyperlinks != nil { - for i, v := range xlsx.Hyperlinks.Hyperlink { - axis := v.Ref - col := string(strings.Map(letterOnlyMapF, axis)) - row, _ := strconv.Atoi(strings.Map(intOnlyMapF, axis)) - xAxis := row + offset - yAxis := TitleToNumber(col) - if rowIndex != -1 && row >= rowIndex { - xlsx.Hyperlinks.Hyperlink[i].Ref = col + strconv.Itoa(xAxis) - } - if column != -1 && yAxis >= column { - xlsx.Hyperlinks.Hyperlink[i].Ref = ToAlphaString(yAxis+offset) + strconv.Itoa(row) - } - } - } -} - -// adjustMergeCellsHelper provides a function to update merged cells when -// inserting or deleting rows or columns. -func (f *File) adjustMergeCellsHelper(xlsx *xlsxWorksheet, column, rowIndex, offset int) { if xlsx.MergeCells != nil { - for k, v := range xlsx.MergeCells.Cells { - beg := strings.Split(v.Ref, ":")[0] - end := strings.Split(v.Ref, ":")[1] - - begcol := string(strings.Map(letterOnlyMapF, beg)) - begrow, _ := strconv.Atoi(strings.Map(intOnlyMapF, beg)) - begxAxis := begrow + offset - begyAxis := TitleToNumber(begcol) - - endcol := string(strings.Map(letterOnlyMapF, end)) - endrow, _ := strconv.Atoi(strings.Map(intOnlyMapF, end)) - endxAxis := endrow + offset - endyAxis := TitleToNumber(endcol) - - if rowIndex != -1 { - if begrow > 1 && begrow >= rowIndex { - beg = begcol + strconv.Itoa(begxAxis) - } - if endrow > 1 && endrow >= rowIndex { - end = endcol + strconv.Itoa(endxAxis) - } - } - - if column != -1 { - if begyAxis >= column { - beg = ToAlphaString(begyAxis+offset) + strconv.Itoa(endrow) - } - if endyAxis >= column { - end = ToAlphaString(endyAxis+offset) + strconv.Itoa(endrow) - } - } - - xlsx.MergeCells.Cells[k].Ref = beg + ":" + end - } - } -} - -// adjustMergeCells provides a function to update merged cells when inserting -// or deleting rows or columns. -func (f *File) adjustMergeCells(xlsx *xlsxWorksheet, column, rowIndex, offset int) { - f.adjustMergeCellsHelper(xlsx, column, rowIndex, offset) - - if xlsx.MergeCells != nil && offset < 0 { - for k, v := range xlsx.MergeCells.Cells { - beg := strings.Split(v.Ref, ":")[0] - end := strings.Split(v.Ref, ":")[1] - if beg == end { - xlsx.MergeCells.Count += offset - if len(xlsx.MergeCells.Cells) > 1 { - xlsx.MergeCells.Cells = append(xlsx.MergeCells.Cells[:k], xlsx.MergeCells.Cells[k+1:]...) - } else { - xlsx.MergeCells = nil - } - } - } - } -} - -// adjustAutoFilter provides a function to update the auto filter when -// inserting or deleting rows or columns. -func (f *File) adjustAutoFilter(xlsx *xlsxWorksheet, column, rowIndex, offset int) { - f.adjustAutoFilterHelper(xlsx, column, rowIndex, offset) - - if xlsx.AutoFilter != nil { - beg := strings.Split(xlsx.AutoFilter.Ref, ":")[0] - end := strings.Split(xlsx.AutoFilter.Ref, ":")[1] + mergeCells = make([]MergeCell, 0, len(xlsx.MergeCells.Cells)) - begcol := string(strings.Map(letterOnlyMapF, beg)) - begrow, _ := strconv.Atoi(strings.Map(intOnlyMapF, beg)) - begxAxis := begrow + offset - - endcol := string(strings.Map(letterOnlyMapF, end)) - endrow, _ := strconv.Atoi(strings.Map(intOnlyMapF, end)) - endxAxis := endrow + offset - endyAxis := TitleToNumber(endcol) - - if rowIndex != -1 { - if begrow >= rowIndex { - beg = begcol + strconv.Itoa(begxAxis) - } - if endrow >= rowIndex { - end = endcol + strconv.Itoa(endxAxis) - } - } - - if column != -1 && endyAxis >= column { - end = ToAlphaString(endyAxis+offset) + strconv.Itoa(endrow) - } - xlsx.AutoFilter.Ref = beg + ":" + end - } -} - -// adjustAutoFilterHelper provides a function to update the auto filter when -// inserting or deleting rows or columns. -func (f *File) adjustAutoFilterHelper(xlsx *xlsxWorksheet, column, rowIndex, offset int) { - if xlsx.AutoFilter != nil { - beg := strings.Split(xlsx.AutoFilter.Ref, ":")[0] - end := strings.Split(xlsx.AutoFilter.Ref, ":")[1] - - begcol := string(strings.Map(letterOnlyMapF, beg)) - begrow, _ := strconv.Atoi(strings.Map(intOnlyMapF, beg)) - begyAxis := TitleToNumber(begcol) - - endcol := string(strings.Map(letterOnlyMapF, end)) - endyAxis := TitleToNumber(endcol) - endrow, _ := strconv.Atoi(strings.Map(intOnlyMapF, end)) - - if (begrow == rowIndex && offset < 0) || (column == begyAxis && column == endyAxis) { - xlsx.AutoFilter = nil - for i, r := range xlsx.SheetData.Row { - if begrow < r.R && r.R <= endrow { - xlsx.SheetData.Row[i].Hidden = false - } - } - } - } -} - -// GetMergeCells provides a function to get all merged cells from a worksheet currently. -func (f *File) GetMergeCells(sheet string) []MergeCell { - mergeCells := []MergeCell{} - - xlsx := f.workSheetReader(sheet) - if xlsx.MergeCells != nil { - for i := 0; i < len(xlsx.MergeCells.Cells); i++ { + for i := range xlsx.MergeCells.Cells { ref := xlsx.MergeCells.Cells[i].Ref axis := strings.Split(ref, ":")[0] mergeCells = append(mergeCells, []string{ref, f.GetCellValue(sheet, axis)}) diff --git a/excelize_test.go b/excelize_test.go index 47b9561c07..694f505be7 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -35,13 +35,22 @@ func TestOpenFile(t *testing.T) { t.Log("\r\n") } xlsx.UpdateLinkedValue() + xlsx.SetCellDefault("Sheet2", "A1", strconv.FormatFloat(float64(100.1588), 'f', -1, 32)) xlsx.SetCellDefault("Sheet2", "A1", strconv.FormatFloat(float64(-100.1588), 'f', -1, 64)) + // Test set cell value with illegal row number. - xlsx.SetCellDefault("Sheet2", "A", strconv.FormatFloat(float64(-100.1588), 'f', -1, 64)) + assert.Panics(t, func() { + xlsx.SetCellDefault("Sheet2", "A", strconv.FormatFloat(float64(-100.1588), 'f', -1, 64)) + }) + xlsx.SetCellInt("Sheet2", "A1", 100) + // Test set cell integer value with illegal row number. - xlsx.SetCellInt("Sheet2", "A", 100) + assert.Panics(t, func() { + xlsx.SetCellInt("Sheet2", "A", 100) + }) + xlsx.SetCellStr("Sheet2", "C11", "Knowns") // Test max characters in a cell. xlsx.SetCellStr("Sheet2", "D11", strings.Repeat("c", 32769)) @@ -51,23 +60,38 @@ func TestOpenFile(t *testing.T) { xlsx.SetCellInt("Sheet3", "A23", 10) xlsx.SetCellStr("Sheet3", "b230", "10") xlsx.SetCellStr("Sheet10", "b230", "10") + // Test set cell string value with illegal row number. - xlsx.SetCellStr("Sheet10", "A", "10") + assert.Panics(t, func() { + xlsx.SetCellStr("Sheet10", "A", "10") + }) + xlsx.SetActiveSheet(2) // Test get cell formula with given rows number. xlsx.GetCellFormula("Sheet1", "B19") // Test get cell formula with illegal worksheet name. xlsx.GetCellFormula("Sheet2", "B20") - // Test get cell formula with illegal rows number. xlsx.GetCellFormula("Sheet1", "B20") - xlsx.GetCellFormula("Sheet1", "B") + + // Test get cell formula with illegal rows number. + assert.Panics(t, func() { + xlsx.GetCellFormula("Sheet1", "B") + }) + // Test get shared cell formula xlsx.GetCellFormula("Sheet2", "H11") xlsx.GetCellFormula("Sheet2", "I11") getSharedForumula(&xlsxWorksheet{}, "") + // Test read cell value with given illegal rows number. - xlsx.GetCellValue("Sheet2", "a-1") - xlsx.GetCellValue("Sheet2", "A") + assert.Panics(t, func() { + xlsx.GetCellValue("Sheet2", "a-1") + }) + + assert.Panics(t, func() { + xlsx.GetCellValue("Sheet2", "A") + }) + // Test read cell value with given lowercase column number. xlsx.GetCellValue("Sheet2", "a5") xlsx.GetCellValue("Sheet2", "C11") @@ -92,10 +116,7 @@ func TestOpenFile(t *testing.T) { xlsx.SetCellValue("Sheet2", "F15", uint64(1<<32-1)) xlsx.SetCellValue("Sheet2", "F16", true) xlsx.SetCellValue("Sheet2", "F17", complex64(5+10i)) - t.Log(letterOnlyMapF('x')) - shiftJulianToNoon(1, -0.6) - timeFromExcelTime(61, true) - timeFromExcelTime(62, true) + // Test boolean write booltest := []struct { value bool @@ -108,8 +129,14 @@ func TestOpenFile(t *testing.T) { xlsx.SetCellValue("Sheet2", "F16", test.value) assert.Equal(t, test.expected, xlsx.GetCellValue("Sheet2", "F16")) } + xlsx.SetCellValue("Sheet2", "G2", nil) - xlsx.SetCellValue("Sheet2", "G4", time.Now()) + + assert.Panics(t, func() { + xlsx.SetCellValue("Sheet2", "G4", time.Now()) + }) + + xlsx.SetCellValue("Sheet2", "G4", time.Now().UTC()) // 02:46:40 xlsx.SetCellValue("Sheet2", "G5", time.Duration(1e13)) // Test completion column. @@ -298,8 +325,15 @@ func TestSetCellHyperLink(t *testing.T) { xlsx.SetCellHyperLink("Sheet2", "C1", "https://github.com/360EntSecGroup-Skylar/excelize", "External") // Test add Location hyperlink in a work sheet. xlsx.SetCellHyperLink("Sheet2", "D6", "Sheet1!D8", "Location") - xlsx.SetCellHyperLink("Sheet2", "C3", "Sheet1!D8", "") - xlsx.SetCellHyperLink("Sheet2", "", "Sheet1!D60", "Location") + + assert.Panics(t, func() { + xlsx.SetCellHyperLink("Sheet2", "C3", "Sheet1!D8", "") + }) + + assert.Panics(t, func() { + xlsx.SetCellHyperLink("Sheet2", "", "Sheet1!D60", "Location") + }) + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetCellHyperLink.xlsx"))) } @@ -309,9 +343,11 @@ func TestGetCellHyperLink(t *testing.T) { t.FailNow() } - link, target := xlsx.GetCellHyperLink("Sheet1", "") - t.Log(link, target) - link, target = xlsx.GetCellHyperLink("Sheet1", "A22") + assert.Panics(t, func() { + xlsx.GetCellHyperLink("Sheet1", "") + }) + + link, target := xlsx.GetCellHyperLink("Sheet1", "A22") t.Log(link, target) link, target = xlsx.GetCellHyperLink("Sheet2", "D6") t.Log(link, target) @@ -327,8 +363,12 @@ func TestSetCellFormula(t *testing.T) { xlsx.SetCellFormula("Sheet1", "B19", "SUM(Sheet2!D2,Sheet2!D11)") xlsx.SetCellFormula("Sheet1", "C19", "SUM(Sheet2!D2,Sheet2!D9)") + // Test set cell formula with illegal rows number. - xlsx.SetCellFormula("Sheet1", "C", "SUM(Sheet2!D2,Sheet2!D9)") + assert.Panics(t, func() { + xlsx.SetCellFormula("Sheet1", "C", "SUM(Sheet2!D2,Sheet2!D9)") + }) + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetCellFormula1.xlsx"))) xlsx, err = OpenFile(filepath.Join("test", "CalcChain.xlsx")) @@ -408,51 +448,39 @@ func TestGetMergeCells(t *testing.T) { value string start string end string - }{ - { - value: "A1", - start: "A1", - end: "B1", - }, - { - value: "A2", - start: "A2", - end: "A3", - }, - { - value: "A4", - start: "A4", - end: "B5", - }, - { - value: "A7", - start: "A7", - end: "C10", - }, - } + }{{ + value: "A1", + start: "A1", + end: "B1", + }, { + value: "A2", + start: "A2", + end: "A3", + }, { + value: "A4", + start: "A4", + end: "B5", + }, { + value: "A7", + start: "A7", + end: "C10", + }} xlsx, err := OpenFile(filepath.Join("test", "MergeCell.xlsx")) if !assert.NoError(t, err) { t.FailNow() } + sheet1 := xlsx.GetSheetName(1) - mergeCells := xlsx.GetMergeCells("Sheet1") - if len(mergeCells) != len(wants) { - t.Fatalf("Expected count of merge cells %d, but got %d\n", len(wants), len(mergeCells)) + mergeCells := xlsx.GetMergeCells(sheet1) + if !assert.Len(t, mergeCells, len(wants)) { + t.FailNow() } for i, m := range mergeCells { - if wants[i].value != m.GetCellValue() { - t.Fatalf("Expected merged cell value %s, but got %s\n", wants[i].value, m.GetCellValue()) - } - - if wants[i].start != m.GetStartAxis() { - t.Fatalf("Expected merged cell value %s, but got %s\n", wants[i].start, m.GetStartAxis()) - } - - if wants[i].end != m.GetEndAxis() { - t.Fatalf("Expected merged cell value %s, but got %s\n", wants[i].end, m.GetEndAxis()) - } + assert.Equal(t, wants[i].value, m.GetCellValue()) + assert.Equal(t, wants[i].start, m.GetStartAxis()) + assert.Equal(t, wants[i].end, m.GetEndAxis()) } } @@ -469,11 +497,20 @@ func TestSetCellStyleAlignment(t *testing.T) { } xlsx.SetCellStyle("Sheet1", "A22", "A22", style) + // Test set cell style with given illegal rows number. - xlsx.SetCellStyle("Sheet1", "A", "A22", style) - xlsx.SetCellStyle("Sheet1", "A22", "A", style) + assert.Panics(t, func() { + xlsx.SetCellStyle("Sheet1", "A", "A22", style) + }) + + assert.Panics(t, func() { + xlsx.SetCellStyle("Sheet1", "A22", "A", style) + }) + // Test get cell style with given illegal rows number. - xlsx.GetCellStyle("Sheet1", "A") + assert.Panics(t, func() { + xlsx.GetCellStyle("Sheet1", "A") + }) assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetCellStyleAlignment.xlsx"))) } @@ -782,46 +819,48 @@ func TestGetPicture(t *testing.T) { } file, raw := xlsx.GetPicture("Sheet1", "F21") - if file == "" { - err = ioutil.WriteFile(file, raw, 0644) - if !assert.NoError(t, err) { - t.FailNow() - } + if !assert.NotEmpty(t, file) || !assert.NotEmpty(t, raw) || + !assert.NoError(t, ioutil.WriteFile(file, raw, 0644)) { + + t.FailNow() } // Try to get picture from a worksheet that doesn't contain any images. file, raw = xlsx.GetPicture("Sheet3", "I9") - if file != "" { - err = ioutil.WriteFile(file, raw, 0644) - if !assert.NoError(t, err) { - t.FailNow() - } - } + assert.Empty(t, file) + assert.Empty(t, raw) + // Try to get picture from a cell that doesn't contain an image. file, raw = xlsx.GetPicture("Sheet2", "A2") - t.Log(file, len(raw)) + assert.Empty(t, file) + assert.Empty(t, raw) + xlsx.getDrawingRelationships("xl/worksheets/_rels/sheet1.xml.rels", "rId8") xlsx.getDrawingRelationships("", "") xlsx.getSheetRelationshipsTargetByID("", "") xlsx.deleteSheetRelationships("", "") // Try to get picture from a local storage file. - assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestGetPicture.xlsx"))) + if !assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestGetPicture.xlsx"))) { + t.FailNow() + } + xlsx, err = OpenFile(filepath.Join("test", "TestGetPicture.xlsx")) if !assert.NoError(t, err) { t.FailNow() } + file, raw = xlsx.GetPicture("Sheet1", "F21") - if file == "" { - err = ioutil.WriteFile(file, raw, 0644) - if !assert.NoError(t, err) { - t.FailNow() - } + if !assert.NotEmpty(t, file) || !assert.NotEmpty(t, raw) || + !assert.NoError(t, ioutil.WriteFile(file, raw, 0644)) { + + t.FailNow() } // Try to get picture from a local storage file that doesn't contain an image. file, raw = xlsx.GetPicture("Sheet1", "F22") - t.Log(file, len(raw)) + assert.Empty(t, file) + assert.Empty(t, raw) } func TestSheetVisibility(t *testing.T) { @@ -838,21 +877,6 @@ func TestSheetVisibility(t *testing.T) { assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSheetVisibility.xlsx"))) } -func TestRowVisibility(t *testing.T) { - xlsx, err := prepareTestBook1() - if !assert.NoError(t, err) { - t.FailNow() - } - - xlsx.SetRowVisible("Sheet3", 2, false) - xlsx.SetRowVisible("Sheet3", 2, true) - xlsx.SetRowVisible("Sheet3", 0, true) - xlsx.GetRowVisible("Sheet3", 2) - xlsx.GetRowVisible("Sheet3", 0) - - assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestRowVisibility.xlsx"))) -} - func TestColumnVisibility(t *testing.T) { t.Run("TestBook1", func(t *testing.T) { xlsx, err := prepareTestBook1() @@ -1065,38 +1089,37 @@ func TestAddChart(t *testing.T) { func TestInsertCol(t *testing.T) { xlsx := NewFile() - for j := 1; j <= 10; j++ { - for i := 0; i <= 10; i++ { - axis := ToAlphaString(i) + strconv.Itoa(j) - xlsx.SetCellStr("Sheet1", axis, axis) - } - } - xlsx.SetCellHyperLink("Sheet1", "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External") - xlsx.MergeCell("Sheet1", "A1", "C3") - err := xlsx.AutoFilter("Sheet1", "A2", "B2", `{"column":"B","expression":"x != blanks"}`) + sheet1 := xlsx.GetSheetName(1) + + fillCells(xlsx, sheet1, 10, 10) + + xlsx.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External") + xlsx.MergeCell(sheet1, "A1", "C3") + + err := xlsx.AutoFilter(sheet1, "A2", "B2", `{"column":"B","expression":"x != blanks"}`) if !assert.NoError(t, err) { t.FailNow() } - xlsx.InsertCol("Sheet1", "A") + xlsx.InsertCol(sheet1, "A") assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestInsertCol.xlsx"))) } func TestRemoveCol(t *testing.T) { xlsx := NewFile() - for j := 1; j <= 10; j++ { - for i := 0; i <= 10; i++ { - axis := ToAlphaString(i) + strconv.Itoa(j) - xlsx.SetCellStr("Sheet1", axis, axis) - } - } - xlsx.SetCellHyperLink("Sheet1", "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External") - xlsx.SetCellHyperLink("Sheet1", "C5", "https://github.com", "External") - xlsx.MergeCell("Sheet1", "A1", "B1") - xlsx.MergeCell("Sheet1", "A2", "B2") - xlsx.RemoveCol("Sheet1", "A") - xlsx.RemoveCol("Sheet1", "A") + sheet1 := xlsx.GetSheetName(1) + + fillCells(xlsx, sheet1, 10, 15) + + xlsx.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External") + xlsx.SetCellHyperLink(sheet1, "C5", "https://github.com", "External") + + xlsx.MergeCell(sheet1, "A1", "B1") + xlsx.MergeCell(sheet1, "A2", "B2") + + xlsx.RemoveCol(sheet1, "A") + xlsx.RemoveCol(sheet1, "A") assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestRemoveCol.xlsx"))) } @@ -1117,11 +1140,10 @@ func TestSetPane(t *testing.T) { func TestConditionalFormat(t *testing.T) { xlsx := NewFile() - for j := 1; j <= 10; j++ { - for i := 0; i <= 15; i++ { - xlsx.SetCellInt("Sheet1", ToAlphaString(i)+strconv.Itoa(j), j) - } - } + sheet1 := xlsx.GetSheetName(1) + + fillCells(xlsx, sheet1, 10, 15) + var format1, format2, format3 int var err error // Rose format for bad conditional. @@ -1143,31 +1165,31 @@ func TestConditionalFormat(t *testing.T) { } // Color scales: 2 color. - xlsx.SetConditionalFormat("Sheet1", "A1:A10", `[{"type":"2_color_scale","criteria":"=","min_type":"min","max_type":"max","min_color":"#F8696B","max_color":"#63BE7B"}]`) + xlsx.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. - xlsx.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"}]`) + xlsx.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... - xlsx.SetConditionalFormat("Sheet1", "C1:C10", fmt.Sprintf(`[{"type":"cell","criteria":"between","format":%d,"minimum":"6","maximum":"8"}]`, format1)) + xlsx.SetConditionalFormat(sheet1, "C1:C10", fmt.Sprintf(`[{"type":"cell","criteria":"between","format":%d,"minimum":"6","maximum":"8"}]`, format1)) // Hightlight cells rules: Greater Than... - xlsx.SetConditionalFormat("Sheet1", "D1:D10", fmt.Sprintf(`[{"type":"cell","criteria":">","format":%d,"value":"6"}]`, format3)) + xlsx.SetConditionalFormat(sheet1, "D1:D10", fmt.Sprintf(`[{"type":"cell","criteria":">","format":%d,"value":"6"}]`, format3)) // Hightlight cells rules: Equal To... - xlsx.SetConditionalFormat("Sheet1", "E1:E10", fmt.Sprintf(`[{"type":"top","criteria":"=","format":%d}]`, format3)) + xlsx.SetConditionalFormat(sheet1, "E1:E10", fmt.Sprintf(`[{"type":"top","criteria":"=","format":%d}]`, format3)) // Hightlight cells rules: Not Equal To... - xlsx.SetConditionalFormat("Sheet1", "F1:F10", fmt.Sprintf(`[{"type":"unique","criteria":"=","format":%d}]`, format2)) + xlsx.SetConditionalFormat(sheet1, "F1:F10", fmt.Sprintf(`[{"type":"unique","criteria":"=","format":%d}]`, format2)) // Hightlight cells rules: Duplicate Values... - xlsx.SetConditionalFormat("Sheet1", "G1:G10", fmt.Sprintf(`[{"type":"duplicate","criteria":"=","format":%d}]`, format2)) + xlsx.SetConditionalFormat(sheet1, "G1:G10", fmt.Sprintf(`[{"type":"duplicate","criteria":"=","format":%d}]`, format2)) // Top/Bottom rules: Top 10%. - xlsx.SetConditionalFormat("Sheet1", "H1:H10", fmt.Sprintf(`[{"type":"top","criteria":"=","format":%d,"value":"6","percent":true}]`, format1)) + xlsx.SetConditionalFormat(sheet1, "H1:H10", fmt.Sprintf(`[{"type":"top","criteria":"=","format":%d,"value":"6","percent":true}]`, format1)) // Top/Bottom rules: Above Average... - xlsx.SetConditionalFormat("Sheet1", "I1:I10", fmt.Sprintf(`[{"type":"average","criteria":"=","format":%d, "above_average": true}]`, format3)) + xlsx.SetConditionalFormat(sheet1, "I1:I10", fmt.Sprintf(`[{"type":"average","criteria":"=","format":%d, "above_average": true}]`, format3)) // Top/Bottom rules: Below Average... - xlsx.SetConditionalFormat("Sheet1", "J1:J10", fmt.Sprintf(`[{"type":"average","criteria":"=","format":%d, "above_average": false}]`, format1)) + xlsx.SetConditionalFormat(sheet1, "J1:J10", fmt.Sprintf(`[{"type":"average","criteria":"=","format":%d, "above_average": false}]`, format1)) // Data Bars: Gradient Fill. - xlsx.SetConditionalFormat("Sheet1", "K1:K10", `[{"type":"data_bar", "criteria":"=", "min_type":"min","max_type":"max","bar_color":"#638EC6"}]`) + xlsx.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. - xlsx.SetConditionalFormat("Sheet1", "L1:L10", fmt.Sprintf(`[{"type":"formula", "criteria":"L2<3", "format":%d}]`, format1)) + xlsx.SetConditionalFormat(sheet1, "L1:L10", fmt.Sprintf(`[{"type":"formula", "criteria":"L2<3", "format":%d}]`, format1)) // Test set invalid format set in conditional format - xlsx.SetConditionalFormat("Sheet1", "L1:L10", "") + xlsx.SetConditionalFormat(sheet1, "L1:L10", "") err = xlsx.SaveAs(filepath.Join("test", "TestConditionalFormat.xlsx")) if !assert.NoError(t, err) { @@ -1175,9 +1197,9 @@ func TestConditionalFormat(t *testing.T) { } // Set conditional format with illegal valid type. - xlsx.SetConditionalFormat("Sheet1", "K1:K10", `[{"type":"", "criteria":"=", "min_type":"min","max_type":"max","bar_color":"#638EC6"}]`) + xlsx.SetConditionalFormat(sheet1, "K1:K10", `[{"type":"", "criteria":"=", "min_type":"min","max_type":"max","bar_color":"#638EC6"}]`) // Set conditional format with illegal criteria type. - xlsx.SetConditionalFormat("Sheet1", "K1:K10", `[{"type":"data_bar", "criteria":"", "min_type":"min","max_type":"max","bar_color":"#638EC6"}]`) + xlsx.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 shold not return error. xlsx, err = OpenFile(filepath.Join("test", "Book1.xlsx")) @@ -1193,11 +1215,9 @@ func TestConditionalFormat(t *testing.T) { func TestConditionalFormatError(t *testing.T) { xlsx := NewFile() - for j := 1; j <= 10; j++ { - for i := 0; i <= 15; i++ { - xlsx.SetCellInt("Sheet1", ToAlphaString(i)+strconv.Itoa(j), j) - } - } + sheet1 := xlsx.GetSheetName(1) + + fillCells(xlsx, sheet1, 10, 15) // Set conditional format with illegal JSON string should return error _, err := xlsx.NewConditionalStyle("") @@ -1206,15 +1226,6 @@ func TestConditionalFormatError(t *testing.T) { } } -func TestTitleToNumber(t *testing.T) { - assert.Equal(t, 0, TitleToNumber("A")) - assert.Equal(t, 25, TitleToNumber("Z")) - assert.Equal(t, 26, TitleToNumber("AA")) - assert.Equal(t, 36, TitleToNumber("AK")) - assert.Equal(t, 36, TitleToNumber("ak")) - assert.Equal(t, 51, TitleToNumber("AZ")) -} - func TestSharedStrings(t *testing.T) { xlsx, err := OpenFile(filepath.Join("test", "SharedStrings.xlsx")) if !assert.NoError(t, err) { @@ -1229,10 +1240,19 @@ func TestSetSheetRow(t *testing.T) { t.FailNow() } - xlsx.SetSheetRow("Sheet1", "B27", &[]interface{}{"cell", nil, int32(42), float64(42), time.Now()}) - xlsx.SetSheetRow("Sheet1", "", &[]interface{}{"cell", nil, 2}) - xlsx.SetSheetRow("Sheet1", "B27", []interface{}{}) - xlsx.SetSheetRow("Sheet1", "B27", &xlsx) + xlsx.SetSheetRow("Sheet1", "B27", &[]interface{}{"cell", nil, int32(42), float64(42), time.Now().UTC()}) + + assert.Panics(t, func() { + xlsx.SetSheetRow("Sheet1", "", &[]interface{}{"cell", nil, 2}) + }) + + assert.Panics(t, func() { + xlsx.SetSheetRow("Sheet1", "B27", []interface{}{}) + }) + + assert.Panics(t, func() { + xlsx.SetSheetRow("Sheet1", "B27", &xlsx) + }) assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetSheetRow.xlsx"))) } @@ -1245,10 +1265,17 @@ func TestOutlineLevel(t *testing.T) { xlsx.GetColOutlineLevel("Shee2", "A") xlsx.SetColWidth("Sheet2", "A", "D", 13) xlsx.SetColOutlineLevel("Sheet2", "B", 2) - xlsx.SetRowOutlineLevel("Sheet1", 2, 1) - xlsx.SetRowOutlineLevel("Sheet1", 0, 1) - xlsx.GetRowOutlineLevel("Sheet1", 2) - xlsx.GetRowOutlineLevel("Sheet1", 0) + xlsx.SetRowOutlineLevel("Sheet1", 2, 250) + + assert.Panics(t, func() { + xlsx.SetRowOutlineLevel("Sheet1", 0, 1) + }) + + assert.Equal(t, uint8(250), xlsx.GetRowOutlineLevel("Sheet1", 2)) + + assert.Panics(t, func() { + xlsx.GetRowOutlineLevel("Sheet1", 0) + }) err := xlsx.SaveAs(filepath.Join("test", "TestOutlineLevel.xlsx")) if !assert.NoError(t, err) { t.FailNow() @@ -1258,7 +1285,6 @@ func TestOutlineLevel(t *testing.T) { if !assert.NoError(t, err) { t.FailNow() } - xlsx.SetColOutlineLevel("Sheet2", "B", 2) } @@ -1388,3 +1414,12 @@ func prepareTestBook4() (*File, error) { return xlsx, nil } + +func fillCells(xlsx *File, sheet string, colCount, rowCount int) { + for col := 1; col <= colCount; col++ { + for row := 1; row <= rowCount; row++ { + cell := MustCoordinatesToCellName(col, row) + xlsx.SetCellStr(sheet, cell, cell) + } + } +} diff --git a/lib.go b/lib.go index 30a20e03a0..c33c934bde 100644 --- a/lib.go +++ b/lib.go @@ -12,11 +12,11 @@ package excelize import ( "archive/zip" "bytes" + "fmt" "io" "log" "strconv" "strings" - "unicode" ) // ReadZipReader can be used to read an XLSX in memory without touching the @@ -64,116 +64,188 @@ func readFile(file *zip.File) []byte { return buff.Bytes() } -// ToAlphaString provides a function to convert integer to Excel sheet column -// title. For example convert 36 to column title AK: +// SplitCellName splits cell name to column name and row number. // -// excelize.ToAlphaString(36) +// Example: // -func ToAlphaString(value int) string { - if value < 0 { - return "" +// 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') + } + + if strings.IndexFunc(cell, alpha) == 0 { + i := strings.LastIndexFunc(cell, alpha) + if i >= 0 && i < len(cell)-1 { + col, rowstr := cell[:i+1], cell[i+1:] + if row, err := strconv.Atoi(rowstr); err == nil && row > 0 { + return col, row, nil + } + } + } + return "", -1, newInvalidCellNameError(cell) +} + +// JoinCellName joins cell name from column name and row number +func JoinCellName(col string, row int) (string, error) { + normCol := strings.Map(func(rune rune) rune { + switch { + case 'A' <= rune && rune <= 'Z': + return rune + case 'a' <= rune && rune <= 'z': + return rune - 32 + } + return -1 + }, col) + if len(col) == 0 || len(col) != len(normCol) { + return "", newInvalidColumnNameError(col) } - var ans string - i := value + 1 - for i > 0 { - ans = string((i-1)%26+65) + ans - i = (i - 1) / 26 + if row < 1 { + return "", newInvalidRowNumberError(row) } - return ans + return fmt.Sprintf("%s%d", normCol, row), nil } -// TitleToNumber provides a function to convert Excel sheet column title to -// int (this function doesn't do value check currently). For example convert -// AK and ak to column title 36: +// ColumnNameToNumber provides a function to convert Excel sheet +// column name to int. Column name case insencitive +// Function returns error if column name incorrect. // -// excelize.TitleToNumber("AK") -// excelize.TitleToNumber("ak") +// Example: // -func TitleToNumber(s string) int { - weight := 1 - sum := 0 - for i := len(s) - 1; i >= 0; i-- { - ch := s[i] - if ch >= 'a' && ch <= 'z' { - ch -= 32 +// excelize.ColumnNameToNumber("AK") // returns 37, nil +// +func ColumnNameToNumber(name string) (int, error) { + if len(name) == 0 { + return -1, newInvalidColumnNameError(name) + } + col := 0 + multi := 1 + for i := len(name) - 1; i >= 0; i-- { + r := name[i] + if r >= 'A' && r <= 'Z' { + col += int(r-'A'+1) * multi + } else if r >= 'a' && r <= 'z' { + col += int(r-'a'+1) * multi + } else { + return -1, newInvalidColumnNameError(name) } - sum += int(ch-'A'+1) * weight - weight *= 26 + multi *= 26 } - return sum - 1 + return col, nil } -// letterOnlyMapF is used in conjunction with strings.Map to return only the -// characters A-Z and a-z in a string. -func letterOnlyMapF(rune rune) rune { - switch { - case 'A' <= rune && rune <= 'Z': - return rune - case 'a' <= rune && rune <= 'z': - return rune - 32 +// MustColumnNameToNumber provides a function to convert Excel sheet column +// name to int. Column name case insencitive. +// Function returns error if column name incorrect. +// +// Example: +// +// excelize.MustColumnNameToNumber("AK") // returns 37 +// +func MustColumnNameToNumber(name string) int { + n, err := ColumnNameToNumber(name) + if err != nil { + panic(err) } - return -1 + return n } -// intOnlyMapF is used in conjunction with strings.Map to return only the -// numeric portions of a string. -func intOnlyMapF(rune rune) rune { - if rune >= 48 && rune < 58 { - return rune +// ColumnNumberToName provides a function to convert integer +// to Excel sheet column title. +// +// Example: +// +// excelize.ToAlphaString(37) // returns "AK", nil +// +func ColumnNumberToName(num int) (string, error) { + if num < 1 { + return "", fmt.Errorf("incorrect column number %d", num) } - return -1 + var col string + for num > 0 { + col = string((num-1)%26+65) + col + num = (num - 1) / 26 + } + return col, nil } -// boolPtr returns a pointer to a bool with the given value. -func boolPtr(b bool) *bool { return &b } +// CellNameToCoordinates converts alpha-numeric cell name +// to [X, Y] coordinates or retrusn an error. +// +// Example: +// CellCoordinates("A1") // returns 1, 1, nil +// CellCoordinates("Z3") // returns 26, 3, nil +// +func CellNameToCoordinates(cell string) (int, int, error) { + const msg = "cannot convert cell %q to coordinates: %v" -// defaultTrue returns true if b is nil, or the pointed value. -func defaultTrue(b *bool) bool { - if b == nil { - return true + colname, row, err := SplitCellName(cell) + if err != nil { + return -1, -1, fmt.Errorf(msg, cell, err) } - return *b + + col, err := ColumnNameToNumber(colname) + if err != nil { + return -1, -1, fmt.Errorf(msg, cell, err) + } + + return col, row, nil } -// axisLowerOrEqualThan returns true if axis1 <= axis2 axis1/axis2 can be -// either a column or a row axis, e.g. "A", "AAE", "42", "1", etc. +// MustCellNameToCoordinates converts alpha-numeric cell name +// to [X, Y] coordinates or panics. // -// For instance, the following comparisons are all true: +// Example: +// MustCellNameToCoordinates("A1") // returns 1, 1 +// MustCellNameToCoordinates("Z3") // returns 26, 3 // -// "A" <= "B" -// "A" <= "AA" -// "B" <= "AA" -// "BC" <= "ABCD" (in a XLSX sheet, the BC col comes before the ABCD col) -// "1" <= "2" -// "2" <= "11" (in a XLSX sheet, the row 2 comes before the row 11) -// and so on -func axisLowerOrEqualThan(axis1, axis2 string) bool { - if len(axis1) < len(axis2) { - return true - } else if len(axis1) > len(axis2) { - return false - } else { - return axis1 <= axis2 +func MustCellNameToCoordinates(cell string) (int, int) { + c, r, err := CellNameToCoordinates(cell) + if err != nil { + panic(err) } + return c, r } -// getCellColRow returns the two parts of a cell identifier (its col and row) -// as strings +// CoordinatesToCellName converts [X, Y] coordinates to alpha-numeric cell name or returns an error. // -// For instance: +// Example: +// CoordinatesToCellName(1, 1) // returns "A1", nil // -// "C220" => "C", "220" -// "aaef42" => "aaef", "42" -// "" => "", "" -func getCellColRow(cell string) (col, row string) { - for index, rune := range cell { - if unicode.IsDigit(rune) { - return cell[:index], cell[index:] - } +func CoordinatesToCellName(col, row int) (string, error) { + if col < 1 || row < 1 { + return "", fmt.Errorf("invalid cell coordinates [%d, %d]", col, row) + } + colname, err := ColumnNumberToName(col) + if err != nil { + return "", fmt.Errorf("invalid cell coordinates [%d, %d]: %v", col, row, err) + } + return fmt.Sprintf("%s%d", colname, row), nil +} +// MustCoordinatesToCellName converts [X, Y] coordinates to alpha-numeric cell name or panics. +// +// Example: +// MustCoordinatesToCellName(1, 1) // returns "A1" +// +func MustCoordinatesToCellName(col, row int) string { + n, err := CoordinatesToCellName(col, row) + if err != nil { + panic(err) } + return n +} + +// boolPtr returns a pointer to a bool with the given value. +func boolPtr(b bool) *bool { return &b } - return cell, "" +// defaultTrue returns true if b is nil, or the pointed value. +func defaultTrue(b *bool) bool { + if b == nil { + return true + } + return *b } // parseFormatSet provides a method to convert format string to []byte and @@ -208,7 +280,9 @@ func namespaceStrictToTransitional(content []byte) []byte { // is great, numerous passwords will match the same hash. Here is the // algorithm to create the hash value: // -// take the ASCII values of all characters shift left the first character 1 bit, the second 2 bits and so on (use only the lower 15 bits and rotate all higher bits, the highest bit of the 16-bit value is always 0 [signed short]) +// take the ASCII values of all characters shift left the first character 1 bit, +// the second 2 bits and so on (use only the lower 15 bits and rotate all higher bits, +// the highest bit of the 16-bit value is always 0 [signed short]) // XOR all these values // XOR the count of characters // XOR the constant 0xCE4B diff --git a/lib_test.go b/lib_test.go index ef0d8f5b31..4c19f7344c 100644 --- a/lib_test.go +++ b/lib_test.go @@ -2,59 +2,213 @@ package excelize import ( "fmt" + "strconv" + "strings" "testing" "github.com/stretchr/testify/assert" ) -func TestAxisLowerOrEqualThanIsTrue(t *testing.T) { - trueExpectedInputList := [][2]string{ - {"A", "B"}, - {"A", "AA"}, - {"B", "AA"}, - {"BC", "ABCD"}, - {"1", "2"}, - {"2", "11"}, +var validColumns = []struct { + Name string + Num int +}{ + {Name: "A", Num: 1}, + {Name: "Z", Num: 26}, + {Name: "AA", Num: 26 + 1}, + {Name: "AK", Num: 26 + 11}, + {Name: "ak", Num: 26 + 11}, + {Name: "Ak", Num: 26 + 11}, + {Name: "aK", Num: 26 + 11}, + {Name: "AZ", Num: 26 + 26}, + {Name: "ZZ", Num: 26 + 26*26}, + {Name: "AAA", Num: 26 + 26*26 + 1}, + {Name: "ZZZ", Num: 26 + 26*26 + 26*26*26}, +} + +var invalidColumns = []struct { + Name string + Num int +}{ + {Name: "", Num: -1}, + {Name: " ", Num: -1}, + {Name: "_", Num: -1}, + {Name: "__", Num: -1}, + {Name: "-1", Num: -1}, + {Name: "0", Num: -1}, + {Name: " A", Num: -1}, + {Name: "A ", Num: -1}, + {Name: "A1", Num: -1}, + {Name: "1A", Num: -1}, + {Name: " a", Num: -1}, + {Name: "a ", Num: -1}, + {Name: "a1", Num: -1}, + {Name: "1a", Num: -1}, + {Name: " _", Num: -1}, + {Name: "_ ", Num: -1}, + {Name: "_1", Num: -1}, + {Name: "1_", Num: -1}, +} + +var invalidCells = []string{"", "A", "AA", " A", "A ", "1A", "A1A", "A1 ", " A1", "1A1", "a-1", "A-1"} + +var invalidIndexes = []int{-100, -2, -1, 0} + +func TestColumnNameToNumber_OK(t *testing.T) { + const msg = "Column %q" + for _, col := range validColumns { + out, err := ColumnNameToNumber(col.Name) + if assert.NoErrorf(t, err, msg, col.Name) { + assert.Equalf(t, col.Num, out, msg, col.Name) + } } +} + +func TestColumnNameToNumber_Error(t *testing.T) { + const msg = "Column %q" + for _, col := range invalidColumns { + out, err := ColumnNameToNumber(col.Name) + if assert.Errorf(t, err, msg, col.Name) { + assert.Equalf(t, col.Num, out, msg, col.Name) + } + assert.Panicsf(t, func() { + MustColumnNameToNumber(col.Name) + }, msg, col.Name) + } +} - for i, trueExpectedInput := range trueExpectedInputList { - t.Run(fmt.Sprintf("TestData%d", i), func(t *testing.T) { - assert.True(t, axisLowerOrEqualThan(trueExpectedInput[0], trueExpectedInput[1])) - }) +func TestColumnNumberToName_OK(t *testing.T) { + const msg = "Column %q" + for _, col := range validColumns { + out, err := ColumnNumberToName(col.Num) + if assert.NoErrorf(t, err, msg, col.Name) { + assert.Equalf(t, strings.ToUpper(col.Name), out, msg, col.Name) + } } } -func TestAxisLowerOrEqualThanIsFalse(t *testing.T) { - falseExpectedInputList := [][2]string{ - {"B", "A"}, - {"AA", "A"}, - {"AA", "B"}, - {"ABCD", "AB"}, - {"2", "1"}, - {"11", "2"}, +func TestColumnNumberToName_Error(t *testing.T) { + out, err := ColumnNumberToName(-1) + if assert.Error(t, err) { + assert.Equal(t, "", out) } - for i, falseExpectedInput := range falseExpectedInputList { - t.Run(fmt.Sprintf("TestData%d", i), func(t *testing.T) { - assert.False(t, axisLowerOrEqualThan(falseExpectedInput[0], falseExpectedInput[1])) - }) + out, err = ColumnNumberToName(0) + if assert.Error(t, err) { + assert.Equal(t, "", out) } } -func TestGetCellColRow(t *testing.T) { - cellExpectedColRowList := [][3]string{ - {"C220", "C", "220"}, - {"aaef42", "aaef", "42"}, - {"bonjour", "bonjour", ""}, - {"59", "", "59"}, - {"", "", ""}, +func TestSplitCellName_OK(t *testing.T) { + const msg = "Cell \"%s%d\"" + for i, col := range validColumns { + row := i + 1 + c, r, err := SplitCellName(col.Name + strconv.Itoa(row)) + if assert.NoErrorf(t, err, msg, col.Name, row) { + assert.Equalf(t, col.Name, c, msg, col.Name, row) + assert.Equalf(t, row, r, msg, col.Name, row) + } + } +} + +func TestSplitCellName_Error(t *testing.T) { + const msg = "Cell %q" + for _, cell := range invalidCells { + c, r, err := SplitCellName(cell) + if assert.Errorf(t, err, msg, cell) { + assert.Equalf(t, "", c, msg, cell) + assert.Equalf(t, -1, r, msg, cell) + } + } +} + +func TestJoinCellName_OK(t *testing.T) { + const msg = "Cell \"%s%d\"" + + for i, col := range validColumns { + row := i + 1 + cell, err := JoinCellName(col.Name, row) + if assert.NoErrorf(t, err, msg, col.Name, row) { + assert.Equalf(t, strings.ToUpper(fmt.Sprintf("%s%d", col.Name, row)), cell, msg, row) + } + } +} + +func TestJoinCellName_Error(t *testing.T) { + const msg = "Cell \"%s%d\"" + + test := func(col string, row int) { + cell, err := JoinCellName(col, row) + if assert.Errorf(t, err, msg, col, row) { + assert.Equalf(t, "", cell, msg, col, row) + } + } + + for _, col := range invalidColumns { + test(col.Name, 1) + for _, row := range invalidIndexes { + test("A", row) + test(col.Name, row) + } + } + +} + +func TestCellNameToCoordinates_OK(t *testing.T) { + const msg = "Cell \"%s%d\"" + for i, col := range validColumns { + row := i + 1 + c, r, err := CellNameToCoordinates(col.Name + strconv.Itoa(row)) + if assert.NoErrorf(t, err, msg, col.Name, row) { + assert.Equalf(t, col.Num, c, msg, col.Name, row) + assert.Equalf(t, i+1, r, msg, col.Name, row) + } + } +} + +func TestCellNameToCoordinates_Error(t *testing.T) { + const msg = "Cell %q" + for _, cell := range invalidCells { + c, r, err := CellNameToCoordinates(cell) + if assert.Errorf(t, err, msg, cell) { + assert.Equalf(t, -1, c, msg, cell) + assert.Equalf(t, -1, r, msg, cell) + } + assert.Panicsf(t, func() { + MustCellNameToCoordinates(cell) + }, msg, cell) + } +} + +func TestCoordinatesToCellName_OK(t *testing.T) { + const msg = "Coordinates [%d, %d]" + for i, col := range validColumns { + row := i + 1 + cell, err := CoordinatesToCellName(col.Num, row) + if assert.NoErrorf(t, err, msg, col.Num, row) { + assert.Equalf(t, strings.ToUpper(col.Name+strconv.Itoa(row)), cell, msg, col.Num, row) + } + } +} + +func TestCoordinatesToCellName_Error(t *testing.T) { + const msg = "Coordinates [%d, %d]" + + test := func(col, row int) { + cell, err := CoordinatesToCellName(col, row) + if assert.Errorf(t, err, msg, col, row) { + assert.Equalf(t, "", cell, msg, col, row) + } + assert.Panicsf(t, func() { + MustCoordinatesToCellName(col, row) + }, msg, col, row) } - for i, test := range cellExpectedColRowList { - t.Run(fmt.Sprintf("TestData%d", i), func(t *testing.T) { - col, row := getCellColRow(test[0]) - assert.Equal(t, test[1], col, "Unexpected col") - assert.Equal(t, test[2], row, "Unexpected row") - }) + for _, col := range invalidIndexes { + test(col, 1) + for _, row := range invalidIndexes { + test(1, row) + test(col, row) + } } } diff --git a/picture.go b/picture.go index 131b15ce7d..f3463aaf77 100644 --- a/picture.go +++ b/picture.go @@ -143,7 +143,7 @@ func (f *File) AddPictureFromBytes(sheet, cell, format, name, extension string, if err != nil { return err } - image, _, err := image.DecodeConfig(bytes.NewReader(file)) + img, _, err := image.DecodeConfig(bytes.NewReader(file)) if err != nil { return err } @@ -162,7 +162,7 @@ func (f *File) AddPictureFromBytes(sheet, cell, format, name, extension string, } drawingHyperlinkRID = f.addDrawingRelationships(drawingID, SourceRelationshipHyperLink, formatSet.Hyperlink, hyperlinkType) } - f.addDrawingPicture(sheet, drawingXML, cell, name, image.Width, image.Height, drawingRID, drawingHyperlinkRID, formatSet) + f.addDrawingPicture(sheet, drawingXML, cell, name, img.Width, img.Height, drawingRID, drawingHyperlinkRID, formatSet) f.addMedia(file, ext) f.addContentTypePart(drawingID, "drawings") return err @@ -263,14 +263,11 @@ func (f *File) countDrawings() int { // 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) { - cell = strings.ToUpper(cell) - fromCol := string(strings.Map(letterOnlyMapF, cell)) - fromRow, _ := strconv.Atoi(strings.Map(intOnlyMapF, cell)) - row := fromRow - 1 - col := TitleToNumber(fromCol) + col, row := MustCellNameToCoordinates(cell) width = int(float64(width) * formatSet.XScale) height = int(float64(height) * formatSet.YScale) - 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, formatSet.OffsetX, formatSet.OffsetY, width, height) content, cNvPrID := f.drawingParser(drawingXML) twoCellAnchor := xdrCellAnchor{} twoCellAnchor.EditAs = formatSet.Positioning @@ -471,30 +468,36 @@ func (f *File) getSheetRelationshipsTargetByID(sheet, rID string) string { // } // func (f *File) GetPicture(sheet, cell string) (string, []byte) { + col, row := MustCellNameToCoordinates(cell) + xlsx := f.workSheetReader(sheet) if xlsx.Drawing == nil { return "", []byte{} } + target := f.getSheetRelationshipsTargetByID(sheet, xlsx.Drawing.RID) drawingXML := strings.Replace(target, "..", "xl", -1) - cell = strings.ToUpper(cell) - fromCol := string(strings.Map(letterOnlyMapF, cell)) - fromRow, _ := strconv.Atoi(strings.Map(intOnlyMapF, cell)) - row := fromRow - 1 - col := TitleToNumber(fromCol) - drawingRelationships := strings.Replace(strings.Replace(target, "../drawings", "xl/drawings/_rels", -1), ".xml", ".xml.rels", -1) + + drawingRelationships := strings.Replace( + strings.Replace(target, "../drawings", "xl/drawings/_rels", -1), ".xml", ".xml.rels", -1) + wsDr, _ := f.drawingParser(drawingXML) + for _, anchor := range wsDr.TwoCellAnchor { if anchor.From != nil && anchor.Pic != nil { if anchor.From.Col == col && anchor.From.Row == row { - xlsxWorkbookRelation := f.getDrawingRelationships(drawingRelationships, anchor.Pic.BlipFill.Blip.Embed) + xlsxWorkbookRelation := f.getDrawingRelationships(drawingRelationships, + anchor.Pic.BlipFill.Blip.Embed) _, ok := supportImageTypes[filepath.Ext(xlsxWorkbookRelation.Target)] if ok { - return filepath.Base(xlsxWorkbookRelation.Target), []byte(f.XLSX[strings.Replace(xlsxWorkbookRelation.Target, "..", "xl", -1)]) + return filepath.Base(xlsxWorkbookRelation.Target), + []byte(f.XLSX[strings.Replace(xlsxWorkbookRelation.Target, + "..", "xl", -1)]) } } } } + _, ok := f.XLSX[drawingXML] if !ok { return "", nil diff --git a/rows.go b/rows.go index 9575876baf..8b4f8eca0a 100644 --- a/rows.go +++ b/rows.go @@ -16,7 +16,6 @@ import ( "io" "math" "strconv" - "strings" ) // GetRows return all the rows in a sheet by given worksheet name (case @@ -30,24 +29,30 @@ import ( // } // func (f *File) GetRows(sheet string) [][]string { - xlsx := f.workSheetReader(sheet) name, ok := f.sheetMap[trimSheetName(sheet)] if !ok { - return [][]string{} + return nil } + + xlsx := f.workSheetReader(sheet) if xlsx != nil { output, _ := xml.Marshal(f.Sheet[name]) f.saveFileList(name, replaceWorkSheetsRelationshipsNameSpaceBytes(output)) } + xml.NewDecoder(bytes.NewReader(f.readXML(name))) d := f.sharedStringsReader() - var inElement string - var r xlsxRow - tr, tc := f.getTotalRowsCols(name) - rows := make([][]string, tr) + var ( + inElement string + rowData xlsxRow + ) + + rowCount, colCount := f.getTotalRowsCols(name) + rows := make([][]string, rowCount) for i := range rows { - rows[i] = make([]string, tc+1) + rows[i] = make([]string, colCount+1) } + var row int decoder := xml.NewDecoder(bytes.NewReader(f.readXML(name))) for { @@ -59,15 +64,15 @@ func (f *File) GetRows(sheet string) [][]string { case xml.StartElement: inElement = startElement.Name.Local if inElement == "row" { - r = xlsxRow{} - _ = decoder.DecodeElement(&r, &startElement) - cr := r.R - 1 - for _, colCell := range r.C { - c := TitleToNumber(strings.Map(letterOnlyMapF, colCell.R)) + rowData = xlsxRow{} + _ = decoder.DecodeElement(&rowData, &startElement) + cr := rowData.R - 1 + for _, colCell := range rowData.C { + col, _ := MustCellNameToCoordinates(colCell.R) val, _ := colCell.getValueFrom(f, d) - rows[cr][c] = val + rows[cr][col-1] = val if val != "" { - row = r.R + row = rowData.R } } } @@ -120,13 +125,13 @@ func (rows *Rows) Columns() []string { r := xlsxRow{} _ = rows.decoder.DecodeElement(&r, &startElement) d := rows.f.sharedStringsReader() - row := make([]string, len(r.C)) + columns := make([]string, len(r.C)) for _, colCell := range r.C { - c := TitleToNumber(strings.Map(letterOnlyMapF, colCell.R)) + col, _ := MustCellNameToCoordinates(colCell.R) val, _ := colCell.getValueFrom(rows.f, d) - row[c] = val + columns[col-1] = val } - return row + return columns } // ErrSheetNotExist defines an error of sheet is not exist @@ -184,7 +189,7 @@ func (f *File) getTotalRowsCols(name string) (int, int) { _ = decoder.DecodeElement(&r, &startElement) tr = r.R for _, colCell := range r.C { - col := TitleToNumber(strings.Map(letterOnlyMapF, colCell.R)) + col, _ := MustCellNameToCoordinates(colCell.R) if col > tc { tc = col } @@ -202,13 +207,15 @@ func (f *File) getTotalRowsCols(name string) (int, int) { // xlsx.SetRowHeight("Sheet1", 1, 50) // func (f *File) SetRowHeight(sheet string, row int, height float64) { - xlsx := f.workSheetReader(sheet) if row < 1 { - return + panic(newInvalidRowNumberError(row)) // Fail fats to avoid possible future side effects! } - cells := 0 + + xlsx := f.workSheetReader(sheet) + + prepareSheetXML(xlsx, 0, row) + rowIdx := row - 1 - completeRow(xlsx, row, cells) xlsx.SheetData.Row[rowIdx].Ht = height xlsx.SheetData.Row[rowIdx].CustomHeight = true } @@ -232,8 +239,12 @@ func (f *File) getRowHeight(sheet string, row int) int { // xlsx.GetRowHeight("Sheet1", 1) // func (f *File) GetRowHeight(sheet string, row int) float64 { + if row < 1 { + panic(newInvalidRowNumberError(row)) // Fail fats to avoid possible future side effects! + } + xlsx := f.workSheetReader(sheet) - if row < 1 || row > len(xlsx.SheetData.Row) { + if row > len(xlsx.SheetData.Row) { return defaultRowHeightPixels // it will be better to use 0, but we take care with BC } for _, v := range xlsx.SheetData.Row { @@ -291,18 +302,13 @@ func (xlsx *xlsxC) getValueFrom(f *File, d *xlsxSST) (string, error) { // xlsx.SetRowVisible("Sheet1", 2, false) // func (f *File) SetRowVisible(sheet string, row int, visible bool) { - xlsx := f.workSheetReader(sheet) if row < 1 { - return - } - cells := 0 - completeRow(xlsx, row, cells) - rowIdx := row - 1 - if visible { - xlsx.SheetData.Row[rowIdx].Hidden = false - return + panic(newInvalidRowNumberError(row)) // Fail fats to avoid possible future side effects! } - xlsx.SheetData.Row[rowIdx].Hidden = true + + xlsx := f.workSheetReader(sheet) + prepareSheetXML(xlsx, 0, row) + xlsx.SheetData.Row[row-1].Hidden = !visible } // GetRowVisible provides a function to get visible of a single row by given @@ -312,14 +318,15 @@ func (f *File) SetRowVisible(sheet string, row int, visible bool) { // xlsx.GetRowVisible("Sheet1", 2) // func (f *File) GetRowVisible(sheet string, row int) bool { + if row < 1 { + panic(newInvalidRowNumberError(row)) // Fail fats to avoid possible future side effects! + } + xlsx := f.workSheetReader(sheet) - if row < 1 || row > len(xlsx.SheetData.Row) { + if row > len(xlsx.SheetData.Row) { return false } - rowIndex := row - 1 - cells := 0 - completeRow(xlsx, row, cells) - return !xlsx.SheetData.Row[rowIndex].Hidden + return !xlsx.SheetData.Row[row-1].Hidden } // SetRowOutlineLevel provides a function to set outline level number of a @@ -329,12 +336,11 @@ func (f *File) GetRowVisible(sheet string, row int) bool { // xlsx.SetRowOutlineLevel("Sheet1", 2, 1) // func (f *File) SetRowOutlineLevel(sheet string, row int, level uint8) { - xlsx := f.workSheetReader(sheet) if row < 1 { - return + panic(newInvalidRowNumberError(row)) // Fail fats to avoid possible future side effects! } - cells := 0 - completeRow(xlsx, row, cells) + xlsx := f.workSheetReader(sheet) + prepareSheetXML(xlsx, 0, row) xlsx.SheetData.Row[row-1].OutlineLevel = level } @@ -345,8 +351,11 @@ func (f *File) SetRowOutlineLevel(sheet string, row int, level uint8) { // xlsx.GetRowOutlineLevel("Sheet1", 2) // func (f *File) GetRowOutlineLevel(sheet string, row int) uint8 { + if row < 1 { + panic(newInvalidRowNumberError(row)) // Fail fats to avoid possible future side effects! + } xlsx := f.workSheetReader(sheet) - if row < 1 || row > len(xlsx.SheetData.Row) { + if row > len(xlsx.SheetData.Row) { return 0 } return xlsx.SheetData.Row[row-1].OutlineLevel @@ -362,14 +371,18 @@ func (f *File) GetRowOutlineLevel(sheet string, row int) uint8 { // worksheet, it will cause a file error when you open it. The excelize only // partially updates these references currently. func (f *File) RemoveRow(sheet string, row int) { + if row < 1 { + panic(newInvalidRowNumberError(row)) // Fail fats to avoid possible future side effects! + } + xlsx := f.workSheetReader(sheet) - if row < 1 || row > len(xlsx.SheetData.Row) { + if row > len(xlsx.SheetData.Row) { return } for i, r := range xlsx.SheetData.Row { if r.R == row { xlsx.SheetData.Row = append(xlsx.SheetData.Row[:i], xlsx.SheetData.Row[i+1:]...) - f.adjustHelper(sheet, -1, row, -1) + f.adjustHelper(sheet, rows, row, -1) return } } @@ -383,9 +396,9 @@ func (f *File) RemoveRow(sheet string, row int) { // func (f *File) InsertRow(sheet string, row int) { if row < 1 { - return + panic(newInvalidRowNumberError(row)) // Fail fats to avoid possible future side effects! } - f.adjustHelper(sheet, -1, row, 1) + f.adjustHelper(sheet, rows, row, 1) } // DuplicateRow inserts a copy of specified row (by it Excel row number) below @@ -410,9 +423,12 @@ func (f *File) DuplicateRow(sheet string, row int) { // worksheet, it will cause a file error when you open it. The excelize only // partially updates these references currently. func (f *File) DuplicateRowTo(sheet string, row, row2 int) { - xlsx := f.workSheetReader(sheet) + if row < 1 { + panic(newInvalidRowNumberError(row)) // Fail fats to avoid possible future side effects! + } - if row < 1 || row > len(xlsx.SheetData.Row) || row2 < 1 || row == row2 { + xlsx := f.workSheetReader(sheet) + if row > len(xlsx.SheetData.Row) || row2 < 1 || row == row2 { return } @@ -430,7 +446,7 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) { return } - f.adjustHelper(sheet, -1, row2, 1) + f.adjustHelper(sheet, rows, row2, 1) idx2 := -1 for i, r := range xlsx.SheetData.Row { @@ -478,62 +494,31 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) { // Noteice: this method could be very slow for large spreadsheets (more than // 3000 rows one sheet). func checkRow(xlsx *xlsxWorksheet) { - buffer := bytes.Buffer{} - for k := range xlsx.SheetData.Row { - lenCol := len(xlsx.SheetData.Row[k].C) - if lenCol > 0 { - endR := string(strings.Map(letterOnlyMapF, xlsx.SheetData.Row[k].C[lenCol-1].R)) - endRow, _ := strconv.Atoi(strings.Map(intOnlyMapF, xlsx.SheetData.Row[k].C[lenCol-1].R)) - endCol := TitleToNumber(endR) + 1 - if lenCol < endCol { - oldRow := xlsx.SheetData.Row[k].C - xlsx.SheetData.Row[k].C = xlsx.SheetData.Row[k].C[:0] - var tmp []xlsxC - for i := 0; i < endCol; i++ { - buffer.WriteString(ToAlphaString(i)) - buffer.WriteString(strconv.Itoa(endRow)) - tmp = append(tmp, xlsxC{ - R: buffer.String(), - }) - buffer.Reset() - } - xlsx.SheetData.Row[k].C = tmp - for _, y := range oldRow { - colAxis := TitleToNumber(string(strings.Map(letterOnlyMapF, y.R))) - xlsx.SheetData.Row[k].C[colAxis] = y - } - } - } - } -} + for rowIdx := range xlsx.SheetData.Row { + rowData := &xlsx.SheetData.Row[rowIdx] -// completeRow provides a function to check and fill each column element for a -// single row and make that is continuous in a worksheet of XML by given row -// index and axis. -func completeRow(xlsx *xlsxWorksheet, row, cell int) { - currentRows := len(xlsx.SheetData.Row) - if currentRows > 1 { - lastRow := xlsx.SheetData.Row[currentRows-1].R - if lastRow >= row { - row = lastRow + colCount := len(rowData.C) + if colCount == 0 { + continue } - } - for i := currentRows; i < row; i++ { - xlsx.SheetData.Row = append(xlsx.SheetData.Row, xlsxRow{ - R: i + 1, - }) - } - buffer := bytes.Buffer{} - for ii := currentRows; ii < row; ii++ { - start := len(xlsx.SheetData.Row[ii].C) - if start == 0 { - for iii := start; iii < cell; iii++ { - buffer.WriteString(ToAlphaString(iii)) - buffer.WriteString(strconv.Itoa(ii + 1)) - xlsx.SheetData.Row[ii].C = append(xlsx.SheetData.Row[ii].C, xlsxC{ - R: buffer.String(), - }) - buffer.Reset() + lastCol, _ := MustCellNameToCoordinates(rowData.C[colCount-1].R) + + if colCount < lastCol { + oldList := rowData.C + newlist := make([]xlsxC, 0, lastCol) + + rowData.C = xlsx.SheetData.Row[rowIdx].C[:0] + + for colIdx := 0; colIdx < lastCol; colIdx++ { + newlist = append(newlist, xlsxC{R: MustCoordinatesToCellName(colIdx+1, rowIdx+1)}) + } + + rowData.C = newlist + + for colIdx := range oldList { + colData := &oldList[colIdx] + colNum, _ := MustCellNameToCoordinates(colData.R) + xlsx.SheetData.Row[rowIdx].C[colNum-1] = *colData } } } diff --git a/rows_test.go b/rows_test.go index b83d37761d..50e26dd2eb 100644 --- a/rows_test.go +++ b/rows_test.go @@ -3,44 +3,38 @@ package excelize import ( "fmt" "path/filepath" - "strconv" "testing" "github.com/stretchr/testify/assert" ) func TestRows(t *testing.T) { + const sheet2 = "Sheet2" + xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if !assert.NoError(t, err) { t.FailNow() } - rows, err := xlsx.Rows("Sheet2") + rows, err := xlsx.Rows(sheet2) if !assert.NoError(t, err) { t.FailNow() } - rowStrs := make([][]string, 0) - var i = 0 + collectedRows := make([][]string, 0) for rows.Next() { - i++ - columns := rows.Columns() - rowStrs = append(rowStrs, columns) + collectedRows = append(collectedRows, trimSliceSpace(rows.Columns())) } - if !assert.NoError(t, rows.Error()) { t.FailNow() } - dstRows := xlsx.GetRows("Sheet2") - if !assert.Equal(t, len(rowStrs), len(dstRows)) { - t.FailNow() + returnedRows := xlsx.GetRows(sheet2) + for i := range returnedRows { + returnedRows[i] = trimSliceSpace(returnedRows[i]) } - - for i := 0; i < len(rowStrs); i++ { - if !assert.Equal(t, trimSliceSpace(dstRows[i]), trimSliceSpace(rowStrs[i])) { - t.FailNow() - } + if !assert.Equal(t, collectedRows, returnedRows) { + t.FailNow() } r := Rows{} @@ -60,8 +54,13 @@ func TestRowHeight(t *testing.T) { xlsx := NewFile() sheet1 := xlsx.GetSheetName(1) - xlsx.SetRowHeight(sheet1, 0, defaultRowHeightPixels+1.0) // should no effect - assert.Equal(t, defaultRowHeightPixels, xlsx.GetRowHeight("Sheet1", 0)) + assert.Panics(t, func() { + xlsx.SetRowHeight(sheet1, 0, defaultRowHeightPixels+1.0) + }) + + assert.Panics(t, func() { + xlsx.GetRowHeight("Sheet1", 0) + }) xlsx.SetRowHeight(sheet1, 1, 111.0) assert.Equal(t, 111.0, xlsx.GetRowHeight(sheet1, 1)) @@ -77,32 +76,47 @@ func TestRowHeight(t *testing.T) { convertColWidthToPixels(0) } +func TestRowVisibility(t *testing.T) { + xlsx, err := prepareTestBook1() + if !assert.NoError(t, err) { + t.FailNow() + } + + xlsx.SetRowVisible("Sheet3", 2, false) + xlsx.SetRowVisible("Sheet3", 2, true) + xlsx.GetRowVisible("Sheet3", 2) + + assert.Panics(t, func() { + xlsx.SetRowVisible("Sheet3", 0, true) + }) + + assert.Panics(t, func() { + xlsx.GetRowVisible("Sheet3", 0) + }) + + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestRowVisibility.xlsx"))) +} + func TestRemoveRow(t *testing.T) { xlsx := NewFile() sheet1 := xlsx.GetSheetName(1) r := xlsx.workSheetReader(sheet1) const ( - cellCount = 10 - rowCount = 10 + colCount = 10 + rowCount = 10 ) - for j := 1; j <= cellCount; j++ { - for i := 1; i <= rowCount; i++ { - axis := ToAlphaString(i) + strconv.Itoa(j) - xlsx.SetCellStr(sheet1, axis, axis) - } - } + fillCells(xlsx, sheet1, colCount, rowCount) + xlsx.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External") - xlsx.RemoveRow(sheet1, -1) - if !assert.Len(t, r.SheetData.Row, rowCount) { - t.FailNow() - } + assert.Panics(t, func() { + xlsx.RemoveRow(sheet1, -1) + }) - xlsx.RemoveRow(sheet1, 0) - if !assert.Len(t, r.SheetData.Row, rowCount) { - t.FailNow() - } + assert.Panics(t, func() { + xlsx.RemoveRow(sheet1, 0) + }) xlsx.RemoveRow(sheet1, 4) if !assert.Len(t, r.SheetData.Row, rowCount-1) { @@ -150,26 +164,20 @@ func TestInsertRow(t *testing.T) { r := xlsx.workSheetReader(sheet1) const ( - cellCount = 10 - rowCount = 10 + colCount = 10 + rowCount = 10 ) - for j := 1; j <= cellCount; j++ { - for i := 1; i < rowCount; i++ { - axis := ToAlphaString(i) + strconv.Itoa(j) - xlsx.SetCellStr(sheet1, axis, axis) - } - } + fillCells(xlsx, sheet1, colCount, rowCount) + xlsx.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External") - xlsx.InsertRow(sheet1, -1) - if !assert.Len(t, r.SheetData.Row, rowCount) { - t.FailNow() - } + assert.Panics(t, func() { + xlsx.InsertRow(sheet1, -1) + }) - xlsx.InsertRow(sheet1, 0) - if !assert.Len(t, r.SheetData.Row, rowCount) { - t.FailNow() - } + assert.Panics(t, func() { + xlsx.InsertRow(sheet1, 0) + }) xlsx.InsertRow(sheet1, 1) if !assert.Len(t, r.SheetData.Row, rowCount+1) { @@ -304,19 +312,24 @@ func TestDuplicateRow(t *testing.T) { t.Run("ZeroWithNoRows", func(t *testing.T) { xlsx := NewFile() - xlsx.DuplicateRow(sheet, 0) + assert.Panics(t, func() { + xlsx.DuplicateRow(sheet, 0) + }) if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.ZeroWithNoRows"))) { t.FailNow() } + assert.Equal(t, "", xlsx.GetCellValue(sheet, "A1")) assert.Equal(t, "", xlsx.GetCellValue(sheet, "B1")) assert.Equal(t, "", xlsx.GetCellValue(sheet, "A2")) assert.Equal(t, "", xlsx.GetCellValue(sheet, "B2")) + expect := map[string]string{ "A1": "", "B1": "", "A2": "", "B2": "", } + for cell, val := range expect { if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { t.FailNow() @@ -444,31 +457,19 @@ func TestDuplicateRowInvalidRownum(t *testing.T) { "B3": "B3 Value", } - testRows := []int{-2, -1} - - testRowPairs := []struct { - row1 int - row2 int - }{ - {-1, -1}, - {-1, 0}, - {-1, 1}, - {0, -1}, - {0, 0}, - {0, 1}, - {1, -1}, - {1, 1}, - {1, 0}, - } + invalidIndexes := []int{-100, -2, -1, 0} - for i, row := range testRows { - name := fmt.Sprintf("TestRow_%d", i+1) + for _, row := range invalidIndexes { + name := fmt.Sprintf("%d", row) t.Run(name, func(t *testing.T) { xlsx := NewFile() for col, val := range cells { xlsx.SetCellStr(sheet, col, val) } - xlsx.DuplicateRow(sheet, row) + + assert.Panics(t, func() { + xlsx.DuplicateRow(sheet, row) + }) for col, val := range cells { if !assert.Equal(t, val, xlsx.GetCellValue(sheet, col)) { @@ -479,22 +480,27 @@ func TestDuplicateRowInvalidRownum(t *testing.T) { }) } - for i, pair := range testRowPairs { - name := fmt.Sprintf("TestRowPair_%d", i+1) - t.Run(name, func(t *testing.T) { - xlsx := NewFile() - for col, val := range cells { - xlsx.SetCellStr(sheet, col, val) - } - xlsx.DuplicateRowTo(sheet, pair.row1, pair.row2) + for _, row1 := range invalidIndexes { + for _, row2 := range invalidIndexes { + name := fmt.Sprintf("[%d,%d]", row1, row2) + t.Run(name, func(t *testing.T) { + xlsx := NewFile() + for col, val := range cells { + xlsx.SetCellStr(sheet, col, val) + } - for col, val := range cells { - if !assert.Equal(t, val, xlsx.GetCellValue(sheet, col)) { - t.FailNow() + assert.Panics(t, func() { + xlsx.DuplicateRowTo(sheet, row1, row2) + }) + + for col, val := range cells { + if !assert.Equal(t, val, xlsx.GetCellValue(sheet, col)) { + t.FailNow() + } } - } - assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, name))) - }) + assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, name))) + }) + } } } diff --git a/shape.go b/shape.go index 3cf09d8aa5..c58038c7a2 100644 --- a/shape.go +++ b/shape.go @@ -283,15 +283,37 @@ 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) { - 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} - cell = strings.ToUpper(cell) - fromCol := string(strings.Map(letterOnlyMapF, cell)) - fromRow, _ := strconv.Atoi(strings.Map(intOnlyMapF, cell)) - row := fromRow - 1 - col := TitleToNumber(fromCol) + fromCol, fromRow := MustCellNameToCoordinates(cell) + 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(formatSet.Width) * formatSet.Format.XScale) height := int(float64(formatSet.Height) * formatSet.Format.YScale) - colStart, rowStart, _, _, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, col, row, formatSet.Format.OffsetX, formatSet.Format.OffsetY, width, height) + + colStart, rowStart, _, _, colEnd, rowEnd, x2, y2 := + f.positionObjectPixels(sheet, colIdx, rowIdx, formatSet.Format.OffsetX, formatSet.Format.OffsetY, + width, height) content, cNvPrID := f.drawingParser(drawingXML) twoCellAnchor := xdrCellAnchor{} twoCellAnchor.EditAs = formatSet.Format.Positioning diff --git a/sheet.go b/sheet.go index 26d4b4cb0d..ee96277364 100644 --- a/sheet.go +++ b/sheet.go @@ -14,7 +14,6 @@ import ( "encoding/json" "encoding/xml" "errors" - "fmt" "io/ioutil" "os" "path" @@ -100,16 +99,16 @@ func (f *File) workBookWriter() { // workSheetWriter provides a function to save xl/worksheets/sheet%d.xml after // serialize structure. func (f *File) workSheetWriter() { - for path, sheet := range f.Sheet { + for p, sheet := range f.Sheet { if sheet != nil { for k, v := range sheet.SheetData.Row { - f.Sheet[path].SheetData.Row[k].C = trimCell(v.C) + f.Sheet[p].SheetData.Row[k].C = trimCell(v.C) } output, _ := xml.Marshal(sheet) - f.saveFileList(path, replaceWorkSheetsRelationshipsNameSpaceBytes(output)) - ok := f.checked[path] + f.saveFileList(p, replaceWorkSheetsRelationshipsNameSpaceBytes(output)) + ok := f.checked[p] if ok { - f.checked[path] = false + f.checked[p] = false } } } @@ -679,7 +678,9 @@ func (f *File) SearchSheet(sheet, value string, reg ...bool) []string { regSearch = r } xlsx := f.workSheetReader(sheet) - result := []string{} + var ( + result []string + ) name, ok := f.sheetMap[trimSheetName(sheet)] if !ok { return result @@ -716,7 +717,9 @@ func (f *File) SearchSheet(sheet, value string, reg ...bool) []string { continue } } - result = append(result, fmt.Sprintf("%s%d", strings.Map(letterOnlyMapF, colCell.R), r.R)) + + cellCol, _ := MustCellNameToCoordinates(colCell.R) + result = append(result, MustCoordinatesToCellName(cellCol, r.R)) } } default: @@ -775,7 +778,7 @@ func (f *File) UnprotectSheet(sheet string) { // trimSheetName provides a function to trim invaild characters by given worksheet // name. func trimSheetName(name string) string { - r := []rune{} + var r []rune for _, v := range name { switch v { case 58, 92, 47, 63, 42, 91, 93: // replace :\/?*[] @@ -852,7 +855,7 @@ func (p *PageLayoutPaperSize) getPageLayout(ps *xlsxPageSetUp) { // // Available options: // PageLayoutOrientation(string) -// PageLayoutPaperSize(int) +// PageLayoutPaperSize(int) // // The following shows the paper size sorted by excelize index number: // @@ -1021,10 +1024,31 @@ func (f *File) workSheetRelsReader(path string) *xlsxWorkbookRels { // workSheetRelsWriter provides a function to save // xl/worksheets/_rels/sheet%d.xml.rels after serialize structure. func (f *File) workSheetRelsWriter() { - for path, r := range f.WorkSheetRels { + for p, r := range f.WorkSheetRels { if r != nil { v, _ := xml.Marshal(r) - f.saveFileList(path, v) + f.saveFileList(p, v) + } + } +} + +// fillSheetData fill missing row and cell XML data to made it continous from first cell [1, 1] to last cell [col, row] +func prepareSheetXML(xlsx *xlsxWorksheet, col int, row int) { + rowCount := len(xlsx.SheetData.Row) + if rowCount < row { + // append missing rows + for rowIdx := rowCount; rowIdx < row; rowIdx++ { + xlsx.SheetData.Row = append(xlsx.SheetData.Row, xlsxRow{R: rowIdx + 1}) + } + } + for rowIdx := range xlsx.SheetData.Row { + rowData := &xlsx.SheetData.Row[rowIdx] // take reference + cellCount := len(rowData.C) + if cellCount < col { + for colIdx := cellCount; colIdx < col; colIdx++ { + cellName, _ := CoordinatesToCellName(colIdx+1, rowIdx+1) + rowData.C = append(rowData.C, xlsxC{R: cellName}) + } } } } diff --git a/styles.go b/styles.go index 7ffc8ff17d..50b30b8680 100644 --- a/styles.go +++ b/styles.go @@ -2263,6 +2263,14 @@ func setCellXfs(style *xlsxStyleSheet, fontID, numFmtID, fillID, borderID int, a return style.CellXfs.Count - 1 } +// GetCellStyle provides a function to get cell style index by given worksheet +// name and cell coordinates. +func (f *File) GetCellStyle(sheet, axis string) int { + xlsx := f.workSheetReader(sheet) + cellData, col, _ := f.prepareCell(xlsx, sheet, axis) + return f.prepareCellStyle(xlsx, col, cellData.S) +} + // 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 @@ -2329,42 +2337,36 @@ func setCellXfs(style *xlsxStyleSheet, fontID, numFmtID, fillID, borderID int, a // xlsx.SetCellStyle("Sheet1", "H9", "H9", style) // func (f *File) SetCellStyle(sheet, hcell, vcell string, styleID int) { - hcell = strings.ToUpper(hcell) - vcell = strings.ToUpper(vcell) - - // Coordinate conversion, convert C1:B3 to 2,0,1,2. - hcol := string(strings.Map(letterOnlyMapF, hcell)) - hrow, err := strconv.Atoi(strings.Map(intOnlyMapF, hcell)) + hcol, hrow, err := CellNameToCoordinates(hcell) if err != nil { - return + panic(err) } - hyAxis := hrow - 1 - hxAxis := TitleToNumber(hcol) - vcol := string(strings.Map(letterOnlyMapF, vcell)) - vrow, err := strconv.Atoi(strings.Map(intOnlyMapF, vcell)) + vcol, vrow, err := CellNameToCoordinates(vcell) if err != nil { - return + panic(err) } - vyAxis := vrow - 1 - vxAxis := TitleToNumber(vcol) - // Correct the coordinate area, such correct C1:B3 to B1:C3. - if vxAxis < hxAxis { - vxAxis, hxAxis = hxAxis, vxAxis + // Normalize the coordinate area, such correct C1:B3 to B1:C3. + if vcol < hcol { + vcol, hcol = hcol, vcol } - if vyAxis < hyAxis { - vyAxis, hyAxis = hyAxis, vyAxis + if vrow < hrow { + vrow, hrow = hrow, vrow } - xlsx := f.workSheetReader(sheet) + hcolIdx := hcol - 1 + hrowIdx := hrow - 1 + + vcolIdx := vcol - 1 + vrowIdx := vrow - 1 - completeRow(xlsx, vyAxis+1, vxAxis+1) - completeCol(xlsx, vyAxis+1, vxAxis+1) + xlsx := f.workSheetReader(sheet) + prepareSheetXML(xlsx, vcol, vrow) - for r := hyAxis; r <= vyAxis; r++ { - for k := hxAxis; k <= vxAxis; k++ { + for r := hrowIdx; r <= vrowIdx; r++ { + for k := hcolIdx; k <= vcolIdx; k++ { xlsx.SheetData.Row[r].C[k].S = styleID } } diff --git a/table.go b/table.go index 7c7e061921..e33264bbc1 100644 --- a/table.go +++ b/table.go @@ -55,31 +55,25 @@ func (f *File) AddTable(sheet, hcell, vcell, format string) error { if err != nil { return err } - hcell = strings.ToUpper(hcell) - vcell = strings.ToUpper(vcell) // Coordinate conversion, convert C1:B3 to 2,0,1,2. - hcol := string(strings.Map(letterOnlyMapF, hcell)) - hrow, _ := strconv.Atoi(strings.Map(intOnlyMapF, hcell)) - hyAxis := hrow - 1 - hxAxis := TitleToNumber(hcol) + hcol, hrow := MustCellNameToCoordinates(hcell) + vcol, vrow := MustCellNameToCoordinates(vcell) - vcol := string(strings.Map(letterOnlyMapF, vcell)) - vrow, _ := strconv.Atoi(strings.Map(intOnlyMapF, vcell)) - vyAxis := vrow - 1 - vxAxis := TitleToNumber(vcol) - if vxAxis < hxAxis { - vxAxis, hxAxis = hxAxis, vxAxis + if vcol < hcol { + vcol, hcol = hcol, vcol } - if vyAxis < hyAxis { - vyAxis, hyAxis = hyAxis, vyAxis + + if vrow < hrow { + vrow, hrow = hrow, vrow } + tableID := f.countTables() + 1 sheetRelationshipsTableXML := "../tables/table" + strconv.Itoa(tableID) + ".xml" tableXML := strings.Replace(sheetRelationshipsTableXML, "..", "xl", -1) // Add first table for given sheet. rID := f.addSheetRelationships(sheet, SourceRelationshipTable, sheetRelationshipsTableXML, "") f.addSheetTable(sheet, rID) - f.addTable(sheet, tableXML, hxAxis, hyAxis, vxAxis, vyAxis, tableID, formatSet) + f.addTable(sheet, tableXML, hcol, hrow, vcol, vrow, tableID, formatSet) f.addContentTypePart(tableID, "table") return err } @@ -112,18 +106,23 @@ func (f *File) addSheetTable(sheet string, rID int) { // addTable provides a function to add table by given worksheet name, // coordinate area and format set. -func (f *File) addTable(sheet, tableXML string, hxAxis, hyAxis, vxAxis, vyAxis, i int, formatSet *formatTable) { +func (f *File) addTable(sheet, tableXML string, hcol, hrow, vcol, vrow, i int, formatSet *formatTable) { // Correct the minimum number of rows, the table at least two lines. - if hyAxis == vyAxis { - vyAxis++ + if hrow == vrow { + vrow++ } + // Correct table reference coordinate area, such correct C1:B3 to B1:C3. - ref := ToAlphaString(hxAxis) + strconv.Itoa(hyAxis+1) + ":" + ToAlphaString(vxAxis) + strconv.Itoa(vyAxis+1) - tableColumn := []*xlsxTableColumn{} + ref := MustCoordinatesToCellName(hcol, hrow) + ":" + MustCoordinatesToCellName(vcol, vrow) + + var ( + tableColumn []*xlsxTableColumn + ) + idx := 0 - for i := hxAxis; i <= vxAxis; i++ { + for i := hcol; i <= vcol; i++ { idx++ - cell := ToAlphaString(i) + strconv.Itoa(hyAxis+1) + cell := MustCoordinatesToCellName(i, hrow) name := f.GetCellValue(sheet, cell) if _, err := strconv.Atoi(name); err == nil { f.SetCellStr(sheet, cell, name) @@ -245,37 +244,26 @@ func parseAutoFilterSet(formatSet string) (*formatAutoFilter, error) { // Price < 2000 // func (f *File) AutoFilter(sheet, hcell, vcell, format string) error { - formatSet, _ := parseAutoFilterSet(format) + hcol, hrow := MustCellNameToCoordinates(hcell) + vcol, vrow := MustCellNameToCoordinates(vcell) - hcell = strings.ToUpper(hcell) - vcell = strings.ToUpper(vcell) - - // Coordinate conversion, convert C1:B3 to 2,0,1,2. - hcol := string(strings.Map(letterOnlyMapF, hcell)) - hrow, _ := strconv.Atoi(strings.Map(intOnlyMapF, hcell)) - hyAxis := hrow - 1 - hxAxis := TitleToNumber(hcol) - - vcol := string(strings.Map(letterOnlyMapF, vcell)) - vrow, _ := strconv.Atoi(strings.Map(intOnlyMapF, vcell)) - vyAxis := vrow - 1 - vxAxis := TitleToNumber(vcol) - - if vxAxis < hxAxis { - vxAxis, hxAxis = hxAxis, vxAxis + if vcol < hcol { + vcol, hcol = hcol, vcol } - if vyAxis < hyAxis { - vyAxis, hyAxis = hyAxis, vyAxis + if vrow < hrow { + vrow, hrow = hrow, vrow } - ref := ToAlphaString(hxAxis) + strconv.Itoa(hyAxis+1) + ":" + ToAlphaString(vxAxis) + strconv.Itoa(vyAxis+1) - refRange := vxAxis - hxAxis - return f.autoFilter(sheet, ref, refRange, hxAxis, formatSet) + + formatSet, _ := parseAutoFilterSet(format) + ref := MustCoordinatesToCellName(hcol, hrow) + ":" + MustCoordinatesToCellName(vcol, vrow) + refRange := vcol - hcol + return f.autoFilter(sheet, ref, refRange, hcol, formatSet) } // 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, hxAxis int, formatSet *formatAutoFilter) error { +func (f *File) autoFilter(sheet, ref string, refRange, col int, formatSet *formatAutoFilter) error { xlsx := f.workSheetReader(sheet) if xlsx.SheetPr != nil { xlsx.SheetPr.FilterMode = true @@ -288,11 +276,13 @@ func (f *File) autoFilter(sheet, ref string, refRange, hxAxis int, formatSet *fo if formatSet.Column == "" || formatSet.Expression == "" { return nil } - col := TitleToNumber(formatSet.Column) - offset := col - hxAxis + + fsCol := MustColumnNameToNumber(formatSet.Column) + offset := fsCol - col if offset < 0 || offset > refRange { return fmt.Errorf("incorrect index of column '%s'", formatSet.Column) } + filter.FilterColumn = &xlsxFilterColumn{ ColID: offset, } @@ -315,7 +305,7 @@ func (f *File) autoFilter(sheet, ref string, refRange, hxAxis int, formatSet *fo func (f *File) writeAutoFilter(filter *xlsxAutoFilter, exp []int, tokens []string) { if len(exp) == 1 && exp[0] == 2 { // Single equality. - filters := []*xlsxFilter{} + var filters []*xlsxFilter filters = append(filters, &xlsxFilter{Val: tokens[0]}) filter.FilterColumn.Filters = &xlsxFilters{Filter: filters} } else if len(exp) == 3 && exp[0] == 2 && exp[1] == 1 && exp[2] == 2 { From beff7b4f3c1c9a964d5f09181e1368d3a2b9a096 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 20 Mar 2019 15:13:41 +0800 Subject: [PATCH 065/957] Typo fixed and godoc updated --- README.md | 2 +- README_zh.md | 2 +- adjust.go | 13 ++++++++++--- col.go | 4 +--- errors.go | 13 ++++++++++--- excelize.png | Bin 62974 -> 27196 bytes lib.go | 2 +- logo.png | Bin 7085 -> 3005 bytes rows.go | 26 +++++++++++++------------- 9 files changed, 37 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index faa4bf63bf..7e8120282b 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ which take place in some methods in eraler versions. ### Installation -```go +```bash go get github.com/360EntSecGroup-Skylar/excelize ``` diff --git a/README_zh.md b/README_zh.md index 806a2f5881..cb0de08760 100644 --- a/README_zh.md +++ b/README_zh.md @@ -23,7 +23,7 @@ Excelize 是 Go 语言编写的用于操作 Office Excel 文档类库,基于 E ### 安装 -```go +```bash go get github.com/360EntSecGroup-Skylar/excelize ``` diff --git a/adjust.go b/adjust.go index ee2065f4d8..e69eee1b16 100644 --- a/adjust.go +++ b/adjust.go @@ -1,8 +1,15 @@ +// Copyright 2016 - 2019 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. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. + package excelize -import ( - "strings" -) +import "strings" type adjustDirection bool diff --git a/col.go b/col.go index 131af1e0c7..2362c84bd8 100644 --- a/col.go +++ b/col.go @@ -9,9 +9,7 @@ package excelize -import ( - "math" -) +import "math" // Define the default cell size and EMU unit of measurement. const ( diff --git a/errors.go b/errors.go index 4dec815920..3404c7e4fe 100644 --- a/errors.go +++ b/errors.go @@ -1,8 +1,15 @@ +// Copyright 2016 - 2019 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. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. + package excelize -import ( - "fmt" -) +import "fmt" func newInvalidColumnNameError(col string) error { return fmt.Errorf("invalid column name %q", col) diff --git a/excelize.png b/excelize.png index 5d0766ba13ab1bfc3584eaec2e9d6fc207a9d9b6..8ba520e91b975fc240f37cda9a381ced765bc954 100644 GIT binary patch literal 27196 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 aD@5Cz6BvIU`f{SHkFK_nR*lB<@c#qR7Ws_; diff --git a/lib.go b/lib.go index c33c934bde..809a16bd90 100644 --- a/lib.go +++ b/lib.go @@ -156,7 +156,7 @@ func MustColumnNameToNumber(name string) int { // // Example: // -// excelize.ToAlphaString(37) // returns "AK", nil +// excelize.ColumnNumberToName(37) // returns "AK", nil // func ColumnNumberToName(num int) (string, error) { if num < 1 { diff --git a/logo.png b/logo.png index 32235b8aeb985020036bb3f29e1aa9a3a4c39aa5..c37ac156f691607f2b1f5c645e15b367b8000dc9 100644 GIT binary patch literal 3005 zcmV;u3qtgXP)r;0TU$~=d|X{!R}f`2Nmo}*R8)3(b2vLYh^UukY-ekHd`n$hI$Tp$dVX<$ ze^zW~96>7~MKO$scTHA4BseWtjgl?9d`fkAK){A6y?RN2hh(6oY=CT2k(NJtSSoBq zEOSRLbxA~ggDG`KC1^k^sBT-PuT!3+ONfpOQ7KM)ep$G|3tlc2KqeMMDHlmA8c{GE zS2ZJGJ11#Dwx4|_N;3{QA4Gh9MaPa)rL0z~vv+V~O`oMzhHhKB#bCk9V$0QKxWR0| z%GJM-08t$R1qA|09Rgq|0%$D*RwV^YBL`3>3sWi!RxAr$E(>BY3{NQxS}hD_GYoDw z3~@OPYc&mXJ`Hv|4SGWjdp!D7|7N=hqNGcbjUKgrh7&SK-Q7;&+ zWEn~<8D}^dt793kW*W0-8%-`7Q861-G8?vQ99A?Oy>A^J936O39gR{QxosW6a~`v7 zA6YdY#&{rHI3TWUAiH!S&V3?ZIwG@iBw;)xiDe|VbtPm!CY4(zxp*g#YA3yVC%}Cu zXhA7cS1H1ODaC;)$%89xMk~#RE7FN9drdCZjWMl(F|vj++>$tegE=`lI(Ae$p=>{$ zl}AQKM}S^PkCjcczf`q)RL$L3aAR1-(pj*%TUl6Jm2+Lo*In1=W1N{|(A;C-?_{d5 zW!U9r(%)v=>1WmAXXo~4q^fDx<7wCAY2WQ?+2(57=xdLHY~Ac^;_z6CZJ4{QoS&MWtE#2Dxv#{(v9GSS(8agA zy1T@~zN?4Fz`w`T(a6Wg(Ba+H)6?F%h~eAYFE0M@&Dne z|MIy1`}+U?|L%0%Bme*aNOV$8Qvd}D4;&{fFgQt2R%d8=dzP4)rm(WIw70jpxw^c> z$H&OZ&(F`%)7IbL;p67)?(gsK@9*#L@A2~U_WJtz`u+a?{{H^{{-%}=_y7P1KS@ME zRA}DqnOlrrRTam7YwvaTIp1|Iy;`ZZ1S-Xl#$bx5#3y`Ed@zt`FcBZXV0;j?4}g_| z5mKW>;K2}$kccrNUhvVNiKYZJ5^zjV0)e(zI&G)zbZ+0}oW0NS;e20brgqx-zMA+l z>)}i0oW0lOzt{S&wa>tstjU_J$^TzuRjZ8AO4T;TXV({}Mou5;X4Us?H0fJY>Hoxl z*eJei(*se26rmHDf^SYW_SbWn{o+x7(cOAc^-;3c7Z z(Z-JX)0Y~7Xl3i;<78bcg?1J$8?Q%2YxW)6+;i)*?MMA*PJ~N~z{VSXIx;^OqZD?& zyJLfb)N0Xde80;&yeTdXB3s|Rul;q{(};^Sp5L*dw|aE@+g1I!-@mt&4-b)%Lh4VC zfBVtc(4B18tJBph8vU1NCTg+0TMkdALr0J}4O~~xeo~FJ@Qu^|Ze$}0(6TXwO}a4; z+`XDKUBN(Y`{r-$<9^vTDj?CA%83$Z;#?yKAXJThJg6zr?niyAopissGn&gw|EurXoonFH>snxEd${`1vu71Mh@@zpD2 zJOGIJKhF8#A~<}P8+{}gJCotAWM^EQxwG@c#DR%ga@XYE@n#R0+`UJKEwoy5-S;+i z%zZyxSDX3#kH!}c=HI#Tzzx4oc>F8-eSc5kW^S1cLsqiZu6<-a?wr|POQhX79JY;e z%gC>g)<5ct*>5|yy_*hM$v$NPpKP4mZ^iFv&PF5h{4F!MG&74nhg!uBPjZdM zK?kpB%Tx7|*rJ)4$^OIs+eUwPQ77FygDuX?4`{%S{9;{80UIWt3)MC@ z*t2`$jpKL{uqo65W`6s^fP+__1yKNV-)GZJRH0pI_k#%IidwK#Fa=^6x#NYwvWTL6 zXJJcVBMZ=`4MZ7uz+%fBF(P6`)C@d27PGBt_w0gp{|q8LUMTe&5G;ZegVq7$`93)( zjUy=i&#+jFK?Ey;U}ewtw4#Bo#^z@hR%Zj9+D_@;X!fW{)0H@OF z{^CR+b8j|+hCVuSp_5$6fW#Z`0#J{Fh=9@oIveuT#dt*f&YKI7+DZoO=B)}YMFkK+ z2_O)r$85+#ekudVV<0WHuOBP|rw(ww8xykIa z37uUZuAKZ$uvoww81;&EhzfKZo+z_}8t^Dtb9a3DjS2X|MqcjJu%M8`Ox@xFDk#oF zL^LoLsd~}Bwku;dWZ7*SDl-R`|J3d4c*V^5mu z)Gu*M8?DbpMR7hV)hnzUyx{=Ef4KZ3qcZ#IL-R1|#+DPVKq_S&D}t^tw&(>0CIRT=*VhVZ-tcO$

5TMpLK0RbMEOEW;6d zq>@28;A|>Ash%(>Wl5zVc0GGLmP`scRnYPMdSXf<~? z=Ss3G<95JV_tjeKTw}mA4JexNx%J(X&ptEF`Y#o|4cbFC17eSmS&Iu{@TtP%JGbwO zXa62`UTeqK+`3ToOB{~}TnwUWEiS0$gHwh2#s_uw^=DoF;}fmQHH|((9;KvcFHq_q zxaniTYvaXe;rWP!eK*;~JS>L*N9+Z)7Q)Imsg(_c2z2hbEJ|{)3(p|EuSeEnL+=1k zzy$>zqT!$rMFa$DmV9Y&5jKD*_98>NuHtSb5X#ZrmXCu4??fVNEh;$YfiP50?v0KX z+jG;{2>k4Mat@q~Z=<1<7P3D8L}Bp79w+4NlEbAWnX`X~|8AD=GJGR-Rg6_axAY?zck2F3J) zx&#Af!_}2^xJxlmEZuI)%T7o7R`UxWv`PbVo-#~|KB+H)IIMv@Z?+c^3OFJ_3<)eA z!BdxzrrAruByQZ}T`N+c00000NkvXXu0mjfQ+vO~ literal 7085 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 len(xlsx.SheetData.Row) { @@ -372,7 +372,7 @@ func (f *File) GetRowOutlineLevel(sheet string, row int) uint8 { // partially updates these references currently. func (f *File) RemoveRow(sheet string, row int) { if row < 1 { - panic(newInvalidRowNumberError(row)) // Fail fats to avoid possible future side effects! + panic(newInvalidRowNumberError(row)) // Fail fast to avoid possible future side effects! } xlsx := f.workSheetReader(sheet) @@ -396,7 +396,7 @@ func (f *File) RemoveRow(sheet string, row int) { // func (f *File) InsertRow(sheet string, row int) { if row < 1 { - panic(newInvalidRowNumberError(row)) // Fail fats to avoid possible future side effects! + panic(newInvalidRowNumberError(row)) // Fail fast to avoid possible future side effects! } f.adjustHelper(sheet, rows, row, 1) } @@ -424,7 +424,7 @@ func (f *File) DuplicateRow(sheet string, row int) { // partially updates these references currently. func (f *File) DuplicateRowTo(sheet string, row, row2 int) { if row < 1 { - panic(newInvalidRowNumberError(row)) // Fail fats to avoid possible future side effects! + panic(newInvalidRowNumberError(row)) // Fail fast to avoid possible future side effects! } xlsx := f.workSheetReader(sheet) From 40ea8eb014c200c5ed8d81918ee56b0579aca324 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 20 Mar 2019 16:52:33 +0800 Subject: [PATCH 066/957] resolve #360, fix axis parse issue when add / get pictures; typo fixed and go test updated --- excelize_test.go | 8 ++++---- picture.go | 5 ++++- sheet.go | 3 ++- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/excelize_test.go b/excelize_test.go index 694f505be7..9671130c27 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -819,8 +819,8 @@ func TestGetPicture(t *testing.T) { } file, raw := xlsx.GetPicture("Sheet1", "F21") - if !assert.NotEmpty(t, file) || !assert.NotEmpty(t, raw) || - !assert.NoError(t, ioutil.WriteFile(file, raw, 0644)) { + if !assert.NotEmpty(t, filepath.Join("test", file)) || !assert.NotEmpty(t, raw) || + !assert.NoError(t, ioutil.WriteFile(filepath.Join("test", file), raw, 0644)) { t.FailNow() } @@ -851,8 +851,8 @@ func TestGetPicture(t *testing.T) { } file, raw = xlsx.GetPicture("Sheet1", "F21") - if !assert.NotEmpty(t, file) || !assert.NotEmpty(t, raw) || - !assert.NoError(t, ioutil.WriteFile(file, raw, 0644)) { + if !assert.NotEmpty(t, filepath.Join("test", file)) || !assert.NotEmpty(t, raw) || + !assert.NoError(t, ioutil.WriteFile(filepath.Join("test", file), raw, 0644)) { t.FailNow() } diff --git a/picture.go b/picture.go index f3463aaf77..cacc453639 100644 --- a/picture.go +++ b/picture.go @@ -266,6 +266,8 @@ func (f *File) addDrawingPicture(sheet, drawingXML, cell, file string, width, he col, row := MustCellNameToCoordinates(cell) width = int(float64(width) * formatSet.XScale) height = int(float64(height) * formatSet.YScale) + col-- + row-- colStart, rowStart, _, _, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, col, row, formatSet.OffsetX, formatSet.OffsetY, width, height) content, cNvPrID := f.drawingParser(drawingXML) @@ -469,7 +471,8 @@ func (f *File) getSheetRelationshipsTargetByID(sheet, rID string) string { // func (f *File) GetPicture(sheet, cell string) (string, []byte) { col, row := MustCellNameToCoordinates(cell) - + col-- + row-- xlsx := f.workSheetReader(sheet) if xlsx.Drawing == nil { return "", []byte{} diff --git a/sheet.go b/sheet.go index ee96277364..768d0a8d9e 100644 --- a/sheet.go +++ b/sheet.go @@ -1032,7 +1032,8 @@ func (f *File) workSheetRelsWriter() { } } -// fillSheetData fill missing row and cell XML data to made it continous from first cell [1, 1] to last cell [col, row] +// fillSheetData fill missing row and cell XML data to made it continuous from +// first cell [1, 1] to last cell [col, row] func prepareSheetXML(xlsx *xlsxWorksheet, col int, row int) { rowCount := len(xlsx.SheetData.Row) if rowCount < row { From b2c12d784e94bfb14da800962c769f5a0f6f783e Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 20 Mar 2019 22:41:46 -0500 Subject: [PATCH 067/957] SetCellFloat for floats with specific precision (#361) This allows the user to set a floating point value into a cell with a specific number of places after the decimal. Closes #357 --- cell.go | 19 +++++++++++++++++-- cell_test.go | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/cell.go b/cell.go index 126b1ff59c..3a3474f7b4 100644 --- a/cell.go +++ b/cell.go @@ -91,9 +91,9 @@ func (f *File) SetCellValue(sheet, axis string, value interface{}) { case uint64: f.SetCellInt(sheet, axis, int(v)) case float32: - f.SetCellDefault(sheet, axis, strconv.FormatFloat(float64(v), 'f', -1, 32)) + f.SetCellFloat(sheet, axis, float64(v), -1, 32) case float64: - f.SetCellDefault(sheet, axis, strconv.FormatFloat(v, 'f', -1, 64)) + f.SetCellFloat(sheet, axis, v, -1, 64) case string: f.SetCellStr(sheet, axis, v) case []byte: @@ -142,6 +142,21 @@ func (f *File) SetCellBool(sheet, axis string, value bool) { } } +// SetCellFloat sets a floating point value into a cell. The prec 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 +// 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) { + xlsx := f.workSheetReader(sheet) + cellData, col, _ := f.prepareCell(xlsx, sheet, axis) + cellData.S = f.prepareCellStyle(xlsx, col, cellData.S) + cellData.T = "" + cellData.V = strconv.FormatFloat(value, 'f', prec, 64) +} + // 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) { diff --git a/cell_test.go b/cell_test.go index d388c7f2f2..ba326d945f 100644 --- a/cell_test.go +++ b/cell_test.go @@ -1,6 +1,7 @@ package excelize import ( + "fmt" "testing" "github.com/stretchr/testify/assert" @@ -39,3 +40,34 @@ func TestCheckCellInArea(t *testing.T) { checkCellInArea("AA0", "Z0:AB1") }) } + +func TestSetCellFloat(t *testing.T) { + sheet := "Sheet1" + t.Run("with no decimal", func(t *testing.T) { + f := NewFile() + f.SetCellFloat(sheet, "A1", 123.0, -1, 64) + f.SetCellFloat(sheet, "A2", 123.0, 1, 64) + assert.Equal(t, "123", f.GetCellValue(sheet, "A1"), "A1 should be 123") + assert.Equal(t, "123.0", f.GetCellValue(sheet, "A2"), "A2 should be 123.0") + }) + + t.Run("with a decimal and precision limit", func(t *testing.T) { + f := NewFile() + f.SetCellFloat(sheet, "A1", 123.42, 1, 64) + assert.Equal(t, "123.4", f.GetCellValue(sheet, "A1"), "A1 should be 123.4") + }) + + t.Run("with a decimal and no limit", func(t *testing.T) { + f := NewFile() + f.SetCellFloat(sheet, "A1", 123.42, -1, 64) + assert.Equal(t, "123.42", f.GetCellValue(sheet, "A1"), "A1 should be 123.42") + }) +} + +func ExampleFile_SetCellFloat() { + f := NewFile() + var x float64 = 3.14159265 + f.SetCellFloat("Sheet1", "A1", x, 2, 64) + fmt.Println(f.GetCellValue("Sheet1", "A1")) + // Output: 3.14 +} From 70b1a29165867643e961ceef27592349a122ab7c Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 21 Mar 2019 14:09:25 +0800 Subject: [PATCH 068/957] Use bitSize for float32 type numbers conversion, relate PR #361 --- cell.go | 16 +++++++++------- cell_test.go | 2 +- date_test.go | 4 ++-- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/cell.go b/cell.go index 3a3474f7b4..a3a1c198a6 100644 --- a/cell.go +++ b/cell.go @@ -143,18 +143,20 @@ func (f *File) SetCellBool(sheet, axis string, value bool) { } // SetCellFloat sets a floating point value into a cell. The prec 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 -// var x float32 = 1.325 -// f.SetCellFloat("Sheet1", "A1", float64(x), 2, 32) +// 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, prec, bitSize int) { xlsx := f.workSheetReader(sheet) cellData, col, _ := f.prepareCell(xlsx, sheet, axis) cellData.S = f.prepareCellStyle(xlsx, col, cellData.S) cellData.T = "" - cellData.V = strconv.FormatFloat(value, 'f', prec, 64) + cellData.V = strconv.FormatFloat(value, 'f', prec, bitSize) } // SetCellStr provides a function to set string type value of a cell. Total diff --git a/cell_test.go b/cell_test.go index ba326d945f..12efc170f5 100644 --- a/cell_test.go +++ b/cell_test.go @@ -66,7 +66,7 @@ func TestSetCellFloat(t *testing.T) { func ExampleFile_SetCellFloat() { f := NewFile() - var x float64 = 3.14159265 + var x = 3.14159265 f.SetCellFloat("Sheet1", "A1", x, 2, 64) fmt.Println(f.GetCellValue("Sheet1", "A1")) // Output: 3.14 diff --git a/date_test.go b/date_test.go index 709fb001cd..3ec0b69265 100644 --- a/date_test.go +++ b/date_test.go @@ -38,14 +38,14 @@ func TestTimeToExcelTime(t *testing.T) { } func TestTimeToExcelTime_Timezone(t *testing.T) { - msk, err := time.LoadLocation("Europe/Moscow") + location, err := time.LoadLocation("America/Los_Angeles") if !assert.NoError(t, err) { t.FailNow() } for i, test := range trueExpectedDateList { t.Run(fmt.Sprintf("TestData%d", i+1), func(t *testing.T) { assert.Panics(t, func() { - timeToExcelTime(test.GoValue.In(msk)) + timeToExcelTime(test.GoValue.In(location)) }, "Time: %s", test.GoValue.String()) }) } From 7d197c6d8963c4d7b6ba12b1f37c4bf1c9d0dade Mon Sep 17 00:00:00 2001 From: Veniamin Albaev Date: Thu, 21 Mar 2019 13:44:30 +0300 Subject: [PATCH 069/957] Fixed PR #356 regression RemoveCol() broken (#365) --- col.go | 9 +++++---- rows.go | 7 ++++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/col.go b/col.go index 2362c84bd8..7eb7831c00 100644 --- a/col.go +++ b/col.go @@ -337,11 +337,12 @@ func (f *File) RemoveCol(sheet, col string) { xlsx := f.workSheetReader(sheet) for rowIdx := range xlsx.SheetData.Row { - rowData := xlsx.SheetData.Row[rowIdx] - for colIdx, cellData := range rowData.C { - colName, _, _ := SplitCellName(cellData.R) + rowData := &xlsx.SheetData.Row[rowIdx] + for colIdx := range rowData.C { + colName, _, _ := SplitCellName(rowData.C[colIdx].R) if colName == col { - rowData.C = append(rowData.C[:colIdx], rowData.C[colIdx+1:]...) + rowData.C = append(rowData.C[:colIdx], rowData.C[colIdx+1:]...)[:len(rowData.C)-1] + break } } } diff --git a/rows.go b/rows.go index c2b493928a..54c20467f4 100644 --- a/rows.go +++ b/rows.go @@ -379,9 +379,10 @@ func (f *File) RemoveRow(sheet string, row int) { if row > len(xlsx.SheetData.Row) { return } - for i, r := range xlsx.SheetData.Row { - if r.R == row { - xlsx.SheetData.Row = append(xlsx.SheetData.Row[:i], xlsx.SheetData.Row[i+1:]...) + for rowIdx := range xlsx.SheetData.Row { + if xlsx.SheetData.Row[rowIdx].R == row { + xlsx.SheetData.Row = append(xlsx.SheetData.Row[:rowIdx], + xlsx.SheetData.Row[rowIdx+1:]...)[:len(xlsx.SheetData.Row)-1] f.adjustHelper(sheet, rows, row, -1) return } From 677a22d99497fcc24135c949ab721d80ba5aa92a Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 22 Mar 2019 14:26:43 +0800 Subject: [PATCH 070/957] resolve #366 fix image duplication --- picture.go | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/picture.go b/picture.go index cacc453639..d78bcdfb95 100644 --- a/picture.go +++ b/picture.go @@ -250,13 +250,21 @@ func (f *File) addSheetPicture(sheet string, rID int) { // countDrawings provides a function to get drawing files count storage in the // folder xl/drawings. func (f *File) countDrawings() int { - count := 0 + c1, c2 := 0, 0 for k := range f.XLSX { if strings.Contains(k, "xl/drawings/drawing") { - count++ + c1++ } } - return count + for rel := range f.Drawings { + if strings.Contains(rel, "xl/drawings/drawing") { + c2++ + } + } + if c1 < c2 { + return c2 + } + return c1 } // addDrawingPicture provides a function to add picture by given sheet, From 2874d75555102b8266477cdda2966ff37dde6b12 Mon Sep 17 00:00:00 2001 From: Michael Date: Sat, 23 Mar 2019 03:09:48 -0500 Subject: [PATCH 071/957] Add benchmark for adding images to sheet (#367) * Add benchmark for adding images to sheet This should help track performance regressions in future changes. * Only transform sheet name if necessary --- .gitignore | 3 +++ picture_test.go | 20 ++++++++++++++++++++ sheet.go | 24 +++++++++++++----------- 3 files changed, 36 insertions(+), 11 deletions(-) create mode 100644 picture_test.go diff --git a/.gitignore b/.gitignore index 29e0f2b7b9..096dbddf07 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ ~$*.xlsx test/Test*.xlsx +*.out +test/image3.png +*.test \ No newline at end of file diff --git a/picture_test.go b/picture_test.go new file mode 100644 index 0000000000..97d3cd9156 --- /dev/null +++ b/picture_test.go @@ -0,0 +1,20 @@ +package excelize + +import ( + "fmt" + _ "image/png" + "io/ioutil" + "testing" +) + +func BenchmarkAddPictureFromBytes(b *testing.B) { + f := NewFile() + imgFile, err := ioutil.ReadFile("logo.png") + if err != nil { + panic("unable to load image for benchmark") + } + b.ResetTimer() + for i := 1; i <= b.N; i++ { + f.AddPictureFromBytes("Sheet1", fmt.Sprint("A", i), "", "logo", ".png", imgFile) + } +} diff --git a/sheet.go b/sheet.go index 768d0a8d9e..ce1f24113d 100644 --- a/sheet.go +++ b/sheet.go @@ -778,18 +778,20 @@ func (f *File) UnprotectSheet(sheet string) { // trimSheetName provides a function to trim invaild characters by given worksheet // name. func trimSheetName(name string) string { - var r []rune - for _, v := range name { - switch v { - case 58, 92, 47, 63, 42, 91, 93: // replace :\/?*[] - continue - default: - r = append(r, v) + 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) - if utf8.RuneCountInString(name) > 31 { - name = string([]rune(name)[0:31]) + name = string(r) } return name } From 40ff5dc1a7d7aa42f5db9cf9dfe858cc3820b44e Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 23 Mar 2019 20:08:06 +0800 Subject: [PATCH 072/957] refactor: handler error instead of panic, Exported functions: SetCellStyle InsertCol RemoveCol RemoveRow InsertRow DuplicateRow DuplicateRowTo SetRowHeight GetRowHeight GetCellValue GetCellFormula GetCellHyperLink SetCellHyperLink SetCellInt SetCellBool SetCellFloat SetCellStr SetCellDefault GetCellStyle SetCellValue MergeCell SetSheetRow SetRowVisible GetRowVisible SetRowOutlineLevel GetRowOutlineLevel GetRows Columns SearchSheet AddTable GetPicture AutoFilter GetColVisible SetColVisible GetColOutlineLevel SetColOutlineLevel SetColWidth GetColWidth inner functions: adjustHelper adjustMergeCells adjustAutoFilter prepareCell setDefaultTimeStyle timeToExcelTime addDrawingChart addDrawingVML addDrawingPicture getTotalRowsCols checkRow addDrawingShape addTable --- .gitignore | 1 - README.md | 6 -- README_zh.md | 4 - adjust.go | 35 ++++---- cell.go | 210 +++++++++++++++++++++++++++++------------------ cell_test.go | 35 +++++--- chart.go | 13 ++- col.go | 100 +++++++++++++--------- comment.go | 15 +++- date.go | 9 +- date_test.go | 9 +- excelize.go | 12 ++- excelize_test.go | 202 +++++++++++++++++++++------------------------ lib.go | 53 ++---------- lib_test.go | 9 -- picture.go | 32 +++++--- picture_test.go | 7 +- rows.go | 157 +++++++++++++++++++++-------------- rows_test.go | 175 ++++++++++++++++++++++----------------- shape.go | 13 ++- sheet.go | 31 ++++--- styles.go | 34 ++++---- table.go | 70 ++++++++++++---- 23 files changed, 692 insertions(+), 540 deletions(-) diff --git a/.gitignore b/.gitignore index 096dbddf07..bafda04266 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ ~$*.xlsx test/Test*.xlsx *.out -test/image3.png *.test \ No newline at end of file diff --git a/README.md b/README.md index 7e8120282b..84ddde0d7d 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,6 @@ Excelize is a library written in pure Go and providing a set of functions that allow you to write to and read from XLSX files. Support reads and writes XLSX file generated by Microsoft Excel™ 2007 and later. Support save file without losing original charts of XLSX. This library needs Go version 1.8 or later. The full API docs can be seen using go's built-in documentation tool, or online at [godoc.org](https://godoc.org/github.com/360EntSecGroup-Skylar/excelize) and [docs reference](https://xuri.me/excelize/). -**WARNING!** - -From version 1.5.0 all row manipulation methods uses Excel row numbering starting with `1` instead of zero-based numbering -which take place in some methods in eraler versions. - ## Basic Usage ### Installation @@ -123,7 +118,6 @@ func main() { fmt.Println(err) } } - ``` ### Add picture to XLSX file diff --git a/README_zh.md b/README_zh.md index cb0de08760..f8ace2845e 100644 --- a/README_zh.md +++ b/README_zh.md @@ -15,10 +15,6 @@ Excelize 是 Go 语言编写的用于操作 Office Excel 文档类库,基于 ECMA-376 Office OpenXML 标准。可以使用它来读取、写入由 Microsoft Excel™ 2007 及以上版本创建的 XLSX 文档。相比较其他的开源类库,Excelize 支持写入原本带有图片(表)、透视表和切片器等复杂样式的文档,还支持向 Excel 文档中插入图片与图表,并且在保存后不会丢失文档原有样式,可以应用于各类报表系统中。使用本类库要求使用的 Go 语言为 1.8 或更高版本,完整的 API 使用文档请访问 [godoc.org](https://godoc.org/github.com/360EntSecGroup-Skylar/excelize) 或查看 [参考文档](https://xuri.me/excelize/)。 -**重要提示** - -从版本 1.5.0 开始,所有行操作方法都使用从 `1` 开始的 Excel 行编号,早期版本中某些方法中的基于 `0` 的行编号将不再使用。 - ## 快速上手 ### 安装 diff --git a/adjust.go b/adjust.go index e69eee1b16..009860b155 100644 --- a/adjust.go +++ b/adjust.go @@ -30,7 +30,7 @@ const ( // TODO: adjustCalcChain, adjustPageBreaks, adjustComments, // adjustDataValidations, adjustProtectedCells // -func (f *File) adjustHelper(sheet string, dir adjustDirection, num, offset int) { +func (f *File) adjustHelper(sheet string, dir adjustDirection, num, offset int) error { xlsx := f.workSheetReader(sheet) if dir == rows { @@ -39,11 +39,16 @@ func (f *File) adjustHelper(sheet string, dir adjustDirection, num, offset int) f.adjustColDimensions(xlsx, num, offset) } f.adjustHyperlinks(xlsx, sheet, dir, num, offset) - f.adjustMergeCells(xlsx, dir, num, offset) - f.adjustAutoFilter(xlsx, dir, num, offset) + if err := f.adjustMergeCells(xlsx, dir, num, offset); err != nil { + return err + } + if err := f.adjustAutoFilter(xlsx, dir, num, offset); err != nil { + return err + } checkSheet(xlsx) checkRow(xlsx) + return nil } // adjustColDimensions provides a function to update column dimensions when @@ -127,9 +132,9 @@ func (f *File) adjustHyperlinks(xlsx *xlsxWorksheet, sheet string, dir adjustDir // adjustAutoFilter provides a function to update the auto filter when // inserting or deleting rows or columns. -func (f *File) adjustAutoFilter(xlsx *xlsxWorksheet, dir adjustDirection, num, offset int) { +func (f *File) adjustAutoFilter(xlsx *xlsxWorksheet, dir adjustDirection, num, offset int) error { if xlsx.AutoFilter == nil { - return + return nil } rng := strings.Split(xlsx.AutoFilter.Ref, ":") @@ -138,12 +143,12 @@ func (f *File) adjustAutoFilter(xlsx *xlsxWorksheet, dir adjustDirection, num, o firstCol, firstRow, err := CellNameToCoordinates(firstCell) if err != nil { - panic(err) + return err } lastCol, lastRow, err := CellNameToCoordinates(lastCell) if err != nil { - panic(err) + return err } if (dir == rows && firstRow == num && offset < 0) || (dir == columns && firstCol == num && lastCol == num) { @@ -154,7 +159,7 @@ func (f *File) adjustAutoFilter(xlsx *xlsxWorksheet, dir adjustDirection, num, o rowData.Hidden = false } } - return + return nil } if dir == rows { @@ -171,13 +176,14 @@ func (f *File) adjustAutoFilter(xlsx *xlsxWorksheet, dir adjustDirection, num, o } xlsx.AutoFilter.Ref = firstCell + ":" + lastCell + return nil } // adjustMergeCells provides a function to update merged cells when inserting // or deleting rows or columns. -func (f *File) adjustMergeCells(xlsx *xlsxWorksheet, dir adjustDirection, num, offset int) { +func (f *File) adjustMergeCells(xlsx *xlsxWorksheet, dir adjustDirection, num, offset int) error { if xlsx.MergeCells == nil { - return + return nil } for i, areaData := range xlsx.MergeCells.Cells { @@ -187,12 +193,12 @@ func (f *File) adjustMergeCells(xlsx *xlsxWorksheet, dir adjustDirection, num, o firstCol, firstRow, err := CellNameToCoordinates(firstCell) if err != nil { - panic(err) + return err } lastCol, lastRow, err := CellNameToCoordinates(lastCell) if err != nil { - panic(err) + return err } adjust := func(v int) int { @@ -224,13 +230,14 @@ func (f *File) adjustMergeCells(xlsx *xlsxWorksheet, dir adjustDirection, num, o } if firstCell, err = CoordinatesToCellName(firstCol, firstRow); err != nil { - panic(err) + return err } if lastCell, err = CoordinatesToCellName(lastCol, lastRow); err != nil { - panic(err) + return err } areaData.Ref = firstCell + ":" + lastCell } + return nil } diff --git a/cell.go b/cell.go index a3a1c198a6..a1b6dbfe63 100644 --- a/cell.go +++ b/cell.go @@ -34,13 +34,13 @@ const ( // worksheet name and axis in XLSX 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. -func (f *File) GetCellValue(sheet, axis string) string { - return f.getCellStringFunc(sheet, axis, func(x *xlsxWorksheet, c *xlsxC) (string, bool) { +func (f *File) GetCellValue(sheet, axis string) (string, error) { + return f.getCellStringFunc(sheet, axis, func(x *xlsxWorksheet, c *xlsxC) (string, bool, error) { val, err := c.getValueFrom(f, f.sharedStringsReader()) if err != nil { - panic(err) // Fail fast to avoid future side effects! + return val, false, err } - return val, true + return val, true, err }) } @@ -68,7 +68,7 @@ func (f *File) GetCellValue(sheet, axis string) string { // // Note that default date format is m/d/yy h:mm of time.Time type value. You can // set numbers format by SetCellStyle() method. -func (f *File) SetCellValue(sheet, axis string, value interface{}) { +func (f *File) SetCellValue(sheet, axis string, value interface{}) error { switch v := value.(type) { case int: f.SetCellInt(sheet, axis, v) @@ -102,9 +102,12 @@ func (f *File) SetCellValue(sheet, axis string, value interface{}) { f.SetCellDefault(sheet, axis, strconv.FormatFloat(v.Seconds()/86400.0, 'f', -1, 32)) f.setDefaultTimeStyle(sheet, axis, 21) case time.Time: - vv := timeToExcelTime(v) - if vv > 0 { - f.SetCellDefault(sheet, axis, strconv.FormatFloat(timeToExcelTime(v), 'f', -1, 64)) + excelTime, err := timeToExcelTime(v) + if err != nil { + return err + } + if excelTime > 0 { + f.SetCellDefault(sheet, axis, strconv.FormatFloat(excelTime, 'f', -1, 64)) f.setDefaultTimeStyle(sheet, axis, 22) } else { f.SetCellStr(sheet, axis, v.Format(time.RFC3339Nano)) @@ -116,23 +119,31 @@ func (f *File) SetCellValue(sheet, axis string, value interface{}) { default: f.SetCellStr(sheet, axis, fmt.Sprintf("%v", value)) } + return nil } // 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) { +func (f *File) SetCellInt(sheet, axis string, value int) error { xlsx := f.workSheetReader(sheet) - cellData, col, _ := f.prepareCell(xlsx, sheet, axis) + cellData, col, _, err := f.prepareCell(xlsx, sheet, axis) + if err != nil { + return err + } cellData.S = f.prepareCellStyle(xlsx, col, cellData.S) cellData.T = "" cellData.V = strconv.Itoa(value) + return err } // 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) { +func (f *File) SetCellBool(sheet, axis string, value bool) error { xlsx := f.workSheetReader(sheet) - cellData, col, _ := f.prepareCell(xlsx, sheet, axis) + cellData, col, _, err := f.prepareCell(xlsx, sheet, axis) + if err != nil { + return err + } cellData.S = f.prepareCellStyle(xlsx, col, cellData.S) cellData.T = "b" if value { @@ -140,6 +151,7 @@ func (f *File) SetCellBool(sheet, axis string, value bool) { } else { cellData.V = "0" } + return err } // SetCellFloat sets a floating point value into a cell. The prec parameter @@ -151,20 +163,26 @@ func (f *File) SetCellBool(sheet, axis string, value bool) { // 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) { +func (f *File) SetCellFloat(sheet, axis string, value float64, prec, bitSize int) error { xlsx := f.workSheetReader(sheet) - cellData, col, _ := f.prepareCell(xlsx, sheet, axis) + cellData, col, _, err := f.prepareCell(xlsx, sheet, axis) + if err != nil { + return err + } cellData.S = f.prepareCellStyle(xlsx, col, cellData.S) cellData.T = "" cellData.V = strconv.FormatFloat(value, 'f', prec, bitSize) + return err } // 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) { +func (f *File) SetCellStr(sheet, axis, value string) error { xlsx := f.workSheetReader(sheet) - cellData, col, _ := f.prepareCell(xlsx, sheet, axis) - + cellData, col, _, err := f.prepareCell(xlsx, sheet, axis) + if err != nil { + return err + } // Leading space(s) character detection. if len(value) > 0 && value[0] == 32 { cellData.XMLSpace = xml.Attr{ @@ -176,42 +194,49 @@ func (f *File) SetCellStr(sheet, axis, value string) { cellData.S = f.prepareCellStyle(xlsx, col, cellData.S) cellData.T = "str" cellData.V = value + return err } // 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) { +func (f *File) SetCellDefault(sheet, axis, value string) error { xlsx := f.workSheetReader(sheet) - cellData, col, _ := f.prepareCell(xlsx, sheet, axis) + cellData, col, _, err := f.prepareCell(xlsx, sheet, axis) + if err != nil { + return err + } cellData.S = f.prepareCellStyle(xlsx, col, cellData.S) cellData.T = "" cellData.V = value + return err } // 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 { - return f.getCellStringFunc(sheet, axis, func(x *xlsxWorksheet, c *xlsxC) (string, bool) { +func (f *File) GetCellFormula(sheet, axis string) (string, error) { + return f.getCellStringFunc(sheet, axis, func(x *xlsxWorksheet, c *xlsxC) (string, bool, error) { if c.F == nil { - return "", false + return "", false, nil } if c.F.T == STCellFormulaTypeShared { - return getSharedForumula(x, c.F.Si), true + return getSharedForumula(x, c.F.Si), true, nil } - return c.F.Content, true + return c.F.Content, true, nil }) } // SetCellFormula provides a function to set cell formula by given string and // worksheet name. -func (f *File) SetCellFormula(sheet, axis, formula string) { +func (f *File) SetCellFormula(sheet, axis, formula string) error { xlsx := f.workSheetReader(sheet) - cellData, _, _ := f.prepareCell(xlsx, sheet, axis) - + cellData, _, _, err := f.prepareCell(xlsx, sheet, axis) + if err != nil { + return err + } if formula == "" { cellData.F = nil f.deleteCalcChain(axis) - return + return err } if cellData.F != nil { @@ -219,6 +244,7 @@ func (f *File) SetCellFormula(sheet, axis, formula string) { } else { cellData.F = &xlsxF{Content: formula} } + return err } // GetCellHyperLink provides a function to get cell hyperlink by given @@ -227,28 +253,30 @@ func (f *File) SetCellFormula(sheet, axis, formula string) { // 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: // -// link, target := xlsx.GetCellHyperLink("Sheet1", "H6") +// link, target, err := xlsx.GetCellHyperLink("Sheet1", "H6") // -func (f *File) GetCellHyperLink(sheet, axis string) (bool, string) { +func (f *File) GetCellHyperLink(sheet, axis string) (bool, string, error) { // Check for correct cell name if _, _, err := SplitCellName(axis); err != nil { - panic(err) // Fail fast to avoid possible future side effects + return false, "", err } xlsx := f.workSheetReader(sheet) - axis = f.mergeCellsParser(xlsx, axis) - + axis, err := f.mergeCellsParser(xlsx, axis) + if err != nil { + return false, "", err + } if xlsx.Hyperlinks != nil { for _, link := range xlsx.Hyperlinks.Hyperlink { if link.Ref == axis { if link.RID != "" { - return true, f.getSheetRelationshipsTargetByID(sheet, link.RID) + return true, f.getSheetRelationshipsTargetByID(sheet, link.RID), err } - return true, link.Location + return true, link.Location, err } } } - return false, "" + return false, "", err } // SetCellHyperLink provides a function to set cell hyperlink by given @@ -256,23 +284,26 @@ func (f *File) GetCellHyperLink(sheet, axis string) (bool, string) { // hyperlink "External" for web site or "Location" for moving to one of cell // in this workbook. The below is example for external link. // -// xlsx.SetCellHyperLink("Sheet1", "A3", "https://github.com/360EntSecGroup-Skylar/excelize", "External") +// err := xlsx.SetCellHyperLink("Sheet1", "A3", "https://github.com/360EntSecGroup-Skylar/excelize", "External") // // Set underline and font color style for the cell. -// style, _ := xlsx.NewStyle(`{"font":{"color":"#1265BE","underline":"single"}}`) -// xlsx.SetCellStyle("Sheet1", "A3", "A3", style) +// style, err := xlsx.NewStyle(`{"font":{"color":"#1265BE","underline":"single"}}`) +// err = xlsx.SetCellStyle("Sheet1", "A3", "A3", style) // // A this is another example for "Location": // -// xlsx.SetCellHyperLink("Sheet1", "A3", "Sheet1!A40", "Location") +// err := xlsx.SetCellHyperLink("Sheet1", "A3", "Sheet1!A40", "Location") // -func (f *File) SetCellHyperLink(sheet, axis, link, linkType string) { +func (f *File) SetCellHyperLink(sheet, axis, link, linkType string) error { // Check for correct cell name if _, _, err := SplitCellName(axis); err != nil { - panic(err) // Fail fast to avoid possible future side effects + return err } xlsx := f.workSheetReader(sheet) - axis = f.mergeCellsParser(xlsx, axis) + axis, err := f.mergeCellsParser(xlsx, axis) + if err != nil { + return err + } var linkData xlsxHyperlink @@ -289,35 +320,36 @@ func (f *File) SetCellHyperLink(sheet, axis, link, linkType string) { Location: link, } default: - panic(fmt.Errorf("invalid link type %q", linkType)) + return fmt.Errorf("invalid link type %q", linkType) } if xlsx.Hyperlinks == nil { xlsx.Hyperlinks = new(xlsxHyperlinks) } xlsx.Hyperlinks.Hyperlink = append(xlsx.Hyperlinks.Hyperlink, linkData) + return nil } // MergeCell provides a function to merge cells by given coordinate area and // sheet name. For example create a merged cell of D3:E9 on Sheet1: // -// xlsx.MergeCell("Sheet1", "D3", "E9") +// err := xlsx.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. -func (f *File) MergeCell(sheet, hcell, vcell string) { +func (f *File) MergeCell(sheet, hcell, vcell string) error { hcol, hrow, err := CellNameToCoordinates(hcell) if err != nil { - panic(err) + return err } vcol, vrow, err := CellNameToCoordinates(vcell) if err != nil { - panic(err) + return err } if hcol == vcol && hrow == vrow { - return + return err } if vcol < hcol { @@ -340,11 +372,13 @@ func (f *File) MergeCell(sheet, hcell, vcell string) { for _, cellData := range xlsx.MergeCells.Cells { cc := strings.Split(cellData.Ref, ":") if len(cc) != 2 { - panic(fmt.Errorf("invalid area %q", cellData.Ref)) + return fmt.Errorf("invalid area %q", cellData.Ref) } - - if !checkCellInArea(hcell, cellData.Ref) && !checkCellInArea(vcell, cellData.Ref) && - !checkCellInArea(cc[0], ref) && !checkCellInArea(cc[1], ref) { + c1, _ := checkCellInArea(hcell, cellData.Ref) + c2, _ := checkCellInArea(vcell, cellData.Ref) + c3, _ := checkCellInArea(cc[0], ref) + c4, _ := checkCellInArea(cc[1], ref) + if !c1 && !c2 && !c3 && !c4 { cells = append(cells, cellData) } } @@ -353,24 +387,25 @@ func (f *File) MergeCell(sheet, hcell, vcell string) { } else { xlsx.MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: hcell + ":" + vcell}}} } + return err } // 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: // -// xlsx.SetSheetRow("Sheet1", "B6", &[]interface{}{"1", nil, 2}) +// err := xlsx.SetSheetRow("Sheet1", "B6", &[]interface{}{"1", nil, 2}) // -func (f *File) SetSheetRow(sheet, axis string, slice interface{}) { +func (f *File) SetSheetRow(sheet, axis string, slice interface{}) error { col, row, err := CellNameToCoordinates(axis) if err != nil { - panic(err) // Fail fast to avoid future side effects! + return err } // Make sure 'slice' is a Ptr to Slice v := reflect.ValueOf(slice) if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Slice { - panic(errors.New("pointer to slice expected")) // Fail fast to avoid future side effects! + return errors.New("pointer to slice expected") } v = v.Elem() @@ -379,35 +414,42 @@ func (f *File) SetSheetRow(sheet, axis string, slice interface{}) { // Error should never happens here. But keep ckecking to early detect regresions // if it will be introduced in furure if err != nil { - panic(err) // Fail fast to avoid future side effects! + return err } f.SetCellValue(sheet, cell, v.Index(i).Interface()) } + return err } // getCellInfo does common preparation for all SetCell* methods. -func (f *File) prepareCell(xlsx *xlsxWorksheet, sheet, cell string) (*xlsxC, int, int) { - cell = f.mergeCellsParser(xlsx, cell) - +func (f *File) prepareCell(xlsx *xlsxWorksheet, sheet, cell string) (*xlsxC, int, int, error) { + var err error + cell, err = f.mergeCellsParser(xlsx, cell) + if err != nil { + return nil, 0, 0, err + } col, row, err := CellNameToCoordinates(cell) if err != nil { - panic(err) // Fail fast and prevent future side effects + return nil, 0, 0, err } prepareSheetXML(xlsx, col, row) - return &xlsx.SheetData.Row[row-1].C[col-1], col, row + return &xlsx.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. -func (f *File) getCellStringFunc(sheet, axis string, fn func(x *xlsxWorksheet, c *xlsxC) (string, bool)) string { +// 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) { xlsx := f.workSheetReader(sheet) - axis = f.mergeCellsParser(xlsx, axis) - + var err error + axis, err = f.mergeCellsParser(xlsx, axis) + if err != nil { + return "", err + } _, row, err := CellNameToCoordinates(axis) if err != nil { - panic(err) // Fail fast to avoid future side effects! + return "", err } lastRowNum := 0 @@ -417,7 +459,7 @@ func (f *File) getCellStringFunc(sheet, axis string, fn func(x *xlsxWorksheet, c // keep in mind: row starts from 1 if row > lastRowNum { - return "" + return "", nil } for rowIdx := range xlsx.SheetData.Row { @@ -430,12 +472,16 @@ func (f *File) getCellStringFunc(sheet, axis string, fn func(x *xlsxWorksheet, c if axis != colData.R { continue } - if val, ok := fn(xlsx, colData); ok { - return val + val, ok, err := fn(xlsx, colData) + if err != nil { + return "", err + } + if ok { + return val, nil } } } - return "" + return "", nil } // formattedValue provides a function to returns a value after formatted. If @@ -468,35 +514,39 @@ func (f *File) prepareCellStyle(xlsx *xlsxWorksheet, col, style int) int { // mergeCellsParser provides a function to check merged cells in worksheet by // given axis. -func (f *File) mergeCellsParser(xlsx *xlsxWorksheet, axis string) string { +func (f *File) mergeCellsParser(xlsx *xlsxWorksheet, axis string) (string, error) { axis = strings.ToUpper(axis) if xlsx.MergeCells != nil { for i := 0; i < len(xlsx.MergeCells.Cells); i++ { - if checkCellInArea(axis, xlsx.MergeCells.Cells[i].Ref) { + ok, err := checkCellInArea(axis, xlsx.MergeCells.Cells[i].Ref) + if err != nil { + return axis, err + } + if ok { axis = strings.Split(xlsx.MergeCells.Cells[i].Ref, ":")[0] } } } - return axis + return axis, nil } // checkCellInArea provides a function to determine if a given coordinate is // within an area. -func checkCellInArea(cell, area string) bool { +func checkCellInArea(cell, area string) (bool, error) { col, row, err := CellNameToCoordinates(cell) if err != nil { - panic(err) + return false, err } rng := strings.Split(area, ":") if len(rng) != 2 { - return false + return false, err } firstCol, firtsRow, _ := CellNameToCoordinates(rng[0]) lastCol, lastRow, _ := CellNameToCoordinates(rng[1]) - return col >= firstCol && col <= lastCol && row >= firtsRow && row <= lastRow + return col >= firstCol && col <= lastCol && row >= firtsRow && row <= lastRow, err } // getSharedForumula find a cell contains the same formula as another cell, diff --git a/cell_test.go b/cell_test.go index 12efc170f5..7b1381f042 100644 --- a/cell_test.go +++ b/cell_test.go @@ -17,8 +17,9 @@ func TestCheckCellInArea(t *testing.T) { for _, expectedTrueCellInArea := range expectedTrueCellInAreaList { cell := expectedTrueCellInArea[0] area := expectedTrueCellInArea[1] - - assert.Truef(t, checkCellInArea(cell, area), + ok, err := checkCellInArea(cell, area) + assert.NoError(t, err) + assert.Truef(t, ok, "Expected cell %v to be in area %v, got false\n", cell, area) } @@ -31,14 +32,15 @@ func TestCheckCellInArea(t *testing.T) { for _, expectedFalseCellInArea := range expectedFalseCellInAreaList { cell := expectedFalseCellInArea[0] area := expectedFalseCellInArea[1] - - assert.Falsef(t, checkCellInArea(cell, area), + ok, err := checkCellInArea(cell, area) + assert.NoError(t, err) + assert.Falsef(t, ok, "Expected cell %v not to be inside of area %v, but got true\n", cell, area) } - assert.Panics(t, func() { - checkCellInArea("AA0", "Z0:AB1") - }) + ok, err := checkCellInArea("AA0", "Z0:AB1") + assert.EqualError(t, err, `cannot convert cell "AA0" to coordinates: invalid cell name "AA0"`) + assert.False(t, ok) } func TestSetCellFloat(t *testing.T) { @@ -47,20 +49,28 @@ func TestSetCellFloat(t *testing.T) { f := NewFile() f.SetCellFloat(sheet, "A1", 123.0, -1, 64) f.SetCellFloat(sheet, "A2", 123.0, 1, 64) - assert.Equal(t, "123", f.GetCellValue(sheet, "A1"), "A1 should be 123") - assert.Equal(t, "123.0", f.GetCellValue(sheet, "A2"), "A2 should be 123.0") + val, err := f.GetCellValue(sheet, "A1") + assert.NoError(t, err) + assert.Equal(t, "123", val, "A1 should be 123") + val, err = f.GetCellValue(sheet, "A2") + assert.NoError(t, err) + assert.Equal(t, "123.0", val, "A2 should be 123.0") }) t.Run("with a decimal and precision limit", func(t *testing.T) { f := NewFile() f.SetCellFloat(sheet, "A1", 123.42, 1, 64) - assert.Equal(t, "123.4", f.GetCellValue(sheet, "A1"), "A1 should be 123.4") + val, err := f.GetCellValue(sheet, "A1") + assert.NoError(t, err) + assert.Equal(t, "123.4", val, "A1 should be 123.4") }) t.Run("with a decimal and no limit", func(t *testing.T) { f := NewFile() f.SetCellFloat(sheet, "A1", 123.42, -1, 64) - assert.Equal(t, "123.42", f.GetCellValue(sheet, "A1"), "A1 should be 123.42") + val, err := f.GetCellValue(sheet, "A1") + assert.NoError(t, err) + assert.Equal(t, "123.42", val, "A1 should be 123.42") }) } @@ -68,6 +78,7 @@ func ExampleFile_SetCellFloat() { f := NewFile() var x = 3.14159265 f.SetCellFloat("Sheet1", "A1", x, 2, 64) - fmt.Println(f.GetCellValue("Sheet1", "A1")) + val, _ := f.GetCellValue("Sheet1", "A1") + fmt.Println(val) // Output: 3.14 } diff --git a/chart.go b/chart.go index c31995e1e4..5429b77112 100644 --- a/chart.go +++ b/chart.go @@ -456,7 +456,10 @@ func (f *File) AddChart(sheet, cell, format string) error { drawingXML := "xl/drawings/drawing" + strconv.Itoa(drawingID) + ".xml" drawingID, drawingXML = f.prepareDrawing(xlsx, drawingID, sheet, drawingXML) drawingRID := f.addDrawingRelationships(drawingID, SourceRelationshipChart, "../charts/chart"+strconv.Itoa(chartID)+".xml", "") - f.addDrawingChart(sheet, drawingXML, cell, formatSet.Dimension.Width, formatSet.Dimension.Height, drawingRID, &formatSet.Format) + err = f.addDrawingChart(sheet, drawingXML, cell, formatSet.Dimension.Width, formatSet.Dimension.Height, drawingRID, &formatSet.Format) + if err != nil { + return err + } f.addChart(formatSet) f.addContentTypePart(chartID, "chart") f.addContentTypePart(drawingID, "drawings") @@ -1239,8 +1242,11 @@ 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) { - col, row := MustCellNameToCoordinates(cell) +func (f *File) addDrawingChart(sheet, drawingXML, cell string, width, height, rID int, formatSet *formatPicture) error { + col, row, err := CellNameToCoordinates(cell) + if err != nil { + return err + } colIdx := col - 1 rowIdx := row - 1 @@ -1290,4 +1296,5 @@ func (f *File) addDrawingChart(sheet, drawingXML, cell string, width, height, rI } content.TwoCellAnchor = append(content.TwoCellAnchor, &twoCellAnchor) f.Drawings[drawingXML] = content + return err } diff --git a/col.go b/col.go index 7eb7831c00..e3e057ce4a 100644 --- a/col.go +++ b/col.go @@ -22,33 +22,39 @@ const ( // worksheet name and column name. For example, get visible state of column D // in Sheet1: // -// xlsx.GetColVisible("Sheet1", "D") +// visiable, err := xlsx.GetColVisible("Sheet1", "D") // -func (f *File) GetColVisible(sheet, col string) bool { - colNum := MustColumnNameToNumber(col) +func (f *File) GetColVisible(sheet, col string) (bool, error) { + visible := true + colNum, err := ColumnNameToNumber(col) + if err != nil { + return visible, err + } xlsx := f.workSheetReader(sheet) if xlsx.Cols == nil { - return true + return visible, err } - visible := true for c := range xlsx.Cols.Col { colData := &xlsx.Cols.Col[c] if colData.Min <= colNum && colNum <= colData.Max { visible = !colData.Hidden } } - return visible + return visible, err } // SetColVisible provides a function to set visible of a single column by given // worksheet name and column name. For example, hide column D in Sheet1: // -// xlsx.SetColVisible("Sheet1", "D", false) +// err := xlsx.SetColVisible("Sheet1", "D", false) // -func (f *File) SetColVisible(sheet, col string, visible bool) { - colNum := MustColumnNameToNumber(col) +func (f *File) SetColVisible(sheet, col string, visible bool) error { + colNum, err := ColumnNameToNumber(col) + if err != nil { + return err + } colData := xlsxCol{ Min: colNum, Max: colNum, @@ -60,7 +66,7 @@ func (f *File) SetColVisible(sheet, col string, visible bool) { cols := xlsxCols{} cols.Col = append(cols.Col, colData) xlsx.Cols = &cols - return + return err } for v := range xlsx.Cols.Col { if xlsx.Cols.Col[v].Min <= colNum && colNum <= xlsx.Cols.Col[v].Max { @@ -72,20 +78,24 @@ func (f *File) SetColVisible(sheet, col string, visible bool) { colData.Hidden = !visible colData.CustomWidth = true xlsx.Cols.Col = append(xlsx.Cols.Col, colData) + return err } // GetColOutlineLevel provides a function to get outline level of a single // column by given worksheet name and column name. For example, get outline // level of column D in Sheet1: // -// xlsx.GetColOutlineLevel("Sheet1", "D") +// level, err := xlsx.GetColOutlineLevel("Sheet1", "D") // -func (f *File) GetColOutlineLevel(sheet, col string) uint8 { - colNum := MustColumnNameToNumber(col) - xlsx := f.workSheetReader(sheet) +func (f *File) GetColOutlineLevel(sheet, col string) (uint8, error) { level := uint8(0) + colNum, err := ColumnNameToNumber(col) + if err != nil { + return level, err + } + xlsx := f.workSheetReader(sheet) if xlsx.Cols == nil { - return level + return level, err } for c := range xlsx.Cols.Col { colData := &xlsx.Cols.Col[c] @@ -93,17 +103,20 @@ func (f *File) GetColOutlineLevel(sheet, col string) uint8 { level = colData.OutlineLevel } } - return level + return level, err } // SetColOutlineLevel provides a function to set outline level of a single // column by given worksheet name and column name. For example, set outline // level of column D in Sheet1 to 2: // -// xlsx.SetColOutlineLevel("Sheet1", "D", 2) +// err := xlsx.SetColOutlineLevel("Sheet1", "D", 2) // -func (f *File) SetColOutlineLevel(sheet, col string, level uint8) { - colNum := MustColumnNameToNumber(col) +func (f *File) SetColOutlineLevel(sheet, col string, level uint8) error { + colNum, err := ColumnNameToNumber(col) + if err != nil { + return err + } colData := xlsxCol{ Min: colNum, Max: colNum, @@ -115,7 +128,7 @@ func (f *File) SetColOutlineLevel(sheet, col string, level uint8) { cols := xlsxCols{} cols.Col = append(cols.Col, colData) xlsx.Cols = &cols - return + return err } for v := range xlsx.Cols.Col { if xlsx.Cols.Col[v].Min <= colNum && colNum <= xlsx.Cols.Col[v].Max { @@ -127,21 +140,24 @@ func (f *File) SetColOutlineLevel(sheet, col string, level uint8) { colData.OutlineLevel = level colData.CustomWidth = true xlsx.Cols.Col = append(xlsx.Cols.Col, colData) + return err } // SetColWidth provides a function to set the width of a single column or // multiple columns. For example: // // xlsx := excelize.NewFile() -// xlsx.SetColWidth("Sheet1", "A", "H", 20) -// err := xlsx.Save() -// if err != nil { -// fmt.Println(err) -// } +// err := xlsx.SetColWidth("Sheet1", "A", "H", 20) // -func (f *File) SetColWidth(sheet, startcol, endcol string, width float64) { - min := MustColumnNameToNumber(startcol) - max := MustColumnNameToNumber(endcol) +func (f *File) SetColWidth(sheet, startcol, endcol string, width float64) error { + min, err := ColumnNameToNumber(startcol) + if err != nil { + return err + } + max, err := ColumnNameToNumber(endcol) + if err != nil { + return err + } if min > max { min, max = max, min } @@ -160,6 +176,7 @@ func (f *File) SetColWidth(sheet, startcol, endcol string, width float64) { cols.Col = append(cols.Col, col) xlsx.Cols = &cols } + return err } // positionObjectPixels calculate the vertices that define the position of a @@ -289,8 +306,11 @@ func (f *File) getColWidth(sheet string, col int) int { // GetColWidth provides a function to get column width by given worksheet name // and column index. -func (f *File) GetColWidth(sheet, col string) float64 { - colNum := MustColumnNameToNumber(col) +func (f *File) GetColWidth(sheet, col string) (float64, error) { + colNum, err := ColumnNameToNumber(col) + if err != nil { + return defaultColWidthPixels, err + } xlsx := f.workSheetReader(sheet) if xlsx.Cols != nil { var width float64 @@ -300,39 +320,39 @@ func (f *File) GetColWidth(sheet, col string) float64 { } } if width != 0 { - return width + return width, err } } // Optimisation for when the column widths haven't changed. - return defaultColWidthPixels + return defaultColWidthPixels, 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: // -// xlsx.InsertCol("Sheet1", "C") +// err := xlsx.InsertCol("Sheet1", "C") // -func (f *File) InsertCol(sheet, col string) { +func (f *File) InsertCol(sheet, col string) error { num, err := ColumnNameToNumber(col) if err != nil { - panic(err) + return err } - f.adjustHelper(sheet, columns, num, 1) + return f.adjustHelper(sheet, columns, num, 1) } // RemoveCol provides a function to remove single column by given worksheet // name and column index. For example, remove column C in Sheet1: // -// xlsx.RemoveCol("Sheet1", "C") +// err := xlsx.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 // worksheet, it will cause a file error when you open it. The excelize only // partially updates these references currently. -func (f *File) RemoveCol(sheet, col string) { +func (f *File) RemoveCol(sheet, col string) error { num, err := ColumnNameToNumber(col) if err != nil { - panic(err) // Fail fast to avoid possible future side effects! + return err } xlsx := f.workSheetReader(sheet) @@ -346,7 +366,7 @@ func (f *File) RemoveCol(sheet, col string) { } } } - f.adjustHelper(sheet, columns, num, -1) + return f.adjustHelper(sheet, columns, num, -1) } // convertColWidthToPixels provieds function to convert the width of a cell diff --git a/comment.go b/comment.go index a94194dd61..ca79779b25 100644 --- a/comment.go +++ b/comment.go @@ -72,7 +72,7 @@ func (f *File) getSheetComments(sheetID int) string { // author length is 255 and the max text length is 32512. For example, add a // comment in Sheet1!$A$30: // -// xlsx.AddComment("Sheet1", "A30", `{"author":"Excelize: ","text":"This is a comment."}`) +// err := xlsx.AddComment("Sheet1", "A30", `{"author":"Excelize: ","text":"This is a comment."}`) // func (f *File) AddComment(sheet, cell, format string) error { formatSet, err := parseFormatCommentsSet(format) @@ -107,15 +107,21 @@ func (f *File) AddComment(sheet, cell, format string) error { colCount = ll } } - f.addDrawingVML(commentID, drawingVML, cell, strings.Count(formatSet.Text, "\n")+1, colCount) + err = f.addDrawingVML(commentID, drawingVML, cell, strings.Count(formatSet.Text, "\n")+1, colCount) + if err != nil { + return err + } f.addContentTypePart(commentID, "comments") return err } // 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) { - col, row := MustCellNameToCoordinates(cell) +func (f *File) addDrawingVML(commentID int, drawingVML, cell string, lineCount, colCount int) error { + col, row, err := CellNameToCoordinates(cell) + if err != nil { + return err + } yAxis := col - 1 xAxis := row - 1 vml := f.VMLDrawing[drawingVML] @@ -206,6 +212,7 @@ func (f *File) addDrawingVML(commentID int, drawingVML, cell string, lineCount, } vml.Shape = append(vml.Shape, shape) f.VMLDrawing[drawingVML] = vml + return err } // addComment provides a function to create chart as xl/comments%d.xml by diff --git a/date.go b/date.go index e550feb2bd..b49a6958da 100644 --- a/date.go +++ b/date.go @@ -10,6 +10,7 @@ package excelize import ( + "errors" "math" "time" ) @@ -25,18 +26,18 @@ var ( ) // timeToExcelTime provides a function to convert time to Excel time. -func timeToExcelTime(t time.Time) float64 { +func timeToExcelTime(t time.Time) (float64, error) { // TODO in future this should probably also handle date1904 and like TimeFromExcelTime // Force user to explicit convet passed value to UTC time. // Because for example 1900-01-01 00:00:00 +0300 MSK converts to 1900-01-01 00:00:00 +0230 LMT // probably due to daylight saving. if t.Location() != time.UTC { - panic("only UTC time expected") + return 0.0, errors.New("only UTC time expected") } if t.Before(excelMinTime1900) { - return 0.0 + return 0.0, nil } tt := t @@ -60,7 +61,7 @@ func timeToExcelTime(t time.Time) float64 { if t.After(excelBuggyPeriodStart) { result += 1.0 } - return result + return result, nil } // shiftJulianToNoon provides a function to process julian date to noon. diff --git a/date_test.go b/date_test.go index 3ec0b69265..63cb19e3bc 100644 --- a/date_test.go +++ b/date_test.go @@ -31,7 +31,9 @@ var trueExpectedDateList = []dateTest{ func TestTimeToExcelTime(t *testing.T) { for i, test := range trueExpectedDateList { t.Run(fmt.Sprintf("TestData%d", i+1), func(t *testing.T) { - assert.Equalf(t, test.ExcelValue, timeToExcelTime(test.GoValue), + excelTime, err := timeToExcelTime(test.GoValue) + assert.NoError(t, err) + assert.Equalf(t, test.ExcelValue, excelTime, "Time: %s", test.GoValue.String()) }) } @@ -44,9 +46,8 @@ func TestTimeToExcelTime_Timezone(t *testing.T) { } for i, test := range trueExpectedDateList { t.Run(fmt.Sprintf("TestData%d", i+1), func(t *testing.T) { - assert.Panics(t, func() { - timeToExcelTime(test.GoValue.In(location)) - }, "Time: %s", test.GoValue.String()) + _, err := timeToExcelTime(test.GoValue.In(location)) + assert.EqualError(t, err, "only UTC time expected") }) } } diff --git a/excelize.go b/excelize.go index 7a5046043e..857f3ace6a 100644 --- a/excelize.go +++ b/excelize.go @@ -98,11 +98,16 @@ func OpenReader(r io.Reader) (*File, error) { // setDefaultTimeStyle provides a function to set default numbers format for // time.Time type cell value by given worksheet name, cell coordinates and // number format code. -func (f *File) setDefaultTimeStyle(sheet, axis string, format int) { - if f.GetCellStyle(sheet, axis) == 0 { +func (f *File) setDefaultTimeStyle(sheet, axis string, format int) error { + s, err := f.GetCellStyle(sheet, axis) + if err != nil { + return err + } + if s == 0 { style, _ := f.NewStyle(`{"number_format": ` + strconv.Itoa(format) + `}`) f.SetCellStyle(sheet, axis, axis, style) } + return err } // workSheetReader provides a function to get the pointer to the structure @@ -218,7 +223,8 @@ func (f *File) GetMergeCells(sheet string) []MergeCell { for i := range xlsx.MergeCells.Cells { ref := xlsx.MergeCells.Cells[i].Ref axis := strings.Split(ref, ":")[0] - mergeCells = append(mergeCells, []string{ref, f.GetCellValue(sheet, axis)}) + val, _ := f.GetCellValue(sheet, axis) + mergeCells = append(mergeCells, []string{ref, val}) } } diff --git a/excelize_test.go b/excelize_test.go index 9671130c27..ab5e17b98c 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -27,7 +27,8 @@ func TestOpenFile(t *testing.T) { // Test get all the rows in a not exists worksheet. xlsx.GetRows("Sheet4") // Test get all the rows in a worksheet. - rows := xlsx.GetRows("Sheet2") + rows, err := xlsx.GetRows("Sheet2") + assert.NoError(t, err) for _, row := range rows { for _, cell := range row { t.Log(cell, "\t") @@ -40,16 +41,13 @@ func TestOpenFile(t *testing.T) { xlsx.SetCellDefault("Sheet2", "A1", strconv.FormatFloat(float64(-100.1588), 'f', -1, 64)) // Test set cell value with illegal row number. - assert.Panics(t, func() { - xlsx.SetCellDefault("Sheet2", "A", strconv.FormatFloat(float64(-100.1588), 'f', -1, 64)) - }) + assert.EqualError(t, xlsx.SetCellDefault("Sheet2", "A", strconv.FormatFloat(float64(-100.1588), 'f', -1, 64)), + `cannot convert cell "A" to coordinates: invalid cell name "A"`) xlsx.SetCellInt("Sheet2", "A1", 100) // Test set cell integer value with illegal row number. - assert.Panics(t, func() { - xlsx.SetCellInt("Sheet2", "A", 100) - }) + assert.EqualError(t, xlsx.SetCellInt("Sheet2", "A", 100), `cannot convert cell "A" to coordinates: invalid cell name "A"`) xlsx.SetCellStr("Sheet2", "C11", "Knowns") // Test max characters in a cell. @@ -62,35 +60,31 @@ func TestOpenFile(t *testing.T) { xlsx.SetCellStr("Sheet10", "b230", "10") // Test set cell string value with illegal row number. - assert.Panics(t, func() { - xlsx.SetCellStr("Sheet10", "A", "10") - }) + assert.EqualError(t, xlsx.SetCellStr("Sheet10", "A", "10"), `cannot convert cell "A" to coordinates: invalid cell name "A"`) xlsx.SetActiveSheet(2) // Test get cell formula with given rows number. - xlsx.GetCellFormula("Sheet1", "B19") + _, err = xlsx.GetCellFormula("Sheet1", "B19") + assert.NoError(t, err) // Test get cell formula with illegal worksheet name. - xlsx.GetCellFormula("Sheet2", "B20") - xlsx.GetCellFormula("Sheet1", "B20") + _, err = xlsx.GetCellFormula("Sheet2", "B20") + assert.NoError(t, err) + _, err = xlsx.GetCellFormula("Sheet1", "B20") + assert.NoError(t, err) // Test get cell formula with illegal rows number. - assert.Panics(t, func() { - xlsx.GetCellFormula("Sheet1", "B") - }) - + _, err = xlsx.GetCellFormula("Sheet1", "B") + assert.EqualError(t, err, `cannot convert cell "B" to coordinates: invalid cell name "B"`) // Test get shared cell formula xlsx.GetCellFormula("Sheet2", "H11") xlsx.GetCellFormula("Sheet2", "I11") getSharedForumula(&xlsxWorksheet{}, "") // Test read cell value with given illegal rows number. - assert.Panics(t, func() { - xlsx.GetCellValue("Sheet2", "a-1") - }) - - assert.Panics(t, func() { - xlsx.GetCellValue("Sheet2", "A") - }) + _, err = xlsx.GetCellValue("Sheet2", "a-1") + assert.EqualError(t, err, `cannot convert cell "A-1" to coordinates: invalid cell name "A-1"`) + _, err = xlsx.GetCellValue("Sheet2", "A") + assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`) // Test read cell value with given lowercase column number. xlsx.GetCellValue("Sheet2", "a5") @@ -127,14 +121,14 @@ func TestOpenFile(t *testing.T) { } for _, test := range booltest { xlsx.SetCellValue("Sheet2", "F16", test.value) - assert.Equal(t, test.expected, xlsx.GetCellValue("Sheet2", "F16")) + val, err := xlsx.GetCellValue("Sheet2", "F16") + assert.NoError(t, err) + assert.Equal(t, test.expected, val) } xlsx.SetCellValue("Sheet2", "G2", nil) - assert.Panics(t, func() { - xlsx.SetCellValue("Sheet2", "G4", time.Now()) - }) + assert.EqualError(t, xlsx.SetCellValue("Sheet2", "G4", time.Now()), "only UTC time expected") xlsx.SetCellValue("Sheet2", "G4", time.Now().UTC()) // 02:46:40 @@ -320,19 +314,15 @@ func TestSetCellHyperLink(t *testing.T) { t.Log(err) } // Test set cell hyperlink in a work sheet already have hyperlinks. - xlsx.SetCellHyperLink("Sheet1", "B19", "https://github.com/360EntSecGroup-Skylar/excelize", "External") + assert.NoError(t, xlsx.SetCellHyperLink("Sheet1", "B19", "https://github.com/360EntSecGroup-Skylar/excelize", "External")) // Test add first hyperlink in a work sheet. - xlsx.SetCellHyperLink("Sheet2", "C1", "https://github.com/360EntSecGroup-Skylar/excelize", "External") + assert.NoError(t, xlsx.SetCellHyperLink("Sheet2", "C1", "https://github.com/360EntSecGroup-Skylar/excelize", "External")) // Test add Location hyperlink in a work sheet. - xlsx.SetCellHyperLink("Sheet2", "D6", "Sheet1!D8", "Location") + assert.NoError(t, xlsx.SetCellHyperLink("Sheet2", "D6", "Sheet1!D8", "Location")) - assert.Panics(t, func() { - xlsx.SetCellHyperLink("Sheet2", "C3", "Sheet1!D8", "") - }) + assert.EqualError(t, xlsx.SetCellHyperLink("Sheet2", "C3", "Sheet1!D8", ""), `invalid link type ""`) - assert.Panics(t, func() { - xlsx.SetCellHyperLink("Sheet2", "", "Sheet1!D60", "Location") - }) + assert.EqualError(t, xlsx.SetCellHyperLink("Sheet2", "", "Sheet1!D60", "Location"), `invalid cell name ""`) assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetCellHyperLink.xlsx"))) } @@ -343,15 +333,17 @@ func TestGetCellHyperLink(t *testing.T) { t.FailNow() } - assert.Panics(t, func() { - xlsx.GetCellHyperLink("Sheet1", "") - }) + link, target, err := xlsx.GetCellHyperLink("Sheet1", "") + assert.EqualError(t, err, `invalid cell name ""`) - link, target := xlsx.GetCellHyperLink("Sheet1", "A22") + link, target, err = xlsx.GetCellHyperLink("Sheet1", "A22") + assert.NoError(t, err) t.Log(link, target) - link, target = xlsx.GetCellHyperLink("Sheet2", "D6") + link, target, err = xlsx.GetCellHyperLink("Sheet2", "D6") + assert.NoError(t, err) t.Log(link, target) - link, target = xlsx.GetCellHyperLink("Sheet3", "H3") + link, target, err = xlsx.GetCellHyperLink("Sheet3", "H3") + assert.NoError(t, err) t.Log(link, target) } @@ -365,9 +357,7 @@ func TestSetCellFormula(t *testing.T) { xlsx.SetCellFormula("Sheet1", "C19", "SUM(Sheet2!D2,Sheet2!D9)") // Test set cell formula with illegal rows number. - assert.Panics(t, func() { - xlsx.SetCellFormula("Sheet1", "C", "SUM(Sheet2!D2,Sheet2!D9)") - }) + assert.EqualError(t, xlsx.SetCellFormula("Sheet1", "C", "SUM(Sheet2!D2,Sheet2!D9)"), `cannot convert cell "C" to coordinates: invalid cell name "C"`) assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetCellFormula1.xlsx"))) @@ -496,21 +486,16 @@ func TestSetCellStyleAlignment(t *testing.T) { t.FailNow() } - xlsx.SetCellStyle("Sheet1", "A22", "A22", style) + assert.NoError(t, xlsx.SetCellStyle("Sheet1", "A22", "A22", style)) // Test set cell style with given illegal rows number. - assert.Panics(t, func() { - xlsx.SetCellStyle("Sheet1", "A", "A22", style) - }) - - assert.Panics(t, func() { - xlsx.SetCellStyle("Sheet1", "A22", "A", style) - }) + assert.EqualError(t, xlsx.SetCellStyle("Sheet1", "A", "A22", style), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.EqualError(t, xlsx.SetCellStyle("Sheet1", "A22", "A", style), `cannot convert cell "A" to coordinates: invalid cell name "A"`) // Test get cell style with given illegal rows number. - assert.Panics(t, func() { - xlsx.GetCellStyle("Sheet1", "A") - }) + index, err := xlsx.GetCellStyle("Sheet1", "A") + assert.Equal(t, 0, index) + assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`) assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetCellStyleAlignment.xlsx"))) } @@ -528,19 +513,19 @@ func TestSetCellStyleBorder(t *testing.T) { if !assert.NoError(t, err) { t.FailNow() } - xlsx.SetCellStyle("Sheet1", "J21", "L25", style) + assert.NoError(t, xlsx.SetCellStyle("Sheet1", "J21", "L25", style)) style, err = xlsx.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() } - xlsx.SetCellStyle("Sheet1", "M28", "K24", style) + assert.NoError(t, xlsx.SetCellStyle("Sheet1", "M28", "K24", style)) style, err = xlsx.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() } - xlsx.SetCellStyle("Sheet1", "M28", "K24", style) + assert.NoError(t, xlsx.SetCellStyle("Sheet1", "M28", "K24", style)) // Test set border and solid style pattern fill for a single cell. style, err = xlsx.NewStyle(`{"border":[{"type":"left","color":"0000FF","style":8},{"type":"top","color":"00FF00","style":9},{"type":"bottom","color":"FFFF00","style":10},{"type":"right","color":"FF0000","style":11},{"type":"diagonalDown","color":"A020F0","style":12},{"type":"diagonalUp","color":"A020F0","style":13}],"fill":{"type":"pattern","color":["#E0EBF5"],"pattern":1}}`) @@ -548,7 +533,7 @@ func TestSetCellStyleBorder(t *testing.T) { t.FailNow() } - xlsx.SetCellStyle("Sheet1", "O22", "O22", style) + assert.NoError(t, xlsx.SetCellStyle("Sheet1", "O22", "O22", style)) assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetCellStyleBorder.xlsx"))) } @@ -596,7 +581,7 @@ func TestSetCellStyleNumberFormat(t *testing.T) { if !assert.NoError(t, err) { t.FailNow() } - xlsx.SetCellStyle("Sheet2", c, c, style) + assert.NoError(t, xlsx.SetCellStyle("Sheet2", c, c, style)) t.Log(xlsx.GetCellValue("Sheet2", c)) } } @@ -605,7 +590,7 @@ func TestSetCellStyleNumberFormat(t *testing.T) { if !assert.NoError(t, err) { t.FailNow() } - xlsx.SetCellStyle("Sheet2", "L33", "L33", style) + assert.NoError(t, xlsx.SetCellStyle("Sheet2", "L33", "L33", style)) assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetCellStyleNumberFormat.xlsx"))) } @@ -625,13 +610,13 @@ func TestSetCellStyleCurrencyNumberFormat(t *testing.T) { t.FailNow() } - xlsx.SetCellStyle("Sheet1", "A1", "A1", style) + assert.NoError(t, xlsx.SetCellStyle("Sheet1", "A1", "A1", style)) style, err = xlsx.NewStyle(`{"number_format": 188, "decimal_places": 31, "negred": true}`) if !assert.NoError(t, err) { t.FailNow() } - xlsx.SetCellStyle("Sheet1", "A2", "A2", style) + assert.NoError(t, xlsx.SetCellStyle("Sheet1", "A2", "A2", style)) assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetCellStyleCurrencyNumberFormat.TestBook3.xlsx"))) }) @@ -654,19 +639,19 @@ func TestSetCellStyleCurrencyNumberFormat(t *testing.T) { t.FailNow() } - xlsx.SetCellStyle("Sheet1", "A1", "A1", style) + assert.NoError(t, xlsx.SetCellStyle("Sheet1", "A1", "A1", style)) style, err = xlsx.NewStyle(`{"number_format": 31, "lang": "ko-kr"}`) if !assert.NoError(t, err) { t.FailNow() } - xlsx.SetCellStyle("Sheet1", "A2", "A2", style) + assert.NoError(t, xlsx.SetCellStyle("Sheet1", "A2", "A2", style)) style, err = xlsx.NewStyle(`{"number_format": 71, "lang": "th-th"}`) if !assert.NoError(t, err) { t.FailNow() } - xlsx.SetCellStyle("Sheet1", "A2", "A2", style) + assert.NoError(t, xlsx.SetCellStyle("Sheet1", "A2", "A2", style)) assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetCellStyleCurrencyNumberFormat.TestBook4.xlsx"))) }) @@ -680,12 +665,12 @@ func TestSetCellStyleCustomNumberFormat(t *testing.T) { if err != nil { t.Log(err) } - xlsx.SetCellStyle("Sheet1", "A1", "A1", style) + assert.NoError(t, xlsx.SetCellStyle("Sheet1", "A1", "A1", style)) style, err = xlsx.NewStyle(`{"custom_number_format": "[$-380A]dddd\\,\\ dd\" de \"mmmm\" de \"yyyy;@"}`) if err != nil { t.Log(err) } - xlsx.SetCellStyle("Sheet1", "A2", "A2", style) + assert.NoError(t, xlsx.SetCellStyle("Sheet1", "A2", "A2", style)) assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetCellStyleCustomNumberFormat.xlsx"))) } @@ -702,25 +687,25 @@ func TestSetCellStyleFill(t *testing.T) { if !assert.NoError(t, err) { t.FailNow() } - xlsx.SetCellStyle("Sheet1", "O23", "O23", style) + assert.NoError(t, xlsx.SetCellStyle("Sheet1", "O23", "O23", style)) style, err = xlsx.NewStyle(`{"fill":{"type":"gradient","color":["#FFFFFF"],"shading":1}}`) if !assert.NoError(t, err) { t.FailNow() } - xlsx.SetCellStyle("Sheet1", "O23", "O23", style) + assert.NoError(t, xlsx.SetCellStyle("Sheet1", "O23", "O23", style)) style, err = xlsx.NewStyle(`{"fill":{"type":"pattern","color":[],"pattern":1}}`) if !assert.NoError(t, err) { t.FailNow() } - xlsx.SetCellStyle("Sheet1", "O23", "O23", style) + assert.NoError(t, xlsx.SetCellStyle("Sheet1", "O23", "O23", style)) style, err = xlsx.NewStyle(`{"fill":{"type":"pattern","color":["#E0EBF5"],"pattern":19}}`) if !assert.NoError(t, err) { t.FailNow() } - xlsx.SetCellStyle("Sheet1", "O23", "O23", style) + assert.NoError(t, xlsx.SetCellStyle("Sheet1", "O23", "O23", style)) assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetCellStyleFill.xlsx"))) } @@ -737,35 +722,35 @@ func TestSetCellStyleFont(t *testing.T) { t.FailNow() } - xlsx.SetCellStyle("Sheet2", "A1", "A1", style) + assert.NoError(t, xlsx.SetCellStyle("Sheet2", "A1", "A1", style)) style, err = xlsx.NewStyle(`{"font":{"italic":true,"underline":"double"}}`) if !assert.NoError(t, err) { t.FailNow() } - xlsx.SetCellStyle("Sheet2", "A2", "A2", style) + assert.NoError(t, xlsx.SetCellStyle("Sheet2", "A2", "A2", style)) style, err = xlsx.NewStyle(`{"font":{"bold":true}}`) if !assert.NoError(t, err) { t.FailNow() } - xlsx.SetCellStyle("Sheet2", "A3", "A3", style) + assert.NoError(t, xlsx.SetCellStyle("Sheet2", "A3", "A3", style)) style, err = xlsx.NewStyle(`{"font":{"bold":true,"family":"","size":0,"color":"","underline":""}}`) if !assert.NoError(t, err) { t.FailNow() } - xlsx.SetCellStyle("Sheet2", "A4", "A4", style) + assert.NoError(t, xlsx.SetCellStyle("Sheet2", "A4", "A4", style)) style, err = xlsx.NewStyle(`{"font":{"color":"#777777"}}`) if !assert.NoError(t, err) { t.FailNow() } - xlsx.SetCellStyle("Sheet2", "A5", "A5", style) + assert.NoError(t, xlsx.SetCellStyle("Sheet2", "A5", "A5", style)) assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetCellStyleFont.xlsx"))) } @@ -782,7 +767,7 @@ func TestSetCellStyleProtection(t *testing.T) { t.FailNow() } - xlsx.SetCellStyle("Sheet2", "A6", "A6", style) + assert.NoError(t, xlsx.SetCellStyle("Sheet2", "A6", "A6", style)) err = xlsx.SaveAs(filepath.Join("test", "TestSetCellStyleProtection.xlsx")) if !assert.NoError(t, err) { t.FailNow() @@ -818,7 +803,8 @@ func TestGetPicture(t *testing.T) { t.FailNow() } - file, raw := xlsx.GetPicture("Sheet1", "F21") + file, raw, err := xlsx.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)) { @@ -826,12 +812,14 @@ func TestGetPicture(t *testing.T) { } // Try to get picture from a worksheet that doesn't contain any images. - file, raw = xlsx.GetPicture("Sheet3", "I9") + file, raw, err = xlsx.GetPicture("Sheet3", "I9") + assert.NoError(t, err) assert.Empty(t, file) assert.Empty(t, raw) // Try to get picture from a cell that doesn't contain an image. - file, raw = xlsx.GetPicture("Sheet2", "A2") + file, raw, err = xlsx.GetPicture("Sheet2", "A2") + assert.NoError(t, err) assert.Empty(t, file) assert.Empty(t, raw) @@ -850,7 +838,8 @@ func TestGetPicture(t *testing.T) { t.FailNow() } - file, raw = xlsx.GetPicture("Sheet1", "F21") + file, raw, err = xlsx.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)) { @@ -858,7 +847,8 @@ func TestGetPicture(t *testing.T) { } // Try to get picture from a local storage file that doesn't contain an image. - file, raw = xlsx.GetPicture("Sheet1", "F22") + file, raw, err = xlsx.GetPicture("Sheet1", "F22") + assert.NoError(t, err) assert.Empty(t, file) assert.Empty(t, raw) } @@ -913,7 +903,9 @@ func TestCopySheet(t *testing.T) { } xlsx.SetCellValue("Sheet4", "F1", "Hello") - assert.NotEqual(t, "Hello", xlsx.GetCellValue("Sheet1", "F1")) + val, err := xlsx.GetCellValue("Sheet1", "F1") + assert.NoError(t, err) + assert.NotEqual(t, "Hello", val) assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestCopySheet.xlsx"))) } @@ -1101,7 +1093,7 @@ func TestInsertCol(t *testing.T) { t.FailNow() } - xlsx.InsertCol(sheet1, "A") + assert.NoError(t, xlsx.InsertCol(sheet1, "A")) assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestInsertCol.xlsx"))) } @@ -1118,8 +1110,8 @@ func TestRemoveCol(t *testing.T) { xlsx.MergeCell(sheet1, "A1", "B1") xlsx.MergeCell(sheet1, "A2", "B2") - xlsx.RemoveCol(sheet1, "A") - xlsx.RemoveCol(sheet1, "A") + assert.NoError(t, xlsx.RemoveCol(sheet1, "A")) + assert.NoError(t, xlsx.RemoveCol(sheet1, "A")) assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestRemoveCol.xlsx"))) } @@ -1242,18 +1234,11 @@ func TestSetSheetRow(t *testing.T) { xlsx.SetSheetRow("Sheet1", "B27", &[]interface{}{"cell", nil, int32(42), float64(42), time.Now().UTC()}) - assert.Panics(t, func() { - xlsx.SetSheetRow("Sheet1", "", &[]interface{}{"cell", nil, 2}) - }) - - assert.Panics(t, func() { - xlsx.SetSheetRow("Sheet1", "B27", []interface{}{}) - }) - - assert.Panics(t, func() { - xlsx.SetSheetRow("Sheet1", "B27", &xlsx) - }) + assert.EqualError(t, xlsx.SetSheetRow("Sheet1", "", &[]interface{}{"cell", nil, 2}), + `cannot convert cell "" to coordinates: invalid cell name ""`) + assert.EqualError(t, xlsx.SetSheetRow("Sheet1", "B27", []interface{}{}), `pointer to slice expected`) + assert.EqualError(t, xlsx.SetSheetRow("Sheet1", "B27", &xlsx), `pointer to slice expected`) assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetSheetRow.xlsx"))) } @@ -1267,16 +1252,15 @@ func TestOutlineLevel(t *testing.T) { xlsx.SetColOutlineLevel("Sheet2", "B", 2) xlsx.SetRowOutlineLevel("Sheet1", 2, 250) - assert.Panics(t, func() { - xlsx.SetRowOutlineLevel("Sheet1", 0, 1) - }) + assert.EqualError(t, xlsx.SetRowOutlineLevel("Sheet1", 0, 1), "invalid row number 0") + level, err := xlsx.GetRowOutlineLevel("Sheet1", 2) + assert.NoError(t, err) + assert.Equal(t, uint8(250), level) - assert.Equal(t, uint8(250), xlsx.GetRowOutlineLevel("Sheet1", 2)) + _, err = xlsx.GetRowOutlineLevel("Sheet1", 0) + assert.EqualError(t, err, `invalid row number 0`) - assert.Panics(t, func() { - xlsx.GetRowOutlineLevel("Sheet1", 0) - }) - err := xlsx.SaveAs(filepath.Join("test", "TestOutlineLevel.xlsx")) + err = xlsx.SaveAs(filepath.Join("test", "TestOutlineLevel.xlsx")) if !assert.NoError(t, err) { t.FailNow() } @@ -1418,7 +1402,7 @@ func prepareTestBook4() (*File, error) { func fillCells(xlsx *File, sheet string, colCount, rowCount int) { for col := 1; col <= colCount; col++ { for row := 1; row <= rowCount; row++ { - cell := MustCoordinatesToCellName(col, row) + cell, _ := CoordinatesToCellName(col, row) xlsx.SetCellStr(sheet, cell, cell) } } diff --git a/lib.go b/lib.go index 809a16bd90..ad4f79a47c 100644 --- a/lib.go +++ b/lib.go @@ -135,22 +135,6 @@ func ColumnNameToNumber(name string) (int, error) { return col, nil } -// MustColumnNameToNumber provides a function to convert Excel sheet column -// name to int. Column name case insencitive. -// Function returns error if column name incorrect. -// -// Example: -// -// excelize.MustColumnNameToNumber("AK") // returns 37 -// -func MustColumnNameToNumber(name string) int { - n, err := ColumnNameToNumber(name) - if err != nil { - panic(err) - } - return n -} - // ColumnNumberToName provides a function to convert integer // to Excel sheet column title. // @@ -174,8 +158,9 @@ func ColumnNumberToName(num int) (string, error) { // to [X, Y] coordinates or retrusn an error. // // Example: -// CellCoordinates("A1") // returns 1, 1, nil -// CellCoordinates("Z3") // returns 26, 3, nil +// +// CellCoordinates("A1") // returns 1, 1, nil +// CellCoordinates("Z3") // returns 26, 3, nil // func CellNameToCoordinates(cell string) (int, int, error) { const msg = "cannot convert cell %q to coordinates: %v" @@ -193,25 +178,12 @@ func CellNameToCoordinates(cell string) (int, int, error) { return col, row, nil } -// MustCellNameToCoordinates converts alpha-numeric cell name -// to [X, Y] coordinates or panics. +// CoordinatesToCellName converts [X, Y] coordinates to alpha-numeric cell +// name or returns an error. // // Example: -// MustCellNameToCoordinates("A1") // returns 1, 1 -// MustCellNameToCoordinates("Z3") // returns 26, 3 // -func MustCellNameToCoordinates(cell string) (int, int) { - c, r, err := CellNameToCoordinates(cell) - if err != nil { - panic(err) - } - return c, r -} - -// CoordinatesToCellName converts [X, Y] coordinates to alpha-numeric cell name or returns an error. -// -// Example: -// CoordinatesToCellName(1, 1) // returns "A1", nil +// CoordinatesToCellName(1, 1) // returns "A1", nil // func CoordinatesToCellName(col, row int) (string, error) { if col < 1 || row < 1 { @@ -224,19 +196,6 @@ func CoordinatesToCellName(col, row int) (string, error) { return fmt.Sprintf("%s%d", colname, row), nil } -// MustCoordinatesToCellName converts [X, Y] coordinates to alpha-numeric cell name or panics. -// -// Example: -// MustCoordinatesToCellName(1, 1) // returns "A1" -// -func MustCoordinatesToCellName(col, row int) string { - n, err := CoordinatesToCellName(col, row) - if err != nil { - panic(err) - } - return n -} - // boolPtr returns a pointer to a bool with the given value. func boolPtr(b bool) *bool { return &b } diff --git a/lib_test.go b/lib_test.go index 4c19f7344c..1c30c0e1cd 100644 --- a/lib_test.go +++ b/lib_test.go @@ -71,9 +71,6 @@ func TestColumnNameToNumber_Error(t *testing.T) { if assert.Errorf(t, err, msg, col.Name) { assert.Equalf(t, col.Num, out, msg, col.Name) } - assert.Panicsf(t, func() { - MustColumnNameToNumber(col.Name) - }, msg, col.Name) } } @@ -174,9 +171,6 @@ func TestCellNameToCoordinates_Error(t *testing.T) { assert.Equalf(t, -1, c, msg, cell) assert.Equalf(t, -1, r, msg, cell) } - assert.Panicsf(t, func() { - MustCellNameToCoordinates(cell) - }, msg, cell) } } @@ -199,9 +193,6 @@ func TestCoordinatesToCellName_Error(t *testing.T) { if assert.Errorf(t, err, msg, col, row) { assert.Equalf(t, "", cell, msg, col, row) } - assert.Panicsf(t, func() { - MustCoordinatesToCellName(col, row) - }, msg, col, row) } for _, col := range invalidIndexes { diff --git a/picture.go b/picture.go index d78bcdfb95..9a9ff09ed6 100644 --- a/picture.go +++ b/picture.go @@ -162,7 +162,10 @@ func (f *File) AddPictureFromBytes(sheet, cell, format, name, extension string, } drawingHyperlinkRID = f.addDrawingRelationships(drawingID, SourceRelationshipHyperLink, formatSet.Hyperlink, hyperlinkType) } - 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, formatSet) + if err != nil { + return err + } f.addMedia(file, ext) f.addContentTypePart(drawingID, "drawings") return err @@ -270,8 +273,11 @@ 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 string, width, height, rID, hyperlinkRID int, formatSet *formatPicture) { - col, row := MustCellNameToCoordinates(cell) +func (f *File) addDrawingPicture(sheet, drawingXML, cell, file string, width, height, rID, hyperlinkRID int, formatSet *formatPicture) error { + col, row, err := CellNameToCoordinates(cell) + if err != nil { + return err + } width = int(float64(width) * formatSet.XScale) height = int(float64(height) * formatSet.YScale) col-- @@ -315,6 +321,7 @@ func (f *File) addDrawingPicture(sheet, drawingXML, cell, file string, width, he } content.TwoCellAnchor = append(content.TwoCellAnchor, &twoCellAnchor) f.Drawings[drawingXML] = content + return err } // addDrawingRelationships provides a function to add image part relationships @@ -468,7 +475,7 @@ func (f *File) getSheetRelationshipsTargetByID(sheet, rID string) string { // fmt.Println(err) // return // } -// file, raw := xlsx.GetPicture("Sheet1", "A2") +// file, raw, err := xlsx.GetPicture("Sheet1", "A2") // if file == "" { // return // } @@ -477,13 +484,16 @@ func (f *File) getSheetRelationshipsTargetByID(sheet, rID string) string { // fmt.Println(err) // } // -func (f *File) GetPicture(sheet, cell string) (string, []byte) { - col, row := MustCellNameToCoordinates(cell) +func (f *File) GetPicture(sheet, cell string) (string, []byte, error) { + col, row, err := CellNameToCoordinates(cell) + if err != nil { + return "", []byte{}, err + } col-- row-- xlsx := f.workSheetReader(sheet) if xlsx.Drawing == nil { - return "", []byte{} + return "", []byte{}, err } target := f.getSheetRelationshipsTargetByID(sheet, xlsx.Drawing.RID) @@ -503,7 +513,7 @@ func (f *File) GetPicture(sheet, cell string) (string, []byte) { if ok { return filepath.Base(xlsxWorkbookRelation.Target), []byte(f.XLSX[strings.Replace(xlsxWorkbookRelation.Target, - "..", "xl", -1)]) + "..", "xl", -1)]), err } } } @@ -511,7 +521,7 @@ func (f *File) GetPicture(sheet, cell string) (string, []byte) { _, ok := f.XLSX[drawingXML] if !ok { - return "", nil + return "", nil, err } decodeWsDr := decodeWsDr{} _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML(drawingXML)), &decodeWsDr) @@ -523,12 +533,12 @@ func (f *File) GetPicture(sheet, cell string) (string, []byte) { xlsxWorkbookRelation := f.getDrawingRelationships(drawingRelationships, decodeTwoCellAnchor.Pic.BlipFill.Blip.Embed) _, ok := supportImageTypes[filepath.Ext(xlsxWorkbookRelation.Target)] if ok { - return filepath.Base(xlsxWorkbookRelation.Target), []byte(f.XLSX[strings.Replace(xlsxWorkbookRelation.Target, "..", "xl", -1)]) + return filepath.Base(xlsxWorkbookRelation.Target), []byte(f.XLSX[strings.Replace(xlsxWorkbookRelation.Target, "..", "xl", -1)]), err } } } } - return "", []byte{} + return "", []byte{}, err } // getDrawingRelationships provides a function to get drawing relationships diff --git a/picture_test.go b/picture_test.go index 97d3cd9156..8c8d2e4049 100644 --- a/picture_test.go +++ b/picture_test.go @@ -4,17 +4,18 @@ import ( "fmt" _ "image/png" "io/ioutil" + "path/filepath" "testing" ) func BenchmarkAddPictureFromBytes(b *testing.B) { f := NewFile() - imgFile, err := ioutil.ReadFile("logo.png") + imgFile, err := ioutil.ReadFile(filepath.Join("test", "images", "excel.png")) if err != nil { - panic("unable to load image for benchmark") + b.Error("unable to load image for benchmark") } b.ResetTimer() for i := 1; i <= b.N; i++ { - f.AddPictureFromBytes("Sheet1", fmt.Sprint("A", i), "", "logo", ".png", imgFile) + f.AddPictureFromBytes("Sheet1", fmt.Sprint("A", i), "", "excel", ".png", imgFile) } } diff --git a/rows.go b/rows.go index 54c20467f4..5b8d7d8e29 100644 --- a/rows.go +++ b/rows.go @@ -21,17 +21,18 @@ import ( // GetRows return all the rows in a sheet by given worksheet name (case // sensitive). For example: // -// for _, row := range xlsx.GetRows("Sheet1") { +// rows, err := xlsx.GetRows("Sheet1") +// for _, row := range rows { // for _, colCell := range row { // fmt.Print(colCell, "\t") // } // fmt.Println() // } // -func (f *File) GetRows(sheet string) [][]string { +func (f *File) GetRows(sheet string) ([][]string, error) { name, ok := f.sheetMap[trimSheetName(sheet)] if !ok { - return nil + return nil, nil } xlsx := f.workSheetReader(sheet) @@ -47,7 +48,10 @@ func (f *File) GetRows(sheet string) [][]string { rowData xlsxRow ) - rowCount, colCount := f.getTotalRowsCols(name) + rowCount, colCount, err := f.getTotalRowsCols(name) + if err != nil { + return nil, nil + } rows := make([][]string, rowCount) for i := range rows { rows[i] = make([]string, colCount+1) @@ -68,7 +72,10 @@ func (f *File) GetRows(sheet string) [][]string { _ = decoder.DecodeElement(&rowData, &startElement) cr := rowData.R - 1 for _, colCell := range rowData.C { - col, _ := MustCellNameToCoordinates(colCell.R) + col, _, err := CellNameToCoordinates(colCell.R) + if err != nil { + return nil, err + } val, _ := colCell.getValueFrom(f, d) rows[cr][col-1] = val if val != "" { @@ -79,7 +86,7 @@ func (f *File) GetRows(sheet string) [][]string { default: } } - return rows[:row] + return rows[:row], nil } // Rows defines an iterator to a sheet @@ -117,9 +124,9 @@ func (rows *Rows) Error() error { } // Columns return the current row's column values -func (rows *Rows) Columns() []string { +func (rows *Rows) Columns() ([]string, error) { if rows.token == nil { - return []string{} + return []string{}, nil } startElement := rows.token.(xml.StartElement) r := xlsxRow{} @@ -127,11 +134,14 @@ func (rows *Rows) Columns() []string { d := rows.f.sharedStringsReader() columns := make([]string, len(r.C)) for _, colCell := range r.C { - col, _ := MustCellNameToCoordinates(colCell.R) + col, _, err := CellNameToCoordinates(colCell.R) + if err != nil { + return columns, err + } val, _ := colCell.getValueFrom(rows.f, d) columns[col-1] = val } - return columns + return columns, nil } // ErrSheetNotExist defines an error of sheet is not exist @@ -147,7 +157,8 @@ func (err ErrSheetNotExist) Error() string { // // rows, err := xlsx.Rows("Sheet1") // for rows.Next() { -// for _, colCell := range rows.Columns() { +// row, err := rows.Columns() +// for _, colCell := range row { // fmt.Print(colCell, "\t") // } // fmt.Println() @@ -171,7 +182,7 @@ func (f *File) Rows(sheet string) (*Rows, error) { // getTotalRowsCols provides a function to get total columns and rows in a // worksheet. -func (f *File) getTotalRowsCols(name string) (int, int) { +func (f *File) getTotalRowsCols(name string) (int, int, error) { decoder := xml.NewDecoder(bytes.NewReader(f.readXML(name))) var inElement string var r xlsxRow @@ -189,7 +200,10 @@ func (f *File) getTotalRowsCols(name string) (int, int) { _ = decoder.DecodeElement(&r, &startElement) tr = r.R for _, colCell := range r.C { - col, _ := MustCellNameToCoordinates(colCell.R) + col, _, err := CellNameToCoordinates(colCell.R) + if err != nil { + return tr, tc, err + } if col > tc { tc = col } @@ -198,17 +212,17 @@ func (f *File) getTotalRowsCols(name string) (int, int) { default: } } - return tr, tc + return tr, tc, nil } // SetRowHeight provides a function to set the height of a single row. For // example, set the height of the first row in Sheet1: // -// xlsx.SetRowHeight("Sheet1", 1, 50) +// err := xlsx.SetRowHeight("Sheet1", 1, 50) // -func (f *File) SetRowHeight(sheet string, row int, height float64) { +func (f *File) SetRowHeight(sheet string, row int, height float64) error { if row < 1 { - panic(newInvalidRowNumberError(row)) // Fail fast to avoid possible future side effects! + return newInvalidRowNumberError(row) } xlsx := f.workSheetReader(sheet) @@ -218,6 +232,7 @@ func (f *File) SetRowHeight(sheet string, row int, height float64) { rowIdx := row - 1 xlsx.SheetData.Row[rowIdx].Ht = height xlsx.SheetData.Row[rowIdx].CustomHeight = true + return nil } // getRowHeight provides a function to get row height in pixels by given sheet @@ -236,24 +251,24 @@ func (f *File) getRowHeight(sheet string, row int) int { // GetRowHeight provides a function to get row height by given worksheet name // and row index. For example, get the height of the first row in Sheet1: // -// xlsx.GetRowHeight("Sheet1", 1) +// height, err := xlsx.GetRowHeight("Sheet1", 1) // -func (f *File) GetRowHeight(sheet string, row int) float64 { +func (f *File) GetRowHeight(sheet string, row int) (float64, error) { if row < 1 { - panic(newInvalidRowNumberError(row)) // Fail fast to avoid possible future side effects! + return defaultRowHeightPixels, newInvalidRowNumberError(row) } xlsx := f.workSheetReader(sheet) if row > len(xlsx.SheetData.Row) { - return defaultRowHeightPixels // it will be better to use 0, but we take care with BC + return defaultRowHeightPixels, nil // it will be better to use 0, but we take care with BC } for _, v := range xlsx.SheetData.Row { if v.R == row && v.Ht != 0 { - return v.Ht + return v.Ht, nil } } // Optimisation for when the row heights haven't changed. - return defaultRowHeightPixels + return defaultRowHeightPixels, nil } // sharedStringsReader provides a function to get the pointer to the structure @@ -299,138 +314,140 @@ func (xlsx *xlsxC) getValueFrom(f *File, d *xlsxSST) (string, error) { // 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: // -// xlsx.SetRowVisible("Sheet1", 2, false) +// err := xlsx.SetRowVisible("Sheet1", 2, false) // -func (f *File) SetRowVisible(sheet string, row int, visible bool) { +func (f *File) SetRowVisible(sheet string, row int, visible bool) error { if row < 1 { - panic(newInvalidRowNumberError(row)) // Fail fast to avoid possible future side effects! + return newInvalidRowNumberError(row) } xlsx := f.workSheetReader(sheet) prepareSheetXML(xlsx, 0, row) xlsx.SheetData.Row[row-1].Hidden = !visible + return nil } // GetRowVisible provides a function to get visible of a single row by given // worksheet name and Excel row number. For example, get visible state of row // 2 in Sheet1: // -// xlsx.GetRowVisible("Sheet1", 2) +// visible, err := xlsx.GetRowVisible("Sheet1", 2) // -func (f *File) GetRowVisible(sheet string, row int) bool { +func (f *File) GetRowVisible(sheet string, row int) (bool, error) { if row < 1 { - panic(newInvalidRowNumberError(row)) // Fail fast to avoid possible future side effects! + return false, newInvalidRowNumberError(row) } xlsx := f.workSheetReader(sheet) if row > len(xlsx.SheetData.Row) { - return false + return false, nil } - return !xlsx.SheetData.Row[row-1].Hidden + return !xlsx.SheetData.Row[row-1].Hidden, nil } // SetRowOutlineLevel provides a function to set outline level number of a // single row by given worksheet name and Excel row number. For example, // outline row 2 in Sheet1 to level 1: // -// xlsx.SetRowOutlineLevel("Sheet1", 2, 1) +// err := xlsx.SetRowOutlineLevel("Sheet1", 2, 1) // -func (f *File) SetRowOutlineLevel(sheet string, row int, level uint8) { +func (f *File) SetRowOutlineLevel(sheet string, row int, level uint8) error { if row < 1 { - panic(newInvalidRowNumberError(row)) // Fail fast to avoid possible future side effects! + return newInvalidRowNumberError(row) } xlsx := f.workSheetReader(sheet) prepareSheetXML(xlsx, 0, row) xlsx.SheetData.Row[row-1].OutlineLevel = level + return nil } // GetRowOutlineLevel provides a function to get outline level number of a // single row by given worksheet name and Excel row number. For example, get // outline number of row 2 in Sheet1: // -// xlsx.GetRowOutlineLevel("Sheet1", 2) +// level, err := xlsx.GetRowOutlineLevel("Sheet1", 2) // -func (f *File) GetRowOutlineLevel(sheet string, row int) uint8 { +func (f *File) GetRowOutlineLevel(sheet string, row int) (uint8, error) { if row < 1 { - panic(newInvalidRowNumberError(row)) // Fail fast to avoid possible future side effects! + return 0, newInvalidRowNumberError(row) } xlsx := f.workSheetReader(sheet) if row > len(xlsx.SheetData.Row) { - return 0 + return 0, nil } - return xlsx.SheetData.Row[row-1].OutlineLevel + return xlsx.SheetData.Row[row-1].OutlineLevel, nil } // RemoveRow provides a function to remove single row by given worksheet name // and Excel row number. For example, remove row 3 in Sheet1: // -// xlsx.RemoveRow("Sheet1", 3) +// err := xlsx.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 // worksheet, it will cause a file error when you open it. The excelize only // partially updates these references currently. -func (f *File) RemoveRow(sheet string, row int) { +func (f *File) RemoveRow(sheet string, row int) error { if row < 1 { - panic(newInvalidRowNumberError(row)) // Fail fast to avoid possible future side effects! + return newInvalidRowNumberError(row) } xlsx := f.workSheetReader(sheet) if row > len(xlsx.SheetData.Row) { - return + return nil } for rowIdx := range xlsx.SheetData.Row { if xlsx.SheetData.Row[rowIdx].R == row { xlsx.SheetData.Row = append(xlsx.SheetData.Row[:rowIdx], xlsx.SheetData.Row[rowIdx+1:]...)[:len(xlsx.SheetData.Row)-1] - f.adjustHelper(sheet, rows, row, -1) - return + return f.adjustHelper(sheet, rows, row, -1) } } + return nil } // 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: // -// xlsx.InsertRow("Sheet1", 3) +// err := elsx.InsertRow("Sheet1", 3) // -func (f *File) InsertRow(sheet string, row int) { +func (f *File) InsertRow(sheet string, row int) error { if row < 1 { - panic(newInvalidRowNumberError(row)) // Fail fast to avoid possible future side effects! + return newInvalidRowNumberError(row) } - f.adjustHelper(sheet, rows, row, 1) + return f.adjustHelper(sheet, rows, row, 1) } // DuplicateRow inserts a copy of specified row (by it Excel row number) below // -// xlsx.DuplicateRow("Sheet1", 2) +// err := xlsx.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 // worksheet, it will cause a file error when you open it. The excelize only // partially updates these references currently. -func (f *File) DuplicateRow(sheet string, row int) { - f.DuplicateRowTo(sheet, row, row+1) +func (f *File) DuplicateRow(sheet string, row int) error { + return f.DuplicateRowTo(sheet, row, row+1) } // DuplicateRowTo inserts a copy of specified row by it Excel number // to specified row position moving down exists rows after target position // -// xlsx.DuplicateRowTo("Sheet1", 2, 7) +// err := xlsx.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 // worksheet, it will cause a file error when you open it. The excelize only // partially updates these references currently. -func (f *File) DuplicateRowTo(sheet string, row, row2 int) { +func (f *File) DuplicateRowTo(sheet string, row, row2 int) error { if row < 1 { - panic(newInvalidRowNumberError(row)) // Fail fast to avoid possible future side effects! + return newInvalidRowNumberError(row) } xlsx := f.workSheetReader(sheet) if row > len(xlsx.SheetData.Row) || row2 < 1 || row == row2 { - return + return nil } var ok bool @@ -444,10 +461,12 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) { } } if !ok { - return + return nil } - f.adjustHelper(sheet, rows, row2, 1) + if err := f.adjustHelper(sheet, rows, row2, 1); err != nil { + return err + } idx2 := -1 for i, r := range xlsx.SheetData.Row { @@ -457,7 +476,7 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) { } } if idx2 == -1 && len(xlsx.SheetData.Row) >= row2 { - return + return nil } rowCopy.C = append(make([]xlsxC, 0, len(rowCopy.C)), rowCopy.C...) @@ -468,6 +487,7 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) { } else { xlsx.SheetData.Row = append(xlsx.SheetData.Row, rowCopy) } + return nil } // checkRow provides a function to check and fill each column element for all @@ -494,7 +514,7 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) { // // Noteice: this method could be very slow for large spreadsheets (more than // 3000 rows one sheet). -func checkRow(xlsx *xlsxWorksheet) { +func checkRow(xlsx *xlsxWorksheet) error { for rowIdx := range xlsx.SheetData.Row { rowData := &xlsx.SheetData.Row[rowIdx] @@ -502,7 +522,10 @@ func checkRow(xlsx *xlsxWorksheet) { if colCount == 0 { continue } - lastCol, _ := MustCellNameToCoordinates(rowData.C[colCount-1].R) + lastCol, _, err := CellNameToCoordinates(rowData.C[colCount-1].R) + if err != nil { + return err + } if colCount < lastCol { oldList := rowData.C @@ -511,18 +534,26 @@ func checkRow(xlsx *xlsxWorksheet) { rowData.C = xlsx.SheetData.Row[rowIdx].C[:0] for colIdx := 0; colIdx < lastCol; colIdx++ { - newlist = append(newlist, xlsxC{R: MustCoordinatesToCellName(colIdx+1, rowIdx+1)}) + cellName, err := CoordinatesToCellName(colIdx+1, rowIdx+1) + if err != nil { + return err + } + newlist = append(newlist, xlsxC{R: cellName}) } rowData.C = newlist for colIdx := range oldList { colData := &oldList[colIdx] - colNum, _ := MustCellNameToCoordinates(colData.R) + colNum, _, err := CellNameToCoordinates(colData.R) + if err != nil { + return err + } xlsx.SheetData.Row[rowIdx].C[colNum-1] = *colData } } } + return nil } // convertRowHeightToPixels provides a function to convert the height of a diff --git a/rows_test.go b/rows_test.go index 50e26dd2eb..53c06772b0 100644 --- a/rows_test.go +++ b/rows_test.go @@ -23,13 +23,16 @@ func TestRows(t *testing.T) { collectedRows := make([][]string, 0) for rows.Next() { - collectedRows = append(collectedRows, trimSliceSpace(rows.Columns())) + columns, err := rows.Columns() + assert.NoError(t, err) + collectedRows = append(collectedRows, trimSliceSpace(columns)) } if !assert.NoError(t, rows.Error()) { t.FailNow() } - returnedRows := xlsx.GetRows(sheet2) + returnedRows, err := xlsx.GetRows(sheet2) + assert.NoError(t, err) for i := range returnedRows { returnedRows[i] = trimSliceSpace(returnedRows[i]) } @@ -54,21 +57,22 @@ func TestRowHeight(t *testing.T) { xlsx := NewFile() sheet1 := xlsx.GetSheetName(1) - assert.Panics(t, func() { - xlsx.SetRowHeight(sheet1, 0, defaultRowHeightPixels+1.0) - }) + assert.EqualError(t, xlsx.SetRowHeight(sheet1, 0, defaultRowHeightPixels+1.0), "invalid row number 0") - assert.Panics(t, func() { - xlsx.GetRowHeight("Sheet1", 0) - }) + height, err := xlsx.GetRowHeight("Sheet1", 0) + assert.EqualError(t, err, "invalid row number 0") - xlsx.SetRowHeight(sheet1, 1, 111.0) - assert.Equal(t, 111.0, xlsx.GetRowHeight(sheet1, 1)) + assert.NoError(t, xlsx.SetRowHeight(sheet1, 1, 111.0)) + height, err = xlsx.GetRowHeight(sheet1, 1) + assert.NoError(t, err) + assert.Equal(t, 111.0, height) - xlsx.SetRowHeight(sheet1, 4, 444.0) - assert.Equal(t, 444.0, xlsx.GetRowHeight(sheet1, 4)) + assert.NoError(t, xlsx.SetRowHeight(sheet1, 4, 444.0)) + height, err = xlsx.GetRowHeight(sheet1, 4) + assert.NoError(t, err) + assert.Equal(t, 444.0, height) - err := xlsx.SaveAs(filepath.Join("test", "TestRowHeight.xlsx")) + err = xlsx.SaveAs(filepath.Join("test", "TestRowHeight.xlsx")) if !assert.NoError(t, err) { t.FailNow() } @@ -86,13 +90,11 @@ func TestRowVisibility(t *testing.T) { xlsx.SetRowVisible("Sheet3", 2, true) xlsx.GetRowVisible("Sheet3", 2) - assert.Panics(t, func() { - xlsx.SetRowVisible("Sheet3", 0, true) - }) + assert.EqualError(t, xlsx.SetRowVisible("Sheet3", 0, true), "invalid row number 0") - assert.Panics(t, func() { - xlsx.GetRowVisible("Sheet3", 0) - }) + visible, err := xlsx.GetRowVisible("Sheet3", 0) + assert.Equal(t, false, visible) + assert.EqualError(t, err, "invalid row number 0") assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestRowVisibility.xlsx"))) } @@ -110,27 +112,23 @@ func TestRemoveRow(t *testing.T) { xlsx.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External") - assert.Panics(t, func() { - xlsx.RemoveRow(sheet1, -1) - }) + assert.EqualError(t, xlsx.RemoveRow(sheet1, -1), "invalid row number -1") - assert.Panics(t, func() { - xlsx.RemoveRow(sheet1, 0) - }) + assert.EqualError(t, xlsx.RemoveRow(sheet1, 0), "invalid row number 0") - xlsx.RemoveRow(sheet1, 4) + assert.NoError(t, xlsx.RemoveRow(sheet1, 4)) if !assert.Len(t, r.SheetData.Row, rowCount-1) { t.FailNow() } xlsx.MergeCell(sheet1, "B3", "B5") - xlsx.RemoveRow(sheet1, 2) + assert.NoError(t, xlsx.RemoveRow(sheet1, 2)) if !assert.Len(t, r.SheetData.Row, rowCount-2) { t.FailNow() } - xlsx.RemoveRow(sheet1, 4) + assert.NoError(t, xlsx.RemoveRow(sheet1, 4)) if !assert.Len(t, r.SheetData.Row, rowCount-3) { t.FailNow() } @@ -140,17 +138,17 @@ func TestRemoveRow(t *testing.T) { t.FailNow() } - xlsx.RemoveRow(sheet1, 1) + assert.NoError(t, xlsx.RemoveRow(sheet1, 1)) if !assert.Len(t, r.SheetData.Row, rowCount-4) { t.FailNow() } - xlsx.RemoveRow(sheet1, 2) + assert.NoError(t, xlsx.RemoveRow(sheet1, 2)) if !assert.Len(t, r.SheetData.Row, rowCount-5) { t.FailNow() } - xlsx.RemoveRow(sheet1, 1) + assert.NoError(t, xlsx.RemoveRow(sheet1, 1)) if !assert.Len(t, r.SheetData.Row, rowCount-6) { t.FailNow() } @@ -171,20 +169,16 @@ func TestInsertRow(t *testing.T) { xlsx.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External") - assert.Panics(t, func() { - xlsx.InsertRow(sheet1, -1) - }) + assert.EqualError(t, xlsx.InsertRow(sheet1, -1), "invalid row number -1") - assert.Panics(t, func() { - xlsx.InsertRow(sheet1, 0) - }) + assert.EqualError(t, xlsx.InsertRow(sheet1, 0), "invalid row number 0") - xlsx.InsertRow(sheet1, 1) + assert.NoError(t, xlsx.InsertRow(sheet1, 1)) if !assert.Len(t, r.SheetData.Row, rowCount+1) { t.FailNow() } - xlsx.InsertRow(sheet1, 4) + assert.NoError(t, xlsx.InsertRow(sheet1, 4)) if !assert.Len(t, r.SheetData.Row, rowCount+2) { t.FailNow() } @@ -198,11 +192,11 @@ func TestInsertRowInEmptyFile(t *testing.T) { xlsx := NewFile() sheet1 := xlsx.GetSheetName(1) r := xlsx.workSheetReader(sheet1) - xlsx.InsertRow(sheet1, 1) + assert.NoError(t, xlsx.InsertRow(sheet1, 1)) assert.Len(t, r.SheetData.Row, 0) - xlsx.InsertRow(sheet1, 2) + assert.NoError(t, xlsx.InsertRow(sheet1, 2)) assert.Len(t, r.SheetData.Row, 0) - xlsx.InsertRow(sheet1, 99) + assert.NoError(t, xlsx.InsertRow(sheet1, 99)) assert.Len(t, r.SheetData.Row, 0) assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestInsertRowInEmptyFile.xlsx"))) } @@ -234,7 +228,7 @@ func TestDuplicateRow(t *testing.T) { xlsx.SetCellStr(sheet, "A1", cells["A1"]) xlsx.SetCellStr(sheet, "B1", cells["B1"]) - xlsx.DuplicateRow(sheet, 1) + assert.NoError(t, xlsx.DuplicateRow(sheet, 1)) if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.FromSingleRow_1"))) { t.FailNow() } @@ -243,12 +237,14 @@ func TestDuplicateRow(t *testing.T) { "A2": cells["A1"], "B2": cells["B1"], } for cell, val := range expect { - if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { + v, err := xlsx.GetCellValue(sheet, cell) + assert.NoError(t, err) + if !assert.Equal(t, val, v, cell) { t.FailNow() } } - xlsx.DuplicateRow(sheet, 2) + assert.NoError(t, xlsx.DuplicateRow(sheet, 2)) if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.FromSingleRow_2"))) { t.FailNow() } @@ -258,7 +254,9 @@ func TestDuplicateRow(t *testing.T) { "A3": cells["A1"], "B3": cells["B1"], } for cell, val := range expect { - if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { + v, err := xlsx.GetCellValue(sheet, cell) + assert.NoError(t, err) + if !assert.Equal(t, val, v, cell) { t.FailNow() } } @@ -269,7 +267,7 @@ func TestDuplicateRow(t *testing.T) { xlsx.SetCellStr(sheet, "A1", cells["A1"]) xlsx.SetCellStr(sheet, "B1", cells["B1"]) - xlsx.DuplicateRow(sheet, 1) + assert.NoError(t, xlsx.DuplicateRow(sheet, 1)) xlsx.SetCellStr(sheet, "A2", cells["A2"]) xlsx.SetCellStr(sheet, "B2", cells["B2"]) @@ -282,7 +280,9 @@ func TestDuplicateRow(t *testing.T) { "A2": cells["A2"], "B2": cells["B2"], } for cell, val := range expect { - if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { + v, err := xlsx.GetCellValue(sheet, cell) + assert.NoError(t, err) + if !assert.Equal(t, val, v, cell) { t.FailNow() } } @@ -291,7 +291,7 @@ func TestDuplicateRow(t *testing.T) { t.Run("FirstOfMultipleRows", func(t *testing.T) { xlsx := newFileWithDefaults() - xlsx.DuplicateRow(sheet, 1) + assert.NoError(t, xlsx.DuplicateRow(sheet, 1)) if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.FirstOfMultipleRows"))) { t.FailNow() @@ -303,7 +303,9 @@ func TestDuplicateRow(t *testing.T) { "A4": cells["A3"], "B4": cells["B3"], } for cell, val := range expect { - if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { + v, err := xlsx.GetCellValue(sheet, cell) + assert.NoError(t, err) + if !assert.Equal(t, val, v, cell) { t.FailNow() } } @@ -312,26 +314,35 @@ func TestDuplicateRow(t *testing.T) { t.Run("ZeroWithNoRows", func(t *testing.T) { xlsx := NewFile() - assert.Panics(t, func() { - xlsx.DuplicateRow(sheet, 0) - }) + assert.EqualError(t, xlsx.DuplicateRow(sheet, 0), "invalid row number 0") if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.ZeroWithNoRows"))) { t.FailNow() } - assert.Equal(t, "", xlsx.GetCellValue(sheet, "A1")) - assert.Equal(t, "", xlsx.GetCellValue(sheet, "B1")) - assert.Equal(t, "", xlsx.GetCellValue(sheet, "A2")) - assert.Equal(t, "", xlsx.GetCellValue(sheet, "B2")) - + val, err := xlsx.GetCellValue(sheet, "A1") + assert.NoError(t, err) + assert.Equal(t, "", val) + val, err = xlsx.GetCellValue(sheet, "B1") + assert.NoError(t, err) + assert.Equal(t, "", val) + val, err = xlsx.GetCellValue(sheet, "A2") + assert.NoError(t, err) + assert.Equal(t, "", val) + val, err = xlsx.GetCellValue(sheet, "B2") + assert.NoError(t, err) + assert.Equal(t, "", val) + + assert.NoError(t, err) expect := map[string]string{ "A1": "", "B1": "", "A2": "", "B2": "", } for cell, val := range expect { - if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { + v, err := xlsx.GetCellValue(sheet, cell) + assert.NoError(t, err) + if !assert.Equal(t, val, v, cell) { t.FailNow() } } @@ -340,7 +351,7 @@ func TestDuplicateRow(t *testing.T) { t.Run("MiddleRowOfEmptyFile", func(t *testing.T) { xlsx := NewFile() - xlsx.DuplicateRow(sheet, 99) + assert.NoError(t, xlsx.DuplicateRow(sheet, 99)) if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.MiddleRowOfEmptyFile"))) { t.FailNow() @@ -351,7 +362,9 @@ func TestDuplicateRow(t *testing.T) { "A100": "", } for cell, val := range expect { - if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { + v, err := xlsx.GetCellValue(sheet, cell) + assert.NoError(t, err) + if !assert.Equal(t, val, v, cell) { t.FailNow() } } @@ -360,7 +373,7 @@ func TestDuplicateRow(t *testing.T) { t.Run("WithLargeOffsetToMiddleOfData", func(t *testing.T) { xlsx := newFileWithDefaults() - xlsx.DuplicateRowTo(sheet, 1, 3) + assert.NoError(t, xlsx.DuplicateRowTo(sheet, 1, 3)) if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.WithLargeOffsetToMiddleOfData"))) { t.FailNow() @@ -372,7 +385,9 @@ func TestDuplicateRow(t *testing.T) { "A4": cells["A3"], "B4": cells["B3"], } for cell, val := range expect { - if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { + v, err := xlsx.GetCellValue(sheet, cell) + assert.NoError(t, err) + if !assert.Equal(t, val, v, cell) { t.FailNow() } } @@ -381,7 +396,7 @@ func TestDuplicateRow(t *testing.T) { t.Run("WithLargeOffsetToEmptyRows", func(t *testing.T) { xlsx := newFileWithDefaults() - xlsx.DuplicateRowTo(sheet, 1, 7) + assert.NoError(t, xlsx.DuplicateRowTo(sheet, 1, 7)) if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.WithLargeOffsetToEmptyRows"))) { t.FailNow() @@ -393,7 +408,9 @@ func TestDuplicateRow(t *testing.T) { "A7": cells["A1"], "B7": cells["B1"], } for cell, val := range expect { - if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { + v, err := xlsx.GetCellValue(sheet, cell) + assert.NoError(t, err) + if !assert.Equal(t, val, v, cell) { t.FailNow() } } @@ -402,7 +419,7 @@ func TestDuplicateRow(t *testing.T) { t.Run("InsertBefore", func(t *testing.T) { xlsx := newFileWithDefaults() - xlsx.DuplicateRowTo(sheet, 2, 1) + assert.NoError(t, xlsx.DuplicateRowTo(sheet, 2, 1)) if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.InsertBefore"))) { t.FailNow() @@ -415,7 +432,9 @@ func TestDuplicateRow(t *testing.T) { "A4": cells["A3"], "B4": cells["B3"], } for cell, val := range expect { - if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell), cell) { + v, err := xlsx.GetCellValue(sheet, cell) + assert.NoError(t, err) + if !assert.Equal(t, val, v, cell) { t.FailNow() } } @@ -424,7 +443,7 @@ func TestDuplicateRow(t *testing.T) { t.Run("InsertBeforeWithLargeOffset", func(t *testing.T) { xlsx := newFileWithDefaults() - xlsx.DuplicateRowTo(sheet, 3, 1) + assert.NoError(t, xlsx.DuplicateRowTo(sheet, 3, 1)) if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.InsertBeforeWithLargeOffset"))) { t.FailNow() @@ -437,7 +456,9 @@ func TestDuplicateRow(t *testing.T) { "A4": cells["A3"], "B4": cells["B3"], } for cell, val := range expect { - if !assert.Equal(t, val, xlsx.GetCellValue(sheet, cell)) { + v, err := xlsx.GetCellValue(sheet, cell) + assert.NoError(t, err) + if !assert.Equal(t, val, v) { t.FailNow() } } @@ -467,12 +488,12 @@ func TestDuplicateRowInvalidRownum(t *testing.T) { xlsx.SetCellStr(sheet, col, val) } - assert.Panics(t, func() { - xlsx.DuplicateRow(sheet, row) - }) + assert.EqualError(t, xlsx.DuplicateRow(sheet, row), fmt.Sprintf("invalid row number %d", row)) for col, val := range cells { - if !assert.Equal(t, val, xlsx.GetCellValue(sheet, col)) { + v, err := xlsx.GetCellValue(sheet, col) + assert.NoError(t, err) + if !assert.Equal(t, val, v) { t.FailNow() } } @@ -489,12 +510,12 @@ func TestDuplicateRowInvalidRownum(t *testing.T) { xlsx.SetCellStr(sheet, col, val) } - assert.Panics(t, func() { - xlsx.DuplicateRowTo(sheet, row1, row2) - }) + assert.EqualError(t, xlsx.DuplicateRowTo(sheet, row1, row2), fmt.Sprintf("invalid row number %d", row1)) for col, val := range cells { - if !assert.Equal(t, val, xlsx.GetCellValue(sheet, col)) { + v, err := xlsx.GetCellValue(sheet, col) + assert.NoError(t, err) + if !assert.Equal(t, val, v) { t.FailNow() } } diff --git a/shape.go b/shape.go index c58038c7a2..e6b0456d80 100644 --- a/shape.go +++ b/shape.go @@ -275,15 +275,21 @@ func (f *File) AddShape(sheet, cell, format string) error { rID := f.addSheetRelationships(sheet, SourceRelationshipDrawingML, sheetRelationshipsDrawingXML, "") f.addSheetDrawing(sheet, rID) } - f.addDrawingShape(sheet, drawingXML, cell, formatSet) + err = f.addDrawingShape(sheet, drawingXML, cell, formatSet) + if err != nil { + return err + } f.addContentTypePart(drawingID, "drawings") return err } // addDrawingShape provides a function to add preset geometry by given sheet, // drawingXMLand format sets. -func (f *File) addDrawingShape(sheet, drawingXML, cell string, formatSet *formatShape) { - fromCol, fromRow := MustCellNameToCoordinates(cell) +func (f *File) addDrawingShape(sheet, drawingXML, cell string, formatSet *formatShape) error { + fromCol, fromRow, err := CellNameToCoordinates(cell) + if err != nil { + return err + } colIdx := fromCol - 1 rowIdx := fromRow - 1 @@ -421,6 +427,7 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, formatSet *format } content.TwoCellAnchor = append(content.TwoCellAnchor, &twoCellAnchor) f.Drawings[drawingXML] = content + return err } // setShapeRef provides a function to set color with hex model by given actual diff --git a/sheet.go b/sheet.go index ce1f24113d..ec4df774e4 100644 --- a/sheet.go +++ b/sheet.go @@ -275,11 +275,11 @@ func (f *File) GetActiveSheetIndex() int { return 0 } -// SetSheetName provides a function to set the worksheet name be given old and new -// worksheet name. 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. +// SetSheetName provides a function to set the worksheet name be given old and +// new worksheet name. 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) @@ -665,14 +665,14 @@ func (f *File) GetSheetVisible(name string) bool { // // An example of search the coordinates of the value of "100" on Sheet1: // -// xlsx.SearchSheet("Sheet1", "100") +// result, err := xlsx.SearchSheet("Sheet1", "100") // // An example of search the coordinates where the numerical value in the range // of "0-9" of Sheet1 is described: // -// xlsx.SearchSheet("Sheet1", "[0-9]", true) +// result, err := xlsx.SearchSheet("Sheet1", "[0-9]", true) // -func (f *File) SearchSheet(sheet, value string, reg ...bool) []string { +func (f *File) SearchSheet(sheet, value string, reg ...bool) ([]string, error) { var regSearch bool for _, r := range reg { regSearch = r @@ -683,7 +683,7 @@ func (f *File) SearchSheet(sheet, value string, reg ...bool) []string { ) name, ok := f.sheetMap[trimSheetName(sheet)] if !ok { - return result + return result, nil } if xlsx != nil { output, _ := xml.Marshal(f.Sheet[name]) @@ -718,14 +718,21 @@ func (f *File) SearchSheet(sheet, value string, reg ...bool) []string { } } - cellCol, _ := MustCellNameToCoordinates(colCell.R) - result = append(result, MustCoordinatesToCellName(cellCol, r.R)) + cellCol, _, err := CellNameToCoordinates(colCell.R) + if err != nil { + return result, err + } + cellName, err := CoordinatesToCellName(cellCol, r.R) + if err != nil { + return result, err + } + result = append(result, cellName) } } default: } } - return result + return result, nil } // ProtectSheet provides a function to prevent other users from accidentally diff --git a/styles.go b/styles.go index 50b30b8680..1cc025cc73 100644 --- a/styles.go +++ b/styles.go @@ -1880,8 +1880,8 @@ func parseFormatStyleSet(style string) (*formatStyle, error) { // // xlsx := excelize.NewFile() // xlsx.SetCellValue("Sheet1", "A6", 42920.5) -// style, _ := xlsx.NewStyle(`{"custom_number_format": "[$-380A]dddd\\,\\ dd\" de \"mmmm\" de \"yyyy;@"}`) -// xlsx.SetCellStyle("Sheet1", "A6", "A6", style) +// style, err := xlsx.NewStyle(`{"custom_number_format": "[$-380A]dddd\\,\\ dd\" de \"mmmm\" de \"yyyy;@"}`) +// err = xlsx.SetCellStyle("Sheet1", "A6", "A6", style) // // Cell Sheet1!A6 in the Excel Application: martes, 04 de Julio de 2017 // @@ -2265,10 +2265,13 @@ 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 { +func (f *File) GetCellStyle(sheet, axis string) (int, error) { xlsx := f.workSheetReader(sheet) - cellData, col, _ := f.prepareCell(xlsx, sheet, axis) - return f.prepareCellStyle(xlsx, col, cellData.S) + cellData, col, _, err := f.prepareCell(xlsx, sheet, axis) + if err != nil { + return 0, err + } + return f.prepareCellStyle(xlsx, col, cellData.S), err } // SetCellStyle provides a function to add style attribute for cells by given @@ -2282,7 +2285,7 @@ func (f *File) GetCellStyle(sheet, axis string) int { // if err != nil { // fmt.Println(err) // } -// xlsx.SetCellStyle("Sheet1", "H9", "H9", style) +// err = xlsx.SetCellStyle("Sheet1", "H9", "H9", style) // // Set gradient fill with vertical variants shading styles for cell H9 on // Sheet1: @@ -2291,7 +2294,7 @@ func (f *File) GetCellStyle(sheet, axis string) int { // if err != nil { // fmt.Println(err) // } -// xlsx.SetCellStyle("Sheet1", "H9", "H9", style) +// err = xlsx.SetCellStyle("Sheet1", "H9", "H9", style) // // Set solid style pattern fill for cell H9 on Sheet1: // @@ -2299,7 +2302,7 @@ func (f *File) GetCellStyle(sheet, axis string) int { // if err != nil { // fmt.Println(err) // } -// xlsx.SetCellStyle("Sheet1", "H9", "H9", style) +// err = xlsx.SetCellStyle("Sheet1", "H9", "H9", style) // // Set alignment style for cell H9 on Sheet1: // @@ -2307,7 +2310,7 @@ func (f *File) GetCellStyle(sheet, axis string) int { // if err != nil { // fmt.Println(err) // } -// xlsx.SetCellStyle("Sheet1", "H9", "H9", style) +// err = xlsx.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 @@ -2318,7 +2321,7 @@ func (f *File) GetCellStyle(sheet, axis string) int { // if err != nil { // fmt.Println(err) // } -// xlsx.SetCellStyle("Sheet1", "H9", "H9", style) +// err = xlsx.SetCellStyle("Sheet1", "H9", "H9", style) // // Set font style for cell H9 on Sheet1: // @@ -2326,7 +2329,7 @@ func (f *File) GetCellStyle(sheet, axis string) int { // if err != nil { // fmt.Println(err) // } -// xlsx.SetCellStyle("Sheet1", "H9", "H9", style) +// err = xlsx.SetCellStyle("Sheet1", "H9", "H9", style) // // Hide and lock for cell H9 on Sheet1: // @@ -2334,17 +2337,17 @@ func (f *File) GetCellStyle(sheet, axis string) int { // if err != nil { // fmt.Println(err) // } -// xlsx.SetCellStyle("Sheet1", "H9", "H9", style) +// err = xlsx.SetCellStyle("Sheet1", "H9", "H9", style) // -func (f *File) SetCellStyle(sheet, hcell, vcell string, styleID int) { +func (f *File) SetCellStyle(sheet, hcell, vcell string, styleID int) error { hcol, hrow, err := CellNameToCoordinates(hcell) if err != nil { - panic(err) + return err } vcol, vrow, err := CellNameToCoordinates(vcell) if err != nil { - panic(err) + return err } // Normalize the coordinate area, such correct C1:B3 to B1:C3. @@ -2370,6 +2373,7 @@ func (f *File) SetCellStyle(sheet, hcell, vcell string, styleID int) { xlsx.SheetData.Row[r].C[k].S = styleID } } + return nil } // SetConditionalFormat provides a function to create conditional formatting diff --git a/table.go b/table.go index e33264bbc1..2ed8654783 100644 --- a/table.go +++ b/table.go @@ -33,11 +33,11 @@ func parseFormatTableSet(formatSet string) (*formatTable, error) { // name, coordinate area and format set. For example, create a table of A1:D5 // on Sheet1: // -// xlsx.AddTable("Sheet1", "A1", "D5", ``) +// err := xlsx.AddTable("Sheet1", "A1", "D5", ``) // // Create a table of F2:H6 on Sheet2 with format set: // -// xlsx.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 := xlsx.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 at least two lines include string type header. Multiple // tables coordinate areas can't have an intersection. @@ -56,8 +56,14 @@ func (f *File) AddTable(sheet, hcell, vcell, format string) error { return err } // Coordinate conversion, convert C1:B3 to 2,0,1,2. - hcol, hrow := MustCellNameToCoordinates(hcell) - vcol, vrow := MustCellNameToCoordinates(vcell) + hcol, hrow, err := CellNameToCoordinates(hcell) + if err != nil { + return err + } + vcol, vrow, err := CellNameToCoordinates(vcell) + if err != nil { + return err + } if vcol < hcol { vcol, hcol = hcol, vcol @@ -73,7 +79,10 @@ func (f *File) AddTable(sheet, hcell, vcell, format string) error { // Add first table for given sheet. rID := f.addSheetRelationships(sheet, SourceRelationshipTable, sheetRelationshipsTableXML, "") f.addSheetTable(sheet, rID) - f.addTable(sheet, tableXML, hcol, hrow, vcol, vrow, tableID, formatSet) + err = f.addTable(sheet, tableXML, hcol, hrow, vcol, vrow, tableID, formatSet) + if err != nil { + return err + } f.addContentTypePart(tableID, "table") return err } @@ -106,24 +115,33 @@ func (f *File) addSheetTable(sheet string, rID int) { // addTable provides a function to add table by given worksheet name, // coordinate area and format set. -func (f *File) addTable(sheet, tableXML string, hcol, hrow, vcol, vrow, i int, formatSet *formatTable) { +func (f *File) addTable(sheet, tableXML string, hcol, hrow, vcol, vrow, i int, formatSet *formatTable) error { // Correct the minimum number of rows, the table at least two lines. if hrow == vrow { vrow++ } // Correct table reference coordinate area, such correct C1:B3 to B1:C3. - ref := MustCoordinatesToCellName(hcol, hrow) + ":" + MustCoordinatesToCellName(vcol, vrow) + hcell, err := CoordinatesToCellName(hcol, hrow) + if err != nil { + return err + } + vcell, err := CoordinatesToCellName(vcol, vrow) + if err != nil { + return err + } + ref := hcell + ":" + vcell - var ( - tableColumn []*xlsxTableColumn - ) + var tableColumn []*xlsxTableColumn idx := 0 for i := hcol; i <= vcol; i++ { idx++ - cell := MustCoordinatesToCellName(i, hrow) - name := f.GetCellValue(sheet, cell) + cell, err := CoordinatesToCellName(i, hrow) + if err != nil { + return err + } + name, _ := f.GetCellValue(sheet, cell) if _, err := strconv.Atoi(name); err == nil { f.SetCellStr(sheet, cell, name) } @@ -163,6 +181,7 @@ func (f *File) addTable(sheet, tableXML string, hcol, hrow, vcol, vrow, i int, f } table, _ := xml.Marshal(t) f.saveFileList(tableXML, table) + return nil } // parseAutoFilterSet provides a function to parse the settings of the auto @@ -244,8 +263,14 @@ func parseAutoFilterSet(formatSet string) (*formatAutoFilter, error) { // Price < 2000 // func (f *File) AutoFilter(sheet, hcell, vcell, format string) error { - hcol, hrow := MustCellNameToCoordinates(hcell) - vcol, vrow := MustCellNameToCoordinates(vcell) + hcol, hrow, err := CellNameToCoordinates(hcell) + if err != nil { + return err + } + vcol, vrow, err := CellNameToCoordinates(vcell) + if err != nil { + return err + } if vcol < hcol { vcol, hcol = hcol, vcol @@ -256,7 +281,17 @@ func (f *File) AutoFilter(sheet, hcell, vcell, format string) error { } formatSet, _ := parseAutoFilterSet(format) - ref := MustCoordinatesToCellName(hcol, hrow) + ":" + MustCoordinatesToCellName(vcol, vrow) + + var cellStart, cellEnd string + cellStart, err = CoordinatesToCellName(hcol, hrow) + if err != nil { + return err + } + cellEnd, err = CoordinatesToCellName(vcol, vrow) + if err != nil { + return err + } + ref := cellStart + ":" + cellEnd refRange := vcol - hcol return f.autoFilter(sheet, ref, refRange, hcol, formatSet) } @@ -277,7 +312,10 @@ func (f *File) autoFilter(sheet, ref string, refRange, col int, formatSet *forma return nil } - fsCol := MustColumnNameToNumber(formatSet.Column) + fsCol, err := ColumnNameToNumber(formatSet.Column) + if err != nil { + return err + } offset := fsCol - col if offset < 0 || offset > refRange { return fmt.Errorf("incorrect index of column '%s'", formatSet.Column) From f0244c00161ad6372ceb1ec951f3a82c741cd46a Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 24 Mar 2019 13:08:32 +0800 Subject: [PATCH 073/957] Add unit test to improve testing coverage --- adjust_test.go | 66 +++++++++++++++++ excelize_test.go | 181 ++++++++++++++++------------------------------- picture_test.go | 127 +++++++++++++++++++++++++++++++++ rows_test.go | 17 ++++- 4 files changed, 268 insertions(+), 123 deletions(-) create mode 100644 adjust_test.go diff --git a/adjust_test.go b/adjust_test.go new file mode 100644 index 0000000000..104eff9505 --- /dev/null +++ b/adjust_test.go @@ -0,0 +1,66 @@ +package excelize + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAdjustMergeCells(t *testing.T) { + f := NewFile() + // testing adjustAutoFilter with illegal cell coordinates. + assert.EqualError(t, f.adjustMergeCells(&xlsxWorksheet{ + MergeCells: &xlsxMergeCells{ + Cells: []*xlsxMergeCell{ + &xlsxMergeCell{ + Ref: "A:B1", + }, + }, + }, + }, rows, 0, 0), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.EqualError(t, f.adjustMergeCells(&xlsxWorksheet{ + MergeCells: &xlsxMergeCells{ + Cells: []*xlsxMergeCell{ + &xlsxMergeCell{ + Ref: "A1:B", + }, + }, + }, + }, rows, 0, 0), `cannot convert cell "B" to coordinates: invalid cell name "B"`) +} + +func TestAdjustAutoFilter(t *testing.T) { + f := NewFile() + // testing adjustAutoFilter with illegal cell coordinates. + assert.EqualError(t, f.adjustAutoFilter(&xlsxWorksheet{ + AutoFilter: &xlsxAutoFilter{ + Ref: "A:B1", + }, + }, rows, 0, 0), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.EqualError(t, f.adjustAutoFilter(&xlsxWorksheet{ + AutoFilter: &xlsxAutoFilter{ + Ref: "A1:B", + }, + }, rows, 0, 0), `cannot convert cell "B" to coordinates: invalid cell name "B"`) +} + +func TestAdjustHelper(t *testing.T) { + f := NewFile() + f.Sheet["xl/worksheets/sheet1.xml"] = &xlsxWorksheet{ + MergeCells: &xlsxMergeCells{ + Cells: []*xlsxMergeCell{ + &xlsxMergeCell{ + Ref: "A:B1", + }, + }, + }, + } + f.Sheet["xl/worksheets/sheet2.xml"] = &xlsxWorksheet{ + AutoFilter: &xlsxAutoFilter{ + Ref: "A1:B", + }, + } + // testing adjustHelper with illegal cell coordinates. + assert.EqualError(t, f.adjustHelper("sheet1", rows, 0, 0), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.EqualError(t, f.adjustHelper("sheet2", rows, 0, 0), `cannot convert cell "B" to coordinates: invalid cell name "B"`) +} diff --git a/excelize_test.go b/excelize_test.go index ab5e17b98c..2fea9a94c6 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -175,64 +175,6 @@ func TestSaveAsWrongPath(t *testing.T) { } } -func TestAddPicture(t *testing.T) { - xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } - - // Test add picture to worksheet with offset and location hyperlink. - err = xlsx.AddPicture("Sheet2", "I9", filepath.Join("test", "images", "excel.jpg"), - `{"x_offset": 140, "y_offset": 120, "hyperlink": "#Sheet2!D8", "hyperlink_type": "Location"}`) - if !assert.NoError(t, err) { - t.FailNow() - } - - // Test add picture to worksheet with offset, external hyperlink and positioning. - err = xlsx.AddPicture("Sheet1", "F21", filepath.Join("test", "images", "excel.jpg"), - `{"x_offset": 10, "y_offset": 10, "hyperlink": "https://github.com/360EntSecGroup-Skylar/excelize", "hyperlink_type": "External", "positioning": "oneCell"}`) - if !assert.NoError(t, err) { - t.FailNow() - } - - file, err := ioutil.ReadFile(filepath.Join("test", "images", "excel.jpg")) - if !assert.NoError(t, err) { - t.FailNow() - } - - // Test add picture to worksheet from bytes. - err = xlsx.AddPictureFromBytes("Sheet1", "Q1", "", "Excel Logo", ".jpg", file) - assert.NoError(t, err) - - // Test write file to given path. - err = xlsx.SaveAs(filepath.Join("test", "TestAddPicture.xlsx")) - assert.NoError(t, err) -} - -func TestAddPictureErrors(t *testing.T) { - xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } - - // Test add picture to worksheet with invalid file path. - err = xlsx.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") - } - - // Test add picture to worksheet with unsupport file type. - err = xlsx.AddPicture("Sheet1", "G21", filepath.Join("test", "Book1.xlsx"), "") - assert.EqualError(t, err, "unsupported image extension") - - err = xlsx.AddPictureFromBytes("Sheet1", "G21", "", "Excel Logo", "jpg", make([]byte, 1)) - assert.EqualError(t, err, "unsupported image extension") - - // Test add picture to worksheet with invalid file data. - err = xlsx.AddPictureFromBytes("Sheet1", "G21", "", "Excel Logo", ".jpg", make([]byte, 1)) - assert.EqualError(t, err, "image: unknown format") -} - func TestBrokenFile(t *testing.T) { // Test write file with broken file struct. xlsx := File{} @@ -301,13 +243,26 @@ func TestColWidth(t *testing.T) { xlsx.SetColWidth("Sheet1", "A", "B", 12) xlsx.GetColWidth("Sheet1", "A") xlsx.GetColWidth("Sheet1", "C") - err := xlsx.SaveAs(filepath.Join("test", "TestColWidth.xlsx")) + + // Test set and get column width with illegal cell coordinates. + _, err := xlsx.GetColWidth("Sheet1", "*") + assert.EqualError(t, err, `invalid column name "*"`) + assert.EqualError(t, xlsx.SetColWidth("Sheet1", "*", "B", 1), `invalid column name "*"`) + assert.EqualError(t, xlsx.SetColWidth("Sheet1", "A", "*", 1), `invalid column name "*"`) + + err = xlsx.SaveAs(filepath.Join("test", "TestColWidth.xlsx")) if err != nil { t.Error(err) } convertRowHeightToPixels(0) } +func TestAddDrawingVML(t *testing.T) { + // Test addDrawingVML with illegal cell coordinates. + f := NewFile() + assert.EqualError(t, f.addDrawingVML(0, "", "*", 0, 0), `cannot convert cell "*" to coordinates: invalid cell name "*"`) +} + func TestSetCellHyperLink(t *testing.T) { xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if err != nil { @@ -797,62 +752,6 @@ func TestSetDeleteSheet(t *testing.T) { }) } -func TestGetPicture(t *testing.T) { - xlsx, err := prepareTestBook1() - if !assert.NoError(t, err) { - t.FailNow() - } - - file, raw, err := xlsx.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)) { - - t.FailNow() - } - - // Try to get picture from a worksheet that doesn't contain any images. - file, raw, err = xlsx.GetPicture("Sheet3", "I9") - assert.NoError(t, err) - assert.Empty(t, file) - assert.Empty(t, raw) - - // Try to get picture from a cell that doesn't contain an image. - file, raw, err = xlsx.GetPicture("Sheet2", "A2") - assert.NoError(t, err) - assert.Empty(t, file) - assert.Empty(t, raw) - - xlsx.getDrawingRelationships("xl/worksheets/_rels/sheet1.xml.rels", "rId8") - xlsx.getDrawingRelationships("", "") - xlsx.getSheetRelationshipsTargetByID("", "") - xlsx.deleteSheetRelationships("", "") - - // Try to get picture from a local storage file. - if !assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestGetPicture.xlsx"))) { - t.FailNow() - } - - xlsx, err = OpenFile(filepath.Join("test", "TestGetPicture.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } - - file, raw, err = xlsx.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)) { - - t.FailNow() - } - - // Try to get picture from a local storage file that doesn't contain an image. - file, raw, err = xlsx.GetPicture("Sheet1", "F22") - assert.NoError(t, err) - assert.Empty(t, file) - assert.Empty(t, raw) -} - func TestSheetVisibility(t *testing.T) { xlsx, err := prepareTestBook1() if !assert.NoError(t, err) { @@ -874,10 +773,18 @@ func TestColumnVisibility(t *testing.T) { t.FailNow() } - xlsx.SetColVisible("Sheet1", "F", false) - xlsx.SetColVisible("Sheet1", "F", true) - xlsx.GetColVisible("Sheet1", "F") - xlsx.SetColVisible("Sheet3", "E", false) + assert.NoError(t, xlsx.SetColVisible("Sheet1", "F", false)) + assert.NoError(t, xlsx.SetColVisible("Sheet1", "F", true)) + visible, err := xlsx.GetColVisible("Sheet1", "F") + assert.Equal(t, true, visible) + assert.NoError(t, err) + + // Test get column visiable with illegal cell coordinates. + _, err = xlsx.GetColVisible("Sheet1", "*") + assert.EqualError(t, err, `invalid column name "*"`) + assert.EqualError(t, xlsx.SetColVisible("Sheet1", "*", false), `invalid column name "*"`) + + assert.NoError(t, xlsx.SetColVisible("Sheet3", "E", false)) assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestColumnVisibility.xlsx"))) }) @@ -945,7 +852,18 @@ func TestAddTable(t *testing.T) { t.FailNow() } + // Test add table with illegal formatset. + assert.EqualError(t, xlsx.AddTable("Sheet1", "B26", "A21", `{x}`), "invalid character 'x' looking for beginning of object key string") + // Test add table with illegal cell coordinates. + assert.EqualError(t, xlsx.AddTable("Sheet1", "A", "B1", `{}`), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.EqualError(t, xlsx.AddTable("Sheet1", "A1", "B", `{}`), `cannot convert cell "B" to coordinates: invalid cell name "B"`) + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestAddTable.xlsx"))) + + // Test addTable with illegal cell coordinates. + 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]") } func TestAddShape(t *testing.T) { @@ -978,6 +896,11 @@ func TestAddComments(t *testing.T) { } } +func TestGetSheetComments(t *testing.T) { + f := NewFile() + assert.Equal(t, "", f.getSheetComments(0)) +} + func TestAutoFilter(t *testing.T) { outFile := filepath.Join("test", "TestAutoFilter%d.xlsx") @@ -1006,6 +929,9 @@ func TestAutoFilter(t *testing.T) { }) } + // testing AutoFilter with illegal cell coordinates. + assert.EqualError(t, xlsx.AutoFilter("Sheet1", "A", "B1", ""), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.EqualError(t, xlsx.AutoFilter("Sheet1", "A1", "B", ""), `cannot convert cell "B" to coordinates: invalid cell name "B"`) } func TestAutoFilterError(t *testing.T) { @@ -1095,6 +1021,9 @@ func TestInsertCol(t *testing.T) { assert.NoError(t, xlsx.InsertCol(sheet1, "A")) + // Test insert column with illegal cell coordinates. + assert.EqualError(t, xlsx.InsertCol("Sheet1", "*"), `invalid column name "*"`) + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestInsertCol.xlsx"))) } @@ -1113,6 +1042,9 @@ func TestRemoveCol(t *testing.T) { assert.NoError(t, xlsx.RemoveCol(sheet1, "A")) assert.NoError(t, xlsx.RemoveCol(sheet1, "A")) + // Test remove column with illegal cell coordinates. + assert.EqualError(t, xlsx.RemoveCol("Sheet1", "*"), `invalid column name "*"`) + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestRemoveCol.xlsx"))) } @@ -1252,14 +1184,23 @@ func TestOutlineLevel(t *testing.T) { xlsx.SetColOutlineLevel("Sheet2", "B", 2) xlsx.SetRowOutlineLevel("Sheet1", 2, 250) + // Test set and get column outline level with illegal cell coordinates. + assert.EqualError(t, xlsx.SetColOutlineLevel("Sheet1", "*", 1), `invalid column name "*"`) + level, err := xlsx.GetColOutlineLevel("Sheet1", "*") + assert.EqualError(t, err, `invalid column name "*"`) + assert.EqualError(t, xlsx.SetRowOutlineLevel("Sheet1", 0, 1), "invalid row number 0") - level, err := xlsx.GetRowOutlineLevel("Sheet1", 2) + level, err = xlsx.GetRowOutlineLevel("Sheet1", 2) assert.NoError(t, err) assert.Equal(t, uint8(250), level) _, err = xlsx.GetRowOutlineLevel("Sheet1", 0) assert.EqualError(t, err, `invalid row number 0`) + level, err = xlsx.GetRowOutlineLevel("Sheet1", 10) + assert.NoError(t, err) + assert.Equal(t, uint8(0), level) + err = xlsx.SaveAs(filepath.Join("test", "TestOutlineLevel.xlsx")) if !assert.NoError(t, err) { t.FailNow() diff --git a/picture_test.go b/picture_test.go index 8c8d2e4049..518713fff0 100644 --- a/picture_test.go +++ b/picture_test.go @@ -4,8 +4,11 @@ import ( "fmt" _ "image/png" "io/ioutil" + "os" "path/filepath" "testing" + + "github.com/stretchr/testify/assert" ) func BenchmarkAddPictureFromBytes(b *testing.B) { @@ -19,3 +22,127 @@ func BenchmarkAddPictureFromBytes(b *testing.B) { f.AddPictureFromBytes("Sheet1", fmt.Sprint("A", i), "", "excel", ".png", imgFile) } } + +func TestAddPicture(t *testing.T) { + xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + if !assert.NoError(t, err) { + t.FailNow() + } + + // Test add picture to worksheet with offset and location hyperlink. + err = xlsx.AddPicture("Sheet2", "I9", filepath.Join("test", "images", "excel.jpg"), + `{"x_offset": 140, "y_offset": 120, "hyperlink": "#Sheet2!D8", "hyperlink_type": "Location"}`) + if !assert.NoError(t, err) { + t.FailNow() + } + + // Test add picture to worksheet with offset, external hyperlink and positioning. + err = xlsx.AddPicture("Sheet1", "F21", filepath.Join("test", "images", "excel.jpg"), + `{"x_offset": 10, "y_offset": 10, "hyperlink": "https://github.com/360EntSecGroup-Skylar/excelize", "hyperlink_type": "External", "positioning": "oneCell"}`) + if !assert.NoError(t, err) { + t.FailNow() + } + + file, err := ioutil.ReadFile(filepath.Join("test", "images", "excel.jpg")) + if !assert.NoError(t, err) { + t.FailNow() + } + + // Test add picture to worksheet from bytes. + assert.NoError(t, xlsx.AddPictureFromBytes("Sheet1", "Q1", "", "Excel Logo", ".jpg", file)) + // Test add picture to worksheet from bytes with illegal cell coordinates. + assert.EqualError(t, xlsx.AddPictureFromBytes("Sheet1", "A", "", "Excel Logo", ".jpg", file), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + + // Test write file to given path. + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestAddPicture.xlsx"))) +} + +func TestAddPictureErrors(t *testing.T) { + xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + if !assert.NoError(t, err) { + t.FailNow() + } + + // Test add picture to worksheet with invalid file path. + err = xlsx.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") + } + + // Test add picture to worksheet with unsupport file type. + err = xlsx.AddPicture("Sheet1", "G21", filepath.Join("test", "Book1.xlsx"), "") + assert.EqualError(t, err, "unsupported image extension") + + err = xlsx.AddPictureFromBytes("Sheet1", "G21", "", "Excel Logo", "jpg", make([]byte, 1)) + assert.EqualError(t, err, "unsupported image extension") + + // Test add picture to worksheet with invalid file data. + err = xlsx.AddPictureFromBytes("Sheet1", "G21", "", "Excel Logo", ".jpg", make([]byte, 1)) + assert.EqualError(t, err, "image: unknown format") +} + +func TestGetPicture(t *testing.T) { + xlsx, err := prepareTestBook1() + if !assert.NoError(t, err) { + t.FailNow() + } + + file, raw, err := xlsx.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)) { + + t.FailNow() + } + + // Try to get picture from a worksheet with illegal cell coordinates. + file, raw, err = xlsx.GetPicture("Sheet1", "A") + assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`) + + // Try to get picture from a worksheet that doesn't contain any images. + file, raw, err = xlsx.GetPicture("Sheet3", "I9") + assert.NoError(t, err) + assert.Empty(t, file) + assert.Empty(t, raw) + + // Try to get picture from a cell that doesn't contain an image. + file, raw, err = xlsx.GetPicture("Sheet2", "A2") + assert.NoError(t, err) + assert.Empty(t, file) + assert.Empty(t, raw) + + xlsx.getDrawingRelationships("xl/worksheets/_rels/sheet1.xml.rels", "rId8") + xlsx.getDrawingRelationships("", "") + xlsx.getSheetRelationshipsTargetByID("", "") + xlsx.deleteSheetRelationships("", "") + + // Try to get picture from a local storage file. + if !assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestGetPicture.xlsx"))) { + t.FailNow() + } + + xlsx, err = OpenFile(filepath.Join("test", "TestGetPicture.xlsx")) + if !assert.NoError(t, err) { + t.FailNow() + } + + file, raw, err = xlsx.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)) { + + t.FailNow() + } + + // Try to get picture from a local storage file that doesn't contain an image. + file, raw, err = xlsx.GetPicture("Sheet1", "F22") + assert.NoError(t, err) + assert.Empty(t, file) + assert.Empty(t, raw) +} + +func TestAddDrawingPicture(t *testing.T) { + // testing addDrawingPicture with illegal cell coordinates. + f := NewFile() + assert.EqualError(t, f.addDrawingPicture("sheet1", "", "A", "", 0, 0, 0, 0, nil), `cannot convert cell "A" to coordinates: invalid cell name "A"`) +} diff --git a/rows_test.go b/rows_test.go index 53c06772b0..f576efcc54 100644 --- a/rows_test.go +++ b/rows_test.go @@ -72,6 +72,16 @@ func TestRowHeight(t *testing.T) { assert.NoError(t, err) assert.Equal(t, 444.0, height) + // Test get row height that rows index over exists rows. + height, err = xlsx.GetRowHeight(sheet1, 5) + assert.NoError(t, err) + assert.Equal(t, defaultRowHeightPixels, height) + + // Test get row height that rows heights haven't changed. + height, err = xlsx.GetRowHeight(sheet1, 3) + assert.NoError(t, err) + assert.Equal(t, defaultRowHeightPixels, height) + err = xlsx.SaveAs(filepath.Join("test", "TestRowHeight.xlsx")) if !assert.NoError(t, err) { t.FailNow() @@ -86,10 +96,10 @@ func TestRowVisibility(t *testing.T) { t.FailNow() } - xlsx.SetRowVisible("Sheet3", 2, false) - xlsx.SetRowVisible("Sheet3", 2, true) + assert.NoError(t, xlsx.SetRowVisible("Sheet3", 2, false)) + assert.NoError(t, xlsx.SetRowVisible("Sheet3", 2, true)) xlsx.GetRowVisible("Sheet3", 2) - + xlsx.GetRowVisible("Sheet3", 25) assert.EqualError(t, xlsx.SetRowVisible("Sheet3", 0, true), "invalid row number 0") visible, err := xlsx.GetRowVisible("Sheet3", 0) @@ -153,6 +163,7 @@ func TestRemoveRow(t *testing.T) { t.FailNow() } + assert.NoError(t, xlsx.RemoveRow(sheet1, 10)) assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestRemoveRow.xlsx"))) } From a94dcb9918b5fec133faf5df65144d48e8722ca8 Mon Sep 17 00:00:00 2001 From: Michael Date: Sat, 23 Mar 2019 20:07:57 -0500 Subject: [PATCH 074/957] Do not save duplicate images Adding the same image should create a drawing referencing the already stored copy of the image. Closes #359 --- README.md | 3 ++- picture.go | 21 +++++++++++++++------ picture_test.go | 18 ++++++++++++++++++ 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 84ddde0d7d..7774a508f9 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,8 @@ ## Introduction -Excelize is a library written in pure Go and providing a set of functions that allow you to write to and read from XLSX files. Support reads and writes XLSX file generated by Microsoft Excel™ 2007 and later. Support save file without losing original charts of XLSX. This library needs Go version 1.8 or later. The full API docs can be seen using go's built-in documentation tool, or online at [godoc.org](https://godoc.org/github.com/360EntSecGroup-Skylar/excelize) 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 XLSX files. Supports reading and writing XLSX file generated by Microsoft Excel™ 2007 and later. +Supports saving a file without losing original charts of XLSX. This library needs Go version 1.8 or later. The full API docs can be seen using go's built-in documentation tool, or online at [godoc.org](https://godoc.org/github.com/360EntSecGroup-Skylar/excelize) and [docs reference](https://xuri.me/excelize/). ## Basic Usage diff --git a/picture.go b/picture.go index 9a9ff09ed6..16572d41c6 100644 --- a/picture.go +++ b/picture.go @@ -151,10 +151,10 @@ func (f *File) AddPictureFromBytes(sheet, cell, format, name, extension string, xlsx := f.workSheetReader(sheet) // Add first picture for given sheet, create xl/drawings/ and xl/drawings/_rels/ folder. drawingID := f.countDrawings() + 1 - pictureID := f.countMedia() + 1 drawingXML := "xl/drawings/drawing" + strconv.Itoa(drawingID) + ".xml" drawingID, drawingXML = f.prepareDrawing(xlsx, drawingID, sheet, drawingXML) - drawingRID := f.addDrawingRelationships(drawingID, SourceRelationshipImage, "../media/image"+strconv.Itoa(pictureID)+ext, hyperlinkType) + mediaStr := ".." + strings.TrimPrefix(f.addMedia(file, ext), "xl") + drawingRID := f.addDrawingRelationships(drawingID, SourceRelationshipImage, mediaStr, hyperlinkType) // Add picture with hyperlink. if formatSet.Hyperlink != "" && formatSet.HyperlinkType != "" { if formatSet.HyperlinkType == "External" { @@ -166,7 +166,6 @@ func (f *File) AddPictureFromBytes(sheet, cell, format, name, extension string, if err != nil { return err } - f.addMedia(file, ext) f.addContentTypePart(drawingID, "drawings") return err } @@ -363,12 +362,22 @@ func (f *File) countMedia() int { return count } -// addMedia provides a function to add picture into folder xl/media/image by -// given file and extension name. -func (f *File) addMedia(file []byte, ext string) { +// addMedia provides a function to add a picture into folder xl/media/image by +// given file and extension name. Duplicate images are only actually stored once +// and drawings that use it will reference the same image. +func (f *File) addMedia(file []byte, ext string) string { count := f.countMedia() + for name, existing := range f.XLSX { + if !strings.HasPrefix(name, "xl/media/image") { + continue + } + if bytes.Equal(file, existing) { + return name + } + } media := "xl/media/image" + strconv.Itoa(count+1) + ext f.XLSX[media] = file + return media } // setContentTypePartImageExtensions provides a function to set the content diff --git a/picture_test.go b/picture_test.go index 518713fff0..2b39ed8021 100644 --- a/picture_test.go +++ b/picture_test.go @@ -6,6 +6,7 @@ import ( "io/ioutil" "os" "path/filepath" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -146,3 +147,20 @@ func TestAddDrawingPicture(t *testing.T) { f := NewFile() assert.EqualError(t, f.addDrawingPicture("sheet1", "", "A", "", 0, 0, 0, 0, nil), `cannot convert cell "A" to coordinates: invalid cell name "A"`) } + +func TestAddPictureFromBytes(t *testing.T) { + f := NewFile() + imgFile, err := ioutil.ReadFile("logo.png") + if err != nil { + t.Error("Unable to load logo for test") + } + f.AddPictureFromBytes("Sheet1", fmt.Sprint("A", 1), "", "logo", ".png", imgFile) + f.AddPictureFromBytes("Sheet1", fmt.Sprint("A", 50), "", "logo", ".png", imgFile) + imageCount := 0 + for fileName := range f.XLSX { + if strings.Contains(fileName, "media/image") { + imageCount++ + } + } + assert.Equal(t, 1, imageCount, "Duplicate image should only be stored once.") +} From 28c02e3aaf585037f7d9035c21901844bdda91a2 Mon Sep 17 00:00:00 2001 From: nabeyama yoshihide Date: Thu, 4 Apr 2019 17:04:10 +0900 Subject: [PATCH 075/957] Fixed bug in column cell to check. The target cell for calclator the width was shifted by 1. --- col.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/col.go b/col.go index e3e057ce4a..637f6e084d 100644 --- a/col.go +++ b/col.go @@ -268,7 +268,7 @@ func (f *File) positionObjectPixels(sheet string, col, row, x1, y1, width, heigh height += y1 // Subtract the underlying cell widths to find end cell of the object. - for width >= f.getColWidth(sheet, colEnd) { + for width >= f.getColWidth(sheet, colEnd + 1) { colEnd++ width -= f.getColWidth(sheet, colEnd) } From b0acd922ef9e4874eeadd905a636ebc911b1b299 Mon Sep 17 00:00:00 2001 From: nabeyama yoshihide Date: Thu, 4 Apr 2019 17:26:26 +0900 Subject: [PATCH 076/957] Fixed bug in the calculation target cell(row). The target cell for calclator the height was shifted by 1. --- col.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/col.go b/col.go index 637f6e084d..5fc7461293 100644 --- a/col.go +++ b/col.go @@ -275,8 +275,8 @@ func (f *File) positionObjectPixels(sheet string, col, row, x1, y1, width, heigh // Subtract the underlying cell heights to find end cell of the object. for height >= f.getRowHeight(sheet, rowEnd) { - rowEnd++ height -= f.getRowHeight(sheet, rowEnd) + rowEnd++ } // The end vertices are whatever is left from the width and height. From 8134197b07c5aa7615d97b586d69d8ff02f19df3 Mon Sep 17 00:00:00 2001 From: nabeyama yoshihide Date: Thu, 4 Apr 2019 17:34:05 +0900 Subject: [PATCH 077/957] Adjust coding style --- col.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/col.go b/col.go index 5fc7461293..3660783c82 100644 --- a/col.go +++ b/col.go @@ -268,7 +268,7 @@ func (f *File) positionObjectPixels(sheet string, col, row, x1, y1, width, heigh height += y1 // Subtract the underlying cell widths to find end cell of the object. - for width >= f.getColWidth(sheet, colEnd + 1) { + for width >= f.getColWidth(sheet, colEnd+1) { colEnd++ width -= f.getColWidth(sheet, colEnd) } From 4e7d93a77796aa6814339d377a94d4b66226f1ce Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 7 Apr 2019 14:04:41 +0800 Subject: [PATCH 078/957] Resolve #377, avoid empty column in GetRows result --- rows.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rows.go b/rows.go index 5b8d7d8e29..6ece77bd83 100644 --- a/rows.go +++ b/rows.go @@ -54,7 +54,7 @@ func (f *File) GetRows(sheet string) ([][]string, error) { } rows := make([][]string, rowCount) for i := range rows { - rows[i] = make([]string, colCount+1) + rows[i] = make([]string, colCount) } var row int From 841ff4a03e2b30378f6bb2930752c8e9dcfe0dca Mon Sep 17 00:00:00 2001 From: Aplulu Date: Tue, 9 Apr 2019 23:18:31 +0900 Subject: [PATCH 079/957] Fix out of range panic when removing formula. Fix file corruption issue when deleting a sheet containing a formula. --- calcchain.go | 22 ++++++++++++++++------ cell.go | 2 +- sheet.go | 1 + 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/calcchain.go b/calcchain.go index 285a3e9ee9..6fbdcd71bc 100644 --- a/calcchain.go +++ b/calcchain.go @@ -33,14 +33,12 @@ func (f *File) calcChainWriter() { // deleteCalcChain provides a function to remove cell reference on the // calculation chain. -func (f *File) deleteCalcChain(axis string) { +func (f *File) deleteCalcChain(index int, axis string) { calc := f.calcChainReader() if calc != nil { - for i, c := range calc.C { - if c.R == axis { - calc.C = append(calc.C[:i], calc.C[i+1:]...) - } - } + calc.C = xlsxCalcChainCollection(calc.C).Filter(func(c xlsxCalcChainC) bool { + return !((c.I == index && c.R == axis) || (c.I == index && axis == "")) + }) } if len(calc.C) == 0 { f.CalcChain = nil @@ -53,3 +51,15 @@ func (f *File) deleteCalcChain(axis string) { } } } + +type xlsxCalcChainCollection []xlsxCalcChainC + +func (c xlsxCalcChainCollection) Filter(fn func(v xlsxCalcChainC) bool) []xlsxCalcChainC { + results := make([]xlsxCalcChainC, 0) + for _, v := range c { + if fn(v) { + results = append(results, v) + } + } + return results +} diff --git a/cell.go b/cell.go index a1b6dbfe63..36f2d93685 100644 --- a/cell.go +++ b/cell.go @@ -235,7 +235,7 @@ func (f *File) SetCellFormula(sheet, axis, formula string) error { } if formula == "" { cellData.F = nil - f.deleteCalcChain(axis) + f.deleteCalcChain(f.GetSheetIndex(sheet), axis) return err } diff --git a/sheet.go b/sheet.go index ec4df774e4..9960ef8ef5 100644 --- a/sheet.go +++ b/sheet.go @@ -403,6 +403,7 @@ func (f *File) DeleteSheet(name string) { rels := "xl/worksheets/_rels/sheet" + strconv.Itoa(v.SheetID) + ".xml.rels" target := f.deleteSheetFromWorkbookRels(v.ID) f.deleteSheetFromContentTypes(target) + f.deleteCalcChain(v.SheetID, "") // Delete CalcChain delete(f.sheetMap, name) delete(f.XLSX, sheet) delete(f.XLSX, rels) From c423617e9d948b61cf9397710bf8f2098efe7634 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 14 Apr 2019 12:55:44 +0800 Subject: [PATCH 080/957] Check max length for SetCellStr and fix coordinate issue for MergeCell --- calcchain.go | 1 + cell.go | 13 +++++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/calcchain.go b/calcchain.go index 6fbdcd71bc..22aab7e323 100644 --- a/calcchain.go +++ b/calcchain.go @@ -54,6 +54,7 @@ func (f *File) deleteCalcChain(index int, axis string) { type xlsxCalcChainCollection []xlsxCalcChainC +// Filter provides a function to filter calculation chain. func (c xlsxCalcChainCollection) Filter(fn func(v xlsxCalcChainC) bool) []xlsxCalcChainC { results := make([]xlsxCalcChainC, 0) for _, v := range c { diff --git a/cell.go b/cell.go index 36f2d93685..f9bf1741bb 100644 --- a/cell.go +++ b/cell.go @@ -183,6 +183,9 @@ func (f *File) SetCellStr(sheet, axis, value string) error { if err != nil { return err } + if len(value) > 32767 { + value = value[0:32767] + } // Leading space(s) character detection. if len(value) > 0 && value[0] == 32 { cellData.XMLSpace = xml.Attr{ @@ -352,6 +355,7 @@ func (f *File) MergeCell(sheet, hcell, vcell string) error { return err } + // Correct the coordinate area, such correct C1:B3 to B1:C3. if vcol < hcol { hcol, vcol = vcol, hcol } @@ -378,9 +382,10 @@ func (f *File) MergeCell(sheet, hcell, vcell string) error { c2, _ := checkCellInArea(vcell, cellData.Ref) c3, _ := checkCellInArea(cc[0], ref) c4, _ := checkCellInArea(cc[1], ref) - if !c1 && !c2 && !c3 && !c4 { - cells = append(cells, cellData) + if !(!c1 && !c2 && !c3 && !c4) { + return nil } + cells = append(cells, cellData) } cells = append(xlsx.MergeCells.Cells, &xlsxMergeCell{Ref: ref}) xlsx.MergeCells.Cells = cells @@ -543,10 +548,10 @@ func checkCellInArea(cell, area string) (bool, error) { return false, err } - firstCol, firtsRow, _ := CellNameToCoordinates(rng[0]) + firstCol, firstRow, _ := CellNameToCoordinates(rng[0]) lastCol, lastRow, _ := CellNameToCoordinates(rng[1]) - return col >= firstCol && col <= lastCol && row >= firtsRow && row <= lastRow, err + return col >= firstCol && col <= lastCol && row >= firstRow && row <= lastRow, err } // getSharedForumula find a cell contains the same formula as another cell, From f2df344739146189a1dea7cfb81239231af5135b Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 15 Apr 2019 11:22:57 +0800 Subject: [PATCH 081/957] Resolve #369,#370 add error return value exported functions: GetMergeCells ProtectSheet UnprotectSheet UpdateLinkedValue GetMergeCells SetSheetVisible inner functions: workSheetReader copySheet --- adjust.go | 10 +++-- adjust_test.go | 5 ++- cell.go | 102 ++++++++++++++++++++++++++++++---------------- chart.go | 5 ++- col.go | 37 +++++++++++++---- comment.go | 5 ++- datavalidation.go | 14 ++++--- excelize.go | 26 +++++++----- excelize_test.go | 21 ++++------ file.go | 2 +- picture.go | 17 +++++--- picture_test.go | 2 +- rows.go | 52 ++++++++++++++++++----- rows_test.go | 17 ++++---- shape.go | 5 ++- sheet.go | 82 +++++++++++++++++++++++++------------ sheetpr.go | 14 +++++-- sheetview.go | 7 +++- styles.go | 17 ++++++-- styles_test.go | 3 +- table.go | 7 +++- 21 files changed, 304 insertions(+), 146 deletions(-) diff --git a/adjust.go b/adjust.go index 009860b155..51db57ec7e 100644 --- a/adjust.go +++ b/adjust.go @@ -31,18 +31,20 @@ const ( // adjustDataValidations, adjustProtectedCells // func (f *File) adjustHelper(sheet string, dir adjustDirection, num, offset int) error { - xlsx := f.workSheetReader(sheet) - + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return err + } if dir == rows { f.adjustRowDimensions(xlsx, num, offset) } else { f.adjustColDimensions(xlsx, num, offset) } f.adjustHyperlinks(xlsx, sheet, dir, num, offset) - if err := f.adjustMergeCells(xlsx, dir, num, offset); err != nil { + if err = f.adjustMergeCells(xlsx, dir, num, offset); err != nil { return err } - if err := f.adjustAutoFilter(xlsx, dir, num, offset); err != nil { + if err = f.adjustAutoFilter(xlsx, dir, num, offset); err != nil { return err } diff --git a/adjust_test.go b/adjust_test.go index 104eff9505..a35e6098c5 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -46,6 +46,7 @@ func TestAdjustAutoFilter(t *testing.T) { func TestAdjustHelper(t *testing.T) { f := NewFile() + f.NewSheet("Sheet2") f.Sheet["xl/worksheets/sheet1.xml"] = &xlsxWorksheet{ MergeCells: &xlsxMergeCells{ Cells: []*xlsxMergeCell{ @@ -61,6 +62,6 @@ func TestAdjustHelper(t *testing.T) { }, } // testing adjustHelper with illegal cell coordinates. - assert.EqualError(t, f.adjustHelper("sheet1", rows, 0, 0), `cannot convert cell "A" to coordinates: invalid cell name "A"`) - assert.EqualError(t, f.adjustHelper("sheet2", rows, 0, 0), `cannot convert cell "B" to coordinates: invalid cell name "B"`) + assert.EqualError(t, f.adjustHelper("Sheet1", rows, 0, 0), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.EqualError(t, f.adjustHelper("Sheet2", rows, 0, 0), `cannot convert cell "B" to coordinates: invalid cell name "B"`) } diff --git a/cell.go b/cell.go index f9bf1741bb..6a8eebecd6 100644 --- a/cell.go +++ b/cell.go @@ -69,63 +69,67 @@ func (f *File) GetCellValue(sheet, axis string) (string, error) { // Note that default date format is m/d/yy h:mm of time.Time type value. You can // set numbers format by SetCellStyle() method. func (f *File) SetCellValue(sheet, axis string, value interface{}) error { + var err error switch v := value.(type) { case int: - f.SetCellInt(sheet, axis, v) + err = f.SetCellInt(sheet, axis, v) case int8: - f.SetCellInt(sheet, axis, int(v)) + err = f.SetCellInt(sheet, axis, int(v)) case int16: - f.SetCellInt(sheet, axis, int(v)) + err = f.SetCellInt(sheet, axis, int(v)) case int32: - f.SetCellInt(sheet, axis, int(v)) + err = f.SetCellInt(sheet, axis, int(v)) case int64: - f.SetCellInt(sheet, axis, int(v)) + err = f.SetCellInt(sheet, axis, int(v)) case uint: - f.SetCellInt(sheet, axis, int(v)) + err = f.SetCellInt(sheet, axis, int(v)) case uint8: - f.SetCellInt(sheet, axis, int(v)) + err = f.SetCellInt(sheet, axis, int(v)) case uint16: - f.SetCellInt(sheet, axis, int(v)) + err = f.SetCellInt(sheet, axis, int(v)) case uint32: - f.SetCellInt(sheet, axis, int(v)) + err = f.SetCellInt(sheet, axis, int(v)) case uint64: - f.SetCellInt(sheet, axis, int(v)) + err = f.SetCellInt(sheet, axis, int(v)) case float32: - f.SetCellFloat(sheet, axis, float64(v), -1, 32) + err = f.SetCellFloat(sheet, axis, float64(v), -1, 32) case float64: - f.SetCellFloat(sheet, axis, v, -1, 64) + err = f.SetCellFloat(sheet, axis, v, -1, 64) case string: - f.SetCellStr(sheet, axis, v) + err = f.SetCellStr(sheet, axis, v) case []byte: - f.SetCellStr(sheet, axis, string(v)) + err = f.SetCellStr(sheet, axis, string(v)) case time.Duration: - f.SetCellDefault(sheet, axis, strconv.FormatFloat(v.Seconds()/86400.0, 'f', -1, 32)) - f.setDefaultTimeStyle(sheet, axis, 21) + err = f.SetCellDefault(sheet, axis, strconv.FormatFloat(v.Seconds()/86400.0, 'f', -1, 32)) + err = f.setDefaultTimeStyle(sheet, axis, 21) case time.Time: excelTime, err := timeToExcelTime(v) if err != nil { return err } if excelTime > 0 { - f.SetCellDefault(sheet, axis, strconv.FormatFloat(excelTime, 'f', -1, 64)) - f.setDefaultTimeStyle(sheet, axis, 22) + err = f.SetCellDefault(sheet, axis, strconv.FormatFloat(excelTime, 'f', -1, 64)) + err = f.setDefaultTimeStyle(sheet, axis, 22) } else { - f.SetCellStr(sheet, axis, v.Format(time.RFC3339Nano)) + err = f.SetCellStr(sheet, axis, v.Format(time.RFC3339Nano)) } case bool: - f.SetCellBool(sheet, axis, v) + err = f.SetCellBool(sheet, axis, v) case nil: - f.SetCellStr(sheet, axis, "") + err = f.SetCellStr(sheet, axis, "") default: - f.SetCellStr(sheet, axis, fmt.Sprintf("%v", value)) + err = f.SetCellStr(sheet, axis, fmt.Sprintf("%v", value)) } - return nil + return err } // 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 { - xlsx := f.workSheetReader(sheet) + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return err + } cellData, col, _, err := f.prepareCell(xlsx, sheet, axis) if err != nil { return err @@ -139,7 +143,10 @@ func (f *File) SetCellInt(sheet, axis string, value int) error { // 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 { - xlsx := f.workSheetReader(sheet) + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return err + } cellData, col, _, err := f.prepareCell(xlsx, sheet, axis) if err != nil { return err @@ -164,7 +171,10 @@ func (f *File) SetCellBool(sheet, axis string, value bool) error { // f.SetCellFloat("Sheet1", "A1", float64(x), 2, 32) // func (f *File) SetCellFloat(sheet, axis string, value float64, prec, bitSize int) error { - xlsx := f.workSheetReader(sheet) + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return err + } cellData, col, _, err := f.prepareCell(xlsx, sheet, axis) if err != nil { return err @@ -178,7 +188,10 @@ func (f *File) SetCellFloat(sheet, axis string, value float64, prec, bitSize int // 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 { - xlsx := f.workSheetReader(sheet) + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return err + } cellData, col, _, err := f.prepareCell(xlsx, sheet, axis) if err != nil { return err @@ -203,7 +216,10 @@ func (f *File) SetCellStr(sheet, axis, value 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, axis, value string) error { - xlsx := f.workSheetReader(sheet) + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return err + } cellData, col, _, err := f.prepareCell(xlsx, sheet, axis) if err != nil { return err @@ -231,7 +247,10 @@ func (f *File) GetCellFormula(sheet, axis string) (string, error) { // SetCellFormula provides a function to set cell formula by given string and // worksheet name. func (f *File) SetCellFormula(sheet, axis, formula string) error { - xlsx := f.workSheetReader(sheet) + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return err + } cellData, _, _, err := f.prepareCell(xlsx, sheet, axis) if err != nil { return err @@ -264,8 +283,11 @@ func (f *File) GetCellHyperLink(sheet, axis string) (bool, string, error) { return false, "", err } - xlsx := f.workSheetReader(sheet) - axis, err := f.mergeCellsParser(xlsx, axis) + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return false, "", err + } + axis, err = f.mergeCellsParser(xlsx, axis) if err != nil { return false, "", err } @@ -302,8 +324,11 @@ func (f *File) SetCellHyperLink(sheet, axis, link, linkType string) error { return err } - xlsx := f.workSheetReader(sheet) - axis, err := f.mergeCellsParser(xlsx, axis) + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return err + } + axis, err = f.mergeCellsParser(xlsx, axis) if err != nil { return err } @@ -367,7 +392,10 @@ func (f *File) MergeCell(sheet, hcell, vcell string) error { hcell, _ = CoordinatesToCellName(hcol, hrow) vcell, _ = CoordinatesToCellName(vcol, vrow) - xlsx := f.workSheetReader(sheet) + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return err + } if xlsx.MergeCells != nil { ref := hcell + ":" + vcell cells := make([]*xlsxMergeCell, 0, len(xlsx.MergeCells.Cells)) @@ -446,8 +474,10 @@ func (f *File) prepareCell(xlsx *xlsxWorksheet, sheet, cell string) (*xlsxC, int // 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) { - xlsx := f.workSheetReader(sheet) - var err error + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return "", err + } axis, err = f.mergeCellsParser(xlsx, axis) if err != nil { return "", err diff --git a/chart.go b/chart.go index 5429b77112..c657be8bda 100644 --- a/chart.go +++ b/chart.go @@ -449,7 +449,10 @@ func (f *File) AddChart(sheet, cell, format string) error { return err } // Read sheet data. - xlsx := f.workSheetReader(sheet) + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return err + } // Add first picture for given sheet, create xl/drawings/ and xl/drawings/_rels/ folder. drawingID := f.countDrawings() + 1 chartID := f.countCharts() + 1 diff --git a/col.go b/col.go index 3660783c82..ad63b8ca36 100644 --- a/col.go +++ b/col.go @@ -31,7 +31,10 @@ func (f *File) GetColVisible(sheet, col string) (bool, error) { return visible, err } - xlsx := f.workSheetReader(sheet) + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return false, err + } if xlsx.Cols == nil { return visible, err } @@ -61,7 +64,10 @@ func (f *File) SetColVisible(sheet, col string, visible bool) error { Hidden: !visible, CustomWidth: true, } - xlsx := f.workSheetReader(sheet) + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return err + } if xlsx.Cols == nil { cols := xlsxCols{} cols.Col = append(cols.Col, colData) @@ -93,7 +99,10 @@ func (f *File) GetColOutlineLevel(sheet, col string) (uint8, error) { if err != nil { return level, err } - xlsx := f.workSheetReader(sheet) + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return 0, err + } if xlsx.Cols == nil { return level, err } @@ -123,7 +132,10 @@ func (f *File) SetColOutlineLevel(sheet, col string, level uint8) error { OutlineLevel: level, CustomWidth: true, } - xlsx := f.workSheetReader(sheet) + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return err + } if xlsx.Cols == nil { cols := xlsxCols{} cols.Col = append(cols.Col, colData) @@ -162,7 +174,10 @@ func (f *File) SetColWidth(sheet, startcol, endcol string, width float64) error min, max = max, min } - xlsx := f.workSheetReader(sheet) + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return err + } col := xlsxCol{ Min: min, Max: max, @@ -288,7 +303,7 @@ func (f *File) positionObjectPixels(sheet string, col, row, x1, y1, width, heigh // getColWidth provides a function to get column width in pixels by given // sheet name and column index. func (f *File) getColWidth(sheet string, col int) int { - xlsx := f.workSheetReader(sheet) + xlsx, _ := f.workSheetReader(sheet) if xlsx.Cols != nil { var width float64 for _, v := range xlsx.Cols.Col { @@ -311,7 +326,10 @@ func (f *File) GetColWidth(sheet, col string) (float64, error) { if err != nil { return defaultColWidthPixels, err } - xlsx := f.workSheetReader(sheet) + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return defaultColWidthPixels, err + } if xlsx.Cols != nil { var width float64 for _, v := range xlsx.Cols.Col { @@ -355,7 +373,10 @@ func (f *File) RemoveCol(sheet, col string) error { return err } - xlsx := f.workSheetReader(sheet) + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return err + } for rowIdx := range xlsx.SheetData.Row { rowData := &xlsx.SheetData.Row[rowIdx] for colIdx := range rowData.C { diff --git a/comment.go b/comment.go index ca79779b25..ed3d4a7433 100644 --- a/comment.go +++ b/comment.go @@ -80,7 +80,10 @@ func (f *File) AddComment(sheet, cell, format string) error { return err } // Read sheet data. - xlsx := f.workSheetReader(sheet) + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return err + } commentID := f.countComments() + 1 drawingVML := "xl/drawings/vmlDrawing" + strconv.Itoa(commentID) + ".vml" sheetRelationshipsComments := "../comments" + strconv.Itoa(commentID) + ".xml" diff --git a/datavalidation.go b/datavalidation.go index 3035bb2e63..e5ea5a5448 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -208,7 +208,7 @@ func convDataValidationOperatior(o DataValidationOperator) string { // dvRange.Sqref = "A1:B2" // dvRange.SetRange(10, 20, excelize.DataValidationTypeWhole, excelize.DataValidationOperatorBetween) // dvRange.SetError(excelize.DataValidationErrorStyleStop, "error title", "error body") -// xlsx.AddDataValidation("Sheet1", dvRange) +// err := xlsx.AddDataValidation("Sheet1", dvRange) // // Example 2, set data validation on Sheet1!A3:B4 with validation criteria // settings, and show input message when cell is selected: @@ -217,7 +217,7 @@ func convDataValidationOperatior(o DataValidationOperator) string { // dvRange.Sqref = "A3:B4" // dvRange.SetRange(10, 20, excelize.DataValidationTypeWhole, excelize.DataValidationOperatorGreaterThan) // dvRange.SetInput("input title", "input body") -// xlsx.AddDataValidation("Sheet1", dvRange) +// err = xlsx.AddDataValidation("Sheet1", dvRange) // // Example 3, set data validation on Sheet1!A5:B6 with validation criteria // settings, create in-cell dropdown by allowing list source: @@ -225,13 +225,17 @@ func convDataValidationOperatior(o DataValidationOperator) string { // dvRange = excelize.NewDataValidation(true) // dvRange.Sqref = "A5:B6" // dvRange.SetDropList([]string{"1", "2", "3"}) -// xlsx.AddDataValidation("Sheet1", dvRange) +// err = xlsx.AddDataValidation("Sheet1", dvRange) // -func (f *File) AddDataValidation(sheet string, dv *DataValidation) { - xlsx := f.workSheetReader(sheet) +func (f *File) AddDataValidation(sheet string, dv *DataValidation) error { + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return err + } if nil == xlsx.DataValidations { xlsx.DataValidations = new(xlsxDataValidations) } xlsx.DataValidations.DataValidation = append(xlsx.DataValidations.DataValidation, dv) xlsx.DataValidations.Count = len(xlsx.DataValidations.DataValidation) + return err } diff --git a/excelize.go b/excelize.go index 857f3ace6a..2f0db1e22f 100644 --- a/excelize.go +++ b/excelize.go @@ -14,6 +14,7 @@ import ( "archive/zip" "bytes" "encoding/xml" + "fmt" "io" "io/ioutil" "os" @@ -112,10 +113,10 @@ func (f *File) setDefaultTimeStyle(sheet, axis 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) *xlsxWorksheet { +func (f *File) workSheetReader(sheet string) (*xlsxWorksheet, error) { name, ok := f.sheetMap[trimSheetName(sheet)] if !ok { - name = "xl/worksheets/" + strings.ToLower(sheet) + ".xml" + return nil, fmt.Errorf("Sheet %s is not exist", sheet) } if f.Sheet[name] == nil { var xlsx xlsxWorksheet @@ -131,7 +132,7 @@ func (f *File) workSheetReader(sheet string) *xlsxWorksheet { } f.Sheet[name] = &xlsx } - return f.Sheet[name] + return f.Sheet[name], nil } // checkSheet provides a function to fill each row element and make that is @@ -197,9 +198,12 @@ func replaceWorkSheetsRelationshipsNameSpaceBytes(workbookMarshal []byte) []byte // // // -func (f *File) UpdateLinkedValue() { +func (f *File) UpdateLinkedValue() error { for _, name := range f.GetSheetMap() { - xlsx := f.workSheetReader(name) + xlsx, err := f.workSheetReader(name) + if err != nil { + return err + } for indexR := range xlsx.SheetData.Row { for indexC, col := range xlsx.SheetData.Row[indexR].C { if col.F != nil && col.V != "" { @@ -209,14 +213,16 @@ func (f *File) UpdateLinkedValue() { } } } + return nil } // GetMergeCells provides a function to get all merged cells from a worksheet currently. -func (f *File) GetMergeCells(sheet string) []MergeCell { - xlsx := f.workSheetReader(sheet) - +func (f *File) GetMergeCells(sheet string) ([]MergeCell, error) { var mergeCells []MergeCell - + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return mergeCells, err + } if xlsx.MergeCells != nil { mergeCells = make([]MergeCell, 0, len(xlsx.MergeCells.Cells)) @@ -228,7 +234,7 @@ func (f *File) GetMergeCells(sheet string) []MergeCell { } } - return mergeCells + return mergeCells, err } // MergeCell define a merged cell data. diff --git a/excelize_test.go b/excelize_test.go index 2fea9a94c6..4bd53236e5 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -57,10 +57,10 @@ func TestOpenFile(t *testing.T) { xlsx.SetSheetName("Maximum 31 characters allowed i", "[Rename]:\\/?* Maximum 31 characters allowed in sheet title.") xlsx.SetCellInt("Sheet3", "A23", 10) xlsx.SetCellStr("Sheet3", "b230", "10") - xlsx.SetCellStr("Sheet10", "b230", "10") + assert.EqualError(t, xlsx.SetCellStr("Sheet10", "b230", "10"), "Sheet Sheet10 is not exist") // Test set cell string value with illegal row number. - assert.EqualError(t, xlsx.SetCellStr("Sheet10", "A", "10"), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.EqualError(t, xlsx.SetCellStr("Sheet1", "A", "10"), `cannot convert cell "A" to coordinates: invalid cell name "A"`) xlsx.SetActiveSheet(2) // Test get cell formula with given rows number. @@ -298,7 +298,7 @@ func TestGetCellHyperLink(t *testing.T) { assert.NoError(t, err) t.Log(link, target) link, target, err = xlsx.GetCellHyperLink("Sheet3", "H3") - assert.NoError(t, err) + assert.EqualError(t, err, "Sheet Sheet3 is not exist") t.Log(link, target) } @@ -417,7 +417,7 @@ func TestGetMergeCells(t *testing.T) { } sheet1 := xlsx.GetSheetName(1) - mergeCells := xlsx.GetMergeCells(sheet1) + mergeCells, err := xlsx.GetMergeCells(sheet1) if !assert.Len(t, mergeCells, len(wants)) { t.FailNow() } @@ -784,7 +784,8 @@ func TestColumnVisibility(t *testing.T) { assert.EqualError(t, err, `invalid column name "*"`) assert.EqualError(t, xlsx.SetColVisible("Sheet1", "*", false), `invalid column name "*"`) - assert.NoError(t, xlsx.SetColVisible("Sheet3", "E", false)) + err = xlsx.SetColVisible("Sheet3", "E", false) + assert.EqualError(t, err, "Sheet Sheet3 is not exist") assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestColumnVisibility.xlsx"))) }) @@ -804,10 +805,7 @@ func TestCopySheet(t *testing.T) { } idx := xlsx.NewSheet("CopySheet") - err = xlsx.CopySheet(1, idx) - if !assert.NoError(t, err) { - t.FailNow() - } + assert.EqualError(t, xlsx.CopySheet(1, idx), "Sheet sheet1 is not exist") xlsx.SetCellValue("Sheet4", "F1", "Hello") val, err := xlsx.GetCellValue("Sheet1", "F1") @@ -923,9 +921,8 @@ func TestAutoFilter(t *testing.T) { for i, format := range formats { t.Run(fmt.Sprintf("Expression%d", i+1), func(t *testing.T) { err = xlsx.AutoFilter("Sheet3", "D4", "B1", format) - if assert.NoError(t, err) { - assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, i+1))) - } + assert.EqualError(t, err, "Sheet Sheet3 is not exist") + assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, i+1))) }) } diff --git a/file.go b/file.go index 2f5164f39e..a9e7eecf95 100644 --- a/file.go +++ b/file.go @@ -50,7 +50,7 @@ func NewFile() *File { f.WorkBook = f.workbookReader() f.WorkBookRels = f.workbookRelsReader() f.WorkSheetRels = make(map[string]*xlsxWorkbookRels) - f.Sheet["xl/worksheets/sheet1.xml"] = f.workSheetReader("Sheet1") + f.Sheet["xl/worksheets/sheet1.xml"], _ = f.workSheetReader("Sheet1") f.sheetMap["Sheet1"] = "xl/worksheets/sheet1.xml" f.Theme = f.themeReader() return f diff --git a/picture.go b/picture.go index 16572d41c6..274a4acd20 100644 --- a/picture.go +++ b/picture.go @@ -132,7 +132,6 @@ func (f *File) AddPicture(sheet, cell, picture, format string) error { // } // func (f *File) AddPictureFromBytes(sheet, cell, format, name, extension string, file []byte) error { - var err error var drawingHyperlinkRID int var hyperlinkType string ext, ok := supportImageTypes[extension] @@ -148,7 +147,10 @@ func (f *File) AddPictureFromBytes(sheet, cell, format, name, extension string, return err } // Read sheet data. - xlsx := f.workSheetReader(sheet) + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return err + } // 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" @@ -225,7 +227,7 @@ func (f *File) deleteSheetRelationships(sheet, rID string) { // addSheetLegacyDrawing provides a function to add legacy drawing element to // xl/worksheets/sheet%d.xml by given worksheet name and relationship index. func (f *File) addSheetLegacyDrawing(sheet string, rID int) { - xlsx := f.workSheetReader(sheet) + xlsx, _ := f.workSheetReader(sheet) xlsx.LegacyDrawing = &xlsxLegacyDrawing{ RID: "rId" + strconv.Itoa(rID), } @@ -234,7 +236,7 @@ func (f *File) addSheetLegacyDrawing(sheet string, rID int) { // addSheetDrawing provides a function to add drawing element to // xl/worksheets/sheet%d.xml by given worksheet name and relationship index. func (f *File) addSheetDrawing(sheet string, rID int) { - xlsx := f.workSheetReader(sheet) + xlsx, _ := f.workSheetReader(sheet) xlsx.Drawing = &xlsxDrawing{ RID: "rId" + strconv.Itoa(rID), } @@ -243,7 +245,7 @@ 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) { - xlsx := f.workSheetReader(sheet) + xlsx, _ := f.workSheetReader(sheet) xlsx.Picture = &xlsxPicture{ RID: "rId" + strconv.Itoa(rID), } @@ -500,7 +502,10 @@ func (f *File) GetPicture(sheet, cell string) (string, []byte, error) { } col-- row-- - xlsx := f.workSheetReader(sheet) + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return "", []byte{}, err + } if xlsx.Drawing == nil { return "", []byte{}, err } diff --git a/picture_test.go b/picture_test.go index 2b39ed8021..5b1a9e324b 100644 --- a/picture_test.go +++ b/picture_test.go @@ -102,7 +102,7 @@ func TestGetPicture(t *testing.T) { // Try to get picture from a worksheet that doesn't contain any images. file, raw, err = xlsx.GetPicture("Sheet3", "I9") - assert.NoError(t, err) + assert.EqualError(t, err, "Sheet Sheet3 is not exist") assert.Empty(t, file) assert.Empty(t, raw) diff --git a/rows.go b/rows.go index 6ece77bd83..7de18d305b 100644 --- a/rows.go +++ b/rows.go @@ -35,7 +35,10 @@ func (f *File) GetRows(sheet string) ([][]string, error) { return nil, nil } - xlsx := f.workSheetReader(sheet) + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return nil, err + } if xlsx != nil { output, _ := xml.Marshal(f.Sheet[name]) f.saveFileList(name, replaceWorkSheetsRelationshipsNameSpaceBytes(output)) @@ -165,7 +168,10 @@ func (err ErrSheetNotExist) Error() string { // } // func (f *File) Rows(sheet string) (*Rows, error) { - xlsx := f.workSheetReader(sheet) + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return nil, err + } name, ok := f.sheetMap[trimSheetName(sheet)] if !ok { return nil, ErrSheetNotExist{sheet} @@ -225,7 +231,10 @@ func (f *File) SetRowHeight(sheet string, row int, height float64) error { return newInvalidRowNumberError(row) } - xlsx := f.workSheetReader(sheet) + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return err + } prepareSheetXML(xlsx, 0, row) @@ -238,7 +247,7 @@ func (f *File) SetRowHeight(sheet string, row int, height float64) error { // getRowHeight provides a function to get row height in pixels by given sheet // name and row index. func (f *File) getRowHeight(sheet string, row int) int { - xlsx := f.workSheetReader(sheet) + xlsx, _ := f.workSheetReader(sheet) for _, v := range xlsx.SheetData.Row { if v.R == row+1 && v.Ht != 0 { return int(convertRowHeightToPixels(v.Ht)) @@ -258,7 +267,10 @@ func (f *File) GetRowHeight(sheet string, row int) (float64, error) { return defaultRowHeightPixels, newInvalidRowNumberError(row) } - xlsx := f.workSheetReader(sheet) + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return defaultRowHeightPixels, err + } if row > len(xlsx.SheetData.Row) { return defaultRowHeightPixels, nil // it will be better to use 0, but we take care with BC } @@ -321,7 +333,10 @@ func (f *File) SetRowVisible(sheet string, row int, visible bool) error { return newInvalidRowNumberError(row) } - xlsx := f.workSheetReader(sheet) + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return err + } prepareSheetXML(xlsx, 0, row) xlsx.SheetData.Row[row-1].Hidden = !visible return nil @@ -338,7 +353,10 @@ func (f *File) GetRowVisible(sheet string, row int) (bool, error) { return false, newInvalidRowNumberError(row) } - xlsx := f.workSheetReader(sheet) + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return false, err + } if row > len(xlsx.SheetData.Row) { return false, nil } @@ -355,7 +373,10 @@ func (f *File) SetRowOutlineLevel(sheet string, row int, level uint8) error { if row < 1 { return newInvalidRowNumberError(row) } - xlsx := f.workSheetReader(sheet) + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return err + } prepareSheetXML(xlsx, 0, row) xlsx.SheetData.Row[row-1].OutlineLevel = level return nil @@ -371,7 +392,10 @@ func (f *File) GetRowOutlineLevel(sheet string, row int) (uint8, error) { if row < 1 { return 0, newInvalidRowNumberError(row) } - xlsx := f.workSheetReader(sheet) + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return 0, err + } if row > len(xlsx.SheetData.Row) { return 0, nil } @@ -392,7 +416,10 @@ func (f *File) RemoveRow(sheet string, row int) error { return newInvalidRowNumberError(row) } - xlsx := f.workSheetReader(sheet) + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return err + } if row > len(xlsx.SheetData.Row) { return nil } @@ -445,7 +472,10 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) error { return newInvalidRowNumberError(row) } - xlsx := f.workSheetReader(sheet) + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return err + } if row > len(xlsx.SheetData.Row) || row2 < 1 || row == row2 { return nil } diff --git a/rows_test.go b/rows_test.go index f576efcc54..6a107ad30e 100644 --- a/rows_test.go +++ b/rows_test.go @@ -96,8 +96,8 @@ func TestRowVisibility(t *testing.T) { t.FailNow() } - assert.NoError(t, xlsx.SetRowVisible("Sheet3", 2, false)) - assert.NoError(t, xlsx.SetRowVisible("Sheet3", 2, true)) + assert.EqualError(t, xlsx.SetRowVisible("Sheet3", 2, false), "Sheet Sheet3 is not exist") + assert.EqualError(t, xlsx.SetRowVisible("Sheet3", 2, true), "Sheet Sheet3 is not exist") xlsx.GetRowVisible("Sheet3", 2) xlsx.GetRowVisible("Sheet3", 25) assert.EqualError(t, xlsx.SetRowVisible("Sheet3", 0, true), "invalid row number 0") @@ -112,8 +112,8 @@ func TestRowVisibility(t *testing.T) { func TestRemoveRow(t *testing.T) { xlsx := NewFile() sheet1 := xlsx.GetSheetName(1) - r := xlsx.workSheetReader(sheet1) - + r, err := xlsx.workSheetReader(sheet1) + assert.NoError(t, err) const ( colCount = 10 rowCount = 10 @@ -143,7 +143,7 @@ func TestRemoveRow(t *testing.T) { t.FailNow() } - err := xlsx.AutoFilter(sheet1, "A2", "A2", `{"column":"A","expression":"x != blanks"}`) + err = xlsx.AutoFilter(sheet1, "A2", "A2", `{"column":"A","expression":"x != blanks"}`) if !assert.NoError(t, err) { t.FailNow() } @@ -170,8 +170,8 @@ func TestRemoveRow(t *testing.T) { func TestInsertRow(t *testing.T) { xlsx := NewFile() sheet1 := xlsx.GetSheetName(1) - r := xlsx.workSheetReader(sheet1) - + r, err := xlsx.workSheetReader(sheet1) + assert.NoError(t, err) const ( colCount = 10 rowCount = 10 @@ -202,7 +202,8 @@ func TestInsertRow(t *testing.T) { func TestInsertRowInEmptyFile(t *testing.T) { xlsx := NewFile() sheet1 := xlsx.GetSheetName(1) - r := xlsx.workSheetReader(sheet1) + r, err := xlsx.workSheetReader(sheet1) + assert.NoError(t, err) assert.NoError(t, xlsx.InsertRow(sheet1, 1)) assert.Len(t, r.SheetData.Row, 0) assert.NoError(t, xlsx.InsertRow(sheet1, 2)) diff --git a/shape.go b/shape.go index e6b0456d80..5cac776e28 100644 --- a/shape.go +++ b/shape.go @@ -259,7 +259,10 @@ func (f *File) AddShape(sheet, cell, format string) error { return err } // Read sheet data. - xlsx := f.workSheetReader(sheet) + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return err + } // Add first shape for given sheet, create xl/drawings/ and xl/drawings/_rels/ folder. drawingID := f.countDrawings() + 1 drawingXML := "xl/drawings/drawing" + strconv.Itoa(drawingID) + ".xml" diff --git a/sheet.go b/sheet.go index 9960ef8ef5..f1f89bab18 100644 --- a/sheet.go +++ b/sheet.go @@ -245,7 +245,7 @@ func (f *File) SetActiveSheet(index int) { } } for idx, name := range f.GetSheetMap() { - xlsx := f.workSheetReader(name) + xlsx, _ := f.workSheetReader(name) if len(xlsx.SheetViews.SheetView) > 0 { xlsx.SheetViews.SheetView[0].TabSelected = false } @@ -265,7 +265,7 @@ func (f *File) SetActiveSheet(index int) { // XLSX. If not found the active sheet will be return integer 0. func (f *File) GetActiveSheetIndex() int { for idx, name := range f.GetSheetMap() { - xlsx := f.workSheetReader(name) + xlsx, _ := f.workSheetReader(name) for _, sheetView := range xlsx.SheetViews.SheetView { if sheetView.TabSelected { return idx @@ -380,11 +380,10 @@ func (f *File) SetSheetBackground(sheet, picture string) error { if !ok { return errors.New("unsupported image extension") } - pictureID := f.countMedia() + 1 - rID := f.addSheetRelationships(sheet, SourceRelationshipImage, "../media/image"+strconv.Itoa(pictureID)+ext, "") - f.addSheetPicture(sheet, rID) file, _ := ioutil.ReadFile(picture) - f.addMedia(file, ext) + name := f.addMedia(file, ext) + rID := f.addSheetRelationships(sheet, SourceRelationshipImage, strings.Replace(name, "xl", "..", 1), "") + f.addSheetPicture(sheet, rID) f.setContentTypePartImageExtensions() return err } @@ -452,14 +451,16 @@ func (f *File) CopySheet(from, to int) error { if from < 1 || to < 1 || from == to || f.GetSheetName(from) == "" || f.GetSheetName(to) == "" { return errors.New("invalid worksheet index") } - f.copySheet(from, to) - return nil + return f.copySheet(from, to) } // copySheet provides a function to duplicate a worksheet by gave source and // target worksheet name. -func (f *File) copySheet(from, to int) { - sheet := f.workSheetReader("sheet" + strconv.Itoa(from)) +func (f *File) copySheet(from, to int) error { + sheet, err := f.workSheetReader("sheet" + strconv.Itoa(from)) + if err != nil { + return err + } worksheet := deepcopy.Copy(sheet).(*xlsxWorksheet) path := "xl/worksheets/sheet" + strconv.Itoa(to) + ".xml" if len(worksheet.SheetViews.SheetView) > 0 { @@ -475,6 +476,7 @@ func (f *File) copySheet(from, to int) { if ok { f.XLSX[toRels] = f.XLSX[fromRels] } + return err } // SetSheetVisible provides a function to set worksheet visible by given worksheet @@ -488,9 +490,9 @@ func (f *File) copySheet(from, to int) { // // For example, hide Sheet1: // -// xlsx.SetSheetVisible("Sheet1", false) +// err := xlsx.SetSheetVisible("Sheet1", false) // -func (f *File) SetSheetVisible(name string, visible bool) { +func (f *File) SetSheetVisible(name string, visible bool) error { name = trimSheetName(name) content := f.workbookReader() if visible { @@ -499,7 +501,7 @@ func (f *File) SetSheetVisible(name string, visible bool) { content.Sheets.Sheet[k].State = "" } } - return + return nil } count := 0 for _, v := range content.Sheets.Sheet { @@ -508,7 +510,10 @@ func (f *File) SetSheetVisible(name string, visible bool) { } } for k, v := range content.Sheets.Sheet { - xlsx := f.workSheetReader(f.GetSheetMap()[k]) + xlsx, err := f.workSheetReader(f.GetSheetMap()[k]) + if err != nil { + return err + } tabSelected := false if len(xlsx.SheetViews.SheetView) > 0 { tabSelected = xlsx.SheetViews.SheetView[0].TabSelected @@ -517,6 +522,7 @@ func (f *File) SetSheetVisible(name string, visible bool) { content.Sheets.Sheet[k].State = "hidden" } } + return nil } // parseFormatPanesSet provides a function to parse the panes settings. @@ -611,9 +617,12 @@ func parseFormatPanesSet(formatSet string) (*formatPanes, error) { // // xlsx.SetPanes("Sheet1", `{"freeze":false,"split":false}`) // -func (f *File) SetPanes(sheet, panes string) { +func (f *File) SetPanes(sheet, panes string) error { fs, _ := parseFormatPanesSet(panes) - xlsx := f.workSheetReader(sheet) + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return err + } p := &xlsxPane{ ActivePane: fs.ActivePane, TopLeftCell: fs.TopLeftCell, @@ -638,6 +647,7 @@ func (f *File) SetPanes(sheet, panes string) { }) } xlsx.SheetViews.SheetView[len(xlsx.SheetViews.SheetView)-1].Selection = s + return err } // GetSheetVisible provides a function to get worksheet visible by given worksheet @@ -678,10 +688,16 @@ func (f *File) SearchSheet(sheet, value string, reg ...bool) ([]string, error) { for _, r := range reg { regSearch = r } - xlsx := f.workSheetReader(sheet) + var ( result []string ) + + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return result, err + } + name, ok := f.sheetMap[trimSheetName(sheet)] if !ok { return result, nil @@ -740,13 +756,16 @@ func (f *File) SearchSheet(sheet, value string, reg ...bool) ([]string, error) { // or deliberately changing, moving, or deleting data in a worksheet. For // example, protect Sheet1 with protection settings: // -// xlsx.ProtectSheet("Sheet1", &excelize.FormatSheetProtection{ +// err := xlsx.ProtectSheet("Sheet1", &excelize.FormatSheetProtection{ // Password: "password", // EditScenarios: false, // }) // -func (f *File) ProtectSheet(sheet string, settings *FormatSheetProtection) { - xlsx := f.workSheetReader(sheet) +func (f *File) ProtectSheet(sheet string, settings *FormatSheetProtection) error { + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return err + } if settings == nil { settings = &FormatSheetProtection{ EditObjects: true, @@ -775,12 +794,17 @@ func (f *File) ProtectSheet(sheet string, settings *FormatSheetProtection) { if settings.Password != "" { xlsx.SheetProtection.Password = genSheetPasswd(settings.Password) } + return err } // UnprotectSheet provides a function to unprotect an Excel worksheet. -func (f *File) UnprotectSheet(sheet string) { - xlsx := f.workSheetReader(sheet) +func (f *File) UnprotectSheet(sheet string) error { + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return err + } xlsx.SheetProtection = nil + return err } // trimSheetName provides a function to trim invaild characters by given worksheet @@ -989,7 +1013,10 @@ func (p *PageLayoutPaperSize) getPageLayout(ps *xlsxPageSetUp) { // 118 | PRC Envelope #10 Rotated (458 mm x 324 mm) // func (f *File) SetPageLayout(sheet string, opts ...PageLayoutOption) error { - s := f.workSheetReader(sheet) + s, err := f.workSheetReader(sheet) + if err != nil { + return err + } ps := s.PageSetUp if ps == nil { ps = new(xlsxPageSetUp) @@ -999,7 +1026,7 @@ func (f *File) SetPageLayout(sheet string, opts ...PageLayoutOption) error { for _, opt := range opts { opt.setPageLayout(ps) } - return nil + return err } // GetPageLayout provides a function to gets worksheet page layout. @@ -1008,13 +1035,16 @@ func (f *File) SetPageLayout(sheet string, opts ...PageLayoutOption) error { // PageLayoutOrientation(string) // PageLayoutPaperSize(int) func (f *File) GetPageLayout(sheet string, opts ...PageLayoutOptionPtr) error { - s := f.workSheetReader(sheet) + s, err := f.workSheetReader(sheet) + if err != nil { + return err + } ps := s.PageSetUp for _, opt := range opts { opt.getPageLayout(ps) } - return nil + return err } // workSheetRelsReader provides a function to get the pointer to the structure diff --git a/sheetpr.go b/sheetpr.go index 14c18da0f5..66761f3f7f 100644 --- a/sheetpr.go +++ b/sheetpr.go @@ -154,7 +154,10 @@ func (o *AutoPageBreaks) getSheetPrOption(pr *xlsxSheetPr) { // AutoPageBreaks(bool) // OutlineSummaryBelow(bool) func (f *File) SetSheetPrOptions(name string, opts ...SheetPrOption) error { - sheet := f.workSheetReader(name) + sheet, err := f.workSheetReader(name) + if err != nil { + return err + } pr := sheet.SheetPr if pr == nil { pr = new(xlsxSheetPr) @@ -164,7 +167,7 @@ func (f *File) SetSheetPrOptions(name string, opts ...SheetPrOption) error { for _, opt := range opts { opt.setSheetPrOption(pr) } - return nil + return err } // GetSheetPrOptions provides a function to gets worksheet properties. @@ -177,11 +180,14 @@ func (f *File) SetSheetPrOptions(name string, opts ...SheetPrOption) error { // AutoPageBreaks(bool) // OutlineSummaryBelow(bool) func (f *File) GetSheetPrOptions(name string, opts ...SheetPrOptionPtr) error { - sheet := f.workSheetReader(name) + sheet, err := f.workSheetReader(name) + if err != nil { + return err + } pr := sheet.SheetPr for _, opt := range opts { opt.getSheetPrOption(pr) } - return nil + return err } diff --git a/sheetview.go b/sheetview.go index 6b191e92f5..8ffc9bcd6d 100644 --- a/sheetview.go +++ b/sheetview.go @@ -110,7 +110,10 @@ func (o *ZoomScale) getSheetViewOption(view *xlsxSheetView) { // getSheetView returns the SheetView object func (f *File) getSheetView(sheetName string, viewIndex int) (*xlsxSheetView, error) { - xlsx := f.workSheetReader(sheetName) + xlsx, err := f.workSheetReader(sheetName) + if err != nil { + return nil, err + } if viewIndex < 0 { if viewIndex < -len(xlsx.SheetViews.SheetView) { return nil, fmt.Errorf("view index %d out of range", viewIndex) @@ -120,7 +123,7 @@ func (f *File) getSheetView(sheetName string, viewIndex int) (*xlsxSheetView, er return nil, fmt.Errorf("view index %d out of range", viewIndex) } - return &(xlsx.SheetViews.SheetView[viewIndex]), nil + return &(xlsx.SheetViews.SheetView[viewIndex]), err } // SetSheetViewOptions sets sheet view options. diff --git a/styles.go b/styles.go index 1cc025cc73..a515756ef9 100644 --- a/styles.go +++ b/styles.go @@ -2266,7 +2266,10 @@ 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) { - xlsx := f.workSheetReader(sheet) + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return 0, err + } cellData, col, _, err := f.prepareCell(xlsx, sheet, axis) if err != nil { return 0, err @@ -2365,7 +2368,10 @@ func (f *File) SetCellStyle(sheet, hcell, vcell string, styleID int) error { vcolIdx := vcol - 1 vrowIdx := vrow - 1 - xlsx := f.workSheetReader(sheet) + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return err + } prepareSheetXML(xlsx, vcol, vrow) for r := hrowIdx; r <= vrowIdx; r++ { @@ -2373,7 +2379,7 @@ func (f *File) SetCellStyle(sheet, hcell, vcell string, styleID int) error { xlsx.SheetData.Row[r].C[k].S = styleID } } - return nil + return err } // SetConditionalFormat provides a function to create conditional formatting @@ -2605,7 +2611,10 @@ func (f *File) SetConditionalFormat(sheet, area, formatSet string) error { "expression": drawConfFmtExp, } - xlsx := f.workSheetReader(sheet) + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return err + } cfRule := []*xlsxCfRule{} for p, v := range format { var vt, ct string diff --git a/styles_test.go b/styles_test.go index c6fbbef0b7..b6f6042ef7 100644 --- a/styles_test.go +++ b/styles_test.go @@ -158,7 +158,8 @@ func TestSetConditionalFormat(t *testing.T) { t.Fatalf("%s", err) } - xlsx := xl.workSheetReader(sheet) + xlsx, err := xl.workSheetReader(sheet) + assert.NoError(t, err) cf := xlsx.ConditionalFormatting assert.Len(t, cf, 1, testCase.label) assert.Len(t, cf[0].CfRule, 1, testCase.label) diff --git a/table.go b/table.go index 2ed8654783..4505994be3 100644 --- a/table.go +++ b/table.go @@ -102,7 +102,7 @@ func (f *File) countTables() int { // addSheetTable provides a function to add tablePart element to // xl/worksheets/sheet%d.xml by given worksheet name and relationship index. func (f *File) addSheetTable(sheet string, rID int) { - xlsx := f.workSheetReader(sheet) + xlsx, _ := f.workSheetReader(sheet) table := &xlsxTablePart{ RID: "rId" + strconv.Itoa(rID), } @@ -299,7 +299,10 @@ func (f *File) AutoFilter(sheet, hcell, vcell, format string) 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, refRange, col int, formatSet *formatAutoFilter) error { - xlsx := f.workSheetReader(sheet) + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return err + } if xlsx.SheetPr != nil { xlsx.SheetPr.FilterMode = true } From a88459d5f1e83006ba421f334a1513d1c231eb6b Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 16 Apr 2019 10:57:21 +0800 Subject: [PATCH 082/957] add unit tests to functions --- adjust_test.go | 8 ++- cell.go | 83 +++++++++++++++-------- chart_test.go | 5 ++ date_test.go | 2 +- excelize.go | 2 +- excelize_test.go | 158 ++++++++++++++++++++++++++++---------------- picture_test.go | 4 +- rows_test.go | 167 ++++++++++++++++++++++++++++++++++++++++++----- sheet.go | 14 ++-- sheet_test.go | 50 ++++++-------- sheetpr_test.go | 36 +++------- styles_test.go | 2 - 12 files changed, 356 insertions(+), 175 deletions(-) diff --git a/adjust_test.go b/adjust_test.go index a35e6098c5..7b708ab70c 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -12,7 +12,7 @@ func TestAdjustMergeCells(t *testing.T) { assert.EqualError(t, f.adjustMergeCells(&xlsxWorksheet{ MergeCells: &xlsxMergeCells{ Cells: []*xlsxMergeCell{ - &xlsxMergeCell{ + { Ref: "A:B1", }, }, @@ -21,7 +21,7 @@ func TestAdjustMergeCells(t *testing.T) { assert.EqualError(t, f.adjustMergeCells(&xlsxWorksheet{ MergeCells: &xlsxMergeCells{ Cells: []*xlsxMergeCell{ - &xlsxMergeCell{ + { Ref: "A1:B", }, }, @@ -50,7 +50,7 @@ func TestAdjustHelper(t *testing.T) { f.Sheet["xl/worksheets/sheet1.xml"] = &xlsxWorksheet{ MergeCells: &xlsxMergeCells{ Cells: []*xlsxMergeCell{ - &xlsxMergeCell{ + { Ref: "A:B1", }, }, @@ -64,4 +64,6 @@ func TestAdjustHelper(t *testing.T) { // testing adjustHelper with illegal cell coordinates. assert.EqualError(t, f.adjustHelper("Sheet1", rows, 0, 0), `cannot convert cell "A" to coordinates: invalid cell name "A"`) assert.EqualError(t, f.adjustHelper("Sheet2", rows, 0, 0), `cannot convert cell "B" to coordinates: invalid cell name "B"`) + // testing 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 6a8eebecd6..4d22842e63 100644 --- a/cell.go +++ b/cell.go @@ -69,6 +69,38 @@ func (f *File) GetCellValue(sheet, axis string) (string, error) { // Note that default date format is m/d/yy h:mm of time.Time type value. You can // set numbers format by SetCellStyle() method. func (f *File) SetCellValue(sheet, axis 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) + case float32: + err = f.SetCellFloat(sheet, axis, float64(v), -1, 32) + case float64: + err = f.SetCellFloat(sheet, axis, v, -1, 64) + case string: + err = f.SetCellStr(sheet, axis, v) + case []byte: + err = f.SetCellStr(sheet, axis, string(v)) + case time.Duration: + err = f.SetCellDefault(sheet, axis, strconv.FormatFloat(v.Seconds()/86400.0, 'f', -1, 32)) + if err != nil { + return err + } + err = f.setDefaultTimeStyle(sheet, axis, 21) + case time.Time: + err = f.setCellTimeFunc(sheet, axis, v) + case bool: + err = f.SetCellBool(sheet, axis, v) + case nil: + err = f.SetCellStr(sheet, axis, "") + default: + err = f.SetCellStr(sheet, axis, fmt.Sprintf("%v", value)) + } + return err +} + +// setCellIntFunc is a wrapper of SetCellInt. +func (f *File) setCellIntFunc(sheet, axis string, value interface{}) error { var err error switch v := value.(type) { case int: @@ -91,34 +123,31 @@ func (f *File) SetCellValue(sheet, axis string, value interface{}) error { err = f.SetCellInt(sheet, axis, int(v)) case uint64: err = f.SetCellInt(sheet, axis, int(v)) - case float32: - err = f.SetCellFloat(sheet, axis, float64(v), -1, 32) - case float64: - err = f.SetCellFloat(sheet, axis, v, -1, 64) - case string: - err = f.SetCellStr(sheet, axis, v) - case []byte: - err = f.SetCellStr(sheet, axis, string(v)) - case time.Duration: - err = f.SetCellDefault(sheet, axis, strconv.FormatFloat(v.Seconds()/86400.0, 'f', -1, 32)) - err = f.setDefaultTimeStyle(sheet, axis, 21) - case time.Time: - excelTime, err := timeToExcelTime(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 { + excelTime, err := timeToExcelTime(value) + if err != nil { + return err + } + if excelTime > 0 { + err = f.SetCellDefault(sheet, axis, strconv.FormatFloat(excelTime, 'f', -1, 64)) if err != nil { return err } - if excelTime > 0 { - err = f.SetCellDefault(sheet, axis, strconv.FormatFloat(excelTime, 'f', -1, 64)) - err = f.setDefaultTimeStyle(sheet, axis, 22) - } else { - err = f.SetCellStr(sheet, axis, v.Format(time.RFC3339Nano)) + err = f.setDefaultTimeStyle(sheet, axis, 22) + if err != nil { + return err + } + } else { + err = f.SetCellStr(sheet, axis, value.Format(time.RFC3339Nano)) + if err != nil { + return err } - case bool: - err = f.SetCellBool(sheet, axis, v) - case nil: - err = f.SetCellStr(sheet, axis, "") - default: - err = f.SetCellStr(sheet, axis, fmt.Sprintf("%v", value)) } return err } @@ -398,8 +427,6 @@ func (f *File) MergeCell(sheet, hcell, vcell string) error { } if xlsx.MergeCells != nil { ref := hcell + ":" + vcell - cells := make([]*xlsxMergeCell, 0, len(xlsx.MergeCells.Cells)) - // Delete the merged cells of the overlapping area. for _, cellData := range xlsx.MergeCells.Cells { cc := strings.Split(cellData.Ref, ":") @@ -413,10 +440,8 @@ func (f *File) MergeCell(sheet, hcell, vcell string) error { if !(!c1 && !c2 && !c3 && !c4) { return nil } - cells = append(cells, cellData) } - cells = append(xlsx.MergeCells.Cells, &xlsxMergeCell{Ref: ref}) - xlsx.MergeCells.Cells = cells + xlsx.MergeCells.Cells = append(xlsx.MergeCells.Cells, &xlsxMergeCell{Ref: ref}) } else { xlsx.MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: hcell + ":" + vcell}}} } diff --git a/chart_test.go b/chart_test.go index 1dfc46869e..98baeddb4e 100644 --- a/chart_test.go +++ b/chart_test.go @@ -96,3 +96,8 @@ func TestChartSize(t *testing.T) { t.FailNow() } } + +func TestAddDrawingChart(t *testing.T) { + f := NewFile() + assert.EqualError(t, f.addDrawingChart("SheetN", "", "", 0, 0, 0, nil), `cannot convert cell "" to coordinates: invalid cell name ""`) +} diff --git a/date_test.go b/date_test.go index 63cb19e3bc..2885af07a6 100644 --- a/date_test.go +++ b/date_test.go @@ -69,7 +69,7 @@ func TestTimeFromExcelTime(t *testing.T) { } func TestTimeFromExcelTime_1904(t *testing.T) { - shiftJulianToNoon(1, -0.6) + _, _ = shiftJulianToNoon(1, -0.6) timeFromExcelTime(61, true) timeFromExcelTime(62, true) } diff --git a/excelize.go b/excelize.go index 2f0db1e22f..6e93f8d0e8 100644 --- a/excelize.go +++ b/excelize.go @@ -116,7 +116,7 @@ func (f *File) setDefaultTimeStyle(sheet, axis string, format int) error { func (f *File) workSheetReader(sheet string) (*xlsxWorksheet, error) { name, ok := f.sheetMap[trimSheetName(sheet)] if !ok { - return nil, fmt.Errorf("Sheet %s is not exist", sheet) + return nil, fmt.Errorf("sheet %s is not exist", sheet) } if f.Sheet[name] == nil { var xlsx xlsxWorksheet diff --git a/excelize_test.go b/excelize_test.go index 4bd53236e5..619211050a 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -57,7 +57,7 @@ func TestOpenFile(t *testing.T) { xlsx.SetSheetName("Maximum 31 characters allowed i", "[Rename]:\\/?* Maximum 31 characters allowed in sheet title.") xlsx.SetCellInt("Sheet3", "A23", 10) xlsx.SetCellStr("Sheet3", "b230", "10") - assert.EqualError(t, xlsx.SetCellStr("Sheet10", "b230", "10"), "Sheet Sheet10 is not exist") + assert.EqualError(t, xlsx.SetCellStr("Sheet10", "b230", "10"), "sheet Sheet10 is not exist") // Test set cell string value with illegal row number. assert.EqualError(t, xlsx.SetCellStr("Sheet1", "A", "10"), `cannot convert cell "A" to coordinates: invalid cell name "A"`) @@ -92,24 +92,31 @@ func TestOpenFile(t *testing.T) { xlsx.GetCellValue("Sheet2", "D11") xlsx.GetCellValue("Sheet2", "D12") // Test SetCellValue function. - xlsx.SetCellValue("Sheet2", "F1", " Hello") - xlsx.SetCellValue("Sheet2", "G1", []byte("World")) - xlsx.SetCellValue("Sheet2", "F2", 42) - xlsx.SetCellValue("Sheet2", "F3", int8(1<<8/2-1)) - xlsx.SetCellValue("Sheet2", "F4", int16(1<<16/2-1)) - xlsx.SetCellValue("Sheet2", "F5", int32(1<<32/2-1)) - xlsx.SetCellValue("Sheet2", "F6", int64(1<<32/2-1)) - xlsx.SetCellValue("Sheet2", "F7", float32(42.65418)) - xlsx.SetCellValue("Sheet2", "F8", float64(-42.65418)) - xlsx.SetCellValue("Sheet2", "F9", float32(42)) - xlsx.SetCellValue("Sheet2", "F10", float64(42)) - xlsx.SetCellValue("Sheet2", "F11", uint(1<<32-1)) - xlsx.SetCellValue("Sheet2", "F12", uint8(1<<8-1)) - xlsx.SetCellValue("Sheet2", "F13", uint16(1<<16-1)) - xlsx.SetCellValue("Sheet2", "F14", uint32(1<<32-1)) - xlsx.SetCellValue("Sheet2", "F15", uint64(1<<32-1)) - xlsx.SetCellValue("Sheet2", "F16", true) - xlsx.SetCellValue("Sheet2", "F17", complex64(5+10i)) + assert.NoError(t, xlsx.SetCellValue("Sheet2", "F1", " Hello")) + assert.NoError(t, xlsx.SetCellValue("Sheet2", "G1", []byte("World"))) + assert.NoError(t, xlsx.SetCellValue("Sheet2", "F2", 42)) + assert.NoError(t, xlsx.SetCellValue("Sheet2", "F3", int8(1<<8/2-1))) + assert.NoError(t, xlsx.SetCellValue("Sheet2", "F4", int16(1<<16/2-1))) + assert.NoError(t, xlsx.SetCellValue("Sheet2", "F5", int32(1<<32/2-1))) + assert.NoError(t, xlsx.SetCellValue("Sheet2", "F6", int64(1<<32/2-1))) + assert.NoError(t, xlsx.SetCellValue("Sheet2", "F7", float32(42.65418))) + assert.NoError(t, xlsx.SetCellValue("Sheet2", "F8", float64(-42.65418))) + assert.NoError(t, xlsx.SetCellValue("Sheet2", "F9", float32(42))) + assert.NoError(t, xlsx.SetCellValue("Sheet2", "F10", float64(42))) + assert.NoError(t, xlsx.SetCellValue("Sheet2", "F11", uint(1<<32-1))) + assert.NoError(t, xlsx.SetCellValue("Sheet2", "F12", uint8(1<<8-1))) + assert.NoError(t, xlsx.SetCellValue("Sheet2", "F13", uint16(1<<16-1))) + assert.NoError(t, xlsx.SetCellValue("Sheet2", "F14", uint32(1<<32-1))) + assert.NoError(t, xlsx.SetCellValue("Sheet2", "F15", uint64(1<<32-1))) + assert.NoError(t, xlsx.SetCellValue("Sheet2", "F16", true)) + assert.NoError(t, xlsx.SetCellValue("Sheet2", "F17", complex64(5+10i))) + + // Test on not exists worksheet. + assert.EqualError(t, xlsx.SetCellDefault("SheetN", "A1", ""), "sheet SheetN is not exist") + assert.EqualError(t, xlsx.SetCellFloat("SheetN", "A1", 42.65418, 2, 32), "sheet SheetN is not exist") + assert.EqualError(t, xlsx.SetCellBool("SheetN", "A1", true), "sheet SheetN is not exist") + assert.EqualError(t, xlsx.SetCellFormula("SheetN", "A1", ""), "sheet SheetN is not exist") + assert.EqualError(t, xlsx.SetCellHyperLink("SheetN", "A1", "Sheet1!A40", "Location"), "sheet SheetN is not exist") // Test boolean write booltest := []struct { @@ -250,6 +257,10 @@ func TestColWidth(t *testing.T) { assert.EqualError(t, xlsx.SetColWidth("Sheet1", "*", "B", 1), `invalid column name "*"`) assert.EqualError(t, xlsx.SetColWidth("Sheet1", "A", "*", 1), `invalid column name "*"`) + // Test get column width on not exists worksheet. + _, err = xlsx.GetColWidth("SheetN", "A") + assert.EqualError(t, err, "sheet SheetN is not exist") + err = xlsx.SaveAs(filepath.Join("test", "TestColWidth.xlsx")) if err != nil { t.Error(err) @@ -288,17 +299,17 @@ func TestGetCellHyperLink(t *testing.T) { t.FailNow() } - link, target, err := xlsx.GetCellHyperLink("Sheet1", "") + _, _, err = xlsx.GetCellHyperLink("Sheet1", "") assert.EqualError(t, err, `invalid cell name ""`) - link, target, err = xlsx.GetCellHyperLink("Sheet1", "A22") + link, target, err := xlsx.GetCellHyperLink("Sheet1", "A22") assert.NoError(t, err) t.Log(link, target) link, target, err = xlsx.GetCellHyperLink("Sheet2", "D6") assert.NoError(t, err) t.Log(link, target) link, target, err = xlsx.GetCellHyperLink("Sheet3", "H3") - assert.EqualError(t, err, "Sheet Sheet3 is not exist") + assert.EqualError(t, err, "sheet Sheet3 is not exist") t.Log(link, target) } @@ -421,12 +432,17 @@ func TestGetMergeCells(t *testing.T) { if !assert.Len(t, mergeCells, len(wants)) { t.FailNow() } + assert.NoError(t, err) for i, m := range mergeCells { assert.Equal(t, wants[i].value, m.GetCellValue()) assert.Equal(t, wants[i].start, m.GetStartAxis()) assert.Equal(t, wants[i].end, m.GetEndAxis()) } + + // Test get merged cells on not exists worksheet. + _, err = xlsx.GetMergeCells("SheetN") + assert.EqualError(t, err, "sheet SheetN is not exist") } func TestSetCellStyleAlignment(t *testing.T) { @@ -779,13 +795,19 @@ func TestColumnVisibility(t *testing.T) { assert.Equal(t, true, visible) assert.NoError(t, err) + // Test get column visiable on not exists worksheet. + _, err = xlsx.GetColVisible("SheetN", "F") + assert.EqualError(t, err, "sheet SheetN is not exist") + // Test get column visiable with illegal cell coordinates. _, err = xlsx.GetColVisible("Sheet1", "*") assert.EqualError(t, err, `invalid column name "*"`) assert.EqualError(t, xlsx.SetColVisible("Sheet1", "*", false), `invalid column name "*"`) - err = xlsx.SetColVisible("Sheet3", "E", false) - assert.EqualError(t, err, "Sheet Sheet3 is not exist") + xlsx.NewSheet("Sheet3") + assert.NoError(t, xlsx.SetColVisible("Sheet3", "E", false)) + + assert.EqualError(t, xlsx.SetColVisible("SheetN", "E", false), "sheet SheetN is not exist") assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestColumnVisibility.xlsx"))) }) @@ -805,7 +827,7 @@ func TestCopySheet(t *testing.T) { } idx := xlsx.NewSheet("CopySheet") - assert.EqualError(t, xlsx.CopySheet(1, idx), "Sheet sheet1 is not exist") + assert.EqualError(t, xlsx.CopySheet(1, idx), "sheet sheet1 is not exist") xlsx.SetCellValue("Sheet4", "F1", "Hello") val, err := xlsx.GetCellValue("Sheet1", "F1") @@ -920,8 +942,8 @@ func TestAutoFilter(t *testing.T) { for i, format := range formats { t.Run(fmt.Sprintf("Expression%d", i+1), func(t *testing.T) { - err = xlsx.AutoFilter("Sheet3", "D4", "B1", format) - assert.EqualError(t, err, "Sheet Sheet3 is not exist") + err = xlsx.AutoFilter("Sheet1", "D4", "B1", format) + assert.NoError(t, err) assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, i+1))) }) } @@ -966,38 +988,42 @@ func TestAddChart(t *testing.T) { categories := map[string]string{"A30": "Small", "A31": "Normal", "A32": "Large", "B29": "Apple", "C29": "Orange", "D29": "Pear"} values := map[string]int{"B30": 2, "C30": 3, "D30": 3, "B31": 5, "C31": 2, "D31": 4, "B32": 6, "C32": 7, "D32": 8} for k, v := range categories { - xlsx.SetCellValue("Sheet1", k, v) + assert.NoError(t, xlsx.SetCellValue("Sheet1", k, v)) } for k, v := range values { - xlsx.SetCellValue("Sheet1", k, v) - } - xlsx.AddChart("Sheet1", "P1", "") - xlsx.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"}],"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":"Fruit 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"}`) - xlsx.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"}],"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":"Fruit 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"}`) - xlsx.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"}],"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":"Fruit 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"}`) - xlsx.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"}],"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":"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},"show_blanks_as":"zero"}`) - xlsx.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"}],"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":"Fruit 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"}`) - xlsx.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"}],"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":"Fruit 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"}`) - xlsx.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"}],"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":"Fruit 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"}`) - xlsx.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"}],"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":"Fruit 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"}`) - xlsx.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"}],"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":"Fruit 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"}`) - xlsx.AddChart("Sheet2", "P16", `{"type":"doughnut","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"}],"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":"Fruit 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"}`) - xlsx.AddChart("Sheet2", "X16", `{"type":"line","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"}],"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":"Fruit 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"}`) - xlsx.AddChart("Sheet2", "P32", `{"type":"pie3D","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"}],"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":"Fruit 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"}`) - xlsx.AddChart("Sheet2", "X32", `{"type":"pie","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"}],"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":"Fruit 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"}`) - xlsx.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"}],"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":"Fruit 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"}`) - xlsx.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"}],"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":"Fruit 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"}`) - xlsx.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"}],"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":"Fruit 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"}`) - xlsx.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"}],"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":"Fruit 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"}`) - xlsx.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"}],"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":"Fruit 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}}`) - xlsx.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"}],"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":"Fruit 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, xlsx.SetCellValue("Sheet1", k, v)) + } + assert.EqualError(t, xlsx.AddChart("Sheet1", "P1", ""), "unexpected end of JSON input") + + // Test add chart on not exists worksheet. + assert.EqualError(t, xlsx.AddChart("SheetN", "P1", "{}"), "sheet SheetN is not exist") + + assert.NoError(t, xlsx.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"}],"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":"Fruit 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, xlsx.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"}],"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":"Fruit 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, xlsx.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"}],"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":"Fruit 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, xlsx.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"}],"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":"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},"show_blanks_as":"zero"}`)) + assert.NoError(t, xlsx.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"}],"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":"Fruit 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"}`)) + assert.NoError(t, xlsx.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"}],"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":"Fruit 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, xlsx.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"}],"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":"Fruit 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, xlsx.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"}],"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":"Fruit 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, xlsx.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"}],"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":"Fruit 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, xlsx.AddChart("Sheet2", "P16", `{"type":"doughnut","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"}],"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":"Fruit 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"}`)) + assert.NoError(t, xlsx.AddChart("Sheet2", "X16", `{"type":"line","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"}],"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":"Fruit 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"}`)) + assert.NoError(t, xlsx.AddChart("Sheet2", "P32", `{"type":"pie3D","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"}],"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":"Fruit 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, xlsx.AddChart("Sheet2", "X32", `{"type":"pie","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"}],"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":"Fruit 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, xlsx.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"}],"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":"Fruit 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, xlsx.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"}],"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":"Fruit 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, xlsx.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"}],"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":"Fruit 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, xlsx.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"}],"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":"Fruit 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, xlsx.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"}],"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":"Fruit 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, xlsx.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"}],"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":"Fruit 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}}`)) // area series charts - xlsx.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"}],"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":"Fruit 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"}`) - xlsx.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"}],"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":"Fruit 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"}`) - xlsx.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"}],"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":"Fruit 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"}`) - xlsx.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"}],"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":"Fruit 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"}`) - xlsx.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"}],"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":"Fruit 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"}`) - xlsx.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"}],"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":"Fruit 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"}`) + assert.NoError(t, xlsx.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"}],"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":"Fruit 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, xlsx.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"}],"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":"Fruit 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, xlsx.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"}],"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":"Fruit 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, xlsx.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"}],"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":"Fruit 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, xlsx.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"}],"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":"Fruit 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, xlsx.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"}],"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":"Fruit 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"}`)) assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestAddChart.xlsx"))) } @@ -1042,6 +1068,9 @@ func TestRemoveCol(t *testing.T) { // Test remove column with illegal cell coordinates. assert.EqualError(t, xlsx.RemoveCol("Sheet1", "*"), `invalid column name "*"`) + // Test remove column on not exists worksheet. + assert.EqualError(t, xlsx.RemoveCol("SheetN", "B"), "sheet SheetN is not exist") + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestRemoveCol.xlsx"))) } @@ -1183,11 +1212,14 @@ func TestOutlineLevel(t *testing.T) { // Test set and get column outline level with illegal cell coordinates. assert.EqualError(t, xlsx.SetColOutlineLevel("Sheet1", "*", 1), `invalid column name "*"`) - level, err := xlsx.GetColOutlineLevel("Sheet1", "*") + _, err := xlsx.GetColOutlineLevel("Sheet1", "*") assert.EqualError(t, err, `invalid column name "*"`) + // Test set column outline level on not exists worksheet. + assert.EqualError(t, xlsx.SetColOutlineLevel("SheetN", "E", 2), "sheet SheetN is not exist") + assert.EqualError(t, xlsx.SetRowOutlineLevel("Sheet1", 0, 1), "invalid row number 0") - level, err = xlsx.GetRowOutlineLevel("Sheet1", 2) + level, err := xlsx.GetRowOutlineLevel("Sheet1", 2) assert.NoError(t, err) assert.Equal(t, uint8(250), level) @@ -1260,6 +1292,8 @@ func TestProtectSheet(t *testing.T) { }) assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestProtectSheet.xlsx"))) + // Test protect not exists worksheet. + assert.EqualError(t, xlsx.ProtectSheet("SheetN", nil), "sheet SheetN is not exist") } func TestUnprotectSheet(t *testing.T) { @@ -1267,11 +1301,19 @@ func TestUnprotectSheet(t *testing.T) { if !assert.NoError(t, err) { t.FailNow() } + // Test unprotect not exists worksheet. + assert.EqualError(t, xlsx.UnprotectSheet("SheetN"), "sheet SheetN is not exist") xlsx.UnprotectSheet("Sheet1") assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestUnprotectSheet.xlsx"))) } +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") +} + func prepareTestBook1() (*File, error) { xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if err != nil { diff --git a/picture_test.go b/picture_test.go index 5b1a9e324b..890092e9f9 100644 --- a/picture_test.go +++ b/picture_test.go @@ -97,12 +97,12 @@ func TestGetPicture(t *testing.T) { } // Try to get picture from a worksheet with illegal cell coordinates. - file, raw, err = xlsx.GetPicture("Sheet1", "A") + _, _, err = xlsx.GetPicture("Sheet1", "A") assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`) // Try to get picture from a worksheet that doesn't contain any images. file, raw, err = xlsx.GetPicture("Sheet3", "I9") - assert.EqualError(t, err, "Sheet Sheet3 is not exist") + assert.EqualError(t, err, "sheet Sheet3 is not exist") assert.Empty(t, file) assert.Empty(t, raw) diff --git a/rows_test.go b/rows_test.go index 6a107ad30e..f7d49b4695 100644 --- a/rows_test.go +++ b/rows_test.go @@ -50,7 +50,7 @@ func TestRowsError(t *testing.T) { t.FailNow() } _, err = xlsx.Rows("SheetN") - assert.EqualError(t, err, "Sheet SheetN is not exist") + assert.EqualError(t, err, "sheet SheetN is not exist") } func TestRowHeight(t *testing.T) { @@ -59,11 +59,11 @@ func TestRowHeight(t *testing.T) { assert.EqualError(t, xlsx.SetRowHeight(sheet1, 0, defaultRowHeightPixels+1.0), "invalid row number 0") - height, err := xlsx.GetRowHeight("Sheet1", 0) + _, err := xlsx.GetRowHeight("Sheet1", 0) assert.EqualError(t, err, "invalid row number 0") assert.NoError(t, xlsx.SetRowHeight(sheet1, 1, 111.0)) - height, err = xlsx.GetRowHeight(sheet1, 1) + height, err := xlsx.GetRowHeight(sheet1, 1) assert.NoError(t, err) assert.Equal(t, 111.0, height) @@ -82,6 +82,11 @@ func TestRowHeight(t *testing.T) { assert.NoError(t, err) assert.Equal(t, defaultRowHeightPixels, height) + // Test set and get row height on not exists worksheet. + assert.EqualError(t, xlsx.SetRowHeight("SheetN", 1, 111.0), "sheet SheetN is not exist") + _, err = xlsx.GetRowHeight("SheetN", 3) + assert.EqualError(t, err, "sheet SheetN is not exist") + err = xlsx.SaveAs(filepath.Join("test", "TestRowHeight.xlsx")) if !assert.NoError(t, err) { t.FailNow() @@ -95,9 +100,9 @@ func TestRowVisibility(t *testing.T) { if !assert.NoError(t, err) { t.FailNow() } - - assert.EqualError(t, xlsx.SetRowVisible("Sheet3", 2, false), "Sheet Sheet3 is not exist") - assert.EqualError(t, xlsx.SetRowVisible("Sheet3", 2, true), "Sheet Sheet3 is not exist") + xlsx.NewSheet("Sheet3") + assert.NoError(t, xlsx.SetRowVisible("Sheet3", 2, false)) + assert.NoError(t, xlsx.SetRowVisible("Sheet3", 2, true)) xlsx.GetRowVisible("Sheet3", 2) xlsx.GetRowVisible("Sheet3", 25) assert.EqualError(t, xlsx.SetRowVisible("Sheet3", 0, true), "invalid row number 0") @@ -213,7 +218,7 @@ func TestInsertRowInEmptyFile(t *testing.T) { assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestInsertRowInEmptyFile.xlsx"))) } -func TestDuplicateRow(t *testing.T) { +func TestDuplicateRowFromSingleRow(t *testing.T) { const sheet = "Sheet1" outFile := filepath.Join("test", "TestDuplicateRow.%s.xlsx") @@ -226,15 +231,6 @@ func TestDuplicateRow(t *testing.T) { "B3": "B3 Value", } - newFileWithDefaults := func() *File { - f := NewFile() - for cell, val := range cells { - f.SetCellStr(sheet, cell, val) - - } - return f - } - t.Run("FromSingleRow", func(t *testing.T) { xlsx := NewFile() xlsx.SetCellStr(sheet, "A1", cells["A1"]) @@ -273,6 +269,20 @@ func TestDuplicateRow(t *testing.T) { } } }) +} + +func TestDuplicateRowUpdateDuplicatedRows(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", + } t.Run("UpdateDuplicatedRows", func(t *testing.T) { xlsx := NewFile() @@ -299,6 +309,29 @@ func TestDuplicateRow(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", + "A3": "A3 Value", + "B1": "B1 Value", + "B2": "B2 Value", + "B3": "B3 Value", + } + + newFileWithDefaults := func() *File { + f := NewFile() + for cell, val := range cells { + f.SetCellStr(sheet, cell, val) + + } + return f + } t.Run("FirstOfMultipleRows", func(t *testing.T) { xlsx := newFileWithDefaults() @@ -322,6 +355,11 @@ func TestDuplicateRow(t *testing.T) { } } }) +} + +func TestDuplicateRowZeroWithNoRows(t *testing.T) { + const sheet = "Sheet1" + outFile := filepath.Join("test", "TestDuplicateRow.%s.xlsx") t.Run("ZeroWithNoRows", func(t *testing.T) { xlsx := NewFile() @@ -359,6 +397,11 @@ func TestDuplicateRow(t *testing.T) { } } }) +} + +func TestDuplicateRowMiddleRowOfEmptyFile(t *testing.T) { + const sheet = "Sheet1" + outFile := filepath.Join("test", "TestDuplicateRow.%s.xlsx") t.Run("MiddleRowOfEmptyFile", func(t *testing.T) { xlsx := NewFile() @@ -381,6 +424,29 @@ func TestDuplicateRow(t *testing.T) { } } }) +} + +func TestDuplicateRowWithLargeOffsetToMiddleOfData(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 { + f.SetCellStr(sheet, cell, val) + + } + return f + } t.Run("WithLargeOffsetToMiddleOfData", func(t *testing.T) { xlsx := newFileWithDefaults() @@ -404,6 +470,29 @@ func TestDuplicateRow(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", + "A3": "A3 Value", + "B1": "B1 Value", + "B2": "B2 Value", + "B3": "B3 Value", + } + + newFileWithDefaults := func() *File { + f := NewFile() + for cell, val := range cells { + f.SetCellStr(sheet, cell, val) + + } + return f + } t.Run("WithLargeOffsetToEmptyRows", func(t *testing.T) { xlsx := newFileWithDefaults() @@ -427,6 +516,29 @@ func TestDuplicateRow(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", + "A3": "A3 Value", + "B1": "B1 Value", + "B2": "B2 Value", + "B3": "B3 Value", + } + + newFileWithDefaults := func() *File { + f := NewFile() + for cell, val := range cells { + f.SetCellStr(sheet, cell, val) + + } + return f + } t.Run("InsertBefore", func(t *testing.T) { xlsx := newFileWithDefaults() @@ -451,6 +563,29 @@ func TestDuplicateRow(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", + "A3": "A3 Value", + "B1": "B1 Value", + "B2": "B2 Value", + "B3": "B3 Value", + } + + newFileWithDefaults := func() *File { + f := NewFile() + for cell, val := range cells { + f.SetCellStr(sheet, cell, val) + + } + return f + } t.Run("InsertBeforeWithLargeOffset", func(t *testing.T) { xlsx := newFileWithDefaults() diff --git a/sheet.go b/sheet.go index f1f89bab18..72d8921367 100644 --- a/sheet.go +++ b/sheet.go @@ -684,15 +684,16 @@ func (f *File) GetSheetVisible(name string) bool { // result, err := xlsx.SearchSheet("Sheet1", "[0-9]", true) // func (f *File) SearchSheet(sheet, value string, reg ...bool) ([]string, error) { - var regSearch bool + var ( + regSearch bool + result []string + inElement string + r xlsxRow + ) for _, r := range reg { regSearch = r } - var ( - result []string - ) - xlsx, err := f.workSheetReader(sheet) if err != nil { return result, err @@ -708,8 +709,7 @@ func (f *File) SearchSheet(sheet, value string, reg ...bool) ([]string, error) { } xml.NewDecoder(bytes.NewReader(f.readXML(name))) d := f.sharedStringsReader() - var inElement string - var r xlsxRow + decoder := xml.NewDecoder(bytes.NewReader(f.readXML(name))) for { token, _ := decoder.Token() diff --git a/sheet_test.go b/sheet_test.go index 1307dc5aec..7db982ae31 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -11,7 +11,6 @@ import ( func ExampleFile_SetPageLayout() { xl := excelize.NewFile() - const sheet = "Sheet1" if err := xl.SetPageLayout( "Sheet1", @@ -30,7 +29,6 @@ func ExampleFile_SetPageLayout() { func ExampleFile_GetPageLayout() { xl := excelize.NewFile() - const sheet = "Sheet1" var ( orientation excelize.PageLayoutOrientation paperSize excelize.PageLayoutPaperSize @@ -73,38 +71,24 @@ func TestPageLayoutOption(t *testing.T) { xl := excelize.NewFile() // Get the default value - if !assert.NoError(t, xl.GetPageLayout(sheet, def), opt) { - t.FailNow() - } + assert.NoError(t, xl.GetPageLayout(sheet, def), opt) // Get again and check - if !assert.NoError(t, xl.GetPageLayout(sheet, val1), opt) { - t.FailNow() - } + assert.NoError(t, xl.GetPageLayout(sheet, val1), opt) if !assert.Equal(t, val1, def, opt) { t.FailNow() } // Set the same value - if !assert.NoError(t, xl.SetPageLayout(sheet, val1), opt) { - t.FailNow() - } + assert.NoError(t, xl.SetPageLayout(sheet, val1), opt) // Get again and check - if !assert.NoError(t, xl.GetPageLayout(sheet, val1), opt) { - t.FailNow() - } + assert.NoError(t, xl.GetPageLayout(sheet, val1), opt) if !assert.Equal(t, val1, def, "%T: value should not have changed", opt) { t.FailNow() } // Set a different value - if !assert.NoError(t, xl.SetPageLayout(sheet, test.nonDefault), opt) { - t.FailNow() - } - if !assert.NoError(t, xl.GetPageLayout(sheet, val1), opt) { - t.FailNow() - } + assert.NoError(t, xl.SetPageLayout(sheet, test.nonDefault), opt) + assert.NoError(t, xl.GetPageLayout(sheet, val1), opt) // Get again and compare - if !assert.NoError(t, xl.GetPageLayout(sheet, val2), opt) { - t.FailNow() - } + assert.NoError(t, xl.GetPageLayout(sheet, val2), opt) if !assert.Equal(t, val1, val2, "%T: value should not have changed", opt) { t.FailNow() } @@ -113,15 +97,23 @@ func TestPageLayoutOption(t *testing.T) { t.FailNow() } // Restore the default value - if !assert.NoError(t, xl.SetPageLayout(sheet, def), opt) { - t.FailNow() - } - if !assert.NoError(t, xl.GetPageLayout(sheet, val1), opt) { - t.FailNow() - } + assert.NoError(t, xl.SetPageLayout(sheet, def), opt) + assert.NoError(t, xl.GetPageLayout(sheet, val1), opt) if !assert.Equal(t, def, val1) { t.FailNow() } }) } } + +func TestSetPageLayout(t *testing.T) { + f := excelize.NewFile() + // Test set page layout on not exists worksheet. + assert.EqualError(t, f.SetPageLayout("SheetN"), "sheet SheetN is not exist") +} + +func TestGetPageLayout(t *testing.T) { + f := excelize.NewFile() + // Test get page layout on not exists worksheet. + assert.EqualError(t, f.GetPageLayout("SheetN"), "sheet SheetN is not exist") +} diff --git a/sheetpr_test.go b/sheetpr_test.go index 22dbd42437..48d330e498 100644 --- a/sheetpr_test.go +++ b/sheetpr_test.go @@ -112,38 +112,24 @@ func TestSheetPrOptions(t *testing.T) { xl := excelize.NewFile() // Get the default value - if !assert.NoError(t, xl.GetSheetPrOptions(sheet, def), opt) { - t.FailNow() - } + assert.NoError(t, xl.GetSheetPrOptions(sheet, def), opt) // Get again and check - if !assert.NoError(t, xl.GetSheetPrOptions(sheet, val1), opt) { - t.FailNow() - } + assert.NoError(t, xl.GetSheetPrOptions(sheet, val1), opt) if !assert.Equal(t, val1, def, opt) { t.FailNow() } // Set the same value - if !assert.NoError(t, xl.SetSheetPrOptions(sheet, val1), opt) { - t.FailNow() - } + assert.NoError(t, xl.SetSheetPrOptions(sheet, val1), opt) // Get again and check - if !assert.NoError(t, xl.GetSheetPrOptions(sheet, val1), opt) { - t.FailNow() - } + assert.NoError(t, xl.GetSheetPrOptions(sheet, val1), opt) if !assert.Equal(t, val1, def, "%T: value should not have changed", opt) { t.FailNow() } // Set a different value - if !assert.NoError(t, xl.SetSheetPrOptions(sheet, test.nonDefault), opt) { - t.FailNow() - } - if !assert.NoError(t, xl.GetSheetPrOptions(sheet, val1), opt) { - t.FailNow() - } + assert.NoError(t, xl.SetSheetPrOptions(sheet, test.nonDefault), opt) + assert.NoError(t, xl.GetSheetPrOptions(sheet, val1), opt) // Get again and compare - if !assert.NoError(t, xl.GetSheetPrOptions(sheet, val2), opt) { - t.FailNow() - } + assert.NoError(t, xl.GetSheetPrOptions(sheet, val2), opt) if !assert.Equal(t, val1, val2, "%T: value should not have changed", opt) { t.FailNow() } @@ -152,12 +138,8 @@ func TestSheetPrOptions(t *testing.T) { t.FailNow() } // Restore the default value - if !assert.NoError(t, xl.SetSheetPrOptions(sheet, def), opt) { - t.FailNow() - } - if !assert.NoError(t, xl.GetSheetPrOptions(sheet, val1), opt) { - t.FailNow() - } + assert.NoError(t, xl.SetSheetPrOptions(sheet, def), opt) + assert.NoError(t, xl.GetSheetPrOptions(sheet, val1), opt) if !assert.Equal(t, def, val1) { t.FailNow() } diff --git a/styles_test.go b/styles_test.go index b6f6042ef7..54321bb8e0 100644 --- a/styles_test.go +++ b/styles_test.go @@ -23,8 +23,6 @@ func TestStyleFill(t *testing.T) { for _, testCase := range cases { xl := NewFile() - const sheet = "Sheet1" - styleID, err := xl.NewStyle(testCase.format) if err != nil { t.Fatalf("%v", err) From 0f9170a03b9fe19c1c22687fba8bcbdfd69a6347 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 16 Apr 2019 01:50:16 -0500 Subject: [PATCH 083/957] Resolve #382, rewrite prepareSheetXML to scale linearly (#383) * Rewrite prepareSheetXML to scale linearly We don't need to backfill columns into every row for most purposes Provided makeContiguousColumns for setting styles where we do need it for a specific region. Added a benchmark to monitor progress. For 50,000 rows this went from about 11 seconds to 1 second. The improvements are more dramatic as the row/column count increases. * Assigning that row value was redundant --- cell_test.go | 12 ++++++++++++ rows.go | 2 +- sheet.go | 29 +++++++++++++++++++---------- styles.go | 1 + 4 files changed, 33 insertions(+), 11 deletions(-) diff --git a/cell_test.go b/cell_test.go index 7b1381f042..d4a5b02ea0 100644 --- a/cell_test.go +++ b/cell_test.go @@ -82,3 +82,15 @@ func ExampleFile_SetCellFloat() { fmt.Println(val) // Output: 3.14 } + +func BenchmarkSetCellValue(b *testing.B) { + values := []string{"First", "Second", "Third", "Fourth", "Fifth", "Sixth"} + cols := []string{"A", "B", "C", "D", "E", "F"} + f := NewFile() + b.ResetTimer() + for i := 0; i < b.N; i++ { + for j := 0; j < len(values); j++ { + f.SetCellValue("Sheet1", fmt.Sprint(cols[j], i), values[j]) + } + } +} diff --git a/rows.go b/rows.go index 7de18d305b..ff268cfa6f 100644 --- a/rows.go +++ b/rows.go @@ -446,7 +446,7 @@ func (f *File) InsertRow(sheet string, row int) error { return f.adjustHelper(sheet, rows, row, 1) } -// DuplicateRow inserts a copy of specified row (by it Excel row number) below +// DuplicateRow inserts a copy of specified row (by its Excel row number) below // // err := xlsx.DuplicateRow("Sheet1", 2) // diff --git a/sheet.go b/sheet.go index 72d8921367..5c681d2243 100644 --- a/sheet.go +++ b/sheet.go @@ -1072,8 +1072,8 @@ func (f *File) workSheetRelsWriter() { } } -// fillSheetData fill missing row and cell XML data to made it continuous from -// first cell [1, 1] to last cell [col, row] +// fillSheetData ensures there are enough rows, and columns in the chosen +// row to accept data. Missing rows are backfilled and given their row number func prepareSheetXML(xlsx *xlsxWorksheet, col int, row int) { rowCount := len(xlsx.SheetData.Row) if rowCount < row { @@ -1082,14 +1082,23 @@ func prepareSheetXML(xlsx *xlsxWorksheet, col int, row int) { xlsx.SheetData.Row = append(xlsx.SheetData.Row, xlsxRow{R: rowIdx + 1}) } } - for rowIdx := range xlsx.SheetData.Row { - rowData := &xlsx.SheetData.Row[rowIdx] // take reference - cellCount := len(rowData.C) - if cellCount < col { - for colIdx := cellCount; colIdx < col; colIdx++ { - cellName, _ := CoordinatesToCellName(colIdx+1, rowIdx+1) - rowData.C = append(rowData.C, xlsxC{R: cellName}) - } + rowData := &xlsx.SheetData.Row[row-1] + fillColumns(rowData, col, row) +} + +func fillColumns(rowData *xlsxRow, col, row int) { + cellCount := len(rowData.C) + if cellCount < col { + for colIdx := cellCount; colIdx < col; colIdx++ { + cellName, _ := CoordinatesToCellName(colIdx+1, row) + rowData.C = append(rowData.C, xlsxC{R: cellName}) } } } + +func makeContiguousColumns(xlsx *xlsxWorksheet, fromRow, toRow, colCount int) { + for ; fromRow < toRow; fromRow++ { + rowData := &xlsx.SheetData.Row[fromRow-1] + fillColumns(rowData, colCount, fromRow) + } +} diff --git a/styles.go b/styles.go index a515756ef9..81d03becb0 100644 --- a/styles.go +++ b/styles.go @@ -2373,6 +2373,7 @@ func (f *File) SetCellStyle(sheet, hcell, vcell string, styleID int) error { return err } prepareSheetXML(xlsx, vcol, vrow) + makeContiguousColumns(xlsx, hrow, vrow, vcol) for r := hrowIdx; r <= vrowIdx; r++ { for k := hcolIdx; k <= vcolIdx; k++ { From 0660f30cddc06de7883d40eb4f8e4945c18a0252 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 20 Apr 2019 14:57:50 +0800 Subject: [PATCH 084/957] godoc update and typo fixed --- README.md | 42 +++++++++++++++------------ README_zh.md | 42 +++++++++++++++------------ cell.go | 14 ++++----- col.go | 16 +++++----- datavalidation.go | 8 ++--- excelize.go | 3 +- lib.go | 16 +++++----- picture.go | 25 ++++++++-------- rows.go | 24 +++++++-------- shape.go | 2 +- sheet.go | 26 ++++++++--------- styles.go | 74 +++++++++++++++++++++++------------------------ table.go | 8 ++--- 13 files changed, 155 insertions(+), 145 deletions(-) diff --git a/README.md b/README.md index 7774a508f9..472ed6049c 100644 --- a/README.md +++ b/README.md @@ -38,16 +38,16 @@ import ( ) func main() { - xlsx := excelize.NewFile() + f := excelize.NewFile() // Create a new sheet. - index := xlsx.NewSheet("Sheet2") + index := f.NewSheet("Sheet2") // Set value of a cell. - xlsx.SetCellValue("Sheet2", "A2", "Hello world.") - xlsx.SetCellValue("Sheet1", "B2", 100) + f.SetCellValue("Sheet2", "A2", "Hello world.") + f.SetCellValue("Sheet1", "B2", 100) // Set active sheet of the workbook. - xlsx.SetActiveSheet(index) + f.SetActiveSheet(index) // Save xlsx file by the given path. - err := xlsx.SaveAs("./Book1.xlsx") + err := f.SaveAs("./Book1.xlsx") if err != nil { fmt.Println(err) } @@ -68,16 +68,16 @@ import ( ) func main() { - xlsx, err := excelize.OpenFile("./Book1.xlsx") + f, err := excelize.OpenFile("./Book1.xlsx") if err != nil { fmt.Println(err) return } // Get value from cell by given worksheet name and axis. - cell := xlsx.GetCellValue("Sheet1", "B2") + cell := f.GetCellValue("Sheet1", "B2") fmt.Println(cell) // Get all the rows in the Sheet1. - rows := xlsx.GetRows("Sheet1") + rows, err := f.GetRows("Sheet1") for _, row := range rows { for _, colCell := range row { fmt.Print(colCell, "\t") @@ -105,16 +105,20 @@ 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} - xlsx := excelize.NewFile() + f := excelize.NewFile() for k, v := range categories { - xlsx.SetCellValue("Sheet1", k, v) + f.SetCellValue("Sheet1", k, v) } for k, v := range values { - xlsx.SetCellValue("Sheet1", k, v) + f.SetCellValue("Sheet1", k, v) + } + 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"}}`) + if err != nil { + fmt.Println(err) + return } - xlsx.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"}}`) // Save xlsx file by the given path. - err := xlsx.SaveAs("./Book1.xlsx") + err = f.SaveAs("./Book1.xlsx") if err != nil { fmt.Println(err) } @@ -136,28 +140,28 @@ import ( ) func main() { - xlsx, err := excelize.OpenFile("./Book1.xlsx") + f, err := excelize.OpenFile("./Book1.xlsx") if err != nil { fmt.Println(err) return } // Insert a picture. - err = xlsx.AddPicture("Sheet1", "A2", "./image1.png", "") + err = f.AddPicture("Sheet1", "A2", "./image1.png", "") if err != nil { fmt.Println(err) } // Insert a picture to worksheet with scaling. - err = xlsx.AddPicture("Sheet1", "D2", "./image2.jpg", `{"x_scale": 0.5, "y_scale": 0.5}`) + err = f.AddPicture("Sheet1", "D2", "./image2.jpg", `{"x_scale": 0.5, "y_scale": 0.5}`) if err != nil { fmt.Println(err) } // Insert a picture offset in the cell with printing support. - err = xlsx.AddPicture("Sheet1", "H2", "./image3.gif", `{"x_offset": 15, "y_offset": 10, "print_obj": true, "lock_aspect_ratio": false, "locked": false}`) + err = f.AddPicture("Sheet1", "H2", "./image3.gif", `{"x_offset": 15, "y_offset": 10, "print_obj": true, "lock_aspect_ratio": false, "locked": false}`) if err != nil { fmt.Println(err) } // Save the xlsx file with the origin path. - err = xlsx.Save() + err = f.Save() if err != nil { fmt.Println(err) } diff --git a/README_zh.md b/README_zh.md index f8ace2845e..addc9f2efb 100644 --- a/README_zh.md +++ b/README_zh.md @@ -37,16 +37,16 @@ import ( ) func main() { - xlsx := excelize.NewFile() + f := excelize.NewFile() // 创建一个工作表 - index := xlsx.NewSheet("Sheet2") + index := f.NewSheet("Sheet2") // 设置单元格的值 - xlsx.SetCellValue("Sheet2", "A2", "Hello world.") - xlsx.SetCellValue("Sheet1", "B2", 100) + f.SetCellValue("Sheet2", "A2", "Hello world.") + f.SetCellValue("Sheet1", "B2", 100) // 设置工作簿的默认工作表 - xlsx.SetActiveSheet(index) + f.SetActiveSheet(index) // 根据指定路径保存文件 - err := xlsx.SaveAs("./Book1.xlsx") + err := f.SaveAs("./Book1.xlsx") if err != nil { fmt.Println(err) } @@ -67,16 +67,16 @@ import ( ) func main() { - xlsx, err := excelize.OpenFile("./Book1.xlsx") + f, err := excelize.OpenFile("./Book1.xlsx") if err != nil { fmt.Println(err) return } // 获取工作表中指定单元格的值 - cell := xlsx.GetCellValue("Sheet1", "B2") + cell := f.GetCellValue("Sheet1", "B2") fmt.Println(cell) // 获取 Sheet1 上所有单元格 - rows := xlsx.GetRows("Sheet1") + rows, err := f.GetRows("Sheet1") for _, row := range rows { for _, colCell := range row { fmt.Print(colCell, "\t") @@ -104,16 +104,20 @@ 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} - xlsx := excelize.NewFile() + f := excelize.NewFile() for k, v := range categories { - xlsx.SetCellValue("Sheet1", k, v) + f.SetCellValue("Sheet1", k, v) } for k, v := range values { - xlsx.SetCellValue("Sheet1", k, v) + f.SetCellValue("Sheet1", k, v) + } + 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"}}`) + if err != nil { + fmt.Println(err) + return } - xlsx.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"}}`) // 根据指定路径保存文件 - err := xlsx.SaveAs("./Book1.xlsx") + err = f.SaveAs("./Book1.xlsx") if err != nil { fmt.Println(err) } @@ -136,28 +140,28 @@ import ( ) func main() { - xlsx, err := excelize.OpenFile("./Book1.xlsx") + f, err := excelize.OpenFile("./Book1.xlsx") if err != nil { fmt.Println(err) return } // 插入图片 - err = xlsx.AddPicture("Sheet1", "A2", "./image1.png", "") + err = f.AddPicture("Sheet1", "A2", "./image1.png", "") if err != nil { fmt.Println(err) } // 在工作表中插入图片,并设置图片的缩放比例 - err = xlsx.AddPicture("Sheet1", "D2", "./image2.jpg", `{"x_scale": 0.5, "y_scale": 0.5}`) + err = f.AddPicture("Sheet1", "D2", "./image2.jpg", `{"x_scale": 0.5, "y_scale": 0.5}`) if err != nil { fmt.Println(err) } // 在工作表中插入图片,并设置图片的打印属性 - err = xlsx.AddPicture("Sheet1", "H2", "./image3.gif", `{"x_offset": 15, "y_offset": 10, "print_obj": true, "lock_aspect_ratio": false, "locked": false}`) + err = f.AddPicture("Sheet1", "H2", "./image3.gif", `{"x_offset": 15, "y_offset": 10, "print_obj": true, "lock_aspect_ratio": false, "locked": false}`) if err != nil { fmt.Println(err) } // 保存文件 - err = xlsx.Save() + err = f.Save() if err != nil { fmt.Println(err) } diff --git a/cell.go b/cell.go index 4d22842e63..484fb59b2f 100644 --- a/cell.go +++ b/cell.go @@ -304,7 +304,7 @@ func (f *File) SetCellFormula(sheet, axis, formula string) error { // 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: // -// link, target, err := xlsx.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 @@ -338,14 +338,14 @@ func (f *File) GetCellHyperLink(sheet, axis string) (bool, string, error) { // hyperlink "External" for web site or "Location" for moving to one of cell // in this workbook. The below is example for external link. // -// err := xlsx.SetCellHyperLink("Sheet1", "A3", "https://github.com/360EntSecGroup-Skylar/excelize", "External") +// err := f.SetCellHyperLink("Sheet1", "A3", "https://github.com/360EntSecGroup-Skylar/excelize", "External") // // Set underline and font color style for the cell. -// style, err := xlsx.NewStyle(`{"font":{"color":"#1265BE","underline":"single"}}`) -// err = xlsx.SetCellStyle("Sheet1", "A3", "A3", style) +// style, err := f.NewStyle(`{"font":{"color":"#1265BE","underline":"single"}}`) +// err = f.SetCellStyle("Sheet1", "A3", "A3", style) // // A this is another example for "Location": // -// err := xlsx.SetCellHyperLink("Sheet1", "A3", "Sheet1!A40", "Location") +// err := f.SetCellHyperLink("Sheet1", "A3", "Sheet1!A40", "Location") // func (f *File) SetCellHyperLink(sheet, axis, link, linkType string) error { // Check for correct cell name @@ -390,7 +390,7 @@ func (f *File) SetCellHyperLink(sheet, axis, link, linkType string) error { // MergeCell provides a function to merge cells by given coordinate area and // sheet name. For example create a merged cell of D3:E9 on Sheet1: // -// err := xlsx.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. @@ -452,7 +452,7 @@ func (f *File) MergeCell(sheet, hcell, vcell string) 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 := xlsx.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) diff --git a/col.go b/col.go index ad63b8ca36..6b73fdc22a 100644 --- a/col.go +++ b/col.go @@ -22,7 +22,7 @@ const ( // worksheet name and column name. For example, get visible state of column D // in Sheet1: // -// visiable, err := xlsx.GetColVisible("Sheet1", "D") +// visiable, err := f.GetColVisible("Sheet1", "D") // func (f *File) GetColVisible(sheet, col string) (bool, error) { visible := true @@ -51,7 +51,7 @@ func (f *File) GetColVisible(sheet, col string) (bool, error) { // SetColVisible provides a function to set visible of a single column by given // worksheet name and column name. For example, hide column D in Sheet1: // -// err := xlsx.SetColVisible("Sheet1", "D", false) +// err := f.SetColVisible("Sheet1", "D", false) // func (f *File) SetColVisible(sheet, col string, visible bool) error { colNum, err := ColumnNameToNumber(col) @@ -91,7 +91,7 @@ func (f *File) SetColVisible(sheet, col string, visible bool) error { // column by given worksheet name and column name. For example, get outline // level of column D in Sheet1: // -// level, err := xlsx.GetColOutlineLevel("Sheet1", "D") +// level, err := f.GetColOutlineLevel("Sheet1", "D") // func (f *File) GetColOutlineLevel(sheet, col string) (uint8, error) { level := uint8(0) @@ -119,7 +119,7 @@ func (f *File) GetColOutlineLevel(sheet, col string) (uint8, error) { // column by given worksheet name and column name. For example, set outline // level of column D in Sheet1 to 2: // -// err := xlsx.SetColOutlineLevel("Sheet1", "D", 2) +// err := f.SetColOutlineLevel("Sheet1", "D", 2) // func (f *File) SetColOutlineLevel(sheet, col string, level uint8) error { colNum, err := ColumnNameToNumber(col) @@ -158,8 +158,8 @@ func (f *File) SetColOutlineLevel(sheet, col string, level uint8) error { // SetColWidth provides a function to set the width of a single column or // multiple columns. For example: // -// xlsx := excelize.NewFile() -// err := xlsx.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) @@ -348,7 +348,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 := xlsx.InsertCol("Sheet1", "C") +// err := f.InsertCol("Sheet1", "C") // func (f *File) InsertCol(sheet, col string) error { num, err := ColumnNameToNumber(col) @@ -361,7 +361,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 := xlsx.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/datavalidation.go b/datavalidation.go index e5ea5a5448..8fb9623b26 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -142,7 +142,7 @@ func (dd *DataValidation) SetRange(f1, f2 int, t DataValidationType, o DataValid // dvRange := excelize.NewDataValidation(true) // dvRange.Sqref = "A7:B8" // dvRange.SetSqrefDropList("E1:E3", true) -// xlsx.AddDataValidation("Sheet1", dvRange) +// f.AddDataValidation("Sheet1", dvRange) // func (dd *DataValidation) SetSqrefDropList(sqref string, isCurrentSheet bool) error { if isCurrentSheet { @@ -208,7 +208,7 @@ func convDataValidationOperatior(o DataValidationOperator) string { // dvRange.Sqref = "A1:B2" // dvRange.SetRange(10, 20, excelize.DataValidationTypeWhole, excelize.DataValidationOperatorBetween) // dvRange.SetError(excelize.DataValidationErrorStyleStop, "error title", "error body") -// err := xlsx.AddDataValidation("Sheet1", dvRange) +// 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: @@ -217,7 +217,7 @@ func convDataValidationOperatior(o DataValidationOperator) string { // dvRange.Sqref = "A3:B4" // dvRange.SetRange(10, 20, excelize.DataValidationTypeWhole, excelize.DataValidationOperatorGreaterThan) // dvRange.SetInput("input title", "input body") -// err = xlsx.AddDataValidation("Sheet1", dvRange) +// 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: @@ -225,7 +225,7 @@ func convDataValidationOperatior(o DataValidationOperator) string { // dvRange = excelize.NewDataValidation(true) // dvRange.Sqref = "A5:B6" // dvRange.SetDropList([]string{"1", "2", "3"}) -// err = xlsx.AddDataValidation("Sheet1", dvRange) +// err = f.AddDataValidation("Sheet1", dvRange) // func (f *File) AddDataValidation(sheet string, dv *DataValidation) error { xlsx, err := f.workSheetReader(sheet) diff --git a/excelize.go b/excelize.go index 6e93f8d0e8..41fba37cb8 100644 --- a/excelize.go +++ b/excelize.go @@ -216,7 +216,8 @@ func (f *File) UpdateLinkedValue() error { return nil } -// GetMergeCells provides a function to get all merged cells from a worksheet currently. +// GetMergeCells provides a function to get all merged cells from a worksheet +// currently. func (f *File) GetMergeCells(sheet string) ([]MergeCell, error) { var mergeCells []MergeCell xlsx, err := f.workSheetReader(sheet) diff --git a/lib.go b/lib.go index ad4f79a47c..b99b175bb0 100644 --- a/lib.go +++ b/lib.go @@ -87,7 +87,7 @@ func SplitCellName(cell string) (string, int, error) { return "", -1, newInvalidCellNameError(cell) } -// JoinCellName joins cell name from column name and row number +// JoinCellName joins cell name from column name and row number. func JoinCellName(col string, row int) (string, error) { normCol := strings.Map(func(rune rune) rune { switch { @@ -107,9 +107,9 @@ func JoinCellName(col string, row int) (string, error) { return fmt.Sprintf("%s%d", normCol, row), nil } -// ColumnNameToNumber provides a function to convert Excel sheet -// column name to int. Column name case insencitive -// Function returns error if column name incorrect. +// 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. // // Example: // @@ -135,8 +135,8 @@ func ColumnNameToNumber(name string) (int, error) { return col, nil } -// ColumnNumberToName provides a function to convert integer -// to Excel sheet column title. +// ColumnNumberToName provides a function to convert the integer to Excel +// sheet column title. // // Example: // @@ -154,8 +154,8 @@ func ColumnNumberToName(num int) (string, error) { return col, nil } -// CellNameToCoordinates converts alpha-numeric cell name -// to [X, Y] coordinates or retrusn an error. +// CellNameToCoordinates converts alphanumeric cell name to [X, Y] coordinates +// or returns an error. // // Example: // diff --git a/picture.go b/picture.go index 274a4acd20..3cfcbf573d 100644 --- a/picture.go +++ b/picture.go @@ -55,23 +55,23 @@ func parseFormatPictureSet(formatSet string) (*formatPicture, error) { // ) // // func main() { -// xlsx := excelize.NewFile() +// f := excelize.NewFile() // // Insert a picture. -// err := xlsx.AddPicture("Sheet1", "A2", "./image1.jpg", "") +// err := f.AddPicture("Sheet1", "A2", "./image1.jpg", "") // if err != nil { // fmt.Println(err) // } // // Insert a picture scaling in the cell with location hyperlink. -// err = xlsx.AddPicture("Sheet1", "D2", "./image1.png", `{"x_scale": 0.5, "y_scale": 0.5, "hyperlink": "#Sheet2!D8", "hyperlink_type": "Location"}`) +// err = f.AddPicture("Sheet1", "D2", "./image1.png", `{"x_scale": 0.5, "y_scale": 0.5, "hyperlink": "#Sheet2!D8", "hyperlink_type": "Location"}`) // if err != nil { // fmt.Println(err) // } // // Insert a picture offset in the cell with external hyperlink, printing and positioning support. -// err = xlsx.AddPicture("Sheet1", "H2", "./image3.gif", `{"x_offset": 15, "y_offset": 10, "hyperlink": "https://github.com/360EntSecGroup-Skylar/excelize", "hyperlink_type": "External", "print_obj": true, "lock_aspect_ratio": false, "locked": false, "positioning": "oneCell"}`) +// err = f.AddPicture("Sheet1", "H2", "./image3.gif", `{"x_offset": 15, "y_offset": 10, "hyperlink": "https://github.com/360EntSecGroup-Skylar/excelize", "hyperlink_type": "External", "print_obj": true, "lock_aspect_ratio": false, "locked": false, "positioning": "oneCell"}`) // if err != nil { // fmt.Println(err) // } -// err = xlsx.SaveAs("./Book1.xlsx") +// err = f.SaveAs("./Book1.xlsx") // if err != nil { // fmt.Println(err) // } @@ -115,17 +115,17 @@ func (f *File) AddPicture(sheet, cell, picture, format string) error { // ) // // func main() { -// xlsx := excelize.NewFile() +// f := excelize.NewFile() // // file, err := ioutil.ReadFile("./image1.jpg") // if err != nil { // fmt.Println(err) // } -// err = xlsx.AddPictureFromBytes("Sheet1", "A2", "", "Excel Logo", ".jpg", file) +// err = f.AddPictureFromBytes("Sheet1", "A2", "", "Excel Logo", ".jpg", file) // if err != nil { // fmt.Println(err) // } -// err = xlsx.SaveAs("./Book1.xlsx") +// err = f.SaveAs("./Book1.xlsx") // if err != nil { // fmt.Println(err) // } @@ -481,16 +481,17 @@ func (f *File) getSheetRelationshipsTargetByID(sheet, rID string) string { // embed in XLSX by given worksheet and cell name. This function returns the // file name in XLSX and file contents as []byte data types. For example: // -// xlsx, err := excelize.OpenFile("./Book1.xlsx") +// f, err := excelize.OpenFile("./Book1.xlsx") // if err != nil { // fmt.Println(err) // return // } -// file, raw, err := xlsx.GetPicture("Sheet1", "A2") -// if file == "" { +// file, raw, err := f.GetPicture("Sheet1", "A2") +// if err != nil { +// fmt.Println(err) // return // } -// err := ioutil.WriteFile(file, raw, 0644) +// err = ioutil.WriteFile(file, raw, 0644) // if err != nil { // fmt.Println(err) // } diff --git a/rows.go b/rows.go index ff268cfa6f..b228fc2c55 100644 --- a/rows.go +++ b/rows.go @@ -21,7 +21,7 @@ import ( // GetRows return all the rows in a sheet by given worksheet name (case // sensitive). For example: // -// rows, err := xlsx.GetRows("Sheet1") +// rows, err := f.GetRows("Sheet1") // for _, row := range rows { // for _, colCell := range row { // fmt.Print(colCell, "\t") @@ -158,7 +158,7 @@ func (err ErrSheetNotExist) Error() string { // Rows return a rows iterator. For example: // -// rows, err := xlsx.Rows("Sheet1") +// rows, err := f.Rows("Sheet1") // for rows.Next() { // row, err := rows.Columns() // for _, colCell := range row { @@ -224,7 +224,7 @@ func (f *File) getTotalRowsCols(name string) (int, int, 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 := xlsx.SetRowHeight("Sheet1", 1, 50) +// err := f.SetRowHeight("Sheet1", 1, 50) // func (f *File) SetRowHeight(sheet string, row int, height float64) error { if row < 1 { @@ -260,7 +260,7 @@ func (f *File) getRowHeight(sheet string, row int) int { // GetRowHeight provides a function to get row height by given worksheet name // and row index. For example, get the height of the first row in Sheet1: // -// height, err := xlsx.GetRowHeight("Sheet1", 1) +// height, err := f.GetRowHeight("Sheet1", 1) // func (f *File) GetRowHeight(sheet string, row int) (float64, error) { if row < 1 { @@ -326,7 +326,7 @@ func (xlsx *xlsxC) getValueFrom(f *File, d *xlsxSST) (string, error) { // 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 := xlsx.SetRowVisible("Sheet1", 2, false) +// err := f.SetRowVisible("Sheet1", 2, false) // func (f *File) SetRowVisible(sheet string, row int, visible bool) error { if row < 1 { @@ -346,7 +346,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 := xlsx.GetRowVisible("Sheet1", 2) +// visible, err := f.GetRowVisible("Sheet1", 2) // func (f *File) GetRowVisible(sheet string, row int) (bool, error) { if row < 1 { @@ -367,7 +367,7 @@ func (f *File) GetRowVisible(sheet string, row int) (bool, error) { // single row by given worksheet name and Excel row number. For example, // outline row 2 in Sheet1 to level 1: // -// err := xlsx.SetRowOutlineLevel("Sheet1", 2, 1) +// err := f.SetRowOutlineLevel("Sheet1", 2, 1) // func (f *File) SetRowOutlineLevel(sheet string, row int, level uint8) error { if row < 1 { @@ -386,7 +386,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 := xlsx.GetRowOutlineLevel("Sheet1", 2) +// level, err := f.GetRowOutlineLevel("Sheet1", 2) // func (f *File) GetRowOutlineLevel(sheet string, row int) (uint8, error) { if row < 1 { @@ -405,7 +405,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 := xlsx.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 @@ -437,7 +437,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 := elsx.InsertRow("Sheet1", 3) +// err := f.InsertRow("Sheet1", 3) // func (f *File) InsertRow(sheet string, row int) error { if row < 1 { @@ -448,7 +448,7 @@ func (f *File) InsertRow(sheet string, row int) error { // DuplicateRow inserts a copy of specified row (by its Excel row number) below // -// err := xlsx.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 @@ -461,7 +461,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 := xlsx.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 diff --git a/shape.go b/shape.go index 5cac776e28..f404a7a352 100644 --- a/shape.go +++ b/shape.go @@ -40,7 +40,7 @@ func parseFormatShapeSet(formatSet string) (*formatShape, error) { // print settings) and properties set. For example, add text box (rect shape) // in Sheet1: // -// xlsx.AddShape("Sheet1", "G6", `{"type":"rect","color":{"line":"#4286F4","fill":"#8eb9ff"},"paragraph":[{"text":"Rectangle Shape","font":{"bold":true,"italic":true,"family":"Berlin Sans FB Demi","size":36,"color":"#777777","underline":"sng"}}],"width":180,"height": 90}`) +// f.AddShape("Sheet1", "G6", `{"type":"rect","color":{"line":"#4286F4","fill":"#8eb9ff"},"paragraph":[{"text":"Rectangle Shape","font":{"bold":true,"italic":true,"family":"Berlin Sans FB Demi","size":36,"color":"#777777","underline":"sng"}}],"width":180,"height": 90}`) // // The following shows the type of shape supported by excelize: // diff --git a/sheet.go b/sheet.go index 5c681d2243..32d12d1dcc 100644 --- a/sheet.go +++ b/sheet.go @@ -334,11 +334,11 @@ func (f *File) GetSheetIndex(name string) int { // GetSheetMap provides a function to get worksheet name and index map of XLSX. // For example: // -// xlsx, err := excelize.OpenFile("./Book1.xlsx") +// f, err := excelize.OpenFile("./Book1.xlsx") // if err != nil { // return // } -// for index, name := range xlsx.GetSheetMap() { +// for index, name := range f.GetSheetMap() { // fmt.Println(index, name) // } // @@ -443,8 +443,8 @@ func (f *File) deleteSheetFromContentTypes(target string) { // workbooks that contain tables, charts or pictures. For Example: // // // Sheet1 already exists... -// index := xlsx.NewSheet("Sheet2") -// err := xlsx.CopySheet(1, index) +// index := f.NewSheet("Sheet2") +// err := f.CopySheet(1, index) // return err // func (f *File) CopySheet(from, to int) error { @@ -490,7 +490,7 @@ func (f *File) copySheet(from, to int) error { // // For example, hide Sheet1: // -// err := xlsx.SetSheetVisible("Sheet1", false) +// err := f.SetSheetVisible("Sheet1", false) // func (f *File) SetSheetVisible(name string, visible bool) error { name = trimSheetName(name) @@ -601,21 +601,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: // -// xlsx.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: // -// xlsx.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: // -// xlsx.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: // -// xlsx.SetPanes("Sheet1", `{"freeze":false,"split":false}`) +// f.SetPanes("Sheet1", `{"freeze":false,"split":false}`) // func (f *File) SetPanes(sheet, panes string) error { fs, _ := parseFormatPanesSet(panes) @@ -653,7 +653,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: // -// xlsx.GetSheetVisible("Sheet1") +// f.GetSheetVisible("Sheet1") // func (f *File) GetSheetVisible(name string) bool { content := f.workbookReader() @@ -676,12 +676,12 @@ func (f *File) GetSheetVisible(name string) bool { // // An example of search the coordinates of the value of "100" on Sheet1: // -// result, err := xlsx.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 := xlsx.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 ( @@ -756,7 +756,7 @@ func (f *File) SearchSheet(sheet, value string, reg ...bool) ([]string, error) { // or deliberately changing, moving, or deleting data in a worksheet. For // example, protect Sheet1 with protection settings: // -// err := xlsx.ProtectSheet("Sheet1", &excelize.FormatSheetProtection{ +// err := f.ProtectSheet("Sheet1", &excelize.FormatSheetProtection{ // Password: "password", // EditScenarios: false, // }) diff --git a/styles.go b/styles.go index 81d03becb0..d6d267dd0f 100644 --- a/styles.go +++ b/styles.go @@ -1878,10 +1878,10 @@ func parseFormatStyleSet(style string) (*formatStyle, error) { // Excelize support set custom number format for cell. For example, set number // as date type in Uruguay (Spanish) format for Sheet1!A6: // -// xlsx := excelize.NewFile() -// xlsx.SetCellValue("Sheet1", "A6", 42920.5) -// style, err := xlsx.NewStyle(`{"custom_number_format": "[$-380A]dddd\\,\\ dd\" de \"mmmm\" de \"yyyy;@"}`) -// err = xlsx.SetCellStyle("Sheet1", "A6", "A6", style) +// f := excelize.NewFile() +// f.SetCellValue("Sheet1", "A6", 42920.5) +// style, err := f.NewStyle(`{"custom_number_format": "[$-380A]dddd\\,\\ dd\" de \"mmmm\" de \"yyyy;@"}`) +// err = f.SetCellStyle("Sheet1", "A6", "A6", style) // // Cell Sheet1!A6 in the Excel Application: martes, 04 de Julio de 2017 // @@ -2284,63 +2284,63 @@ func (f *File) GetCellStyle(sheet, axis string) (int, error) { // // For example create a borders of cell H9 on Sheet1: // -// style, err := xlsx.NewStyle(`{"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}]}`) +// style, err := f.NewStyle(`{"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 = xlsx.SetCellStyle("Sheet1", "H9", "H9", style) +// err = f.SetCellStyle("Sheet1", "H9", "H9", style) // // Set gradient fill with vertical variants shading styles for cell H9 on // Sheet1: // -// style, err := xlsx.NewStyle(`{"fill":{"type":"gradient","color":["#FFFFFF","#E0EBF5"],"shading":1}}`) +// style, err := f.NewStyle(`{"fill":{"type":"gradient","color":["#FFFFFF","#E0EBF5"],"shading":1}}`) // if err != nil { // fmt.Println(err) // } -// err = xlsx.SetCellStyle("Sheet1", "H9", "H9", style) +// err = f.SetCellStyle("Sheet1", "H9", "H9", style) // // Set solid style pattern fill for cell H9 on Sheet1: // -// style, err := xlsx.NewStyle(`{"fill":{"type":"pattern","color":["#E0EBF5"],"pattern":1}}`) +// style, err := f.NewStyle(`{"fill":{"type":"pattern","color":["#E0EBF5"],"pattern":1}}`) // if err != nil { // fmt.Println(err) // } -// err = xlsx.SetCellStyle("Sheet1", "H9", "H9", style) +// err = f.SetCellStyle("Sheet1", "H9", "H9", style) // // Set alignment style for cell H9 on Sheet1: // -// style, err := xlsx.NewStyle(`{"alignment":{"horizontal":"center","ident":1,"justify_last_line":true,"reading_order":0,"relative_indent":1,"shrink_to_fit":true,"text_rotation":45,"vertical":"","wrap_text":true}}`) +// 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":"","wrap_text":true}}`) // if err != nil { // fmt.Println(err) // } -// err = xlsx.SetCellStyle("Sheet1", "H9", "H9", style) +// 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: // -// xlsx.SetCellValue("Sheet1", "H9", 42920.5) -// style, err := xlsx.NewStyle(`{"number_format": 22}`) +// f.SetCellValue("Sheet1", "H9", 42920.5) +// style, err := f.NewStyle(`{"number_format": 22}`) // if err != nil { // fmt.Println(err) // } -// err = xlsx.SetCellStyle("Sheet1", "H9", "H9", style) +// err = f.SetCellStyle("Sheet1", "H9", "H9", style) // // Set font style for cell H9 on Sheet1: // -// style, err := xlsx.NewStyle(`{"font":{"bold":true,"italic":true,"family":"Berlin Sans FB Demi","size":36,"color":"#777777"}}`) +// style, err := f.NewStyle(`{"font":{"bold":true,"italic":true,"family":"Berlin Sans FB Demi","size":36,"color":"#777777"}}`) // if err != nil { // fmt.Println(err) // } -// err = xlsx.SetCellStyle("Sheet1", "H9", "H9", style) +// err = f.SetCellStyle("Sheet1", "H9", "H9", style) // // Hide and lock for cell H9 on Sheet1: // -// style, err := xlsx.NewStyle(`{"protection":{"hidden":true, "locked":true}}`) +// style, err := f.NewStyle(`{"protection":{"hidden":true, "locked":true}}`) // if err != nil { // fmt.Println(err) // } -// err = xlsx.SetCellStyle("Sheet1", "H9", "H9", style) +// err = f.SetCellStyle("Sheet1", "H9", "H9", style) // func (f *File) SetCellStyle(sheet, hcell, vcell string, styleID int) error { hcol, hrow, err := CellNameToCoordinates(hcell) @@ -2459,22 +2459,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: // -// xlsx.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: // -// xlsx.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 = xlsx.NewConditionalStyle(`{"font":{"color":"#9A0511"},"fill":{"type":"pattern","color":["#FEC7CE"],"pattern":1}}`) +// format, err = f.NewConditionalStyle(`{"font":{"color":"#9A0511"},"fill":{"type":"pattern","color":["#FEC7CE"],"pattern":1}}`) // if err != nil { // fmt.Println(err) // } -// xlsx.SetConditionalFormat("Sheet1", "A1:A10", fmt.Sprintf(`[{"type":"cell","criteria":">","format":%d,"value":"6"}]`, format)) +// 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 @@ -2486,19 +2486,19 @@ 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 = xlsx.NewConditionalStyle(`{"font":{"color":"#9A0511"},"fill":{"type":"pattern","color":["#FEC7CE"],"pattern":1}}`) +// format1, err = f.NewConditionalStyle(`{"font":{"color":"#9A0511"},"fill":{"type":"pattern","color":["#FEC7CE"],"pattern":1}}`) // // // Light yellow format for neutral conditional. -// format2, err = xlsx.NewConditionalStyle(`{"font":{"color":"#9B5713"},"fill":{"type":"pattern","color":["#FEEAA0"],"pattern":1}}`) +// format2, err = f.NewConditionalStyle(`{"font":{"color":"#9B5713"},"fill":{"type":"pattern","color":["#FEEAA0"],"pattern":1}}`) // // // Light green format for good conditional. -// format3, err = xlsx.NewConditionalStyle(`{"font":{"color":"#09600B"},"fill":{"type":"pattern","color":["#C7EECF"],"pattern":1}}`) +// 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... -// xlsx.SetConditionalFormat("Sheet1", "A1:A10", fmt.Sprintf(`[{"type":"cell","criteria":"between","format":%d,"minimum":"6","maximum":"8"}]`, format)) +// 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 @@ -2508,35 +2508,35 @@ func (f *File) SetCellStyle(sheet, hcell, vcell string, styleID int) error { // conditional format: // // // Top/Bottom rules: Above Average... -// xlsx.SetConditionalFormat("Sheet1", "A1:A10", fmt.Sprintf(`[{"type":"average","criteria":"=","format":%d, "above_average": true}]`, format1)) +// f.SetConditionalFormat("Sheet1", "A1:A10", fmt.Sprintf(`[{"type":"average","criteria":"=","format":%d, "above_average": true}]`, format1)) // // // Top/Bottom rules: Below Average... -// xlsx.SetConditionalFormat("Sheet1", "B1:B10", fmt.Sprintf(`[{"type":"average","criteria":"=","format":%d, "above_average": false}]`, format2)) +// 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... -// xlsx.SetConditionalFormat("Sheet1", "A1:A10", fmt.Sprintf(`[{"type":"duplicate","criteria":"=","format":%d}]`, format)) +// 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... -// xlsx.SetConditionalFormat("Sheet1", "A1:A10", fmt.Sprintf(`[{"type":"unique","criteria":"=","format":%d}]`, format)) +// 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. -// xlsx.SetConditionalFormat("Sheet1", "H1:H10", fmt.Sprintf(`[{"type":"top","criteria":"=","format":%d,"value":"6"}]`, format)) +// 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: // -// xlsx.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. -// xlsx.SetConditionalFormat("Sheet1", "A1:A10", `[{"type":"2_color_scale","criteria":"=","min_type":"min","max_type":"max","min_color":"#F8696B","max_color":"#63BE7B"}]`) +// 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. @@ -2545,7 +2545,7 @@ func (f *File) SetCellStyle(sheet, hcell, vcell string, styleID int) error { // Color Scale" style conditional format: // // // Color scales: 3 color. -// xlsx.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"}]`) +// 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 @@ -2557,7 +2557,7 @@ 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. -// xlsx.SetConditionalFormat("Sheet1", "K1:K10", `[{"type":"data_bar", "criteria":"=", "min_type":"min","max_type":"max","bar_color":"#638EC6"}]`) +// 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: // @@ -2586,7 +2586,7 @@ func (f *File) SetCellStyle(sheet, hcell, vcell string, styleID int) error { // follows: // // // Color scales: 3 color. -// xlsx.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"}]`) +// 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. // diff --git a/table.go b/table.go index 4505994be3..f3819d3fbf 100644 --- a/table.go +++ b/table.go @@ -33,11 +33,11 @@ func parseFormatTableSet(formatSet string) (*formatTable, error) { // name, coordinate area and format set. For example, create a table of A1:D5 // on Sheet1: // -// err := xlsx.AddTable("Sheet1", "A1", "D5", ``) +// err := f.AddTable("Sheet1", "A1", "D5", ``) // // Create a table of F2:H6 on Sheet2 with format set: // -// err := xlsx.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 at least two lines include string type header. Multiple // tables coordinate areas can't have an intersection. @@ -197,11 +197,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 = xlsx.AutoFilter("Sheet1", "A1", "D4", "") +// err := f.AutoFilter("Sheet1", "A1", "D4", "") // // Filter data in an autofilter: // -// err = xlsx.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 From b45c4b094c6b2a7bfbc9944fc04c51c45b3454a3 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 21 Apr 2019 00:04:42 +0800 Subject: [PATCH 085/957] Add a check for maximum limit hyperlinks in a worksheet typo fixed --- cell.go | 14 +- chart.go | 14 +- comment.go | 2 +- excelize_test.go | 894 ++++++++++++++++++++++++----------------------- shape.go | 2 +- 5 files changed, 472 insertions(+), 454 deletions(-) diff --git a/cell.go b/cell.go index 484fb59b2f..bd4d93be98 100644 --- a/cell.go +++ b/cell.go @@ -336,7 +336,8 @@ func (f *File) GetCellHyperLink(sheet, axis string) (bool, string, error) { // 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. The below is example for external link. +// in this workbook. Maximum limit hyperlinks in a worksheet is 65530. The +// below is example for external link. // // err := f.SetCellHyperLink("Sheet1", "A3", "https://github.com/360EntSecGroup-Skylar/excelize", "External") // // Set underline and font color style for the cell. @@ -364,6 +365,14 @@ func (f *File) SetCellHyperLink(sheet, axis, link, linkType string) error { var linkData xlsxHyperlink + if xlsx.Hyperlinks == nil { + xlsx.Hyperlinks = new(xlsxHyperlinks) + } + + if len(xlsx.Hyperlinks.Hyperlink) > 65529 { + return errors.New("over maximum limit hyperlinks in a worksheet") + } + switch linkType { case "External": linkData = xlsxHyperlink{ @@ -380,9 +389,6 @@ func (f *File) SetCellHyperLink(sheet, axis, link, linkType string) error { return fmt.Errorf("invalid link type %q", linkType) } - if xlsx.Hyperlinks == nil { - xlsx.Hyperlinks = new(xlsxHyperlinks) - } xlsx.Hyperlinks.Hyperlink = append(xlsx.Hyperlinks.Hyperlink, linkData) return nil } diff --git a/chart.go b/chart.go index c657be8bda..d669a4739e 100644 --- a/chart.go +++ b/chart.go @@ -314,16 +314,20 @@ func parseFormatChartSet(formatSet string) (*formatChart, 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} -// xlsx := excelize.NewFile() +// f := excelize.NewFile() // for k, v := range categories { -// xlsx.SetCellValue("Sheet1", k, v) +// f.SetCellValue("Sheet1", k, v) // } // for k, v := range values { -// xlsx.SetCellValue("Sheet1", k, v) +// f.SetCellValue("Sheet1", k, v) +// } +// err := f.AddChart("Sheet1", "E1", `{"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"}],"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":"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},"show_blanks_as":"zero","x_axis":{"reverse_order":true},"y_axis":{"maximum":7.5,"minimum":0.5}}`) +// if err != nil { +// fmt.Println(err) +// return // } -// xlsx.AddChart("Sheet1", "E1", `{"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"}],"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":"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},"show_blanks_as":"zero","x_axis":{"reverse_order":true},"y_axis":{"maximum":7.5,"minimum":0.5}}`) // // Save xlsx file by the given path. -// err := xlsx.SaveAs("./Book1.xlsx") +// err = xlsx.SaveAs("./Book1.xlsx") // if err != nil { // fmt.Println(err) // } diff --git a/comment.go b/comment.go index ed3d4a7433..79f6feccd8 100644 --- a/comment.go +++ b/comment.go @@ -72,7 +72,7 @@ func (f *File) getSheetComments(sheetID int) string { // author length is 255 and the max text length is 32512. For example, add a // comment in Sheet1!$A$30: // -// err := xlsx.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) diff --git a/excelize_test.go b/excelize_test.go index 619211050a..87fd806871 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -19,15 +19,15 @@ import ( func TestOpenFile(t *testing.T) { // Test update a XLSX file. - xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if !assert.NoError(t, err) { t.FailNow() } // Test get all the rows in a not exists worksheet. - xlsx.GetRows("Sheet4") + f.GetRows("Sheet4") // Test get all the rows in a worksheet. - rows, err := xlsx.GetRows("Sheet2") + rows, err := f.GetRows("Sheet2") assert.NoError(t, err) for _, row := range rows { for _, cell := range row { @@ -35,88 +35,88 @@ func TestOpenFile(t *testing.T) { } t.Log("\r\n") } - xlsx.UpdateLinkedValue() + f.UpdateLinkedValue() - xlsx.SetCellDefault("Sheet2", "A1", strconv.FormatFloat(float64(100.1588), 'f', -1, 32)) - xlsx.SetCellDefault("Sheet2", "A1", strconv.FormatFloat(float64(-100.1588), 'f', -1, 64)) + f.SetCellDefault("Sheet2", "A1", strconv.FormatFloat(float64(100.1588), 'f', -1, 32)) + f.SetCellDefault("Sheet2", "A1", strconv.FormatFloat(float64(-100.1588), 'f', -1, 64)) // Test set cell value with illegal row number. - assert.EqualError(t, xlsx.SetCellDefault("Sheet2", "A", strconv.FormatFloat(float64(-100.1588), 'f', -1, 64)), + assert.EqualError(t, f.SetCellDefault("Sheet2", "A", strconv.FormatFloat(float64(-100.1588), 'f', -1, 64)), `cannot convert cell "A" to coordinates: invalid cell name "A"`) - xlsx.SetCellInt("Sheet2", "A1", 100) + f.SetCellInt("Sheet2", "A1", 100) // Test set cell integer value with illegal row number. - assert.EqualError(t, xlsx.SetCellInt("Sheet2", "A", 100), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.EqualError(t, f.SetCellInt("Sheet2", "A", 100), `cannot convert cell "A" to coordinates: invalid cell name "A"`) - xlsx.SetCellStr("Sheet2", "C11", "Knowns") + f.SetCellStr("Sheet2", "C11", "Knowns") // Test max characters in a cell. - xlsx.SetCellStr("Sheet2", "D11", strings.Repeat("c", 32769)) - xlsx.NewSheet(":\\/?*[]Maximum 31 characters allowed in sheet title.") + f.SetCellStr("Sheet2", "D11", strings.Repeat("c", 32769)) + f.NewSheet(":\\/?*[]Maximum 31 characters allowed in sheet title.") // Test set worksheet name with illegal name. - xlsx.SetSheetName("Maximum 31 characters allowed i", "[Rename]:\\/?* Maximum 31 characters allowed in sheet title.") - xlsx.SetCellInt("Sheet3", "A23", 10) - xlsx.SetCellStr("Sheet3", "b230", "10") - assert.EqualError(t, xlsx.SetCellStr("Sheet10", "b230", "10"), "sheet Sheet10 is not exist") + f.SetSheetName("Maximum 31 characters allowed i", "[Rename]:\\/?* Maximum 31 characters allowed in sheet title.") + f.SetCellInt("Sheet3", "A23", 10) + f.SetCellStr("Sheet3", "b230", "10") + assert.EqualError(t, f.SetCellStr("Sheet10", "b230", "10"), "sheet Sheet10 is not exist") // Test set cell string value with illegal row number. - assert.EqualError(t, xlsx.SetCellStr("Sheet1", "A", "10"), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.EqualError(t, f.SetCellStr("Sheet1", "A", "10"), `cannot convert cell "A" to coordinates: invalid cell name "A"`) - xlsx.SetActiveSheet(2) + f.SetActiveSheet(2) // Test get cell formula with given rows number. - _, err = xlsx.GetCellFormula("Sheet1", "B19") + _, err = f.GetCellFormula("Sheet1", "B19") assert.NoError(t, err) // Test get cell formula with illegal worksheet name. - _, err = xlsx.GetCellFormula("Sheet2", "B20") + _, err = f.GetCellFormula("Sheet2", "B20") assert.NoError(t, err) - _, err = xlsx.GetCellFormula("Sheet1", "B20") + _, err = f.GetCellFormula("Sheet1", "B20") assert.NoError(t, err) // Test get cell formula with illegal rows number. - _, err = xlsx.GetCellFormula("Sheet1", "B") + _, err = f.GetCellFormula("Sheet1", "B") assert.EqualError(t, err, `cannot convert cell "B" to coordinates: invalid cell name "B"`) // Test get shared cell formula - xlsx.GetCellFormula("Sheet2", "H11") - xlsx.GetCellFormula("Sheet2", "I11") + f.GetCellFormula("Sheet2", "H11") + f.GetCellFormula("Sheet2", "I11") getSharedForumula(&xlsxWorksheet{}, "") // Test read cell value with given illegal rows number. - _, err = xlsx.GetCellValue("Sheet2", "a-1") + _, err = f.GetCellValue("Sheet2", "a-1") assert.EqualError(t, err, `cannot convert cell "A-1" to coordinates: invalid cell name "A-1"`) - _, err = xlsx.GetCellValue("Sheet2", "A") + _, err = f.GetCellValue("Sheet2", "A") assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`) // Test read cell value with given lowercase column number. - xlsx.GetCellValue("Sheet2", "a5") - xlsx.GetCellValue("Sheet2", "C11") - xlsx.GetCellValue("Sheet2", "D11") - xlsx.GetCellValue("Sheet2", "D12") + f.GetCellValue("Sheet2", "a5") + f.GetCellValue("Sheet2", "C11") + f.GetCellValue("Sheet2", "D11") + f.GetCellValue("Sheet2", "D12") // Test SetCellValue function. - assert.NoError(t, xlsx.SetCellValue("Sheet2", "F1", " Hello")) - assert.NoError(t, xlsx.SetCellValue("Sheet2", "G1", []byte("World"))) - assert.NoError(t, xlsx.SetCellValue("Sheet2", "F2", 42)) - assert.NoError(t, xlsx.SetCellValue("Sheet2", "F3", int8(1<<8/2-1))) - assert.NoError(t, xlsx.SetCellValue("Sheet2", "F4", int16(1<<16/2-1))) - assert.NoError(t, xlsx.SetCellValue("Sheet2", "F5", int32(1<<32/2-1))) - assert.NoError(t, xlsx.SetCellValue("Sheet2", "F6", int64(1<<32/2-1))) - assert.NoError(t, xlsx.SetCellValue("Sheet2", "F7", float32(42.65418))) - assert.NoError(t, xlsx.SetCellValue("Sheet2", "F8", float64(-42.65418))) - assert.NoError(t, xlsx.SetCellValue("Sheet2", "F9", float32(42))) - assert.NoError(t, xlsx.SetCellValue("Sheet2", "F10", float64(42))) - assert.NoError(t, xlsx.SetCellValue("Sheet2", "F11", uint(1<<32-1))) - assert.NoError(t, xlsx.SetCellValue("Sheet2", "F12", uint8(1<<8-1))) - assert.NoError(t, xlsx.SetCellValue("Sheet2", "F13", uint16(1<<16-1))) - assert.NoError(t, xlsx.SetCellValue("Sheet2", "F14", uint32(1<<32-1))) - assert.NoError(t, xlsx.SetCellValue("Sheet2", "F15", uint64(1<<32-1))) - assert.NoError(t, xlsx.SetCellValue("Sheet2", "F16", true)) - assert.NoError(t, xlsx.SetCellValue("Sheet2", "F17", complex64(5+10i))) + 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)) + assert.NoError(t, f.SetCellValue("Sheet2", "F3", int8(1<<8/2-1))) + assert.NoError(t, f.SetCellValue("Sheet2", "F4", int16(1<<16/2-1))) + 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", "F9", float32(42))) + assert.NoError(t, f.SetCellValue("Sheet2", "F10", float64(42))) + assert.NoError(t, f.SetCellValue("Sheet2", "F11", uint(1<<32-1))) + assert.NoError(t, f.SetCellValue("Sheet2", "F12", uint8(1<<8-1))) + assert.NoError(t, f.SetCellValue("Sheet2", "F13", uint16(1<<16-1))) + assert.NoError(t, f.SetCellValue("Sheet2", "F14", uint32(1<<32-1))) + assert.NoError(t, f.SetCellValue("Sheet2", "F15", uint64(1<<32-1))) + assert.NoError(t, f.SetCellValue("Sheet2", "F16", true)) + assert.NoError(t, f.SetCellValue("Sheet2", "F17", complex64(5+10i))) // Test on not exists worksheet. - assert.EqualError(t, xlsx.SetCellDefault("SheetN", "A1", ""), "sheet SheetN is not exist") - assert.EqualError(t, xlsx.SetCellFloat("SheetN", "A1", 42.65418, 2, 32), "sheet SheetN is not exist") - assert.EqualError(t, xlsx.SetCellBool("SheetN", "A1", true), "sheet SheetN is not exist") - assert.EqualError(t, xlsx.SetCellFormula("SheetN", "A1", ""), "sheet SheetN is not exist") - assert.EqualError(t, xlsx.SetCellHyperLink("SheetN", "A1", "Sheet1!A40", "Location"), "sheet SheetN is not exist") + 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") // Test boolean write booltest := []struct { @@ -127,55 +127,55 @@ func TestOpenFile(t *testing.T) { {true, "1"}, } for _, test := range booltest { - xlsx.SetCellValue("Sheet2", "F16", test.value) - val, err := xlsx.GetCellValue("Sheet2", "F16") + f.SetCellValue("Sheet2", "F16", test.value) + val, err := f.GetCellValue("Sheet2", "F16") assert.NoError(t, err) assert.Equal(t, test.expected, val) } - xlsx.SetCellValue("Sheet2", "G2", nil) + f.SetCellValue("Sheet2", "G2", nil) - assert.EqualError(t, xlsx.SetCellValue("Sheet2", "G4", time.Now()), "only UTC time expected") + assert.EqualError(t, f.SetCellValue("Sheet2", "G4", time.Now()), "only UTC time expected") - xlsx.SetCellValue("Sheet2", "G4", time.Now().UTC()) + f.SetCellValue("Sheet2", "G4", time.Now().UTC()) // 02:46:40 - xlsx.SetCellValue("Sheet2", "G5", time.Duration(1e13)) + f.SetCellValue("Sheet2", "G5", time.Duration(1e13)) // Test completion column. - xlsx.SetCellValue("Sheet2", "M2", nil) + f.SetCellValue("Sheet2", "M2", nil) // Test read cell value with given axis large than exists row. - xlsx.GetCellValue("Sheet2", "E231") + f.GetCellValue("Sheet2", "E231") // Test get active worksheet of XLSX and get worksheet name of XLSX by given worksheet index. - xlsx.GetSheetName(xlsx.GetActiveSheetIndex()) + f.GetSheetName(f.GetActiveSheetIndex()) // Test get worksheet index of XLSX by given worksheet name. - xlsx.GetSheetIndex("Sheet1") + f.GetSheetIndex("Sheet1") // Test get worksheet name of XLSX by given invalid worksheet index. - xlsx.GetSheetName(4) - // Test get worksheet map of XLSX. - xlsx.GetSheetMap() + f.GetSheetName(4) + // Test get worksheet map of f. + f.GetSheetMap() for i := 1; i <= 300; i++ { - xlsx.SetCellStr("Sheet3", "c"+strconv.Itoa(i), strconv.Itoa(i)) + f.SetCellStr("Sheet3", "c"+strconv.Itoa(i), strconv.Itoa(i)) } - assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestOpenFile.xlsx"))) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestOpenFile.xlsx"))) } func TestSaveFile(t *testing.T) { - xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if !assert.NoError(t, err) { t.FailNow() } - assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSaveFile.xlsx"))) - xlsx, err = OpenFile(filepath.Join("test", "TestSaveFile.xlsx")) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSaveFile.xlsx"))) + f, err = OpenFile(filepath.Join("test", "TestSaveFile.xlsx")) if !assert.NoError(t, err) { t.FailNow() } - assert.NoError(t, xlsx.Save()) + assert.NoError(t, f.Save()) } func TestSaveAsWrongPath(t *testing.T) { - xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if assert.NoError(t, err) { // Test write file to not exist directory. - err = xlsx.SaveAs("") + err = f.SaveAs("") if assert.Error(t, err) { assert.True(t, os.IsNotExist(err), "Error: %v: Expected os.IsNotExists(err) == true", err) } @@ -184,15 +184,15 @@ func TestSaveAsWrongPath(t *testing.T) { func TestBrokenFile(t *testing.T) { // Test write file with broken file struct. - xlsx := File{} + f := File{} t.Run("SaveWithoutName", func(t *testing.T) { - assert.EqualError(t, xlsx.Save(), "no path defined for file, consider File.WriteTo or File.Write") + assert.EqualError(t, f.Save(), "no path defined for file, consider File.WriteTo or File.Write") }) t.Run("SaveAsEmptyStruct", func(t *testing.T) { // Test write file with broken file struct with given path. - assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestBrokenFile.SaveAsEmptyStruct.xlsx"))) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestBrokenFile.SaveAsEmptyStruct.xlsx"))) }) t.Run("OpenBadWorkbook", func(t *testing.T) { @@ -214,34 +214,34 @@ func TestBrokenFile(t *testing.T) { func TestNewFile(t *testing.T) { // Test create a XLSX file. - xlsx := NewFile() - xlsx.NewSheet("Sheet1") - xlsx.NewSheet("XLSXSheet2") - xlsx.NewSheet("XLSXSheet3") - xlsx.SetCellInt("XLSXSheet2", "A23", 56) - xlsx.SetCellStr("Sheet1", "B20", "42") - xlsx.SetActiveSheet(0) + f := NewFile() + f.NewSheet("Sheet1") + f.NewSheet("XLSXSheet2") + f.NewSheet("XLSXSheet3") + f.SetCellInt("XLSXSheet2", "A23", 56) + f.SetCellStr("Sheet1", "B20", "42") + f.SetActiveSheet(0) // Test add picture to sheet with scaling and positioning. - err := xlsx.AddPicture("Sheet1", "H2", filepath.Join("test", "images", "excel.gif"), + 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 formatset. - err = xlsx.AddPicture("Sheet1", "C2", filepath.Join("test", "images", "excel.png"), "") + 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. - err = xlsx.AddPicture("Sheet1", "C2", filepath.Join("test", "images", "excel.png"), `{`) + err = f.AddPicture("Sheet1", "C2", filepath.Join("test", "images", "excel.png"), `{`) if !assert.Error(t, err) { t.FailNow() } - assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestNewFile.xlsx"))) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestNewFile.xlsx"))) } func TestColWidth(t *testing.T) { @@ -275,128 +275,136 @@ func TestAddDrawingVML(t *testing.T) { } func TestSetCellHyperLink(t *testing.T) { - xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if err != nil { t.Log(err) } // Test set cell hyperlink in a work sheet already have hyperlinks. - assert.NoError(t, xlsx.SetCellHyperLink("Sheet1", "B19", "https://github.com/360EntSecGroup-Skylar/excelize", "External")) + assert.NoError(t, f.SetCellHyperLink("Sheet1", "B19", "https://github.com/360EntSecGroup-Skylar/excelize", "External")) // Test add first hyperlink in a work sheet. - assert.NoError(t, xlsx.SetCellHyperLink("Sheet2", "C1", "https://github.com/360EntSecGroup-Skylar/excelize", "External")) + assert.NoError(t, f.SetCellHyperLink("Sheet2", "C1", "https://github.com/360EntSecGroup-Skylar/excelize", "External")) // Test add Location hyperlink in a work sheet. - assert.NoError(t, xlsx.SetCellHyperLink("Sheet2", "D6", "Sheet1!D8", "Location")) + assert.NoError(t, f.SetCellHyperLink("Sheet2", "D6", "Sheet1!D8", "Location")) - assert.EqualError(t, xlsx.SetCellHyperLink("Sheet2", "C3", "Sheet1!D8", ""), `invalid link type ""`) + assert.EqualError(t, f.SetCellHyperLink("Sheet2", "C3", "Sheet1!D8", ""), `invalid link type ""`) - assert.EqualError(t, xlsx.SetCellHyperLink("Sheet2", "", "Sheet1!D60", "Location"), `invalid cell name ""`) + assert.EqualError(t, f.SetCellHyperLink("Sheet2", "", "Sheet1!D60", "Location"), `invalid cell name ""`) - assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetCellHyperLink.xlsx"))) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellHyperLink.xlsx"))) + + file := NewFile() + for row := 1; row <= 65530; row++ { + cell, err := CoordinatesToCellName(1, row) + assert.NoError(t, err) + assert.NoError(t, file.SetCellHyperLink("Sheet1", cell, "https://github.com/360EntSecGroup-Skylar/excelize", "External")) + } + assert.EqualError(t, file.SetCellHyperLink("Sheet1", "A65531", "https://github.com/360EntSecGroup-Skylar/excelize", "External"), "over maximum limit hyperlinks in a worksheet") } func TestGetCellHyperLink(t *testing.T) { - xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if !assert.NoError(t, err) { t.FailNow() } - _, _, err = xlsx.GetCellHyperLink("Sheet1", "") + _, _, err = f.GetCellHyperLink("Sheet1", "") assert.EqualError(t, err, `invalid cell name ""`) - link, target, err := xlsx.GetCellHyperLink("Sheet1", "A22") + link, target, err := f.GetCellHyperLink("Sheet1", "A22") assert.NoError(t, err) t.Log(link, target) - link, target, err = xlsx.GetCellHyperLink("Sheet2", "D6") + link, target, err = f.GetCellHyperLink("Sheet2", "D6") assert.NoError(t, err) t.Log(link, target) - link, target, err = xlsx.GetCellHyperLink("Sheet3", "H3") + link, target, err = f.GetCellHyperLink("Sheet3", "H3") assert.EqualError(t, err, "sheet Sheet3 is not exist") t.Log(link, target) } func TestSetCellFormula(t *testing.T) { - xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if !assert.NoError(t, err) { t.FailNow() } - xlsx.SetCellFormula("Sheet1", "B19", "SUM(Sheet2!D2,Sheet2!D11)") - xlsx.SetCellFormula("Sheet1", "C19", "SUM(Sheet2!D2,Sheet2!D9)") + f.SetCellFormula("Sheet1", "B19", "SUM(Sheet2!D2,Sheet2!D11)") + f.SetCellFormula("Sheet1", "C19", "SUM(Sheet2!D2,Sheet2!D9)") // Test set cell formula with illegal rows number. - assert.EqualError(t, xlsx.SetCellFormula("Sheet1", "C", "SUM(Sheet2!D2,Sheet2!D9)"), `cannot convert cell "C" to coordinates: invalid cell name "C"`) + assert.EqualError(t, f.SetCellFormula("Sheet1", "C", "SUM(Sheet2!D2,Sheet2!D9)"), `cannot convert cell "C" to coordinates: invalid cell name "C"`) - assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetCellFormula1.xlsx"))) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellFormula1.xlsx"))) - xlsx, err = OpenFile(filepath.Join("test", "CalcChain.xlsx")) + f, err = OpenFile(filepath.Join("test", "CalcChain.xlsx")) if !assert.NoError(t, err) { t.FailNow() } // Test remove cell formula. - xlsx.SetCellFormula("Sheet1", "A1", "") - assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetCellFormula2.xlsx"))) + f.SetCellFormula("Sheet1", "A1", "") + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellFormula2.xlsx"))) // Test remove all cell formula. - xlsx.SetCellFormula("Sheet1", "B1", "") - assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetCellFormula3.xlsx"))) + f.SetCellFormula("Sheet1", "B1", "") + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellFormula3.xlsx"))) } func TestSetSheetBackground(t *testing.T) { - xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if !assert.NoError(t, err) { t.FailNow() } - err = xlsx.SetSheetBackground("Sheet2", filepath.Join("test", "images", "background.jpg")) + err = f.SetSheetBackground("Sheet2", filepath.Join("test", "images", "background.jpg")) if !assert.NoError(t, err) { t.FailNow() } - err = xlsx.SetSheetBackground("Sheet2", filepath.Join("test", "images", "background.jpg")) + err = f.SetSheetBackground("Sheet2", filepath.Join("test", "images", "background.jpg")) if !assert.NoError(t, err) { t.FailNow() } - assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetSheetBackground.xlsx"))) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetSheetBackground.xlsx"))) } func TestSetSheetBackgroundErrors(t *testing.T) { - xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if !assert.NoError(t, err) { t.FailNow() } - err = xlsx.SetSheetBackground("Sheet2", filepath.Join("test", "not_exists", "not_exists.png")) + err = f.SetSheetBackground("Sheet2", filepath.Join("test", "not_exists", "not_exists.png")) if assert.Error(t, err) { assert.True(t, os.IsNotExist(err), "Expected os.IsNotExists(err) == true") } - err = xlsx.SetSheetBackground("Sheet2", filepath.Join("test", "Book1.xlsx")) + err = f.SetSheetBackground("Sheet2", filepath.Join("test", "Book1.xlsx")) assert.EqualError(t, err, "unsupported image extension") } func TestMergeCell(t *testing.T) { - xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if !assert.NoError(t, err) { t.FailNow() } - xlsx.MergeCell("Sheet1", "D9", "D9") - xlsx.MergeCell("Sheet1", "D9", "E9") - xlsx.MergeCell("Sheet1", "H14", "G13") - xlsx.MergeCell("Sheet1", "C9", "D8") - xlsx.MergeCell("Sheet1", "F11", "G13") - xlsx.MergeCell("Sheet1", "H7", "B15") - xlsx.MergeCell("Sheet1", "D11", "F13") - xlsx.MergeCell("Sheet1", "G10", "K12") - xlsx.SetCellValue("Sheet1", "G11", "set value in merged cell") - xlsx.SetCellInt("Sheet1", "H11", 100) - xlsx.SetCellValue("Sheet1", "I11", float64(0.5)) - xlsx.SetCellHyperLink("Sheet1", "J11", "https://github.com/360EntSecGroup-Skylar/excelize", "External") - xlsx.SetCellFormula("Sheet1", "G12", "SUM(Sheet1!B19,Sheet1!C19)") - xlsx.GetCellValue("Sheet1", "H11") - xlsx.GetCellValue("Sheet2", "A6") // Merged cell ref is single coordinate. - xlsx.GetCellFormula("Sheet1", "G12") + f.MergeCell("Sheet1", "D9", "D9") + f.MergeCell("Sheet1", "D9", "E9") + f.MergeCell("Sheet1", "H14", "G13") + f.MergeCell("Sheet1", "C9", "D8") + f.MergeCell("Sheet1", "F11", "G13") + f.MergeCell("Sheet1", "H7", "B15") + f.MergeCell("Sheet1", "D11", "F13") + f.MergeCell("Sheet1", "G10", "K12") + f.SetCellValue("Sheet1", "G11", "set value in merged cell") + f.SetCellInt("Sheet1", "H11", 100) + f.SetCellValue("Sheet1", "I11", float64(0.5)) + f.SetCellHyperLink("Sheet1", "J11", "https://github.com/360EntSecGroup-Skylar/excelize", "External") + f.SetCellFormula("Sheet1", "G12", "SUM(Sheet1!B19,Sheet1!C19)") + f.GetCellValue("Sheet1", "H11") + f.GetCellValue("Sheet2", "A6") // Merged cell ref is single coordinate. + f.GetCellFormula("Sheet1", "G12") - assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestMergeCell.xlsx"))) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestMergeCell.xlsx"))) } func TestGetMergeCells(t *testing.T) { @@ -422,13 +430,13 @@ func TestGetMergeCells(t *testing.T) { end: "C10", }} - xlsx, err := OpenFile(filepath.Join("test", "MergeCell.xlsx")) + f, err := OpenFile(filepath.Join("test", "MergeCell.xlsx")) if !assert.NoError(t, err) { t.FailNow() } - sheet1 := xlsx.GetSheetName(1) + sheet1 := f.GetSheetName(1) - mergeCells, err := xlsx.GetMergeCells(sheet1) + mergeCells, err := f.GetMergeCells(sheet1) if !assert.Len(t, mergeCells, len(wants)) { t.FailNow() } @@ -441,38 +449,38 @@ func TestGetMergeCells(t *testing.T) { } // Test get merged cells on not exists worksheet. - _, err = xlsx.GetMergeCells("SheetN") + _, err = f.GetMergeCells("SheetN") assert.EqualError(t, err, "sheet SheetN is not exist") } func TestSetCellStyleAlignment(t *testing.T) { - xlsx, err := prepareTestBook1() + f, err := prepareTestBook1() if !assert.NoError(t, err) { t.FailNow() } var style int - style, err = xlsx.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}}`) + 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() } - assert.NoError(t, xlsx.SetCellStyle("Sheet1", "A22", "A22", style)) + assert.NoError(t, f.SetCellStyle("Sheet1", "A22", "A22", style)) // Test set cell style with given illegal rows number. - assert.EqualError(t, xlsx.SetCellStyle("Sheet1", "A", "A22", style), `cannot convert cell "A" to coordinates: invalid cell name "A"`) - assert.EqualError(t, xlsx.SetCellStyle("Sheet1", "A22", "A", style), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.EqualError(t, f.SetCellStyle("Sheet1", "A", "A22", style), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.EqualError(t, f.SetCellStyle("Sheet1", "A22", "A", style), `cannot convert cell "A" to coordinates: invalid cell name "A"`) // Test get cell style with given illegal rows number. - index, err := xlsx.GetCellStyle("Sheet1", "A") + index, err := f.GetCellStyle("Sheet1", "A") assert.Equal(t, 0, index) assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`) - assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetCellStyleAlignment.xlsx"))) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellStyleAlignment.xlsx"))) } func TestSetCellStyleBorder(t *testing.T) { - xlsx, err := prepareTestBook1() + f, err := prepareTestBook1() if !assert.NoError(t, err) { t.FailNow() } @@ -480,56 +488,56 @@ func TestSetCellStyleBorder(t *testing.T) { var style int // Test set border on overlapping area with vertical variants shading styles gradient fill. - style, err = xlsx.NewStyle(`{"border":[{"type":"left","color":"0000FF","style":2},{"type":"top","color":"00FF00","style":12},{"type":"bottom","color":"FFFF00","style":5},{"type":"right","color":"FF0000","style":6},{"type":"diagonalDown","color":"A020F0","style":9},{"type":"diagonalUp","color":"A020F0","style":8}]}`) + style, err = f.NewStyle(`{"border":[{"type":"left","color":"0000FF","style":2},{"type":"top","color":"00FF00","style":12},{"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() } - assert.NoError(t, xlsx.SetCellStyle("Sheet1", "J21", "L25", style)) + assert.NoError(t, f.SetCellStyle("Sheet1", "J21", "L25", style)) - style, err = xlsx.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}}`) + 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() } - assert.NoError(t, xlsx.SetCellStyle("Sheet1", "M28", "K24", style)) + assert.NoError(t, f.SetCellStyle("Sheet1", "M28", "K24", style)) - style, err = xlsx.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}}`) + 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() } - assert.NoError(t, xlsx.SetCellStyle("Sheet1", "M28", "K24", style)) + assert.NoError(t, f.SetCellStyle("Sheet1", "M28", "K24", style)) // Test set border and solid style pattern fill for a single cell. - style, err = xlsx.NewStyle(`{"border":[{"type":"left","color":"0000FF","style":8},{"type":"top","color":"00FF00","style":9},{"type":"bottom","color":"FFFF00","style":10},{"type":"right","color":"FF0000","style":11},{"type":"diagonalDown","color":"A020F0","style":12},{"type":"diagonalUp","color":"A020F0","style":13}],"fill":{"type":"pattern","color":["#E0EBF5"],"pattern":1}}`) + style, err = f.NewStyle(`{"border":[{"type":"left","color":"0000FF","style":8},{"type":"top","color":"00FF00","style":9},{"type":"bottom","color":"FFFF00","style":10},{"type":"right","color":"FF0000","style":11},{"type":"diagonalDown","color":"A020F0","style":12},{"type":"diagonalUp","color":"A020F0","style":13}],"fill":{"type":"pattern","color":["#E0EBF5"],"pattern":1}}`) if !assert.NoError(t, err) { t.FailNow() } - assert.NoError(t, xlsx.SetCellStyle("Sheet1", "O22", "O22", style)) + assert.NoError(t, f.SetCellStyle("Sheet1", "O22", "O22", style)) - assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetCellStyleBorder.xlsx"))) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellStyleBorder.xlsx"))) } func TestSetCellStyleBorderErrors(t *testing.T) { - xlsx, err := prepareTestBook1() + f, err := prepareTestBook1() if !assert.NoError(t, err) { t.FailNow() } // Set border with invalid style parameter. - _, err = xlsx.NewStyle("") + _, err = f.NewStyle("") if !assert.EqualError(t, err, "unexpected end of JSON input") { t.FailNow() } // Set border with invalid style index number. - _, err = xlsx.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}]}`) + _, 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() } } func TestSetCellStyleNumberFormat(t *testing.T) { - xlsx, err := prepareTestBook1() + f, err := prepareTestBook1() if !assert.NoError(t, err) { t.FailNow() } @@ -544,202 +552,202 @@ func TestSetCellStyleNumberFormat(t *testing.T) { var val float64 val, err = strconv.ParseFloat(v, 64) if err != nil { - xlsx.SetCellValue("Sheet2", c, v) + f.SetCellValue("Sheet2", c, v) } else { - xlsx.SetCellValue("Sheet2", c, val) + f.SetCellValue("Sheet2", c, val) } - style, err := xlsx.NewStyle(`{"fill":{"type":"gradient","color":["#FFFFFF","#E0EBF5"],"shading":5},"number_format": ` + strconv.Itoa(d) + `}`) + style, err := f.NewStyle(`{"fill":{"type":"gradient","color":["#FFFFFF","#E0EBF5"],"shading":5},"number_format": ` + strconv.Itoa(d) + `}`) if !assert.NoError(t, err) { t.FailNow() } - assert.NoError(t, xlsx.SetCellStyle("Sheet2", c, c, style)) - t.Log(xlsx.GetCellValue("Sheet2", c)) + assert.NoError(t, f.SetCellStyle("Sheet2", c, c, style)) + t.Log(f.GetCellValue("Sheet2", c)) } } var style int - style, err = xlsx.NewStyle(`{"number_format":-1}`) + style, err = f.NewStyle(`{"number_format":-1}`) if !assert.NoError(t, err) { t.FailNow() } - assert.NoError(t, xlsx.SetCellStyle("Sheet2", "L33", "L33", style)) + assert.NoError(t, f.SetCellStyle("Sheet2", "L33", "L33", style)) - assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetCellStyleNumberFormat.xlsx"))) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellStyleNumberFormat.xlsx"))) } func TestSetCellStyleCurrencyNumberFormat(t *testing.T) { t.Run("TestBook3", func(t *testing.T) { - xlsx, err := prepareTestBook3() + f, err := prepareTestBook3() if !assert.NoError(t, err) { t.FailNow() } - xlsx.SetCellValue("Sheet1", "A1", 56) - xlsx.SetCellValue("Sheet1", "A2", -32.3) + f.SetCellValue("Sheet1", "A1", 56) + f.SetCellValue("Sheet1", "A2", -32.3) var style int - style, err = xlsx.NewStyle(`{"number_format": 188, "decimal_places": -1}`) + style, err = f.NewStyle(`{"number_format": 188, "decimal_places": -1}`) if !assert.NoError(t, err) { t.FailNow() } - assert.NoError(t, xlsx.SetCellStyle("Sheet1", "A1", "A1", style)) - style, err = xlsx.NewStyle(`{"number_format": 188, "decimal_places": 31, "negred": true}`) + 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() } - assert.NoError(t, xlsx.SetCellStyle("Sheet1", "A2", "A2", style)) + assert.NoError(t, f.SetCellStyle("Sheet1", "A2", "A2", style)) - assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetCellStyleCurrencyNumberFormat.TestBook3.xlsx"))) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellStyleCurrencyNumberFormat.TestBook3.xlsx"))) }) t.Run("TestBook4", func(t *testing.T) { - xlsx, err := prepareTestBook4() + f, err := prepareTestBook4() if !assert.NoError(t, err) { t.FailNow() } - xlsx.SetCellValue("Sheet1", "A1", 42920.5) - xlsx.SetCellValue("Sheet1", "A2", 42920.5) + f.SetCellValue("Sheet1", "A1", 42920.5) + f.SetCellValue("Sheet1", "A2", 42920.5) - _, err = xlsx.NewStyle(`{"number_format": 26, "lang": "zh-tw"}`) + _, err = f.NewStyle(`{"number_format": 26, "lang": "zh-tw"}`) if !assert.NoError(t, err) { t.FailNow() } - style, err := xlsx.NewStyle(`{"number_format": 27}`) + style, err := f.NewStyle(`{"number_format": 27}`) if !assert.NoError(t, err) { t.FailNow() } - assert.NoError(t, xlsx.SetCellStyle("Sheet1", "A1", "A1", style)) - style, err = xlsx.NewStyle(`{"number_format": 31, "lang": "ko-kr"}`) + 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() } - assert.NoError(t, xlsx.SetCellStyle("Sheet1", "A2", "A2", style)) + assert.NoError(t, f.SetCellStyle("Sheet1", "A2", "A2", style)) - style, err = xlsx.NewStyle(`{"number_format": 71, "lang": "th-th"}`) + style, err = f.NewStyle(`{"number_format": 71, "lang": "th-th"}`) if !assert.NoError(t, err) { t.FailNow() } - assert.NoError(t, xlsx.SetCellStyle("Sheet1", "A2", "A2", style)) + assert.NoError(t, f.SetCellStyle("Sheet1", "A2", "A2", style)) - assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetCellStyleCurrencyNumberFormat.TestBook4.xlsx"))) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellStyleCurrencyNumberFormat.TestBook4.xlsx"))) }) } func TestSetCellStyleCustomNumberFormat(t *testing.T) { - xlsx := NewFile() - xlsx.SetCellValue("Sheet1", "A1", 42920.5) - xlsx.SetCellValue("Sheet1", "A2", 42920.5) - style, err := xlsx.NewStyle(`{"custom_number_format": "[$-380A]dddd\\,\\ dd\" de \"mmmm\" de \"yyyy;@"}`) + f := NewFile() + f.SetCellValue("Sheet1", "A1", 42920.5) + f.SetCellValue("Sheet1", "A2", 42920.5) + style, err := f.NewStyle(`{"custom_number_format": "[$-380A]dddd\\,\\ dd\" de \"mmmm\" de \"yyyy;@"}`) if err != nil { t.Log(err) } - assert.NoError(t, xlsx.SetCellStyle("Sheet1", "A1", "A1", style)) - style, err = xlsx.NewStyle(`{"custom_number_format": "[$-380A]dddd\\,\\ dd\" de \"mmmm\" de \"yyyy;@"}`) + assert.NoError(t, f.SetCellStyle("Sheet1", "A1", "A1", style)) + style, err = f.NewStyle(`{"custom_number_format": "[$-380A]dddd\\,\\ dd\" de \"mmmm\" de \"yyyy;@"}`) if err != nil { t.Log(err) } - assert.NoError(t, xlsx.SetCellStyle("Sheet1", "A2", "A2", style)) + assert.NoError(t, f.SetCellStyle("Sheet1", "A2", "A2", style)) - assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetCellStyleCustomNumberFormat.xlsx"))) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellStyleCustomNumberFormat.xlsx"))) } func TestSetCellStyleFill(t *testing.T) { - xlsx, err := prepareTestBook1() + f, err := prepareTestBook1() if !assert.NoError(t, err) { t.FailNow() } var style int // Test set fill for cell with invalid parameter. - style, err = xlsx.NewStyle(`{"fill":{"type":"gradient","color":["#FFFFFF","#E0EBF5"],"shading":6}}`) + style, err = f.NewStyle(`{"fill":{"type":"gradient","color":["#FFFFFF","#E0EBF5"],"shading":6}}`) if !assert.NoError(t, err) { t.FailNow() } - assert.NoError(t, xlsx.SetCellStyle("Sheet1", "O23", "O23", style)) + assert.NoError(t, f.SetCellStyle("Sheet1", "O23", "O23", style)) - style, err = xlsx.NewStyle(`{"fill":{"type":"gradient","color":["#FFFFFF"],"shading":1}}`) + style, err = f.NewStyle(`{"fill":{"type":"gradient","color":["#FFFFFF"],"shading":1}}`) if !assert.NoError(t, err) { t.FailNow() } - assert.NoError(t, xlsx.SetCellStyle("Sheet1", "O23", "O23", style)) + assert.NoError(t, f.SetCellStyle("Sheet1", "O23", "O23", style)) - style, err = xlsx.NewStyle(`{"fill":{"type":"pattern","color":[],"pattern":1}}`) + style, err = f.NewStyle(`{"fill":{"type":"pattern","color":[],"pattern":1}}`) if !assert.NoError(t, err) { t.FailNow() } - assert.NoError(t, xlsx.SetCellStyle("Sheet1", "O23", "O23", style)) + assert.NoError(t, f.SetCellStyle("Sheet1", "O23", "O23", style)) - style, err = xlsx.NewStyle(`{"fill":{"type":"pattern","color":["#E0EBF5"],"pattern":19}}`) + style, err = f.NewStyle(`{"fill":{"type":"pattern","color":["#E0EBF5"],"pattern":19}}`) if !assert.NoError(t, err) { t.FailNow() } - assert.NoError(t, xlsx.SetCellStyle("Sheet1", "O23", "O23", style)) + assert.NoError(t, f.SetCellStyle("Sheet1", "O23", "O23", style)) - assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetCellStyleFill.xlsx"))) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellStyleFill.xlsx"))) } func TestSetCellStyleFont(t *testing.T) { - xlsx, err := prepareTestBook1() + f, err := prepareTestBook1() if !assert.NoError(t, err) { t.FailNow() } var style int - style, err = xlsx.NewStyle(`{"font":{"bold":true,"italic":true,"family":"Berlin Sans FB Demi","size":36,"color":"#777777","underline":"single"}}`) + style, err = f.NewStyle(`{"font":{"bold":true,"italic":true,"family":"Berlin Sans FB Demi","size":36,"color":"#777777","underline":"single"}}`) if !assert.NoError(t, err) { t.FailNow() } - assert.NoError(t, xlsx.SetCellStyle("Sheet2", "A1", "A1", style)) + assert.NoError(t, f.SetCellStyle("Sheet2", "A1", "A1", style)) - style, err = xlsx.NewStyle(`{"font":{"italic":true,"underline":"double"}}`) + style, err = f.NewStyle(`{"font":{"italic":true,"underline":"double"}}`) if !assert.NoError(t, err) { t.FailNow() } - assert.NoError(t, xlsx.SetCellStyle("Sheet2", "A2", "A2", style)) + assert.NoError(t, f.SetCellStyle("Sheet2", "A2", "A2", style)) - style, err = xlsx.NewStyle(`{"font":{"bold":true}}`) + style, err = f.NewStyle(`{"font":{"bold":true}}`) if !assert.NoError(t, err) { t.FailNow() } - assert.NoError(t, xlsx.SetCellStyle("Sheet2", "A3", "A3", style)) + assert.NoError(t, f.SetCellStyle("Sheet2", "A3", "A3", style)) - style, err = xlsx.NewStyle(`{"font":{"bold":true,"family":"","size":0,"color":"","underline":""}}`) + style, err = f.NewStyle(`{"font":{"bold":true,"family":"","size":0,"color":"","underline":""}}`) if !assert.NoError(t, err) { t.FailNow() } - assert.NoError(t, xlsx.SetCellStyle("Sheet2", "A4", "A4", style)) + assert.NoError(t, f.SetCellStyle("Sheet2", "A4", "A4", style)) - style, err = xlsx.NewStyle(`{"font":{"color":"#777777"}}`) + style, err = f.NewStyle(`{"font":{"color":"#777777"}}`) if !assert.NoError(t, err) { t.FailNow() } - assert.NoError(t, xlsx.SetCellStyle("Sheet2", "A5", "A5", style)) + assert.NoError(t, f.SetCellStyle("Sheet2", "A5", "A5", style)) - assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetCellStyleFont.xlsx"))) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellStyleFont.xlsx"))) } func TestSetCellStyleProtection(t *testing.T) { - xlsx, err := prepareTestBook1() + f, err := prepareTestBook1() if !assert.NoError(t, err) { t.FailNow() } var style int - style, err = xlsx.NewStyle(`{"protection":{"hidden":true, "locked":true}}`) + style, err = f.NewStyle(`{"protection":{"hidden":true, "locked":true}}`) if !assert.NoError(t, err) { t.FailNow() } - assert.NoError(t, xlsx.SetCellStyle("Sheet2", "A6", "A6", style)) - err = xlsx.SaveAs(filepath.Join("test", "TestSetCellStyleProtection.xlsx")) + assert.NoError(t, f.SetCellStyle("Sheet2", "A6", "A6", style)) + err = f.SaveAs(filepath.Join("test", "TestSetCellStyleProtection.xlsx")) if !assert.NoError(t, err) { t.FailNow() } @@ -747,172 +755,172 @@ func TestSetCellStyleProtection(t *testing.T) { func TestSetDeleteSheet(t *testing.T) { t.Run("TestBook3", func(t *testing.T) { - xlsx, err := prepareTestBook3() + f, err := prepareTestBook3() if !assert.NoError(t, err) { t.FailNow() } - xlsx.DeleteSheet("XLSXSheet3") - assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetDeleteSheet.TestBook3.xlsx"))) + f.DeleteSheet("XLSXSheet3") + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetDeleteSheet.TestBook3.xlsx"))) }) t.Run("TestBook4", func(t *testing.T) { - xlsx, err := prepareTestBook4() + f, err := prepareTestBook4() if !assert.NoError(t, err) { t.FailNow() } - xlsx.DeleteSheet("Sheet1") - xlsx.AddComment("Sheet1", "A1", "") - xlsx.AddComment("Sheet1", "A1", `{"author":"Excelize: ","text":"This is a comment."}`) - assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetDeleteSheet.TestBook4.xlsx"))) + f.DeleteSheet("Sheet1") + f.AddComment("Sheet1", "A1", "") + f.AddComment("Sheet1", "A1", `{"author":"Excelize: ","text":"This is a comment."}`) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetDeleteSheet.TestBook4.xlsx"))) }) } func TestSheetVisibility(t *testing.T) { - xlsx, err := prepareTestBook1() + f, err := prepareTestBook1() if !assert.NoError(t, err) { t.FailNow() } - xlsx.SetSheetVisible("Sheet2", false) - xlsx.SetSheetVisible("Sheet1", false) - xlsx.SetSheetVisible("Sheet1", true) - xlsx.GetSheetVisible("Sheet1") + f.SetSheetVisible("Sheet2", false) + f.SetSheetVisible("Sheet1", false) + f.SetSheetVisible("Sheet1", true) + f.GetSheetVisible("Sheet1") - assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSheetVisibility.xlsx"))) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSheetVisibility.xlsx"))) } func TestColumnVisibility(t *testing.T) { t.Run("TestBook1", func(t *testing.T) { - xlsx, err := prepareTestBook1() + f, err := prepareTestBook1() if !assert.NoError(t, err) { t.FailNow() } - assert.NoError(t, xlsx.SetColVisible("Sheet1", "F", false)) - assert.NoError(t, xlsx.SetColVisible("Sheet1", "F", true)) - visible, err := xlsx.GetColVisible("Sheet1", "F") + assert.NoError(t, f.SetColVisible("Sheet1", "F", false)) + assert.NoError(t, f.SetColVisible("Sheet1", "F", true)) + visible, err := f.GetColVisible("Sheet1", "F") assert.Equal(t, true, visible) assert.NoError(t, err) // Test get column visiable on not exists worksheet. - _, err = xlsx.GetColVisible("SheetN", "F") + _, err = f.GetColVisible("SheetN", "F") assert.EqualError(t, err, "sheet SheetN is not exist") // Test get column visiable with illegal cell coordinates. - _, err = xlsx.GetColVisible("Sheet1", "*") + _, err = f.GetColVisible("Sheet1", "*") assert.EqualError(t, err, `invalid column name "*"`) - assert.EqualError(t, xlsx.SetColVisible("Sheet1", "*", false), `invalid column name "*"`) + assert.EqualError(t, f.SetColVisible("Sheet1", "*", false), `invalid column name "*"`) - xlsx.NewSheet("Sheet3") - assert.NoError(t, xlsx.SetColVisible("Sheet3", "E", false)) + f.NewSheet("Sheet3") + assert.NoError(t, f.SetColVisible("Sheet3", "E", false)) - assert.EqualError(t, xlsx.SetColVisible("SheetN", "E", false), "sheet SheetN is not exist") - assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestColumnVisibility.xlsx"))) + assert.EqualError(t, f.SetColVisible("SheetN", "E", false), "sheet SheetN is not exist") + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestColumnVisibility.xlsx"))) }) t.Run("TestBook3", func(t *testing.T) { - xlsx, err := prepareTestBook3() + f, err := prepareTestBook3() if !assert.NoError(t, err) { t.FailNow() } - xlsx.GetColVisible("Sheet1", "B") + f.GetColVisible("Sheet1", "B") }) } func TestCopySheet(t *testing.T) { - xlsx, err := prepareTestBook1() + f, err := prepareTestBook1() if !assert.NoError(t, err) { t.FailNow() } - idx := xlsx.NewSheet("CopySheet") - assert.EqualError(t, xlsx.CopySheet(1, idx), "sheet sheet1 is not exist") + idx := f.NewSheet("CopySheet") + assert.EqualError(t, f.CopySheet(1, idx), "sheet sheet1 is not exist") - xlsx.SetCellValue("Sheet4", "F1", "Hello") - val, err := xlsx.GetCellValue("Sheet1", "F1") + f.SetCellValue("Sheet4", "F1", "Hello") + val, err := f.GetCellValue("Sheet1", "F1") assert.NoError(t, err) assert.NotEqual(t, "Hello", val) - assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestCopySheet.xlsx"))) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestCopySheet.xlsx"))) } func TestCopySheetError(t *testing.T) { - xlsx, err := prepareTestBook1() + f, err := prepareTestBook1() if !assert.NoError(t, err) { t.FailNow() } - err = xlsx.CopySheet(0, -1) + err = f.CopySheet(0, -1) if !assert.EqualError(t, err, "invalid worksheet index") { t.FailNow() } - assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestCopySheetError.xlsx"))) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestCopySheetError.xlsx"))) } func TestAddTable(t *testing.T) { - xlsx, err := prepareTestBook1() + f, err := prepareTestBook1() if !assert.NoError(t, err) { t.FailNow() } - err = xlsx.AddTable("Sheet1", "B26", "A21", `{}`) + err = f.AddTable("Sheet1", "B26", "A21", `{}`) if !assert.NoError(t, err) { t.FailNow() } - err = xlsx.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}`) + 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() } - err = xlsx.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 with illegal formatset. - assert.EqualError(t, xlsx.AddTable("Sheet1", "B26", "A21", `{x}`), "invalid character 'x' looking for beginning of object key string") + 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. - assert.EqualError(t, xlsx.AddTable("Sheet1", "A", "B1", `{}`), `cannot convert cell "A" to coordinates: invalid cell name "A"`) - assert.EqualError(t, xlsx.AddTable("Sheet1", "A1", "B", `{}`), `cannot convert cell "B" to coordinates: invalid cell name "B"`) + assert.EqualError(t, f.AddTable("Sheet1", "A", "B1", `{}`), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.EqualError(t, f.AddTable("Sheet1", "A1", "B", `{}`), `cannot convert cell "B" to coordinates: invalid cell name "B"`) - assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestAddTable.xlsx"))) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddTable.xlsx"))) // Test addTable with illegal cell coordinates. - f := NewFile() + 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]") } func TestAddShape(t *testing.T) { - xlsx, err := prepareTestBook1() + f, err := prepareTestBook1() if !assert.NoError(t, err) { t.FailNow() } - xlsx.AddShape("Sheet1", "A30", `{"type":"rect","paragraph":[{"text":"Rectangle","font":{"color":"CD5C5C"}},{"text":"Shape","font":{"bold":true,"color":"2980B9"}}]}`) - xlsx.AddShape("Sheet1", "B30", `{"type":"rect","paragraph":[{"text":"Rectangle"},{}]}`) - xlsx.AddShape("Sheet1", "C30", `{"type":"rect","paragraph":[]}`) - xlsx.AddShape("Sheet3", "H1", `{"type":"ellipseRibbon", "color":{"line":"#4286f4","fill":"#8eb9ff"}, "paragraph":[{"font":{"bold":true,"italic":true,"family":"Berlin Sans FB Demi","size":36,"color":"#777777","underline":"single"}}], "height": 90}`) - xlsx.AddShape("Sheet3", "H1", "") + f.AddShape("Sheet1", "A30", `{"type":"rect","paragraph":[{"text":"Rectangle","font":{"color":"CD5C5C"}},{"text":"Shape","font":{"bold":true,"color":"2980B9"}}]}`) + f.AddShape("Sheet1", "B30", `{"type":"rect","paragraph":[{"text":"Rectangle"},{}]}`) + f.AddShape("Sheet1", "C30", `{"type":"rect","paragraph":[]}`) + f.AddShape("Sheet3", "H1", `{"type":"ellipseRibbon", "color":{"line":"#4286f4","fill":"#8eb9ff"}, "paragraph":[{"font":{"bold":true,"italic":true,"family":"Berlin Sans FB Demi","size":36,"color":"#777777","underline":"single"}}], "height": 90}`) + f.AddShape("Sheet3", "H1", "") - assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestAddShape.xlsx"))) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddShape.xlsx"))) } func TestAddComments(t *testing.T) { - xlsx, err := prepareTestBook1() + f, err := prepareTestBook1() if !assert.NoError(t, err) { t.FailNow() } s := strings.Repeat("c", 32768) - xlsx.AddComment("Sheet1", "A30", `{"author":"`+s+`","text":"`+s+`"}`) - xlsx.AddComment("Sheet2", "B7", `{"author":"Excelize: ","text":"This is a comment."}`) + f.AddComment("Sheet1", "A30", `{"author":"`+s+`","text":"`+s+`"}`) + f.AddComment("Sheet2", "B7", `{"author":"Excelize: ","text":"This is a comment."}`) - if assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestAddComments.xlsx"))) { - assert.Len(t, xlsx.GetComments(), 2) + if assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddComments.xlsx"))) { + assert.Len(t, f.GetComments(), 2) } } @@ -924,7 +932,7 @@ func TestGetSheetComments(t *testing.T) { func TestAutoFilter(t *testing.T) { outFile := filepath.Join("test", "TestAutoFilter%d.xlsx") - xlsx, err := prepareTestBook1() + f, err := prepareTestBook1() if !assert.NoError(t, err) { t.FailNow() } @@ -942,21 +950,21 @@ func TestAutoFilter(t *testing.T) { for i, format := range formats { t.Run(fmt.Sprintf("Expression%d", i+1), func(t *testing.T) { - err = xlsx.AutoFilter("Sheet1", "D4", "B1", format) + err = f.AutoFilter("Sheet1", "D4", "B1", format) assert.NoError(t, err) - assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, i+1))) + assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, i+1))) }) } // testing AutoFilter with illegal cell coordinates. - assert.EqualError(t, xlsx.AutoFilter("Sheet1", "A", "B1", ""), `cannot convert cell "A" to coordinates: invalid cell name "A"`) - assert.EqualError(t, xlsx.AutoFilter("Sheet1", "A1", "B", ""), `cannot convert cell "B" to coordinates: invalid cell name "B"`) + assert.EqualError(t, f.AutoFilter("Sheet1", "A", "B1", ""), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.EqualError(t, f.AutoFilter("Sheet1", "A1", "B", ""), `cannot convert cell "B" to coordinates: invalid cell name "B"`) } func TestAutoFilterError(t *testing.T) { outFile := filepath.Join("test", "TestAutoFilterError%d.xlsx") - xlsx, err := prepareTestBook1() + f, err := prepareTestBook1() if !assert.NoError(t, err) { t.FailNow() } @@ -971,16 +979,16 @@ func TestAutoFilterError(t *testing.T) { } for i, format := range formats { t.Run(fmt.Sprintf("Expression%d", i+1), func(t *testing.T) { - err = xlsx.AutoFilter("Sheet3", "D4", "B1", format) + err = f.AutoFilter("Sheet3", "D4", "B1", format) if assert.Error(t, err) { - assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, i+1))) + assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, i+1))) } }) } } func TestAddChart(t *testing.T) { - xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if !assert.NoError(t, err) { t.FailNow() } @@ -988,258 +996,258 @@ func TestAddChart(t *testing.T) { categories := map[string]string{"A30": "Small", "A31": "Normal", "A32": "Large", "B29": "Apple", "C29": "Orange", "D29": "Pear"} values := map[string]int{"B30": 2, "C30": 3, "D30": 3, "B31": 5, "C31": 2, "D31": 4, "B32": 6, "C32": 7, "D32": 8} for k, v := range categories { - assert.NoError(t, xlsx.SetCellValue("Sheet1", k, v)) + assert.NoError(t, f.SetCellValue("Sheet1", k, v)) } for k, v := range values { - assert.NoError(t, xlsx.SetCellValue("Sheet1", k, v)) + assert.NoError(t, f.SetCellValue("Sheet1", k, v)) } - assert.EqualError(t, xlsx.AddChart("Sheet1", "P1", ""), "unexpected end of JSON input") + assert.EqualError(t, f.AddChart("Sheet1", "P1", ""), "unexpected end of JSON input") // Test add chart on not exists worksheet. - assert.EqualError(t, xlsx.AddChart("SheetN", "P1", "{}"), "sheet SheetN is not exist") - - assert.NoError(t, xlsx.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"}],"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":"Fruit 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, xlsx.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"}],"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":"Fruit 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, xlsx.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"}],"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":"Fruit 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, xlsx.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"}],"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":"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},"show_blanks_as":"zero"}`)) - assert.NoError(t, xlsx.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"}],"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":"Fruit 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"}`)) - assert.NoError(t, xlsx.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"}],"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":"Fruit 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, xlsx.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"}],"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":"Fruit 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, xlsx.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"}],"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":"Fruit 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, xlsx.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"}],"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":"Fruit 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, xlsx.AddChart("Sheet2", "P16", `{"type":"doughnut","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"}],"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":"Fruit 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"}`)) - assert.NoError(t, xlsx.AddChart("Sheet2", "X16", `{"type":"line","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"}],"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":"Fruit 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"}`)) - assert.NoError(t, xlsx.AddChart("Sheet2", "P32", `{"type":"pie3D","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"}],"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":"Fruit 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, xlsx.AddChart("Sheet2", "X32", `{"type":"pie","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"}],"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":"Fruit 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, xlsx.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"}],"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":"Fruit 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, xlsx.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"}],"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":"Fruit 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, xlsx.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"}],"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":"Fruit 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, xlsx.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"}],"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":"Fruit 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, xlsx.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"}],"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":"Fruit 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, xlsx.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"}],"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":"Fruit 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.EqualError(t, f.AddChart("SheetN", "P1", "{}"), "sheet SheetN is 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"}],"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":"Fruit 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"}],"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":"Fruit 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"}],"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":"Fruit 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"}],"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":"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},"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"}],"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":"Fruit 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"}`)) + 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"}],"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":"Fruit 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", "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"}],"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":"Fruit 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"}],"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":"Fruit 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"}],"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":"Fruit 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!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"}],"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":"Fruit 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"}`)) + 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"},{"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"}],"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":"Fruit 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"}`)) + assert.NoError(t, f.AddChart("Sheet2", "P32", `{"type":"pie3D","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"}],"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":"Fruit 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!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"}],"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":"Fruit 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"}],"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":"Fruit 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"}],"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":"Fruit 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"}],"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":"Fruit 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"}],"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":"Fruit 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"}],"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":"Fruit 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"}],"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":"Fruit 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}}`)) // area series charts - assert.NoError(t, xlsx.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"}],"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":"Fruit 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, xlsx.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"}],"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":"Fruit 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, xlsx.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"}],"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":"Fruit 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, xlsx.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"}],"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":"Fruit 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, xlsx.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"}],"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":"Fruit 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, xlsx.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"}],"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":"Fruit 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"}`)) - - assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestAddChart.xlsx"))) + 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"}],"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":"Fruit 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"}],"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":"Fruit 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"}],"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":"Fruit 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"}],"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":"Fruit 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"}],"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":"Fruit 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"}],"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":"Fruit 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"}`)) + + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddChart.xlsx"))) } func TestInsertCol(t *testing.T) { - xlsx := NewFile() - sheet1 := xlsx.GetSheetName(1) + f := NewFile() + sheet1 := f.GetSheetName(1) - fillCells(xlsx, sheet1, 10, 10) + fillCells(f, sheet1, 10, 10) - xlsx.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External") - xlsx.MergeCell(sheet1, "A1", "C3") + f.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External") + f.MergeCell(sheet1, "A1", "C3") - err := xlsx.AutoFilter(sheet1, "A2", "B2", `{"column":"B","expression":"x != blanks"}`) + err := f.AutoFilter(sheet1, "A2", "B2", `{"column":"B","expression":"x != blanks"}`) if !assert.NoError(t, err) { t.FailNow() } - assert.NoError(t, xlsx.InsertCol(sheet1, "A")) + assert.NoError(t, f.InsertCol(sheet1, "A")) // Test insert column with illegal cell coordinates. - assert.EqualError(t, xlsx.InsertCol("Sheet1", "*"), `invalid column name "*"`) + assert.EqualError(t, f.InsertCol("Sheet1", "*"), `invalid column name "*"`) - assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestInsertCol.xlsx"))) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestInsertCol.xlsx"))) } func TestRemoveCol(t *testing.T) { - xlsx := NewFile() - sheet1 := xlsx.GetSheetName(1) + f := NewFile() + sheet1 := f.GetSheetName(1) - fillCells(xlsx, sheet1, 10, 15) + fillCells(f, sheet1, 10, 15) - xlsx.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External") - xlsx.SetCellHyperLink(sheet1, "C5", "https://github.com", "External") + f.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External") + f.SetCellHyperLink(sheet1, "C5", "https://github.com", "External") - xlsx.MergeCell(sheet1, "A1", "B1") - xlsx.MergeCell(sheet1, "A2", "B2") + f.MergeCell(sheet1, "A1", "B1") + f.MergeCell(sheet1, "A2", "B2") - assert.NoError(t, xlsx.RemoveCol(sheet1, "A")) - assert.NoError(t, xlsx.RemoveCol(sheet1, "A")) + assert.NoError(t, f.RemoveCol(sheet1, "A")) + assert.NoError(t, f.RemoveCol(sheet1, "A")) // Test remove column with illegal cell coordinates. - assert.EqualError(t, xlsx.RemoveCol("Sheet1", "*"), `invalid column name "*"`) + assert.EqualError(t, f.RemoveCol("Sheet1", "*"), `invalid column name "*"`) // Test remove column on not exists worksheet. - assert.EqualError(t, xlsx.RemoveCol("SheetN", "B"), "sheet SheetN is not exist") + assert.EqualError(t, f.RemoveCol("SheetN", "B"), "sheet SheetN is not exist") - assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestRemoveCol.xlsx"))) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestRemoveCol.xlsx"))) } func TestSetPane(t *testing.T) { - xlsx := NewFile() - xlsx.SetPanes("Sheet1", `{"freeze":false,"split":false}`) - xlsx.NewSheet("Panes 2") - xlsx.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"}]}`) - xlsx.NewSheet("Panes 3") - xlsx.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"}]}`) - xlsx.NewSheet("Panes 4") - xlsx.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"}]}`) - xlsx.SetPanes("Panes 4", "") - - assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetPane.xlsx"))) + f := NewFile() + f.SetPanes("Sheet1", `{"freeze":false,"split":false}`) + f.NewSheet("Panes 2") + 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") + 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") + 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"}]}`) + f.SetPanes("Panes 4", "") + + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetPane.xlsx"))) } func TestConditionalFormat(t *testing.T) { - xlsx := NewFile() - sheet1 := xlsx.GetSheetName(1) + f := NewFile() + sheet1 := f.GetSheetName(1) - fillCells(xlsx, sheet1, 10, 15) + fillCells(f, sheet1, 10, 15) var format1, format2, format3 int var err error // Rose format for bad conditional. - format1, err = xlsx.NewConditionalStyle(`{"font":{"color":"#9A0511"},"fill":{"type":"pattern","color":["#FEC7CE"],"pattern":1}}`) + format1, err = f.NewConditionalStyle(`{"font":{"color":"#9A0511"},"fill":{"type":"pattern","color":["#FEC7CE"],"pattern":1}}`) if !assert.NoError(t, err) { t.FailNow() } // Light yellow format for neutral conditional. - format2, err = xlsx.NewConditionalStyle(`{"fill":{"type":"pattern","color":["#FEEAA0"],"pattern":1}}`) + format2, err = f.NewConditionalStyle(`{"fill":{"type":"pattern","color":["#FEEAA0"],"pattern":1}}`) if !assert.NoError(t, err) { t.FailNow() } // Light green format for good conditional. - format3, err = xlsx.NewConditionalStyle(`{"font":{"color":"#09600B"},"fill":{"type":"pattern","color":["#C7EECF"],"pattern":1}}`) + format3, err = f.NewConditionalStyle(`{"font":{"color":"#09600B"},"fill":{"type":"pattern","color":["#C7EECF"],"pattern":1}}`) if !assert.NoError(t, err) { t.FailNow() } // Color scales: 2 color. - xlsx.SetConditionalFormat(sheet1, "A1:A10", `[{"type":"2_color_scale","criteria":"=","min_type":"min","max_type":"max","min_color":"#F8696B","max_color":"#63BE7B"}]`) + 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. - xlsx.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"}]`) + 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... - xlsx.SetConditionalFormat(sheet1, "C1:C10", fmt.Sprintf(`[{"type":"cell","criteria":"between","format":%d,"minimum":"6","maximum":"8"}]`, format1)) + f.SetConditionalFormat(sheet1, "C1:C10", fmt.Sprintf(`[{"type":"cell","criteria":"between","format":%d,"minimum":"6","maximum":"8"}]`, format1)) // Hightlight cells rules: Greater Than... - xlsx.SetConditionalFormat(sheet1, "D1:D10", fmt.Sprintf(`[{"type":"cell","criteria":">","format":%d,"value":"6"}]`, format3)) + f.SetConditionalFormat(sheet1, "D1:D10", fmt.Sprintf(`[{"type":"cell","criteria":">","format":%d,"value":"6"}]`, format3)) // Hightlight cells rules: Equal To... - xlsx.SetConditionalFormat(sheet1, "E1:E10", fmt.Sprintf(`[{"type":"top","criteria":"=","format":%d}]`, format3)) + f.SetConditionalFormat(sheet1, "E1:E10", fmt.Sprintf(`[{"type":"top","criteria":"=","format":%d}]`, format3)) // Hightlight cells rules: Not Equal To... - xlsx.SetConditionalFormat(sheet1, "F1:F10", fmt.Sprintf(`[{"type":"unique","criteria":"=","format":%d}]`, format2)) + f.SetConditionalFormat(sheet1, "F1:F10", fmt.Sprintf(`[{"type":"unique","criteria":"=","format":%d}]`, format2)) // Hightlight cells rules: Duplicate Values... - xlsx.SetConditionalFormat(sheet1, "G1:G10", fmt.Sprintf(`[{"type":"duplicate","criteria":"=","format":%d}]`, format2)) + f.SetConditionalFormat(sheet1, "G1:G10", fmt.Sprintf(`[{"type":"duplicate","criteria":"=","format":%d}]`, format2)) // Top/Bottom rules: Top 10%. - xlsx.SetConditionalFormat(sheet1, "H1:H10", fmt.Sprintf(`[{"type":"top","criteria":"=","format":%d,"value":"6","percent":true}]`, format1)) + f.SetConditionalFormat(sheet1, "H1:H10", fmt.Sprintf(`[{"type":"top","criteria":"=","format":%d,"value":"6","percent":true}]`, format1)) // Top/Bottom rules: Above Average... - xlsx.SetConditionalFormat(sheet1, "I1:I10", fmt.Sprintf(`[{"type":"average","criteria":"=","format":%d, "above_average": true}]`, format3)) + f.SetConditionalFormat(sheet1, "I1:I10", fmt.Sprintf(`[{"type":"average","criteria":"=","format":%d, "above_average": true}]`, format3)) // Top/Bottom rules: Below Average... - xlsx.SetConditionalFormat(sheet1, "J1:J10", fmt.Sprintf(`[{"type":"average","criteria":"=","format":%d, "above_average": false}]`, format1)) + f.SetConditionalFormat(sheet1, "J1:J10", fmt.Sprintf(`[{"type":"average","criteria":"=","format":%d, "above_average": false}]`, format1)) // Data Bars: Gradient Fill. - xlsx.SetConditionalFormat(sheet1, "K1:K10", `[{"type":"data_bar", "criteria":"=", "min_type":"min","max_type":"max","bar_color":"#638EC6"}]`) + 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. - xlsx.SetConditionalFormat(sheet1, "L1:L10", fmt.Sprintf(`[{"type":"formula", "criteria":"L2<3", "format":%d}]`, format1)) + f.SetConditionalFormat(sheet1, "L1:L10", fmt.Sprintf(`[{"type":"formula", "criteria":"L2<3", "format":%d}]`, format1)) // Test set invalid format set in conditional format - xlsx.SetConditionalFormat(sheet1, "L1:L10", "") + f.SetConditionalFormat(sheet1, "L1:L10", "") - err = xlsx.SaveAs(filepath.Join("test", "TestConditionalFormat.xlsx")) + err = f.SaveAs(filepath.Join("test", "TestConditionalFormat.xlsx")) if !assert.NoError(t, err) { t.FailNow() } // Set conditional format with illegal valid type. - xlsx.SetConditionalFormat(sheet1, "K1:K10", `[{"type":"", "criteria":"=", "min_type":"min","max_type":"max","bar_color":"#638EC6"}]`) + f.SetConditionalFormat(sheet1, "K1:K10", `[{"type":"", "criteria":"=", "min_type":"min","max_type":"max","bar_color":"#638EC6"}]`) // Set conditional format with illegal criteria type. - xlsx.SetConditionalFormat(sheet1, "K1:K10", `[{"type":"data_bar", "criteria":"", "min_type":"min","max_type":"max","bar_color":"#638EC6"}]`) + 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 shold not return error. - xlsx, err = OpenFile(filepath.Join("test", "Book1.xlsx")) + f, err = OpenFile(filepath.Join("test", "Book1.xlsx")) if !assert.NoError(t, err) { t.FailNow() } - _, err = xlsx.NewConditionalStyle(`{"font":{"color":"#9A0511"},"fill":{"type":"pattern","color":["#FEC7CE"],"pattern":1}}`) + _, err = f.NewConditionalStyle(`{"font":{"color":"#9A0511"},"fill":{"type":"pattern","color":["#FEC7CE"],"pattern":1}}`) if !assert.NoError(t, err) { t.FailNow() } } func TestConditionalFormatError(t *testing.T) { - xlsx := NewFile() - sheet1 := xlsx.GetSheetName(1) + f := NewFile() + sheet1 := f.GetSheetName(1) - fillCells(xlsx, sheet1, 10, 15) + fillCells(f, sheet1, 10, 15) // Set conditional format with illegal JSON string should return error - _, err := xlsx.NewConditionalStyle("") + _, err := f.NewConditionalStyle("") if !assert.EqualError(t, err, "unexpected end of JSON input") { t.FailNow() } } func TestSharedStrings(t *testing.T) { - xlsx, err := OpenFile(filepath.Join("test", "SharedStrings.xlsx")) + f, err := OpenFile(filepath.Join("test", "SharedStrings.xlsx")) if !assert.NoError(t, err) { t.FailNow() } - xlsx.GetRows("Sheet1") + f.GetRows("Sheet1") } func TestSetSheetRow(t *testing.T) { - xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if !assert.NoError(t, err) { t.FailNow() } - xlsx.SetSheetRow("Sheet1", "B27", &[]interface{}{"cell", nil, int32(42), float64(42), time.Now().UTC()}) + f.SetSheetRow("Sheet1", "B27", &[]interface{}{"cell", nil, int32(42), float64(42), time.Now().UTC()}) - assert.EqualError(t, xlsx.SetSheetRow("Sheet1", "", &[]interface{}{"cell", nil, 2}), + assert.EqualError(t, f.SetSheetRow("Sheet1", "", &[]interface{}{"cell", nil, 2}), `cannot convert cell "" to coordinates: invalid cell name ""`) - assert.EqualError(t, xlsx.SetSheetRow("Sheet1", "B27", []interface{}{}), `pointer to slice expected`) - assert.EqualError(t, xlsx.SetSheetRow("Sheet1", "B27", &xlsx), `pointer to slice expected`) - assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestSetSheetRow.xlsx"))) + assert.EqualError(t, f.SetSheetRow("Sheet1", "B27", []interface{}{}), `pointer to slice expected`) + assert.EqualError(t, f.SetSheetRow("Sheet1", "B27", &f), `pointer to slice expected`) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetSheetRow.xlsx"))) } func TestOutlineLevel(t *testing.T) { - xlsx := NewFile() - xlsx.NewSheet("Sheet2") - xlsx.SetColOutlineLevel("Sheet1", "D", 4) - xlsx.GetColOutlineLevel("Sheet1", "D") - xlsx.GetColOutlineLevel("Shee2", "A") - xlsx.SetColWidth("Sheet2", "A", "D", 13) - xlsx.SetColOutlineLevel("Sheet2", "B", 2) - xlsx.SetRowOutlineLevel("Sheet1", 2, 250) + f := NewFile() + f.NewSheet("Sheet2") + f.SetColOutlineLevel("Sheet1", "D", 4) + f.GetColOutlineLevel("Sheet1", "D") + f.GetColOutlineLevel("Shee2", "A") + f.SetColWidth("Sheet2", "A", "D", 13) + f.SetColOutlineLevel("Sheet2", "B", 2) + f.SetRowOutlineLevel("Sheet1", 2, 250) // Test set and get column outline level with illegal cell coordinates. - assert.EqualError(t, xlsx.SetColOutlineLevel("Sheet1", "*", 1), `invalid column name "*"`) - _, err := xlsx.GetColOutlineLevel("Sheet1", "*") + assert.EqualError(t, f.SetColOutlineLevel("Sheet1", "*", 1), `invalid column name "*"`) + _, err := f.GetColOutlineLevel("Sheet1", "*") assert.EqualError(t, err, `invalid column name "*"`) // Test set column outline level on not exists worksheet. - assert.EqualError(t, xlsx.SetColOutlineLevel("SheetN", "E", 2), "sheet SheetN is not exist") + assert.EqualError(t, f.SetColOutlineLevel("SheetN", "E", 2), "sheet SheetN is not exist") - assert.EqualError(t, xlsx.SetRowOutlineLevel("Sheet1", 0, 1), "invalid row number 0") - level, err := xlsx.GetRowOutlineLevel("Sheet1", 2) + assert.EqualError(t, f.SetRowOutlineLevel("Sheet1", 0, 1), "invalid row number 0") + level, err := f.GetRowOutlineLevel("Sheet1", 2) assert.NoError(t, err) assert.Equal(t, uint8(250), level) - _, err = xlsx.GetRowOutlineLevel("Sheet1", 0) + _, err = f.GetRowOutlineLevel("Sheet1", 0) assert.EqualError(t, err, `invalid row number 0`) - level, err = xlsx.GetRowOutlineLevel("Sheet1", 10) + level, err = f.GetRowOutlineLevel("Sheet1", 10) assert.NoError(t, err) assert.Equal(t, uint8(0), level) - err = xlsx.SaveAs(filepath.Join("test", "TestOutlineLevel.xlsx")) + err = f.SaveAs(filepath.Join("test", "TestOutlineLevel.xlsx")) if !assert.NoError(t, err) { t.FailNow() } - xlsx, err = OpenFile(filepath.Join("test", "Book1.xlsx")) + f, err = OpenFile(filepath.Join("test", "Book1.xlsx")) if !assert.NoError(t, err) { t.FailNow() } - xlsx.SetColOutlineLevel("Sheet2", "B", 2) + f.SetColOutlineLevel("Sheet2", "B", 2) } func TestThemeColor(t *testing.T) { @@ -1268,44 +1276,44 @@ func TestHSL(t *testing.T) { } func TestSearchSheet(t *testing.T) { - xlsx, err := OpenFile(filepath.Join("test", "SharedStrings.xlsx")) + f, err := OpenFile(filepath.Join("test", "SharedStrings.xlsx")) if !assert.NoError(t, err) { t.FailNow() } // Test search in a not exists worksheet. - t.Log(xlsx.SearchSheet("Sheet4", "")) + t.Log(f.SearchSheet("Sheet4", "")) // Test search a not exists value. - t.Log(xlsx.SearchSheet("Sheet1", "X")) - t.Log(xlsx.SearchSheet("Sheet1", "A")) + t.Log(f.SearchSheet("Sheet1", "X")) + t.Log(f.SearchSheet("Sheet1", "A")) // Test search the coordinates where the numerical value in the range of // "0-9" of Sheet1 is described by regular expression: - t.Log(xlsx.SearchSheet("Sheet1", "[0-9]", true)) + t.Log(f.SearchSheet("Sheet1", "[0-9]", true)) } func TestProtectSheet(t *testing.T) { - xlsx := NewFile() - xlsx.ProtectSheet("Sheet1", nil) - xlsx.ProtectSheet("Sheet1", &FormatSheetProtection{ + f := NewFile() + f.ProtectSheet("Sheet1", nil) + f.ProtectSheet("Sheet1", &FormatSheetProtection{ Password: "password", EditScenarios: false, }) - assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestProtectSheet.xlsx"))) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestProtectSheet.xlsx"))) // Test protect not exists worksheet. - assert.EqualError(t, xlsx.ProtectSheet("SheetN", nil), "sheet SheetN is not exist") + assert.EqualError(t, f.ProtectSheet("SheetN", nil), "sheet SheetN is not exist") } func TestUnprotectSheet(t *testing.T) { - xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if !assert.NoError(t, err) { t.FailNow() } // Test unprotect not exists worksheet. - assert.EqualError(t, xlsx.UnprotectSheet("SheetN"), "sheet SheetN is not exist") + assert.EqualError(t, f.UnprotectSheet("SheetN"), "sheet SheetN is not exist") - xlsx.UnprotectSheet("Sheet1") - assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestUnprotectSheet.xlsx"))) + f.UnprotectSheet("Sheet1") + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestUnprotectSheet.xlsx"))) } func TestSetDefaultTimeStyle(t *testing.T) { @@ -1315,19 +1323,19 @@ func TestSetDefaultTimeStyle(t *testing.T) { } func prepareTestBook1() (*File, error) { - xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if err != nil { return nil, err } - err = xlsx.AddPicture("Sheet2", "I9", filepath.Join("test", "images", "excel.jpg"), + 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 { return nil, err } // Test add picture to worksheet with offset, external hyperlink and positioning. - err = xlsx.AddPicture("Sheet1", "F21", filepath.Join("test", "images", "excel.png"), + err = f.AddPicture("Sheet1", "F21", filepath.Join("test", "images", "excel.png"), `{"x_offset": 10, "y_offset": 10, "hyperlink": "https://github.com/360EntSecGroup-Skylar/excelize", "hyperlink_type": "External", "positioning": "oneCell"}`) if err != nil { return nil, err @@ -1338,52 +1346,52 @@ func prepareTestBook1() (*File, error) { return nil, err } - err = xlsx.AddPictureFromBytes("Sheet1", "Q1", "", "Excel Logo", ".jpg", file) + err = f.AddPictureFromBytes("Sheet1", "Q1", "", "Excel Logo", ".jpg", file) if err != nil { return nil, err } - return xlsx, nil + return f, nil } func prepareTestBook3() (*File, error) { - xlsx := NewFile() - xlsx.NewSheet("Sheet1") - xlsx.NewSheet("XLSXSheet2") - xlsx.NewSheet("XLSXSheet3") - xlsx.SetCellInt("XLSXSheet2", "A23", 56) - xlsx.SetCellStr("Sheet1", "B20", "42") - xlsx.SetActiveSheet(0) - - err := xlsx.AddPicture("Sheet1", "H2", filepath.Join("test", "images", "excel.gif"), + f := NewFile() + f.NewSheet("Sheet1") + f.NewSheet("XLSXSheet2") + f.NewSheet("XLSXSheet3") + f.SetCellInt("XLSXSheet2", "A23", 56) + f.SetCellStr("Sheet1", "B20", "42") + 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 { return nil, err } - err = xlsx.AddPicture("Sheet1", "C2", filepath.Join("test", "images", "excel.png"), "") + err = f.AddPicture("Sheet1", "C2", filepath.Join("test", "images", "excel.png"), "") if err != nil { return nil, err } - return xlsx, nil + return f, nil } func prepareTestBook4() (*File, error) { - xlsx := NewFile() - xlsx.SetColWidth("Sheet1", "B", "A", 12) - xlsx.SetColWidth("Sheet1", "A", "B", 12) - xlsx.GetColWidth("Sheet1", "A") - xlsx.GetColWidth("Sheet1", "C") + f := NewFile() + f.SetColWidth("Sheet1", "B", "A", 12) + f.SetColWidth("Sheet1", "A", "B", 12) + f.GetColWidth("Sheet1", "A") + f.GetColWidth("Sheet1", "C") - return xlsx, nil + return f, nil } -func fillCells(xlsx *File, sheet string, colCount, rowCount int) { +func fillCells(f *File, sheet string, colCount, rowCount int) { for col := 1; col <= colCount; col++ { for row := 1; row <= rowCount; row++ { cell, _ := CoordinatesToCellName(col, row) - xlsx.SetCellStr(sheet, cell, cell) + f.SetCellStr(sheet, cell, cell) } } } diff --git a/shape.go b/shape.go index f404a7a352..c90963cf87 100644 --- a/shape.go +++ b/shape.go @@ -40,7 +40,7 @@ func parseFormatShapeSet(formatSet string) (*formatShape, error) { // print settings) and properties set. For example, add text box (rect shape) // in Sheet1: // -// f.AddShape("Sheet1", "G6", `{"type":"rect","color":{"line":"#4286F4","fill":"#8eb9ff"},"paragraph":[{"text":"Rectangle Shape","font":{"bold":true,"italic":true,"family":"Berlin Sans FB Demi","size":36,"color":"#777777","underline":"sng"}}],"width":180,"height": 90}`) +// err := f.AddShape("Sheet1", "G6", `{"type":"rect","color":{"line":"#4286F4","fill":"#8eb9ff"},"paragraph":[{"text":"Rectangle Shape","font":{"bold":true,"italic":true,"family":"Berlin Sans FB Demi","size":36,"color":"#777777","underline":"sng"}}],"width":180,"height": 90}`) // // The following shows the type of shape supported by excelize: // From 095b5fb62a4bfea5c2163e415ad26e46c8c3b720 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 22 Apr 2019 16:59:41 +0800 Subject: [PATCH 086/957] Resolve #387, skip saving empty calculation chains --- calcchain.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/calcchain.go b/calcchain.go index 22aab7e323..ce679e531c 100644 --- a/calcchain.go +++ b/calcchain.go @@ -25,7 +25,7 @@ func (f *File) calcChainReader() *xlsxCalcChain { // calcChainWriter provides a function to save xl/calcChain.xml after // serialize structure. func (f *File) calcChainWriter() { - if f.CalcChain != nil { + if f.CalcChain != nil && f.CalcChain.C != nil { output, _ := xml.Marshal(f.CalcChain) f.saveFileList("xl/calcChain.xml", output) } From 29b2854e53560beb3c238786929097421fa9a942 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 23 Apr 2019 13:34:24 +0800 Subject: [PATCH 087/957] Update readme --- README.md | 6 +++++- README_zh.md | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 472ed6049c..eae00727cb 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,11 @@ func main() { return } // Get value from cell by given worksheet name and axis. - cell := f.GetCellValue("Sheet1", "B2") + cell, err := f.GetCellValue("Sheet1", "B2") + if err != nil { + fmt.Println(err) + return + } fmt.Println(cell) // Get all the rows in the Sheet1. rows, err := f.GetRows("Sheet1") diff --git a/README_zh.md b/README_zh.md index addc9f2efb..dfed749ddc 100644 --- a/README_zh.md +++ b/README_zh.md @@ -73,7 +73,11 @@ func main() { return } // 获取工作表中指定单元格的值 - cell := f.GetCellValue("Sheet1", "B2") + cell, err := f.GetCellValue("Sheet1", "B2") + if err != nil { + fmt.Println(err) + return + } fmt.Println(cell) // 获取 Sheet1 上所有单元格 rows, err := f.GetRows("Sheet1") From 01a418bda8502890e89ab20a2b41220372877bae Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 27 Apr 2019 23:40:57 +0800 Subject: [PATCH 088/957] Resolve #392, compatible with strict relations name space inspection --- sheet.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/sheet.go b/sheet.go index 32d12d1dcc..5e9fcae952 100644 --- a/sheet.go +++ b/sheet.go @@ -92,7 +92,7 @@ func (f *File) workbookReader() *xlsxWorkbook { func (f *File) workBookWriter() { if f.WorkBook != nil { output, _ := xml.Marshal(f.WorkBook) - f.saveFileList("xl/workbook.xml", replaceRelationshipsNameSpaceBytes(output)) + f.saveFileList("xl/workbook.xml", replaceRelationshipsBytes(replaceRelationshipsNameSpaceBytes(output))) } } @@ -105,7 +105,7 @@ func (f *File) workSheetWriter() { f.Sheet[p].SheetData.Row[k].C = trimCell(v.C) } output, _ := xml.Marshal(sheet) - f.saveFileList(p, replaceWorkSheetsRelationshipsNameSpaceBytes(output)) + f.saveFileList(p, replaceRelationshipsBytes(replaceWorkSheetsRelationshipsNameSpaceBytes(output))) ok := f.checked[p] if ok { f.checked[p] = false @@ -211,6 +211,15 @@ func (f *File) setAppXML() { f.saveFileList("docProps/app.xml", []byte(templateDocpropsApp)) } +// replaceRelationshipsBytes; Some tools that read XLSX 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. +func replaceRelationshipsBytes(content []byte) []byte { + oldXmlns := []byte(`xmlns:relationships="http://schemas.openxmlformats.org/officeDocument/2006/relationships" relationships`) + newXmlns := []byte("r") + return bytes.Replace(content, oldXmlns, newXmlns, -1) +} + // replaceRelationshipsNameSpaceBytes; Some tools that read XLSX files have // very strict requirements about the structure of the input XML. In // particular both Numbers on the Mac and SAS dislike inline XML namespace From b1f632d4084130628f10906ff6a7bb55022e4c08 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 30 Apr 2019 18:39:27 +0800 Subject: [PATCH 089/957] Resolve #393, upgrade Go module to v2 --- README.md | 10 ++++----- README_zh.md | 10 ++++----- cellmerged.go | 48 +++++++++++++++++++++++++++++++++++++++++ cellmerged_test.go | 54 ++++++++++++++++++++++++++++++++++++++++++++++ chart.go | 2 +- excelize.go | 47 ---------------------------------------- excelize_test.go | 46 --------------------------------------- go.mod | 5 +++-- go.sum | 3 +-- picture.go | 4 ++-- sheet_test.go | 2 +- sheetpr_test.go | 30 +++++++++++++------------- sheetview_test.go | 44 ++++++++++++++++++------------------- 13 files changed, 157 insertions(+), 148 deletions(-) create mode 100644 cellmerged.go create mode 100644 cellmerged_test.go diff --git a/README.md b/README.md index eae00727cb..91155e3545 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Supports saving a file without losing original charts of XLSX. This library need ### Installation ```bash -go get github.com/360EntSecGroup-Skylar/excelize +go get github.com/360EntSecGroup-Skylar/excelize/v2 ``` ### Create XLSX file @@ -34,7 +34,7 @@ package main import ( "fmt" - "github.com/360EntSecGroup-Skylar/excelize" + "github.com/360EntSecGroup-Skylar/excelize/v2" ) func main() { @@ -64,7 +64,7 @@ package main import ( "fmt" - "github.com/360EntSecGroup-Skylar/excelize" + "github.com/360EntSecGroup-Skylar/excelize/v2" ) func main() { @@ -103,7 +103,7 @@ package main import ( "fmt" - "github.com/360EntSecGroup-Skylar/excelize" + "github.com/360EntSecGroup-Skylar/excelize/v2" ) func main() { @@ -140,7 +140,7 @@ import ( _ "image/jpeg" _ "image/png" - "github.com/360EntSecGroup-Skylar/excelize" + "github.com/360EntSecGroup-Skylar/excelize/v2" ) func main() { diff --git a/README_zh.md b/README_zh.md index dfed749ddc..044d9305ae 100644 --- a/README_zh.md +++ b/README_zh.md @@ -20,7 +20,7 @@ Excelize 是 Go 语言编写的用于操作 Office Excel 文档类库,基于 E ### 安装 ```bash -go get github.com/360EntSecGroup-Skylar/excelize +go get github.com/360EntSecGroup-Skylar/excelize/v2 ``` ### 创建 Excel 文档 @@ -33,7 +33,7 @@ package main import ( "fmt" - "github.com/360EntSecGroup-Skylar/excelize" + "github.com/360EntSecGroup-Skylar/excelize/v2" ) func main() { @@ -63,7 +63,7 @@ package main import ( "fmt" - "github.com/360EntSecGroup-Skylar/excelize" + "github.com/360EntSecGroup-Skylar/excelize/v2" ) func main() { @@ -102,7 +102,7 @@ package main import ( "fmt" - "github.com/360EntSecGroup-Skylar/excelize" + "github.com/360EntSecGroup-Skylar/excelize/v2" ) func main() { @@ -140,7 +140,7 @@ import ( _ "image/jpeg" _ "image/png" - "github.com/360EntSecGroup-Skylar/excelize" + "github.com/360EntSecGroup-Skylar/excelize/v2" ) func main() { diff --git a/cellmerged.go b/cellmerged.go new file mode 100644 index 0000000000..53924637c2 --- /dev/null +++ b/cellmerged.go @@ -0,0 +1,48 @@ +package excelize + +import "strings" + +// GetMergeCells provides a function to get all merged cells from a worksheet currently. +func (f *File) GetMergeCells(sheet string) ([]MergeCell, error) { + var mergeCells []MergeCell + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return mergeCells, err + } + if xlsx.MergeCells != nil { + mergeCells = make([]MergeCell, 0, len(xlsx.MergeCells.Cells)) + + for i := range xlsx.MergeCells.Cells { + ref := xlsx.MergeCells.Cells[i].Ref + axis := strings.Split(ref, ":")[0] + val, _ := f.GetCellValue(sheet, axis) + mergeCells = append(mergeCells, []string{ref, val}) + } + } + + return mergeCells, err +} + +// MergeCell define a merged cell data. +// It consists of the following structure. +// example: []string{"D4:E10", "cell value"} +type MergeCell []string + +// GetCellValue returns merged cell value. +func (m *MergeCell) GetCellValue() string { + return (*m)[1] +} + +// GetStartAxis returns the merge start axis. +// example: "C2" +func (m *MergeCell) GetStartAxis() string { + axis := strings.Split((*m)[0], ":") + return axis[0] +} + +// GetEndAxis returns the merge end axis. +// example: "D4" +func (m *MergeCell) GetEndAxis() string { + axis := strings.Split((*m)[0], ":") + return axis[1] +} \ No newline at end of file diff --git a/cellmerged_test.go b/cellmerged_test.go new file mode 100644 index 0000000000..d53acc2eb3 --- /dev/null +++ b/cellmerged_test.go @@ -0,0 +1,54 @@ +package excelize + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetMergeCells(t *testing.T) { + wants := []struct { + value string + start string + end string + }{{ + value: "A1", + start: "A1", + end: "B1", + }, { + value: "A2", + start: "A2", + end: "A3", + }, { + value: "A4", + start: "A4", + end: "B5", + }, { + value: "A7", + start: "A7", + end: "C10", + }} + + f, err := OpenFile(filepath.Join("test", "MergeCell.xlsx")) + if !assert.NoError(t, err) { + t.FailNow() + } + sheet1 := f.GetSheetName(1) + + mergeCells, err := f.GetMergeCells(sheet1) + if !assert.Len(t, mergeCells, len(wants)) { + t.FailNow() + } + assert.NoError(t, err) + + for i, m := range mergeCells { + assert.Equal(t, wants[i].value, m.GetCellValue()) + assert.Equal(t, wants[i].start, m.GetStartAxis()) + assert.Equal(t, wants[i].end, m.GetEndAxis()) + } + + // Test get merged cells on not exists worksheet. + _, err = f.GetMergeCells("SheetN") + assert.EqualError(t, err, "sheet SheetN is not exist") +} diff --git a/chart.go b/chart.go index d669a4739e..88f48b2135 100644 --- a/chart.go +++ b/chart.go @@ -308,7 +308,7 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // import ( // "fmt" // -// "github.com/360EntSecGroup-Skylar/excelize" +// "github.com/360EntSecGroup-Skylar/excelize/v2" // ) // // func main() { diff --git a/excelize.go b/excelize.go index 41fba37cb8..6fb98c4297 100644 --- a/excelize.go +++ b/excelize.go @@ -19,7 +19,6 @@ import ( "io/ioutil" "os" "strconv" - "strings" ) // File define a populated XLSX file struct. @@ -215,49 +214,3 @@ func (f *File) UpdateLinkedValue() error { } return nil } - -// GetMergeCells provides a function to get all merged cells from a worksheet -// currently. -func (f *File) GetMergeCells(sheet string) ([]MergeCell, error) { - var mergeCells []MergeCell - xlsx, err := f.workSheetReader(sheet) - if err != nil { - return mergeCells, err - } - if xlsx.MergeCells != nil { - mergeCells = make([]MergeCell, 0, len(xlsx.MergeCells.Cells)) - - for i := range xlsx.MergeCells.Cells { - ref := xlsx.MergeCells.Cells[i].Ref - axis := strings.Split(ref, ":")[0] - val, _ := f.GetCellValue(sheet, axis) - mergeCells = append(mergeCells, []string{ref, val}) - } - } - - return mergeCells, err -} - -// MergeCell define a merged cell data. -// It consists of the following structure. -// example: []string{"D4:E10", "cell value"} -type MergeCell []string - -// GetCellValue returns merged cell value. -func (m *MergeCell) GetCellValue() string { - return (*m)[1] -} - -// GetStartAxis returns the merge start axis. -// example: "C2" -func (m *MergeCell) GetStartAxis() string { - axis := strings.Split((*m)[0], ":") - return axis[0] -} - -// GetEndAxis returns the merge end axis. -// example: "D4" -func (m *MergeCell) GetEndAxis() string { - axis := strings.Split((*m)[0], ":") - return axis[1] -} diff --git a/excelize_test.go b/excelize_test.go index 87fd806871..c76aa92b06 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -407,52 +407,6 @@ func TestMergeCell(t *testing.T) { assert.NoError(t, f.SaveAs(filepath.Join("test", "TestMergeCell.xlsx"))) } -func TestGetMergeCells(t *testing.T) { - wants := []struct { - value string - start string - end string - }{{ - value: "A1", - start: "A1", - end: "B1", - }, { - value: "A2", - start: "A2", - end: "A3", - }, { - value: "A4", - start: "A4", - end: "B5", - }, { - value: "A7", - start: "A7", - end: "C10", - }} - - f, err := OpenFile(filepath.Join("test", "MergeCell.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } - sheet1 := f.GetSheetName(1) - - mergeCells, err := f.GetMergeCells(sheet1) - if !assert.Len(t, mergeCells, len(wants)) { - t.FailNow() - } - assert.NoError(t, err) - - for i, m := range mergeCells { - assert.Equal(t, wants[i].value, m.GetCellValue()) - assert.Equal(t, wants[i].start, m.GetStartAxis()) - assert.Equal(t, wants[i].end, m.GetEndAxis()) - } - - // Test get merged cells on not exists worksheet. - _, err = f.GetMergeCells("SheetN") - assert.EqualError(t, err, "sheet SheetN is not exist") -} - func TestSetCellStyleAlignment(t *testing.T) { f, err := prepareTestBook1() if !assert.NoError(t, err) { diff --git a/go.mod b/go.mod index b96dbe2c5f..9f36b59039 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,8 @@ -module github.com/360EntSecGroup-Skylar/excelize +module github.com/360EntSecGroup-Skylar/excelize/v2 + +go 1.12 require ( - github.com/davecgh/go-spew v1.1.1 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 github.com/stretchr/testify v1.3.0 ) diff --git a/go.sum b/go.sum index 106a41735b..890277c721 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,5 @@ +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= diff --git a/picture.go b/picture.go index 3cfcbf573d..01c2ae2e9b 100644 --- a/picture.go +++ b/picture.go @@ -51,7 +51,7 @@ func parseFormatPictureSet(formatSet string) (*formatPicture, error) { // _ "image/jpeg" // _ "image/png" // -// "github.com/360EntSecGroup-Skylar/excelize" +// "github.com/360EntSecGroup-Skylar/excelize/v2" // ) // // func main() { @@ -111,7 +111,7 @@ func (f *File) AddPicture(sheet, cell, picture, format string) error { // _ "image/jpeg" // "io/ioutil" // -// "github.com/360EntSecGroup-Skylar/excelize" +// "github.com/360EntSecGroup-Skylar/excelize/v2" // ) // // func main() { diff --git a/sheet_test.go b/sheet_test.go index 7db982ae31..f0a1963e43 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -4,7 +4,7 @@ import ( "fmt" "testing" - "github.com/360EntSecGroup-Skylar/excelize" + "github.com/360EntSecGroup-Skylar/excelize/v2" "github.com/mohae/deepcopy" "github.com/stretchr/testify/assert" ) diff --git a/sheetpr_test.go b/sheetpr_test.go index 48d330e498..97a314c918 100644 --- a/sheetpr_test.go +++ b/sheetpr_test.go @@ -7,7 +7,7 @@ import ( "github.com/mohae/deepcopy" "github.com/stretchr/testify/assert" - "github.com/360EntSecGroup-Skylar/excelize" + "github.com/360EntSecGroup-Skylar/excelize/v2" ) var _ = []excelize.SheetPrOption{ @@ -29,10 +29,10 @@ var _ = []excelize.SheetPrOptionPtr{ } func ExampleFile_SetSheetPrOptions() { - xl := excelize.NewFile() + f := excelize.NewFile() const sheet = "Sheet1" - if err := xl.SetSheetPrOptions(sheet, + if err := f.SetSheetPrOptions(sheet, excelize.CodeName("code"), excelize.EnableFormatConditionsCalculation(false), excelize.Published(false), @@ -46,7 +46,7 @@ func ExampleFile_SetSheetPrOptions() { } func ExampleFile_GetSheetPrOptions() { - xl := excelize.NewFile() + f := excelize.NewFile() const sheet = "Sheet1" var ( @@ -58,7 +58,7 @@ func ExampleFile_GetSheetPrOptions() { outlineSummaryBelow excelize.OutlineSummaryBelow ) - if err := xl.GetSheetPrOptions(sheet, + if err := f.GetSheetPrOptions(sheet, &codeName, &enableFormatConditionsCalculation, &published, @@ -110,26 +110,26 @@ func TestSheetPrOptions(t *testing.T) { val1 := deepcopy.Copy(def).(excelize.SheetPrOptionPtr) val2 := deepcopy.Copy(def).(excelize.SheetPrOptionPtr) - xl := excelize.NewFile() + f := excelize.NewFile() // Get the default value - assert.NoError(t, xl.GetSheetPrOptions(sheet, def), opt) + assert.NoError(t, f.GetSheetPrOptions(sheet, def), opt) // Get again and check - assert.NoError(t, xl.GetSheetPrOptions(sheet, val1), opt) + assert.NoError(t, f.GetSheetPrOptions(sheet, val1), opt) if !assert.Equal(t, val1, def, opt) { t.FailNow() } // Set the same value - assert.NoError(t, xl.SetSheetPrOptions(sheet, val1), opt) + assert.NoError(t, f.SetSheetPrOptions(sheet, val1), opt) // Get again and check - assert.NoError(t, xl.GetSheetPrOptions(sheet, val1), opt) + assert.NoError(t, f.GetSheetPrOptions(sheet, val1), opt) if !assert.Equal(t, val1, def, "%T: value should not have changed", opt) { t.FailNow() } // Set a different value - assert.NoError(t, xl.SetSheetPrOptions(sheet, test.nonDefault), opt) - assert.NoError(t, xl.GetSheetPrOptions(sheet, val1), opt) + assert.NoError(t, f.SetSheetPrOptions(sheet, test.nonDefault), opt) + assert.NoError(t, f.GetSheetPrOptions(sheet, val1), opt) // Get again and compare - assert.NoError(t, xl.GetSheetPrOptions(sheet, val2), opt) + assert.NoError(t, f.GetSheetPrOptions(sheet, val2), opt) if !assert.Equal(t, val1, val2, "%T: value should not have changed", opt) { t.FailNow() } @@ -138,8 +138,8 @@ func TestSheetPrOptions(t *testing.T) { t.FailNow() } // Restore the default value - assert.NoError(t, xl.SetSheetPrOptions(sheet, def), opt) - assert.NoError(t, xl.GetSheetPrOptions(sheet, val1), opt) + assert.NoError(t, f.SetSheetPrOptions(sheet, def), opt) + assert.NoError(t, f.GetSheetPrOptions(sheet, val1), opt) if !assert.Equal(t, def, val1) { t.FailNow() } diff --git a/sheetview_test.go b/sheetview_test.go index b565a12221..2e697b8540 100644 --- a/sheetview_test.go +++ b/sheetview_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/assert" - "github.com/360EntSecGroup-Skylar/excelize" + "github.com/360EntSecGroup-Skylar/excelize/v2" ) var _ = []excelize.SheetViewOption{ @@ -35,10 +35,10 @@ var _ = []excelize.SheetViewOptionPtr{ } func ExampleFile_SetSheetViewOptions() { - xl := excelize.NewFile() + f := excelize.NewFile() const sheet = "Sheet1" - if err := xl.SetSheetViewOptions(sheet, 0, + if err := f.SetSheetViewOptions(sheet, 0, excelize.DefaultGridColor(false), excelize.RightToLeft(false), excelize.ShowFormulas(true), @@ -54,22 +54,22 @@ func ExampleFile_SetSheetViewOptions() { fmt.Println("Default:") fmt.Println("- zoomScale: 80") - if err := xl.SetSheetViewOptions(sheet, 0, excelize.ZoomScale(500)); err != nil { + if err := f.SetSheetViewOptions(sheet, 0, excelize.ZoomScale(500)); err != nil { panic(err) } - if err := xl.GetSheetViewOptions(sheet, 0, &zoomScale); err != nil { + if err := f.GetSheetViewOptions(sheet, 0, &zoomScale); err != nil { panic(err) } fmt.Println("Used out of range value:") fmt.Println("- zoomScale:", zoomScale) - if err := xl.SetSheetViewOptions(sheet, 0, excelize.ZoomScale(123)); err != nil { + if err := f.SetSheetViewOptions(sheet, 0, excelize.ZoomScale(123)); err != nil { panic(err) } - if err := xl.GetSheetViewOptions(sheet, 0, &zoomScale); err != nil { + if err := f.GetSheetViewOptions(sheet, 0, &zoomScale); err != nil { panic(err) } @@ -87,7 +87,7 @@ func ExampleFile_SetSheetViewOptions() { } func ExampleFile_GetSheetViewOptions() { - xl := excelize.NewFile() + f := excelize.NewFile() const sheet = "Sheet1" var ( @@ -100,7 +100,7 @@ func ExampleFile_GetSheetViewOptions() { topLeftCell excelize.TopLeftCell ) - if err := xl.GetSheetViewOptions(sheet, 0, + if err := f.GetSheetViewOptions(sheet, 0, &defaultGridColor, &rightToLeft, &showFormulas, @@ -121,19 +121,19 @@ func ExampleFile_GetSheetViewOptions() { fmt.Println("- zoomScale:", zoomScale) fmt.Println("- topLeftCell:", `"`+topLeftCell+`"`) - if err := xl.SetSheetViewOptions(sheet, 0, excelize.TopLeftCell("B2")); err != nil { + if err := f.SetSheetViewOptions(sheet, 0, excelize.TopLeftCell("B2")); err != nil { panic(err) } - if err := xl.GetSheetViewOptions(sheet, 0, &topLeftCell); err != nil { + if err := f.GetSheetViewOptions(sheet, 0, &topLeftCell); err != nil { panic(err) } - if err := xl.SetSheetViewOptions(sheet, 0, excelize.ShowGridLines(false)); err != nil { + if err := f.SetSheetViewOptions(sheet, 0, excelize.ShowGridLines(false)); err != nil { panic(err) } - if err := xl.GetSheetViewOptions(sheet, 0, &showGridLines); err != nil { + if err := f.GetSheetViewOptions(sheet, 0, &showGridLines); err != nil { panic(err) } @@ -156,15 +156,15 @@ func ExampleFile_GetSheetViewOptions() { } func TestSheetViewOptionsErrors(t *testing.T) { - xl := excelize.NewFile() + f := excelize.NewFile() const sheet = "Sheet1" - assert.NoError(t, xl.GetSheetViewOptions(sheet, 0)) - assert.NoError(t, xl.GetSheetViewOptions(sheet, -1)) - assert.Error(t, xl.GetSheetViewOptions(sheet, 1)) - assert.Error(t, xl.GetSheetViewOptions(sheet, -2)) - assert.NoError(t, xl.SetSheetViewOptions(sheet, 0)) - assert.NoError(t, xl.SetSheetViewOptions(sheet, -1)) - assert.Error(t, xl.SetSheetViewOptions(sheet, 1)) - assert.Error(t, xl.SetSheetViewOptions(sheet, -2)) + 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)) } From 63e97ffc9aae35780cdbd69ad966fb101fc5217f Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 3 May 2019 02:11:53 +0800 Subject: [PATCH 090/957] Remove Go 1.8 test in TravisCI --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 6c061a87a4..9f892c506f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,6 @@ install: - go get -d -t -v ./... && go build -v ./... go: - - 1.8.x - 1.9.x - 1.10.x - 1.11.x From 72701e89c7145f9d08a79c93040e232b2875c855 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 4 May 2019 00:10:11 +0800 Subject: [PATCH 091/957] Fix structs fields definition errors and keep double quotes in data validation formula --- datavalidation.go | 8 +++--- xmlWorksheet.go | 68 ++++++++++++++++++++++++++++++----------------- 2 files changed, 47 insertions(+), 29 deletions(-) diff --git a/datavalidation.go b/datavalidation.go index 8fb9623b26..56b96fd6db 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -112,7 +112,7 @@ func (dd *DataValidation) SetDropList(keys []string) error { if dataValidationFormulaStrLen < len(formula) { return fmt.Errorf(dataValidationFormulaStrLenErr) } - dd.Formula1 = formula + dd.Formula1 = fmt.Sprintf("%s", formula) dd.Type = convDataValidationType(typeList) return nil } @@ -121,12 +121,12 @@ func (dd *DataValidation) SetDropList(keys []string) error { func (dd *DataValidation) SetRange(f1, f2 int, t DataValidationType, o DataValidationOperator) error { formula1 := fmt.Sprintf("%d", f1) formula2 := fmt.Sprintf("%d", f2) - if dataValidationFormulaStrLen < len(dd.Formula1) || dataValidationFormulaStrLen < len(dd.Formula2) { + if dataValidationFormulaStrLen+21 < len(dd.Formula1) || dataValidationFormulaStrLen+21 < len(dd.Formula2) { return fmt.Errorf(dataValidationFormulaStrLenErr) } - dd.Formula1 = formula1 - dd.Formula2 = formula2 + dd.Formula1 = fmt.Sprintf("%s", formula1) + dd.Formula2 = fmt.Sprintf("%s", formula2) dd.Type = convDataValidationType(t) dd.Operator = convDataValidationOperatior(o) return nil diff --git a/xmlWorksheet.go b/xmlWorksheet.go index f3323cbb71..f2eb47abb5 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -53,23 +53,27 @@ 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 { - DifferentFirst bool `xml:"differentFirst,attr,omitempty"` - DifferentOddEven bool `xml:"differentOddEven,attr,omitempty"` - OddHeader []*xlsxOddHeader `xml:"oddHeader"` - OddFooter []*xlsxOddFooter `xml:"oddFooter"` -} - -// xlsxOddHeader directly maps the oddHeader element in the namespace -// http://schemas.openxmlformats.org/spreadsheetml/2006/main - currently I have -// not checked it for completeness - it does as much as I need. -type xlsxOddHeader struct { - Content string `xml:",chardata"` -} - -// xlsxOddFooter directly maps the oddFooter element in the namespace -// http://schemas.openxmlformats.org/spreadsheetml/2006/main - currently I have -// not checked it for completeness - it does as much as I need. -type xlsxOddFooter struct { + 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"` +} + +// xlsxDrawingHF (Drawing Reference in Header Footer) specifies the usage of +// drawing objects to be rendered in the headers and footers of the sheet. It +// specifies an explicit relationship to the part containing the DrawingML +// shapes used in the headers and footers. It also indicates where in the +// headers and footers each shape belongs. One drawing object can appear in +// each of the left section, center section and right section of a header and +// a footer. +type xlsxDrawingHF struct { Content string `xml:",chardata"` } @@ -324,16 +328,16 @@ type DataValidation struct { Error *string `xml:"error,attr"` ErrorStyle *string `xml:"errorStyle,attr"` ErrorTitle *string `xml:"errorTitle,attr"` - Operator string `xml:"operator,attr"` + Operator string `xml:"operator,attr,omitempty"` Prompt *string `xml:"prompt,attr"` - PromptTitle *string `xml:"promptTitle"` - ShowDropDown bool `xml:"showDropDown,attr"` - ShowErrorMessage bool `xml:"showErrorMessage,attr"` - ShowInputMessage bool `xml:"showInputMessage,attr"` + PromptTitle *string `xml:"promptTitle,attr"` + ShowDropDown bool `xml:"showDropDown,attr,omitempty"` + ShowErrorMessage bool `xml:"showErrorMessage,attr,omitempty"` + ShowInputMessage bool `xml:"showInputMessage,attr,omitempty"` Sqref string `xml:"sqref,attr"` Type string `xml:"type,attr"` - Formula1 string `xml:"formula1"` - Formula2 string `xml:"formula2"` + Formula1 string `xml:",innerxml"` + Formula2 string `xml:",innerxml"` } // xlsxC directly maps the c element in the namespace @@ -482,7 +486,7 @@ type xlsxIconSet struct { type xlsxCfvo struct { Gte bool `xml:"gte,attr,omitempty"` Type string `xml:"type,attr,omitempty"` - Val string `xml:"val,attr"` + Val string `xml:"val,attr,omitempty"` ExtLst *xlsxExtLst `xml:"extLst"` } @@ -627,3 +631,17 @@ type FormatSheetProtection struct { 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 + FirstFooter string + FirstHeader string +} From 69b38ddcd60f7cf4c158c706ddbbeb89a8ff2108 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 5 May 2019 16:25:57 +0800 Subject: [PATCH 092/957] Resolve #394, init set header and footer support --- datavalidation_test.go | 30 ++++---- excelize_test.go | 8 +-- sheet.go | 155 ++++++++++++++++++++++++++++++++++++++++- sheet_test.go | 57 ++++++++++----- 4 files changed, 213 insertions(+), 37 deletions(-) diff --git a/datavalidation_test.go b/datavalidation_test.go index afb659c2e8..0fee092001 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -19,7 +19,7 @@ import ( func TestDataValidation(t *testing.T) { resultFile := filepath.Join("test", "TestDataValidation.xlsx") - xlsx := NewFile() + f := NewFile() dvRange := NewDataValidation(true) dvRange.Sqref = "A1:B2" @@ -27,8 +27,8 @@ func TestDataValidation(t *testing.T) { dvRange.SetError(DataValidationErrorStyleStop, "error title", "error body") dvRange.SetError(DataValidationErrorStyleWarning, "error title", "error body") dvRange.SetError(DataValidationErrorStyleInformation, "error title", "error body") - xlsx.AddDataValidation("Sheet1", dvRange) - if !assert.NoError(t, xlsx.SaveAs(resultFile)) { + f.AddDataValidation("Sheet1", dvRange) + if !assert.NoError(t, f.SaveAs(resultFile)) { t.FailNow() } @@ -36,16 +36,16 @@ func TestDataValidation(t *testing.T) { dvRange.Sqref = "A3:B4" dvRange.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorGreaterThan) dvRange.SetInput("input title", "input body") - xlsx.AddDataValidation("Sheet1", dvRange) - if !assert.NoError(t, xlsx.SaveAs(resultFile)) { + f.AddDataValidation("Sheet1", dvRange) + if !assert.NoError(t, f.SaveAs(resultFile)) { t.FailNow() } dvRange = NewDataValidation(true) dvRange.Sqref = "A5:B6" dvRange.SetDropList([]string{"1", "2", "3"}) - xlsx.AddDataValidation("Sheet1", dvRange) - if !assert.NoError(t, xlsx.SaveAs(resultFile)) { + f.AddDataValidation("Sheet1", dvRange) + if !assert.NoError(t, f.SaveAs(resultFile)) { t.FailNow() } } @@ -53,10 +53,10 @@ func TestDataValidation(t *testing.T) { func TestDataValidationError(t *testing.T) { resultFile := filepath.Join("test", "TestDataValidationError.xlsx") - xlsx := NewFile() - xlsx.SetCellStr("Sheet1", "E1", "E1") - xlsx.SetCellStr("Sheet1", "E2", "E2") - xlsx.SetCellStr("Sheet1", "E3", "E3") + f := NewFile() + f.SetCellStr("Sheet1", "E1", "E1") + f.SetCellStr("Sheet1", "E2", "E2") + f.SetCellStr("Sheet1", "E3", "E3") dvRange := NewDataValidation(true) dvRange.SetSqref("A7:B8") @@ -66,8 +66,8 @@ func TestDataValidationError(t *testing.T) { err := dvRange.SetSqrefDropList("$E$1:$E$3", false) assert.EqualError(t, err, "cross-sheet sqref cell are not supported") - xlsx.AddDataValidation("Sheet1", dvRange) - if !assert.NoError(t, xlsx.SaveAs(resultFile)) { + f.AddDataValidation("Sheet1", dvRange) + if !assert.NoError(t, f.SaveAs(resultFile)) { t.FailNow() } @@ -81,8 +81,8 @@ func TestDataValidationError(t *testing.T) { dvRange.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorGreaterThan) dvRange.SetSqref("A9:B10") - xlsx.AddDataValidation("Sheet1", dvRange) - if !assert.NoError(t, xlsx.SaveAs(resultFile)) { + f.AddDataValidation("Sheet1", dvRange) + if !assert.NoError(t, f.SaveAs(resultFile)) { t.FailNow() } } diff --git a/excelize_test.go b/excelize_test.go index c76aa92b06..3509cb8091 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -789,9 +789,9 @@ func TestCopySheet(t *testing.T) { } idx := f.NewSheet("CopySheet") - assert.EqualError(t, f.CopySheet(1, idx), "sheet sheet1 is not exist") + assert.NoError(t, f.CopySheet(1, idx)) - f.SetCellValue("Sheet4", "F1", "Hello") + f.SetCellValue("CopySheet", "F1", "Hello") val, err := f.GetCellValue("Sheet1", "F1") assert.NoError(t, err) assert.NotEqual(t, "Hello", val) @@ -805,8 +805,8 @@ func TestCopySheetError(t *testing.T) { t.FailNow() } - err = f.CopySheet(0, -1) - if !assert.EqualError(t, err, "invalid worksheet index") { + assert.EqualError(t, f.copySheet(0, -1), "sheet is not exist") + if !assert.EqualError(t, f.CopySheet(0, -1), "invalid worksheet index") { t.FailNow() } diff --git a/sheet.go b/sheet.go index 5e9fcae952..b22592db32 100644 --- a/sheet.go +++ b/sheet.go @@ -14,9 +14,11 @@ import ( "encoding/json" "encoding/xml" "errors" + "fmt" "io/ioutil" "os" "path" + "reflect" "regexp" "strconv" "strings" @@ -466,7 +468,7 @@ func (f *File) CopySheet(from, to int) error { // copySheet provides a function to duplicate a worksheet by gave source and // target worksheet name. func (f *File) copySheet(from, to int) error { - sheet, err := f.workSheetReader("sheet" + strconv.Itoa(from)) + sheet, err := f.workSheetReader(f.GetSheetName(from)) if err != nil { return err } @@ -761,6 +763,155 @@ func (f *File) SearchSheet(sheet, value string, reg ...bool) ([]string, error) { return result, nil } +// SetHeaderFooter provides a function to set headers and footers by given +// worksheet name and the control characters. +// +// 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 +// +// 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 +// +// 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`, +// }) +// +// This example shows: +// +// - The first page has its own header and footer +// +// - Odd and even-numbered pages have different headers and footers +// +// - Current page number in the right section of odd-page headers +// +// - Current workbook's file name in the center section of odd-page footers +// +// - Current page number in the left section of even-page headers +// +// - Current date in the left section and the current time in the right section +// of even-page footers +// +// - The text "Center Bold Header" on the first line of the center section of +// the first page, and the date on the second line of the center section of +// that same page +// +// - No footer on the first page +// +func (f *File) SetHeaderFooter(sheet string, settings *FormatHeaderFooter) error { + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return err + } + if settings == nil { + xlsx.HeaderFooter = nil + return err + } + + v := reflect.ValueOf(*settings) + // Check 6 string type fields: OddHeader, OddFooter, EvenHeader, EvenFooter, + // FirstFooter, FirstHeader + for i := 4; i < v.NumField()-1; i++ { + if v.Field(i).Len() >= 255 { + return fmt.Errorf("field %s must be less than 255 characters", v.Type().Field(i).Name) + } + } + xlsx.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, + } + return err +} + // ProtectSheet provides a function to prevent other users from accidentally // or deliberately changing, moving, or deleting data in a worksheet. For // example, protect Sheet1 with protection settings: @@ -898,7 +1049,7 @@ func (p *PageLayoutPaperSize) getPageLayout(ps *xlsxPageSetUp) { // // Available options: // PageLayoutOrientation(string) -// PageLayoutPaperSize(int) +// PageLayoutPaperSize(int) // // The following shows the paper size sorted by excelize index number: // diff --git a/sheet_test.go b/sheet_test.go index f0a1963e43..beee10bbae 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -2,6 +2,8 @@ package excelize_test import ( "fmt" + "path/filepath" + "strings" "testing" "github.com/360EntSecGroup-Skylar/excelize/v2" @@ -10,15 +12,15 @@ import ( ) func ExampleFile_SetPageLayout() { - xl := excelize.NewFile() + f := excelize.NewFile() - if err := xl.SetPageLayout( + if err := f.SetPageLayout( "Sheet1", excelize.PageLayoutOrientation(excelize.OrientationLandscape), ); err != nil { panic(err) } - if err := xl.SetPageLayout( + if err := f.SetPageLayout( "Sheet1", excelize.PageLayoutPaperSize(10), ); err != nil { @@ -28,15 +30,15 @@ func ExampleFile_SetPageLayout() { } func ExampleFile_GetPageLayout() { - xl := excelize.NewFile() + f := excelize.NewFile() var ( orientation excelize.PageLayoutOrientation paperSize excelize.PageLayoutPaperSize ) - if err := xl.GetPageLayout("Sheet1", &orientation); err != nil { + if err := f.GetPageLayout("Sheet1", &orientation); err != nil { panic(err) } - if err := xl.GetPageLayout("Sheet1", &paperSize); err != nil { + if err := f.GetPageLayout("Sheet1", &paperSize); err != nil { panic(err) } fmt.Println("Defaults:") @@ -69,26 +71,26 @@ func TestPageLayoutOption(t *testing.T) { val1 := deepcopy.Copy(def).(excelize.PageLayoutOptionPtr) val2 := deepcopy.Copy(def).(excelize.PageLayoutOptionPtr) - xl := excelize.NewFile() + f := excelize.NewFile() // Get the default value - assert.NoError(t, xl.GetPageLayout(sheet, def), opt) + assert.NoError(t, f.GetPageLayout(sheet, def), opt) // Get again and check - assert.NoError(t, xl.GetPageLayout(sheet, val1), opt) + assert.NoError(t, f.GetPageLayout(sheet, val1), opt) if !assert.Equal(t, val1, def, opt) { t.FailNow() } // Set the same value - assert.NoError(t, xl.SetPageLayout(sheet, val1), opt) + assert.NoError(t, f.SetPageLayout(sheet, val1), opt) // Get again and check - assert.NoError(t, xl.GetPageLayout(sheet, val1), opt) + assert.NoError(t, f.GetPageLayout(sheet, val1), opt) if !assert.Equal(t, val1, def, "%T: value should not have changed", opt) { t.FailNow() } // Set a different value - assert.NoError(t, xl.SetPageLayout(sheet, test.nonDefault), opt) - assert.NoError(t, xl.GetPageLayout(sheet, val1), opt) + assert.NoError(t, f.SetPageLayout(sheet, test.nonDefault), opt) + assert.NoError(t, f.GetPageLayout(sheet, val1), opt) // Get again and compare - assert.NoError(t, xl.GetPageLayout(sheet, val2), opt) + assert.NoError(t, f.GetPageLayout(sheet, val2), opt) if !assert.Equal(t, val1, val2, "%T: value should not have changed", opt) { t.FailNow() } @@ -97,8 +99,8 @@ func TestPageLayoutOption(t *testing.T) { t.FailNow() } // Restore the default value - assert.NoError(t, xl.SetPageLayout(sheet, def), opt) - assert.NoError(t, xl.GetPageLayout(sheet, val1), opt) + assert.NoError(t, f.SetPageLayout(sheet, def), opt) + assert.NoError(t, f.GetPageLayout(sheet, val1), opt) if !assert.Equal(t, def, val1) { t.FailNow() } @@ -117,3 +119,26 @@ func TestGetPageLayout(t *testing.T) { // Test get page layout on not exists worksheet. assert.EqualError(t, f.GetPageLayout("SheetN"), "sheet SheetN is not exist") } + +func TestSetHeaderFooter(t *testing.T) { + f := excelize.NewFile() + 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") + // Test set header and footer with illegal setting. + assert.EqualError(t, f.SetHeaderFooter("Sheet1", &excelize.FormatHeaderFooter{ + OddHeader: strings.Repeat("c", 256), + }), "field OddHeader must be less than 255 characters") + + assert.NoError(t, f.SetHeaderFooter("Sheet1", nil)) + assert.NoError(t, 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`, + })) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetHeaderFooter.xlsx"))) +} From 25763ba3e1af39bf2fd00bfa6aabcb054ca78327 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 11 May 2019 09:46:20 +0800 Subject: [PATCH 093/957] fixed #373, comments duplicate caused by inner counting errors --- comment.go | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/comment.go b/comment.go index 79f6feccd8..3cf0c1d744 100644 --- a/comment.go +++ b/comment.go @@ -277,13 +277,21 @@ func (f *File) addComment(commentsXML, cell string, formatSet *formatComment) { // countComments provides a function to get comments files count storage in // the folder xl. func (f *File) countComments() int { - count := 0 + c1, c2 := 0, 0 for k := range f.XLSX { if strings.Contains(k, "xl/comments") { - count++ + c1++ } } - return count + for rel := range f.Comments { + if strings.Contains(rel, "xl/comments") { + c2++ + } + } + if c1 < c2 { + return c2 + } + return c1 } // decodeVMLDrawingReader provides a function to get the pointer to the From 7e77e14814658486267e3f237f484fa8e63a0cd4 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 16 May 2019 13:36:50 +0800 Subject: [PATCH 094/957] Resolve #397, support set style by columns --- col.go | 62 ++++++++++++++++- col_test.go | 176 +++++++++++++++++++++++++++++++++++++++++++++++ excelize_test.go | 157 ++---------------------------------------- 3 files changed, 242 insertions(+), 153 deletions(-) create mode 100644 col_test.go diff --git a/col.go b/col.go index 6b73fdc22a..db3a901d32 100644 --- a/col.go +++ b/col.go @@ -9,7 +9,10 @@ package excelize -import "math" +import ( + "math" + "strings" +) // Define the default cell size and EMU unit of measurement. const ( @@ -155,6 +158,63 @@ func (f *File) SetColOutlineLevel(sheet, col string, level uint8) error { return err } +// SetColStyle provides a function to set style of columns by given worksheet +// name, columns range and style ID. +// +// For example set style of column H on Sheet1: +// +// err = f.SetColStyle("Sheet1", "H", style) +// +// Set style of columns C:F on Sheet1: +// +// err = f.SetColStyle("Sheet1", "C:F", style) +// +func (f *File) SetColStyle(sheet, columns string, styleID int) error { + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return err + } + var c1, c2 string + var min, max int + cols := strings.Split(columns, ":") + c1 = cols[0] + min, err = ColumnNameToNumber(c1) + if err != nil { + return err + } + if len(cols) == 2 { + c2 = cols[1] + max, err = ColumnNameToNumber(c2) + if err != nil { + return err + } + } else { + max = min + } + if max < min { + min, max = max, min + } + if xlsx.Cols == nil { + xlsx.Cols = &xlsxCols{} + } + var find bool + for idx, col := range xlsx.Cols.Col { + if col.Min == min && col.Max == max { + xlsx.Cols.Col[idx].Style = styleID + find = true + } + } + if !find { + xlsx.Cols.Col = append(xlsx.Cols.Col, xlsxCol{ + Min: min, + Max: max, + Width: 9, + Style: styleID, + }) + } + return nil +} + // SetColWidth provides a function to set the width of a single column or // multiple columns. For example: // diff --git a/col_test.go b/col_test.go new file mode 100644 index 0000000000..e3164d49db --- /dev/null +++ b/col_test.go @@ -0,0 +1,176 @@ +package excelize + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestColumnVisibility(t *testing.T) { + t.Run("TestBook1", func(t *testing.T) { + f, err := prepareTestBook1() + if !assert.NoError(t, err) { + t.FailNow() + } + + assert.NoError(t, f.SetColVisible("Sheet1", "F", false)) + assert.NoError(t, f.SetColVisible("Sheet1", "F", true)) + visible, err := f.GetColVisible("Sheet1", "F") + assert.Equal(t, true, visible) + assert.NoError(t, err) + + // Test get column visiable on not exists worksheet. + _, err = f.GetColVisible("SheetN", "F") + assert.EqualError(t, err, "sheet SheetN is not exist") + + // Test get column visiable with illegal cell coordinates. + _, err = f.GetColVisible("Sheet1", "*") + assert.EqualError(t, err, `invalid column name "*"`) + assert.EqualError(t, f.SetColVisible("Sheet1", "*", false), `invalid column name "*"`) + + f.NewSheet("Sheet3") + assert.NoError(t, f.SetColVisible("Sheet3", "E", false)) + + assert.EqualError(t, f.SetColVisible("SheetN", "E", false), "sheet SheetN is not exist") + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestColumnVisibility.xlsx"))) + }) + + t.Run("TestBook3", func(t *testing.T) { + f, err := prepareTestBook3() + if !assert.NoError(t, err) { + t.FailNow() + } + f.GetColVisible("Sheet1", "B") + }) +} + +func TestOutlineLevel(t *testing.T) { + f := NewFile() + f.GetColOutlineLevel("Sheet1", "D") + f.NewSheet("Sheet2") + f.SetColOutlineLevel("Sheet1", "D", 4) + f.GetColOutlineLevel("Sheet1", "D") + f.GetColOutlineLevel("Shee2", "A") + f.SetColWidth("Sheet2", "A", "D", 13) + f.SetColOutlineLevel("Sheet2", "B", 2) + f.SetRowOutlineLevel("Sheet1", 2, 250) + + // Test set and get column outline level with illegal cell coordinates. + assert.EqualError(t, f.SetColOutlineLevel("Sheet1", "*", 1), `invalid column name "*"`) + _, err := f.GetColOutlineLevel("Sheet1", "*") + assert.EqualError(t, err, `invalid column name "*"`) + + // 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.SetRowOutlineLevel("Sheet1", 0, 1), "invalid row number 0") + level, err := f.GetRowOutlineLevel("Sheet1", 2) + assert.NoError(t, err) + assert.Equal(t, uint8(250), level) + + _, err = f.GetRowOutlineLevel("Sheet1", 0) + assert.EqualError(t, err, `invalid row number 0`) + + level, err = f.GetRowOutlineLevel("Sheet1", 10) + assert.NoError(t, err) + assert.Equal(t, uint8(0), level) + + err = f.SaveAs(filepath.Join("test", "TestOutlineLevel.xlsx")) + if !assert.NoError(t, err) { + t.FailNow() + } + + f, err = OpenFile(filepath.Join("test", "Book1.xlsx")) + if !assert.NoError(t, err) { + t.FailNow() + } + f.SetColOutlineLevel("Sheet2", "B", 2) +} + +func TestSetColStyle(t *testing.T) { + f := NewFile() + style, 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", style), "sheet SheetN is not exist") + // Test set column style with illegal cell coordinates. + assert.EqualError(t, f.SetColStyle("Sheet1", "*", style), `invalid column name "*"`) + assert.EqualError(t, f.SetColStyle("Sheet1", "A:*", style), `invalid column name "*"`) + + assert.NoError(t, f.SetColStyle("Sheet1", "B", style)) + // Test set column style with already exists column with style. + assert.NoError(t, f.SetColStyle("Sheet1", "B", style)) + assert.NoError(t, f.SetColStyle("Sheet1", "D:C", style)) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetColStyle.xlsx"))) +} + +func TestColWidth(t *testing.T) { + f := NewFile() + f.SetColWidth("Sheet1", "B", "A", 12) + f.SetColWidth("Sheet1", "A", "B", 12) + f.GetColWidth("Sheet1", "A") + f.GetColWidth("Sheet1", "C") + + // Test set and get column width with illegal cell coordinates. + _, err := f.GetColWidth("Sheet1", "*") + assert.EqualError(t, err, `invalid column name "*"`) + assert.EqualError(t, f.SetColWidth("Sheet1", "*", "B", 1), `invalid column name "*"`) + assert.EqualError(t, f.SetColWidth("Sheet1", "A", "*", 1), `invalid column name "*"`) + + // Test set column width on not exists worksheet. + assert.EqualError(t, f.SetColWidth("SheetN", "B", "A", 12), "sheet SheetN is 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.NoError(t, f.SaveAs(filepath.Join("test", "TestColWidth.xlsx"))) + convertRowHeightToPixels(0) +} + +func TestInsertCol(t *testing.T) { + f := NewFile() + sheet1 := f.GetSheetName(1) + + fillCells(f, sheet1, 10, 10) + + f.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External") + f.MergeCell(sheet1, "A1", "C3") + + err := f.AutoFilter(sheet1, "A2", "B2", `{"column":"B","expression":"x != blanks"}`) + if !assert.NoError(t, err) { + t.FailNow() + } + + assert.NoError(t, f.InsertCol(sheet1, "A")) + + // Test insert column with illegal cell coordinates. + assert.EqualError(t, f.InsertCol("Sheet1", "*"), `invalid column name "*"`) + + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestInsertCol.xlsx"))) +} + +func TestRemoveCol(t *testing.T) { + f := NewFile() + sheet1 := f.GetSheetName(1) + + fillCells(f, sheet1, 10, 15) + + f.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External") + f.SetCellHyperLink(sheet1, "C5", "https://github.com", "External") + + f.MergeCell(sheet1, "A1", "B1") + f.MergeCell(sheet1, "A2", "B2") + + assert.NoError(t, f.RemoveCol(sheet1, "A")) + assert.NoError(t, f.RemoveCol(sheet1, "A")) + + // Test remove column with illegal cell coordinates. + assert.EqualError(t, f.RemoveCol("Sheet1", "*"), `invalid column name "*"`) + + // Test remove column on not exists worksheet. + assert.EqualError(t, f.RemoveCol("SheetN", "B"), "sheet SheetN is not exist") + + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestRemoveCol.xlsx"))) +} diff --git a/excelize_test.go b/excelize_test.go index 3509cb8091..85df09b92f 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -244,30 +244,6 @@ func TestNewFile(t *testing.T) { assert.NoError(t, f.SaveAs(filepath.Join("test", "TestNewFile.xlsx"))) } -func TestColWidth(t *testing.T) { - xlsx := NewFile() - xlsx.SetColWidth("Sheet1", "B", "A", 12) - xlsx.SetColWidth("Sheet1", "A", "B", 12) - xlsx.GetColWidth("Sheet1", "A") - xlsx.GetColWidth("Sheet1", "C") - - // Test set and get column width with illegal cell coordinates. - _, err := xlsx.GetColWidth("Sheet1", "*") - assert.EqualError(t, err, `invalid column name "*"`) - assert.EqualError(t, xlsx.SetColWidth("Sheet1", "*", "B", 1), `invalid column name "*"`) - assert.EqualError(t, xlsx.SetColWidth("Sheet1", "A", "*", 1), `invalid column name "*"`) - - // Test get column width on not exists worksheet. - _, err = xlsx.GetColWidth("SheetN", "A") - assert.EqualError(t, err, "sheet SheetN is not exist") - - err = xlsx.SaveAs(filepath.Join("test", "TestColWidth.xlsx")) - if err != nil { - t.Error(err) - } - convertRowHeightToPixels(0) -} - func TestAddDrawingVML(t *testing.T) { // Test addDrawingVML with illegal cell coordinates. f := NewFile() @@ -744,44 +720,6 @@ func TestSheetVisibility(t *testing.T) { assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSheetVisibility.xlsx"))) } -func TestColumnVisibility(t *testing.T) { - t.Run("TestBook1", func(t *testing.T) { - f, err := prepareTestBook1() - if !assert.NoError(t, err) { - t.FailNow() - } - - assert.NoError(t, f.SetColVisible("Sheet1", "F", false)) - assert.NoError(t, f.SetColVisible("Sheet1", "F", true)) - visible, err := f.GetColVisible("Sheet1", "F") - assert.Equal(t, true, visible) - assert.NoError(t, err) - - // Test get column visiable on not exists worksheet. - _, err = f.GetColVisible("SheetN", "F") - assert.EqualError(t, err, "sheet SheetN is not exist") - - // Test get column visiable with illegal cell coordinates. - _, err = f.GetColVisible("Sheet1", "*") - assert.EqualError(t, err, `invalid column name "*"`) - assert.EqualError(t, f.SetColVisible("Sheet1", "*", false), `invalid column name "*"`) - - f.NewSheet("Sheet3") - assert.NoError(t, f.SetColVisible("Sheet3", "E", false)) - - assert.EqualError(t, f.SetColVisible("SheetN", "E", false), "sheet SheetN is not exist") - assert.NoError(t, f.SaveAs(filepath.Join("test", "TestColumnVisibility.xlsx"))) - }) - - t.Run("TestBook3", func(t *testing.T) { - f, err := prepareTestBook3() - if !assert.NoError(t, err) { - t.FailNow() - } - f.GetColVisible("Sheet1", "B") - }) -} - func TestCopySheet(t *testing.T) { f, err := prepareTestBook1() if !assert.NoError(t, err) { @@ -870,8 +808,11 @@ func TestAddComments(t *testing.T) { } s := strings.Repeat("c", 32768) - f.AddComment("Sheet1", "A30", `{"author":"`+s+`","text":"`+s+`"}`) - f.AddComment("Sheet2", "B7", `{"author":"Excelize: ","text":"This is a comment."}`) + 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."}`)) + + // 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") if assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddComments.xlsx"))) { assert.Len(t, f.GetComments(), 2) @@ -990,52 +931,6 @@ func TestAddChart(t *testing.T) { assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddChart.xlsx"))) } -func TestInsertCol(t *testing.T) { - f := NewFile() - sheet1 := f.GetSheetName(1) - - fillCells(f, sheet1, 10, 10) - - f.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External") - f.MergeCell(sheet1, "A1", "C3") - - err := f.AutoFilter(sheet1, "A2", "B2", `{"column":"B","expression":"x != blanks"}`) - if !assert.NoError(t, err) { - t.FailNow() - } - - assert.NoError(t, f.InsertCol(sheet1, "A")) - - // Test insert column with illegal cell coordinates. - assert.EqualError(t, f.InsertCol("Sheet1", "*"), `invalid column name "*"`) - - assert.NoError(t, f.SaveAs(filepath.Join("test", "TestInsertCol.xlsx"))) -} - -func TestRemoveCol(t *testing.T) { - f := NewFile() - sheet1 := f.GetSheetName(1) - - fillCells(f, sheet1, 10, 15) - - f.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External") - f.SetCellHyperLink(sheet1, "C5", "https://github.com", "External") - - f.MergeCell(sheet1, "A1", "B1") - f.MergeCell(sheet1, "A2", "B2") - - assert.NoError(t, f.RemoveCol(sheet1, "A")) - assert.NoError(t, f.RemoveCol(sheet1, "A")) - - // Test remove column with illegal cell coordinates. - assert.EqualError(t, f.RemoveCol("Sheet1", "*"), `invalid column name "*"`) - - // Test remove column on not exists worksheet. - assert.EqualError(t, f.RemoveCol("SheetN", "B"), "sheet SheetN is not exist") - - assert.NoError(t, f.SaveAs(filepath.Join("test", "TestRemoveCol.xlsx"))) -} - func TestSetPane(t *testing.T) { f := NewFile() f.SetPanes("Sheet1", `{"freeze":false,"split":false}`) @@ -1162,48 +1057,6 @@ func TestSetSheetRow(t *testing.T) { assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetSheetRow.xlsx"))) } -func TestOutlineLevel(t *testing.T) { - f := NewFile() - f.NewSheet("Sheet2") - f.SetColOutlineLevel("Sheet1", "D", 4) - f.GetColOutlineLevel("Sheet1", "D") - f.GetColOutlineLevel("Shee2", "A") - f.SetColWidth("Sheet2", "A", "D", 13) - f.SetColOutlineLevel("Sheet2", "B", 2) - f.SetRowOutlineLevel("Sheet1", 2, 250) - - // Test set and get column outline level with illegal cell coordinates. - assert.EqualError(t, f.SetColOutlineLevel("Sheet1", "*", 1), `invalid column name "*"`) - _, err := f.GetColOutlineLevel("Sheet1", "*") - assert.EqualError(t, err, `invalid column name "*"`) - - // 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.SetRowOutlineLevel("Sheet1", 0, 1), "invalid row number 0") - level, err := f.GetRowOutlineLevel("Sheet1", 2) - assert.NoError(t, err) - assert.Equal(t, uint8(250), level) - - _, err = f.GetRowOutlineLevel("Sheet1", 0) - assert.EqualError(t, err, `invalid row number 0`) - - level, err = f.GetRowOutlineLevel("Sheet1", 10) - assert.NoError(t, err) - assert.Equal(t, uint8(0), level) - - err = f.SaveAs(filepath.Join("test", "TestOutlineLevel.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } - - f, err = OpenFile(filepath.Join("test", "Book1.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } - f.SetColOutlineLevel("Sheet2", "B", 2) -} - func TestThemeColor(t *testing.T) { t.Log(ThemeColor("000000", -0.1)) t.Log(ThemeColor("000000", 0)) From f91f548614a7182ce66d55d10ed311e9b7e08a2a Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 17 May 2019 22:58:12 +0800 Subject: [PATCH 095/957] Resolve #404, get sheet map by target rels. --- sheet.go | 16 +++++++++++----- styles.go | 9 +++------ xmlStyles.go | 13 ++++--------- xmlWorkbook.go | 5 ++--- 4 files changed, 20 insertions(+), 23 deletions(-) diff --git a/sheet.go b/sheet.go index b22592db32..e873118e5b 100644 --- a/sheet.go +++ b/sheet.go @@ -369,12 +369,18 @@ func (f *File) GetSheetMap() map[int]string { return sheetMap } -// getSheetMap provides a function to get worksheet name and XML file path map of -// XLSX. +// getSheetMap provides a function to get worksheet name and XML file path map +// of XLSX. func (f *File) getSheetMap() map[string]string { - maps := make(map[string]string) - for idx, name := range f.GetSheetMap() { - maps[name] = "xl/worksheets/sheet" + strconv.Itoa(idx) + ".xml" + content := f.workbookReader() + rels := f.workbookRelsReader() + maps := map[string]string{} + for _, v := range content.Sheets.Sheet { + for _, rel := range rels.Relationships { + if rel.ID == v.ID { + maps[v.Name] = fmt.Sprintf("xl/%s", rel.Target) + } + } } return maps } diff --git a/styles.go b/styles.go index d6d267dd0f..fc8a29067e 100644 --- a/styles.go +++ b/styles.go @@ -1895,11 +1895,8 @@ func (f *File) NewStyle(style string) (int, error) { numFmtID := setNumFmt(s, fs) if fs.Font != nil { - font, _ := xml.Marshal(setFont(fs)) s.Fonts.Count++ - s.Fonts.Font = append(s.Fonts.Font, &xlsxFont{ - Font: string(font[6 : len(font)-7]), - }) + s.Fonts.Font = append(s.Fonts.Font, setFont(fs)) fontID = s.Fonts.Count - 1 } @@ -1950,7 +1947,7 @@ func (f *File) NewConditionalStyle(style string) (int, error) { // setFont provides a function to add font style by given cell format // settings. -func setFont(formatStyle *formatStyle) *font { +func setFont(formatStyle *formatStyle) *xlsxFont { fontUnderlineType := map[string]string{"single": "single", "double": "double"} if formatStyle.Font.Size < 1 { formatStyle.Font.Size = 11 @@ -1958,7 +1955,7 @@ func setFont(formatStyle *formatStyle) *font { if formatStyle.Font.Color == "" { formatStyle.Font.Color = "#000000" } - f := font{ + f := xlsxFont{ B: formatStyle.Font.Bold, I: formatStyle.Font.Italic, Sz: &attrValInt{Val: formatStyle.Font.Size}, diff --git a/xmlStyles.go b/xmlStyles.go index fc53f7756c..5c198e7a43 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -82,8 +82,9 @@ type xlsxFonts struct { Font []*xlsxFont `xml:"font"` } -// font directly maps the font element. -type font struct { +// xlsxFont directly maps the font element. This element defines the +// properties for one of the fonts used in this workbook. +type xlsxFont struct { Name *attrValString `xml:"name"` Charset *attrValInt `xml:"charset"` Family *attrValInt `xml:"family"` @@ -100,12 +101,6 @@ type font struct { Scheme *attrValString `xml:"scheme"` } -// xlsxFont directly maps the font element. This element defines the properties -// for one of the fonts used in this workbook. -type xlsxFont struct { - Font string `xml:",innerxml"` -} - // 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 @@ -262,7 +257,7 @@ type xlsxDxf struct { // dxf directly maps the dxf element. type dxf struct { - Font *font `xml:"font"` + Font *xlsxFont `xml:"font"` NumFmt *xlsxNumFmt `xml:"numFmt"` Fill *xlsxFill `xml:"fill"` Alignment *xlsxAlignment `xml:"alignment"` diff --git a/xmlWorkbook.go b/xmlWorkbook.go index ad66f4295c..90a14272ed 100644 --- a/xmlWorkbook.go +++ b/xmlWorkbook.go @@ -146,9 +146,8 @@ type xlsxSheets struct { Sheet []xlsxSheet `xml:"sheet"` } -// xlsxSheet directly maps the sheet element from the namespace -// http://schemas.openxmlformats.org/spreadsheetml/2006/main - currently I have -// not checked it for completeness - it does as much as I need. +// xlsxSheet defines a sheet in this workbook. Sheet data is stored in a +// separate part. type xlsxSheet struct { Name string `xml:"name,attr,omitempty"` SheetID int `xml:"sheetId,attr,omitempty"` From b1c9884f6d186bd1bfb4fc1d34061856345b8530 Mon Sep 17 00:00:00 2001 From: Harris Date: Thu, 25 Apr 2019 11:24:25 -0500 Subject: [PATCH 096/957] Add the ability to change the default font Closes #390 --- comment.go | 5 +++-- shape.go | 2 +- styles.go | 40 +++++++++++++++++++++++++++++++--------- styles_test.go | 30 +++++++++++++++++++++++++++++- templates.go | 2 +- 5 files changed, 65 insertions(+), 14 deletions(-) diff --git a/comment.go b/comment.go index 3cf0c1d744..af70820a82 100644 --- a/comment.go +++ b/comment.go @@ -239,6 +239,7 @@ func (f *File) addComment(commentsXML, cell string, formatSet *formatComment) { }, } } + defaultFont := f.GetDefaultFont() cmt := xlsxComment{ Ref: cell, AuthorID: 0, @@ -251,7 +252,7 @@ func (f *File) addComment(commentsXML, cell string, formatSet *formatComment) { Color: &xlsxColor{ Indexed: 81, }, - RFont: &attrValString{Val: "Calibri"}, + RFont: &attrValString{Val: defaultFont}, Family: &attrValInt{Val: 2}, }, T: a, @@ -262,7 +263,7 @@ func (f *File) addComment(commentsXML, cell string, formatSet *formatComment) { Color: &xlsxColor{ Indexed: 81, }, - RFont: &attrValString{Val: "Calibri"}, + RFont: &attrValString{Val: defaultFont}, Family: &attrValInt{Val: 2}, }, T: t, diff --git a/shape.go b/shape.go index c90963cf87..7dc702135b 100644 --- a/shape.go +++ b/shape.go @@ -381,7 +381,7 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, formatSet *format Bold: false, Italic: false, Underline: "none", - Family: "Calibri", + Family: f.GetDefaultFont(), Size: 11, Color: "#000000", }, diff --git a/styles.go b/styles.go index fc8a29067e..e0e6f78308 100644 --- a/styles.go +++ b/styles.go @@ -1896,7 +1896,7 @@ func (f *File) NewStyle(style string) (int, error) { if fs.Font != nil { s.Fonts.Count++ - s.Fonts.Font = append(s.Fonts.Font, setFont(fs)) + s.Fonts.Font = append(s.Fonts.Font, f.setFont(fs)) fontID = s.Fonts.Count - 1 } @@ -1932,7 +1932,7 @@ func (f *File) NewConditionalStyle(style string) (int, error) { Border: setBorders(fs), } if fs.Font != nil { - dxf.Font = setFont(fs) + dxf.Font = f.setFont(fs) } dxfStr, _ := xml.Marshal(dxf) if s.Dxfs == nil { @@ -1945,9 +1945,32 @@ func (f *File) NewConditionalStyle(style string) (int, error) { return s.Dxfs.Count - 1, nil } +// GetDefaultFont provides the default font name currently set in the workbook +// Documents generated by excelize start with Calibri +func (f *File) GetDefaultFont() string { + font := f.readDefaultFont() + return font.Name.Val +} + +// SetDefaultFont changes the default font in the workbook +func (f *File) SetDefaultFont(fontName string) { + font := f.readDefaultFont() + font.Name.Val = fontName + s := f.stylesReader() + s.Fonts.Font[0] = font + custom := true + s.CellStyles.CellStyle[0].CustomBuiltIn = &custom +} + +// readDefaultFont provides an unmarshalled font value +func (f *File) readDefaultFont() *xlsxFont { + s := f.stylesReader() + return s.Fonts.Font[0] +} + // setFont provides a function to add font style by given cell format // settings. -func setFont(formatStyle *formatStyle) *xlsxFont { +func (f *File) setFont(formatStyle *formatStyle) *xlsxFont { fontUnderlineType := map[string]string{"single": "single", "double": "double"} if formatStyle.Font.Size < 1 { formatStyle.Font.Size = 11 @@ -1955,7 +1978,7 @@ func setFont(formatStyle *formatStyle) *xlsxFont { if formatStyle.Font.Color == "" { formatStyle.Font.Color = "#000000" } - f := xlsxFont{ + fnt := xlsxFont{ B: formatStyle.Font.Bold, I: formatStyle.Font.Italic, Sz: &attrValInt{Val: formatStyle.Font.Size}, @@ -1963,15 +1986,14 @@ func setFont(formatStyle *formatStyle) *xlsxFont { Name: &attrValString{Val: formatStyle.Font.Family}, Family: &attrValInt{Val: 2}, } - if f.Name.Val == "" { - f.Name.Val = "Calibri" - f.Scheme = &attrValString{Val: "minor"} + if fnt.Name.Val == "" { + fnt.Name.Val = f.GetDefaultFont() } val, ok := fontUnderlineType[formatStyle.Font.Underline] if ok { - f.U = &attrValString{Val: val} + fnt.U = &attrValString{Val: val} } - return &f + return &fnt } // setNumFmt provides a function to check if number format code in the range diff --git a/styles_test.go b/styles_test.go index 54321bb8e0..decfbb9224 100644 --- a/styles_test.go +++ b/styles_test.go @@ -25,7 +25,7 @@ func TestStyleFill(t *testing.T) { xl := NewFile() styleID, err := xl.NewStyle(testCase.format) if err != nil { - t.Fatalf("%v", err) + t.Fatal(err) } styles := xl.stylesReader() @@ -165,3 +165,31 @@ func TestSetConditionalFormat(t *testing.T) { assert.EqualValues(t, testCase.rules, cf[0].CfRule, testCase.label) } } + +func TestNewStyle(t *testing.T) { + f := NewFile() + styleID, err := f.NewStyle(`{"font":{"bold":true,"italic":true,"family":"Berlin Sans FB Demi","size":36,"color":"#777777"}}`) + if err != nil { + t.Fatal(err) + } + styles := f.stylesReader() + fontID := styles.CellXfs.Xf[styleID].FontID + font := styles.Fonts.Font[fontID] + assert.Contains(t, font.Name.Val, "Berlin Sans FB Demi", "Stored font should contain font name") + assert.Equal(t, 2, styles.CellXfs.Count, "Should have 2 styles") +} + +func TestGetDefaultFont(t *testing.T) { + f := NewFile() + s := f.GetDefaultFont() + assert.Equal(t, s, "Calibri", "Default font should be Calibri") +} + +func TestSetDefaultFont(t *testing.T) { + f := NewFile() + f.SetDefaultFont("Ariel") + styles := f.stylesReader() + s := f.GetDefaultFont() + assert.Equal(t, s, "Ariel", "Default font should change to Ariel") + assert.Equal(t, *styles.CellStyles.CellStyle[0].CustomBuiltIn, true) +} diff --git a/templates.go b/templates.go index 17fc8d4011..923cebd1fa 100644 --- a/templates.go +++ b/templates.go @@ -27,7 +27,7 @@ const templateContentTypes = `` -const templateStyles = `` +const templateStyles = `` const templateSheet = `` From d038ca2e9c755abb6458606a90129559f14dc33b Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 1 Jun 2019 15:37:47 +0800 Subject: [PATCH 097/957] Fix #413, make pivot cache ID not omit empty --- xmlWorkbook.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xmlWorkbook.go b/xmlWorkbook.go index 90a14272ed..53849779e1 100644 --- a/xmlWorkbook.go +++ b/xmlWorkbook.go @@ -175,7 +175,7 @@ type xlsxPivotCaches struct { // xlsxPivotCache directly maps the pivotCache element. type xlsxPivotCache struct { - CacheID int `xml:"cacheId,attr,omitempty"` + CacheID int `xml:"cacheId,attr"` RID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr,omitempty"` } From db99373b25a3f5c986d8f7d26ec983853574a17d Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 4 Jun 2019 23:30:55 +0800 Subject: [PATCH 098/957] Resolve #415, init set and get doc properties support --- docProps.go | 140 ++++++++++++++++++++++++++++++++++++++++++++++++++ xmlCore.go | 89 ++++++++++++++++++++++++++++++++ xmlDrawing.go | 50 +++++++++--------- 3 files changed, 256 insertions(+), 23 deletions(-) create mode 100755 docProps.go create mode 100755 xmlCore.go mode change 100644 => 100755 xmlDrawing.go diff --git a/docProps.go b/docProps.go new file mode 100755 index 0000000000..0f44ac42c5 --- /dev/null +++ b/docProps.go @@ -0,0 +1,140 @@ +// Copyright 2016 - 2019 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. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. + +package excelize + +import ( + "encoding/xml" + "reflect" +) + +// 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. +// +// 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", +// }) +// +func (f *File) SetDocProps(docProperties *DocProperties) error { + core := decodeCoreProperties{} + err := xml.Unmarshal(namespaceStrictToTransitional(f.readXML("docProps/core.xml")), &core) + if err != nil { + return err + } + newProps := xlsxCoreProperties{ + Dc: NameSpaceDublinCore, + Dcterms: NameSpaceDublinCoreTerms, + Dcmitype: NameSpaceDublinCoreMetadataIntiative, + XSI: NameSpaceXMLSchemaInstance, + Title: core.Title, + Subject: core.Subject, + Creator: core.Creator, + Keywords: core.Keywords, + Description: core.Description, + LastModifiedBy: core.LastModifiedBy, + Language: core.Language, + Identifier: core.Identifier, + Revision: core.Revision, + ContentStatus: core.ContentStatus, + Category: core.Category, + Version: core.Version, + } + newProps.Created.Text = core.Created.Text + newProps.Created.Type = core.Created.Type + newProps.Modified.Text = core.Modified.Text + newProps.Modified.Type = core.Modified.Type + + fields := []string{"Category", "ContentStatus", "Creator", "Description", "Identifier", "Keywords", "LastModifiedBy", "Revision", "Subject", "Title", "Language", "Version"} + immutable := reflect.ValueOf(*docProperties) + mutable := reflect.ValueOf(&newProps).Elem() + for _, field := range fields { + val := immutable.FieldByName(field).String() + if val != "" { + mutable.FieldByName(field).SetString(val) + } + } + if docProperties.Created != "" { + newProps.Created.Text = docProperties.Created + } + if docProperties.Modified != "" { + newProps.Modified.Text = docProperties.Modified + } + output, err := xml.Marshal(&newProps) + f.saveFileList("docProps/core.xml", output) + return err +} + +// GetDocProps provides a function to get document core properties. +func (f *File) GetDocProps() (*DocProperties, error) { + core := decodeCoreProperties{} + err := xml.Unmarshal(namespaceStrictToTransitional(f.readXML("docProps/core.xml")), &core) + if err != nil { + return nil, err + } + return &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 +} diff --git a/xmlCore.go b/xmlCore.go new file mode 100755 index 0000000000..357f688a6b --- /dev/null +++ b/xmlCore.go @@ -0,0 +1,89 @@ +// Copyright 2016 - 2019 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. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. + +package excelize + +import "encoding/xml" + +// DocProperties directly maps the document core properties. +type DocProperties struct { + Category string + ContentStatus string + Created string + Creator string + Description string + Identifier string + Keywords string + LastModifiedBy string + Modified string + Revision string + Subject string + Title string + Language string + Version string +} + +// 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"` +} + +// 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"` +} diff --git a/xmlDrawing.go b/xmlDrawing.go old mode 100644 new mode 100755 index 89496c413f..13e164ead5 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -13,29 +13,33 @@ import "encoding/xml" // Source relationship and namespace. const ( - SourceRelationship = "http://schemas.openxmlformats.org/officeDocument/2006/relationships" - 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" - SourceRelationshipChart201506 = "http://schemas.microsoft.com/office/drawing/2015/06/chart" - SourceRelationshipChart20070802 = "http://schemas.microsoft.com/office/drawing/2007/8/2/chart" - SourceRelationshipChart2014 = "http://schemas.microsoft.com/office/drawing/2014/chart" - SourceRelationshipCompatibility = "http://schemas.openxmlformats.org/markup-compatibility/2006" - NameSpaceDrawingML = "http://schemas.openxmlformats.org/drawingml/2006/main" - NameSpaceDrawingMLChart = "http://schemas.openxmlformats.org/drawingml/2006/chart" - NameSpaceDrawingMLSpreadSheet = "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" - NameSpaceSpreadSheet = "http://schemas.openxmlformats.org/spreadsheetml/2006/main" - NameSpaceXML = "http://www.w3.org/XML/1998/namespace" - 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" - StrictNameSpaceSpreadSheet = "http://purl.oclc.org/ooxml/spreadsheetml/main" + SourceRelationship = "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + 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" + SourceRelationshipChart201506 = "http://schemas.microsoft.com/office/drawing/2015/06/chart" + SourceRelationshipChart20070802 = "http://schemas.microsoft.com/office/drawing/2007/8/2/chart" + SourceRelationshipChart2014 = "http://schemas.microsoft.com/office/drawing/2014/chart" + SourceRelationshipCompatibility = "http://schemas.openxmlformats.org/markup-compatibility/2006" + NameSpaceDrawingML = "http://schemas.openxmlformats.org/drawingml/2006/main" + NameSpaceDrawingMLChart = "http://schemas.openxmlformats.org/drawingml/2006/chart" + NameSpaceDrawingMLSpreadSheet = "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" + NameSpaceSpreadSheet = "http://schemas.openxmlformats.org/spreadsheetml/2006/main" + NameSpaceXML = "http://www.w3.org/XML/1998/namespace" + NameSpaceXMLSchemaInstance = "http://www.w3.org/2001/XMLSchema-instance" + 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" + StrictNameSpaceSpreadSheet = "http://purl.oclc.org/ooxml/spreadsheetml/main" + NameSpaceDublinCore = "http://purl.org/dc/elements/1.1/" + NameSpaceDublinCoreTerms = "http://purl.org/dc/terms/" + NameSpaceDublinCoreMetadataIntiative = "http://purl.org/dc/dcmitype/" ) var supportImageTypes = map[string]string{".gif": ".gif", ".jpg": ".jpeg", ".jpeg": ".jpeg", ".png": ".png"} From cff16fa8118291fd885f3f3f75fa07e28bba5eec Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 5 Jun 2019 22:06:15 +0800 Subject: [PATCH 099/957] - Supplemental worksheet struct fields and field order adjustment - Testing case for set and get doc properties - Update charts struct XML tags --- cellmerged.go | 14 +- docProps.go | 0 docProps_test.go | 56 ++++++++ excelize_test.go | 2 +- xmlChart.go | 328 +++++++++++++++++++++++------------------------ xmlCore.go | 0 xmlDrawing.go | 0 xmlWorksheet.go | 86 +++++++++++-- 8 files changed, 306 insertions(+), 180 deletions(-) mode change 100755 => 100644 docProps.go create mode 100644 docProps_test.go mode change 100755 => 100644 xmlCore.go mode change 100755 => 100644 xmlDrawing.go diff --git a/cellmerged.go b/cellmerged.go index 53924637c2..a78b244530 100644 --- a/cellmerged.go +++ b/cellmerged.go @@ -1,8 +1,18 @@ +// Copyright 2016 - 2019 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. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. + package excelize import "strings" -// GetMergeCells provides a function to get all merged cells from a worksheet currently. +// GetMergeCells provides a function to get all merged cells from a worksheet +// currently. func (f *File) GetMergeCells(sheet string) ([]MergeCell, error) { var mergeCells []MergeCell xlsx, err := f.workSheetReader(sheet) @@ -45,4 +55,4 @@ func (m *MergeCell) GetStartAxis() string { func (m *MergeCell) GetEndAxis() string { axis := strings.Split((*m)[0], ":") return axis[1] -} \ No newline at end of file +} diff --git a/docProps.go b/docProps.go old mode 100755 new mode 100644 diff --git a/docProps_test.go b/docProps_test.go new file mode 100644 index 0000000000..1f52beb299 --- /dev/null +++ b/docProps_test.go @@ -0,0 +1,56 @@ +// Copyright 2016 - 2019 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. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. + +package excelize + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSetDocProps(t *testing.T) { + f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + if !assert.NoError(t, err) { + t.FailNow() + } + assert.NoError(t, f.SetDocProps(&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", + })) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetDocProps.xlsx"))) + f.XLSX["docProps/core.xml"] = nil + assert.EqualError(t, f.SetDocProps(&DocProperties{}), "EOF") +} + +func TestGetDocProps(t *testing.T) { + f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + if !assert.NoError(t, err) { + t.FailNow() + } + props, err := f.GetDocProps() + assert.NoError(t, err) + assert.Equal(t, props.Creator, "Microsoft Office User") + f.XLSX["docProps/core.xml"] = nil + _, err = f.GetDocProps() + assert.EqualError(t, err, "EOF") +} diff --git a/excelize_test.go b/excelize_test.go index 85df09b92f..1a3dde6c17 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -192,7 +192,7 @@ func TestBrokenFile(t *testing.T) { t.Run("SaveAsEmptyStruct", func(t *testing.T) { // Test write file with broken file struct with given path. - assert.NoError(t, f.SaveAs(filepath.Join("test", "TestBrokenFile.SaveAsEmptyStruct.xlsx"))) + assert.NoError(t, f.SaveAs(filepath.Join("test", "BrokenFile.SaveAsEmptyStruct.xlsx"))) }) t.Run("OpenBadWorkbook", func(t *testing.T) { diff --git a/xmlChart.go b/xmlChart.go index 163812d542..972ead37c8 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -11,66 +11,66 @@ package excelize import "encoding/xml" -// xlsxChartSpace directly maps the c:chartSpace element. The chart namespace in +// xlsxChartSpace directly maps the chartSpace element. The chart namespace in // DrawingML is for representing visualizations of numeric data with column // charts, pie charts, scatter charts, or other types of charts. type xlsxChartSpace struct { - XMLName xml.Name `xml:"c:chartSpace"` + XMLName xml.Name `xml:"http://schemas.openxmlformats.org/drawingml/2006/chart chartSpace"` XMLNSc string `xml:"xmlns:c,attr"` XMLNSa string `xml:"xmlns:a,attr"` XMLNSr string `xml:"xmlns:r,attr"` XMLNSc16r2 string `xml:"xmlns:c16r2,attr"` - Date1904 *attrValBool `xml:"c:date1904"` - Lang *attrValString `xml:"c:lang"` - RoundedCorners *attrValBool `xml:"c:roundedCorners"` - Chart cChart `xml:"c:chart"` - SpPr *cSpPr `xml:"c:spPr"` - TxPr *cTxPr `xml:"c:txPr"` - PrintSettings *cPrintSettings `xml:"c:printSettings"` -} - -// cThicknessSpPr directly maps the element that specifies the thickness of the -// walls or floor as a percentage of the largest dimension of the plot volume -// and SpPr element. + Date1904 *attrValBool `xml:"date1904"` + Lang *attrValString `xml:"lang"` + RoundedCorners *attrValBool `xml:"roundedCorners"` + Chart cChart `xml:"chart"` + SpPr *cSpPr `xml:"spPr"` + TxPr *cTxPr `xml:"txPr"` + PrintSettings *cPrintSettings `xml:"printSettings"` +} + +// cThicknessSpPr directly maps the element that specifies the thickness of +// the walls or floor as a percentage of the largest dimension of the plot +// volume and SpPr element. type cThicknessSpPr struct { - Thickness *attrValInt `xml:"c:thickness"` - SpPr *cSpPr `xml:"c:spPr"` + Thickness *attrValInt `xml:"thickness"` + SpPr *cSpPr `xml:"spPr"` } -// cChart (Chart) directly maps the c:chart element. This element specifies a +// cChart (Chart) directly maps the chart element. This element specifies a // title. type cChart struct { - Title *cTitle `xml:"c:title"` - AutoTitleDeleted *cAutoTitleDeleted `xml:"c:autoTitleDeleted"` - View3D *cView3D `xml:"c:view3D"` - Floor *cThicknessSpPr `xml:"c:floor"` - SideWall *cThicknessSpPr `xml:"c:sideWall"` - BackWall *cThicknessSpPr `xml:"c:backWall"` - PlotArea *cPlotArea `xml:"c:plotArea"` - Legend *cLegend `xml:"c:legend"` - PlotVisOnly *attrValBool `xml:"c:plotVisOnly"` - DispBlanksAs *attrValString `xml:"c:dispBlanksAs"` - ShowDLblsOverMax *attrValBool `xml:"c:showDLblsOverMax"` -} - -// cTitle (Title) directly maps the c:title element. This element specifies a + Title *cTitle `xml:"title"` + AutoTitleDeleted *cAutoTitleDeleted `xml:"autoTitleDeleted"` + View3D *cView3D `xml:"view3D"` + Floor *cThicknessSpPr `xml:"floor"` + SideWall *cThicknessSpPr `xml:"sideWall"` + BackWall *cThicknessSpPr `xml:"backWall"` + PlotArea *cPlotArea `xml:"plotArea"` + Legend *cLegend `xml:"legend"` + PlotVisOnly *attrValBool `xml:"plotVisOnly"` + DispBlanksAs *attrValString `xml:"dispBlanksAs"` + ShowDLblsOverMax *attrValBool `xml:"showDLblsOverMax"` +} + +// cTitle (Title) directly maps the title element. This element specifies a // title. type cTitle struct { - Tx cTx `xml:"c:tx,omitempty"` - Layout string `xml:"c:layout,omitempty"` - Overlay attrValBool `xml:"c:overlay,omitempty"` - SpPr cSpPr `xml:"c:spPr,omitempty"` - TxPr cTxPr `xml:"c:txPr,omitempty"` + Tx cTx `xml:"tx,omitempty"` + Layout string `xml:"layout,omitempty"` + Overlay attrValBool `xml:"overlay,omitempty"` + SpPr cSpPr `xml:"spPr,omitempty"` + TxPr cTxPr `xml:"txPr,omitempty"` } -// cTx (Chart Text) directly maps the c:tx element. This element specifies text +// cTx (Chart Text) directly maps the tx element. This element specifies text // to use on a chart, including rich text formatting. type cTx struct { - StrRef *cStrRef `xml:"c:strRef"` - Rich *cRich `xml:"c:rich,omitempty"` + StrRef *cStrRef `xml:"strRef"` + Rich *cRich `xml:"rich,omitempty"` } -// cRich (Rich Text) directly maps the c:rich element. This element contains a +// cRich (Rich Text) directly maps the rich element. This element contains a // string with rich text formatting. type cRich struct { BodyPr aBodyPr `xml:"a:bodyPr,omitempty"` @@ -186,7 +186,7 @@ type aR struct { T string `xml:"a:t,omitempty"` } -// aRPr (Run Properties) directly maps the c:rPr element. This element +// aRPr (Run Properties) directly maps the rPr element. This element // specifies a set of run properties which shall be applied to the contents of // the parent run after all style formatting has been applied to the text. These // properties are defined as direct formatting, since they are directly applied @@ -217,7 +217,7 @@ type aRPr struct { Cs *aCs `xml:"a:cs"` } -// cSpPr (Shape Properties) directly maps the c:spPr element. This element +// cSpPr (Shape Properties) directly maps the spPr element. This element // specifies the visual shape properties that can be applied to a shape. These // properties include the shape fill, outline, geometry, effects, and 3D // orientation. @@ -259,7 +259,7 @@ type aLn struct { SolidFill *aSolidFill `xml:"a:solidFill"` } -// cTxPr (Text Properties) directly maps the c:txPr element. This element +// cTxPr (Text Properties) directly maps the txPr element. This element // specifies text formatting. The lstStyle element is not supported. type cTxPr struct { BodyPr aBodyPr `xml:"a:bodyPr,omitempty"` @@ -282,207 +282,207 @@ type aEndParaRPr struct { } // cAutoTitleDeleted (Auto Title Is Deleted) directly maps the -// c:autoTitleDeleted element. This element specifies the title shall not be +// autoTitleDeleted element. This element specifies the title shall not be // shown for this chart. type cAutoTitleDeleted struct { Val bool `xml:"val,attr"` } -// cView3D (View In 3D) directly maps the c:view3D element. This element +// cView3D (View In 3D) directly maps the view3D element. This element // specifies the 3-D view of the chart. type cView3D struct { - RotX *attrValInt `xml:"c:rotX"` - RotY *attrValInt `xml:"c:rotY"` - DepthPercent *attrValInt `xml:"c:depthPercent"` - RAngAx *attrValInt `xml:"c:rAngAx"` + RotX *attrValInt `xml:"rotX"` + RotY *attrValInt `xml:"rotY"` + DepthPercent *attrValInt `xml:"depthPercent"` + RAngAx *attrValInt `xml:"rAngAx"` } -// cPlotArea directly maps the c:plotArea element. This element specifies the +// cPlotArea directly maps the plotArea element. This element specifies the // plot area of the chart. type cPlotArea struct { - Layout *string `xml:"c:layout"` - AreaChart *cCharts `xml:"c:areaChart"` - Area3DChart *cCharts `xml:"c:area3DChart"` - BarChart *cCharts `xml:"c:barChart"` - Bar3DChart *cCharts `xml:"c:bar3DChart"` - DoughnutChart *cCharts `xml:"c:doughnutChart"` - LineChart *cCharts `xml:"c:lineChart"` - PieChart *cCharts `xml:"c:pieChart"` - Pie3DChart *cCharts `xml:"c:pie3DChart"` - RadarChart *cCharts `xml:"c:radarChart"` - ScatterChart *cCharts `xml:"c:scatterChart"` - CatAx []*cAxs `xml:"c:catAx"` - ValAx []*cAxs `xml:"c:valAx"` - SpPr *cSpPr `xml:"c:spPr"` + Layout *string `xml:"layout"` + AreaChart *cCharts `xml:"areaChart"` + Area3DChart *cCharts `xml:"area3DChart"` + BarChart *cCharts `xml:"barChart"` + Bar3DChart *cCharts `xml:"bar3DChart"` + DoughnutChart *cCharts `xml:"doughnutChart"` + LineChart *cCharts `xml:"lineChart"` + PieChart *cCharts `xml:"pieChart"` + Pie3DChart *cCharts `xml:"pie3DChart"` + RadarChart *cCharts `xml:"radarChart"` + ScatterChart *cCharts `xml:"scatterChart"` + CatAx []*cAxs `xml:"catAx"` + ValAx []*cAxs `xml:"valAx"` + SpPr *cSpPr `xml:"spPr"` } // cCharts specifies the common element of the chart. type cCharts struct { - BarDir *attrValString `xml:"c:barDir"` - Grouping *attrValString `xml:"c:grouping"` - RadarStyle *attrValString `xml:"c:radarStyle"` - ScatterStyle *attrValString `xml:"c:scatterStyle"` - VaryColors *attrValBool `xml:"c:varyColors"` - Ser *[]cSer `xml:"c:ser"` - DLbls *cDLbls `xml:"c:dLbls"` - HoleSize *attrValInt `xml:"c:holeSize"` - Smooth *attrValBool `xml:"c:smooth"` - Overlap *attrValInt `xml:"c:overlap"` - AxID []*attrValInt `xml:"c:axId"` -} - -// cAxs directly maps the c:catAx and c:valAx element. + BarDir *attrValString `xml:"barDir"` + Grouping *attrValString `xml:"grouping"` + RadarStyle *attrValString `xml:"radarStyle"` + ScatterStyle *attrValString `xml:"scatterStyle"` + VaryColors *attrValBool `xml:"varyColors"` + Ser *[]cSer `xml:"ser"` + DLbls *cDLbls `xml:"dLbls"` + HoleSize *attrValInt `xml:"holeSize"` + Smooth *attrValBool `xml:"smooth"` + Overlap *attrValInt `xml:"overlap"` + AxID []*attrValInt `xml:"axId"` +} + +// cAxs directly maps the catAx and valAx element. type cAxs struct { - AxID *attrValInt `xml:"c:axId"` - Scaling *cScaling `xml:"c:scaling"` - Delete *attrValBool `xml:"c:delete"` - AxPos *attrValString `xml:"c:axPos"` - NumFmt *cNumFmt `xml:"c:numFmt"` - MajorTickMark *attrValString `xml:"c:majorTickMark"` - MinorTickMark *attrValString `xml:"c:minorTickMark"` - TickLblPos *attrValString `xml:"c:tickLblPos"` - SpPr *cSpPr `xml:"c:spPr"` - TxPr *cTxPr `xml:"c:txPr"` - CrossAx *attrValInt `xml:"c:crossAx"` - Crosses *attrValString `xml:"c:crosses"` - CrossBetween *attrValString `xml:"c:crossBetween"` - Auto *attrValBool `xml:"c:auto"` - LblAlgn *attrValString `xml:"c:lblAlgn"` - LblOffset *attrValInt `xml:"c:lblOffset"` - NoMultiLvlLbl *attrValBool `xml:"c:noMultiLvlLbl"` -} - -// cScaling directly maps the c:scaling element. This element contains + AxID *attrValInt `xml:"axId"` + Scaling *cScaling `xml:"scaling"` + Delete *attrValBool `xml:"delete"` + AxPos *attrValString `xml:"axPos"` + NumFmt *cNumFmt `xml:"numFmt"` + MajorTickMark *attrValString `xml:"majorTickMark"` + MinorTickMark *attrValString `xml:"minorTickMark"` + TickLblPos *attrValString `xml:"tickLblPos"` + SpPr *cSpPr `xml:"spPr"` + TxPr *cTxPr `xml:"txPr"` + CrossAx *attrValInt `xml:"crossAx"` + Crosses *attrValString `xml:"crosses"` + CrossBetween *attrValString `xml:"crossBetween"` + Auto *attrValBool `xml:"auto"` + LblAlgn *attrValString `xml:"lblAlgn"` + LblOffset *attrValInt `xml:"lblOffset"` + NoMultiLvlLbl *attrValBool `xml:"noMultiLvlLbl"` +} + +// cScaling directly maps the scaling element. This element contains // additional axis settings. type cScaling struct { - Orientation *attrValString `xml:"c:orientation"` - Max *attrValFloat `xml:"c:max"` - Min *attrValFloat `xml:"c:min"` + Orientation *attrValString `xml:"orientation"` + Max *attrValFloat `xml:"max"` + Min *attrValFloat `xml:"min"` } -// cNumFmt (Numbering Format) directly maps the c:numFmt element. This element +// cNumFmt (Numbering Format) directly maps the numFmt element. This element // specifies number formatting for the parent element. type cNumFmt struct { FormatCode string `xml:"formatCode,attr"` SourceLinked bool `xml:"sourceLinked,attr"` } -// cSer directly maps the c:ser element. This element specifies a series on a +// cSer directly maps the ser element. This element specifies a series on a // chart. type cSer struct { - IDx *attrValInt `xml:"c:idx"` - Order *attrValInt `xml:"c:order"` - Tx *cTx `xml:"c:tx"` - SpPr *cSpPr `xml:"c:spPr"` - DPt []*cDPt `xml:"c:dPt"` - DLbls *cDLbls `xml:"c:dLbls"` - Marker *cMarker `xml:"c:marker"` - InvertIfNegative *attrValBool `xml:"c:invertIfNegative"` - Cat *cCat `xml:"c:cat"` - Val *cVal `xml:"c:val"` - XVal *cCat `xml:"c:xVal"` - YVal *cVal `xml:"c:yVal"` - Smooth *attrValBool `xml:"c:smooth"` -} - -// cMarker (Marker) directly maps the c:marker element. This element specifies a + IDx *attrValInt `xml:"idx"` + Order *attrValInt `xml:"order"` + Tx *cTx `xml:"tx"` + SpPr *cSpPr `xml:"spPr"` + DPt []*cDPt `xml:"dPt"` + DLbls *cDLbls `xml:"dLbls"` + Marker *cMarker `xml:"marker"` + InvertIfNegative *attrValBool `xml:"invertIfNegative"` + Cat *cCat `xml:"cat"` + Val *cVal `xml:"val"` + XVal *cCat `xml:"xVal"` + YVal *cVal `xml:"yVal"` + Smooth *attrValBool `xml:"smooth"` +} + +// cMarker (Marker) directly maps the marker element. This element specifies a // data marker. type cMarker struct { - Symbol *attrValString `xml:"c:symbol"` - Size *attrValInt `xml:"c:size"` - SpPr *cSpPr `xml:"c:spPr"` + Symbol *attrValString `xml:"symbol"` + Size *attrValInt `xml:"size"` + SpPr *cSpPr `xml:"spPr"` } -// cDPt (Data Point) directly maps the c:dPt element. This element specifies a +// cDPt (Data Point) directly maps the dPt element. This element specifies a // single data point. type cDPt struct { - IDx *attrValInt `xml:"c:idx"` - Bubble3D *attrValBool `xml:"c:bubble3D"` - SpPr *cSpPr `xml:"c:spPr"` + IDx *attrValInt `xml:"idx"` + Bubble3D *attrValBool `xml:"bubble3D"` + SpPr *cSpPr `xml:"spPr"` } -// cCat (Category Axis Data) directly maps the c:cat element. This element +// cCat (Category Axis Data) directly maps the cat element. This element // specifies the data used for the category axis. type cCat struct { - StrRef *cStrRef `xml:"c:strRef"` + StrRef *cStrRef `xml:"strRef"` } -// cStrRef (String Reference) directly maps the c:strRef element. This element +// cStrRef (String Reference) directly maps the strRef element. This element // specifies a reference to data for a single data label or title with a cache // of the last values used. type cStrRef struct { - F string `xml:"c:f"` - StrCache *cStrCache `xml:"c:strCache"` + F string `xml:"f"` + StrCache *cStrCache `xml:"strCache"` } -// cStrCache (String Cache) directly maps the c:strCache element. This element +// cStrCache (String Cache) directly maps the strCache element. This element // specifies the last string data used for a chart. type cStrCache struct { - Pt []*cPt `xml:"c:pt"` - PtCount *attrValInt `xml:"c:ptCount"` + Pt []*cPt `xml:"pt"` + PtCount *attrValInt `xml:"ptCount"` } -// cPt directly maps the c:pt element. This element specifies data for a +// cPt directly maps the pt element. This element specifies data for a // particular data point. type cPt struct { IDx int `xml:"idx,attr"` - V *string `xml:"c:v"` + V *string `xml:"v"` } -// cVal directly maps the c:val element. This element specifies the data values +// cVal directly maps the val element. This element specifies the data values // which shall be used to define the location of data markers on a chart. type cVal struct { - NumRef *cNumRef `xml:"c:numRef"` + NumRef *cNumRef `xml:"numRef"` } -// cNumRef directly maps the c:numRef element. This element specifies a +// cNumRef directly maps the numRef element. This element specifies a // reference to numeric data with a cache of the last values used. type cNumRef struct { - F string `xml:"c:f"` - NumCache *cNumCache `xml:"c:numCache"` + F string `xml:"f"` + NumCache *cNumCache `xml:"numCache"` } -// cNumCache directly maps the c:numCache element. This element specifies the +// cNumCache directly maps the numCache element. This element specifies the // last data shown on the chart for a series. type cNumCache struct { - FormatCode string `xml:"c:formatCode"` - Pt []*cPt `xml:"c:pt"` - PtCount *attrValInt `xml:"c:ptCount"` + FormatCode string `xml:"formatCode"` + Pt []*cPt `xml:"pt"` + PtCount *attrValInt `xml:"ptCount"` } -// cDLbls (Data Lables) directly maps the c:dLbls element. This element serves +// cDLbls (Data Lables) 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. type cDLbls struct { - ShowLegendKey *attrValBool `xml:"c:showLegendKey"` - ShowVal *attrValBool `xml:"c:showVal"` - ShowCatName *attrValBool `xml:"c:showCatName"` - ShowSerName *attrValBool `xml:"c:showSerName"` - ShowPercent *attrValBool `xml:"c:showPercent"` - ShowBubbleSize *attrValBool `xml:"c:showBubbleSize"` - ShowLeaderLines *attrValBool `xml:"c:showLeaderLines"` + ShowLegendKey *attrValBool `xml:"showLegendKey"` + ShowVal *attrValBool `xml:"showVal"` + ShowCatName *attrValBool `xml:"showCatName"` + ShowSerName *attrValBool `xml:"showSerName"` + ShowPercent *attrValBool `xml:"showPercent"` + ShowBubbleSize *attrValBool `xml:"showBubbleSize"` + ShowLeaderLines *attrValBool `xml:"showLeaderLines"` } -// cLegend (Legend) directly maps the c:legend element. This element specifies +// cLegend (Legend) directly maps the legend element. This element specifies // the legend. type cLegend struct { - Layout *string `xml:"c:layout"` - LegendPos *attrValString `xml:"c:legendPos"` - Overlay *attrValBool `xml:"c:overlay"` - SpPr *cSpPr `xml:"c:spPr"` - TxPr *cTxPr `xml:"c:txPr"` + Layout *string `xml:"layout"` + LegendPos *attrValString `xml:"legendPos"` + Overlay *attrValBool `xml:"overlay"` + SpPr *cSpPr `xml:"spPr"` + TxPr *cTxPr `xml:"txPr"` } -// cPrintSettings directly maps the c:printSettings element. This element +// cPrintSettings directly maps the printSettings element. This element // specifies the print settings for the chart. type cPrintSettings struct { - HeaderFooter *string `xml:"c:headerFooter"` - PageMargins *cPageMargins `xml:"c:pageMargins"` - PageSetup *string `xml:"c:pageSetup"` + HeaderFooter *string `xml:"headerFooter"` + PageMargins *cPageMargins `xml:"pageMargins"` + PageSetup *string `xml:"pageSetup"` } -// cPageMargins directly maps the c:pageMargins element. This element specifies +// cPageMargins directly maps the pageMargins element. This element specifies // the page margins for a chart. type cPageMargins struct { B float64 `xml:"b,attr"` diff --git a/xmlCore.go b/xmlCore.go old mode 100755 new mode 100644 diff --git a/xmlDrawing.go b/xmlDrawing.go old mode 100755 new mode 100644 diff --git a/xmlWorksheet.go b/xmlWorksheet.go index f2eb47abb5..4d19cde5b5 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -24,6 +24,7 @@ type xlsxWorksheet struct { SheetData xlsxSheetData `xml:"sheetData"` SheetProtection *xlsxSheetProtection `xml:"sheetProtection"` AutoFilter *xlsxAutoFilter `xml:"autoFilter"` + CustomSheetViews *xlsxCustomSheetViews `xml:"customSheetViews"` MergeCells *xlsxMergeCells `xml:"mergeCells"` PhoneticPr *xlsxPhoneticPr `xml:"phoneticPr"` ConditionalFormatting []*xlsxConditionalFormatting `xml:"conditionalFormatting"` @@ -33,6 +34,8 @@ type xlsxWorksheet struct { PageMargins *xlsxPageMargins `xml:"pageMargins"` PageSetUp *xlsxPageSetUp `xml:"pageSetup"` HeaderFooter *xlsxHeaderFooter `xml:"headerFooter"` + RowBreaks *xlsxBreaks `xml:"rowBreaks"` + ColBreaks *xlsxBreaks `xml:"colBreaks"` Drawing *xlsxDrawing `xml:"drawing"` LegacyDrawing *xlsxLegacyDrawing `xml:"legacyDrawing"` Picture *xlsxPicture `xml:"picture"` @@ -299,6 +302,63 @@ type xlsxRow struct { C []xlsxC `xml:"c"` } +// xlsxCustomSheetViews directly maps the customSheetViews element. This is a +// collection of custom sheet views. +type xlsxCustomSheetViews struct { + CustomSheetView []*xlsxCustomSheetView `xml:"customSheetView"` +} + +// xlsxBrk directly maps the row or column break to use when paginating a +// worksheet. +type xlsxBrk struct { + ID int `xml:"id,attr,omitempty"` + Min int `xml:"min,attr,omitempty"` + Max int `xml:"max,attr,omitempty"` + Man bool `xml:"man,attr,omitempty"` + Pt bool `xml:"pt,attr,omitempty"` +} + +// xlsxBreaks directly maps a collection of the row or column breaks. +type xlsxBreaks struct { + Brk *xlsxBrk `xml:"brk"` + Count int `xml:"count,attr,omitempty"` + ManualBreakCount int `xml:"manualBreakCount,attr,omitempty"` +} + +// xlsxCustomSheetView directly maps the customSheetView element. +type xlsxCustomSheetView struct { + Pane *xlsxPane `xml:"pane"` + Selection *xlsxSelection `xml:"selection"` + RowBreaks *xlsxBreaks `xml:"rowBreaks"` + ColBreaks *xlsxBreaks `xml:"colBreaks"` + PageMargins *xlsxPageMargins `xml:"pageMargins"` + PrintOptions *xlsxPrintOptions `xml:"printOptions"` + PageSetup *xlsxPageSetUp `xml:"pageSetup"` + HeaderFooter *xlsxHeaderFooter `xml:"headerFooter"` + AutoFilter *xlsxAutoFilter `xml:"autoFilter"` + ExtLst *xlsxExt `xml:"extLst"` + GUID string `xml:"guid,attr"` + Scale int `xml:"scale,attr,omitempty"` + ColorID int `xml:"colorId,attr,omitempty"` + ShowPageBreaks bool `xml:"showPageBreaks,attr,omitempty"` + ShowFormulas bool `xml:"showFormulas,attr,omitempty"` + ShowGridLines bool `xml:"showGridLines,attr,omitempty"` + ShowRowCol bool `xml:"showRowCol,attr,omitempty"` + OutlineSymbols bool `xml:"outlineSymbols,attr,omitempty"` + ZeroValues bool `xml:"zeroValues,attr,omitempty"` + FitToPage bool `xml:"fitToPage,attr,omitempty"` + PrintArea bool `xml:"printArea,attr,omitempty"` + Filter bool `xml:"filter,attr,omitempty"` + ShowAutoFilter bool `xml:"showAutoFilter,attr,omitempty"` + HiddenRows bool `xml:"hiddenRows,attr,omitempty"` + HiddenColumns bool `xml:"hiddenColumns,attr,omitempty"` + State string `xml:"state,attr,omitempty"` + FilterUnique bool `xml:"filterUnique,attr,omitempty"` + View string `xml:"view,attr,omitempty"` + ShowRuler bool `xml:"showRuler,attr,omitempty"` + TopLeftCell string `xml:"topLeftCell,attr,omitempty"` +} + // xlsxMergeCell directly maps the mergeCell element. A single merged cell. type xlsxMergeCell struct { Ref string `xml:"ref,attr,omitempty"` @@ -389,26 +449,26 @@ type xlsxF struct { // enforce when the sheet is protected. type xlsxSheetProtection struct { AlgorithmName string `xml:"algorithmName,attr,omitempty"` - AutoFilter bool `xml:"autoFilter,attr,omitempty"` - DeleteColumns bool `xml:"deleteColumns,attr,omitempty"` - DeleteRows bool `xml:"deleteRows,attr,omitempty"` + Password string `xml:"password,attr,omitempty"` + HashValue string `xml:"hashValue,attr,omitempty"` + SaltValue string `xml:"saltValue,attr,omitempty"` + SpinCount int `xml:"spinCount,attr,omitempty"` + Sheet bool `xml:"sheet,attr,omitempty"` + Objects bool `xml:"objects,attr,omitempty"` + Scenarios bool `xml:"scenarios,attr,omitempty"` FormatCells bool `xml:"formatCells,attr,omitempty"` FormatColumns bool `xml:"formatColumns,attr,omitempty"` FormatRows bool `xml:"formatRows,attr,omitempty"` - HashValue string `xml:"hashValue,attr,omitempty"` InsertColumns bool `xml:"insertColumns,attr,omitempty"` - InsertHyperlinks bool `xml:"insertHyperlinks,attr,omitempty"` InsertRows bool `xml:"insertRows,attr,omitempty"` - Objects bool `xml:"objects,attr,omitempty"` - Password string `xml:"password,attr,omitempty"` - PivotTables bool `xml:"pivotTables,attr,omitempty"` - SaltValue string `xml:"saltValue,attr,omitempty"` - Scenarios bool `xml:"scenarios,attr,omitempty"` + InsertHyperlinks bool `xml:"insertHyperlinks,attr,omitempty"` + DeleteColumns bool `xml:"deleteColumns,attr,omitempty"` + DeleteRows bool `xml:"deleteRows,attr,omitempty"` SelectLockedCells bool `xml:"selectLockedCells,attr,omitempty"` - SelectUnlockedCells bool `xml:"selectUnlockedCells,attr,omitempty"` - Sheet bool `xml:"sheet,attr,omitempty"` Sort bool `xml:"sort,attr,omitempty"` - SpinCount int `xml:"spinCount,attr,omitempty"` + AutoFilter bool `xml:"autoFilter,attr,omitempty"` + PivotTables bool `xml:"pivotTables,attr,omitempty"` + SelectUnlockedCells bool `xml:"selectUnlockedCells,attr,omitempty"` } // xlsxPhoneticPr (Phonetic Properties) represents a collection of phonetic From 3997dee1f58c81444733e1756da6138d4ce445f1 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 7 Jun 2019 09:16:55 +0800 Subject: [PATCH 100/957] Fix #411, change font size to float type --- styles.go | 2 +- xmlChart.go | 2 +- xmlStyles.go | 14 +++++++------- 3 files changed, 9 insertions(+), 9 deletions(-) mode change 100644 => 100755 styles.go mode change 100644 => 100755 xmlChart.go mode change 100644 => 100755 xmlStyles.go diff --git a/styles.go b/styles.go old mode 100644 new mode 100755 index e0e6f78308..1cae18642f --- a/styles.go +++ b/styles.go @@ -1981,7 +1981,7 @@ func (f *File) setFont(formatStyle *formatStyle) *xlsxFont { fnt := xlsxFont{ B: formatStyle.Font.Bold, I: formatStyle.Font.Italic, - Sz: &attrValInt{Val: formatStyle.Font.Size}, + Sz: &attrValFloat{Val: formatStyle.Font.Size}, Color: &xlsxColor{RGB: getPaletteColor(formatStyle.Font.Color)}, Name: &attrValString{Val: formatStyle.Font.Family}, Family: &attrValInt{Val: 2}, diff --git a/xmlChart.go b/xmlChart.go old mode 100644 new mode 100755 index 972ead37c8..d23364c160 --- a/xmlChart.go +++ b/xmlChart.go @@ -209,7 +209,7 @@ type aRPr struct { SmtID uint64 `xml:"smtId,attr,omitempty"` Spc int `xml:"spc,attr"` Strike string `xml:"strike,attr,omitempty"` - Sz int `xml:"sz,attr,omitempty"` + Sz float64 `xml:"sz,attr,omitempty"` U string `xml:"u,attr,omitempty"` SolidFill *aSolidFill `xml:"a:solidFill"` Latin *aLatin `xml:"a:latin"` diff --git a/xmlStyles.go b/xmlStyles.go old mode 100644 new mode 100755 index 5c198e7a43..46587daf99 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -96,7 +96,7 @@ type xlsxFont struct { Condense bool `xml:"condense,omitempty"` Extend bool `xml:"extend,omitempty"` Color *xlsxColor `xml:"color"` - Sz *attrValInt `xml:"sz"` + Sz *attrValFloat `xml:"sz"` U *attrValString `xml:"u"` Scheme *attrValString `xml:"scheme"` } @@ -315,12 +315,12 @@ type xlsxStyleColors struct { // formatFont directly maps the styles settings of the fonts. type formatFont struct { - Bold bool `json:"bold"` - Italic bool `json:"italic"` - Underline string `json:"underline"` - Family string `json:"family"` - Size int `json:"size"` - Color string `json:"color"` + Bold bool `json:"bold"` + Italic bool `json:"italic"` + Underline string `json:"underline"` + Family string `json:"family"` + Size float64 `json:"size"` + Color string `json:"color"` } // formatStyle directly maps the styles settings of the cells. From 421f945f51f254054991127758db0520cf0f5456 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 8 Jun 2019 00:00:55 +0800 Subject: [PATCH 101/957] Fixed #418, #420, #421, init adjust calculation chain support Update testing case --- adjust.go | 32 +++++++++++++++++++++++++++++--- adjust_test.go | 16 ++++++++++++++++ rows.go | 4 ++++ sheet.go | 2 +- styles.go | 4 ++-- xmlStyles.go | 14 +++++++------- 6 files changed, 59 insertions(+), 13 deletions(-) diff --git a/adjust.go b/adjust.go index 51db57ec7e..d88990df3c 100644 --- a/adjust.go +++ b/adjust.go @@ -27,8 +27,7 @@ const ( // row: Index number of the row we're inserting/deleting before // offset: Number of rows/column to insert/delete negative values indicate deletion // -// TODO: adjustCalcChain, adjustPageBreaks, adjustComments, -// adjustDataValidations, adjustProtectedCells +// TODO: adjustPageBreaks, adjustComments, adjustDataValidations, adjustProtectedCells // func (f *File) adjustHelper(sheet string, dir adjustDirection, num, offset int) error { xlsx, err := f.workSheetReader(sheet) @@ -47,7 +46,9 @@ func (f *File) adjustHelper(sheet string, dir adjustDirection, num, offset int) if err = f.adjustAutoFilter(xlsx, dir, num, offset); err != nil { return err } - + if err = f.adjustCalcChain(dir, num, offset); err != nil { + return err + } checkSheet(xlsx) checkRow(xlsx) return nil @@ -243,3 +244,28 @@ func (f *File) adjustMergeCells(xlsx *xlsxWorksheet, dir adjustDirection, num, o } return nil } + +// adjustCalcChain provides a function to update the calculation chain when +// inserting or deleting rows or columns. +func (f *File) adjustCalcChain(dir adjustDirection, num, offset int) error { + if f.CalcChain == nil { + return nil + } + for index, c := range f.CalcChain.C { + colNum, rowNum, err := CellNameToCoordinates(c.R) + if err != nil { + return err + } + if dir == rows && num <= rowNum { + if newRow := rowNum + offset; newRow > 0 { + f.CalcChain.C[index].R, _ = CoordinatesToCellName(colNum, newRow) + } + } + if dir == columns && num <= colNum { + if newCol := colNum + offset; newCol > 0 { + f.CalcChain.C[index].R, _ = CoordinatesToCellName(newCol, rowNum) + } + } + } + return nil +} diff --git a/adjust_test.go b/adjust_test.go index 7b708ab70c..28432bc631 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -67,3 +67,19 @@ func TestAdjustHelper(t *testing.T) { // testing adjustHelper on not exists worksheet. assert.EqualError(t, f.adjustHelper("SheetN", rows, 0, 0), "sheet SheetN is not exist") } + +func TestAdjustCalcChain(t *testing.T) { + f := NewFile() + f.CalcChain = &xlsxCalcChain{ + C: []xlsxCalcChainC{ + {R: "B2"}, + }, + } + assert.NoError(t, f.InsertCol("Sheet1", "A")) + assert.NoError(t, f.InsertRow("Sheet1", 1)) + + f.CalcChain.C[0].R = "invalid coordinates" + assert.EqualError(t, f.InsertCol("Sheet1", "A"), `cannot convert cell "invalid coordinates" to coordinates: invalid cell name "invalid coordinates"`) + f.CalcChain = nil + assert.NoError(t, f.InsertCol("Sheet1", "A")) +} diff --git a/rows.go b/rows.go index b228fc2c55..249ca2f62c 100644 --- a/rows.go +++ b/rows.go @@ -439,6 +439,10 @@ func (f *File) RemoveRow(sheet string, row int) error { // // 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 +// 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 { if row < 1 { return newInvalidRowNumberError(row) diff --git a/sheet.go b/sheet.go index e873118e5b..d3099fb46a 100644 --- a/sheet.go +++ b/sheet.go @@ -527,7 +527,7 @@ func (f *File) SetSheetVisible(name string, visible bool) error { } } for k, v := range content.Sheets.Sheet { - xlsx, err := f.workSheetReader(f.GetSheetMap()[k]) + xlsx, err := f.workSheetReader(v.Name) if err != nil { return err } diff --git a/styles.go b/styles.go index 1cae18642f..5c4f66e748 100755 --- a/styles.go +++ b/styles.go @@ -1979,8 +1979,8 @@ func (f *File) setFont(formatStyle *formatStyle) *xlsxFont { formatStyle.Font.Color = "#000000" } fnt := xlsxFont{ - B: formatStyle.Font.Bold, - I: formatStyle.Font.Italic, + B: &formatStyle.Font.Bold, + I: &formatStyle.Font.Italic, Sz: &attrValFloat{Val: formatStyle.Font.Size}, Color: &xlsxColor{RGB: getPaletteColor(formatStyle.Font.Color)}, Name: &attrValString{Val: formatStyle.Font.Family}, diff --git a/xmlStyles.go b/xmlStyles.go index 46587daf99..49abe3c636 100755 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -88,13 +88,13 @@ type xlsxFont struct { Name *attrValString `xml:"name"` Charset *attrValInt `xml:"charset"` Family *attrValInt `xml:"family"` - B bool `xml:"b,omitempty"` - I bool `xml:"i,omitempty"` - Strike bool `xml:"strike,omitempty"` - Outline bool `xml:"outline,omitempty"` - Shadow bool `xml:"shadow,omitempty"` - Condense bool `xml:"condense,omitempty"` - Extend bool `xml:"extend,omitempty"` + B *bool `xml:"b,omitempty"` + I *bool `xml:"i,omitempty"` + Strike *bool `xml:"strike,omitempty"` + Outline *bool `xml:"outline,omitempty"` + Shadow *bool `xml:"shadow,omitempty"` + Condense *bool `xml:"condense,omitempty"` + Extend *bool `xml:"extend,omitempty"` Color *xlsxColor `xml:"color"` Sz *attrValFloat `xml:"sz"` U *attrValString `xml:"u"` From 46a3632ee0f441c8990a7205445dfdb00823a6ad Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 9 Jun 2019 09:53:02 +0800 Subject: [PATCH 102/957] Fix #422, avoid accent theme color index overflow --- chart.go | 14 +++++++++----- chart_test.go | 50 ++++++++++++++++++++++++++++++++++++++++++++++++ excelize_test.go | 49 ----------------------------------------------- 3 files changed, 59 insertions(+), 54 deletions(-) diff --git a/chart.go b/chart.go index 88f48b2135..73c4728f27 100644 --- a/chart.go +++ b/chart.go @@ -946,11 +946,13 @@ func (f *File) drawChartSeriesSpPr(i int, formatSet *formatChart) *cSpPr { Ln: &aLn{ W: 25400, Cap: "rnd", // rnd, sq, flat - SolidFill: &aSolidFill{ - SchemeClr: &aSchemeClr{Val: "accent" + strconv.Itoa(i+1)}, - }, }, } + if i < 6 { + spPrLine.Ln.SolidFill = &aSolidFill{ + SchemeClr: &aSchemeClr{Val: "accent" + strconv.Itoa(i+1)}, + } + } chartSeriesSpPr := map[string]*cSpPr{Area: nil, AreaStacked: nil, AreaPercentStacked: nil, Area3D: nil, Area3DStacked: nil, Area3DPercentStacked: nil, Bar: nil, BarStacked: nil, BarPercentStacked: nil, Bar3DClustered: nil, Bar3DStacked: nil, Bar3DPercentStacked: nil, Col: nil, ColStacked: nil, ColPercentStacked: nil, Col3DClustered: nil, Col3D: nil, Col3DStacked: nil, Col3DPercentStacked: nil, Doughnut: nil, Line: spPrLine, Pie: nil, Pie3D: nil, Radar: nil, Scatter: spPrScatter} return chartSeriesSpPr[formatSet.Type] } @@ -1014,7 +1016,9 @@ func (f *File) drawChartSeriesMarker(i int, formatSet *formatChart) *cMarker { marker := &cMarker{ Symbol: &attrValString{Val: "circle"}, Size: &attrValInt{Val: 5}, - SpPr: &cSpPr{ + } + if i < 6 { + marker.SpPr = &cSpPr{ SolidFill: &aSolidFill{ SchemeClr: &aSchemeClr{ Val: "accent" + strconv.Itoa(i+1), @@ -1028,7 +1032,7 @@ func (f *File) drawChartSeriesMarker(i int, formatSet *formatChart) *cMarker { }, }, }, - }, + } } chartSeriesMarker := map[string]*cMarker{Area: nil, AreaStacked: nil, AreaPercentStacked: nil, Area3D: nil, Area3DStacked: nil, Area3DPercentStacked: nil, Bar: nil, BarStacked: nil, BarPercentStacked: nil, Bar3DClustered: nil, Bar3DStacked: nil, Bar3DPercentStacked: nil, Col: nil, ColStacked: nil, ColPercentStacked: nil, Col3DClustered: nil, Col3D: nil, Col3DStacked: nil, Col3DPercentStacked: nil, Doughnut: nil, Line: nil, Pie: nil, Pie3D: nil, Radar: nil, Scatter: marker} return chartSeriesMarker[formatSet.Type] diff --git a/chart_test.go b/chart_test.go index 98baeddb4e..b1cd3b0045 100644 --- a/chart_test.go +++ b/chart_test.go @@ -3,6 +3,7 @@ package excelize import ( "bytes" "encoding/xml" + "path/filepath" "testing" "github.com/stretchr/testify/assert" @@ -101,3 +102,52 @@ func TestAddDrawingChart(t *testing.T) { f := NewFile() assert.EqualError(t, f.addDrawingChart("SheetN", "", "", 0, 0, 0, nil), `cannot convert cell "" to coordinates: invalid cell name ""`) } + +func TestAddChart(t *testing.T) { + f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + if !assert.NoError(t, err) { + t.FailNow() + } + + categories := map[string]string{"A30": "SS", "A31": "S", "A32": "M", "A33": "L", "A34": "LL", "A35": "XL", "A36": "XXL", "A37": "XXXL", "B29": "Apple", "C29": "Orange", "D29": "Pear"} + values := map[string]int{"B30": 1, "C30": 1, "D30": 1, "B31": 2, "C31": 2, "D31": 2, "B32": 3, "C32": 3, "D32": 3, "B33": 4, "C33": 4, "D33": 4, "B34": 5, "C34": 5, "D34": 5, "B35": 6, "C35": 6, "D35": 6, "B36": 7, "C36": 7, "D36": 7, "B37": 8, "C37": 8, "D37": 8} + for k, v := range categories { + assert.NoError(t, f.SetCellValue("Sheet1", k, v)) + } + 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 is 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":{"position":"left","show_legend_key":false},"title":{"name":"Fruit 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":"Fruit 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":"Fruit 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":"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},"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":"Fruit 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"}`)) + 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":"Fruit 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", "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":"Fruit 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":"Fruit 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":"Fruit 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":"Fruit 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"}`)) + 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"},{"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","show_legend_key":false},"title":{"name":"Fruit 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"}`)) + 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":"Fruit 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":"Fruit 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":"Fruit 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":"Fruit 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":"Fruit 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":"Fruit 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":"Fruit 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":"Fruit 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}}`)) + // 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":"Fruit 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":"Fruit 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":"Fruit 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":"Fruit 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":"Fruit 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":"Fruit 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"}`)) + + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddChart.xlsx"))) +} diff --git a/excelize_test.go b/excelize_test.go index 1a3dde6c17..e4b2548467 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -882,55 +882,6 @@ func TestAutoFilterError(t *testing.T) { } } -func TestAddChart(t *testing.T) { - f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } - - categories := map[string]string{"A30": "Small", "A31": "Normal", "A32": "Large", "B29": "Apple", "C29": "Orange", "D29": "Pear"} - values := map[string]int{"B30": 2, "C30": 3, "D30": 3, "B31": 5, "C31": 2, "D31": 4, "B32": 6, "C32": 7, "D32": 8} - for k, v := range categories { - assert.NoError(t, f.SetCellValue("Sheet1", k, v)) - } - 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 is 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"}],"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":"Fruit 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"}],"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":"Fruit 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"}],"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":"Fruit 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"}],"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":"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},"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"}],"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":"Fruit 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"}`)) - 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"}],"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":"Fruit 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", "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"}],"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":"Fruit 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"}],"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":"Fruit 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"}],"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":"Fruit 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!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"}],"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":"Fruit 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"}`)) - 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"},{"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"}],"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":"Fruit 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"}`)) - assert.NoError(t, f.AddChart("Sheet2", "P32", `{"type":"pie3D","series":[{"name":"Sheet1!$A$30","categories":"Sheet1!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"}],"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":"Fruit 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!$B$29:$D$29","values":"Sheet1!$B$30:$D$30"}],"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":"Fruit 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"}],"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":"Fruit 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"}],"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":"Fruit 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"}],"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":"Fruit 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"}],"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":"Fruit 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"}],"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":"Fruit 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"}],"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":"Fruit 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}}`)) - // 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"}],"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":"Fruit 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"}],"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":"Fruit 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"}],"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":"Fruit 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"}],"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":"Fruit 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"}],"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":"Fruit 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"}],"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":"Fruit 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"}`)) - - assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddChart.xlsx"))) -} - func TestSetPane(t *testing.T) { f := NewFile() f.SetPanes("Sheet1", `{"freeze":false,"split":false}`) From 821632cf89422b9955160a3af7f28f05a12f70f8 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 12 Jun 2019 08:10:33 +0800 Subject: [PATCH 103/957] Fix #424, refactor merged cells adjuster --- adjust.go | 161 +++++++++++++++++++++++++++----------------- adjust_test.go | 31 +++++++++ cell.go | 22 +++--- rows.go | 2 +- styles.go | 0 table.go | 17 ++--- xmlChart.go | 0 xmlDecodeDrawing.go | 111 ++++++++++++++++++++---------- xmlStyles.go | 0 9 files changed, 221 insertions(+), 123 deletions(-) mode change 100755 => 100644 styles.go mode change 100755 => 100644 xmlChart.go mode change 100755 => 100644 xmlStyles.go diff --git a/adjust.go b/adjust.go index d88990df3c..56d812fc72 100644 --- a/adjust.go +++ b/adjust.go @@ -9,7 +9,10 @@ package excelize -import "strings" +import ( + "errors" + "strings" +) type adjustDirection bool @@ -140,46 +143,85 @@ func (f *File) adjustAutoFilter(xlsx *xlsxWorksheet, dir adjustDirection, num, o return nil } - rng := strings.Split(xlsx.AutoFilter.Ref, ":") - firstCell := rng[0] - lastCell := rng[1] - - firstCol, firstRow, err := CellNameToCoordinates(firstCell) + coordinates, err := f.areaRefToCoordinates(xlsx.AutoFilter.Ref) if err != nil { return err } + x1, y1, x2, y2 := coordinates[0], coordinates[1], coordinates[2], coordinates[3] - lastCol, lastRow, err := CellNameToCoordinates(lastCell) - if err != nil { - return err - } - - if (dir == rows && firstRow == num && offset < 0) || (dir == columns && firstCol == num && lastCol == num) { + if (dir == rows && y1 == num && offset < 0) || (dir == columns && x1 == num && x2 == num) { xlsx.AutoFilter = nil for rowIdx := range xlsx.SheetData.Row { rowData := &xlsx.SheetData.Row[rowIdx] - if rowData.R > firstRow && rowData.R <= lastRow { + if rowData.R > y1 && rowData.R <= y2 { rowData.Hidden = false } } return nil } + coordinates = f.adjustAutoFilterHelper(dir, coordinates, num, offset) + x1, y1, x2, y2 = coordinates[0], coordinates[1], coordinates[2], coordinates[3] + + if xlsx.AutoFilter.Ref, err = f.coordinatesToAreaRef([]int{x1, y1, x2, y2}); err != nil { + return err + } + return nil +} + +// adjustAutoFilterHelper provides a function for adjusting auto filter to +// compare and calculate cell axis by the given adjust direction, operation +// axis and offset. +func (f *File) adjustAutoFilterHelper(dir adjustDirection, coordinates []int, num, offset int) []int { if dir == rows { - if firstRow >= num { - firstCell, _ = CoordinatesToCellName(firstCol, firstRow+offset) + if coordinates[1] >= num { + coordinates[1] += offset } - if lastRow >= num { - lastCell, _ = CoordinatesToCellName(lastCol, lastRow+offset) + if coordinates[3] >= num { + coordinates[3] += offset } } else { - if lastCol >= num { - lastCell, _ = CoordinatesToCellName(lastCol+offset, lastRow) + if coordinates[2] >= num { + coordinates[2] += offset } } + return coordinates +} - xlsx.AutoFilter.Ref = firstCell + ":" + lastCell - return nil +// areaRefToCoordinates provides a function to convert area reference to a +// pair of coordinates. +func (f *File) areaRefToCoordinates(ref string) ([]int, error) { + coordinates := make([]int, 4) + rng := strings.Split(ref, ":") + firstCell := rng[0] + lastCell := rng[1] + var err error + coordinates[0], coordinates[1], err = CellNameToCoordinates(firstCell) + if err != nil { + return coordinates, err + } + coordinates[2], coordinates[3], err = CellNameToCoordinates(lastCell) + if err != nil { + return coordinates, err + } + return coordinates, err +} + +// coordinatesToAreaRef provides a function to convert a pair of coordinates +// to area reference. +func (f *File) coordinatesToAreaRef(coordinates []int) (string, error) { + if len(coordinates) != 4 { + return "", errors.New("coordinates length must be 4") + } + firstCell, err := CoordinatesToCellName(coordinates[0], coordinates[1]) + if err != nil { + return "", err + } + lastCell, err := CoordinatesToCellName(coordinates[2], coordinates[3]) + if err != nil { + return "", err + } + return firstCell + ":" + lastCell, err } // adjustMergeCells provides a function to update merged cells when inserting @@ -190,59 +232,56 @@ func (f *File) adjustMergeCells(xlsx *xlsxWorksheet, dir adjustDirection, num, o } for i, areaData := range xlsx.MergeCells.Cells { - rng := strings.Split(areaData.Ref, ":") - firstCell := rng[0] - lastCell := rng[1] - - firstCol, firstRow, err := CellNameToCoordinates(firstCell) + coordinates, err := f.areaRefToCoordinates(areaData.Ref) if err != nil { return err } - - lastCol, lastRow, err := CellNameToCoordinates(lastCell) - if err != nil { - return err - } - - adjust := func(v int) int { - if v >= num { - v += offset - if v < 1 { - return 1 - } - return v - } - return v - } - + x1, y1, x2, y2 := coordinates[0], coordinates[1], coordinates[2], coordinates[3] if dir == rows { - firstRow = adjust(firstRow) - lastRow = adjust(lastRow) + if y1 == num && y2 == num && offset < 0 { + f.deleteMergeCell(xlsx, i) + } + y1 = f.adjustMergeCellsHelper(y1, num, offset) + y2 = f.adjustMergeCellsHelper(y2, num, offset) } else { - firstCol = adjust(firstCol) - lastCol = adjust(lastCol) - } - - if firstCol == lastCol && firstRow == lastRow { - if len(xlsx.MergeCells.Cells) > 1 { - xlsx.MergeCells.Cells = append(xlsx.MergeCells.Cells[:i], xlsx.MergeCells.Cells[i+1:]...) - xlsx.MergeCells.Count = len(xlsx.MergeCells.Cells) - } else { - xlsx.MergeCells = nil + if x1 == num && x2 == num && offset < 0 { + f.deleteMergeCell(xlsx, i) } + x1 = f.adjustMergeCellsHelper(x1, num, offset) + x2 = f.adjustMergeCellsHelper(x2, num, offset) } - - if firstCell, err = CoordinatesToCellName(firstCol, firstRow); err != nil { + if x1 == x2 && y1 == y2 { + f.deleteMergeCell(xlsx, i) + } + if areaData.Ref, err = f.coordinatesToAreaRef([]int{x1, y1, x2, y2}); err != nil { return err } + } + return nil +} - if lastCell, err = CoordinatesToCellName(lastCol, lastRow); err != nil { - return err +// adjustMergeCellsHelper provides a function for adjusting merge cells to +// compare and calculate cell axis by the given pivot, operation axis and +// offset. +func (f *File) adjustMergeCellsHelper(pivot, num, offset int) int { + if pivot >= num { + pivot += offset + if pivot < 1 { + return 1 } + return pivot + } + return pivot +} - areaData.Ref = firstCell + ":" + lastCell +// deleteMergeCell provides a function to delete merged cell by given index. +func (f *File) deleteMergeCell(sheet *xlsxWorksheet, idx int) { + if len(sheet.MergeCells.Cells) > 1 { + sheet.MergeCells.Cells = append(sheet.MergeCells.Cells[:idx], sheet.MergeCells.Cells[idx+1:]...) + sheet.MergeCells.Count = len(sheet.MergeCells.Cells) + } else { + sheet.MergeCells = nil } - return nil } // adjustCalcChain provides a function to update the calculation chain when diff --git a/adjust_test.go b/adjust_test.go index 28432bc631..364a8b8ab1 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -27,6 +27,24 @@ func TestAdjustMergeCells(t *testing.T) { }, }, }, rows, 0, 0), `cannot convert cell "B" to coordinates: invalid cell name "B"`) + assert.NoError(t, f.adjustMergeCells(&xlsxWorksheet{ + MergeCells: &xlsxMergeCells{ + Cells: []*xlsxMergeCell{ + { + Ref: "A1:B1", + }, + }, + }, + }, rows, 1, -1)) + assert.NoError(t, f.adjustMergeCells(&xlsxWorksheet{ + MergeCells: &xlsxMergeCells{ + Cells: []*xlsxMergeCell{ + { + Ref: "A1:A2", + }, + }, + }, + }, columns, 1, -1)) } func TestAdjustAutoFilter(t *testing.T) { @@ -83,3 +101,16 @@ func TestAdjustCalcChain(t *testing.T) { f.CalcChain = nil assert.NoError(t, f.InsertCol("Sheet1", "A")) } + +func TestCoordinatesToAreaRef(t *testing.T) { + f := NewFile() + ref, err := f.coordinatesToAreaRef([]int{}) + assert.EqualError(t, err, "coordinates length must be 4") + ref, err = f.coordinatesToAreaRef([]int{1, -1, 1, 1}) + assert.EqualError(t, err, "invalid cell coordinates [1, -1]") + ref, err = f.coordinatesToAreaRef([]int{1, 1, 1, -1}) + assert.EqualError(t, err, "invalid cell coordinates [1, -1]") + ref, err = f.coordinatesToAreaRef([]int{1, 1, 1, 1}) + assert.NoError(t, err) + assert.EqualValues(t, ref, "A1:A1") +} diff --git a/cell.go b/cell.go index bd4d93be98..6743e2a592 100644 --- a/cell.go +++ b/cell.go @@ -401,31 +401,27 @@ func (f *File) SetCellHyperLink(sheet, axis, link, linkType string) error { // If you create a merged cell that overlaps with another existing merged cell, // those merged cells that already exist will be removed. func (f *File) MergeCell(sheet, hcell, vcell string) error { - hcol, hrow, err := CellNameToCoordinates(hcell) + coordinates, err := f.areaRefToCoordinates(hcell + ":" + vcell) if err != nil { return err } + x1, y1, x2, y2 := coordinates[0], coordinates[1], coordinates[2], coordinates[3] - vcol, vrow, err := CellNameToCoordinates(vcell) - if err != nil { - return err - } - - if hcol == vcol && hrow == vrow { + if x1 == x2 && y1 == y2 { return err } // Correct the coordinate area, such correct C1:B3 to B1:C3. - if vcol < hcol { - hcol, vcol = vcol, hcol + if x2 < x1 { + x1, x2 = x2, x1 } - if vrow < hrow { - hrow, vrow = vrow, hrow + if y2 < y1 { + y1, y2 = y2, y1 } - hcell, _ = CoordinatesToCellName(hcol, hrow) - vcell, _ = CoordinatesToCellName(vcol, vrow) + hcell, _ = CoordinatesToCellName(x1, y1) + vcell, _ = CoordinatesToCellName(x2, y2) xlsx, err := f.workSheetReader(sheet) if err != nil { diff --git a/rows.go b/rows.go index 249ca2f62c..064fefe4b7 100644 --- a/rows.go +++ b/rows.go @@ -421,7 +421,7 @@ func (f *File) RemoveRow(sheet string, row int) error { return err } if row > len(xlsx.SheetData.Row) { - return nil + return f.adjustHelper(sheet, rows, row, -1) } for rowIdx := range xlsx.SheetData.Row { if xlsx.SheetData.Row[rowIdx].R == row { diff --git a/styles.go b/styles.go old mode 100755 new mode 100644 diff --git a/table.go b/table.go index f3819d3fbf..3d8d40279a 100644 --- a/table.go +++ b/table.go @@ -115,29 +115,24 @@ func (f *File) addSheetTable(sheet string, rID int) { // addTable provides a function to add table by given worksheet name, // coordinate area and format set. -func (f *File) addTable(sheet, tableXML string, hcol, hrow, vcol, vrow, i int, formatSet *formatTable) error { +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 hrow == vrow { - vrow++ + if y1 == y2 { + y2++ } // Correct table reference coordinate area, such correct C1:B3 to B1:C3. - hcell, err := CoordinatesToCellName(hcol, hrow) + ref, err := f.coordinatesToAreaRef([]int{x1, y1, x2, y2}) if err != nil { return err } - vcell, err := CoordinatesToCellName(vcol, vrow) - if err != nil { - return err - } - ref := hcell + ":" + vcell var tableColumn []*xlsxTableColumn idx := 0 - for i := hcol; i <= vcol; i++ { + for i := x1; i <= x2; i++ { idx++ - cell, err := CoordinatesToCellName(i, hrow) + cell, err := CoordinatesToCellName(i, y1) if err != nil { return err } diff --git a/xmlChart.go b/xmlChart.go old mode 100755 new mode 100644 diff --git a/xmlDecodeDrawing.go b/xmlDecodeDrawing.go index eead575265..6cb224a7a8 100644 --- a/xmlDecodeDrawing.go +++ b/xmlDecodeDrawing.go @@ -11,19 +11,55 @@ package excelize import "encoding/xml" -// decodeCellAnchor directly maps the oneCellAnchor (One Cell Anchor Shape Size) -// and twoCellAnchor (Two Cell Anchor Shape Size). This element specifies a two -// cell anchor placeholder for a group, a shape, or a drawing element. It moves -// with cells and its extents are in EMU units. +// decodeCellAnchor directly maps the oneCellAnchor (One Cell Anchor Shape +// Size) and twoCellAnchor (Two Cell Anchor Shape Size). This element +// specifies a two cell anchor placeholder for a group, a shape, or a drawing +// element. It moves with cells and its extents are in EMU units. type decodeCellAnchor struct { - EditAs string `xml:"editAs,attr,omitempty"` - Content string `xml:",innerxml"` + EditAs string `xml:"editAs,attr,omitempty"` + From *decodeFrom `xml:"from"` + To *decodeTo `xml:"to"` + Sp *decodeSp `xml:"sp"` + ClientData *decodeClientData `xml:"clientData"` + Content string `xml:",innerxml"` +} + +// xdrSp (Shape) directly maps the sp element. This element specifies the +// existence of a single shape. A shape can either be a preset or a custom +// geometry, defined using the SpreadsheetDrawingML framework. In addition to +// a geometry each shape can have both visual and non-visual properties +// attached. Text and corresponding styling information can also be attached +// to a shape. This shape is specified along with all other shapes within +// either the shape tree or group shape elements. +type decodeSp struct { + NvSpPr *decodeNvSpPr `xml:"nvSpPr"` + SpPr *decodeSpPr `xml:"spPr"` +} + +// decodeSp (Non-Visual Properties for a Shape) directly maps the nvSpPr +// element. This element specifies all non-visual properties for a shape. This +// element is a container for the non-visual identification properties, shape +// properties and application properties that are to be associated with a +// shape. This allows for additional information that does not affect the +// appearance of the shape to be stored. +type decodeNvSpPr struct { + CNvPr *decodeCNvPr `xml:"cNvPr"` + ExtLst *decodeExt `xml:"extLst"` + CNvSpPr *decodeCNvSpPr `xml:"cNvSpPr"` +} + +// decodeCNvSpPr (Connection Non-Visual Shape Properties) directly maps the +// cNvSpPr element. This element specifies the set of non-visual properties +// for a connection shape. These properties specify all data about the +// connection shape which do not affect its display within a spreadsheet. +type decodeCNvSpPr struct { + TxBox bool `xml:"txBox,attr"` } // decodeWsDr directly maps the root element for a part of this content type -// shall wsDr. In order to solve the problem that the label structure is changed -// after serialization and deserialization, two different structures are -// defined. decodeWsDr just for deserialization. +// shall wsDr. In order to solve the problem that the label structure is +// changed after serialization and deserialization, two different structures +// are defined. decodeWsDr just for deserialization. type decodeWsDr struct { A string `xml:"xmlns a,attr"` Xdr string `xml:"xmlns xdr,attr"` @@ -34,9 +70,9 @@ type decodeWsDr struct { } // decodeTwoCellAnchor directly maps the oneCellAnchor (One Cell Anchor Shape -// Size) and twoCellAnchor (Two Cell Anchor Shape Size). This element specifies -// a two cell anchor placeholder for a group, a shape, or a drawing element. It -// moves with cells and its extents are in EMU units. +// Size) and twoCellAnchor (Two Cell Anchor Shape Size). This element +// specifies a two cell anchor placeholder for a group, a shape, or a drawing +// element. It moves with cells and its extents are in EMU units. type decodeTwoCellAnchor struct { From *decodeFrom `xml:"from"` To *decodeTo `xml:"to"` @@ -46,7 +82,8 @@ type decodeTwoCellAnchor struct { // decodeCNvPr 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. +// information that does not affect the appearance of the picture to be +// stored. type decodeCNvPr struct { ID int `xml:"id,attr"` Name string `xml:"name,attr"` @@ -55,8 +92,8 @@ type decodeCNvPr struct { } // decodePicLocks directly maps the picLocks (Picture Locks). This element -// specifies all locking properties for a graphic frame. These properties inform -// the generating application about specific properties that have been +// specifies all locking properties for a graphic frame. These properties +// inform the generating application about specific properties that have been // previously locked and thus should not be changed. type decodePicLocks struct { NoAdjustHandles bool `xml:"noAdjustHandles,attr,omitempty"` @@ -82,9 +119,9 @@ type decodeBlip struct { R string `xml:"r,attr"` } -// decodeStretch directly maps the stretch element. This element specifies that -// a BLIP should be stretched to fill the target rectangle. The other option is -// a tile where a BLIP is tiled to fill the available area. +// decodeStretch directly maps the stretch element. This element specifies +// that a BLIP should be stretched to fill the target rectangle. The other +// option is a tile where a BLIP is tiled to fill the available area. type decodeStretch struct { FillRect string `xml:"fillRect"` } @@ -128,12 +165,12 @@ type decodeCNvPicPr struct { PicLocks decodePicLocks `xml:"picLocks"` } -// directly maps the nvPicPr (Non-Visual Properties for a Picture). This element -// specifies all non-visual properties for a picture. This element is a -// container for the non-visual identification properties, shape properties and -// application properties that are to be associated with a picture. This allows -// for additional information that does not affect the appearance of the picture -// to be stored. +// directly maps the nvPicPr (Non-Visual Properties for a Picture). This +// element specifies all non-visual properties for a picture. This element is +// a container for the non-visual identification properties, shape properties +// and application properties that are to be associated with a picture. This +// allows for additional information that does not affect the appearance of +// the picture to be stored. type decodeNvPicPr struct { CNvPr decodeCNvPr `xml:"cNvPr"` CNvPicPr decodeCNvPicPr `xml:"cNvPicPr"` @@ -148,20 +185,20 @@ type decodeBlipFill struct { Stretch decodeStretch `xml:"stretch"` } -// decodeSpPr directly maps the spPr (Shape Properties). This element specifies -// the visual shape properties that can be applied to a picture. These are the -// same properties that are allowed to describe the visual properties of a shape -// but are used here to describe the visual appearance of a picture within a -// document. +// decodeSpPr directly maps the spPr (Shape Properties). This element +// specifies the visual shape properties that can be applied to a picture. +// These are the same properties that are allowed to describe the visual +// properties of a shape but are used here to describe the visual appearance +// of a picture within a document. type decodeSpPr struct { - Xfrm decodeXfrm `xml:"a:xfrm"` - PrstGeom decodePrstGeom `xml:"a:prstGeom"` + Xfrm decodeXfrm `xml:"xfrm"` + PrstGeom decodePrstGeom `xml:"prstGeom"` } -// 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. +// 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. type decodePic struct { NvPicPr decodeNvPicPr `xml:"nvPicPr"` BlipFill decodeBlipFill `xml:"blipFill"` @@ -184,8 +221,8 @@ type decodeTo struct { RowOff int `xml:"rowOff"` } -// decodeClientData directly maps the clientData element. An empty element which -// specifies (via attributes) certain properties related to printing and +// decodeClientData directly maps the clientData element. An empty element +// which specifies (via attributes) certain properties related to printing and // selection of the drawing object. The fLocksWithSheet attribute (either true // or false) determines whether to disable selection when the sheet is // protected, and fPrintsWithSheet attribute (either true or false) determines diff --git a/xmlStyles.go b/xmlStyles.go old mode 100755 new mode 100644 From e124f6000a2ea731b96a07d6bf2901781e272d90 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 13 Jun 2019 08:25:35 +0800 Subject: [PATCH 104/957] Fix #425, handle empty font style format --- styles.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/styles.go b/styles.go index 5c4f66e748..1c014218f8 100644 --- a/styles.go +++ b/styles.go @@ -1979,13 +1979,17 @@ func (f *File) setFont(formatStyle *formatStyle) *xlsxFont { formatStyle.Font.Color = "#000000" } fnt := xlsxFont{ - B: &formatStyle.Font.Bold, - I: &formatStyle.Font.Italic, Sz: &attrValFloat{Val: formatStyle.Font.Size}, Color: &xlsxColor{RGB: getPaletteColor(formatStyle.Font.Color)}, Name: &attrValString{Val: formatStyle.Font.Family}, Family: &attrValInt{Val: 2}, } + if formatStyle.Font.Bold { + fnt.B = &formatStyle.Font.Bold + } + if formatStyle.Font.Italic { + fnt.I = &formatStyle.Font.Italic + } if fnt.Name.Val == "" { fnt.Name.Val = f.GetDefaultFont() } From dc0869fde3a717009eb4aeff6a26387f0495b655 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 14 Jun 2019 00:05:10 +0800 Subject: [PATCH 105/957] support to create cone, pyramid and cylinder series chart for column and bar types --- chart.go | 930 +++++++++++++++++++++++++++++++++++--------------- chart_test.go | 75 ++-- xmlChart.go | 1 + 3 files changed, 706 insertions(+), 300 deletions(-) diff --git a/chart.go b/chart.go index 73c4728f27..8ecd8c73a7 100644 --- a/chart.go +++ b/chart.go @@ -12,148 +12,254 @@ package excelize import ( "encoding/json" "encoding/xml" + "errors" "strconv" "strings" ) // This section defines the currently supported chart types. 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" - Col = "col" - ColStacked = "colStacked" - ColPercentStacked = "colPercentStacked" - Col3DClustered = "col3DClustered" - Col3D = "col3D" - Col3DStacked = "col3DStacked" - Col3DPercentStacked = "col3DPercentStacked" - Doughnut = "doughnut" - Line = "line" - Pie = "pie" - Pie3D = "pie3D" - Radar = "radar" - Scatter = "scatter" + 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" + Pie = "pie" + Pie3D = "pie3D" + Radar = "radar" + Scatter = "scatter" ) // This section defines the default value of chart properties. var ( chartView3DRotX = map[string]int{ - Area: 0, - AreaStacked: 0, - AreaPercentStacked: 0, - Area3D: 15, - Area3DStacked: 15, - Area3DPercentStacked: 15, - Bar: 0, - BarStacked: 0, - BarPercentStacked: 0, - Bar3DClustered: 15, - Bar3DStacked: 15, - Bar3DPercentStacked: 15, - Col: 0, - ColStacked: 0, - ColPercentStacked: 0, - Col3DClustered: 15, - Col3D: 15, - Col3DStacked: 15, - Col3DPercentStacked: 15, - Doughnut: 0, - Line: 0, - Pie: 0, - Pie3D: 30, - Radar: 0, - Scatter: 0, + Area: 0, + AreaStacked: 0, + AreaPercentStacked: 0, + Area3D: 15, + Area3DStacked: 15, + Area3DPercentStacked: 15, + Bar: 0, + BarStacked: 0, + BarPercentStacked: 0, + Bar3DClustered: 15, + Bar3DStacked: 15, + Bar3DPercentStacked: 15, + Bar3DConeClustered: 15, + Bar3DConeStacked: 15, + Bar3DConePercentStacked: 15, + Bar3DPyramidClustered: 15, + Bar3DPyramidStacked: 15, + Bar3DPyramidPercentStacked: 15, + Bar3DCylinderClustered: 15, + Bar3DCylinderStacked: 15, + Bar3DCylinderPercentStacked: 15, + Col: 0, + ColStacked: 0, + ColPercentStacked: 0, + Col3D: 15, + Col3DClustered: 15, + Col3DStacked: 15, + Col3DPercentStacked: 15, + Col3DCone: 15, + Col3DConeClustered: 15, + Col3DConeStacked: 15, + Col3DConePercentStacked: 15, + Col3DPyramid: 15, + Col3DPyramidClustered: 15, + Col3DPyramidStacked: 15, + Col3DPyramidPercentStacked: 15, + Col3DCylinder: 15, + Col3DCylinderClustered: 15, + Col3DCylinderStacked: 15, + Col3DCylinderPercentStacked: 15, + Doughnut: 0, + Line: 0, + Pie: 0, + Pie3D: 30, + Radar: 0, + Scatter: 0, } chartView3DRotY = map[string]int{ - Area: 0, - AreaStacked: 0, - AreaPercentStacked: 0, - Area3D: 20, - Area3DStacked: 20, - Area3DPercentStacked: 20, - Bar: 0, - BarStacked: 0, - BarPercentStacked: 0, - Bar3DClustered: 20, - Bar3DStacked: 20, - Bar3DPercentStacked: 20, - Col: 0, - ColStacked: 0, - ColPercentStacked: 0, - Col3DClustered: 20, - Col3D: 20, - Col3DStacked: 20, - Col3DPercentStacked: 20, - Doughnut: 0, - Line: 0, - Pie: 0, - Pie3D: 0, - Radar: 0, - Scatter: 0, + Area: 0, + AreaStacked: 0, + AreaPercentStacked: 0, + Area3D: 20, + Area3DStacked: 20, + Area3DPercentStacked: 20, + Bar: 0, + BarStacked: 0, + BarPercentStacked: 0, + Bar3DClustered: 20, + Bar3DStacked: 20, + Bar3DPercentStacked: 20, + Bar3DConeClustered: 20, + Bar3DConeStacked: 20, + Bar3DConePercentStacked: 20, + Bar3DPyramidClustered: 20, + Bar3DPyramidStacked: 20, + Bar3DPyramidPercentStacked: 20, + Bar3DCylinderClustered: 20, + Bar3DCylinderStacked: 20, + Bar3DCylinderPercentStacked: 20, + Col: 0, + ColStacked: 0, + ColPercentStacked: 0, + Col3D: 20, + Col3DClustered: 20, + Col3DStacked: 20, + Col3DPercentStacked: 20, + Col3DCone: 20, + Col3DConeClustered: 20, + Col3DConeStacked: 20, + Col3DConePercentStacked: 20, + Col3DPyramid: 20, + Col3DPyramidClustered: 20, + Col3DPyramidStacked: 20, + Col3DPyramidPercentStacked: 20, + Col3DCylinder: 20, + Col3DCylinderClustered: 20, + Col3DCylinderStacked: 20, + Col3DCylinderPercentStacked: 20, + Doughnut: 0, + Line: 0, + Pie: 0, + Pie3D: 0, + Radar: 0, + Scatter: 0, } chartView3DDepthPercent = map[string]int{ - Area: 100, - AreaStacked: 100, - AreaPercentStacked: 100, - Area3D: 100, - Area3DStacked: 100, - Area3DPercentStacked: 100, - Bar: 100, - BarStacked: 100, - BarPercentStacked: 100, - Bar3DClustered: 100, - Bar3DStacked: 100, - Bar3DPercentStacked: 100, - Col: 100, - ColStacked: 100, - ColPercentStacked: 100, - Col3DClustered: 100, - Col3D: 100, - Col3DStacked: 100, - Col3DPercentStacked: 100, - Doughnut: 100, - Line: 100, - Pie: 100, - Pie3D: 100, - Radar: 100, - Scatter: 100, + Area: 100, + AreaStacked: 100, + AreaPercentStacked: 100, + Area3D: 100, + Area3DStacked: 100, + Area3DPercentStacked: 100, + Bar: 100, + BarStacked: 100, + BarPercentStacked: 100, + Bar3DClustered: 100, + Bar3DStacked: 100, + Bar3DPercentStacked: 100, + Bar3DConeClustered: 100, + Bar3DConeStacked: 100, + Bar3DConePercentStacked: 100, + Bar3DPyramidClustered: 100, + Bar3DPyramidStacked: 100, + Bar3DPyramidPercentStacked: 100, + Bar3DCylinderClustered: 100, + Bar3DCylinderStacked: 100, + Bar3DCylinderPercentStacked: 100, + Col: 100, + ColStacked: 100, + ColPercentStacked: 100, + Col3D: 100, + Col3DClustered: 100, + Col3DStacked: 100, + Col3DPercentStacked: 100, + Col3DCone: 100, + Col3DConeClustered: 100, + Col3DConeStacked: 100, + Col3DConePercentStacked: 100, + Col3DPyramid: 100, + Col3DPyramidClustered: 100, + Col3DPyramidStacked: 100, + Col3DPyramidPercentStacked: 100, + Col3DCylinder: 100, + Col3DCylinderClustered: 100, + Col3DCylinderStacked: 100, + Col3DCylinderPercentStacked: 100, + Doughnut: 100, + Line: 100, + Pie: 100, + Pie3D: 100, + Radar: 100, + Scatter: 100, } chartView3DRAngAx = map[string]int{ - Area: 0, - AreaStacked: 0, - AreaPercentStacked: 0, - Area3D: 1, - Area3DStacked: 1, - Area3DPercentStacked: 1, - Bar: 0, - BarStacked: 0, - BarPercentStacked: 0, - Bar3DClustered: 1, - Bar3DStacked: 1, - Bar3DPercentStacked: 1, - Col: 0, - ColStacked: 0, - ColPercentStacked: 0, - Col3DClustered: 1, - Col3D: 1, - Col3DStacked: 1, - Col3DPercentStacked: 1, - Doughnut: 0, - Line: 0, - Pie: 0, - Pie3D: 0, - Radar: 0, - Scatter: 0, + Area: 0, + AreaStacked: 0, + AreaPercentStacked: 0, + Area3D: 1, + Area3DStacked: 1, + Area3DPercentStacked: 1, + Bar: 0, + BarStacked: 0, + BarPercentStacked: 0, + Bar3DClustered: 1, + Bar3DStacked: 1, + Bar3DPercentStacked: 1, + Bar3DConeClustered: 1, + Bar3DConeStacked: 1, + Bar3DConePercentStacked: 1, + Bar3DPyramidClustered: 1, + Bar3DPyramidStacked: 1, + Bar3DPyramidPercentStacked: 1, + Bar3DCylinderClustered: 1, + Bar3DCylinderStacked: 1, + Bar3DCylinderPercentStacked: 1, + Col: 0, + ColStacked: 0, + ColPercentStacked: 0, + Col3D: 1, + Col3DClustered: 1, + Col3DStacked: 1, + Col3DPercentStacked: 1, + Col3DCone: 1, + Col3DConeClustered: 1, + Col3DConeStacked: 1, + Col3DConePercentStacked: 1, + Col3DPyramid: 1, + Col3DPyramidClustered: 1, + Col3DPyramidStacked: 1, + Col3DPyramidPercentStacked: 1, + Col3DCylinder: 1, + Col3DCylinderClustered: 1, + Col3DCylinderStacked: 1, + Col3DCylinderPercentStacked: 1, + Doughnut: 0, + Line: 0, + Pie: 0, + Pie3D: 0, + Radar: 0, + Scatter: 0, } chartLegendPosition = map[string]string{ "bottom": "b", @@ -163,96 +269,180 @@ var ( "top_right": "tr", } chartValAxNumFmtFormatCode = map[string]string{ - Area: "General", - AreaStacked: "General", - AreaPercentStacked: "0%", - Area3D: "General", - Area3DStacked: "General", - Area3DPercentStacked: "0%", - Bar: "General", - BarStacked: "General", - BarPercentStacked: "0%", - Bar3DClustered: "General", - Bar3DStacked: "General", - Bar3DPercentStacked: "0%", - Col: "General", - ColStacked: "General", - ColPercentStacked: "0%", - Col3DClustered: "General", - Col3D: "General", - Col3DStacked: "General", - Col3DPercentStacked: "0%", - Doughnut: "General", - Line: "General", - Pie: "General", - Pie3D: "General", - Radar: "General", - Scatter: "General", + Area: "General", + AreaStacked: "General", + AreaPercentStacked: "0%", + Area3D: "General", + Area3DStacked: "General", + Area3DPercentStacked: "0%", + Bar: "General", + BarStacked: "General", + BarPercentStacked: "0%", + Bar3DClustered: "General", + Bar3DStacked: "General", + Bar3DPercentStacked: "0%", + Bar3DConeClustered: "General", + Bar3DConeStacked: "General", + Bar3DConePercentStacked: "0%", + Bar3DPyramidClustered: "General", + Bar3DPyramidStacked: "General", + Bar3DPyramidPercentStacked: "0%", + Bar3DCylinderClustered: "General", + Bar3DCylinderStacked: "General", + Bar3DCylinderPercentStacked: "0%", + Col: "General", + ColStacked: "General", + ColPercentStacked: "0%", + Col3D: "General", + Col3DClustered: "General", + Col3DStacked: "General", + Col3DPercentStacked: "0%", + Col3DCone: "General", + Col3DConeClustered: "General", + Col3DConeStacked: "General", + Col3DConePercentStacked: "0%", + Col3DPyramid: "General", + Col3DPyramidClustered: "General", + Col3DPyramidStacked: "General", + Col3DPyramidPercentStacked: "0%", + Col3DCylinder: "General", + Col3DCylinderClustered: "General", + Col3DCylinderStacked: "General", + Col3DCylinderPercentStacked: "0%", + Doughnut: "General", + Line: "General", + Pie: "General", + Pie3D: "General", + Radar: "General", + Scatter: "General", } chartValAxCrossBetween = map[string]string{ - Area: "midCat", - AreaStacked: "midCat", - AreaPercentStacked: "midCat", - Area3D: "midCat", - Area3DStacked: "midCat", - Area3DPercentStacked: "midCat", - Bar: "between", - BarStacked: "between", - BarPercentStacked: "between", - Bar3DClustered: "between", - Bar3DStacked: "between", - Bar3DPercentStacked: "between", - Col: "between", - ColStacked: "between", - ColPercentStacked: "between", - Col3DClustered: "between", - Col3D: "between", - Col3DStacked: "between", - Col3DPercentStacked: "between", - Doughnut: "between", - Line: "between", - Pie: "between", - Pie3D: "between", - Radar: "between", - Scatter: "between", + Area: "midCat", + AreaStacked: "midCat", + AreaPercentStacked: "midCat", + Area3D: "midCat", + Area3DStacked: "midCat", + Area3DPercentStacked: "midCat", + Bar: "between", + BarStacked: "between", + BarPercentStacked: "between", + Bar3DClustered: "between", + Bar3DStacked: "between", + Bar3DPercentStacked: "between", + Bar3DConeClustered: "between", + Bar3DConeStacked: "between", + Bar3DConePercentStacked: "between", + Bar3DPyramidClustered: "between", + Bar3DPyramidStacked: "between", + Bar3DPyramidPercentStacked: "between", + Bar3DCylinderClustered: "between", + Bar3DCylinderStacked: "between", + Bar3DCylinderPercentStacked: "between", + Col: "between", + ColStacked: "between", + ColPercentStacked: "between", + Col3D: "between", + Col3DClustered: "between", + Col3DStacked: "between", + Col3DPercentStacked: "between", + Col3DCone: "between", + Col3DConeClustered: "between", + Col3DConeStacked: "between", + Col3DConePercentStacked: "between", + Col3DPyramid: "between", + Col3DPyramidClustered: "between", + Col3DPyramidStacked: "between", + Col3DPyramidPercentStacked: "between", + Col3DCylinder: "between", + Col3DCylinderClustered: "between", + Col3DCylinderStacked: "between", + Col3DCylinderPercentStacked: "between", + Doughnut: "between", + Line: "between", + Pie: "between", + Pie3D: "between", + Radar: "between", + Scatter: "between", } plotAreaChartGrouping = map[string]string{ - Area: "standard", - AreaStacked: "stacked", - AreaPercentStacked: "percentStacked", - Area3D: "standard", - Area3DStacked: "stacked", - Area3DPercentStacked: "percentStacked", - Bar: "clustered", - BarStacked: "stacked", - BarPercentStacked: "percentStacked", - Bar3DClustered: "clustered", - Bar3DStacked: "stacked", - Bar3DPercentStacked: "percentStacked", - Col: "clustered", - ColStacked: "stacked", - ColPercentStacked: "percentStacked", - Col3DClustered: "clustered", - Col3D: "standard", - Col3DStacked: "stacked", - Col3DPercentStacked: "percentStacked", - Line: "standard", + Area: "standard", + AreaStacked: "stacked", + AreaPercentStacked: "percentStacked", + Area3D: "standard", + Area3DStacked: "stacked", + Area3DPercentStacked: "percentStacked", + Bar: "clustered", + BarStacked: "stacked", + BarPercentStacked: "percentStacked", + Bar3DClustered: "clustered", + Bar3DStacked: "stacked", + Bar3DPercentStacked: "percentStacked", + Bar3DConeClustered: "clustered", + Bar3DConeStacked: "stacked", + Bar3DConePercentStacked: "percentStacked", + Bar3DPyramidClustered: "clustered", + Bar3DPyramidStacked: "stacked", + Bar3DPyramidPercentStacked: "percentStacked", + Bar3DCylinderClustered: "clustered", + Bar3DCylinderStacked: "stacked", + Bar3DCylinderPercentStacked: "percentStacked", + Col: "clustered", + ColStacked: "stacked", + ColPercentStacked: "percentStacked", + Col3D: "standard", + Col3DClustered: "clustered", + Col3DStacked: "stacked", + Col3DPercentStacked: "percentStacked", + Col3DCone: "standard", + Col3DConeClustered: "clustered", + Col3DConeStacked: "stacked", + Col3DConePercentStacked: "percentStacked", + Col3DPyramid: "standard", + Col3DPyramidClustered: "clustered", + Col3DPyramidStacked: "stacked", + Col3DPyramidPercentStacked: "percentStacked", + Col3DCylinder: "standard", + Col3DCylinderClustered: "clustered", + Col3DCylinderStacked: "stacked", + Col3DCylinderPercentStacked: "percentStacked", + Line: "standard", } plotAreaChartBarDir = map[string]string{ - Bar: "bar", - BarStacked: "bar", - BarPercentStacked: "bar", - Bar3DClustered: "bar", - Bar3DStacked: "bar", - Bar3DPercentStacked: "bar", - Col: "col", - ColStacked: "col", - ColPercentStacked: "col", - Col3DClustered: "col", - Col3D: "col", - Col3DStacked: "col", - Col3DPercentStacked: "col", - Line: "standard", + Bar: "bar", + BarStacked: "bar", + BarPercentStacked: "bar", + Bar3DClustered: "bar", + Bar3DStacked: "bar", + Bar3DPercentStacked: "bar", + Bar3DConeClustered: "bar", + Bar3DConeStacked: "bar", + Bar3DConePercentStacked: "bar", + Bar3DPyramidClustered: "bar", + Bar3DPyramidStacked: "bar", + Bar3DPyramidPercentStacked: "bar", + Bar3DCylinderClustered: "bar", + Bar3DCylinderStacked: "bar", + Bar3DCylinderPercentStacked: "bar", + Col: "col", + ColStacked: "col", + ColPercentStacked: "col", + Col3D: "col", + Col3DClustered: "col", + Col3DStacked: "col", + Col3DPercentStacked: "col", + Col3DCone: "col", + Col3DConeStacked: "col", + Col3DConeClustered: "col", + Col3DConePercentStacked: "col", + Col3DPyramid: "col", + Col3DPyramidClustered: "col", + Col3DPyramidStacked: "col", + Col3DPyramidPercentStacked: "col", + Col3DCylinder: "col", + Col3DCylinderClustered: "col", + Col3DCylinderStacked: "col", + Col3DCylinderPercentStacked: "col", + Line: "standard", } orientation = map[bool]string{ true: "maxMin", @@ -335,33 +525,54 @@ func parseFormatChartSet(formatSet string) (*formatChart, 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 -// 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 -// doughnut | doughnut chart -// line | line chart -// pie | pie chart -// pie3D | 3D pie chart -// radar | radar chart -// scatter | scatter 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 +// radar | radar chart +// scatter | scatter chart // // In Excel a chart series is a collection of information that defines which data is plotted such as values, axis labels and formatting. // @@ -457,6 +668,9 @@ func (f *File) AddChart(sheet, cell, format string) error { if err != nil { return err } + if _, ok := chartValAxNumFmtFormatCode[formatSet.Type]; !ok { + return errors.New("unsupported chart type " + formatSet.Type) + } // Add first picture for given sheet, create xl/drawings/ and xl/drawings/_rels/ folder. drawingID := f.countDrawings() + 1 chartID := f.countCharts() + 1 @@ -631,31 +845,52 @@ func (f *File) addChart(formatSet *formatChart) { }, } plotAreaFunc := map[string]func(*formatChart) *cPlotArea{ - Area: f.drawBaseChart, - AreaStacked: f.drawBaseChart, - AreaPercentStacked: f.drawBaseChart, - Area3D: f.drawBaseChart, - Area3DStacked: f.drawBaseChart, - Area3DPercentStacked: f.drawBaseChart, - Bar: f.drawBaseChart, - BarStacked: f.drawBaseChart, - BarPercentStacked: f.drawBaseChart, - Bar3DClustered: f.drawBaseChart, - Bar3DStacked: f.drawBaseChart, - Bar3DPercentStacked: f.drawBaseChart, - Col: f.drawBaseChart, - ColStacked: f.drawBaseChart, - ColPercentStacked: f.drawBaseChart, - Col3DClustered: f.drawBaseChart, - Col3D: f.drawBaseChart, - Col3DStacked: f.drawBaseChart, - Col3DPercentStacked: f.drawBaseChart, - Doughnut: f.drawDoughnutChart, - Line: f.drawLineChart, - Pie3D: f.drawPie3DChart, - Pie: f.drawPieChart, - Radar: f.drawRadarChart, - Scatter: f.drawScatterChart, + Area: f.drawBaseChart, + AreaStacked: f.drawBaseChart, + AreaPercentStacked: f.drawBaseChart, + Area3D: f.drawBaseChart, + Area3DStacked: f.drawBaseChart, + Area3DPercentStacked: f.drawBaseChart, + Bar: f.drawBaseChart, + BarStacked: f.drawBaseChart, + BarPercentStacked: f.drawBaseChart, + Bar3DClustered: f.drawBaseChart, + Bar3DStacked: f.drawBaseChart, + Bar3DPercentStacked: f.drawBaseChart, + Bar3DConeClustered: f.drawBaseChart, + Bar3DConeStacked: f.drawBaseChart, + Bar3DConePercentStacked: f.drawBaseChart, + Bar3DPyramidClustered: f.drawBaseChart, + Bar3DPyramidStacked: f.drawBaseChart, + Bar3DPyramidPercentStacked: f.drawBaseChart, + Bar3DCylinderClustered: f.drawBaseChart, + Bar3DCylinderStacked: f.drawBaseChart, + Bar3DCylinderPercentStacked: f.drawBaseChart, + Col: f.drawBaseChart, + ColStacked: f.drawBaseChart, + ColPercentStacked: f.drawBaseChart, + Col3D: f.drawBaseChart, + Col3DClustered: f.drawBaseChart, + Col3DStacked: f.drawBaseChart, + Col3DPercentStacked: f.drawBaseChart, + Col3DCone: f.drawBaseChart, + Col3DConeClustered: f.drawBaseChart, + Col3DConeStacked: f.drawBaseChart, + Col3DConePercentStacked: f.drawBaseChart, + Col3DPyramid: f.drawBaseChart, + Col3DPyramidClustered: f.drawBaseChart, + Col3DPyramidStacked: f.drawBaseChart, + Col3DPyramidPercentStacked: f.drawBaseChart, + Col3DCylinder: f.drawBaseChart, + Col3DCylinderClustered: f.drawBaseChart, + Col3DCylinderStacked: f.drawBaseChart, + Col3DCylinderPercentStacked: f.drawBaseChart, + Doughnut: f.drawDoughnutChart, + Line: f.drawLineChart, + Pie3D: f.drawPie3DChart, + Pie: f.drawPieChart, + Radar: f.drawRadarChart, + Scatter: f.drawScatterChart, } xlsxChartSpace.Chart.PlotArea = plotAreaFunc[formatSet.Type](formatSet) @@ -678,6 +913,7 @@ func (f *File) drawBaseChart(formatSet *formatChart) *cPlotArea { Val: true, }, Ser: f.drawChartSeries(formatSet), + Shape: f.drawChartShape(formatSet), DLbls: f.drawChartDLbls(formatSet), AxID: []*attrValInt{ {Val: 754001152}, @@ -756,6 +992,51 @@ func (f *File) drawBaseChart(formatSet *formatChart) *cPlotArea { CatAx: catAx, ValAx: valAx, }, + "bar3DConeClustered": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "bar3DConeStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "bar3DConePercentStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "bar3DPyramidClustered": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "bar3DPyramidStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "bar3DPyramidPercentStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "bar3DCylinderClustered": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "bar3DCylinderStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "bar3DCylinderPercentStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, "col": { BarChart: &c, CatAx: catAx, @@ -771,12 +1052,12 @@ func (f *File) drawBaseChart(formatSet *formatChart) *cPlotArea { CatAx: catAx, ValAx: valAx, }, - "col3DClustered": { + "col3D": { Bar3DChart: &c, CatAx: catAx, ValAx: valAx, }, - "col3D": { + "col3DClustered": { Bar3DChart: &c, CatAx: catAx, ValAx: valAx, @@ -791,6 +1072,66 @@ func (f *File) drawBaseChart(formatSet *formatChart) *cPlotArea { CatAx: catAx, ValAx: valAx, }, + "col3DCone": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3DConeClustered": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3DConeStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3DConePercentStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3DPyramid": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3DPyramidClustered": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3DPyramidStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3DPyramidPercentStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3DCylinder": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3DCylinderClustered": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3DCylinderStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3DCylinderPercentStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, } return charts[formatSet.Type] } @@ -907,6 +1248,38 @@ func (f *File) drawScatterChart(formatSet *formatChart) *cPlotArea { } } +func (f *File) drawChartShape(formatSet *formatChart) *attrValString { + shapes := map[string]string{ + Bar3DConeClustered: "cone", + Bar3DConeStacked: "cone", + Bar3DConePercentStacked: "cone", + Bar3DPyramidClustered: "pyramid", + Bar3DPyramidStacked: "pyramid", + Bar3DPyramidPercentStacked: "pyramid", + Bar3DCylinderClustered: "cylinder", + Bar3DCylinderStacked: "cylinder", + Bar3DCylinderPercentStacked: "cylinder", + Col3DCone: "cone", + Col3DConeClustered: "cone", + Col3DConeStacked: "cone", + Col3DConePercentStacked: "cone", + Col3DPyramid: "pyramid", + Col3DPyramidClustered: "pyramid", + Col3DPyramidStacked: "pyramid", + Col3DPyramidPercentStacked: "pyramid", + Col3DCylinder: "cylinder", + Col3DCylinderClustered: "cylinder", + Col3DCylinderStacked: "cylinder", + Col3DCylinderPercentStacked: "cylinder", + } + if shape, ok := shapes[formatSet.Type]; ok { + return &attrValString{ + Val: shape, + } + } + return nil +} + // drawChartSeries provides a function to draw the c:ser element by given // format sets. func (f *File) drawChartSeries(formatSet *formatChart) *[]cSer { @@ -953,7 +1326,7 @@ func (f *File) drawChartSeriesSpPr(i int, formatSet *formatChart) *cSpPr { SchemeClr: &aSchemeClr{Val: "accent" + strconv.Itoa(i+1)}, } } - chartSeriesSpPr := map[string]*cSpPr{Area: nil, AreaStacked: nil, AreaPercentStacked: nil, Area3D: nil, Area3DStacked: nil, Area3DPercentStacked: nil, Bar: nil, BarStacked: nil, BarPercentStacked: nil, Bar3DClustered: nil, Bar3DStacked: nil, Bar3DPercentStacked: nil, Col: nil, ColStacked: nil, ColPercentStacked: nil, Col3DClustered: nil, Col3D: nil, Col3DStacked: nil, Col3DPercentStacked: nil, Doughnut: nil, Line: spPrLine, Pie: nil, Pie3D: nil, Radar: nil, Scatter: spPrScatter} + chartSeriesSpPr := map[string]*cSpPr{Line: spPrLine, Scatter: spPrScatter} return chartSeriesSpPr[formatSet.Type] } @@ -982,7 +1355,7 @@ func (f *File) drawChartSeriesDPt(i int, formatSet *formatChart) []*cDPt { }, }, }} - chartSeriesDPt := map[string][]*cDPt{Area: nil, AreaStacked: nil, AreaPercentStacked: nil, Area3D: nil, Area3DStacked: nil, Area3DPercentStacked: nil, Bar: nil, BarStacked: nil, BarPercentStacked: nil, Bar3DClustered: nil, Bar3DStacked: nil, Bar3DPercentStacked: nil, Col: nil, ColStacked: nil, ColPercentStacked: nil, Col3DClustered: nil, Col3D: nil, Col3DStacked: nil, Col3DPercentStacked: nil, Doughnut: nil, Line: nil, Pie: dpt, Pie3D: dpt, Radar: nil, Scatter: nil} + chartSeriesDPt := map[string][]*cDPt{Pie: dpt, Pie3D: dpt} return chartSeriesDPt[formatSet.Type] } @@ -994,8 +1367,11 @@ func (f *File) drawChartSeriesCat(v formatChartSeries, formatSet *formatChart) * F: v.Categories, }, } - chartSeriesCat := map[string]*cCat{Area: cat, AreaStacked: cat, AreaPercentStacked: cat, Area3D: cat, Area3DStacked: cat, Area3DPercentStacked: cat, Bar: cat, BarStacked: cat, BarPercentStacked: cat, Bar3DClustered: cat, Bar3DStacked: cat, Bar3DPercentStacked: cat, Col: cat, ColStacked: cat, ColPercentStacked: cat, Col3DClustered: cat, Col3D: cat, Col3DStacked: cat, Col3DPercentStacked: cat, Doughnut: cat, Line: cat, Pie: cat, Pie3D: cat, Radar: cat, Scatter: nil} - return chartSeriesCat[formatSet.Type] + chartSeriesCat := map[string]*cCat{Scatter: nil} + if _, ok := chartSeriesCat[formatSet.Type]; ok { + return nil + } + return cat } // drawChartSeriesVal provides a function to draw the c:val element by given @@ -1006,8 +1382,11 @@ func (f *File) drawChartSeriesVal(v formatChartSeries, formatSet *formatChart) * F: v.Values, }, } - chartSeriesVal := map[string]*cVal{Area: val, AreaStacked: val, AreaPercentStacked: val, Area3D: val, Area3DStacked: val, Area3DPercentStacked: val, Bar: val, BarStacked: val, BarPercentStacked: val, Bar3DClustered: val, Bar3DStacked: val, Bar3DPercentStacked: val, Col: val, ColStacked: val, ColPercentStacked: val, Col3DClustered: val, Col3D: val, Col3DStacked: val, Col3DPercentStacked: val, Doughnut: val, Line: val, Pie: val, Pie3D: val, Radar: val, Scatter: nil} - return chartSeriesVal[formatSet.Type] + chartSeriesVal := map[string]*cVal{Scatter: nil} + if _, ok := chartSeriesVal[formatSet.Type]; ok { + return nil + } + return val } // drawChartSeriesMarker provides a function to draw the c:marker element by @@ -1034,7 +1413,7 @@ func (f *File) drawChartSeriesMarker(i int, formatSet *formatChart) *cMarker { }, } } - chartSeriesMarker := map[string]*cMarker{Area: nil, AreaStacked: nil, AreaPercentStacked: nil, Area3D: nil, Area3DStacked: nil, Area3DPercentStacked: nil, Bar: nil, BarStacked: nil, BarPercentStacked: nil, Bar3DClustered: nil, Bar3DStacked: nil, Bar3DPercentStacked: nil, Col: nil, ColStacked: nil, ColPercentStacked: nil, Col3DClustered: nil, Col3D: nil, Col3DStacked: nil, Col3DPercentStacked: nil, Doughnut: nil, Line: nil, Pie: nil, Pie3D: nil, Radar: nil, Scatter: marker} + chartSeriesMarker := map[string]*cMarker{Scatter: marker} return chartSeriesMarker[formatSet.Type] } @@ -1046,7 +1425,7 @@ func (f *File) drawChartSeriesXVal(v formatChartSeries, formatSet *formatChart) F: v.Categories, }, } - chartSeriesXVal := map[string]*cCat{Area: nil, AreaStacked: nil, AreaPercentStacked: nil, Area3D: nil, Area3DStacked: nil, Area3DPercentStacked: nil, Bar: nil, BarStacked: nil, BarPercentStacked: nil, Bar3DClustered: nil, Bar3DStacked: nil, Bar3DPercentStacked: nil, Col: nil, ColStacked: nil, ColPercentStacked: nil, Col3DClustered: nil, Col3D: nil, Col3DStacked: nil, Col3DPercentStacked: nil, Doughnut: nil, Line: nil, Pie: nil, Pie3D: nil, Radar: nil, Scatter: cat} + chartSeriesXVal := map[string]*cCat{Scatter: cat} return chartSeriesXVal[formatSet.Type] } @@ -1058,7 +1437,7 @@ func (f *File) drawChartSeriesYVal(v formatChartSeries, formatSet *formatChart) F: v.Values, }, } - chartSeriesYVal := map[string]*cVal{Area: nil, AreaStacked: nil, AreaPercentStacked: nil, Area3D: nil, Area3DStacked: nil, Area3DPercentStacked: nil, Bar: nil, BarStacked: nil, BarPercentStacked: nil, Bar3DClustered: nil, Bar3DStacked: nil, Bar3DPercentStacked: nil, Col: nil, ColStacked: nil, ColPercentStacked: nil, Col3DClustered: nil, Col3D: nil, Col3DStacked: nil, Col3DPercentStacked: nil, Doughnut: nil, Line: nil, Pie: nil, Pie3D: nil, Radar: nil, Scatter: val} + chartSeriesYVal := map[string]*cVal{Scatter: val} return chartSeriesYVal[formatSet.Type] } @@ -1080,8 +1459,11 @@ func (f *File) drawChartDLbls(formatSet *formatChart) *cDLbls { // given format sets. func (f *File) drawChartSeriesDLbls(formatSet *formatChart) *cDLbls { dLbls := f.drawChartDLbls(formatSet) - chartSeriesDLbls := map[string]*cDLbls{Area: dLbls, AreaStacked: dLbls, AreaPercentStacked: dLbls, Area3D: dLbls, Area3DStacked: dLbls, Area3DPercentStacked: dLbls, Bar: dLbls, BarStacked: dLbls, BarPercentStacked: dLbls, Bar3DClustered: dLbls, Bar3DStacked: dLbls, Bar3DPercentStacked: dLbls, Col: dLbls, ColStacked: dLbls, ColPercentStacked: dLbls, Col3DClustered: dLbls, Col3D: dLbls, Col3DStacked: dLbls, Col3DPercentStacked: dLbls, Doughnut: dLbls, Line: dLbls, Pie: dLbls, Pie3D: dLbls, Radar: dLbls, Scatter: nil} - return chartSeriesDLbls[formatSet.Type] + chartSeriesDLbls := map[string]*cDLbls{Scatter: nil} + if _, ok := chartSeriesDLbls[formatSet.Type]; ok { + return nil + } + return dLbls } // drawPlotAreaCatAx provides a function to draw the c:catAx element. diff --git a/chart_test.go b/chart_test.go index b1cd3b0045..e69767a977 100644 --- a/chart_test.go +++ b/chart_test.go @@ -44,7 +44,7 @@ func TestChartSize(t *testing.T) { `"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"}}`) + `"title":{"name":"3D Clustered Column Chart"}}`) var ( buffer bytes.Buffer @@ -122,32 +122,55 @@ func TestAddChart(t *testing.T) { // Test add chart on not exists worksheet. assert.EqualError(t, f.AddChart("SheetN", "P1", "{}"), "sheet SheetN is 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":{"position":"left","show_legend_key":false},"title":{"name":"Fruit 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":"Fruit 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":"Fruit 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":"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},"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":"Fruit 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"}`)) - 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":"Fruit 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", "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":"Fruit 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":"Fruit 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":"Fruit 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":"Fruit 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"}`)) - 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"},{"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","show_legend_key":false},"title":{"name":"Fruit 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"}`)) - 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":"Fruit 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":"Fruit 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":"Fruit 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":"Fruit 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":"Fruit 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":"Fruit 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":"Fruit 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":"Fruit 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("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.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 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"}`)) + 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", "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":"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"}`)) + 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"},{"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","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"}`)) + 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,"maximum":0,"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":"Fruit 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":"Fruit 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":"Fruit 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":"Fruit 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":"Fruit 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":"Fruit 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"}`)) + 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"}`)) + + 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"}`)) + + 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"}`)) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddChart.xlsx"))) } diff --git a/xmlChart.go b/xmlChart.go index d23364c160..15c88121b0 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -324,6 +324,7 @@ type cCharts struct { ScatterStyle *attrValString `xml:"scatterStyle"` VaryColors *attrValBool `xml:"varyColors"` Ser *[]cSer `xml:"ser"` + Shape *attrValString `xml:"shape"` DLbls *cDLbls `xml:"dLbls"` HoleSize *attrValInt `xml:"holeSize"` Smooth *attrValBool `xml:"smooth"` From 5cf1c05ed48ad92b6c58d3dfe7d3598526b77b01 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 15 Jun 2019 20:55:56 +0800 Subject: [PATCH 106/957] Add surface 3D, wireframe Surface 3D, contour, and wireframe contour chart support --- chart.go | 139 +++++++++++++++++++++++++++++++++++++++++++++++--- chart_test.go | 10 ++-- xmlChart.go | 74 ++++++++++++++++----------- 3 files changed, 183 insertions(+), 40 deletions(-) diff --git a/chart.go b/chart.go index 8ecd8c73a7..c0060c9ee9 100644 --- a/chart.go +++ b/chart.go @@ -65,6 +65,10 @@ const ( Pie3D = "pie3D" Radar = "radar" Scatter = "scatter" + Surface3D = "surface3D" + WireframeSurface3D = "wireframeSurface3D" + Contour = "contour" + WireframeContour = "wireframeContour" ) // This section defines the default value of chart properties. @@ -116,6 +120,10 @@ var ( Pie3D: 30, Radar: 0, Scatter: 0, + Surface3D: 15, + WireframeSurface3D: 15, + Contour: 90, + WireframeContour: 90, } chartView3DRotY = map[string]int{ Area: 0, @@ -164,6 +172,10 @@ var ( Pie3D: 0, Radar: 0, Scatter: 0, + Surface3D: 20, + WireframeSurface3D: 20, + Contour: 0, + WireframeContour: 0, } chartView3DDepthPercent = map[string]int{ Area: 100, @@ -212,6 +224,14 @@ var ( Pie3D: 100, Radar: 100, Scatter: 100, + Surface3D: 100, + WireframeSurface3D: 100, + Contour: 100, + WireframeContour: 100, + } + chartView3DPerspective = map[string]int{ + Contour: 0, + WireframeContour: 0, } chartView3DRAngAx = map[string]int{ Area: 0, @@ -260,6 +280,9 @@ var ( Pie3D: 0, Radar: 0, Scatter: 0, + Surface3D: 0, + WireframeSurface3D: 0, + Contour: 0, } chartLegendPosition = map[string]string{ "bottom": "b", @@ -315,6 +338,10 @@ var ( Pie3D: "General", Radar: "General", Scatter: "General", + Surface3D: "General", + WireframeSurface3D: "General", + Contour: "General", + WireframeContour: "General", } chartValAxCrossBetween = map[string]string{ Area: "midCat", @@ -363,6 +390,10 @@ var ( Pie3D: "between", Radar: "between", Scatter: "between", + Surface3D: "midCat", + WireframeSurface3D: "midCat", + Contour: "midCat", + WireframeContour: "midCat", } plotAreaChartGrouping = map[string]string{ Area: "standard", @@ -456,6 +487,10 @@ var ( true: "r", false: "l", } + valTickLblPos = map[string]string{ + Contour: "none", + WireframeContour: "none", + } ) // parseFormatChartSet provides a function to parse the format settings of the @@ -573,6 +608,10 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // pie3D | 3D pie chart // radar | radar chart // scatter | scatter chart +// surface3D | 3D surface chart +// wireframeSurface3D | 3D wireframe surface chart +// contour | contour chart +// wireframeContour | wireframe contour // // In Excel a chart series is a collection of information that defines which data is plotted such as values, axis labels and formatting. // @@ -791,6 +830,7 @@ func (f *File) addChart(formatSet *formatChart) { RotX: &attrValInt{Val: chartView3DRotX[formatSet.Type]}, RotY: &attrValInt{Val: chartView3DRotY[formatSet.Type]}, DepthPercent: &attrValInt{Val: chartView3DDepthPercent[formatSet.Type]}, + Perspective: &attrValInt{Val: chartView3DPerspective[formatSet.Type]}, RAngAx: &attrValInt{Val: chartView3DRAngAx[formatSet.Type]}, }, Floor: &cThicknessSpPr{ @@ -891,6 +931,10 @@ func (f *File) addChart(formatSet *formatChart) { Pie: f.drawPieChart, Radar: f.drawRadarChart, Scatter: f.drawScatterChart, + Surface3D: f.drawSurface3DChart, + WireframeSurface3D: f.drawSurface3DChart, + Contour: f.drawSurfaceChart, + WireframeContour: f.drawSurfaceChart, } xlsxChartSpace.Chart.PlotArea = plotAreaFunc[formatSet.Type](formatSet) @@ -1248,6 +1292,52 @@ func (f *File) drawScatterChart(formatSet *formatChart) *cPlotArea { } } +// drawSurface3DChart provides a function to draw the c:surface3DChart element by +// given format sets. +func (f *File) drawSurface3DChart(formatSet *formatChart) *cPlotArea { + plotArea := &cPlotArea{ + Surface3DChart: &cCharts{ + Ser: f.drawChartSeries(formatSet), + AxID: []*attrValInt{ + {Val: 754001152}, + {Val: 753999904}, + {Val: 832256642}, + }, + }, + CatAx: f.drawPlotAreaCatAx(formatSet), + ValAx: f.drawPlotAreaValAx(formatSet), + SerAx: f.drawPlotAreaSerAx(formatSet), + } + if formatSet.Type == WireframeSurface3D { + plotArea.Surface3DChart.Wireframe = &attrValBool{Val: true} + } + return plotArea +} + +// drawSurfaceChart provides a function to draw the c:surfaceChart element by +// given format sets. +func (f *File) drawSurfaceChart(formatSet *formatChart) *cPlotArea { + plotArea := &cPlotArea{ + SurfaceChart: &cCharts{ + Ser: f.drawChartSeries(formatSet), + AxID: []*attrValInt{ + {Val: 754001152}, + {Val: 753999904}, + {Val: 832256642}, + }, + }, + CatAx: f.drawPlotAreaCatAx(formatSet), + ValAx: f.drawPlotAreaValAx(formatSet), + SerAx: f.drawPlotAreaSerAx(formatSet), + } + if formatSet.Type == WireframeContour { + plotArea.SurfaceChart.Wireframe = &attrValBool{Val: true} + } + return plotArea +} + +// drawChartShape provides a function to draw the c:shape element by given +// format sets. func (f *File) drawChartShape(formatSet *formatChart) *attrValString { shapes := map[string]string{ Bar3DConeClustered: "cone", @@ -1273,9 +1363,7 @@ func (f *File) drawChartShape(formatSet *formatChart) *attrValString { Col3DCylinderPercentStacked: "cylinder", } if shape, ok := shapes[formatSet.Type]; ok { - return &attrValString{ - Val: shape, - } + return &attrValString{Val: shape} } return nil } @@ -1459,7 +1547,7 @@ func (f *File) drawChartDLbls(formatSet *formatChart) *cDLbls { // given format sets. func (f *File) drawChartSeriesDLbls(formatSet *formatChart) *cDLbls { dLbls := f.drawChartDLbls(formatSet) - chartSeriesDLbls := map[string]*cDLbls{Scatter: nil} + chartSeriesDLbls := map[string]*cDLbls{Scatter: nil, Surface3D: nil, WireframeSurface3D: nil, Contour: nil, WireframeContour: nil} if _, ok := chartSeriesDLbls[formatSet.Type]; ok { return nil } @@ -1476,7 +1564,7 @@ func (f *File) drawPlotAreaCatAx(formatSet *formatChart) []*cAxs { if formatSet.XAxis.Maximum == 0 { max = nil } - return []*cAxs{ + axs := []*cAxs{ { AxID: &attrValInt{Val: 754001152}, Scaling: &cScaling{ @@ -1503,6 +1591,10 @@ func (f *File) drawPlotAreaCatAx(formatSet *formatChart) []*cAxs { NoMultiLvlLbl: &attrValBool{Val: false}, }, } + if formatSet.XAxis.MajorGridlines { + axs[0].MajorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()} + } + return axs } // drawPlotAreaValAx provides a function to draw the c:valAx element. @@ -1515,7 +1607,7 @@ func (f *File) drawPlotAreaValAx(formatSet *formatChart) []*cAxs { if formatSet.YAxis.Maximum == 0 { max = nil } - return []*cAxs{ + axs := []*cAxs{ { AxID: &attrValInt{Val: 753999904}, Scaling: &cScaling{ @@ -1539,6 +1631,41 @@ func (f *File) drawPlotAreaValAx(formatSet *formatChart) []*cAxs { CrossBetween: &attrValString{Val: chartValAxCrossBetween[formatSet.Type]}, }, } + if formatSet.YAxis.MajorGridlines { + axs[0].MajorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()} + } + if pos, ok := valTickLblPos[formatSet.Type]; ok { + axs[0].TickLblPos.Val = pos + } + return axs +} + +// drawPlotAreaSerAx provides a function to draw the c:serAx element. +func (f *File) drawPlotAreaSerAx(formatSet *formatChart) []*cAxs { + min := &attrValFloat{Val: formatSet.YAxis.Minimum} + max := &attrValFloat{Val: formatSet.YAxis.Maximum} + if formatSet.YAxis.Minimum == 0 { + min = nil + } + if formatSet.YAxis.Maximum == 0 { + max = nil + } + return []*cAxs{ + { + AxID: &attrValInt{Val: 832256642}, + Scaling: &cScaling{ + Orientation: &attrValString{Val: orientation[formatSet.YAxis.ReverseOrder]}, + Max: max, + Min: min, + }, + Delete: &attrValBool{Val: false}, + AxPos: &attrValString{Val: catAxPos[formatSet.XAxis.ReverseOrder]}, + TickLblPos: &attrValString{Val: "nextTo"}, + SpPr: f.drawPlotAreaSpPr(), + TxPr: f.drawPlotAreaTxPr(), + CrossAx: &attrValInt{Val: 753999904}, + }, + } } // drawPlotAreaSpPr provides a function to draw the c:spPr element. diff --git a/chart_test.go b/chart_test.go index e69767a977..326354a95f 100644 --- a/chart_test.go +++ b/chart_test.go @@ -160,17 +160,21 @@ func TestAddChart(t *testing.T) { 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", "AV30", `{"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"}`)) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddChart.xlsx"))) } diff --git a/xmlChart.go b/xmlChart.go index 15c88121b0..ff28bd381c 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -294,26 +294,30 @@ type cView3D struct { RotX *attrValInt `xml:"rotX"` RotY *attrValInt `xml:"rotY"` DepthPercent *attrValInt `xml:"depthPercent"` + Perspective *attrValInt `xml:"perspective"` RAngAx *attrValInt `xml:"rAngAx"` } // cPlotArea directly maps the plotArea element. This element specifies the // plot area of the chart. type cPlotArea struct { - Layout *string `xml:"layout"` - AreaChart *cCharts `xml:"areaChart"` - Area3DChart *cCharts `xml:"area3DChart"` - BarChart *cCharts `xml:"barChart"` - Bar3DChart *cCharts `xml:"bar3DChart"` - DoughnutChart *cCharts `xml:"doughnutChart"` - LineChart *cCharts `xml:"lineChart"` - PieChart *cCharts `xml:"pieChart"` - Pie3DChart *cCharts `xml:"pie3DChart"` - RadarChart *cCharts `xml:"radarChart"` - ScatterChart *cCharts `xml:"scatterChart"` - CatAx []*cAxs `xml:"catAx"` - ValAx []*cAxs `xml:"valAx"` - SpPr *cSpPr `xml:"spPr"` + Layout *string `xml:"layout"` + AreaChart *cCharts `xml:"areaChart"` + Area3DChart *cCharts `xml:"area3DChart"` + BarChart *cCharts `xml:"barChart"` + Bar3DChart *cCharts `xml:"bar3DChart"` + DoughnutChart *cCharts `xml:"doughnutChart"` + LineChart *cCharts `xml:"lineChart"` + PieChart *cCharts `xml:"pieChart"` + Pie3DChart *cCharts `xml:"pie3DChart"` + RadarChart *cCharts `xml:"radarChart"` + ScatterChart *cCharts `xml:"scatterChart"` + Surface3DChart *cCharts `xml:"surface3DChart"` + SurfaceChart *cCharts `xml:"surfaceChart"` + CatAx []*cAxs `xml:"catAx"` + ValAx []*cAxs `xml:"valAx"` + SerAx []*cAxs `xml:"serAx"` + SpPr *cSpPr `xml:"spPr"` } // cCharts specifies the common element of the chart. @@ -323,6 +327,7 @@ type cCharts struct { RadarStyle *attrValString `xml:"radarStyle"` ScatterStyle *attrValString `xml:"scatterStyle"` VaryColors *attrValBool `xml:"varyColors"` + Wireframe *attrValBool `xml:"wireframe"` Ser *[]cSer `xml:"ser"` Shape *attrValString `xml:"shape"` DLbls *cDLbls `xml:"dLbls"` @@ -334,23 +339,29 @@ type cCharts struct { // cAxs directly maps the catAx and valAx element. type cAxs struct { - AxID *attrValInt `xml:"axId"` - Scaling *cScaling `xml:"scaling"` - Delete *attrValBool `xml:"delete"` - AxPos *attrValString `xml:"axPos"` - NumFmt *cNumFmt `xml:"numFmt"` - MajorTickMark *attrValString `xml:"majorTickMark"` - MinorTickMark *attrValString `xml:"minorTickMark"` - TickLblPos *attrValString `xml:"tickLblPos"` - SpPr *cSpPr `xml:"spPr"` - TxPr *cTxPr `xml:"txPr"` - CrossAx *attrValInt `xml:"crossAx"` - Crosses *attrValString `xml:"crosses"` - CrossBetween *attrValString `xml:"crossBetween"` - Auto *attrValBool `xml:"auto"` - LblAlgn *attrValString `xml:"lblAlgn"` - LblOffset *attrValInt `xml:"lblOffset"` - NoMultiLvlLbl *attrValBool `xml:"noMultiLvlLbl"` + AxID *attrValInt `xml:"axId"` + Scaling *cScaling `xml:"scaling"` + Delete *attrValBool `xml:"delete"` + AxPos *attrValString `xml:"axPos"` + MajorGridlines *cChartLines `xml:"majorGridlines"` + NumFmt *cNumFmt `xml:"numFmt"` + MajorTickMark *attrValString `xml:"majorTickMark"` + MinorTickMark *attrValString `xml:"minorTickMark"` + TickLblPos *attrValString `xml:"tickLblPos"` + SpPr *cSpPr `xml:"spPr"` + TxPr *cTxPr `xml:"txPr"` + CrossAx *attrValInt `xml:"crossAx"` + Crosses *attrValString `xml:"crosses"` + CrossBetween *attrValString `xml:"crossBetween"` + Auto *attrValBool `xml:"auto"` + LblAlgn *attrValString `xml:"lblAlgn"` + LblOffset *attrValInt `xml:"lblOffset"` + NoMultiLvlLbl *attrValBool `xml:"noMultiLvlLbl"` +} + +// cChartLines directly maps the chart lines content model. +type cChartLines struct { + SpPr *cSpPr `xml:"spPr"` } // cScaling directly maps the scaling element. This element contains @@ -497,6 +508,7 @@ type cPageMargins struct { // formatChartAxis directly maps the format settings of the chart axis. type formatChartAxis struct { Crossing string `json:"crossing"` + MajorGridlines bool `json:"major_grid_lines"` MajorTickMark string `json:"major_tick_mark"` MinorTickMark string `json:"minor_tick_mark"` MinorUnitType string `json:"minor_unit_type"` From a335be7e4e6824e65f3d8a34b7b45ffa8d78fe4b Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 18 Jun 2019 23:07:44 +0800 Subject: [PATCH 107/957] New functions: SetDefinedName and GetDefinedName added --- excelize_test.go | 21 +++---------- sheet.go | 80 +++++++++++++++++++++++++++++++++++++++++++++--- sheet_test.go | 45 +++++++++++++++++++++++++++ xmlWorkbook.go | 9 ++++++ 4 files changed, 135 insertions(+), 20 deletions(-) diff --git a/excelize_test.go b/excelize_test.go index e4b2548467..c7c3aec11f 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -182,6 +182,11 @@ func TestSaveAsWrongPath(t *testing.T) { } } +func TestOpenReader(t *testing.T) { + _, err := OpenReader(strings.NewReader("")) + assert.EqualError(t, err, "zip: not a valid zip file") +} + func TestBrokenFile(t *testing.T) { // Test write file with broken file struct. f := File{} @@ -1033,22 +1038,6 @@ func TestHSL(t *testing.T) { t.Log(RGBToHSL(250, 50, 100)) } -func TestSearchSheet(t *testing.T) { - f, err := OpenFile(filepath.Join("test", "SharedStrings.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } - - // Test search in a not exists worksheet. - t.Log(f.SearchSheet("Sheet4", "")) - // Test search a not exists value. - t.Log(f.SearchSheet("Sheet1", "X")) - t.Log(f.SearchSheet("Sheet1", "A")) - // Test search the coordinates where the numerical value in the range of - // "0-9" of Sheet1 is described by regular expression: - t.Log(f.SearchSheet("Sheet1", "[0-9]", true)) -} - func TestProtectSheet(t *testing.T) { f := NewFile() f.ProtectSheet("Sheet1", nil) diff --git a/sheet.go b/sheet.go index d3099fb46a..c0eba561cf 100644 --- a/sheet.go +++ b/sheet.go @@ -704,18 +704,14 @@ func (f *File) SearchSheet(sheet, value string, reg ...bool) ([]string, error) { var ( regSearch bool result []string - inElement string - r xlsxRow ) for _, r := range reg { regSearch = r } - xlsx, err := f.workSheetReader(sheet) if err != nil { return result, err } - name, ok := f.sheetMap[trimSheetName(sheet)] if !ok { return result, nil @@ -724,6 +720,17 @@ func (f *File) SearchSheet(sheet, value string, reg ...bool) ([]string, error) { output, _ := xml.Marshal(f.Sheet[name]) f.saveFileList(name, replaceWorkSheetsRelationshipsNameSpaceBytes(output)) } + return f.searchSheet(name, value, regSearch) +} + +// searchSheet provides a function to get coordinates by given worksheet name, +// cell value, and regular expression. +func (f *File) searchSheet(name, value string, regSearch bool) ([]string, error) { + var ( + inElement string + result []string + r xlsxRow + ) xml.NewDecoder(bytes.NewReader(f.readXML(name))) d := f.sharedStringsReader() @@ -1213,6 +1220,71 @@ func (f *File) GetPageLayout(sheet string, opts ...PageLayoutOptionPtr) error { return err } +// SetDefinedName provides a function to set the defined names of the workbook +// or worksheet. If not specified scopr, 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", +// }) +// +func (f *File) SetDefinedName(definedName *DefinedName) error { + wb := f.workbookReader() + d := xlsxDefinedName{ + Name: definedName.Name, + Comment: definedName.Comment, + Data: definedName.RefersTo, + } + if definedName.Scope != "" { + if sheetID := f.GetSheetIndex(definedName.Scope); sheetID != 0 { + sheetID-- + d.LocalSheetID = &sheetID + } + } + if wb.DefinedNames != nil { + for _, dn := range wb.DefinedNames.DefinedName { + var scope string + if dn.LocalSheetID != nil { + scope = f.GetSheetName(*dn.LocalSheetID + 1) + } + if scope == definedName.Scope && dn.Name == definedName.Name { + return errors.New("the same name already exists on scope") + } + } + wb.DefinedNames.DefinedName = append(wb.DefinedNames.DefinedName, d) + return nil + } + wb.DefinedNames = &xlsxDefinedNames{ + DefinedName: []xlsxDefinedName{d}, + } + return nil +} + +// GetDefinedName provides a function to get the defined names of the workbook +// or worksheet. +func (f *File) GetDefinedName() []DefinedName { + var definedNames []DefinedName + wb := f.workbookReader() + if wb.DefinedNames != nil { + for _, dn := range wb.DefinedNames.DefinedName { + definedName := DefinedName{ + Name: dn.Name, + Comment: dn.Comment, + RefersTo: dn.Data, + Scope: "Workbook", + } + if dn.LocalSheetID != nil { + definedName.Scope = f.GetSheetName(*dn.LocalSheetID + 1) + } + definedNames = append(definedNames, definedName) + } + } + return definedNames +} + // workSheetRelsReader provides a function to get the pointer to the structure // after deserialization of xl/worksheets/_rels/sheet%d.xml.rels. func (f *File) workSheetRelsReader(path string) *xlsxWorkbookRels { diff --git a/sheet_test.go b/sheet_test.go index beee10bbae..a7fd9e940f 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -108,6 +108,29 @@ func TestPageLayoutOption(t *testing.T) { } } +func TestSearchSheet(t *testing.T) { + f, err := excelize.OpenFile(filepath.Join("test", "SharedStrings.xlsx")) + if !assert.NoError(t, err) { + t.FailNow() + } + // Test search in a not exists worksheet. + _, err = f.SearchSheet("Sheet4", "") + assert.EqualError(t, err, "sheet Sheet4 is not exist") + var expected []string + // Test search a not exists value. + result, err := f.SearchSheet("Sheet1", "X") + assert.NoError(t, err) + assert.EqualValues(t, expected, result) + result, err = f.SearchSheet("Sheet1", "A") + assert.NoError(t, err) + assert.EqualValues(t, []string{"A1"}, result) + // Test search the coordinates where the numerical value in the range of + // "0-9" of Sheet1 is described by regular expression: + result, err = f.SearchSheet("Sheet1", "[0-9]", true) + assert.NoError(t, err) + assert.EqualValues(t, expected, result) +} + func TestSetPageLayout(t *testing.T) { f := excelize.NewFile() // Test set page layout on not exists worksheet. @@ -142,3 +165,25 @@ func TestSetHeaderFooter(t *testing.T) { })) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetHeaderFooter.xlsx"))) } + +func TestDefinedName(t *testing.T) { + f := excelize.NewFile() + assert.NoError(t, f.SetDefinedName(&excelize.DefinedName{ + Name: "Amount", + RefersTo: "Sheet1!$A$2:$D$5", + Comment: "defined name comment", + Scope: "Sheet1", + })) + assert.NoError(t, f.SetDefinedName(&excelize.DefinedName{ + Name: "Amount", + RefersTo: "Sheet1!$A$2:$D$5", + Comment: "defined name comment", + })) + assert.EqualError(t, f.SetDefinedName(&excelize.DefinedName{ + Name: "Amount", + RefersTo: "Sheet1!$A$2:$D$5", + Comment: "defined name comment", + }), "the same name already exists on scope") + assert.Exactly(t, "Sheet1!$A$2:$D$5", f.GetDefinedName()[1].RefersTo) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDefinedName.xlsx"))) +} diff --git a/xmlWorkbook.go b/xmlWorkbook.go index 53849779e1..01186851ea 100644 --- a/xmlWorkbook.go +++ b/xmlWorkbook.go @@ -288,3 +288,12 @@ type xlsxCustomWorkbookView struct { XWindow *int `xml:"xWindow,attr"` YWindow *int `xml:"yWindow,attr"` } + +// 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 +} From e77c462d3f1c29b009186d42832e6d5f42ac069f Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 19 Jun 2019 00:01:18 +0800 Subject: [PATCH 108/957] Support to create bubble and 3D bubble chart --- chart.go | 79 +++++++++++++++++++++++++++++++++++++++++---------- chart_test.go | 7 ++++- xmlChart.go | 4 +++ 3 files changed, 74 insertions(+), 16 deletions(-) diff --git a/chart.go b/chart.go index c0060c9ee9..b9439ca7e0 100644 --- a/chart.go +++ b/chart.go @@ -69,6 +69,8 @@ const ( WireframeSurface3D = "wireframeSurface3D" Contour = "contour" WireframeContour = "wireframeContour" + Bubble = "bubble" + Bubble3D = "bubble3D" ) // This section defines the default value of chart properties. @@ -228,6 +230,8 @@ var ( WireframeSurface3D: 100, Contour: 100, WireframeContour: 100, + Bubble: 100, + Bubble3D: 100, } chartView3DPerspective = map[string]int{ Contour: 0, @@ -283,6 +287,8 @@ var ( Surface3D: 0, WireframeSurface3D: 0, Contour: 0, + Bubble: 0, + Bubble3D: 0, } chartLegendPosition = map[string]string{ "bottom": "b", @@ -342,6 +348,8 @@ var ( WireframeSurface3D: "General", Contour: "General", WireframeContour: "General", + Bubble: "General", + Bubble3D: "General", } chartValAxCrossBetween = map[string]string{ Area: "midCat", @@ -394,6 +402,8 @@ var ( WireframeSurface3D: "midCat", Contour: "midCat", WireframeContour: "midCat", + Bubble: "midCat", + Bubble3D: "midCat", } plotAreaChartGrouping = map[string]string{ Area: "standard", @@ -611,7 +621,9 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // surface3D | 3D surface chart // wireframeSurface3D | 3D wireframe surface chart // contour | contour chart -// wireframeContour | wireframe contour +// 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. // @@ -935,6 +947,8 @@ func (f *File) addChart(formatSet *formatChart) { WireframeSurface3D: f.drawSurface3DChart, Contour: f.drawSurfaceChart, WireframeContour: f.drawSurfaceChart, + Bubble: f.drawBaseChart, + Bubble3D: f.drawBaseChart, } xlsxChartSpace.Chart.PlotArea = plotAreaFunc[formatSet.Type](formatSet) @@ -965,12 +979,13 @@ func (f *File) drawBaseChart(formatSet *formatChart) *cPlotArea { }, } var ok bool - c.BarDir.Val, ok = plotAreaChartBarDir[formatSet.Type] - if !ok { + if c.BarDir.Val, ok = plotAreaChartBarDir[formatSet.Type]; !ok { c.BarDir = nil } - c.Grouping.Val = plotAreaChartGrouping[formatSet.Type] - if formatSet.Type == "colStacked" || formatSet.Type == "barStacked" || formatSet.Type == "barPercentStacked" || formatSet.Type == "colPercentStacked" || formatSet.Type == "areaPercentStacked" { + if c.Grouping.Val, ok = plotAreaChartGrouping[formatSet.Type]; !ok { + c.Grouping = nil + } + if strings.HasSuffix(formatSet.Type, "Stacked") { c.Overlap = &attrValInt{Val: 100} } catAx := f.drawPlotAreaCatAx(formatSet) @@ -1176,6 +1191,16 @@ func (f *File) drawBaseChart(formatSet *formatChart) *cPlotArea { CatAx: catAx, ValAx: valAx, }, + "bubble": { + BubbleChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "bubble3D": { + BubbleChart: &c, + CatAx: catAx, + ValAx: valAx, + }, } return charts[formatSet.Type] } @@ -1381,14 +1406,16 @@ func (f *File) drawChartSeries(formatSet *formatChart) *[]cSer { F: formatSet.Series[k].Name, }, }, - SpPr: f.drawChartSeriesSpPr(k, formatSet), - Marker: f.drawChartSeriesMarker(k, formatSet), - DPt: f.drawChartSeriesDPt(k, formatSet), - DLbls: f.drawChartSeriesDLbls(formatSet), - Cat: f.drawChartSeriesCat(formatSet.Series[k], formatSet), - Val: f.drawChartSeriesVal(formatSet.Series[k], formatSet), - XVal: f.drawChartSeriesXVal(formatSet.Series[k], formatSet), - YVal: f.drawChartSeriesYVal(formatSet.Series[k], formatSet), + SpPr: f.drawChartSeriesSpPr(k, formatSet), + Marker: f.drawChartSeriesMarker(k, formatSet), + DPt: f.drawChartSeriesDPt(k, formatSet), + DLbls: f.drawChartSeriesDLbls(formatSet), + Cat: f.drawChartSeriesCat(formatSet.Series[k], formatSet), + 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), }) } return &ser @@ -1525,10 +1552,32 @@ func (f *File) drawChartSeriesYVal(v formatChartSeries, formatSet *formatChart) F: v.Values, }, } - chartSeriesYVal := map[string]*cVal{Scatter: val} + chartSeriesYVal := map[string]*cVal{Scatter: val, Bubble: val, Bubble3D: val} return chartSeriesYVal[formatSet.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 { + return nil + } + return &cVal{ + NumRef: &cNumRef{ + F: v.Values, + }, + } +} + +// 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 { + return nil + } + return &attrValBool{Val: true} +} + // drawChartDLbls provides a function to draw the c:dLbls element by given // format sets. func (f *File) drawChartDLbls(formatSet *formatChart) *cDLbls { @@ -1547,7 +1596,7 @@ func (f *File) drawChartDLbls(formatSet *formatChart) *cDLbls { // given format sets. func (f *File) drawChartSeriesDLbls(formatSet *formatChart) *cDLbls { dLbls := f.drawChartDLbls(formatSet) - chartSeriesDLbls := map[string]*cDLbls{Scatter: nil, Surface3D: nil, WireframeSurface3D: nil, Contour: nil, WireframeContour: nil} + chartSeriesDLbls := map[string]*cDLbls{Scatter: nil, Surface3D: nil, WireframeSurface3D: nil, Contour: nil, WireframeContour: nil, Bubble: nil, Bubble3D: nil} if _, ok := chartSeriesDLbls[formatSet.Type]; ok { return nil } diff --git a/chart_test.go b/chart_test.go index 326354a95f..c0bae33d95 100644 --- a/chart_test.go +++ b/chart_test.go @@ -174,7 +174,12 @@ func TestAddChart(t *testing.T) { // 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", "AV30", `{"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", "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}}`)) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddChart.xlsx"))) + + 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/xmlChart.go b/xmlChart.go index ff28bd381c..8a3a680c2e 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -306,6 +306,7 @@ type cPlotArea struct { Area3DChart *cCharts `xml:"area3DChart"` BarChart *cCharts `xml:"barChart"` Bar3DChart *cCharts `xml:"bar3DChart"` + BubbleChart *cCharts `xml:"bubbleChart"` DoughnutChart *cCharts `xml:"doughnutChart"` LineChart *cCharts `xml:"lineChart"` PieChart *cCharts `xml:"pieChart"` @@ -323,6 +324,7 @@ type cPlotArea struct { // cCharts specifies the common element of the chart. type cCharts struct { BarDir *attrValString `xml:"barDir"` + BubbleScale *attrValFloat `xml:"bubbleScale"` Grouping *attrValString `xml:"grouping"` RadarStyle *attrValString `xml:"radarStyle"` ScatterStyle *attrValString `xml:"scatterStyle"` @@ -395,6 +397,8 @@ type cSer struct { XVal *cCat `xml:"xVal"` YVal *cVal `xml:"yVal"` Smooth *attrValBool `xml:"smooth"` + BubbleSize *cVal `xml:"bubbleSize"` + Bubble3D *attrValBool `xml:"bubble3D"` } // cMarker (Marker) directly maps the marker element. This element specifies a From 9f8623047d2fc38e12c3b214475710d25ec88c55 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 20 Jun 2019 00:00:40 +0800 Subject: [PATCH 109/957] Optimize code, fix golint issues --- adjust_test.go | 8 ++++---- picture.go | 29 ++++++++++++++++------------- rows.go | 4 ++-- 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/adjust_test.go b/adjust_test.go index 364a8b8ab1..a0de844a88 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -104,13 +104,13 @@ func TestAdjustCalcChain(t *testing.T) { func TestCoordinatesToAreaRef(t *testing.T) { f := NewFile() - ref, err := f.coordinatesToAreaRef([]int{}) + _, err := f.coordinatesToAreaRef([]int{}) assert.EqualError(t, err, "coordinates length must be 4") - ref, err = f.coordinatesToAreaRef([]int{1, -1, 1, 1}) + _, err = f.coordinatesToAreaRef([]int{1, -1, 1, 1}) assert.EqualError(t, err, "invalid cell coordinates [1, -1]") - ref, err = f.coordinatesToAreaRef([]int{1, 1, 1, -1}) + _, err = f.coordinatesToAreaRef([]int{1, 1, 1, -1}) assert.EqualError(t, err, "invalid cell coordinates [1, -1]") - ref, err = f.coordinatesToAreaRef([]int{1, 1, 1, 1}) + ref, err := f.coordinatesToAreaRef([]int{1, 1, 1, 1}) assert.NoError(t, err) assert.EqualValues(t, ref, "A1:A1") } diff --git a/picture.go b/picture.go index 01c2ae2e9b..7804bce613 100644 --- a/picture.go +++ b/picture.go @@ -499,26 +499,33 @@ func (f *File) getSheetRelationshipsTargetByID(sheet, rID string) string { func (f *File) GetPicture(sheet, cell string) (string, []byte, error) { col, row, err := CellNameToCoordinates(cell) if err != nil { - return "", []byte{}, err + return "", nil, err } col-- row-- xlsx, err := f.workSheetReader(sheet) if err != nil { - return "", []byte{}, err + return "", nil, err } if xlsx.Drawing == nil { - return "", []byte{}, err + return "", nil, err } - target := f.getSheetRelationshipsTargetByID(sheet, xlsx.Drawing.RID) drawingXML := strings.Replace(target, "..", "xl", -1) - + _, ok := f.XLSX[drawingXML] + if !ok { + return "", nil, err + } drawingRelationships := strings.Replace( strings.Replace(target, "../drawings", "xl/drawings/_rels", -1), ".xml", ".xml.rels", -1) - wsDr, _ := f.drawingParser(drawingXML) + return f.getPicture(row, col, drawingXML, drawingRelationships) +} +// getPicture provides a function to get picture base name and raw content +// embed in XLSX by given coordinates and drawing relationships. +func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) (string, []byte, error) { + wsDr, _ := f.drawingParser(drawingXML) for _, anchor := range wsDr.TwoCellAnchor { if anchor.From != nil && anchor.Pic != nil { if anchor.From.Col == col && anchor.From.Row == row { @@ -528,16 +535,12 @@ func (f *File) GetPicture(sheet, cell string) (string, []byte, error) { if ok { return filepath.Base(xlsxWorkbookRelation.Target), []byte(f.XLSX[strings.Replace(xlsxWorkbookRelation.Target, - "..", "xl", -1)]), err + "..", "xl", -1)]), nil } } } } - _, ok := f.XLSX[drawingXML] - if !ok { - return "", nil, err - } decodeWsDr := decodeWsDr{} _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML(drawingXML)), &decodeWsDr) for _, anchor := range decodeWsDr.TwoCellAnchor { @@ -548,12 +551,12 @@ func (f *File) GetPicture(sheet, cell string) (string, []byte, error) { xlsxWorkbookRelation := f.getDrawingRelationships(drawingRelationships, decodeTwoCellAnchor.Pic.BlipFill.Blip.Embed) _, ok := supportImageTypes[filepath.Ext(xlsxWorkbookRelation.Target)] if ok { - return filepath.Base(xlsxWorkbookRelation.Target), []byte(f.XLSX[strings.Replace(xlsxWorkbookRelation.Target, "..", "xl", -1)]), err + return filepath.Base(xlsxWorkbookRelation.Target), []byte(f.XLSX[strings.Replace(xlsxWorkbookRelation.Target, "..", "xl", -1)]), nil } } } } - return "", []byte{}, err + return "", nil, nil } // getDrawingRelationships provides a function to get drawing relationships diff --git a/rows.go b/rows.go index 064fefe4b7..3079d5a4e8 100644 --- a/rows.go +++ b/rows.go @@ -21,7 +21,7 @@ import ( // GetRows return all the rows in a sheet by given worksheet name (case // sensitive). For example: // -// rows, err := f.GetRows("Sheet1") +// rows, err := f.GetRows("Sheet1") // for _, row := range rows { // for _, colCell := range row { // fmt.Print(colCell, "\t") @@ -160,7 +160,7 @@ func (err ErrSheetNotExist) Error() string { // // rows, err := f.Rows("Sheet1") // for rows.Next() { -// row, err := rows.Columns() +// row, err := rows.Columns() // for _, colCell := range row { // fmt.Print(colCell, "\t") // } From 54def7eaad9ee0469ca495b3661798919239384a Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 27 Jun 2019 21:58:14 +0800 Subject: [PATCH 110/957] Add TIF, TIFF format images and more detailed error information when open the encrypted file --- excelize.go | 12 +++++++++ excelize_test.go | 9 +++++++ picture.go | 2 +- picture_test.go | 25 +++++++++++++------ test/images/excel.tif | Bin 0 -> 27052 bytes xmlApp.go | 55 ++++++++++++++++++++++++++++++++++++++++++ xmlDrawing.go | 2 +- 7 files changed, 95 insertions(+), 10 deletions(-) create mode 100644 test/images/excel.tif create mode 100644 xmlApp.go diff --git a/excelize.go b/excelize.go index 6fb98c4297..f636a84061 100644 --- a/excelize.go +++ b/excelize.go @@ -14,6 +14,7 @@ import ( "archive/zip" "bytes" "encoding/xml" + "errors" "fmt" "io" "io/ioutil" @@ -69,6 +70,17 @@ func OpenReader(r io.Reader) (*File, error) { zr, err := zip.NewReader(bytes.NewReader(b), int64(len(b))) if err != nil { + identifier := []byte{ + // checking protect workbook by [MS-OFFCRYPTO] - v20181211 3.1 FeatureIdentifier + 0x3c, 0x00, 0x00, 0x00, 0x4d, 0x00, 0x69, 0x00, 0x63, 0x00, 0x72, 0x00, 0x6f, 0x00, 0x73, 0x00, + 0x6f, 0x00, 0x66, 0x00, 0x74, 0x00, 0x2e, 0x00, 0x43, 0x00, 0x6f, 0x00, 0x6e, 0x00, 0x74, 0x00, + 0x61, 0x00, 0x69, 0x00, 0x6e, 0x00, 0x65, 0x00, 0x72, 0x00, 0x2e, 0x00, 0x44, 0x00, 0x61, 0x00, + 0x74, 0x00, 0x61, 0x00, 0x53, 0x00, 0x70, 0x00, 0x61, 0x00, 0x63, 0x00, 0x65, 0x00, 0x73, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + } + if bytes.Contains(b, identifier) { + return nil, errors.New("not support encrypted file currently") + } return nil, err } diff --git a/excelize_test.go b/excelize_test.go index c7c3aec11f..c4a06a54f6 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -1,6 +1,7 @@ package excelize import ( + "bytes" "fmt" "image/color" _ "image/gif" @@ -185,6 +186,14 @@ func TestSaveAsWrongPath(t *testing.T) { func TestOpenReader(t *testing.T) { _, err := OpenReader(strings.NewReader("")) assert.EqualError(t, err, "zip: not a valid zip file") + _, err = OpenReader(bytes.NewReader([]byte{ + 0x3c, 0x00, 0x00, 0x00, 0x4d, 0x00, 0x69, 0x00, 0x63, 0x00, 0x72, 0x00, 0x6f, 0x00, 0x73, 0x00, + 0x6f, 0x00, 0x66, 0x00, 0x74, 0x00, 0x2e, 0x00, 0x43, 0x00, 0x6f, 0x00, 0x6e, 0x00, 0x74, 0x00, + 0x61, 0x00, 0x69, 0x00, 0x6e, 0x00, 0x65, 0x00, 0x72, 0x00, 0x2e, 0x00, 0x44, 0x00, 0x61, 0x00, + 0x74, 0x00, 0x61, 0x00, 0x53, 0x00, 0x70, 0x00, 0x61, 0x00, 0x63, 0x00, 0x65, 0x00, 0x73, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + })) + assert.EqualError(t, err, "not support encrypted file currently") } func TestBrokenFile(t *testing.T) { diff --git a/picture.go b/picture.go index 7804bce613..812eb5c509 100644 --- a/picture.go +++ b/picture.go @@ -385,7 +385,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() { - var imageTypes = map[string]bool{"jpeg": false, "png": false, "gif": false} + var imageTypes = map[string]bool{"jpeg": false, "png": false, "gif": false, "tiff": false} content := f.contentTypesReader() for _, v := range content.Defaults { _, ok := imageTypes[v.Extension] diff --git a/picture_test.go b/picture_test.go index 890092e9f9..9a2edda9ae 100644 --- a/picture_test.go +++ b/picture_test.go @@ -1,8 +1,13 @@ package excelize import ( - "fmt" + _ "image/gif" + _ "image/jpeg" _ "image/png" + + _ "golang.org/x/image/tiff" + + "fmt" "io/ioutil" "os" "path/filepath" @@ -25,37 +30,41 @@ func BenchmarkAddPictureFromBytes(b *testing.B) { } func TestAddPicture(t *testing.T) { - xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if !assert.NoError(t, err) { t.FailNow() } // Test add picture to worksheet with offset and location hyperlink. - err = xlsx.AddPicture("Sheet2", "I9", filepath.Join("test", "images", "excel.jpg"), + err = f.AddPicture("Sheet2", "I9", filepath.Join("test", "images", "excel.jpg"), `{"x_offset": 140, "y_offset": 120, "hyperlink": "#Sheet2!D8", "hyperlink_type": "Location"}`) if !assert.NoError(t, err) { t.FailNow() } // Test add picture to worksheet with offset, external hyperlink and positioning. - err = xlsx.AddPicture("Sheet1", "F21", filepath.Join("test", "images", "excel.jpg"), + err = f.AddPicture("Sheet1", "F21", filepath.Join("test", "images", "excel.jpg"), `{"x_offset": 10, "y_offset": 10, "hyperlink": "https://github.com/360EntSecGroup-Skylar/excelize", "hyperlink_type": "External", "positioning": "oneCell"}`) if !assert.NoError(t, err) { t.FailNow() } - file, err := ioutil.ReadFile(filepath.Join("test", "images", "excel.jpg")) + file, err := ioutil.ReadFile(filepath.Join("test", "images", "excel.png")) if !assert.NoError(t, err) { t.FailNow() } // Test add picture to worksheet from bytes. - assert.NoError(t, xlsx.AddPictureFromBytes("Sheet1", "Q1", "", "Excel Logo", ".jpg", file)) + assert.NoError(t, f.AddPictureFromBytes("Sheet1", "Q1", "", "Excel Logo", ".png", file)) // Test add picture to worksheet from bytes with illegal cell coordinates. - assert.EqualError(t, xlsx.AddPictureFromBytes("Sheet1", "A", "", "Excel Logo", ".jpg", file), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.EqualError(t, f.AddPictureFromBytes("Sheet1", "A", "", "Excel Logo", ".png", file), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + + 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. - assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestAddPicture.xlsx"))) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddPicture.xlsx"))) } func TestAddPictureErrors(t *testing.T) { diff --git a/test/images/excel.tif b/test/images/excel.tif new file mode 100644 index 0000000000000000000000000000000000000000..4ce5effc4144d7e045fd14c6f40c32185a5543ad GIT binary patch literal 27052 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 be=X5y;GekVE&fj;`d?PUmS{3aFP8rSy$??x literal 0 HcmV?d00001 diff --git a/xmlApp.go b/xmlApp.go new file mode 100644 index 0000000000..ad414fae06 --- /dev/null +++ b/xmlApp.go @@ -0,0 +1,55 @@ +// Copyright 2016 - 2019 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. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. + +package excelize + +import "encoding/xml" + +type xlsxProperties struct { + XMLName xml.Name `xml:"http://schemas.openxmlformats.org/officeDocument/2006/extended-properties Properties"` + Template string + Manager string + Company string + Pages int + Words int + Characters int + PresentationFormat string + Lines int + Paragraphs int + Slides int + Notes int + TotalTime int + HiddenSlides int + MMClips int + ScaleCrop bool + HeadingPairs *xlsxVectorVariant + TitlesOfParts *xlsxVectorLpstr + LinksUpToDate bool + CharactersWithSpaces int + SharedDoc bool + HyperlinkBase string + HLinks *xlsxVectorVariant + HyperlinksChanged bool + DigSig *xlsxDigSig + Application string + AppVersion string + DocSecurity int +} + +type xlsxVectorVariant struct { + Content string `xml:",innerxml"` +} + +type xlsxVectorLpstr struct { + Content string `xml:",innerxml"` +} + +type xlsxDigSig struct { + Content string `xml:",innerxml"` +} diff --git a/xmlDrawing.go b/xmlDrawing.go index 13e164ead5..bb468bc017 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -42,7 +42,7 @@ const ( NameSpaceDublinCoreMetadataIntiative = "http://purl.org/dc/dcmitype/" ) -var supportImageTypes = map[string]string{".gif": ".gif", ".jpg": ".jpeg", ".jpeg": ".jpeg", ".png": ".png"} +var supportImageTypes = map[string]string{".gif": ".gif", ".jpg": ".jpeg", ".jpeg": ".jpeg", ".png": ".png", ".tif": ".tiff", ".tiff": ".tiff"} // xlsxCNvPr directly maps the cNvPr (Non-Visual Drawing Properties). This // element specifies non-visual canvas properties. This allows for additional From dc8210d4a7d18f6425f6f18dc383b26778883715 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 30 Jun 2019 19:50:47 +0800 Subject: [PATCH 111/957] Update GoDoc and typo fixed --- docProps.go | 4 ++-- sheet.go | 2 +- styles.go | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docProps.go b/docProps.go index 0f44ac42c5..ff19fdaf2e 100644 --- a/docProps.go +++ b/docProps.go @@ -31,7 +31,7 @@ import ( // Description | An explanation of the content of the resource. // | // LastModifiedBy | The user who performed the last modification. The identification is -// | environment-specific. +// | environment-specific. // | // Language | The language of the intellectual content of the resource. // | @@ -40,7 +40,7 @@ import ( // Revision | The topic of the content of the resource. // | // ContentStatus | The status of the content. For example: Values might include "Draft", -// | "Reviewed", and "Final" +// | "Reviewed" and "Final" // | // Category | A categorization of the content of this package. // | diff --git a/sheet.go b/sheet.go index c0eba561cf..a43ca6bcdb 100644 --- a/sheet.go +++ b/sheet.go @@ -1221,7 +1221,7 @@ func (f *File) GetPageLayout(sheet string, opts ...PageLayoutOptionPtr) error { } // SetDefinedName provides a function to set the defined names of the workbook -// or worksheet. If not specified scopr, the default scope is workbook. +// or worksheet. If not specified scope, the default scope is workbook. // For example: // // f.SetDefinedName(&excelize.DefinedName{ diff --git a/styles.go b/styles.go index 1c014218f8..b246e30607 100644 --- a/styles.go +++ b/styles.go @@ -1946,13 +1946,13 @@ func (f *File) NewConditionalStyle(style string) (int, error) { } // GetDefaultFont provides the default font name currently set in the workbook -// Documents generated by excelize start with Calibri +// Documents generated by excelize start with Calibri. func (f *File) GetDefaultFont() string { font := f.readDefaultFont() return font.Name.Val } -// SetDefaultFont changes the default font in the workbook +// SetDefaultFont changes the default font in the workbook. func (f *File) SetDefaultFont(fontName string) { font := f.readDefaultFont() font.Name.Val = fontName @@ -1962,7 +1962,7 @@ func (f *File) SetDefaultFont(fontName string) { s.CellStyles.CellStyle[0].CustomBuiltIn = &custom } -// readDefaultFont provides an unmarshalled font value +// readDefaultFont provides an unmarshalled font value. func (f *File) readDefaultFont() *xlsxFont { s := f.stylesReader() return s.Fonts.Font[0] From 8b2d4cb697420e5daeb85e5d9593c563bd77db53 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 3 Jul 2019 00:50:10 +0800 Subject: [PATCH 112/957] New feature: group and ungroup sheets support New functions `GroupSheets` and `UngroupSheets` added Refactor sheet index calculation --- sheet.go | 155 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 109 insertions(+), 46 deletions(-) diff --git a/sheet.go b/sheet.go index a43ca6bcdb..d579c6afee 100644 --- a/sheet.go +++ b/sheet.go @@ -275,13 +275,20 @@ func (f *File) SetActiveSheet(index int) { // GetActiveSheetIndex provides a function to get active sheet index of the // XLSX. If not found the active sheet will be return integer 0. func (f *File) GetActiveSheetIndex() int { - for idx, name := range f.GetSheetMap() { - xlsx, _ := f.workSheetReader(name) - for _, sheetView := range xlsx.SheetViews.SheetView { - if sheetView.TabSelected { - return idx + wb := f.workbookReader() + if wb != nil { + view := wb.BookViews.WorkBookView + sheets := wb.Sheets.Sheet + var activeTab int + if len(view) > 0 { + activeTab = view[0].ActiveTab + if len(sheets) > activeTab && sheets[activeTab].SheetID != 0 { + return sheets[activeTab].SheetID } } + if len(wb.Sheets.Sheet) == 1 { + return wb.Sheets.Sheet[0].SheetID + } } return 0 } @@ -308,34 +315,26 @@ func (f *File) SetSheetName(oldName, newName string) { // worksheet index. If given sheet index is invalid, will return an empty // string. func (f *File) GetSheetName(index int) string { - content := f.workbookReader() - rels := f.workbookRelsReader() - for _, rel := range rels.Relationships { - rID, _ := strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(rel.Target, "worksheets/sheet"), ".xml")) - if rID == index { - for _, v := range content.Sheets.Sheet { - if v.ID == rel.ID { - return v.Name - } + wb := f.workbookReader() + if wb != nil { + for _, sheet := range wb.Sheets.Sheet { + if sheet.SheetID == index { + return sheet.Name } } } return "" } -// GetSheetIndex provides a function to get worksheet index of XLSX by given sheet -// name. If given worksheet name is invalid, will return an integer type value -// 0. +// GetSheetIndex provides a function to get worksheet index of XLSX by given +// sheet name. If given worksheet name is invalid, will return an integer type +// value 0. func (f *File) GetSheetIndex(name string) int { - content := f.workbookReader() - rels := f.workbookRelsReader() - for _, v := range content.Sheets.Sheet { - if v.Name == name { - for _, rel := range rels.Relationships { - if v.ID == rel.ID { - rID, _ := strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(rel.Target, "worksheets/sheet"), ".xml")) - return rID - } + wb := f.workbookReader() + if wb != nil { + for _, sheet := range wb.Sheets.Sheet { + if sheet.Name == trimSheetName(name) { + return sheet.SheetID } } } @@ -354,16 +353,11 @@ func (f *File) GetSheetIndex(name string) int { // } // func (f *File) GetSheetMap() map[int]string { - content := f.workbookReader() - rels := f.workbookRelsReader() + wb := f.workbookReader() sheetMap := map[int]string{} - for _, v := range content.Sheets.Sheet { - for _, rel := range rels.Relationships { - relStr := strings.SplitN(rel.Target, "worksheets/sheet", 2) - if rel.ID == v.ID && len(relStr) == 2 { - rID, _ := strconv.Atoi(strings.TrimSuffix(relStr[1], ".xml")) - sheetMap[rID] = v.Name - } + if wb != nil { + for _, sheet := range wb.Sheets.Sheet { + sheetMap[sheet.SheetID] = sheet.Name } } return sheetMap @@ -411,19 +405,31 @@ func (f *File) SetSheetBackground(sheet, picture string) error { // 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. func (f *File) DeleteSheet(name string) { - content := f.workbookReader() - for k, v := range content.Sheets.Sheet { - if v.Name == trimSheetName(name) && len(content.Sheets.Sheet) > 1 { - content.Sheets.Sheet = append(content.Sheets.Sheet[:k], content.Sheets.Sheet[k+1:]...) - sheet := "xl/worksheets/sheet" + strconv.Itoa(v.SheetID) + ".xml" - rels := "xl/worksheets/_rels/sheet" + strconv.Itoa(v.SheetID) + ".xml.rels" - target := f.deleteSheetFromWorkbookRels(v.ID) + if f.SheetCount == 1 || f.GetSheetIndex(name) == 0 { + return + } + sheetName := trimSheetName(name) + wb := f.workbookReader() + wbRels := f.workbookRelsReader() + for idx, sheet := range wb.Sheets.Sheet { + if sheet.Name == sheetName { + wb.Sheets.Sheet = append(wb.Sheets.Sheet[:idx], wb.Sheets.Sheet[idx+1:]...) + var sheetXML, rels string + if wbRels != nil { + for _, rel := range wbRels.Relationships { + if rel.ID == sheet.ID { + sheetXML = fmt.Sprintf("xl/%s", rel.Target) + rels = strings.Replace(fmt.Sprintf("xl/%s.rels", rel.Target), "xl/worksheets/", "xl/worksheets/_rels/", -1) + } + } + } + target := f.deleteSheetFromWorkbookRels(sheet.ID) f.deleteSheetFromContentTypes(target) - f.deleteCalcChain(v.SheetID, "") // Delete CalcChain - delete(f.sheetMap, name) - delete(f.XLSX, sheet) + f.deleteCalcChain(sheet.SheetID, "") // Delete CalcChain + delete(f.sheetMap, sheetName) + delete(f.XLSX, sheetXML) delete(f.XLSX, rels) - delete(f.Sheet, sheet) + delete(f.Sheet, sheetXML) f.SheetCount-- } } @@ -1285,6 +1291,63 @@ func (f *File) GetDefinedName() []DefinedName { return definedNames } +// 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 + var inActiveSheet bool + activeSheet := f.GetActiveSheetIndex() + sheetMap := f.GetSheetMap() + for idx, sheetName := range sheetMap { + for _, s := range sheets { + if s == sheetName && idx == activeSheet { + inActiveSheet = true + } + } + } + if !inActiveSheet { + return errors.New("group worksheet must contain an active worksheet") + } + // check worksheet exists + ws := []*xlsxWorksheet{} + for _, sheet := range sheets { + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return err + } + ws = append(ws, xlsx) + } + for _, s := range ws { + sheetViews := s.SheetViews.SheetView + if len(sheetViews) > 0 { + for idx := range sheetViews { + s.SheetViews.SheetView[idx].TabSelected = true + } + continue + } + } + return nil +} + +// UngroupSheets provides a function to ungroup worksheets. +func (f *File) UngroupSheets() error { + activeSheet := f.GetActiveSheetIndex() + sheetMap := f.GetSheetMap() + for sheetID, sheet := range sheetMap { + if activeSheet == sheetID { + continue + } + xlsx, _ := f.workSheetReader(sheet) + sheetViews := xlsx.SheetViews.SheetView + if len(sheetViews) > 0 { + for idx := range sheetViews { + xlsx.SheetViews.SheetView[idx].TabSelected = false + } + } + } + return nil +} + // workSheetRelsReader provides a function to get the pointer to the structure // after deserialization of xl/worksheets/_rels/sheet%d.xml.rels. func (f *File) workSheetRelsReader(path string) *xlsxWorkbookRels { From e780aa27c8037fadbc4a7c08466b52d0f4ac268a Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 4 Jul 2019 16:15:20 +0800 Subject: [PATCH 113/957] Add unit test for GroupSheets and UngroupSheets --- sheet_test.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/sheet_test.go b/sheet_test.go index a7fd9e940f..3a7f579001 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -187,3 +187,24 @@ func TestDefinedName(t *testing.T) { assert.Exactly(t, "Sheet1!$A$2:$D$5", f.GetDefinedName()[1].RefersTo) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDefinedName.xlsx"))) } + +func TestGroupSheets(t *testing.T) { + f := excelize.NewFile() + sheets := []string{"Sheet2", "Sheet3"} + 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{"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"))) +} + +func TestUngroupSheets(t *testing.T) { + f := excelize.NewFile() + sheets := []string{"Sheet2", "Sheet3", "Sheet4", "Sheet5"} + for _, sheet := range sheets { + f.NewSheet(sheet) + } + assert.NoError(t, f.UngroupSheets()) +} From 4897276c68474c5a3e16ac4e07fae55738c66eca Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 5 Jul 2019 23:15:39 +0800 Subject: [PATCH 114/957] Make fitToHeight tag omit empty --- xmlWorksheet.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 4d19cde5b5..b94c521187 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -90,7 +90,7 @@ type xlsxPageSetUp struct { Draft bool `xml:"draft,attr,omitempty"` Errors string `xml:"errors,attr,omitempty"` FirstPageNumber int `xml:"firstPageNumber,attr,omitempty"` - FitToHeight *int `xml:"fitToHeight,attr"` + FitToHeight int `xml:"fitToHeight,attr,omitempty"` FitToWidth int `xml:"fitToWidth,attr,omitempty"` HorizontalDPI float32 `xml:"horizontalDpi,attr,omitempty"` RID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr,omitempty"` From e14d2febc880f5dc0a8352f9f57af5ac3a9d37f5 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 6 Jul 2019 15:11:51 +0800 Subject: [PATCH 115/957] Resolve #432, supplement the function of SetPageLayout SetPageLayout support to set fit to width and height --- sheet.go | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/sheet.go b/sheet.go index d579c6afee..1c19e86435 100644 --- a/sheet.go +++ b/sheet.go @@ -1025,6 +1025,10 @@ type ( PageLayoutOrientation string // PageLayoutPaperSize defines the paper size of the worksheet PageLayoutPaperSize int + // FitToHeight specified number of vertical pages to fit on + FitToHeight int + // FitToWidth specified number of horizontal pages to fit on + FitToWidth int ) const ( @@ -1064,6 +1068,38 @@ func (p *PageLayoutPaperSize) getPageLayout(ps *xlsxPageSetUp) { *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 = 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 == 0 { + *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 = 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 == 0 { + *p = 1 + return + } + *p = FitToWidth(ps.FitToWidth) +} + // SetPageLayout provides a function to sets worksheet page layout. // // Available options: @@ -1213,6 +1249,8 @@ func (f *File) SetPageLayout(sheet string, opts ...PageLayoutOption) error { // Available options: // 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 { From 14d490c83d9744e635c88bca9018994dfe8981af Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 7 Jul 2019 00:17:15 +0800 Subject: [PATCH 116/957] Add unit test for SetPageLayout --- sheet_test.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/sheet_test.go b/sheet_test.go index 3a7f579001..ef795ada56 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -23,6 +23,8 @@ func ExampleFile_SetPageLayout() { if err := f.SetPageLayout( "Sheet1", excelize.PageLayoutPaperSize(10), + excelize.FitToHeight(2), + excelize.FitToWidth(2), ); err != nil { panic(err) } @@ -34,6 +36,8 @@ func ExampleFile_GetPageLayout() { var ( orientation excelize.PageLayoutOrientation paperSize excelize.PageLayoutPaperSize + fitToHeight excelize.FitToHeight + fitToWidth excelize.FitToWidth ) if err := f.GetPageLayout("Sheet1", &orientation); err != nil { panic(err) @@ -41,13 +45,24 @@ func ExampleFile_GetPageLayout() { if err := f.GetPageLayout("Sheet1", &paperSize); err != nil { panic(err) } + if err := f.GetPageLayout("Sheet1", &fitToHeight); err != nil { + panic(err) + } + + if err := f.GetPageLayout("Sheet1", &fitToWidth); err != nil { + panic(err) + } fmt.Println("Defaults:") 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) // Output: // Defaults: // - orientation: "portrait" // - paper size: 1 + // - fit to height: 1 + // - fit to width: 1 } func TestPageLayoutOption(t *testing.T) { @@ -59,6 +74,8 @@ func TestPageLayoutOption(t *testing.T) { }{ {new(excelize.PageLayoutOrientation), excelize.PageLayoutOrientation(excelize.OrientationLandscape)}, {new(excelize.PageLayoutPaperSize), excelize.PageLayoutPaperSize(10)}, + {new(excelize.FitToHeight), excelize.FitToHeight(2)}, + {new(excelize.FitToWidth), excelize.FitToWidth(2)}, } for i, test := range testData { From 821b90d1f0e53518c84dce732e265348c46e8f12 Mon Sep 17 00:00:00 2001 From: Farmerx Date: Mon, 8 Jul 2019 15:18:36 +0800 Subject: [PATCH 117/957] # add: add Remarks for CalcChain c Attributes --- xmlCalcChain.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/xmlCalcChain.go b/xmlCalcChain.go index 9c916bf35a..f34aab1cbb 100644 --- a/xmlCalcChain.go +++ b/xmlCalcChain.go @@ -18,6 +18,19 @@ type xlsxCalcChain struct { } // xlsxCalcChainC directly maps the c element. +// | Attributes | Description | +// ----------------------------------------------------------------------------------- +// | 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 are 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 boolean datatype. type xlsxCalcChainC struct { R string `xml:"r,attr"` I int `xml:"i,attr"` @@ -26,3 +39,4 @@ type xlsxCalcChainC struct { T bool `xml:"t,attr,omitempty"` A bool `xml:"a,attr,omitempty"` } + From 6962061200e2198acc39392c35344d1826268f34 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 8 Jul 2019 20:17:47 +0800 Subject: [PATCH 118/957] add comments for SheetView parameters --- sheetview.go | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/sheetview.go b/sheetview.go index 8ffc9bcd6d..91260fe6fa 100644 --- a/sheetview.go +++ b/sheetview.go @@ -23,22 +23,36 @@ type SheetViewOptionPtr interface { } type ( - // DefaultGridColor is a SheetViewOption. + // DefaultGridColor specified a flag indicating that the consuming + // application should use the default grid lines color (system dependent). + // Overrides any color specified in colorId. DefaultGridColor bool - // RightToLeft is a SheetViewOption. + // RightToLeft specified 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 - // ShowFormulas is a SheetViewOption. + // ShowFormulas specified a flag indicating whether this sheet should display + // formulas. ShowFormulas bool - // ShowGridLines is a SheetViewOption. + // ShowGridLines specified a flag indicating whether this sheet should + // display gridlines. ShowGridLines bool - // ShowRowColHeaders is a SheetViewOption. + // ShowRowColHeaders specified a flag indicating whether the sheet should + // display row and column headings. ShowRowColHeaders bool - // ZoomScale is a SheetViewOption. + // ZoomScale specified 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 - // TopLeftCell is a SheetViewOption. + // TopLeftCell specified 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 /* TODO - // ShowWhiteSpace is a SheetViewOption. + // ShowWhiteSpace specified flag indicating whether page layout view shall + // display margins. False means do not display left, right, top (header), and + // bottom (footer) margins (even when there is data in the header or footer). ShowWhiteSpace bool // ShowZeros is a SheetViewOption. ShowZeros bool @@ -98,7 +112,7 @@ func (o *ShowRowColHeaders) getSheetViewOption(view *xlsxSheetView) { } func (o ZoomScale) setSheetViewOption(view *xlsxSheetView) { - //This attribute is restricted to values ranging from 10 to 400. + // This attribute is restricted to values ranging from 10 to 400. if float64(o) >= 10 && float64(o) <= 400 { view.ZoomScale = float64(o) } @@ -135,6 +149,8 @@ func (f *File) getSheetView(sheetName string, viewIndex int) (*xlsxSheetView, er // ShowFormulas(bool) // ShowGridLines(bool) // ShowRowColHeaders(bool) +// ZoomScale(float64) +// TopLeftCell(string) // Example: // err = f.SetSheetViewOptions("Sheet1", -1, ShowGridLines(false)) func (f *File) SetSheetViewOptions(name string, viewIndex int, opts ...SheetViewOption) error { @@ -158,6 +174,8 @@ func (f *File) SetSheetViewOptions(name string, viewIndex int, opts ...SheetView // ShowFormulas(bool) // ShowGridLines(bool) // ShowRowColHeaders(bool) +// ZoomScale(float64) +// TopLeftCell(string) // Example: // var showGridLines excelize.ShowGridLines // err = f.GetSheetViewOptions("Sheet1", -1, &showGridLines) From 363a05734591c4d1056958bec758a3819bed90d8 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 14 Jul 2019 13:13:10 +0800 Subject: [PATCH 119/957] Structure update #434 Add a missing element of the comment text elements --- xmlComments.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/xmlComments.go b/xmlComments.go index 5ffbecf740..47d8f5156f 100644 --- a/xmlComments.go +++ b/xmlComments.go @@ -54,7 +54,20 @@ type xlsxComment struct { // spreadsheet application implementation detail. A recommended guideline is // 32767 chars. type xlsxText struct { - R []xlsxR `xml:"r"` + T *string `xml:"t"` + R []xlsxR `xml:"r"` + RPh *xlsxPhoneticRun `xml:"rPh"` + PhoneticPr *xlsxPhoneticPr `xml:"phoneticPr"` +} + +// xlsxPhoneticRun element represents a run of text which displays a phonetic +// hint for this String Item (si). Phonetic hints are used to give information +// about the pronunciation of an East Asian language. The hints are displayed +// as text within the spreadsheet cells across the top portion of the cell. +type xlsxPhoneticRun struct { + Sb uint32 `xml:"sb,attr"` + Eb uint32 `xml:"eb,attr"` + T string `xml:"t,attr"` } // formatComment directly maps the format settings of the comment. From 74c61126581736e2214e440718fba9843d91ab94 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 15 Jul 2019 09:13:55 +0800 Subject: [PATCH 120/957] Fix #434, add missing comments --- comment.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/comment.go b/comment.go index af70820a82..bc6fa2771b 100644 --- a/comment.go +++ b/comment.go @@ -42,6 +42,9 @@ func (f *File) GetComments() (comments map[string][]Comment) { } sheetComment.Ref = comment.Ref sheetComment.AuthorID = comment.AuthorID + if comment.Text.T != nil { + sheetComment.Text += *comment.Text.T + } for _, text := range comment.Text.R { sheetComment.Text += text.T } From 4f469530de9877f94001da1056de7d320b6dae35 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 17 Jul 2019 19:25:41 +0800 Subject: [PATCH 121/957] Update docs --- xmlCalcChain.go | 69 +++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 55 insertions(+), 14 deletions(-) diff --git a/xmlCalcChain.go b/xmlCalcChain.go index f34aab1cbb..05a176d1b2 100644 --- a/xmlCalcChain.go +++ b/xmlCalcChain.go @@ -18,19 +18,61 @@ type xlsxCalcChain struct { } // xlsxCalcChainC directly maps the c element. -// | Attributes | Description | -// ----------------------------------------------------------------------------------- -// | 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 are 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 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 are 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 +// | boolean datatype. +// type xlsxCalcChainC struct { R string `xml:"r,attr"` I int `xml:"i,attr"` @@ -39,4 +81,3 @@ type xlsxCalcChainC struct { T bool `xml:"t,attr,omitempty"` A bool `xml:"a,attr,omitempty"` } - From 855c3605f6fce4916cdde1dadba2dd73d9f4b744 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 20 Jul 2019 19:24:57 +0800 Subject: [PATCH 122/957] Fix #437, recalculate offset for merged cells adjuster --- adjust.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/adjust.go b/adjust.go index 56d812fc72..f26f132597 100644 --- a/adjust.go +++ b/adjust.go @@ -231,7 +231,8 @@ func (f *File) adjustMergeCells(xlsx *xlsxWorksheet, dir adjustDirection, num, o return nil } - for i, areaData := range xlsx.MergeCells.Cells { + for i := 0; i < len(xlsx.MergeCells.Cells); i++ { + areaData := xlsx.MergeCells.Cells[i] coordinates, err := f.areaRefToCoordinates(areaData.Ref) if err != nil { return err @@ -240,18 +241,21 @@ func (f *File) adjustMergeCells(xlsx *xlsxWorksheet, dir adjustDirection, num, o if dir == rows { if y1 == num && y2 == num && offset < 0 { f.deleteMergeCell(xlsx, i) + i-- } y1 = f.adjustMergeCellsHelper(y1, num, offset) y2 = f.adjustMergeCellsHelper(y2, num, offset) } else { if x1 == num && x2 == num && offset < 0 { f.deleteMergeCell(xlsx, i) + i-- } x1 = f.adjustMergeCellsHelper(x1, num, offset) x2 = f.adjustMergeCellsHelper(x2, num, offset) } if x1 == x2 && y1 == y2 { f.deleteMergeCell(xlsx, i) + i-- } if areaData.Ref, err = f.coordinatesToAreaRef([]int{x1, y1, x2, y2}); err != nil { return err @@ -276,7 +280,7 @@ func (f *File) adjustMergeCellsHelper(pivot, num, offset int) int { // deleteMergeCell provides a function to delete merged cell by given index. func (f *File) deleteMergeCell(sheet *xlsxWorksheet, idx int) { - if len(sheet.MergeCells.Cells) > 1 { + if len(sheet.MergeCells.Cells) > idx { sheet.MergeCells.Cells = append(sheet.MergeCells.Cells[:idx], sheet.MergeCells.Cells[idx+1:]...) sheet.MergeCells.Count = len(sheet.MergeCells.Cells) } else { From 35e485756f1d3f5eb1e5f78a5cee06b5ed902645 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 21 Jul 2019 12:56:36 +0800 Subject: [PATCH 123/957] Resolve #217, new function add VBA project supported. --- excelize.go | 77 ++++++++++++++++++++++++++++++++++++++++++++ excelize_test.go | 11 +++++++ test/vbaProject.bin | Bin 0 -> 16896 bytes xmlDrawing.go | 1 + 4 files changed, 89 insertions(+) create mode 100755 test/vbaProject.bin diff --git a/excelize.go b/excelize.go index f636a84061..c7eff103b4 100644 --- a/excelize.go +++ b/excelize.go @@ -19,7 +19,9 @@ import ( "io" "io/ioutil" "os" + "path" "strconv" + "strings" ) // File define a populated XLSX file struct. @@ -226,3 +228,78 @@ func (f *File) UpdateLinkedValue() error { } return nil } + +// AddVBAProject provides the method to add vbaProject.bin file which contains +// functions and/or macros. The file extension should be .xlsm. For example: +// +// err := f.SetSheetPrOptions("Sheet1", excelize.CodeName("Sheet1")) +// if err != nil { +// fmt.Println(err) +// } +// err = f.AddVBAProject("vbaProject.bin") +// if err != nil { +// fmt.Println(err) +// } +// err = f.SaveAs("macros.xlsm") +// if err != nil { +// fmt.Println(err) +// } +// +func (f *File) AddVBAProject(bin string) error { + var err error + // Check vbaProject.bin exists first. + if _, err = os.Stat(bin); os.IsNotExist(err) { + return err + } + if path.Ext(bin) != ".bin" { + return errors.New("unsupported VBA project extension") + } + f.setContentTypePartVBAProjectExtensions() + wb := f.workbookRelsReader() + var rID int + var ok bool + for _, rel := range wb.Relationships { + if rel.Target == "vbaProject.bin" && rel.Type == SourceRelationshipVBAProject { + ok = true + continue + } + t, _ := strconv.Atoi(strings.TrimPrefix(rel.ID, "rId")) + if t > rID { + rID = t + } + } + rID++ + if !ok { + wb.Relationships = append(wb.Relationships, xlsxWorkbookRelation{ + ID: "rId" + strconv.Itoa(rID), + Target: "vbaProject.bin", + Type: SourceRelationshipVBAProject, + }) + } + file, _ := ioutil.ReadFile(bin) + f.XLSX["xl/vbaProject.bin"] = file + return err +} + +// setContentTypePartVBAProjectExtensions provides a function to set the +// content type for relationship parts and the main document part. +func (f *File) setContentTypePartVBAProjectExtensions() { + var ok bool + content := f.contentTypesReader() + for _, v := range content.Defaults { + if v.Extension == "bin" { + ok = true + } + } + for idx, o := range content.Overrides { + if o.PartName == "/xl/workbook.xml" { + content.Overrides[idx].ContentType = "application/vnd.ms-excel.sheet.macroEnabled.main+xml" + } + } + if !ok { + content.Defaults = append(content.Defaults, xlsxDefault{ + Extension: "bin", + ContentType: "application/vnd.ms-office.vbaProject", + }) + } +} diff --git a/excelize_test.go b/excelize_test.go index c4a06a54f6..79010b1180 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -1078,6 +1078,17 @@ func TestSetDefaultTimeStyle(t *testing.T) { assert.EqualError(t, f.setDefaultTimeStyle("SheetN", "", 0), "sheet SheetN is not exist") } +func TestAddVBAProject(t *testing.T) { + f := NewFile() + assert.NoError(t, f.SetSheetPrOptions("Sheet1", CodeName("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")), "unsupported VBA project extension") + 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"))) +} + func prepareTestBook1() (*File, error) { f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if err != nil { diff --git a/test/vbaProject.bin b/test/vbaProject.bin new file mode 100755 index 0000000000000000000000000000000000000000..fc15dca28e5ffd30e75f7464d4306110caaf3ff9 GIT binary patch literal 16896 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 Date: Mon, 22 Jul 2019 22:36:44 +0800 Subject: [PATCH 124/957] Fix invalid formula in data validation drop list --- datavalidation.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/datavalidation.go b/datavalidation.go index 56b96fd6db..209204ae28 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -141,12 +141,12 @@ func (dd *DataValidation) SetRange(f1, f2 int, t DataValidationType, o DataValid // // dvRange := excelize.NewDataValidation(true) // dvRange.Sqref = "A7:B8" -// dvRange.SetSqrefDropList("E1:E3", true) +// dvRange.SetSqrefDropList("$E$1:$E$3", true) // f.AddDataValidation("Sheet1", dvRange) // func (dd *DataValidation) SetSqrefDropList(sqref string, isCurrentSheet bool) error { if isCurrentSheet { - dd.Formula1 = sqref + dd.Formula1 = fmt.Sprintf("%s", sqref) dd.Type = convDataValidationType(typeList) return nil } From 53e653f28ef38e8b2175fdb88de72156eab14ee4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=91=A3=E5=87=AF?= <13122321216@163.com> Date: Thu, 25 Jul 2019 20:27:03 +0800 Subject: [PATCH 125/957] Fix #443 --- sheet.go | 1 + 1 file changed, 1 insertion(+) diff --git a/sheet.go b/sheet.go index 1c19e86435..9c288b0a74 100644 --- a/sheet.go +++ b/sheet.go @@ -110,6 +110,7 @@ func (f *File) workSheetWriter() { f.saveFileList(p, replaceRelationshipsBytes(replaceWorkSheetsRelationshipsNameSpaceBytes(output))) ok := f.checked[p] if ok { + delete(f.Sheet, p) f.checked[p] = false } } From 0c9e5137e35b32e6046d25604edcb9a33f8353a2 Mon Sep 17 00:00:00 2001 From: Sustainedhhh <15829307082_pp@sina.cn> Date: Thu, 25 Jul 2019 20:31:21 +0800 Subject: [PATCH 126/957] Fix #442 --- sheet.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/sheet.go b/sheet.go index 1c19e86435..32fc351ed4 100644 --- a/sheet.go +++ b/sheet.go @@ -372,7 +372,12 @@ func (f *File) getSheetMap() map[string]string { for _, v := range content.Sheets.Sheet { for _, rel := range rels.Relationships { if rel.ID == v.ID { - maps[v.Name] = fmt.Sprintf("xl/%s", rel.Target) + // Construct a target XML as xl/worksheets/sheet%d by split path, compatible with different types of relative paths in workbook.xml.rels, for example: worksheets/sheet%d.xml and /xl/worksheets/sheet%d.xml + pathInfo := strings.Split(rel.Target, "/") + pathInfoLen := len(pathInfo) + if pathInfoLen > 0 { + maps[v.Name] = fmt.Sprintf("xl/worksheets/%s", pathInfo[pathInfoLen-1]) + } } } } From 9279c86d85ab0077f3696b8ec4cfb49ad8222530 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 31 Jul 2019 23:53:49 +0800 Subject: [PATCH 127/957] Add extensions URI of spreadsheetML --- xmlDrawing.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/xmlDrawing.go b/xmlDrawing.go index 1201cc89dd..2f75eef270 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -31,6 +31,8 @@ const ( NameSpaceDrawingMLChart = "http://schemas.openxmlformats.org/drawingml/2006/chart" NameSpaceDrawingMLSpreadSheet = "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" NameSpaceSpreadSheet = "http://schemas.openxmlformats.org/spreadsheetml/2006/main" + NameSpaceSpreadSheetX14 = "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main" + NameSpaceSpreadSheetExcel2006Main = "http://schemas.microsoft.com/office/excel/2006/main" NameSpaceXML = "http://www.w3.org/XML/1998/namespace" NameSpaceXMLSchemaInstance = "http://www.w3.org/2001/XMLSchema-instance" StrictSourceRelationship = "http://purl.oclc.org/ooxml/officeDocument/relationships" @@ -41,6 +43,19 @@ const ( NameSpaceDublinCore = "http://purl.org/dc/elements/1.1/" NameSpaceDublinCoreTerms = "http://purl.org/dc/terms/" NameSpaceDublinCoreMetadataIntiative = "http://purl.org/dc/dcmitype/" + // 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}" + ExtURISlicerList = "{A8765BA9-456A-4DAB-B4F3-ACF838C121DE}" + 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}" ) var supportImageTypes = map[string]string{".gif": ".gif", ".jpg": ".jpeg", ".jpeg": ".jpeg", ".png": ".png", ".tif": ".tiff", ".tiff": ".tiff"} From 1092009541430c711676efb95b876598f59bb53c Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 3 Aug 2019 23:10:01 +0800 Subject: [PATCH 128/957] Fixed doc corruption when deleting all merged cells --- adjust.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/adjust.go b/adjust.go index f26f132597..ccc5ce94d0 100644 --- a/adjust.go +++ b/adjust.go @@ -54,6 +54,11 @@ func (f *File) adjustHelper(sheet string, dir adjustDirection, num, offset int) } checkSheet(xlsx) checkRow(xlsx) + + if xlsx.MergeCells != nil && len(xlsx.MergeCells.Cells) == 0 { + xlsx.MergeCells = nil + } + return nil } @@ -283,8 +288,6 @@ func (f *File) deleteMergeCell(sheet *xlsxWorksheet, idx int) { if len(sheet.MergeCells.Cells) > idx { sheet.MergeCells.Cells = append(sheet.MergeCells.Cells[:idx], sheet.MergeCells.Cells[idx+1:]...) sheet.MergeCells.Count = len(sheet.MergeCells.Cells) - } else { - sheet.MergeCells = nil } } From cbe919fdf6c00733513494680b89171b8b1b41a1 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 4 Aug 2019 20:24:59 +0800 Subject: [PATCH 129/957] New feature: sparkline supported --- excelize.go | 12 +- sheet.go | 2 +- sparkline.go | 509 ++++++++++++++++++++++++++++++++++++++++++++++ sparkline_test.go | 297 +++++++++++++++++++++++++++ styles.go | 2 +- xmlDrawing.go | 7 +- xmlWorksheet.go | 108 +++++++++- 7 files changed, 932 insertions(+), 5 deletions(-) create mode 100644 sparkline.go create mode 100644 sparkline_test.go diff --git a/excelize.go b/excelize.go index c7eff103b4..6d014a0942 100644 --- a/excelize.go +++ b/excelize.go @@ -181,11 +181,21 @@ func checkSheet(xlsx *xlsxWorksheet) { // Office Excel 2007. func replaceWorkSheetsRelationshipsNameSpaceBytes(workbookMarshal []byte) []byte { var oldXmlns = []byte(``) - var newXmlns = []byte(``) + var newXmlns = []byte(``) workbookMarshal = bytes.Replace(workbookMarshal, oldXmlns, newXmlns, -1) return workbookMarshal } +// replaceStyleRelationshipsNameSpaceBytes provides a function to replace +// xl/styles.xml XML tags to self-closing for compatible Microsoft Office +// Excel 2007. +func replaceStyleRelationshipsNameSpaceBytes(contentMarshal []byte) []byte { + var oldXmlns = []byte(``) + var newXmlns = []byte(``) + contentMarshal = bytes.Replace(contentMarshal, oldXmlns, newXmlns, -1) + return contentMarshal +} + // 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 // cell have a linked value. Reference diff --git a/sheet.go b/sheet.go index 347f25562e..e02782a7af 100644 --- a/sheet.go +++ b/sheet.go @@ -232,7 +232,7 @@ func replaceRelationshipsBytes(content []byte) []byte { // a horrible hack to fix that after the XML marshalling is completed. func replaceRelationshipsNameSpaceBytes(workbookMarshal []byte) []byte { oldXmlns := []byte(``) - newXmlns := []byte(``) + newXmlns := []byte(``) return bytes.Replace(workbookMarshal, oldXmlns, newXmlns, -1) } diff --git a/sparkline.go b/sparkline.go new file mode 100644 index 0000000000..73e125ed4d --- /dev/null +++ b/sparkline.go @@ -0,0 +1,509 @@ +// Copyright 2016 - 2019 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. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.8 or later. + +package excelize + +import ( + "encoding/xml" + "errors" + "strings" +) + +// addSparklineGroupByStyle provides a function to create x14:sparklineGroups +// element by given sparkline style ID. +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}, + }, // 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}, + }, // 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}, + }, // 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}, + }, // 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}, + }, // 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}, + }, // 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}, + }, // 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}, + }, // 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}, + }, // 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}, + }, // 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}, + }, // 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}, + }, // 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}, + }, // 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}, + }, // 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}, + }, // 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}, + }, // 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}, + }, // 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}, + }, // 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}, + }, // 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}, + }, // 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}, + }, // 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}, + }, // 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}, + }, // 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}, + }, // 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}, + }, // 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}, + }, // 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}, + }, // 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"}, + }, // 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"}, + }, // 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"}, + }, // 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"}, + }, // 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"}, + }, // 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"}, + }, // 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"}, + }, // 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"}, + }, // 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}, + }, // 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}, + }, // 36 + } + return groups[ID] +} + +// AddSparkline provides a function to add sparklines to the worksheet by +// 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 +// 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, +// }) +// +// 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 sparkine high points +// Low | Toggle sparkine low points +// First | Toggle sparkine first points +// Last | Toggle sparkine last points +// Negative | Toggle sparkine negative points +// Markers | Toggle sparkine markers +// ColorAxis | An RGB Color is specified as RRGGBB +// Axis | Show sparkline axis +// +func (f *File) AddSparkline(sheet string, opt *SparklineOption) error { + // parameter validation + ws, err := f.parseFormatAddSparklineSet(sheet, opt) + if err != nil { + return err + } + // Handle the sparkline type + sparkType := "line" + sparkTypes := map[string]string{"line": "line", "column": "column", "win_loss": "stacked"} + if opt.Type != "" { + specifiedSparkTypes, ok := sparkTypes[opt.Type] + if !ok { + return errors.New("parameter 'Type' must be 'line', 'column' or 'win_loss'") + } + sparkType = specifiedSparkTypes + } + group := f.addSparklineGroupByStyle(opt.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.ColorSeries = &xlsxTabColor{ + RGB: getPaletteColor(opt.SeriesColor), + } + } + if opt.Reverse { + group.RightToLeft = opt.Reverse + } + f.addSparkline(opt, group) + if ws.ExtLst.Ext != "" { // append mode ext + decodeExtLst := decodeWorksheetExt{} + err = xml.Unmarshal([]byte(""+ws.ExtLst.Ext+""), &decodeExtLst) + if err != nil { + return err + } + for idx, ext := range decodeExtLst.Ext { + // hack: add back missing namespace + decodeExtLst.Ext[idx].XMLNSX14 = decodeExtLst.Ext[idx].X14 + decodeExtLst.Ext[idx].XMLNSX15 = decodeExtLst.Ext[idx].X15 + decodeExtLst.Ext[idx].XMLNSX14 = "" + decodeExtLst.Ext[idx].XMLNSX15 = "" + if ext.URI == ExtURISparklineGroups { + decodeSparklineGroups := decodeX14SparklineGroups{} + _ = xml.Unmarshal([]byte(ext.Content), &decodeSparklineGroups) + sparklineGroupBytes, _ := xml.Marshal(group) + groups := xlsxX14SparklineGroups{ + XMLNSXM: NameSpaceSpreadSheetExcel2006Main, + Content: decodeSparklineGroups.Content + string(sparklineGroupBytes), + } + sparklineGroupsBytes, _ := xml.Marshal(groups) + decodeExtLst.Ext[idx].Content = string(sparklineGroupsBytes) + } + } + extLstBytes, _ := xml.Marshal(decodeExtLst) + extLst := string(extLstBytes) + ws.ExtLst = &xlsxExtLst{ + Ext: strings.TrimSuffix(strings.TrimPrefix(extLst, ""), ""), + } + } else { + groups := xlsxX14SparklineGroups{ + XMLNSXM: NameSpaceSpreadSheetExcel2006Main, + SparklineGroups: []*xlsxX14SparklineGroup{group}, + } + sparklineGroupsBytes, _ := xml.Marshal(groups) + extLst := xlsxWorksheetExt{ + XMLNSX14: NameSpaceSpreadSheetX14, + URI: ExtURISparklineGroups, + Content: string(sparklineGroupsBytes), + } + extBytes, _ := xml.Marshal(extLst) + ws.ExtLst.Ext = string(extBytes) + } + return nil +} + +// parseFormatAddSparklineSet provides a function to validate sparkline +// properties. +func (f *File) parseFormatAddSparklineSet(sheet string, opt *SparklineOption) (*xlsxWorksheet, error) { + ws, err := f.workSheetReader(sheet) + if err != nil { + return ws, err + } + if opt == nil { + return ws, errors.New("parameter is required") + } + if len(opt.Location) < 1 { + return ws, errors.New("parameter 'Location' is required") + } + if len(opt.Range) < 1 { + return ws, errors.New("parameter 'Range' is required") + } + // The ranges and locations must match.\ + if len(opt.Location) != len(opt.Range) { + return ws, errors.New(`must have the same number of 'Location' and 'Range' parameters`) + } + if opt.Style < 0 || opt.Style > 35 { + return ws, errors.New("parameter 'Style' must betweent 0-35") + } + if ws.ExtLst == nil { + ws.ExtLst = &xlsxExtLst{} + } + return ws, err +} + +// 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 { + group.Sparklines.Sparkline = append(group.Sparklines.Sparkline, &xlsxX14Sparkline{ + F: opt.Range[idx], + Sqref: location, + }) + } +} diff --git a/sparkline_test.go b/sparkline_test.go new file mode 100644 index 0000000000..d52929bde5 --- /dev/null +++ b/sparkline_test.go @@ -0,0 +1,297 @@ +package excelize + +import ( + "fmt" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAddSparkline(t *testing.T) { + f := prepareSparklineDataset() + + // Set the columns widths to make the output clearer + 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))) + + f.SetColWidth("Sheet1", "A", "A", 14) + f.SetColWidth("Sheet1", "B", "B", 50) + // Headings + f.SetCellValue("Sheet1", "A1", "Sparkline") + f.SetCellValue("Sheet1", "B1", "Description") + + f.SetCellValue("Sheet1", "B2", `A default "line" sparkline.`) + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Location: []string{"A2"}, + Range: []string{"Sheet3!A1:J1"}, + })) + + f.SetCellValue("Sheet1", "B3", `A default "column" sparkline.`) + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Location: []string{"A3"}, + Range: []string{"Sheet3!A2:J2"}, + Type: "column", + })) + + f.SetCellValue("Sheet1", "B4", `A default "win/loss" sparkline.`) + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Location: []string{"A4"}, + Range: []string{"Sheet3!A3:J3"}, + Type: "win_loss", + })) + + f.SetCellValue("Sheet1", "B6", "Line with markers.") + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Location: []string{"A6"}, + Range: []string{"Sheet3!A1:J1"}, + Markers: true, + })) + + f.SetCellValue("Sheet1", "B7", "Line with high and low points.") + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Location: []string{"A7"}, + Range: []string{"Sheet3!A1:J1"}, + High: true, + Low: true, + })) + + f.SetCellValue("Sheet1", "B8", "Line with first and last point markers.") + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Location: []string{"A8"}, + Range: []string{"Sheet3!A1:J1"}, + First: true, + Last: true, + })) + + f.SetCellValue("Sheet1", "B9", "Line with negative point markers.") + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Location: []string{"A9"}, + Range: []string{"Sheet3!A1:J1"}, + Negative: true, + })) + + f.SetCellValue("Sheet1", "B10", "Line with axis.") + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Location: []string{"A10"}, + Range: []string{"Sheet3!A1:J1"}, + Axis: true, + })) + + f.SetCellValue("Sheet1", "B12", "Column with default style (1).") + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Location: []string{"A12"}, + Range: []string{"Sheet3!A2:J2"}, + Type: "column", + })) + + f.SetCellValue("Sheet1", "B13", "Column with style 2.") + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Location: []string{"A13"}, + Range: []string{"Sheet3!A2:J2"}, + Type: "column", + Style: 2, + })) + + f.SetCellValue("Sheet1", "B14", "Column with style 3.") + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Location: []string{"A14"}, + Range: []string{"Sheet3!A2:J2"}, + Type: "column", + Style: 3, + })) + + f.SetCellValue("Sheet1", "B15", "Column with style 4.") + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Location: []string{"A15"}, + Range: []string{"Sheet3!A2:J2"}, + Type: "column", + Style: 4, + })) + + f.SetCellValue("Sheet1", "B16", "Column with style 5.") + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Location: []string{"A16"}, + Range: []string{"Sheet3!A2:J2"}, + Type: "column", + Style: 5, + })) + + f.SetCellValue("Sheet1", "B17", "Column with style 6.") + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Location: []string{"A17"}, + Range: []string{"Sheet3!A2:J2"}, + Type: "column", + Style: 6, + })) + + f.SetCellValue("Sheet1", "B18", "Column with a user defined color.") + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Location: []string{"A18"}, + Range: []string{"Sheet3!A2:J2"}, + Type: "column", + SeriesColor: "#E965E0", + })) + + f.SetCellValue("Sheet1", "B20", "A win/loss sparkline.") + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Location: []string{"A20"}, + Range: []string{"Sheet3!A3:J3"}, + Type: "win_loss", + })) + + f.SetCellValue("Sheet1", "B21", "A win/loss sparkline with negative points highlighted.") + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Location: []string{"A21"}, + Range: []string{"Sheet3!A3:J3"}, + Type: "win_loss", + Negative: true, + })) + + f.SetCellValue("Sheet1", "B23", "A left to right column (the default).") + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Location: []string{"A23"}, + Range: []string{"Sheet3!A4:J4"}, + Type: "column", + Style: 20, + })) + + f.SetCellValue("Sheet1", "B24", "A right to left column.") + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Location: []string{"A24"}, + Range: []string{"Sheet3!A4:J4"}, + Type: "column", + Style: 20, + Reverse: true, + })) + + f.SetCellValue("Sheet1", "B25", "Sparkline and text in one cell.") + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Location: []string{"A25"}, + Range: []string{"Sheet3!A4:J4"}, + Type: "column", + Style: 20, + })) + f.SetCellValue("Sheet1", "A25", "Growth") + + f.SetCellValue("Sheet1", "B27", "A grouped sparkline. Changes are applied to all three.") + assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ + 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{ + Location: []string{"F3"}, + Range: []string{"Sheet2!A3:E3"}, + Type: "win_loss", + Negative: true, + })) + + assert.NoError(t, f.AddSparkline("Sheet2", &SparklineOption{ + Location: []string{"F1"}, + Range: []string{"Sheet2!A1:E1"}, + Markers: true, + })) + + assert.NoError(t, f.AddSparkline("Sheet2", &SparklineOption{ + Location: []string{"F2"}, + Range: []string{"Sheet2!A2:E2"}, + Type: "column", + Style: 12, + })) + + assert.NoError(t, f.AddSparkline("Sheet2", &SparklineOption{ + Location: []string{"F3"}, + Range: []string{"Sheet2!A3:E3"}, + Type: "win_loss", + Negative: true, + })) + + // Save xlsx file by the given path. + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddSparkline.xlsx"))) + + // Test error exceptions + assert.EqualError(t, f.AddSparkline("SheetN", &SparklineOption{ + Location: []string{"F3"}, + Range: []string{"Sheet2!A3:E3"}, + }), "sheet SheetN is not exist") + + assert.EqualError(t, f.AddSparkline("Sheet1", nil), "parameter is required") + + assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Range: []string{"Sheet2!A3:E3"}, + }), `parameter 'Location' is required`) + + assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Location: []string{"F3"}, + }), `parameter 'Range' is required`) + + assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Location: []string{"F2", "F3"}, + Range: []string{"Sheet2!A3:E3"}, + }), `must have the same number of 'Location' and 'Range' parameters`) + + assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Location: []string{"F3"}, + Range: []string{"Sheet2!A3:E3"}, + Type: "unknown_type", + }), `parameter 'Type' must be 'line', 'column' or 'win_loss'`) + + assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Location: []string{"F3"}, + Range: []string{"Sheet2!A3:E3"}, + Style: -1, + }), `parameter 'Style' must betweent 0-35`) + + assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Location: []string{"F3"}, + Range: []string{"Sheet2!A3:E3"}, + Style: -1, + }), `parameter 'Style' must betweent 0-35`) + + f.Sheet["xl/worksheets/sheet1.xml"].ExtLst.Ext = ` + + + + + + + + ` + assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOption{ + Location: []string{"A2"}, + Range: []string{"Sheet3!A1:J1"}, + }), "XML syntax error on line 6: element closed by ") +} + +func prepareSparklineDataset() *File { + f := NewFile() + sheet2 := [][]int{ + {-2, 2, 3, -1, 0}, + {30, 20, 33, 20, 15}, + {1, -1, -1, 1, -1}, + } + sheet3 := [][]int{ + {-2, 2, 3, -1, 0, -2, 3, 2, 1, 0}, + {30, 20, 33, 20, 15, 5, 5, 15, 10, 15}, + {1, 1, -1, -1, 1, -1, 1, 1, 1, -1}, + {5, 6, 7, 10, 15, 20, 30, 50, 70, 100}, + {-2, 2, 3, -1, 0, -2, 3, 2, 1, 0}, + {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") + for row, data := range sheet2 { + f.SetSheetRow("Sheet2", fmt.Sprintf("A%d", row+1), &data) + } + for row, data := range sheet3 { + f.SetSheetRow("Sheet3", fmt.Sprintf("A%d", row+1), &data) + } + return f +} diff --git a/styles.go b/styles.go index b246e30607..04a5c3349a 100644 --- a/styles.go +++ b/styles.go @@ -1010,7 +1010,7 @@ func (f *File) stylesReader() *xlsxStyleSheet { func (f *File) styleSheetWriter() { if f.Styles != nil { output, _ := xml.Marshal(f.Styles) - f.saveFileList("xl/styles.xml", replaceWorkSheetsRelationshipsNameSpaceBytes(output)) + f.saveFileList("xl/styles.xml", replaceStyleRelationshipsNameSpaceBytes(output)) } } diff --git a/xmlDrawing.go b/xmlDrawing.go index 2f75eef270..20cb83dd99 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -32,7 +32,9 @@ const ( NameSpaceDrawingMLSpreadSheet = "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" NameSpaceSpreadSheet = "http://schemas.openxmlformats.org/spreadsheetml/2006/main" NameSpaceSpreadSheetX14 = "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main" + NameSpaceSpreadSheetX15 = "http://schemas.microsoft.com/office/spreadsheetml/2010/11/main" NameSpaceSpreadSheetExcel2006Main = "http://schemas.microsoft.com/office/excel/2006/main" + NameSpaceMacExcel2008Main = "http://schemas.microsoft.com/office/mac/excel/2008/main" NameSpaceXML = "http://www.w3.org/XML/1998/namespace" NameSpaceXMLSchemaInstance = "http://www.w3.org/2001/XMLSchema-instance" StrictSourceRelationship = "http://purl.oclc.org/ooxml/officeDocument/relationships" @@ -50,12 +52,15 @@ const ( ExtURIConditionalFormattings = "{78C0D931-6437-407D-A8EE-F0AAD7539E65}" ExtURIDataValidations = "{CCE6A557-97BC-4B89-ADB6-D9C93CAAB3DF}" ExtURISparklineGroups = "{05C60535-1F16-4fd2-B633-F4F36F0B64E0}" - ExtURISlicerList = "{A8765BA9-456A-4DAB-B4F3-ACF838C121DE}" + 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}" + ExtURIMacExcelMX = "{64002731-A6B0-56B0-2670-7721B7C09600}" ) var supportImageTypes = map[string]string{".gif": ".gif", ".jpg": ".jpeg", ".jpeg": ".jpeg", ".png": ".png", ".tif": ".tiff", ".tiff": ".tiff"} diff --git a/xmlWorksheet.go b/xmlWorksheet.go index b94c521187..7168df6cc7 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -238,6 +238,7 @@ type xlsxPageSetUpPr struct { // xlsxTabColor directly maps the tabColor element in the namespace currently I // have not checked it for completeness - it does as much as I need. type xlsxTabColor struct { + RGB string `xml:"rgb,attr,omitempty"` Theme int `xml:"theme,attr,omitempty"` Tint float64 `xml:"tint,attr,omitempty"` } @@ -336,7 +337,7 @@ type xlsxCustomSheetView struct { PageSetup *xlsxPageSetUp `xml:"pageSetup"` HeaderFooter *xlsxHeaderFooter `xml:"headerFooter"` AutoFilter *xlsxAutoFilter `xml:"autoFilter"` - ExtLst *xlsxExt `xml:"extLst"` + ExtLst *xlsxExtLst `xml:"extLst"` GUID string `xml:"guid,attr"` Scale int `xml:"scale,attr,omitempty"` ColorID int `xml:"colorId,attr,omitempty"` @@ -632,6 +633,111 @@ type xlsxLegacyDrawing struct { RID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr,omitempty"` } +// xlsxWorksheetExt directly maps the ext element in the worksheet. +type xlsxWorksheetExt struct { + XMLName xml.Name `xml:"ext"` + XMLNSX14 string `xml:"xmlns:x14,attr,omitempty"` + XMLNSX15 string `xml:"xmlns:x15,attr,omitempty"` + X14 string `xml:"x14,attr,omitempty"` + X15 string `xml:"x15,attr,omitempty"` + URI string `xml:"uri,attr"` + Content string `xml:",innerxml"` +} + +// decodeWorksheetExt directly maps the ext element. +type decodeWorksheetExt struct { + XMLName xml.Name `xml:"extLst"` + Ext []*xlsxWorksheetExt `xml:"ext"` +} + +// decodeX14SparklineGroups directly maps the sparklineGroups element. +type decodeX14SparklineGroups struct { + XMLName xml.Name `xml:"sparklineGroups"` + XMLNSXM string `xml:"xmlns:xm,attr"` + Content string `xml:",innerxml"` +} + +// xlsxX14SparklineGroups directly maps the sparklineGroups element. +type xlsxX14SparklineGroups struct { + XMLName xml.Name `xml:"x14:sparklineGroups"` + XMLNSXM string `xml:"xmlns:xm,attr"` + SparklineGroups []*xlsxX14SparklineGroup `xml:"x14:sparklineGroup"` + Content string `xml:",innerxml"` +} + +// xlsxX14SparklineGroup directly maps the sparklineGroup element. +type xlsxX14SparklineGroup struct { + XMLName xml.Name `xml:"x14:sparklineGroup"` + ManualMax int `xml:"manualMax,attr,omitempty"` + ManualMin int `xml:"manualMin,attr,omitempty"` + LineWeight float64 `xml:"lineWeight,attr,omitempty"` + Type string `xml:"type,attr,omitempty"` + DateAxis bool `xml:"dateAxis,attr,omitempty"` + DisplayEmptyCellsAs string `xml:"displayEmptyCellsAs,attr,omitempty"` + Markers bool `xml:"markers,attr,omitempty"` + High bool `xml:"high,attr,omitempty"` + Low bool `xml:"low,attr,omitempty"` + First bool `xml:"first,attr,omitempty"` + Last bool `xml:"last,attr,omitempty"` + Negative bool `xml:"negative,attr,omitempty"` + DisplayXAxis bool `xml:"displayXAxis,attr,omitempty"` + DisplayHidden bool `xml:"displayHidden,attr,omitempty"` + 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"` + 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"` + Sparklines xlsxX14Sparklines `xml:"x14:sparklines"` +} + +// xlsxX14Sparklines directly maps the sparklines element. +type xlsxX14Sparklines struct { + Sparkline []*xlsxX14Sparkline `xml:"x14:sparkline"` +} + +// xlsxX14Sparkline directly maps the sparkline element. +type xlsxX14Sparkline struct { + F string `xml:"xm:f"` + 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 { Freeze bool `json:"freeze"` From ac91ca0ded4111ed9f22578d4a0570a9084c97b0 Mon Sep 17 00:00:00 2001 From: Harris Date: Sun, 4 Aug 2019 17:23:42 -0500 Subject: [PATCH 130/957] Only parse xml once when reading We were parsing the whole sheet twice since the sheet reader already reads in all the rows. getTotalRowsCols function is unused after these changes so it has been deleted as well. Closes #439 --- rows.go | 142 ++++++++------------------------------------------- rows_test.go | 18 +++++-- 2 files changed, 36 insertions(+), 124 deletions(-) diff --git a/rows.go b/rows.go index 3079d5a4e8..cb0e31fde2 100644 --- a/rows.go +++ b/rows.go @@ -10,10 +10,8 @@ package excelize import ( - "bytes" "encoding/xml" "fmt" - "io" "math" "strconv" ) @@ -30,95 +28,35 @@ import ( // } // func (f *File) GetRows(sheet string) ([][]string, error) { - name, ok := f.sheetMap[trimSheetName(sheet)] - if !ok { - return nil, nil - } - - xlsx, err := f.workSheetReader(sheet) + rows, err := f.Rows(sheet) if err != nil { return nil, err } - if xlsx != nil { - output, _ := xml.Marshal(f.Sheet[name]) - f.saveFileList(name, replaceWorkSheetsRelationshipsNameSpaceBytes(output)) - } - - xml.NewDecoder(bytes.NewReader(f.readXML(name))) - d := f.sharedStringsReader() - var ( - inElement string - rowData xlsxRow - ) - - rowCount, colCount, err := f.getTotalRowsCols(name) - if err != nil { - return nil, nil - } - rows := make([][]string, rowCount) - for i := range rows { - rows[i] = make([]string, colCount) - } - - var row int - decoder := xml.NewDecoder(bytes.NewReader(f.readXML(name))) - for { - token, _ := decoder.Token() - if token == nil { + results := make([][]string, 0, 64) + for rows.Next() { + if rows.Error() != nil { break } - switch startElement := token.(type) { - case xml.StartElement: - inElement = startElement.Name.Local - if inElement == "row" { - rowData = xlsxRow{} - _ = decoder.DecodeElement(&rowData, &startElement) - cr := rowData.R - 1 - for _, colCell := range rowData.C { - col, _, err := CellNameToCoordinates(colCell.R) - if err != nil { - return nil, err - } - val, _ := colCell.getValueFrom(f, d) - rows[cr][col-1] = val - if val != "" { - row = rowData.R - } - } - } - default: + row, err := rows.Columns() + if err != nil { + break } + results = append(results, row) } - return rows[:row], nil + return results, nil } // Rows defines an iterator to a sheet type Rows struct { - decoder *xml.Decoder - token xml.Token - err error - f *File + err error + f *File + rows []xlsxRow + curRow int } // Next will return true if find the next row element. func (rows *Rows) Next() bool { - for { - rows.token, rows.err = rows.decoder.Token() - if rows.err == io.EOF { - rows.err = nil - } - if rows.token == nil { - return false - } - - switch startElement := rows.token.(type) { - case xml.StartElement: - inElement := startElement.Name.Local - if inElement == "row" { - return true - } - } - } + return rows.curRow < len(rows.rows) } // Error will return the error when the find next row element @@ -128,15 +66,12 @@ func (rows *Rows) Error() error { // Columns return the current row's column values func (rows *Rows) Columns() ([]string, error) { - if rows.token == nil { - return []string{}, nil - } - startElement := rows.token.(xml.StartElement) - r := xlsxRow{} - _ = rows.decoder.DecodeElement(&r, &startElement) + curRow := rows.rows[rows.curRow] + rows.curRow++ + + columns := make([]string, len(curRow.C)) d := rows.f.sharedStringsReader() - columns := make([]string, len(r.C)) - for _, colCell := range r.C { + for _, colCell := range curRow.C { col, _, err := CellNameToCoordinates(colCell.R) if err != nil { return columns, err @@ -181,46 +116,11 @@ func (f *File) Rows(sheet string) (*Rows, error) { f.saveFileList(name, replaceWorkSheetsRelationshipsNameSpaceBytes(output)) } return &Rows{ - f: f, - decoder: xml.NewDecoder(bytes.NewReader(f.readXML(name))), + f: f, + rows: xlsx.SheetData.Row, }, nil } -// getTotalRowsCols provides a function to get total columns and rows in a -// worksheet. -func (f *File) getTotalRowsCols(name string) (int, int, error) { - decoder := xml.NewDecoder(bytes.NewReader(f.readXML(name))) - var inElement string - var r xlsxRow - var tr, tc int - for { - token, _ := decoder.Token() - if token == nil { - break - } - switch startElement := token.(type) { - case xml.StartElement: - inElement = startElement.Name.Local - if inElement == "row" { - r = xlsxRow{} - _ = decoder.DecodeElement(&r, &startElement) - tr = r.R - for _, colCell := range r.C { - col, _, err := CellNameToCoordinates(colCell.R) - if err != nil { - return tr, tc, err - } - if col > tc { - tc = col - } - } - } - default: - } - } - return tr, tc, nil -} - // SetRowHeight provides a function to set the height of a single row. For // example, set the height of the first row in Sheet1: // diff --git a/rows_test.go b/rows_test.go index f7d49b4695..d52c635590 100644 --- a/rows_test.go +++ b/rows_test.go @@ -39,9 +39,6 @@ func TestRows(t *testing.T) { if !assert.Equal(t, collectedRows, returnedRows) { t.FailNow() } - - r := Rows{} - r.Columns() } func TestRowsError(t *testing.T) { @@ -672,6 +669,21 @@ func TestDuplicateRowInvalidRownum(t *testing.T) { } } +func BenchmarkRows(b *testing.B) { + for i := 0; i < b.N; i++ { + f, _ := OpenFile(filepath.Join("test", "Book1.xlsx")) + rows, _ := f.Rows("Sheet2") + for rows.Next() { + row, _ := rows.Columns() + for i := range row { + if i >= 0 { + continue + } + } + } + } +} + func trimSliceSpace(s []string) []string { for { if len(s) > 0 && s[len(s)-1] == "" { From 58a79b41720974009aacf57209a7d5afdd75a7fd Mon Sep 17 00:00:00 2001 From: zhaov <779137069@qq.com> Date: Tue, 6 Aug 2019 09:50:45 +0800 Subject: [PATCH 131/957] Update comments --- sheetview.go | 67 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 39 insertions(+), 28 deletions(-) diff --git a/sheetview.go b/sheetview.go index 91260fe6fa..9712d8534b 100644 --- a/sheetview.go +++ b/sheetview.go @@ -11,48 +11,51 @@ package excelize import "fmt" -// SheetViewOption is an option of a view of a worksheet. See SetSheetViewOptions(). +// 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(). +// SheetViewOptionPtr is a writable SheetViewOption. See +// GetSheetViewOptions(). type SheetViewOptionPtr interface { SheetViewOption getSheetViewOption(view *xlsxSheetView) } type ( - // DefaultGridColor specified a flag indicating that the consuming - // application should use the default grid lines color (system dependent). - // Overrides any color specified in colorId. + // 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 - // RightToLeft specified 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 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 - // ShowFormulas specified a flag indicating whether this sheet should display - // formulas. + // ShowFormulas is a SheetViewOption. It specifies a flag indicating whether + // this sheet should display formulas. ShowFormulas bool - // ShowGridLines specified a flag indicating whether this sheet should - // display gridlines. + // ShowGridLines is a SheetViewOption. It specifies a flag indicating whether + // this sheet should display gridlines. ShowGridLines bool - // ShowRowColHeaders specified a flag indicating whether the sheet should - // display row and column headings. + // ShowRowColHeaders is a SheetViewOption. It specifies a flag indicating + // whether the sheet should display row and column headings. ShowRowColHeaders bool - // ZoomScale specified 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 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 - // TopLeftCell specified 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 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 /* TODO - // ShowWhiteSpace specified flag indicating whether page layout view shall - // display margins. False means do not display left, right, top (header), and - // bottom (footer) margins (even when there is data in the header or footer). + // ShowWhiteSpace is a SheetViewOption. It specifies a flag indicating + // whether page layout view shall display margins. False means do not display + // left, right, top (header), and bottom (footer) margins (even when there is + // data in the header or footer). ShowWhiteSpace bool // ShowZeros is a SheetViewOption. ShowZeros bool @@ -140,10 +143,11 @@ func (f *File) getSheetView(sheetName string, viewIndex int) (*xlsxSheetView, er return &(xlsx.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). +// 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) // RightToLeft(bool) // ShowFormulas(bool) @@ -151,8 +155,11 @@ func (f *File) getSheetView(sheetName string, viewIndex int) (*xlsxSheetView, er // ShowRowColHeaders(bool) // ZoomScale(float64) // TopLeftCell(string) +// // Example: +// // err = f.SetSheetViewOptions("Sheet1", -1, ShowGridLines(false)) +// func (f *File) SetSheetViewOptions(name string, viewIndex int, opts ...SheetViewOption) error { view, err := f.getSheetView(name, viewIndex) if err != nil { @@ -165,10 +172,11 @@ func (f *File) SetSheetViewOptions(name string, viewIndex int, opts ...SheetView 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). +// 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) // RightToLeft(bool) // ShowFormulas(bool) @@ -176,9 +184,12 @@ func (f *File) SetSheetViewOptions(name string, viewIndex int, opts ...SheetView // ShowRowColHeaders(bool) // ZoomScale(float64) // TopLeftCell(string) +// // Example: +// // 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) if err != nil { From 497ad8f4be669525a85e49b44dacacc16cf450fd Mon Sep 17 00:00:00 2001 From: WuXu1995 <49405879+WuXu1995@users.noreply.github.com> Date: Tue, 6 Aug 2019 10:00:10 +0800 Subject: [PATCH 132/957] Bugfix #454 --- xmlWorksheet.go | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 7168df6cc7..9727866311 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -454,22 +454,22 @@ type xlsxSheetProtection struct { HashValue string `xml:"hashValue,attr,omitempty"` SaltValue string `xml:"saltValue,attr,omitempty"` SpinCount int `xml:"spinCount,attr,omitempty"` - Sheet bool `xml:"sheet,attr,omitempty"` - Objects bool `xml:"objects,attr,omitempty"` - Scenarios bool `xml:"scenarios,attr,omitempty"` - FormatCells bool `xml:"formatCells,attr,omitempty"` - FormatColumns bool `xml:"formatColumns,attr,omitempty"` - FormatRows bool `xml:"formatRows,attr,omitempty"` - InsertColumns bool `xml:"insertColumns,attr,omitempty"` - InsertRows bool `xml:"insertRows,attr,omitempty"` - InsertHyperlinks bool `xml:"insertHyperlinks,attr,omitempty"` - DeleteColumns bool `xml:"deleteColumns,attr,omitempty"` - DeleteRows bool `xml:"deleteRows,attr,omitempty"` - SelectLockedCells bool `xml:"selectLockedCells,attr,omitempty"` - Sort bool `xml:"sort,attr,omitempty"` - AutoFilter bool `xml:"autoFilter,attr,omitempty"` - PivotTables bool `xml:"pivotTables,attr,omitempty"` - SelectUnlockedCells bool `xml:"selectUnlockedCells,attr,omitempty"` + Sheet bool `xml:"sheet,attr"` + Objects bool `xml:"objects,attr"` + Scenarios bool `xml:"scenarios,attr"` + FormatCells bool `xml:"formatCells,attr"` + FormatColumns bool `xml:"formatColumns,attr"` + FormatRows bool `xml:"formatRows,attr"` + InsertColumns bool `xml:"insertColumns,attr"` + InsertRows bool `xml:"insertRows,attr"` + InsertHyperlinks bool `xml:"insertHyperlinks,attr"` + DeleteColumns bool `xml:"deleteColumns,attr"` + DeleteRows bool `xml:"deleteRows,attr"` + SelectLockedCells bool `xml:"selectLockedCells,attr"` + Sort bool `xml:"sort,attr"` + AutoFilter bool `xml:"autoFilter,attr"` + PivotTables bool `xml:"pivotTables,attr"` + SelectUnlockedCells bool `xml:"selectUnlockedCells,attr"` } // xlsxPhoneticPr (Phonetic Properties) represents a collection of phonetic From e07581e980444b64bc15fce328ff07736ac9dbf6 Mon Sep 17 00:00:00 2001 From: Harris Date: Tue, 6 Aug 2019 16:43:56 -0500 Subject: [PATCH 133/957] Further improve read performance Instead of re-encoding the full sheet to change the namespaces in the encoded bytes, read the sheet again and do the byte replacements there. In this case, file access ends up being more performant than marshaling the sheet back to XML. In the SharedStrings test, ensure the strings are actually read. Fix #439 --- excelize_test.go | 6 +++++- rows.go | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/excelize_test.go b/excelize_test.go index 79010b1180..4169983299 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -1003,7 +1003,11 @@ func TestSharedStrings(t *testing.T) { if !assert.NoError(t, err) { t.FailNow() } - f.GetRows("Sheet1") + rows, err := f.GetRows("Sheet1") + if !assert.NoError(t, err) { + t.FailNow() + } + assert.Equal(t, "A", rows[0][0]) } func TestSetSheetRow(t *testing.T) { diff --git a/rows.go b/rows.go index cb0e31fde2..220c233800 100644 --- a/rows.go +++ b/rows.go @@ -112,8 +112,8 @@ func (f *File) Rows(sheet string) (*Rows, error) { return nil, ErrSheetNotExist{sheet} } if xlsx != nil { - output, _ := xml.Marshal(f.Sheet[name]) - f.saveFileList(name, replaceWorkSheetsRelationshipsNameSpaceBytes(output)) + data := f.readXML(name) + f.saveFileList(name, replaceWorkSheetsRelationshipsNameSpaceBytes(namespaceStrictToTransitional(data))) } return &Rows{ f: f, From faaaa52cb862499454a7f893b92e8430d00172a5 Mon Sep 17 00:00:00 2001 From: Harris Date: Wed, 7 Aug 2019 08:53:37 -0500 Subject: [PATCH 134/957] Get sheet names based on index SheetID only seems to indicate the file name for the sheet. Check the sheets list based on index instead. Reordering sheets in Excel changes the order they appear in that list. Fixes #457 --- sheet.go | 15 ++++++--------- sheet_test.go | 21 +++++++++++++++++++++ 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/sheet.go b/sheet.go index e02782a7af..935deac230 100644 --- a/sheet.go +++ b/sheet.go @@ -317,14 +317,11 @@ func (f *File) SetSheetName(oldName, newName string) { // string. func (f *File) GetSheetName(index int) string { wb := f.workbookReader() - if wb != nil { - for _, sheet := range wb.Sheets.Sheet { - if sheet.SheetID == index { - return sheet.Name - } - } + realIdx := index - 1 // sheets are 1 based index, but we're checking against an array + if wb == nil || realIdx < 0 || realIdx >= len(wb.Sheets.Sheet) { + return "" } - return "" + return wb.Sheets.Sheet[realIdx].Name } // GetSheetIndex provides a function to get worksheet index of XLSX by given @@ -357,8 +354,8 @@ func (f *File) GetSheetMap() map[int]string { wb := f.workbookReader() sheetMap := map[int]string{} if wb != nil { - for _, sheet := range wb.Sheets.Sheet { - sheetMap[sheet.SheetID] = sheet.Name + for i, sheet := range wb.Sheets.Sheet { + sheetMap[i+1] = sheet.Name } } return sheetMap diff --git a/sheet_test.go b/sheet_test.go index ef795ada56..3baa084a32 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -225,3 +225,24 @@ func TestUngroupSheets(t *testing.T) { } assert.NoError(t, f.UngroupSheets()) } + +func TestGetSheetName(t *testing.T) { + f, _ := excelize.OpenFile(filepath.Join("test", "Book1.xlsx")) + assert.Equal(t, "Sheet1", f.GetSheetName(1)) + assert.Equal(t, "Sheet2", f.GetSheetName(2)) + assert.Equal(t, "", f.GetSheetName(0)) + assert.Equal(t, "", f.GetSheetName(3)) +} + +func TestGetSheetMap(t *testing.T) { + expectedMap := map[int]string{ + 1: "Sheet1", + 2: "Sheet2", + } + f, _ := excelize.OpenFile(filepath.Join("test", "Book1.xlsx")) + sheetMap := f.GetSheetMap() + for idx, name := range sheetMap { + assert.Equal(t, expectedMap[idx], name) + } + assert.Equal(t, len(sheetMap), 2) +} From 51079288923076d00a8b36ecec07980a158d742c Mon Sep 17 00:00:00 2001 From: zaddok Date: Fri, 9 Aug 2019 09:47:06 +1000 Subject: [PATCH 135/957] Fix potential memory leak Fix potential memory leak where zw is not Close() when an error occurs. --- file.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/file.go b/file.go index a9e7eecf95..a4aa11d0c9 100644 --- a/file.go +++ b/file.go @@ -109,10 +109,12 @@ func (f *File) WriteToBuffer() (*bytes.Buffer, error) { for path, content := range f.XLSX { fi, err := zw.Create(path) if err != nil { + zw.Close() return buf, err } _, err = fi.Write(content) if err != nil { + zw.Close() return buf, err } } From acd76425c2ee55c45a51cf7f71c8a6187a09f507 Mon Sep 17 00:00:00 2001 From: Harris Date: Wed, 7 Aug 2019 16:26:13 -0500 Subject: [PATCH 136/957] Handle multi row inline strings The inline string struct is actually the same as the shared strings struct, reuse it. Note that Go version 1.10 is required. Fixes #462 --- README.md | 2 +- excelize_test.go | 5 +++++ rows.go | 11 ++--------- test/SharedStrings.xlsx | Bin 6462 -> 9419 bytes xmlSharedStrings.go | 16 +++++++++++++++- xmlWorksheet.go | 14 ++++---------- 6 files changed, 27 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 91155e3545..4bb7d6682c 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ ## Introduction Excelize is a library written in pure Go providing a set of functions that allow you to write to and read from XLSX files. Supports reading and writing XLSX file generated by Microsoft Excel™ 2007 and later. -Supports saving a file without losing original charts of XLSX. This library needs Go version 1.8 or later. The full API docs can be seen using go's built-in documentation tool, or online at [godoc.org](https://godoc.org/github.com/360EntSecGroup-Skylar/excelize) and [docs reference](https://xuri.me/excelize/). +Supports saving a file without losing original charts of XLSX. This library needs Go version 1.10 or later. The full API docs can be seen using go's built-in documentation tool, or online at [godoc.org](https://godoc.org/github.com/360EntSecGroup-Skylar/excelize) and [docs reference](https://xuri.me/excelize/). ## Basic Usage diff --git a/excelize_test.go b/excelize_test.go index 4169983299..d61dd7bf40 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -1008,6 +1008,11 @@ func TestSharedStrings(t *testing.T) { t.FailNow() } assert.Equal(t, "A", rows[0][0]) + rows, err = f.GetRows("Sheet2") + if !assert.NoError(t, err) { + t.FailNow() + } + assert.Equal(t, "Test Weight (Kgs)", rows[0][0]) } func TestSetSheetRow(t *testing.T) { diff --git a/rows.go b/rows.go index 220c233800..c17179faac 100644 --- a/rows.go +++ b/rows.go @@ -206,18 +206,11 @@ func (xlsx *xlsxC) getValueFrom(f *File, d *xlsxSST) (string, error) { case "s": xlsxSI := 0 xlsxSI, _ = strconv.Atoi(xlsx.V) - if len(d.SI[xlsxSI].R) > 0 { - value := "" - for _, v := range d.SI[xlsxSI].R { - value += v.T - } - return value, nil - } - return f.formattedValue(xlsx.S, d.SI[xlsxSI].T), nil + return f.formattedValue(xlsx.S, d.SI[xlsxSI].String()), nil case "str": return f.formattedValue(xlsx.S, xlsx.V), nil case "inlineStr": - return f.formattedValue(xlsx.S, xlsx.IS.T), nil + return f.formattedValue(xlsx.S, xlsx.IS.String()), nil default: return f.formattedValue(xlsx.S, xlsx.V), nil } diff --git a/test/SharedStrings.xlsx b/test/SharedStrings.xlsx index 7b722d9ee4733d24d5b9956ec0b517570c501560..d6004c0cd2b60c0324378fde7017b55d21a79dbb 100644 GIT binary patch literal 9419 zcmeHN1y@_?)(-A2#ih7w(PBk{ySo>6NpOn0L-A6qXtClH*Fv%4?ox`oe(B7;-^_I8 z{(^hYS~>5^O7_kZy zrrN5MdnTAb$xxhffi7DOg}X6+G3C^Z6hQb|kXcdz_pq?1G*rrD4Tf(y{brVaeTFYh zOatWw0{`xXgT)qUV1AY^8V8YZmpluLg_7-KD@}y4iaEwY$8;GkBdjf>q&%aR;*-mu zCpz5gB7$;-4&pw{n#sJSn+4;vtD(XODRCZ^1Vi<10vG$O;*#>ceY0Q~2bu z$>8KMmzT{&Fc>lp8-3{iyI;hH7kDWD$^Vnf#+Z&bGwc+iOhDK2Ok#2 ztO3>M+}PKgqriTecNhM;b=)UkvcWyPNZn-v$AfR8$UiJY^3>l+B3n{qatnDPKo|gk z2!IF$vSa(5C!UTjwx*7bwm);&zc~X1Nn{W&|J_Gx;)qQTJ8JMz$mgK(Hs{1g@Wp8vl0>TT}JXDPgOzMSUxrP8ON~Y9sl_{DV6&&1APq%Z1a5P*H6fX zS4vt}6ycw^ny=9DqNS@USlM_+XKfXg%v(gCm++|?r*{V>tLu?)TL{0COY7?wb0sok z3aZ3tmekq4$&PM+@h-zUGFt`HbDOBg4|VXy(SC*7ASUj!AMw};A7% zPvf3o?6q!2ja;dw!;J0YDs*y7pplBlpw^6uv>c9{izebPU|@a5K6wiTC8|QhS6d&A zR05)P(ahde#l=jwP2xfkucgqihLtOmN6cixzV?%Fp+#wx3?I>BKlfp6BCs{wd0(Z6 zNrhW0+CF5i^T~?eA4UJ`u(*Mj{tk`ka6zKSWPoY%8K{}Q&V&oDW~MG`8r~1<~6DzrtlOrt*9-2f#g-C_JI$`)V%=w>-}En5Q6~pgOrS)LWC*i>}Nn4v)0s!;=X& z^0ZAWw)p9C#PpIY%JH!qYf9si?z8He`#~2ihL8l)l!ruFrU}?9;}2Tv>{zDe`={?y zk>7udyN9CfZp&svlkLr#8Zrz@&;w}`1dCxdr5H!MT7)pX#iKINK3-Ft0Se6+$8n;e z1KWF!bcvu4Q10P$^DJIk*6t_Hp-xHByj@hM$k|L^A#@WUJ=K^m&%vtRel;laMjKNh zrzx3~JmTq?VMFmH)P-K?!qrSV;61HF8^{{%&DGwu;J1xPmOO#gKsQfNG3!J2-J8s} z%OaO_&z1%yXU}d`{8uGScD;j^tu|vG$b`s)AI=_wg#!t2di87PEnB(-cVWY_{e%6g z+==u2^uL^oZj7%8<$O1KVSg})>SA5@nIuQC}RF1gH(ej~K4edQG@9cJL)m$k0HpPPW~chI86uP$gI_?&ZeJVtGgH zp2~PuD3j*`v>VrqImseT z!5fE)^%5NlEO`Ay^`$4xnppboQsG=|=MS?RZ;{Pk*nz=O*s)W^uHRD=8?+~>EYywQ z4F=Y>F2$(|x`6P?cg8Vt#Tce|0&N-5sKqO@Yjx^1NuNH$wB?lB>Lxm_(HgF=xlF@m z7!@;wzZCK5Yt#Dpt?m*R{GQf_UdfihZ72DZ*)3EP@U7AKPG(^AJ-Zg>+KEPvhfx;Y z@G7;aVNPnI%$5}*Dae5j>n+aRIvJOj`nQ*59K{Ewaou8v!5Rqs#u^pV+jav1DyeGW zVf^Q)JW)>d*>5v*t#_?fqa_f`Qj2Y32^ss^cL>n0srmg9zDt{2yNQNy);rY;4X6#s zDkRgdP7}_A$)OnHDT_|yI9=p^Bo1K{BUoY`laXS=S=#ms-x}b!T#*|5`hnl?s7p_j zCy;0rjocWVT>9pcokjx@|3D>&Q`L5?%*vJ1h(*s#(guwtxfriE2KR0HtP-}Ij1|+E z7-xaf7*r8#mM9&^J;q{8!gyZ{HcSrG*BEGkPnU0gFwG5Te*$x3C3UnAQKJ8p)>HcJ zcUxcmwZ)Zdr;CKYM)G#9H`f=z1Vw(k9zI|5O3wp7FqQ14)6Ho4kI|JN;1OobkSnmjS64$N zi+Cst9J_ywwA}sPeQ00fPyfnuVEu;A8&4Ufp) zKi4&mGvrtBWkWpB=UL6KD9U1IEYeoM(#?4lZ+yo_-O7Nye4okigz!&HZF|?=CI|V1 z^!d+b{I`VUW@TY-!S>sk<0oGabmbh0c`-jf+x4fo%+ao)>F0cokjmUEbN{BIU?4$P zrKPUv3!Kc{c!hK<95Sv#lp*r@wD>rWDD-*aBT)mxJnn$eZoIyXTvTL5pQMpT0Em1A zb!TKQ&vDr+ncYo3xZq_4!q)dfKmzxsP=?gSs$a3MJ&ma9T3r~Sl8g`mHsPYK_fuOY zNTZjU5^-0pDe}8mxq5wb6@@q`ddod00eAVLNxJHd9u~_vtT5DPBt32Bl+Y`EYPlr+sOGA10^sI~|;`E@N+?MNGW-oH;b9nI?vDHkrs?JdjZo zRSu4AUu^Im7R8qL*|Cjc&o|V0XoH3OO*ekABdhUphWCe{ahDd=mTHb|M}WM-hHiys ztp?c{7l0)6T}eveK<$Jf1FzIe6bf(tkrtiOR7Td%9sbs`+q(JVAEsS&nAKov(oz`i zWv!cPKJTug4Xl@`UCWxa&FOm_!<(OcQMxNBoP)9*;)k-FszrgvB`V4_D?4pn1a6SJ=V z#>&$9bZgYBuW^0~N?C=@_Qf440qSB?i;Hn-U>rivyaRyGP8WFDa`PdtY+*m=+v~7N zIweJNe@8HXa3)_&WbnuK$IUA?5s7x8r~Qk`(R+|i&(r-9P~_pdgdFka#rV_I&Zr;w zaGJC5W-^15vmF3_jPUzv-mpfzv={`e1tmO`-F^th&nOQ-bjG!d`Ccj zH|??pw1lsDJh>!Rg2xcifCVxEySy-KCtLPi zOFkqu!nB_mYgw_2t189Ot|WnGnEZKdl8D`OjX=a^|9&d5px3b@bgx!o@=EQnjf|Ii z9m@CblBIWJ+_<>1i}ch5D7{6W3ybIcSWEBX>al*)%luVv)t%1G#PKY3!T_HPFHjKI zOE~=5M->m6V|rLmTl|?e0>PHl&uNb!QsjRBXM-FAU%J6(*j59J+{&|TcA4mwU!ju% z_KYLzeYWtWNv@Qn&p+D?z#BGy&aA`X73JOJ=0{RknLc4hMt?bZqW)2UY@sIm2gp(* zb%e|JEAwJ|Rm)sQkXu|wD5pVlWkzP%HtqE&FK?~OO=)41jdi6i(F$+?(uw{|K-O43Y$) ziNVq3Z#Pi%-!_c!f9WoGAx}Rg925Vpio8S{vj)LpP-2CDc<5%?LZ>Dx;RieI`p|IX z5>0=wyBnJRw6}MFDWbcY!YQY*)9a1Vwo$GR#CxQZx2dSAK$b|7b>5l z=!uQAT&3sO=%X*!RF5`I%}2f{JTblK(zAz0z;$9Rp`d-m_=hpx>0Qm!UWtzYCkKt;$3uT-knGeoMev$#QX- z?3n#WDYvA|D?xz=I8bFa`5ITsC8f7~f_(va2i0~vg{3QPW;OA$dz8AtF}Wh6D_r6? zZeuWWGCO#0BQ0fQBek@~di zFgo$0!Q*lxzR;RPS=OD_WL4hMdx2(PTxwS%b5JOr*q3p|o^#f;ZthXKP@Ot%dA<*fayHzk|PPm{D9_%nb^zLUt; z1sr9+#V@Z3{7Pug3C;=5V(e}_5O+GmHcirtzc9CF)3tX#)~XwjJE6qJ z=CEaK$a)3lg%{J_*sVt*7^b0L!nS3YAnTC1QZrROti-r8tgEWaU~Z{ARVvEWi<3g5 zB+^RK)&1Zg4Z{9V1c&o}zwyF~Llf;~5ctcURl-*GeV0LTG+mG#Q0<7pnU2N!%?|(% zawE{d77^0Xvf)sBP<*g>Ddo049f7Qw4NgyijS(do*ns7Kq zz@$mNH*LRK|1k3Upm)#$8R^9$TPg*~bP3X|HK@$PGmavhg{NL=mpeMv*sSs+E>_e; zR6!>fhAl(MC9AG(L8nQRwOVYdT!x^GxJNzZ5lO#cW$G6MLVHn6cY)`&+JStg*L=-C zlpJv3@@HQ%4N9SX4tM=0&Fos!tr}8-xZU=UjA|Ln%D??mT^$ z>paX$QLpw+bW{85!|e%W_*HHXN%^Vu5JRer1Er>Y&f+X-y#>&P=7OqaJ0ZM zvWH(A<7TN%f>Ie(6dzrumz|~fCOsPbZLxbyA)nf=ZV`>WnX5;Xjby(eJL4dPXTaMj zh<#xRS6WCu{Db<(Me9&i&Vlnn(oa|#abWTrRcROQ>)uBEH1@L_g#s~|{0ayCaC47o zpUlUMiu8D`3HwJ+lj}snStg?nE%kdInftY&NF$Rd-mXNZVUaW$&(g1ZMU12nLp4{L%g!n#M@4wiiAS${vx+oKQC1q`^nM^(7?YVgvMTA=tW#~#TQut)LD_tw#rRFnN-->(o=<>p zCs#di4f~l38$%Y1!uycXt~#5#f-p<`+V?$KQ&NxwS~4jr$GR=o$I3^uTo%gAB#UL7ecR5;L+C0&gq(UA|n zHfoNoH_o<^543uv3@gR0nywLOSO(6@8vr{{eT~N4p;V1Ohy|UXPX@2&Rw9%waPS4fD?sG;wnJ9|9rJ``3|`sANmbf!TW$ z;uj`)rES{~Y+8p*V?CIc2QRn>F3rA(9GG~Y&g>!L``R<2)&V1Ad3$tYub3`9bAu9B z$numy)24}DBXGQMzTdZy%+u(jaLF42Q#4A+@^45oOS4x;!?eZdT;2t*n4fEuS{zR6 zCfS|<>f+s#6_QS&wC7uAN~~METQ&{R33NUVo$78Ra7=3(xxW^F!C&e9Xf!F%6d0G| zQLGqHakJ>&DQMhO=zGjcCyVwBW7TIFWkG_=!HL9=+QCv z1GuHtFqvEyyfQbu+puE)Q}O-1%7ubvfmA5}yy4^D|Jc9Rf7t$^EcaIje;rEy+wkX_ z4f)AGji`S${Oh3Z&xT!)0nC3M+x_b2*RjnXp6(%gT7DVg{A&EG82*Rx2Bge`82=%S z|7!ZHI`@ZZ45X!inEtBO{p#S?7UK^GNYDQN!vEHA{OaY`!t)O={Di;X!LMcLuO5D_ zy#DauMf8u?@mn4CtCzp#<3D@=0Hq`Vz~8d-ujYS^rGGYOA^(&4zap!$92^8AKX*Z( O0Qw*Wz!BxocmD@YLNdYt literal 6462 zcmbtZ2{@E(_aDZVb+Ru-7;Cc2l4vB^_mMqhA7kuG)(DYZ$WDkiW#2_e_K__Klbw)V zyw<)^ubR~Vdav*M&viZS8PA#DIrs0J=bZbTQC9-uPyzq|0`$cUSi0LEr-lUpMB@Me z6zKmLTu0cuSlGMV*7kI;a5m!gfZM&$R#JV<4??jzhO`?DR$JTy*^c05uxURz*Ek&4 z`s|KDx{VFuEjWt+l%u`Ot~2Aw^oq_)5;e$ca9xJMn^|sG-F(88e~6E8UC@u#@FB5+ zXSU&*`h!cEk5zl#wr;|4x@nh7KvWtpW5```E1D%`*IsG`YhRoiFOSw($kq3Md5*Lw zhcvx$z@MH}fKIyPo;iuRQ2Zy2ch6ZxRmsaBJLhM+IZ8)K+eZwan7Qa&G@Ex?Mv}dQ za4Hov;nY8)i>(r-A$fS91+rNJL?tvozDD;PkniSsvsSAiQ1it(iya2bRT;I3HU9c2 zerLH#5#0~NdA)v_N6H^%^c#YUC0)6Z$?rS&8q)ba5@cjMG;cEavx9(^APO4x(y{nM zD@4Q-yTs|dH7|mUgZ;Tu%`@FXYz^@oOmBdWWM{T0?N<2gubVY4FC}Rkl{U+jEF@l;s_A?uLi8D zJB@dICS)m1tA9`|8uJCqEM?c!GPz$T&ByEE1&Ly5XS+D>UGh?t%5*f7Rt|&TR@=bwKB@X=yYN2Ts zbsNm@Lk1nYJqSrSNOZcDjw0nh($JB3j98bAeL5%?b7D~n?UkQ+C?!pocz>uXsIbU7 zeceikKm#Qw*fI#KlyKny+lXOeuyrNu8bv82*M(-Ed)mLPVkDo-8T< z^aKAp8WbX|Tzx~u{9-2J#hP!*`CH)0UAoYoW_Wo95dk>F)0f#)0i?+JHrXh17#yj; zhp?uDw}ljyL0^078;M4^4}mf?rNvWNTEj}~a&e1h8uG=g3AcsB^%o2~4s}#stUJ7J zsug5}bogd6Ho7RK&?c%EB*ZV)`aB>giIM)u8xO$K?Y_y zwmP;&HoUd&>D6n~dvTS59$s}=K%l5`ditvOJjbOxgLhyC2V;7pEJ;>QEM!FVU1-+M z8=gz*Q^0`$5xMI@ZWDdw4fR|#xnp7;uB`z?`^BG?RLGKe8j& zF)WC-mZ~z`uZ(KqiVV9h#3TSW_=>45KC1*LF4Lp!Hj(;k4#Xs!y-m}SUF)GF6OUf$ z_geMHv%T(VhHgDR_vu9TBhB{yR=uY@>%9LuzJSCYAA-`b}Uq9-r1l^ zIeH;|20A_?-HLx#TAWrbKsA(4O6HL0gj{r@IF(p{XmUO5PGyy_Qf{mg^bHjHI<5z3 z0WE7&Ckt~e7bhEgE9YYeh(tBnqEo-@YS2;OuveKqSuUks77k(kmuPSQV$qVu5oq~R z^nuT#=yM^H0*HgXR4ex<&lIXdcA)r?9?=5pSs|$Hon@j_{TYFt=E2uNltYtW-V`Lm zcwl64db@IircchfvISL(c>N`=|L}{yKKC094wfzRT2d9(kagso9@lW~n zp46knMfB}jbR5!Sin$}4U}gve?3jvg>Gnk|@|chNt3eHDJ}zCkl>4Z?=8;Sxd=DU_ zZ@^v&E+1uj<#R*Emza5GBJT0oE1OL#PG3Z~rtm3Tn=a;uk>lK@u$wo)y`8cXLIY|U zF+kx*5z2ve-|e_}k$o1KXLd3I$7o4%Idrc>6$MH0Rc<`^w5dkXpDEro*qM*isp6!u z*yS1U#2buLE~zB!ACU?e4nK?NAXOQl^Cp_}y7rca8@xWuIxCx|Z;tC_G;=OGaf!xx z$T|Iu*1qZra*_VJ2*+!Lyn)jL?uXaHl=mogI>qiAMuKMG>j!$&*1`_&v7=20iEHdaYpWEd%IW7DoRJfg=#Z8^=E;-&08DL7PC0mU++G7TBo7L;$A zu)w?Dd=^qSPh-K3!gp&iV`_`!xOsF|h$Q4F1?3^~bpQQ)z^n<%YyRSz)vR`*dJJ8D zDA7y%5s%oVSX!`CLWE*LYnEV)Q=u4xvO$1=yQ6r+pr0dIMdL?Cp85i(4p{3zk)@rr zfK_tGB)x@8Aa(RqSS;_fNW$l4@2Km@45IoQhkeav`Aj1a%5k|@gX8M=xb0Wk{$^38 z)XVjP%TLd!JZB)gyCV|&YNkN1YC|IiC+lnMRAodpnBrQ+8o62TGPWLdQrp1R})J8 zmlcHG)n?^iEidy%WO!~`*80o``NuhHbF()mKtFuey&s%TTk0@Ys=DQsl1!~Z-^}cA zwm(56w{xWC&UEl{vC6)1CeFh33KIQOuR)v3&l}FWcKd)PP1Dq2m;&IMNFN4a&!|BgDBD?zU`H-$Lza@OO}~E!11xW2iptNqE$r=b$+h_<7}qUAv=24sKk}u#@%7@ z?*;pl&Qxh^`Iz7muAQ|3xK-Q+Fs5URPL7m+dKpJf;eV#u%IRt5lH+YvVIct7-w-Sh zd4iAB{nkBLkD&Zk+(3BwJvyU)DEuQzVC2WI-j#kS@3UlcR63GL^i$+?oFm}DK=sY<2RtibDbwIGFVZXJVp|vSZ}-Jj^x}Tw7dE z52vMMIiH%b#S!Y|k&vGW62*d1t(s#;;iW~^ES%?u1WJK}7aT6tG0Y8{@KMLmXkW*J z5f#991x%^hcJXYe-t!<2p9o)>R~aM0*#VW>YHRgXoB0ngM}moCiw}}PTui|()Y)Nu z(TZ@DDv?(!6{+PfDwyIbRiihi7B9BcNHZ&{aw)obUfC!fW}Vl#Shc9%5}0ohQ5@TB z31XI};n{V<%swsWRF!X@ zFF25mTPB@|AuCHAOl|YUiW*HK9ln@vMdR|-RLfSnk8)>Rgw41{w{j@BBZ#mIa0VP( zqAz26+k`sId+rOEnlzMWkSGJ{NblORGc$9m-+A+;$b1ZYO-WHyi9igoP}(+I?#(aV zJw^Nl+GU#}ckZVJ)N(8O;cuOrJ@nYQ-0M3t$??HlE@P?_Ql?S|<6k*!idRGJrf*!J z(Ehq7kVuihn>(9rvJ~~#TEtEGJ3GgBM*H~@Ya5U$KIJjxyOITn{V7_x3?6R903t^% z=gLFsK;U3a4L|Sf4y-Gm2M5XY+~rl@hYG45x1mC*SI7^AP48C{jo^t@`Du0SE%h5r zYGw|27OSlf`|rcz8`zB+3oNa+6CvGgJ@W3}r;g#B;p}75=;BU=KK_t>FYcHJp9qbv zpI!cM;k`Ce*U7ijQ=NGW{r`uP|*lzTclk`j$c$7C)JqPa?f2i;j)i*`}(m6 zVIAum%t>#qsZ%8rxjtE|@vEuBH#@JDs_-JZ8TXRi=4gsa9&%S{NoXCrQ!_smHl$(x zkPv2RRSTKDtq8ZBlN8Qtzl=v$MfIe>;!*3m;Ikxke(vW;KVcWWFzgJMzM=dvPBV4E z`Q=eXVd@PPE-+)O?0pd=LqYwjF?LLMtrhP%Cqg1J*APFvr*K9CW?=gi@sk7k4HPJn z4VF9V1vB=ky;UneU}1tc1TnE{%RLiX)7e9&%x#L0P&VC>$^U{%nOaJfGo6;)yyJD_ zWAAzClY`Xn#g+6Y*Ur`!7A~iI*WL}A2o1Df#L<4C{Z01USH6|m2n{6|x`%Bb+hz)1 zg~dkLXM?#ca_vHzhL=_(6wQ)#TXj;tjH{_KQM!F-k|S#f$?-lKa5ux3qAd^VX8()Q zL`0|ca?rt^Axcv5to#Q>SL>{3F9IiHAiK~p9)$q=tXrl5q&)7n zTlFleK5;nn5GZT@YV4?Rz1m%i=aT&02YzD3g%QDQi*T|8QZS>$jnC5Mt+;w~@sq~q z2ozposec?c!DK)I1(~@pzW^AO8=uY z9rB5Ye%Ks~%j(~p)ET{uQM46lwiD3Z*5L~h`AO1glJb)uCb`&M?g+=5MaT|klqRU* zNLjka#z#kXR6p44>$;0!SzSu=KR3@*4to$ z|1U$F`@sN<6vs|cjNywe!$e=je}wQ$uQ!- zQ}L%J{6xjEQ$L`O&gdgECaApleBW_|oUA7doo43O#{C5J0vdGhJLqq1Jf@a^XXp2L z{MEH6^qlfXCyfbxV({tp^ag(Iw@)_UkKO-HbKF!X_=hH0x+F)T?E{+NPv)#f}^6&P4r{~w&kEsNRj#4x|m`?cb z6#O1Wq`zbc1bXE6kAi>7A13|@E2r1fEd5#}F*lI%8~)FdiK*%TrV96XAdem!g8+Ew LcPz(0j{yDyMS%Gc diff --git a/xmlSharedStrings.go b/xmlSharedStrings.go index 3fcf3d5bb7..48d4464ff9 100644 --- a/xmlSharedStrings.go +++ b/xmlSharedStrings.go @@ -9,7 +9,10 @@ package excelize -import "encoding/xml" +import ( + "encoding/xml" + "strings" +) // xlsxSST directly maps the sst element from the namespace // http://schemas.openxmlformats.org/spreadsheetml/2006/main. String values may @@ -33,6 +36,17 @@ type xlsxSI struct { R []xlsxR `xml:"r"` } +func (x xlsxSI) String() string { + if len(x.R) > 0 { + var rows strings.Builder + for _, s := range x.R { + rows.WriteString(s.T) + } + return rows.String() + } + return x.T +} + // xlsxR directly maps the r element from the namespace // http://schemas.openxmlformats.org/spreadsheetml/2006/main - currently I have // not checked this for completeness - it does as much as I need. diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 9727866311..a5db776bbb 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -9,7 +9,9 @@ package excelize -import "encoding/xml" +import ( + "encoding/xml" +) // xlsxWorksheet directly maps the worksheet element in the namespace // http://schemas.openxmlformats.org/spreadsheetml/2006/main - currently I have @@ -424,18 +426,10 @@ type xlsxC struct { T string `xml:"t,attr,omitempty"` // Type. F *xlsxF `xml:"f,omitempty"` // Formula V string `xml:"v,omitempty"` // Value - IS *xlsxIS `xml:"is"` + IS *xlsxSI `xml:"is"` XMLSpace xml.Attr `xml:"space,attr,omitempty"` } -// xlsxIS directly maps the t element. 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). -type xlsxIS struct { - T string `xml:"t"` -} - // xlsxF directly maps the f element in the namespace // http://schemas.openxmlformats.org/spreadsheetml/2006/main - currently I have // not checked it for completeness - it does as much as I need. From 9c70d0ac868f66badf2663cc7b4b3c46d5411131 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 11 Aug 2019 00:36:14 +0800 Subject: [PATCH 137/957] Documentation updated, Go 1.10+ required --- .travis.yml | 1 - adjust.go | 2 +- calcchain.go | 2 +- cell.go | 2 +- cellmerged.go | 2 +- chart.go | 2 +- col.go | 2 +- comment.go | 2 +- datavalidation.go | 2 +- datavalidation_test.go | 2 +- date.go | 2 +- docProps.go | 2 +- docProps_test.go | 2 +- errors.go | 2 +- excelize.go | 2 +- file.go | 2 +- lib.go | 2 +- picture.go | 2 +- rows.go | 2 +- shape.go | 2 +- sheet.go | 4 +--- sheetpr.go | 2 +- sheetview.go | 2 +- sparkline.go | 2 +- styles.go | 2 +- table.go | 2 +- templates.go | 2 +- test/SharedStrings.xlsx | Bin 9419 -> 7386 bytes vmlDrawing.go | 2 +- xmlApp.go | 8 +++++++- xmlCalcChain.go | 2 +- xmlChart.go | 2 +- xmlComments.go | 2 +- xmlContentTypes.go | 2 +- xmlCore.go | 2 +- xmlDecodeDrawing.go | 2 +- xmlDrawing.go | 2 +- xmlSharedStrings.go | 2 +- xmlStyles.go | 2 +- xmlTable.go | 2 +- xmlTheme.go | 2 +- xmlWorkbook.go | 2 +- xmlWorksheet.go | 6 ++---- 43 files changed, 48 insertions(+), 47 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9f892c506f..faf9916b1e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,6 @@ install: - go get -d -t -v ./... && go build -v ./... go: - - 1.9.x - 1.10.x - 1.11.x - 1.12.x diff --git a/adjust.go b/adjust.go index ccc5ce94d0..186112d6f5 100644 --- a/adjust.go +++ b/adjust.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/calcchain.go b/calcchain.go index ce679e531c..b4cadefe0b 100644 --- a/calcchain.go +++ b/calcchain.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/cell.go b/cell.go index 6743e2a592..f61e268a24 100644 --- a/cell.go +++ b/cell.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/cellmerged.go b/cellmerged.go index a78b244530..c1df9b3db9 100644 --- a/cellmerged.go +++ b/cellmerged.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/chart.go b/chart.go index b9439ca7e0..6a106f07f8 100644 --- a/chart.go +++ b/chart.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/col.go b/col.go index db3a901d32..ffa0ca6d66 100644 --- a/col.go +++ b/col.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/comment.go b/comment.go index bc6fa2771b..97e0e9bb3b 100644 --- a/comment.go +++ b/comment.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/datavalidation.go b/datavalidation.go index 209204ae28..2499035fbc 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/datavalidation_test.go b/datavalidation_test.go index 0fee092001..211830d359 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/date.go b/date.go index b49a6958da..8f637029fd 100644 --- a/date.go +++ b/date.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/docProps.go b/docProps.go index ff19fdaf2e..166512f73c 100644 --- a/docProps.go +++ b/docProps.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/docProps_test.go b/docProps_test.go index 1f52beb299..671d998cc9 100644 --- a/docProps_test.go +++ b/docProps_test.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/errors.go b/errors.go index 3404c7e4fe..8520a012f5 100644 --- a/errors.go +++ b/errors.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/excelize.go b/excelize.go index 6d014a0942..b734e57b99 100644 --- a/excelize.go +++ b/excelize.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. // // See https://xuri.me/excelize for more information about this package. package excelize diff --git a/file.go b/file.go index a4aa11d0c9..46f1f62524 100644 --- a/file.go +++ b/file.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/lib.go b/lib.go index b99b175bb0..4dea16a126 100644 --- a/lib.go +++ b/lib.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/picture.go b/picture.go index 812eb5c509..62d48dc5fc 100644 --- a/picture.go +++ b/picture.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/rows.go b/rows.go index c17179faac..6281e625cd 100644 --- a/rows.go +++ b/rows.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/shape.go b/shape.go index 7dc702135b..e3ed968e33 100644 --- a/shape.go +++ b/shape.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/sheet.go b/sheet.go index 935deac230..ed6d888ee5 100644 --- a/sheet.go +++ b/sheet.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize @@ -740,9 +740,7 @@ func (f *File) searchSheet(name, value string, regSearch bool) ([]string, error) result []string r xlsxRow ) - xml.NewDecoder(bytes.NewReader(f.readXML(name))) d := f.sharedStringsReader() - decoder := xml.NewDecoder(bytes.NewReader(f.readXML(name))) for { token, _ := decoder.Token() diff --git a/sheetpr.go b/sheetpr.go index 66761f3f7f..a273ac1a5f 100644 --- a/sheetpr.go +++ b/sheetpr.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/sheetview.go b/sheetview.go index 9712d8534b..09f5789219 100644 --- a/sheetview.go +++ b/sheetview.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/sparkline.go b/sparkline.go index 73e125ed4d..18eae6c8ba 100644 --- a/sparkline.go +++ b/sparkline.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/styles.go b/styles.go index 04a5c3349a..c19cee0f23 100644 --- a/styles.go +++ b/styles.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/table.go b/table.go index 3d8d40279a..45a1622698 100644 --- a/table.go +++ b/table.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/templates.go b/templates.go index 923cebd1fa..0d3a0c58a4 100644 --- a/templates.go +++ b/templates.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. // // This file contains default templates for XML files we don't yet populated // based on content. diff --git a/test/SharedStrings.xlsx b/test/SharedStrings.xlsx index d6004c0cd2b60c0324378fde7017b55d21a79dbb..bcea2c81caa16f9905b53cb72987ff467f76c7b0 100644 GIT binary patch literal 7386 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 O0Qw*Wz!BxocmD@YLNdYt diff --git a/vmlDrawing.go b/vmlDrawing.go index 8b1d00fe96..24b615fd4d 100644 --- a/vmlDrawing.go +++ b/vmlDrawing.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/xmlApp.go b/xmlApp.go index ad414fae06..48450e3ff3 100644 --- a/xmlApp.go +++ b/xmlApp.go @@ -5,12 +5,15 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize import "encoding/xml" +// xlsxProperties specifies to an OOXML document properties such as the +// template used, the number of pages and words, and the application name and +// version. type xlsxProperties struct { XMLName xml.Name `xml:"http://schemas.openxmlformats.org/officeDocument/2006/extended-properties Properties"` Template string @@ -42,6 +45,8 @@ type xlsxProperties struct { DocSecurity int } +// xlsxVectorVariant specifies the set of hyperlinks that were in this +// document when last saved. type xlsxVectorVariant struct { Content string `xml:",innerxml"` } @@ -50,6 +55,7 @@ type xlsxVectorLpstr struct { Content string `xml:",innerxml"` } +// xlsxDigSig contains the signature of a digitally signed document. type xlsxDigSig struct { Content string `xml:",innerxml"` } diff --git a/xmlCalcChain.go b/xmlCalcChain.go index 05a176d1b2..343f15f6ef 100644 --- a/xmlCalcChain.go +++ b/xmlCalcChain.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/xmlChart.go b/xmlChart.go index 8a3a680c2e..bb4b4bc8b1 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/xmlComments.go b/xmlComments.go index 47d8f5156f..f13d00243b 100644 --- a/xmlComments.go +++ b/xmlComments.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/xmlContentTypes.go b/xmlContentTypes.go index e99b0b32d5..fa4d3475a6 100644 --- a/xmlContentTypes.go +++ b/xmlContentTypes.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/xmlCore.go b/xmlCore.go index 357f688a6b..96482fc160 100644 --- a/xmlCore.go +++ b/xmlCore.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/xmlDecodeDrawing.go b/xmlDecodeDrawing.go index 6cb224a7a8..e11bb00d16 100644 --- a/xmlDecodeDrawing.go +++ b/xmlDecodeDrawing.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/xmlDrawing.go b/xmlDrawing.go index 20cb83dd99..ade6261252 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/xmlSharedStrings.go b/xmlSharedStrings.go index 48d4464ff9..79837411bd 100644 --- a/xmlSharedStrings.go +++ b/xmlSharedStrings.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/xmlStyles.go b/xmlStyles.go index 49abe3c636..5823bc99f1 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/xmlTable.go b/xmlTable.go index 6d27dc9d44..ca4ce03610 100644 --- a/xmlTable.go +++ b/xmlTable.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/xmlTheme.go b/xmlTheme.go index 01d0054c27..f764c20784 100644 --- a/xmlTheme.go +++ b/xmlTheme.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/xmlWorkbook.go b/xmlWorkbook.go index 01186851ea..8150e295c1 100644 --- a/xmlWorkbook.go +++ b/xmlWorkbook.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize diff --git a/xmlWorksheet.go b/xmlWorksheet.go index a5db776bbb..09dec5e738 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -5,13 +5,11 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.8 or later. +// charts of XLSX. This library needs Go version 1.10 or later. package excelize -import ( - "encoding/xml" -) +import "encoding/xml" // xlsxWorksheet directly maps the worksheet element in the namespace // http://schemas.openxmlformats.org/spreadsheetml/2006/main - currently I have From ac395a60ed2ac643403678991ff4745231ff48c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Mengu=C3=A9?= Date: Tue, 13 Aug 2019 15:39:12 +0200 Subject: [PATCH 138/957] SetCellValue: use fmt.Sprint(v) instead of fmt.Sprintf("%v", v) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Because that does the same thing, but without having to parse a format string. Signed-off-by: Olivier Mengué --- cell.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cell.go b/cell.go index f61e268a24..9d478a5964 100644 --- a/cell.go +++ b/cell.go @@ -94,7 +94,7 @@ func (f *File) SetCellValue(sheet, axis string, value interface{}) error { case nil: err = f.SetCellStr(sheet, axis, "") default: - err = f.SetCellStr(sheet, axis, fmt.Sprintf("%v", value)) + err = f.SetCellStr(sheet, axis, fmt.Sprint(value)) } return err } From 64809db2c9ee30779e4839e9d60a315479092ce6 Mon Sep 17 00:00:00 2001 From: mqy Date: Mon, 19 Aug 2019 15:53:56 +0800 Subject: [PATCH 139/957] add missing error check in SetSheetRow() --- cell.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/cell.go b/cell.go index 9d478a5964..e897379447 100644 --- a/cell.go +++ b/cell.go @@ -471,12 +471,14 @@ 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 ckecking to early detect regresions - // if it will be introduced in furure + // Error should never happens here. But keep checking to early detect regresions + // if it will be introduced in future. if err != nil { return err } - f.SetCellValue(sheet, cell, v.Index(i).Interface()) + if err := f.SetCellValue(sheet, cell, v.Index(i).Interface()); err != nil { + return err + } } return err } From 407fb55c20a2524c4eccad9361120dee2a2719cd Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 21 Aug 2019 23:03:34 +0800 Subject: [PATCH 140/957] Update the Godoc --- README_zh.md | 2 +- excelize_test.go | 4 ++-- shape.go | 2 +- sparkline.go | 14 +++++++------- styles.go | 2 +- styles_test.go | 4 ++-- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/README_zh.md b/README_zh.md index 044d9305ae..d57f3012fe 100644 --- a/README_zh.md +++ b/README_zh.md @@ -13,7 +13,7 @@ ## 简介 -Excelize 是 Go 语言编写的用于操作 Office Excel 文档类库,基于 ECMA-376 Office OpenXML 标准。可以使用它来读取、写入由 Microsoft Excel™ 2007 及以上版本创建的 XLSX 文档。相比较其他的开源类库,Excelize 支持写入原本带有图片(表)、透视表和切片器等复杂样式的文档,还支持向 Excel 文档中插入图片与图表,并且在保存后不会丢失文档原有样式,可以应用于各类报表系统中。使用本类库要求使用的 Go 语言为 1.8 或更高版本,完整的 API 使用文档请访问 [godoc.org](https://godoc.org/github.com/360EntSecGroup-Skylar/excelize) 或查看 [参考文档](https://xuri.me/excelize/)。 +Excelize 是 Go 语言编写的用于操作 Office Excel 文档类库,基于 ECMA-376 Office OpenXML 标准。可以使用它来读取、写入由 Microsoft Excel™ 2007 及以上版本创建的 XLSX 文档。相比较其他的开源类库,Excelize 支持写入原本带有图片(表)、透视表和切片器等复杂样式的文档,还支持向 Excel 文档中插入图片与图表,并且在保存后不会丢失文档原有样式,可以应用于各类报表系统中。使用本类库要求使用的 Go 语言为 1.10 或更高版本,完整的 API 使用文档请访问 [godoc.org](https://godoc.org/github.com/360EntSecGroup-Skylar/excelize) 或查看 [参考文档](https://xuri.me/excelize/)。 ## 快速上手 diff --git a/excelize_test.go b/excelize_test.go index d61dd7bf40..a5d7671baf 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -640,7 +640,7 @@ func TestSetCellStyleFont(t *testing.T) { } var style int - style, err = f.NewStyle(`{"font":{"bold":true,"italic":true,"family":"Berlin Sans FB Demi","size":36,"color":"#777777","underline":"single"}}`) + 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() } @@ -809,7 +809,7 @@ func TestAddShape(t *testing.T) { f.AddShape("Sheet1", "A30", `{"type":"rect","paragraph":[{"text":"Rectangle","font":{"color":"CD5C5C"}},{"text":"Shape","font":{"bold":true,"color":"2980B9"}}]}`) f.AddShape("Sheet1", "B30", `{"type":"rect","paragraph":[{"text":"Rectangle"},{}]}`) f.AddShape("Sheet1", "C30", `{"type":"rect","paragraph":[]}`) - f.AddShape("Sheet3", "H1", `{"type":"ellipseRibbon", "color":{"line":"#4286f4","fill":"#8eb9ff"}, "paragraph":[{"font":{"bold":true,"italic":true,"family":"Berlin Sans FB Demi","size":36,"color":"#777777","underline":"single"}}], "height": 90}`) + f.AddShape("Sheet3", "H1", `{"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}`) f.AddShape("Sheet3", "H1", "") assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddShape.xlsx"))) diff --git a/shape.go b/shape.go index e3ed968e33..8d95849c03 100644 --- a/shape.go +++ b/shape.go @@ -40,7 +40,7 @@ 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":"Berlin Sans FB Demi","size":36,"color":"#777777","underline":"sng"}}],"width":180,"height": 90}`) +// 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}`) // // The following shows the type of shape supported by excelize: // diff --git a/sparkline.go b/sparkline.go index 18eae6c8ba..314ea834f0 100644 --- a/sparkline.go +++ b/sparkline.go @@ -374,15 +374,15 @@ func (f *File) addSparklineGroupByStyle(ID int) *xlsxX14SparklineGroup { // Parameter | Description // -----------+-------------------------------------------- // Location | Required, must have the same number with 'Range' parameter -// Range |Required, must have the same number with 'Location' 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 sparkine high points -// Low | Toggle sparkine low points -// First | Toggle sparkine first points -// Last | Toggle sparkine last points -// Negative | Toggle sparkine negative points -// Markers | Toggle sparkine markers +// 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 // diff --git a/styles.go b/styles.go index c19cee0f23..16f80305b6 100644 --- a/styles.go +++ b/styles.go @@ -2351,7 +2351,7 @@ func (f *File) GetCellStyle(sheet, axis string) (int, error) { // // Set font style for cell H9 on Sheet1: // -// style, err := f.NewStyle(`{"font":{"bold":true,"italic":true,"family":"Berlin Sans FB Demi","size":36,"color":"#777777"}}`) +// style, err := f.NewStyle(`{"font":{"bold":true,"italic":true,"family":"Times New Roman","size":36,"color":"#777777"}}`) // if err != nil { // fmt.Println(err) // } diff --git a/styles_test.go b/styles_test.go index decfbb9224..36a78ed8ca 100644 --- a/styles_test.go +++ b/styles_test.go @@ -168,14 +168,14 @@ func TestSetConditionalFormat(t *testing.T) { func TestNewStyle(t *testing.T) { f := NewFile() - styleID, err := f.NewStyle(`{"font":{"bold":true,"italic":true,"family":"Berlin Sans FB Demi","size":36,"color":"#777777"}}`) + styleID, err := f.NewStyle(`{"font":{"bold":true,"italic":true,"family":"Times New Roman","size":36,"color":"#777777"}}`) if err != nil { t.Fatal(err) } styles := f.stylesReader() fontID := styles.CellXfs.Xf[styleID].FontID font := styles.Fonts.Font[fontID] - assert.Contains(t, font.Name.Val, "Berlin Sans FB Demi", "Stored font should contain font name") + assert.Contains(t, font.Name.Val, "Times New Roman", "Stored font should contain font name") assert.Equal(t, 2, styles.CellXfs.Count, "Should have 2 styles") } From 875dd22bd013ef3873711c8e82f3d4d5e1675ebc Mon Sep 17 00:00:00 2001 From: Matthew McFarling Date: Wed, 28 Aug 2019 17:05:27 -0400 Subject: [PATCH 141/957] Updating Readme Removing the /v2 on the package url as it does not work with the ```go get``` command. --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 4bb7d6682c..7f9cf7039e 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Supports saving a file without losing original charts of XLSX. This library need ### Installation ```bash -go get github.com/360EntSecGroup-Skylar/excelize/v2 +go get github.com/360EntSecGroup-Skylar/excelize ``` ### Create XLSX file @@ -34,7 +34,7 @@ package main import ( "fmt" - "github.com/360EntSecGroup-Skylar/excelize/v2" + "github.com/360EntSecGroup-Skylar/excelize" ) func main() { @@ -64,7 +64,7 @@ package main import ( "fmt" - "github.com/360EntSecGroup-Skylar/excelize/v2" + "github.com/360EntSecGroup-Skylar/excelize" ) func main() { @@ -103,7 +103,7 @@ package main import ( "fmt" - "github.com/360EntSecGroup-Skylar/excelize/v2" + "github.com/360EntSecGroup-Skylar/excelize" ) func main() { @@ -140,7 +140,7 @@ import ( _ "image/jpeg" _ "image/png" - "github.com/360EntSecGroup-Skylar/excelize/v2" + "github.com/360EntSecGroup-Skylar/excelize" ) func main() { From 1fc4bc52fb8a0160eb624e7a24619d8c0e47e540 Mon Sep 17 00:00:00 2001 From: Vsevolod Balashov Date: Sun, 1 Sep 2019 07:30:14 +0300 Subject: [PATCH 142/957] Fix #386 regression test added (#440) * #386 regression test added * closes #386 string to bigint on GOARCH=386 --- cell_test.go | 13 +++++++++++++ styles.go | 2 +- test/OverflowNumericCell.xlsx | Bin 0 -> 11445 bytes 3 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 test/OverflowNumericCell.xlsx diff --git a/cell_test.go b/cell_test.go index d4a5b02ea0..09627c2ff8 100644 --- a/cell_test.go +++ b/cell_test.go @@ -2,6 +2,7 @@ package excelize import ( "fmt" + "path/filepath" "testing" "github.com/stretchr/testify/assert" @@ -94,3 +95,15 @@ func BenchmarkSetCellValue(b *testing.B) { } } } + +func TestOverflowNumericCell(t *testing.T) { + f, err := OpenFile(filepath.Join("test", "OverflowNumericCell.xlsx")) + if !assert.NoError(t, err) { + t.FailNow() + } + // source of xlsx file is Russia, don`t touch it, elsewhere bug not reproduced + val, err := f.GetCellValue("Лист1", "A1") + assert.NoError(t, err) + // GOARCH=amd64 - all ok; GOARCH=386 - actual : "-2147483648" + assert.Equal(t, "8595602512225", val, "A1 should be 8595602512225") +} diff --git a/styles.go b/styles.go index 16f80305b6..4d6071a9ba 100644 --- a/styles.go +++ b/styles.go @@ -852,7 +852,7 @@ func formatToInt(i int, v string) string { if err != nil { return v } - return fmt.Sprintf("%d", int(f)) + return fmt.Sprintf("%d", int64(f)) } // formatToFloat provides a function to convert original string to float diff --git a/test/OverflowNumericCell.xlsx b/test/OverflowNumericCell.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..184488827224fdd784315d8c3de25b7ae31f47be GIT binary patch literal 11445 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 literal 0 HcmV?d00001 From 0acb3ef9685e80d51dfda5ab9a9db870af7e1614 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 2 Sep 2019 21:52:55 +0800 Subject: [PATCH 143/957] Testing files updated --- README_zh.md | 10 +++++----- cell_test.go | 5 ++--- chart.go | 2 +- picture.go | 4 ++-- sheet_test.go | 3 ++- sheetpr_test.go | 2 +- sheetview_test.go | 2 +- test/BadWorkbook.xlsx | Bin 6019 -> 4955 bytes test/CalcChain.xlsx | Bin 5959 -> 5838 bytes test/MergeCell.xlsx | Bin 8583 -> 6343 bytes test/OverflowNumericCell.xlsx | Bin 11445 -> 5094 bytes 11 files changed, 14 insertions(+), 14 deletions(-) diff --git a/README_zh.md b/README_zh.md index d57f3012fe..6c2b190a12 100644 --- a/README_zh.md +++ b/README_zh.md @@ -20,7 +20,7 @@ Excelize 是 Go 语言编写的用于操作 Office Excel 文档类库,基于 E ### 安装 ```bash -go get github.com/360EntSecGroup-Skylar/excelize/v2 +go get github.com/360EntSecGroup-Skylar/excelize ``` ### 创建 Excel 文档 @@ -33,7 +33,7 @@ package main import ( "fmt" - "github.com/360EntSecGroup-Skylar/excelize/v2" + "github.com/360EntSecGroup-Skylar/excelize" ) func main() { @@ -63,7 +63,7 @@ package main import ( "fmt" - "github.com/360EntSecGroup-Skylar/excelize/v2" + "github.com/360EntSecGroup-Skylar/excelize" ) func main() { @@ -102,7 +102,7 @@ package main import ( "fmt" - "github.com/360EntSecGroup-Skylar/excelize/v2" + "github.com/360EntSecGroup-Skylar/excelize" ) func main() { @@ -140,7 +140,7 @@ import ( _ "image/jpeg" _ "image/png" - "github.com/360EntSecGroup-Skylar/excelize/v2" + "github.com/360EntSecGroup-Skylar/excelize" ) func main() { diff --git a/cell_test.go b/cell_test.go index 09627c2ff8..653aaab874 100644 --- a/cell_test.go +++ b/cell_test.go @@ -101,9 +101,8 @@ func TestOverflowNumericCell(t *testing.T) { if !assert.NoError(t, err) { t.FailNow() } - // source of xlsx file is Russia, don`t touch it, elsewhere bug not reproduced - val, err := f.GetCellValue("Лист1", "A1") + val, err := f.GetCellValue("Sheet1", "A1") assert.NoError(t, err) - // GOARCH=amd64 - all ok; GOARCH=386 - actual : "-2147483648" + // GOARCH=amd64 - all ok; GOARCH=386 - actual: "-2147483648" assert.Equal(t, "8595602512225", val, "A1 should be 8595602512225") } diff --git a/chart.go b/chart.go index 6a106f07f8..e1eb81f1f9 100644 --- a/chart.go +++ b/chart.go @@ -543,7 +543,7 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // import ( // "fmt" // -// "github.com/360EntSecGroup-Skylar/excelize/v2" +// "github.com/360EntSecGroup-Skylar/excelize" // ) // // func main() { diff --git a/picture.go b/picture.go index 62d48dc5fc..a5904ffea2 100644 --- a/picture.go +++ b/picture.go @@ -51,7 +51,7 @@ func parseFormatPictureSet(formatSet string) (*formatPicture, error) { // _ "image/jpeg" // _ "image/png" // -// "github.com/360EntSecGroup-Skylar/excelize/v2" +// "github.com/360EntSecGroup-Skylar/excelize" // ) // // func main() { @@ -111,7 +111,7 @@ func (f *File) AddPicture(sheet, cell, picture, format string) error { // _ "image/jpeg" // "io/ioutil" // -// "github.com/360EntSecGroup-Skylar/excelize/v2" +// "github.com/360EntSecGroup-Skylar/excelize" // ) // // func main() { diff --git a/sheet_test.go b/sheet_test.go index 3baa084a32..145e302b1e 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -6,7 +6,8 @@ import ( "strings" "testing" - "github.com/360EntSecGroup-Skylar/excelize/v2" + "github.com/360EntSecGroup-Skylar/excelize" + "github.com/mohae/deepcopy" "github.com/stretchr/testify/assert" ) diff --git a/sheetpr_test.go b/sheetpr_test.go index 97a314c918..b9f9e3be38 100644 --- a/sheetpr_test.go +++ b/sheetpr_test.go @@ -7,7 +7,7 @@ import ( "github.com/mohae/deepcopy" "github.com/stretchr/testify/assert" - "github.com/360EntSecGroup-Skylar/excelize/v2" + "github.com/360EntSecGroup-Skylar/excelize" ) var _ = []excelize.SheetPrOption{ diff --git a/sheetview_test.go b/sheetview_test.go index 2e697b8540..07f59ed337 100644 --- a/sheetview_test.go +++ b/sheetview_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/assert" - "github.com/360EntSecGroup-Skylar/excelize/v2" + "github.com/360EntSecGroup-Skylar/excelize" ) var _ = []excelize.SheetViewOption{ diff --git a/test/BadWorkbook.xlsx b/test/BadWorkbook.xlsx index f917a20a139afe270a7a8708bcce5e25ed1802cf..a1901b0cb2f2c61cd88664c95ab3868b3966d456 100644 GIT binary patch literal 4955 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 I?-%<20YQ^rP5=M^ literal 6019 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 delta 4270 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 diff --git a/test/MergeCell.xlsx b/test/MergeCell.xlsx index d4dad181d620ae074eab39329943b700e795ea06..3539e4bd5482911929bbd8765efaefb851946c19 100644 GIT binary patch literal 6343 zcmaJ_bySr7)o@o-e=o6*JF%Q6}@LrQ5g(x*wm@=m4;+tU}Ix<&&QQn77wl(8#c0PuOYG`yO zk6k-UH`+$>TU||ND`pcEph~Am z$Q3E-EJ=E=d4V^$$mT0L5bi?GFbvHR2r%1FQ`C`3W#@!hwiqbzQ)*3#nBz#_j#=;% zJiQg?4xHn%C|aYm=M;yHjUUwg%^29l+CA+O+lCw%R` zS-=x+0UsoD*LxOZ;c;4+mZXGnDNw$B3tR##NFyN&@xJMheF6g$0S^PC@L#)ucHa$c zTSE&wCdP+n0i;I?js@$}p7$Qs>TFNBcOd(eG`1^l&QoTZQqVRHF63os=haS(f ze2D|s5gOIT-DJ$tu6X%)87ljH)0x0XW81I!s5b4@>D{o3X$QWRhHs7A4+DK3zdOU!^PWAwig1cP9<~;y1u>S-R*8LX- zV0{H!u(chNuC?_8TazGy;3;Nwzr8Cw@7=I%9a?_@mA7T2t|lcgl0QpnhDsDfSw%)A zPPfE)LlQE0dw~29&)aKSkJ2f`0b#KeJ?buX3Ivf@GcvAdclD3;mox~FFenC{5cS!o z!&Y}}*El7lIKEV!kick*plUzGb&YY2L>pe)EJ+vhAj8@0VfNVu58FM>z>HNJEkHB+vM7icFKHe+3k6d9-ic_~uX?6x(5y@mQkvsL)&Q zO$^NfG?Jv!RC^IC^}$2qpHfG}1=q!kg{`Hx*nVuA4)~-;AzaQS&b$xS#@E%!H2{}Z zL@v)&`S1kxQy3nxt#Da9L59W$>+z+?rl8!7L@gncF&d9Fvjw7!LbG7b)72+s?(~@C zC&ckxP=68$boHeUtufxDxVtySS|#l=G_Z6Xy$G0@MCV}AL3Mj+Jst{a+Hch|Uqakm z>R)TEyd7(z=2~%A*=Mu5QPckwer0ic&toKHqA1~Rr+Z%G!TnRyupc)~A8c#*(6um# zmUI&{z<=)skK)X7au*K666xhnTiQyN14gPW&EgC4AjhjSBZs`E9ietv=ai%6MB&Z} zIp{iCVyu`dw>(>@fE{JV>^o;@BCvws;#ZkY!GOX7zcXJRSfPVqrL`f14gY<_{9ATY z`Kp3_PIzgQ19!CG3w9hH|1g_iKW7JGJ;FO3Vsg3J2XQ7kg#9*hZl1uKwVGUq+MI-~I>Q9deD{Xe?$!MH)3`sy0(S}O7^ypRJvs6&-9xKr^iRp&JWFE=1vUZ#3Ng=&9 zSWF#iD}A~W;Z{LwH*|^Y&AH;^qm(tmu$5bVlJTDj9c>Cnw#wG4Ld3jeavUE$Txuq4 z#X9_ZF*fErX-=Rvg*B7$L zdrCZV7ydjqW&O3fHVy`1SlpDfo)q~!%l$D;&28vLZ0C=_Au^Lnd@h5O&#RME2M0?N zc7Na=&QKgf1gTYqfUhPGSaNxmru;qEHep)Kob{9arX%rNC0X^>$Pcy&VB@tMg>(IjQHd!Rc`nr0UC zfxWoY{)76LqhqVKIQGnfC{x?pBej~3^WEvp(J@2D&wa?)dbU2ID@8MZPmf`BziAgULzT!KOt@VS+(>_E~y^jcGBsX4-k zjO+D)|0QtihM@~3k-wWU6dSp&c) znSsw1zj=CmziWA^DS44hXCso(} z*@s7TQM#JiG=!#SR9xd5^(cTY;vG)?&NFm^ehfFl;tKxl%wMMoJQg$I<0UI-gi5Bo z=NEW36#jKKx`qp1Kk1`TkXdC?TrJ`V>ls;WId$fgZr$ zR6>(qgQ@kwY|KqI0f#7RlqQpu*3rko8zi>GLr=P@m++{*S2TnnL4E>6jPoRq zgrmGckaLQTPuR-vs+l3Y-Zn{-`<#ayXyV9EaBv<5msu>kXT^#N?NJ3J=Iqw!IbwC3fX|} zL?M*Y8kbPCFt&;jZhe|#z}B$o`o%l3n5Ag{RG=>kdti{F+@R8HsLUI$a;3;8#uo7t z3OO6iDEuu#YIsaEwE*L)DXUH;+-tvhfmXd^8Iuaq#=^J>0$A9a4A#TgsP*tyr_cHm zraM?Q=`u>i0*lSe55Ies9Z3`JA!3YM|cZNToh1N_p-)oKAnuwX;3vWkS8TWvlR4264OD zO;bww1>2lqF&dmcBni|Typau;V7q>0`VlHKI?!9%XQ;hcMH+%GsCW}`fn6IKmM0|W zfc!BuYxDyA`QgD8`#ej&nSyswO(N=!zIGjC`XM=k<(UgVGH1?pG|d|D!!{~IC-Q3B z2L*4RYa7aUs>6v4%U`G-7G9{jGyXx(uL1PW~tdNB5NbV zx#|>wkR&IHGdwd|cIFt1P?w&?>}eiYm?lO{5g2Sed(vv%h7zle$vTpMpck`W_-rF{ z>&{TVGjq@*e-MXTTa%kIJzk>O1d&?T&)=r0S)-QJ{gBhqTYfbBR&2e)S>}ZnhheC$ z14Tz|9m2Ic73%me7b5L+Z2Dz8^klRtXq>oy?r~==OSIT+(Tr=}5JTRmYnE{on~_WK zGw~>gS5dyU#jG9E@}_&Or|wzO;vLqSZI?* zt}v5!NSOSC-0)~PU&`e3g_L`&>C##%UzB)?M^#vtTV#}jr{`jtdhS{R2k2=d-%Rs9 zlgVod2_|;7-C&rDD4J?Z^w@v;Ai3D9B=oEZFfeBKL*9R1C7=E+xsPYR0wqoG5HrA2 zd=aGP*_Y_XWX9uAD?si0vbEfJ%!WAcGpR*DIr%Lw5M=|^jW96woTTmIT1$?mB1`dG z(X?8cvcC^88D-(uUcX7>^M#{MVm$*=<=4>~2k0qPEfqr*U>b2tOwEF)$S_eo)(h`B zXqta`#}3b~2h%&34t>wS)F=g(vnH;V&re4~eCtvm5L7>2-x3!L|Jfb*CA6MR2>qBB z0b>r_WjR*#&VbY@Q6(f^Sj$Bw7Ei#!53oD!t@rN7fooA&nkLE*dBH(S;S1PmK(|Sr z(=KAFXQX=?*S;i{JtXT(Qp9yhDd|A&r9{^%eo$n_D1+UCbFywK#YrRx+)NNxU7hFW zOGAQwgDqy@+M%b9HP2(kDdwKJjqar7spg|UT8j-*nw@Y^>ghig%_C@5!SNKE1FQ0Yd7 z&E4DmbPvpQ(}4}i?Du4v^?e?sS^cC_tCGuwpS}(<*KOCX>1lJ19}nq1!>#fL)1A^Q z?o3Z}afr<9794Rpvf(Ryb$D)%b>kE&g=jbUYNvx49ge%POb^OJZiETGyqE``HB>wL zC5rghKNZWPdXAyUq}GkYBKB{fDHzsc*(6iX5#wxLaKy+0J7!Z?W7t`$KHruXQ^M}(NEwL^9 z_ZE_(&6;b&<(scuCOM+gED_c`PlTeh+?{ZnJY-qLUNbbqPbUfXu(io;f65wp0knoC zbPQS0GX*(IZeEnH&cpPC-z{6)#`30GX@izc`^~$ist> zY8sTGWkXoD=S5SZn7T-7n<}K*DldC8~b2hV=oxnH=YkWy3?rers%=C{65} zcIeDAoi!c){AoYCPfG@Ccw1YjYZepPd9}Bj??zTtd!GEDPX@vEaqaV2wn2*mJ+-H@ zHL@dfHP~f{Ldh|XnG4_yeFWf?pgD-&%?_B=xM5$h3upV8k`mEjMz64i$&c_k5m@uU zTAPXV7F&EbgFuPnPn)d?OgLdpzCTy!nyv=+Ld$Z$72R>TF4_~=y@9o}qEY#|v{i*I z(FEyL&YxgbVTYw>9W+(r^rsGc7fkF&`CJg27~pTHuQsTbS@{-VaC7VK;(&oURK_Bk z!|stfO$#@hEj9O(PN}x@Y*^yw82`xGNqkXmLs;;97~NH}Mq+JT@1&A`67-B5o+mFC zF}Zbb_m8QATYa z>TS?30Vu8h!@~(qDD{kL~bhg~xl=!;<|wtT;=`Ay_vh1n KpIPu9-u?#^vg}v@ literal 8583 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

jt665zy=IXZKl9T+W{g(|5Nc}$pT$R$7ONHO| z68)D~Rt$eSUX^o~O8d9L(a-s(?*7yJsy4Zle80^Q&Aa~3`+o)CpYB(Q`=x;SZC&U& z^6zry&p21f-X+cdZ9i~+#raM8|8%`NwEjRI|G#p>KLcECz02A5+n~h10$iPRe>z{S liOcT%Z5!xVXaFvq|6i|y)B)(%!oa{se`L{Fiu(oq{{R;Xl1Bgl literal 11445 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 From b30c642e2bf2a328cf087e03399b783e02e1e647 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 5 Sep 2019 23:42:40 +0800 Subject: [PATCH 144/957] Prepare pivot table support, add pivot table definition struct --- xmlPivotTable.go | 289 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 289 insertions(+) create mode 100644 xmlPivotTable.go diff --git a/xmlPivotTable.go b/xmlPivotTable.go new file mode 100644 index 0000000000..16c469f1aa --- /dev/null +++ b/xmlPivotTable.go @@ -0,0 +1,289 @@ +// Copyright 2016 - 2019 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. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.10 or later. + +package excelize + +import "encoding/xml" + +// xlsxPivotTableDefinition represents the PivotTable root element for +// non-null PivotTables. There exists one pivotTableDefinition for each +// PivotTableDefinition part +type xlsxPivotTableDefinition struct { + XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main pivotTableDefinition"` + Name string `xml:"name,attr"` + CacheID int `xml:"cacheId,attr"` + DataOnRows bool `xml:"dataOnRows,attr"` + DataPosition int `xml:"dataPosition,attr"` + DataCaption string `xml:"dataCaption,attr"` + GrandTotalCaption string `xml:"grandTotalCaption,attr"` + ErrorCaption string `xml:"errorCaption,attr"` + ShowError bool `xml:"showError,attr"` + MissingCaption string `xml:"missingCaption,attr"` + ShowMissing bool `xml:"showMissing,attr"` + PageStyle string `xml:"pageStyle,attr"` + PivotTableStyle string `xml:"pivotTableStyle,attr"` + VacatedStyle string `xml:"vacatedStyle,attr"` + Tag string `xml:"tag,attr"` + UpdatedVersion int `xml:"updatedVersion,attr"` + MinRefreshableVersion int `xml:"minRefreshableVersion,attr"` + AsteriskTotals bool `xml:"asteriskTotals,attr"` + ShowItems bool `xml:"showItems,attr"` + EditData bool `xml:"editData,attr"` + DisableFieldList bool `xml:"disableFieldList,attr"` + ShowCalcMbrs bool `xml:"showCalcMbrs,attr"` + VisualTotals bool `xml:"visualTotals,attr"` + ShowMultipleLabel bool `xml:"showMultipleLabel,attr"` + ShowDataDropDown bool `xml:"showDataDropDown,attr"` + ShowDrill bool `xml:"showDrill,attr"` + PrintDrill bool `xml:"printDrill,attr"` + ShowMemberPropertyTips bool `xml:"showMemberPropertyTips,attr"` + ShowDataTips bool `xml:"showDataTips,attr"` + EnableWizard bool `xml:"enableWizard,attr"` + EnableDrill bool `xml:"enableDrill,attr"` + EnableFieldProperties bool `xml:"enableFieldProperties,attr"` + PreserveFormatting bool `xml:"preserveFormatting,attr"` + UseAutoFormatting bool `xml:"useAutoFormatting,attr"` + PageWrap int `xml:"pageWrap,attr"` + PageOverThenDown bool `xml:"pageOverThenDown,attr"` + SubtotalHiddenItems bool `xml:"subtotalHiddenItems,attr"` + RowGrandTotals bool `xml:"rowGrandTotals,attr"` + ColGrandTotals bool `xml:"colGrandTotals,attr"` + FieldPrintTitles bool `xml:"fieldPrintTitles,attr"` + ItemPrintTitles bool `xml:"itemPrintTitles,attr"` + MergeItem bool `xml:"mergeItem,attr"` + ShowDropZones bool `xml:"showDropZones,attr"` + CreatedVersion int `xml:"createdVersion,attr"` + Indent int `xml:"indent,attr"` + ShowEmptyRow bool `xml:"showEmptyRow,attr"` + ShowEmptyCol bool `xml:"showEmptyCol,attr"` + ShowHeaders bool `xml:"showHeaders,attr"` + Compact bool `xml:"compact,attr"` + Outline bool `xml:"outline,attr"` + OutlineData bool `xml:"outlineData,attr"` + CompactData bool `xml:"compactData,attr"` + Published bool `xml:"published,attr"` + GridDropZones bool `xml:"gridDropZones,attr"` + Immersive bool `xml:"immersive,attr"` + MultipleFieldFilters bool `xml:"multipleFieldFilters,attr"` + ChartFormat int `xml:"chartFormat,attr"` + RowHeaderCaption string `xml:"rowHeaderCaption,attr"` + ColHeaderCaption string `xml:"colHeaderCaption,attr"` + FieldListSortAscending bool `xml:"fieldListSortAscending,attr"` + MdxSubqueries bool `xml:"mdxSubqueries,attr"` + CustomListSort bool `xml:"customListSort,attr"` + Location *xlsxLocation `xml:"location"` + PivotFields *xlsxPivotFields `xml:"pivotFields"` + RowFields *xlsxRowFields `xml:"rowFields"` + RowItems *xlsxRowItems `xml:"rowItems"` + ColFields *xlsxColFields `xml:"colFields"` + ColItems *xlsxColItems `xml:"colItems"` + PageFields *xlsxPageFields `xml:"pageFields"` + DataFields *xlsxDataFields `xml:"dataFields"` + ConditionalFormats *xlsxConditionalFormats `xml:"conditionalFormats"` + PivotTableStyleInfo *xlsxPivotTableStyleInfo `xml:"pivotTableStyleInfo"` +} + +// xlsxLocation represents location information for the PivotTable. +type xlsxLocation struct { + Ref string `xml:"ref,attr"` + FirstHeaderRow int `xml:"firstHeaderRow,attr"` + FirstDataRow int `xml:"firstDataRow,attr"` + FirstDataCol int `xml:"firstDataCol,attr"` + RowPageCount int `xml:"rowPageCount,attr"` + ColPageCount int `xml:"colPageCount,attr"` +} + +// xlsxPivotFields represents the collection of fields that appear on the +// PivotTable. +type xlsxPivotFields struct { + Count int `xml:"count,attr"` + PivotField []*xlsxPivotField `xml:"pivotField"` +} + +// xlsxPivotField represents a single field in the PivotTable. This element +// contains information about the field, including the collection of items in +// the field. +type xlsxPivotField struct { + Name string `xml:"name,attr"` + Axis string `xml:"axis,attr,omitempty"` + DataField bool `xml:"dataField,attr"` + SubtotalCaption string `xml:"subtotalCaption,attr"` + ShowDropDowns bool `xml:"showDropDowns,attr"` + HiddenLevel bool `xml:"hiddenLevel,attr"` + UniqueMemberProperty string `xml:"uniqueMemberProperty,attr"` + Compact bool `xml:"compact,attr"` + AllDrilled bool `xml:"allDrilled,attr"` + NumFmtId string `xml:"numFmtId,attr,omitempty"` + Outline bool `xml:"outline,attr"` + SubtotalTop bool `xml:"subtotalTop,attr"` + DragToRow bool `xml:"dragToRow,attr"` + DragToCol bool `xml:"dragToCol,attr"` + MultipleItemSelectionAllowed bool `xml:"multipleItemSelectionAllowed,attr"` + DragToPage bool `xml:"dragToPage,attr"` + DragToData bool `xml:"dragToData,attr"` + DragOff bool `xml:"dragOff,attr"` + ShowAll bool `xml:"showAll,attr"` + InsertBlankRow bool `xml:"insertBlankRow,attr"` + ServerField bool `xml:"serverField,attr"` + InsertPageBreak bool `xml:"insertPageBreak,attr"` + AutoShow bool `xml:"autoShow,attr"` + TopAutoShow bool `xml:"topAutoShow,attr"` + HideNewItems bool `xml:"hideNewItems,attr"` + MeasureFilter bool `xml:"measureFilter,attr"` + IncludeNewItemsInFilter bool `xml:"includeNewItemsInFilter,attr"` + ItemPageCount int `xml:"itemPageCount,attr"` + SortType string `xml:"sortType,attr"` + DataSourceSort bool `xml:"dataSourceSort,attr,omitempty"` + NonAutoSortDefault bool `xml:"nonAutoSortDefault,attr"` + RankBy int `xml:"rankBy,attr,omitempty"` + DefaultSubtotal bool `xml:"defaultSubtotal,attr"` + SumSubtotal bool `xml:"sumSubtotal,attr"` + CountASubtotal bool `xml:"countASubtotal,attr"` + AvgSubtotal bool `xml:"avgSubtotal,attr"` + MaxSubtotal bool `xml:"maxSubtotal,attr"` + MinSubtotal bool `xml:"minSubtotal,attr"` + ProductSubtotal bool `xml:"productSubtotal,attr"` + CountSubtotal bool `xml:"countSubtotal,attr"` + StdDevSubtotal bool `xml:"stdDevSubtotal,attr"` + StdDevPSubtotal bool `xml:"stdDevPSubtotal,attr"` + VarSubtotal bool `xml:"varSubtotal,attr"` + VarPSubtotal bool `xml:"varPSubtotal,attr"` + ShowPropCell bool `xml:"showPropCell,attr,omitempty"` + ShowPropTip bool `xml:"showPropTip,attr,omitempty"` + ShowPropAsCaption bool `xml:"showPropAsCaption,attr,omitempty"` + DefaultAttributeDrillState bool `xml:"defaultAttributeDrillState,attr,omitempty"` + Items *xlsxItems `xml:"items"` + AutoSortScope *xlsxAutoSortScope `xml:"autoSortScope"` + ExtLst *xlsxExtLst `xml:"extLst"` +} + +// xlsxItems represents the collection of items in a PivotTable field. The +// items in the collection are ordered by index. Items represent the unique +// entries from the field in the source data. +type xlsxItems struct { + Count int `xml:"count,attr"` + Item []*xlsxItem `xml:"item"` +} + +// xlsxItem represents a single item in PivotTable field. +type xlsxItem struct { + N string `xml:"n,attr"` + T string `xml:"t,attr"` + H bool `xml:"h,attr"` + S bool `xml:"s,attr"` + SD bool `xml:"sd,attr"` + F bool `xml:"f,attr"` + M bool `xml:"m,attr"` + C bool `xml:"c,attr"` + X int `xml:"x,attr,omitempty"` + D bool `xml:"d,attr"` + E bool `xml:"e,attr"` +} + +// xlsxAutoSortScope represents the sorting scope for the PivotTable. +type xlsxAutoSortScope struct { +} + +// xlsxRowFields represents the collection of row fields for the PivotTable. +type xlsxRowFields struct { + Count int `xml:"count,attr"` + Fields []*xlsxField `xml:"fields"` +} + +// xlsxField represents a generic field that can appear either on the column +// or the row region of the PivotTable. There areas many elements as there +// are item values in any particular column or row. +type xlsxField struct { + X int `xml:"x,attr"` +} + +// xlsxRowItems represents the collection of items in row axis of the +// PivotTable. +type xlsxRowItems struct { + Count int `xml:"count,attr"` + I []*xlsxI `xml:"i"` +} + +// xlsxI represents the collection of items in the row region of the +// PivotTable. +type xlsxI struct { + X []*xlsxX `xml:"x"` +} + +// xlsxX represents an array of indexes to cached shared item values. +type xlsxX struct { + XMLName xml.Name `xml:"x"` +} + +// xlsxColFields represents the collection of fields that are on the column +// axis of the PivotTable. +type xlsxColFields struct { + Count int `xml:"count,attr"` + Fields []*xlsxField `xml:"fields"` +} + +// xlsxColItems represents the collection of column items of the PivotTable. +type xlsxColItems struct { + Count int `xml:"count,attr"` + I []*xlsxI `xml:"i"` +} + +// xlsxPageFields represents the collection of items in the page or report +// filter region of the PivotTable. +type xlsxPageFields struct { + Count int `xml:"count,attr"` + PageField []*xlsxPageField `xml:"pageField"` +} + +// xlsxPageField represents a field on the page or report filter of the +// PivotTable. +type xlsxPageField struct { + Fld int `xml:"fld,attr"` + Item int `xml:"item,attr,omitempty"` + Hier int `xml:"hier,attr"` + Name string `xml:"name,attr"` + Cap string `xml:"cap,attr"` + ExtLst *xlsxExtLst `xml:"extLst"` +} + +// xlsxDataFields represents the collection of items in the data region of the +// PivotTable. +type xlsxDataFields struct { + Count int `xml:"count,attr"` + DataField *xlsxDataField `xml:"dataField"` +} + +// xlsxDataField represents a field from a source list, table, or database +// that contains data that is summarized in a PivotTable. +type xlsxDataField struct { + Name string `xml:"name,attr,omitempty"` + Fld int `xml:"fld,attr"` + Subtotal string `xml:"subtotal,attr"` + ShowDataAs string `xml:"showDataAs,attr"` + BaseField int `xml:"baseField,attr"` + BaseItem int64 `xml:"baseItem,attr"` + NumFmtId string `xml:"numFmtId,attr,omitempty"` + ExtLst *xlsxExtLst `xml:"extLst"` +} + +// xlsxConditionalFormats represents the collection of conditional formats +// applied to a PivotTable. +type xlsxConditionalFormats struct { +} + +// xlsxPivotTableStyleInfo represent information on style applied to the +// PivotTable. +type xlsxPivotTableStyleInfo struct { + Name string `xml:"name,attr"` + ShowRowHeaders bool `xml:"showRowHeaders,attr"` + ShowColHeaders bool `xml:"showColHeaders,attr"` + ShowRowStripes bool `xml:"showRowStripes,attr"` + ShowColStripes bool `xml:"showColStripes,attr"` + ShowLastColumn bool `xml:"showLastColumn,attr,omitempty"` +} From acbabcf8fcf2e27dc466ca17185eb6a9e7397356 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 8 Sep 2019 21:57:55 +0800 Subject: [PATCH 145/957] Add pivot table cache definition struct --- xmlPivotCache.go | 196 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100644 xmlPivotCache.go diff --git a/xmlPivotCache.go b/xmlPivotCache.go new file mode 100644 index 0000000000..9e07931e95 --- /dev/null +++ b/xmlPivotCache.go @@ -0,0 +1,196 @@ +package excelize + +import "encoding/xml" + +// pivotCacheDefinition represents the pivotCacheDefinition part. This part +// defines each field in the source data, including the name, the string +// resources of the instance data (for shared items), and information about +// the type of data that appears in the field. +type xmlPivotCacheDefinition struct { + XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main pivotCacheDefinition"` + RID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr,omitempty"` + Invalid bool `xml:"invalid,attr,omitempty"` + SaveData bool `xml:"saveData,attr,omitempty"` + RefreshOnLoad bool `xml:"refreshOnLoad,attr,omitempty"` + OptimizeMemory bool `xml:"optimizeMemory,attr,omitempty"` + EnableRefresh bool `xml:"enableRefresh,attr,omitempty"` + RefreshedBy string `xml:"refreshedBy,attr,omitempty"` + RefreshedDate float64 `xml:"refreshedDate,attr,omitempty"` + RefreshedDateIso float64 `xml:"refreshedDateIso,attr,omitempty"` + BackgroundQuery bool `xml:"backgroundQuery,attr"` + MissingItemsLimit int `xml:"missingItemsLimit,attr,omitempty"` + CreatedVersion int `xml:"createdVersion,attr,omitempty"` + RefreshedVersion int `xml:"refreshedVersion,attr,omitempty"` + MinRefreshableVersion int `xml:"minRefreshableVersion,attr,omitempty"` + RecordCount int `xml:"recordCount,attr,omitempty"` + UpgradeOnRefresh bool `xml:"upgradeOnRefresh,attr,omitempty"` + TupleCacheAttr bool `xml:"tupleCache,attr,omitempty"` + SupportSubquery bool `xml:"supportSubquery,attr,omitempty"` + SupportAdvancedDrill bool `xml:"supportAdvancedDrill,attr,omitempty"` + CacheSource *xlsxCacheSource `xml:"cacheSource"` + CacheFields *xlsxCacheFields `xml:"cacheFields"` + CacheHierarchies *xlsxCacheHierarchies `xml:"cacheHierarchies"` + Kpis *xlsxKpis `xml:"kpis"` + TupleCache *xlsxTupleCache `xml:"tupleCache"` + CalculatedItems *xlsxCalculatedItems `xml:"calculatedItems"` + CalculatedMembers *xlsxCalculatedMembers `xml:"calculatedMembers"` + Dimensions *xlsxDimensions `xml:"dimensions"` + MeasureGroups *xlsxMeasureGroups `xml:"measureGroups"` + Maps *xlsxMaps `xml:"maps"` + ExtLst *xlsxExtLst `xml:"extLst"` +} + +// xlsxCacheSource represents the description of data source whose data is +// stored in the pivot cache. The data source refers to the underlying rows or +// database records that provide the data for a PivotTable. You can create a +// PivotTable report from a SpreadsheetML table, an external database +// (including OLAP cubes), multiple SpreadsheetML worksheets, or another +// PivotTable. +type xlsxCacheSource struct { +} + +// xlsxCacheFields represents the collection of field definitions in the +// source data. +type xlsxCacheFields struct { + Count int `xml:"count,attr"` + CacheField []*xlsxCacheField `xml:"cacheField"` +} + +// xlsxCacheField represent a single field in the PivotCache. This definition +// contains information about the field, such as its source, data type, and +// location within a level or hierarchy. The sharedItems element stores +// additional information about the data in this field. If there are no shared +// items, then values are stored directly in the pivotCacheRecords part. +type xlsxCacheField struct { + Name string `xml:"name,attr"` + Caption string `xml:"caption,attr,omitempty"` + PropertyName string `xml:"propertyName,attr,omitempty"` + ServerField bool `xml:"serverField,attr,omitempty"` + UniqueList bool `xml:"uniqueList,attr,omitempty"` + NumFmtId string `xml:"numFmtId,attr,omitempty"` + Formula string `xml:"formula,attr,omitempty"` + SQLType int `xml:"sqlType,attr,omitempty"` + Hierarchy int `xml:"hierarchy,attr,omitempty"` + Level int `xml:"level,attr,omitempty"` + DatabaseField bool `xml:"databaseField,attr"` + MappingCount int `xml:"mappingCount,attr,omitempty"` + MemberPropertyField bool `xml:"memberPropertyField,attr,omitempty"` + SharedItems *xlsxSharedItems `xml:"sharedItems"` + FieldGroup *xlsxFieldGroup `xml:"fieldGroup"` + MpMap *xlsxX `xml:"map"` + ExtLst *xlsxExtLst `xml:"exrLst"` +} + +// xlsxSharedItems represents the collection of unique items for a field in +// the PivotCacheDefinition. The sharedItems complex type stores data type and +// formatting information about the data in a field. Items in the +// PivotCacheDefinition can be shared in order to reduce the redundancy of +// those values that are referenced in multiple places across all the +// PivotTable parts. +type xlsxSharedItems struct { + ContainsSemiMixedTypes bool `xml:"containsSemiMixedTypes,attr,omitempty"` + ContainsNonDate bool `xml:"containsNonDate,attr,omitempty"` + ContainsDate bool `xml:"containsDate,attr,omitempty"` + ContainsString bool `xml:"containsString,attr,omitempty"` + ContainsBlank bool `xml:"containsBlank,attr,omitempty"` + ContainsMixedTypes bool `xml:"containsMixedTypes,attr,omitempty"` + ContainsNumber bool `xml:"containsNumber,attr,omitempty"` + ContainsInteger bool `xml:"containsInteger,attr,omitempty"` + MinValue float64 `xml:"minValue,attr,omitempty"` + MaxValue float64 `xml:"maxValue,attr,omitempty"` + MinDate string `xml:"minDate,attr,omitempty"` + MaxDate string `xml:"maxDate,attr,omitempty"` + Count int `xml:"count,attr,omitempty"` + LongText bool `xml:"longText,attr,omitempty"` + M *xlsxMissing `xml:"m"` + N *xlsxNumber `xml:"n"` + B *xlsxBoolean `xml:"b"` + E *xlsxError `xml:"e"` + S *xlsxString `xml:"s"` + D *xlsxDateTime `xml:"d"` +} + +// xlsxMissing represents a value that was not specified. +type xlsxMissing struct { +} + +// xlsxNumber represents a numeric value in the PivotTable. +type xlsxNumber struct { + V float64 `xml:"v,attr"` + U bool `xml:"u,attr,omitempty"` + F bool `xml:"f,attr,omitempty"` + C string `xml:"c,attr,omitempty"` + Cp int `xml:"cp,attr,omitempty"` + In int `xml:"in,attr,omitempty"` + Bc string `xml:"bc,attr,omitempty"` + Fc string `xml:"fc,attr,omitempty"` + I bool `xml:"i,attr,omitempty"` + Un bool `xml:"un,attr,omitempty"` + St bool `xml:"st,attr,omitempty"` + B bool `xml:"b,attr,omitempty"` + Tpls *xlsxTuples `xml:"tpls"` + X *attrValInt `xml:"x"` +} + +// xlsxTuples represents members for the OLAP sheet data entry, also known as +// a tuple. +type xlsxTuples struct { +} + +// xlsxBoolean represents a boolean value for an item in the PivotTable. +type xlsxBoolean struct { +} + +// xlsxError represents an error value. The use of this item indicates that an +// error value is present in the PivotTable source. The error is recorded in +// the value attribute. +type xlsxError struct { +} + +// xlsxString represents a character value in a PivotTable. +type xlsxString struct { +} + +// xlsxDateTime represents a date-time value in the PivotTable. +type xlsxDateTime struct { +} + +// xlsxFieldGroup represents the collection of properties for a field group. +type xlsxFieldGroup struct { +} + +// xlsxCacheHierarchies represents the collection of OLAP hierarchies in the +// PivotCache. +type xlsxCacheHierarchies struct { +} + +// xlsxKpis represents the collection of Key Performance Indicators (KPIs) +// defined on the OLAP server and stored in the PivotCache. +type xlsxKpis struct { +} + +// xlsxTupleCache represents the cache of OLAP sheet data members, or tuples. +type xlsxTupleCache struct { +} + +// xlsxCalculatedItems represents the collection of calculated items. +type xlsxCalculatedItems struct { +} + +// xlsxCalculatedMembers represents the collection of calculated members in an +// OLAP PivotTable. +type xlsxCalculatedMembers struct { +} + +// xlsxDimensions represents the collection of PivotTable OLAP dimensions. +type xlsxDimensions struct { +} + +// xlsxMeasureGroups represents the collection of PivotTable OLAP measure +// groups. +type xlsxMeasureGroups struct { +} + +// xlsxMaps represents the PivotTable OLAP measure group - Dimension maps. +type xlsxMaps struct { +} From 3c327413d963f6ffa934a72beb483d1fedd25660 Mon Sep 17 00:00:00 2001 From: Ben Wells Date: Fri, 13 Sep 2019 11:38:44 +0100 Subject: [PATCH 146/957] Fix dependency on github.com/360EntSecGroup-Skylar/excelize v1 --- go.mod | 2 ++ go.sum | 5 +++++ sheet_test.go | 2 +- sheetpr_test.go | 2 +- sheetview_test.go | 2 +- 5 files changed, 10 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 9f36b59039..892f3066da 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,8 @@ module github.com/360EntSecGroup-Skylar/excelize/v2 go 1.12 require ( + github.com/davecgh/go-spew v1.1.1 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 github.com/stretchr/testify v1.3.0 + golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a ) diff --git a/go.sum b/go.sum index 890277c721..2d29d33a51 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ 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= @@ -7,3 +9,6 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a h1:gHevYm0pO4QUbwy8Dmdr01R5r1BuKtfYqRqF0h/Cbh0= +golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/sheet_test.go b/sheet_test.go index 145e302b1e..51797935b0 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -6,7 +6,7 @@ import ( "strings" "testing" - "github.com/360EntSecGroup-Skylar/excelize" + "github.com/360EntSecGroup-Skylar/excelize/v2" "github.com/mohae/deepcopy" "github.com/stretchr/testify/assert" diff --git a/sheetpr_test.go b/sheetpr_test.go index b9f9e3be38..97a314c918 100644 --- a/sheetpr_test.go +++ b/sheetpr_test.go @@ -7,7 +7,7 @@ import ( "github.com/mohae/deepcopy" "github.com/stretchr/testify/assert" - "github.com/360EntSecGroup-Skylar/excelize" + "github.com/360EntSecGroup-Skylar/excelize/v2" ) var _ = []excelize.SheetPrOption{ diff --git a/sheetview_test.go b/sheetview_test.go index 07f59ed337..2e697b8540 100644 --- a/sheetview_test.go +++ b/sheetview_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/assert" - "github.com/360EntSecGroup-Skylar/excelize" + "github.com/360EntSecGroup-Skylar/excelize/v2" ) var _ = []excelize.SheetViewOption{ From 8922f659788187afa6d0a5d3248e999c2c1bb846 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 16 Sep 2019 01:17:35 +0800 Subject: [PATCH 147/957] Combine functions: workBookRelsWriter, drawingRelsWriter into relsWriter; drawingRelsReader, workbookRelsReader, workSheetRelsReader into relsReader; addDrawingRelationships, addSheetRelationships into addRels --- README.md | 2 +- README_zh.md | 2 +- cell.go | 4 +- chart.go | 7 ++- comment.go | 8 ++- excelize.go | 33 ++++++++--- excelize.png | Bin 27196 -> 0 bytes excelize.svg | 1 + file.go | 9 +-- picture.go | 136 +++++++++--------------------------------- shape.go | 4 +- sheet.go | 85 +++++++-------------------- table.go | 4 +- xmlDrawing.go | 2 + xmlPivotCache.go | 4 +- xmlPivotTable.go | 150 ++++++++++++++++++++++++----------------------- xmlWorkbook.go | 12 ++-- 17 files changed, 190 insertions(+), 273 deletions(-) delete mode 100644 excelize.png create mode 100644 excelize.svg diff --git a/README.md b/README.md index 7f9cf7039e..998a4c1e88 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -

Excelize logo

+

Excelize logo

Build Status diff --git a/README_zh.md b/README_zh.md index 6c2b190a12..d4cac666b9 100644 --- a/README_zh.md +++ b/README_zh.md @@ -1,4 +1,4 @@ -

Excelize logo

+

Excelize logo

Build Status diff --git a/cell.go b/cell.go index e897379447..1da46aa347 100644 --- a/cell.go +++ b/cell.go @@ -378,7 +378,9 @@ func (f *File) SetCellHyperLink(sheet, axis, link, linkType string) error { linkData = xlsxHyperlink{ Ref: axis, } - rID := f.addSheetRelationships(sheet, SourceRelationshipHyperLink, link, linkType) + 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) case "Location": linkData = xlsxHyperlink{ diff --git a/chart.go b/chart.go index e1eb81f1f9..db2df1e00f 100644 --- a/chart.go +++ b/chart.go @@ -727,7 +727,8 @@ func (f *File) AddChart(sheet, cell, format string) error { chartID := f.countCharts() + 1 drawingXML := "xl/drawings/drawing" + strconv.Itoa(drawingID) + ".xml" drawingID, drawingXML = f.prepareDrawing(xlsx, drawingID, sheet, drawingXML) - drawingRID := f.addDrawingRelationships(drawingID, SourceRelationshipChart, "../charts/chart"+strconv.Itoa(chartID)+".xml", "") + 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) if err != nil { return err @@ -761,7 +762,9 @@ func (f *File) prepareDrawing(xlsx *xlsxWorksheet, drawingID int, sheet, drawing drawingXML = strings.Replace(sheetRelationshipsDrawingXML, "..", "xl", -1) } else { // Add first picture for given sheet. - rID := f.addSheetRelationships(sheet, SourceRelationshipDrawingML, sheetRelationshipsDrawingXML, "") + sheetPath, _ := f.sheetMap[trimSheetName(sheet)] + sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetPath, "xl/worksheets/") + ".rels" + rID := f.addRels(sheetRels, SourceRelationshipDrawingML, sheetRelationshipsDrawingXML, "") f.addSheetDrawing(sheet, rID) } return drawingID, drawingXML diff --git a/comment.go b/comment.go index 97e0e9bb3b..7f3b10dbac 100644 --- a/comment.go +++ b/comment.go @@ -60,7 +60,7 @@ func (f *File) GetComments() (comments map[string][]Comment) { // given worksheet index. func (f *File) getSheetComments(sheetID int) string { var rels = "xl/worksheets/_rels/sheet" + strconv.Itoa(sheetID) + ".xml.rels" - if sheetRels := f.workSheetRelsReader(rels); sheetRels != nil { + if sheetRels := f.relsReader(rels); sheetRels != nil { for _, v := range sheetRels.Relationships { if v.Type == SourceRelationshipComments { return v.Target @@ -98,8 +98,10 @@ func (f *File) AddComment(sheet, cell, format string) error { drawingVML = strings.Replace(sheetRelationshipsDrawingVML, "..", "xl", -1) } else { // Add first comment for given sheet. - rID := f.addSheetRelationships(sheet, SourceRelationshipDrawingVML, sheetRelationshipsDrawingVML, "") - f.addSheetRelationships(sheet, SourceRelationshipComments, sheetRelationshipsComments, "") + sheetPath, _ := f.sheetMap[trimSheetName(sheet)] + sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetPath, "xl/worksheets/") + ".rels" + rID := f.addRels(sheetRels, SourceRelationshipDrawingVML, sheetRelationshipsDrawingVML, "") + f.addRels(sheetRels, SourceRelationshipComments, sheetRelationshipsComments, "") f.addSheetLegacyDrawing(sheet, rID) } commentsXML := "xl/comments" + strconv.Itoa(commentID) + ".xml" diff --git a/excelize.go b/excelize.go index b734e57b99..4d46b94e9b 100644 --- a/excelize.go +++ b/excelize.go @@ -31,7 +31,6 @@ type File struct { CalcChain *xlsxCalcChain Comments map[string]*xlsxComments ContentTypes *xlsxTypes - DrawingRels map[string]*xlsxWorkbookRels Drawings map[string]*xlsxWsDr Path string SharedStrings *xlsxSST @@ -42,8 +41,7 @@ type File struct { DecodeVMLDrawing map[string]*decodeVmlDrawing VMLDrawing map[string]*vmlDrawing WorkBook *xlsxWorkbook - WorkBookRels *xlsxWorkbookRels - WorkSheetRels map[string]*xlsxWorkbookRels + Relationships map[string]*xlsxRelationships XLSX map[string][]byte } @@ -93,13 +91,12 @@ func OpenReader(r io.Reader) (*File, error) { f := &File{ checked: make(map[string]bool), Comments: make(map[string]*xlsxComments), - DrawingRels: make(map[string]*xlsxWorkbookRels), Drawings: make(map[string]*xlsxWsDr), Sheet: make(map[string]*xlsxWorksheet), SheetCount: sheetCount, DecodeVMLDrawing: make(map[string]*decodeVmlDrawing), VMLDrawing: make(map[string]*vmlDrawing), - WorkSheetRels: make(map[string]*xlsxWorkbookRels), + Relationships: make(map[string]*xlsxRelationships), XLSX: file, } f.CalcChain = f.calcChainReader() @@ -176,6 +173,28 @@ func checkSheet(xlsx *xlsxWorksheet) { xlsx.SheetData = sheetData } +// 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 { + rels := f.relsReader(relPath) + rID := 0 + if rels == nil { + rels = &xlsxRelationships{} + } + rID = len(rels.Relationships) + 1 + var ID bytes.Buffer + ID.WriteString("rId") + ID.WriteString(strconv.Itoa(rID)) + rels.Relationships = append(rels.Relationships, xlsxRelationship{ + ID: ID.String(), + Type: relType, + Target: target, + TargetMode: targetMode, + }) + f.Relationships[relPath] = rels + return rID +} + // replaceWorkSheetsRelationshipsNameSpaceBytes provides a function to replace // xl/worksheets/sheet%d.xml XML tags to self-closing for compatible Microsoft // Office Excel 2007. @@ -265,7 +284,7 @@ func (f *File) AddVBAProject(bin string) error { return errors.New("unsupported VBA project extension") } f.setContentTypePartVBAProjectExtensions() - wb := f.workbookRelsReader() + wb := f.relsReader("xl/_rels/workbook.xml.rels") var rID int var ok bool for _, rel := range wb.Relationships { @@ -280,7 +299,7 @@ func (f *File) AddVBAProject(bin string) error { } rID++ if !ok { - wb.Relationships = append(wb.Relationships, xlsxWorkbookRelation{ + wb.Relationships = append(wb.Relationships, xlsxRelationship{ ID: "rId" + strconv.Itoa(rID), Target: "vbaProject.bin", Type: SourceRelationshipVBAProject, diff --git a/excelize.png b/excelize.png deleted file mode 100644 index 8ba520e91b975fc240f37cda9a381ced765bc954..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 27196 zcmb4qbx@o^^Cs>Vf=eK{6I=rc8r*$x*IgWfLxAAH9YXNnzPP&u*M-G>aS3|l`>yVH z|J)z9wY636PR-N(_Dnz1Gt(WRsw|6*L52Yb2Zt^9MM@nG4iN+g2XBLh`1;R#jssyh zID}hOMNR3~N4PHpaO&i60D3evCNxcEbY&)VeKrhjRt#fK3~O!-dp-lXV_s`i02LzlG1hK!ViOrDL*S)8oal3bIUywaMy-j;%2fI|Ee zMUy>6s}rT10;M)DWtYo4J^pw7?o}Gf z*Xl)2Pe)$?qHlDkUkGDR;b*WAX8?OLwEHmX3Nz+8Ft&O#_60C~;$^atVCwW`>h@>W z&|)48W*!Y?=?i2X4q=@LXEQcqbCPA7j$~hqVP8++bO3&kmge>e<{r3IiPPSNl$|rHL&vQSd;}WIQb7g`IzbGnvF*cI_S*R3csZ?C1Tv@9zJg=## zp`|3PRo|-H(reH&VmLfw3eYnh95j3l<$e9) zGqoD{{1WQt7haR~Jv=;eZaps4A}KaDB_$=TxioEQGcO|}KR>?!vQwDmR@hlrUR+!S z-LI*xuAl5{C=F}^H-Y=xTRK`>wh!Ct!rO+r+hzva+uAxC8akK8`n$V_hK5ECVH3l{ zldCJ!lM}O>8*{U>^L;4`rx%MmJ1YnKkfo*dlamc7bo1hJd#+&b{OaK5_UQ2N^zXyP z>FLc>-p%#(-Q&~W*@FAOf1e&7pEoL=Z@ZtLo}OP`USQzY9}we|3mlv^oSf8WO|RwS zcArnPTF)=F+D_Af{&y2Y(Eo1Y zMbG-bn*P(}U(E3F8^O>|D(YFAKL#>;Gf$6N4NhMnlKhalpXoNsR|hfoCo4Beq7JG-J!@1BB|M=nB)=jL*STyx&>&uX76X>^DI z>z=nz{))-J!a^o^a~uQ#I=4&Rzds(iPESt{h^MC~zkbEUKH4wUM(+nwA`3m(b)2>2 zfaX9ZbsSvgj{g2mRx-;uetg#32Fywb3q&FiU1J}U^_<|WtgEqAKKokZwOZTWfnSl_ zk_Sdd{99|aj+y+sd%H!lM;;Ug$UW7glVP zso);#eH7#3qGk2$otN-gT8mWI2`!s7aZVwjaBA%~(mrHyO(p2aYUiL8uDTzS)9ib) z-CA&Db#9@cr}s$hJy}jj2LIJa7l~&GvL7eIr4m-HG|tS%*57dvqI7DSapEFgiNuo0 zG~>#Q8kd&tmqsaE1egi50wUcd=MS?elNmog%y2g~8y50sF8xY#q_>hy_fzngAijVk zF$A@KaLm;v+K!$C)QxM_cvg*WxGY6QMmlICo782neacee>L&Ef*JVrD#Q#~@(_jg? zs~`87b~&NH!H?>9sL^#ilXQ*9A4_k3X8r5&ZwxFLgBq_}`p)&pj|nYf@fEnx(wm3O zG`phD`puYYiB{O8Q9^3^tUeHAZV(lI;S$b@oyK08nj*gyhQbq-A$Nb{w5n@1)6wp> zzIb}oZ-$J4G?PqhP3I?hr^9#=4Y8Cd%1`8GHAU?xGhThgFL)*v{Aq;Yqo$sx?n?@o z-JbPK{AW}rmb*#^Q}(MI;oU_f8+o6Y(DIsx8XT8)6ECon8Gzd8?0i7*ss(&g{O>o|Va@&h{FOz4NQ{ zy;IpL-l^Jka&0{!QmFa$^PQ~(zotO`TqI2)LFNg)eZD7!6W5;0J*MAoQ9C^v+lAI9 zzx5A|<{8VMY{gRz2g4z?oeKKJsIaqVow_EipDjV_Ojk-V5Z$G5GqxVqRuKU*#2Qz2 z`nyR!%}Ne7b3!0;U9v$uxG!6|PKk>w+3X=@AMusvvnjw~AtJT>OQRGhJW@8=@fm#< z=+1)@f~;GUMvWX_*F!fy0FpI+Xl-k<9iLmZx|*APa;qE#L=z$#I1dj5#_breRJbDE z&UNUxHtB%JP@=|BaOmBgOq0od$IvNy78e zvLX-^HX>bz{z^bLTc6xfTUQVr-@BpFHMU5y&%N3!`mH;7t%I4#FES<6DD;m zlrl4p+V6_=SryuE#?lqA(uvs%=7=p$*!d08B<=W9PmEG(8pb}Iuz1)Hh>`Zl9rbbS zSRc(8djz*J^VfCj9=fywoSJ%J9)v4n(j=4djJ7l}8V)J=d&~7vE#Hc9MVrSR-x{)5 zwc4ATX-k*FKQKd%e(kgXZL>+hajpYEHxV|Jaw`!Chflow!;Rre6L4k6Lx{4!KdF{C zGBRs?cQs``OgaARQ%0>iBE9}uP~pw$KD#AK-S{yZr7`uos%0Q5k8Bk=EzGA65>;FMkPNt~a zGAvb>T$S}779qGu#htX3^>}xaM$%ULVBmO8=*V;b_ISk;`pNtA#9{fdFCzcF26m>U z>ajc#%WQKLTaB%7n}6|&VjPm}tPLCUC{?b(22<%`lCHmgN8YqBHVAhLz;eohXp`$t4r@57CQvT{;W&2I$F8Hk4cVb1)u=6x&enH?{c3GgJyCV(6j25 z;VC3bpJy@L!CJ0rKRDg5O`{vkbVWadJ{znG4-%LPb1FT>N64eCLG7dz6jy~xV6_fmwg_|f{8q^=3q{is!5(G9Mu#9_iiujtR%)^kcI3F}hJrl5;KOje3;XoOIwJGGX=Mq8z=%+_te&rXzh?7#(DdBUKD^ zhW?Q2ci>6YERR~#Q?qGE0$=9idg&v>#;osCS-WoQNxKUHE53PM>B|)QrY#-%B93{V zbNB1ZF0Y1;VM-C1g|DPQ(UmQf)>vN9qoz0>V^wEwpg+I1Nz=c-n%HlI8|~*P)G_V)McPO^ zwA+f6!-X{Ed8J)9yg=v55W-gbnwaJ#s(LAqXz9m<;xueVXIK4FTPUykm&`1apM zRZoqmZ-Z7UJRT~`v4~r~TkWM}sjO_rJ57L+(l+1=D^qi4VFS)#h{T4-My#1<7~)T_ zuDS7nm=zDa0U~%i&R@eMT{yufRC~u>(TjMirOY22$;K^Wc*M=aGf*svWX+^a{d+k{ z=hQZ&Z|-zx5r$$m&hM!5Im?OOgq-r?6z%xNhVq6QzBn zH%ba{$d&+%U6ZYAW!Z-RtFJvNg3jTHq49bOqs?lQgGgpJU~@Ui=)fH_QyFGHNs^sP zdx%iXItXN8<&Ts*km;AFWYmURGo6H}asI<22lG7Odx}IU?wZ{a$;f`v)+=4IVrJp9 zII%Ivc{Z^!Wp>2327wn2f466{@Xu8c>&bUmcMbcc8;l-H^Ow`=Mmzj@H<_s$wQ8^s z!17jOe4>sC zgTLy+K9&Uu)Okt>+ZS2X5n*xI-}viACeGIcm9cs?{~EH>eBG*&l8?$~3BH|#@>Wz_ zjgxIAjoeq+9c6^NzYZLLVLn`RHI572q-q%?H!G@nlfCsYj3OsmrL z{PT32@z&D8Gm|iDv^))a;$+kXhd**#R%2^;K$6eBIDV??s-?V#+w~2M#zgbhxgMjU zw*sZnLVMU#Q$JiAT;)>Bg2kW^-wGmSRo@Gl)QQG^yg42p4^#AS2eCv9i>m;(bC6vpG77%2@IQ{xg*sP!09 z&#K44UL_Vv$Ll4H%9;TDVixW!2a{xp71r|_>}^5$DA)3eKGn0Q7?)30bX^O1q>k#@ zEgEVzdfLlmbTsC5E9@Lh-t5FouI<*6N7_J`*GJ+{4?d$4-_1jr3=(NY(hA)jLPtkg z+)2v^@Qd9kVenZsGci20N(1;hl`3(adsqGqJlrO^GT(8;nrq5QehGxT?+vVuMO)f? z!XVk%$)zi5hQ`JQmrHpWPY*Y38QI3V+F@;ace|kNFOk5d5y854hu*spSC>D*^*C67 z5V8E~F;y&T!BqopgkN>t`F%?}R{iak>5T!MrT*>2f=Z#4YU2K>D<)Odd)Bo|t%&&W z&9#R11I2q4{@k*}+2F4yVHKe`)^cj`p^cEw-n4y-FcF4Tq|q;&=t0}NiLW6cB?*`Z zbpY6{{lz@KdxO`3BzMOxjgYQqlTE*Df#{d`OoaA!+df5AonDi_QfPC5gSB>j>R_L* zj$WwGXv&Gf#ifALs`|c*hyt5TU5{nViS*Cv@tqQmF8&#=l;t{CiyU=*S(J%Oy)}L7 z(xqRzQDglu$Ac13f4%9a2%nj_NN*Voedv2xf z^;NxaGT4az)82B9_Z+9>NB1^%kC6az_3`HrMvk3gd~lS3-)ET(BZn7NEK39KCUDxG zb$xJDdQ~!bz|R-Z^NiR+!A4@z@=}+@*3|1Xak0ZrE$oLa<;d}euivUyKhumGqwP<6 zeMU8zHBG-p6K7a%D%i!77qHWL4he9IdV>5{SS#m$gsn8|8xC-!_L^`Jr&f$EqI8iu zhp3k|ldMGG`VeDLY#zzw=Ofq6h<099XV#nSADP7j z>8u{?WpoVbeJ62F{CNeRaXa$iElNp&Ex_l8C)CUt?#5$xB4RyuLA2~L5@{gVnx!Rq z+vN8eyqH{ zFZgPlLFX9khWo6!^sPF!STh_1-S~dJ7KX1puGHu)VUoTLUFr9TLR$h!&0Tq`8G2`7 z%72PReQqD9BAQ+v_l}ov-A2oucJs-E?6k-CsSEV;u8Xy`al@&dBsZUVUIhRKQE-m0 zhFdb8yhj^bqoYwJx*S|nb{U8$9AK{}vA!m1j9bOo7ga z;ahPUp!nk%Rh!s@%<@1j4VT$cNy(9mrg#FEHTM*|c3Ga8{iaqD^ zc@!htYDy!TZP7t_VV(PX=*z+#S{-KHMH)y5Of)ENSm@@axL6r+crz zpn;yeZrHkR5QP)BRk^D!27wpz??KwX;m`U7!3)(9NJ*;-p`jjVKh?NW!Id`~hK>(! z*Jss&j*jOmyZBP9iG{z(+R_~-nS~6St~c6B-rO-R5FdMA4_{qHm-87MTUMHd=)s3` zpF^kupk91URCr+Q`i|tLRBBVjWi}$m!oWl5mu$0=#rk`w&|mMQm+shiy?NQIsiB$h zJUAc}p&B&{H44JYUp-3pMr%pAowf#frLzZ2{i~5?tSdimhvUS4)*_}v7mxcT21pxzDVWA4;v|zi<^bjHp=dt zF`F2vf#&oRY8kD2XLHB#a?sZ!PD^W#==EI|nLJYm6O|P7x)lDxA%_nl6hyAh5fwv$ zJ~{X)9@ZXf0@l)FDD{3X!>z!!a~SHn@5}ft-jrXRkYAa`#%)7MHp7^8;aCAvH~OczFHd}urF%Q|SblC=;U72S|1QXdJ79siIsW`iM+=ck! z1|iOOksr+!VZvvhUxY7HG0zRE_D`e#NVh?H?dImZ>3QX^iRo&FY+b>n*xrU-5jUnVEB- zP6U^2^R&ZxAij90kuB_u?(zF6c1guhUIiOrkW7+5KepmVl#Db~yF+nhNFFt;w#PEF zrVc5%>2!T=$IrKxx@A7Zvb|SmsSxk@O^F@7p&HxTTycg{cz0`k?Hp2MKeW~ek_;Y2 zsJuR$GjQ}7(a7zbIrd(=KbneaJNp~AQFT7CSIluF?j;xHsow-S@f`EVSZ!9(0lKAo z>7bRx#v_@Ee@~8_Wl*X_G+}0O-zf$@qx3d-x=!Cv+<0A0@m*V zwz*=o137%7Wo$CGfZAfU>+cEY7ebg-rEY#}2CH)?9w^uNjOT71Sv(}ySX+FI6LWP> z#~W8ggh2@e-^8(U^5Ksvl{!C7Y+NjFZG?fgwp2b}0jO+AD$UQ zs|G_VH8r-XY=53RTtm3WvZ}^ zLxIF!{0sOc5+vaMfaw-mhU0ERWMry4o7?5=-mz(Q)=Fx_xZc=XHUO>G5J4=vbw)&~ zgt`qs8B6UbJt+0c^Cf$9CNd?J?=iT`@P_Mw6;$kLp_gry$znT3)tA+7GlWmBq!uE+ zro!Bqj(vk2fr?yf_AH@wk3x*(tiArNK&#A=A#tR(2R~mXN^hxMK*yLds5J?^JNeRa zrMn-D`?M3-`e=SLDNJz}{7{EsxNG*d4DrKLHycfitW`~xT;@_S)slnN!VQ*~T9vb` zKHnv?{j>|@h9=5G35Ol`=M|%lSXrpM?>Phat~1$^wSg5L zHZp?HT19t%sStWOYXNg3i}db}X}Y|8ZxAUTJGD7}u;pH}w4qxUg6wR4BYTuCepkw| zuC-}MZ1PJg;`?l$;QNmLrZZkW^gmi@IAS|O=&PZW6`s%jSNnU*fqPz=?=DX}J6xSn zPZ$jX@8_j!sdPYN{+O%9Wdg#H(gIwj3?587Ik|JKKBZsFYKAsfnWah@*zdPy1nW&@ zcR5f~P88cD(k&uS5`Je?c)~V2>(}S4>@J*VM!*l{CVpcoZFsA2nB$rZ6(k9bwO= z5;sxiWFy)}`$j_W>W$J`;16P`mtkzbZ78LMta! zqwyk5W6!Tr<&mw+&%Z|2XqfAvtS4I}t7+_zO`Z49{3^_zsADN<|8`SPQNg*UUsXFz z&nRGb_29|&?@G$fz5ptn6Nn(wJCR4}YlC`J#19!(+J(*W!0AQgXo4vD5u=GxA;zFB z52L;QsT8pS$AGb^D1t_RGH9&PYg{K})#=WxLR>&B90jH^#Z37^{p)RsZofz58fJ~H z23DbuK*5oHhwzUSJ^kHPFoSD_DX?^@wZ6;lS{v-mJZXRrZ4k^)=8UatZY-t}oXHDp zO*r2GHfV>bKh1R64Up+liGlk=FXZ+ zgj?;SyqKH(8bq82q?FrKNPHsyZ#Igceg=3qzV(R} zlIKN$U)V8`8gxmGB-yf4_r&M|Mn;YIqeMaIGLq0GX;tEE8chlY_4_fIwyS^JcbDR! z+O#yqREf>R2ioT>-n{8~ORP);fXP0}C-9!&u6i@aZJihipD(Z#4r(-iwJuewKtPM{ z{WP|Hjn>a1kYh#gG!z+{q~!O*vzKCfBJk7J|X`$Z{)1;npp$8y!@%g1~9= z;od7JGE;iRq31q5CIg;M=FK2!!(Z8scY(X>5oS%)qfC)& zGV!cjwH{oKb?8>C*s-;iM@Pa0pwos;4Za7q_VuYid&!=HxBt$JQhJ^E?LO3Al!t4D zWJx4gs}aXXip)G(blAhKpvSso5p~E>d@c5khcmVhpET8)J;yFzwdQa)Zof?B#06^* zPk0NcFl8N@bT!bMPDcdUk0~^zEKAL=Nwls&%S8cc6(y`ocLhj%_QNs*>I?Mt%lPkq z_8Y7hF&nP&Vrq>GS(=?8WJ8P0_U6xZ><=C;0!6CS&Ym!LG@fAxx?K&-|3EK~jk9WYHg%SSCD*c>2kpG7p@-mhAyP5 zxQkt#MP@wsT_b{W9!)S7WBO>$me+iV1I1`Ta)H2PcQ)F zZy9vHjy+?orXP-}w=T8Dkx%ku5R{c@uo`{jCacJ>6d;;Hu-c|=DG!zk9pKWH9*j(e zbbYz9%Z~`t*_3lWk$+j_B!H=}{oOhRSdbo|tDqvn+75>D>kH7hJhu)% zJUSA=00pi-5Ci5O=hsyl)-jDJyZ9P}Os7kOL;%&X>~mHR5EAAy?o!hfbRd6(OArmq zssl8lsZ5GXZ&q#eQMMyHEsz?)>s@u&X?x-n6AXR=S;p{s{5v5NU>Z|=JZygvuzGu+Wmu4lJ~{~es2YR z;R?R^-J4r1@rSKjsKdP(@(`CJ2FUVNAHgJP3RjeVHo27_~X zY2P6`^4Ue8=W4-}!P{y(nLJ%DaFN&OhRi9PTh8YVN+vd%0$K8?QSP-aC2#+`$t9|Q z$8*SjTZ`*64uHHi>C}@N%JJ%c%|T_82HN|Y3y?q0E7}-qf|wa4qrVO7*ry*H3I*47 zW7)B*&ytNkR-R={gu~$nk&6}F^!3VP{ z2&+C7ODa5O8wvRTnR?g&bIVQbrgWX9YE*T#rc(9f@s;?oJp^VMH9Twb5oV%Oe%EEF z61(dqZ?qYq&r!8MCkF9GGZx{BcEBbwtP{9jiT`>(1kLEZCbGm+Zo9^G%K!|UuiTHN zrhKrCb4YqY*!xT47*e)qQ%DIcms$H>UP;6kS>#{C5Zm-a7yVtnr=uXb?;!EPpBdGq z@sVW~Ti+ZxwP>+ehIMJ2%OuzsE(u87_hm}p#ZRE(CR!Zvb}f{pE1+H1c9AovweOo8 zBPz^oEn`}}j!A=D>9Ffff9qOU;BvENZ&*yKV`f{?lhJBDkNuNut}oKfFsio-(f)kE zU0lH9R_+U)PB{4^2KX(p)5g~qYh(5^9yrek^h9NxtYUn|oU$#Nm~!L3FOtWzgrGCX zy!%qq+V_Dx2uaN_=(93p>BF_6=D<$FfPuI`pW7^zsIWR6f#mc#=OG7NIO&4Vvurz!si#@O9>+<{UNV{74#$h7=FRl< z^f5{q!Y(Y!L-AfY!Y;xOR@*}dGqTFL!e56&a7DW&a7EAlAmVAyMzHmJzc)a`&AI;JyhQf^Nkjev1=-X5#sT zI}vcF5wi|=>us;08gP@&AkGI$&G~RJ+hxiPGS<$UaoF0iqop#oHan*{C0)9|c)>zm z>@uK;EY9;ob~zSBCIpheg|`TJELN8)J`^#;NH2eS7*Ni@jitRM(<;<(Ht&1dbryUB zjC<=u=MjewoD{tuY{JdGd<$J`O)51!ZOY*Q?v{1*AFrpLk(}!UieI&Rpuo8QJN+v_ zXUt-E7XG11vn#sqW2y%8yl{Ad4=pdGBTVKQ9$N%=s51iKjNoc`E4R?D_z@*a{uGl- zmaIR5GAki{WSci|;8ei6>MCBN77|5|3^?fH!(bEnBNF%~OTqiMwEJV*Nu2Go4z%Kb zM8ecJEfQjwL7!%u;IvNDjvz-pwdUJ8rGf$#|52 zqQ12`L#Y$YyR!}z@IhtTT*eo+bD4CTIRW>$K&qR!&}G*Pm(la}bAIt%(GSad8yt+L zE`@`~dtI-wJ5ZUkeudiQ@`lV6TJAgM{hU-o}< zICe+%LkM>=GX*Lv2sI-V7|d^~619rnw3i#Oo4)>ZKt=fm9CgforX002so`K|afIv_KUOf9nF4Go zm&B7UOvJ~5?o6J}`!zzqK;n5@vWEm%_Rg5zW~&sR5WJULY_a1Sa9^AY=ShDl{Jbpx z#qCPsGL7?{jG>J|Fe?)5;=cok3vg8S2yGv-)38oC>)_D3=Swf(B?3ig;P`nWno>lBL4C!fCj3lp=DMoMUMT3RpjH-6SOSrTWMn$?g{$tyRAzf0aV?KiRipFri2`7uYE9Zhpnoi0MztEeDw@EY>6i8FH3(i>DDSSLLYjoqt2{T6iI`DLOpRIl2R$7d!pS0qmK!Ji?c>9dkrJsc6PZJgT7n-h{5K)AZ%S ztzHdnHAHNRoVBfHd4&Z?OWf}?`4(tH=g0}?G}(T(dMbN zRfg!b(2_^-l1SIL-KF$Jr1xMHXqai-g=$I)g^1e-LBr5TMc<IRZpMr@ob+2L&Kp zvA$jbY*3uW-;*h=GBEg~lAN3iW3F}g3MSerCa37_DrPn+So`J1xTJm+rG3$F^AG{% z64y)%=jkGAW}5yPGB9p4_quvfgCv=qB;6$=dIMt&HBjlb5Two>3y7OO?`ynHe>=RE zB;1+~d=Gz$xfnZ0nN)UWu{@k015o9|at`g9|4Mq`OhMnuri#%qSC4>w?|i2?Cn(i> zj1nF^@H?==i6{4~b?x*3P&@PlP1YT3n)b5~E_QzNOnawB)G+vvKRRo;fG&vqZ^9jf zNf0|j3|PwV<@4p5K?i*~XE#D_URxqGkQn^&wg(slo8T z**9)Fmg`b{P-kzakJ)}hfws!@qlHxA-P!z_c7<7)B9*lx{p1-32TY-s1rBp92l0U& z{&r>k1;0z~X!a%%&NFu2o&R{4&7C}w@OM{Db47+;C3(@sgoVDP{5oBH-;@pVuDlt0 z;6h8HpaYXLHSqf?mK$@x)_6*FksOt`Pgn!U+u0`;aq!sl-_57g1{to_f%^eU6MWM+ zS1mY(^VSMeT|iL6T)G7ncd%H(=ljH7BSIKhRZ?1f=2$Etwcc zO%qjU9ws*OBGU8ue0C$E3Ko}#7PzB9#aJgthgY)dh7HxF$a!i%V@6ti`y1wi4$dI&Go$;-*sr6>DuEkAV{41y{f+`>9=7iq#Y! zQgr$CmU>Jjc%ZaPt*-V^<#&TX2V8h^b7_nMB6$spbt!0C7y*sek-#U}2N4<{Nr*Qpivy1uw>S3^&6!3OG-ZLdK;u>M6a8 zdfjiAqI2*$)`f5>slLizR)F#RuL)bRaDaTHTlmc|0~=q0AEz8#b#&-3+Yn;bm0!K} z8_EOAx;e~pJz5YsISSqcK{Ygz;ml?{Va+DFnfKqwr7^-W*T1Z5e>{v@xvbwjLEcri z^OH;dhl>OEdgeAo^>~N4QonmF(85Pph!T2|Y2N2ordVE~Uc;n&x7$Xq#vXVtPvYUg zjKk@U`^#e3O)(A247N>It5yKA0}ki^dcHw2xE$a~{4MYoz_SP^@R~Z`i3($d#J>E_ zl=7>iwdX{Py`k5-Q;sCkzS~s-$T?L1)rCpdJ`cyYE!Ip%DR;=@vBMtJ0iT6-S((+F80g@Txd%yx214;F8FU0%{Yl6qLHd^-|Z9} zK-I;58yU?Px~RbVt4!b0v9d= z)=o=*emRb{pU)fk+;-I5RtWSUcLrgSw4xlXG@Zr>;3%o*3yh?E|2ACUohYM11Fc`% ze(PW{d`aWwGqKW7KYMZ;+_0|jDnJ}mj8ni47JHw<#=xH!faM#39;ET|{N77L(Lr!E zDR3_hwg7NddU;-O>nsWo^u<2KO-*~|y2GoY2T+My5*)E@;LtDcdnfL8Tkf8e<@jqF zQk;!lF1y16AUT))O5lW^XQrUU-G#0>opu7^X-c(2e{7R)#!zEOWd!=^e1*YlY0S&d zpJzXRr2YJf=T!6N&71dc-gn1$cgK67$3$TvN9C85lz_oQVDeLoiEXAB5h2?vo9GRH zTA7qTkF9&s4F(l%n9y%6Ps)Oc0l!#Z9rTeLeBaqNPo;H4)`wNo8rHqg?$cxs*LrrA z_S0#|pvmbo*ZZ0?!!KxX#8|V^D7`}5-1u@)Q6W6l{BRs`lskwoZx7P3Hhk)x@P6-`!ZRx8gXv-x5Q=?a!g3y(!iF8 z>;KB-x-_}H=2{{_l>@G=vVPk=W;#i|yGukfcsX<0;M#y{1*5Oh1WVG;Oa;HKM2#_j zwg6Db0SJ}@;qwq>C-L~5imEfM*jlzitwg=m5UzW8mZ;`Nsm_nh|LKbfkeq9W3aLXG z9*1FL4aaMg+t~TwXRZw{uJ_c=KY@6I{R1c=3UY1IGNAATW&l%j(kF7=*+7xO@l1i! zFmE22$;`Zl!rPCYRAhh`BIR4iZL9Ii%|d7I%<2C9ZRakdgzAU8Bk_{gvYXw`F21>T zZ+k%~(L|Pf3g>6ZTy6%6jvw`TDx^{e2|s?KB?-!>{1}jq{3b_t$2B4RrY?FNO7wsq&r#tn?_d*{opf8s}++!|ttlQhj5J7%aIPTX7N zwVqggc`ANH`f~YtXLgrt&{`KAp|jTVJ}{7ko_VSeDUO>-^xHNzAqPK=?iY$PqT=5M zniQ^#YFob~!mndaXIecc*Nj?gJlEIEPqW+*7uv}8zwTmP@SRI|@YG2Qa7tu~P&sfJ zvFo*Y4R6wejP76l`6fH)&#P;2^DqYcLfzvtBB zUrgc&<1q&@XxKkfE_6Z2VjCRjqW>m+0SMr?+P8$?p3bwQOl&~R0-H$W(@bLVF%fK> z(Xs?&N$GiUBFOm(ypM*}jv!4yplA#-6v>5$Z;9X&wylAbPSozi&L&DDNQ|cG+Z8tG zwG!zv09qaJ&HV>x``+`EXpJ%o@pVFQZRmzZ#eCoB3hLsr#>X}j*k(^)S<5p@b@?)-y>4k9N}Ir$+E#4|WfQrwcr&BFnqnZVpr34cdUa^JIKpSi-q{>T)PCp%V5piWxxkXD1 z6ctq!@v1yg>(DrY3KdBl)ieY0w=Fam;jGj&!r?(5k!{+;gL|$mISK`IdfMgQw6XW_ zM6S^6D$zIx^V7iIe^P#3Dkc*dHj;|A8^9oFIeF~@6?GpHePi0U?=mBcct6?Gu3eq| zqRbJ(H^}J~hVr80s6G%9R0^Qt^fq<)sqH81j*}1CZzOs*ZtrG#AvJwZilJ9X6PIh2 zdk<7NkpBly2+KHo-iQXnk}~NQ@Za}2$a$%HDSr9h=twFln8J--2tZFpme51S@xnxe zZua(k#RIJt)2$96ftDY)6$u0q1V@sq-OyE(E~L;$Q}BQar$1gt(jM9pItZ#tn1`5n z%6Z~9!MJLu9nZGDf2W?BC6wz68bFpEC{-E_%#^2)0!aZh@mT;sK1sRMJTwYfM-)#+ zV2d}yjDWT;;xbV@*jAmM4k)!U{hv{!6lB9ttwJY6`_TwnP@NM&VgHugDz`S2+lLSM zFsU3C%!uB09=0P0V(vxhyCwPXQ`T2E0mQ7=PyU@8`w z_=Z0vs*w<;Zz22a@8~$9`$1g>R8_J1zqT0$)H^UDX{6oNLpWb zf4DPLcjDVyf%@Ao>R{(!fj8}tv1xi55$0v^7YOtM?>+$(mZ$stRPpF_ z7lKrx_Yf9$8K|m)&^Ay?)cm<6k`S4+XkI&h?PWb<_Q3-owtuEjP`^15xzOR^J!JUW z%w)j5zzP)gHE5BV6WBiSND%yQR7FOEg9*RzqcKQYvCgCbf%LRL|G^f8?*X9olmOZp;tL*?Y)NyW_z^N+&VHD`qKK1QGZ}jtcQj zE2k;1bL62w4(l!fuh*1h0ecV8{fOvpIuqFO%uu(0FxLR@g@b)^Kbr~cJpp+?oEWrn zTPFy&z}3T_gCJ1^IEaGpdb$g#Xc!4#MVV4BbT=t*PMGn6EUz^YH_qqB+b8<}_yh>; zv3(?p)!Lu@C=a0hv(sM{7vPChvC-G0;=D<-RkBODwBtC*Y_<11&qvUzP_jx%B(p(o z^tE4wYQD~TQmlOH8#?qlI?h`g(=jV13kMb(50QgWQ=Epw1Xtk#T`+nWRnI8XhJTG zK3AVY$>;N%UOC3I*0?k8>0VhD%3zNuSb#knV0{Wb znkv)5h0oG~-qV0W@C4_@n7vxlH=Ft~=O*yyvMnx(3SX;M$6w3^$qEB;=&?Q887PRe zf(P8a4_2y#oEg_}uwr^-I4n4L&yvk$J4J%~yq}2{lT|VEkM649DMABZea+`Oh&q;d z<9?&mSloqqX~YS^X=>c;WJRLkv2h!|;X)LDo2c*{Q!Yj&4WK( z1)KG>mfsy3Y@XOB2u7}d+CeZDasCFs=iRD6aFc@dy_k#w6T0~s@~1d!t*W>PO@P4U zNWg)lIcZG~gUz(Z4*s&;zD3LLvghWCvgft3_cgeCWzHI1+%As9XD%=X2SZ6f6CT=D zOPhZ*;w|Nu)j+`vu_ZoOE~D4L_R*h8#5=nzFSf2A$p|q^&RzAJUW!6IK{uvwc=wqA zzL2b8A;ooFj|838x$SL91+TNlBs+*_32ggq*T0QOz#!;-paIszKEh#T_cwZOVth=R zg~Epy%J*xZHlOjoI|B0Z)PkyVg(cuuxhCzIKuK&7)+KM8z5uG;=@O|DV_=<#xY=~( z^+GWSU{gy;i_z$m_Pd*f&Vw+cx4nN@}$+YX50 z>knL+v+JV#MbhQn`rve+E`NDjAjdWjg{vpYG6@zMIs5s_ff5p3rGURXZ`_zaZ^pI} zpurnJqt+?+=)nT5)!v`a7c0k-Dp2R3o2WN^1;f=jepM$G!>TvrC_^#oh9lChdy{iR zEBKBc>X}&t^>6;|>HTp5Xb8k1qptUZc+NF75Kw~L7q+`JLnHC$>r{!&Pa6p90~rF; z3s+T@hVO%WUEpjCaNl(Y3CQtR>!f|((EVR+od-};QP-{^gx)(yk>0yNg7l*F4uaAJ zq>}(59Rkvu^denAnu0V5J@hUpB}h%^0#c<&z43nY&;7rdJ9B1E=A7Bd-r4P}^*(Ez z-pHR~qR^%XPUx4qjVDyBT`m!zI>iw?oOF?L$$LBz9%&xB4o#C*bEtqlNT&j0On zRX6a>?<+Kap*VIYaVZ(~R|fps5!OWT8YwJqA$^-KszW^6d&?5?H|aOy7jxtNxletq zZ%=t%f}$6fyZ^22KG_m>?`w$>zUL` zrD}W>Q8+72yWyYc$`bWBP3o+2hg}{nJwE2oVHR9BBjvk4usqr}40R(;JO`H=%b?!v z0xowXmTE$z#PL1xpsVy-mnIbr9d`qu$mPMwaEtm>xmw&_Z+ljii;PfWUD%#DTPPbu zJkAMzwp#^2zB2_Xt)!)4!8Ng%&jlxKxOh6RMi-5Wo;nJ<>#QF9`}e#3_+8}8z(6QX z??CkH(&H`3Q7%5N%CS6N160@T!Ekn6SV+d%>sxBGz(!Az=G}?+Z3b(BWNN67=Vh4y zmBpi_y4AAWNAwd`W#>M}cWJMFQl;iObPz5TK3Omp z&u#{WyUBF|W^m@GfsNr~Y7-8E(vOaw@)RxBOL*t|vp6)#pHH{`j@aF5pef+33Vqrg z>8%|kgwIAtEwvG-&FeSxrr zCg#Bt?k2Vh{LkTikD?$rL8SZB#!2Xzcr1<>gyCqv?B1-bac+h7fkAHjq6I0NSKnBO zgbT;SVLDH}od4dt(dbpbzFXkD?P)Bvc-Bj1fHV=Npz&`h+C9PR8`Dal)7rg*RRAMgjk3DoY>t7dEoj$wc00_qG573Q{Yw zWUQ8kvAm(K_5N_gEw@-k1n9eB+~XmOtXyrGFXRGC&M8gGOL*iD#@&}baIff5t98`t@M z=hE(-*TEI&#rDDp`p|o}|LDthf$gRe^fSwwjuL$D}}oZAyGvGmZuN}c7JNG zR}2-+0mQ1kK}(4|OR~fi+JQ>i11h1F)(<+kj~~D4L(2t&t)p!~=ulmbCqnEDgQKrC zH966dHc!3nn4_Gg1~he8)u(6gZ+>szCn2BJtj?K&EPwjUn(hiN9u8N#E&lCZIMAOj zoHHmFg*aDSpy_c$)&eQOFO$zl$mL z0kKTI(d&Ga@Vz=F1&7`oan(VtibO_n9&RaaZbzNbhzi66jU2mCDOlSUl*lSiUYnc5 z1hQvdaQKqP)7Ya5nrL~dxCr(2Jt8^tO(K=Ye(TU-L@G`E7kIC$)G@yH_LHE-VnC=78tEicY_VAQEY$t%{(s*wAP&hqN z;f}-mI^+sM>e+gGY?c}FF*FA7n$+y>>GrQ(xuoCY>dH#9lf;&9MU<{Ds*>u+-%%B+ zklu=%Ev!1yhA~wiTd>^IN^Q9mwMi7?h5DIds4cW zm?y;R1F%SxY~UZ~>6mc4#OCk~eX{*o;y<79Yflemsc~za9vZ-Dk)({OP1ukTe@$Io zN1~yQ0mCv#DGxRzox%Mrgsp^g;nflXm>i2Uv$Z6d`)jH992XNlreu~B=I(Yfwxh{k$-i|9%j}u9yyIRB; zZdAZ~(39*K}x!gH~-s%QC0WfeiGKD496;c+s8<*&*g5`) z1ZYm1cjl=>89KvWXXTDxOdLsf*&ZAT7Cg$scsHiIh>(@Ud=z0iV|$`&Msv(XP3lyYA1htJCDql=n(k;!w(A3lcRib~uex zmYn7%ivFi~>S}oEZ%A)aWHMHzI?n^CPw=QNeBbe9NJ+~OoF$$4oNX8~AIgIY+-8?h^si!{eE)+39cNh0d5G1PckgnI=xg^oVoSi|3Pn39l4m^6Zzau`m7+&ctA9#E~brWGVmLbL%OEhFPN{P~z2*(NY zezqGMRA}Vp^A@kPSNCOy-5+VlpAIJT8|k-05Sq>jE*o#M5hlw8TJRyAK+ zc95}*KAsFf&`Mq>?R_tq33|*5Wv?eB*a2*?9kEM4$nm8@;OSyVKNLQG$Y+yAc2Y0=j3_MJ=D~P;zauwY?LnABP zOnihMpj>f+e~9hunKcS!j=K8#Dp^Z)XyCA9W7T5cshj$;9$t`|$wDa!b4n7EN`f`r zfl&|*rhN8D;u~Uch6Z!iY8%<*2@UGa{=DI2cUv?mO#+UXl|;&ewjCna-f{(bKXk|7 zV;NWapfUj{0k%%Y{fZZVde)h;+YSq)nuOvhmDT*tRK>Z zjUqYtO6VE>%!bQBG19)Un~Xy#%YNF;^V7z|PcLZQsA|l9#xN z+L`5b+uLijY1N~BL<+?+S90uxL=1pvUkIn==$OesXW!d!Uf=KR*1gF-xdJ|m=@S!) zRgoDr#EVf=i+&S8YDQFyC!*RilEQX)CxQE7UvRb9V1E9u_d9@$^TWTr}K8#1H=*)+^c<_nZ zQ`_hhk1nz8;W#tsiML5M@_q6{*-)C;)8P4L`l}Qne@?#!UWmjmA7Qo_@ueL0s*xSN zk31D+Jd;!iLuIVoJV|qK#wi?O7kU!y^+?>`9^gm4C!y@;{e4LWPOmJBjD&VGSL|2Tu$&{FIjyW^pLExSv~Fb~~PT1(0Q~%byXn1r`hAAfu5n-C~*5$O=f- zCD`N_cdbBiadE)Ji5!Y7E{FyZ^j)cTXtqc)PN%qx(1^ZIf``4wpD;PLuSA&y{*l}Z z!+}=TTHXItBl5e6+-|OYdCMd+q#Up}U9qjSWpMuMv?ep79H_0yD(?2oTVNI!QO^8{ z4(R{aL#$!&TKkm&7APIL+5}g|iV-xU7je-?{aH}J1Ju4KSG{&Q!miZ|!j}sBX`Wj# zsJ@%@_$Fh^gMbxq{H$C`+HHgOXRTrP#At&^wq1ljMMNMvSIsf_R*jZ z{&Xg@R=*KXxG1~`DaC2J2crAGWL6LohNs@kee;)nvtN$D_A)f|!ohfAeAmFwr;&Dc zIE>z%a0I$HU!ilPeE$(-)0cXX%RYe+p21($n3nF;^Q(|v{Uq&zYb904bw$XgcMQAO zEB$XogbbVsT&_)*+S@D$m3jd=jEBALUfOK7K0fk4t65R+5+MeUnm@zcFrD>^YS%%% zkQ*k(!FQNRzphfY{$@|g03n^S zx7dtW>S$++W=hRZOWb}AT>7*@dC`&6AC$!#APD^sPUV%S3;^jh2{rEgew6`Q>hMDM zo}0p-7Hs`ypX*?JQt&h2*J~@aA^uNbpC7aAoblD+qP`p?MTMKhqQoNeGT#G4Hu+BY#8nBWbYG4E z%@vU(^-5Si87RGPx#Qt^2@2emi@i^NlQ8pWyN1BrXk?lmcyQ0*gIPx#W&IVu`mV$d zSn!IHSAPHX0;XB!fD-*%LJX)FO!rkG`t``XVq_=7h5-KR!g-VJms^705UO5DeafUk zdhja_@`O5`$WrE-`^tg8`7^->0P>U5=}lSOFgFrZ&CeckZPvfY2nT!fbE@#^6DB*V zs2QlKs5u~O!(@-8=rQh~$hBusngQI<9rkdXxN8fuW>!Qtq1aL_?pp>CN0K+f6}FTj zGQQ7;V_q&ggF2d;C=ZUsh+TNQXt-jSil{i9gMOy`na<4$6`^{_1mF zjy*3Q*F*{NfqfkyrWf(p-jp%L#%?>=-4*yWJ?9N1{u6+$kX&4+UG-LW=aQv=h;C*3 zWLLn-@evBqd=hd$piXyC*XK^aJ&9?EyxfCBwTEq5vFPfv$6Aa=vqWJ=LF}lRAm=<9 z3ptIhxyfz9PIh{2iy2_-pLTL6%5?kJnMdetU^CHMy-oJ!*JRP%zL$rYjW)4QX^ESX z2md1MI;}ex{|E{bI1iO;DH@`gUczGuMvaX|WH}iA2!oS(@WsPr&jRV$Jg`optsPM| za$Hb0FZDp*ftOgB9r!0aTHbth147&cmJ;xcN)J9S+hXW6KKUB!=1$Md7rA%5IytH` zJ`!$XcE^jl3?0iI1TYJ5*d50aAHAscj}>AACV}U~>fz2rkAaZ(Zxwqz#sh(msw>s3 z-@Tj4M7o4V6!F9&PpjpxyniQcx8TI%1?k(NJJJstd~1w9?gOM07^#Ww<|>fhl3-8MM$`6 z#}*5zE!W;#|BC++y$8gLuN5Iei@8X=1SUO0G=O$3Tz4ZVE1@r|0T2nwN3@qj;a&pJ zj-Ve*UBZLMD%!Vhu_<~N#GTO62h65^qsZ=ftI=VNh{Sj(!?!1qYHGl)*Pr5WU3ul^ zXJ`jtrA?Y9$V_#4-Elc}{AkjB)lY^+aidH;x`4FW^qJ?>$|xQ^oq@8EGQ;!^ zT(I7 zH$~qQAb!{17V_5H-{m4+UY01n#6H%ouv^moqOj|2#qMC?Pfnt%Umdw>MJJ73j(JN6 zE8xcEffHbTBGmiOw{9&8T$O7G`Foy zU|Ce{M)dZ|>=87TL`tVX&32gymXMf0Kti&Q#|!o&B~3~o#!V$IXA>b2u`^92gWnPOmWTx-(_%<2E2r58;{jPotosnGue)YQs~H=I}P0ag|^rVHul38OW(e zMMa%aVCTOtGr`!RBb-LB*gb@v#Mv>=qB}^fK;!G&5X0jwxb7?Bjwr=e|GVC((^P5r zB_%Ybx+ZKyNXbgq_pD^_+x5S;nJ4slrW3f9CXyr**tMIY@o+E*Psxt)ASNd29hE;$ zLOcPjGnYIL>2t2kM3!}Z3plsGqtFqWaD=4s^7%Nr(MO>F70ewRj^--5+Umv8jO z8-4S4Lr0{nfFNshWbB`c0gu#hwH}Ois5{35w0CqWi6%J!9{pAj?Ll{Bf$>GC~@ABt2o;1xnQxs1lcZJH*R7pPZLD{gu2gG;ls(;E@ zLAj0wLedc+7G01*2^ZsKmjkB?nkH<gjc39*U5A_)vg{t_e<9E0VR zQ|*8#(PkXY*QkY<1BRCM89jGQUVd+PdiXF-HCsX^fCQd4GSWz=qcebuL(Qx3$rS#5 zAz5UVc7+5yo;}XHkVUYPwIqTOKvxk_5r`*h9B$@`ePx=!*)8Y8i5g3@mIG!hXvUd}v$+`LqSHX(vebU_KhBl%A{ zP@Orjb&Ss=ed|yh!!J1iMF+q48cU~8Z6iK(J&7JnMHupn&`f8bjHAyOV6vptKAnN} z$3)d#^|Js!yXRPM!8EPU8`J2=UwmOZL({7VYzs{u1!m~1zH$YU;R`tLVgp*s|9Rp= zNbp_p?zk517&v6Sg}m(H8Vv>p@L@hYW*`1OD2_=aDNS+=6a$*ntdv3f_#GwO*3?MW z=!$#oDn=^5mqB(FWPR9wsTV*Wd);M5|&Nj=NMwH<4PtKPT zW%s0uiC~ARrPieawUtOke*1qsDYM%_?op8}?(FezJz(DMqcu6qsfi_Nzb@C{1Xl3M&qj&-8WExMkT>^mTnq-;BgMKKv@ehY4KOeh#t|7l91am!5?~hR9r3e2!%fIPIyfjp| zL@d5^24ra#w%@0t>>&nNuCH+fEX!t`WZ2C(0RS=WWYY?X@&D;Z_`7d5ohMwN5A z2;m%Q(mI}%(=}27oAi(?&B*KF=ETL{;|Fk@Z_a+JUKd6GG-En=8}g83&|2%UdSoO< z%a1c;77XBYnV*d^g_L-Xmyq;PBt!B`CvwH&aEiRA>WrefD#l;_9l>>I>^XQIu!m6# z`~~Ne_QyZ{gDaz^Gx&|8dmTs3!MdG(yE8$@5-eRyvLROm(}xb{iuWl+ST1)&ma3a> zC4@+I4kwhpQ5YfUhHQ~)E>DdA`q~FQ;>krnf*Z z9%kY#?c=sAKe7W%9QX2PDshIkE7>>F3&FPwgFJhtg~>&2?!*eR>dcSl?cWK9CH_pf z*AE4h{S|~db@|_ZOW9i&bEINa?&O|v0OZ(h=9Tli$XJ^2)ddm9;mof+r}bp}z4_g^ zK!cD7>4a%(%=Fil;BwILl3MH<&&tVB#LsD^U!JXw=N>J~06epnYwf(Fy?16qJDo9)YPZI^8^p>^Y^M#Rf>vQM###Y5)FpW5wn%JU z(*5V=W>q7-7NpbNu1P<>$MQ7AeUL3DF6G-J8NdEY7l`_(s~T=RKuYS&nQ6gJR7<=+cqo36P=s(_owg?9BS1{< zlt8%2?p`l3JfqPwcfwNZ5AR5x+Ky*9g(ogKT4UKHz@ln&(OADkpa1{v2<+a_LH6`N z!l9yrdanCRW-<`>(38hDOtqnm-@3>iCY#wqC-Z4ym%lW-Ei^f}KgyLR1$wS|kc*`)euSH`VAw zMhU>hxTY22O*GC7&r9CT{+}q5ZyebtbfVtlMH?VId9^*g!tFwh<@ zR5zxE0ZN_HK$tIwaKDUX_m_V)oI>%5;o7jebcXu4GEKv2#~_$_O$4!^$4MrF3Wm6@U@dWC zI=p}YOeG%a-U~cu`N)GUG1Y?R&yi+)yI2?j(!ArEDC4U0UTca|T5fV_k&h+<;)I=D&wLgkbzu-&BmrLbJ{;X7hC++svzQgzV?-$mgi}9N}>(44pA-dnx*7X(6 zVlk{QF~ z?n4V}epvfG<$+$BHvfNyFB|~p3oV@|4d-;}+Y`4p3J8=4$ok^4RFhAN(51GrO8iPI z7uufSY*AG!hZ}PuqA;K~Ve~IF=o_A(ztgO&PmN-NEriVP38FK`x+mfLTqYkM9M zH~lJnP&e9bK^Gt9>7;-hj-HXt?M>scHz#(~ z+NIFRVe^AWsSD`tzXwxVYUQ&q2YVKNoe3hP=Q{jLM&wS5S{tuV8 zMos1EpwGY?NfzD``Z;A-KJL%x56jDFHYEvGUg=wY3vd6OwKzklB4^*A(zueR`|E@6 z%EjtrP#f?cQ&gafR{Ndjo2A15=&=}OAlNW+$snv9)zs7-5Za7zphHa1AwCylMnkB; zT(21w?x2bUTFpq?`LZ4n9ht2377yVx?D#rN1IU`LpU3(-`$^ovu;je zBP!Kj=djS>CZu~)v#b)OrP1)?cY1n?){o74f>6iSK!ylkVQ_d?F1lcH(Tm%_M?bJiuVHn-3|}KuK|^%=b zrl<*M$Ep9Q2c{^!RdiJ?7v2`ebd6SksWjP@$7EDaZVZu(p1c&(4l`OW)x z#BJqPqT4-Gr_{C=C1-^NUp8n^rgt<6DxzNx{vsWsU}yM7xIMGP(vHrcM zceJYB`ypN3`yN-)tF@lNk)n2yFXdytsEJnVc%aXf#HguX!M?;Q0X`1pfxn+jf+4kO zcH~NagU$p%+O+2vrCHRtAM;$fu4!{8dSUaGG^FCWswH!4hsNn!1!1$XV0lxGz?iw| zse;qrg{xQ$gM=4HQeT7L-HJ!d*TKI^KFsM`IID zA80W?BNCbwEdDH0Q~P!A4Ch|(8Pw6t`E5WCQsB)FU5oE+9lh%Ot->fR z{*3Np;xg1XKglTo4t@xEz@&dl8Q`K+7`aDpa#o(P*sO>uyd;I$$=DFgzjIJ zq^rn757hj$_8xx0a{MWo^|#Q4r+ispPm|>Vu4~XhRFe{1PtRV|(lyHBH4eXStFPW@ zZ-I^F^3iEb34vd5+r5`e&5xO4Lw|(nmE^NUm!Hm`TLL?050?AO{H*kEl719HJ-$ji z-SKcviH<$*9!30&`k{#Ogd6li_2Ap{o6^A}=K88J(mf4%#3N8qExcelize logo \ No newline at end of file diff --git a/file.go b/file.go index 46f1f62524..2e0d27bef4 100644 --- a/file.go +++ b/file.go @@ -42,14 +42,13 @@ func NewFile() *File { f.CalcChain = f.calcChainReader() f.Comments = make(map[string]*xlsxComments) f.ContentTypes = f.contentTypesReader() - f.DrawingRels = make(map[string]*xlsxWorkbookRels) f.Drawings = make(map[string]*xlsxWsDr) f.Styles = f.stylesReader() f.DecodeVMLDrawing = make(map[string]*decodeVmlDrawing) f.VMLDrawing = make(map[string]*vmlDrawing) f.WorkBook = f.workbookReader() - f.WorkBookRels = f.workbookRelsReader() - f.WorkSheetRels = make(map[string]*xlsxWorkbookRels) + f.Relationships = make(map[string]*xlsxRelationships) + f.Relationships["xl/_rels/workbook.xml.rels"] = f.relsReader("xl/_rels/workbook.xml.rels") f.Sheet["xl/worksheets/sheet1.xml"], _ = f.workSheetReader("Sheet1") f.sheetMap["Sheet1"] = "xl/worksheets/sheet1.xml" f.Theme = f.themeReader() @@ -97,13 +96,11 @@ func (f *File) WriteToBuffer() (*bytes.Buffer, error) { f.calcChainWriter() f.commentsWriter() f.contentTypesWriter() - f.drawingRelsWriter() f.drawingsWriter() f.vmlDrawingWriter() f.workBookWriter() - f.workBookRelsWriter() f.workSheetWriter() - f.workSheetRelsWriter() + f.relsWriter() f.styleSheetWriter() for path, content := range f.XLSX { diff --git a/picture.go b/picture.go index a5904ffea2..518463a5df 100644 --- a/picture.go +++ b/picture.go @@ -155,14 +155,15 @@ func (f *File) AddPictureFromBytes(sheet, cell, format, name, extension string, drawingID := f.countDrawings() + 1 drawingXML := "xl/drawings/drawing" + strconv.Itoa(drawingID) + ".xml" drawingID, drawingXML = f.prepareDrawing(xlsx, drawingID, sheet, drawingXML) + drawingRels := "xl/drawings/_rels/drawing" + strconv.Itoa(drawingID) + ".xml.rels" mediaStr := ".." + strings.TrimPrefix(f.addMedia(file, ext), "xl") - drawingRID := f.addDrawingRelationships(drawingID, SourceRelationshipImage, mediaStr, hyperlinkType) + drawingRID := f.addRels(drawingRels, SourceRelationshipImage, mediaStr, hyperlinkType) // Add picture with hyperlink. if formatSet.Hyperlink != "" && formatSet.HyperlinkType != "" { if formatSet.HyperlinkType == "External" { hyperlinkType = formatSet.HyperlinkType } - drawingHyperlinkRID = f.addDrawingRelationships(drawingID, SourceRelationshipHyperLink, formatSet.Hyperlink, hyperlinkType) + drawingHyperlinkRID = f.addRels(drawingRels, SourceRelationshipHyperLink, formatSet.Hyperlink, hyperlinkType) } err = f.addDrawingPicture(sheet, drawingXML, cell, name, img.Width, img.Height, drawingRID, drawingHyperlinkRID, formatSet) if err != nil { @@ -172,37 +173,6 @@ func (f *File) AddPictureFromBytes(sheet, cell, format, name, extension string, return err } -// addSheetRelationships provides a function to add -// xl/worksheets/_rels/sheet%d.xml.rels by given worksheet name, relationship -// type and target. -func (f *File) addSheetRelationships(sheet, relType, target, targetMode string) int { - name, ok := f.sheetMap[trimSheetName(sheet)] - if !ok { - name = strings.ToLower(sheet) + ".xml" - } - var rels = "xl/worksheets/_rels/" + strings.TrimPrefix(name, "xl/worksheets/") + ".rels" - sheetRels := f.workSheetRelsReader(rels) - if sheetRels == nil { - sheetRels = &xlsxWorkbookRels{} - } - var rID = 1 - var ID bytes.Buffer - ID.WriteString("rId") - ID.WriteString(strconv.Itoa(rID)) - ID.Reset() - rID = len(sheetRels.Relationships) + 1 - ID.WriteString("rId") - ID.WriteString(strconv.Itoa(rID)) - sheetRels.Relationships = append(sheetRels.Relationships, xlsxWorkbookRelation{ - ID: ID.String(), - Type: relType, - Target: target, - TargetMode: targetMode, - }) - f.WorkSheetRels[rels] = sheetRels - return rID -} - // deleteSheetRelationships provides a function to delete relationships in // xl/worksheets/_rels/sheet%d.xml.rels by given worksheet name and // relationship index. @@ -212,16 +182,16 @@ func (f *File) deleteSheetRelationships(sheet, rID string) { name = strings.ToLower(sheet) + ".xml" } var rels = "xl/worksheets/_rels/" + strings.TrimPrefix(name, "xl/worksheets/") + ".rels" - sheetRels := f.workSheetRelsReader(rels) + sheetRels := f.relsReader(rels) if sheetRels == nil { - sheetRels = &xlsxWorkbookRels{} + sheetRels = &xlsxRelationships{} } for k, v := range sheetRels.Relationships { if v.ID == rID { sheetRels.Relationships = append(sheetRels.Relationships[:k], sheetRels.Relationships[k+1:]...) } } - f.WorkSheetRels[rels] = sheetRels + f.Relationships[rels] = sheetRels } // addSheetLegacyDrawing provides a function to add legacy drawing element to @@ -325,33 +295,6 @@ func (f *File) addDrawingPicture(sheet, drawingXML, cell, file string, width, he return err } -// addDrawingRelationships provides a function to add image part relationships -// in the file xl/drawings/_rels/drawing%d.xml.rels by given drawing index, -// relationship type and target. -func (f *File) addDrawingRelationships(index int, relType, target, targetMode string) int { - var rels = "xl/drawings/_rels/drawing" + strconv.Itoa(index) + ".xml.rels" - var rID = 1 - var ID bytes.Buffer - ID.WriteString("rId") - ID.WriteString(strconv.Itoa(rID)) - drawingRels := f.drawingRelsReader(rels) - if drawingRels == nil { - drawingRels = &xlsxWorkbookRels{} - } - ID.Reset() - rID = len(drawingRels.Relationships) + 1 - ID.WriteString("rId") - ID.WriteString(strconv.Itoa(rID)) - drawingRels.Relationships = append(drawingRels.Relationships, xlsxWorkbookRelation{ - ID: ID.String(), - Type: relType, - Target: target, - TargetMode: targetMode, - }) - f.DrawingRels[rels] = drawingRels - return rID -} - // countMedia provides a function to get media files count storage in the // folder xl/media/image. func (f *File) countMedia() int { @@ -429,16 +372,20 @@ func (f *File) addContentTypePart(index int, contentType string) { "drawings": f.setContentTypePartImageExtensions, } partNames := map[string]string{ - "chart": "/xl/charts/chart" + strconv.Itoa(index) + ".xml", - "comments": "/xl/comments" + strconv.Itoa(index) + ".xml", - "drawings": "/xl/drawings/drawing" + strconv.Itoa(index) + ".xml", - "table": "/xl/tables/table" + strconv.Itoa(index) + ".xml", + "chart": "/xl/charts/chart" + strconv.Itoa(index) + ".xml", + "comments": "/xl/comments" + strconv.Itoa(index) + ".xml", + "drawings": "/xl/drawings/drawing" + strconv.Itoa(index) + ".xml", + "table": "/xl/tables/table" + strconv.Itoa(index) + ".xml", + "pivotTable": "/xl/pivotTables/pivotTable" + strconv.Itoa(index) + ".xml", + "pivotCache": "/xl/pivotCache/pivotCacheDefinition" + strconv.Itoa(index) + ".xml", } contentTypes := map[string]string{ - "chart": "application/vnd.openxmlformats-officedocument.drawingml.chart+xml", - "comments": "application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml", - "drawings": "application/vnd.openxmlformats-officedocument.drawing+xml", - "table": "application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml", + "chart": "application/vnd.openxmlformats-officedocument.drawingml.chart+xml", + "comments": "application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml", + "drawings": "application/vnd.openxmlformats-officedocument.drawing+xml", + "table": "application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml", + "pivotTable": "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml", + "pivotCache": "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml", } s, ok := setContentType[contentType] if ok { @@ -465,9 +412,9 @@ func (f *File) getSheetRelationshipsTargetByID(sheet, rID string) string { name = strings.ToLower(sheet) + ".xml" } var rels = "xl/worksheets/_rels/" + strings.TrimPrefix(name, "xl/worksheets/") + ".rels" - sheetRels := f.workSheetRelsReader(rels) + sheetRels := f.relsReader(rels) if sheetRels == nil { - sheetRels = &xlsxWorkbookRels{} + sheetRels = &xlsxRelationships{} } for _, v := range sheetRels.Relationships { if v.ID == rID { @@ -529,12 +476,12 @@ func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) for _, anchor := range wsDr.TwoCellAnchor { if anchor.From != nil && anchor.Pic != nil { if anchor.From.Col == col && anchor.From.Row == row { - xlsxWorkbookRelation := f.getDrawingRelationships(drawingRelationships, + xlsxRelationship := f.getDrawingRelationships(drawingRelationships, anchor.Pic.BlipFill.Blip.Embed) - _, ok := supportImageTypes[filepath.Ext(xlsxWorkbookRelation.Target)] + _, ok := supportImageTypes[filepath.Ext(xlsxRelationship.Target)] if ok { - return filepath.Base(xlsxWorkbookRelation.Target), - []byte(f.XLSX[strings.Replace(xlsxWorkbookRelation.Target, + return filepath.Base(xlsxRelationship.Target), + []byte(f.XLSX[strings.Replace(xlsxRelationship.Target, "..", "xl", -1)]), nil } } @@ -548,10 +495,10 @@ func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) _ = xml.Unmarshal([]byte(""+anchor.Content+""), &decodeTwoCellAnchor) if decodeTwoCellAnchor.From != nil && decodeTwoCellAnchor.Pic != nil { if decodeTwoCellAnchor.From.Col == col && decodeTwoCellAnchor.From.Row == row { - xlsxWorkbookRelation := f.getDrawingRelationships(drawingRelationships, decodeTwoCellAnchor.Pic.BlipFill.Blip.Embed) - _, ok := supportImageTypes[filepath.Ext(xlsxWorkbookRelation.Target)] + xlsxRelationship := f.getDrawingRelationships(drawingRelationships, decodeTwoCellAnchor.Pic.BlipFill.Blip.Embed) + _, ok := supportImageTypes[filepath.Ext(xlsxRelationship.Target)] if ok { - return filepath.Base(xlsxWorkbookRelation.Target), []byte(f.XLSX[strings.Replace(xlsxWorkbookRelation.Target, "..", "xl", -1)]), nil + return filepath.Base(xlsxRelationship.Target), []byte(f.XLSX[strings.Replace(xlsxRelationship.Target, "..", "xl", -1)]), nil } } } @@ -562,8 +509,8 @@ func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) // getDrawingRelationships provides a function to get drawing relationships // from xl/drawings/_rels/drawing%s.xml.rels by given file name and // relationship ID. -func (f *File) getDrawingRelationships(rels, rID string) *xlsxWorkbookRelation { - if drawingRels := f.drawingRelsReader(rels); drawingRels != nil { +func (f *File) getDrawingRelationships(rels, rID string) *xlsxRelationship { + if drawingRels := f.relsReader(rels); drawingRels != nil { for _, v := range drawingRels.Relationships { if v.ID == rID { return &v @@ -573,31 +520,6 @@ func (f *File) getDrawingRelationships(rels, rID string) *xlsxWorkbookRelation { return nil } -// drawingRelsReader provides a function to get the pointer to the structure -// after deserialization of xl/drawings/_rels/drawing%d.xml.rels. -func (f *File) drawingRelsReader(rel string) *xlsxWorkbookRels { - if f.DrawingRels[rel] == nil { - _, ok := f.XLSX[rel] - if ok { - d := xlsxWorkbookRels{} - _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML(rel)), &d) - f.DrawingRels[rel] = &d - } - } - return f.DrawingRels[rel] -} - -// drawingRelsWriter provides a function to save -// xl/drawings/_rels/drawing%d.xml.rels after serialize structure. -func (f *File) drawingRelsWriter() { - for path, d := range f.DrawingRels { - if d != nil { - v, _ := xml.Marshal(d) - f.saveFileList(path, v) - } - } -} - // drawingsWriter provides a function to save xl/drawings/drawing%d.xml after // serialize structure. func (f *File) drawingsWriter() { diff --git a/shape.go b/shape.go index 8d95849c03..e6a2ff3742 100644 --- a/shape.go +++ b/shape.go @@ -275,7 +275,9 @@ func (f *File) AddShape(sheet, cell, format string) error { drawingXML = strings.Replace(sheetRelationshipsDrawingXML, "..", "xl", -1) } else { // Add first shape for given sheet. - rID := f.addSheetRelationships(sheet, SourceRelationshipDrawingML, sheetRelationshipsDrawingXML, "") + name, _ := f.sheetMap[trimSheetName(sheet)] + sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(name, "xl/worksheets/") + ".rels" + rID := f.addRels(sheetRels, SourceRelationshipDrawingML, sheetRelationshipsDrawingXML, "") f.addSheetDrawing(sheet, rID) } err = f.addDrawingShape(sheet, drawingXML, cell, formatSet) diff --git a/sheet.go b/sheet.go index ed6d888ee5..951baf9239 100644 --- a/sheet.go +++ b/sheet.go @@ -52,7 +52,7 @@ func (f *File) NewSheet(name string) int { // Create new sheet /xl/worksheets/sheet%d.xml f.setSheet(sheetID, name) // Update xl/_rels/workbook.xml.rels - rID := f.addXlsxWorkbookRels(sheetID) + rID := f.addRels("xl/_rels/workbook.xml.rels", SourceRelationshipWorkSheet, fmt.Sprintf("worksheets/sheet%d.xml", sheetID), "") // Update xl/workbook.xml f.setWorkbook(name, sheetID, rID) return sheetID @@ -163,50 +163,18 @@ func (f *File) setWorkbook(name string, sheetID, rid int) { }) } -// workbookRelsReader provides a function to read and unmarshal workbook -// relationships of XLSX file. -func (f *File) workbookRelsReader() *xlsxWorkbookRels { - if f.WorkBookRels == nil { - var content xlsxWorkbookRels - _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML("xl/_rels/workbook.xml.rels")), &content) - f.WorkBookRels = &content - } - return f.WorkBookRels -} - -// workBookRelsWriter provides a function to save xl/_rels/workbook.xml.rels after +// relsWriter provides a function to save relationships after // serialize structure. -func (f *File) workBookRelsWriter() { - if f.WorkBookRels != nil { - output, _ := xml.Marshal(f.WorkBookRels) - f.saveFileList("xl/_rels/workbook.xml.rels", output) - } -} - -// addXlsxWorkbookRels update workbook relationships property of XLSX. -func (f *File) addXlsxWorkbookRels(sheet int) int { - content := f.workbookRelsReader() - rID := 0 - for _, v := range content.Relationships { - t, _ := strconv.Atoi(strings.TrimPrefix(v.ID, "rId")) - if t > rID { - rID = t +func (f *File) relsWriter() { + for path, rel := range f.Relationships { + if rel != nil { + output, _ := xml.Marshal(rel) + if strings.HasPrefix(path, "xl/worksheets/sheet/rels/sheet") { + output = replaceWorkSheetsRelationshipsNameSpaceBytes(output) + } + f.saveFileList(path, replaceRelationshipsBytes(output)) } } - rID++ - ID := bytes.Buffer{} - ID.WriteString("rId") - ID.WriteString(strconv.Itoa(rID)) - target := bytes.Buffer{} - target.WriteString("worksheets/sheet") - target.WriteString(strconv.Itoa(sheet)) - target.WriteString(".xml") - content.Relationships = append(content.Relationships, xlsxWorkbookRelation{ - ID: ID.String(), - Target: target.String(), - Type: SourceRelationshipWorkSheet, - }) - return rID } // setAppXML update docProps/app.xml file of XML. @@ -365,7 +333,7 @@ func (f *File) GetSheetMap() map[int]string { // of XLSX. func (f *File) getSheetMap() map[string]string { content := f.workbookReader() - rels := f.workbookRelsReader() + rels := f.relsReader("xl/_rels/workbook.xml.rels") maps := map[string]string{} for _, v := range content.Sheets.Sheet { for _, rel := range rels.Relationships { @@ -396,7 +364,9 @@ func (f *File) SetSheetBackground(sheet, picture string) error { } file, _ := ioutil.ReadFile(picture) name := f.addMedia(file, ext) - rID := f.addSheetRelationships(sheet, SourceRelationshipImage, strings.Replace(name, "xl", "..", 1), "") + sheetPath, _ := f.sheetMap[trimSheetName(sheet)] + sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetPath, "xl/worksheets/") + ".rels" + rID := f.addRels(sheetRels, SourceRelationshipImage, strings.Replace(name, "xl", "..", 1), "") f.addSheetPicture(sheet, rID) f.setContentTypePartImageExtensions() return err @@ -413,7 +383,7 @@ func (f *File) DeleteSheet(name string) { } sheetName := trimSheetName(name) wb := f.workbookReader() - wbRels := f.workbookRelsReader() + wbRels := f.relsReader("xl/_rels/workbook.xml.rels") for idx, sheet := range wb.Sheets.Sheet { if sheet.Name == sheetName { wb.Sheets.Sheet = append(wb.Sheets.Sheet[:idx], wb.Sheets.Sheet[idx+1:]...) @@ -443,7 +413,7 @@ func (f *File) DeleteSheet(name string) { // relationships by given relationships ID in the file // xl/_rels/workbook.xml.rels. func (f *File) deleteSheetFromWorkbookRels(rID string) string { - content := f.workbookRelsReader() + content := f.relsReader("xl/_rels/workbook.xml.rels") for k, v := range content.Relationships { if v.ID == rID { content.Relationships = append(content.Relationships[:k], content.Relationships[k+1:]...) @@ -1387,29 +1357,18 @@ func (f *File) UngroupSheets() error { return nil } -// workSheetRelsReader provides a function to get the pointer to the structure +// relsReader provides a function to get the pointer to the structure // after deserialization of xl/worksheets/_rels/sheet%d.xml.rels. -func (f *File) workSheetRelsReader(path string) *xlsxWorkbookRels { - if f.WorkSheetRels[path] == nil { +func (f *File) relsReader(path string) *xlsxRelationships { + if f.Relationships[path] == nil { _, ok := f.XLSX[path] if ok { - c := xlsxWorkbookRels{} + c := xlsxRelationships{} _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML(path)), &c) - f.WorkSheetRels[path] = &c - } - } - return f.WorkSheetRels[path] -} - -// workSheetRelsWriter provides a function to save -// xl/worksheets/_rels/sheet%d.xml.rels after serialize structure. -func (f *File) workSheetRelsWriter() { - for p, r := range f.WorkSheetRels { - if r != nil { - v, _ := xml.Marshal(r) - f.saveFileList(p, v) + f.Relationships[path] = &c } } + return f.Relationships[path] } // fillSheetData ensures there are enough rows, and columns in the chosen diff --git a/table.go b/table.go index 45a1622698..d26f8fd462 100644 --- a/table.go +++ b/table.go @@ -77,7 +77,9 @@ func (f *File) AddTable(sheet, hcell, vcell, format string) error { sheetRelationshipsTableXML := "../tables/table" + strconv.Itoa(tableID) + ".xml" tableXML := strings.Replace(sheetRelationshipsTableXML, "..", "xl", -1) // Add first table for given sheet. - rID := f.addSheetRelationships(sheet, SourceRelationshipTable, sheetRelationshipsTableXML, "") + sheetPath, _ := f.sheetMap[trimSheetName(sheet)] + sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetPath, "xl/worksheets/") + ".rels" + rID := f.addRels(sheetRels, SourceRelationshipTable, sheetRelationshipsTableXML, "") f.addSheetTable(sheet, rID) err = f.addTable(sheet, tableXML, hcol, hrow, vcol, vrow, tableID, formatSet) if err != nil { diff --git a/xmlDrawing.go b/xmlDrawing.go index ade6261252..4338c5e6a0 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -22,6 +22,8 @@ const ( 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" + SourceRelationshipPivotTable = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable" + SourceRelationshipPivotCache = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition" SourceRelationshipVBAProject = "http://schemas.microsoft.com/office/2006/relationships/vbaProject" SourceRelationshipChart201506 = "http://schemas.microsoft.com/office/drawing/2015/06/chart" SourceRelationshipChart20070802 = "http://schemas.microsoft.com/office/drawing/2007/8/2/chart" diff --git a/xmlPivotCache.go b/xmlPivotCache.go index 9e07931e95..0c008321fa 100644 --- a/xmlPivotCache.go +++ b/xmlPivotCache.go @@ -2,11 +2,11 @@ package excelize import "encoding/xml" -// pivotCacheDefinition represents the pivotCacheDefinition part. This part +// xlsxPivotCacheDefinition represents the pivotCacheDefinition part. This part // defines each field in the source data, including the name, the string // resources of the instance data (for shared items), and information about // the type of data that appears in the field. -type xmlPivotCacheDefinition struct { +type xlsxPivotCacheDefinition struct { XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main pivotCacheDefinition"` RID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr,omitempty"` Invalid bool `xml:"invalid,attr,omitempty"` diff --git a/xmlPivotTable.go b/xmlPivotTable.go index 16c469f1aa..6f2a8e77d3 100644 --- a/xmlPivotTable.go +++ b/xmlPivotTable.go @@ -15,78 +15,84 @@ import "encoding/xml" // non-null PivotTables. There exists one pivotTableDefinition for each // PivotTableDefinition part type xlsxPivotTableDefinition struct { - XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main pivotTableDefinition"` - Name string `xml:"name,attr"` - CacheID int `xml:"cacheId,attr"` - DataOnRows bool `xml:"dataOnRows,attr"` - DataPosition int `xml:"dataPosition,attr"` - DataCaption string `xml:"dataCaption,attr"` - GrandTotalCaption string `xml:"grandTotalCaption,attr"` - ErrorCaption string `xml:"errorCaption,attr"` - ShowError bool `xml:"showError,attr"` - MissingCaption string `xml:"missingCaption,attr"` - ShowMissing bool `xml:"showMissing,attr"` - PageStyle string `xml:"pageStyle,attr"` - PivotTableStyle string `xml:"pivotTableStyle,attr"` - VacatedStyle string `xml:"vacatedStyle,attr"` - Tag string `xml:"tag,attr"` - UpdatedVersion int `xml:"updatedVersion,attr"` - MinRefreshableVersion int `xml:"minRefreshableVersion,attr"` - AsteriskTotals bool `xml:"asteriskTotals,attr"` - ShowItems bool `xml:"showItems,attr"` - EditData bool `xml:"editData,attr"` - DisableFieldList bool `xml:"disableFieldList,attr"` - ShowCalcMbrs bool `xml:"showCalcMbrs,attr"` - VisualTotals bool `xml:"visualTotals,attr"` - ShowMultipleLabel bool `xml:"showMultipleLabel,attr"` - ShowDataDropDown bool `xml:"showDataDropDown,attr"` - ShowDrill bool `xml:"showDrill,attr"` - PrintDrill bool `xml:"printDrill,attr"` - ShowMemberPropertyTips bool `xml:"showMemberPropertyTips,attr"` - ShowDataTips bool `xml:"showDataTips,attr"` - EnableWizard bool `xml:"enableWizard,attr"` - EnableDrill bool `xml:"enableDrill,attr"` - EnableFieldProperties bool `xml:"enableFieldProperties,attr"` - PreserveFormatting bool `xml:"preserveFormatting,attr"` - UseAutoFormatting bool `xml:"useAutoFormatting,attr"` - PageWrap int `xml:"pageWrap,attr"` - PageOverThenDown bool `xml:"pageOverThenDown,attr"` - SubtotalHiddenItems bool `xml:"subtotalHiddenItems,attr"` - RowGrandTotals bool `xml:"rowGrandTotals,attr"` - ColGrandTotals bool `xml:"colGrandTotals,attr"` - FieldPrintTitles bool `xml:"fieldPrintTitles,attr"` - ItemPrintTitles bool `xml:"itemPrintTitles,attr"` - MergeItem bool `xml:"mergeItem,attr"` - ShowDropZones bool `xml:"showDropZones,attr"` - CreatedVersion int `xml:"createdVersion,attr"` - Indent int `xml:"indent,attr"` - ShowEmptyRow bool `xml:"showEmptyRow,attr"` - ShowEmptyCol bool `xml:"showEmptyCol,attr"` - ShowHeaders bool `xml:"showHeaders,attr"` - Compact bool `xml:"compact,attr"` - Outline bool `xml:"outline,attr"` - OutlineData bool `xml:"outlineData,attr"` - CompactData bool `xml:"compactData,attr"` - Published bool `xml:"published,attr"` - GridDropZones bool `xml:"gridDropZones,attr"` - Immersive bool `xml:"immersive,attr"` - MultipleFieldFilters bool `xml:"multipleFieldFilters,attr"` - ChartFormat int `xml:"chartFormat,attr"` - RowHeaderCaption string `xml:"rowHeaderCaption,attr"` - ColHeaderCaption string `xml:"colHeaderCaption,attr"` - FieldListSortAscending bool `xml:"fieldListSortAscending,attr"` - MdxSubqueries bool `xml:"mdxSubqueries,attr"` - CustomListSort bool `xml:"customListSort,attr"` - Location *xlsxLocation `xml:"location"` - PivotFields *xlsxPivotFields `xml:"pivotFields"` - RowFields *xlsxRowFields `xml:"rowFields"` - RowItems *xlsxRowItems `xml:"rowItems"` - ColFields *xlsxColFields `xml:"colFields"` - ColItems *xlsxColItems `xml:"colItems"` - PageFields *xlsxPageFields `xml:"pageFields"` - DataFields *xlsxDataFields `xml:"dataFields"` - ConditionalFormats *xlsxConditionalFormats `xml:"conditionalFormats"` - PivotTableStyleInfo *xlsxPivotTableStyleInfo `xml:"pivotTableStyleInfo"` + XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main pivotTableDefinition"` + Name string `xml:"name,attr"` + CacheID int `xml:"cacheId,attr"` + ApplyNumberFormats bool `xml:"applyNumberFormats,attr,omitempty"` + ApplyBorderFormats bool `xml:"applyBorderFormats,attr,omitempty"` + ApplyFontFormats bool `xml:"applyFontFormats,attr,omitempty"` + ApplyPatternFormats bool `xml:"applyPatternFormats,attr,omitempty"` + ApplyAlignmentFormats bool `xml:"applyAlignmentFormats,attr,omitempty"` + ApplyWidthHeightFormats bool `xml:"applyWidthHeightFormats,attr,omitempty"` + DataOnRows bool `xml:"dataOnRows,attr,omitempty"` + DataPosition int `xml:"dataPosition,attr,omitempty"` + DataCaption string `xml:"dataCaption,attr"` + GrandTotalCaption string `xml:"grandTotalCaption,attr,omitempty"` + ErrorCaption string `xml:"errorCaption,attr,omitempty"` + ShowError bool `xml:"showError,attr,omitempty"` + MissingCaption string `xml:"missingCaption,attr,omitempty"` + ShowMissing bool `xml:"showMissing,attr,omitempty"` + PageStyle string `xml:"pageStyle,attr,omitempty"` + PivotTableStyle string `xml:"pivotTableStyle,attr,omitempty"` + VacatedStyle string `xml:"vacatedStyle,attr,omitempty"` + Tag string `xml:"tag,attr,omitempty"` + UpdatedVersion int `xml:"updatedVersion,attr"` + MinRefreshableVersion int `xml:"minRefreshableVersion,attr"` + AsteriskTotals bool `xml:"asteriskTotals,attr,omitempty"` + ShowItems bool `xml:"showItems,attr,omitempty"` + EditData bool `xml:"editData,attr,omitempty"` + DisableFieldList bool `xml:"disableFieldList,attr,omitempty"` + ShowCalcMbrs bool `xml:"showCalcMbrs,attr,omitempty"` + VisualTotals bool `xml:"visualTotals,attr,omitempty"` + ShowMultipleLabel bool `xml:"showMultipleLabel,attr,omitempty"` + ShowDataDropDown bool `xml:"showDataDropDown,attr,omitempty"` + ShowDrill bool `xml:"showDrill,attr,omitempty"` + PrintDrill bool `xml:"printDrill,attr,omitempty"` + ShowMemberPropertyTips bool `xml:"showMemberPropertyTips,attr,omitempty"` + ShowDataTips bool `xml:"showDataTips,attr,omitempty"` + EnableWizard bool `xml:"enableWizard,attr,omitempty"` + EnableDrill bool `xml:"enableDrill,attr,omitempty"` + EnableFieldProperties bool `xml:"enableFieldProperties,attr,omitempty"` + PreserveFormatting bool `xml:"preserveFormatting,attr,omitempty"` + UseAutoFormatting bool `xml:"useAutoFormatting,attr"` + PageWrap int `xml:"pageWrap,attr,omitempty"` + PageOverThenDown bool `xml:"pageOverThenDown,attr,omitempty"` + SubtotalHiddenItems bool `xml:"subtotalHiddenItems,attr,omitempty"` + RowGrandTotals bool `xml:"rowGrandTotals,attr,omitempty"` + ColGrandTotals bool `xml:"colGrandTotals,attr,omitempty"` + FieldPrintTitles bool `xml:"fieldPrintTitles,attr,omitempty"` + ItemPrintTitles bool `xml:"itemPrintTitles,attr"` + MergeItem bool `xml:"mergeItem,attr,omitempty"` + ShowDropZones bool `xml:"showDropZones,attr,omitempty"` + CreatedVersion int `xml:"createdVersion,attr"` + Indent int `xml:"indent,attr,omitempty"` + ShowEmptyRow bool `xml:"showEmptyRow,attr,omitempty"` + ShowEmptyCol bool `xml:"showEmptyCol,attr,omitempty"` + ShowHeaders bool `xml:"showHeaders,attr,omitempty"` + Compact bool `xml:"compact,attr,omitempty"` + Outline bool `xml:"outline,attr,omitempty"` + OutlineData bool `xml:"outlineData,attr,omitempty"` + CompactData bool `xml:"compactData,attr,omitempty"` + Published bool `xml:"published,attr,omitempty"` + GridDropZones bool `xml:"gridDropZones,attr"` + Immersive bool `xml:"immersive,attr,omitempty"` + MultipleFieldFilters bool `xml:"multipleFieldFilters,attr,omitempty"` + ChartFormat int `xml:"chartFormat,attr,omitempty"` + RowHeaderCaption string `xml:"rowHeaderCaption,attr,omitempty"` + ColHeaderCaption string `xml:"colHeaderCaption,attr,omitempty"` + FieldListSortAscending bool `xml:"fieldListSortAscending,attr,omitempty"` + MdxSubqueries bool `xml:"mdxSubqueries,attr,omitempty"` + CustomListSort bool `xml:"customListSort,attr,omitempty"` + Location *xlsxLocation `xml:"location"` + PivotFields *xlsxPivotFields `xml:"pivotFields"` + RowFields *xlsxRowFields `xml:"rowFields"` + RowItems *xlsxRowItems `xml:"rowItems"` + ColFields *xlsxColFields `xml:"colFields"` + ColItems *xlsxColItems `xml:"colItems"` + PageFields *xlsxPageFields `xml:"pageFields"` + DataFields *xlsxDataFields `xml:"dataFields"` + ConditionalFormats *xlsxConditionalFormats `xml:"conditionalFormats"` + PivotTableStyleInfo *xlsxPivotTableStyleInfo `xml:"pivotTableStyleInfo"` } // xlsxLocation represents location information for the PivotTable. diff --git a/xmlWorkbook.go b/xmlWorkbook.go index 8150e295c1..765563bc7a 100644 --- a/xmlWorkbook.go +++ b/xmlWorkbook.go @@ -11,14 +11,14 @@ package excelize import "encoding/xml" -// xmlxWorkbookRels contains xmlxWorkbookRelations which maps sheet id and sheet XML. -type xlsxWorkbookRels struct { - XMLName xml.Name `xml:"http://schemas.openxmlformats.org/package/2006/relationships Relationships"` - Relationships []xlsxWorkbookRelation `xml:"Relationship"` +// xlsxRelationships describe references from parts to other internal resources in the package or to external resources. +type xlsxRelationships struct { + XMLName xml.Name `xml:"http://schemas.openxmlformats.org/package/2006/relationships Relationships"` + Relationships []xlsxRelationship `xml:"Relationship"` } -// xmlxWorkbookRelation maps sheet id and xl/worksheets/_rels/sheet%d.xml.rels -type xlsxWorkbookRelation struct { +// xlsxRelationship contains relations which maps id and XML. +type xlsxRelationship struct { ID string `xml:"Id,attr"` Target string `xml:",attr"` Type string `xml:",attr"` From eef232f09ecd41b1f8fc199906ce0be64865802e Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 18 Sep 2019 00:47:31 +0800 Subject: [PATCH 148/957] Fix #483, adjust the order of fields in the structure --- xmlWorksheet.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 09dec5e738..7e8cfde550 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -209,16 +209,18 @@ type xlsxPane struct { // properties. type xlsxSheetPr struct { XMLName xml.Name `xml:"sheetPr"` - CodeName string `xml:"codeName,attr,omitempty"` - EnableFormatConditionsCalculation *bool `xml:"enableFormatConditionsCalculation,attr"` - FilterMode bool `xml:"filterMode,attr,omitempty"` - Published *bool `xml:"published,attr"` SyncHorizontal bool `xml:"syncHorizontal,attr,omitempty"` SyncVertical bool `xml:"syncVertical,attr,omitempty"` + SyncRef string `xml:"syncRef,attr,omitempty"` + TransitionEvaluation bool `xml:"transitionEvaluation,attr,omitempty"` + Published *bool `xml:"published,attr"` + CodeName string `xml:"codeName,attr,omitempty"` + FilterMode bool `xml:"filterMode,attr,omitempty"` + EnableFormatConditionsCalculation *bool `xml:"enableFormatConditionsCalculation,attr"` TransitionEntry bool `xml:"transitionEntry,attr,omitempty"` TabColor *xlsxTabColor `xml:"tabColor,omitempty"` - PageSetUpPr *xlsxPageSetUpPr `xml:"pageSetUpPr,omitempty"` OutlinePr *xlsxOutlinePr `xml:"outlinePr,omitempty"` + PageSetUpPr *xlsxPageSetUpPr `xml:"pageSetUpPr,omitempty"` } // xlsxOutlinePr maps to the outlinePr element From 3c636da46029b1c578871dfab3e1692e989af9f7 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 20 Sep 2019 00:20:30 +0800 Subject: [PATCH 149/957] Resolve #40, init pivot table support --- pivotTable.go | 434 +++++++++++++++++++++++++++++++++++++++++++++ pivotTable_test.go | 164 +++++++++++++++++ xmlPivotCache.go | 34 +++- xmlPivotTable.go | 141 ++++++++------- 4 files changed, 696 insertions(+), 77 deletions(-) create mode 100644 pivotTable.go create mode 100644 pivotTable_test.go diff --git a/pivotTable.go b/pivotTable.go new file mode 100644 index 0000000000..881d774192 --- /dev/null +++ b/pivotTable.go @@ -0,0 +1,434 @@ +// Copyright 2016 - 2019 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. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.10 or later. + +package excelize + +import ( + "encoding/xml" + "errors" + "fmt" + "strconv" + "strings" +) + +// PivotTableOption directly maps the format settings of the pivot table. +type PivotTableOption struct { + DataRange string + PivotTableRange string + Rows []string + Columns []string + Data []string + Page []string +} + +// AddPivotTable provides the method to add pivot table by given pivot table +// options. 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" +// +// "github.com/360EntSecGroup-Skylar/excelize" +// ) +// +// 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 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)]) +// } +// err := f.AddPivotTable(&excelize.PivotTableOption{ +// DataRange: "Sheet1!$A$1:$E$31", +// PivotTableRange: "Sheet1!$G$2:$M$34", +// Rows: []string{"Month", "Year"}, +// Columns: []string{"Type"}, +// Data: []string{"Sales"}, +// }) +// if err != nil { +// fmt.Println(err) +// } +// err = f.SaveAs("Book1.xlsx") +// if err != nil { +// fmt.Println(err) +// } +// } +// +func (f *File) AddPivotTable(opt *PivotTableOption) error { + // parameter validation + dataSheet, pivotTableSheetPath, err := f.parseFormatPivotTableSet(opt) + if err != nil { + return err + } + + pivotTableID := f.countPivotTables() + 1 + pivotCacheID := f.countPivotCache() + 1 + + 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) + if err != nil { + return err + } + + // workbook pivot cache + workBookPivotCacheRID := f.addRels("xl/_rels/workbook.xml.rels", SourceRelationshipPivotCache, fmt.Sprintf("pivotCache/pivotCacheDefinition%d.xml", pivotCacheID), "") + cacheID := f.addWorkbookPivotCache(workBookPivotCacheRID) + + 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) + if err != nil { + return err + } + 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 +} + +// parseFormatPivotTableSet provides a function to validate pivot table +// properties. +func (f *File) parseFormatPivotTableSet(opt *PivotTableOption) (*xlsxWorksheet, string, error) { + if opt == nil { + return nil, "", errors.New("parameter is required") + } + dataSheetName, _, err := f.adjustRange(opt.DataRange) + if err != nil { + return nil, "", fmt.Errorf("parameter 'DataRange' parsing error: %s", err.Error()) + } + pivotTableSheetName, _, err := f.adjustRange(opt.PivotTableRange) + if err != nil { + return nil, "", fmt.Errorf("parameter 'PivotTableRange' parsing error: %s", err.Error()) + } + dataSheet, err := f.workSheetReader(dataSheetName) + if err != nil { + return dataSheet, "", err + } + pivotTableSheetPath, ok := f.sheetMap[trimSheetName(pivotTableSheetName)] + if !ok { + return dataSheet, pivotTableSheetPath, fmt.Errorf("sheet %s is not exist", pivotTableSheetName) + } + return dataSheet, pivotTableSheetPath, err +} + +// adjustRange adjust range, for example: adjust Sheet1!$E$31:$A$1 to Sheet1!$A$1:$E$31 +func (f *File) adjustRange(rangeStr string) (string, []int, error) { + if len(rangeStr) < 1 { + return "", []int{}, errors.New("parameter is required") + } + rng := strings.Split(rangeStr, "!") + if len(rng) != 2 { + return "", []int{}, errors.New("parameter is invalid") + } + trimRng := strings.Replace(rng[1], "$", "", -1) + coordinates, err := f.areaRefToCoordinates(trimRng) + if err != nil { + return rng[0], []int{}, err + } + x1, y1, x2, y2 := coordinates[0], coordinates[1], coordinates[2], coordinates[3] + if x1 == x2 && y1 == y2 { + return rng[0], []int{}, errors.New("parameter is invalid") + } + + // Correct the coordinate area, such correct C1:B3 to B1:C3. + if x2 < x1 { + x1, x2 = x2, x1 + } + + if y2 < y1 { + y1, y2 = y2, y1 + } + return rng[0], []int{x1, y1, x2, y2}, nil +} + +func (f *File) getPivotFieldsOrder(dataRange string) ([]string, error) { + order := []string{} + // data range has been checked + dataSheet, coordinates, err := f.adjustRange(dataRange) + if err != nil { + return order, fmt.Errorf("parameter 'DataRange' parsing error: %s", err.Error()) + } + for col := coordinates[0]; col <= coordinates[2]; col++ { + coordinate, _ := CoordinatesToCellName(col, coordinates[1]) + name, err := f.GetCellValue(dataSheet, coordinate) + if err != nil { + return order, err + } + order = append(order, name) + } + return order, nil +} + +// 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 { + // validate data range + dataSheet, coordinates, err := f.adjustRange(opt.DataRange) + if err != nil { + return fmt.Errorf("parameter 'DataRange' parsing error: %s", err.Error()) + } + order, err := f.getPivotFieldsOrder(opt.DataRange) + if err != nil { + return err + } + hcell, _ := CoordinatesToCellName(coordinates[0], coordinates[1]) + vcell, _ := CoordinatesToCellName(coordinates[2], coordinates[3]) + pc := xlsxPivotCacheDefinition{ + SaveData: false, + RefreshOnLoad: true, + CacheSource: &xlsxCacheSource{ + Type: "worksheet", + WorksheetSource: &xlsxWorksheetSource{ + Ref: hcell + ":" + vcell, + Sheet: dataSheet, + }, + }, + CacheFields: &xlsxCacheFields{}, + } + for _, name := range order { + pc.CacheFields.CacheField = append(pc.CacheFields.CacheField, &xlsxCacheField{ + Name: name, + SharedItems: &xlsxSharedItems{ + Count: 0, + }, + }) + } + pc.CacheFields.Count = len(pc.CacheFields.CacheField) + pivotCache, err := xml.Marshal(pc) + f.saveFileList(pivotCacheXML, pivotCache) + return err +} + +// 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 { + // validate pivot table range + _, coordinates, err := f.adjustRange(opt.PivotTableRange) + if err != nil { + return fmt.Errorf("parameter 'PivotTableRange' parsing error: %s", err.Error()) + } + + hcell, _ := CoordinatesToCellName(coordinates[0], coordinates[1]) + vcell, _ := CoordinatesToCellName(coordinates[2], coordinates[3]) + + pt := xlsxPivotTableDefinition{ + Name: fmt.Sprintf("Pivot Table%d", pivotTableID), + CacheID: cacheID, + DataCaption: "Values", + Location: &xlsxLocation{ + Ref: hcell + ":" + vcell, + FirstDataCol: 1, + FirstDataRow: 1, + FirstHeaderRow: 1, + }, + PivotFields: &xlsxPivotFields{}, + RowFields: &xlsxRowFields{}, + RowItems: &xlsxRowItems{ + Count: 1, + I: []*xlsxI{ + { + []*xlsxX{{}, {}}, + }, + }, + }, + ColFields: &xlsxColFields{}, + DataFields: &xlsxDataFields{}, + PivotTableStyleInfo: &xlsxPivotTableStyleInfo{ + Name: "PivotStyleLight16", + ShowRowHeaders: true, + ShowColHeaders: true, + ShowLastColumn: true, + }, + } + + // pivot fields + err = f.addPivotFields(&pt, opt) + if err != nil { + return err + } + + // count pivot fields + pt.PivotFields.Count = len(pt.PivotFields.PivotField) + + // row fields + rowFieldsIndex, err := f.getPivotFieldsIndex(opt.Rows, opt) + if err != nil { + return err + } + for _, filedIdx := range rowFieldsIndex { + pt.RowFields.Field = append(pt.RowFields.Field, &xlsxField{ + X: filedIdx, + }) + } + + // count row fields + pt.RowFields.Count = len(pt.RowFields.Field) + + // col fields + colFieldsIndex, err := f.getPivotFieldsIndex(opt.Columns, opt) + if err != nil { + return err + } + for _, filedIdx := range colFieldsIndex { + pt.ColFields.Field = append(pt.ColFields.Field, &xlsxField{ + X: filedIdx, + }) + } + + // count col fields + pt.ColFields.Count = len(pt.ColFields.Field) + + // data fields + dataFieldsIndex, err := f.getPivotFieldsIndex(opt.Data, opt) + if err != nil { + return err + } + for _, dataField := range dataFieldsIndex { + pt.DataFields.DataField = append(pt.DataFields.DataField, &xlsxDataField{ + Fld: dataField, + }) + } + + // count data fields + pt.DataFields.Count = len(pt.DataFields.DataField) + + pivotTable, err := xml.Marshal(pt) + f.saveFileList(pivotTableXML, pivotTable) + return err +} + +// inStrSlice provides a method to check if an element is present in an array, +// and return the index of its location, otherwise return -1. +func inStrSlice(a []string, x string) int { + for idx, n := range a { + if x == n { + return idx + } + } + return -1 +} + +// 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.DataRange) + if err != nil { + return err + } + for _, name := range order { + if inStrSlice(opt.Rows, name) != -1 { + pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, &xlsxPivotField{ + Axis: "axisRow", + Items: &xlsxItems{ + Count: 1, + Item: []*xlsxItem{ + {T: "default"}, + }, + }, + }) + continue + } + if inStrSlice(opt.Columns, name) != -1 { + pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, &xlsxPivotField{ + Axis: "axisCol", + Items: &xlsxItems{ + Count: 1, + Item: []*xlsxItem{ + {T: "default"}, + }, + }, + }) + continue + } + if inStrSlice(opt.Data, name) != -1 { + pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, &xlsxPivotField{ + DataField: true, + }) + continue + } + pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, &xlsxPivotField{}) + } + return err +} + +// countPivotTables provides a function to get drawing files count storage in +// the folder xl/pivotTables. +func (f *File) countPivotTables() int { + count := 0 + for k := range f.XLSX { + if strings.Contains(k, "xl/pivotTables/pivotTable") { + count++ + } + } + return count +} + +// countPivotCache provides a function to get drawing files count storage in +// the folder xl/pivotCache. +func (f *File) countPivotCache() int { + count := 0 + for k := range f.XLSX { + if strings.Contains(k, "xl/pivotCache/pivotCacheDefinition") { + count++ + } + } + return count +} + +// 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 []string, opt *PivotTableOption) ([]int, error) { + pivotFieldsIndex := []int{} + orders, err := f.getPivotFieldsOrder(opt.DataRange) + if err != nil { + return pivotFieldsIndex, err + } + for _, field := range fields { + if pos := inStrSlice(orders, field); pos != -1 { + pivotFieldsIndex = append(pivotFieldsIndex, pos) + } + } + return pivotFieldsIndex, nil +} + +// addWorkbookPivotCache add the association ID of the pivot cache in xl/workbook.xml. +func (f *File) addWorkbookPivotCache(RID int) int { + wb := f.workbookReader() + if wb.PivotCaches == nil { + wb.PivotCaches = &xlsxPivotCaches{} + } + cacheID := 1 + for _, pivotCache := range wb.PivotCaches.PivotCache { + if pivotCache.CacheID > cacheID { + cacheID = pivotCache.CacheID + } + } + cacheID++ + wb.PivotCaches.PivotCache = append(wb.PivotCaches.PivotCache, xlsxPivotCache{ + CacheID: cacheID, + RID: fmt.Sprintf("rId%d", RID), + }) + return cacheID +} diff --git a/pivotTable_test.go b/pivotTable_test.go new file mode 100644 index 0000000000..27e5914f82 --- /dev/null +++ b/pivotTable_test.go @@ -0,0 +1,164 @@ +package excelize + +import ( + "fmt" + "math/rand" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAddPivotTable(t *testing.T) { + f := 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 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)]) + } + assert.NoError(t, f.AddPivotTable(&PivotTableOption{ + DataRange: "Sheet1!$A$1:$E$31", + PivotTableRange: "Sheet1!$G$2:$M$34", + Rows: []string{"Month", "Year"}, + Columns: []string{"Type"}, + Data: []string{"Sales"}, + })) + // Use different order of coordinate tests + assert.NoError(t, f.AddPivotTable(&PivotTableOption{ + DataRange: "Sheet1!$A$1:$E$31", + PivotTableRange: "Sheet1!$U$34:$O$2", + Rows: []string{"Month", "Year"}, + Columns: []string{"Type"}, + Data: []string{"Sales"}, + })) + + assert.NoError(t, f.AddPivotTable(&PivotTableOption{ + DataRange: "Sheet1!$A$1:$E$31", + PivotTableRange: "Sheet1!$W$2:$AC$34", + Rows: []string{"Month", "Year"}, + Columns: []string{"Region"}, + Data: []string{"Sales"}, + })) + assert.NoError(t, f.AddPivotTable(&PivotTableOption{ + DataRange: "Sheet1!$A$1:$E$31", + PivotTableRange: "Sheet1!$G$37:$W$50", + Rows: []string{"Month"}, + Columns: []string{"Region", "Year"}, + Data: []string{"Sales"}, + })) + f.NewSheet("Sheet2") + assert.NoError(t, f.AddPivotTable(&PivotTableOption{ + DataRange: "Sheet1!$A$1:$E$31", + PivotTableRange: "Sheet2!$A$1:$AR$15", + Rows: []string{"Month"}, + Columns: []string{"Region", "Type", "Year"}, + Data: []string{"Sales"}, + })) + assert.NoError(t, f.AddPivotTable(&PivotTableOption{ + DataRange: "Sheet1!$A$1:$E$31", + PivotTableRange: "Sheet2!$A$18:$AR$54", + Rows: []string{"Month", "Type"}, + Columns: []string{"Region", "Year"}, + Data: []string{"Sales"}, + })) + + // Test empty pivot table options + assert.EqualError(t, f.AddPivotTable(nil), "parameter is required") + // Test invalid data range + assert.EqualError(t, f.AddPivotTable(&PivotTableOption{ + DataRange: "Sheet1!$A$1:$A$1", + PivotTableRange: "Sheet1!$U$34:$O$2", + Rows: []string{"Month", "Year"}, + Columns: []string{"Type"}, + Data: []string{"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{ + DataRange: "$A$1:$E$31", + PivotTableRange: "Sheet1!$U$34:$O$2", + Rows: []string{"Month", "Year"}, + Columns: []string{"Type"}, + Data: []string{"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{ + DataRange: "SheetN!$A$1:$E$31", + PivotTableRange: "Sheet1!$U$34:$O$2", + Rows: []string{"Month", "Year"}, + Columns: []string{"Type"}, + Data: []string{"Sales"}, + }), "sheet SheetN is 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", + PivotTableRange: "$U$34:$O$2", + Rows: []string{"Month", "Year"}, + Columns: []string{"Type"}, + Data: []string{"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{ + DataRange: "Sheet1!$A$1:$E$31", + PivotTableRange: "SheetN!$U$34:$O$2", + Rows: []string{"Month", "Year"}, + Columns: []string{"Type"}, + Data: []string{"Sales"}, + }), "sheet SheetN is not exist") + // Test not exists worksheet in data range + assert.EqualError(t, f.AddPivotTable(&PivotTableOption{ + DataRange: "SheetN!$A$1:$E$31", + PivotTableRange: "Sheet1!$U$34:$O$2", + Rows: []string{"Month", "Year"}, + Columns: []string{"Type"}, + Data: []string{"Sales"}, + }), "sheet SheetN is not exist") + // Test invalid row number in data range + assert.EqualError(t, f.AddPivotTable(&PivotTableOption{ + DataRange: "Sheet1!$A$0:$E$31", + PivotTableRange: "Sheet1!$U$34:$O$2", + Rows: []string{"Month", "Year"}, + Columns: []string{"Type"}, + Data: []string{"Sales"}, + }), `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 adjust range with invalid range + _, _, err := f.adjustRange("") + assert.EqualError(t, err, "parameter is required") + // Test get pivot fields order with empty data range + _, err = f.getPivotFieldsOrder("") + 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") + // Test add pivot cache with invalid data range + assert.EqualError(t, f.addPivotCache(0, "", &PivotTableOption{ + DataRange: "$A$1:$E$31", + PivotTableRange: "Sheet1!$U$34:$O$2", + Rows: []string{"Month", "Year"}, + Columns: []string{"Type"}, + Data: []string{"Sales"}, + }, nil), "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 + assert.EqualError(t, f.addPivotTable(0, 0, "", &PivotTableOption{}), "parameter 'PivotTableRange' parsing error: parameter is required") + // Test add pivot fields with empty data range + assert.EqualError(t, f.addPivotFields(nil, &PivotTableOption{ + DataRange: "$A$1:$E$31", + PivotTableRange: "Sheet1!$U$34:$O$2", + Rows: []string{"Month", "Year"}, + Columns: []string{"Type"}, + Data: []string{"Sales"}, + }), `parameter 'DataRange' parsing error: parameter is invalid`) + // Test get pivot fields index with empty data range + _, err = f.getPivotFieldsIndex([]string{}, &PivotTableOption{}) + assert.EqualError(t, err, `parameter 'DataRange' parsing error: parameter is required`) +} diff --git a/xmlPivotCache.go b/xmlPivotCache.go index 0c008321fa..a4b07118e8 100644 --- a/xmlPivotCache.go +++ b/xmlPivotCache.go @@ -10,7 +10,7 @@ type xlsxPivotCacheDefinition struct { XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main pivotCacheDefinition"` RID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr,omitempty"` Invalid bool `xml:"invalid,attr,omitempty"` - SaveData bool `xml:"saveData,attr,omitempty"` + SaveData bool `xml:"saveData,attr"` RefreshOnLoad bool `xml:"refreshOnLoad,attr,omitempty"` OptimizeMemory bool `xml:"optimizeMemory,attr,omitempty"` EnableRefresh bool `xml:"enableRefresh,attr,omitempty"` @@ -47,6 +47,28 @@ type xlsxPivotCacheDefinition struct { // (including OLAP cubes), multiple SpreadsheetML worksheets, or another // PivotTable. type xlsxCacheSource struct { + Type string `xml:"type,attr"` + ConnectionId int `xml:"connectionId,attr,omitempty"` + WorksheetSource *xlsxWorksheetSource `xml:"worksheetSource"` + Consolidation *xlsxConsolidation `xml:"consolidation"` + ExtLst *xlsxExtLst `xml:"extLst"` +} + +// xlsxWorksheetSource represents the location of the source of the data that +// is stored in the cache. +type xlsxWorksheetSource struct { + RID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr,omitempty"` + Ref string `xml:"ref,attr,omitempty"` + Name string `xml:"name,attr,omitempty"` + Sheet string `xml:"sheet,attr,omitempty"` +} + +// xlsxConsolidation represents the description of the PivotCache source using +// multiple consolidation ranges. This element is used when the source of the +// PivotTable is a collection of ranges in the workbook. The ranges are +// specified in the rangeSets collection. The logic for how the application +// consolidates the data in the ranges is application- defined. +type xlsxConsolidation struct { } // xlsxCacheFields represents the collection of field definitions in the @@ -67,18 +89,18 @@ type xlsxCacheField struct { PropertyName string `xml:"propertyName,attr,omitempty"` ServerField bool `xml:"serverField,attr,omitempty"` UniqueList bool `xml:"uniqueList,attr,omitempty"` - NumFmtId string `xml:"numFmtId,attr,omitempty"` + NumFmtId int `xml:"numFmtId,attr"` Formula string `xml:"formula,attr,omitempty"` SQLType int `xml:"sqlType,attr,omitempty"` Hierarchy int `xml:"hierarchy,attr,omitempty"` Level int `xml:"level,attr,omitempty"` - DatabaseField bool `xml:"databaseField,attr"` + DatabaseField bool `xml:"databaseField,attr,omitempty"` MappingCount int `xml:"mappingCount,attr,omitempty"` MemberPropertyField bool `xml:"memberPropertyField,attr,omitempty"` SharedItems *xlsxSharedItems `xml:"sharedItems"` FieldGroup *xlsxFieldGroup `xml:"fieldGroup"` - MpMap *xlsxX `xml:"map"` - ExtLst *xlsxExtLst `xml:"exrLst"` + MpMap *xlsxX `xml:"mpMap"` + ExtLst *xlsxExtLst `xml:"extLst"` } // xlsxSharedItems represents the collection of unique items for a field in @@ -100,7 +122,7 @@ type xlsxSharedItems struct { MaxValue float64 `xml:"maxValue,attr,omitempty"` MinDate string `xml:"minDate,attr,omitempty"` MaxDate string `xml:"maxDate,attr,omitempty"` - Count int `xml:"count,attr,omitempty"` + Count int `xml:"count,attr"` LongText bool `xml:"longText,attr,omitempty"` M *xlsxMissing `xml:"m"` N *xlsxNumber `xml:"n"` diff --git a/xmlPivotTable.go b/xmlPivotTable.go index 6f2a8e77d3..3738ed82b2 100644 --- a/xmlPivotTable.go +++ b/xmlPivotTable.go @@ -36,8 +36,8 @@ type xlsxPivotTableDefinition struct { PivotTableStyle string `xml:"pivotTableStyle,attr,omitempty"` VacatedStyle string `xml:"vacatedStyle,attr,omitempty"` Tag string `xml:"tag,attr,omitempty"` - UpdatedVersion int `xml:"updatedVersion,attr"` - MinRefreshableVersion int `xml:"minRefreshableVersion,attr"` + UpdatedVersion int `xml:"updatedVersion,attr,omitempty"` + MinRefreshableVersion int `xml:"minRefreshableVersion,attr,omitempty"` AsteriskTotals bool `xml:"asteriskTotals,attr,omitempty"` ShowItems bool `xml:"showItems,attr,omitempty"` EditData bool `xml:"editData,attr,omitempty"` @@ -54,27 +54,27 @@ type xlsxPivotTableDefinition struct { EnableDrill bool `xml:"enableDrill,attr,omitempty"` EnableFieldProperties bool `xml:"enableFieldProperties,attr,omitempty"` PreserveFormatting bool `xml:"preserveFormatting,attr,omitempty"` - UseAutoFormatting bool `xml:"useAutoFormatting,attr"` + UseAutoFormatting bool `xml:"useAutoFormatting,attr,omitempty"` PageWrap int `xml:"pageWrap,attr,omitempty"` PageOverThenDown bool `xml:"pageOverThenDown,attr,omitempty"` SubtotalHiddenItems bool `xml:"subtotalHiddenItems,attr,omitempty"` RowGrandTotals bool `xml:"rowGrandTotals,attr,omitempty"` ColGrandTotals bool `xml:"colGrandTotals,attr,omitempty"` FieldPrintTitles bool `xml:"fieldPrintTitles,attr,omitempty"` - ItemPrintTitles bool `xml:"itemPrintTitles,attr"` + ItemPrintTitles bool `xml:"itemPrintTitles,attr,omitempty"` MergeItem bool `xml:"mergeItem,attr,omitempty"` ShowDropZones bool `xml:"showDropZones,attr,omitempty"` - CreatedVersion int `xml:"createdVersion,attr"` + CreatedVersion int `xml:"createdVersion,attr,omitempty"` Indent int `xml:"indent,attr,omitempty"` ShowEmptyRow bool `xml:"showEmptyRow,attr,omitempty"` ShowEmptyCol bool `xml:"showEmptyCol,attr,omitempty"` ShowHeaders bool `xml:"showHeaders,attr,omitempty"` - Compact bool `xml:"compact,attr,omitempty"` - Outline bool `xml:"outline,attr,omitempty"` + Compact bool `xml:"compact,attr"` + Outline bool `xml:"outline,attr"` OutlineData bool `xml:"outlineData,attr,omitempty"` CompactData bool `xml:"compactData,attr,omitempty"` Published bool `xml:"published,attr,omitempty"` - GridDropZones bool `xml:"gridDropZones,attr"` + GridDropZones bool `xml:"gridDropZones,attr,omitempty"` Immersive bool `xml:"immersive,attr,omitempty"` MultipleFieldFilters bool `xml:"multipleFieldFilters,attr,omitempty"` ChartFormat int `xml:"chartFormat,attr,omitempty"` @@ -101,8 +101,8 @@ type xlsxLocation struct { FirstHeaderRow int `xml:"firstHeaderRow,attr"` FirstDataRow int `xml:"firstDataRow,attr"` FirstDataCol int `xml:"firstDataCol,attr"` - RowPageCount int `xml:"rowPageCount,attr"` - ColPageCount int `xml:"colPageCount,attr"` + RowPageCount int `xml:"rowPageCount,attr,omitempty"` + ColPageCount int `xml:"colPageCount,attr,omitempty"` } // xlsxPivotFields represents the collection of fields that appear on the @@ -116,50 +116,50 @@ type xlsxPivotFields struct { // contains information about the field, including the collection of items in // the field. type xlsxPivotField struct { - Name string `xml:"name,attr"` + Name string `xml:"name,attr,omitempty"` Axis string `xml:"axis,attr,omitempty"` - DataField bool `xml:"dataField,attr"` - SubtotalCaption string `xml:"subtotalCaption,attr"` - ShowDropDowns bool `xml:"showDropDowns,attr"` - HiddenLevel bool `xml:"hiddenLevel,attr"` - UniqueMemberProperty string `xml:"uniqueMemberProperty,attr"` + DataField bool `xml:"dataField,attr,omitempty"` + SubtotalCaption string `xml:"subtotalCaption,attr,omitempty"` + ShowDropDowns bool `xml:"showDropDowns,attr,omitempty"` + HiddenLevel bool `xml:"hiddenLevel,attr,omitempty"` + UniqueMemberProperty string `xml:"uniqueMemberProperty,attr,omitempty"` Compact bool `xml:"compact,attr"` - AllDrilled bool `xml:"allDrilled,attr"` + AllDrilled bool `xml:"allDrilled,attr,omitempty"` NumFmtId string `xml:"numFmtId,attr,omitempty"` Outline bool `xml:"outline,attr"` - SubtotalTop bool `xml:"subtotalTop,attr"` - DragToRow bool `xml:"dragToRow,attr"` - DragToCol bool `xml:"dragToCol,attr"` - MultipleItemSelectionAllowed bool `xml:"multipleItemSelectionAllowed,attr"` - DragToPage bool `xml:"dragToPage,attr"` - DragToData bool `xml:"dragToData,attr"` - DragOff bool `xml:"dragOff,attr"` + SubtotalTop bool `xml:"subtotalTop,attr,omitempty"` + DragToRow bool `xml:"dragToRow,attr,omitempty"` + DragToCol bool `xml:"dragToCol,attr,omitempty"` + MultipleItemSelectionAllowed bool `xml:"multipleItemSelectionAllowed,attr,omitempty"` + DragToPage bool `xml:"dragToPage,attr,omitempty"` + DragToData bool `xml:"dragToData,attr,omitempty"` + DragOff bool `xml:"dragOff,attr,omitempty"` ShowAll bool `xml:"showAll,attr"` - InsertBlankRow bool `xml:"insertBlankRow,attr"` - ServerField bool `xml:"serverField,attr"` - InsertPageBreak bool `xml:"insertPageBreak,attr"` - AutoShow bool `xml:"autoShow,attr"` - TopAutoShow bool `xml:"topAutoShow,attr"` - HideNewItems bool `xml:"hideNewItems,attr"` - MeasureFilter bool `xml:"measureFilter,attr"` - IncludeNewItemsInFilter bool `xml:"includeNewItemsInFilter,attr"` - ItemPageCount int `xml:"itemPageCount,attr"` - SortType string `xml:"sortType,attr"` + InsertBlankRow bool `xml:"insertBlankRow,attr,omitempty"` + ServerField bool `xml:"serverField,attr,omitempty"` + InsertPageBreak bool `xml:"insertPageBreak,attr,omitempty"` + AutoShow bool `xml:"autoShow,attr,omitempty"` + TopAutoShow bool `xml:"topAutoShow,attr,omitempty"` + HideNewItems bool `xml:"hideNewItems,attr,omitempty"` + MeasureFilter bool `xml:"measureFilter,attr,omitempty"` + IncludeNewItemsInFilter bool `xml:"includeNewItemsInFilter,attr,omitempty"` + ItemPageCount int `xml:"itemPageCount,attr,omitempty"` + SortType string `xml:"sortType,attr,omitempty"` DataSourceSort bool `xml:"dataSourceSort,attr,omitempty"` - NonAutoSortDefault bool `xml:"nonAutoSortDefault,attr"` + NonAutoSortDefault bool `xml:"nonAutoSortDefault,attr,omitempty"` RankBy int `xml:"rankBy,attr,omitempty"` - DefaultSubtotal bool `xml:"defaultSubtotal,attr"` - SumSubtotal bool `xml:"sumSubtotal,attr"` - CountASubtotal bool `xml:"countASubtotal,attr"` - AvgSubtotal bool `xml:"avgSubtotal,attr"` - MaxSubtotal bool `xml:"maxSubtotal,attr"` - MinSubtotal bool `xml:"minSubtotal,attr"` - ProductSubtotal bool `xml:"productSubtotal,attr"` - CountSubtotal bool `xml:"countSubtotal,attr"` - StdDevSubtotal bool `xml:"stdDevSubtotal,attr"` - StdDevPSubtotal bool `xml:"stdDevPSubtotal,attr"` - VarSubtotal bool `xml:"varSubtotal,attr"` - VarPSubtotal bool `xml:"varPSubtotal,attr"` + DefaultSubtotal bool `xml:"defaultSubtotal,attr,omitempty"` + SumSubtotal bool `xml:"sumSubtotal,attr,omitempty"` + CountASubtotal bool `xml:"countASubtotal,attr,omitempty"` + AvgSubtotal bool `xml:"avgSubtotal,attr,omitempty"` + MaxSubtotal bool `xml:"maxSubtotal,attr,omitempty"` + MinSubtotal bool `xml:"minSubtotal,attr,omitempty"` + ProductSubtotal bool `xml:"productSubtotal,attr,omitempty"` + CountSubtotal bool `xml:"countSubtotal,attr,omitempty"` + StdDevSubtotal bool `xml:"stdDevSubtotal,attr,omitempty"` + StdDevPSubtotal bool `xml:"stdDevPSubtotal,attr,omitempty"` + VarSubtotal bool `xml:"varSubtotal,attr,omitempty"` + VarPSubtotal bool `xml:"varPSubtotal,attr,omitempty"` ShowPropCell bool `xml:"showPropCell,attr,omitempty"` ShowPropTip bool `xml:"showPropTip,attr,omitempty"` ShowPropAsCaption bool `xml:"showPropAsCaption,attr,omitempty"` @@ -179,17 +179,17 @@ type xlsxItems struct { // xlsxItem represents a single item in PivotTable field. type xlsxItem struct { - N string `xml:"n,attr"` - T string `xml:"t,attr"` - H bool `xml:"h,attr"` - S bool `xml:"s,attr"` - SD bool `xml:"sd,attr"` - F bool `xml:"f,attr"` - M bool `xml:"m,attr"` - C bool `xml:"c,attr"` - X int `xml:"x,attr,omitempty"` - D bool `xml:"d,attr"` - E bool `xml:"e,attr"` + N string `xml:"n,attr,omitempty"` + T string `xml:"t,attr,omitempty"` + H bool `xml:"h,attr,omitempty"` + S bool `xml:"s,attr,omitempty"` + SD bool `xml:"sd,attr,omitempty"` + F bool `xml:"f,attr,omitempty"` + M bool `xml:"m,attr,omitempty"` + C bool `xml:"c,attr,omitempty"` + X int `xml:"x,attr,omitempty,omitempty"` + D bool `xml:"d,attr,omitempty"` + E bool `xml:"e,attr,omitempty"` } // xlsxAutoSortScope represents the sorting scope for the PivotTable. @@ -198,8 +198,8 @@ type xlsxAutoSortScope struct { // xlsxRowFields represents the collection of row fields for the PivotTable. type xlsxRowFields struct { - Count int `xml:"count,attr"` - Fields []*xlsxField `xml:"fields"` + Count int `xml:"count,attr"` + Field []*xlsxField `xml:"field"` } // xlsxField represents a generic field that can appear either on the column @@ -224,14 +224,13 @@ type xlsxI struct { // xlsxX represents an array of indexes to cached shared item values. type xlsxX struct { - XMLName xml.Name `xml:"x"` } // xlsxColFields represents the collection of fields that are on the column // axis of the PivotTable. type xlsxColFields struct { - Count int `xml:"count,attr"` - Fields []*xlsxField `xml:"fields"` + Count int `xml:"count,attr"` + Field []*xlsxField `xml:"field"` } // xlsxColItems represents the collection of column items of the PivotTable. @@ -261,8 +260,8 @@ type xlsxPageField struct { // xlsxDataFields represents the collection of items in the data region of the // PivotTable. type xlsxDataFields struct { - Count int `xml:"count,attr"` - DataField *xlsxDataField `xml:"dataField"` + Count int `xml:"count,attr"` + DataField []*xlsxDataField `xml:"dataField"` } // xlsxDataField represents a field from a source list, table, or database @@ -270,10 +269,10 @@ type xlsxDataFields struct { type xlsxDataField struct { Name string `xml:"name,attr,omitempty"` Fld int `xml:"fld,attr"` - Subtotal string `xml:"subtotal,attr"` - ShowDataAs string `xml:"showDataAs,attr"` - BaseField int `xml:"baseField,attr"` - BaseItem int64 `xml:"baseItem,attr"` + Subtotal string `xml:"subtotal,attr,omitempty"` + ShowDataAs string `xml:"showDataAs,attr,omitempty"` + BaseField int `xml:"baseField,attr,omitempty"` + BaseItem int64 `xml:"baseItem,attr,omitempty"` NumFmtId string `xml:"numFmtId,attr,omitempty"` ExtLst *xlsxExtLst `xml:"extLst"` } @@ -289,7 +288,7 @@ type xlsxPivotTableStyleInfo struct { Name string `xml:"name,attr"` ShowRowHeaders bool `xml:"showRowHeaders,attr"` ShowColHeaders bool `xml:"showColHeaders,attr"` - ShowRowStripes bool `xml:"showRowStripes,attr"` - ShowColStripes bool `xml:"showColStripes,attr"` + ShowRowStripes bool `xml:"showRowStripes,attr,omitempty"` + ShowColStripes bool `xml:"showColStripes,attr,omitempty"` ShowLastColumn bool `xml:"showLastColumn,attr,omitempty"` } From 3280e1b68664e12143cbd2b3a408f9f494a72897 Mon Sep 17 00:00:00 2001 From: Christian Fiedler Date: Sun, 22 Sep 2019 14:52:01 +0200 Subject: [PATCH 150/957] Allow access to more formula attributes in SetCellFormula (#484) * Allow access to more formula attributes in SetCellFormula Make SetCellFormula variadic to not break API. The new arguments are option arguments in which the type of the formula and the ref attribute may be set. These need to be set for an array formula to work. * Add TestWriteArrayFormula to test optional parameters of SetCellFormula TestWriteArrayFormula writes a document to the test directory that contains array formulas that are used to calculate standard deviations. The file also contains values calculated by the Go testcase, so the results can be verified. It should be tested, if the array formula works (i.e. shows a number, not an error) and that the values calculated by the formula and those calculated by Go are the same. --- cell.go | 19 ++++++++- excelize_test.go | 107 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+), 1 deletion(-) diff --git a/cell.go b/cell.go index 1da46aa347..f9868de4cc 100644 --- a/cell.go +++ b/cell.go @@ -273,9 +273,15 @@ func (f *File) GetCellFormula(sheet, axis string) (string, error) { }) } +// FormulaOpts can be passed to SetCellFormula to use other formula types. +type FormulaOpts struct { + Type *string // Formula type + Ref *string // Shared formula ref +} + // SetCellFormula provides a function to set cell formula by given string and // worksheet name. -func (f *File) SetCellFormula(sheet, axis, formula string) error { +func (f *File) SetCellFormula(sheet, axis, formula string, opts ...FormulaOpts) error { xlsx, err := f.workSheetReader(sheet) if err != nil { return err @@ -295,6 +301,17 @@ func (f *File) SetCellFormula(sheet, axis, formula string) error { } else { cellData.F = &xlsxF{Content: formula} } + + for _, o := range opts { + if o.Type != nil { + cellData.F.T = *o.Type + } + + if o.Ref != nil { + cellData.F.Ref = *o.Ref + } + } + return err } diff --git a/excelize_test.go b/excelize_test.go index a5d7671baf..daf9e7da39 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -8,6 +8,7 @@ import ( _ "image/jpeg" _ "image/png" "io/ioutil" + "math" "os" "path/filepath" "strconv" @@ -397,6 +398,112 @@ func TestMergeCell(t *testing.T) { assert.NoError(t, f.SaveAs(filepath.Join("test", "TestMergeCell.xlsx"))) } +// TestWriteArrayFormula tests the extended options of SetCellFormula by writing an array function +// to a workbook. In the resulting file, the lines 2 and 3 as well as 4 and 5 should have matching +// contents. +func TestWriteArrayFormula(t *testing.T) { + cell := func(col, row int) string { + c, err := CoordinatesToCellName(col, row) + if err != nil { + t.Fatal(err) + } + + return c + } + + f := NewFile() + + sample := []string{"Sample 1", "Sample 2", "Sample 3"} + values := []int{1855, 1709, 1462, 1115, 1524, 625, 773, 126, 1027, 1696, 1078, 1917, 1109, 1753, 1884, 659, 994, 1911, 1925, 899, 196, 244, 1488, 1056, 1986, 66, 784, 725, 767, 1722, 1541, 1026, 1455, 264, 1538, 877, 1581, 1098, 383, 762, 237, 493, 29, 1923, 474, 430, 585, 688, 308, 200, 1259, 622, 798, 1048, 996, 601, 582, 332, 377, 805, 250, 1860, 1360, 840, 911, 1346, 1651, 1651, 665, 584, 1057, 1145, 925, 1752, 202, 149, 1917, 1398, 1894, 818, 714, 624, 1085, 1566, 635, 78, 313, 1686, 1820, 494, 614, 1913, 271, 1016, 338, 1301, 489, 1733, 1483, 1141} + assoc := []int{2, 0, 0, 0, 0, 1, 1, 0, 0, 1, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 1, 0, 2, 0, 2, 1, 2, 2, 2, 1, 0, 1, 0, 1, 1, 2, 0, 2, 1, 0, 2, 1, 0, 1, 0, 0, 2, 0, 2, 2, 1, 2, 2, 1, 2, 2, 1, 2, 1, 2, 2, 1, 1, 1, 0, 1, 0, 2, 0, 0, 1, 2, 1, 0, 1, 0, 0, 2, 1, 1, 2, 0, 2, 1, 0, 2, 2, 2, 1, 0, 0, 1, 1, 1, 2, 0, 2, 0, 1, 1} + if len(values) != len(assoc) { + t.Fatal("values and assoc must be of same length") + } + + // Average calculates the average of the n-th sample (0 <= n < len(sample)). + average := func(n int) int { + sum := 0 + count := 0 + for i := 0; i != len(values); i++ { + if assoc[i] == n { + sum += values[i] + count++ + } + } + + return int(math.Round(float64(sum) / float64(count))) + } + + // Stdev calculates the standard deviation of the n-th sample (0 <= n < len(sample)). + stdev := func(n int) int { + avg := average(n) + + sum := 0 + count := 0 + for i := 0; i != len(values); i++ { + if assoc[i] == n { + sum += (values[i] - avg) * (values[i] - avg) + count++ + } + } + + return int(math.Round(math.Sqrt(float64(sum) / float64(count)))) + } + + // Line 2 contains the results of AVERAGEIF + f.SetCellStr("Sheet1", "A2", "Average") + + // Line 3 contains the average that was calculated in Go + f.SetCellStr("Sheet1", "A3", "Average (calculated)") + + // Line 4 contains the results of the array function that calculates the standard deviation + f.SetCellStr("Sheet1", "A4", "Std. deviation") + + // Line 5 contains the standard deviations calculated in Go + f.SetCellStr("Sheet1", "A5", "Std. deviation (calculated)") + + f.SetCellStr("Sheet1", "B1", sample[0]) + f.SetCellStr("Sheet1", "C1", sample[1]) + f.SetCellStr("Sheet1", "D1", sample[2]) + + firstResLine := 8 + f.SetCellStr("Sheet1", cell(1, firstResLine-1), "Result Values") + f.SetCellStr("Sheet1", cell(2, firstResLine-1), "Sample") + + for i := 0; i != len(values); i++ { + valCell := cell(1, i+firstResLine) + assocCell := cell(2, i+firstResLine) + + f.SetCellInt("Sheet1", valCell, values[i]) + f.SetCellStr("Sheet1", assocCell, sample[assoc[i]]) + } + + valRange := fmt.Sprintf("$A$%d:$A$%d", firstResLine, len(values)+firstResLine-1) + assocRange := fmt.Sprintf("$B$%d:$B$%d", firstResLine, len(values)+firstResLine-1) + + for i := 0; i != len(sample); i++ { + nameCell := cell(i+2, 1) + avgCell := cell(i+2, 2) + calcAvgCell := cell(i+2, 3) + stdevCell := cell(i+2, 4) + calcStdevCell := cell(i+2, 5) + + f.SetCellInt("Sheet1", calcAvgCell, average(i)) + f.SetCellInt("Sheet1", calcStdevCell, stdev(i)) + + // Average can be done with AVERAGEIF + f.SetCellFormula("Sheet1", avgCell, fmt.Sprintf("ROUND(AVERAGEIF(%s,%s,%s),0)", assocRange, nameCell, valRange)) + + ref := stdevCell + ":" + stdevCell + t := STCellFormulaTypeArray + // Use an array formula for standard deviation + f.SetCellFormula("Sheet1", stdevCell, fmt.Sprintf("ROUND(STDEVP(IF(%s=%s,%s)),0)", assocRange, nameCell, valRange), + FormulaOpts{}, FormulaOpts{Type: &t}, FormulaOpts{Ref: &ref}) + } + + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestWriteArrayFormula.xlsx"))) +} + func TestSetCellStyleAlignment(t *testing.T) { f, err := prepareTestBook1() if !assert.NoError(t, err) { From 75d66a03f33f25c29167c5f75ee8a4cc58598420 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 23 Sep 2019 21:50:03 +0800 Subject: [PATCH 151/957] Fix #482, font strike style support --- excelize_test.go | 2 +- styles.go | 4 ++++ xmlStyles.go | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/excelize_test.go b/excelize_test.go index daf9e7da39..7b6b674f18 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -775,7 +775,7 @@ func TestSetCellStyleFont(t *testing.T) { assert.NoError(t, f.SetCellStyle("Sheet2", "A4", "A4", style)) - style, err = f.NewStyle(`{"font":{"color":"#777777"}}`) + style, err = f.NewStyle(`{"font":{"color":"#777777","strike":true}}`) if !assert.NoError(t, err) { t.FailNow() } diff --git a/styles.go b/styles.go index 4d6071a9ba..3244be240a 100644 --- a/styles.go +++ b/styles.go @@ -1993,6 +1993,10 @@ func (f *File) setFont(formatStyle *formatStyle) *xlsxFont { if fnt.Name.Val == "" { fnt.Name.Val = f.GetDefaultFont() } + if formatStyle.Font.Strike { + strike := true + fnt.Strike = &strike + } val, ok := fontUnderlineType[formatStyle.Font.Underline] if ok { fnt.U = &attrValString{Val: val} diff --git a/xmlStyles.go b/xmlStyles.go index 5823bc99f1..7e02d6eaaf 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -320,6 +320,7 @@ type formatFont struct { Underline string `json:"underline"` Family string `json:"family"` Size float64 `json:"size"` + Strike bool `json:"strike"` Color string `json:"color"` } From a34d3b8c86d67d3ad0bc0dbedb69d3b4ebbc210f Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 24 Sep 2019 21:53:19 +0800 Subject: [PATCH 152/957] Compatibility improvement --- chart.go | 2 +- col.go | 10 +++++++--- col_test.go | 35 ++++++++++++----------------------- picture.go | 2 +- rows.go | 8 ++++++-- sparkline.go | 10 ++-------- test/Book1.xlsx | Bin 20899 -> 20750 bytes xmlChart.go | 3 ++- xmlStyles.go | 10 +++++----- xmlWorksheet.go | 10 +++------- 10 files changed, 39 insertions(+), 51 deletions(-) diff --git a/chart.go b/chart.go index db2df1e00f..7db7eeee5f 100644 --- a/chart.go +++ b/chart.go @@ -1845,7 +1845,7 @@ func (f *File) addDrawingChart(sheet, drawingXML, cell string, width, height, rI graphicFrame := xlsxGraphicFrame{ NvGraphicFramePr: xlsxNvGraphicFramePr{ CNvPr: &xlsxCNvPr{ - ID: f.countCharts() + f.countMedia() + 1, + ID: len(content.OneCellAnchor) + len(content.TwoCellAnchor) + 2, Name: "Chart " + strconv.Itoa(cNvPrID), }, }, diff --git a/col.go b/col.go index ffa0ca6d66..be08c2906f 100644 --- a/col.go +++ b/col.go @@ -10,6 +10,7 @@ package excelize import ( + "errors" "math" "strings" ) @@ -112,19 +113,22 @@ func (f *File) GetColOutlineLevel(sheet, col string) (uint8, error) { for c := range xlsx.Cols.Col { colData := &xlsx.Cols.Col[c] if colData.Min <= colNum && colNum <= colData.Max { - level = colData.OutlineLevel + level = colData.OutlineLevel + 1 } } return level, err } // SetColOutlineLevel provides a function to set outline level of a single -// column by given worksheet name and column name. For example, set outline -// level of column D in Sheet1 to 2: +// 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) // func (f *File) SetColOutlineLevel(sheet, col string, level uint8) error { + if level > 7 || level < 1 { + return errors.New("invalid outline level") + } colNum, err := ColumnNameToNumber(col) if err != nil { return err diff --git a/col_test.go b/col_test.go index e3164d49db..a696caad65 100644 --- a/col_test.go +++ b/col_test.go @@ -10,9 +10,7 @@ import ( func TestColumnVisibility(t *testing.T) { t.Run("TestBook1", func(t *testing.T) { f, err := prepareTestBook1() - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, err) assert.NoError(t, f.SetColVisible("Sheet1", "F", false)) assert.NoError(t, f.SetColVisible("Sheet1", "F", true)) @@ -38,9 +36,7 @@ func TestColumnVisibility(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.GetColVisible("Sheet1", "B") }) } @@ -49,12 +45,14 @@ func TestOutlineLevel(t *testing.T) { f := NewFile() f.GetColOutlineLevel("Sheet1", "D") f.NewSheet("Sheet2") - f.SetColOutlineLevel("Sheet1", "D", 4) + assert.NoError(t, f.SetColOutlineLevel("Sheet1", "D", 4)) f.GetColOutlineLevel("Sheet1", "D") f.GetColOutlineLevel("Shee2", "A") - f.SetColWidth("Sheet2", "A", "D", 13) - f.SetColOutlineLevel("Sheet2", "B", 2) - f.SetRowOutlineLevel("Sheet1", 2, 250) + assert.NoError(t, f.SetColWidth("Sheet2", "A", "D", 13)) + assert.NoError(t, f.SetColOutlineLevel("Sheet2", "B", 2)) + assert.NoError(t, f.SetRowOutlineLevel("Sheet1", 2, 7)) + assert.EqualError(t, f.SetColOutlineLevel("Sheet1", "D", 8), "invalid outline level") + assert.EqualError(t, f.SetRowOutlineLevel("Sheet1", 2, 8), "invalid outline level") // Test set and get column outline level with illegal cell coordinates. assert.EqualError(t, f.SetColOutlineLevel("Sheet1", "*", 1), `invalid column name "*"`) @@ -67,7 +65,7 @@ func TestOutlineLevel(t *testing.T) { assert.EqualError(t, f.SetRowOutlineLevel("Sheet1", 0, 1), "invalid row number 0") level, err := f.GetRowOutlineLevel("Sheet1", 2) assert.NoError(t, err) - assert.Equal(t, uint8(250), level) + assert.Equal(t, uint8(7), level) _, err = f.GetRowOutlineLevel("Sheet1", 0) assert.EqualError(t, err, `invalid row number 0`) @@ -76,15 +74,10 @@ func TestOutlineLevel(t *testing.T) { assert.NoError(t, err) assert.Equal(t, uint8(0), level) - err = f.SaveAs(filepath.Join("test", "TestOutlineLevel.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestOutlineLevel.xlsx"))) f, err = OpenFile(filepath.Join("test", "Book1.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, err) f.SetColOutlineLevel("Sheet2", "B", 2) } @@ -138,11 +131,7 @@ func TestInsertCol(t *testing.T) { f.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External") f.MergeCell(sheet1, "A1", "C3") - err := f.AutoFilter(sheet1, "A2", "B2", `{"column":"B","expression":"x != blanks"}`) - if !assert.NoError(t, err) { - t.FailNow() - } - + assert.NoError(t, f.AutoFilter(sheet1, "A2", "B2", `{"column":"B","expression":"x != blanks"}`)) assert.NoError(t, f.InsertCol(sheet1, "A")) // Test insert column with illegal cell coordinates. diff --git a/picture.go b/picture.go index 518463a5df..4470fa177a 100644 --- a/picture.go +++ b/picture.go @@ -272,7 +272,7 @@ func (f *File) addDrawingPicture(sheet, drawingXML, cell, file string, width, he twoCellAnchor.To = &to pic := xlsxPic{} pic.NvPicPr.CNvPicPr.PicLocks.NoChangeAspect = formatSet.NoChangeAspect - pic.NvPicPr.CNvPr.ID = f.countCharts() + f.countMedia() + 1 + pic.NvPicPr.CNvPr.ID = len(content.OneCellAnchor) + len(content.TwoCellAnchor) + 2 pic.NvPicPr.CNvPr.Descr = file pic.NvPicPr.CNvPr.Name = "Picture " + strconv.Itoa(cNvPrID) if hyperlinkRID != 0 { diff --git a/rows.go b/rows.go index 6281e625cd..379644110c 100644 --- a/rows.go +++ b/rows.go @@ -11,6 +11,7 @@ package excelize import ( "encoding/xml" + "errors" "fmt" "math" "strconv" @@ -257,8 +258,8 @@ func (f *File) GetRowVisible(sheet string, row int) (bool, error) { } // SetRowOutlineLevel provides a function to set outline level number of a -// single row by given worksheet name and Excel row number. For example, -// outline row 2 in Sheet1 to level 1: +// 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) // @@ -266,6 +267,9 @@ func (f *File) SetRowOutlineLevel(sheet string, row int, level uint8) error { if row < 1 { return newInvalidRowNumberError(row) } + if level > 7 || level < 1 { + return errors.New("invalid outline level") + } xlsx, err := f.workSheetReader(sheet) if err != nil { return err diff --git a/sparkline.go b/sparkline.go index 314ea834f0..b09dbf4051 100644 --- a/sparkline.go +++ b/sparkline.go @@ -429,11 +429,6 @@ func (f *File) AddSparkline(sheet string, opt *SparklineOption) error { return err } for idx, ext := range decodeExtLst.Ext { - // hack: add back missing namespace - decodeExtLst.Ext[idx].XMLNSX14 = decodeExtLst.Ext[idx].X14 - decodeExtLst.Ext[idx].XMLNSX15 = decodeExtLst.Ext[idx].X15 - decodeExtLst.Ext[idx].XMLNSX14 = "" - decodeExtLst.Ext[idx].XMLNSX15 = "" if ext.URI == ExtURISparklineGroups { decodeSparklineGroups := decodeX14SparklineGroups{} _ = xml.Unmarshal([]byte(ext.Content), &decodeSparklineGroups) @@ -458,9 +453,8 @@ func (f *File) AddSparkline(sheet string, opt *SparklineOption) error { } sparklineGroupsBytes, _ := xml.Marshal(groups) extLst := xlsxWorksheetExt{ - XMLNSX14: NameSpaceSpreadSheetX14, - URI: ExtURISparklineGroups, - Content: string(sparklineGroupsBytes), + URI: ExtURISparklineGroups, + Content: string(sparklineGroupsBytes), } extBytes, _ := xml.Marshal(extLst) ws.ExtLst.Ext = string(extBytes) diff --git a/test/Book1.xlsx b/test/Book1.xlsx index 78431dceaa3c5bd2ee952b1bbbf8482fdc5cd39f..d5a059121b7d591b7fab2dc10f7ed6202255dc28 100644 GIT binary patch delta 1143 zcmZ3yn6YmWBVT|wGm8iV2L}g3QiX;8Mm|<177$~zIFmdhgk{Bi5X$<`QU_%ba7||7d{lzc$-bdwV+x}#T7O; zOK?GroG4k!3R5q?SyNt~1>z7NCP}UOl4G^aT?G@gft#y|JW`^+Q zc;kIgRf?u{%n0#q7A8>fca|bhH?lb>*8Aq)HsEP{|68=_U2hJ*Z?l5m#bwgdP6w3y zwK#1g%stV?f70_O_xra^z7nk`yJ512SKW_KQ}^t+8-2L0kzXqI+-1*0k`}E?%Wluv zZ&SZ_R-xFUlBFgu6rQtl{3-j{sT=iWH&fwpjt5TXR!h9zDc~5IkUArQx3A3X#oC@J zzt(cpZ)J~c<5GxH-lQ^J%-!=z`mEXa1*WR{gAcec|+>3paj$`Q@_g15@tSPZ9;f>Md4XduEvXy3FEJ z*1wswn0F7~KI6#i_m{rCC2_8Y=cD&Z7Oq`iyR|mCrVD^c#h(`Kb2%k?Msh zN%t#syL{#^eZMt-!S>K4@0V?s6kV({Z`bk}pI@t3zLGM3dvreY zvtQQ#FCNwWn|gQGvh1Zd&YQB<#V%uh`l!atYJTsz^yBNE`*}&Z_AiRAGUN2-Sao6L zvFcB$uGW_JD{XIX`okOG&CcN%qPO-wBLjmZ3j+gcCScgS!>J9DE+&6-Q{J5F(hJQL z-`oyCQ_mhRTSzkc>tzaIn0Z@47$x4;5XNS27YO6GwMOACHnb7)NJx|f-rvgxk4C@{!m@b zFvd}TbFkE;0H};pfGr=$X^7C6+#3LO+<^coMv=+41B@W12?k0r&3B$0;3B0H;LXS+ n!hk3?_;zl8TgS}6@Qa&)L4pAp1iJw>h)(VZ6lD|l0!aV>Lm|J7 delta 1290 zcmeBM#JG4dBVT|wGm8iV2L}g(?nFhujeM+3EFi{aaVB|22+NB3Ae8l=r4Gt!WHX1d z_OJ^>Sd;g#t4{vG@gA&c@EFZ4TjshPbBAZRcwV>Ke#T7O;OK?Gr zny)F=fVNEv_y6QOFb^HKGd}veUd4 zerMhFe;40zje>cvCV4HcY@D~#S^nAk`)B!%+2!_DaydBcW(vJ`J-A19^^QgFPF;yP zJbP~a#;z)kw{p906-DjpDqgRm7C0r@wbgC!p)31b55FmE`ZzVQ_tCBIO4GbwG2Rbd zRMtEzDSu+{6+6??6FsXOleCw7I6qf|OJsK5w_6drrUra7Jja%LBhEwn;F`j;=j$C_ znLlyeTiEh=hkm8qZO)VORkxX=uFqeYwb?_&yFxqYXPCtG|eslM))(6~wC z)y#82Wj78>y;yfK{#Gqdec0~21=BZd4`!6!t@+sR(IYF%-G1lRO}2YHp)K%a0^hBr z!fZ=8L}p%Gvr+6%#G%uEX6M)}^=*2IC=&xi1S3@=j%W1*K7gmK@?8p4qCc7ZU`ygea|%}~Z9A3KPE6O7T~V-FEI4r6fn zLe==d7=6A_8_xLpLKMsTIYAi7ey$M48b7G6cQA&5zd1xI+aD^m*58&7T=IZI02~05 zfB5S|6ln)YF|Bl(Y~Z9iIVXSzB31(wGjW-m?P; diff --git a/xmlChart.go b/xmlChart.go index bb4b4bc8b1..19e86e2c96 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -293,9 +293,10 @@ type cAutoTitleDeleted struct { type cView3D struct { RotX *attrValInt `xml:"rotX"` RotY *attrValInt `xml:"rotY"` + RAngAx *attrValInt `xml:"rAngAx"` DepthPercent *attrValInt `xml:"depthPercent"` Perspective *attrValInt `xml:"perspective"` - RAngAx *attrValInt `xml:"rAngAx"` + ExtLst *xlsxExtLst `xml:"extLst"` } // cPlotArea directly maps the plotArea element. This element specifies the diff --git a/xmlStyles.go b/xmlStyles.go index 7e02d6eaaf..16a89abdec 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -85,9 +85,6 @@ type xlsxFonts struct { // xlsxFont directly maps the font element. This element defines the // properties for one of the fonts used in this workbook. type xlsxFont struct { - Name *attrValString `xml:"name"` - Charset *attrValInt `xml:"charset"` - Family *attrValInt `xml:"family"` B *bool `xml:"b,omitempty"` I *bool `xml:"i,omitempty"` Strike *bool `xml:"strike,omitempty"` @@ -95,9 +92,12 @@ type xlsxFont struct { Shadow *bool `xml:"shadow,omitempty"` Condense *bool `xml:"condense,omitempty"` Extend *bool `xml:"extend,omitempty"` - Color *xlsxColor `xml:"color"` - Sz *attrValFloat `xml:"sz"` U *attrValString `xml:"u"` + Sz *attrValFloat `xml:"sz"` + Color *xlsxColor `xml:"color"` + Name *attrValString `xml:"name"` + Family *attrValInt `xml:"family"` + Charset *attrValInt `xml:"charset"` Scheme *attrValString `xml:"scheme"` } diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 7e8cfde550..fa07974272 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -629,13 +629,9 @@ type xlsxLegacyDrawing struct { // xlsxWorksheetExt directly maps the ext element in the worksheet. type xlsxWorksheetExt struct { - XMLName xml.Name `xml:"ext"` - XMLNSX14 string `xml:"xmlns:x14,attr,omitempty"` - XMLNSX15 string `xml:"xmlns:x15,attr,omitempty"` - X14 string `xml:"x14,attr,omitempty"` - X15 string `xml:"x15,attr,omitempty"` - URI string `xml:"uri,attr"` - Content string `xml:",innerxml"` + XMLName xml.Name `xml:"ext"` + URI string `xml:"uri,attr"` + Content string `xml:",innerxml"` } // decodeWorksheetExt directly maps the ext element. From 475fbf3856dd83c4813874570ae5ae2cb48ed421 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 25 Sep 2019 11:12:16 +0800 Subject: [PATCH 153/957] Create SECURITY.md --- SECURITY.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000..9d032ded6a --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,9 @@ +# Security Policy + +## Supported Versions + +We will dive into any security-related issue as long as your Excelize version is still supported by us. When reporting an issue, include as much information as possible, but no need to fill fancy forms or answer tedious questions. Just tell us what you found, how to reproduce it, and any concerns you have about it. We will respond as soon as possible and follow up with any missing information. + +## Reporting a Vulnerability + +Please e-mail us directly at `xuri.me@gmail.com` or use the security issue template on GitHub. In general, public disclosure is made after the issue has been fully identified and a patch is ready to be released. A security issue gets the highest priority assigned and a reply regarding the vulnerability is given within a typical 24 hours. Thank you! From eb520ae27757d4bca276fa2894bf461d75df9b37 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 26 Sep 2019 22:28:14 +0800 Subject: [PATCH 154/957] Improve compatibility for charts --- chart.go | 83 +++++++++++---------------------------------------- chart_test.go | 2 +- col.go | 2 +- picture.go | 2 +- shape.go | 13 ++++---- xmlChart.go | 2 +- 6 files changed, 29 insertions(+), 75 deletions(-) diff --git a/chart.go b/chart.go index 7db7eeee5f..7d40405a26 100644 --- a/chart.go +++ b/chart.go @@ -179,59 +179,11 @@ var ( Contour: 0, WireframeContour: 0, } - chartView3DDepthPercent = map[string]int{ - Area: 100, - AreaStacked: 100, - AreaPercentStacked: 100, - Area3D: 100, - Area3DStacked: 100, - Area3DPercentStacked: 100, - Bar: 100, - BarStacked: 100, - BarPercentStacked: 100, - Bar3DClustered: 100, - Bar3DStacked: 100, - Bar3DPercentStacked: 100, - Bar3DConeClustered: 100, - Bar3DConeStacked: 100, - Bar3DConePercentStacked: 100, - Bar3DPyramidClustered: 100, - Bar3DPyramidStacked: 100, - Bar3DPyramidPercentStacked: 100, - Bar3DCylinderClustered: 100, - Bar3DCylinderStacked: 100, - Bar3DCylinderPercentStacked: 100, - Col: 100, - ColStacked: 100, - ColPercentStacked: 100, - Col3D: 100, - Col3DClustered: 100, - Col3DStacked: 100, - Col3DPercentStacked: 100, - Col3DCone: 100, - Col3DConeClustered: 100, - Col3DConeStacked: 100, - Col3DConePercentStacked: 100, - Col3DPyramid: 100, - Col3DPyramidClustered: 100, - Col3DPyramidStacked: 100, - Col3DPyramidPercentStacked: 100, - Col3DCylinder: 100, - Col3DCylinderClustered: 100, - Col3DCylinderStacked: 100, - Col3DCylinderPercentStacked: 100, - Doughnut: 100, - Line: 100, - Pie: 100, - Pie3D: 100, - Radar: 100, - Scatter: 100, - Surface3D: 100, - WireframeSurface3D: 100, - Contour: 100, - WireframeContour: 100, - Bubble: 100, - Bubble3D: 100, + plotAreaChartOverlap = map[string]int{ + BarStacked: 100, + BarPercentStacked: 100, + ColStacked: 100, + ColPercentStacked: 100, } chartView3DPerspective = map[string]int{ Contour: 0, @@ -842,11 +794,10 @@ func (f *File) addChart(formatSet *formatChart) { }, }, View3D: &cView3D{ - RotX: &attrValInt{Val: chartView3DRotX[formatSet.Type]}, - RotY: &attrValInt{Val: chartView3DRotY[formatSet.Type]}, - DepthPercent: &attrValInt{Val: chartView3DDepthPercent[formatSet.Type]}, - Perspective: &attrValInt{Val: chartView3DPerspective[formatSet.Type]}, - RAngAx: &attrValInt{Val: chartView3DRAngAx[formatSet.Type]}, + RotX: &attrValInt{Val: chartView3DRotX[formatSet.Type]}, + RotY: &attrValInt{Val: chartView3DRotY[formatSet.Type]}, + Perspective: &attrValInt{Val: chartView3DPerspective[formatSet.Type]}, + RAngAx: &attrValInt{Val: chartView3DRAngAx[formatSet.Type]}, }, Floor: &cThicknessSpPr{ Thickness: &attrValInt{Val: 0}, @@ -980,6 +931,7 @@ func (f *File) drawBaseChart(formatSet *formatChart) *cPlotArea { {Val: 754001152}, {Val: 753999904}, }, + Overlap: &attrValInt{Val: 100}, } var ok bool if c.BarDir.Val, ok = plotAreaChartBarDir[formatSet.Type]; !ok { @@ -988,8 +940,8 @@ func (f *File) drawBaseChart(formatSet *formatChart) *cPlotArea { if c.Grouping.Val, ok = plotAreaChartGrouping[formatSet.Type]; !ok { c.Grouping = nil } - if strings.HasSuffix(formatSet.Type, "Stacked") { - c.Overlap = &attrValInt{Val: 100} + if c.Overlap.Val, ok = plotAreaChartOverlap[formatSet.Type]; !ok { + c.Overlap = nil } catAx := f.drawPlotAreaCatAx(formatSet) valAx := f.drawPlotAreaValAx(formatSet) @@ -1485,7 +1437,7 @@ func (f *File) drawChartSeriesCat(v formatChartSeries, formatSet *formatChart) * F: v.Categories, }, } - chartSeriesCat := map[string]*cCat{Scatter: nil} + chartSeriesCat := map[string]*cCat{Scatter: nil, Bubble: nil, Bubble3D: nil} if _, ok := chartSeriesCat[formatSet.Type]; ok { return nil } @@ -1500,7 +1452,7 @@ func (f *File) drawChartSeriesVal(v formatChartSeries, formatSet *formatChart) * F: v.Values, }, } - chartSeriesVal := map[string]*cVal{Scatter: nil} + chartSeriesVal := map[string]*cVal{Scatter: nil, Bubble: nil, Bubble3D: nil} if _, ok := chartSeriesVal[formatSet.Type]; ok { return nil } @@ -1783,7 +1735,6 @@ func (f *File) drawPlotAreaTxPr() *cTxPr { // deserialization, two different structures: decodeWsDr and encodeWsDr are // defined. func (f *File) drawingParser(path string) (*xlsxWsDr, int) { - cNvPrID := 1 if f.Drawings[path] == nil { content := xlsxWsDr{} content.A = NameSpaceDrawingML @@ -1793,7 +1744,6 @@ func (f *File) drawingParser(path string) (*xlsxWsDr, int) { decodeWsDr := decodeWsDr{} _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML(path)), &decodeWsDr) content.R = decodeWsDr.R - cNvPrID = len(decodeWsDr.OneCellAnchor) + len(decodeWsDr.TwoCellAnchor) + 1 for _, v := range decodeWsDr.OneCellAnchor { content.OneCellAnchor = append(content.OneCellAnchor, &xdrCellAnchor{ EditAs: v.EditAs, @@ -1809,7 +1759,8 @@ func (f *File) drawingParser(path string) (*xlsxWsDr, int) { } f.Drawings[path] = &content } - return f.Drawings[path], cNvPrID + wsDr := f.Drawings[path] + return wsDr, len(wsDr.OneCellAnchor) + len(wsDr.TwoCellAnchor) + 2 } // addDrawingChart provides a function to add chart graphic frame by given @@ -1845,7 +1796,7 @@ func (f *File) addDrawingChart(sheet, drawingXML, cell string, width, height, rI graphicFrame := xlsxGraphicFrame{ NvGraphicFramePr: xlsxNvGraphicFramePr{ CNvPr: &xlsxCNvPr{ - ID: len(content.OneCellAnchor) + len(content.TwoCellAnchor) + 2, + ID: cNvPrID, Name: "Chart " + strconv.Itoa(cNvPrID), }, }, diff --git a/chart_test.go b/chart_test.go index c0bae33d95..932e873328 100644 --- a/chart_test.go +++ b/chart_test.go @@ -126,7 +126,7 @@ func TestAddChart(t *testing.T) { 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 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"}`)) + 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", "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"}`)) diff --git a/col.go b/col.go index be08c2906f..5d4e764b19 100644 --- a/col.go +++ b/col.go @@ -113,7 +113,7 @@ func (f *File) GetColOutlineLevel(sheet, col string) (uint8, error) { for c := range xlsx.Cols.Col { colData := &xlsx.Cols.Col[c] if colData.Min <= colNum && colNum <= colData.Max { - level = colData.OutlineLevel + 1 + level = colData.OutlineLevel } } return level, err diff --git a/picture.go b/picture.go index 4470fa177a..ff40863369 100644 --- a/picture.go +++ b/picture.go @@ -272,7 +272,7 @@ func (f *File) addDrawingPicture(sheet, drawingXML, cell, file string, width, he twoCellAnchor.To = &to pic := xlsxPic{} pic.NvPicPr.CNvPicPr.PicLocks.NoChangeAspect = formatSet.NoChangeAspect - pic.NvPicPr.CNvPr.ID = len(content.OneCellAnchor) + len(content.TwoCellAnchor) + 2 + pic.NvPicPr.CNvPr.ID = cNvPrID pic.NvPicPr.CNvPr.Descr = file pic.NvPicPr.CNvPr.Name = "Picture " + strconv.Itoa(cNvPrID) if hyperlinkRID != 0 { diff --git a/shape.go b/shape.go index e6a2ff3742..f284e43d61 100644 --- a/shape.go +++ b/shape.go @@ -411,11 +411,6 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, formatSet *format U: u, Sz: p.Font.Size * 100, Latin: &aLatin{Typeface: p.Font.Family}, - SolidFill: &aSolidFill{ - SrgbClr: &attrValString{ - Val: strings.Replace(strings.ToUpper(p.Font.Color), "#", "", -1), - }, - }, }, T: text, }, @@ -423,6 +418,14 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, formatSet *format Lang: "en-US", }, } + srgbClr := strings.Replace(strings.ToUpper(p.Font.Color), "#", "", -1) + if len(srgbClr) == 6 { + paragraph.R.RPr.SolidFill = &aSolidFill{ + SrgbClr: &attrValString{ + Val: srgbClr, + }, + } + } shape.TxBody.P = append(shape.TxBody.P, paragraph) } twoCellAnchor.Sp = &shape diff --git a/xmlChart.go b/xmlChart.go index 19e86e2c96..69e119a662 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -332,8 +332,8 @@ type cCharts struct { VaryColors *attrValBool `xml:"varyColors"` Wireframe *attrValBool `xml:"wireframe"` Ser *[]cSer `xml:"ser"` - Shape *attrValString `xml:"shape"` DLbls *cDLbls `xml:"dLbls"` + Shape *attrValString `xml:"shape"` HoleSize *attrValInt `xml:"holeSize"` Smooth *attrValBool `xml:"smooth"` Overlap *attrValInt `xml:"overlap"` From babfeb6b57ad3e63f68f5e031869efc54c9cfe0b Mon Sep 17 00:00:00 2001 From: jaby Date: Mon, 30 Sep 2019 14:37:52 +0200 Subject: [PATCH 155/957] Add missing ShowZeros SheetViewOption implementation --- sheetview.go | 16 ++++++++++++++-- sheetview_test.go | 14 ++++++++++++++ xmlWorksheet.go | 2 +- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/sheetview.go b/sheetview.go index 09f5789219..8a5091f710 100644 --- a/sheetview.go +++ b/sheetview.go @@ -51,14 +51,18 @@ type ( // visible cell Location of the top left visible cell in the bottom right // pane (when in Left-to-Right mode). TopLeftCell string + // 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 + /* TODO // ShowWhiteSpace is a SheetViewOption. It specifies a flag indicating // whether page layout view shall display margins. False means do not display // left, right, top (header), and bottom (footer) margins (even when there is // data in the header or footer). ShowWhiteSpace bool - // ShowZeros is a SheetViewOption. - ShowZeros bool // WindowProtection is a SheetViewOption. WindowProtection bool */ @@ -106,6 +110,14 @@ func (o *ShowGridLines) getSheetViewOption(view *xlsxSheetView) { *o = ShowGridLines(defaultTrue(view.ShowGridLines)) // 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 ShowRowColHeaders) setSheetViewOption(view *xlsxSheetView) { view.ShowRowColHeaders = boolPtr(bool(o)) } diff --git a/sheetview_test.go b/sheetview_test.go index 2e697b8540..e45b8cec35 100644 --- a/sheetview_test.go +++ b/sheetview_test.go @@ -95,6 +95,7 @@ func ExampleFile_GetSheetViewOptions() { rightToLeft excelize.RightToLeft showFormulas excelize.ShowFormulas showGridLines excelize.ShowGridLines + showZeros excelize.ShowZeros showRowColHeaders excelize.ShowRowColHeaders zoomScale excelize.ZoomScale topLeftCell excelize.TopLeftCell @@ -105,6 +106,7 @@ func ExampleFile_GetSheetViewOptions() { &rightToLeft, &showFormulas, &showGridLines, + &showZeros, &showRowColHeaders, &zoomScale, &topLeftCell, @@ -117,6 +119,7 @@ func ExampleFile_GetSheetViewOptions() { fmt.Println("- rightToLeft:", rightToLeft) fmt.Println("- showFormulas:", showFormulas) fmt.Println("- showGridLines:", showGridLines) + fmt.Println("- showZeros:", showZeros) fmt.Println("- showRowColHeaders:", showRowColHeaders) fmt.Println("- zoomScale:", zoomScale) fmt.Println("- topLeftCell:", `"`+topLeftCell+`"`) @@ -137,8 +140,17 @@ func ExampleFile_GetSheetViewOptions() { panic(err) } + if err := f.SetSheetViewOptions(sheet, 0, excelize.ShowZeros(false)); err != nil { + panic(err) + } + + if err := f.GetSheetViewOptions(sheet, 0, &showZeros); err != nil { + panic(err) + } + fmt.Println("After change:") fmt.Println("- showGridLines:", showGridLines) + fmt.Println("- showZeros:", showZeros) fmt.Println("- topLeftCell:", topLeftCell) // Output: @@ -147,11 +159,13 @@ func ExampleFile_GetSheetViewOptions() { // - rightToLeft: false // - showFormulas: false // - showGridLines: true + // - showZeros: true // - showRowColHeaders: true // - zoomScale: 0 // - topLeftCell: "" // After change: // - showGridLines: false + // - showZeros: false // - topLeftCell: B2 } diff --git a/xmlWorksheet.go b/xmlWorksheet.go index fa07974272..c78d3ef7a7 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -167,7 +167,7 @@ type xlsxSheetView struct { ShowFormulas bool `xml:"showFormulas,attr,omitempty"` ShowGridLines *bool `xml:"showGridLines,attr"` ShowRowColHeaders *bool `xml:"showRowColHeaders,attr"` - ShowZeros bool `xml:"showZeros,attr,omitempty"` + ShowZeros *bool `xml:"showZeros,attr,omitempty"` RightToLeft bool `xml:"rightToLeft,attr,omitempty"` TabSelected bool `xml:"tabSelected,attr,omitempty"` ShowWhiteSpace *bool `xml:"showWhiteSpace,attr"` From 810139f5fc46b1002c0998379b18af3d2feffbb7 Mon Sep 17 00:00:00 2001 From: heiy <287789299@qq.com> Date: Thu, 10 Oct 2019 20:04:33 +0800 Subject: [PATCH 156/957] solve ending space missing --- cell.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cell.go b/cell.go index f9868de4cc..ab42b7237a 100644 --- a/cell.go +++ b/cell.go @@ -229,7 +229,7 @@ func (f *File) SetCellStr(sheet, axis, value string) error { value = value[0:32767] } // Leading space(s) character detection. - if len(value) > 0 && value[0] == 32 { + if len(value) > 0 && (value[0] == 32 || value[len(value)-1] == 32) { cellData.XMLSpace = xml.Attr{ Name: xml.Name{Space: NameSpaceXML, Local: "space"}, Value: "preserve", From 2d21b5b50f30ae9868b2f8b1f7299ceefcf87fd2 Mon Sep 17 00:00:00 2001 From: streboryaj Date: Tue, 15 Oct 2019 09:26:08 -0500 Subject: [PATCH 157/957] Added accessors for Getting/Setting Page Margins (#497) * Added accessors for Getting/Setting Page Margins * Added test cases --- sheet.go | 159 ++++++++++++++++++++++++++++++++++++++++++++++++ sheet_test.go | 62 +++++++++++++++++++ xmlWorksheet.go | 10 +++ 3 files changed, 231 insertions(+) diff --git a/sheet.go b/sheet.go index 951baf9239..ce3e6450fb 100644 --- a/sheet.go +++ b/sheet.go @@ -1401,3 +1401,162 @@ func makeContiguousColumns(xlsx *xlsxWorksheet, fromRow, toRow, colCount int) { fillColumns(rowData, colCount, fromRow) } } + +type ( + PageMarginBottom float64 + PageMarginFooter float64 + PageMarginHeader float64 + PageMarginLeft float64 + PageMarginRight float64 + PageMarginTop float64 +) + +// setPageMargins provides a method to set the bottom margin for the worksheet. +func (p PageMarginBottom) setPageMargins(ps *xlsxPageMargins) { + ps.Bottom = float64(p) +} + +// setPageMargins provides a method to get the bottom margin for the worksheet. +func (o *PageMarginBottom) getPageMargins(ps *xlsxPageMargins) { + // Excel default: portrait + if ps == nil || ps.Bottom == 0 { + *o = 0.75 + return + } + *o = PageMarginBottom(ps.Bottom) +} + +// setPageMargins provides a method to set the Footer margin for the worksheet. +func (p PageMarginFooter) setPageMargins(ps *xlsxPageMargins) { + ps.Footer = float64(p) +} + +// setPageMargins provides a method to get the Footer margin for the worksheet. +func (o *PageMarginFooter) getPageMargins(ps *xlsxPageMargins) { + // Excel default: portrait + if ps == nil || ps.Footer == 0 { + *o = 0.3 + return + } + *o = PageMarginFooter(ps.Footer) +} + +// setPageMargins provides a method to set the Header margin for the worksheet. +func (p PageMarginHeader) setPageMargins(ps *xlsxPageMargins) { + ps.Header = float64(p) +} + +// setPageMargins provides a method to get the Header margin for the worksheet. +func (o *PageMarginHeader) getPageMargins(ps *xlsxPageMargins) { + // Excel default: portrait + if ps == nil || ps.Header == 0 { + *o = 0.3 + return + } + *o = PageMarginHeader(ps.Header) +} + +// setPageMargins provides a method to set the left margin for the worksheet. +func (p PageMarginLeft) setPageMargins(ps *xlsxPageMargins) { + ps.Left = float64(p) +} + +// setPageMargins provides a method to get the left margin for the worksheet. +func (o *PageMarginLeft) getPageMargins(ps *xlsxPageMargins) { + // Excel default: portrait + if ps == nil || ps.Left == 0 { + *o = 0.7 + return + } + *o = PageMarginLeft(ps.Left) +} + +// setPageMargins provides a method to set the right margin for the worksheet. +func (p PageMarginRight) setPageMargins(ps *xlsxPageMargins) { + ps.Right = float64(p) +} + +// setPageMargins provides a method to get the right margin for the worksheet. +func (o *PageMarginRight) getPageMargins(ps *xlsxPageMargins) { + // Excel default: portrait + if ps == nil || ps.Right == 0 { + *o = 0.7 + return + } + *o = PageMarginRight(ps.Right) +} + +// setPageMargins provides a method to set the top margin for the worksheet. +func (p PageMarginTop) setPageMargins(ps *xlsxPageMargins) { + ps.Top = float64(p) +} + +// setPageMargins provides a method to get the top margin for the worksheet. +func (o *PageMarginTop) getPageMargins(ps *xlsxPageMargins) { + // Excel default: portrait + if ps == nil || ps.Top == 0 { + *o = 0.75 + return + } + *o = PageMarginTop(ps.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) +} + +// SetPageMargins provides a function to set worksheet page lmargins. +// +// Available options: +// PageMarginBotom(float64) +// PageMarginFooter(float64) +// PageMarginHeader(float64) +// PageMarginLeft(float64) +// PageMarginRightfloat64) +// PageMarginTop(float64) +func (f *File) SetPageMargins(sheet string, opts ...PageMarginsOptions) error { + s, err := f.workSheetReader(sheet) + if err != nil { + return err + } + ps := s.PageMargins + if ps == nil { + ps = new(xlsxPageMargins) + s.PageMargins = ps + } + + for _, opt := range opts { + opt.setPageMargins(ps) + } + return err +} + +// GetPageMargins provides a function to get worksheet page margins. +// +// Available options: +// PageMarginBotom(float64) +// PageMarginFooter(float64) +// PageMarginHeader(float64) +// PageMarginLeft(float64) +// PageMarginRightfloat64) +// PageMarginTop(float64) +func (f *File) GetPageMargins(sheet string, opts ...PageMarginsOptionsPtr) error { + s, err := f.workSheetReader(sheet) + if err != nil { + return err + } + ps := s.PageMargins + + for _, opt := range opts { + opt.getPageMargins(ps) + } + return err +} diff --git a/sheet_test.go b/sheet_test.go index 51797935b0..6bfa7dcaf6 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -247,3 +247,65 @@ func TestGetSheetMap(t *testing.T) { } assert.Equal(t, len(sheetMap), 2) } + +func TestPageMarginsOption(t *testing.T) { + const sheet = "Sheet1" + + testData := []struct { + container excelize.PageMarginsOptionsPtr + nonDefault excelize.PageMarginsOptions + }{ + {new(excelize.PageMarginTop), excelize.PageMarginTop(1.0)}, + {new(excelize.PageMarginBottom), excelize.PageMarginBottom(1.0)}, + {new(excelize.PageMarginLeft), excelize.PageMarginLeft(1.0)}, + {new(excelize.PageMarginRight), excelize.PageMarginRight(1.0)}, + {new(excelize.PageMarginHeader), excelize.PageMarginHeader(1.0)}, + {new(excelize.PageMarginFooter), excelize.PageMarginFooter(1.0)}, + } + + for i, test := range testData { + t.Run(fmt.Sprintf("TestData%d", i), func(t *testing.T) { + + opt := test.nonDefault + t.Logf("option %T", opt) + + def := deepcopy.Copy(test.container).(excelize.PageMarginsOptionsPtr) + val1 := deepcopy.Copy(def).(excelize.PageMarginsOptionsPtr) + val2 := deepcopy.Copy(def).(excelize.PageMarginsOptionsPtr) + + f := excelize.NewFile() + // Get the default value + assert.NoError(t, f.GetPageMargins(sheet, def), opt) + // Get again and check + assert.NoError(t, f.GetPageMargins(sheet, val1), opt) + if !assert.Equal(t, val1, def, opt) { + t.FailNow() + } + // Set the same value + assert.NoError(t, f.SetPageMargins(sheet, val1), opt) + // 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) { + t.FailNow() + } + // Set a different value + assert.NoError(t, f.SetPageMargins(sheet, test.nonDefault), opt) + assert.NoError(t, f.GetPageMargins(sheet, val1), opt) + // 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) { + 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) { + t.FailNow() + } + // Restore the default value + assert.NoError(t, f.SetPageMargins(sheet, def), opt) + assert.NoError(t, f.GetPageMargins(sheet, val1), opt) + if !assert.Equal(t, def, val1) { + t.FailNow() + } + }) + } +} diff --git a/xmlWorksheet.go b/xmlWorksheet.go index c78d3ef7a7..96ca235782 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -801,3 +801,13 @@ type FormatHeaderFooter struct { FirstFooter string FirstHeader string } + +// FormatPageMargins directly maps the settings of page margins +type FormatPageMargins struct { + Bottom string + Footer string + Header string + Left string + Right string + Top string +} From 2e791fa433def282ee2e7a5049a46fc4a76796cf Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 16 Oct 2019 01:03:29 +0800 Subject: [PATCH 158/957] Optimize code of Getting/Setting Page Margins --- cell.go | 2 +- rows_test.go | 5 ++ sheet.go | 159 --------------------------------------------- sheet_test.go | 62 ------------------ sheetpr.go | 166 +++++++++++++++++++++++++++++++++++++++++++++++ sheetpr_test.go | 161 +++++++++++++++++++++++++++++++++++++++++++++ templates.go | 2 +- xmlPivotCache.go | 4 +- xmlPivotTable.go | 4 +- 9 files changed, 338 insertions(+), 227 deletions(-) diff --git a/cell.go b/cell.go index ab42b7237a..a25f2e4cf3 100644 --- a/cell.go +++ b/cell.go @@ -228,7 +228,7 @@ func (f *File) SetCellStr(sheet, axis, value string) error { if len(value) > 32767 { value = value[0:32767] } - // Leading space(s) character detection. + // Leading and ending space(s) character detection. if len(value) > 0 && (value[0] == 32 || value[len(value)-1] == 32) { cellData.XMLSpace = xml.Attr{ Name: xml.Name{Space: NameSpaceXML, Local: "space"}, diff --git a/rows_test.go b/rows_test.go index d52c635590..a99a594dd3 100644 --- a/rows_test.go +++ b/rows_test.go @@ -669,6 +669,11 @@ func TestDuplicateRowInvalidRownum(t *testing.T) { } } +func TestErrSheetNotExistError(t *testing.T) { + err := ErrSheetNotExist{SheetName: "Sheet1"} + assert.EqualValues(t, err.Error(), "Sheet Sheet1 is not exist") +} + func BenchmarkRows(b *testing.B) { for i := 0; i < b.N; i++ { f, _ := OpenFile(filepath.Join("test", "Book1.xlsx")) diff --git a/sheet.go b/sheet.go index ce3e6450fb..951baf9239 100644 --- a/sheet.go +++ b/sheet.go @@ -1401,162 +1401,3 @@ func makeContiguousColumns(xlsx *xlsxWorksheet, fromRow, toRow, colCount int) { fillColumns(rowData, colCount, fromRow) } } - -type ( - PageMarginBottom float64 - PageMarginFooter float64 - PageMarginHeader float64 - PageMarginLeft float64 - PageMarginRight float64 - PageMarginTop float64 -) - -// setPageMargins provides a method to set the bottom margin for the worksheet. -func (p PageMarginBottom) setPageMargins(ps *xlsxPageMargins) { - ps.Bottom = float64(p) -} - -// setPageMargins provides a method to get the bottom margin for the worksheet. -func (o *PageMarginBottom) getPageMargins(ps *xlsxPageMargins) { - // Excel default: portrait - if ps == nil || ps.Bottom == 0 { - *o = 0.75 - return - } - *o = PageMarginBottom(ps.Bottom) -} - -// setPageMargins provides a method to set the Footer margin for the worksheet. -func (p PageMarginFooter) setPageMargins(ps *xlsxPageMargins) { - ps.Footer = float64(p) -} - -// setPageMargins provides a method to get the Footer margin for the worksheet. -func (o *PageMarginFooter) getPageMargins(ps *xlsxPageMargins) { - // Excel default: portrait - if ps == nil || ps.Footer == 0 { - *o = 0.3 - return - } - *o = PageMarginFooter(ps.Footer) -} - -// setPageMargins provides a method to set the Header margin for the worksheet. -func (p PageMarginHeader) setPageMargins(ps *xlsxPageMargins) { - ps.Header = float64(p) -} - -// setPageMargins provides a method to get the Header margin for the worksheet. -func (o *PageMarginHeader) getPageMargins(ps *xlsxPageMargins) { - // Excel default: portrait - if ps == nil || ps.Header == 0 { - *o = 0.3 - return - } - *o = PageMarginHeader(ps.Header) -} - -// setPageMargins provides a method to set the left margin for the worksheet. -func (p PageMarginLeft) setPageMargins(ps *xlsxPageMargins) { - ps.Left = float64(p) -} - -// setPageMargins provides a method to get the left margin for the worksheet. -func (o *PageMarginLeft) getPageMargins(ps *xlsxPageMargins) { - // Excel default: portrait - if ps == nil || ps.Left == 0 { - *o = 0.7 - return - } - *o = PageMarginLeft(ps.Left) -} - -// setPageMargins provides a method to set the right margin for the worksheet. -func (p PageMarginRight) setPageMargins(ps *xlsxPageMargins) { - ps.Right = float64(p) -} - -// setPageMargins provides a method to get the right margin for the worksheet. -func (o *PageMarginRight) getPageMargins(ps *xlsxPageMargins) { - // Excel default: portrait - if ps == nil || ps.Right == 0 { - *o = 0.7 - return - } - *o = PageMarginRight(ps.Right) -} - -// setPageMargins provides a method to set the top margin for the worksheet. -func (p PageMarginTop) setPageMargins(ps *xlsxPageMargins) { - ps.Top = float64(p) -} - -// setPageMargins provides a method to get the top margin for the worksheet. -func (o *PageMarginTop) getPageMargins(ps *xlsxPageMargins) { - // Excel default: portrait - if ps == nil || ps.Top == 0 { - *o = 0.75 - return - } - *o = PageMarginTop(ps.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) -} - -// SetPageMargins provides a function to set worksheet page lmargins. -// -// Available options: -// PageMarginBotom(float64) -// PageMarginFooter(float64) -// PageMarginHeader(float64) -// PageMarginLeft(float64) -// PageMarginRightfloat64) -// PageMarginTop(float64) -func (f *File) SetPageMargins(sheet string, opts ...PageMarginsOptions) error { - s, err := f.workSheetReader(sheet) - if err != nil { - return err - } - ps := s.PageMargins - if ps == nil { - ps = new(xlsxPageMargins) - s.PageMargins = ps - } - - for _, opt := range opts { - opt.setPageMargins(ps) - } - return err -} - -// GetPageMargins provides a function to get worksheet page margins. -// -// Available options: -// PageMarginBotom(float64) -// PageMarginFooter(float64) -// PageMarginHeader(float64) -// PageMarginLeft(float64) -// PageMarginRightfloat64) -// PageMarginTop(float64) -func (f *File) GetPageMargins(sheet string, opts ...PageMarginsOptionsPtr) error { - s, err := f.workSheetReader(sheet) - if err != nil { - return err - } - ps := s.PageMargins - - for _, opt := range opts { - opt.getPageMargins(ps) - } - return err -} diff --git a/sheet_test.go b/sheet_test.go index 6bfa7dcaf6..51797935b0 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -247,65 +247,3 @@ func TestGetSheetMap(t *testing.T) { } assert.Equal(t, len(sheetMap), 2) } - -func TestPageMarginsOption(t *testing.T) { - const sheet = "Sheet1" - - testData := []struct { - container excelize.PageMarginsOptionsPtr - nonDefault excelize.PageMarginsOptions - }{ - {new(excelize.PageMarginTop), excelize.PageMarginTop(1.0)}, - {new(excelize.PageMarginBottom), excelize.PageMarginBottom(1.0)}, - {new(excelize.PageMarginLeft), excelize.PageMarginLeft(1.0)}, - {new(excelize.PageMarginRight), excelize.PageMarginRight(1.0)}, - {new(excelize.PageMarginHeader), excelize.PageMarginHeader(1.0)}, - {new(excelize.PageMarginFooter), excelize.PageMarginFooter(1.0)}, - } - - for i, test := range testData { - t.Run(fmt.Sprintf("TestData%d", i), func(t *testing.T) { - - opt := test.nonDefault - t.Logf("option %T", opt) - - def := deepcopy.Copy(test.container).(excelize.PageMarginsOptionsPtr) - val1 := deepcopy.Copy(def).(excelize.PageMarginsOptionsPtr) - val2 := deepcopy.Copy(def).(excelize.PageMarginsOptionsPtr) - - f := excelize.NewFile() - // Get the default value - assert.NoError(t, f.GetPageMargins(sheet, def), opt) - // Get again and check - assert.NoError(t, f.GetPageMargins(sheet, val1), opt) - if !assert.Equal(t, val1, def, opt) { - t.FailNow() - } - // Set the same value - assert.NoError(t, f.SetPageMargins(sheet, val1), opt) - // 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) { - t.FailNow() - } - // Set a different value - assert.NoError(t, f.SetPageMargins(sheet, test.nonDefault), opt) - assert.NoError(t, f.GetPageMargins(sheet, val1), opt) - // 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) { - 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) { - t.FailNow() - } - // Restore the default value - assert.NoError(t, f.SetPageMargins(sheet, def), opt) - assert.NoError(t, f.GetPageMargins(sheet, val1), opt) - if !assert.Equal(t, def, val1) { - t.FailNow() - } - }) - } -} diff --git a/sheetpr.go b/sheetpr.go index a273ac1a5f..086bd3a15d 100644 --- a/sheetpr.go +++ b/sheetpr.go @@ -191,3 +191,169 @@ func (f *File) GetSheetPrOptions(name string, opts ...SheetPrOptionPtr) error { } 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) +} + +// SetPageMargins provides a function to set worksheet page margins. +// +// Available options: +// PageMarginBotom(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 + } + pm := s.PageMargins + if pm == nil { + pm = new(xlsxPageMargins) + s.PageMargins = pm + } + + for _, opt := range opts { + opt.setPageMargins(pm) + } + return err +} + +// GetPageMargins provides a function to get worksheet page margins. +// +// Available options: +// PageMarginBotom(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 + } + pm := s.PageMargins + + for _, opt := range opts { + opt.getPageMargins(pm) + } + return err +} diff --git a/sheetpr_test.go b/sheetpr_test.go index 97a314c918..d1ae2f1308 100644 --- a/sheetpr_test.go +++ b/sheetpr_test.go @@ -146,3 +146,164 @@ func TestSheetPrOptions(t *testing.T) { }) } } + +func TestSetSheetrOptions(t *testing.T) { + f := excelize.NewFile() + // Test SetSheetrOptions on not exists worksheet. + assert.EqualError(t, f.SetSheetPrOptions("SheetN"), "sheet SheetN is not exist") +} + +func TestGetSheetPrOptions(t *testing.T) { + f := excelize.NewFile() + // Test GetSheetPrOptions on not exists worksheet. + assert.EqualError(t, f.GetSheetPrOptions("SheetN"), "sheet SheetN is not exist") +} + +var _ = []excelize.PageMarginsOptions{ + excelize.PageMarginBottom(1.0), + excelize.PageMarginFooter(1.0), + excelize.PageMarginHeader(1.0), + excelize.PageMarginLeft(1.0), + excelize.PageMarginRight(1.0), + excelize.PageMarginTop(1.0), +} + +var _ = []excelize.PageMarginsOptionsPtr{ + (*excelize.PageMarginBottom)(nil), + (*excelize.PageMarginFooter)(nil), + (*excelize.PageMarginHeader)(nil), + (*excelize.PageMarginLeft)(nil), + (*excelize.PageMarginRight)(nil), + (*excelize.PageMarginTop)(nil), +} + +func ExampleFile_SetPageMargins() { + f := excelize.NewFile() + const sheet = "Sheet1" + + if err := f.SetPageMargins(sheet, + excelize.PageMarginBottom(1.0), + excelize.PageMarginFooter(1.0), + excelize.PageMarginHeader(1.0), + excelize.PageMarginLeft(1.0), + excelize.PageMarginRight(1.0), + excelize.PageMarginTop(1.0), + ); err != nil { + panic(err) + } + // Output: +} + +func ExampleFile_GetPageMargins() { + f := excelize.NewFile() + const sheet = "Sheet1" + + var ( + marginBottom excelize.PageMarginBottom + marginFooter excelize.PageMarginFooter + marginHeader excelize.PageMarginHeader + marginLeft excelize.PageMarginLeft + marginRight excelize.PageMarginRight + marginTop excelize.PageMarginTop + ) + + if err := f.GetPageMargins(sheet, + &marginBottom, + &marginFooter, + &marginHeader, + &marginLeft, + &marginRight, + &marginTop, + ); err != nil { + panic(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 excelize.PageMarginsOptionsPtr + nonDefault excelize.PageMarginsOptions + }{ + {new(excelize.PageMarginTop), excelize.PageMarginTop(1.0)}, + {new(excelize.PageMarginBottom), excelize.PageMarginBottom(1.0)}, + {new(excelize.PageMarginLeft), excelize.PageMarginLeft(1.0)}, + {new(excelize.PageMarginRight), excelize.PageMarginRight(1.0)}, + {new(excelize.PageMarginHeader), excelize.PageMarginHeader(1.0)}, + {new(excelize.PageMarginFooter), excelize.PageMarginFooter(1.0)}, + } + + for i, test := range testData { + t.Run(fmt.Sprintf("TestData%d", i), func(t *testing.T) { + + opt := test.nonDefault + t.Logf("option %T", opt) + + def := deepcopy.Copy(test.container).(excelize.PageMarginsOptionsPtr) + val1 := deepcopy.Copy(def).(excelize.PageMarginsOptionsPtr) + val2 := deepcopy.Copy(def).(excelize.PageMarginsOptionsPtr) + + f := excelize.NewFile() + // Get the default value + assert.NoError(t, f.GetPageMargins(sheet, def), opt) + // Get again and check + assert.NoError(t, f.GetPageMargins(sheet, val1), opt) + if !assert.Equal(t, val1, def, opt) { + t.FailNow() + } + // Set the same value + assert.NoError(t, f.SetPageMargins(sheet, val1), opt) + // 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) { + t.FailNow() + } + // Set a different value + assert.NoError(t, f.SetPageMargins(sheet, test.nonDefault), opt) + assert.NoError(t, f.GetPageMargins(sheet, val1), opt) + // 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) { + 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) { + t.FailNow() + } + // Restore the default value + assert.NoError(t, f.SetPageMargins(sheet, def), opt) + assert.NoError(t, f.GetPageMargins(sheet, val1), opt) + if !assert.Equal(t, def, val1) { + t.FailNow() + } + }) + } +} + +func TestSetPageMargins(t *testing.T) { + f := excelize.NewFile() + // Test set page margins on not exists worksheet. + assert.EqualError(t, f.SetPageMargins("SheetN"), "sheet SheetN is not exist") +} + +func TestGetPageMargins(t *testing.T) { + f := excelize.NewFile() + // Test get page margins on not exists worksheet. + assert.EqualError(t, f.GetPageMargins("SheetN"), "sheet SheetN is not exist") +} diff --git a/templates.go b/templates.go index 0d3a0c58a4..b570910793 100644 --- a/templates.go +++ b/templates.go @@ -29,7 +29,7 @@ const templateWorkbook = `` -const templateSheet = `` +const templateSheet = `` const templateWorkbookRels = `` diff --git a/xmlPivotCache.go b/xmlPivotCache.go index a4b07118e8..45b48de99c 100644 --- a/xmlPivotCache.go +++ b/xmlPivotCache.go @@ -48,7 +48,7 @@ type xlsxPivotCacheDefinition struct { // PivotTable. type xlsxCacheSource struct { Type string `xml:"type,attr"` - ConnectionId int `xml:"connectionId,attr,omitempty"` + ConnectionID int `xml:"connectionId,attr,omitempty"` WorksheetSource *xlsxWorksheetSource `xml:"worksheetSource"` Consolidation *xlsxConsolidation `xml:"consolidation"` ExtLst *xlsxExtLst `xml:"extLst"` @@ -89,7 +89,7 @@ type xlsxCacheField struct { PropertyName string `xml:"propertyName,attr,omitempty"` ServerField bool `xml:"serverField,attr,omitempty"` UniqueList bool `xml:"uniqueList,attr,omitempty"` - NumFmtId int `xml:"numFmtId,attr"` + NumFmtID int `xml:"numFmtId,attr"` Formula string `xml:"formula,attr,omitempty"` SQLType int `xml:"sqlType,attr,omitempty"` Hierarchy int `xml:"hierarchy,attr,omitempty"` diff --git a/xmlPivotTable.go b/xmlPivotTable.go index 3738ed82b2..0549c5e39c 100644 --- a/xmlPivotTable.go +++ b/xmlPivotTable.go @@ -125,7 +125,7 @@ type xlsxPivotField struct { UniqueMemberProperty string `xml:"uniqueMemberProperty,attr,omitempty"` Compact bool `xml:"compact,attr"` AllDrilled bool `xml:"allDrilled,attr,omitempty"` - NumFmtId string `xml:"numFmtId,attr,omitempty"` + NumFmtID string `xml:"numFmtId,attr,omitempty"` Outline bool `xml:"outline,attr"` SubtotalTop bool `xml:"subtotalTop,attr,omitempty"` DragToRow bool `xml:"dragToRow,attr,omitempty"` @@ -273,7 +273,7 @@ type xlsxDataField struct { ShowDataAs string `xml:"showDataAs,attr,omitempty"` BaseField int `xml:"baseField,attr,omitempty"` BaseItem int64 `xml:"baseItem,attr,omitempty"` - NumFmtId string `xml:"numFmtId,attr,omitempty"` + NumFmtID string `xml:"numFmtId,attr,omitempty"` ExtLst *xlsxExtLst `xml:"extLst"` } From 866fda230028a3a9e6ff1c5234e432ad850d3c6b Mon Sep 17 00:00:00 2001 From: ducquangkstn Date: Fri, 18 Oct 2019 13:57:35 +0700 Subject: [PATCH 159/957] fix #503 rows next issue --- rows.go | 6 +++--- rows_test.go | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/rows.go b/rows.go index 379644110c..c8ad2b1ffb 100644 --- a/rows.go +++ b/rows.go @@ -57,7 +57,8 @@ type Rows struct { // Next will return true if find the next row element. func (rows *Rows) Next() bool { - return rows.curRow < len(rows.rows) + rows.curRow++ + return rows.curRow <= len(rows.rows) } // Error will return the error when the find next row element @@ -67,8 +68,7 @@ func (rows *Rows) Error() error { // Columns return the current row's column values func (rows *Rows) Columns() ([]string, error) { - curRow := rows.rows[rows.curRow] - rows.curRow++ + curRow := rows.rows[rows.curRow-1] columns := make([]string, len(curRow.C)) d := rows.f.sharedStringsReader() diff --git a/rows_test.go b/rows_test.go index a99a594dd3..ba81f9f1ca 100644 --- a/rows_test.go +++ b/rows_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestRows(t *testing.T) { @@ -41,6 +42,25 @@ func TestRows(t *testing.T) { } } +// test bug https://github.com/360EntSecGroup-Skylar/excelize/issues/502 +func TestRowsIterator(t *testing.T) { + const ( + sheet2 = "Sheet2" + expectedNumRow = 11 + ) + xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + require.NoError(t, err) + + rows, err := xlsx.Rows(sheet2) + require.NoError(t, err) + var rowCount int + for rows.Next() { + rowCount++ + require.True(t, rowCount <= expectedNumRow, "rowCount is greater than expected") + } + assert.Equal(t, expectedNumRow, rowCount) +} + func TestRowsError(t *testing.T) { xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if !assert.NoError(t, err) { From 7716968abc1d330492e311504af8951c34fb7520 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 21 Oct 2019 00:04:18 +0800 Subject: [PATCH 160/957] Fix #505, support set line width of the line chart --- chart.go | 17 +++++++++++++++-- chart_test.go | 2 +- xmlChart.go | 5 +++-- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/chart.go b/chart.go index 7d40405a26..289903e5a5 100644 --- a/chart.go +++ b/chart.go @@ -584,6 +584,7 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // name // categories // values +// line // // 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 // @@ -591,6 +592,8 @@ 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. +// // Set properties of the chart legend. The options that can be set are: // // position @@ -1387,7 +1390,7 @@ func (f *File) drawChartSeriesSpPr(i int, formatSet *formatChart) *cSpPr { } spPrLine := &cSpPr{ Ln: &aLn{ - W: 25400, + W: f.ptToEMUs(formatSet.Series[i].Line.Width), Cap: "rnd", // rnd, sq, flat }, } @@ -1438,7 +1441,7 @@ func (f *File) drawChartSeriesCat(v formatChartSeries, formatSet *formatChart) * }, } chartSeriesCat := map[string]*cCat{Scatter: nil, Bubble: nil, Bubble3D: nil} - if _, ok := chartSeriesCat[formatSet.Type]; ok { + if _, ok := chartSeriesCat[formatSet.Type]; ok || v.Categories == "" { return nil } return cat @@ -1821,3 +1824,13 @@ func (f *File) addDrawingChart(sheet, drawingXML, cell string, width, height, rI f.Drawings[drawingXML] = content return err } + +// ptToEMUs provides a function to convert pt to EMUs, 1 pt = 12700 EMUs. The +// range of pt is 0.25pt - 999pt. If the value of pt is outside the range, the +// default EMUs will be returned. +func (f *File) ptToEMUs(pt float64) int { + if 0.25 > pt || pt > 999 { + return 25400 + } + return int(12700 * pt) +} diff --git a/chart_test.go b/chart_test.go index 932e873328..20df373cdd 100644 --- a/chart_test.go +++ b/chart_test.go @@ -144,7 +144,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"}`)) - 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"},{"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","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"}`)) + 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"},{"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"}`)) 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/xmlChart.go b/xmlChart.go index 69e119a662..50d0b3e9ec 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -606,8 +606,9 @@ type formatChartSeries struct { Categories string `json:"categories"` Values string `json:"values"` Line struct { - None bool `json:"none"` - Color string `json:"color"` + None bool `json:"none"` + Color string `json:"color"` + Width float64 `json:"width"` } `json:"line"` Marker struct { Type string `json:"type"` From e7581ebf3e14f096b6e2d56ed34d381b4af6d310 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 23 Oct 2019 10:08:29 +0800 Subject: [PATCH 161/957] Fix corrupted Excel file issue #413 --- sheet.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sheet.go b/sheet.go index 951baf9239..9e8d50483f 100644 --- a/sheet.go +++ b/sheet.go @@ -406,6 +406,11 @@ func (f *File) DeleteSheet(name string) { f.SheetCount-- } } + for idx, bookView := range wb.BookViews.WorkBookView { + if bookView.ActiveTab >= f.SheetCount { + wb.BookViews.WorkBookView[idx].ActiveTab-- + } + } f.SetActiveSheet(len(f.GetSheetMap())) } From 9fe267ffcfa06545223160cdb8c35cd91163730e Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 24 Oct 2019 09:14:33 -0500 Subject: [PATCH 162/957] Pre-allocate some memory when reading files (#510) --- excelize_test.go | 6 ++++++ lib.go | 11 +++++------ rows_test.go | 2 +- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/excelize_test.go b/excelize_test.go index 7b6b674f18..8d7e7f79a2 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -1278,3 +1278,9 @@ func fillCells(f *File, sheet string, colCount, rowCount int) { } } } + +func BenchmarkOpenFile(b *testing.B) { + for i := 0; i < b.N; i++ { + OpenFile(filepath.Join("test", "Book1.xlsx")) + } +} diff --git a/lib.go b/lib.go index 4dea16a126..edac98a8c4 100644 --- a/lib.go +++ b/lib.go @@ -22,14 +22,12 @@ import ( // ReadZipReader can be used to read an XLSX in memory without touching the // filesystem. func ReadZipReader(r *zip.Reader) (map[string][]byte, int, error) { - fileList := make(map[string][]byte) + fileList := make(map[string][]byte, len(r.File)) worksheets := 0 for _, v := range r.File { fileList[v.Name] = readFile(v) - if len(v.Name) > 18 { - if v.Name[0:19] == "xl/worksheets/sheet" { - worksheets++ - } + if strings.HasPrefix(v.Name, "xl/worksheets/sheet") { + worksheets++ } } return fileList, worksheets, nil @@ -58,7 +56,8 @@ func readFile(file *zip.File) []byte { if err != nil { log.Fatal(err) } - buff := bytes.NewBuffer(nil) + dat := make([]byte, 0, file.FileInfo().Size()) + buff := bytes.NewBuffer(dat) _, _ = io.Copy(buff, rc) rc.Close() return buff.Bytes() diff --git a/rows_test.go b/rows_test.go index ba81f9f1ca..f0fbe03d74 100644 --- a/rows_test.go +++ b/rows_test.go @@ -695,8 +695,8 @@ func TestErrSheetNotExistError(t *testing.T) { } func BenchmarkRows(b *testing.B) { + f, _ := OpenFile(filepath.Join("test", "Book1.xlsx")) for i := 0; i < b.N; i++ { - f, _ := OpenFile(filepath.Join("test", "Book1.xlsx")) rows, _ := f.Rows("Sheet2") for rows.Next() { row, _ := rows.Columns() From 87390cdd99b3afbe07daeef9abe96f57d03cb352 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 24 Oct 2019 23:18:02 +0800 Subject: [PATCH 163/957] Resolve #511, allow empty columns in the pivot table --- calcchain.go | 2 +- pivotTable.go | 41 ++++++++++++++++++++++++++++++----------- pivotTable_test.go | 6 ++++++ rows_test.go | 2 +- sheet_test.go | 9 +++++++++ xmlDrawing.go | 8 ++++---- 6 files changed, 51 insertions(+), 17 deletions(-) diff --git a/calcchain.go b/calcchain.go index b4cadefe0b..7cc175c2f4 100644 --- a/calcchain.go +++ b/calcchain.go @@ -56,7 +56,7 @@ type xlsxCalcChainCollection []xlsxCalcChainC // Filter provides a function to filter calculation chain. func (c xlsxCalcChainCollection) Filter(fn func(v xlsxCalcChainC) bool) []xlsxCalcChainC { - results := make([]xlsxCalcChainC, 0) + var results []xlsxCalcChainC for _, v := range c { if fn(v) { results = append(results, v) diff --git a/pivotTable.go b/pivotTable.go index 881d774192..6045e41c8c 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -253,7 +253,10 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op }, }, }, - ColFields: &xlsxColFields{}, + ColItems: &xlsxColItems{ + Count: 1, + I: []*xlsxI{{}}, + }, DataFields: &xlsxDataFields{}, PivotTableStyleInfo: &xlsxPivotTableStyleInfo{ Name: "PivotStyleLight16", @@ -286,19 +289,10 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op // count row fields pt.RowFields.Count = len(pt.RowFields.Field) - // col fields - colFieldsIndex, err := f.getPivotFieldsIndex(opt.Columns, opt) + err = f.addPivotColFields(&pt, opt) if err != nil { return err } - for _, filedIdx := range colFieldsIndex { - pt.ColFields.Field = append(pt.ColFields.Field, &xlsxField{ - X: filedIdx, - }) - } - - // count col fields - pt.ColFields.Count = len(pt.ColFields.Field) // data fields dataFieldsIndex, err := f.getPivotFieldsIndex(opt.Data, opt) @@ -330,6 +324,31 @@ func inStrSlice(a []string, x string) int { return -1 } +// 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 { + return nil + } + + pt.ColFields = &xlsxColFields{} + + // col fields + colFieldsIndex, err := f.getPivotFieldsIndex(opt.Columns, opt) + if err != nil { + return err + } + for _, filedIdx := range colFieldsIndex { + pt.ColFields.Field = append(pt.ColFields.Field, &xlsxField{ + X: filedIdx, + }) + } + + // count col fields + pt.ColFields.Count = len(pt.ColFields.Field) + return err +} + // 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 { diff --git a/pivotTable_test.go b/pivotTable_test.go index 27e5914f82..9bf08e8197 100644 --- a/pivotTable_test.go +++ b/pivotTable_test.go @@ -54,6 +54,12 @@ func TestAddPivotTable(t *testing.T) { Columns: []string{"Region", "Year"}, Data: []string{"Sales"}, })) + assert.NoError(t, f.AddPivotTable(&PivotTableOption{ + DataRange: "Sheet1!$A$1:$E$31", + PivotTableRange: "Sheet1!$AE$2:$AG$33", + Rows: []string{"Month", "Year"}, + Data: []string{"Sales"}, + })) f.NewSheet("Sheet2") assert.NoError(t, f.AddPivotTable(&PivotTableOption{ DataRange: "Sheet1!$A$1:$E$31", diff --git a/rows_test.go b/rows_test.go index f0fbe03d74..a443e89268 100644 --- a/rows_test.go +++ b/rows_test.go @@ -22,7 +22,7 @@ func TestRows(t *testing.T) { t.FailNow() } - collectedRows := make([][]string, 0) + var collectedRows [][]string for rows.Next() { columns, err := rows.Columns() assert.NoError(t, err) diff --git a/sheet_test.go b/sheet_test.go index 51797935b0..ea345a33ef 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -66,6 +66,15 @@ func ExampleFile_GetPageLayout() { // - fit to width: 1 } +func TestNewSheet(t *testing.T) { + f := excelize.NewFile() + sheetID := f.NewSheet("Sheet2") + f.SetActiveSheet(sheetID) + // delete original sheet + f.DeleteSheet(f.GetSheetName(f.GetSheetIndex("Sheet1"))) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestNewSheet.xlsx"))) +} + func TestPageLayoutOption(t *testing.T) { const sheet = "Sheet1" diff --git a/xmlDrawing.go b/xmlDrawing.go index 4338c5e6a0..1c24f08eda 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -47,10 +47,10 @@ const ( NameSpaceDublinCore = "http://purl.org/dc/elements/1.1/" NameSpaceDublinCoreTerms = "http://purl.org/dc/terms/" NameSpaceDublinCoreMetadataIntiative = "http://purl.org/dc/dcmitype/" - // 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 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}" From 5e418ebd665f38d1211b27d7157ec7e5868451bc Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 26 Oct 2019 20:55:24 +0800 Subject: [PATCH 164/957] Resolve #507, add the new function `DeleteDefinedName` --- cell_test.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ col_test.go | 7 ++++++- excelize_test.go | 43 +++++++++++-------------------------------- rows_test.go | 26 ++++++++++++++++---------- sheet.go | 28 +++++++++++++++++++++++++++- sheet_test.go | 10 +++++++++- 6 files changed, 114 insertions(+), 45 deletions(-) diff --git a/cell_test.go b/cell_test.go index 653aaab874..da0c1f1f68 100644 --- a/cell_test.go +++ b/cell_test.go @@ -4,6 +4,7 @@ import ( "fmt" "path/filepath" "testing" + "time" "github.com/stretchr/testify/assert" ) @@ -73,6 +74,50 @@ func TestSetCellFloat(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "123.42", val, "A1 should be 123.42") }) + f := NewFile() + assert.EqualError(t, f.SetCellFloat(sheet, "A", 123.42, -1, 64), `cannot convert cell "A" to coordinates: invalid cell name "A"`) +} + +func TestSetCellValue(t *testing.T) { + f := NewFile() + assert.EqualError(t, f.SetCellValue("Sheet1", "A", time.Now().UTC()), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.EqualError(t, f.SetCellValue("Sheet1", "A", time.Duration(1e13)), `cannot convert cell "A" to coordinates: invalid cell name "A"`) +} + +func TestSetCellBool(t *testing.T) { + f := NewFile() + assert.EqualError(t, f.SetCellBool("Sheet1", "A", true), `cannot convert cell "A" to coordinates: invalid cell name "A"`) +} + +func TestGetCellFormula(t *testing.T) { + f := NewFile() + f.GetCellFormula("Sheet", "A1") +} + +func TestMergeCell(t *testing.T) { + f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + if !assert.NoError(t, err) { + t.FailNow() + } + assert.EqualError(t, f.MergeCell("Sheet1", "A", "B"), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + f.MergeCell("Sheet1", "D9", "D9") + f.MergeCell("Sheet1", "D9", "E9") + f.MergeCell("Sheet1", "H14", "G13") + f.MergeCell("Sheet1", "C9", "D8") + f.MergeCell("Sheet1", "F11", "G13") + f.MergeCell("Sheet1", "H7", "B15") + f.MergeCell("Sheet1", "D11", "F13") + f.MergeCell("Sheet1", "G10", "K12") + f.SetCellValue("Sheet1", "G11", "set value in merged cell") + f.SetCellInt("Sheet1", "H11", 100) + f.SetCellValue("Sheet1", "I11", float64(0.5)) + f.SetCellHyperLink("Sheet1", "J11", "https://github.com/360EntSecGroup-Skylar/excelize", "External") + f.SetCellFormula("Sheet1", "G12", "SUM(Sheet1!B19,Sheet1!C19)") + f.GetCellValue("Sheet1", "H11") + f.GetCellValue("Sheet2", "A6") // Merged cell ref is single coordinate. + f.GetCellFormula("Sheet1", "G12") + + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestMergeCell.xlsx"))) } func ExampleFile_SetCellFloat() { diff --git a/col_test.go b/col_test.go index a696caad65..edbdae7a17 100644 --- a/col_test.go +++ b/col_test.go @@ -53,10 +53,15 @@ func TestOutlineLevel(t *testing.T) { assert.NoError(t, f.SetRowOutlineLevel("Sheet1", 2, 7)) assert.EqualError(t, f.SetColOutlineLevel("Sheet1", "D", 8), "invalid outline level") assert.EqualError(t, f.SetRowOutlineLevel("Sheet1", 2, 8), "invalid outline level") + // Test set row outline level on not exists worksheet. + assert.EqualError(t, f.SetRowOutlineLevel("SheetN", 1, 4), "sheet SheetN is 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") // Test set and get column outline level with illegal cell coordinates. assert.EqualError(t, f.SetColOutlineLevel("Sheet1", "*", 1), `invalid column name "*"`) - _, err := f.GetColOutlineLevel("Sheet1", "*") + _, err = f.GetColOutlineLevel("Sheet1", "*") assert.EqualError(t, err, `invalid column name "*"`) // Test set column outline level on not exists worksheet. diff --git a/excelize_test.go b/excelize_test.go index 8d7e7f79a2..38a35b0060 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -372,32 +372,6 @@ func TestSetSheetBackgroundErrors(t *testing.T) { assert.EqualError(t, err, "unsupported image extension") } -func TestMergeCell(t *testing.T) { - f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } - - f.MergeCell("Sheet1", "D9", "D9") - f.MergeCell("Sheet1", "D9", "E9") - f.MergeCell("Sheet1", "H14", "G13") - f.MergeCell("Sheet1", "C9", "D8") - f.MergeCell("Sheet1", "F11", "G13") - f.MergeCell("Sheet1", "H7", "B15") - f.MergeCell("Sheet1", "D11", "F13") - f.MergeCell("Sheet1", "G10", "K12") - f.SetCellValue("Sheet1", "G11", "set value in merged cell") - f.SetCellInt("Sheet1", "H11", 100) - f.SetCellValue("Sheet1", "I11", float64(0.5)) - f.SetCellHyperLink("Sheet1", "J11", "https://github.com/360EntSecGroup-Skylar/excelize", "External") - f.SetCellFormula("Sheet1", "G12", "SUM(Sheet1!B19,Sheet1!C19)") - f.GetCellValue("Sheet1", "H11") - f.GetCellValue("Sheet2", "A6") // Merged cell ref is single coordinate. - f.GetCellFormula("Sheet1", "G12") - - assert.NoError(t, f.SaveAs(filepath.Join("test", "TestMergeCell.xlsx"))) -} - // TestWriteArrayFormula tests the extended options of SetCellFormula by writing an array function // to a workbook. In the resulting file, the lines 2 and 3 as well as 4 and 5 should have matching // contents. @@ -913,13 +887,18 @@ func TestAddShape(t *testing.T) { t.FailNow() } - f.AddShape("Sheet1", "A30", `{"type":"rect","paragraph":[{"text":"Rectangle","font":{"color":"CD5C5C"}},{"text":"Shape","font":{"bold":true,"color":"2980B9"}}]}`) - f.AddShape("Sheet1", "B30", `{"type":"rect","paragraph":[{"text":"Rectangle"},{}]}`) - f.AddShape("Sheet1", "C30", `{"type":"rect","paragraph":[]}`) - f.AddShape("Sheet3", "H1", `{"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}`) - f.AddShape("Sheet3", "H1", "") + 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"}, "paragraph":[{"font":{"bold":true,"italic":true,"family":"Times New Roman","size":36,"color":"#777777","underline":"single"}}], "height": 90}`), "sheet Sheet3 is 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"}},{"text":"Shape","font":{"bold":true,"color":"2980B9"}}]}`), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddShape1.xlsx"))) - assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddShape.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}`)) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddShape2.xlsx"))) } func TestAddComments(t *testing.T) { diff --git a/rows_test.go b/rows_test.go index a443e89268..ff70118b33 100644 --- a/rows_test.go +++ b/rows_test.go @@ -42,7 +42,6 @@ func TestRows(t *testing.T) { } } -// test bug https://github.com/360EntSecGroup-Skylar/excelize/issues/502 func TestRowsIterator(t *testing.T) { const ( sheet2 = "Sheet2" @@ -59,6 +58,10 @@ func TestRowsIterator(t *testing.T) { require.True(t, rowCount <= expectedNumRow, "rowCount is greater than expected") } assert.Equal(t, expectedNumRow, rowCount) + + rows = &Rows{f: xlsx, rows: []xlsxRow{{C: []xlsxC{{R: "A"}}}}, curRow: 1} + _, err = rows.Columns() + assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`) } func TestRowsError(t *testing.T) { @@ -113,22 +116,25 @@ func TestRowHeight(t *testing.T) { } func TestRowVisibility(t *testing.T) { - xlsx, err := prepareTestBook1() + f, err := prepareTestBook1() if !assert.NoError(t, err) { t.FailNow() } - xlsx.NewSheet("Sheet3") - assert.NoError(t, xlsx.SetRowVisible("Sheet3", 2, false)) - assert.NoError(t, xlsx.SetRowVisible("Sheet3", 2, true)) - xlsx.GetRowVisible("Sheet3", 2) - xlsx.GetRowVisible("Sheet3", 25) - assert.EqualError(t, xlsx.SetRowVisible("Sheet3", 0, true), "invalid row number 0") + f.NewSheet("Sheet3") + assert.NoError(t, f.SetRowVisible("Sheet3", 2, false)) + assert.NoError(t, f.SetRowVisible("Sheet3", 2, true)) + f.GetRowVisible("Sheet3", 2) + f.GetRowVisible("Sheet3", 25) + assert.EqualError(t, f.SetRowVisible("Sheet3", 0, true), "invalid row number 0") + assert.EqualError(t, f.SetRowVisible("SheetN", 2, false), "sheet SheetN is not exist") - visible, err := xlsx.GetRowVisible("Sheet3", 0) + visible, err := f.GetRowVisible("Sheet3", 0) assert.Equal(t, false, visible) assert.EqualError(t, err, "invalid row number 0") + _, err = f.GetRowVisible("SheetN", 1) + assert.EqualError(t, err, "sheet SheetN is not exist") - assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestRowVisibility.xlsx"))) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestRowVisibility.xlsx"))) } func TestRemoveRow(t *testing.T) { diff --git a/sheet.go b/sheet.go index 9e8d50483f..335c4fc7d4 100644 --- a/sheet.go +++ b/sheet.go @@ -1271,7 +1271,7 @@ func (f *File) SetDefinedName(definedName *DefinedName) error { scope = f.GetSheetName(*dn.LocalSheetID + 1) } if scope == definedName.Scope && dn.Name == definedName.Name { - return errors.New("the same name already exists on scope") + return errors.New("the same name already exists on the scope") } } wb.DefinedNames.DefinedName = append(wb.DefinedNames.DefinedName, d) @@ -1283,6 +1283,32 @@ func (f *File) SetDefinedName(definedName *DefinedName) error { return nil } +// DeleteDefinedName provides a function to delete the defined names of the +// workbook or worksheet. If not specified scope, the default scope is +// workbook. For example: +// +// f.DeleteDefinedName(&excelize.DefinedName{ +// Name: "Amount", +// Scope: "Sheet2", +// }) +// +func (f *File) DeleteDefinedName(definedName *DefinedName) error { + wb := f.workbookReader() + if wb.DefinedNames != nil { + for idx, dn := range wb.DefinedNames.DefinedName { + var scope string + if dn.LocalSheetID != nil { + scope = f.GetSheetName(*dn.LocalSheetID + 1) + } + if scope == definedName.Scope && dn.Name == definedName.Name { + wb.DefinedNames.DefinedName = append(wb.DefinedNames.DefinedName[:idx], wb.DefinedNames.DefinedName[idx+1:]...) + return nil + } + } + } + return errors.New("no defined name on the scope") +} + // GetDefinedName provides a function to get the defined names of the workbook // or worksheet. func (f *File) GetDefinedName() []DefinedName { diff --git a/sheet_test.go b/sheet_test.go index ea345a33ef..b9e4abf788 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -210,8 +210,16 @@ func TestDefinedName(t *testing.T) { Name: "Amount", RefersTo: "Sheet1!$A$2:$D$5", Comment: "defined name comment", - }), "the same name already exists on scope") + }), "the same name already exists on the scope") + assert.EqualError(t, f.DeleteDefinedName(&excelize.DefinedName{ + Name: "No Exist Defined Name", + }), "no defined name on the scope") assert.Exactly(t, "Sheet1!$A$2:$D$5", f.GetDefinedName()[1].RefersTo) + assert.NoError(t, f.DeleteDefinedName(&excelize.DefinedName{ + Name: "Amount", + })) + 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"))) } From 6abf8bf9723512086f009ca574bde1d6682fc83d Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 27 Oct 2019 14:16:02 +0800 Subject: [PATCH 165/957] Resolve #501, support set minor grid lines for the chart --- chart.go | 12 ++++++++++++ chart_test.go | 2 +- xmlChart.go | 2 ++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/chart.go b/chart.go index 289903e5a5..bbd71d6676 100644 --- a/chart.go +++ b/chart.go @@ -652,10 +652,16 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // // Set the primary horizontal and vertical axis options by x_axis and y_axis. The properties that can be set are: // +// major_grid_lines +// minor_grid_lines // reverse_order // maximum // minimum // +// major_grid_lines: Specifies major gridlines. +// +// minor_grid_lines: Specifies minor gridlines. +// // 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. // // maximum: Specifies that the fixed maximum, 0 is auto. The maximum property is optional. The default value is auto. @@ -1601,6 +1607,9 @@ func (f *File) drawPlotAreaCatAx(formatSet *formatChart) []*cAxs { if formatSet.XAxis.MajorGridlines { axs[0].MajorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()} } + if formatSet.XAxis.MinorGridlines { + axs[0].MinorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()} + } return axs } @@ -1641,6 +1650,9 @@ func (f *File) drawPlotAreaValAx(formatSet *formatChart) []*cAxs { if formatSet.YAxis.MajorGridlines { axs[0].MajorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()} } + if formatSet.YAxis.MinorGridlines { + axs[0].MinorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()} + } if pos, ok := valTickLblPos[formatSet.Type]; ok { axs[0].TickLblPos.Val = pos } diff --git a/chart_test.go b/chart_test.go index 20df373cdd..bc5c30ae9f 100644 --- a/chart_test.go +++ b/chart_test.go @@ -144,7 +144,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"}`)) - 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"},{"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"}`)) + 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"},{"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},"y_axis":{"major_grid_lines":true,"minor_grid_lines":true}}`)) 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/xmlChart.go b/xmlChart.go index 50d0b3e9ec..a02da2a423 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -347,6 +347,7 @@ type cAxs struct { Delete *attrValBool `xml:"delete"` AxPos *attrValString `xml:"axPos"` MajorGridlines *cChartLines `xml:"majorGridlines"` + MinorGridlines *cChartLines `xml:"minorGridlines"` NumFmt *cNumFmt `xml:"numFmt"` MajorTickMark *attrValString `xml:"majorTickMark"` MinorTickMark *attrValString `xml:"minorTickMark"` @@ -514,6 +515,7 @@ type cPageMargins struct { type formatChartAxis struct { 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"` From bf9a8355494eac18812f3caf6d469962824f627f Mon Sep 17 00:00:00 2001 From: Harris Date: Mon, 28 Oct 2019 10:34:21 -0500 Subject: [PATCH 166/957] Reduce allocations when writing Fix #494 If a row is full, don't bother allocating a new one, just return it. Use the last populated row as a hint for the size of new rows. Simplify checkSheet to remove row map --- excelize.go | 18 +++++------------- file_test.go | 27 +++++++++++++++++++++++++++ sheet.go | 18 +++++++++++++++--- xmlWorksheet.go | 4 ++++ 4 files changed, 51 insertions(+), 16 deletions(-) create mode 100644 file_test.go diff --git a/excelize.go b/excelize.go index 4d46b94e9b..ba6445fc18 100644 --- a/excelize.go +++ b/excelize.go @@ -155,20 +155,12 @@ func checkSheet(xlsx *xlsxWorksheet) { row = lastRow } } - sheetData := xlsxSheetData{} - existsRows := map[int]int{} - for k := range xlsx.SheetData.Row { - existsRows[xlsx.SheetData.Row[k].R] = k + sheetData := xlsxSheetData{Row: make([]xlsxRow, row)} + for _, r := range xlsx.SheetData.Row { + sheetData.Row[r.R-1] = r } - for i := 0; i < row; i++ { - _, ok := existsRows[i+1] - if ok { - sheetData.Row = append(sheetData.Row, xlsx.SheetData.Row[existsRows[i+1]]) - } else { - sheetData.Row = append(sheetData.Row, xlsxRow{ - R: i + 1, - }) - } + for i := 1; i <= row; i++ { + sheetData.Row[i-1].R = i } xlsx.SheetData = sheetData } diff --git a/file_test.go b/file_test.go new file mode 100644 index 0000000000..6c30f4acf2 --- /dev/null +++ b/file_test.go @@ -0,0 +1,27 @@ +package excelize + +import ( + "testing" +) + +func BenchmarkWrite(b *testing.B) { + const s = "This is test data" + for i := 0; i < b.N; i++ { + f := NewFile() + for row := 1; row <= 10000; row++ { + for col := 1; col <= 20; col++ { + val, err := CoordinatesToCellName(col, row) + if err != nil { + panic(err) + } + f.SetCellDefault("Sheet1", val, s) + } + } + // Save xlsx file by the given path. + err := f.SaveAs("./test.xlsx") + if err != nil { + panic(err) + } + } + +} diff --git a/sheet.go b/sheet.go index 335c4fc7d4..43c7cc028b 100644 --- a/sheet.go +++ b/sheet.go @@ -117,12 +117,19 @@ func (f *File) workSheetWriter() { } } -// trimCell provides a function to trim blank cells which created by completeCol. +// trimCell provides a function to trim blank cells which created by fillColumns. func trimCell(column []xlsxC) []xlsxC { + rowFull := true + for i := range column { + rowFull = column[i].hasValue() && rowFull + } + if rowFull { + return column + } col := make([]xlsxC, len(column)) i := 0 for _, c := range column { - if c.S != 0 || c.V != "" || c.F != nil || c.T != "" { + if c.hasValue() { col[i] = c i++ } @@ -1404,12 +1411,17 @@ func (f *File) relsReader(path string) *xlsxRelationships { // 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(xlsx *xlsxWorksheet, col int, row int) { rowCount := len(xlsx.SheetData.Row) + sizeHint := 0 + if rowCount > 0 { + sizeHint = len(xlsx.SheetData.Row[rowCount-1].C) + } if rowCount < row { // append missing rows for rowIdx := rowCount; rowIdx < row; rowIdx++ { - xlsx.SheetData.Row = append(xlsx.SheetData.Row, xlsxRow{R: rowIdx + 1}) + xlsx.SheetData.Row = append(xlsx.SheetData.Row, xlsxRow{R: rowIdx + 1, C: make([]xlsxC, 0, sizeHint)}) } } rowData := &xlsx.SheetData.Row[row-1] diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 96ca235782..8408cfadbc 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -430,6 +430,10 @@ type xlsxC struct { XMLSpace xml.Attr `xml:"space,attr,omitempty"` } +func (c *xlsxC) hasValue() bool { + return c.S != 0 || c.V != "" || c.F != nil || c.T != "" +} + // xlsxF directly maps the f element in the namespace // http://schemas.openxmlformats.org/spreadsheetml/2006/main - currently I have // not checked it for completeness - it does as much as I need. From aa7eadbffe6ae2f9f86201bbaaa4c1d1e8829cae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A9=AC=E5=BD=A6=E5=86=9B?= Date: Thu, 14 Nov 2019 14:30:25 +0800 Subject: [PATCH 167/957] fix go lock --- cell.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/cell.go b/cell.go index bd4d93be98..44a590c6db 100644 --- a/cell.go +++ b/cell.go @@ -16,6 +16,7 @@ import ( "reflect" "strconv" "strings" + "sync" "time" ) @@ -30,6 +31,8 @@ const ( STCellFormulaTypeShared = "shared" ) +var rwMutex sync.RWMutex + // GetCellValue provides a function to get formatted value from cell by given // worksheet name and axis in XLSX 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, @@ -155,6 +158,8 @@ func (f *File) setCellTimeFunc(sheet, axis string, value time.Time) error { // 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 { + rwMutex.Lock() + defer rwMutex.Unlock() xlsx, err := f.workSheetReader(sheet) if err != nil { return err @@ -172,6 +177,8 @@ func (f *File) SetCellInt(sheet, axis string, value int) error { // 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 { + rwMutex.Lock() + defer rwMutex.Unlock() xlsx, err := f.workSheetReader(sheet) if err != nil { return err @@ -200,6 +207,8 @@ func (f *File) SetCellBool(sheet, axis string, value bool) error { // f.SetCellFloat("Sheet1", "A1", float64(x), 2, 32) // func (f *File) SetCellFloat(sheet, axis string, value float64, prec, bitSize int) error { + rwMutex.Lock() + defer rwMutex.Unlock() xlsx, err := f.workSheetReader(sheet) if err != nil { return err @@ -217,6 +226,8 @@ func (f *File) SetCellFloat(sheet, axis string, value float64, prec, bitSize int // 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 { + rwMutex.Lock() + defer rwMutex.Unlock() xlsx, err := f.workSheetReader(sheet) if err != nil { return err @@ -276,6 +287,8 @@ func (f *File) GetCellFormula(sheet, axis string) (string, error) { // SetCellFormula provides a function to set cell formula by given string and // worksheet name. func (f *File) SetCellFormula(sheet, axis, formula string) error { + rwMutex.Lock() + defer rwMutex.Unlock() xlsx, err := f.workSheetReader(sheet) if err != nil { return err From 7965e1231b736f8507f93f6383b76332eb15ff5f Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 23 Nov 2019 04:13:59 +0800 Subject: [PATCH 168/957] Resolve #146, make the GetRow function read data as streaming. Ref: #382, #515 --- rows.go | 122 +++++++++++++++++++++++++++++++++++++++------------ rows_test.go | 23 +++++++--- sheet.go | 63 ++++++++++++++------------ 3 files changed, 146 insertions(+), 62 deletions(-) diff --git a/rows.go b/rows.go index c8ad2b1ffb..69a984610e 100644 --- a/rows.go +++ b/rows.go @@ -10,6 +10,7 @@ package excelize import ( + "bytes" "encoding/xml" "errors" "fmt" @@ -49,16 +50,19 @@ func (f *File) GetRows(sheet string) ([][]string, error) { // Rows defines an iterator to a sheet type Rows struct { - err error - f *File - rows []xlsxRow - curRow int + err error + f *File + rows []xlsxRow + sheet string + curRow int + totalRow int + decoder *xml.Decoder } // Next will return true if find the next row element. func (rows *Rows) Next() bool { rows.curRow++ - return rows.curRow <= len(rows.rows) + return rows.curRow <= rows.totalRow } // Error will return the error when the find next row element @@ -68,19 +72,57 @@ func (rows *Rows) Error() error { // Columns return the current row's column values func (rows *Rows) Columns() ([]string, error) { - curRow := rows.rows[rows.curRow-1] - - columns := make([]string, len(curRow.C)) + var ( + err error + inElement string + row, cellCol int + columns []string + ) d := rows.f.sharedStringsReader() - for _, colCell := range curRow.C { - col, _, err := CellNameToCoordinates(colCell.R) - if err != nil { - return columns, err + for { + token, _ := rows.decoder.Token() + if token == nil { + break + } + switch startElement := token.(type) { + case xml.StartElement: + inElement = startElement.Name.Local + if inElement == "row" { + for _, attr := range startElement.Attr { + if attr.Name.Local == "r" { + row, err = strconv.Atoi(attr.Value) + if err != nil { + return columns, err + } + if row > rows.curRow { + return columns, err + } + } + } + } + if inElement == "c" { + colCell := xlsxC{} + _ = rows.decoder.DecodeElement(&colCell, &startElement) + cellCol, _, err = CellNameToCoordinates(colCell.R) + if err != nil { + return columns, err + } + blank := cellCol - len(columns) + for i := 1; i < blank; i++ { + columns = append(columns, "") + } + val, _ := colCell.getValueFrom(rows.f, d) + columns = append(columns, val) + } + case xml.EndElement: + inElement = startElement.Name.Local + if inElement == "row" { + return columns, err + } + default: } - val, _ := colCell.getValueFrom(rows.f, d) - columns[col-1] = val } - return columns, nil + return columns, err } // ErrSheetNotExist defines an error of sheet is not exist @@ -89,7 +131,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", string(err.SheetName)) } // Rows return a rows iterator. For example: @@ -104,22 +146,48 @@ func (err ErrSheetNotExist) Error() string { // } // func (f *File) Rows(sheet string) (*Rows, error) { - xlsx, err := f.workSheetReader(sheet) - if err != nil { - return nil, err - } name, ok := f.sheetMap[trimSheetName(sheet)] if !ok { return nil, ErrSheetNotExist{sheet} } - if xlsx != nil { - data := f.readXML(name) - f.saveFileList(name, replaceWorkSheetsRelationshipsNameSpaceBytes(namespaceStrictToTransitional(data))) + if f.Sheet[name] != nil { + // flush data + output, _ := xml.Marshal(f.Sheet[name]) + f.saveFileList(name, replaceWorkSheetsRelationshipsNameSpaceBytes(output)) + } + var ( + err error + inElement string + row int + rows Rows + ) + decoder := xml.NewDecoder(bytes.NewReader(f.readXML(name))) + for { + token, _ := decoder.Token() + if token == nil { + break + } + switch startElement := token.(type) { + case xml.StartElement: + inElement = startElement.Name.Local + if inElement == "row" { + for _, attr := range startElement.Attr { + if attr.Name.Local == "r" { + row, err = strconv.Atoi(attr.Value) + if err != nil { + return &rows, err + } + } + } + rows.totalRow = row + } + default: + } } - return &Rows{ - f: f, - rows: xlsx.SheetData.Row, - }, nil + rows.f = f + rows.sheet = name + rows.decoder = xml.NewDecoder(bytes.NewReader(f.readXML(name))) + return &rows, nil } // SetRowHeight provides a function to set the height of a single row. For diff --git a/rows_test.go b/rows_test.go index ff70118b33..47c9d96946 100644 --- a/rows_test.go +++ b/rows_test.go @@ -47,10 +47,10 @@ func TestRowsIterator(t *testing.T) { sheet2 = "Sheet2" expectedNumRow = 11 ) - xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) require.NoError(t, err) - rows, err := xlsx.Rows(sheet2) + rows, err := f.Rows(sheet2) require.NoError(t, err) var rowCount int for rows.Next() { @@ -59,9 +59,20 @@ func TestRowsIterator(t *testing.T) { } assert.Equal(t, expectedNumRow, rowCount) - rows = &Rows{f: xlsx, rows: []xlsxRow{{C: []xlsxC{{R: "A"}}}}, curRow: 1} - _, err = rows.Columns() - assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`) + // Valued cell sparse distribution test + f = NewFile() + cells := []string{"C1", "E1", "A3", "B3", "C3", "D3", "E3"} + for _, cell := range cells { + f.SetCellValue("Sheet1", cell, 1) + } + rows, err = f.Rows("Sheet1") + require.NoError(t, err) + rowCount = 0 + for rows.Next() { + rowCount++ + require.True(t, rowCount <= 3, "rowCount is greater than expected") + } + assert.Equal(t, 3, rowCount) } func TestRowsError(t *testing.T) { @@ -697,7 +708,7 @@ func TestDuplicateRowInvalidRownum(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 is not exist") } func BenchmarkRows(b *testing.B) { diff --git a/sheet.go b/sheet.go index 43c7cc028b..c2e6bf6d2f 100644 --- a/sheet.go +++ b/sheet.go @@ -699,15 +699,12 @@ func (f *File) SearchSheet(sheet, value string, reg ...bool) ([]string, error) { for _, r := range reg { regSearch = r } - xlsx, err := f.workSheetReader(sheet) - if err != nil { - return result, err - } name, ok := f.sheetMap[trimSheetName(sheet)] if !ok { - return result, nil + return result, ErrSheetNotExist{sheet} } - if xlsx != nil { + if f.Sheet[name] != nil { + // flush data output, _ := xml.Marshal(f.Sheet[name]) f.saveFileList(name, replaceWorkSheetsRelationshipsNameSpaceBytes(output)) } @@ -718,9 +715,10 @@ func (f *File) SearchSheet(sheet, value string, reg ...bool) ([]string, error) { // cell value, and regular expression. func (f *File) searchSheet(name, value string, regSearch bool) ([]string, error) { var ( - inElement string - result []string - r xlsxRow + err error + cellName, inElement string + result []string + cellCol, row int ) d := f.sharedStringsReader() decoder := xml.NewDecoder(bytes.NewReader(f.readXML(name))) @@ -733,31 +731,38 @@ func (f *File) searchSheet(name, value string, regSearch bool) ([]string, error) case xml.StartElement: inElement = startElement.Name.Local if inElement == "row" { - r = xlsxRow{} - _ = decoder.DecodeElement(&r, &startElement) - for _, colCell := range r.C { - val, _ := colCell.getValueFrom(f, d) - if regSearch { - regex := regexp.MustCompile(value) - if !regex.MatchString(val) { - continue - } - } else { - if val != value { - continue + for _, attr := range startElement.Attr { + if attr.Name.Local == "r" { + row, err = strconv.Atoi(attr.Value) + if err != nil { + return result, err } } - - cellCol, _, err := CellNameToCoordinates(colCell.R) - if err != nil { - return result, err + } + } + if inElement == "c" { + colCell := xlsxC{} + _ = decoder.DecodeElement(&colCell, &startElement) + val, _ := colCell.getValueFrom(f, d) + if regSearch { + regex := regexp.MustCompile(value) + if !regex.MatchString(val) { + continue } - cellName, err := CoordinatesToCellName(cellCol, r.R) - if err != nil { - return result, err + } else { + if val != value { + continue } - result = append(result, cellName) } + cellCol, _, err = CellNameToCoordinates(colCell.R) + if err != nil { + return result, err + } + cellName, err = CoordinatesToCellName(cellCol, row) + if err != nil { + return result, err + } + result = append(result, cellName) } default: } From 8d6e431dcd8d96dc51f74308e49b5d4a5b2b9d2e Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 28 Nov 2019 21:53:50 +0800 Subject: [PATCH 169/957] Resolve #521, fix missing elements when parsing --- excelize.go | 2 +- sheet.go | 2 +- xmlWorksheet.go | 18 ++++++++++++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/excelize.go b/excelize.go index ba6445fc18..c59ec8cff0 100644 --- a/excelize.go +++ b/excelize.go @@ -192,7 +192,7 @@ func (f *File) addRels(relPath, relType, target, targetMode string) int { // Office Excel 2007. func replaceWorkSheetsRelationshipsNameSpaceBytes(workbookMarshal []byte) []byte { var oldXmlns = []byte(``) - var newXmlns = []byte(``) + var newXmlns = []byte(``) workbookMarshal = bytes.Replace(workbookMarshal, oldXmlns, newXmlns, -1) return workbookMarshal } diff --git a/sheet.go b/sheet.go index c2e6bf6d2f..566e6e78a4 100644 --- a/sheet.go +++ b/sheet.go @@ -207,7 +207,7 @@ func replaceRelationshipsBytes(content []byte) []byte { // a horrible hack to fix that after the XML marshalling is completed. func replaceRelationshipsNameSpaceBytes(workbookMarshal []byte) []byte { oldXmlns := []byte(``) - newXmlns := []byte(``) + newXmlns := []byte(``) return bytes.Replace(workbookMarshal, oldXmlns, newXmlns, -1) } diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 8408cfadbc..cb854cdbdb 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -22,8 +22,13 @@ type xlsxWorksheet struct { SheetFormatPr *xlsxSheetFormatPr `xml:"sheetFormatPr"` Cols *xlsxCols `xml:"cols,omitempty"` SheetData xlsxSheetData `xml:"sheetData"` + SheetCalcPr *xlsxInnerXML `xml:"sheetCalcPr"` SheetProtection *xlsxSheetProtection `xml:"sheetProtection"` + ProtectedRanges *xlsxInnerXML `xml:"protectedRanges"` + Scenarios *xlsxInnerXML `xml:"scenarios"` AutoFilter *xlsxAutoFilter `xml:"autoFilter"` + SortState *xlsxInnerXML `xml:"sortState"` + DataConsolidate *xlsxInnerXML `xml:"dataConsolidate"` CustomSheetViews *xlsxCustomSheetViews `xml:"customSheetViews"` MergeCells *xlsxMergeCells `xml:"mergeCells"` PhoneticPr *xlsxPhoneticPr `xml:"phoneticPr"` @@ -36,9 +41,18 @@ type xlsxWorksheet struct { HeaderFooter *xlsxHeaderFooter `xml:"headerFooter"` RowBreaks *xlsxBreaks `xml:"rowBreaks"` ColBreaks *xlsxBreaks `xml:"colBreaks"` + CustomProperties *xlsxInnerXML `xml:"customProperties"` + CellWatches *xlsxInnerXML `xml:"cellWatches"` + IgnoredErrors *xlsxInnerXML `xml:"ignoredErrors"` + SmartTags *xlsxInnerXML `xml:"smartTags"` Drawing *xlsxDrawing `xml:"drawing"` LegacyDrawing *xlsxLegacyDrawing `xml:"legacyDrawing"` + LegacyDrawingHF *xlsxInnerXML `xml:"legacyDrawingHF"` + DrawingHF *xlsxDrawingHF `xml:"drawingHF"` Picture *xlsxPicture `xml:"picture"` + OleObjects *xlsxInnerXML `xml:"oleObjects"` + Controls *xlsxInnerXML `xml:"controls"` + WebPublishItems *xlsxInnerXML `xml:"webPublishItems"` TableParts *xlsxTableParts `xml:"tableParts"` ExtLst *xlsxExtLst `xml:"extLst"` } @@ -631,6 +645,10 @@ type xlsxLegacyDrawing struct { RID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr,omitempty"` } +type xlsxInnerXML struct { + Content string `xml:",innerxml"` +} + // xlsxWorksheetExt directly maps the ext element in the worksheet. type xlsxWorksheetExt struct { XMLName xml.Name `xml:"ext"` From 402ad2f62b04d44f1ab866b32b9e7314a713e5f0 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 30 Nov 2019 00:06:36 +0800 Subject: [PATCH 170/957] Update XML namespace --- excelize.go | 4 ++-- sheet.go | 2 +- templates.go | 2 ++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/excelize.go b/excelize.go index c59ec8cff0..cbe7231ceb 100644 --- a/excelize.go +++ b/excelize.go @@ -192,7 +192,7 @@ func (f *File) addRels(relPath, relType, target, targetMode string) int { // Office Excel 2007. func replaceWorkSheetsRelationshipsNameSpaceBytes(workbookMarshal []byte) []byte { var oldXmlns = []byte(``) - var newXmlns = []byte(``) + var newXmlns = []byte(``) - var newXmlns = []byte(``) + var newXmlns = []byte(``) - newXmlns := []byte(``) + newXmlns := []byte(`` const templateTheme = `` + +const templateNamespaceIDMap = ` xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:ap="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" xmlns:op="http://schemas.openxmlformats.org/officeDocument/2006/custom-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:ac="http://schemas.openxmlformats.org/officeDocument/2006/characteristics" xmlns:b="http://schemas.openxmlformats.org/officeDocument/2006/bibliography" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:cdr="http://schemas.openxmlformats.org/drawingml/2006/chartDrawing" xmlns:comp="http://schemas.openxmlformats.org/drawingml/2006/compatibility" xmlns:dgm="http://schemas.openxmlformats.org/drawingml/2006/diagram" xmlns:lc="http://schemas.openxmlformats.org/drawingml/2006/lockedCanvas" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture" xmlns:xdr="http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:ds="http://schemas.openxmlformats.org/officeDocument/2006/customXml" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:x="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:sl="http://schemas.openxmlformats.org/schemaLibrary/2006/main" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:xne="http://schemas.microsoft.com/office/excel/2006/main" xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml" xmlns:mso="http://schemas.microsoft.com/office/2006/01/customui" xmlns:ax="http://schemas.microsoft.com/office/2006/activeX" xmlns:cppr="http://schemas.microsoft.com/office/2006/coverPageProps" xmlns:cdip="http://schemas.microsoft.com/office/2006/customDocumentInformationPanel" xmlns:ct="http://schemas.microsoft.com/office/2006/metadata/contentType" xmlns:ntns="http://schemas.microsoft.com/office/2006/metadata/customXsn" xmlns:lp="http://schemas.microsoft.com/office/2006/metadata/longProperties" xmlns:ma="http://schemas.microsoft.com/office/2006/metadata/properties/metaAttributes" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:inkml="http://www.w3.org/2003/InkML" xmlns:emma="http://www.w3.org/2003/04/emma" xmlns:msink="http://schemas.microsoft.com/ink/2010/main" mc:Ignorable="c14 cdr14 a14 p14 pic14 wp14 w14 x14 xdr14 x14ac mso14 dgm14 wpc wpg wps sle com14 c15 cs we a15 p15 w15 wetp x15 x12ac thm15 x15ac wp15 pRoam tsle p16 a16 cx c16ac c16 xr xr2 xr3 xr4 xr5 xr6 xr7 xr8 xr9 xr10 x16 x16r2 w16se mo mx mv o v" xmlns:c14="http://schemas.microsoft.com/office/drawing/2007/8/2/chart" xmlns:cdr14="http://schemas.microsoft.com/office/drawing/2010/chartDrawing" xmlns:a14="http://schemas.microsoft.com/office/drawing/2010/main" xmlns:p14="http://schemas.microsoft.com/office/powerpoint/2010/main" xmlns:pic14="http://schemas.microsoft.com/office/drawing/2010/picture" xmlns:wp14="http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" xmlns:x14="http://schemas.microsoft.com/office/spreadsheetml/2009/9/main" xmlns:xdr14="http://schemas.microsoft.com/office/excel/2010/spreadsheetDrawing" xmlns:x14ac="http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac" xmlns:dsp="http://schemas.microsoft.com/office/drawing/2008/diagram" xmlns:mso14="http://schemas.microsoft.com/office/2009/07/customui" xmlns:dgm14="http://schemas.microsoft.com/office/drawing/2010/diagram" xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas" xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup" xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape" xmlns:sle="http://schemas.microsoft.com/office/drawing/2010/slicer" xmlns:com14="http://schemas.microsoft.com/office/drawing/2010/compatibility" xmlns:c15="http://schemas.microsoft.com/office/drawing/2012/chart" xmlns:cs="http://schemas.microsoft.com/office/drawing/2012/chartStyle" xmlns:we="http://schemas.microsoft.com/office/webextensions/webextension/2010/11" xmlns:a15="http://schemas.microsoft.com/office/drawing/2012/main" xmlns:p15="http://schemas.microsoft.com/office/powerpoint/2012/main" xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml" xmlns:wetp="http://schemas.microsoft.com/office/webextensions/taskpanes/2010/11" xmlns:x15="http://schemas.microsoft.com/office/spreadsheetml/2010/11/main" xmlns:x12ac="http://schemas.microsoft.com/office/spreadsheetml/2011/1/ac" xmlns:thm15="http://schemas.microsoft.com/office/thememl/2012/main" xmlns:x15ac="http://schemas.microsoft.com/office/spreadsheetml/2010/11/ac" xmlns:wp15="http://schemas.microsoft.com/office/word/2012/wordprocessingDrawing" xmlns:pRoam="http://schemas.microsoft.com/office/powerpoint/2012/roamingSettings" xmlns:tsle="http://schemas.microsoft.com/office/drawing/2012/timeslicer" xmlns:p16="http://schemas.microsoft.com/office/powerpoint/2015/main" xmlns:a16="http://schemas.microsoft.com/office/drawing/2014/main" xmlns:cx="http://schemas.microsoft.com/office/drawing/2014/chartex" xmlns:c16ac="http://schemas.microsoft.com/office/drawing/2014/chart/ac" xmlns:c16="http://schemas.microsoft.com/office/drawing/2014/chart" xmlns:xr="http://schemas.microsoft.com/office/spreadsheetml/2014/revision" xmlns:xr2="http://schemas.microsoft.com/office/spreadsheetml/2015/revision2" xmlns:xr3="http://schemas.microsoft.com/office/spreadsheetml/2016/revision3" xmlns:xr4="http://schemas.microsoft.com/office/spreadsheetml/2016/revision4" xmlns:xr5="http://schemas.microsoft.com/office/spreadsheetml/2016/revision5" xmlns:xr6="http://schemas.microsoft.com/office/spreadsheetml/2016/revision6" xmlns:xr7="http://schemas.microsoft.com/office/spreadsheetml/2016/revision7" xmlns:xr8="http://schemas.microsoft.com/office/spreadsheetml/2016/revision8" xmlns:xr9="http://schemas.microsoft.com/office/spreadsheetml/2016/revision9" xmlns:xr10="http://schemas.microsoft.com/office/spreadsheetml/2016/revision10" xmlns:xr11="http://schemas.microsoft.com/office/spreadsheetml/2016/revision11" xmlns:xr12="http://schemas.microsoft.com/office/spreadsheetml/2016/revision12" xmlns:xr13="http://schemas.microsoft.com/office/spreadsheetml/2016/revision13" xmlns:xr14="http://schemas.microsoft.com/office/spreadsheetml/2016/revision14" xmlns:xr15="http://schemas.microsoft.com/office/spreadsheetml/2016/revision15" xmlns:xr16="http://schemas.microsoft.com/office/spreadsheetml/2017/revision16" xmlns:xr17="http://schemas.microsoft.com/office/spreadsheetml/2017/revision17" xmlns:xr18="http://schemas.microsoft.com/office/spreadsheetml/2017/revision18" xmlns:xr19="http://schemas.microsoft.com/office/spreadsheetml/2017/revision19" xmlns:xr20="http://schemas.microsoft.com/office/spreadsheetml/2017/revision20" xmlns:xr21="http://schemas.microsoft.com/office/spreadsheetml/2018/revision21" xmlns:xr22="http://schemas.microsoft.com/office/spreadsheetml/2018/revision22" xmlns:x16="http://schemas.microsoft.com/office/spreadsheetml/2014/11/main" xmlns:x16r2="http://schemas.microsoft.com/office/spreadsheetml/2015/02/main" xmlns:w16se="http://schemas.microsoft.com/office/word/2015/wordml/symex" xmlns:mo="http://schemas.microsoft.com/office/mac/office/2008/main" xmlns:mx="http://schemas.microsoft.com/office/mac/excel/2008/main" xmlns:mv="urn:schemas-microsoft-com:mac:vml" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:v="urn:schemas-microsoft-com:vml" xr:uid="{00000000-0001-0000-0000-000000000000}">` From 842b942c71df8a7bcc6e8f32232851679bd6a090 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 2 Dec 2019 22:39:32 +0800 Subject: [PATCH 171/957] Compatible with up to 64 namespaces of Kingsoft WPS --- templates.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates.go b/templates.go index 7af55045e4..5b79b0c806 100644 --- a/templates.go +++ b/templates.go @@ -39,4 +39,4 @@ const templateRels = `` -const templateNamespaceIDMap = ` xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:ap="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" xmlns:op="http://schemas.openxmlformats.org/officeDocument/2006/custom-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:ac="http://schemas.openxmlformats.org/officeDocument/2006/characteristics" xmlns:b="http://schemas.openxmlformats.org/officeDocument/2006/bibliography" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:cdr="http://schemas.openxmlformats.org/drawingml/2006/chartDrawing" xmlns:comp="http://schemas.openxmlformats.org/drawingml/2006/compatibility" xmlns:dgm="http://schemas.openxmlformats.org/drawingml/2006/diagram" xmlns:lc="http://schemas.openxmlformats.org/drawingml/2006/lockedCanvas" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture" xmlns:xdr="http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:ds="http://schemas.openxmlformats.org/officeDocument/2006/customXml" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:x="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:sl="http://schemas.openxmlformats.org/schemaLibrary/2006/main" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:xne="http://schemas.microsoft.com/office/excel/2006/main" xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml" xmlns:mso="http://schemas.microsoft.com/office/2006/01/customui" xmlns:ax="http://schemas.microsoft.com/office/2006/activeX" xmlns:cppr="http://schemas.microsoft.com/office/2006/coverPageProps" xmlns:cdip="http://schemas.microsoft.com/office/2006/customDocumentInformationPanel" xmlns:ct="http://schemas.microsoft.com/office/2006/metadata/contentType" xmlns:ntns="http://schemas.microsoft.com/office/2006/metadata/customXsn" xmlns:lp="http://schemas.microsoft.com/office/2006/metadata/longProperties" xmlns:ma="http://schemas.microsoft.com/office/2006/metadata/properties/metaAttributes" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:inkml="http://www.w3.org/2003/InkML" xmlns:emma="http://www.w3.org/2003/04/emma" xmlns:msink="http://schemas.microsoft.com/ink/2010/main" mc:Ignorable="c14 cdr14 a14 p14 pic14 wp14 w14 x14 xdr14 x14ac mso14 dgm14 wpc wpg wps sle com14 c15 cs we a15 p15 w15 wetp x15 x12ac thm15 x15ac wp15 pRoam tsle p16 a16 cx c16ac c16 xr xr2 xr3 xr4 xr5 xr6 xr7 xr8 xr9 xr10 x16 x16r2 w16se mo mx mv o v" xmlns:c14="http://schemas.microsoft.com/office/drawing/2007/8/2/chart" xmlns:cdr14="http://schemas.microsoft.com/office/drawing/2010/chartDrawing" xmlns:a14="http://schemas.microsoft.com/office/drawing/2010/main" xmlns:p14="http://schemas.microsoft.com/office/powerpoint/2010/main" xmlns:pic14="http://schemas.microsoft.com/office/drawing/2010/picture" xmlns:wp14="http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" xmlns:x14="http://schemas.microsoft.com/office/spreadsheetml/2009/9/main" xmlns:xdr14="http://schemas.microsoft.com/office/excel/2010/spreadsheetDrawing" xmlns:x14ac="http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac" xmlns:dsp="http://schemas.microsoft.com/office/drawing/2008/diagram" xmlns:mso14="http://schemas.microsoft.com/office/2009/07/customui" xmlns:dgm14="http://schemas.microsoft.com/office/drawing/2010/diagram" xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas" xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup" xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape" xmlns:sle="http://schemas.microsoft.com/office/drawing/2010/slicer" xmlns:com14="http://schemas.microsoft.com/office/drawing/2010/compatibility" xmlns:c15="http://schemas.microsoft.com/office/drawing/2012/chart" xmlns:cs="http://schemas.microsoft.com/office/drawing/2012/chartStyle" xmlns:we="http://schemas.microsoft.com/office/webextensions/webextension/2010/11" xmlns:a15="http://schemas.microsoft.com/office/drawing/2012/main" xmlns:p15="http://schemas.microsoft.com/office/powerpoint/2012/main" xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml" xmlns:wetp="http://schemas.microsoft.com/office/webextensions/taskpanes/2010/11" xmlns:x15="http://schemas.microsoft.com/office/spreadsheetml/2010/11/main" xmlns:x12ac="http://schemas.microsoft.com/office/spreadsheetml/2011/1/ac" xmlns:thm15="http://schemas.microsoft.com/office/thememl/2012/main" xmlns:x15ac="http://schemas.microsoft.com/office/spreadsheetml/2010/11/ac" xmlns:wp15="http://schemas.microsoft.com/office/word/2012/wordprocessingDrawing" xmlns:pRoam="http://schemas.microsoft.com/office/powerpoint/2012/roamingSettings" xmlns:tsle="http://schemas.microsoft.com/office/drawing/2012/timeslicer" xmlns:p16="http://schemas.microsoft.com/office/powerpoint/2015/main" xmlns:a16="http://schemas.microsoft.com/office/drawing/2014/main" xmlns:cx="http://schemas.microsoft.com/office/drawing/2014/chartex" xmlns:c16ac="http://schemas.microsoft.com/office/drawing/2014/chart/ac" xmlns:c16="http://schemas.microsoft.com/office/drawing/2014/chart" xmlns:xr="http://schemas.microsoft.com/office/spreadsheetml/2014/revision" xmlns:xr2="http://schemas.microsoft.com/office/spreadsheetml/2015/revision2" xmlns:xr3="http://schemas.microsoft.com/office/spreadsheetml/2016/revision3" xmlns:xr4="http://schemas.microsoft.com/office/spreadsheetml/2016/revision4" xmlns:xr5="http://schemas.microsoft.com/office/spreadsheetml/2016/revision5" xmlns:xr6="http://schemas.microsoft.com/office/spreadsheetml/2016/revision6" xmlns:xr7="http://schemas.microsoft.com/office/spreadsheetml/2016/revision7" xmlns:xr8="http://schemas.microsoft.com/office/spreadsheetml/2016/revision8" xmlns:xr9="http://schemas.microsoft.com/office/spreadsheetml/2016/revision9" xmlns:xr10="http://schemas.microsoft.com/office/spreadsheetml/2016/revision10" xmlns:xr11="http://schemas.microsoft.com/office/spreadsheetml/2016/revision11" xmlns:xr12="http://schemas.microsoft.com/office/spreadsheetml/2016/revision12" xmlns:xr13="http://schemas.microsoft.com/office/spreadsheetml/2016/revision13" xmlns:xr14="http://schemas.microsoft.com/office/spreadsheetml/2016/revision14" xmlns:xr15="http://schemas.microsoft.com/office/spreadsheetml/2016/revision15" xmlns:xr16="http://schemas.microsoft.com/office/spreadsheetml/2017/revision16" xmlns:xr17="http://schemas.microsoft.com/office/spreadsheetml/2017/revision17" xmlns:xr18="http://schemas.microsoft.com/office/spreadsheetml/2017/revision18" xmlns:xr19="http://schemas.microsoft.com/office/spreadsheetml/2017/revision19" xmlns:xr20="http://schemas.microsoft.com/office/spreadsheetml/2017/revision20" xmlns:xr21="http://schemas.microsoft.com/office/spreadsheetml/2018/revision21" xmlns:xr22="http://schemas.microsoft.com/office/spreadsheetml/2018/revision22" xmlns:x16="http://schemas.microsoft.com/office/spreadsheetml/2014/11/main" xmlns:x16r2="http://schemas.microsoft.com/office/spreadsheetml/2015/02/main" xmlns:w16se="http://schemas.microsoft.com/office/word/2015/wordml/symex" xmlns:mo="http://schemas.microsoft.com/office/mac/office/2008/main" xmlns:mx="http://schemas.microsoft.com/office/mac/excel/2008/main" xmlns:mv="urn:schemas-microsoft-com:mac:vml" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:v="urn:schemas-microsoft-com:vml" xr:uid="{00000000-0001-0000-0000-000000000000}">` +const templateNamespaceIDMap = ` xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:ap="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" xmlns:op="http://schemas.openxmlformats.org/officeDocument/2006/custom-properties" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:cdr="http://schemas.openxmlformats.org/drawingml/2006/chartDrawing" xmlns:comp="http://schemas.openxmlformats.org/drawingml/2006/compatibility" xmlns:dgm="http://schemas.openxmlformats.org/drawingml/2006/diagram" xmlns:lc="http://schemas.openxmlformats.org/drawingml/2006/lockedCanvas" xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture" xmlns:xdr="http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:ds="http://schemas.openxmlformats.org/officeDocument/2006/customXml" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:x="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:sl="http://schemas.openxmlformats.org/schemaLibrary/2006/main" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:xne="http://schemas.microsoft.com/office/excel/2006/main" xmlns:mso="http://schemas.microsoft.com/office/2006/01/customui" xmlns:ax="http://schemas.microsoft.com/office/2006/activeX" xmlns:cppr="http://schemas.microsoft.com/office/2006/coverPageProps" xmlns:cdip="http://schemas.microsoft.com/office/2006/customDocumentInformationPanel" xmlns:ct="http://schemas.microsoft.com/office/2006/metadata/contentType" xmlns:ntns="http://schemas.microsoft.com/office/2006/metadata/customXsn" xmlns:lp="http://schemas.microsoft.com/office/2006/metadata/longProperties" xmlns:ma="http://schemas.microsoft.com/office/2006/metadata/properties/metaAttributes" xmlns:msink="http://schemas.microsoft.com/ink/2010/main" xmlns:c14="http://schemas.microsoft.com/office/drawing/2007/8/2/chart" xmlns:cdr14="http://schemas.microsoft.com/office/drawing/2010/chartDrawing" xmlns:a14="http://schemas.microsoft.com/office/drawing/2010/main" xmlns:pic14="http://schemas.microsoft.com/office/drawing/2010/picture" xmlns:x14="http://schemas.microsoft.com/office/spreadsheetml/2009/9/main" xmlns:xdr14="http://schemas.microsoft.com/office/excel/2010/spreadsheetDrawing" xmlns:x14ac="http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac" xmlns:dsp="http://schemas.microsoft.com/office/drawing/2008/diagram" xmlns:mso14="http://schemas.microsoft.com/office/2009/07/customui" xmlns:dgm14="http://schemas.microsoft.com/office/drawing/2010/diagram" xmlns:x15="http://schemas.microsoft.com/office/spreadsheetml/2010/11/main" xmlns:x12ac="http://schemas.microsoft.com/office/spreadsheetml/2011/1/ac" xmlns:x15ac="http://schemas.microsoft.com/office/spreadsheetml/2010/11/ac" xmlns:xr="http://schemas.microsoft.com/office/spreadsheetml/2014/revision" xmlns:xr2="http://schemas.microsoft.com/office/spreadsheetml/2015/revision2" xmlns:xr3="http://schemas.microsoft.com/office/spreadsheetml/2016/revision3" xmlns:xr4="http://schemas.microsoft.com/office/spreadsheetml/2016/revision4" xmlns:xr5="http://schemas.microsoft.com/office/spreadsheetml/2016/revision5" xmlns:xr6="http://schemas.microsoft.com/office/spreadsheetml/2016/revision6" xmlns:xr7="http://schemas.microsoft.com/office/spreadsheetml/2016/revision7" xmlns:xr8="http://schemas.microsoft.com/office/spreadsheetml/2016/revision8" xmlns:xr9="http://schemas.microsoft.com/office/spreadsheetml/2016/revision9" xmlns:xr10="http://schemas.microsoft.com/office/spreadsheetml/2016/revision10" xmlns:xr11="http://schemas.microsoft.com/office/spreadsheetml/2016/revision11" xmlns:xr12="http://schemas.microsoft.com/office/spreadsheetml/2016/revision12" xmlns:xr13="http://schemas.microsoft.com/office/spreadsheetml/2016/revision13" xmlns:xr14="http://schemas.microsoft.com/office/spreadsheetml/2016/revision14" xmlns:xr15="http://schemas.microsoft.com/office/spreadsheetml/2016/revision15" xmlns:x16="http://schemas.microsoft.com/office/spreadsheetml/2014/11/main" xmlns:x16r2="http://schemas.microsoft.com/office/spreadsheetml/2015/02/main" mc:Ignorable="c14 cdr14 a14 pic14 x14 xdr14 x14ac dsp mso14 dgm14 x15 x12ac x15ac xr xr2 xr3 xr4 xr5 xr6 xr7 xr8 xr9 xr10 xr11 xr12 xr13 xr14 xr15 x15 x16 x16r2 mo mx mv o v" xmlns:mo="http://schemas.microsoft.com/office/mac/office/2008/main" xmlns:mx="http://schemas.microsoft.com/office/mac/excel/2008/main" xmlns:mv="urn:schemas-microsoft-com:mac:vml" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:v="urn:schemas-microsoft-com:vml" xr:uid="{00000000-0001-0000-0000-000000000000}">` From 08d1a86c3a1bffdf431dba6a3d5a3b369ef740a7 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 10 Dec 2019 00:16:17 +0800 Subject: [PATCH 172/957] Fix #523, add stream writer for generate new worksheet with huge amounts of data --- stream.go | 219 ++++++++++++++++++++++++++++++++++++++++++++++++ stream_test.go | 66 +++++++++++++++ xmlTable.go | 1 + xmlWorksheet.go | 180 +++++++++++++++++++++------------------ 4 files changed, 386 insertions(+), 80 deletions(-) create mode 100644 stream.go create mode 100644 stream_test.go diff --git a/stream.go b/stream.go new file mode 100644 index 0000000000..0d91ddd3d1 --- /dev/null +++ b/stream.go @@ -0,0 +1,219 @@ +// Copyright 2016 - 2019 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. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.10 or later. + +package excelize + +import ( + "bytes" + "encoding/xml" + "errors" + "fmt" + "io/ioutil" + "os" + "reflect" +) + +// StreamWriter defined the type of stream writer. +type StreamWriter struct { + tmpFile *os.File + File *File + Sheet string + SheetID int + SheetData bytes.Buffer +} + +// 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. For +// example, set data for worksheet of size 102400 rows x 50 columns with +// numbers: +// +// file := excelize.NewFile() +// streamWriter, err := file.NewStreamWriter("Sheet1") +// if err != nil { +// panic(err) +// } +// for rowID := 1; 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 { +// panic(err) +// } +// } +// if err := streamWriter.Flush(); err != nil { +// panic(err) +// } +// if err := file.SaveAs("Book1.xlsx"); err != nil { +// panic(err) +// } +// +func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) { + sheetID := f.GetSheetIndex(sheet) + if sheetID == 0 { + return nil, fmt.Errorf("sheet %s is not exist", sheet) + } + rsw := &StreamWriter{ + File: f, + Sheet: sheet, + SheetID: sheetID, + } + rsw.SheetData.WriteString("") + return rsw, nil +} + +// SetRow writes an array to streaming row by given worksheet name, starting +// coordinate and a pointer to array type 'slice'. Note that, cell settings +// with styles are not supported currently and after set rows, you must call the +// 'Flush' method to end the streaming writing process. The following +// shows the supported data types: +// +// int +// string +// +func (sw *StreamWriter) SetRow(axis string, slice interface{}) 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 errors.New("pointer to slice expected") + } + v = v.Elem() + sw.SheetData.WriteString(fmt.Sprintf(``, row)) + for i := 0; i < v.Len(); i++ { + axis, err := CoordinatesToCellName(col+i, row) + if err != nil { + return err + } + switch val := v.Index(i).Interface().(type) { + case int: + sw.SheetData.WriteString(fmt.Sprintf(`%d`, axis, val)) + case string: + sw.SheetData.WriteString(sw.setCellStr(axis, val)) + default: + sw.SheetData.WriteString(sw.setCellStr(axis, fmt.Sprint(val))) + } + } + sw.SheetData.WriteString(``) + // Try to use local storage + chunk := 1 << 24 + if sw.SheetData.Len() >= chunk { + if sw.tmpFile == nil { + err := sw.createTmp() + if err != nil { + // can not use local storage + return nil + } + } + // use local storage + _, err := sw.tmpFile.Write(sw.SheetData.Bytes()) + if err != nil { + return nil + } + sw.SheetData.Reset() + } + return err +} + +// Flush ending the streaming writing process. +func (sw *StreamWriter) Flush() error { + sw.SheetData.WriteString(``) + + ws, err := sw.File.workSheetReader(sw.Sheet) + if err != nil { + return err + } + sheetXML := fmt.Sprintf("xl/worksheets/sheet%d.xml", sw.SheetID) + delete(sw.File.Sheet, sheetXML) + delete(sw.File.checked, sheetXML) + var sheetDataByte []byte + if sw.tmpFile != nil { + // close the local storage file + if err = sw.tmpFile.Close(); err != nil { + return err + } + + file, err := os.Open(sw.tmpFile.Name()) + if err != nil { + return err + } + + sheetDataByte, err = ioutil.ReadAll(file) + if err != nil { + return err + } + + if err := file.Close(); err != nil { + return err + } + + err = os.Remove(sw.tmpFile.Name()) + if err != nil { + return err + } + } + + sheetDataByte = append(sheetDataByte, sw.SheetData.Bytes()...) + replaceMap := map[string][]byte{ + "XMLName": []byte{}, + "SheetData": sheetDataByte, + } + sw.SheetData.Reset() + sw.File.XLSX[fmt.Sprintf("xl/worksheets/sheet%d.xml", sw.SheetID)] = + StreamMarshalSheet(ws, replaceMap) + return err +} + +// createTmp creates a temporary file in the operating system default +// temporary directory. +func (sw *StreamWriter) createTmp() (err error) { + sw.tmpFile, err = ioutil.TempFile(os.TempDir(), "excelize-") + return err +} + +// StreamMarshalSheet provides method to serialization worksheets by field as +// streaming. +func StreamMarshalSheet(ws *xlsxWorksheet, replaceMap map[string][]byte) []byte { + s := reflect.ValueOf(ws).Elem() + typeOfT := s.Type() + var marshalResult []byte + marshalResult = append(marshalResult, []byte(XMLHeader+``)...) + return marshalResult +} + +// setCellStr provides a function to set string type value of a cell as +// streaming. Total number of characters that a cell can contain 32767 +// characters. +func (sw *StreamWriter) setCellStr(axis, value string) string { + if len(value) > 32767 { + value = value[0:32767] + } + // Leading and ending space(s) character detection. + if len(value) > 0 && (value[0] == 32 || value[len(value)-1] == 32) { + return fmt.Sprintf(`%s`, axis, value) + } + return fmt.Sprintf(`%s`, axis, value) +} diff --git a/stream_test.go b/stream_test.go new file mode 100644 index 0000000000..97c55a7e09 --- /dev/null +++ b/stream_test.go @@ -0,0 +1,66 @@ +package excelize + +import ( + "math/rand" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestStreamWriter(t *testing.T) { + file := NewFile() + streamWriter, err := file.NewStreamWriter("Sheet1") + assert.NoError(t, err) + + // Test max characters in a cell. + row := make([]interface{}, 1) + row[0] = strings.Repeat("c", 32769) + assert.NoError(t, streamWriter.SetRow("A1", &row)) + + // 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)) + + row = make([]interface{}, 1) + row[0] = []byte("Word") + assert.NoError(t, streamWriter.SetRow("A3", &row)) + + for rowID := 10; rowID <= 51200; rowID++ { + row := make([]interface{}, 50) + for colID := 0; colID < 50; colID++ { + row[colID] = rand.Intn(640000) + } + cell, _ := CoordinatesToCellName(1, rowID) + assert.NoError(t, streamWriter.SetRow(cell, &row)) + } + + err = streamWriter.Flush() + assert.NoError(t, err) + // Save xlsx file by the given path. + assert.NoError(t, file.SaveAs(filepath.Join("test", "TestStreamWriter.xlsx"))) + + // Test error exceptions + streamWriter, err = file.NewStreamWriter("SheetN") + assert.EqualError(t, err, "sheet SheetN is not exist") +} + +func TestFlush(t *testing.T) { + // Test error exceptions + file := NewFile() + streamWriter, err := file.NewStreamWriter("Sheet1") + assert.NoError(t, err) + streamWriter.Sheet = "SheetN" + assert.EqualError(t, streamWriter.Flush(), "sheet SheetN is not exist") +} + +func TestSetRow(t *testing.T) { + // Test error exceptions + file := NewFile() + streamWriter, err := file.NewStreamWriter("Sheet1") + assert.NoError(t, err) + assert.EqualError(t, streamWriter.SetRow("A", &[]interface{}{}), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.EqualError(t, streamWriter.SetRow("A1", []interface{}{}), `pointer to slice expected`) +} diff --git a/xmlTable.go b/xmlTable.go index ca4ce03610..017bda1062 100644 --- a/xmlTable.go +++ b/xmlTable.go @@ -44,6 +44,7 @@ type xlsxTable struct { // applied column by column to a table of data in the worksheet. This collection // expresses AutoFilter settings. type xlsxAutoFilter struct { + XMLName xml.Name `xml:"autoFilter"` Ref string `xml:"ref,attr"` FilterColumn *xlsxFilterColumn `xml:"filterColumn"` } diff --git a/xmlWorksheet.go b/xmlWorksheet.go index cb854cdbdb..a071e4d5d9 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -59,7 +59,8 @@ type xlsxWorksheet struct { // xlsxDrawing change r:id to rid in the namespace. type xlsxDrawing struct { - RID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr,omitempty"` + XMLName xml.Name `xml:"drawing"` + RID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr,omitempty"` } // xlsxHeaderFooter directly maps the headerFooter element in the namespace @@ -70,6 +71,7 @@ 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"` @@ -91,32 +93,33 @@ type xlsxHeaderFooter struct { // each of the left section, center section and right section of a header and // a footer. type xlsxDrawingHF struct { - Content string `xml:",chardata"` + Content string `xml:",innerxml"` } // xlsxPageSetUp directly maps the pageSetup element in the namespace // http://schemas.openxmlformats.org/spreadsheetml/2006/main - Page setup // settings for the worksheet. type xlsxPageSetUp struct { - BlackAndWhite bool `xml:"blackAndWhite,attr,omitempty"` - CellComments string `xml:"cellComments,attr,omitempty"` - Copies int `xml:"copies,attr,omitempty"` - Draft bool `xml:"draft,attr,omitempty"` - Errors string `xml:"errors,attr,omitempty"` - FirstPageNumber int `xml:"firstPageNumber,attr,omitempty"` - FitToHeight int `xml:"fitToHeight,attr,omitempty"` - FitToWidth int `xml:"fitToWidth,attr,omitempty"` - HorizontalDPI float32 `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"` - PaperHeight string `xml:"paperHeight,attr,omitempty"` - PaperSize int `xml:"paperSize,attr,omitempty"` - PaperWidth string `xml:"paperWidth,attr,omitempty"` - Scale int `xml:"scale,attr,omitempty"` - UseFirstPageNumber bool `xml:"useFirstPageNumber,attr,omitempty"` - UsePrinterDefaults bool `xml:"usePrinterDefaults,attr,omitempty"` - VerticalDPI float32 `xml:"verticalDpi,attr,omitempty"` + XMLName xml.Name `xml:"pageSetup"` + BlackAndWhite bool `xml:"blackAndWhite,attr,omitempty"` + CellComments string `xml:"cellComments,attr,omitempty"` + Copies int `xml:"copies,attr,omitempty"` + Draft bool `xml:"draft,attr,omitempty"` + Errors string `xml:"errors,attr,omitempty"` + FirstPageNumber int `xml:"firstPageNumber,attr,omitempty"` + FitToHeight int `xml:"fitToHeight,attr,omitempty"` + FitToWidth int `xml:"fitToWidth,attr,omitempty"` + HorizontalDPI float32 `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"` + PaperHeight string `xml:"paperHeight,attr,omitempty"` + PaperSize int `xml:"paperSize,attr,omitempty"` + PaperWidth string `xml:"paperWidth,attr,omitempty"` + Scale int `xml:"scale,attr,omitempty"` + UseFirstPageNumber bool `xml:"useFirstPageNumber,attr,omitempty"` + UsePrinterDefaults bool `xml:"usePrinterDefaults,attr,omitempty"` + VerticalDPI float32 `xml:"verticalDpi,attr,omitempty"` } // xlsxPrintOptions directly maps the printOptions element in the namespace @@ -124,44 +127,48 @@ type xlsxPageSetUp struct { // the sheet. Printer-specific settings are stored separately in the Printer // Settings part. type xlsxPrintOptions struct { - GridLines bool `xml:"gridLines,attr,omitempty"` - GridLinesSet bool `xml:"gridLinesSet,attr,omitempty"` - Headings bool `xml:"headings,attr,omitempty"` - HorizontalCentered bool `xml:"horizontalCentered,attr,omitempty"` - VerticalCentered bool `xml:"verticalCentered,attr,omitempty"` + XMLName xml.Name `xml:"printOptions"` + GridLines bool `xml:"gridLines,attr,omitempty"` + GridLinesSet bool `xml:"gridLinesSet,attr,omitempty"` + Headings bool `xml:"headings,attr,omitempty"` + HorizontalCentered bool `xml:"horizontalCentered,attr,omitempty"` + VerticalCentered bool `xml:"verticalCentered,attr,omitempty"` } // xlsxPageMargins directly maps the pageMargins element in the namespace // http://schemas.openxmlformats.org/spreadsheetml/2006/main - Page margins for // a sheet or a custom sheet view. type xlsxPageMargins struct { - 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"` + 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"` } // xlsxSheetFormatPr directly maps the sheetFormatPr element in the namespace // http://schemas.openxmlformats.org/spreadsheetml/2006/main. This element // specifies the sheet formatting properties. type xlsxSheetFormatPr struct { - BaseColWidth uint8 `xml:"baseColWidth,attr,omitempty"` - DefaultColWidth float64 `xml:"defaultColWidth,attr,omitempty"` - DefaultRowHeight float64 `xml:"defaultRowHeight,attr"` - CustomHeight bool `xml:"customHeight,attr,omitempty"` - ZeroHeight bool `xml:"zeroHeight,attr,omitempty"` - ThickTop bool `xml:"thickTop,attr,omitempty"` - ThickBottom bool `xml:"thickBottom,attr,omitempty"` - OutlineLevelRow uint8 `xml:"outlineLevelRow,attr,omitempty"` - OutlineLevelCol uint8 `xml:"outlineLevelCol,attr,omitempty"` + XMLName xml.Name `xml:"sheetFormatPr"` + BaseColWidth uint8 `xml:"baseColWidth,attr,omitempty"` + DefaultColWidth float64 `xml:"defaultColWidth,attr,omitempty"` + DefaultRowHeight float64 `xml:"defaultRowHeight,attr"` + CustomHeight bool `xml:"customHeight,attr,omitempty"` + ZeroHeight bool `xml:"zeroHeight,attr,omitempty"` + ThickTop bool `xml:"thickTop,attr,omitempty"` + ThickBottom bool `xml:"thickBottom,attr,omitempty"` + OutlineLevelRow uint8 `xml:"outlineLevelRow,attr,omitempty"` + OutlineLevelCol uint8 `xml:"outlineLevelCol,attr,omitempty"` } // xlsxSheetViews directly maps the sheetViews element in the namespace // http://schemas.openxmlformats.org/spreadsheetml/2006/main - Worksheet views // collection. type xlsxSheetViews struct { + XMLName xml.Name `xml:"sheetViews"` SheetView []xlsxSheetView `xml:"sheetView"` } @@ -263,7 +270,8 @@ type xlsxTabColor struct { // http://schemas.openxmlformats.org/spreadsheetml/2006/main - currently I have // not checked it for completeness - it does as much as I need. type xlsxCols struct { - Col []xlsxCol `xml:"col"` + XMLName xml.Name `xml:"cols"` + Col []xlsxCol `xml:"col"` } // xlsxCol directly maps the col (Column Width & Formatting). Defines column @@ -289,7 +297,8 @@ type xlsxCol struct { // When an entire column is formatted, only the first cell in that column is // considered used. type xlsxDimension struct { - Ref string `xml:"ref,attr"` + XMLName xml.Name `xml:"dimension"` + Ref string `xml:"ref,attr"` } // xlsxSheetData directly maps the sheetData element in the namespace @@ -322,6 +331,7 @@ type xlsxRow struct { // xlsxCustomSheetViews directly maps the customSheetViews element. This is a // collection of custom sheet views. type xlsxCustomSheetViews struct { + XMLName xml.Name `xml:"customSheetViews"` CustomSheetView []*xlsxCustomSheetView `xml:"customSheetView"` } @@ -384,13 +394,15 @@ type xlsxMergeCell struct { // xlsxMergeCells directly maps the mergeCells element. This collection // expresses all the merged cells in the sheet. type xlsxMergeCells struct { - Count int `xml:"count,attr,omitempty"` - Cells []*xlsxMergeCell `xml:"mergeCell,omitempty"` + XMLName xml.Name `xml:"mergeCells"` + Count int `xml:"count,attr,omitempty"` + Cells []*xlsxMergeCell `xml:"mergeCell,omitempty"` } // xlsxDataValidations expresses all data validation information for cells in a // sheet which have data validation features applied. type xlsxDataValidations struct { + XMLName xml.Name `xml:"dataValidations"` Count int `xml:"count,attr,omitempty"` DisablePrompts bool `xml:"disablePrompts,attr,omitempty"` XWindow int `xml:"xWindow,attr,omitempty"` @@ -434,14 +446,15 @@ type DataValidation struct { // str (String) | Cell containing a formula string. // type xlsxC struct { - R string `xml:"r,attr"` // 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. - F *xlsxF `xml:"f,omitempty"` // Formula - V string `xml:"v,omitempty"` // Value - IS *xlsxSI `xml:"is"` + XMLName xml.Name `xml:"c"` XMLSpace xml.Attr `xml:"space,attr,omitempty"` + R string `xml:"r,attr"` // 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. + F *xlsxF `xml:"f,omitempty"` // Formula + V string `xml:"v,omitempty"` // Value + IS *xlsxSI `xml:"is"` } func (c *xlsxC) hasValue() bool { @@ -461,27 +474,28 @@ type xlsxF struct { // xlsxSheetProtection collection expresses the sheet protection options to // enforce when the sheet is protected. type xlsxSheetProtection struct { - AlgorithmName string `xml:"algorithmName,attr,omitempty"` - Password string `xml:"password,attr,omitempty"` - HashValue string `xml:"hashValue,attr,omitempty"` - SaltValue string `xml:"saltValue,attr,omitempty"` - SpinCount int `xml:"spinCount,attr,omitempty"` - Sheet bool `xml:"sheet,attr"` - Objects bool `xml:"objects,attr"` - Scenarios bool `xml:"scenarios,attr"` - FormatCells bool `xml:"formatCells,attr"` - FormatColumns bool `xml:"formatColumns,attr"` - FormatRows bool `xml:"formatRows,attr"` - InsertColumns bool `xml:"insertColumns,attr"` - InsertRows bool `xml:"insertRows,attr"` - InsertHyperlinks bool `xml:"insertHyperlinks,attr"` - DeleteColumns bool `xml:"deleteColumns,attr"` - DeleteRows bool `xml:"deleteRows,attr"` - SelectLockedCells bool `xml:"selectLockedCells,attr"` - Sort bool `xml:"sort,attr"` - AutoFilter bool `xml:"autoFilter,attr"` - PivotTables bool `xml:"pivotTables,attr"` - SelectUnlockedCells bool `xml:"selectUnlockedCells,attr"` + XMLName xml.Name `xml:"sheetProtection"` + AlgorithmName string `xml:"algorithmName,attr,omitempty"` + Password string `xml:"password,attr,omitempty"` + HashValue string `xml:"hashValue,attr,omitempty"` + SaltValue string `xml:"saltValue,attr,omitempty"` + SpinCount int `xml:"spinCount,attr,omitempty"` + Sheet bool `xml:"sheet,attr"` + Objects bool `xml:"objects,attr"` + Scenarios bool `xml:"scenarios,attr"` + FormatCells bool `xml:"formatCells,attr"` + FormatColumns bool `xml:"formatColumns,attr"` + FormatRows bool `xml:"formatRows,attr"` + InsertColumns bool `xml:"insertColumns,attr"` + InsertRows bool `xml:"insertRows,attr"` + InsertHyperlinks bool `xml:"insertHyperlinks,attr"` + DeleteColumns bool `xml:"deleteColumns,attr"` + DeleteRows bool `xml:"deleteRows,attr"` + SelectLockedCells bool `xml:"selectLockedCells,attr"` + Sort bool `xml:"sort,attr"` + AutoFilter bool `xml:"autoFilter,attr"` + PivotTables bool `xml:"pivotTables,attr"` + SelectUnlockedCells bool `xml:"selectUnlockedCells,attr"` } // xlsxPhoneticPr (Phonetic Properties) represents a collection of phonetic @@ -492,9 +506,10 @@ type xlsxSheetProtection struct { // every phonetic hint is expressed as a phonetic run (rPh), and these // properties specify how to display that phonetic run. type xlsxPhoneticPr struct { - Alignment string `xml:"alignment,attr,omitempty"` - FontID *int `xml:"fontId,attr"` - Type string `xml:"type,attr,omitempty"` + XMLName xml.Name `xml:"phoneticPr"` + Alignment string `xml:"alignment,attr,omitempty"` + FontID *int `xml:"fontId,attr"` + Type string `xml:"type,attr,omitempty"` } // A Conditional Format is a format, such as cell shading or font color, that a @@ -502,8 +517,9 @@ type xlsxPhoneticPr struct { // condition is true. This collection expresses conditional formatting rules // applied to a particular cell or range. type xlsxConditionalFormatting struct { - SQRef string `xml:"sqref,attr,omitempty"` - CfRule []*xlsxCfRule `xml:"cfRule"` + XMLName xml.Name `xml:"conditionalFormatting"` + SQRef string `xml:"sqref,attr,omitempty"` + CfRule []*xlsxCfRule `xml:"cfRule"` } // xlsxCfRule (Conditional Formatting Rule) represents a description of a @@ -568,6 +584,7 @@ type xlsxCfvo struct { // be stored in a package as a relationship. Hyperlinks shall be identified by // containing a target which specifies the destination of the given hyperlink. type xlsxHyperlinks struct { + XMLName xml.Name `xml:"hyperlinks"` Hyperlink []xlsxHyperlink `xml:"hyperlink"` } @@ -612,6 +629,7 @@ type xlsxHyperlink struct { // // type xlsxTableParts struct { + XMLName xml.Name `xml:"tableParts"` Count int `xml:"count,attr,omitempty"` TableParts []*xlsxTablePart `xml:"tablePart"` } @@ -629,7 +647,8 @@ type xlsxTablePart struct { // // type xlsxPicture struct { - RID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr,omitempty"` + XMLName xml.Name `xml:"picture"` + RID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr,omitempty"` } // xlsxLegacyDrawing directly maps the legacyDrawing element in the namespace @@ -642,7 +661,8 @@ type xlsxPicture struct { // can also be used to explain assumptions made in a formula or to call out // something special about the cell. type xlsxLegacyDrawing struct { - RID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr,omitempty"` + XMLName xml.Name `xml:"legacyDrawing"` + RID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr,omitempty"` } type xlsxInnerXML struct { From 5d8365ca17240f5b144d437a7b47052f22c4f3c6 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 11 Dec 2019 00:02:33 +0800 Subject: [PATCH 173/957] Fix #529, handle empty inline rich text --- rows.go | 5 ++++- rows_test.go | 9 +++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/rows.go b/rows.go index 69a984610e..ff4aa0f974 100644 --- a/rows.go +++ b/rows.go @@ -279,7 +279,10 @@ func (xlsx *xlsxC) getValueFrom(f *File, d *xlsxSST) (string, error) { case "str": return f.formattedValue(xlsx.S, xlsx.V), nil case "inlineStr": - return f.formattedValue(xlsx.S, xlsx.IS.String()), nil + if xlsx.IS != nil { + return f.formattedValue(xlsx.S, xlsx.IS.String()), nil + } + return f.formattedValue(xlsx.S, xlsx.V), nil default: return f.formattedValue(xlsx.S, xlsx.V), nil } diff --git a/rows_test.go b/rows_test.go index 47c9d96946..6b50c75256 100644 --- a/rows_test.go +++ b/rows_test.go @@ -706,6 +706,15 @@ func TestDuplicateRowInvalidRownum(t *testing.T) { } } +func TestGetValueFrom(t *testing.T) { + c := &xlsxC{T: "inlineStr"} + f := NewFile() + d := &xlsxSST{} + val, err := c.getValueFrom(f, d) + assert.NoError(t, err) + assert.Equal(t, "", val) +} + func TestErrSheetNotExistError(t *testing.T) { err := ErrSheetNotExist{SheetName: "Sheet1"} assert.EqualValues(t, err.Error(), "sheet Sheet1 is not exist") From 4c433c57e65734094f959d25b50f138a6ca88020 Mon Sep 17 00:00:00 2001 From: Xudong Zhang Date: Fri, 13 Dec 2019 21:43:59 +0800 Subject: [PATCH 174/957] Resolve #527, unmerge an area (#528) --- cellmerged.go | 50 ++++++++++++++++++++++++++++++++++++++++++++++ cellmerged_test.go | 36 +++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) diff --git a/cellmerged.go b/cellmerged.go index c1df9b3db9..4a5d11f4d2 100644 --- a/cellmerged.go +++ b/cellmerged.go @@ -33,6 +33,56 @@ func (f *File) GetMergeCells(sheet string) ([]MergeCell, error) { return mergeCells, err } +// UnmergeCell provides a function to unmerge a given coordinate area. +// 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 { + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return err + } + coordinates, err := f.areaRefToCoordinates(hcell + ":" + vcell) + if err != nil { + return err + } + x1, y1, x2, y2 := coordinates[0], coordinates[1], coordinates[2], coordinates[3] + + if x2 < x1 { + x1, x2 = x2, x1 + } + if y2 < y1 { + y1, y2 = y2, y1 + } + hcell, _ = CoordinatesToCellName(x1, y1) + vcell, _ = CoordinatesToCellName(x2, y2) + + // return nil since no MergeCells in the sheet + if xlsx.MergeCells == nil { + return nil + } + + ref := hcell + ":" + vcell + i := 0 + for _, cellData := range xlsx.MergeCells.Cells { + cc := strings.Split(cellData.Ref, ":") + c1, _ := checkCellInArea(hcell, cellData.Ref) + c2, _ := checkCellInArea(vcell, cellData.Ref) + c3, _ := checkCellInArea(cc[0], ref) + c4, _ := checkCellInArea(cc[1], ref) + // skip the overlapped mergecell + if c1 || c2 || c3 || c4 { + continue + } + xlsx.MergeCells.Cells[i] = cellData + i++ + } + xlsx.MergeCells.Cells = xlsx.MergeCells.Cells[:i] + return nil +} + // MergeCell define a merged cell data. // It consists of the following structure. // example: []string{"D4:E10", "cell value"} diff --git a/cellmerged_test.go b/cellmerged_test.go index d53acc2eb3..0c5ac76e44 100644 --- a/cellmerged_test.go +++ b/cellmerged_test.go @@ -52,3 +52,39 @@ func TestGetMergeCells(t *testing.T) { _, err = f.GetMergeCells("SheetN") assert.EqualError(t, err, "sheet SheetN is not exist") } + +func TestUnmergeCell(t *testing.T) { + f, err := OpenFile(filepath.Join("test", "MergeCell.xlsx")) + if !assert.NoError(t, err) { + t.FailNow() + } + sheet1 := f.GetSheetName(1) + + xlsx, err := f.workSheetReader(sheet1) + assert.NoError(t, err) + + mergeCellNum := len(xlsx.MergeCells.Cells) + + assert.EqualError(t, f.UnmergeCell("Sheet1", "A", "A"), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + + // unmerge the mergecell that contains A1 + err = f.UnmergeCell(sheet1, "A1", "A1") + assert.NoError(t, err) + + if len(xlsx.MergeCells.Cells) != mergeCellNum-1 { + t.FailNow() + } + + // unmerge area A7:D3(A3:D7) + // this will unmerge all since this area overlaps with all others + err = f.UnmergeCell(sheet1, "D7", "A3") + assert.NoError(t, err) + + if len(xlsx.MergeCells.Cells) != 0 { + t.FailNow() + } + + // Test unmerged area on not exists worksheet. + err = f.UnmergeCell("SheetN", "A1", "A1") + assert.EqualError(t, err, "sheet SheetN is not exist") +} From da0d2ffbb6ebdfb7b1e5cf501a1986421311017b Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 14 Dec 2019 19:57:37 +0800 Subject: [PATCH 175/957] Fix #533, add support overlapped mergecells --- adjust.go | 3 - cell.go | 86 +++++++----------------- cell_test.go | 37 +++-------- cellmerged.go | 159 ++++++++++++++++++++++++++++++++++++--------- cellmerged_test.go | 99 ++++++++++++++++++++++++---- excelize_test.go | 23 +++++++ xmlWorksheet.go | 4 +- 7 files changed, 273 insertions(+), 138 deletions(-) diff --git a/adjust.go b/adjust.go index 186112d6f5..bb583f17dc 100644 --- a/adjust.go +++ b/adjust.go @@ -206,9 +206,6 @@ func (f *File) areaRefToCoordinates(ref string) ([]int, error) { return coordinates, err } coordinates[2], coordinates[3], err = CellNameToCoordinates(lastCell) - if err != nil { - return coordinates, err - } return coordinates, err } diff --git a/cell.go b/cell.go index a25f2e4cf3..ad4bcdb962 100644 --- a/cell.go +++ b/cell.go @@ -412,63 +412,6 @@ func (f *File) SetCellHyperLink(sheet, axis, link, linkType string) error { return nil } -// MergeCell provides a function to merge cells by given coordinate area and -// sheet name. For example create a merged cell of D3:E9 on Sheet1: -// -// 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. -func (f *File) MergeCell(sheet, hcell, vcell string) error { - coordinates, err := f.areaRefToCoordinates(hcell + ":" + vcell) - if err != nil { - return err - } - x1, y1, x2, y2 := coordinates[0], coordinates[1], coordinates[2], coordinates[3] - - if x1 == x2 && y1 == y2 { - return err - } - - // Correct the coordinate area, such correct C1:B3 to B1:C3. - if x2 < x1 { - x1, x2 = x2, x1 - } - - if y2 < y1 { - y1, y2 = y2, y1 - } - - hcell, _ = CoordinatesToCellName(x1, y1) - vcell, _ = CoordinatesToCellName(x2, y2) - - xlsx, err := f.workSheetReader(sheet) - if err != nil { - return err - } - if xlsx.MergeCells != nil { - ref := hcell + ":" + vcell - // Delete the merged cells of the overlapping area. - for _, cellData := range xlsx.MergeCells.Cells { - cc := strings.Split(cellData.Ref, ":") - if len(cc) != 2 { - return fmt.Errorf("invalid area %q", cellData.Ref) - } - c1, _ := checkCellInArea(hcell, cellData.Ref) - c2, _ := checkCellInArea(vcell, cellData.Ref) - c3, _ := checkCellInArea(cc[0], ref) - c4, _ := checkCellInArea(cc[1], ref) - if !(!c1 && !c2 && !c3 && !c4) { - return nil - } - } - xlsx.MergeCells.Cells = append(xlsx.MergeCells.Cells, &xlsxMergeCell{Ref: ref}) - } else { - xlsx.MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: hcell + ":" + vcell}}} - } - return err -} - // 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: @@ -601,7 +544,7 @@ func (f *File) mergeCellsParser(xlsx *xlsxWorksheet, axis string) (string, error axis = strings.ToUpper(axis) if xlsx.MergeCells != nil { for i := 0; i < len(xlsx.MergeCells.Cells); i++ { - ok, err := checkCellInArea(axis, xlsx.MergeCells.Cells[i].Ref) + ok, err := f.checkCellInArea(axis, xlsx.MergeCells.Cells[i].Ref) if err != nil { return axis, err } @@ -615,7 +558,7 @@ func (f *File) mergeCellsParser(xlsx *xlsxWorksheet, axis string) (string, error // checkCellInArea provides a function to determine if a given coordinate is // within an area. -func checkCellInArea(cell, area string) (bool, error) { +func (f *File) checkCellInArea(cell, area string) (bool, error) { col, row, err := CellNameToCoordinates(cell) if err != nil { return false, err @@ -625,11 +568,30 @@ func checkCellInArea(cell, area string) (bool, error) { if len(rng) != 2 { return false, err } + coordinates, err := f.areaRefToCoordinates(area) + if err != nil { + return false, err + } + + return cellInRef([]int{col, row}, coordinates), err +} - firstCol, firstRow, _ := CellNameToCoordinates(rng[0]) - lastCol, lastRow, _ := CellNameToCoordinates(rng[1]) +// cellInRef provides a function to determine if a given range is within an +// range. +func cellInRef(cell, ref []int) bool { + return cell[0] >= ref[0] && cell[0] <= ref[2] && cell[1] >= ref[1] && cell[1] <= ref[3] +} - return col >= firstCol && col <= lastCol && row >= firstRow && row <= lastRow, err +// 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) } // getSharedForumula find a cell contains the same formula as another cell, diff --git a/cell_test.go b/cell_test.go index da0c1f1f68..b030622326 100644 --- a/cell_test.go +++ b/cell_test.go @@ -10,6 +10,7 @@ import ( ) func TestCheckCellInArea(t *testing.T) { + f := NewFile() expectedTrueCellInAreaList := [][2]string{ {"c2", "A1:AAZ32"}, {"B9", "A1:B9"}, @@ -19,7 +20,7 @@ func TestCheckCellInArea(t *testing.T) { for _, expectedTrueCellInArea := range expectedTrueCellInAreaList { cell := expectedTrueCellInArea[0] area := expectedTrueCellInArea[1] - ok, err := checkCellInArea(cell, area) + ok, err := f.checkCellInArea(cell, area) assert.NoError(t, err) assert.Truef(t, ok, "Expected cell %v to be in area %v, got false\n", cell, area) @@ -34,13 +35,17 @@ func TestCheckCellInArea(t *testing.T) { for _, expectedFalseCellInArea := range expectedFalseCellInAreaList { cell := expectedFalseCellInArea[0] area := expectedFalseCellInArea[1] - ok, err := checkCellInArea(cell, area) + ok, err := f.checkCellInArea(cell, area) assert.NoError(t, err) assert.Falsef(t, ok, "Expected cell %v not to be inside of area %v, but got true\n", cell, area) } - ok, err := checkCellInArea("AA0", "Z0:AB1") + ok, err := f.checkCellInArea("A1", "A:B") + assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.False(t, ok) + + ok, err = f.checkCellInArea("AA0", "Z0:AB1") assert.EqualError(t, err, `cannot convert cell "AA0" to coordinates: invalid cell name "AA0"`) assert.False(t, ok) } @@ -94,32 +99,6 @@ func TestGetCellFormula(t *testing.T) { f.GetCellFormula("Sheet", "A1") } -func TestMergeCell(t *testing.T) { - f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } - assert.EqualError(t, f.MergeCell("Sheet1", "A", "B"), `cannot convert cell "A" to coordinates: invalid cell name "A"`) - f.MergeCell("Sheet1", "D9", "D9") - f.MergeCell("Sheet1", "D9", "E9") - f.MergeCell("Sheet1", "H14", "G13") - f.MergeCell("Sheet1", "C9", "D8") - f.MergeCell("Sheet1", "F11", "G13") - f.MergeCell("Sheet1", "H7", "B15") - f.MergeCell("Sheet1", "D11", "F13") - f.MergeCell("Sheet1", "G10", "K12") - f.SetCellValue("Sheet1", "G11", "set value in merged cell") - f.SetCellInt("Sheet1", "H11", 100) - f.SetCellValue("Sheet1", "I11", float64(0.5)) - f.SetCellHyperLink("Sheet1", "J11", "https://github.com/360EntSecGroup-Skylar/excelize", "External") - f.SetCellFormula("Sheet1", "G12", "SUM(Sheet1!B19,Sheet1!C19)") - f.GetCellValue("Sheet1", "H11") - f.GetCellValue("Sheet2", "A6") // Merged cell ref is single coordinate. - f.GetCellFormula("Sheet1", "G12") - - assert.NoError(t, f.SaveAs(filepath.Join("test", "TestMergeCell.xlsx"))) -} - func ExampleFile_SetCellFloat() { f := NewFile() var x = 3.14159265 diff --git a/cellmerged.go b/cellmerged.go index 4a5d11f4d2..968a28a7f2 100644 --- a/cellmerged.go +++ b/cellmerged.go @@ -9,28 +9,102 @@ package excelize -import "strings" +import ( + "fmt" + "strings" +) + +// MergeCell provides a function to merge cells by given coordinate area and +// sheet name. For example create a merged cell of D3:E9 on Sheet1: +// +// 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. +// +// 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 { + rect1, err := f.areaRefToCoordinates(hcell + ":" + vcell) + if err != nil { + return err + } + // Correct the coordinate area, such correct C1:B3 to B1:C3. + if rect1[2] < rect1[0] { + rect1[0], rect1[2] = rect1[2], rect1[0] + } + + if rect1[3] < rect1[1] { + rect1[1], rect1[3] = rect1[3], rect1[1] + } + + hcell, _ = CoordinatesToCellName(rect1[0], rect1[1]) + vcell, _ = CoordinatesToCellName(rect1[2], rect1[3]) -// GetMergeCells provides a function to get all merged cells from a worksheet -// currently. -func (f *File) GetMergeCells(sheet string) ([]MergeCell, error) { - var mergeCells []MergeCell xlsx, err := f.workSheetReader(sheet) if err != nil { - return mergeCells, err + return err } + ref := hcell + ":" + vcell if xlsx.MergeCells != nil { - mergeCells = make([]MergeCell, 0, len(xlsx.MergeCells.Cells)) + for i := 0; i < len(xlsx.MergeCells.Cells); i++ { + cellData := xlsx.MergeCells.Cells[i] + if cellData == nil { + continue + } + cc := strings.Split(cellData.Ref, ":") + if len(cc) != 2 { + return fmt.Errorf("invalid area %q", cellData.Ref) + } - for i := range xlsx.MergeCells.Cells { - ref := xlsx.MergeCells.Cells[i].Ref - axis := strings.Split(ref, ":")[0] - val, _ := f.GetCellValue(sheet, axis) - mergeCells = append(mergeCells, []string{ref, val}) + rect2, err := f.areaRefToCoordinates(cellData.Ref) + if err != nil { + return err + } + + // Delete the merged cells of the overlapping area. + if isOverlap(rect1, rect2) { + xlsx.MergeCells.Cells = append(xlsx.MergeCells.Cells[:i], xlsx.MergeCells.Cells[i+1:]...) + i-- + + if rect1[0] > rect2[0] { + rect1[0], rect2[0] = rect2[0], rect1[0] + } + + if rect1[2] < rect2[2] { + rect1[2], rect2[2] = rect2[2], rect1[2] + } + + if rect1[1] > rect2[1] { + rect1[1], rect2[1] = rect2[1], rect1[1] + } + + if rect1[3] < rect2[3] { + rect1[3], rect2[3] = rect2[3], rect1[3] + } + hcell, _ = CoordinatesToCellName(rect1[0], rect1[1]) + vcell, _ = CoordinatesToCellName(rect1[2], rect1[3]) + ref = hcell + ":" + vcell + } } + xlsx.MergeCells.Cells = append(xlsx.MergeCells.Cells, &xlsxMergeCell{Ref: ref}) + } else { + xlsx.MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: ref}}} } - - return mergeCells, err + return err } // UnmergeCell provides a function to unmerge a given coordinate area. @@ -44,36 +118,41 @@ func (f *File) UnmergeCell(sheet string, hcell, vcell string) error { if err != nil { return err } - coordinates, err := f.areaRefToCoordinates(hcell + ":" + vcell) + rect1, err := f.areaRefToCoordinates(hcell + ":" + vcell) if err != nil { return err } - x1, y1, x2, y2 := coordinates[0], coordinates[1], coordinates[2], coordinates[3] - if x2 < x1 { - x1, x2 = x2, x1 + if rect1[2] < rect1[0] { + rect1[0], rect1[2] = rect1[2], rect1[0] } - if y2 < y1 { - y1, y2 = y2, y1 + if rect1[3] < rect1[1] { + rect1[1], rect1[3] = rect1[3], rect1[1] } - hcell, _ = CoordinatesToCellName(x1, y1) - vcell, _ = CoordinatesToCellName(x2, y2) + hcell, _ = CoordinatesToCellName(rect1[0], rect1[1]) + vcell, _ = CoordinatesToCellName(rect1[2], rect1[3]) // return nil since no MergeCells in the sheet if xlsx.MergeCells == nil { return nil } - ref := hcell + ":" + vcell i := 0 for _, cellData := range xlsx.MergeCells.Cells { + if cellData == nil { + continue + } cc := strings.Split(cellData.Ref, ":") - c1, _ := checkCellInArea(hcell, cellData.Ref) - c2, _ := checkCellInArea(vcell, cellData.Ref) - c3, _ := checkCellInArea(cc[0], ref) - c4, _ := checkCellInArea(cc[1], ref) - // skip the overlapped mergecell - if c1 || c2 || c3 || c4 { + if len(cc) != 2 { + return fmt.Errorf("invalid area %q", cellData.Ref) + } + + rect2, err := f.areaRefToCoordinates(cellData.Ref) + if err != nil { + return err + } + + if isOverlap(rect1, rect2) { continue } xlsx.MergeCells.Cells[i] = cellData @@ -83,6 +162,28 @@ func (f *File) UnmergeCell(sheet string, hcell, vcell string) error { return nil } +// GetMergeCells provides a function to get all merged cells from a worksheet +// currently. +func (f *File) GetMergeCells(sheet string) ([]MergeCell, error) { + var mergeCells []MergeCell + xlsx, err := f.workSheetReader(sheet) + if err != nil { + return mergeCells, err + } + if xlsx.MergeCells != nil { + mergeCells = make([]MergeCell, 0, len(xlsx.MergeCells.Cells)) + + for i := range xlsx.MergeCells.Cells { + ref := xlsx.MergeCells.Cells[i].Ref + axis := strings.Split(ref, ":")[0] + val, _ := f.GetCellValue(sheet, axis) + mergeCells = append(mergeCells, []string{ref, val}) + } + } + + return mergeCells, err +} + // MergeCell define a merged cell data. // It consists of the following structure. // example: []string{"D4:E10", "cell value"} diff --git a/cellmerged_test.go b/cellmerged_test.go index 0c5ac76e44..1da0eb31e1 100644 --- a/cellmerged_test.go +++ b/cellmerged_test.go @@ -7,6 +7,74 @@ import ( "github.com/stretchr/testify/assert" ) +func TestMergeCell(t *testing.T) { + f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + if !assert.NoError(t, err) { + t.FailNow() + } + assert.EqualError(t, f.MergeCell("Sheet1", "A", "B"), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + 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")) + f.SetCellValue("Sheet1", "G11", "set value in merged cell") + f.SetCellInt("Sheet1", "H11", 100) + f.SetCellValue("Sheet1", "I11", float64(0.5)) + f.SetCellHyperLink("Sheet1", "J11", "https://github.com/360EntSecGroup-Skylar/excelize", "External") + f.SetCellFormula("Sheet1", "G12", "SUM(Sheet1!B19,Sheet1!C19)") + f.GetCellValue("Sheet1", "H11") + f.GetCellValue("Sheet2", "A6") // Merged cell ref is single coordinate. + f.GetCellFormula("Sheet1", "G12") + + f.NewSheet("Sheet3") + 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")) + + // Test get merged cells on not exists worksheet. + assert.EqualError(t, f.MergeCell("SheetN", "N10", "O11"), "sheet SheetN is not exist") + + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestMergeCell.xlsx"))) + + f = NewFile() + assert.NoError(t, f.MergeCell("Sheet1", "A2", "B3")) + f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{nil, nil}} + assert.NoError(t, f.MergeCell("Sheet1", "A2", "B3")) + + f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A1"}}} + assert.EqualError(t, f.MergeCell("Sheet1", "A2", "B3"), `invalid area "A1"`) + + f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:A"}}} + assert.EqualError(t, f.MergeCell("Sheet1", "A2", "B3"), `cannot convert cell "A" to coordinates: invalid cell name "A"`) +} + func TestGetMergeCells(t *testing.T) { wants := []struct { value string @@ -68,23 +136,28 @@ func TestUnmergeCell(t *testing.T) { assert.EqualError(t, f.UnmergeCell("Sheet1", "A", "A"), `cannot convert cell "A" to coordinates: invalid cell name "A"`) // unmerge the mergecell that contains A1 - err = f.UnmergeCell(sheet1, "A1", "A1") - assert.NoError(t, err) - + assert.NoError(t, f.UnmergeCell(sheet1, "A1", "A1")) if len(xlsx.MergeCells.Cells) != mergeCellNum-1 { t.FailNow() } - // unmerge area A7:D3(A3:D7) - // this will unmerge all since this area overlaps with all others - err = f.UnmergeCell(sheet1, "D7", "A3") - assert.NoError(t, err) - - if len(xlsx.MergeCells.Cells) != 0 { - t.FailNow() - } + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestUnmergeCell.xlsx"))) + f = NewFile() + assert.NoError(t, f.MergeCell("Sheet1", "A2", "B3")) // Test unmerged area on not exists worksheet. - err = f.UnmergeCell("SheetN", "A1", "A1") - assert.EqualError(t, err, "sheet SheetN is not exist") + assert.EqualError(t, f.UnmergeCell("SheetN", "A1", "A1"), "sheet SheetN is not exist") + + f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = nil + assert.NoError(t, f.UnmergeCell("Sheet1", "H7", "B15")) + + f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{nil, nil}} + assert.NoError(t, f.UnmergeCell("Sheet1", "H15", "B7")) + + f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A1"}}} + assert.EqualError(t, f.UnmergeCell("Sheet1", "A2", "B3"), `invalid area "A1"`) + + f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:A"}}} + assert.EqualError(t, f.UnmergeCell("Sheet1", "A2", "B3"), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + } diff --git a/excelize_test.go b/excelize_test.go index 38a35b0060..95d63fdd2c 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -290,6 +290,12 @@ func TestSetCellHyperLink(t *testing.T) { assert.NoError(t, file.SetCellHyperLink("Sheet1", cell, "https://github.com/360EntSecGroup-Skylar/excelize", "External")) } assert.EqualError(t, file.SetCellHyperLink("Sheet1", "A65531", "https://github.com/360EntSecGroup-Skylar/excelize", "External"), "over maximum limit hyperlinks in a worksheet") + + f = NewFile() + f.workSheetReader("Sheet1") + f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:A"}}} + err = f.SetCellHyperLink("Sheet1", "A1", "https://github.com/360EntSecGroup-Skylar/excelize", "External") + assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`) } func TestGetCellHyperLink(t *testing.T) { @@ -310,6 +316,23 @@ func TestGetCellHyperLink(t *testing.T) { link, target, err = f.GetCellHyperLink("Sheet3", "H3") assert.EqualError(t, err, "sheet Sheet3 is not exist") t.Log(link, target) + + f = NewFile() + f.workSheetReader("Sheet1") + f.Sheet["xl/worksheets/sheet1.xml"].Hyperlinks = &xlsxHyperlinks{ + Hyperlink: []xlsxHyperlink{{Ref: "A1"}}, + } + link, target, err = f.GetCellHyperLink("Sheet1", "A1") + assert.NoError(t, err) + assert.Equal(t, link, true) + assert.Equal(t, target, "") + + f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:A"}}} + link, target, err = f.GetCellHyperLink("Sheet1", "A1") + assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.Equal(t, link, false) + assert.Equal(t, target, "") + } func TestSetCellFormula(t *testing.T) { diff --git a/xmlWorksheet.go b/xmlWorksheet.go index a071e4d5d9..9a478e1dde 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -109,7 +109,7 @@ type xlsxPageSetUp struct { FirstPageNumber int `xml:"firstPageNumber,attr,omitempty"` FitToHeight int `xml:"fitToHeight,attr,omitempty"` FitToWidth int `xml:"fitToWidth,attr,omitempty"` - HorizontalDPI float32 `xml:"horizontalDpi,attr,omitempty"` + HorizontalDPI int `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"` @@ -119,7 +119,7 @@ type xlsxPageSetUp struct { Scale int `xml:"scale,attr,omitempty"` UseFirstPageNumber bool `xml:"useFirstPageNumber,attr,omitempty"` UsePrinterDefaults bool `xml:"usePrinterDefaults,attr,omitempty"` - VerticalDPI float32 `xml:"verticalDpi,attr,omitempty"` + VerticalDPI int `xml:"verticalDpi,attr,omitempty"` } // xlsxPrintOptions directly maps the printOptions element in the namespace From a526e90404913f5d649d29a7aeee29f5ac9ff590 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 16 Dec 2019 08:32:04 +0800 Subject: [PATCH 176/957] Fix #426, handle empty workbook view --- sheet.go | 35 +++++++++++++++++++---------------- stream.go | 3 +-- xmlWorkbook.go | 2 +- xmlWorksheet.go | 8 ++++---- 4 files changed, 25 insertions(+), 23 deletions(-) diff --git a/sheet.go b/sheet.go index 1ce85b4e40..e2619357ce 100644 --- a/sheet.go +++ b/sheet.go @@ -149,11 +149,12 @@ func (f *File) setContentTypes(index int) { // setSheet provides a function to update sheet property by given index. func (f *File) setSheet(index int, name string) { - var xlsx xlsxWorksheet - xlsx.Dimension.Ref = "A1" - xlsx.SheetViews.SheetView = append(xlsx.SheetViews.SheetView, xlsxSheetView{ - WorkbookViewID: 0, - }) + xlsx := xlsxWorksheet{ + Dimension: &xlsxDimension{Ref: "A1"}, + SheetViews: xlsxSheetViews{ + SheetView: []xlsxSheetView{{WorkbookViewID: 0}}, + }, + } path := "xl/worksheets/sheet" + strconv.Itoa(index) + ".xml" f.sheetMap[trimSheetName(name)] = path f.Sheet[path] = &xlsx @@ -222,6 +223,9 @@ func (f *File) SetActiveSheet(index int) { wb := f.workbookReader() for activeTab, sheet := range wb.Sheets.Sheet { if sheet.SheetID == index { + if wb.BookViews == nil { + wb.BookViews = &xlsxBookViews{} + } if len(wb.BookViews.WorkBookView) > 0 { wb.BookViews.WorkBookView[0].ActiveTab = activeTab } else { @@ -253,16 +257,13 @@ func (f *File) SetActiveSheet(index int) { func (f *File) GetActiveSheetIndex() int { wb := f.workbookReader() if wb != nil { - view := wb.BookViews.WorkBookView - sheets := wb.Sheets.Sheet - var activeTab int - if len(view) > 0 { - activeTab = view[0].ActiveTab - if len(sheets) > activeTab && sheets[activeTab].SheetID != 0 { - return sheets[activeTab].SheetID + if wb.BookViews != nil && len(wb.BookViews.WorkBookView) > 0 { + activeTab := wb.BookViews.WorkBookView[0].ActiveTab + if len(wb.Sheets.Sheet) > activeTab && wb.Sheets.Sheet[activeTab].SheetID != 0 { + return wb.Sheets.Sheet[activeTab].SheetID } } - if len(wb.Sheets.Sheet) == 1 { + if len(wb.Sheets.Sheet) >= 1 { return wb.Sheets.Sheet[0].SheetID } } @@ -413,9 +414,11 @@ func (f *File) DeleteSheet(name string) { f.SheetCount-- } } - for idx, bookView := range wb.BookViews.WorkBookView { - if bookView.ActiveTab >= f.SheetCount { - wb.BookViews.WorkBookView[idx].ActiveTab-- + if wb.BookViews != nil { + for idx, bookView := range wb.BookViews.WorkBookView { + if bookView.ActiveTab >= f.SheetCount { + wb.BookViews.WorkBookView[idx].ActiveTab-- + } } } f.SetActiveSheet(len(f.GetSheetMap())) diff --git a/stream.go b/stream.go index 0d91ddd3d1..5e74e8ee5d 100644 --- a/stream.go +++ b/stream.go @@ -191,13 +191,12 @@ func StreamMarshalSheet(ws *xlsxWorksheet, replaceMap map[string][]byte) []byte var marshalResult []byte marshalResult = append(marshalResult, []byte(XMLHeader+``)...) diff --git a/xmlWorkbook.go b/xmlWorkbook.go index 765563bc7a..e9ded6c3e9 100644 --- a/xmlWorkbook.go +++ b/xmlWorkbook.go @@ -33,7 +33,7 @@ type xlsxWorkbook struct { FileVersion *xlsxFileVersion `xml:"fileVersion"` WorkbookPr *xlsxWorkbookPr `xml:"workbookPr"` WorkbookProtection *xlsxWorkbookProtection `xml:"workbookProtection"` - BookViews xlsxBookViews `xml:"bookViews"` + BookViews *xlsxBookViews `xml:"bookViews"` Sheets xlsxSheets `xml:"sheets"` ExternalReferences *xlsxExternalReferences `xml:"externalReferences"` DefinedNames *xlsxDefinedNames `xml:"definedNames"` diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 9a478e1dde..b785eacbf8 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -17,10 +17,10 @@ import "encoding/xml" type xlsxWorksheet struct { XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main worksheet"` SheetPr *xlsxSheetPr `xml:"sheetPr"` - Dimension xlsxDimension `xml:"dimension"` - SheetViews xlsxSheetViews `xml:"sheetViews,omitempty"` + Dimension *xlsxDimension `xml:"dimension"` + SheetViews xlsxSheetViews `xml:"sheetViews"` SheetFormatPr *xlsxSheetFormatPr `xml:"sheetFormatPr"` - Cols *xlsxCols `xml:"cols,omitempty"` + Cols *xlsxCols `xml:"cols"` SheetData xlsxSheetData `xml:"sheetData"` SheetCalcPr *xlsxInnerXML `xml:"sheetCalcPr"` SheetProtection *xlsxSheetProtection `xml:"sheetProtection"` @@ -33,7 +33,7 @@ type xlsxWorksheet struct { MergeCells *xlsxMergeCells `xml:"mergeCells"` PhoneticPr *xlsxPhoneticPr `xml:"phoneticPr"` ConditionalFormatting []*xlsxConditionalFormatting `xml:"conditionalFormatting"` - DataValidations *xlsxDataValidations `xml:"dataValidations,omitempty"` + DataValidations *xlsxDataValidations `xml:"dataValidations"` Hyperlinks *xlsxHyperlinks `xml:"hyperlinks"` PrintOptions *xlsxPrintOptions `xml:"printOptions"` PageMargins *xlsxPageMargins `xml:"pageMargins"` From b1b3c0d15158abc71267da5893de020f047c3872 Mon Sep 17 00:00:00 2001 From: Alex Geer Date: Thu, 19 Dec 2019 19:30:48 +0300 Subject: [PATCH 177/957] =?UTF-8?q?Fix=20#539=20Fixed=20error=20opening=20?= =?UTF-8?q?excel=20file=20created=20in=20encoding=20d=E2=80=A6=20(#540)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fixed issue #539 Fixed error opening excel file created in encoding different from UTF-8, added logging of possible errors when decoding XML if the function does not provide exit with an error * Added test for CharsetReader * Fixed #discussion_r359397878 Discussion: https://github.com/360EntSecGroup-Skylar/excelize/pull/540#discussion_r359397878 * Fixed go fmt * go mod tidy and removed unused imports * The code has been refactored --- .gitignore | 3 +- calcchain.go | 18 +++++++--- chart.go | 16 +++++++-- comment.go | 23 +++++++++---- docProps.go | 66 ++++++++++++++++++++++-------------- docProps_test.go | 4 +-- excelize.go | 81 +++++++++++++++++++++++++++++++------------- file.go | 8 ++--- go.mod | 2 ++ go.sum | 8 +++++ picture.go | 62 ++++++++++++++++++++++------------ rows.go | 12 +++++-- sheet.go | 78 +++++++++++++++++++++++++++++-------------- sparkline.go | 87 +++++++++++++++++++++++++++++++++--------------- styles.go | 26 ++++++++++++--- 15 files changed, 342 insertions(+), 152 deletions(-) diff --git a/.gitignore b/.gitignore index bafda04266..a3fcff22aa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ ~$*.xlsx test/Test*.xlsx *.out -*.test \ No newline at end of file +*.test +.idea diff --git a/calcchain.go b/calcchain.go index b4cadefe0b..413f470ec1 100644 --- a/calcchain.go +++ b/calcchain.go @@ -9,16 +9,26 @@ package excelize -import "encoding/xml" +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 + if f.CalcChain == nil { - var c xlsxCalcChain - _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML("xl/calcChain.xml")), &c) - f.CalcChain = &c + f.CalcChain = new(xlsxCalcChain) + if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML("xl/calcChain.xml")))). + Decode(f.CalcChain); err != nil && err != io.EOF { + log.Printf("xml decode error: %s", err) + } } + return f.CalcChain } diff --git a/chart.go b/chart.go index 7d40405a26..aaa7cd6844 100644 --- a/chart.go +++ b/chart.go @@ -10,9 +10,12 @@ package excelize import ( + "bytes" "encoding/json" "encoding/xml" "errors" + "io" + "log" "strconv" "strings" ) @@ -1735,14 +1738,21 @@ func (f *File) drawPlotAreaTxPr() *cTxPr { // deserialization, two different structures: decodeWsDr and encodeWsDr are // defined. func (f *File) drawingParser(path string) (*xlsxWsDr, int) { + var ( + err error + ok bool + ) + if f.Drawings[path] == nil { content := xlsxWsDr{} content.A = NameSpaceDrawingML content.Xdr = NameSpaceDrawingMLSpreadSheet - _, ok := f.XLSX[path] - if ok { // Append Model + if _, ok = f.XLSX[path]; ok { // Append Model decodeWsDr := decodeWsDr{} - _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML(path)), &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) + } content.R = decodeWsDr.R for _, v := range decodeWsDr.OneCellAnchor { content.OneCellAnchor = append(content.OneCellAnchor, &xdrCellAnchor{ diff --git a/comment.go b/comment.go index 7f3b10dbac..99630c93f7 100644 --- a/comment.go +++ b/comment.go @@ -10,9 +10,12 @@ package excelize import ( + "bytes" "encoding/json" "encoding/xml" "fmt" + "io" + "log" "strconv" "strings" ) @@ -303,12 +306,16 @@ 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 + if f.DecodeVMLDrawing[path] == nil { c, ok := f.XLSX[path] if ok { - d := decodeVmlDrawing{} - _ = xml.Unmarshal(namespaceStrictToTransitional(c), &d) - f.DecodeVMLDrawing[path] = &d + f.DecodeVMLDrawing[path] = new(decodeVmlDrawing) + if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(c))). + Decode(f.DecodeVMLDrawing[path]); err != nil && err != io.EOF { + log.Printf("xml decode error: %s", err) + } } } return f.DecodeVMLDrawing[path] @@ -328,12 +335,16 @@ 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 + if f.Comments[path] == nil { content, ok := f.XLSX[path] if ok { - c := xlsxComments{} - _ = xml.Unmarshal(namespaceStrictToTransitional(content), &c) - f.Comments[path] = &c + f.Comments[path] = new(xlsxComments) + if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(content))). + Decode(f.Comments[path]); err != nil && err != io.EOF { + log.Printf("xml decode error: %s", err) + } } } return f.Comments[path] diff --git a/docProps.go b/docProps.go index 166512f73c..884eb6317f 100644 --- a/docProps.go +++ b/docProps.go @@ -10,7 +10,10 @@ package excelize import ( + "bytes" "encoding/xml" + "fmt" + "io" "reflect" ) @@ -65,13 +68,23 @@ import ( // Version: "1.0.0", // }) // -func (f *File) SetDocProps(docProperties *DocProperties) error { - core := decodeCoreProperties{} - err := xml.Unmarshal(namespaceStrictToTransitional(f.readXML("docProps/core.xml")), &core) - if err != nil { - return err +func (f *File) SetDocProps(docProperties *DocProperties) (err error) { + var ( + core *decodeCoreProperties + newProps *xlsxCoreProperties + fields []string + output []byte + immutable, mutable reflect.Value + field, val string + ) + + core = new(decodeCoreProperties) + if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML("docProps/core.xml")))). + Decode(core); err != nil && err != io.EOF { + err = fmt.Errorf("xml decode error: %s", err) + return } - newProps := xlsxCoreProperties{ + newProps, err = &xlsxCoreProperties{ Dc: NameSpaceDublinCore, Dcterms: NameSpaceDublinCoreTerms, Dcmitype: NameSpaceDublinCoreMetadataIntiative, @@ -88,18 +101,16 @@ func (f *File) SetDocProps(docProperties *DocProperties) error { ContentStatus: core.ContentStatus, 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 + fields = []string{ + "Category", "ContentStatus", "Creator", "Description", "Identifier", "Keywords", + "LastModifiedBy", "Revision", "Subject", "Title", "Language", "Version", } - newProps.Created.Text = core.Created.Text - newProps.Created.Type = core.Created.Type - newProps.Modified.Text = core.Modified.Text - newProps.Modified.Type = core.Modified.Type - - fields := []string{"Category", "ContentStatus", "Creator", "Description", "Identifier", "Keywords", "LastModifiedBy", "Revision", "Subject", "Title", "Language", "Version"} - immutable := reflect.ValueOf(*docProperties) - mutable := reflect.ValueOf(&newProps).Elem() - for _, field := range fields { - val := immutable.FieldByName(field).String() - if val != "" { + immutable, mutable = reflect.ValueOf(*docProperties), reflect.ValueOf(newProps).Elem() + for _, field = range fields { + if val = immutable.FieldByName(field).String(); val != "" { mutable.FieldByName(field).SetString(val) } } @@ -109,19 +120,22 @@ func (f *File) SetDocProps(docProperties *DocProperties) error { if docProperties.Modified != "" { newProps.Modified.Text = docProperties.Modified } - output, err := xml.Marshal(&newProps) + output, err = xml.Marshal(newProps) f.saveFileList("docProps/core.xml", output) - return err + + return } // GetDocProps provides a function to get document core properties. -func (f *File) GetDocProps() (*DocProperties, error) { - core := decodeCoreProperties{} - err := xml.Unmarshal(namespaceStrictToTransitional(f.readXML("docProps/core.xml")), &core) - if err != nil { - return nil, err +func (f *File) GetDocProps() (ret *DocProperties, err error) { + var core = new(decodeCoreProperties) + + if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML("docProps/core.xml")))). + Decode(core); err != nil && err != io.EOF { + err = fmt.Errorf("xml decode error: %s", err) + return } - return &DocProperties{ + ret, err = &DocProperties{ Category: core.Category, ContentStatus: core.ContentStatus, Created: core.Created.Text, @@ -137,4 +151,6 @@ func (f *File) GetDocProps() (*DocProperties, error) { Language: core.Language, Version: core.Version, }, nil + + return } diff --git a/docProps_test.go b/docProps_test.go index 671d998cc9..df3122b19a 100644 --- a/docProps_test.go +++ b/docProps_test.go @@ -39,7 +39,7 @@ func TestSetDocProps(t *testing.T) { })) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetDocProps.xlsx"))) f.XLSX["docProps/core.xml"] = nil - assert.EqualError(t, f.SetDocProps(&DocProperties{}), "EOF") + assert.NoError(t, f.SetDocProps(&DocProperties{})) } func TestGetDocProps(t *testing.T) { @@ -52,5 +52,5 @@ func TestGetDocProps(t *testing.T) { assert.Equal(t, props.Creator, "Microsoft Office User") f.XLSX["docProps/core.xml"] = nil _, err = f.GetDocProps() - assert.EqualError(t, err, "EOF") + assert.NoError(t, err) } diff --git a/excelize.go b/excelize.go index 4d46b94e9b..fe227b9524 100644 --- a/excelize.go +++ b/excelize.go @@ -22,6 +22,8 @@ import ( "path" "strconv" "strings" + + "golang.org/x/net/html/charset" ) // File define a populated XLSX file struct. @@ -43,8 +45,11 @@ type File struct { WorkBook *xlsxWorkbook Relationships map[string]*xlsxRelationships XLSX map[string][]byte + CharsetReader charsetTranscoderFn } +type charsetTranscoderFn func(charset string, input io.Reader) (rdr io.Reader, err error) + // OpenFile take the name of an XLSX file and returns a populated XLSX file // struct for it. func OpenFile(filename string) (*File, error) { @@ -61,6 +66,21 @@ func OpenFile(filename string) (*File, error) { return f, nil } +// object builder +func newFile() *File { + return &File{ + checked: make(map[string]bool), + sheetMap: make(map[string]string), + Comments: make(map[string]*xlsxComments), + Drawings: make(map[string]*xlsxWsDr), + Sheet: make(map[string]*xlsxWorksheet), + DecodeVMLDrawing: make(map[string]*decodeVmlDrawing), + VMLDrawing: make(map[string]*vmlDrawing), + Relationships: make(map[string]*xlsxRelationships), + CharsetReader: charset.NewReaderLabel, + } +} + // OpenReader take an io.Reader and return a populated XLSX file. func OpenReader(r io.Reader) (*File, error) { b, err := ioutil.ReadAll(r) @@ -88,17 +108,8 @@ func OpenReader(r io.Reader) (*File, error) { if err != nil { return nil, err } - f := &File{ - checked: make(map[string]bool), - Comments: make(map[string]*xlsxComments), - Drawings: make(map[string]*xlsxWsDr), - Sheet: make(map[string]*xlsxWorksheet), - SheetCount: sheetCount, - DecodeVMLDrawing: make(map[string]*decodeVmlDrawing), - VMLDrawing: make(map[string]*vmlDrawing), - Relationships: make(map[string]*xlsxRelationships), - XLSX: file, - } + f := newFile() + f.SheetCount, f.XLSX = sheetCount, file f.CalcChain = f.calcChainReader() f.sheetMap = f.getSheetMap() f.Styles = f.stylesReader() @@ -106,6 +117,16 @@ func OpenReader(r io.Reader) (*File, error) { return f, nil } +// CharsetTranscoder Set user defined codepage transcoder function for open XLSX from non UTF-8 encoding +func (f *File) CharsetTranscoder(fn charsetTranscoderFn) *File { f.CharsetReader = fn; return f } + +// Creates new XML decoder with charset reader +func (f *File) xmlNewDecoder(rdr io.Reader) (ret *xml.Decoder) { + ret = xml.NewDecoder(rdr) + ret.CharsetReader = f.CharsetReader + return +} + // setDefaultTimeStyle provides a function to set default numbers format for // time.Time type cell value by given worksheet name, cell coordinates and // number format code. @@ -123,26 +144,38 @@ func (f *File) setDefaultTimeStyle(sheet, axis 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) (*xlsxWorksheet, error) { - name, ok := f.sheetMap[trimSheetName(sheet)] - if !ok { - return nil, fmt.Errorf("sheet %s is not exist", sheet) +func (f *File) workSheetReader(sheet string) (xlsx *xlsxWorksheet, err error) { + var ( + name string + ok bool + ) + + if name, ok = f.sheetMap[trimSheetName(sheet)]; !ok { + err = fmt.Errorf("sheet %s is not exist", sheet) + return } - if f.Sheet[name] == nil { - var xlsx xlsxWorksheet - _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML(name)), &xlsx) + if xlsx = f.Sheet[name]; f.Sheet[name] == nil { + xlsx = new(xlsxWorksheet) + if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(name)))). + Decode(xlsx); err != nil && err != io.EOF { + err = fmt.Errorf("xml decode error: %s", err) + return + } + err = nil if f.checked == nil { f.checked = make(map[string]bool) } - ok := f.checked[name] - if !ok { - checkSheet(&xlsx) - checkRow(&xlsx) + if ok = f.checked[name]; !ok { + checkSheet(xlsx) + if err = checkRow(xlsx); err != nil { + return + } f.checked[name] = true } - f.Sheet[name] = &xlsx + f.Sheet[name] = xlsx } - return f.Sheet[name], nil + + return } // checkSheet provides a function to fill each row element and make that is diff --git a/file.go b/file.go index 2e0d27bef4..d8f10facfa 100644 --- a/file.go +++ b/file.go @@ -33,12 +33,8 @@ func NewFile() *File { file["xl/styles.xml"] = []byte(XMLHeader + templateStyles) file["xl/workbook.xml"] = []byte(XMLHeader + templateWorkbook) file["[Content_Types].xml"] = []byte(XMLHeader + templateContentTypes) - f := &File{ - sheetMap: make(map[string]string), - Sheet: make(map[string]*xlsxWorksheet), - SheetCount: 1, - XLSX: file, - } + f := newFile() + f.SheetCount, f.XLSX = 1, file f.CalcChain = f.calcChainReader() f.Comments = make(map[string]*xlsxComments) f.ContentTypes = f.contentTypesReader() diff --git a/go.mod b/go.mod index 892f3066da..420c64eb64 100644 --- a/go.mod +++ b/go.mod @@ -7,4 +7,6 @@ require ( github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 github.com/stretchr/testify v1.3.0 golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a + golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 + golang.org/x/text v0.3.2 // indirect ) diff --git a/go.sum b/go.sum index 2d29d33a51..54492ac2ed 100644 --- a/go.sum +++ b/go.sum @@ -9,6 +9,14 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a h1:gHevYm0pO4QUbwy8Dmdr01R5r1BuKtfYqRqF0h/Cbh0= golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/picture.go b/picture.go index ff40863369..09c19559de 100644 --- a/picture.go +++ b/picture.go @@ -14,7 +14,9 @@ import ( "encoding/json" "encoding/xml" "errors" + "fmt" "image" + "io" "io/ioutil" "os" "path" @@ -471,39 +473,55 @@ func (f *File) GetPicture(sheet, cell string) (string, []byte, error) { // getPicture provides a function to get picture base name and raw content // embed in XLSX by given coordinates and drawing relationships. -func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) (string, []byte, error) { - wsDr, _ := f.drawingParser(drawingXML) - for _, anchor := range wsDr.TwoCellAnchor { +func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) (ret string, buf []byte, err error) { + var ( + wsDr *xlsxWsDr + ok bool + anchor *xdrCellAnchor + deWsDr *decodeWsDr + xxRelationship *xlsxRelationship + deTwoCellAnchor *decodeTwoCellAnchor + ) + + wsDr, _ = f.drawingParser(drawingXML) + for _, anchor = range wsDr.TwoCellAnchor { if anchor.From != nil && anchor.Pic != nil { if anchor.From.Col == col && anchor.From.Row == row { - xlsxRelationship := f.getDrawingRelationships(drawingRelationships, + xxRelationship = f.getDrawingRelationships(drawingRelationships, anchor.Pic.BlipFill.Blip.Embed) - _, ok := supportImageTypes[filepath.Ext(xlsxRelationship.Target)] - if ok { - return filepath.Base(xlsxRelationship.Target), - []byte(f.XLSX[strings.Replace(xlsxRelationship.Target, - "..", "xl", -1)]), nil + if _, ok = supportImageTypes[filepath.Ext(xxRelationship.Target)]; ok { + ret, buf = filepath.Base(xxRelationship.Target), []byte(f.XLSX[strings.Replace(xxRelationship.Target, "..", "xl", -1)]) + return } } } } - - decodeWsDr := decodeWsDr{} - _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML(drawingXML)), &decodeWsDr) - for _, anchor := range decodeWsDr.TwoCellAnchor { - decodeTwoCellAnchor := decodeTwoCellAnchor{} - _ = xml.Unmarshal([]byte(""+anchor.Content+""), &decodeTwoCellAnchor) - if decodeTwoCellAnchor.From != nil && decodeTwoCellAnchor.Pic != nil { - if decodeTwoCellAnchor.From.Col == col && decodeTwoCellAnchor.From.Row == row { - xlsxRelationship := f.getDrawingRelationships(drawingRelationships, decodeTwoCellAnchor.Pic.BlipFill.Blip.Embed) - _, ok := supportImageTypes[filepath.Ext(xlsxRelationship.Target)] - if ok { - return filepath.Base(xlsxRelationship.Target), []byte(f.XLSX[strings.Replace(xlsxRelationship.Target, "..", "xl", -1)]), nil + 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) + return + } + err = nil + for _, anchor := range deWsDr.TwoCellAnchor { + deTwoCellAnchor = new(decodeTwoCellAnchor) + if err = f.xmlNewDecoder(bytes.NewReader([]byte("" + anchor.Content + ""))). + Decode(deTwoCellAnchor); err != nil && err != io.EOF { + err = fmt.Errorf("xml decode error: %s", err) + return + } + if err = nil; deTwoCellAnchor.From != nil && deTwoCellAnchor.Pic != nil { + if deTwoCellAnchor.From.Col == col && deTwoCellAnchor.From.Row == row { + xxRelationship = f.getDrawingRelationships(drawingRelationships, deTwoCellAnchor.Pic.BlipFill.Blip.Embed) + if _, ok = supportImageTypes[filepath.Ext(xxRelationship.Target)]; ok { + ret, buf = filepath.Base(xxRelationship.Target), []byte(f.XLSX[strings.Replace(xxRelationship.Target, "..", "xl", -1)]) + return } } } } - return "", nil, nil + + return } // getDrawingRelationships provides a function to get drawing relationships diff --git a/rows.go b/rows.go index 379644110c..e12e349a02 100644 --- a/rows.go +++ b/rows.go @@ -10,9 +10,11 @@ package excelize import ( - "encoding/xml" + "bytes" "errors" "fmt" + "io" + "log" "math" "strconv" ) @@ -187,15 +189,21 @@ 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 { + var err error + if f.SharedStrings == nil { var sharedStrings xlsxSST ss := f.readXML("xl/sharedStrings.xml") if len(ss) == 0 { ss = f.readXML("xl/SharedStrings.xml") } - _ = xml.Unmarshal(namespaceStrictToTransitional(ss), &sharedStrings) + if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(ss))). + Decode(&sharedStrings); err != nil && err != io.EOF { + log.Printf("xml decode error: %s", err) + } f.SharedStrings = &sharedStrings } + return f.SharedStrings } diff --git a/sheet.go b/sheet.go index 951baf9239..42fd6b3394 100644 --- a/sheet.go +++ b/sheet.go @@ -15,7 +15,9 @@ import ( "encoding/xml" "errors" "fmt" + "io" "io/ioutil" + "log" "os" "path" "reflect" @@ -61,11 +63,16 @@ func (f *File) NewSheet(name 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 + if f.ContentTypes == nil { - var content xlsxTypes - _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML("[Content_Types].xml")), &content) - f.ContentTypes = &content + f.ContentTypes = new(xlsxTypes) + if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML("[Content_Types].xml")))). + Decode(f.ContentTypes); err != nil && err != io.EOF { + log.Printf("xml decode error: %s", err) + } } + return f.ContentTypes } @@ -81,11 +88,16 @@ func (f *File) contentTypesWriter() { // workbookReader provides a function to get the pointer to the xl/workbook.xml // structure after deserialization. func (f *File) workbookReader() *xlsxWorkbook { + var err error + if f.WorkBook == nil { - var content xlsxWorkbook - _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML("xl/workbook.xml")), &content) - f.WorkBook = &content + f.WorkBook = new(xlsxWorkbook) + if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML("xl/workbook.xml")))). + Decode(f.WorkBook); err != nil && err != io.EOF { + log.Printf("xml decode error: %s", err) + } } + return f.WorkBook } @@ -679,42 +691,51 @@ func (f *File) GetSheetVisible(name string) bool { // // result, err := f.SearchSheet("Sheet1", "[0-9]", true) // -func (f *File) SearchSheet(sheet, value string, reg ...bool) ([]string, error) { +func (f *File) SearchSheet(sheet, value string, reg ...bool) (result []string, err error) { var ( - regSearch bool - result []string + xlsx *xlsxWorksheet + regSearch, r, ok bool + name string + output []byte ) - for _, r := range reg { + + for _, r = range reg { regSearch = r } - xlsx, err := f.workSheetReader(sheet) - if err != nil { - return result, err + if xlsx, err = f.workSheetReader(sheet); err != nil { + return } - name, ok := f.sheetMap[trimSheetName(sheet)] - if !ok { - return result, nil + if name, ok = f.sheetMap[trimSheetName(sheet)]; !ok { + return } if xlsx != nil { - output, _ := xml.Marshal(f.Sheet[name]) + if output, err = xml.Marshal(f.Sheet[name]); err != nil { + return + } f.saveFileList(name, replaceWorkSheetsRelationshipsNameSpaceBytes(output)) } + return f.searchSheet(name, value, regSearch) } // searchSheet provides a function to get coordinates by given worksheet name, // cell value, and regular expression. -func (f *File) searchSheet(name, value string, regSearch bool) ([]string, error) { +func (f *File) searchSheet(name, value string, regSearch bool) (result []string, err error) { var ( + d *xlsxSST + decoder *xml.Decoder inElement string - result []string r xlsxRow + token xml.Token ) - d := f.sharedStringsReader() - decoder := xml.NewDecoder(bytes.NewReader(f.readXML(name))) + + d = f.sharedStringsReader() + decoder = f.xmlNewDecoder(bytes.NewReader(f.readXML(name))) for { - token, _ := decoder.Token() - if token == nil { + if token, err = decoder.Token(); err != nil || token == nil { + if err == io.EOF { + err = nil + } break } switch startElement := token.(type) { @@ -750,7 +771,8 @@ func (f *File) searchSheet(name, value string, regSearch bool) ([]string, error) default: } } - return result, nil + + return } // SetHeaderFooter provides a function to set headers and footers by given @@ -1360,14 +1382,20 @@ func (f *File) UngroupSheets() 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 + if f.Relationships[path] == nil { _, ok := f.XLSX[path] if ok { c := xlsxRelationships{} - _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML(path)), &c) + if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(path)))). + Decode(&c); err != nil && err != io.EOF { + log.Printf("xml decode error: %s", err) + } f.Relationships[path] = &c } } + return f.Relationships[path] } diff --git a/sparkline.go b/sparkline.go index b09dbf4051..9ad5087775 100644 --- a/sparkline.go +++ b/sparkline.go @@ -10,8 +10,10 @@ package excelize import ( + "bytes" "encoding/xml" "errors" + "io" "strings" ) @@ -386,23 +388,40 @@ func (f *File) addSparklineGroupByStyle(ID int) *xlsxX14SparklineGroup { // ColorAxis | An RGB Color is specified as RRGGBB // Axis | Show sparkline axis // -func (f *File) AddSparkline(sheet string, opt *SparklineOption) error { +func (f *File) AddSparkline(sheet string, opt *SparklineOption) (err error) { + var ( + ws *xlsxWorksheet + sparkType string + sparkTypes map[string]string + specifiedSparkTypes string + ok bool + group *xlsxX14SparklineGroup + groups *xlsxX14SparklineGroups + decodeExtLst *decodeWorksheetExt + idx int + ext *xlsxWorksheetExt + decodeSparklineGroups *decodeX14SparklineGroups + sparklineGroupBytes []byte + sparklineGroupsBytes []byte + extLst string + extLstBytes, extBytes []byte + ) + // parameter validation - ws, err := f.parseFormatAddSparklineSet(sheet, opt) - if err != nil { - return err + if ws, err = f.parseFormatAddSparklineSet(sheet, opt); err != nil { + return } // Handle the sparkline type - sparkType := "line" - sparkTypes := map[string]string{"line": "line", "column": "column", "win_loss": "stacked"} + sparkType = "line" + sparkTypes = map[string]string{"line": "line", "column": "column", "win_loss": "stacked"} if opt.Type != "" { - specifiedSparkTypes, ok := sparkTypes[opt.Type] - if !ok { - return errors.New("parameter 'Type' must be 'line', 'column' or 'win_loss'") + if specifiedSparkTypes, ok = sparkTypes[opt.Type]; !ok { + err = errors.New("parameter 'Type' must be 'line', 'column' or 'win_loss'") + return } sparkType = specifiedSparkTypes } - group := f.addSparklineGroupByStyle(opt.Style) + group = f.addSparklineGroupByStyle(opt.Style) group.Type = sparkType group.ColorAxis = &xlsxColor{RGB: "FF000000"} group.DisplayEmptyCellsAs = "gap" @@ -423,43 +442,57 @@ func (f *File) AddSparkline(sheet string, opt *SparklineOption) error { } f.addSparkline(opt, group) if ws.ExtLst.Ext != "" { // append mode ext - decodeExtLst := decodeWorksheetExt{} - err = xml.Unmarshal([]byte(""+ws.ExtLst.Ext+""), &decodeExtLst) - if err != nil { - return err + decodeExtLst = new(decodeWorksheetExt) + if err = f.xmlNewDecoder(bytes.NewReader([]byte("" + ws.ExtLst.Ext + ""))). + Decode(decodeExtLst); err != nil && err != io.EOF { + return } - for idx, ext := range decodeExtLst.Ext { + for idx, ext = range decodeExtLst.Ext { if ext.URI == ExtURISparklineGroups { - decodeSparklineGroups := decodeX14SparklineGroups{} - _ = xml.Unmarshal([]byte(ext.Content), &decodeSparklineGroups) - sparklineGroupBytes, _ := xml.Marshal(group) - groups := xlsxX14SparklineGroups{ + decodeSparklineGroups = new(decodeX14SparklineGroups) + if err = f.xmlNewDecoder(bytes.NewReader([]byte(ext.Content))). + Decode(decodeSparklineGroups); err != nil && err != io.EOF { + return + } + if sparklineGroupBytes, err = xml.Marshal(group); err != nil { + return + } + groups = &xlsxX14SparklineGroups{ XMLNSXM: NameSpaceSpreadSheetExcel2006Main, Content: decodeSparklineGroups.Content + string(sparklineGroupBytes), } - sparklineGroupsBytes, _ := xml.Marshal(groups) + if sparklineGroupsBytes, err = xml.Marshal(groups); err != nil { + return + } decodeExtLst.Ext[idx].Content = string(sparklineGroupsBytes) } } - extLstBytes, _ := xml.Marshal(decodeExtLst) - extLst := string(extLstBytes) + if extLstBytes, err = xml.Marshal(decodeExtLst); err != nil { + return + } + extLst = string(extLstBytes) ws.ExtLst = &xlsxExtLst{ Ext: strings.TrimSuffix(strings.TrimPrefix(extLst, ""), ""), } } else { - groups := xlsxX14SparklineGroups{ + groups = &xlsxX14SparklineGroups{ XMLNSXM: NameSpaceSpreadSheetExcel2006Main, SparklineGroups: []*xlsxX14SparklineGroup{group}, } - sparklineGroupsBytes, _ := xml.Marshal(groups) - extLst := xlsxWorksheetExt{ + if sparklineGroupsBytes, err = xml.Marshal(groups); err != nil { + return + } + ext = &xlsxWorksheetExt{ URI: ExtURISparklineGroups, Content: string(sparklineGroupsBytes), } - extBytes, _ := xml.Marshal(extLst) + if extBytes, err = xml.Marshal(ext); err != nil { + return + } ws.ExtLst.Ext = string(extBytes) } - return nil + + return } // parseFormatAddSparklineSet provides a function to validate sparkline diff --git a/styles.go b/styles.go index 3244be240a..fa0507ebc9 100644 --- a/styles.go +++ b/styles.go @@ -10,9 +10,12 @@ package excelize import ( + "bytes" "encoding/json" "encoding/xml" "fmt" + "io" + "log" "math" "strconv" "strings" @@ -997,11 +1000,16 @@ func is12HourTime(format string) bool { // stylesReader provides a function to get the pointer to the structure after // deserialization of xl/styles.xml. func (f *File) stylesReader() *xlsxStyleSheet { + var err error + if f.Styles == nil { - var styleSheet xlsxStyleSheet - _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML("xl/styles.xml")), &styleSheet) - f.Styles = &styleSheet + f.Styles = new(xlsxStyleSheet) + if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML("xl/styles.xml")))). + Decode(f.Styles); err != nil && err != io.EOF { + log.Printf("xml decode error: %s", err) + } } + return f.Styles } @@ -2803,8 +2811,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 { - var theme xlsxTheme - _ = xml.Unmarshal(namespaceStrictToTransitional(f.readXML("xl/theme/theme1.xml")), &theme) + var ( + err error + theme xlsxTheme + ) + + if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML("xl/theme/theme1.xml")))). + Decode(&theme); err != nil && err != io.EOF { + log.Printf("xml decoder error: %s", err) + } + return &theme } From 7358dca436f6ca5948a3f2865b14e828863d86a9 Mon Sep 17 00:00:00 2001 From: match-meng <54879059+match-meng@users.noreply.github.com> Date: Fri, 20 Dec 2019 22:22:56 +0800 Subject: [PATCH 178/957] Update comments for the xmlNewDecoder (#542) --- excelize.go | 7 ++++--- rows.go | 5 ++++- sheet.go | 2 +- xmlWorksheet.go | 2 +- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/excelize.go b/excelize.go index 8e386a8bb9..a2e20ffe82 100644 --- a/excelize.go +++ b/excelize.go @@ -66,7 +66,7 @@ func OpenFile(filename string) (*File, error) { return f, nil } -// object builder +// newFile is object builder func newFile() *File { return &File{ checked: make(map[string]bool), @@ -117,10 +117,11 @@ func OpenReader(r io.Reader) (*File, error) { return f, nil } -// CharsetTranscoder Set user defined codepage transcoder function for open XLSX from non UTF-8 encoding +// CharsetTranscoder Set user defined codepage transcoder function for open +// XLSX from non UTF-8 encoding. func (f *File) CharsetTranscoder(fn charsetTranscoderFn) *File { f.CharsetReader = fn; return f } -// Creates new XML decoder with charset reader +// Creates new XML decoder with charset reader. func (f *File) xmlNewDecoder(rdr io.Reader) (ret *xml.Decoder) { ret = xml.NewDecoder(rdr) ret.CharsetReader = f.CharsetReader diff --git a/rows.go b/rows.go index fc7b55ab20..687828c79c 100644 --- a/rows.go +++ b/rows.go @@ -283,7 +283,10 @@ func (xlsx *xlsxC) getValueFrom(f *File, d *xlsxSST) (string, error) { case "s": xlsxSI := 0 xlsxSI, _ = strconv.Atoi(xlsx.V) - return f.formattedValue(xlsx.S, d.SI[xlsxSI].String()), nil + if len(d.SI) > xlsxSI { + return f.formattedValue(xlsx.S, d.SI[xlsxSI].String()), nil + } + return f.formattedValue(xlsx.S, xlsx.V), nil case "str": return f.formattedValue(xlsx.S, xlsx.V), nil case "inlineStr": diff --git a/sheet.go b/sheet.go index 3b22a0ea5a..6ef7c6ebe1 100644 --- a/sheet.go +++ b/sheet.go @@ -163,7 +163,7 @@ func (f *File) setContentTypes(index int) { func (f *File) setSheet(index int, name string) { xlsx := xlsxWorksheet{ Dimension: &xlsxDimension{Ref: "A1"}, - SheetViews: xlsxSheetViews{ + SheetViews: &xlsxSheetViews{ SheetView: []xlsxSheetView{{WorkbookViewID: 0}}, }, } diff --git a/xmlWorksheet.go b/xmlWorksheet.go index b785eacbf8..71ff4cc57a 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -18,7 +18,7 @@ type xlsxWorksheet struct { XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main worksheet"` SheetPr *xlsxSheetPr `xml:"sheetPr"` Dimension *xlsxDimension `xml:"dimension"` - SheetViews xlsxSheetViews `xml:"sheetViews"` + SheetViews *xlsxSheetViews `xml:"sheetViews"` SheetFormatPr *xlsxSheetFormatPr `xml:"sheetFormatPr"` Cols *xlsxCols `xml:"cols"` SheetData xlsxSheetData `xml:"sheetData"` From ae2865d9237cfd27d7bc4fbef3870b3361597be8 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 22 Dec 2019 00:02:09 +0800 Subject: [PATCH 179/957] Improve code coverage unit tests --- calcchain_test.go | 19 ++++++ cell_test.go | 9 ++- comment_test.go | 56 +++++++++++++++++ datavalidation_test.go | 9 +++ docProps_test.go | 13 ++++ excelize_test.go | 133 ++++++++++++++++++++++++++++++++--------- picture.go | 39 ++++++++---- picture_test.go | 42 +++++++------ rows_test.go | 78 +++++++++++++++++------- sheet.go | 28 ++++++--- sheet_test.go | 20 +++++++ sparkline.go | 103 ++++++++++++++++--------------- sparkline_test.go | 9 +++ stream_test.go | 18 +++++- xmlWorkbook.go | 2 +- xmlWorksheet.go | 2 +- 16 files changed, 438 insertions(+), 142 deletions(-) create mode 100644 calcchain_test.go create mode 100644 comment_test.go diff --git a/calcchain_test.go b/calcchain_test.go new file mode 100644 index 0000000000..842dde16df --- /dev/null +++ b/calcchain_test.go @@ -0,0 +1,19 @@ +package excelize + +import "testing" + +func TestCalcChainReader(t *testing.T) { + f := NewFile() + f.CalcChain = nil + f.XLSX["xl/calcChain.xml"] = MacintoshCyrillicCharset + f.calcChainReader() +} + +func TestDeleteCalcChain(t *testing.T) { + f := NewFile() + f.CalcChain = &xlsxCalcChain{C: []xlsxCalcChainC{}} + f.ContentTypes.Overrides = append(f.ContentTypes.Overrides, xlsxOverride{ + PartName: "/xl/calcChain.xml", + }) + f.deleteCalcChain(1, "A1") +} diff --git a/cell_test.go b/cell_test.go index b030622326..7d3339f969 100644 --- a/cell_test.go +++ b/cell_test.go @@ -95,8 +95,15 @@ func TestSetCellBool(t *testing.T) { } func TestGetCellFormula(t *testing.T) { + // Test get cell formula on not exist worksheet. f := NewFile() - f.GetCellFormula("Sheet", "A1") + _, err := f.GetCellFormula("SheetN", "A1") + assert.EqualError(t, err, "sheet SheetN is not exist") + + // Test get cell formula on no formula cell. + f.SetCellValue("Sheet1", "A1", true) + _, err = f.GetCellFormula("Sheet1", "A1") + assert.NoError(t, err) } func ExampleFile_SetCellFloat() { diff --git a/comment_test.go b/comment_test.go new file mode 100644 index 0000000000..dd0795131b --- /dev/null +++ b/comment_test.go @@ -0,0 +1,56 @@ +// Copyright 2016 - 2019 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. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.10 or later. + +package excelize + +import ( + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAddComments(t *testing.T) { + f, err := prepareTestBook1() + if !assert.NoError(t, err) { + 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."}`)) + + // 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") + + if assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddComments.xlsx"))) { + assert.Len(t, f.GetComments(), 2) + } +} + +func TestDecodeVMLDrawingReader(t *testing.T) { + f := NewFile() + path := "xl/drawings/vmlDrawing1.xml" + f.XLSX[path] = MacintoshCyrillicCharset + f.decodeVMLDrawingReader(path) +} + +func TestCommentsReader(t *testing.T) { + f := NewFile() + path := "xl/comments1.xml" + f.XLSX[path] = MacintoshCyrillicCharset + f.commentsReader(path) +} + +func TestCountComments(t *testing.T) { + f := NewFile() + f.Comments["xl/comments1.xml"] = nil + assert.Equal(t, f.countComments(), 1) +} diff --git a/datavalidation_test.go b/datavalidation_test.go index 211830d359..763bad1341 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -11,6 +11,7 @@ package excelize import ( "path/filepath" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -85,4 +86,12 @@ func TestDataValidationError(t *testing.T) { if !assert.NoError(t, f.SaveAs(resultFile)) { t.FailNow() } + + // Test width invalid data validation formula. + dvRange.Formula1 = strings.Repeat("s", dataValidationFormulaStrLen+22) + assert.EqualError(t, dvRange.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorGreaterThan), "data validation must be 0-255 characters") + + // Test add data validation on no exists worksheet. + f = NewFile() + assert.EqualError(t, f.AddDataValidation("SheetN", nil), "sheet SheetN is not exist") } diff --git a/docProps_test.go b/docProps_test.go index df3122b19a..30c31494b1 100644 --- a/docProps_test.go +++ b/docProps_test.go @@ -16,6 +16,8 @@ import ( "github.com/stretchr/testify/assert" ) +var MacintoshCyrillicCharset = []byte{0x8F, 0xF0, 0xE8, 0xE2, 0xE5, 0xF2, 0x20, 0xEC, 0xE8, 0xF0} + func TestSetDocProps(t *testing.T) { f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if !assert.NoError(t, err) { @@ -40,6 +42,11 @@ func TestSetDocProps(t *testing.T) { assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetDocProps.xlsx"))) f.XLSX["docProps/core.xml"] = nil assert.NoError(t, f.SetDocProps(&DocProperties{})) + + // Test unsupport charset + f = NewFile() + f.XLSX["docProps/core.xml"] = MacintoshCyrillicCharset + assert.EqualError(t, f.SetDocProps(&DocProperties{}), "xml decode error: XML syntax error on line 1: invalid UTF-8") } func TestGetDocProps(t *testing.T) { @@ -53,4 +60,10 @@ func TestGetDocProps(t *testing.T) { f.XLSX["docProps/core.xml"] = nil _, err = f.GetDocProps() assert.NoError(t, err) + + // Test unsupport charset + f = NewFile() + f.XLSX["docProps/core.xml"] = MacintoshCyrillicCharset + _, err = f.GetDocProps() + assert.EqualError(t, err, "xml decode error: XML syntax error on line 1: invalid UTF-8") } diff --git a/excelize_test.go b/excelize_test.go index 95d63fdd2c..6929a4fe33 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -2,6 +2,8 @@ package excelize import ( "bytes" + "compress/gzip" + "encoding/xml" "fmt" "image/color" _ "image/gif" @@ -184,6 +186,11 @@ func TestSaveAsWrongPath(t *testing.T) { } } +func TestCharsetTranscoder(t *testing.T) { + f := NewFile() + f.CharsetTranscoder(*new(charsetTranscoderFn)) +} + func TestOpenReader(t *testing.T) { _, err := OpenReader(strings.NewReader("")) assert.EqualError(t, err, "zip: not a valid zip file") @@ -195,6 +202,18 @@ func TestOpenReader(t *testing.T) { 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, })) assert.EqualError(t, err, "not support encrypted file currently") + + // Test unexpected EOF. + var b bytes.Buffer + w := gzip.NewWriter(&b) + defer w.Close() + w.Flush() + + r, _ := gzip.NewReader(&b) + defer r.Close() + + _, err = OpenReader(r) + assert.EqualError(t, err, "unexpected EOF") } func TestBrokenFile(t *testing.T) { @@ -924,24 +943,6 @@ func TestAddShape(t *testing.T) { assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddShape2.xlsx"))) } -func TestAddComments(t *testing.T) { - f, err := prepareTestBook1() - if !assert.NoError(t, err) { - 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."}`)) - - // 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") - - if assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddComments.xlsx"))) { - assert.Len(t, f.GetComments(), 2) - } -} - func TestGetSheetComments(t *testing.T) { f := NewFile() assert.Equal(t, "", f.getSheetComments(0)) @@ -1005,18 +1006,37 @@ func TestAutoFilterError(t *testing.T) { } } -func TestSetPane(t *testing.T) { +func TestSetActiveSheet(t *testing.T) { + f := NewFile() + f.WorkBook.BookViews = nil + f.SetActiveSheet(1) + f.WorkBook.BookViews = &xlsxBookViews{WorkBookView: []xlsxWorkBookView{}} + f.Sheet["xl/worksheets/sheet1.xml"].SheetViews = &xlsxSheetViews{SheetView: []xlsxSheetView{}} + f.SetActiveSheet(1) +} + +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") +} + +func TestGetActiveSheetIndex(t *testing.T) { + f := NewFile() + f.WorkBook.BookViews = nil + assert.Equal(t, 1, f.GetActiveSheetIndex()) +} + +func TestRelsWriter(t *testing.T) { + f := NewFile() + f.Relationships["xl/worksheets/sheet/rels/sheet1.xml.rel"] = &xlsxRelationships{} + f.relsWriter() +} + +func TestGetSheetView(t *testing.T) { f := NewFile() - f.SetPanes("Sheet1", `{"freeze":false,"split":false}`) - f.NewSheet("Panes 2") - 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") - 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") - 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"}]}`) - f.SetPanes("Panes 4", "") - - assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetPane.xlsx"))) + _, err := f.getSheetView("SheetN", 0) + assert.EqualError(t, err, "sheet SheetN is not exist") } func TestConditionalFormat(t *testing.T) { @@ -1207,6 +1227,61 @@ func TestAddVBAProject(t *testing.T) { assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddVBAProject.xlsm"))) } +func TestContentTypesReader(t *testing.T) { + // Test unsupport charset. + f := NewFile() + f.ContentTypes = nil + f.XLSX["[Content_Types].xml"] = MacintoshCyrillicCharset + f.contentTypesReader() +} + +func TestWorkbookReader(t *testing.T) { + // Test unsupport charset. + f := NewFile() + f.WorkBook = nil + f.XLSX["xl/workbook.xml"] = MacintoshCyrillicCharset + f.workbookReader() +} + +func TestWorkSheetReader(t *testing.T) { + // Test unsupport charset. + f := NewFile() + delete(f.Sheet, "xl/worksheets/sheet1.xml") + f.XLSX["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") + + // Test on no checked worksheet. + f = NewFile() + delete(f.Sheet, "xl/worksheets/sheet1.xml") + f.XLSX["xl/worksheets/sheet1.xml"] = []byte(``) + f.checked = nil + _, err = f.workSheetReader("Sheet1") + assert.NoError(t, err) +} + +func TestRelsReader(t *testing.T) { + // Test unsupport charset. + f := NewFile() + rels := "xl/_rels/workbook.xml.rels" + f.Relationships[rels] = nil + f.XLSX[rels] = MacintoshCyrillicCharset + f.relsReader(rels) +} + +func TestDeleteSheetFromWorkbookRels(t *testing.T) { + f := NewFile() + rels := "xl/_rels/workbook.xml.rels" + f.Relationships[rels] = nil + assert.Equal(t, f.deleteSheetFromWorkbookRels("rID"), "") +} + +func TestAttrValToInt(t *testing.T) { + _, err := attrValToInt("r", []xml.Attr{ + {Name: xml.Name{Local: "r"}, Value: "s"}}) + assert.EqualError(t, err, `strconv.Atoi: parsing "s": invalid syntax`) +} + func prepareTestBook1() (*File, error) { f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if err != nil { diff --git a/picture.go b/picture.go index 242035049a..01df8492fe 100644 --- a/picture.go +++ b/picture.go @@ -477,24 +477,14 @@ func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) var ( wsDr *xlsxWsDr ok bool - anchor *xdrCellAnchor deWsDr *decodeWsDr drawRel *xlsxRelationship deTwoCellAnchor *decodeTwoCellAnchor ) wsDr, _ = f.drawingParser(drawingXML) - for _, anchor = range wsDr.TwoCellAnchor { - if anchor.From != nil && anchor.Pic != nil { - if anchor.From.Col == col && anchor.From.Row == row { - drawRel = f.getDrawingRelationships(drawingRelationships, - anchor.Pic.BlipFill.Blip.Embed) - if _, ok = supportImageTypes[filepath.Ext(drawRel.Target)]; ok { - ret, buf = filepath.Base(drawRel.Target), []byte(f.XLSX[strings.Replace(drawRel.Target, "..", "xl", -1)]) - 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)))). @@ -514,13 +504,36 @@ 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 = supportImageTypes[filepath.Ext(drawRel.Target)]; ok { - ret, buf = filepath.Base(drawRel.Target), []byte(f.XLSX[strings.Replace(drawRel.Target, "..", "xl", -1)]) + ret, buf = filepath.Base(drawRel.Target), f.XLSX[strings.Replace(drawRel.Target, "..", "xl", -1)] return } } } } + return +} +// getPictureFromWsDr 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) { + var ( + ok bool + anchor *xdrCellAnchor + drawRel *xlsxRelationship + ) + for _, anchor = range wsDr.TwoCellAnchor { + if anchor.From != nil && anchor.Pic != nil { + if anchor.From.Col == col && anchor.From.Row == row { + drawRel = f.getDrawingRelationships(drawingRelationships, + anchor.Pic.BlipFill.Blip.Embed) + if _, ok = supportImageTypes[filepath.Ext(drawRel.Target)]; ok { + ret, buf = filepath.Base(drawRel.Target), f.XLSX[strings.Replace(drawRel.Target, "..", "xl", -1)] + return + } + } + } + } return } diff --git a/picture_test.go b/picture_test.go index 9a2edda9ae..6af39044a6 100644 --- a/picture_test.go +++ b/picture_test.go @@ -92,12 +92,12 @@ func TestAddPictureErrors(t *testing.T) { } func TestGetPicture(t *testing.T) { - xlsx, err := prepareTestBook1() + f, err := prepareTestBook1() if !assert.NoError(t, err) { t.FailNow() } - file, raw, err := xlsx.GetPicture("Sheet1", "F21") + 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)) { @@ -106,37 +106,37 @@ func TestGetPicture(t *testing.T) { } // Try to get picture from a worksheet with illegal cell coordinates. - _, _, err = xlsx.GetPicture("Sheet1", "A") + _, _, err = f.GetPicture("Sheet1", "A") assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`) // Try to get picture from a worksheet that doesn't contain any images. - file, raw, err = xlsx.GetPicture("Sheet3", "I9") + file, raw, err = f.GetPicture("Sheet3", "I9") assert.EqualError(t, err, "sheet Sheet3 is not exist") assert.Empty(t, file) assert.Empty(t, raw) // Try to get picture from a cell that doesn't contain an image. - file, raw, err = xlsx.GetPicture("Sheet2", "A2") + file, raw, err = f.GetPicture("Sheet2", "A2") assert.NoError(t, err) assert.Empty(t, file) assert.Empty(t, raw) - xlsx.getDrawingRelationships("xl/worksheets/_rels/sheet1.xml.rels", "rId8") - xlsx.getDrawingRelationships("", "") - xlsx.getSheetRelationshipsTargetByID("", "") - xlsx.deleteSheetRelationships("", "") + f.getDrawingRelationships("xl/worksheets/_rels/sheet1.xml.rels", "rId8") + f.getDrawingRelationships("", "") + f.getSheetRelationshipsTargetByID("", "") + f.deleteSheetRelationships("", "") // Try to get picture from a local storage file. - if !assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestGetPicture.xlsx"))) { + if !assert.NoError(t, f.SaveAs(filepath.Join("test", "TestGetPicture.xlsx"))) { t.FailNow() } - xlsx, err = OpenFile(filepath.Join("test", "TestGetPicture.xlsx")) + f, err = OpenFile(filepath.Join("test", "TestGetPicture.xlsx")) if !assert.NoError(t, err) { t.FailNow() } - file, raw, err = xlsx.GetPicture("Sheet1", "F21") + 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)) { @@ -145,7 +145,14 @@ func TestGetPicture(t *testing.T) { } // Try to get picture from a local storage file that doesn't contain an image. - file, raw, err = xlsx.GetPicture("Sheet1", "F22") + file, raw, err = f.GetPicture("Sheet1", "F22") + assert.NoError(t, err) + assert.Empty(t, file) + assert.Empty(t, raw) + + // Test get picture from none drawing worksheet. + f = NewFile() + file, raw, err = f.GetPicture("Sheet1", "F22") assert.NoError(t, err) assert.Empty(t, file) assert.Empty(t, raw) @@ -160,11 +167,9 @@ func TestAddDrawingPicture(t *testing.T) { func TestAddPictureFromBytes(t *testing.T) { f := NewFile() imgFile, err := ioutil.ReadFile("logo.png") - if err != nil { - t.Error("Unable to load logo for test") - } - f.AddPictureFromBytes("Sheet1", fmt.Sprint("A", 1), "", "logo", ".png", imgFile) - f.AddPictureFromBytes("Sheet1", fmt.Sprint("A", 50), "", "logo", ".png", imgFile) + 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)) imageCount := 0 for fileName := range f.XLSX { if strings.Contains(fileName, "media/image") { @@ -172,4 +177,5 @@ 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 is not exist") } diff --git a/rows_test.go b/rows_test.go index 6b50c75256..64942425da 100644 --- a/rows_test.go +++ b/rows_test.go @@ -1,6 +1,7 @@ package excelize import ( + "bytes" "fmt" "path/filepath" "testing" @@ -12,12 +13,12 @@ import ( func TestRows(t *testing.T) { const sheet2 = "Sheet2" - xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if !assert.NoError(t, err) { t.FailNow() } - rows, err := xlsx.Rows(sheet2) + rows, err := f.Rows(sheet2) if !assert.NoError(t, err) { t.FailNow() } @@ -32,7 +33,7 @@ func TestRows(t *testing.T) { t.FailNow() } - returnedRows, err := xlsx.GetRows(sheet2) + returnedRows, err := f.GetRows(sheet2) assert.NoError(t, err) for i := range returnedRows { returnedRows[i] = trimSliceSpace(returnedRows[i]) @@ -40,6 +41,11 @@ func TestRows(t *testing.T) { if !assert.Equal(t, collectedRows, returnedRows) { t.FailNow() } + + f = NewFile() + f.XLSX["xl/worksheets/sheet1.xml"] = []byte(`1B`) + _, err = f.Rows("Sheet1") + assert.EqualError(t, err, `strconv.Atoi: parsing "A": invalid syntax`) } func TestRowsIterator(t *testing.T) { @@ -126,6 +132,35 @@ func TestRowHeight(t *testing.T) { convertColWidthToPixels(0) } +func TestColumns(t *testing.T) { + f := NewFile() + rows, err := f.Rows("Sheet1") + assert.NoError(t, err) + rows.decoder = f.xmlNewDecoder(bytes.NewReader([]byte(`1B`))) + _, err = rows.Columns() + assert.EqualError(t, err, `strconv.Atoi: parsing "A": invalid syntax`) + + rows.decoder = f.xmlNewDecoder(bytes.NewReader([]byte(`1B`))) + _, err = rows.Columns() + assert.NoError(t, err) + + rows.curRow = 3 + rows.decoder = f.xmlNewDecoder(bytes.NewReader([]byte(`1`))) + _, err = rows.Columns() + assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`) + + // Test token is nil + rows.decoder = f.xmlNewDecoder(bytes.NewReader(nil)) + _, err = rows.Columns() + assert.NoError(t, err) +} + +func TestSharedStringsReader(t *testing.T) { + f := NewFile() + f.XLSX["xl/sharedStrings.xml"] = MacintoshCyrillicCharset + f.sharedStringsReader() +} + func TestRowVisibility(t *testing.T) { f, err := prepareTestBook1() if !assert.NoError(t, err) { @@ -149,61 +184,64 @@ func TestRowVisibility(t *testing.T) { } func TestRemoveRow(t *testing.T) { - xlsx := NewFile() - sheet1 := xlsx.GetSheetName(1) - r, err := xlsx.workSheetReader(sheet1) + f := NewFile() + sheet1 := f.GetSheetName(1) + r, err := f.workSheetReader(sheet1) assert.NoError(t, err) const ( colCount = 10 rowCount = 10 ) - fillCells(xlsx, sheet1, colCount, rowCount) + fillCells(f, sheet1, colCount, rowCount) - xlsx.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External") + f.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External") - assert.EqualError(t, xlsx.RemoveRow(sheet1, -1), "invalid row number -1") + assert.EqualError(t, f.RemoveRow(sheet1, -1), "invalid row number -1") - assert.EqualError(t, xlsx.RemoveRow(sheet1, 0), "invalid row number 0") + assert.EqualError(t, f.RemoveRow(sheet1, 0), "invalid row number 0") - assert.NoError(t, xlsx.RemoveRow(sheet1, 4)) + assert.NoError(t, f.RemoveRow(sheet1, 4)) if !assert.Len(t, r.SheetData.Row, rowCount-1) { t.FailNow() } - xlsx.MergeCell(sheet1, "B3", "B5") + f.MergeCell(sheet1, "B3", "B5") - assert.NoError(t, xlsx.RemoveRow(sheet1, 2)) + assert.NoError(t, f.RemoveRow(sheet1, 2)) if !assert.Len(t, r.SheetData.Row, rowCount-2) { t.FailNow() } - assert.NoError(t, xlsx.RemoveRow(sheet1, 4)) + assert.NoError(t, f.RemoveRow(sheet1, 4)) if !assert.Len(t, r.SheetData.Row, rowCount-3) { t.FailNow() } - err = xlsx.AutoFilter(sheet1, "A2", "A2", `{"column":"A","expression":"x != blanks"}`) + err = f.AutoFilter(sheet1, "A2", "A2", `{"column":"A","expression":"x != blanks"}`) if !assert.NoError(t, err) { t.FailNow() } - assert.NoError(t, xlsx.RemoveRow(sheet1, 1)) + assert.NoError(t, f.RemoveRow(sheet1, 1)) if !assert.Len(t, r.SheetData.Row, rowCount-4) { t.FailNow() } - assert.NoError(t, xlsx.RemoveRow(sheet1, 2)) + assert.NoError(t, f.RemoveRow(sheet1, 2)) if !assert.Len(t, r.SheetData.Row, rowCount-5) { t.FailNow() } - assert.NoError(t, xlsx.RemoveRow(sheet1, 1)) + assert.NoError(t, f.RemoveRow(sheet1, 1)) if !assert.Len(t, r.SheetData.Row, rowCount-6) { t.FailNow() } - assert.NoError(t, xlsx.RemoveRow(sheet1, 10)) - assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestRemoveRow.xlsx"))) + assert.NoError(t, f.RemoveRow(sheet1, 10)) + 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`) } func TestInsertRow(t *testing.T) { diff --git a/sheet.go b/sheet.go index 6ef7c6ebe1..7412fce274 100644 --- a/sheet.go +++ b/sheet.go @@ -505,7 +505,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 http://msdn.microsoft.com/en-us/library/office/documentformat.openxml.spreadsheet.sheetstatevalues.aspx +// values as defined by https://docs.microsoft.com/en-us/dotnet/api/documentformat.openxml.spreadsheet.sheetstatevalues // // visible // hidden @@ -738,7 +738,8 @@ func (f *File) searchSheet(name, value string, regSearch bool) (result []string, d = f.sharedStringsReader() decoder := f.xmlNewDecoder(bytes.NewReader(f.readXML(name))) for { - token, err := decoder.Token() + var token xml.Token + token, err = decoder.Token() if err != nil || token == nil { if err == io.EOF { err = nil @@ -749,13 +750,9 @@ func (f *File) searchSheet(name, value string, regSearch bool) (result []string, case xml.StartElement: inElement = startElement.Name.Local if inElement == "row" { - for _, attr := range startElement.Attr { - if attr.Name.Local == "r" { - row, err = strconv.Atoi(attr.Value) - if err != nil { - return result, err - } - } + row, err = attrValToInt("r", startElement.Attr) + if err != nil { + return } } if inElement == "c" { @@ -785,7 +782,20 @@ func (f *File) searchSheet(name, value string, regSearch bool) (result []string, default: } } + return +} +// attrValToInt provides a function to convert the local names to an integer +// by given XML attributes and specified names. +func attrValToInt(name string, attrs []xml.Attr) (val int, err error) { + for _, attr := range attrs { + if attr.Name.Local == name { + val, err = strconv.Atoi(attr.Value) + if err != nil { + return + } + } + } return } diff --git a/sheet_test.go b/sheet_test.go index b9e4abf788..aada60a81d 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -75,6 +75,20 @@ func TestNewSheet(t *testing.T) { assert.NoError(t, f.SaveAs(filepath.Join("test", "TestNewSheet.xlsx"))) } +func TestSetPane(t *testing.T) { + f := excelize.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.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"))) +} + func TestPageLayoutOption(t *testing.T) { const sheet = "Sheet1" @@ -156,6 +170,12 @@ func TestSearchSheet(t *testing.T) { result, err = f.SearchSheet("Sheet1", "[0-9]", true) assert.NoError(t, err) assert.EqualValues(t, expected, result) + + // Test search worksheet data after set cell value + f = excelize.NewFile() + assert.NoError(t, f.SetCellValue("Sheet1", "A1", true)) + _, err = f.SearchSheet("Sheet1", "") + assert.NoError(t, err) } func TestSetPageLayout(t *testing.T) { diff --git a/sparkline.go b/sparkline.go index 9ad5087775..47c8d5a217 100644 --- a/sparkline.go +++ b/sparkline.go @@ -390,21 +390,14 @@ func (f *File) addSparklineGroupByStyle(ID int) *xlsxX14SparklineGroup { // func (f *File) AddSparkline(sheet string, opt *SparklineOption) (err error) { var ( - ws *xlsxWorksheet - sparkType string - sparkTypes map[string]string - specifiedSparkTypes string - ok bool - group *xlsxX14SparklineGroup - groups *xlsxX14SparklineGroups - decodeExtLst *decodeWorksheetExt - idx int - ext *xlsxWorksheetExt - decodeSparklineGroups *decodeX14SparklineGroups - sparklineGroupBytes []byte - sparklineGroupsBytes []byte - extLst string - extLstBytes, extBytes []byte + ws *xlsxWorksheet + sparkType string + sparkTypes map[string]string + specifiedSparkTypes string + ok bool + group *xlsxX14SparklineGroup + groups *xlsxX14SparklineGroups + sparklineGroupsBytes, extBytes []byte ) // parameter validation @@ -442,38 +435,9 @@ func (f *File) AddSparkline(sheet string, opt *SparklineOption) (err error) { } f.addSparkline(opt, group) if ws.ExtLst.Ext != "" { // append mode ext - decodeExtLst = new(decodeWorksheetExt) - if err = f.xmlNewDecoder(bytes.NewReader([]byte("" + ws.ExtLst.Ext + ""))). - Decode(decodeExtLst); err != nil && err != io.EOF { + if err = f.appendSparkline(ws, group, groups); err != nil { return } - for idx, ext = range decodeExtLst.Ext { - if ext.URI == ExtURISparklineGroups { - decodeSparklineGroups = new(decodeX14SparklineGroups) - if err = f.xmlNewDecoder(bytes.NewReader([]byte(ext.Content))). - Decode(decodeSparklineGroups); err != nil && err != io.EOF { - return - } - if sparklineGroupBytes, err = xml.Marshal(group); err != nil { - return - } - groups = &xlsxX14SparklineGroups{ - XMLNSXM: NameSpaceSpreadSheetExcel2006Main, - Content: decodeSparklineGroups.Content + string(sparklineGroupBytes), - } - if sparklineGroupsBytes, err = xml.Marshal(groups); err != nil { - return - } - decodeExtLst.Ext[idx].Content = string(sparklineGroupsBytes) - } - } - if extLstBytes, err = xml.Marshal(decodeExtLst); err != nil { - return - } - extLst = string(extLstBytes) - ws.ExtLst = &xlsxExtLst{ - Ext: strings.TrimSuffix(strings.TrimPrefix(extLst, ""), ""), - } } else { groups = &xlsxX14SparklineGroups{ XMLNSXM: NameSpaceSpreadSheetExcel2006Main, @@ -482,11 +446,10 @@ func (f *File) AddSparkline(sheet string, opt *SparklineOption) (err error) { if sparklineGroupsBytes, err = xml.Marshal(groups); err != nil { return } - ext = &xlsxWorksheetExt{ + if extBytes, err = xml.Marshal(&xlsxWorksheetExt{ URI: ExtURISparklineGroups, Content: string(sparklineGroupsBytes), - } - if extBytes, err = xml.Marshal(ext); err != nil { + }); err != nil { return } ws.ExtLst.Ext = string(extBytes) @@ -534,3 +497,47 @@ func (f *File) addSparkline(opt *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) { + var ( + idx int + decodeExtLst *decodeWorksheetExt + decodeSparklineGroups *decodeX14SparklineGroups + ext *xlsxWorksheetExt + sparklineGroupsBytes, sparklineGroupBytes, extLstBytes []byte + ) + decodeExtLst = new(decodeWorksheetExt) + if err = f.xmlNewDecoder(bytes.NewReader([]byte("" + ws.ExtLst.Ext + ""))). + Decode(decodeExtLst); err != nil && err != io.EOF { + return + } + for idx, ext = range decodeExtLst.Ext { + if ext.URI == ExtURISparklineGroups { + decodeSparklineGroups = new(decodeX14SparklineGroups) + if err = f.xmlNewDecoder(bytes.NewReader([]byte(ext.Content))). + Decode(decodeSparklineGroups); err != nil && err != io.EOF { + return + } + if sparklineGroupBytes, err = xml.Marshal(group); err != nil { + return + } + groups = &xlsxX14SparklineGroups{ + XMLNSXM: NameSpaceSpreadSheetExcel2006Main, + Content: decodeSparklineGroups.Content + string(sparklineGroupBytes), + } + if sparklineGroupsBytes, err = xml.Marshal(groups); err != nil { + return + } + decodeExtLst.Ext[idx].Content = string(sparklineGroupsBytes) + } + } + if extLstBytes, err = xml.Marshal(decodeExtLst); err != nil { + return + } + ws.ExtLst = &xlsxExtLst{ + Ext: strings.TrimSuffix(strings.TrimPrefix(string(extLstBytes), ""), ""), + } + return +} diff --git a/sparkline_test.go b/sparkline_test.go index d52929bde5..a5cb2161cc 100644 --- a/sparkline_test.go +++ b/sparkline_test.go @@ -269,6 +269,15 @@ func TestAddSparkline(t *testing.T) { }), "XML syntax error on line 6: element closed by ") } +func TestAppendSparkline(t *testing.T) { + // Test unsupport charset. + f := NewFile() + ws, err := f.workSheetReader("Sheet1") + assert.NoError(t, err) + ws.ExtLst = &xlsxExtLst{Ext: string(MacintoshCyrillicCharset)} + assert.EqualError(t, f.appendSparkline(ws, &xlsxX14SparklineGroup{}, &xlsxX14SparklineGroups{}), "XML syntax error on line 1: invalid UTF-8") +} + func prepareSparklineDataset() *File { f := NewFile() sheet2 := [][]int{ diff --git a/stream_test.go b/stream_test.go index 97c55a7e09..8371a4e1f4 100644 --- a/stream_test.go +++ b/stream_test.go @@ -37,8 +37,7 @@ func TestStreamWriter(t *testing.T) { assert.NoError(t, streamWriter.SetRow(cell, &row)) } - err = streamWriter.Flush() - assert.NoError(t, err) + assert.NoError(t, streamWriter.Flush()) // Save xlsx file by the given path. assert.NoError(t, file.SaveAs(filepath.Join("test", "TestStreamWriter.xlsx"))) @@ -54,6 +53,21 @@ func TestFlush(t *testing.T) { assert.NoError(t, err) streamWriter.Sheet = "SheetN" assert.EqualError(t, streamWriter.Flush(), "sheet SheetN is not exist") + + // Test close temporary file error + file = NewFile() + streamWriter, err = file.NewStreamWriter("Sheet1") + assert.NoError(t, err) + for rowID := 10; rowID <= 51200; rowID++ { + row := make([]interface{}, 50) + for colID := 0; colID < 50; colID++ { + row[colID] = rand.Intn(640000) + } + cell, _ := CoordinatesToCellName(1, rowID) + assert.NoError(t, streamWriter.SetRow(cell, &row)) + } + assert.NoError(t, streamWriter.tmpFile.Close()) + assert.Error(t, streamWriter.Flush()) } func TestSetRow(t *testing.T) { diff --git a/xmlWorkbook.go b/xmlWorkbook.go index e9ded6c3e9..65606b01e7 100644 --- a/xmlWorkbook.go +++ b/xmlWorkbook.go @@ -203,7 +203,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://msdn.microsoft.com/en-us/library/office/documentformat.openxml.spreadsheet.definedname.aspx +// value. For a descriptions of the attributes see https://docs.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"` diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 71ff4cc57a..8f39adfae1 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -182,7 +182,7 @@ type xlsxSheetViews struct { // last sheetView definition is loaded, and the others are discarded. When // multiple windows are viewing the same sheet, multiple sheetView elements // (with corresponding workbookView entries) are saved. -// See https://msdn.microsoft.com/en-us/library/office/documentformat.openxml.spreadsheet.sheetview.aspx +// See https://docs.microsoft.com/en-us/dotnet/api/documentformat.openxml.spreadsheet.sheetview type xlsxSheetView struct { WindowProtection bool `xml:"windowProtection,attr,omitempty"` ShowFormulas bool `xml:"showFormulas,attr,omitempty"` From 4e4a5b9b3e052d1694442515492792fb1aa74c5a Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 23 Dec 2019 00:07:40 +0800 Subject: [PATCH 180/957] Improve compatibility, fix workbook's rels ID calc error --- adjust.go | 2 +- cell.go | 2 +- cellmerged.go | 2 - chart.go | 219 +++++++++++++++++++++++------------------------ comment.go | 15 ++-- excelize.go | 10 ++- excelize_test.go | 36 ++++---- lib.go | 9 ++ shape.go | 9 +- sheet.go | 22 +++-- stream_test.go | 2 +- styles.go | 16 ++-- styles_test.go | 2 +- table.go | 3 +- xmlChart.go | 8 +- xmlPivotTable.go | 2 +- xmlWorksheet.go | 26 +++++- 17 files changed, 213 insertions(+), 172 deletions(-) diff --git a/adjust.go b/adjust.go index bb583f17dc..c15d4b40b6 100644 --- a/adjust.go +++ b/adjust.go @@ -53,7 +53,7 @@ func (f *File) adjustHelper(sheet string, dir adjustDirection, num, offset int) return err } checkSheet(xlsx) - checkRow(xlsx) + _ = checkRow(xlsx) if xlsx.MergeCells != nil && len(xlsx.MergeCells.Cells) == 0 { xlsx.MergeCells = nil diff --git a/cell.go b/cell.go index ad4bcdb962..e59a659b2f 100644 --- a/cell.go +++ b/cell.go @@ -395,7 +395,7 @@ func (f *File) SetCellHyperLink(sheet, axis, link, linkType string) error { linkData = xlsxHyperlink{ Ref: axis, } - sheetPath, _ := f.sheetMap[trimSheetName(sheet)] + 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) diff --git a/cellmerged.go b/cellmerged.go index 968a28a7f2..5bea0bc708 100644 --- a/cellmerged.go +++ b/cellmerged.go @@ -129,8 +129,6 @@ func (f *File) UnmergeCell(sheet string, hcell, vcell string) error { if rect1[3] < rect1[1] { rect1[1], rect1[3] = rect1[3], rect1[1] } - hcell, _ = CoordinatesToCellName(rect1[0], rect1[1]) - vcell, _ = CoordinatesToCellName(rect1[2], rect1[3]) // return nil since no MergeCells in the sheet if xlsx.MergeCells == nil { diff --git a/chart.go b/chart.go index bf8155aad9..5a42c5bb81 100644 --- a/chart.go +++ b/chart.go @@ -726,8 +726,7 @@ func (f *File) prepareDrawing(xlsx *xlsxWorksheet, drawingID int, sheet, drawing drawingXML = strings.Replace(sheetRelationshipsDrawingXML, "..", "xl", -1) } else { // Add first picture for given sheet. - sheetPath, _ := f.sheetMap[trimSheetName(sheet)] - sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetPath, "xl/worksheets/") + ".rels" + sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(f.sheetMap[trimSheetName(sheet)], "xl/worksheets/") + ".rels" rID := f.addRels(sheetRels, SourceRelationshipDrawingML, sheetRelationshipsDrawingXML, "") f.addSheetDrawing(sheet, rID) } @@ -743,9 +742,9 @@ func (f *File) addChart(formatSet *formatChart) { XMLNSa: NameSpaceDrawingML, XMLNSr: SourceRelationship, XMLNSc16r2: SourceRelationshipChart201506, - Date1904: &attrValBool{Val: false}, - Lang: &attrValString{Val: "en-US"}, - RoundedCorners: &attrValBool{Val: false}, + Date1904: &attrValBool{Val: boolPtr(false)}, + Lang: &attrValString{Val: stringPtr("en-US")}, + RoundedCorners: &attrValBool{Val: boolPtr(false)}, Chart: cChart{ Title: &cTitle{ Tx: cTx{ @@ -761,10 +760,10 @@ func (f *File) addChart(formatSet *formatChart) { SchemeClr: &aSchemeClr{ Val: "tx1", LumMod: &attrValInt{ - Val: 65000, + Val: intPtr(65000), }, LumOff: &attrValInt{ - Val: 35000, + Val: intPtr(35000), }, }, }, @@ -806,29 +805,29 @@ func (f *File) addChart(formatSet *formatChart) { }, }, View3D: &cView3D{ - RotX: &attrValInt{Val: chartView3DRotX[formatSet.Type]}, - RotY: &attrValInt{Val: chartView3DRotY[formatSet.Type]}, - Perspective: &attrValInt{Val: chartView3DPerspective[formatSet.Type]}, - RAngAx: &attrValInt{Val: chartView3DRAngAx[formatSet.Type]}, + 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])}, }, Floor: &cThicknessSpPr{ - Thickness: &attrValInt{Val: 0}, + Thickness: &attrValInt{Val: intPtr(0)}, }, SideWall: &cThicknessSpPr{ - Thickness: &attrValInt{Val: 0}, + Thickness: &attrValInt{Val: intPtr(0)}, }, BackWall: &cThicknessSpPr{ - Thickness: &attrValInt{Val: 0}, + Thickness: &attrValInt{Val: intPtr(0)}, }, PlotArea: &cPlotArea{}, Legend: &cLegend{ - LegendPos: &attrValString{Val: chartLegendPosition[formatSet.Legend.Position]}, - Overlay: &attrValBool{Val: false}, + LegendPos: &attrValString{Val: stringPtr(chartLegendPosition[formatSet.Legend.Position])}, + Overlay: &attrValBool{Val: boolPtr(false)}, }, - PlotVisOnly: &attrValBool{Val: false}, - DispBlanksAs: &attrValString{Val: formatSet.ShowBlanksAs}, - ShowDLblsOverMax: &attrValBool{Val: false}, + PlotVisOnly: &attrValBool{Val: boolPtr(false)}, + DispBlanksAs: &attrValString{Val: stringPtr(formatSet.ShowBlanksAs)}, + ShowDLblsOverMax: &attrValBool{Val: boolPtr(false)}, }, SpPr: &cSpPr{ SolidFill: &aSolidFill{ @@ -842,10 +841,10 @@ func (f *File) addChart(formatSet *formatChart) { SolidFill: &aSolidFill{ SchemeClr: &aSchemeClr{Val: "tx1", LumMod: &attrValInt{ - Val: 15000, + Val: intPtr(15000), }, LumOff: &attrValInt{ - Val: 85000, + Val: intPtr(85000), }, }, }, @@ -928,31 +927,31 @@ func (f *File) addChart(formatSet *formatChart) { func (f *File) drawBaseChart(formatSet *formatChart) *cPlotArea { c := cCharts{ BarDir: &attrValString{ - Val: "col", + Val: stringPtr("col"), }, Grouping: &attrValString{ - Val: "clustered", + Val: stringPtr("clustered"), }, VaryColors: &attrValBool{ - Val: true, + Val: boolPtr(true), }, Ser: f.drawChartSeries(formatSet), Shape: f.drawChartShape(formatSet), DLbls: f.drawChartDLbls(formatSet), AxID: []*attrValInt{ - {Val: 754001152}, - {Val: 753999904}, + {Val: intPtr(754001152)}, + {Val: intPtr(753999904)}, }, - Overlap: &attrValInt{Val: 100}, + Overlap: &attrValInt{Val: intPtr(100)}, } var ok bool - if c.BarDir.Val, ok = plotAreaChartBarDir[formatSet.Type]; !ok { + if *c.BarDir.Val, ok = plotAreaChartBarDir[formatSet.Type]; !ok { c.BarDir = nil } - if c.Grouping.Val, ok = plotAreaChartGrouping[formatSet.Type]; !ok { + if *c.Grouping.Val, ok = plotAreaChartGrouping[formatSet.Type]; !ok { c.Grouping = nil } - if c.Overlap.Val, ok = plotAreaChartOverlap[formatSet.Type]; !ok { + if *c.Overlap.Val, ok = plotAreaChartOverlap[formatSet.Type]; !ok { c.Overlap = nil } catAx := f.drawPlotAreaCatAx(formatSet) @@ -1178,10 +1177,10 @@ func (f *File) drawDoughnutChart(formatSet *formatChart) *cPlotArea { return &cPlotArea{ DoughnutChart: &cCharts{ VaryColors: &attrValBool{ - Val: true, + Val: boolPtr(true), }, Ser: f.drawChartSeries(formatSet), - HoleSize: &attrValInt{Val: 75}, + HoleSize: &attrValInt{Val: intPtr(75)}, }, } } @@ -1192,19 +1191,19 @@ func (f *File) drawLineChart(formatSet *formatChart) *cPlotArea { return &cPlotArea{ LineChart: &cCharts{ Grouping: &attrValString{ - Val: plotAreaChartGrouping[formatSet.Type], + Val: stringPtr(plotAreaChartGrouping[formatSet.Type]), }, VaryColors: &attrValBool{ - Val: false, + Val: boolPtr(false), }, Ser: f.drawChartSeries(formatSet), DLbls: f.drawChartDLbls(formatSet), Smooth: &attrValBool{ - Val: false, + Val: boolPtr(false), }, AxID: []*attrValInt{ - {Val: 754001152}, - {Val: 753999904}, + {Val: intPtr(754001152)}, + {Val: intPtr(753999904)}, }, }, CatAx: f.drawPlotAreaCatAx(formatSet), @@ -1218,7 +1217,7 @@ func (f *File) drawPieChart(formatSet *formatChart) *cPlotArea { return &cPlotArea{ PieChart: &cCharts{ VaryColors: &attrValBool{ - Val: true, + Val: boolPtr(true), }, Ser: f.drawChartSeries(formatSet), }, @@ -1231,7 +1230,7 @@ func (f *File) drawPie3DChart(formatSet *formatChart) *cPlotArea { return &cPlotArea{ Pie3DChart: &cCharts{ VaryColors: &attrValBool{ - Val: true, + Val: boolPtr(true), }, Ser: f.drawChartSeries(formatSet), }, @@ -1244,16 +1243,16 @@ func (f *File) drawRadarChart(formatSet *formatChart) *cPlotArea { return &cPlotArea{ RadarChart: &cCharts{ RadarStyle: &attrValString{ - Val: "marker", + Val: stringPtr("marker"), }, VaryColors: &attrValBool{ - Val: false, + Val: boolPtr(false), }, Ser: f.drawChartSeries(formatSet), DLbls: f.drawChartDLbls(formatSet), AxID: []*attrValInt{ - {Val: 754001152}, - {Val: 753999904}, + {Val: intPtr(754001152)}, + {Val: intPtr(753999904)}, }, }, CatAx: f.drawPlotAreaCatAx(formatSet), @@ -1267,16 +1266,16 @@ func (f *File) drawScatterChart(formatSet *formatChart) *cPlotArea { return &cPlotArea{ ScatterChart: &cCharts{ ScatterStyle: &attrValString{ - Val: "smoothMarker", // line,lineMarker,marker,none,smooth,smoothMarker + Val: stringPtr("smoothMarker"), // line,lineMarker,marker,none,smooth,smoothMarker }, VaryColors: &attrValBool{ - Val: false, + Val: boolPtr(false), }, Ser: f.drawChartSeries(formatSet), DLbls: f.drawChartDLbls(formatSet), AxID: []*attrValInt{ - {Val: 754001152}, - {Val: 753999904}, + {Val: intPtr(754001152)}, + {Val: intPtr(753999904)}, }, }, CatAx: f.drawPlotAreaCatAx(formatSet), @@ -1291,9 +1290,9 @@ func (f *File) drawSurface3DChart(formatSet *formatChart) *cPlotArea { Surface3DChart: &cCharts{ Ser: f.drawChartSeries(formatSet), AxID: []*attrValInt{ - {Val: 754001152}, - {Val: 753999904}, - {Val: 832256642}, + {Val: intPtr(754001152)}, + {Val: intPtr(753999904)}, + {Val: intPtr(832256642)}, }, }, CatAx: f.drawPlotAreaCatAx(formatSet), @@ -1301,7 +1300,7 @@ func (f *File) drawSurface3DChart(formatSet *formatChart) *cPlotArea { SerAx: f.drawPlotAreaSerAx(formatSet), } if formatSet.Type == WireframeSurface3D { - plotArea.Surface3DChart.Wireframe = &attrValBool{Val: true} + plotArea.Surface3DChart.Wireframe = &attrValBool{Val: boolPtr(true)} } return plotArea } @@ -1313,9 +1312,9 @@ func (f *File) drawSurfaceChart(formatSet *formatChart) *cPlotArea { SurfaceChart: &cCharts{ Ser: f.drawChartSeries(formatSet), AxID: []*attrValInt{ - {Val: 754001152}, - {Val: 753999904}, - {Val: 832256642}, + {Val: intPtr(754001152)}, + {Val: intPtr(753999904)}, + {Val: intPtr(832256642)}, }, }, CatAx: f.drawPlotAreaCatAx(formatSet), @@ -1323,7 +1322,7 @@ func (f *File) drawSurfaceChart(formatSet *formatChart) *cPlotArea { SerAx: f.drawPlotAreaSerAx(formatSet), } if formatSet.Type == WireframeContour { - plotArea.SurfaceChart.Wireframe = &attrValBool{Val: true} + plotArea.SurfaceChart.Wireframe = &attrValBool{Val: boolPtr(true)} } return plotArea } @@ -1355,7 +1354,7 @@ func (f *File) drawChartShape(formatSet *formatChart) *attrValString { Col3DCylinderPercentStacked: "cylinder", } if shape, ok := shapes[formatSet.Type]; ok { - return &attrValString{Val: shape} + return &attrValString{Val: stringPtr(shape)} } return nil } @@ -1366,8 +1365,8 @@ func (f *File) drawChartSeries(formatSet *formatChart) *[]cSer { ser := []cSer{} for k := range formatSet.Series { ser = append(ser, cSer{ - IDx: &attrValInt{Val: k}, - Order: &attrValInt{Val: k}, + IDx: &attrValInt{Val: intPtr(k)}, + Order: &attrValInt{Val: intPtr(k)}, Tx: &cTx{ StrRef: &cStrRef{ F: formatSet.Series[k].Name, @@ -1416,8 +1415,8 @@ func (f *File) drawChartSeriesSpPr(i int, formatSet *formatChart) *cSpPr { // data index and format sets. func (f *File) drawChartSeriesDPt(i int, formatSet *formatChart) []*cDPt { dpt := []*cDPt{{ - IDx: &attrValInt{Val: i}, - Bubble3D: &attrValBool{Val: false}, + IDx: &attrValInt{Val: intPtr(i)}, + Bubble3D: &attrValBool{Val: boolPtr(false)}, SpPr: &cSpPr{ SolidFill: &aSolidFill{ SchemeClr: &aSchemeClr{Val: "accent" + strconv.Itoa(i+1)}, @@ -1475,8 +1474,8 @@ func (f *File) drawChartSeriesVal(v formatChartSeries, formatSet *formatChart) * // given data index and format sets. func (f *File) drawChartSeriesMarker(i int, formatSet *formatChart) *cMarker { marker := &cMarker{ - Symbol: &attrValString{Val: "circle"}, - Size: &attrValInt{Val: 5}, + Symbol: &attrValString{Val: stringPtr("circle")}, + Size: &attrValInt{Val: intPtr(5)}, } if i < 6 { marker.SpPr = &cSpPr{ @@ -1542,20 +1541,20 @@ func (f *File) drawCharSeriesBubble3D(formatSet *formatChart) *attrValBool { if _, ok := map[string]bool{Bubble3D: true}[formatSet.Type]; !ok { return nil } - return &attrValBool{Val: true} + return &attrValBool{Val: boolPtr(true)} } // drawChartDLbls provides a function to draw the c:dLbls element by given // format sets. func (f *File) drawChartDLbls(formatSet *formatChart) *cDLbls { return &cDLbls{ - ShowLegendKey: &attrValBool{Val: formatSet.Legend.ShowLegendKey}, - ShowVal: &attrValBool{Val: formatSet.Plotarea.ShowVal}, - ShowCatName: &attrValBool{Val: formatSet.Plotarea.ShowCatName}, - ShowSerName: &attrValBool{Val: formatSet.Plotarea.ShowSerName}, - ShowBubbleSize: &attrValBool{Val: formatSet.Plotarea.ShowBubbleSize}, - ShowPercent: &attrValBool{Val: formatSet.Plotarea.ShowPercent}, - ShowLeaderLines: &attrValBool{Val: formatSet.Plotarea.ShowLeaderLines}, + 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)}, } } @@ -1572,8 +1571,8 @@ 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: formatSet.XAxis.Minimum} - max := &attrValFloat{Val: formatSet.XAxis.Maximum} + min := &attrValFloat{Val: float64Ptr(formatSet.XAxis.Minimum)} + max := &attrValFloat{Val: float64Ptr(formatSet.XAxis.Maximum)} if formatSet.XAxis.Minimum == 0 { min = nil } @@ -1582,29 +1581,29 @@ func (f *File) drawPlotAreaCatAx(formatSet *formatChart) []*cAxs { } axs := []*cAxs{ { - AxID: &attrValInt{Val: 754001152}, + AxID: &attrValInt{Val: intPtr(754001152)}, Scaling: &cScaling{ - Orientation: &attrValString{Val: orientation[formatSet.XAxis.ReverseOrder]}, + Orientation: &attrValString{Val: stringPtr(orientation[formatSet.XAxis.ReverseOrder])}, Max: max, Min: min, }, - Delete: &attrValBool{Val: false}, - AxPos: &attrValString{Val: catAxPos[formatSet.XAxis.ReverseOrder]}, + Delete: &attrValBool{Val: boolPtr(false)}, + AxPos: &attrValString{Val: stringPtr(catAxPos[formatSet.XAxis.ReverseOrder])}, NumFmt: &cNumFmt{ FormatCode: "General", SourceLinked: true, }, - MajorTickMark: &attrValString{Val: "none"}, - MinorTickMark: &attrValString{Val: "none"}, - TickLblPos: &attrValString{Val: "nextTo"}, + MajorTickMark: &attrValString{Val: stringPtr("none")}, + MinorTickMark: &attrValString{Val: stringPtr("none")}, + TickLblPos: &attrValString{Val: stringPtr("nextTo")}, SpPr: f.drawPlotAreaSpPr(), TxPr: f.drawPlotAreaTxPr(), - CrossAx: &attrValInt{Val: 753999904}, - Crosses: &attrValString{Val: "autoZero"}, - Auto: &attrValBool{Val: true}, - LblAlgn: &attrValString{Val: "ctr"}, - LblOffset: &attrValInt{Val: 100}, - NoMultiLvlLbl: &attrValBool{Val: false}, + CrossAx: &attrValInt{Val: intPtr(753999904)}, + Crosses: &attrValString{Val: stringPtr("autoZero")}, + Auto: &attrValBool{Val: boolPtr(true)}, + LblAlgn: &attrValString{Val: stringPtr("ctr")}, + LblOffset: &attrValInt{Val: intPtr(100)}, + NoMultiLvlLbl: &attrValBool{Val: boolPtr(false)}, }, } if formatSet.XAxis.MajorGridlines { @@ -1618,8 +1617,8 @@ 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: formatSet.YAxis.Minimum} - max := &attrValFloat{Val: formatSet.YAxis.Maximum} + min := &attrValFloat{Val: float64Ptr(formatSet.YAxis.Minimum)} + max := &attrValFloat{Val: float64Ptr(formatSet.YAxis.Maximum)} if formatSet.YAxis.Minimum == 0 { min = nil } @@ -1628,26 +1627,26 @@ func (f *File) drawPlotAreaValAx(formatSet *formatChart) []*cAxs { } axs := []*cAxs{ { - AxID: &attrValInt{Val: 753999904}, + AxID: &attrValInt{Val: intPtr(753999904)}, Scaling: &cScaling{ - Orientation: &attrValString{Val: orientation[formatSet.YAxis.ReverseOrder]}, + Orientation: &attrValString{Val: stringPtr(orientation[formatSet.YAxis.ReverseOrder])}, Max: max, Min: min, }, - Delete: &attrValBool{Val: false}, - AxPos: &attrValString{Val: valAxPos[formatSet.YAxis.ReverseOrder]}, + Delete: &attrValBool{Val: boolPtr(false)}, + AxPos: &attrValString{Val: stringPtr(valAxPos[formatSet.YAxis.ReverseOrder])}, NumFmt: &cNumFmt{ FormatCode: chartValAxNumFmtFormatCode[formatSet.Type], SourceLinked: true, }, - MajorTickMark: &attrValString{Val: "none"}, - MinorTickMark: &attrValString{Val: "none"}, - TickLblPos: &attrValString{Val: "nextTo"}, + MajorTickMark: &attrValString{Val: stringPtr("none")}, + MinorTickMark: &attrValString{Val: stringPtr("none")}, + TickLblPos: &attrValString{Val: stringPtr("nextTo")}, SpPr: f.drawPlotAreaSpPr(), TxPr: f.drawPlotAreaTxPr(), - CrossAx: &attrValInt{Val: 754001152}, - Crosses: &attrValString{Val: "autoZero"}, - CrossBetween: &attrValString{Val: chartValAxCrossBetween[formatSet.Type]}, + CrossAx: &attrValInt{Val: intPtr(754001152)}, + Crosses: &attrValString{Val: stringPtr("autoZero")}, + CrossBetween: &attrValString{Val: stringPtr(chartValAxCrossBetween[formatSet.Type])}, }, } if formatSet.YAxis.MajorGridlines { @@ -1657,15 +1656,15 @@ func (f *File) drawPlotAreaValAx(formatSet *formatChart) []*cAxs { axs[0].MinorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()} } if pos, ok := valTickLblPos[formatSet.Type]; ok { - axs[0].TickLblPos.Val = pos + axs[0].TickLblPos.Val = stringPtr(pos) } return axs } // drawPlotAreaSerAx provides a function to draw the c:serAx element. func (f *File) drawPlotAreaSerAx(formatSet *formatChart) []*cAxs { - min := &attrValFloat{Val: formatSet.YAxis.Minimum} - max := &attrValFloat{Val: formatSet.YAxis.Maximum} + min := &attrValFloat{Val: float64Ptr(formatSet.YAxis.Minimum)} + max := &attrValFloat{Val: float64Ptr(formatSet.YAxis.Maximum)} if formatSet.YAxis.Minimum == 0 { min = nil } @@ -1674,18 +1673,18 @@ func (f *File) drawPlotAreaSerAx(formatSet *formatChart) []*cAxs { } return []*cAxs{ { - AxID: &attrValInt{Val: 832256642}, + AxID: &attrValInt{Val: intPtr(832256642)}, Scaling: &cScaling{ - Orientation: &attrValString{Val: orientation[formatSet.YAxis.ReverseOrder]}, + Orientation: &attrValString{Val: stringPtr(orientation[formatSet.YAxis.ReverseOrder])}, Max: max, Min: min, }, - Delete: &attrValBool{Val: false}, - AxPos: &attrValString{Val: catAxPos[formatSet.XAxis.ReverseOrder]}, - TickLblPos: &attrValString{Val: "nextTo"}, + Delete: &attrValBool{Val: boolPtr(false)}, + AxPos: &attrValString{Val: stringPtr(catAxPos[formatSet.XAxis.ReverseOrder])}, + TickLblPos: &attrValString{Val: stringPtr("nextTo")}, SpPr: f.drawPlotAreaSpPr(), TxPr: f.drawPlotAreaTxPr(), - CrossAx: &attrValInt{Val: 753999904}, + CrossAx: &attrValInt{Val: intPtr(753999904)}, }, } } @@ -1701,8 +1700,8 @@ func (f *File) drawPlotAreaSpPr() *cSpPr { SolidFill: &aSolidFill{ SchemeClr: &aSchemeClr{ Val: "tx1", - LumMod: &attrValInt{Val: 15000}, - LumOff: &attrValInt{Val: 85000}, + LumMod: &attrValInt{Val: intPtr(15000)}, + LumOff: &attrValInt{Val: intPtr(85000)}, }, }, }, @@ -1734,8 +1733,8 @@ func (f *File) drawPlotAreaTxPr() *cTxPr { SolidFill: &aSolidFill{ SchemeClr: &aSchemeClr{ Val: "tx1", - LumMod: &attrValInt{Val: 15000}, - LumOff: &attrValInt{Val: 85000}, + LumMod: &attrValInt{Val: intPtr(15000)}, + LumOff: &attrValInt{Val: intPtr(85000)}, }, }, Latin: &aLatin{Typeface: "+mn-lt"}, diff --git a/comment.go b/comment.go index 99630c93f7..486a035ecf 100644 --- a/comment.go +++ b/comment.go @@ -101,8 +101,7 @@ func (f *File) AddComment(sheet, cell, format string) error { drawingVML = strings.Replace(sheetRelationshipsDrawingVML, "..", "xl", -1) } else { // Add first comment for given sheet. - sheetPath, _ := f.sheetMap[trimSheetName(sheet)] - sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetPath, "xl/worksheets/") + ".rels" + sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(f.sheetMap[trimSheetName(sheet)], "xl/worksheets/") + ".rels" rID := f.addRels(sheetRels, SourceRelationshipDrawingVML, sheetRelationshipsDrawingVML, "") f.addRels(sheetRels, SourceRelationshipComments, sheetRelationshipsComments, "") f.addSheetLegacyDrawing(sheet, rID) @@ -256,23 +255,23 @@ func (f *File) addComment(commentsXML, cell string, formatSet *formatComment) { { RPr: &xlsxRPr{ B: " ", - Sz: &attrValFloat{Val: 9}, + Sz: &attrValFloat{Val: float64Ptr(9)}, Color: &xlsxColor{ Indexed: 81, }, - RFont: &attrValString{Val: defaultFont}, - Family: &attrValInt{Val: 2}, + RFont: &attrValString{Val: stringPtr(defaultFont)}, + Family: &attrValInt{Val: intPtr(2)}, }, T: a, }, { RPr: &xlsxRPr{ - Sz: &attrValFloat{Val: 9}, + Sz: &attrValFloat{Val: float64Ptr(9)}, Color: &xlsxColor{ Indexed: 81, }, - RFont: &attrValString{Val: defaultFont}, - Family: &attrValInt{Val: 2}, + RFont: &attrValString{Val: stringPtr(defaultFont)}, + Family: &attrValInt{Val: intPtr(2)}, }, T: t, }, diff --git a/excelize.go b/excelize.go index a2e20ffe82..135028c39a 100644 --- a/excelize.go +++ b/excelize.go @@ -203,11 +203,17 @@ func checkSheet(xlsx *xlsxWorksheet) { // relationship type, target and target mode. func (f *File) addRels(relPath, relType, target, targetMode string) int { rels := f.relsReader(relPath) - rID := 0 if rels == nil { rels = &xlsxRelationships{} } - rID = len(rels.Relationships) + 1 + var rID int + for _, rel := range rels.Relationships { + ID, _ := strconv.Atoi(strings.TrimPrefix(rel.ID, "rId")) + if ID > rID { + rID = ID + } + } + rID++ var ID bytes.Buffer ID.WriteString("rId") ID.WriteString(strconv.Itoa(rID)) diff --git a/excelize_test.go b/excelize_test.go index 6929a4fe33..1d6ed24f49 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -48,7 +48,7 @@ func TestOpenFile(t *testing.T) { assert.EqualError(t, f.SetCellDefault("Sheet2", "A", strconv.FormatFloat(float64(-100.1588), 'f', -1, 64)), `cannot convert cell "A" to coordinates: invalid cell name "A"`) - f.SetCellInt("Sheet2", "A1", 100) + assert.NoError(t, f.SetCellInt("Sheet2", "A1", 100)) // Test set cell integer value with illegal row number. assert.EqualError(t, f.SetCellInt("Sheet2", "A", 100), `cannot convert cell "A" to coordinates: invalid cell name "A"`) @@ -80,8 +80,10 @@ func TestOpenFile(t *testing.T) { _, err = f.GetCellFormula("Sheet1", "B") assert.EqualError(t, err, `cannot convert cell "B" to coordinates: invalid cell name "B"`) // Test get shared cell formula - f.GetCellFormula("Sheet2", "H11") - f.GetCellFormula("Sheet2", "I11") + _, err = f.GetCellFormula("Sheet2", "H11") + assert.NoError(t, err) + _, err = f.GetCellFormula("Sheet2", "I11") + assert.NoError(t, err) getSharedForumula(&xlsxWorksheet{}, "") // Test read cell value with given illegal rows number. @@ -91,10 +93,14 @@ func TestOpenFile(t *testing.T) { assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`) // Test read cell value with given lowercase column number. - f.GetCellValue("Sheet2", "a5") - f.GetCellValue("Sheet2", "C11") - f.GetCellValue("Sheet2", "D11") - f.GetCellValue("Sheet2", "D12") + _, err = f.GetCellValue("Sheet2", "a5") + assert.NoError(t, err) + _, err = f.GetCellValue("Sheet2", "C11") + assert.NoError(t, err) + _, err = f.GetCellValue("Sheet2", "D11") + assert.NoError(t, err) + _, err = f.GetCellValue("Sheet2", "D12") + assert.NoError(t, err) // Test SetCellValue function. assert.NoError(t, f.SetCellValue("Sheet2", "F1", " Hello")) assert.NoError(t, f.SetCellValue("Sheet2", "G1", []byte("World"))) @@ -147,7 +153,8 @@ func TestOpenFile(t *testing.T) { // Test completion column. f.SetCellValue("Sheet2", "M2", nil) // Test read cell value with given axis large than exists row. - f.GetCellValue("Sheet2", "E231") + _, err = f.GetCellValue("Sheet2", "E231") + assert.NoError(t, err) // Test get active worksheet of XLSX and get worksheet name of XLSX by given worksheet index. f.GetSheetName(f.GetActiveSheetIndex()) // Test get worksheet index of XLSX by given worksheet name. @@ -302,13 +309,10 @@ func TestSetCellHyperLink(t *testing.T) { assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellHyperLink.xlsx"))) - file := NewFile() - for row := 1; row <= 65530; row++ { - cell, err := CoordinatesToCellName(1, row) - assert.NoError(t, err) - assert.NoError(t, file.SetCellHyperLink("Sheet1", cell, "https://github.com/360EntSecGroup-Skylar/excelize", "External")) - } - assert.EqualError(t, file.SetCellHyperLink("Sheet1", "A65531", "https://github.com/360EntSecGroup-Skylar/excelize", "External"), "over maximum limit hyperlinks in a worksheet") + f = NewFile() + f.workSheetReader("Sheet1") + f.Sheet["xl/worksheets/sheet1.xml"].Hyperlinks = &xlsxHyperlinks{Hyperlink: make([]xlsxHyperlink, 65530)} + assert.EqualError(t, f.SetCellHyperLink("Sheet1", "A65531", "https://github.com/360EntSecGroup-Skylar/excelize", "External"), "over maximum limit hyperlinks in a worksheet") f = NewFile() f.workSheetReader("Sheet1") @@ -1013,6 +1017,8 @@ func TestSetActiveSheet(t *testing.T) { f.WorkBook.BookViews = &xlsxBookViews{WorkBookView: []xlsxWorkBookView{}} f.Sheet["xl/worksheets/sheet1.xml"].SheetViews = &xlsxSheetViews{SheetView: []xlsxSheetView{}} f.SetActiveSheet(1) + f.Sheet["xl/worksheets/sheet1.xml"].SheetViews = nil + f.SetActiveSheet(1) } func TestSetSheetVisible(t *testing.T) { diff --git a/lib.go b/lib.go index edac98a8c4..86f8d16dc2 100644 --- a/lib.go +++ b/lib.go @@ -198,6 +198,15 @@ func CoordinatesToCellName(col, row int) (string, error) { // 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. +func intPtr(i int) *int { return &i } + +// float64Ptr returns a pofloat64er 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 { diff --git a/shape.go b/shape.go index f284e43d61..2ea66ea4d1 100644 --- a/shape.go +++ b/shape.go @@ -275,8 +275,7 @@ func (f *File) AddShape(sheet, cell, format string) error { drawingXML = strings.Replace(sheetRelationshipsDrawingXML, "..", "xl", -1) } else { // Add first shape for given sheet. - name, _ := f.sheetMap[trimSheetName(sheet)] - sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(name, "xl/worksheets/") + ".rels" + sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(f.sheetMap[trimSheetName(sheet)], "xl/worksheets/") + ".rels" rID := f.addRels(sheetRels, SourceRelationshipDrawingML, sheetRelationshipsDrawingXML, "") f.addSheetDrawing(sheet, rID) } @@ -362,7 +361,7 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, formatSet *format FontRef: &aFontRef{ Idx: "minor", SchemeClr: &attrValString{ - Val: "tx1", + Val: stringPtr("tx1"), }, }, }, @@ -422,7 +421,7 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, formatSet *format if len(srgbClr) == 6 { paragraph.R.RPr.SolidFill = &aSolidFill{ SrgbClr: &attrValString{ - Val: srgbClr, + Val: stringPtr(srgbClr), }, } } @@ -454,7 +453,7 @@ func setShapeRef(color string, i int) *aRef { return &aRef{ Idx: i, SrgbClr: &attrValString{ - Val: strings.Replace(strings.ToUpper(color), "#", "", -1), + Val: stringPtr(strings.Replace(strings.ToUpper(color), "#", "", -1)), }, } } diff --git a/sheet.go b/sheet.go index 7412fce274..954de5bc66 100644 --- a/sheet.go +++ b/sheet.go @@ -249,6 +249,11 @@ func (f *File) SetActiveSheet(index int) { } for idx, name := range f.GetSheetMap() { xlsx, _ := f.workSheetReader(name) + if xlsx.SheetViews == nil { + xlsx.SheetViews = &xlsxSheetViews{ + SheetView: []xlsxSheetView{{WorkbookViewID: 0}}, + } + } if len(xlsx.SheetViews.SheetView) > 0 { xlsx.SheetViews.SheetView[0].TabSelected = false } @@ -305,11 +310,15 @@ func (f *File) SetSheetName(oldName, newName string) { // string. func (f *File) GetSheetName(index int) string { wb := f.workbookReader() - realIdx := index - 1 // sheets are 1 based index, but we're checking against an array - if wb == nil || realIdx < 0 || realIdx >= len(wb.Sheets.Sheet) { + if wb == nil || index < 1 { return "" } - return wb.Sheets.Sheet[realIdx].Name + for _, sheet := range wb.Sheets.Sheet { + if index == sheet.SheetID { + return sheet.Name + } + } + return "" } // GetSheetIndex provides a function to get worksheet index of XLSX by given @@ -342,8 +351,8 @@ func (f *File) GetSheetMap() map[int]string { wb := f.workbookReader() sheetMap := map[int]string{} if wb != nil { - for i, sheet := range wb.Sheets.Sheet { - sheetMap[i+1] = sheet.Name + for _, sheet := range wb.Sheets.Sheet { + sheetMap[sheet.SheetID] = sheet.Name } } return sheetMap @@ -384,8 +393,7 @@ func (f *File) SetSheetBackground(sheet, picture string) error { } file, _ := ioutil.ReadFile(picture) name := f.addMedia(file, ext) - sheetPath, _ := f.sheetMap[trimSheetName(sheet)] - sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetPath, "xl/worksheets/") + ".rels" + sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(f.sheetMap[trimSheetName(sheet)], "xl/worksheets/") + ".rels" rID := f.addRels(sheetRels, SourceRelationshipImage, strings.Replace(name, "xl", "..", 1), "") f.addSheetPicture(sheet, rID) f.setContentTypePartImageExtensions() diff --git a/stream_test.go b/stream_test.go index 8371a4e1f4..4482bd15e4 100644 --- a/stream_test.go +++ b/stream_test.go @@ -42,7 +42,7 @@ func TestStreamWriter(t *testing.T) { assert.NoError(t, file.SaveAs(filepath.Join("test", "TestStreamWriter.xlsx"))) // Test error exceptions - streamWriter, err = file.NewStreamWriter("SheetN") + _, err = file.NewStreamWriter("SheetN") assert.EqualError(t, err, "sheet SheetN is not exist") } diff --git a/styles.go b/styles.go index fa0507ebc9..56c7196c93 100644 --- a/styles.go +++ b/styles.go @@ -1957,13 +1957,13 @@ func (f *File) NewConditionalStyle(style string) (int, error) { // Documents generated by excelize start with Calibri. func (f *File) GetDefaultFont() string { font := f.readDefaultFont() - return font.Name.Val + return *font.Name.Val } // SetDefaultFont changes the default font in the workbook. func (f *File) SetDefaultFont(fontName string) { font := f.readDefaultFont() - font.Name.Val = fontName + font.Name.Val = stringPtr(fontName) s := f.stylesReader() s.Fonts.Font[0] = font custom := true @@ -1987,10 +1987,10 @@ func (f *File) setFont(formatStyle *formatStyle) *xlsxFont { formatStyle.Font.Color = "#000000" } fnt := xlsxFont{ - Sz: &attrValFloat{Val: formatStyle.Font.Size}, + Sz: &attrValFloat{Val: float64Ptr(formatStyle.Font.Size)}, Color: &xlsxColor{RGB: getPaletteColor(formatStyle.Font.Color)}, - Name: &attrValString{Val: formatStyle.Font.Family}, - Family: &attrValInt{Val: 2}, + Name: &attrValString{Val: stringPtr(formatStyle.Font.Family)}, + Family: &attrValInt{Val: intPtr(2)}, } if formatStyle.Font.Bold { fnt.B = &formatStyle.Font.Bold @@ -1998,8 +1998,8 @@ func (f *File) setFont(formatStyle *formatStyle) *xlsxFont { if formatStyle.Font.Italic { fnt.I = &formatStyle.Font.Italic } - if fnt.Name.Val == "" { - fnt.Name.Val = f.GetDefaultFont() + if *fnt.Name.Val == "" { + *fnt.Name.Val = f.GetDefaultFont() } if formatStyle.Font.Strike { strike := true @@ -2007,7 +2007,7 @@ func (f *File) setFont(formatStyle *formatStyle) *xlsxFont { } val, ok := fontUnderlineType[formatStyle.Font.Underline] if ok { - fnt.U = &attrValString{Val: val} + fnt.U = &attrValString{Val: stringPtr(val)} } return &fnt } diff --git a/styles_test.go b/styles_test.go index 36a78ed8ca..e6faccb96c 100644 --- a/styles_test.go +++ b/styles_test.go @@ -175,7 +175,7 @@ func TestNewStyle(t *testing.T) { styles := f.stylesReader() 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") + assert.Contains(t, *font.Name.Val, "Times New Roman", "Stored font should contain font name") assert.Equal(t, 2, styles.CellXfs.Count, "Should have 2 styles") } diff --git a/table.go b/table.go index d26f8fd462..c5a704c66e 100644 --- a/table.go +++ b/table.go @@ -77,8 +77,7 @@ func (f *File) AddTable(sheet, hcell, vcell, format string) error { sheetRelationshipsTableXML := "../tables/table" + strconv.Itoa(tableID) + ".xml" tableXML := strings.Replace(sheetRelationshipsTableXML, "..", "xl", -1) // Add first table for given sheet. - sheetPath, _ := f.sheetMap[trimSheetName(sheet)] - sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetPath, "xl/worksheets/") + ".rels" + sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(f.sheetMap[trimSheetName(sheet)], "xl/worksheets/") + ".rels" rID := f.addRels(sheetRels, SourceRelationshipTable, sheetRelationshipsTableXML, "") f.addSheetTable(sheet, rID) err = f.addTable(sheet, tableXML, hcol, hrow, vcol, vrow, tableID, formatSet) diff --git a/xmlChart.go b/xmlChart.go index a02da2a423..84c1a3b09c 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -141,25 +141,25 @@ type aSchemeClr struct { // attrValInt directly maps the val element with integer data type as an // attribute。 type attrValInt struct { - Val int `xml:"val,attr"` + Val *int `xml:"val,attr"` } // attrValFloat directly maps the val element with float64 data type as an // attribute。 type attrValFloat struct { - Val float64 `xml:"val,attr"` + Val *float64 `xml:"val,attr"` } // attrValBool directly maps the val element with boolean data type as an // attribute。 type attrValBool struct { - Val bool `xml:"val,attr"` + Val *bool `xml:"val,attr"` } // attrValString directly maps the val element with string data type as an // attribute。 type attrValString struct { - Val string `xml:"val,attr"` + Val *string `xml:"val,attr"` } // aCs directly maps the a:cs element. diff --git a/xmlPivotTable.go b/xmlPivotTable.go index 0549c5e39c..6e1dfb84a0 100644 --- a/xmlPivotTable.go +++ b/xmlPivotTable.go @@ -187,7 +187,7 @@ type xlsxItem struct { F bool `xml:"f,attr,omitempty"` M bool `xml:"m,attr,omitempty"` C bool `xml:"c,attr,omitempty"` - X int `xml:"x,attr,omitempty,omitempty"` + X int `xml:"x,attr,omitempty"` D bool `xml:"d,attr,omitempty"` E bool `xml:"e,attr,omitempty"` } diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 8f39adfae1..57fd43f8a2 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -27,7 +27,7 @@ type xlsxWorksheet struct { ProtectedRanges *xlsxInnerXML `xml:"protectedRanges"` Scenarios *xlsxInnerXML `xml:"scenarios"` AutoFilter *xlsxAutoFilter `xml:"autoFilter"` - SortState *xlsxInnerXML `xml:"sortState"` + SortState *xlsxSortState `xml:"sortState"` DataConsolidate *xlsxInnerXML `xml:"dataConsolidate"` CustomSheetViews *xlsxCustomSheetViews `xml:"customSheetViews"` MergeCells *xlsxMergeCells `xml:"mergeCells"` @@ -47,7 +47,7 @@ type xlsxWorksheet struct { SmartTags *xlsxInnerXML `xml:"smartTags"` Drawing *xlsxDrawing `xml:"drawing"` LegacyDrawing *xlsxLegacyDrawing `xml:"legacyDrawing"` - LegacyDrawingHF *xlsxInnerXML `xml:"legacyDrawingHF"` + LegacyDrawingHF *xlsxLegacyDrawingHF `xml:"legacyDrawingHF"` DrawingHF *xlsxDrawingHF `xml:"drawingHF"` Picture *xlsxPicture `xml:"picture"` OleObjects *xlsxInnerXML `xml:"oleObjects"` @@ -328,6 +328,16 @@ type xlsxRow struct { C []xlsxC `xml:"c"` } +// xlsxSortState directly maps the sortState element. This collection +// preserves the AutoFilter sort state. +type xlsxSortState struct { + ColumnSort bool `xml:"columnSort,attr,omitempty"` + CaseSensitive bool `xml:"caseSensitive,attr,omitempty"` + SortMethod string `xml:"sortMethod,attr,omitempty"` + Ref string `xml:"ref,attr"` + Content string `xml:",innerxml"` +} + // xlsxCustomSheetViews directly maps the customSheetViews element. This is a // collection of custom sheet views. type xlsxCustomSheetViews struct { @@ -424,7 +434,7 @@ type DataValidation struct { ShowErrorMessage bool `xml:"showErrorMessage,attr,omitempty"` ShowInputMessage bool `xml:"showInputMessage,attr,omitempty"` Sqref string `xml:"sqref,attr"` - Type string `xml:"type,attr"` + Type string `xml:"type,attr,omitempty"` Formula1 string `xml:",innerxml"` Formula2 string `xml:",innerxml"` } @@ -448,7 +458,7 @@ type DataValidation struct { type xlsxC struct { XMLName xml.Name `xml:"c"` XMLSpace xml.Attr `xml:"space,attr,omitempty"` - R string `xml:"r,attr"` // Cell ID, e.g. A1 + 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. @@ -665,6 +675,14 @@ type xlsxLegacyDrawing struct { RID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr,omitempty"` } +// xlsxLegacyDrawingHF specifies the explicit relationship to the part +// containing the VML defining pictures rendered in the header / footer of the +// sheet. +type xlsxLegacyDrawingHF struct { + XMLName xml.Name `xml:"legacyDrawingHF"` + RID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr,omitempty"` +} + type xlsxInnerXML struct { Content string `xml:",innerxml"` } From 1666d04559d9f5b579ab7c850ccc95863c31bd25 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 24 Dec 2019 01:09:28 +0800 Subject: [PATCH 181/957] optimization: checking error in unit tests --- .travis.yml | 1 + cell_test.go | 18 ++-- cellmerged_test.go | 22 +++-- chart_test.go | 21 ++--- col_test.go | 53 ++++++++---- datavalidation_test.go | 46 ++++------ excelize.go | 2 +- excelize_test.go | 186 ++++++++++++++++++++++------------------- file_test.go | 4 +- picture_test.go | 35 +++----- pivotTable_test.go | 12 +-- rows_test.go | 47 +++++------ sheet_test.go | 2 +- sparkline_test.go | 60 ++++++------- stream.go | 2 +- table.go | 2 +- 16 files changed, 267 insertions(+), 246 deletions(-) diff --git a/.travis.yml b/.travis.yml index faf9916b1e..5012f86d63 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,7 @@ go: - 1.10.x - 1.11.x - 1.12.x + - 1.13.x os: - linux diff --git a/cell_test.go b/cell_test.go index 7d3339f969..1efbc5a871 100644 --- a/cell_test.go +++ b/cell_test.go @@ -54,8 +54,8 @@ func TestSetCellFloat(t *testing.T) { sheet := "Sheet1" t.Run("with no decimal", func(t *testing.T) { f := NewFile() - f.SetCellFloat(sheet, "A1", 123.0, -1, 64) - f.SetCellFloat(sheet, "A2", 123.0, 1, 64) + assert.NoError(t, f.SetCellFloat(sheet, "A1", 123.0, -1, 64)) + assert.NoError(t, f.SetCellFloat(sheet, "A2", 123.0, 1, 64)) val, err := f.GetCellValue(sheet, "A1") assert.NoError(t, err) assert.Equal(t, "123", val, "A1 should be 123") @@ -66,7 +66,7 @@ func TestSetCellFloat(t *testing.T) { t.Run("with a decimal and precision limit", func(t *testing.T) { f := NewFile() - f.SetCellFloat(sheet, "A1", 123.42, 1, 64) + assert.NoError(t, f.SetCellFloat(sheet, "A1", 123.42, 1, 64)) val, err := f.GetCellValue(sheet, "A1") assert.NoError(t, err) assert.Equal(t, "123.4", val, "A1 should be 123.4") @@ -74,7 +74,7 @@ func TestSetCellFloat(t *testing.T) { t.Run("with a decimal and no limit", func(t *testing.T) { f := NewFile() - f.SetCellFloat(sheet, "A1", 123.42, -1, 64) + assert.NoError(t, f.SetCellFloat(sheet, "A1", 123.42, -1, 64)) val, err := f.GetCellValue(sheet, "A1") assert.NoError(t, err) assert.Equal(t, "123.42", val, "A1 should be 123.42") @@ -101,7 +101,7 @@ func TestGetCellFormula(t *testing.T) { assert.EqualError(t, err, "sheet SheetN is not exist") // Test get cell formula on no formula cell. - f.SetCellValue("Sheet1", "A1", true) + assert.NoError(t, f.SetCellValue("Sheet1", "A1", true)) _, err = f.GetCellFormula("Sheet1", "A1") assert.NoError(t, err) } @@ -109,7 +109,9 @@ func TestGetCellFormula(t *testing.T) { func ExampleFile_SetCellFloat() { f := NewFile() var x = 3.14159265 - f.SetCellFloat("Sheet1", "A1", x, 2, 64) + if err := f.SetCellFloat("Sheet1", "A1", x, 2, 64); err != nil { + fmt.Println(err) + } val, _ := f.GetCellValue("Sheet1", "A1") fmt.Println(val) // Output: 3.14 @@ -122,7 +124,9 @@ func BenchmarkSetCellValue(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { for j := 0; j < len(values); j++ { - f.SetCellValue("Sheet1", fmt.Sprint(cols[j], i), values[j]) + if err := f.SetCellValue("Sheet1", fmt.Sprint(cols[j], i), values[j]); err != nil { + b.Error(err) + } } } } diff --git a/cellmerged_test.go b/cellmerged_test.go index 1da0eb31e1..e880d056c2 100644 --- a/cellmerged_test.go +++ b/cellmerged_test.go @@ -21,14 +21,20 @@ func TestMergeCell(t *testing.T) { assert.NoError(t, f.MergeCell("Sheet1", "H7", "B15")) assert.NoError(t, f.MergeCell("Sheet1", "D11", "F13")) assert.NoError(t, f.MergeCell("Sheet1", "G10", "K12")) - f.SetCellValue("Sheet1", "G11", "set value in merged cell") - f.SetCellInt("Sheet1", "H11", 100) - f.SetCellValue("Sheet1", "I11", float64(0.5)) - f.SetCellHyperLink("Sheet1", "J11", "https://github.com/360EntSecGroup-Skylar/excelize", "External") - f.SetCellFormula("Sheet1", "G12", "SUM(Sheet1!B19,Sheet1!C19)") - f.GetCellValue("Sheet1", "H11") - f.GetCellValue("Sheet2", "A6") // Merged cell ref is single coordinate. - f.GetCellFormula("Sheet1", "G12") + 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.SetCellHyperLink("Sheet1", "J11", "https://github.com/360EntSecGroup-Skylar/excelize", "External")) + assert.NoError(t, f.SetCellFormula("Sheet1", "G12", "SUM(Sheet1!B19,Sheet1!C19)")) + value, err := f.GetCellValue("Sheet1", "H11") + assert.Equal(t, "0.5", value) + assert.NoError(t, err) + value, err = f.GetCellValue("Sheet2", "A6") // Merged cell ref is single coordinate. + assert.Equal(t, "", value) + assert.NoError(t, err) + value, err = f.GetCellFormula("Sheet1", "G12") + assert.Equal(t, "SUM(Sheet1!B19,Sheet1!C19)", value) + assert.NoError(t, err) f.NewSheet("Sheet3") assert.NoError(t, f.MergeCell("Sheet3", "D11", "F13")) diff --git a/chart_test.go b/chart_test.go index bc5c30ae9f..2379ddc0e9 100644 --- a/chart_test.go +++ b/chart_test.go @@ -22,7 +22,7 @@ func TestChartSize(t *testing.T) { "D1": "Pear", } for cell, v := range categories { - xlsx.SetCellValue(sheet1, cell, v) + assert.NoError(t, xlsx.SetCellValue(sheet1, cell, v)) } values := map[string]int{ @@ -37,29 +37,22 @@ func TestChartSize(t *testing.T) { "D4": 8, } for cell, v := range values { - xlsx.SetCellValue(sheet1, cell, v) + assert.NoError(t, xlsx.SetCellValue(sheet1, cell, v)) } - xlsx.AddChart("Sheet1", "E4", `{"type":"col3DClustered","dimension":{"width":640, "height":480},`+ + assert.NoError(t, xlsx.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"}}`) + `"title":{"name":"3D Clustered Column Chart"}}`)) - var ( - buffer bytes.Buffer - ) + var buffer bytes.Buffer // Save xlsx file by the given path. - err := xlsx.Write(&buffer) - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, xlsx.Write(&buffer)) newFile, err := OpenReader(&buffer) - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, err) chartsNum := newFile.countCharts() if !assert.Equal(t, 1, chartsNum, "Expected 1 chart, actual %d", chartsNum) { diff --git a/col_test.go b/col_test.go index edbdae7a17..cdb7edfb0e 100644 --- a/col_test.go +++ b/col_test.go @@ -37,17 +37,29 @@ func TestColumnVisibility(t *testing.T) { t.Run("TestBook3", func(t *testing.T) { f, err := prepareTestBook3() assert.NoError(t, err) - f.GetColVisible("Sheet1", "B") + visible, err := f.GetColVisible("Sheet1", "B") + assert.Equal(t, true, visible) + assert.NoError(t, err) }) } func TestOutlineLevel(t *testing.T) { f := NewFile() - f.GetColOutlineLevel("Sheet1", "D") + level, err := f.GetColOutlineLevel("Sheet1", "D") + assert.Equal(t, uint8(0), level) + assert.NoError(t, err) + f.NewSheet("Sheet2") assert.NoError(t, f.SetColOutlineLevel("Sheet1", "D", 4)) - f.GetColOutlineLevel("Sheet1", "D") - f.GetColOutlineLevel("Shee2", "A") + + level, err = f.GetColOutlineLevel("Sheet1", "D") + assert.Equal(t, uint8(4), level) + assert.NoError(t, err) + + level, err = f.GetColOutlineLevel("Shee2", "A") + assert.Equal(t, uint8(0), level) + assert.EqualError(t, err, "sheet Shee2 is not exist") + assert.NoError(t, f.SetColWidth("Sheet2", "A", "D", 13)) assert.NoError(t, f.SetColOutlineLevel("Sheet2", "B", 2)) assert.NoError(t, f.SetRowOutlineLevel("Sheet1", 2, 7)) @@ -56,7 +68,7 @@ func TestOutlineLevel(t *testing.T) { // Test set row outline level on not exists worksheet. assert.EqualError(t, f.SetRowOutlineLevel("SheetN", 1, 4), "sheet SheetN is not exist") // Test get row outline level on not exists worksheet. - _, err := f.GetRowOutlineLevel("SheetN", 1) + _, err = f.GetRowOutlineLevel("SheetN", 1) assert.EqualError(t, err, "sheet SheetN is not exist") // Test set and get column outline level with illegal cell coordinates. @@ -68,7 +80,7 @@ func TestOutlineLevel(t *testing.T) { assert.EqualError(t, f.SetColOutlineLevel("SheetN", "E", 2), "sheet SheetN is not exist") assert.EqualError(t, f.SetRowOutlineLevel("Sheet1", 0, 1), "invalid row number 0") - level, err := f.GetRowOutlineLevel("Sheet1", 2) + level, err = f.GetRowOutlineLevel("Sheet1", 2) assert.NoError(t, err) assert.Equal(t, uint8(7), level) @@ -83,7 +95,7 @@ func TestOutlineLevel(t *testing.T) { f, err = OpenFile(filepath.Join("test", "Book1.xlsx")) assert.NoError(t, err) - f.SetColOutlineLevel("Sheet2", "B", 2) + assert.NoError(t, f.SetColOutlineLevel("Sheet2", "B", 2)) } func TestSetColStyle(t *testing.T) { @@ -105,13 +117,18 @@ func TestSetColStyle(t *testing.T) { func TestColWidth(t *testing.T) { f := NewFile() - f.SetColWidth("Sheet1", "B", "A", 12) - f.SetColWidth("Sheet1", "A", "B", 12) - f.GetColWidth("Sheet1", "A") - f.GetColWidth("Sheet1", "C") + assert.NoError(t, f.SetColWidth("Sheet1", "B", "A", 12)) + assert.NoError(t, f.SetColWidth("Sheet1", "A", "B", 12)) + width, err := f.GetColWidth("Sheet1", "A") + assert.Equal(t, float64(12), width) + assert.NoError(t, err) + width, err = f.GetColWidth("Sheet1", "C") + assert.Equal(t, float64(64), width) + assert.NoError(t, err) // Test set and get column width with illegal cell coordinates. - _, err := f.GetColWidth("Sheet1", "*") + width, err = f.GetColWidth("Sheet1", "*") + assert.Equal(t, float64(64), width) assert.EqualError(t, err, `invalid column name "*"`) assert.EqualError(t, f.SetColWidth("Sheet1", "*", "B", 1), `invalid column name "*"`) assert.EqualError(t, f.SetColWidth("Sheet1", "A", "*", 1), `invalid column name "*"`) @@ -133,8 +150,8 @@ func TestInsertCol(t *testing.T) { fillCells(f, sheet1, 10, 10) - f.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External") - f.MergeCell(sheet1, "A1", "C3") + assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/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.InsertCol(sheet1, "A")) @@ -151,11 +168,11 @@ func TestRemoveCol(t *testing.T) { fillCells(f, sheet1, 10, 15) - f.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External") - f.SetCellHyperLink(sheet1, "C5", "https://github.com", "External") + assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External")) + assert.NoError(t, f.SetCellHyperLink(sheet1, "C5", "https://github.com", "External")) - f.MergeCell(sheet1, "A1", "B1") - f.MergeCell(sheet1, "A2", "B2") + assert.NoError(t, f.MergeCell(sheet1, "A1", "B1")) + assert.NoError(t, f.MergeCell(sheet1, "A2", "B2")) assert.NoError(t, f.RemoveCol(sheet1, "A")) assert.NoError(t, f.RemoveCol(sheet1, "A")) diff --git a/datavalidation_test.go b/datavalidation_test.go index 763bad1341..7e54d55176 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -24,53 +24,45 @@ func TestDataValidation(t *testing.T) { dvRange := NewDataValidation(true) dvRange.Sqref = "A1:B2" - dvRange.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorBetween) + 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") - f.AddDataValidation("Sheet1", dvRange) - if !assert.NoError(t, f.SaveAs(resultFile)) { - t.FailNow() - } + assert.NoError(t, f.AddDataValidation("Sheet1", dvRange)) + assert.NoError(t, f.SaveAs(resultFile)) dvRange = NewDataValidation(true) dvRange.Sqref = "A3:B4" - dvRange.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorGreaterThan) + assert.NoError(t, dvRange.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorGreaterThan)) dvRange.SetInput("input title", "input body") - f.AddDataValidation("Sheet1", dvRange) - if !assert.NoError(t, f.SaveAs(resultFile)) { - t.FailNow() - } + assert.NoError(t, f.AddDataValidation("Sheet1", dvRange)) + assert.NoError(t, f.SaveAs(resultFile)) dvRange = NewDataValidation(true) dvRange.Sqref = "A5:B6" - dvRange.SetDropList([]string{"1", "2", "3"}) - f.AddDataValidation("Sheet1", dvRange) - if !assert.NoError(t, f.SaveAs(resultFile)) { - t.FailNow() - } + assert.NoError(t, dvRange.SetDropList([]string{"1", "2", "3"})) + assert.NoError(t, f.AddDataValidation("Sheet1", dvRange)) + assert.NoError(t, f.SaveAs(resultFile)) } func TestDataValidationError(t *testing.T) { resultFile := filepath.Join("test", "TestDataValidationError.xlsx") f := NewFile() - f.SetCellStr("Sheet1", "E1", "E1") - f.SetCellStr("Sheet1", "E2", "E2") - f.SetCellStr("Sheet1", "E3", "E3") + assert.NoError(t, f.SetCellStr("Sheet1", "E1", "E1")) + 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", true) + assert.NoError(t, dvRange.SetSqrefDropList("$E$1:$E$3", true)) err := dvRange.SetSqrefDropList("$E$1:$E$3", false) assert.EqualError(t, err, "cross-sheet sqref cell are not supported") - f.AddDataValidation("Sheet1", dvRange) - if !assert.NoError(t, f.SaveAs(resultFile)) { - t.FailNow() - } + assert.NoError(t, f.AddDataValidation("Sheet1", dvRange)) + assert.NoError(t, f.SaveAs(resultFile)) dvRange = NewDataValidation(true) err = dvRange.SetDropList(make([]string, 258)) @@ -79,13 +71,11 @@ func TestDataValidationError(t *testing.T) { return } assert.EqualError(t, err, "data validation must be 0-255 characters") - dvRange.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorGreaterThan) + assert.NoError(t, dvRange.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorGreaterThan)) dvRange.SetSqref("A9:B10") - f.AddDataValidation("Sheet1", dvRange) - if !assert.NoError(t, f.SaveAs(resultFile)) { - t.FailNow() - } + assert.NoError(t, f.AddDataValidation("Sheet1", dvRange)) + assert.NoError(t, f.SaveAs(resultFile)) // Test width invalid data validation formula. dvRange.Formula1 = strings.Repeat("s", dataValidationFormulaStrLen+22) diff --git a/excelize.go b/excelize.go index 135028c39a..94f401c0a5 100644 --- a/excelize.go +++ b/excelize.go @@ -138,7 +138,7 @@ func (f *File) setDefaultTimeStyle(sheet, axis string, format int) error { } if s == 0 { style, _ := f.NewStyle(`{"number_format": ` + strconv.Itoa(format) + `}`) - f.SetCellStyle(sheet, axis, axis, style) + _ = f.SetCellStyle(sheet, axis, axis, style) } return err } diff --git a/excelize_test.go b/excelize_test.go index 1d6ed24f49..c4b600d580 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -24,12 +24,11 @@ import ( func TestOpenFile(t *testing.T) { // Test update a XLSX file. f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, err) // Test get all the rows in a not exists worksheet. - f.GetRows("Sheet4") + _, err = f.GetRows("Sheet4") + assert.EqualError(t, err, "sheet Sheet4 is not exist") // Test get all the rows in a worksheet. rows, err := f.GetRows("Sheet2") assert.NoError(t, err) @@ -39,10 +38,10 @@ func TestOpenFile(t *testing.T) { } t.Log("\r\n") } - f.UpdateLinkedValue() + assert.NoError(t, f.UpdateLinkedValue()) - f.SetCellDefault("Sheet2", "A1", strconv.FormatFloat(float64(100.1588), 'f', -1, 32)) - f.SetCellDefault("Sheet2", "A1", strconv.FormatFloat(float64(-100.1588), 'f', -1, 64)) + 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))) // Test set cell value with illegal row number. assert.EqualError(t, f.SetCellDefault("Sheet2", "A", strconv.FormatFloat(float64(-100.1588), 'f', -1, 64)), @@ -53,14 +52,14 @@ func TestOpenFile(t *testing.T) { // Test set cell integer value with illegal row number. assert.EqualError(t, f.SetCellInt("Sheet2", "A", 100), `cannot convert cell "A" to coordinates: invalid cell name "A"`) - f.SetCellStr("Sheet2", "C11", "Knowns") + assert.NoError(t, f.SetCellStr("Sheet2", "C11", "Knowns")) // Test max characters in a cell. - f.SetCellStr("Sheet2", "D11", strings.Repeat("c", 32769)) + assert.NoError(t, f.SetCellStr("Sheet2", "D11", strings.Repeat("c", 32769))) 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.") - f.SetCellInt("Sheet3", "A23", 10) - f.SetCellStr("Sheet3", "b230", "10") + 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") // Test set cell string value with illegal row number. @@ -137,21 +136,21 @@ func TestOpenFile(t *testing.T) { {true, "1"}, } for _, test := range booltest { - f.SetCellValue("Sheet2", "F16", test.value) + assert.NoError(t, f.SetCellValue("Sheet2", "F16", test.value)) val, err := f.GetCellValue("Sheet2", "F16") assert.NoError(t, err) assert.Equal(t, test.expected, val) } - f.SetCellValue("Sheet2", "G2", nil) + assert.NoError(t, f.SetCellValue("Sheet2", "G2", nil)) assert.EqualError(t, f.SetCellValue("Sheet2", "G4", time.Now()), "only UTC time expected") - f.SetCellValue("Sheet2", "G4", time.Now().UTC()) + assert.NoError(t, f.SetCellValue("Sheet2", "G4", time.Now().UTC())) // 02:46:40 - f.SetCellValue("Sheet2", "G5", time.Duration(1e13)) + assert.NoError(t, f.SetCellValue("Sheet2", "G5", time.Duration(1e13))) // Test completion column. - f.SetCellValue("Sheet2", "M2", nil) + assert.NoError(t, f.SetCellValue("Sheet2", "M2", nil)) // Test read cell value with given axis large than exists row. _, err = f.GetCellValue("Sheet2", "E231") assert.NoError(t, err) @@ -161,10 +160,10 @@ func TestOpenFile(t *testing.T) { f.GetSheetIndex("Sheet1") // Test get worksheet name of XLSX by given invalid worksheet index. f.GetSheetName(4) - // Test get worksheet map of f. + // Test get worksheet map of workbook. f.GetSheetMap() for i := 1; i <= 300; i++ { - f.SetCellStr("Sheet3", "c"+strconv.Itoa(i), strconv.Itoa(i)) + assert.NoError(t, f.SetCellStr("Sheet2", "c"+strconv.Itoa(i), strconv.Itoa(i))) } assert.NoError(t, f.SaveAs(filepath.Join("test", "TestOpenFile.xlsx"))) } @@ -259,8 +258,8 @@ func TestNewFile(t *testing.T) { f.NewSheet("Sheet1") f.NewSheet("XLSXSheet2") f.NewSheet("XLSXSheet3") - f.SetCellInt("XLSXSheet2", "A23", 56) - f.SetCellStr("Sheet1", "B20", "42") + 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. @@ -310,12 +309,14 @@ func TestSetCellHyperLink(t *testing.T) { assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellHyperLink.xlsx"))) f = NewFile() - f.workSheetReader("Sheet1") + _, err = f.workSheetReader("Sheet1") + assert.NoError(t, err) f.Sheet["xl/worksheets/sheet1.xml"].Hyperlinks = &xlsxHyperlinks{Hyperlink: make([]xlsxHyperlink, 65530)} assert.EqualError(t, f.SetCellHyperLink("Sheet1", "A65531", "https://github.com/360EntSecGroup-Skylar/excelize", "External"), "over maximum limit hyperlinks in a worksheet") f = NewFile() - f.workSheetReader("Sheet1") + _, err = f.workSheetReader("Sheet1") + assert.NoError(t, err) f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:A"}}} err = f.SetCellHyperLink("Sheet1", "A1", "https://github.com/360EntSecGroup-Skylar/excelize", "External") assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`) @@ -341,7 +342,8 @@ func TestGetCellHyperLink(t *testing.T) { t.Log(link, target) f = NewFile() - f.workSheetReader("Sheet1") + _, err = f.workSheetReader("Sheet1") + assert.NoError(t, err) f.Sheet["xl/worksheets/sheet1.xml"].Hyperlinks = &xlsxHyperlinks{ Hyperlink: []xlsxHyperlink{{Ref: "A1"}}, } @@ -364,8 +366,8 @@ func TestSetCellFormula(t *testing.T) { t.FailNow() } - f.SetCellFormula("Sheet1", "B19", "SUM(Sheet2!D2,Sheet2!D11)") - f.SetCellFormula("Sheet1", "C19", "SUM(Sheet2!D2,Sheet2!D9)") + 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. assert.EqualError(t, f.SetCellFormula("Sheet1", "C", "SUM(Sheet2!D2,Sheet2!D9)"), `cannot convert cell "C" to coordinates: invalid cell name "C"`) @@ -377,10 +379,10 @@ func TestSetCellFormula(t *testing.T) { t.FailNow() } // Test remove cell formula. - f.SetCellFormula("Sheet1", "A1", "") + assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "")) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellFormula2.xlsx"))) // Test remove all cell formula. - f.SetCellFormula("Sheet1", "B1", "") + assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "")) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellFormula3.xlsx"))) } @@ -471,31 +473,31 @@ func TestWriteArrayFormula(t *testing.T) { } // Line 2 contains the results of AVERAGEIF - f.SetCellStr("Sheet1", "A2", "Average") + assert.NoError(t, f.SetCellStr("Sheet1", "A2", "Average")) // Line 3 contains the average that was calculated in Go - f.SetCellStr("Sheet1", "A3", "Average (calculated)") + assert.NoError(t, f.SetCellStr("Sheet1", "A3", "Average (calculated)")) // Line 4 contains the results of the array function that calculates the standard deviation - f.SetCellStr("Sheet1", "A4", "Std. deviation") + assert.NoError(t, f.SetCellStr("Sheet1", "A4", "Std. deviation")) // Line 5 contains the standard deviations calculated in Go - f.SetCellStr("Sheet1", "A5", "Std. deviation (calculated)") + assert.NoError(t, f.SetCellStr("Sheet1", "A5", "Std. deviation (calculated)")) - f.SetCellStr("Sheet1", "B1", sample[0]) - f.SetCellStr("Sheet1", "C1", sample[1]) - f.SetCellStr("Sheet1", "D1", sample[2]) + assert.NoError(t, f.SetCellStr("Sheet1", "B1", sample[0])) + assert.NoError(t, f.SetCellStr("Sheet1", "C1", sample[1])) + assert.NoError(t, f.SetCellStr("Sheet1", "D1", sample[2])) firstResLine := 8 - f.SetCellStr("Sheet1", cell(1, firstResLine-1), "Result Values") - f.SetCellStr("Sheet1", cell(2, firstResLine-1), "Sample") + assert.NoError(t, f.SetCellStr("Sheet1", cell(1, firstResLine-1), "Result Values")) + assert.NoError(t, f.SetCellStr("Sheet1", cell(2, firstResLine-1), "Sample")) for i := 0; i != len(values); i++ { valCell := cell(1, i+firstResLine) assocCell := cell(2, i+firstResLine) - f.SetCellInt("Sheet1", valCell, values[i]) - f.SetCellStr("Sheet1", assocCell, sample[assoc[i]]) + assert.NoError(t, f.SetCellInt("Sheet1", valCell, values[i])) + assert.NoError(t, f.SetCellStr("Sheet1", assocCell, sample[assoc[i]])) } valRange := fmt.Sprintf("$A$%d:$A$%d", firstResLine, len(values)+firstResLine-1) @@ -508,11 +510,11 @@ func TestWriteArrayFormula(t *testing.T) { stdevCell := cell(i+2, 4) calcStdevCell := cell(i+2, 5) - f.SetCellInt("Sheet1", calcAvgCell, average(i)) - f.SetCellInt("Sheet1", calcStdevCell, stdev(i)) + assert.NoError(t, f.SetCellInt("Sheet1", calcAvgCell, average(i))) + assert.NoError(t, f.SetCellInt("Sheet1", calcStdevCell, stdev(i))) // Average can be done with AVERAGEIF - f.SetCellFormula("Sheet1", avgCell, fmt.Sprintf("ROUND(AVERAGEIF(%s,%s,%s),0)", assocRange, nameCell, valRange)) + assert.NoError(t, f.SetCellFormula("Sheet1", avgCell, fmt.Sprintf("ROUND(AVERAGEIF(%s,%s,%s),0)", assocRange, nameCell, valRange))) ref := stdevCell + ":" + stdevCell t := STCellFormulaTypeArray @@ -623,9 +625,9 @@ func TestSetCellStyleNumberFormat(t *testing.T) { var val float64 val, err = strconv.ParseFloat(v, 64) if err != nil { - f.SetCellValue("Sheet2", c, v) + assert.NoError(t, f.SetCellValue("Sheet2", c, v)) } else { - f.SetCellValue("Sheet2", c, val) + 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) + `}`) if !assert.NoError(t, err) { @@ -652,8 +654,8 @@ func TestSetCellStyleCurrencyNumberFormat(t *testing.T) { t.FailNow() } - f.SetCellValue("Sheet1", "A1", 56) - f.SetCellValue("Sheet1", "A2", -32.3) + 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) { @@ -676,8 +678,8 @@ func TestSetCellStyleCurrencyNumberFormat(t *testing.T) { if !assert.NoError(t, err) { t.FailNow() } - f.SetCellValue("Sheet1", "A1", 42920.5) - f.SetCellValue("Sheet1", "A2", 42920.5) + 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) { @@ -709,8 +711,8 @@ func TestSetCellStyleCurrencyNumberFormat(t *testing.T) { func TestSetCellStyleCustomNumberFormat(t *testing.T) { f := NewFile() - f.SetCellValue("Sheet1", "A1", 42920.5) - f.SetCellValue("Sheet1", "A2", 42920.5) + 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;@"}`) if err != nil { t.Log(err) @@ -841,8 +843,8 @@ func TestSetDeleteSheet(t *testing.T) { t.FailNow() } f.DeleteSheet("Sheet1") - f.AddComment("Sheet1", "A1", "") - f.AddComment("Sheet1", "A1", `{"author":"Excelize: ","text":"This is a comment."}`) + 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.SaveAs(filepath.Join("test", "TestSetDeleteSheet.TestBook4.xlsx"))) }) } @@ -853,10 +855,10 @@ func TestSheetVisibility(t *testing.T) { t.FailNow() } - f.SetSheetVisible("Sheet2", false) - f.SetSheetVisible("Sheet1", false) - f.SetSheetVisible("Sheet1", true) - f.GetSheetVisible("Sheet1") + 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")) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSheetVisibility.xlsx"))) } @@ -870,7 +872,7 @@ func TestCopySheet(t *testing.T) { idx := f.NewSheet("CopySheet") assert.NoError(t, f.CopySheet(1, idx)) - f.SetCellValue("CopySheet", "F1", "Hello") + assert.NoError(t, f.SetCellValue("CopySheet", "F1", "Hello")) val, err := f.GetCellValue("Sheet1", "F1") assert.NoError(t, err) assert.NotEqual(t, "Hello", val) @@ -1072,31 +1074,31 @@ func TestConditionalFormat(t *testing.T) { } // 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"}]`) + 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. - 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", `[{"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... - 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", fmt.Sprintf(`[{"type":"cell","criteria":"between","format":%d,"minimum":"6","maximum":"8"}]`, format1))) // Hightlight cells rules: Greater Than... - f.SetConditionalFormat(sheet1, "D1:D10", fmt.Sprintf(`[{"type":"cell","criteria":">","format":%d,"value":"6"}]`, format3)) + assert.NoError(t, f.SetConditionalFormat(sheet1, "D1:D10", fmt.Sprintf(`[{"type":"cell","criteria":">","format":%d,"value":"6"}]`, format3))) // Hightlight cells rules: Equal To... - f.SetConditionalFormat(sheet1, "E1:E10", fmt.Sprintf(`[{"type":"top","criteria":"=","format":%d}]`, format3)) + assert.NoError(t, f.SetConditionalFormat(sheet1, "E1:E10", fmt.Sprintf(`[{"type":"top","criteria":"=","format":%d}]`, format3))) // Hightlight cells rules: Not Equal To... - f.SetConditionalFormat(sheet1, "F1:F10", fmt.Sprintf(`[{"type":"unique","criteria":"=","format":%d}]`, format2)) + assert.NoError(t, f.SetConditionalFormat(sheet1, "F1:F10", fmt.Sprintf(`[{"type":"unique","criteria":"=","format":%d}]`, format2))) // Hightlight cells rules: Duplicate Values... - f.SetConditionalFormat(sheet1, "G1:G10", fmt.Sprintf(`[{"type":"duplicate","criteria":"=","format":%d}]`, format2)) + assert.NoError(t, f.SetConditionalFormat(sheet1, "G1:G10", fmt.Sprintf(`[{"type":"duplicate","criteria":"=","format":%d}]`, format2))) // Top/Bottom rules: Top 10%. - 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", fmt.Sprintf(`[{"type":"top","criteria":"=","format":%d,"value":"6","percent":true}]`, format1))) // Top/Bottom rules: Above Average... - f.SetConditionalFormat(sheet1, "I1:I10", fmt.Sprintf(`[{"type":"average","criteria":"=","format":%d, "above_average": true}]`, format3)) + assert.NoError(t, f.SetConditionalFormat(sheet1, "I1:I10", fmt.Sprintf(`[{"type":"average","criteria":"=","format":%d, "above_average": true}]`, format3))) // Top/Bottom rules: Below Average... - f.SetConditionalFormat(sheet1, "J1:J10", fmt.Sprintf(`[{"type":"average","criteria":"=","format":%d, "above_average": false}]`, format1)) + assert.NoError(t, f.SetConditionalFormat(sheet1, "J1:J10", fmt.Sprintf(`[{"type":"average","criteria":"=","format":%d, "above_average": false}]`, format1))) // Data Bars: Gradient Fill. - 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", `[{"type":"data_bar", "criteria":"=", "min_type":"min","max_type":"max","bar_color":"#638EC6"}]`)) // Use a formula to determine which cells to format. - f.SetConditionalFormat(sheet1, "L1:L10", fmt.Sprintf(`[{"type":"formula", "criteria":"L2<3", "format":%d}]`, format1)) + assert.NoError(t, f.SetConditionalFormat(sheet1, "L1:L10", fmt.Sprintf(`[{"type":"formula", "criteria":"L2<3", "format":%d}]`, format1))) // Test set invalid format set in conditional format - f.SetConditionalFormat(sheet1, "L1:L10", "") + assert.EqualError(t, f.SetConditionalFormat(sheet1, "L1:L10", ""), "unexpected end of JSON input") err = f.SaveAs(filepath.Join("test", "TestConditionalFormat.xlsx")) if !assert.NoError(t, err) { @@ -1104,9 +1106,9 @@ func TestConditionalFormat(t *testing.T) { } // Set conditional format with illegal valid type. - f.SetConditionalFormat(sheet1, "K1:K10", `[{"type":"", "criteria":"=", "min_type":"min","max_type":"max","bar_color":"#638EC6"}]`) + 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. - 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", `[{"type":"data_bar", "criteria":"", "min_type":"min","max_type":"max","bar_color":"#638EC6"}]`)) // Set conditional format with file without dxfs element shold not return error. f, err = OpenFile(filepath.Join("test", "Book1.xlsx")) @@ -1156,7 +1158,7 @@ func TestSetSheetRow(t *testing.T) { t.FailNow() } - f.SetSheetRow("Sheet1", "B27", &[]interface{}{"cell", nil, int32(42), float64(42), time.Now().UTC()}) + assert.NoError(t, f.SetSheetRow("Sheet1", "B27", &[]interface{}{"cell", nil, int32(42), float64(42), time.Now().UTC()})) assert.EqualError(t, f.SetSheetRow("Sheet1", "", &[]interface{}{"cell", nil, 2}), `cannot convert cell "" to coordinates: invalid cell name ""`) @@ -1193,11 +1195,11 @@ func TestHSL(t *testing.T) { func TestProtectSheet(t *testing.T) { f := NewFile() - f.ProtectSheet("Sheet1", nil) - f.ProtectSheet("Sheet1", &FormatSheetProtection{ + assert.NoError(t, f.ProtectSheet("Sheet1", nil)) + assert.NoError(t, f.ProtectSheet("Sheet1", &FormatSheetProtection{ Password: "password", EditScenarios: false, - }) + })) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestProtectSheet.xlsx"))) // Test protect not exists worksheet. @@ -1212,7 +1214,7 @@ func TestUnprotectSheet(t *testing.T) { // Test unprotect not exists worksheet. assert.EqualError(t, f.UnprotectSheet("SheetN"), "sheet SheetN is not exist") - f.UnprotectSheet("Sheet1") + assert.NoError(t, f.UnprotectSheet("Sheet1")) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestUnprotectSheet.xlsx"))) } @@ -1325,8 +1327,12 @@ func prepareTestBook3() (*File, error) { f.NewSheet("Sheet1") f.NewSheet("XLSXSheet2") f.NewSheet("XLSXSheet3") - f.SetCellInt("XLSXSheet2", "A23", 56) - f.SetCellStr("Sheet1", "B20", "42") + if err := f.SetCellInt("XLSXSheet2", "A23", 56); err != nil { + return nil, err + } + if err := f.SetCellStr("Sheet1", "B20", "42"); err != nil { + return nil, err + } f.SetActiveSheet(0) err := f.AddPicture("Sheet1", "H2", filepath.Join("test", "images", "excel.gif"), @@ -1345,10 +1351,18 @@ func prepareTestBook3() (*File, error) { func prepareTestBook4() (*File, error) { f := NewFile() - f.SetColWidth("Sheet1", "B", "A", 12) - f.SetColWidth("Sheet1", "A", "B", 12) - f.GetColWidth("Sheet1", "A") - f.GetColWidth("Sheet1", "C") + if err := f.SetColWidth("Sheet1", "B", "A", 12); err != nil { + return f, err + } + if err := f.SetColWidth("Sheet1", "A", "B", 12); err != nil { + return f, err + } + if _, err := f.GetColWidth("Sheet1", "A"); err != nil { + return f, err + } + if _, err := f.GetColWidth("Sheet1", "C"); err != nil { + return f, err + } return f, nil } @@ -1357,13 +1371,17 @@ func fillCells(f *File, sheet string, colCount, rowCount int) { for col := 1; col <= colCount; col++ { for row := 1; row <= rowCount; row++ { cell, _ := CoordinatesToCellName(col, row) - f.SetCellStr(sheet, cell, cell) + if err := f.SetCellStr(sheet, cell, cell); err != nil { + panic(err) + } } } } func BenchmarkOpenFile(b *testing.B) { for i := 0; i < b.N; i++ { - OpenFile(filepath.Join("test", "Book1.xlsx")) + if _, err := OpenFile(filepath.Join("test", "Book1.xlsx")); err != nil { + b.Error(err) + } } } diff --git a/file_test.go b/file_test.go index 6c30f4acf2..97ff72006b 100644 --- a/file_test.go +++ b/file_test.go @@ -14,7 +14,9 @@ func BenchmarkWrite(b *testing.B) { if err != nil { panic(err) } - f.SetCellDefault("Sheet1", val, s) + if err := f.SetCellDefault("Sheet1", val, s); err != nil { + panic(err) + } } } // Save xlsx file by the given path. diff --git a/picture_test.go b/picture_test.go index 6af39044a6..ca38f412a1 100644 --- a/picture_test.go +++ b/picture_test.go @@ -25,7 +25,9 @@ func BenchmarkAddPictureFromBytes(b *testing.B) { } b.ResetTimer() for i := 1; i <= b.N; i++ { - f.AddPictureFromBytes("Sheet1", fmt.Sprint("A", i), "", "excel", ".png", imgFile) + if err := f.AddPictureFromBytes("Sheet1", fmt.Sprint("A", i), "", "excel", ".png", imgFile); err != nil { + b.Error(err) + } } } @@ -36,23 +38,14 @@ func TestAddPicture(t *testing.T) { } // Test add picture to worksheet with offset and location hyperlink. - err = f.AddPicture("Sheet2", "I9", filepath.Join("test", "images", "excel.jpg"), - `{"x_offset": 140, "y_offset": 120, "hyperlink": "#Sheet2!D8", "hyperlink_type": "Location"}`) - if !assert.NoError(t, err) { - t.FailNow() - } - + 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. - err = f.AddPicture("Sheet1", "F21", filepath.Join("test", "images", "excel.jpg"), - `{"x_offset": 10, "y_offset": 10, "hyperlink": "https://github.com/360EntSecGroup-Skylar/excelize", "hyperlink_type": "External", "positioning": "oneCell"}`) - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, f.AddPicture("Sheet1", "F21", filepath.Join("test", "images", "excel.jpg"), + `{"x_offset": 10, "y_offset": 10, "hyperlink": "https://github.com/360EntSecGroup-Skylar/excelize", "hyperlink_type": "External", "positioning": "oneCell"}`)) file, err := ioutil.ReadFile(filepath.Join("test", "images", "excel.png")) - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, err) // Test add picture to worksheet from bytes. assert.NoError(t, f.AddPictureFromBytes("Sheet1", "Q1", "", "Excel Logo", ".png", file)) @@ -69,9 +62,7 @@ func TestAddPicture(t *testing.T) { func TestAddPictureErrors(t *testing.T) { xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, err) // Test add picture to worksheet with invalid file path. err = xlsx.AddPicture("Sheet1", "G21", filepath.Join("test", "not_exists_dir", "not_exists.icon"), "") @@ -127,14 +118,10 @@ func TestGetPicture(t *testing.T) { f.deleteSheetRelationships("", "") // Try to get picture from a local storage file. - if !assert.NoError(t, f.SaveAs(filepath.Join("test", "TestGetPicture.xlsx"))) { - t.FailNow() - } + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestGetPicture.xlsx"))) f, err = OpenFile(filepath.Join("test", "TestGetPicture.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, err) file, raw, err = f.GetPicture("Sheet1", "F21") assert.NoError(t, err) diff --git a/pivotTable_test.go b/pivotTable_test.go index 9bf08e8197..5d841d8976 100644 --- a/pivotTable_test.go +++ b/pivotTable_test.go @@ -16,13 +16,13 @@ func TestAddPivotTable(t *testing.T) { 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"}) + assert.NoError(t, 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)]) + 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)])) } assert.NoError(t, f.AddPivotTable(&PivotTableOption{ DataRange: "Sheet1!$A$1:$E$31", diff --git a/rows_test.go b/rows_test.go index 64942425da..fc9d866000 100644 --- a/rows_test.go +++ b/rows_test.go @@ -69,7 +69,7 @@ func TestRowsIterator(t *testing.T) { f = NewFile() cells := []string{"C1", "E1", "A3", "B3", "C3", "D3", "E3"} for _, cell := range cells { - f.SetCellValue("Sheet1", cell, 1) + assert.NoError(t, f.SetCellValue("Sheet1", cell, 1)) } rows, err = f.Rows("Sheet1") require.NoError(t, err) @@ -169,8 +169,12 @@ func TestRowVisibility(t *testing.T) { f.NewSheet("Sheet3") assert.NoError(t, f.SetRowVisible("Sheet3", 2, false)) assert.NoError(t, f.SetRowVisible("Sheet3", 2, true)) - f.GetRowVisible("Sheet3", 2) - f.GetRowVisible("Sheet3", 25) + visiable, err := f.GetRowVisible("Sheet3", 2) + assert.Equal(t, true, visiable) + assert.NoError(t, err) + visiable, err = f.GetRowVisible("Sheet3", 25) + assert.Equal(t, false, visiable) + assert.NoError(t, err) assert.EqualError(t, f.SetRowVisible("Sheet3", 0, true), "invalid row number 0") assert.EqualError(t, f.SetRowVisible("SheetN", 2, false), "sheet SheetN is not exist") @@ -194,7 +198,7 @@ func TestRemoveRow(t *testing.T) { ) fillCells(f, sheet1, colCount, rowCount) - f.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External") + assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External")) assert.EqualError(t, f.RemoveRow(sheet1, -1), "invalid row number -1") @@ -205,7 +209,7 @@ func TestRemoveRow(t *testing.T) { t.FailNow() } - f.MergeCell(sheet1, "B3", "B5") + assert.NoError(t, f.MergeCell(sheet1, "B3", "B5")) assert.NoError(t, f.RemoveRow(sheet1, 2)) if !assert.Len(t, r.SheetData.Row, rowCount-2) { @@ -255,7 +259,7 @@ func TestInsertRow(t *testing.T) { ) fillCells(xlsx, sheet1, colCount, rowCount) - xlsx.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External") + assert.NoError(t, xlsx.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External")) assert.EqualError(t, xlsx.InsertRow(sheet1, -1), "invalid row number -1") @@ -305,8 +309,8 @@ func TestDuplicateRowFromSingleRow(t *testing.T) { t.Run("FromSingleRow", func(t *testing.T) { xlsx := NewFile() - xlsx.SetCellStr(sheet, "A1", cells["A1"]) - xlsx.SetCellStr(sheet, "B1", cells["B1"]) + assert.NoError(t, xlsx.SetCellStr(sheet, "A1", cells["A1"])) + assert.NoError(t, xlsx.SetCellStr(sheet, "B1", cells["B1"])) assert.NoError(t, xlsx.DuplicateRow(sheet, 1)) if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.FromSingleRow_1"))) { @@ -358,13 +362,13 @@ func TestDuplicateRowUpdateDuplicatedRows(t *testing.T) { t.Run("UpdateDuplicatedRows", func(t *testing.T) { xlsx := NewFile() - xlsx.SetCellStr(sheet, "A1", cells["A1"]) - xlsx.SetCellStr(sheet, "B1", cells["B1"]) + assert.NoError(t, xlsx.SetCellStr(sheet, "A1", cells["A1"])) + assert.NoError(t, xlsx.SetCellStr(sheet, "B1", cells["B1"])) assert.NoError(t, xlsx.DuplicateRow(sheet, 1)) - xlsx.SetCellStr(sheet, "A2", cells["A2"]) - xlsx.SetCellStr(sheet, "B2", cells["B2"]) + assert.NoError(t, xlsx.SetCellStr(sheet, "A2", cells["A2"])) + assert.NoError(t, xlsx.SetCellStr(sheet, "B2", cells["B2"])) if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.UpdateDuplicatedRows"))) { t.FailNow() @@ -399,8 +403,7 @@ func TestDuplicateRowFirstOfMultipleRows(t *testing.T) { newFileWithDefaults := func() *File { f := NewFile() for cell, val := range cells { - f.SetCellStr(sheet, cell, val) - + assert.NoError(t, f.SetCellStr(sheet, cell, val)) } return f } @@ -514,8 +517,7 @@ func TestDuplicateRowWithLargeOffsetToMiddleOfData(t *testing.T) { newFileWithDefaults := func() *File { f := NewFile() for cell, val := range cells { - f.SetCellStr(sheet, cell, val) - + assert.NoError(t, f.SetCellStr(sheet, cell, val)) } return f } @@ -560,8 +562,7 @@ func TestDuplicateRowWithLargeOffsetToEmptyRows(t *testing.T) { newFileWithDefaults := func() *File { f := NewFile() for cell, val := range cells { - f.SetCellStr(sheet, cell, val) - + assert.NoError(t, f.SetCellStr(sheet, cell, val)) } return f } @@ -606,8 +607,7 @@ func TestDuplicateRowInsertBefore(t *testing.T) { newFileWithDefaults := func() *File { f := NewFile() for cell, val := range cells { - f.SetCellStr(sheet, cell, val) - + assert.NoError(t, f.SetCellStr(sheet, cell, val)) } return f } @@ -653,8 +653,7 @@ func TestDuplicateRowInsertBeforeWithLargeOffset(t *testing.T) { newFileWithDefaults := func() *File { f := NewFile() for cell, val := range cells { - f.SetCellStr(sheet, cell, val) - + assert.NoError(t, f.SetCellStr(sheet, cell, val)) } return f } @@ -704,7 +703,7 @@ func TestDuplicateRowInvalidRownum(t *testing.T) { t.Run(name, func(t *testing.T) { xlsx := NewFile() for col, val := range cells { - xlsx.SetCellStr(sheet, col, val) + assert.NoError(t, xlsx.SetCellStr(sheet, col, val)) } assert.EqualError(t, xlsx.DuplicateRow(sheet, row), fmt.Sprintf("invalid row number %d", row)) @@ -726,7 +725,7 @@ func TestDuplicateRowInvalidRownum(t *testing.T) { t.Run(name, func(t *testing.T) { xlsx := NewFile() for col, val := range cells { - xlsx.SetCellStr(sheet, col, val) + assert.NoError(t, xlsx.SetCellStr(sheet, col, val)) } assert.EqualError(t, xlsx.DuplicateRowTo(sheet, row1, row2), fmt.Sprintf("invalid row number %d", row1)) diff --git a/sheet_test.go b/sheet_test.go index aada60a81d..7a582486ae 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -192,7 +192,7 @@ func TestGetPageLayout(t *testing.T) { func TestSetHeaderFooter(t *testing.T) { f := excelize.NewFile() - f.SetCellStr("Sheet1", "A1", "Test SetHeaderFooter") + 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") // Test set header and footer with illegal setting. diff --git a/sparkline_test.go b/sparkline_test.go index a5cb2161cc..dca32e9c16 100644 --- a/sparkline_test.go +++ b/sparkline_test.go @@ -17,40 +17,40 @@ func TestAddSparkline(t *testing.T) { assert.NoError(t, f.SetCellStyle("Sheet1", "A1", "B1", style)) assert.NoError(t, f.SetSheetViewOptions("Sheet1", 0, ZoomScale(150))) - f.SetColWidth("Sheet1", "A", "A", 14) - f.SetColWidth("Sheet1", "B", "B", 50) + assert.NoError(t, f.SetColWidth("Sheet1", "A", "A", 14)) + assert.NoError(t, f.SetColWidth("Sheet1", "B", "B", 50)) // Headings - f.SetCellValue("Sheet1", "A1", "Sparkline") - f.SetCellValue("Sheet1", "B1", "Description") + assert.NoError(t, f.SetCellValue("Sheet1", "A1", "Sparkline")) + assert.NoError(t, f.SetCellValue("Sheet1", "B1", "Description")) - f.SetCellValue("Sheet1", "B2", `A default "line" sparkline.`) + assert.NoError(t, f.SetCellValue("Sheet1", "B2", `A default "line" sparkline.`)) assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ Location: []string{"A2"}, Range: []string{"Sheet3!A1:J1"}, })) - f.SetCellValue("Sheet1", "B3", `A default "column" sparkline.`) + assert.NoError(t, f.SetCellValue("Sheet1", "B3", `A default "column" sparkline.`)) assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ Location: []string{"A3"}, Range: []string{"Sheet3!A2:J2"}, Type: "column", })) - f.SetCellValue("Sheet1", "B4", `A default "win/loss" sparkline.`) + assert.NoError(t, f.SetCellValue("Sheet1", "B4", `A default "win/loss" sparkline.`)) assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ Location: []string{"A4"}, Range: []string{"Sheet3!A3:J3"}, Type: "win_loss", })) - f.SetCellValue("Sheet1", "B6", "Line with markers.") + assert.NoError(t, f.SetCellValue("Sheet1", "B6", "Line with markers.")) assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ Location: []string{"A6"}, Range: []string{"Sheet3!A1:J1"}, Markers: true, })) - f.SetCellValue("Sheet1", "B7", "Line with high and low points.") + assert.NoError(t, f.SetCellValue("Sheet1", "B7", "Line with high and low points.")) assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ Location: []string{"A7"}, Range: []string{"Sheet3!A1:J1"}, @@ -58,7 +58,7 @@ func TestAddSparkline(t *testing.T) { Low: true, })) - f.SetCellValue("Sheet1", "B8", "Line with first and last point markers.") + assert.NoError(t, f.SetCellValue("Sheet1", "B8", "Line with first and last point markers.")) assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ Location: []string{"A8"}, Range: []string{"Sheet3!A1:J1"}, @@ -66,28 +66,28 @@ func TestAddSparkline(t *testing.T) { Last: true, })) - f.SetCellValue("Sheet1", "B9", "Line with negative point markers.") + assert.NoError(t, f.SetCellValue("Sheet1", "B9", "Line with negative point markers.")) assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ Location: []string{"A9"}, Range: []string{"Sheet3!A1:J1"}, Negative: true, })) - f.SetCellValue("Sheet1", "B10", "Line with axis.") + assert.NoError(t, f.SetCellValue("Sheet1", "B10", "Line with axis.")) assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ Location: []string{"A10"}, Range: []string{"Sheet3!A1:J1"}, Axis: true, })) - f.SetCellValue("Sheet1", "B12", "Column with default style (1).") + assert.NoError(t, f.SetCellValue("Sheet1", "B12", "Column with default style (1).")) assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ Location: []string{"A12"}, Range: []string{"Sheet3!A2:J2"}, Type: "column", })) - f.SetCellValue("Sheet1", "B13", "Column with style 2.") + assert.NoError(t, f.SetCellValue("Sheet1", "B13", "Column with style 2.")) assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ Location: []string{"A13"}, Range: []string{"Sheet3!A2:J2"}, @@ -95,7 +95,7 @@ func TestAddSparkline(t *testing.T) { Style: 2, })) - f.SetCellValue("Sheet1", "B14", "Column with style 3.") + assert.NoError(t, f.SetCellValue("Sheet1", "B14", "Column with style 3.")) assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ Location: []string{"A14"}, Range: []string{"Sheet3!A2:J2"}, @@ -103,7 +103,7 @@ func TestAddSparkline(t *testing.T) { Style: 3, })) - f.SetCellValue("Sheet1", "B15", "Column with style 4.") + assert.NoError(t, f.SetCellValue("Sheet1", "B15", "Column with style 4.")) assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ Location: []string{"A15"}, Range: []string{"Sheet3!A2:J2"}, @@ -111,7 +111,7 @@ func TestAddSparkline(t *testing.T) { Style: 4, })) - f.SetCellValue("Sheet1", "B16", "Column with style 5.") + assert.NoError(t, f.SetCellValue("Sheet1", "B16", "Column with style 5.")) assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ Location: []string{"A16"}, Range: []string{"Sheet3!A2:J2"}, @@ -119,7 +119,7 @@ func TestAddSparkline(t *testing.T) { Style: 5, })) - f.SetCellValue("Sheet1", "B17", "Column with style 6.") + assert.NoError(t, f.SetCellValue("Sheet1", "B17", "Column with style 6.")) assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ Location: []string{"A17"}, Range: []string{"Sheet3!A2:J2"}, @@ -127,7 +127,7 @@ func TestAddSparkline(t *testing.T) { Style: 6, })) - f.SetCellValue("Sheet1", "B18", "Column with a user defined color.") + assert.NoError(t, f.SetCellValue("Sheet1", "B18", "Column with a user defined color.")) assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ Location: []string{"A18"}, Range: []string{"Sheet3!A2:J2"}, @@ -135,14 +135,14 @@ func TestAddSparkline(t *testing.T) { SeriesColor: "#E965E0", })) - f.SetCellValue("Sheet1", "B20", "A win/loss sparkline.") + assert.NoError(t, f.SetCellValue("Sheet1", "B20", "A win/loss sparkline.")) assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ Location: []string{"A20"}, Range: []string{"Sheet3!A3:J3"}, Type: "win_loss", })) - f.SetCellValue("Sheet1", "B21", "A win/loss sparkline with negative points highlighted.") + assert.NoError(t, f.SetCellValue("Sheet1", "B21", "A win/loss sparkline with negative points highlighted.")) assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ Location: []string{"A21"}, Range: []string{"Sheet3!A3:J3"}, @@ -150,7 +150,7 @@ func TestAddSparkline(t *testing.T) { Negative: true, })) - f.SetCellValue("Sheet1", "B23", "A left to right column (the default).") + assert.NoError(t, f.SetCellValue("Sheet1", "B23", "A left to right column (the default).")) assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ Location: []string{"A23"}, Range: []string{"Sheet3!A4:J4"}, @@ -158,7 +158,7 @@ func TestAddSparkline(t *testing.T) { Style: 20, })) - f.SetCellValue("Sheet1", "B24", "A right to left column.") + assert.NoError(t, f.SetCellValue("Sheet1", "B24", "A right to left column.")) assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ Location: []string{"A24"}, Range: []string{"Sheet3!A4:J4"}, @@ -167,16 +167,16 @@ func TestAddSparkline(t *testing.T) { Reverse: true, })) - f.SetCellValue("Sheet1", "B25", "Sparkline and text in one cell.") + assert.NoError(t, f.SetCellValue("Sheet1", "B25", "Sparkline and text in one cell.")) assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ Location: []string{"A25"}, Range: []string{"Sheet3!A4:J4"}, Type: "column", Style: 20, })) - f.SetCellValue("Sheet1", "A25", "Growth") + assert.NoError(t, f.SetCellValue("Sheet1", "A25", "Growth")) - f.SetCellValue("Sheet1", "B27", "A grouped sparkline. Changes are applied to all three.") + assert.NoError(t, f.SetCellValue("Sheet1", "B27", "A grouped sparkline. Changes are applied to all three.")) assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOption{ Location: []string{"A27", "A28", "A29"}, Range: []string{"Sheet3!A5:J5", "Sheet3!A6:J6", "Sheet3!A7:J7"}, @@ -297,10 +297,14 @@ func prepareSparklineDataset() *File { f.NewSheet("Sheet2") f.NewSheet("Sheet3") for row, data := range sheet2 { - f.SetSheetRow("Sheet2", fmt.Sprintf("A%d", row+1), &data) + if err := f.SetSheetRow("Sheet2", fmt.Sprintf("A%d", row+1), &data); err != nil { + panic(err) + } } for row, data := range sheet3 { - f.SetSheetRow("Sheet3", fmt.Sprintf("A%d", row+1), &data) + if err := f.SetSheetRow("Sheet3", fmt.Sprintf("A%d", row+1), &data); err != nil { + panic(err) + } } return f } diff --git a/stream.go b/stream.go index 5e74e8ee5d..1b1bbe3e80 100644 --- a/stream.go +++ b/stream.go @@ -167,7 +167,7 @@ func (sw *StreamWriter) Flush() error { sheetDataByte = append(sheetDataByte, sw.SheetData.Bytes()...) replaceMap := map[string][]byte{ - "XMLName": []byte{}, + "XMLName": {}, "SheetData": sheetDataByte, } sw.SheetData.Reset() diff --git a/table.go b/table.go index c5a704c66e..3d76690305 100644 --- a/table.go +++ b/table.go @@ -139,7 +139,7 @@ func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, formatSet } name, _ := f.GetCellValue(sheet, cell) if _, err := strconv.Atoi(name); err == nil { - f.SetCellStr(sheet, cell, name) + _ = f.SetCellStr(sheet, cell, name) } if name == "" { name = "Column" + strconv.Itoa(idx) From 5f3a4bc39f9cf2987104ffe57242a0526cdd9158 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 25 Dec 2019 00:00:50 +0800 Subject: [PATCH 182/957] Fix #538, added setting a major unit and tick label skip support for the chart --- cell.go | 3 ++- chart.go | 26 +++++++++++++++++++++++++- chart_test.go | 2 +- xmlChart.go | 7 ++++++- 4 files changed, 34 insertions(+), 4 deletions(-) diff --git a/cell.go b/cell.go index e59a659b2f..da091eef2b 100644 --- a/cell.go +++ b/cell.go @@ -44,7 +44,8 @@ func (f *File) GetCellValue(sheet, axis string) (string, error) { }) } -// SetCellValue provides a function to set value of a cell. The following +// SetCellValue provides a function to set value of a cell. The specified +// coordinates should not be in the first row of the table. The following // shows the supported data types: // // int diff --git a/chart.go b/chart.go index 5a42c5bb81..b1446fb748 100644 --- a/chart.go +++ b/chart.go @@ -653,10 +653,21 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // // show_val: Specifies that the value shall be shown in a data label. The show_val property is optional. The default value is false. // -// Set the primary horizontal and vertical axis options by x_axis and y_axis. The properties that can be set are: +// Set the primary horizontal and vertical axis options by x_axis and y_axis. The properties of x_axis that can be set are: // // major_grid_lines // minor_grid_lines +// major_unit +// reverse_order +// maximum +// minimum +// +// The properties of y_axis that can be set are: +// +// major_grid_lines +// minor_grid_lines +// major_unit +// tick_label_skip // reverse_order // maximum // minimum @@ -665,6 +676,10 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // // minor_grid_lines: Specifies minor gridlines. // +// 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. +// +// 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. +// // 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. // // maximum: Specifies that the fixed maximum, 0 is auto. The maximum property is optional. The default value is auto. @@ -1612,6 +1627,12 @@ func (f *File) drawPlotAreaCatAx(formatSet *formatChart) []*cAxs { if formatSet.XAxis.MinorGridlines { axs[0].MinorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()} } + if formatSet.XAxis.MajorUnit != 0 { + axs[0].MajorUnit = &attrValFloat{Val: float64Ptr(formatSet.XAxis.MajorUnit)} + } + if formatSet.XAxis.TickLabelSkip != 0 { + axs[0].TickLblSkip = &attrValInt{Val: intPtr(formatSet.XAxis.TickLabelSkip)} + } return axs } @@ -1658,6 +1679,9 @@ func (f *File) drawPlotAreaValAx(formatSet *formatChart) []*cAxs { if pos, ok := valTickLblPos[formatSet.Type]; ok { axs[0].TickLblPos.Val = stringPtr(pos) } + if formatSet.YAxis.MajorUnit != 0 { + axs[0].MajorUnit = &attrValFloat{Val: float64Ptr(formatSet.YAxis.MajorUnit)} + } return axs } diff --git a/chart_test.go b/chart_test.go index 2379ddc0e9..5d0f2d1561 100644 --- a/chart_test.go +++ b/chart_test.go @@ -137,7 +137,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"}`)) - 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"},{"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},"y_axis":{"major_grid_lines":true,"minor_grid_lines":true}}`)) + 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"},{"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,"major_unit":1,"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/xmlChart.go b/xmlChart.go index 84c1a3b09c..a28d2a70fe 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -357,9 +357,13 @@ type cAxs struct { CrossAx *attrValInt `xml:"crossAx"` Crosses *attrValString `xml:"crosses"` CrossBetween *attrValString `xml:"crossBetween"` + MajorUnit *attrValFloat `xml:"majorUnit"` + MinorUnit *attrValFloat `xml:"minorUnit"` Auto *attrValBool `xml:"auto"` LblAlgn *attrValString `xml:"lblAlgn"` LblOffset *attrValInt `xml:"lblOffset"` + TickLblSkip *attrValInt `xml:"tickLblSkip"` + TickMarkSkip *attrValInt `xml:"tickMarkSkip"` NoMultiLvlLbl *attrValBool `xml:"noMultiLvlLbl"` } @@ -519,8 +523,9 @@ type formatChartAxis struct { MajorTickMark string `json:"major_tick_mark"` MinorTickMark string `json:"minor_tick_mark"` MinorUnitType string `json:"minor_unit_type"` - MajorUnit int `json:"major_unit"` + 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"` From 8b960ee1e624bd2776a351a4a3b2ad04c29bae9a Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 28 Dec 2019 15:05:44 +0800 Subject: [PATCH 183/957] Fix #547 and #546, add default overlay element for the chart --- chart.go | 5 +---- chart_test.go | 2 +- xmlChart.go | 10 +++++----- xmlWorksheet.go | 2 +- 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/chart.go b/chart.go index b1446fb748..8b38d22aa1 100644 --- a/chart.go +++ b/chart.go @@ -657,7 +657,6 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // // major_grid_lines // minor_grid_lines -// major_unit // reverse_order // maximum // minimum @@ -818,6 +817,7 @@ func (f *File) addChart(formatSet *formatChart) { }, }, }, + Overlay: &attrValBool{Val: boolPtr(false)}, }, View3D: &cView3D{ RotX: &attrValInt{Val: intPtr(chartView3DRotX[formatSet.Type])}, @@ -1627,9 +1627,6 @@ func (f *File) drawPlotAreaCatAx(formatSet *formatChart) []*cAxs { if formatSet.XAxis.MinorGridlines { axs[0].MinorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()} } - if formatSet.XAxis.MajorUnit != 0 { - axs[0].MajorUnit = &attrValFloat{Val: float64Ptr(formatSet.XAxis.MajorUnit)} - } if formatSet.XAxis.TickLabelSkip != 0 { axs[0].TickLblSkip = &attrValInt{Val: intPtr(formatSet.XAxis.TickLabelSkip)} } diff --git a/chart_test.go b/chart_test.go index 5d0f2d1561..2ed7944deb 100644 --- a/chart_test.go +++ b/chart_test.go @@ -137,7 +137,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"}`)) - 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"},{"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,"major_unit":1,"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"},{"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/xmlChart.go b/xmlChart.go index a28d2a70fe..fc38dab126 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -56,11 +56,11 @@ type cChart struct { // cTitle (Title) directly maps the title element. This element specifies a // title. type cTitle struct { - Tx cTx `xml:"tx,omitempty"` - Layout string `xml:"layout,omitempty"` - Overlay attrValBool `xml:"overlay,omitempty"` - SpPr cSpPr `xml:"spPr,omitempty"` - TxPr cTxPr `xml:"txPr,omitempty"` + Tx cTx `xml:"tx,omitempty"` + Layout string `xml:"layout,omitempty"` + Overlay *attrValBool `xml:"overlay"` + SpPr cSpPr `xml:"spPr,omitempty"` + TxPr cTxPr `xml:"txPr,omitempty"` } // cTx (Chart Text) directly maps the tx element. This element specifies text diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 57fd43f8a2..c17d12fd56 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -475,7 +475,7 @@ func (c *xlsxC) hasValue() bool { // http://schemas.openxmlformats.org/spreadsheetml/2006/main - currently I have // not checked it for completeness - it does as much as I need. type xlsxF struct { - Content string `xml:",chardata"` + Content string `xml:",innerxml"` T string `xml:"t,attr,omitempty"` // Formula type Ref string `xml:"ref,attr,omitempty"` // Shared formula ref Si string `xml:"si,attr,omitempty"` // Shared formula index From 5c87effc7e6c97fff36a56dea1afac8a2f06fb37 Mon Sep 17 00:00:00 2001 From: Cameron Howey Date: Sat, 28 Dec 2019 20:45:10 -0800 Subject: [PATCH 184/957] Stream to Excel table (#530) * Support all datatypes for StreamWriter * Support setting styles with StreamWriter **NOTE:** This is a breaking change. Values are now explicitly passed as a []interface{} for simplicity. We also let styles to be set at the same time. * Create function to write stream into a table * Write rows directly to buffer Avoiding the xml.Encoder makes the streamer faster and use less memory. Using the included benchmark, the results went from: > BenchmarkStreamWriter-4 514 2576155 ns/op 454918 B/op 6592 allocs/op down to: > BenchmarkStreamWriter-4 1614 777480 ns/op 147608 B/op 5570 allocs/op * Use AddTable instead of SetTable This requires reading the cells after they have been written, which requires additional structure for the temp file. As a bonus, we now efficiently allocate only one buffer when reading the file back into memory, using the same approach as ioutil.ReadFile. * Use an exported Cell type to handle inline styles for StreamWriter --- adjust.go | 23 ++- cell.go | 100 +++++++--- stream.go | 495 ++++++++++++++++++++++++++++++++++++++----------- stream_test.go | 82 +++++--- 4 files changed, 540 insertions(+), 160 deletions(-) diff --git a/adjust.go b/adjust.go index c15d4b40b6..69ded1beee 100644 --- a/adjust.go +++ b/adjust.go @@ -196,10 +196,14 @@ func (f *File) adjustAutoFilterHelper(dir adjustDirection, coordinates []int, nu // areaRefToCoordinates provides a function to convert area reference to a // pair of coordinates. func (f *File) areaRefToCoordinates(ref string) ([]int, error) { - coordinates := make([]int, 4) rng := strings.Split(ref, ":") - firstCell := rng[0] - lastCell := rng[1] + return areaRangeToCoordinates(rng[0], rng[1]) +} + +// areaRangeToCoordinates provides a function to convert cell range to a +// pair of coordinates. +func areaRangeToCoordinates(firstCell, lastCell string) ([]int, error) { + coordinates := make([]int, 4) var err error coordinates[0], coordinates[1], err = CellNameToCoordinates(firstCell) if err != nil { @@ -209,6 +213,19 @@ func (f *File) areaRefToCoordinates(ref string) ([]int, error) { return coordinates, err } +func sortCoordinates(coordinates []int) error { + if len(coordinates) != 4 { + return errors.New("coordinates length must be 4") + } + if coordinates[2] < coordinates[0] { + coordinates[2], coordinates[0] = coordinates[0], coordinates[2] + } + if coordinates[3] < coordinates[1] { + coordinates[3], coordinates[1] = coordinates[1], coordinates[3] + } + return nil +} + // coordinatesToAreaRef provides a function to convert a pair of coordinates // to area reference. func (f *File) coordinatesToAreaRef(coordinates []int) (string, error) { diff --git a/cell.go b/cell.go index da091eef2b..1aeddc1f49 100644 --- a/cell.go +++ b/cell.go @@ -83,7 +83,8 @@ func (f *File) SetCellValue(sheet, axis string, value interface{}) error { case []byte: err = f.SetCellStr(sheet, axis, string(v)) case time.Duration: - err = f.SetCellDefault(sheet, axis, strconv.FormatFloat(v.Seconds()/86400.0, 'f', -1, 32)) + _, d := setCellDuration(v) + err = f.SetCellDefault(sheet, axis, d) if err != nil { return err } @@ -131,28 +132,50 @@ func (f *File) setCellIntFunc(sheet, axis string, value interface{}) error { // setCellTimeFunc provides a method to process time type of value for // SetCellValue. func (f *File) setCellTimeFunc(sheet, axis string, value time.Time) error { - excelTime, err := timeToExcelTime(value) + xlsx, err := f.workSheetReader(sheet) if err != nil { return err } - if excelTime > 0 { - err = f.SetCellDefault(sheet, axis, strconv.FormatFloat(excelTime, 'f', -1, 64)) - if err != nil { - return err - } + cellData, col, _, err := f.prepareCell(xlsx, sheet, axis) + if err != nil { + return err + } + cellData.S = f.prepareCellStyle(xlsx, col, cellData.S) + + var isNum bool + cellData.T, cellData.V, isNum, err = setCellTime(value) + if err != nil { + return err + } + if isNum { err = f.setDefaultTimeStyle(sheet, axis, 22) if err != nil { return err } - } else { - err = f.SetCellStr(sheet, axis, value.Format(time.RFC3339Nano)) - if err != nil { - return err - } } return err } +func setCellTime(value time.Time) (t string, b string, isNum bool, err error) { + var excelTime float64 + excelTime, err = timeToExcelTime(value) + if err != nil { + return + } + isNum = excelTime > 0 + if isNum { + t, b = setCellDefault(strconv.FormatFloat(excelTime, 'f', -1, 64)) + } else { + t, b = setCellDefault(value.Format(time.RFC3339Nano)) + } + return +} + +func setCellDuration(value time.Duration) (t string, v string) { + v = strconv.FormatFloat(value.Seconds()/86400.0, 'f', -1, 32) + return +} + // 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 { @@ -165,11 +188,15 @@ func (f *File) SetCellInt(sheet, axis string, value int) error { return err } cellData.S = f.prepareCellStyle(xlsx, col, cellData.S) - cellData.T = "" - cellData.V = strconv.Itoa(value) + cellData.T, cellData.V = setCellInt(value) return err } +func setCellInt(value int) (t string, v string) { + v = strconv.Itoa(value) + return +} + // 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 { @@ -182,13 +209,18 @@ func (f *File) SetCellBool(sheet, axis string, value bool) error { return err } cellData.S = f.prepareCellStyle(xlsx, col, cellData.S) - cellData.T = "b" + cellData.T, cellData.V = setCellBool(value) + return err +} + +func setCellBool(value bool) (t string, v string) { + t = "b" if value { - cellData.V = "1" + v = "1" } else { - cellData.V = "0" + v = "0" } - return err + return } // SetCellFloat sets a floating point value into a cell. The prec parameter @@ -210,11 +242,15 @@ func (f *File) SetCellFloat(sheet, axis string, value float64, prec, bitSize int return err } cellData.S = f.prepareCellStyle(xlsx, col, cellData.S) - cellData.T = "" - cellData.V = strconv.FormatFloat(value, 'f', prec, bitSize) + cellData.T, cellData.V = setCellFloat(value, prec, bitSize) return err } +func setCellFloat(value float64, prec, bitSize int) (t string, v string) { + v = strconv.FormatFloat(value, 'f', prec, bitSize) + return +} + // 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 { @@ -226,21 +262,25 @@ func (f *File) SetCellStr(sheet, axis, value string) error { if err != nil { return err } + cellData.S = f.prepareCellStyle(xlsx, col, cellData.S) + cellData.T, cellData.V, cellData.XMLSpace = setCellStr(value) + return err +} + +func setCellStr(value string) (t string, v string, ns xml.Attr) { if len(value) > 32767 { value = value[0:32767] } // Leading and ending space(s) character detection. if len(value) > 0 && (value[0] == 32 || value[len(value)-1] == 32) { - cellData.XMLSpace = xml.Attr{ + ns = xml.Attr{ Name: xml.Name{Space: NameSpaceXML, Local: "space"}, Value: "preserve", } } - - cellData.S = f.prepareCellStyle(xlsx, col, cellData.S) - cellData.T = "str" - cellData.V = value - return err + t = "str" + v = value + return } // SetCellDefault provides a function to set string type value of a cell as @@ -255,11 +295,15 @@ func (f *File) SetCellDefault(sheet, axis, value string) error { return err } cellData.S = f.prepareCellStyle(xlsx, col, cellData.S) - cellData.T = "" - cellData.V = value + cellData.T, cellData.V = setCellDefault(value) return err } +func setCellDefault(value string) (t string, v string) { + v = value + return +} + // 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) { diff --git a/stream.go b/stream.go index 1b1bbe3e80..e981f781f4 100644 --- a/stream.go +++ b/stream.go @@ -12,20 +12,23 @@ package excelize import ( "bytes" "encoding/xml" - "errors" "fmt" + "io" "io/ioutil" "os" "reflect" + "strconv" + "strings" + "time" ) // StreamWriter defined the type of stream writer. type StreamWriter struct { - tmpFile *os.File - File *File - Sheet string - SheetID int - SheetData bytes.Buffer + File *File + Sheet string + SheetID int + rawData bufferedWriter + tableParts string } // NewStreamWriter return stream writer struct by given worksheet name for @@ -46,7 +49,7 @@ type StreamWriter struct { // row[colID] = rand.Intn(640000) // } // cell, _ := excelize.CoordinatesToCellName(1, rowID) -// if err := streamWriter.SetRow(cell, &row); err != nil { +// if err := streamWriter.SetRow(cell, row, nil); err != nil { // panic(err) // } // } @@ -62,157 +65,433 @@ func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) { if sheetID == 0 { return nil, fmt.Errorf("sheet %s is not exist", sheet) } - rsw := &StreamWriter{ + sw := &StreamWriter{ File: f, Sheet: sheet, SheetID: sheetID, } - rsw.SheetData.WriteString("") - return rsw, nil + + ws, err := f.workSheetReader(sheet) + if err != nil { + return nil, err + } + sw.rawData.WriteString(XMLHeader + ``) + return sw, nil } -// SetRow writes an array to streaming row by given worksheet name, starting -// coordinate and a pointer to array type 'slice'. Note that, cell settings -// with styles are not supported currently and after set rows, you must call the -// 'Flush' method to end the streaming writing process. The following -// shows the supported data types: +// AddTable creates an Excel table for the StreamWriter using the given +// coordinate area and format set. For example, create a table of A1:D5: // -// int -// string +// err := sw.AddTable("A1", "D5", ``) // -func (sw *StreamWriter) SetRow(axis string, slice interface{}) error { - col, row, err := CellNameToCoordinates(axis) +// 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}`) +// +// 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 +// 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) 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 errors.New("pointer to slice expected") + + coordinates, err := areaRangeToCoordinates(hcell, vcell) + if err != nil { + return err } - v = v.Elem() - sw.SheetData.WriteString(fmt.Sprintf(``, row)) - for i := 0; i < v.Len(); i++ { - axis, err := CoordinatesToCellName(col+i, row) + sortCoordinates(coordinates) + + // Correct the minimum number of rows, the table at least two lines. + if coordinates[1] == coordinates[3] { + coordinates[3]++ + } + + // Correct table reference coordinate area, such correct C1:B3 to B1:C3. + ref, err := sw.File.coordinatesToAreaRef(coordinates) + if err != nil { + return err + } + + // create table columns using the first row + tableHeaders, err := sw.getRowValues(coordinates[1], coordinates[0], coordinates[2]) + if err != nil { + return err + } + tableColumn := make([]*xlsxTableColumn, len(tableHeaders)) + for i, name := range tableHeaders { + tableColumn[i] = &xlsxTableColumn{ + ID: i + 1, + Name: name, + } + } + + tableID := sw.File.countTables() + 1 + + name := formatSet.TableName + if name == "" { + name = "Table" + strconv.Itoa(tableID) + } + + table := xlsxTable{ + XMLNS: NameSpaceSpreadSheet, + ID: tableID, + Name: name, + DisplayName: name, + Ref: ref, + AutoFilter: &xlsxAutoFilter{ + Ref: ref, + }, + TableColumns: &xlsxTableColumns{ + Count: len(tableColumn), + TableColumn: tableColumn, + }, + TableStyleInfo: &xlsxTableStyleInfo{ + Name: formatSet.TableStyle, + ShowFirstColumn: formatSet.ShowFirstColumn, + ShowLastColumn: formatSet.ShowLastColumn, + ShowRowStripes: formatSet.ShowRowStripes, + ShowColumnStripes: formatSet.ShowColumnStripes, + }, + } + + sheetRelationshipsTableXML := "../tables/table" + strconv.Itoa(tableID) + ".xml" + tableXML := strings.Replace(sheetRelationshipsTableXML, "..", "xl", -1) + + // Add first table for given 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, "") + + sw.tableParts = fmt.Sprintf(``, rID) + + sw.File.addContentTypePart(tableID, "table") + + b, _ := xml.Marshal(table) + sw.File.saveFileList(tableXML, b) + return nil +} + +// Extract values from a row in the StreamWriter. +func (sw *StreamWriter) getRowValues(hrow, hcol, vcol int) (res []string, err error) { + res = make([]string, vcol-hcol+1) + + r, err := sw.rawData.Reader() + if err != nil { + return nil, err + } + + dec := xml.NewDecoder(r) + for { + token, err := dec.Token() + if err == io.EOF { + return res, nil + } if err != nil { - return err + return nil, err } - switch val := v.Index(i).Interface().(type) { - case int: - sw.SheetData.WriteString(fmt.Sprintf(`%d`, axis, val)) - case string: - sw.SheetData.WriteString(sw.setCellStr(axis, val)) - default: - sw.SheetData.WriteString(sw.setCellStr(axis, fmt.Sprint(val))) + startElement, ok := getRowElement(token, hrow) + if !ok { + continue } - } - sw.SheetData.WriteString(``) - // Try to use local storage - chunk := 1 << 24 - if sw.SheetData.Len() >= chunk { - if sw.tmpFile == nil { - err := sw.createTmp() + // decode cells + var row xlsxRow + if err := dec.DecodeElement(&row, &startElement); err != nil { + return nil, err + } + for _, c := range row.C { + col, _, err := CellNameToCoordinates(c.R) if err != nil { - // can not use local storage - return nil + return nil, err } + if col < hcol || col > vcol { + continue + } + res[col-hcol] = c.V } - // use local storage - _, err := sw.tmpFile.Write(sw.SheetData.Bytes()) - if err != nil { - return nil + return res, nil + } +} + +// Check if the token is an XLSX row with the matching row number. +func getRowElement(token xml.Token, hrow int) (startElement xml.StartElement, ok bool) { + startElement, ok = token.(xml.StartElement) + if !ok { + return + } + ok = startElement.Name.Local == "row" + if !ok { + return + } + ok = false + for _, attr := range startElement.Attr { + if attr.Name.Local != "r" { + continue + } + row, _ := strconv.Atoi(attr.Value) + if row == hrow { + ok = true + return } - sw.SheetData.Reset() } - return err + return } -// Flush ending the streaming writing process. -func (sw *StreamWriter) Flush() error { - sw.SheetData.WriteString(``) +// Cell can be used directly in StreamWriter.SetRow to specify a style and +// a value. +type Cell struct { + StyleID int + Value interface{} +} - ws, err := sw.File.workSheetReader(sw.Sheet) +// 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. +// +// 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{}) error { + col, row, err := CellNameToCoordinates(axis) if err != nil { return err } - sheetXML := fmt.Sprintf("xl/worksheets/sheet%d.xml", sw.SheetID) - delete(sw.File.Sheet, sheetXML) - delete(sw.File.checked, sheetXML) - var sheetDataByte []byte - if sw.tmpFile != nil { - // close the local storage file - if err = sw.tmpFile.Close(); err != nil { - return err - } - file, err := os.Open(sw.tmpFile.Name()) + fmt.Fprintf(&sw.rawData, ``, row) + for i, val := range values { + axis, err := CoordinatesToCellName(col+i, row) if err != nil { return err } - - sheetDataByte, err = ioutil.ReadAll(file) - if err != nil { - return err + c := xlsxC{R: axis} + if v, ok := val.(Cell); ok { + c.S = v.StyleID + val = v.Value + } else if v, ok := val.(*Cell); ok && v != nil { + c.S = v.StyleID + val = v.Value } - - if err := file.Close(); err != nil { - return err + switch val := val.(type) { + case int: + c.T, c.V = setCellInt(val) + case int8: + c.T, c.V = setCellInt(int(val)) + case int16: + c.T, c.V = setCellInt(int(val)) + case int32: + c.T, c.V = setCellInt(int(val)) + case int64: + c.T, c.V = setCellInt(int(val)) + case uint: + c.T, c.V = setCellInt(int(val)) + case uint8: + c.T, c.V = setCellInt(int(val)) + case uint16: + c.T, c.V = setCellInt(int(val)) + case uint32: + c.T, c.V = setCellInt(int(val)) + case uint64: + c.T, c.V = setCellInt(int(val)) + case float32: + c.T, c.V = setCellFloat(float64(val), -1, 32) + case float64: + c.T, c.V = setCellFloat(val, -1, 64) + case string: + c.T, c.V, c.XMLSpace = setCellStr(val) + case []byte: + c.T, c.V, c.XMLSpace = setCellStr(string(val)) + case time.Duration: + c.T, c.V = setCellDuration(val) + case time.Time: + c.T, c.V, _, err = setCellTime(val) + case bool: + c.T, c.V = setCellBool(val) + case nil: + c.T, c.V, c.XMLSpace = setCellStr("") + default: + c.T, c.V, c.XMLSpace = setCellStr(fmt.Sprint(val)) } - - err = os.Remove(sw.tmpFile.Name()) if err != nil { return err } + writeCell(&sw.rawData, c) } + sw.rawData.WriteString(``) + return sw.rawData.Sync() +} - sheetDataByte = append(sheetDataByte, sw.SheetData.Bytes()...) - replaceMap := map[string][]byte{ - "XMLName": {}, - "SheetData": sheetDataByte, +func writeCell(buf *bufferedWriter, c xlsxC) { + buf.WriteString(``) + if c.V != "" { + buf.WriteString(``) + xml.EscapeText(buf, []byte(c.V)) + buf.WriteString(``) + } + buf.WriteString(``) } -// createTmp creates a temporary file in the operating system default -// temporary directory. -func (sw *StreamWriter) createTmp() (err error) { - sw.tmpFile, err = ioutil.TempFile(os.TempDir(), "excelize-") - return err +// Flush ending the streaming writing process. +func (sw *StreamWriter) Flush() error { + sw.rawData.WriteString(``) + sw.rawData.WriteString(sw.tableParts) + sw.rawData.WriteString(``) + if err := sw.rawData.Flush(); err != nil { + return err + } + + sheetXML := fmt.Sprintf("xl/worksheets/sheet%d.xml", sw.SheetID) + delete(sw.File.Sheet, sheetXML) + delete(sw.File.checked, sheetXML) + + defer sw.rawData.Close() + b, err := sw.rawData.Bytes() + if err != nil { + return err + } + sw.File.XLSX[sheetXML] = b + return nil } -// StreamMarshalSheet provides method to serialization worksheets by field as -// streaming. -func StreamMarshalSheet(ws *xlsxWorksheet, replaceMap map[string][]byte) []byte { +// bulkAppendOtherFields bulk-appends fields in a worksheet, skipping the +// specified field names. +func bulkAppendOtherFields(w io.Writer, ws *xlsxWorksheet, skip ...string) { + skipMap := make(map[string]struct{}) + for _, name := range skip { + skipMap[name] = struct{}{} + } + s := reflect.ValueOf(ws).Elem() typeOfT := s.Type() - var marshalResult []byte - marshalResult = append(marshalResult, []byte(XMLHeader+``)...) - return marshalResult } -// setCellStr provides a function to set string type value of a cell as -// streaming. Total number of characters that a cell can contain 32767 -// characters. -func (sw *StreamWriter) setCellStr(axis, value string) string { - if len(value) > 32767 { - value = value[0:32767] +// bufferedWriter uses a temp file to store an extended buffer. Writes are +// always made to an in-memory buffer, which will always succeed. The buffer +// is written to the temp file with Sync, which may return an error. Therefore, +// Sync should be periodically called and the error checked. +type bufferedWriter struct { + tmp *os.File + buf bytes.Buffer +} + +// Write to the in-memory buffer. The err is always nil. +func (bw *bufferedWriter) Write(p []byte) (n int, err error) { + return bw.buf.Write(p) +} + +// WriteString wites to the in-memory buffer. The err is always nil. +func (bw *bufferedWriter) WriteString(p string) (n int, err error) { + return bw.buf.WriteString(p) +} + +// Reader provides read-access to the underlying buffer/file. +func (bw *bufferedWriter) Reader() (io.Reader, error) { + if bw.tmp == nil { + return bytes.NewReader(bw.buf.Bytes()), nil + } + if err := bw.Flush(); err != nil { + return nil, err + } + fi, err := bw.tmp.Stat() + if err != nil { + return nil, err } - // Leading and ending space(s) character detection. - if len(value) > 0 && (value[0] == 32 || value[len(value)-1] == 32) { - return fmt.Sprintf(`%s`, axis, value) + // os.File.ReadAt does not affect the cursor position and is safe to use here + return io.NewSectionReader(bw.tmp, 0, fi.Size()), nil +} + +// Bytes returns the entire content of the bufferedWriter. If a temp file is +// used, Bytes will efficiently allocate a buffer to prevent re-allocations. +func (bw *bufferedWriter) Bytes() ([]byte, error) { + if bw.tmp == nil { + return bw.buf.Bytes(), nil + } + + if err := bw.Flush(); err != nil { + return nil, err + } + + var buf bytes.Buffer + if fi, err := bw.tmp.Stat(); err == nil { + if size := fi.Size() + bytes.MinRead; size > bytes.MinRead { + if int64(int(size)) == size { + buf.Grow(int(size)) + } else { + return nil, bytes.ErrTooLarge + } + } + } + + if _, err := bw.tmp.Seek(0, 0); err != nil { + return nil, err + } + + _, err := buf.ReadFrom(bw.tmp) + return buf.Bytes(), err +} + +// Sync will write the in-memory buffer to a temp file, if the in-memory buffer +// has grown large enough. Any error will be returned. +func (bw *bufferedWriter) Sync() (err error) { + // Try to use local storage + const chunk = 1 << 24 + if bw.buf.Len() < chunk { + return nil + } + if bw.tmp == nil { + bw.tmp, err = ioutil.TempFile(os.TempDir(), "excelize-") + if err != nil { + // can not use local storage + return nil + } + } + return bw.Flush() +} + +// Flush the entire in-memory buffer to the temp file, if a temp file is being +// used. +func (bw *bufferedWriter) Flush() error { + if bw.tmp == nil { + return nil + } + _, err := bw.buf.WriteTo(bw.tmp) + if err != nil { + return err + } + bw.buf.Reset() + return nil +} + +// Close the underlying temp file and reset the in-memory buffer. +func (bw *bufferedWriter) Close() error { + bw.buf.Reset() + if bw.tmp == nil { + return nil } - return fmt.Sprintf(`%s`, axis, value) + defer os.Remove(bw.tmp.Name()) + return bw.tmp.Close() } diff --git a/stream_test.go b/stream_test.go index 4482bd15e4..015f64b1f0 100644 --- a/stream_test.go +++ b/stream_test.go @@ -1,6 +1,8 @@ package excelize import ( + "encoding/xml" + "fmt" "math/rand" "path/filepath" "strings" @@ -9,6 +11,25 @@ import ( "github.com/stretchr/testify/assert" ) +func BenchmarkStreamWriter(b *testing.B) { + file := NewFile() + + row := make([]interface{}, 10) + for colID := 0; colID < 10; colID++ { + row[colID] = colID + } + + for n := 0; n < b.N; n++ { + streamWriter, _ := file.NewStreamWriter("Sheet1") + for rowID := 10; rowID <= 110; rowID++ { + cell, _ := CoordinatesToCellName(1, rowID) + streamWriter.SetRow(cell, row) + } + } + + b.ReportAllocs() +} + func TestStreamWriter(t *testing.T) { file := NewFile() streamWriter, err := file.NewStreamWriter("Sheet1") @@ -17,16 +38,16 @@ func TestStreamWriter(t *testing.T) { // Test max characters in a cell. row := make([]interface{}, 1) row[0] = strings.Repeat("c", 32769) - assert.NoError(t, streamWriter.SetRow("A1", &row)) + assert.NoError(t, streamWriter.SetRow("A1", row)) // 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)) + assert.NoError(t, streamWriter.SetRow("A2", row)) row = make([]interface{}, 1) row[0] = []byte("Word") - assert.NoError(t, streamWriter.SetRow("A3", &row)) + assert.NoError(t, streamWriter.SetRow("A3", row)) for rowID := 10; rowID <= 51200; rowID++ { row := make([]interface{}, 50) @@ -34,26 +55,13 @@ func TestStreamWriter(t *testing.T) { row[colID] = rand.Intn(640000) } cell, _ := CoordinatesToCellName(1, rowID) - assert.NoError(t, streamWriter.SetRow(cell, &row)) + assert.NoError(t, streamWriter.SetRow(cell, row)) } assert.NoError(t, streamWriter.Flush()) // Save xlsx file by the given path. assert.NoError(t, file.SaveAs(filepath.Join("test", "TestStreamWriter.xlsx"))) - // Test error exceptions - _, err = file.NewStreamWriter("SheetN") - assert.EqualError(t, err, "sheet SheetN is not exist") -} - -func TestFlush(t *testing.T) { - // Test error exceptions - file := NewFile() - streamWriter, err := file.NewStreamWriter("Sheet1") - assert.NoError(t, err) - streamWriter.Sheet = "SheetN" - assert.EqualError(t, streamWriter.Flush(), "sheet SheetN is not exist") - // Test close temporary file error file = NewFile() streamWriter, err = file.NewStreamWriter("Sheet1") @@ -64,17 +72,49 @@ func TestFlush(t *testing.T) { row[colID] = rand.Intn(640000) } cell, _ := CoordinatesToCellName(1, rowID) - assert.NoError(t, streamWriter.SetRow(cell, &row)) + assert.NoError(t, streamWriter.SetRow(cell, row)) } - assert.NoError(t, streamWriter.tmpFile.Close()) + assert.NoError(t, streamWriter.rawData.Close()) assert.Error(t, streamWriter.Flush()) } +func TestStreamTable(t *testing.T) { + file := NewFile() + streamWriter, err := file.NewStreamWriter("Sheet1") + assert.NoError(t, err) + + // 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", ``)) + assert.NoError(t, streamWriter.Flush()) + + // Verify the table has names. + var table xlsxTable + assert.NoError(t, xml.Unmarshal(file.XLSX["xl/tables/table1.xml"], &table)) + assert.Equal(t, "A", table.TableColumns.TableColumn[0].Name) + assert.Equal(t, "B", table.TableColumns.TableColumn[1].Name) + assert.Equal(t, "C", table.TableColumns.TableColumn[2].Name) +} + +func TestNewStreamWriter(t *testing.T) { + // Test error exceptions + file := NewFile() + _, err := file.NewStreamWriter("Sheet1") + assert.NoError(t, err) + _, err = file.NewStreamWriter("SheetN") + assert.EqualError(t, err, "sheet SheetN is not exist") +} + func TestSetRow(t *testing.T) { // Test error exceptions file := NewFile() streamWriter, err := file.NewStreamWriter("Sheet1") assert.NoError(t, err) - assert.EqualError(t, streamWriter.SetRow("A", &[]interface{}{}), `cannot convert cell "A" to coordinates: invalid cell name "A"`) - assert.EqualError(t, streamWriter.SetRow("A1", []interface{}{}), `pointer to slice expected`) + assert.EqualError(t, streamWriter.SetRow("A", []interface{}{}), `cannot convert cell "A" to coordinates: invalid cell name "A"`) } From 09485b3f9f0aefc58d51462aed65c2416205c591 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 29 Dec 2019 16:02:31 +0800 Subject: [PATCH 185/957] Improve code coverage unit tests --- LICENSE | 4 +- README.md | 70 ++++++++--------------- README_zh.md | 71 ++++++++--------------- adjust.go | 4 +- adjust_test.go | 4 ++ calcchain.go | 2 +- cell.go | 2 +- cellmerged.go | 18 ++---- chart.go | 4 +- col.go | 2 +- comment.go | 2 +- comment_test.go | 2 +- datavalidation.go | 2 +- datavalidation_test.go | 2 +- date.go | 2 +- docProps.go | 2 +- docProps_test.go | 2 +- errors.go | 2 +- excelize.go | 2 +- excelize_test.go | 113 ------------------------------------ file.go | 2 +- lib.go | 2 +- picture.go | 2 +- pivotTable.go | 2 +- rows.go | 2 +- shape.go | 2 +- shape_test.go | 28 +++++++++ sheet.go | 2 +- sheetpr.go | 2 +- sheetview.go | 2 +- sparkline.go | 2 +- stream.go | 127 ++++++++++++++++++++++++----------------- stream_test.go | 49 +++++++++++++++- styles.go | 2 +- styles_test.go | 4 +- table.go | 2 +- table_test.go | 125 ++++++++++++++++++++++++++++++++++++++++ templates.go | 2 +- vmlDrawing.go | 2 +- xmlApp.go | 2 +- xmlCalcChain.go | 2 +- xmlChart.go | 2 +- xmlComments.go | 2 +- xmlContentTypes.go | 2 +- xmlCore.go | 2 +- xmlDecodeDrawing.go | 2 +- xmlDrawing.go | 2 +- xmlPivotTable.go | 2 +- xmlSharedStrings.go | 2 +- xmlStyles.go | 2 +- xmlTable.go | 2 +- xmlTheme.go | 2 +- xmlWorkbook.go | 2 +- xmlWorksheet.go | 2 +- 54 files changed, 383 insertions(+), 320 deletions(-) create mode 100644 shape_test.go create mode 100644 table_test.go diff --git a/LICENSE b/LICENSE index 1962b4a393..51ec1fbebb 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ BSD 3-Clause License -Copyright (c) 2016-2019, 360 Enterprise Security Group, Endpoint Security, Inc. -Copyright (c) 2011-2017, Geoffrey J. Teale (complying with the tealeg/xlsx license) +Copyright (c) 2016-2020, 360 Enterprise Security Group, Endpoint Security, Inc. +Copyright (c) 2011-2017, Geoffrey J. Teale All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/README.md b/README.md index 03e33aeed0..d7f696cc51 100644 --- a/README.md +++ b/README.md @@ -31,11 +31,7 @@ Here is a minimal example usage that will create XLSX file. ```go package main -import ( - "fmt" - - "github.com/360EntSecGroup-Skylar/excelize" -) +import "github.com/360EntSecGroup-Skylar/excelize" func main() { f := excelize.NewFile() @@ -47,9 +43,8 @@ func main() { // Set active sheet of the workbook. f.SetActiveSheet(index) // Save xlsx file by the given path. - err := f.SaveAs("./Book1.xlsx") - if err != nil { - fmt.Println(err) + if err := f.SaveAs("Book1.xlsx"); err != nil { + println(err.Error()) } } ``` @@ -61,32 +56,28 @@ The following constitutes the bare to read a XLSX document. ```go package main -import ( - "fmt" - - "github.com/360EntSecGroup-Skylar/excelize" -) +import "github.com/360EntSecGroup-Skylar/excelize" func main() { - f, err := excelize.OpenFile("./Book1.xlsx") + f, err := excelize.OpenFile("Book1.xlsx") if err != nil { - fmt.Println(err) + println(err.Error()) return } // Get value from cell by given worksheet name and axis. cell, err := f.GetCellValue("Sheet1", "B2") if err != nil { - fmt.Println(err) + println(err.Error()) return } - fmt.Println(cell) + println(cell) // Get all the rows in the Sheet1. rows, err := f.GetRows("Sheet1") for _, row := range rows { for _, colCell := range row { - fmt.Print(colCell, "\t") + print(colCell, "\t") } - fmt.Println() + println() } } ``` @@ -100,11 +91,7 @@ With Excelize chart generation and management is as easy as a few lines of code. ```go package main -import ( - "fmt" - - "github.com/360EntSecGroup-Skylar/excelize" -) +import "github.com/360EntSecGroup-Skylar/excelize" func main() { categories := map[string]string{"A2": "Small", "A3": "Normal", "A4": "Large", "B1": "Apple", "C1": "Orange", "D1": "Pear"} @@ -116,15 +103,13 @@ func main() { for k, v := range values { f.SetCellValue("Sheet1", k, v) } - 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"}}`) - if err != nil { - fmt.Println(err) + 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"}}`); err != nil { + println(err.Error()) return } // Save xlsx file by the given path. - err = f.SaveAs("./Book1.xlsx") - if err != nil { - fmt.Println(err) + if err := f.SaveAs("Book1.xlsx"); err != nil { + println(err.Error()) } } ``` @@ -135,7 +120,6 @@ func main() { package main import ( - "fmt" _ "image/gif" _ "image/jpeg" _ "image/png" @@ -144,30 +128,26 @@ import ( ) func main() { - f, err := excelize.OpenFile("./Book1.xlsx") + f, err := excelize.OpenFile("Book1.xlsx") if err != nil { - fmt.Println(err) + println(err.Error()) return } // Insert a picture. - err = f.AddPicture("Sheet1", "A2", "./image1.png", "") - if err != nil { - fmt.Println(err) + if err := f.AddPicture("Sheet1", "A2", "image.png", ""); err != nil { + println(err.Error()) } // Insert a picture to worksheet with scaling. - err = f.AddPicture("Sheet1", "D2", "./image2.jpg", `{"x_scale": 0.5, "y_scale": 0.5}`) - if err != nil { - fmt.Println(err) + if err := f.AddPicture("Sheet1", "D2", "image.jpg", `{"x_scale": 0.5, "y_scale": 0.5}`); err != nil { + println(err.Error()) } // Insert a picture offset in the cell with printing support. - err = f.AddPicture("Sheet1", "H2", "./image3.gif", `{"x_offset": 15, "y_offset": 10, "print_obj": true, "lock_aspect_ratio": false, "locked": false}`) - if 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 { + println(err.Error()) } // Save the xlsx file with the origin path. - err = f.Save() - if err != nil { - fmt.Println(err) + if err = f.Save(); err != nil { + println(err.Error()) } } ``` diff --git a/README_zh.md b/README_zh.md index 57cd645557..c1ee83e3eb 100644 --- a/README_zh.md +++ b/README_zh.md @@ -30,11 +30,7 @@ go get github.com/360EntSecGroup-Skylar/excelize ```go package main -import ( - "fmt" - - "github.com/360EntSecGroup-Skylar/excelize" -) +import "github.com/360EntSecGroup-Skylar/excelize" func main() { f := excelize.NewFile() @@ -46,9 +42,8 @@ func main() { // 设置工作簿的默认工作表 f.SetActiveSheet(index) // 根据指定路径保存文件 - err := f.SaveAs("./Book1.xlsx") - if err != nil { - fmt.Println(err) + if err := f.SaveAs("Book1.xlsx"); err != nil { + println(err.Error()) } } ``` @@ -60,32 +55,28 @@ func main() { ```go package main -import ( - "fmt" - - "github.com/360EntSecGroup-Skylar/excelize" -) +import "github.com/360EntSecGroup-Skylar/excelize" func main() { - f, err := excelize.OpenFile("./Book1.xlsx") + f, err := excelize.OpenFile("Book1.xlsx") if err != nil { - fmt.Println(err) + println(err.Error()) return } // 获取工作表中指定单元格的值 cell, err := f.GetCellValue("Sheet1", "B2") if err != nil { - fmt.Println(err) + println(err.Error()) return } - fmt.Println(cell) + println(cell) // 获取 Sheet1 上所有单元格 rows, err := f.GetRows("Sheet1") for _, row := range rows { for _, colCell := range row { - fmt.Print(colCell, "\t") + print(colCell, "\t") } - fmt.Println() + println() } } ``` @@ -99,11 +90,7 @@ func main() { ```go package main -import ( - "fmt" - - "github.com/360EntSecGroup-Skylar/excelize" -) +import "github.com/360EntSecGroup-Skylar/excelize" func main() { categories := map[string]string{"A2": "Small", "A3": "Normal", "A4": "Large", "B1": "Apple", "C1": "Orange", "D1": "Pear"} @@ -115,18 +102,15 @@ func main() { for k, v := range values { f.SetCellValue("Sheet1", k, v) } - 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"}}`) - if err != nil { - fmt.Println(err) + 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"}}`); err != nil { + println(err.Error()) return } // 根据指定路径保存文件 - err = f.SaveAs("./Book1.xlsx") - if err != nil { - fmt.Println(err) + if err := f.SaveAs("Book1.xlsx"); err != nil { + println(err.Error()) } } - ``` ### 向 Excel 文档中插入图片 @@ -135,7 +119,6 @@ func main() { package main import ( - "fmt" _ "image/gif" _ "image/jpeg" _ "image/png" @@ -144,30 +127,26 @@ import ( ) func main() { - f, err := excelize.OpenFile("./Book1.xlsx") + f, err := excelize.OpenFile("Book1.xlsx") if err != nil { - fmt.Println(err) + println(err.Error()) return } // 插入图片 - err = f.AddPicture("Sheet1", "A2", "./image1.png", "") - if err != nil { - fmt.Println(err) + if err := f.AddPicture("Sheet1", "A2", "image.png", ""); err != nil { + println(err.Error()) } // 在工作表中插入图片,并设置图片的缩放比例 - err = f.AddPicture("Sheet1", "D2", "./image2.jpg", `{"x_scale": 0.5, "y_scale": 0.5}`) - if err != nil { - fmt.Println(err) + if err := f.AddPicture("Sheet1", "D2", "image.jpg", `{"x_scale": 0.5, "y_scale": 0.5}`); err != nil { + println(err.Error()) } // 在工作表中插入图片,并设置图片的打印属性 - err = f.AddPicture("Sheet1", "H2", "./image3.gif", `{"x_offset": 15, "y_offset": 10, "print_obj": true, "lock_aspect_ratio": false, "locked": false}`) - if 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 { + println(err.Error()) } // 保存文件 - err = f.Save() - if err != nil { - fmt.Println(err) + if err = f.Save(); err != nil { + println(err.Error()) } } ``` diff --git a/adjust.go b/adjust.go index 69ded1beee..bedeec0ebd 100644 --- a/adjust.go +++ b/adjust.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 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. // @@ -213,6 +213,8 @@ func areaRangeToCoordinates(firstCell, lastCell string) ([]int, error) { return coordinates, err } +// sortCoordinates provides a function to correct the coordinate area, such +// correct C1:B3 to B1:C3. func sortCoordinates(coordinates []int) error { if len(coordinates) != 4 { return errors.New("coordinates length must be 4") diff --git a/adjust_test.go b/adjust_test.go index a0de844a88..13e47ffeab 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -114,3 +114,7 @@ func TestCoordinatesToAreaRef(t *testing.T) { assert.NoError(t, err) assert.EqualValues(t, ref, "A1:A1") } + +func TestSortCoordinates(t *testing.T) { + assert.EqualError(t, sortCoordinates(make([]int, 3)), "coordinates length must be 4") +} diff --git a/calcchain.go b/calcchain.go index a3d3820496..f50fb1d575 100644 --- a/calcchain.go +++ b/calcchain.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 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. // diff --git a/cell.go b/cell.go index 1aeddc1f49..a65968032f 100644 --- a/cell.go +++ b/cell.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 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. // diff --git a/cellmerged.go b/cellmerged.go index 5bea0bc708..b952a1ede5 100644 --- a/cellmerged.go +++ b/cellmerged.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 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. // @@ -43,13 +43,7 @@ func (f *File) MergeCell(sheet, hcell, vcell string) error { return err } // Correct the coordinate area, such correct C1:B3 to B1:C3. - if rect1[2] < rect1[0] { - rect1[0], rect1[2] = rect1[2], rect1[0] - } - - if rect1[3] < rect1[1] { - rect1[1], rect1[3] = rect1[3], rect1[1] - } + _ = sortCoordinates(rect1) hcell, _ = CoordinatesToCellName(rect1[0], rect1[1]) vcell, _ = CoordinatesToCellName(rect1[2], rect1[3]) @@ -123,12 +117,8 @@ func (f *File) UnmergeCell(sheet string, hcell, vcell string) error { return err } - if rect1[2] < rect1[0] { - rect1[0], rect1[2] = rect1[2], rect1[0] - } - if rect1[3] < rect1[1] { - rect1[1], rect1[3] = rect1[3], rect1[1] - } + // Correct the coordinate area, such correct C1:B3 to B1:C3. + _ = sortCoordinates(rect1) // return nil since no MergeCells in the sheet if xlsx.MergeCells == nil { diff --git a/chart.go b/chart.go index 8b38d22aa1..b5ff3d1177 100644 --- a/chart.go +++ b/chart.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 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. // @@ -657,6 +657,7 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // // major_grid_lines // minor_grid_lines +// tick_label_skip // reverse_order // maximum // minimum @@ -666,7 +667,6 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // major_grid_lines // minor_grid_lines // major_unit -// tick_label_skip // reverse_order // maximum // minimum diff --git a/col.go b/col.go index 5d4e764b19..f7e6bcd215 100644 --- a/col.go +++ b/col.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 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. // diff --git a/comment.go b/comment.go index 486a035ecf..a5b6085fb1 100644 --- a/comment.go +++ b/comment.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 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. // diff --git a/comment_test.go b/comment_test.go index dd0795131b..5b83162c78 100644 --- a/comment_test.go +++ b/comment_test.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 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. // diff --git a/datavalidation.go b/datavalidation.go index 2499035fbc..8b95b407fc 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 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. // diff --git a/datavalidation_test.go b/datavalidation_test.go index 7e54d55176..c245df3315 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 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. // diff --git a/date.go b/date.go index 8f637029fd..dad39b5253 100644 --- a/date.go +++ b/date.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 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. // diff --git a/docProps.go b/docProps.go index 884eb6317f..a61ee7170a 100644 --- a/docProps.go +++ b/docProps.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 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. // diff --git a/docProps_test.go b/docProps_test.go index 30c31494b1..ef930aefc4 100644 --- a/docProps_test.go +++ b/docProps_test.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 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. // diff --git a/errors.go b/errors.go index 8520a012f5..456049743e 100644 --- a/errors.go +++ b/errors.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 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. // diff --git a/excelize.go b/excelize.go index 94f401c0a5..8fbd3153c5 100644 --- a/excelize.go +++ b/excelize.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 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. diff --git a/excelize_test.go b/excelize_test.go index c4b600d580..ea82828829 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -894,124 +894,11 @@ func TestCopySheetError(t *testing.T) { assert.NoError(t, f.SaveAs(filepath.Join("test", "TestCopySheetError.xlsx"))) } -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() - } - - err = f.AddTable("Sheet2", "F1", "F1", `{"table_style":"TableStyleMedium8"}`) - if !assert.NoError(t, err) { - t.FailNow() - } - - // 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. - assert.EqualError(t, f.AddTable("Sheet1", "A", "B1", `{}`), `cannot convert cell "A" to coordinates: invalid cell name "A"`) - assert.EqualError(t, f.AddTable("Sheet1", "A1", "B", `{}`), `cannot convert cell "B" to coordinates: invalid cell name "B"`) - - assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddTable.xlsx"))) - - // Test addTable with illegal cell coordinates. - 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]") -} - -func TestAddShape(t *testing.T) { - f, err := prepareTestBook1() - 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"}, "paragraph":[{"font":{"bold":true,"italic":true,"family":"Times New Roman","size":36,"color":"#777777","underline":"single"}}], "height": 90}`), "sheet Sheet3 is 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"}},{"text":"Shape","font":{"bold":true,"color":"2980B9"}}]}`), `cannot convert cell "A" to coordinates: invalid cell name "A"`) - 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}`)) - assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddShape2.xlsx"))) -} - func TestGetSheetComments(t *testing.T) { f := NewFile() assert.Equal(t, "", f.getSheetComments(0)) } -func TestAutoFilter(t *testing.T) { - outFile := filepath.Join("test", "TestAutoFilter%d.xlsx") - - f, err := prepareTestBook1() - if !assert.NoError(t, err) { - t.FailNow() - } - - 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 { - 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.SaveAs(fmt.Sprintf(outFile, i+1))) - }) - } - - // testing AutoFilter with illegal cell coordinates. - assert.EqualError(t, f.AutoFilter("Sheet1", "A", "B1", ""), `cannot convert cell "A" to coordinates: invalid cell name "A"`) - assert.EqualError(t, f.AutoFilter("Sheet1", "A1", "B", ""), `cannot convert cell "B" to coordinates: invalid cell name "B"`) -} - -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 { - t.Run(fmt.Sprintf("Expression%d", i+1), func(t *testing.T) { - err = f.AutoFilter("Sheet3", "D4", "B1", format) - if assert.Error(t, err) { - assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, i+1))) - } - }) - } -} - func TestSetActiveSheet(t *testing.T) { f := NewFile() f.WorkBook.BookViews = nil diff --git a/file.go b/file.go index d8f10facfa..6213bb169b 100644 --- a/file.go +++ b/file.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 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. // diff --git a/lib.go b/lib.go index 86f8d16dc2..2d606faee4 100644 --- a/lib.go +++ b/lib.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 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. // diff --git a/picture.go b/picture.go index 01df8492fe..80b0a528d9 100644 --- a/picture.go +++ b/picture.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 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. // diff --git a/pivotTable.go b/pivotTable.go index 6045e41c8c..8610280539 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 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. // diff --git a/rows.go b/rows.go index 687828c79c..20b4379da1 100644 --- a/rows.go +++ b/rows.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 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. // diff --git a/shape.go b/shape.go index 2ea66ea4d1..e9bdb42ea4 100644 --- a/shape.go +++ b/shape.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 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. // diff --git a/shape_test.go b/shape_test.go new file mode 100644 index 0000000000..61fb443d7d --- /dev/null +++ b/shape_test.go @@ -0,0 +1,28 @@ +package excelize + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAddShape(t *testing.T) { + f, err := prepareTestBook1() + 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"}, "paragraph":[{"font":{"bold":true,"italic":true,"family":"Times New Roman","size":36,"color":"#777777","underline":"single"}}], "height": 90}`), "sheet Sheet3 is 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"}},{"text":"Shape","font":{"bold":true,"color":"2980B9"}}]}`), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + 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}`)) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddShape2.xlsx"))) +} diff --git a/sheet.go b/sheet.go index 954de5bc66..2654b8f993 100644 --- a/sheet.go +++ b/sheet.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 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. // diff --git a/sheetpr.go b/sheetpr.go index 086bd3a15d..350e189a34 100644 --- a/sheetpr.go +++ b/sheetpr.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 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. // diff --git a/sheetview.go b/sheetview.go index 8a5091f710..fa3cfdfaaa 100644 --- a/sheetview.go +++ b/sheetview.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 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. // diff --git a/sparkline.go b/sparkline.go index 47c8d5a217..ef99da6771 100644 --- a/sparkline.go +++ b/sparkline.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 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. // diff --git a/stream.go b/stream.go index e981f781f4..9facf3136c 100644 --- a/stream.go +++ b/stream.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 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. // @@ -36,20 +36,27 @@ type StreamWriter struct { // rows, you must call the 'Flush' method to end the streaming writing // process and ensure that the order of line numbers is ascending. For // example, set data for worksheet of size 102400 rows x 50 columns with -// numbers: +// numbers and style: // // file := excelize.NewFile() // streamWriter, err := file.NewStreamWriter("Sheet1") // if err != nil { // panic(err) // } -// for rowID := 1; rowID <= 102400; rowID++ { +// styleID, err := file.NewStyle(`{"font":{"color":"#777777"}}`) +// if err != nil { +// panic(err) +// } +// if err := streamWriter.SetRow("A1", []interface{}{excelize.Cell{StyleID: styleID, Value: "Data"}}); err != nil { +// panic(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, nil); err != nil { +// if err := streamWriter.SetRow(cell, row); err != nil { // panic(err) // } // } @@ -107,7 +114,7 @@ func (sw *StreamWriter) AddTable(hcell, vcell, format string) error { if err != nil { return err } - sortCoordinates(coordinates) + _ = sortCoordinates(coordinates) // Correct the minimum number of rows, the table at least two lines. if coordinates[1] == coordinates[3] { @@ -188,7 +195,7 @@ func (sw *StreamWriter) getRowValues(hrow, hcol, vcol int) (res []string, err er return nil, err } - dec := xml.NewDecoder(r) + dec := sw.File.xmlNewDecoder(r) for { token, err := dec.Token() if err == io.EOF { @@ -248,7 +255,7 @@ func getRowElement(token xml.Token, hrow int) (startElement xml.StartElement, ok // a value. type Cell struct { StyleID int - Value interface{} + Value interface{} } // SetRow writes an array to stream rows by giving a worksheet name, starting @@ -277,47 +284,8 @@ func (sw *StreamWriter) SetRow(axis string, values []interface{}) error { c.S = v.StyleID val = v.Value } - switch val := val.(type) { - case int: - c.T, c.V = setCellInt(val) - case int8: - c.T, c.V = setCellInt(int(val)) - case int16: - c.T, c.V = setCellInt(int(val)) - case int32: - c.T, c.V = setCellInt(int(val)) - case int64: - c.T, c.V = setCellInt(int(val)) - case uint: - c.T, c.V = setCellInt(int(val)) - case uint8: - c.T, c.V = setCellInt(int(val)) - case uint16: - c.T, c.V = setCellInt(int(val)) - case uint32: - c.T, c.V = setCellInt(int(val)) - case uint64: - c.T, c.V = setCellInt(int(val)) - case float32: - c.T, c.V = setCellFloat(float64(val), -1, 32) - case float64: - c.T, c.V = setCellFloat(val, -1, 64) - case string: - c.T, c.V, c.XMLSpace = setCellStr(val) - case []byte: - c.T, c.V, c.XMLSpace = setCellStr(string(val)) - case time.Duration: - c.T, c.V = setCellDuration(val) - case time.Time: - c.T, c.V, _, err = setCellTime(val) - case bool: - c.T, c.V = setCellBool(val) - case nil: - c.T, c.V, c.XMLSpace = setCellStr("") - default: - c.T, c.V, c.XMLSpace = setCellStr(fmt.Sprint(val)) - } - if err != nil { + if err = setCellValFunc(&c, val); err != nil { + sw.rawData.WriteString(``) return err } writeCell(&sw.rawData, c) @@ -326,6 +294,61 @@ func (sw *StreamWriter) SetRow(axis string, values []interface{}) error { return sw.rawData.Sync() } +// setCellValFunc provides a function to set value of a cell. +func setCellValFunc(c *xlsxC, val interface{}) (err error) { + switch val := val.(type) { + case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: + err = setCellIntFunc(c, val) + case float32: + c.T, c.V = setCellFloat(float64(val), -1, 32) + case float64: + c.T, c.V = setCellFloat(val, -1, 64) + case string: + c.T, c.V, c.XMLSpace = setCellStr(val) + case []byte: + c.T, c.V, c.XMLSpace = setCellStr(string(val)) + case time.Duration: + c.T, c.V = setCellDuration(val) + case time.Time: + c.T, c.V, _, err = setCellTime(val) + case bool: + c.T, c.V = setCellBool(val) + case nil: + c.T, c.V, c.XMLSpace = setCellStr("") + default: + c.T, c.V, c.XMLSpace = setCellStr(fmt.Sprint(val)) + } + return err +} + +// setCellIntFunc is a wrapper of SetCellInt. +func setCellIntFunc(c *xlsxC, val interface{}) (err error) { + switch val := val.(type) { + case int: + c.T, c.V = setCellInt(val) + case int8: + c.T, c.V = setCellInt(int(val)) + case int16: + c.T, c.V = setCellInt(int(val)) + case int32: + c.T, c.V = setCellInt(int(val)) + case int64: + c.T, c.V = setCellInt(int(val)) + case uint: + c.T, c.V = setCellInt(int(val)) + case uint8: + c.T, c.V = setCellInt(int(val)) + case uint16: + c.T, c.V = setCellInt(int(val)) + case uint32: + c.T, c.V = setCellInt(int(val)) + case uint64: + c.T, c.V = setCellInt(int(val)) + default: + } + return +} + func writeCell(buf *bufferedWriter, c xlsxC) { buf.WriteString(`= 2"}`, + `{"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) + assert.NoError(t, err) + assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, i+1))) + }) + } + + // testing AutoFilter with illegal cell coordinates. + assert.EqualError(t, f.AutoFilter("Sheet1", "A", "B1", ""), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.EqualError(t, f.AutoFilter("Sheet1", "A1", "B", ""), `cannot convert cell "B" to coordinates: invalid cell name "B"`) +} + +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 { + t.Run(fmt.Sprintf("Expression%d", i+1), func(t *testing.T) { + err = f.AutoFilter("Sheet3", "D4", "B1", format) + if assert.Error(t, err) { + assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, i+1))) + } + }) + } + + assert.EqualError(t, f.autoFilter("Sheet1", "A1", 1, 1, &formatAutoFilter{ + Column: "-", + Expression: "-", + }), `invalid column name "-"`) + assert.EqualError(t, f.autoFilter("Sheet1", "A1", 1, 100, &formatAutoFilter{ + Column: "A", + Expression: "-", + }), `incorrect index of column 'A'`) + assert.EqualError(t, f.autoFilter("Sheet1", "A1", 1, 1, &formatAutoFilter{ + Column: "A", + Expression: "-", + }), `incorrect number of tokens in criteria '-'`) +} + +func TestParseFilterTokens(t *testing.T) { + f := NewFile() + // Test with unknown operator. + _, _, err := f.parseFilterTokens("", []string{"", "!"}) + assert.EqualError(t, err, "unknown operator: !") + // 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/templates.go b/templates.go index 5b79b0c806..a7972e6169 100644 --- a/templates.go +++ b/templates.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 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. // diff --git a/vmlDrawing.go b/vmlDrawing.go index 24b615fd4d..f2d55f17f1 100644 --- a/vmlDrawing.go +++ b/vmlDrawing.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 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. // diff --git a/xmlApp.go b/xmlApp.go index 48450e3ff3..5668cf641d 100644 --- a/xmlApp.go +++ b/xmlApp.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 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. // diff --git a/xmlCalcChain.go b/xmlCalcChain.go index 343f15f6ef..69d5d8cad7 100644 --- a/xmlCalcChain.go +++ b/xmlCalcChain.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 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. // diff --git a/xmlChart.go b/xmlChart.go index fc38dab126..b6d041e659 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 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. // diff --git a/xmlComments.go b/xmlComments.go index f13d00243b..687c486d02 100644 --- a/xmlComments.go +++ b/xmlComments.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 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. // diff --git a/xmlContentTypes.go b/xmlContentTypes.go index fa4d3475a6..7acfe08760 100644 --- a/xmlContentTypes.go +++ b/xmlContentTypes.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 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. // diff --git a/xmlCore.go b/xmlCore.go index 96482fc160..6f71a3ef01 100644 --- a/xmlCore.go +++ b/xmlCore.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 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. // diff --git a/xmlDecodeDrawing.go b/xmlDecodeDrawing.go index e11bb00d16..93e0e827f4 100644 --- a/xmlDecodeDrawing.go +++ b/xmlDecodeDrawing.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 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. // diff --git a/xmlDrawing.go b/xmlDrawing.go index 1c24f08eda..5bb5977519 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 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. // diff --git a/xmlPivotTable.go b/xmlPivotTable.go index 6e1dfb84a0..82bbf27ff6 100644 --- a/xmlPivotTable.go +++ b/xmlPivotTable.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 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. // diff --git a/xmlSharedStrings.go b/xmlSharedStrings.go index 79837411bd..61e5727b8a 100644 --- a/xmlSharedStrings.go +++ b/xmlSharedStrings.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 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. // diff --git a/xmlStyles.go b/xmlStyles.go index 16a89abdec..0313008e0b 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 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. // diff --git a/xmlTable.go b/xmlTable.go index 017bda1062..345337f24e 100644 --- a/xmlTable.go +++ b/xmlTable.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 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. // diff --git a/xmlTheme.go b/xmlTheme.go index f764c20784..76f13b4cbc 100644 --- a/xmlTheme.go +++ b/xmlTheme.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 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. // diff --git a/xmlWorkbook.go b/xmlWorkbook.go index 65606b01e7..bc59924911 100644 --- a/xmlWorkbook.go +++ b/xmlWorkbook.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 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. // diff --git a/xmlWorksheet.go b/xmlWorksheet.go index c17d12fd56..ed304ccbee 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2020 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. // From 5f5ec76740704a8362e5a120b4a3582b409a5fdd Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 31 Dec 2019 01:01:16 +0800 Subject: [PATCH 186/957] Fix #551, handle empty rows in streaming reading --- chart.go | 16 +++++----------- excelize.go | 15 ++++++--------- picture.go | 47 +++++++++++++++++++---------------------------- pivotTable.go | 12 +++++------- rows.go | 38 ++++++++++++++++++++++++++------------ rows_test.go | 9 +++++++++ stream.go | 12 ++++++------ styles.go | 16 ++++++++-------- 8 files changed, 84 insertions(+), 81 deletions(-) diff --git a/chart.go b/chart.go index b5ff3d1177..a8fcaf5556 100644 --- a/chart.go +++ b/chart.go @@ -495,11 +495,7 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // // package main // -// import ( -// "fmt" -// -// "github.com/360EntSecGroup-Skylar/excelize" -// ) +// import "github.com/360EntSecGroup-Skylar/excelize" // // func main() { // categories := map[string]string{"A2": "Small", "A3": "Normal", "A4": "Large", "B1": "Apple", "C1": "Orange", "D1": "Pear"} @@ -511,15 +507,13 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // for k, v := range values { // f.SetCellValue("Sheet1", k, v) // } -// err := f.AddChart("Sheet1", "E1", `{"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"}],"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":"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},"show_blanks_as":"zero","x_axis":{"reverse_order":true},"y_axis":{"maximum":7.5,"minimum":0.5}}`) -// if err != nil { -// fmt.Println(err) +// if err := f.AddChart("Sheet1", "E1", `{"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"}],"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":"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},"show_blanks_as":"zero","x_axis":{"reverse_order":true},"y_axis":{"maximum":7.5,"minimum":0.5}}`); err != nil { +// println(err.Error()) // return // } // // Save xlsx file by the given path. -// err = xlsx.SaveAs("./Book1.xlsx") -// if err != nil { -// fmt.Println(err) +// if err := xlsx.SaveAs("Book1.xlsx"); err != nil { +// println(err.Error()) // } // } // diff --git a/excelize.go b/excelize.go index 8fbd3153c5..9832c6a304 100644 --- a/excelize.go +++ b/excelize.go @@ -293,17 +293,14 @@ 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: // -// err := f.SetSheetPrOptions("Sheet1", excelize.CodeName("Sheet1")) -// if err != nil { -// fmt.Println(err) +// if err := f.SetSheetPrOptions("Sheet1", excelize.CodeName("Sheet1")); err != nil { +// println(err.Error()) // } -// err = f.AddVBAProject("vbaProject.bin") -// if err != nil { -// fmt.Println(err) +// if err := f.AddVBAProject("vbaProject.bin"); err != nil { +// println(err.Error()) // } -// err = f.SaveAs("macros.xlsm") -// if err != nil { -// fmt.Println(err) +// if err := f.SaveAs("macros.xlsm"); err != nil { +// println(err.Error()) // } // func (f *File) AddVBAProject(bin string) error { diff --git a/picture.go b/picture.go index 80b0a528d9..639cb66263 100644 --- a/picture.go +++ b/picture.go @@ -48,7 +48,6 @@ func parseFormatPictureSet(formatSet string) (*formatPicture, error) { // package main // // import ( -// "fmt" // _ "image/gif" // _ "image/jpeg" // _ "image/png" @@ -59,23 +58,19 @@ func parseFormatPictureSet(formatSet string) (*formatPicture, error) { // func main() { // f := excelize.NewFile() // // Insert a picture. -// err := f.AddPicture("Sheet1", "A2", "./image1.jpg", "") -// if err != nil { -// fmt.Println(err) +// if err := f.AddPicture("Sheet1", "A2", "image.jpg", ""); err != nil { +// println(err.Error()) // } // // Insert a picture scaling in the cell with location hyperlink. -// err = f.AddPicture("Sheet1", "D2", "./image1.png", `{"x_scale": 0.5, "y_scale": 0.5, "hyperlink": "#Sheet2!D8", "hyperlink_type": "Location"}`) -// if err != nil { -// fmt.Println(err) +// if err := f.AddPicture("Sheet1", "D2", "image.png", `{"x_scale": 0.5, "y_scale": 0.5, "hyperlink": "#Sheet2!D8", "hyperlink_type": "Location"}`); err != nil { +// println(err.Error()) // } // // Insert a picture offset in the cell with external hyperlink, printing and positioning support. -// err = f.AddPicture("Sheet1", "H2", "./image3.gif", `{"x_offset": 15, "y_offset": 10, "hyperlink": "https://github.com/360EntSecGroup-Skylar/excelize", "hyperlink_type": "External", "print_obj": true, "lock_aspect_ratio": false, "locked": false, "positioning": "oneCell"}`) -// if err != nil { -// fmt.Println(err) +// if err := f.AddPicture("Sheet1", "H2", "image.gif", `{"x_offset": 15, "y_offset": 10, "hyperlink": "https://github.com/360EntSecGroup-Skylar/excelize", "hyperlink_type": "External", "print_obj": true, "lock_aspect_ratio": false, "locked": false, "positioning": "oneCell"}`); err != nil { +// println(err.Error()) // } -// err = f.SaveAs("./Book1.xlsx") -// if err != nil { -// fmt.Println(err) +// if err := f.SaveAs("Book1.xlsx"); err != nil { +// println(err.Error()) // } // } // @@ -109,7 +104,6 @@ func (f *File) AddPicture(sheet, cell, picture, format string) error { // package main // // import ( -// "fmt" // _ "image/jpeg" // "io/ioutil" // @@ -119,17 +113,15 @@ func (f *File) AddPicture(sheet, cell, picture, format string) error { // func main() { // f := excelize.NewFile() // -// file, err := ioutil.ReadFile("./image1.jpg") +// file, err := ioutil.ReadFile("image.jpg") // if err != nil { -// fmt.Println(err) +// println(err.Error()) // } -// err = f.AddPictureFromBytes("Sheet1", "A2", "", "Excel Logo", ".jpg", file) -// if err != nil { -// fmt.Println(err) +// if err := f.AddPictureFromBytes("Sheet1", "A2", "", "Excel Logo", ".jpg", file); err != nil { +// println(err.Error()) // } -// err = f.SaveAs("./Book1.xlsx") -// if err != nil { -// fmt.Println(err) +// if err := f.SaveAs("Book1.xlsx"); err != nil { +// println(err.Error()) // } // } // @@ -430,19 +422,18 @@ func (f *File) getSheetRelationshipsTargetByID(sheet, rID string) string { // embed in XLSX by given worksheet and cell name. This function returns the // file name in XLSX and file contents as []byte data types. For example: // -// f, err := excelize.OpenFile("./Book1.xlsx") +// f, err := excelize.OpenFile("Book1.xlsx") // if err != nil { -// fmt.Println(err) +// println(err.Error()) // return // } // file, raw, err := f.GetPicture("Sheet1", "A2") // if err != nil { -// fmt.Println(err) +// println(err.Error()) // return // } -// err = ioutil.WriteFile(file, raw, 0644) -// if err != nil { -// fmt.Println(err) +// if err := ioutil.WriteFile(file, raw, 0644); err != nil { +// println(err.Error()) // } // func (f *File) GetPicture(sheet, cell string) (string, []byte, error) { diff --git a/pivotTable.go b/pivotTable.go index 8610280539..70681cae8e 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -56,19 +56,17 @@ type PivotTableOption struct { // 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)]) // } -// err := f.AddPivotTable(&excelize.PivotTableOption{ +// if err := f.AddPivotTable(&excelize.PivotTableOption{ // DataRange: "Sheet1!$A$1:$E$31", // PivotTableRange: "Sheet1!$G$2:$M$34", // Rows: []string{"Month", "Year"}, // Columns: []string{"Type"}, // Data: []string{"Sales"}, -// }) -// if err != nil { -// fmt.Println(err) +// }); err != nil { +// println(err.Error()) // } -// err = f.SaveAs("Book1.xlsx") -// if err != nil { -// fmt.Println(err) +// if err := f.SaveAs("Book1.xlsx"); err != nil { +// println(err.Error()) // } // } // diff --git a/rows.go b/rows.go index 20b4379da1..d24b1a6cf3 100644 --- a/rows.go +++ b/rows.go @@ -23,12 +23,20 @@ import ( // GetRows return all the rows in a sheet by given worksheet name (case // sensitive). For example: // -// rows, err := f.GetRows("Sheet1") -// for _, row := range rows { +// rows, err := f.Rows("Sheet1") +// if err != nil { +// println(err.Error()) +// return +// } +// for rows.Next() { +// row, err := rows.Columns() +// if err != nil { +// println(err.Error()) +// } // for _, colCell := range row { -// fmt.Print(colCell, "\t") +// print(colCell, "\t") // } -// fmt.Println() +// println() // } // func (f *File) GetRows(sheet string) ([][]string, error) { @@ -52,13 +60,13 @@ func (f *File) GetRows(sheet string) ([][]string, error) { // Rows defines an iterator to a sheet type Rows struct { - err error - f *File - rows []xlsxRow - sheet string - curRow int - totalRow int - decoder *xml.Decoder + err error + curRow, totalRow, stashRow int + sheet string + stashColumn []string + rows []xlsxRow + f *File + decoder *xml.Decoder } // Next will return true if find the next row element. @@ -80,6 +88,11 @@ func (rows *Rows) Columns() ([]string, error) { row, cellCol int columns []string ) + + if rows.stashRow >= rows.curRow { + return columns, err + } + d := rows.f.sharedStringsReader() for { token, _ := rows.decoder.Token() @@ -97,6 +110,8 @@ func (rows *Rows) Columns() ([]string, error) { return columns, err } if row > rows.curRow { + rows.stashRow = row - 1 + rows.stashColumn = columns return columns, err } } @@ -121,7 +136,6 @@ func (rows *Rows) Columns() ([]string, error) { if inElement == "row" { return columns, err } - default: } } return columns, err diff --git a/rows_test.go b/rows_test.go index fc9d866000..1127bb14bc 100644 --- a/rows_test.go +++ b/rows_test.go @@ -136,7 +136,16 @@ func TestColumns(t *testing.T) { f := NewFile() rows, err := f.Rows("Sheet1") assert.NoError(t, err) + + rows.decoder = f.xmlNewDecoder(bytes.NewReader([]byte(`1`))) + _, err = rows.Columns() + assert.NoError(t, err) + rows.decoder = f.xmlNewDecoder(bytes.NewReader([]byte(`1`))) + rows.curRow = 1 + _, err = rows.Columns() + rows.decoder = f.xmlNewDecoder(bytes.NewReader([]byte(`1B`))) + rows.stashRow, rows.curRow = 0, 1 _, err = rows.Columns() assert.EqualError(t, err, `strconv.Atoi: parsing "A": invalid syntax`) diff --git a/stream.go b/stream.go index 9facf3136c..83986229c0 100644 --- a/stream.go +++ b/stream.go @@ -41,14 +41,14 @@ type StreamWriter struct { // file := excelize.NewFile() // streamWriter, err := file.NewStreamWriter("Sheet1") // if err != nil { -// panic(err) +// println(err.Error()) // } // styleID, err := file.NewStyle(`{"font":{"color":"#777777"}}`) // if err != nil { -// panic(err) +// println(err.Error()) // } // if err := streamWriter.SetRow("A1", []interface{}{excelize.Cell{StyleID: styleID, Value: "Data"}}); err != nil { -// panic(err) +// println(err.Error()) // } // for rowID := 2; rowID <= 102400; rowID++ { // row := make([]interface{}, 50) @@ -57,14 +57,14 @@ type StreamWriter struct { // } // cell, _ := excelize.CoordinatesToCellName(1, rowID) // if err := streamWriter.SetRow(cell, row); err != nil { -// panic(err) +// println(err.Error()) // } // } // if err := streamWriter.Flush(); err != nil { -// panic(err) +// println(err.Error()) // } // if err := file.SaveAs("Book1.xlsx"); err != nil { -// panic(err) +// println(err.Error()) // } // func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) { diff --git a/styles.go b/styles.go index 272d7280a7..ad3e825542 100644 --- a/styles.go +++ b/styles.go @@ -2321,7 +2321,7 @@ func (f *File) GetCellStyle(sheet, axis string) (int, error) { // // style, err := f.NewStyle(`{"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) +// println(err.Error()) // } // err = f.SetCellStyle("Sheet1", "H9", "H9", style) // @@ -2330,7 +2330,7 @@ func (f *File) GetCellStyle(sheet, axis string) (int, error) { // // style, err := f.NewStyle(`{"fill":{"type":"gradient","color":["#FFFFFF","#E0EBF5"],"shading":1}}`) // if err != nil { -// fmt.Println(err) +// println(err.Error()) // } // err = f.SetCellStyle("Sheet1", "H9", "H9", style) // @@ -2338,7 +2338,7 @@ func (f *File) GetCellStyle(sheet, axis string) (int, error) { // // style, err := f.NewStyle(`{"fill":{"type":"pattern","color":["#E0EBF5"],"pattern":1}}`) // if err != nil { -// fmt.Println(err) +// println(err.Error()) // } // err = f.SetCellStyle("Sheet1", "H9", "H9", style) // @@ -2346,7 +2346,7 @@ func (f *File) GetCellStyle(sheet, axis string) (int, error) { // // 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":"","wrap_text":true}}`) // if err != nil { -// fmt.Println(err) +// println(err.Error()) // } // err = f.SetCellStyle("Sheet1", "H9", "H9", style) // @@ -2357,7 +2357,7 @@ func (f *File) GetCellStyle(sheet, axis string) (int, error) { // f.SetCellValue("Sheet1", "H9", 42920.5) // style, err := f.NewStyle(`{"number_format": 22}`) // if err != nil { -// fmt.Println(err) +// println(err.Error()) // } // err = f.SetCellStyle("Sheet1", "H9", "H9", style) // @@ -2365,7 +2365,7 @@ func (f *File) GetCellStyle(sheet, axis string) (int, error) { // // style, err := f.NewStyle(`{"font":{"bold":true,"italic":true,"family":"Times New Roman","size":36,"color":"#777777"}}`) // if err != nil { -// fmt.Println(err) +// println(err.Error()) // } // err = f.SetCellStyle("Sheet1", "H9", "H9", style) // @@ -2373,7 +2373,7 @@ func (f *File) GetCellStyle(sheet, axis string) (int, error) { // // style, err := f.NewStyle(`{"protection":{"hidden":true, "locked":true}}`) // if err != nil { -// fmt.Println(err) +// println(err.Error()) // } // err = f.SetCellStyle("Sheet1", "H9", "H9", style) // @@ -2507,7 +2507,7 @@ func (f *File) SetCellStyle(sheet, hcell, vcell string, styleID int) error { // // format, err = f.NewConditionalStyle(`{"font":{"color":"#9A0511"},"fill":{"type":"pattern","color":["#FEC7CE"],"pattern":1}}`) // if err != nil { -// fmt.Println(err) +// println(err.Error()) // } // f.SetConditionalFormat("Sheet1", "A1:A10", fmt.Sprintf(`[{"type":"cell","criteria":">","format":%d,"value":"6"}]`, format)) // From 5ca7231ed408ac264f509ff52b5d28ff4fbda757 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 3 Jan 2020 23:57:25 +0800 Subject: [PATCH 187/957] optimize code and comments: use println errors instead of panic --- cell_test.go | 2 +- excelize_test.go | 2 +- file_test.go | 7 +++---- cellmerged.go => merge.go | 0 cellmerged_test.go => merge_test.go | 0 rows.go | 13 +++++++++---- rows_test.go | 1 + sheet.go | 4 ++-- sheet_test.go | 12 ++++++------ sheetpr_test.go | 8 ++++---- sheetview_test.go | 24 ++++++++++++------------ sparkline_test.go | 4 ++-- 12 files changed, 41 insertions(+), 36 deletions(-) rename cellmerged.go => merge.go (100%) rename cellmerged_test.go => merge_test.go (100%) diff --git a/cell_test.go b/cell_test.go index 1efbc5a871..60f8751a1a 100644 --- a/cell_test.go +++ b/cell_test.go @@ -110,7 +110,7 @@ func ExampleFile_SetCellFloat() { f := NewFile() var x = 3.14159265 if err := f.SetCellFloat("Sheet1", "A1", x, 2, 64); err != nil { - fmt.Println(err) + println(err.Error()) } val, _ := f.GetCellValue("Sheet1", "A1") fmt.Println(val) diff --git a/excelize_test.go b/excelize_test.go index ea82828829..c7f5cad747 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -1259,7 +1259,7 @@ func fillCells(f *File, sheet string, colCount, rowCount int) { for row := 1; row <= rowCount; row++ { cell, _ := CoordinatesToCellName(col, row) if err := f.SetCellStr(sheet, cell, cell); err != nil { - panic(err) + println(err.Error()) } } } diff --git a/file_test.go b/file_test.go index 97ff72006b..8c5050cc12 100644 --- a/file_test.go +++ b/file_test.go @@ -12,18 +12,17 @@ func BenchmarkWrite(b *testing.B) { for col := 1; col <= 20; col++ { val, err := CoordinatesToCellName(col, row) if err != nil { - panic(err) + b.Error(err) } if err := f.SetCellDefault("Sheet1", val, s); err != nil { - panic(err) + b.Error(err) } } } // Save xlsx file by the given path. err := f.SaveAs("./test.xlsx") if err != nil { - panic(err) + b.Error(err) } } - } diff --git a/cellmerged.go b/merge.go similarity index 100% rename from cellmerged.go rename to merge.go diff --git a/cellmerged_test.go b/merge_test.go similarity index 100% rename from cellmerged_test.go rename to merge_test.go diff --git a/rows.go b/rows.go index d24b1a6cf3..40972ae5a7 100644 --- a/rows.go +++ b/rows.go @@ -63,7 +63,6 @@ type Rows struct { err error curRow, totalRow, stashRow int sheet string - stashColumn []string rows []xlsxRow f *File decoder *xml.Decoder @@ -111,7 +110,6 @@ func (rows *Rows) Columns() ([]string, error) { } if row > rows.curRow { rows.stashRow = row - 1 - rows.stashColumn = columns return columns, err } } @@ -153,12 +151,19 @@ func (err ErrSheetNotExist) Error() string { // Rows return a rows iterator. For example: // // rows, err := f.Rows("Sheet1") +// if err != nil { +// println(err.Error()) +// return +// } // for rows.Next() { // row, err := rows.Columns() +// if err != nil { +// println(err.Error()) +// } // for _, colCell := range row { -// fmt.Print(colCell, "\t") +// print(colCell, "\t") // } -// fmt.Println() +// println() // } // func (f *File) Rows(sheet string) (*Rows, error) { diff --git a/rows_test.go b/rows_test.go index 1127bb14bc..9377d5e37b 100644 --- a/rows_test.go +++ b/rows_test.go @@ -143,6 +143,7 @@ func TestColumns(t *testing.T) { rows.decoder = f.xmlNewDecoder(bytes.NewReader([]byte(`1`))) rows.curRow = 1 _, err = rows.Columns() + assert.NoError(t, err) rows.decoder = f.xmlNewDecoder(bytes.NewReader([]byte(`1B`))) rows.stashRow, rows.curRow = 0, 1 diff --git a/sheet.go b/sheet.go index 2654b8f993..19b90c63a7 100644 --- a/sheet.go +++ b/sheet.go @@ -339,12 +339,12 @@ func (f *File) GetSheetIndex(name string) int { // GetSheetMap provides a function to get worksheet name and index map of XLSX. // For example: // -// f, err := excelize.OpenFile("./Book1.xlsx") +// f, err := excelize.OpenFile("Book1.xlsx") // if err != nil { // return // } // for index, name := range f.GetSheetMap() { -// fmt.Println(index, name) +// println(index, name) // } // func (f *File) GetSheetMap() map[int]string { diff --git a/sheet_test.go b/sheet_test.go index 7a582486ae..a03066a7fd 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -19,7 +19,7 @@ func ExampleFile_SetPageLayout() { "Sheet1", excelize.PageLayoutOrientation(excelize.OrientationLandscape), ); err != nil { - panic(err) + println(err.Error()) } if err := f.SetPageLayout( "Sheet1", @@ -27,7 +27,7 @@ func ExampleFile_SetPageLayout() { excelize.FitToHeight(2), excelize.FitToWidth(2), ); err != nil { - panic(err) + println(err.Error()) } // Output: } @@ -41,17 +41,17 @@ func ExampleFile_GetPageLayout() { fitToWidth excelize.FitToWidth ) if err := f.GetPageLayout("Sheet1", &orientation); err != nil { - panic(err) + println(err.Error()) } if err := f.GetPageLayout("Sheet1", &paperSize); err != nil { - panic(err) + println(err.Error()) } if err := f.GetPageLayout("Sheet1", &fitToHeight); err != nil { - panic(err) + println(err.Error()) } if err := f.GetPageLayout("Sheet1", &fitToWidth); err != nil { - panic(err) + println(err.Error()) } fmt.Println("Defaults:") fmt.Printf("- orientation: %q\n", orientation) diff --git a/sheetpr_test.go b/sheetpr_test.go index d1ae2f1308..6a35a6ecc1 100644 --- a/sheetpr_test.go +++ b/sheetpr_test.go @@ -40,7 +40,7 @@ func ExampleFile_SetSheetPrOptions() { excelize.AutoPageBreaks(true), excelize.OutlineSummaryBelow(false), ); err != nil { - panic(err) + println(err.Error()) } // Output: } @@ -66,7 +66,7 @@ func ExampleFile_GetSheetPrOptions() { &autoPageBreaks, &outlineSummaryBelow, ); err != nil { - panic(err) + println(err.Error()) } fmt.Println("Defaults:") fmt.Printf("- codeName: %q\n", codeName) @@ -189,7 +189,7 @@ func ExampleFile_SetPageMargins() { excelize.PageMarginRight(1.0), excelize.PageMarginTop(1.0), ); err != nil { - panic(err) + println(err.Error()) } // Output: } @@ -215,7 +215,7 @@ func ExampleFile_GetPageMargins() { &marginRight, &marginTop, ); err != nil { - panic(err) + println(err.Error()) } fmt.Println("Defaults:") fmt.Println("- marginBottom:", marginBottom) diff --git a/sheetview_test.go b/sheetview_test.go index e45b8cec35..8412002660 100644 --- a/sheetview_test.go +++ b/sheetview_test.go @@ -47,7 +47,7 @@ func ExampleFile_SetSheetViewOptions() { excelize.ZoomScale(80), excelize.TopLeftCell("C3"), ); err != nil { - panic(err) + println(err.Error()) } var zoomScale excelize.ZoomScale @@ -55,22 +55,22 @@ func ExampleFile_SetSheetViewOptions() { fmt.Println("- zoomScale: 80") if err := f.SetSheetViewOptions(sheet, 0, excelize.ZoomScale(500)); err != nil { - panic(err) + println(err.Error()) } if err := f.GetSheetViewOptions(sheet, 0, &zoomScale); err != nil { - panic(err) + println(err.Error()) } fmt.Println("Used out of range value:") fmt.Println("- zoomScale:", zoomScale) if err := f.SetSheetViewOptions(sheet, 0, excelize.ZoomScale(123)); err != nil { - panic(err) + println(err.Error()) } if err := f.GetSheetViewOptions(sheet, 0, &zoomScale); err != nil { - panic(err) + println(err.Error()) } fmt.Println("Used correct value:") @@ -111,7 +111,7 @@ func ExampleFile_GetSheetViewOptions() { &zoomScale, &topLeftCell, ); err != nil { - panic(err) + println(err.Error()) } fmt.Println("Default:") @@ -125,27 +125,27 @@ func ExampleFile_GetSheetViewOptions() { fmt.Println("- topLeftCell:", `"`+topLeftCell+`"`) if err := f.SetSheetViewOptions(sheet, 0, excelize.TopLeftCell("B2")); err != nil { - panic(err) + println(err.Error()) } if err := f.GetSheetViewOptions(sheet, 0, &topLeftCell); err != nil { - panic(err) + println(err.Error()) } if err := f.SetSheetViewOptions(sheet, 0, excelize.ShowGridLines(false)); err != nil { - panic(err) + println(err.Error()) } if err := f.GetSheetViewOptions(sheet, 0, &showGridLines); err != nil { - panic(err) + println(err.Error()) } if err := f.SetSheetViewOptions(sheet, 0, excelize.ShowZeros(false)); err != nil { - panic(err) + println(err.Error()) } if err := f.GetSheetViewOptions(sheet, 0, &showZeros); err != nil { - panic(err) + println(err.Error()) } fmt.Println("After change:") diff --git a/sparkline_test.go b/sparkline_test.go index dca32e9c16..45bf386e56 100644 --- a/sparkline_test.go +++ b/sparkline_test.go @@ -298,12 +298,12 @@ func prepareSparklineDataset() *File { f.NewSheet("Sheet3") for row, data := range sheet2 { if err := f.SetSheetRow("Sheet2", fmt.Sprintf("A%d", row+1), &data); err != nil { - panic(err) + println(err.Error()) } } for row, data := range sheet3 { if err := f.SetSheetRow("Sheet3", fmt.Sprintf("A%d", row+1), &data); err != nil { - panic(err) + println(err.Error()) } } return f From 9ddb52eac4e451f676dabe4eed45ee95fce38eef Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 14 Jan 2020 00:33:36 +0800 Subject: [PATCH 188/957] Fix #554, init combo chart support, new chart pie of pie, bar of pie chart support --- chart.go | 109 ++++++++++++++++++++++++++++++++++++++++++++++---- chart_test.go | 27 ++++++++++++- xmlChart.go | 12 ++++-- 3 files changed, 136 insertions(+), 12 deletions(-) diff --git a/chart.go b/chart.go index a8fcaf5556..f3b1cd86c5 100644 --- a/chart.go +++ b/chart.go @@ -16,6 +16,7 @@ import ( "errors" "io" "log" + "reflect" "strconv" "strings" ) @@ -66,6 +67,8 @@ const ( Line = "line" Pie = "pie" Pie3D = "pie3D" + PieOfPieChart = "pieOfPie" + BarOfPieChart = "barOfPie" Radar = "radar" Scatter = "scatter" Surface3D = "surface3D" @@ -123,6 +126,8 @@ var ( Line: 0, Pie: 0, Pie3D: 30, + PieOfPieChart: 0, + BarOfPieChart: 0, Radar: 0, Scatter: 0, Surface3D: 15, @@ -175,6 +180,8 @@ var ( Line: 0, Pie: 0, Pie3D: 0, + PieOfPieChart: 0, + BarOfPieChart: 0, Radar: 0, Scatter: 0, Surface3D: 20, @@ -237,6 +244,8 @@ var ( Line: 0, Pie: 0, Pie3D: 0, + PieOfPieChart: 0, + BarOfPieChart: 0, Radar: 0, Scatter: 0, Surface3D: 0, @@ -297,6 +306,8 @@ var ( Line: "General", Pie: "General", Pie3D: "General", + PieOfPieChart: "General", + BarOfPieChart: "General", Radar: "General", Scatter: "General", Surface3D: "General", @@ -351,6 +362,8 @@ var ( Line: "between", Pie: "between", Pie3D: "between", + PieOfPieChart: "between", + BarOfPieChart: "between", Radar: "between", Scatter: "between", Surface3D: "midCat", @@ -491,7 +504,7 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // AddChart provides the method to add chart in a sheet by given chart format // set (such as offset, scale, aspect ratio setting and print settings) and // properties set. For example, create 3D clustered column chart with data -// Sheet1!$A$29:$D$32: +// Sheet1!$E$1:$L$15: // // package main // @@ -507,12 +520,12 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // for k, v := range values { // f.SetCellValue("Sheet1", k, v) // } -// if err := f.AddChart("Sheet1", "E1", `{"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"}],"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":"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},"show_blanks_as":"zero","x_axis":{"reverse_order":true},"y_axis":{"maximum":7.5,"minimum":0.5}}`); err != nil { +// 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"},"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 { // println(err.Error()) // return // } // // Save xlsx file by the given path. -// if err := xlsx.SaveAs("Book1.xlsx"); err != nil { +// if err := f.SaveAs("Book1.xlsx"); err != nil { // println(err.Error()) // } // } @@ -565,6 +578,8 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // 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 @@ -681,6 +696,34 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // // Set chart size by dimension property. The dimension property is optional. The default width is 480, and height is 290. // +// combo: Specifies tha create a chart that combines two art types in a single +// chart. For example, create a clustered column - line chart with data +// Sheet1!$E$1:$L$15: +// +// package main +// +// import "github.com/360EntSecGroup-Skylar/excelize" +// +// 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},"legend":{"position":"left","show_legend_key":false},"title":{"name":"Clustered Column - 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},"combo":{"type":"line","series":[{"name":"Sheet1!$A$4","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$4:$D$4"}],"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}}}`); err != nil { +// println(err.Error()) +// return +// } +// // Save xlsx file by the given path. +// if err := f.SaveAs("Book1.xlsx"); err != nil { +// println(err.Error()) +// } +// } +// func (f *File) AddChart(sheet, cell, format string) error { formatSet, err := parseFormatChartSet(format) if err != nil { @@ -915,6 +958,8 @@ func (f *File) addChart(formatSet *formatChart) { Line: f.drawLineChart, Pie3D: f.drawPie3DChart, Pie: f.drawPieChart, + PieOfPieChart: f.drawPieOfPieChart, + BarOfPieChart: f.drawBarOfPieChart, Radar: f.drawRadarChart, Scatter: f.drawScatterChart, Surface3D: f.drawSurface3DChart, @@ -924,8 +969,20 @@ func (f *File) addChart(formatSet *formatChart) { Bubble: f.drawBaseChart, Bubble3D: f.drawBaseChart, } - xlsxChartSpace.Chart.PlotArea = plotAreaFunc[formatSet.Type](formatSet) - + addChart := func(c, p *cPlotArea) { + immutable, mutable := reflect.ValueOf(c).Elem(), reflect.ValueOf(p).Elem() + for i := 0; i < mutable.NumField(); i++ { + field := mutable.Field(i) + if field.IsNil() { + continue + } + immutable.FieldByName(mutable.Type().Field(i).Name).Set(field) + } + } + addChart(xlsxChartSpace.Chart.PlotArea, plotAreaFunc[formatSet.Type](formatSet)) + if formatSet.Combo != nil { + addChart(xlsxChartSpace.Chart.PlotArea, plotAreaFunc[formatSet.Combo.Type](formatSet.Combo)) + } chart, _ := xml.Marshal(xlsxChartSpace) media := "xl/charts/chart" + strconv.Itoa(count+1) + ".xml" f.saveFileList(media, chart) @@ -1246,6 +1303,40 @@ func (f *File) drawPie3DChart(formatSet *formatChart) *cPlotArea { } } +// drawPieOfPieChart provides a function to draw the c:plotArea element for +// pie chart by given format sets. +func (f *File) drawPieOfPieChart(formatSet *formatChart) *cPlotArea { + return &cPlotArea{ + PieChart: &cCharts{ + OfPieType: &attrValString{ + Val: stringPtr("pie"), + }, + VaryColors: &attrValBool{ + Val: boolPtr(true), + }, + Ser: f.drawChartSeries(formatSet), + SerLines: &attrValString{}, + }, + } +} + +// drawBarOfPieChart provides a function to draw the c:plotArea element for +// pie chart by given format sets. +func (f *File) drawBarOfPieChart(formatSet *formatChart) *cPlotArea { + return &cPlotArea{ + PieChart: &cCharts{ + OfPieType: &attrValString{ + Val: stringPtr("bar"), + }, + VaryColors: &attrValBool{ + Val: boolPtr(true), + }, + Ser: f.drawChartSeries(formatSet), + SerLines: &attrValString{}, + }, + } +} + // drawRadarChart provides a function to draw the c:plotArea element for radar // chart by given format sets. func (f *File) drawRadarChart(formatSet *formatChart) *cPlotArea { @@ -1371,11 +1462,15 @@ 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 { + var baseIdx int + if formatSet.Combo != nil { + baseIdx = len(formatSet.Combo.Series) + } ser := []cSer{} for k := range formatSet.Series { ser = append(ser, cSer{ - IDx: &attrValInt{Val: intPtr(k)}, - Order: &attrValInt{Val: intPtr(k)}, + IDx: &attrValInt{Val: intPtr(k + baseIdx)}, + Order: &attrValInt{Val: intPtr(k + baseIdx)}, Tx: &cTx{ StrRef: &cStrRef{ F: formatSet.Series[k].Name, diff --git a/chart_test.go b/chart_test.go index 2ed7944deb..e350657e80 100644 --- a/chart_test.go +++ b/chart_test.go @@ -3,6 +3,7 @@ package excelize import ( "bytes" "encoding/xml" + "fmt" "path/filepath" "testing" @@ -172,7 +173,31 @@ func TestAddChart(t *testing.T) { // 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}}`)) + // combo chart + f.NewSheet("Combo Charts") + clusteredColumnCombo := map[string][]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"}, + } + for axis, props := range clusteredColumnCombo { + assert.NoError(t, f.AddChart("Combo Charts", axis, 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},"combo":{"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], props[0]))) + } + 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"}, + } + 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},"combo":{"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], props[0]))) + } assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddChart.xlsx"))) - + // 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/xmlChart.go b/xmlChart.go index b6d041e659..9d6263fc56 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -312,6 +312,7 @@ type cPlotArea struct { LineChart *cCharts `xml:"lineChart"` PieChart *cCharts `xml:"pieChart"` Pie3DChart *cCharts `xml:"pie3DChart"` + OfPieChart *cCharts `xml:"ofPieChart"` RadarChart *cCharts `xml:"radarChart"` ScatterChart *cCharts `xml:"scatterChart"` Surface3DChart *cCharts `xml:"surface3DChart"` @@ -329,6 +330,8 @@ type cCharts struct { Grouping *attrValString `xml:"grouping"` RadarStyle *attrValString `xml:"radarStyle"` ScatterStyle *attrValString `xml:"scatterStyle"` + OfPieType *attrValString `xml:"ofPieType"` + SerLines *attrValString `xml:"serLines"` VaryColors *attrValBool `xml:"varyColors"` Wireframe *attrValBool `xml:"wireframe"` Ser *[]cSer `xml:"ser"` @@ -590,10 +593,11 @@ type formatChart struct { } `json:"fill"` Layout formatLayout `json:"layout"` } `json:"plotarea"` - ShowBlanksAs string `json:"show_blanks_as"` - ShowHiddenData bool `json:"show_hidden_data"` - SetRotation int `json:"set_rotation"` - SetHoleSize int `json:"set_hole_size"` + ShowBlanksAs string `json:"show_blanks_as"` + ShowHiddenData bool `json:"show_hidden_data"` + SetRotation int `json:"set_rotation"` + SetHoleSize int `json:"set_hole_size"` + Combo *formatChart `json:"combo"` } // formatChartLegend directly maps the format settings of the chart legend. From fa7078f06c82ed30f9573caf3c4d24d49f45df5a Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 16 Jan 2020 01:05:22 +0800 Subject: [PATCH 189/957] Specified combo chart as variadic parameters --- chart.go | 40 +++++++++++++++++++++++++--------------- chart_test.go | 5 +++-- xmlChart.go | 10 +++++----- 3 files changed, 33 insertions(+), 22 deletions(-) diff --git a/chart.go b/chart.go index f3b1cd86c5..738ed4b8ea 100644 --- a/chart.go +++ b/chart.go @@ -696,7 +696,7 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // // Set chart size by dimension property. The dimension property is optional. The default width is 480, and height is 290. // -// combo: Specifies tha create a chart that combines two art types in a single +// combo: Specifies the create a chart that combines two art types in a single // chart. For example, create a clustered column - line chart with data // Sheet1!$E$1:$L$15: // @@ -714,7 +714,7 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // 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},"legend":{"position":"left","show_legend_key":false},"title":{"name":"Clustered Column - 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},"combo":{"type":"line","series":[{"name":"Sheet1!$A$4","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$4:$D$4"}],"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}}}`); err != nil { +// 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},"legend":{"position":"left","show_legend_key":false},"title":{"name":"Clustered Column - 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}}`, `{"type":"line","series":[{"name":"Sheet1!$A$4","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$4:$D$4"}],"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}}`); err != nil { // println(err.Error()) // return // } @@ -724,11 +724,22 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // } // } // -func (f *File) AddChart(sheet, cell, format string) error { +func (f *File) AddChart(sheet, cell, format string, combo ...string) error { formatSet, err := parseFormatChartSet(format) if err != nil { return err } + comboCharts := []*formatChart{} + for _, comboFormat := range combo { + comboChart, err := parseFormatChartSet(comboFormat) + if err != nil { + return err + } + if _, ok := chartValAxNumFmtFormatCode[comboChart.Type]; !ok { + return errors.New("unsupported chart type " + comboChart.Type) + } + comboCharts = append(comboCharts, comboChart) + } // Read sheet data. xlsx, err := f.workSheetReader(sheet) if err != nil { @@ -748,7 +759,7 @@ func (f *File) AddChart(sheet, cell, format string) error { if err != nil { return err } - f.addChart(formatSet) + f.addChart(formatSet, comboCharts) f.addContentTypePart(chartID, "chart") f.addContentTypePart(drawingID, "drawings") return err @@ -786,7 +797,7 @@ func (f *File) prepareDrawing(xlsx *xlsxWorksheet, drawingID int, sheet, drawing // addChart provides a function to create chart as xl/charts/chart%d.xml by // given format sets. -func (f *File) addChart(formatSet *formatChart) { +func (f *File) addChart(formatSet *formatChart, comboCharts []*formatChart) { count := f.countCharts() xlsxChartSpace := xlsxChartSpace{ XMLNSc: NameSpaceDrawingMLChart, @@ -980,8 +991,11 @@ func (f *File) addChart(formatSet *formatChart) { } } addChart(xlsxChartSpace.Chart.PlotArea, plotAreaFunc[formatSet.Type](formatSet)) - if formatSet.Combo != nil { - addChart(xlsxChartSpace.Chart.PlotArea, plotAreaFunc[formatSet.Combo.Type](formatSet.Combo)) + order := len(formatSet.Series) + for idx := range comboCharts { + comboCharts[idx].order = order + addChart(xlsxChartSpace.Chart.PlotArea, plotAreaFunc[comboCharts[idx].Type](comboCharts[idx])) + order += len(comboCharts[idx].Series) } chart, _ := xml.Marshal(xlsxChartSpace) media := "xl/charts/chart" + strconv.Itoa(count+1) + ".xml" @@ -1462,15 +1476,11 @@ 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 { - var baseIdx int - if formatSet.Combo != nil { - baseIdx = len(formatSet.Combo.Series) - } ser := []cSer{} for k := range formatSet.Series { ser = append(ser, cSer{ - IDx: &attrValInt{Val: intPtr(k + baseIdx)}, - Order: &attrValInt{Val: intPtr(k + baseIdx)}, + IDx: &attrValInt{Val: intPtr(k + formatSet.order)}, + Order: &attrValInt{Val: intPtr(k + formatSet.order)}, Tx: &cTx{ StrRef: &cStrRef{ F: formatSet.Series[k].Name, @@ -1506,9 +1516,9 @@ func (f *File) drawChartSeriesSpPr(i int, formatSet *formatChart) *cSpPr { Cap: "rnd", // rnd, sq, flat }, } - if i < 6 { + if i+formatSet.order < 6 { spPrLine.Ln.SolidFill = &aSolidFill{ - SchemeClr: &aSchemeClr{Val: "accent" + strconv.Itoa(i+1)}, + SchemeClr: &aSchemeClr{Val: "accent" + strconv.Itoa(i+formatSet.order+1)}, } } chartSeriesSpPr := map[string]*cSpPr{Line: spPrLine, Scatter: spPrScatter} diff --git a/chart_test.go b/chart_test.go index e350657e80..bb7d12c742 100644 --- a/chart_test.go +++ b/chart_test.go @@ -186,7 +186,7 @@ func TestAddChart(t *testing.T) { "Y1": {"doughnut", "Clustered Column - Doughnut Chart"}, } for axis, props := range clusteredColumnCombo { - assert.NoError(t, f.AddChart("Combo Charts", axis, 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},"combo":{"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], props[0]))) + assert.NoError(t, f.AddChart("Combo Charts", axis, 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[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]))) } stackedAreaCombo := map[string][]string{ "A16": {"line", "Stacked Area - Line Chart"}, @@ -195,9 +195,10 @@ 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},"combo":{"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], props[0]))) + 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 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", "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") } diff --git a/xmlChart.go b/xmlChart.go index 9d6263fc56..55114696d4 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -593,11 +593,11 @@ type formatChart struct { } `json:"fill"` Layout formatLayout `json:"layout"` } `json:"plotarea"` - ShowBlanksAs string `json:"show_blanks_as"` - ShowHiddenData bool `json:"show_hidden_data"` - SetRotation int `json:"set_rotation"` - SetHoleSize int `json:"set_hole_size"` - Combo *formatChart `json:"combo"` + ShowBlanksAs string `json:"show_blanks_as"` + ShowHiddenData bool `json:"show_hidden_data"` + SetRotation int `json:"set_rotation"` + SetHoleSize int `json:"set_hole_size"` + order int } // formatChartLegend directly maps the format settings of the chart legend. From 0bb245523aada34c7b3d30f0f6e9b16d9f78e7b8 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 19 Jan 2020 00:23:00 +0800 Subject: [PATCH 190/957] Resolve #557, init delete chart support --- chart.go | 1240 +++---------------------------------------------- chart_test.go | 24 + drawing.go | 1209 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 1287 insertions(+), 1186 deletions(-) create mode 100644 drawing.go diff --git a/chart.go b/chart.go index 738ed4b8ea..5f06c55870 100644 --- a/chart.go +++ b/chart.go @@ -12,11 +12,9 @@ package excelize import ( "bytes" "encoding/json" - "encoding/xml" "errors" + "fmt" "io" - "log" - "reflect" "strconv" "strings" ) @@ -765,1205 +763,75 @@ func (f *File) AddChart(sheet, cell, format string, combo ...string) error { return err } -// countCharts provides a function to get chart files count storage in the -// folder xl/charts. -func (f *File) countCharts() int { - count := 0 - for k := range f.XLSX { - if strings.Contains(k, "xl/charts/chart") { - count++ - } - } - return count -} - -// prepareDrawing provides a function to prepare drawing ID and XML by given -// drawingID, worksheet name and default drawingXML. -func (f *File) prepareDrawing(xlsx *xlsxWorksheet, drawingID int, sheet, drawingXML string) (int, string) { - sheetRelationshipsDrawingXML := "../drawings/drawing" + strconv.Itoa(drawingID) + ".xml" - if xlsx.Drawing != nil { - // The worksheet already has a picture or chart relationships, use the relationships drawing ../drawings/drawing%d.xml. - sheetRelationshipsDrawingXML = f.getSheetRelationshipsTargetByID(sheet, xlsx.Drawing.RID) - drawingID, _ = strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(sheetRelationshipsDrawingXML, "../drawings/drawing"), ".xml")) - drawingXML = strings.Replace(sheetRelationshipsDrawingXML, "..", "xl", -1) - } else { - // Add first picture for given sheet. - sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(f.sheetMap[trimSheetName(sheet)], "xl/worksheets/") + ".rels" - rID := f.addRels(sheetRels, SourceRelationshipDrawingML, sheetRelationshipsDrawingXML, "") - f.addSheetDrawing(sheet, rID) +// DeleteChart provides a function to delete chart in XLSX by given worksheet +// and cell name. +func (f *File) DeleteChart(sheet, cell string) (err error) { + var wsDr *xlsxWsDr + col, row, err := CellNameToCoordinates(cell) + if err != nil { + return } - return drawingID, drawingXML -} - -// 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) { - count := f.countCharts() - xlsxChartSpace := xlsxChartSpace{ - XMLNSc: NameSpaceDrawingMLChart, - XMLNSa: NameSpaceDrawingML, - XMLNSr: SourceRelationship, - XMLNSc16r2: SourceRelationshipChart201506, - Date1904: &attrValBool{Val: boolPtr(false)}, - Lang: &attrValString{Val: stringPtr("en-US")}, - RoundedCorners: &attrValBool{Val: boolPtr(false)}, - Chart: cChart{ - 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), - }, - }, - }, - Ea: &aEa{ - Typeface: "+mn-ea", - }, - Cs: &aCs{ - Typeface: "+mn-cs", - }, - Latin: &aLatin{ - Typeface: "+mn-lt", - }, - }, - }, - R: &aR{ - RPr: aRPr{ - Lang: "en-US", - AltLang: "en-US", - }, - T: formatSet.Title.Name, - }, - }, - }, - }, - TxPr: cTxPr{ - P: aP{ - PPr: &aPPr{ - DefRPr: aRPr{ - Kern: 1200, - U: "none", - Sz: 14000, - Strike: "noStrike", - }, - }, - EndParaRPr: &aEndParaRPr{ - Lang: "en-US", - }, - }, - }, - 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])}, - }, - Floor: &cThicknessSpPr{ - Thickness: &attrValInt{Val: intPtr(0)}, - }, - SideWall: &cThicknessSpPr{ - Thickness: &attrValInt{Val: intPtr(0)}, - }, - BackWall: &cThicknessSpPr{ - Thickness: &attrValInt{Val: intPtr(0)}, - }, - PlotArea: &cPlotArea{}, - Legend: &cLegend{ - LegendPos: &attrValString{Val: stringPtr(chartLegendPosition[formatSet.Legend.Position])}, - Overlay: &attrValBool{Val: boolPtr(false)}, - }, - - PlotVisOnly: &attrValBool{Val: boolPtr(false)}, - DispBlanksAs: &attrValString{Val: stringPtr(formatSet.ShowBlanksAs)}, - ShowDLblsOverMax: &attrValBool{Val: boolPtr(false)}, - }, - SpPr: &cSpPr{ - SolidFill: &aSolidFill{ - SchemeClr: &aSchemeClr{Val: "bg1"}, - }, - Ln: &aLn{ - W: 9525, - Cap: "flat", - Cmpd: "sng", - Algn: "ctr", - SolidFill: &aSolidFill{ - SchemeClr: &aSchemeClr{Val: "tx1", - LumMod: &attrValInt{ - Val: intPtr(15000), - }, - LumOff: &attrValInt{ - Val: intPtr(85000), - }, - }, - }, - }, - }, - PrintSettings: &cPrintSettings{ - PageMargins: &cPageMargins{ - B: 0.75, - L: 0.7, - R: 0.7, - T: 0.7, - Header: 0.3, - Footer: 0.3, - }, - }, + col-- + row-- + ws, err := f.workSheetReader(sheet) + if err != nil { + return } - plotAreaFunc := map[string]func(*formatChart) *cPlotArea{ - Area: f.drawBaseChart, - AreaStacked: f.drawBaseChart, - AreaPercentStacked: f.drawBaseChart, - Area3D: f.drawBaseChart, - Area3DStacked: f.drawBaseChart, - Area3DPercentStacked: f.drawBaseChart, - Bar: f.drawBaseChart, - BarStacked: f.drawBaseChart, - BarPercentStacked: f.drawBaseChart, - Bar3DClustered: f.drawBaseChart, - Bar3DStacked: f.drawBaseChart, - Bar3DPercentStacked: f.drawBaseChart, - Bar3DConeClustered: f.drawBaseChart, - Bar3DConeStacked: f.drawBaseChart, - Bar3DConePercentStacked: f.drawBaseChart, - Bar3DPyramidClustered: f.drawBaseChart, - Bar3DPyramidStacked: f.drawBaseChart, - Bar3DPyramidPercentStacked: f.drawBaseChart, - Bar3DCylinderClustered: f.drawBaseChart, - Bar3DCylinderStacked: f.drawBaseChart, - Bar3DCylinderPercentStacked: f.drawBaseChart, - Col: f.drawBaseChart, - ColStacked: f.drawBaseChart, - ColPercentStacked: f.drawBaseChart, - Col3D: f.drawBaseChart, - Col3DClustered: f.drawBaseChart, - Col3DStacked: f.drawBaseChart, - Col3DPercentStacked: f.drawBaseChart, - Col3DCone: f.drawBaseChart, - Col3DConeClustered: f.drawBaseChart, - Col3DConeStacked: f.drawBaseChart, - Col3DConePercentStacked: f.drawBaseChart, - Col3DPyramid: f.drawBaseChart, - Col3DPyramidClustered: f.drawBaseChart, - Col3DPyramidStacked: f.drawBaseChart, - Col3DPyramidPercentStacked: f.drawBaseChart, - Col3DCylinder: f.drawBaseChart, - Col3DCylinderClustered: f.drawBaseChart, - Col3DCylinderStacked: f.drawBaseChart, - Col3DCylinderPercentStacked: f.drawBaseChart, - Doughnut: f.drawDoughnutChart, - Line: f.drawLineChart, - Pie3D: f.drawPie3DChart, - Pie: f.drawPieChart, - PieOfPieChart: f.drawPieOfPieChart, - BarOfPieChart: f.drawBarOfPieChart, - Radar: f.drawRadarChart, - Scatter: f.drawScatterChart, - Surface3D: f.drawSurface3DChart, - WireframeSurface3D: f.drawSurface3DChart, - Contour: f.drawSurfaceChart, - WireframeContour: f.drawSurfaceChart, - Bubble: f.drawBaseChart, - Bubble3D: f.drawBaseChart, + if ws.Drawing == nil { + return } - addChart := func(c, p *cPlotArea) { - immutable, mutable := reflect.ValueOf(c).Elem(), reflect.ValueOf(p).Elem() - for i := 0; i < mutable.NumField(); i++ { - field := mutable.Field(i) - if field.IsNil() { - continue + drawingXML := strings.Replace(f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID), "..", "xl", -1) + wsDr, _ = f.drawingParser(drawingXML) + for idx, anchor := range wsDr.TwoCellAnchor { + if err = nil; anchor.From != nil && anchor.Pic == nil { + if anchor.From.Col == col && anchor.From.Row == row { + wsDr.TwoCellAnchor = append(wsDr.TwoCellAnchor[:idx], wsDr.TwoCellAnchor[idx+1:]...) } - immutable.FieldByName(mutable.Type().Field(i).Name).Set(field) - } - } - addChart(xlsxChartSpace.Chart.PlotArea, plotAreaFunc[formatSet.Type](formatSet)) - order := len(formatSet.Series) - for idx := range comboCharts { - comboCharts[idx].order = order - addChart(xlsxChartSpace.Chart.PlotArea, plotAreaFunc[comboCharts[idx].Type](comboCharts[idx])) - order += len(comboCharts[idx].Series) - } - chart, _ := xml.Marshal(xlsxChartSpace) - media := "xl/charts/chart" + strconv.Itoa(count+1) + ".xml" - f.saveFileList(media, chart) -} - -// 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 { - c := cCharts{ - BarDir: &attrValString{ - Val: stringPtr("col"), - }, - Grouping: &attrValString{ - Val: stringPtr("clustered"), - }, - VaryColors: &attrValBool{ - Val: boolPtr(true), - }, - Ser: f.drawChartSeries(formatSet), - Shape: f.drawChartShape(formatSet), - DLbls: f.drawChartDLbls(formatSet), - AxID: []*attrValInt{ - {Val: intPtr(754001152)}, - {Val: intPtr(753999904)}, - }, - Overlap: &attrValInt{Val: intPtr(100)}, - } - var ok bool - if *c.BarDir.Val, ok = plotAreaChartBarDir[formatSet.Type]; !ok { - c.BarDir = nil - } - if *c.Grouping.Val, ok = plotAreaChartGrouping[formatSet.Type]; !ok { - c.Grouping = nil - } - if *c.Overlap.Val, ok = plotAreaChartOverlap[formatSet.Type]; !ok { - c.Overlap = nil - } - catAx := f.drawPlotAreaCatAx(formatSet) - valAx := f.drawPlotAreaValAx(formatSet) - charts := map[string]*cPlotArea{ - "area": { - AreaChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "areaStacked": { - AreaChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "areaPercentStacked": { - AreaChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "area3D": { - Area3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "area3DStacked": { - Area3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "area3DPercentStacked": { - Area3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "bar": { - BarChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "barStacked": { - BarChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "barPercentStacked": { - BarChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "bar3DClustered": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "bar3DStacked": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "bar3DPercentStacked": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "bar3DConeClustered": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "bar3DConeStacked": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "bar3DConePercentStacked": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "bar3DPyramidClustered": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "bar3DPyramidStacked": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "bar3DPyramidPercentStacked": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "bar3DCylinderClustered": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "bar3DCylinderStacked": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "bar3DCylinderPercentStacked": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "col": { - BarChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "colStacked": { - BarChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "colPercentStacked": { - BarChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "col3D": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "col3DClustered": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "col3DStacked": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "col3DPercentStacked": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "col3DCone": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "col3DConeClustered": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "col3DConeStacked": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "col3DConePercentStacked": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "col3DPyramid": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "col3DPyramidClustered": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "col3DPyramidStacked": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "col3DPyramidPercentStacked": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "col3DCylinder": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "col3DCylinderClustered": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "col3DCylinderStacked": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "col3DCylinderPercentStacked": { - Bar3DChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "bubble": { - BubbleChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - "bubble3D": { - BubbleChart: &c, - CatAx: catAx, - ValAx: valAx, - }, - } - return charts[formatSet.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 { - return &cPlotArea{ - DoughnutChart: &cCharts{ - VaryColors: &attrValBool{ - Val: boolPtr(true), - }, - Ser: f.drawChartSeries(formatSet), - HoleSize: &attrValInt{Val: intPtr(75)}, - }, - } -} - -// drawLineChart provides a function to draw the c:plotArea element for line -// chart by given format sets. -func (f *File) drawLineChart(formatSet *formatChart) *cPlotArea { - return &cPlotArea{ - LineChart: &cCharts{ - Grouping: &attrValString{ - Val: stringPtr(plotAreaChartGrouping[formatSet.Type]), - }, - VaryColors: &attrValBool{ - Val: boolPtr(false), - }, - Ser: f.drawChartSeries(formatSet), - DLbls: f.drawChartDLbls(formatSet), - Smooth: &attrValBool{ - Val: boolPtr(false), - }, - AxID: []*attrValInt{ - {Val: intPtr(754001152)}, - {Val: intPtr(753999904)}, - }, - }, - CatAx: f.drawPlotAreaCatAx(formatSet), - ValAx: f.drawPlotAreaValAx(formatSet), - } -} - -// drawPieChart provides a function to draw the c:plotArea element for pie -// chart by given format sets. -func (f *File) drawPieChart(formatSet *formatChart) *cPlotArea { - return &cPlotArea{ - PieChart: &cCharts{ - VaryColors: &attrValBool{ - Val: boolPtr(true), - }, - Ser: f.drawChartSeries(formatSet), - }, - } -} - -// 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 { - return &cPlotArea{ - Pie3DChart: &cCharts{ - VaryColors: &attrValBool{ - Val: boolPtr(true), - }, - Ser: f.drawChartSeries(formatSet), - }, - } -} - -// drawPieOfPieChart provides a function to draw the c:plotArea element for -// pie chart by given format sets. -func (f *File) drawPieOfPieChart(formatSet *formatChart) *cPlotArea { - return &cPlotArea{ - PieChart: &cCharts{ - OfPieType: &attrValString{ - Val: stringPtr("pie"), - }, - VaryColors: &attrValBool{ - Val: boolPtr(true), - }, - Ser: f.drawChartSeries(formatSet), - SerLines: &attrValString{}, - }, - } -} - -// drawBarOfPieChart provides a function to draw the c:plotArea element for -// pie chart by given format sets. -func (f *File) drawBarOfPieChart(formatSet *formatChart) *cPlotArea { - return &cPlotArea{ - PieChart: &cCharts{ - OfPieType: &attrValString{ - Val: stringPtr("bar"), - }, - VaryColors: &attrValBool{ - Val: boolPtr(true), - }, - Ser: f.drawChartSeries(formatSet), - SerLines: &attrValString{}, - }, - } -} - -// drawRadarChart provides a function to draw the c:plotArea element for radar -// chart by given format sets. -func (f *File) drawRadarChart(formatSet *formatChart) *cPlotArea { - return &cPlotArea{ - RadarChart: &cCharts{ - RadarStyle: &attrValString{ - Val: stringPtr("marker"), - }, - VaryColors: &attrValBool{ - Val: boolPtr(false), - }, - Ser: f.drawChartSeries(formatSet), - DLbls: f.drawChartDLbls(formatSet), - AxID: []*attrValInt{ - {Val: intPtr(754001152)}, - {Val: intPtr(753999904)}, - }, - }, - CatAx: f.drawPlotAreaCatAx(formatSet), - ValAx: f.drawPlotAreaValAx(formatSet), - } -} - -// drawScatterChart provides a function to draw the c:plotArea element for -// scatter chart by given format sets. -func (f *File) drawScatterChart(formatSet *formatChart) *cPlotArea { - return &cPlotArea{ - ScatterChart: &cCharts{ - ScatterStyle: &attrValString{ - Val: stringPtr("smoothMarker"), // line,lineMarker,marker,none,smooth,smoothMarker - }, - VaryColors: &attrValBool{ - Val: boolPtr(false), - }, - Ser: f.drawChartSeries(formatSet), - DLbls: f.drawChartDLbls(formatSet), - AxID: []*attrValInt{ - {Val: intPtr(754001152)}, - {Val: intPtr(753999904)}, - }, - }, - CatAx: f.drawPlotAreaCatAx(formatSet), - ValAx: f.drawPlotAreaValAx(formatSet), - } -} - -// drawSurface3DChart provides a function to draw the c:surface3DChart element by -// given format sets. -func (f *File) drawSurface3DChart(formatSet *formatChart) *cPlotArea { - plotArea := &cPlotArea{ - Surface3DChart: &cCharts{ - Ser: f.drawChartSeries(formatSet), - AxID: []*attrValInt{ - {Val: intPtr(754001152)}, - {Val: intPtr(753999904)}, - {Val: intPtr(832256642)}, - }, - }, - CatAx: f.drawPlotAreaCatAx(formatSet), - ValAx: f.drawPlotAreaValAx(formatSet), - SerAx: f.drawPlotAreaSerAx(formatSet), - } - if formatSet.Type == WireframeSurface3D { - plotArea.Surface3DChart.Wireframe = &attrValBool{Val: boolPtr(true)} - } - return plotArea -} - -// drawSurfaceChart provides a function to draw the c:surfaceChart element by -// given format sets. -func (f *File) drawSurfaceChart(formatSet *formatChart) *cPlotArea { - plotArea := &cPlotArea{ - SurfaceChart: &cCharts{ - Ser: f.drawChartSeries(formatSet), - AxID: []*attrValInt{ - {Val: intPtr(754001152)}, - {Val: intPtr(753999904)}, - {Val: intPtr(832256642)}, - }, - }, - CatAx: f.drawPlotAreaCatAx(formatSet), - ValAx: f.drawPlotAreaValAx(formatSet), - SerAx: f.drawPlotAreaSerAx(formatSet), - } - if formatSet.Type == WireframeContour { - plotArea.SurfaceChart.Wireframe = &attrValBool{Val: boolPtr(true)} - } - return plotArea -} - -// drawChartShape provides a function to draw the c:shape element by given -// format sets. -func (f *File) drawChartShape(formatSet *formatChart) *attrValString { - shapes := map[string]string{ - Bar3DConeClustered: "cone", - Bar3DConeStacked: "cone", - Bar3DConePercentStacked: "cone", - Bar3DPyramidClustered: "pyramid", - Bar3DPyramidStacked: "pyramid", - Bar3DPyramidPercentStacked: "pyramid", - Bar3DCylinderClustered: "cylinder", - Bar3DCylinderStacked: "cylinder", - Bar3DCylinderPercentStacked: "cylinder", - Col3DCone: "cone", - Col3DConeClustered: "cone", - Col3DConeStacked: "cone", - Col3DConePercentStacked: "cone", - Col3DPyramid: "pyramid", - Col3DPyramidClustered: "pyramid", - Col3DPyramidStacked: "pyramid", - Col3DPyramidPercentStacked: "pyramid", - Col3DCylinder: "cylinder", - Col3DCylinderClustered: "cylinder", - Col3DCylinderStacked: "cylinder", - Col3DCylinderPercentStacked: "cylinder", - } - if shape, ok := shapes[formatSet.Type]; ok { - return &attrValString{Val: stringPtr(shape)} - } - return nil -} - -// drawChartSeries provides a function to draw the c:ser element by given -// format sets. -func (f *File) drawChartSeries(formatSet *formatChart) *[]cSer { - ser := []cSer{} - for k := range formatSet.Series { - ser = append(ser, cSer{ - IDx: &attrValInt{Val: intPtr(k + formatSet.order)}, - Order: &attrValInt{Val: intPtr(k + formatSet.order)}, - Tx: &cTx{ - StrRef: &cStrRef{ - F: formatSet.Series[k].Name, - }, - }, - SpPr: f.drawChartSeriesSpPr(k, formatSet), - Marker: f.drawChartSeriesMarker(k, formatSet), - DPt: f.drawChartSeriesDPt(k, formatSet), - DLbls: f.drawChartSeriesDLbls(formatSet), - Cat: f.drawChartSeriesCat(formatSet.Series[k], formatSet), - 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), - }) - } - return &ser -} - -// drawChartSeriesSpPr provides a function to draw the c:spPr element by given -// format sets. -func (f *File) drawChartSeriesSpPr(i int, formatSet *formatChart) *cSpPr { - spPrScatter := &cSpPr{ - Ln: &aLn{ - W: 25400, - NoFill: " ", - }, - } - spPrLine := &cSpPr{ - Ln: &aLn{ - W: f.ptToEMUs(formatSet.Series[i].Line.Width), - Cap: "rnd", // rnd, sq, flat - }, - } - if i+formatSet.order < 6 { - spPrLine.Ln.SolidFill = &aSolidFill{ - SchemeClr: &aSchemeClr{Val: "accent" + strconv.Itoa(i+formatSet.order+1)}, - } - } - chartSeriesSpPr := map[string]*cSpPr{Line: spPrLine, Scatter: spPrScatter} - return chartSeriesSpPr[formatSet.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 { - dpt := []*cDPt{{ - IDx: &attrValInt{Val: intPtr(i)}, - Bubble3D: &attrValBool{Val: boolPtr(false)}, - SpPr: &cSpPr{ - SolidFill: &aSolidFill{ - SchemeClr: &aSchemeClr{Val: "accent" + strconv.Itoa(i+1)}, - }, - Ln: &aLn{ - W: 25400, - Cap: "rnd", - SolidFill: &aSolidFill{ - SchemeClr: &aSchemeClr{Val: "lt" + strconv.Itoa(i+1)}, - }, - }, - Sp3D: &aSp3D{ - ContourW: 25400, - ContourClr: &aContourClr{ - SchemeClr: &aSchemeClr{Val: "lt" + strconv.Itoa(i+1)}, - }, - }, - }, - }} - chartSeriesDPt := map[string][]*cDPt{Pie: dpt, Pie3D: dpt} - return chartSeriesDPt[formatSet.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 { - 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 == "" { - return nil - } - return cat -} - -// 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 { - val := &cVal{ - NumRef: &cNumRef{ - F: v.Values, - }, - } - chartSeriesVal := map[string]*cVal{Scatter: nil, Bubble: nil, Bubble3D: nil} - if _, ok := chartSeriesVal[formatSet.Type]; ok { - return nil - } - return val -} - -// 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 { - marker := &cMarker{ - Symbol: &attrValString{Val: stringPtr("circle")}, - Size: &attrValInt{Val: intPtr(5)}, - } - if i < 6 { - marker.SpPr = &cSpPr{ - SolidFill: &aSolidFill{ - SchemeClr: &aSchemeClr{ - Val: "accent" + strconv.Itoa(i+1), - }, - }, - Ln: &aLn{ - W: 9252, - SolidFill: &aSolidFill{ - SchemeClr: &aSchemeClr{ - Val: "accent" + strconv.Itoa(i+1), - }, - }, - }, } } - chartSeriesMarker := map[string]*cMarker{Scatter: marker} - return chartSeriesMarker[formatSet.Type] + return f.deleteChart(col, row, drawingXML, wsDr) } -// 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 { - cat := &cCat{ - StrRef: &cStrRef{ - F: v.Categories, - }, - } - chartSeriesXVal := map[string]*cCat{Scatter: cat} - return chartSeriesXVal[formatSet.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 { - val := &cVal{ - NumRef: &cNumRef{ - F: v.Values, - }, - } - chartSeriesYVal := map[string]*cVal{Scatter: val, Bubble: val, Bubble3D: val} - return chartSeriesYVal[formatSet.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 { - return nil - } - return &cVal{ - NumRef: &cNumRef{ - F: v.Values, - }, - } -} - -// 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 { - return nil - } - return &attrValBool{Val: boolPtr(true)} -} - -// drawChartDLbls provides a function to draw the c:dLbls element by given -// format sets. -func (f *File) drawChartDLbls(formatSet *formatChart) *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)}, - } -} - -// 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) - chartSeriesDLbls := map[string]*cDLbls{Scatter: nil, Surface3D: nil, WireframeSurface3D: nil, Contour: nil, WireframeContour: nil, Bubble: nil, Bubble3D: nil} - if _, ok := chartSeriesDLbls[formatSet.Type]; ok { - return nil - } - return dLbls -} - -// 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 = nil - } - axs := []*cAxs{ - { - AxID: &attrValInt{Val: intPtr(754001152)}, - Scaling: &cScaling{ - Orientation: &attrValString{Val: stringPtr(orientation[formatSet.XAxis.ReverseOrder])}, - Max: max, - Min: min, - }, - Delete: &attrValBool{Val: boolPtr(false)}, - AxPos: &attrValString{Val: stringPtr(catAxPos[formatSet.XAxis.ReverseOrder])}, - NumFmt: &cNumFmt{ - FormatCode: "General", - SourceLinked: true, - }, - MajorTickMark: &attrValString{Val: stringPtr("none")}, - MinorTickMark: &attrValString{Val: stringPtr("none")}, - TickLblPos: &attrValString{Val: stringPtr("nextTo")}, - SpPr: f.drawPlotAreaSpPr(), - TxPr: f.drawPlotAreaTxPr(), - CrossAx: &attrValInt{Val: intPtr(753999904)}, - Crosses: &attrValString{Val: stringPtr("autoZero")}, - Auto: &attrValBool{Val: boolPtr(true)}, - LblAlgn: &attrValString{Val: stringPtr("ctr")}, - LblOffset: &attrValInt{Val: intPtr(100)}, - NoMultiLvlLbl: &attrValBool{Val: boolPtr(false)}, - }, - } - if formatSet.XAxis.MajorGridlines { - axs[0].MajorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()} - } - if formatSet.XAxis.MinorGridlines { - axs[0].MinorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()} - } - if formatSet.XAxis.TickLabelSkip != 0 { - axs[0].TickLblSkip = &attrValInt{Val: intPtr(formatSet.XAxis.TickLabelSkip)} - } - return axs -} - -// 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 = nil - } - axs := []*cAxs{ - { - AxID: &attrValInt{Val: intPtr(753999904)}, - Scaling: &cScaling{ - Orientation: &attrValString{Val: stringPtr(orientation[formatSet.YAxis.ReverseOrder])}, - Max: max, - Min: min, - }, - Delete: &attrValBool{Val: boolPtr(false)}, - AxPos: &attrValString{Val: stringPtr(valAxPos[formatSet.YAxis.ReverseOrder])}, - NumFmt: &cNumFmt{ - FormatCode: chartValAxNumFmtFormatCode[formatSet.Type], - SourceLinked: true, - }, - MajorTickMark: &attrValString{Val: stringPtr("none")}, - MinorTickMark: &attrValString{Val: stringPtr("none")}, - TickLblPos: &attrValString{Val: stringPtr("nextTo")}, - SpPr: f.drawPlotAreaSpPr(), - TxPr: f.drawPlotAreaTxPr(), - CrossAx: &attrValInt{Val: intPtr(754001152)}, - Crosses: &attrValString{Val: stringPtr("autoZero")}, - CrossBetween: &attrValString{Val: stringPtr(chartValAxCrossBetween[formatSet.Type])}, - }, - } - if formatSet.YAxis.MajorGridlines { - axs[0].MajorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()} - } - if formatSet.YAxis.MinorGridlines { - axs[0].MinorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()} - } - if pos, ok := valTickLblPos[formatSet.Type]; ok { - axs[0].TickLblPos.Val = stringPtr(pos) - } - if formatSet.YAxis.MajorUnit != 0 { - axs[0].MajorUnit = &attrValFloat{Val: float64Ptr(formatSet.YAxis.MajorUnit)} - } - return axs -} - -// 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 = nil - } - return []*cAxs{ - { - AxID: &attrValInt{Val: intPtr(832256642)}, - Scaling: &cScaling{ - Orientation: &attrValString{Val: stringPtr(orientation[formatSet.YAxis.ReverseOrder])}, - Max: max, - Min: min, - }, - Delete: &attrValBool{Val: boolPtr(false)}, - AxPos: &attrValString{Val: stringPtr(catAxPos[formatSet.XAxis.ReverseOrder])}, - TickLblPos: &attrValString{Val: stringPtr("nextTo")}, - SpPr: f.drawPlotAreaSpPr(), - TxPr: f.drawPlotAreaTxPr(), - CrossAx: &attrValInt{Val: intPtr(753999904)}, - }, - } -} - -// drawPlotAreaSpPr provides a function to draw the c:spPr element. -func (f *File) drawPlotAreaSpPr() *cSpPr { - return &cSpPr{ - Ln: &aLn{ - W: 9525, - Cap: "flat", - Cmpd: "sng", - Algn: "ctr", - SolidFill: &aSolidFill{ - SchemeClr: &aSchemeClr{ - Val: "tx1", - LumMod: &attrValInt{Val: intPtr(15000)}, - LumOff: &attrValInt{Val: intPtr(85000)}, - }, - }, - }, - } -} - -// drawPlotAreaTxPr provides a function to draw the c:txPr element. -func (f *File) drawPlotAreaTxPr() *cTxPr { - return &cTxPr{ - BodyPr: aBodyPr{ - Rot: -60000000, - SpcFirstLastPara: true, - VertOverflow: "ellipsis", - Vert: "horz", - Wrap: "square", - Anchor: "ctr", - AnchorCtr: true, - }, - P: aP{ - PPr: &aPPr{ - DefRPr: aRPr{ - Sz: 900, - B: false, - I: false, - U: "none", - Strike: "noStrike", - Kern: 1200, - Baseline: 0, - SolidFill: &aSolidFill{ - SchemeClr: &aSchemeClr{ - Val: "tx1", - LumMod: &attrValInt{Val: intPtr(15000)}, - LumOff: &attrValInt{Val: intPtr(85000)}, - }, - }, - Latin: &aLatin{Typeface: "+mn-lt"}, - Ea: &aEa{Typeface: "+mn-ea"}, - Cs: &aCs{Typeface: "+mn-cs"}, - }, - }, - EndParaRPr: &aEndParaRPr{Lang: "en-US"}, - }, - } -} - -// drawingParser provides a function to parse drawingXML. In order to solve -// 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) { +// deleteChart provides a function to delete chart graphic frame by given by +// given coordinates. +func (f *File) deleteChart(col, row int, drawingXML string, wsDr *xlsxWsDr) (err error) { var ( - err error - ok bool + deWsDr *decodeWsDr + deTwoCellAnchor *decodeTwoCellAnchor ) - - if f.Drawings[path] == nil { - content := xlsxWsDr{} - content.A = NameSpaceDrawingML - content.Xdr = NameSpaceDrawingMLSpreadSheet - if _, ok = f.XLSX[path]; ok { // Append Model - 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) - } - content.R = decodeWsDr.R - for _, v := range decodeWsDr.OneCellAnchor { - content.OneCellAnchor = append(content.OneCellAnchor, &xdrCellAnchor{ - EditAs: v.EditAs, - GraphicFrame: v.Content, - }) - } - for _, v := range decodeWsDr.TwoCellAnchor { - content.TwoCellAnchor = append(content.TwoCellAnchor, &xdrCellAnchor{ - EditAs: v.EditAs, - GraphicFrame: v.Content, - }) + 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) + return + } + for idx, anchor := range deWsDr.TwoCellAnchor { + deTwoCellAnchor = new(decodeTwoCellAnchor) + if err = f.xmlNewDecoder(bytes.NewReader([]byte("" + anchor.Content + ""))). + Decode(deTwoCellAnchor); err != nil && err != io.EOF { + err = fmt.Errorf("xml decode error: %s", err) + return + } + if err = nil; deTwoCellAnchor.From != nil && deTwoCellAnchor.Pic == nil { + if anchor.From.Col == col && anchor.From.Row == row { + wsDr.TwoCellAnchor = append(wsDr.TwoCellAnchor[:idx], wsDr.TwoCellAnchor[idx+1:]...) } } - f.Drawings[path] = &content } - wsDr := f.Drawings[path] - return wsDr, len(wsDr.OneCellAnchor) + len(wsDr.TwoCellAnchor) + 2 + f.Drawings[drawingXML] = wsDr + return err } -// 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 { - col, row, err := CellNameToCoordinates(cell) - if err != nil { - return err - } - 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) - content, cNvPrID := f.drawingParser(drawingXML) - twoCellAnchor := xdrCellAnchor{} - twoCellAnchor.EditAs = formatSet.Positioning - from := xlsxFrom{} - from.Col = colStart - from.ColOff = formatSet.OffsetX * EMU - from.Row = rowStart - from.RowOff = formatSet.OffsetY * EMU - to := xlsxTo{} - to.Col = colEnd - to.ColOff = x2 * EMU - to.Row = rowEnd - to.RowOff = y2 * EMU - twoCellAnchor.From = &from - twoCellAnchor.To = &to - - graphicFrame := xlsxGraphicFrame{ - NvGraphicFramePr: xlsxNvGraphicFramePr{ - CNvPr: &xlsxCNvPr{ - ID: cNvPrID, - Name: "Chart " + strconv.Itoa(cNvPrID), - }, - }, - Graphic: &xlsxGraphic{ - GraphicData: &xlsxGraphicData{ - URI: NameSpaceDrawingMLChart, - Chart: &xlsxChart{ - C: NameSpaceDrawingMLChart, - R: SourceRelationship, - RID: "rId" + strconv.Itoa(rID), - }, - }, - }, - } - graphic, _ := xml.Marshal(graphicFrame) - twoCellAnchor.GraphicFrame = string(graphic) - twoCellAnchor.ClientData = &xdrClientData{ - FLocksWithSheet: formatSet.FLocksWithSheet, - FPrintsWithSheet: formatSet.FPrintsWithSheet, +// countCharts provides a function to get chart files count storage in the +// folder xl/charts. +func (f *File) countCharts() int { + count := 0 + for k := range f.XLSX { + if strings.Contains(k, "xl/charts/chart") { + count++ + } } - content.TwoCellAnchor = append(content.TwoCellAnchor, &twoCellAnchor) - f.Drawings[drawingXML] = content - return err + return count } // ptToEMUs provides a function to convert pt to EMUs, 1 pt = 12700 EMUs. The diff --git a/chart_test.go b/chart_test.go index bb7d12c742..d8d36d850f 100644 --- a/chart_test.go +++ b/chart_test.go @@ -200,5 +200,29 @@ func TestAddChart(t *testing.T) { assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddChart.xlsx"))) // 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") + // 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") + // 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") } + +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"}`)) + 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") + // Test delete chart with invalid coordinates. + assert.EqualError(t, f.DeleteChart("Sheet1", ""), `cannot convert cell "" to coordinates: invalid cell name ""`) + // Test delete chart with unsupport charset. + f, err = OpenFile(filepath.Join("test", "Book1.xlsx")) + assert.NoError(t, err) + delete(f.Sheet, "xl/drawings/drawing1.xml") + f.XLSX["xl/drawings/drawing1.xml"] = MacintoshCyrillicCharset + assert.EqualError(t, f.DeleteChart("Sheet1", "A1"), "xml decode error: XML syntax error on line 1: invalid UTF-8") + // Test delete chart on no chart worksheet. + assert.NoError(t, NewFile().DeleteChart("Sheet1", "A1")) +} diff --git a/drawing.go b/drawing.go new file mode 100644 index 0000000000..49506a3003 --- /dev/null +++ b/drawing.go @@ -0,0 +1,1209 @@ +// Copyright 2016 - 2020 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. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.10 or later. + +package excelize + +import ( + "bytes" + "encoding/xml" + "io" + "log" + "reflect" + "strconv" + "strings" +) + +// prepareDrawing provides a function to prepare drawing ID and XML by given +// drawingID, worksheet name and default drawingXML. +func (f *File) prepareDrawing(xlsx *xlsxWorksheet, drawingID int, sheet, drawingXML string) (int, string) { + sheetRelationshipsDrawingXML := "../drawings/drawing" + strconv.Itoa(drawingID) + ".xml" + if xlsx.Drawing != nil { + // The worksheet already has a picture or chart relationships, use the relationships drawing ../drawings/drawing%d.xml. + sheetRelationshipsDrawingXML = f.getSheetRelationshipsTargetByID(sheet, xlsx.Drawing.RID) + drawingID, _ = strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(sheetRelationshipsDrawingXML, "../drawings/drawing"), ".xml")) + drawingXML = strings.Replace(sheetRelationshipsDrawingXML, "..", "xl", -1) + } else { + // Add first picture for given sheet. + sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(f.sheetMap[trimSheetName(sheet)], "xl/worksheets/") + ".rels" + rID := f.addRels(sheetRels, SourceRelationshipDrawingML, sheetRelationshipsDrawingXML, "") + f.addSheetDrawing(sheet, rID) + } + return drawingID, drawingXML +} + +// 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) { + count := f.countCharts() + xlsxChartSpace := xlsxChartSpace{ + XMLNSc: NameSpaceDrawingMLChart, + XMLNSa: NameSpaceDrawingML, + XMLNSr: SourceRelationship, + XMLNSc16r2: SourceRelationshipChart201506, + Date1904: &attrValBool{Val: boolPtr(false)}, + Lang: &attrValString{Val: stringPtr("en-US")}, + RoundedCorners: &attrValBool{Val: boolPtr(false)}, + Chart: cChart{ + 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), + }, + }, + }, + Ea: &aEa{ + Typeface: "+mn-ea", + }, + Cs: &aCs{ + Typeface: "+mn-cs", + }, + Latin: &aLatin{ + Typeface: "+mn-lt", + }, + }, + }, + R: &aR{ + RPr: aRPr{ + Lang: "en-US", + AltLang: "en-US", + }, + T: formatSet.Title.Name, + }, + }, + }, + }, + TxPr: cTxPr{ + P: aP{ + PPr: &aPPr{ + DefRPr: aRPr{ + Kern: 1200, + U: "none", + Sz: 14000, + Strike: "noStrike", + }, + }, + EndParaRPr: &aEndParaRPr{ + Lang: "en-US", + }, + }, + }, + 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])}, + }, + Floor: &cThicknessSpPr{ + Thickness: &attrValInt{Val: intPtr(0)}, + }, + SideWall: &cThicknessSpPr{ + Thickness: &attrValInt{Val: intPtr(0)}, + }, + BackWall: &cThicknessSpPr{ + Thickness: &attrValInt{Val: intPtr(0)}, + }, + PlotArea: &cPlotArea{}, + Legend: &cLegend{ + LegendPos: &attrValString{Val: stringPtr(chartLegendPosition[formatSet.Legend.Position])}, + Overlay: &attrValBool{Val: boolPtr(false)}, + }, + + PlotVisOnly: &attrValBool{Val: boolPtr(false)}, + DispBlanksAs: &attrValString{Val: stringPtr(formatSet.ShowBlanksAs)}, + ShowDLblsOverMax: &attrValBool{Val: boolPtr(false)}, + }, + SpPr: &cSpPr{ + SolidFill: &aSolidFill{ + SchemeClr: &aSchemeClr{Val: "bg1"}, + }, + Ln: &aLn{ + W: 9525, + Cap: "flat", + Cmpd: "sng", + Algn: "ctr", + SolidFill: &aSolidFill{ + SchemeClr: &aSchemeClr{Val: "tx1", + LumMod: &attrValInt{ + Val: intPtr(15000), + }, + LumOff: &attrValInt{ + Val: intPtr(85000), + }, + }, + }, + }, + }, + PrintSettings: &cPrintSettings{ + PageMargins: &cPageMargins{ + B: 0.75, + L: 0.7, + R: 0.7, + T: 0.7, + Header: 0.3, + Footer: 0.3, + }, + }, + } + plotAreaFunc := map[string]func(*formatChart) *cPlotArea{ + Area: f.drawBaseChart, + AreaStacked: f.drawBaseChart, + AreaPercentStacked: f.drawBaseChart, + Area3D: f.drawBaseChart, + Area3DStacked: f.drawBaseChart, + Area3DPercentStacked: f.drawBaseChart, + Bar: f.drawBaseChart, + BarStacked: f.drawBaseChart, + BarPercentStacked: f.drawBaseChart, + Bar3DClustered: f.drawBaseChart, + Bar3DStacked: f.drawBaseChart, + Bar3DPercentStacked: f.drawBaseChart, + Bar3DConeClustered: f.drawBaseChart, + Bar3DConeStacked: f.drawBaseChart, + Bar3DConePercentStacked: f.drawBaseChart, + Bar3DPyramidClustered: f.drawBaseChart, + Bar3DPyramidStacked: f.drawBaseChart, + Bar3DPyramidPercentStacked: f.drawBaseChart, + Bar3DCylinderClustered: f.drawBaseChart, + Bar3DCylinderStacked: f.drawBaseChart, + Bar3DCylinderPercentStacked: f.drawBaseChart, + Col: f.drawBaseChart, + ColStacked: f.drawBaseChart, + ColPercentStacked: f.drawBaseChart, + Col3D: f.drawBaseChart, + Col3DClustered: f.drawBaseChart, + Col3DStacked: f.drawBaseChart, + Col3DPercentStacked: f.drawBaseChart, + Col3DCone: f.drawBaseChart, + Col3DConeClustered: f.drawBaseChart, + Col3DConeStacked: f.drawBaseChart, + Col3DConePercentStacked: f.drawBaseChart, + Col3DPyramid: f.drawBaseChart, + Col3DPyramidClustered: f.drawBaseChart, + Col3DPyramidStacked: f.drawBaseChart, + Col3DPyramidPercentStacked: f.drawBaseChart, + Col3DCylinder: f.drawBaseChart, + Col3DCylinderClustered: f.drawBaseChart, + Col3DCylinderStacked: f.drawBaseChart, + Col3DCylinderPercentStacked: f.drawBaseChart, + Doughnut: f.drawDoughnutChart, + Line: f.drawLineChart, + Pie3D: f.drawPie3DChart, + Pie: f.drawPieChart, + PieOfPieChart: f.drawPieOfPieChart, + BarOfPieChart: f.drawBarOfPieChart, + Radar: f.drawRadarChart, + Scatter: f.drawScatterChart, + Surface3D: f.drawSurface3DChart, + WireframeSurface3D: f.drawSurface3DChart, + Contour: f.drawSurfaceChart, + WireframeContour: f.drawSurfaceChart, + Bubble: f.drawBaseChart, + Bubble3D: f.drawBaseChart, + } + addChart := func(c, p *cPlotArea) { + immutable, mutable := reflect.ValueOf(c).Elem(), reflect.ValueOf(p).Elem() + for i := 0; i < mutable.NumField(); i++ { + field := mutable.Field(i) + if field.IsNil() { + continue + } + immutable.FieldByName(mutable.Type().Field(i).Name).Set(field) + } + } + addChart(xlsxChartSpace.Chart.PlotArea, plotAreaFunc[formatSet.Type](formatSet)) + order := len(formatSet.Series) + for idx := range comboCharts { + comboCharts[idx].order = order + addChart(xlsxChartSpace.Chart.PlotArea, plotAreaFunc[comboCharts[idx].Type](comboCharts[idx])) + order += len(comboCharts[idx].Series) + } + chart, _ := xml.Marshal(xlsxChartSpace) + media := "xl/charts/chart" + strconv.Itoa(count+1) + ".xml" + f.saveFileList(media, chart) +} + +// 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 { + c := cCharts{ + BarDir: &attrValString{ + Val: stringPtr("col"), + }, + Grouping: &attrValString{ + Val: stringPtr("clustered"), + }, + VaryColors: &attrValBool{ + Val: boolPtr(true), + }, + Ser: f.drawChartSeries(formatSet), + Shape: f.drawChartShape(formatSet), + DLbls: f.drawChartDLbls(formatSet), + AxID: []*attrValInt{ + {Val: intPtr(754001152)}, + {Val: intPtr(753999904)}, + }, + Overlap: &attrValInt{Val: intPtr(100)}, + } + var ok bool + if *c.BarDir.Val, ok = plotAreaChartBarDir[formatSet.Type]; !ok { + c.BarDir = nil + } + if *c.Grouping.Val, ok = plotAreaChartGrouping[formatSet.Type]; !ok { + c.Grouping = nil + } + if *c.Overlap.Val, ok = plotAreaChartOverlap[formatSet.Type]; !ok { + c.Overlap = nil + } + catAx := f.drawPlotAreaCatAx(formatSet) + valAx := f.drawPlotAreaValAx(formatSet) + charts := map[string]*cPlotArea{ + "area": { + AreaChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "areaStacked": { + AreaChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "areaPercentStacked": { + AreaChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "area3D": { + Area3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "area3DStacked": { + Area3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "area3DPercentStacked": { + Area3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "bar": { + BarChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "barStacked": { + BarChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "barPercentStacked": { + BarChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "bar3DClustered": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "bar3DStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "bar3DPercentStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "bar3DConeClustered": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "bar3DConeStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "bar3DConePercentStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "bar3DPyramidClustered": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "bar3DPyramidStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "bar3DPyramidPercentStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "bar3DCylinderClustered": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "bar3DCylinderStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "bar3DCylinderPercentStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col": { + BarChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "colStacked": { + BarChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "colPercentStacked": { + BarChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3D": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3DClustered": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3DStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3DPercentStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3DCone": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3DConeClustered": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3DConeStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3DConePercentStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3DPyramid": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3DPyramidClustered": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3DPyramidStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3DPyramidPercentStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3DCylinder": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3DCylinderClustered": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3DCylinderStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "col3DCylinderPercentStacked": { + Bar3DChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "bubble": { + BubbleChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + "bubble3D": { + BubbleChart: &c, + CatAx: catAx, + ValAx: valAx, + }, + } + return charts[formatSet.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 { + return &cPlotArea{ + DoughnutChart: &cCharts{ + VaryColors: &attrValBool{ + Val: boolPtr(true), + }, + Ser: f.drawChartSeries(formatSet), + HoleSize: &attrValInt{Val: intPtr(75)}, + }, + } +} + +// drawLineChart provides a function to draw the c:plotArea element for line +// chart by given format sets. +func (f *File) drawLineChart(formatSet *formatChart) *cPlotArea { + return &cPlotArea{ + LineChart: &cCharts{ + Grouping: &attrValString{ + Val: stringPtr(plotAreaChartGrouping[formatSet.Type]), + }, + VaryColors: &attrValBool{ + Val: boolPtr(false), + }, + Ser: f.drawChartSeries(formatSet), + DLbls: f.drawChartDLbls(formatSet), + Smooth: &attrValBool{ + Val: boolPtr(false), + }, + AxID: []*attrValInt{ + {Val: intPtr(754001152)}, + {Val: intPtr(753999904)}, + }, + }, + CatAx: f.drawPlotAreaCatAx(formatSet), + ValAx: f.drawPlotAreaValAx(formatSet), + } +} + +// drawPieChart provides a function to draw the c:plotArea element for pie +// chart by given format sets. +func (f *File) drawPieChart(formatSet *formatChart) *cPlotArea { + return &cPlotArea{ + PieChart: &cCharts{ + VaryColors: &attrValBool{ + Val: boolPtr(true), + }, + Ser: f.drawChartSeries(formatSet), + }, + } +} + +// 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 { + return &cPlotArea{ + Pie3DChart: &cCharts{ + VaryColors: &attrValBool{ + Val: boolPtr(true), + }, + Ser: f.drawChartSeries(formatSet), + }, + } +} + +// drawPieOfPieChart provides a function to draw the c:plotArea element for +// pie chart by given format sets. +func (f *File) drawPieOfPieChart(formatSet *formatChart) *cPlotArea { + return &cPlotArea{ + PieChart: &cCharts{ + OfPieType: &attrValString{ + Val: stringPtr("pie"), + }, + VaryColors: &attrValBool{ + Val: boolPtr(true), + }, + Ser: f.drawChartSeries(formatSet), + SerLines: &attrValString{}, + }, + } +} + +// drawBarOfPieChart provides a function to draw the c:plotArea element for +// pie chart by given format sets. +func (f *File) drawBarOfPieChart(formatSet *formatChart) *cPlotArea { + return &cPlotArea{ + PieChart: &cCharts{ + OfPieType: &attrValString{ + Val: stringPtr("bar"), + }, + VaryColors: &attrValBool{ + Val: boolPtr(true), + }, + Ser: f.drawChartSeries(formatSet), + SerLines: &attrValString{}, + }, + } +} + +// drawRadarChart provides a function to draw the c:plotArea element for radar +// chart by given format sets. +func (f *File) drawRadarChart(formatSet *formatChart) *cPlotArea { + return &cPlotArea{ + RadarChart: &cCharts{ + RadarStyle: &attrValString{ + Val: stringPtr("marker"), + }, + VaryColors: &attrValBool{ + Val: boolPtr(false), + }, + Ser: f.drawChartSeries(formatSet), + DLbls: f.drawChartDLbls(formatSet), + AxID: []*attrValInt{ + {Val: intPtr(754001152)}, + {Val: intPtr(753999904)}, + }, + }, + CatAx: f.drawPlotAreaCatAx(formatSet), + ValAx: f.drawPlotAreaValAx(formatSet), + } +} + +// drawScatterChart provides a function to draw the c:plotArea element for +// scatter chart by given format sets. +func (f *File) drawScatterChart(formatSet *formatChart) *cPlotArea { + return &cPlotArea{ + ScatterChart: &cCharts{ + ScatterStyle: &attrValString{ + Val: stringPtr("smoothMarker"), // line,lineMarker,marker,none,smooth,smoothMarker + }, + VaryColors: &attrValBool{ + Val: boolPtr(false), + }, + Ser: f.drawChartSeries(formatSet), + DLbls: f.drawChartDLbls(formatSet), + AxID: []*attrValInt{ + {Val: intPtr(754001152)}, + {Val: intPtr(753999904)}, + }, + }, + CatAx: f.drawPlotAreaCatAx(formatSet), + ValAx: f.drawPlotAreaValAx(formatSet), + } +} + +// drawSurface3DChart provides a function to draw the c:surface3DChart element by +// given format sets. +func (f *File) drawSurface3DChart(formatSet *formatChart) *cPlotArea { + plotArea := &cPlotArea{ + Surface3DChart: &cCharts{ + Ser: f.drawChartSeries(formatSet), + AxID: []*attrValInt{ + {Val: intPtr(754001152)}, + {Val: intPtr(753999904)}, + {Val: intPtr(832256642)}, + }, + }, + CatAx: f.drawPlotAreaCatAx(formatSet), + ValAx: f.drawPlotAreaValAx(formatSet), + SerAx: f.drawPlotAreaSerAx(formatSet), + } + if formatSet.Type == WireframeSurface3D { + plotArea.Surface3DChart.Wireframe = &attrValBool{Val: boolPtr(true)} + } + return plotArea +} + +// drawSurfaceChart provides a function to draw the c:surfaceChart element by +// given format sets. +func (f *File) drawSurfaceChart(formatSet *formatChart) *cPlotArea { + plotArea := &cPlotArea{ + SurfaceChart: &cCharts{ + Ser: f.drawChartSeries(formatSet), + AxID: []*attrValInt{ + {Val: intPtr(754001152)}, + {Val: intPtr(753999904)}, + {Val: intPtr(832256642)}, + }, + }, + CatAx: f.drawPlotAreaCatAx(formatSet), + ValAx: f.drawPlotAreaValAx(formatSet), + SerAx: f.drawPlotAreaSerAx(formatSet), + } + if formatSet.Type == WireframeContour { + plotArea.SurfaceChart.Wireframe = &attrValBool{Val: boolPtr(true)} + } + return plotArea +} + +// drawChartShape provides a function to draw the c:shape element by given +// format sets. +func (f *File) drawChartShape(formatSet *formatChart) *attrValString { + shapes := map[string]string{ + Bar3DConeClustered: "cone", + Bar3DConeStacked: "cone", + Bar3DConePercentStacked: "cone", + Bar3DPyramidClustered: "pyramid", + Bar3DPyramidStacked: "pyramid", + Bar3DPyramidPercentStacked: "pyramid", + Bar3DCylinderClustered: "cylinder", + Bar3DCylinderStacked: "cylinder", + Bar3DCylinderPercentStacked: "cylinder", + Col3DCone: "cone", + Col3DConeClustered: "cone", + Col3DConeStacked: "cone", + Col3DConePercentStacked: "cone", + Col3DPyramid: "pyramid", + Col3DPyramidClustered: "pyramid", + Col3DPyramidStacked: "pyramid", + Col3DPyramidPercentStacked: "pyramid", + Col3DCylinder: "cylinder", + Col3DCylinderClustered: "cylinder", + Col3DCylinderStacked: "cylinder", + Col3DCylinderPercentStacked: "cylinder", + } + if shape, ok := shapes[formatSet.Type]; ok { + return &attrValString{Val: stringPtr(shape)} + } + return nil +} + +// drawChartSeries provides a function to draw the c:ser element by given +// format sets. +func (f *File) drawChartSeries(formatSet *formatChart) *[]cSer { + ser := []cSer{} + for k := range formatSet.Series { + ser = append(ser, cSer{ + IDx: &attrValInt{Val: intPtr(k + formatSet.order)}, + Order: &attrValInt{Val: intPtr(k + formatSet.order)}, + Tx: &cTx{ + StrRef: &cStrRef{ + F: formatSet.Series[k].Name, + }, + }, + SpPr: f.drawChartSeriesSpPr(k, formatSet), + Marker: f.drawChartSeriesMarker(k, formatSet), + DPt: f.drawChartSeriesDPt(k, formatSet), + DLbls: f.drawChartSeriesDLbls(formatSet), + Cat: f.drawChartSeriesCat(formatSet.Series[k], formatSet), + 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), + }) + } + return &ser +} + +// drawChartSeriesSpPr provides a function to draw the c:spPr element by given +// format sets. +func (f *File) drawChartSeriesSpPr(i int, formatSet *formatChart) *cSpPr { + spPrScatter := &cSpPr{ + Ln: &aLn{ + W: 25400, + NoFill: " ", + }, + } + spPrLine := &cSpPr{ + Ln: &aLn{ + W: f.ptToEMUs(formatSet.Series[i].Line.Width), + Cap: "rnd", // rnd, sq, flat + }, + } + if i+formatSet.order < 6 { + spPrLine.Ln.SolidFill = &aSolidFill{ + SchemeClr: &aSchemeClr{Val: "accent" + strconv.Itoa(i+formatSet.order+1)}, + } + } + chartSeriesSpPr := map[string]*cSpPr{Line: spPrLine, Scatter: spPrScatter} + return chartSeriesSpPr[formatSet.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 { + dpt := []*cDPt{{ + IDx: &attrValInt{Val: intPtr(i)}, + Bubble3D: &attrValBool{Val: boolPtr(false)}, + SpPr: &cSpPr{ + SolidFill: &aSolidFill{ + SchemeClr: &aSchemeClr{Val: "accent" + strconv.Itoa(i+1)}, + }, + Ln: &aLn{ + W: 25400, + Cap: "rnd", + SolidFill: &aSolidFill{ + SchemeClr: &aSchemeClr{Val: "lt" + strconv.Itoa(i+1)}, + }, + }, + Sp3D: &aSp3D{ + ContourW: 25400, + ContourClr: &aContourClr{ + SchemeClr: &aSchemeClr{Val: "lt" + strconv.Itoa(i+1)}, + }, + }, + }, + }} + chartSeriesDPt := map[string][]*cDPt{Pie: dpt, Pie3D: dpt} + return chartSeriesDPt[formatSet.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 { + 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 == "" { + return nil + } + return cat +} + +// 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 { + val := &cVal{ + NumRef: &cNumRef{ + F: v.Values, + }, + } + chartSeriesVal := map[string]*cVal{Scatter: nil, Bubble: nil, Bubble3D: nil} + if _, ok := chartSeriesVal[formatSet.Type]; ok { + return nil + } + return val +} + +// 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 { + marker := &cMarker{ + Symbol: &attrValString{Val: stringPtr("circle")}, + Size: &attrValInt{Val: intPtr(5)}, + } + if i < 6 { + marker.SpPr = &cSpPr{ + SolidFill: &aSolidFill{ + SchemeClr: &aSchemeClr{ + Val: "accent" + strconv.Itoa(i+1), + }, + }, + Ln: &aLn{ + W: 9252, + SolidFill: &aSolidFill{ + SchemeClr: &aSchemeClr{ + Val: "accent" + strconv.Itoa(i+1), + }, + }, + }, + } + } + chartSeriesMarker := map[string]*cMarker{Scatter: marker} + return chartSeriesMarker[formatSet.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 { + cat := &cCat{ + StrRef: &cStrRef{ + F: v.Categories, + }, + } + chartSeriesXVal := map[string]*cCat{Scatter: cat} + return chartSeriesXVal[formatSet.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 { + val := &cVal{ + NumRef: &cNumRef{ + F: v.Values, + }, + } + chartSeriesYVal := map[string]*cVal{Scatter: val, Bubble: val, Bubble3D: val} + return chartSeriesYVal[formatSet.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 { + return nil + } + return &cVal{ + NumRef: &cNumRef{ + F: v.Values, + }, + } +} + +// 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 { + return nil + } + return &attrValBool{Val: boolPtr(true)} +} + +// drawChartDLbls provides a function to draw the c:dLbls element by given +// format sets. +func (f *File) drawChartDLbls(formatSet *formatChart) *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)}, + } +} + +// 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) + chartSeriesDLbls := map[string]*cDLbls{Scatter: nil, Surface3D: nil, WireframeSurface3D: nil, Contour: nil, WireframeContour: nil, Bubble: nil, Bubble3D: nil} + if _, ok := chartSeriesDLbls[formatSet.Type]; ok { + return nil + } + return dLbls +} + +// 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 = nil + } + axs := []*cAxs{ + { + AxID: &attrValInt{Val: intPtr(754001152)}, + Scaling: &cScaling{ + Orientation: &attrValString{Val: stringPtr(orientation[formatSet.XAxis.ReverseOrder])}, + Max: max, + Min: min, + }, + Delete: &attrValBool{Val: boolPtr(false)}, + AxPos: &attrValString{Val: stringPtr(catAxPos[formatSet.XAxis.ReverseOrder])}, + NumFmt: &cNumFmt{ + FormatCode: "General", + SourceLinked: true, + }, + MajorTickMark: &attrValString{Val: stringPtr("none")}, + MinorTickMark: &attrValString{Val: stringPtr("none")}, + TickLblPos: &attrValString{Val: stringPtr("nextTo")}, + SpPr: f.drawPlotAreaSpPr(), + TxPr: f.drawPlotAreaTxPr(), + CrossAx: &attrValInt{Val: intPtr(753999904)}, + Crosses: &attrValString{Val: stringPtr("autoZero")}, + Auto: &attrValBool{Val: boolPtr(true)}, + LblAlgn: &attrValString{Val: stringPtr("ctr")}, + LblOffset: &attrValInt{Val: intPtr(100)}, + NoMultiLvlLbl: &attrValBool{Val: boolPtr(false)}, + }, + } + if formatSet.XAxis.MajorGridlines { + axs[0].MajorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()} + } + if formatSet.XAxis.MinorGridlines { + axs[0].MinorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()} + } + if formatSet.XAxis.TickLabelSkip != 0 { + axs[0].TickLblSkip = &attrValInt{Val: intPtr(formatSet.XAxis.TickLabelSkip)} + } + return axs +} + +// 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 = nil + } + axs := []*cAxs{ + { + AxID: &attrValInt{Val: intPtr(753999904)}, + Scaling: &cScaling{ + Orientation: &attrValString{Val: stringPtr(orientation[formatSet.YAxis.ReverseOrder])}, + Max: max, + Min: min, + }, + Delete: &attrValBool{Val: boolPtr(false)}, + AxPos: &attrValString{Val: stringPtr(valAxPos[formatSet.YAxis.ReverseOrder])}, + NumFmt: &cNumFmt{ + FormatCode: chartValAxNumFmtFormatCode[formatSet.Type], + SourceLinked: true, + }, + MajorTickMark: &attrValString{Val: stringPtr("none")}, + MinorTickMark: &attrValString{Val: stringPtr("none")}, + TickLblPos: &attrValString{Val: stringPtr("nextTo")}, + SpPr: f.drawPlotAreaSpPr(), + TxPr: f.drawPlotAreaTxPr(), + CrossAx: &attrValInt{Val: intPtr(754001152)}, + Crosses: &attrValString{Val: stringPtr("autoZero")}, + CrossBetween: &attrValString{Val: stringPtr(chartValAxCrossBetween[formatSet.Type])}, + }, + } + if formatSet.YAxis.MajorGridlines { + axs[0].MajorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()} + } + if formatSet.YAxis.MinorGridlines { + axs[0].MinorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()} + } + if pos, ok := valTickLblPos[formatSet.Type]; ok { + axs[0].TickLblPos.Val = stringPtr(pos) + } + if formatSet.YAxis.MajorUnit != 0 { + axs[0].MajorUnit = &attrValFloat{Val: float64Ptr(formatSet.YAxis.MajorUnit)} + } + return axs +} + +// 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 = nil + } + return []*cAxs{ + { + AxID: &attrValInt{Val: intPtr(832256642)}, + Scaling: &cScaling{ + Orientation: &attrValString{Val: stringPtr(orientation[formatSet.YAxis.ReverseOrder])}, + Max: max, + Min: min, + }, + Delete: &attrValBool{Val: boolPtr(false)}, + AxPos: &attrValString{Val: stringPtr(catAxPos[formatSet.XAxis.ReverseOrder])}, + TickLblPos: &attrValString{Val: stringPtr("nextTo")}, + SpPr: f.drawPlotAreaSpPr(), + TxPr: f.drawPlotAreaTxPr(), + CrossAx: &attrValInt{Val: intPtr(753999904)}, + }, + } +} + +// drawPlotAreaSpPr provides a function to draw the c:spPr element. +func (f *File) drawPlotAreaSpPr() *cSpPr { + return &cSpPr{ + Ln: &aLn{ + W: 9525, + Cap: "flat", + Cmpd: "sng", + Algn: "ctr", + SolidFill: &aSolidFill{ + SchemeClr: &aSchemeClr{ + Val: "tx1", + LumMod: &attrValInt{Val: intPtr(15000)}, + LumOff: &attrValInt{Val: intPtr(85000)}, + }, + }, + }, + } +} + +// drawPlotAreaTxPr provides a function to draw the c:txPr element. +func (f *File) drawPlotAreaTxPr() *cTxPr { + return &cTxPr{ + BodyPr: aBodyPr{ + Rot: -60000000, + SpcFirstLastPara: true, + VertOverflow: "ellipsis", + Vert: "horz", + Wrap: "square", + Anchor: "ctr", + AnchorCtr: true, + }, + P: aP{ + PPr: &aPPr{ + DefRPr: aRPr{ + Sz: 900, + B: false, + I: false, + U: "none", + Strike: "noStrike", + Kern: 1200, + Baseline: 0, + SolidFill: &aSolidFill{ + SchemeClr: &aSchemeClr{ + Val: "tx1", + LumMod: &attrValInt{Val: intPtr(15000)}, + LumOff: &attrValInt{Val: intPtr(85000)}, + }, + }, + Latin: &aLatin{Typeface: "+mn-lt"}, + Ea: &aEa{Typeface: "+mn-ea"}, + Cs: &aCs{Typeface: "+mn-cs"}, + }, + }, + EndParaRPr: &aEndParaRPr{Lang: "en-US"}, + }, + } +} + +// drawingParser provides a function to parse drawingXML. In order to solve +// 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) { + var ( + err error + ok bool + ) + + if f.Drawings[path] == nil { + content := xlsxWsDr{} + content.A = NameSpaceDrawingML + content.Xdr = NameSpaceDrawingMLSpreadSheet + if _, ok = f.XLSX[path]; ok { // Append Model + 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) + } + content.R = decodeWsDr.R + for _, v := range decodeWsDr.OneCellAnchor { + content.OneCellAnchor = append(content.OneCellAnchor, &xdrCellAnchor{ + EditAs: v.EditAs, + GraphicFrame: v.Content, + }) + } + for _, v := range decodeWsDr.TwoCellAnchor { + content.TwoCellAnchor = append(content.TwoCellAnchor, &xdrCellAnchor{ + EditAs: v.EditAs, + GraphicFrame: v.Content, + }) + } + } + f.Drawings[path] = &content + } + wsDr := f.Drawings[path] + return wsDr, len(wsDr.OneCellAnchor) + len(wsDr.TwoCellAnchor) + 2 +} + +// 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 { + col, row, err := CellNameToCoordinates(cell) + if err != nil { + return err + } + 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) + content, cNvPrID := f.drawingParser(drawingXML) + twoCellAnchor := xdrCellAnchor{} + twoCellAnchor.EditAs = formatSet.Positioning + from := xlsxFrom{} + from.Col = colStart + from.ColOff = formatSet.OffsetX * EMU + from.Row = rowStart + from.RowOff = formatSet.OffsetY * EMU + to := xlsxTo{} + to.Col = colEnd + to.ColOff = x2 * EMU + to.Row = rowEnd + to.RowOff = y2 * EMU + twoCellAnchor.From = &from + twoCellAnchor.To = &to + + graphicFrame := xlsxGraphicFrame{ + NvGraphicFramePr: xlsxNvGraphicFramePr{ + CNvPr: &xlsxCNvPr{ + ID: cNvPrID, + Name: "Chart " + strconv.Itoa(cNvPrID), + }, + }, + Graphic: &xlsxGraphic{ + GraphicData: &xlsxGraphicData{ + URI: NameSpaceDrawingMLChart, + Chart: &xlsxChart{ + C: NameSpaceDrawingMLChart, + R: SourceRelationship, + RID: "rId" + strconv.Itoa(rID), + }, + }, + }, + } + graphic, _ := xml.Marshal(graphicFrame) + twoCellAnchor.GraphicFrame = string(graphic) + twoCellAnchor.ClientData = &xdrClientData{ + FLocksWithSheet: formatSet.FLocksWithSheet, + FPrintsWithSheet: formatSet.FPrintsWithSheet, + } + content.TwoCellAnchor = append(content.TwoCellAnchor, &twoCellAnchor) + f.Drawings[drawingXML] = content + return err +} From e2bd08c9111b0141c66adf232edb2fd729afa63f Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 21 Jan 2020 23:29:56 +0800 Subject: [PATCH 191/957] Make DeleteChart delete multiple charts located on the same cell --- chart.go | 31 ++++++++++++------------------- drawing.go | 4 ++-- xmlChart.go | 2 +- 3 files changed, 15 insertions(+), 22 deletions(-) diff --git a/chart.go b/chart.go index 5f06c55870..2629f0b900 100644 --- a/chart.go +++ b/chart.go @@ -694,9 +694,9 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // // 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 art types in a single -// chart. For example, create a clustered column - line chart with data -// Sheet1!$E$1:$L$15: +// 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 // @@ -782,10 +782,11 @@ func (f *File) DeleteChart(sheet, cell string) (err error) { } drawingXML := strings.Replace(f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID), "..", "xl", -1) wsDr, _ = f.drawingParser(drawingXML) - for idx, anchor := range wsDr.TwoCellAnchor { - if err = nil; anchor.From != nil && anchor.Pic == nil { - if anchor.From.Col == col && anchor.From.Row == row { + for idx := 0; idx < len(wsDr.TwoCellAnchor); idx++ { + if err = nil; wsDr.TwoCellAnchor[idx].From != nil && wsDr.TwoCellAnchor[idx].Pic == nil { + if wsDr.TwoCellAnchor[idx].From.Col == col && wsDr.TwoCellAnchor[idx].From.Row == row { wsDr.TwoCellAnchor = append(wsDr.TwoCellAnchor[:idx], wsDr.TwoCellAnchor[idx+1:]...) + idx-- } } } @@ -795,26 +796,18 @@ func (f *File) DeleteChart(sheet, cell string) (err error) { // deleteChart provides a function to delete chart graphic frame by given by // given coordinates. func (f *File) deleteChart(col, row int, drawingXML string, wsDr *xlsxWsDr) (err error) { - var ( - deWsDr *decodeWsDr - deTwoCellAnchor *decodeTwoCellAnchor - ) - 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) - return - } - for idx, anchor := range deWsDr.TwoCellAnchor { + var deTwoCellAnchor *decodeTwoCellAnchor + for idx := 0; idx < len(wsDr.TwoCellAnchor); idx++ { deTwoCellAnchor = new(decodeTwoCellAnchor) - if err = f.xmlNewDecoder(bytes.NewReader([]byte("" + anchor.Content + ""))). + if err = f.xmlNewDecoder(bytes.NewReader([]byte("" + wsDr.TwoCellAnchor[idx].GraphicFrame + ""))). Decode(deTwoCellAnchor); err != nil && err != io.EOF { err = fmt.Errorf("xml decode error: %s", err) return } if err = nil; deTwoCellAnchor.From != nil && deTwoCellAnchor.Pic == nil { - if anchor.From.Col == col && anchor.From.Row == row { + if deTwoCellAnchor.From.Col == col && deTwoCellAnchor.From.Row == row { wsDr.TwoCellAnchor = append(wsDr.TwoCellAnchor[:idx], wsDr.TwoCellAnchor[idx+1:]...) + idx-- } } } diff --git a/drawing.go b/drawing.go index 49506a3003..316897b6a6 100644 --- a/drawing.go +++ b/drawing.go @@ -563,7 +563,7 @@ func (f *File) drawPie3DChart(formatSet *formatChart) *cPlotArea { // pie chart by given format sets. func (f *File) drawPieOfPieChart(formatSet *formatChart) *cPlotArea { return &cPlotArea{ - PieChart: &cCharts{ + OfPieChart: &cCharts{ OfPieType: &attrValString{ Val: stringPtr("pie"), }, @@ -580,7 +580,7 @@ func (f *File) drawPieOfPieChart(formatSet *formatChart) *cPlotArea { // pie chart by given format sets. func (f *File) drawBarOfPieChart(formatSet *formatChart) *cPlotArea { return &cPlotArea{ - PieChart: &cCharts{ + OfPieChart: &cCharts{ OfPieType: &attrValString{ Val: stringPtr("bar"), }, diff --git a/xmlChart.go b/xmlChart.go index 55114696d4..8d24552481 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -331,10 +331,10 @@ type cCharts struct { RadarStyle *attrValString `xml:"radarStyle"` ScatterStyle *attrValString `xml:"scatterStyle"` OfPieType *attrValString `xml:"ofPieType"` - SerLines *attrValString `xml:"serLines"` VaryColors *attrValBool `xml:"varyColors"` Wireframe *attrValBool `xml:"wireframe"` Ser *[]cSer `xml:"ser"` + SerLines *attrValString `xml:"serLines"` DLbls *cDLbls `xml:"dLbls"` Shape *attrValString `xml:"shape"` HoleSize *attrValInt `xml:"holeSize"` From cbc3fd21b79fbb819c1c341fc825701c04a0b473 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 22 Jan 2020 01:08:18 +0800 Subject: [PATCH 192/957] Resolve #455, init delete picture from spreadsheet support --- chart.go | 37 +------------------------------------ chart_test.go | 6 ------ drawing.go | 43 +++++++++++++++++++++++++++++++++++++++++++ picture.go | 21 +++++++++++++++++++++ picture_test.go | 15 +++++++++++++++ 5 files changed, 80 insertions(+), 42 deletions(-) diff --git a/chart.go b/chart.go index 2629f0b900..227cdee6e7 100644 --- a/chart.go +++ b/chart.go @@ -10,11 +10,8 @@ package excelize import ( - "bytes" "encoding/json" "errors" - "fmt" - "io" "strconv" "strings" ) @@ -766,7 +763,6 @@ func (f *File) AddChart(sheet, cell, format string, combo ...string) error { // DeleteChart provides a function to delete chart in XLSX by given worksheet // and cell name. func (f *File) DeleteChart(sheet, cell string) (err error) { - var wsDr *xlsxWsDr col, row, err := CellNameToCoordinates(cell) if err != nil { return @@ -781,38 +777,7 @@ func (f *File) DeleteChart(sheet, cell string) (err error) { return } drawingXML := strings.Replace(f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID), "..", "xl", -1) - wsDr, _ = f.drawingParser(drawingXML) - for idx := 0; idx < len(wsDr.TwoCellAnchor); idx++ { - if err = nil; wsDr.TwoCellAnchor[idx].From != nil && wsDr.TwoCellAnchor[idx].Pic == nil { - if wsDr.TwoCellAnchor[idx].From.Col == col && wsDr.TwoCellAnchor[idx].From.Row == row { - wsDr.TwoCellAnchor = append(wsDr.TwoCellAnchor[:idx], wsDr.TwoCellAnchor[idx+1:]...) - idx-- - } - } - } - return f.deleteChart(col, row, drawingXML, wsDr) -} - -// deleteChart provides a function to delete chart graphic frame by given by -// given coordinates. -func (f *File) deleteChart(col, row int, drawingXML string, wsDr *xlsxWsDr) (err error) { - var deTwoCellAnchor *decodeTwoCellAnchor - for idx := 0; idx < len(wsDr.TwoCellAnchor); idx++ { - deTwoCellAnchor = new(decodeTwoCellAnchor) - if err = f.xmlNewDecoder(bytes.NewReader([]byte("" + wsDr.TwoCellAnchor[idx].GraphicFrame + ""))). - Decode(deTwoCellAnchor); err != nil && err != io.EOF { - err = fmt.Errorf("xml decode error: %s", err) - return - } - if err = nil; deTwoCellAnchor.From != nil && deTwoCellAnchor.Pic == nil { - if deTwoCellAnchor.From.Col == col && deTwoCellAnchor.From.Row == row { - wsDr.TwoCellAnchor = append(wsDr.TwoCellAnchor[:idx], wsDr.TwoCellAnchor[idx+1:]...) - idx-- - } - } - } - f.Drawings[drawingXML] = wsDr - return err + return f.deleteDrawing(col, row, drawingXML, "Chart") } // countCharts provides a function to get chart files count storage in the diff --git a/chart_test.go b/chart_test.go index d8d36d850f..98f3555ad7 100644 --- a/chart_test.go +++ b/chart_test.go @@ -217,12 +217,6 @@ func TestDeleteChart(t *testing.T) { assert.EqualError(t, f.DeleteChart("SheetN", "A1"), "sheet SheetN is not exist") // Test delete chart with invalid coordinates. assert.EqualError(t, f.DeleteChart("Sheet1", ""), `cannot convert cell "" to coordinates: invalid cell name ""`) - // Test delete chart with unsupport charset. - f, err = OpenFile(filepath.Join("test", "Book1.xlsx")) - assert.NoError(t, err) - delete(f.Sheet, "xl/drawings/drawing1.xml") - f.XLSX["xl/drawings/drawing1.xml"] = MacintoshCyrillicCharset - assert.EqualError(t, f.DeleteChart("Sheet1", "A1"), "xml decode error: XML syntax error on line 1: invalid UTF-8") // Test delete chart on no chart worksheet. assert.NoError(t, NewFile().DeleteChart("Sheet1", "A1")) } diff --git a/drawing.go b/drawing.go index 316897b6a6..e51b6afcc9 100644 --- a/drawing.go +++ b/drawing.go @@ -12,6 +12,7 @@ package excelize import ( "bytes" "encoding/xml" + "fmt" "io" "log" "reflect" @@ -1207,3 +1208,45 @@ func (f *File) addDrawingChart(sheet, drawingXML, cell string, width, height, rI f.Drawings[drawingXML] = content return err } + +// 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) { + var ( + wsDr *xlsxWsDr + deTwoCellAnchor *decodeTwoCellAnchor + ) + xdrCellAnchorFuncs := map[string]func(anchor *xdrCellAnchor) bool{ + "Chart": func(anchor *xdrCellAnchor) bool { return anchor.Pic == nil }, + "Pic": func(anchor *xdrCellAnchor) bool { return anchor.Pic != nil }, + } + decodeTwoCellAnchorFuncs := map[string]func(anchor *decodeTwoCellAnchor) bool{ + "Chart": func(anchor *decodeTwoCellAnchor) bool { return anchor.Pic == nil }, + "Pic": func(anchor *decodeTwoCellAnchor) bool { return anchor.Pic != nil }, + } + wsDr, _ = f.drawingParser(drawingXML) + 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 { + wsDr.TwoCellAnchor = append(wsDr.TwoCellAnchor[:idx], wsDr.TwoCellAnchor[idx+1:]...) + idx-- + } + } + } + for idx := 0; idx < len(wsDr.TwoCellAnchor); idx++ { + deTwoCellAnchor = new(decodeTwoCellAnchor) + if err = f.xmlNewDecoder(bytes.NewReader([]byte("" + wsDr.TwoCellAnchor[idx].GraphicFrame + ""))). + Decode(deTwoCellAnchor); err != nil && err != io.EOF { + err = fmt.Errorf("xml decode error: %s", err) + return + } + if err = nil; deTwoCellAnchor.From != nil && decodeTwoCellAnchorFuncs[drawingType](deTwoCellAnchor) { + if deTwoCellAnchor.From.Col == col && deTwoCellAnchor.From.Row == row { + wsDr.TwoCellAnchor = append(wsDr.TwoCellAnchor[:idx], wsDr.TwoCellAnchor[idx+1:]...) + idx-- + } + } + } + f.Drawings[drawingXML] = wsDr + return err +} diff --git a/picture.go b/picture.go index 639cb66263..213bae9f94 100644 --- a/picture.go +++ b/picture.go @@ -462,6 +462,27 @@ func (f *File) GetPicture(sheet, cell string) (string, []byte, error) { return f.getPicture(row, col, drawingXML, drawingRelationships) } +// DeletePicture provides a function to delete chart in XLSX by given +// worksheet and cell name. Note that the image file won't deleted from the +// document currently. +func (f *File) DeletePicture(sheet, cell string) (err error) { + col, row, err := CellNameToCoordinates(cell) + if err != nil { + return + } + col-- + row-- + ws, err := f.workSheetReader(sheet) + if err != nil { + return + } + if ws.Drawing == nil { + return + } + drawingXML := strings.Replace(f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID), "..", "xl", -1) + return f.deleteDrawing(col, row, drawingXML, "Pic") +} + // getPicture provides a function to get picture base name and raw content // embed in XLSX by given coordinates and drawing relationships. func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) (ret string, buf []byte, err error) { diff --git a/picture_test.go b/picture_test.go index ca38f412a1..fdc6f0db02 100644 --- a/picture_test.go +++ b/picture_test.go @@ -166,3 +166,18 @@ 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 is not exist") } + +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.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") + // Test delete picture with invalid coordinates. + assert.EqualError(t, f.DeletePicture("Sheet1", ""), `cannot convert cell "" to coordinates: invalid cell name ""`) + // Test delete picture on no chart worksheet. + assert.NoError(t, NewFile().DeletePicture("Sheet1", "A1")) +} From 68754a2075f12ba3c2bdf3646e4a3e7a3fd829f5 Mon Sep 17 00:00:00 2001 From: Jacques Boscq Date: Tue, 21 Jan 2020 23:42:44 +0100 Subject: [PATCH 193/957] SetColVisible() can parse a column range + typos. --- col.go | 59 ++++++++++++++++++++++++++++++++--------------------- col_test.go | 28 +++++++++++++++++++++++-- 2 files changed, 62 insertions(+), 25 deletions(-) diff --git a/col.go b/col.go index f7e6bcd215..ff771f1602 100644 --- a/col.go +++ b/col.go @@ -26,7 +26,7 @@ const ( // worksheet name and column name. For example, get visible state of column D // in Sheet1: // -// visiable, err := f.GetColVisible("Sheet1", "D") +// visible, err := f.GetColVisible("Sheet1", "D") // func (f *File) GetColVisible(sheet, col string) (bool, error) { visible := true @@ -52,45 +52,58 @@ func (f *File) GetColVisible(sheet, col string) (bool, error) { return visible, err } -// SetColVisible provides a function to set visible of a single column by given -// worksheet name and column name. For example, hide column D in Sheet1: +// SetColVisible provides a function to set visible columns by given worksheet +// name, columns range and visibility. +// +// For example hide column D on Sheet1: // // err := f.SetColVisible("Sheet1", "D", false) // -func (f *File) SetColVisible(sheet, col string, visible bool) error { - colNum, err := ColumnNameToNumber(col) +// Hide the columns from D to F (included) +// +// err := f.SetColVisible("Sheet1", "D:F", false) +// +func (f *File) SetColVisible(sheet, columns string, visible bool) error { + var max int + + colsTab := strings.Split(columns, ":") + min, err := ColumnNameToNumber(colsTab[0]) if err != nil { return err } - colData := xlsxCol{ - Min: colNum, - Max: colNum, - Hidden: !visible, - CustomWidth: true, + if len(colsTab) == 2 { + max, err = ColumnNameToNumber(colsTab[1]) + if err != nil { + return err + } + } else { + max = min + } + if max < min { + min, max = max, min } xlsx, err := f.workSheetReader(sheet) if err != nil { return err } - if xlsx.Cols == nil { + colData := xlsxCol{ + Min: min, + Max: max, + Width: 9, // default width + Hidden: !visible, + CustomWidth: true, + } + if xlsx.Cols != nil { + xlsx.Cols.Col = append(xlsx.Cols.Col, colData) + } else { cols := xlsxCols{} cols.Col = append(cols.Col, colData) xlsx.Cols = &cols - return err } - for v := range xlsx.Cols.Col { - if xlsx.Cols.Col[v].Min <= colNum && colNum <= xlsx.Cols.Col[v].Max { - colData = xlsx.Cols.Col[v] - } - } - colData.Min = colNum - colData.Max = colNum - colData.Hidden = !visible - colData.CustomWidth = true - xlsx.Cols.Col = append(xlsx.Cols.Col, colData) - return err + return nil } + // GetColOutlineLevel provides a function to get outline level of a single // column by given worksheet name and column name. For example, get outline // level of column D in Sheet1: diff --git a/col_test.go b/col_test.go index cdb7edfb0e..08fac1ceb4 100644 --- a/col_test.go +++ b/col_test.go @@ -12,17 +12,41 @@ func TestColumnVisibility(t *testing.T) { f, err := prepareTestBook1() assert.NoError(t, err) + // Hide/display a column with SetColVisible assert.NoError(t, f.SetColVisible("Sheet1", "F", false)) assert.NoError(t, f.SetColVisible("Sheet1", "F", true)) visible, err := f.GetColVisible("Sheet1", "F") assert.Equal(t, true, visible) assert.NoError(t, err) - // Test get column visiable on not exists worksheet. + // Test hiding a few columns SetColVisible(...false)... + assert.NoError(t, f.SetColVisible("Sheet1", "F:V", false)) + visible, err = f.GetColVisible("Sheet1", "F") + assert.Equal(t, false, visible) + assert.NoError(t, err) + visible, err = f.GetColVisible("Sheet1", "U") + assert.Equal(t, false, visible) + assert.NoError(t, err) + visible, err = f.GetColVisible("Sheet1", "V") + assert.Equal(t, false, visible) + assert.NoError(t, err) + // ...and displaying them back SetColVisible(...true) + assert.NoError(t, f.SetColVisible("Sheet1", "F:V", true)) + visible, err = f.GetColVisible("Sheet1", "F") + assert.Equal(t, true, visible) + assert.NoError(t, err) + visible, err = f.GetColVisible("Sheet1", "U") + assert.Equal(t, true, visible) + assert.NoError(t, err) + visible, err = f.GetColVisible("Sheet1", "G") + assert.Equal(t, true, visible) + assert.NoError(t, err) + + // Test get column visible on an inexistent worksheet. _, err = f.GetColVisible("SheetN", "F") assert.EqualError(t, err, "sheet SheetN is not exist") - // Test get column visiable with illegal cell coordinates. + // Test get column visible with illegal cell coordinates. _, err = f.GetColVisible("Sheet1", "*") assert.EqualError(t, err, `invalid column name "*"`) assert.EqualError(t, f.SetColVisible("Sheet1", "*", false), `invalid column name "*"`) From e51aff2d9562bbfb290ef76a948facb6d4660eff Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 7 Feb 2020 00:25:01 +0800 Subject: [PATCH 194/957] Resolve #570, flat columns for the column's operation --- .travis.yml | 2 +- CONTRIBUTING.md | 6 ++- col.go | 125 +++++++++++++++++++++++++++++++++--------------- col_test.go | 4 +- merge.go | 25 +++++----- picture.go | 4 +- sheet.go | 4 +- table.go | 6 ++- xmlWorksheet.go | 8 ++-- 9 files changed, 117 insertions(+), 67 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5012f86d63..1cb1d496e6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,7 +14,7 @@ os: - osx env: - matrix: + jobs: - GOARCH=amd64 - GOARCH=386 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index afb7d4eef9..53c650e55f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -234,7 +234,9 @@ By making a contribution to this project, I certify that: Then you just add a line to every git commit message: - Signed-off-by: Ri Xu https://xuri.me +```text +Signed-off-by: Ri Xu https://xuri.me +``` Use your real name (sorry, no pseudonyms or anonymous contributions.) @@ -460,4 +462,4 @@ Do not use package math/rand to generate keys, even throwaway ones. Unseeded, the generator is completely predictable. Seeded with time.Nanoseconds(), there are just a few bits of entropy. Instead, use crypto/rand's Reader, and if you need text, print to -hexadecimal or base64 +hexadecimal or base64. diff --git a/col.go b/col.go index ff771f1602..6f768003fe 100644 --- a/col.go +++ b/col.go @@ -13,6 +13,8 @@ import ( "errors" "math" "strings" + + "github.com/mohae/deepcopy" ) // Define the default cell size and EMU unit of measurement. @@ -59,7 +61,7 @@ func (f *File) GetColVisible(sheet, col string) (bool, error) { // // err := f.SetColVisible("Sheet1", "D", false) // -// Hide the columns from D to F (included) +// Hide the columns from D to F (included): // // err := f.SetColVisible("Sheet1", "D:F", false) // @@ -87,23 +89,31 @@ func (f *File) SetColVisible(sheet, columns string, visible bool) error { return err } colData := xlsxCol{ - Min: min, - Max: max, - Width: 9, // default width - Hidden: !visible, + Min: min, + Max: max, + Width: 9, // default width + Hidden: !visible, CustomWidth: true, } - if xlsx.Cols != nil { - xlsx.Cols.Col = append(xlsx.Cols.Col, colData) - } else { + if xlsx.Cols == nil { cols := xlsxCols{} cols.Col = append(cols.Col, colData) xlsx.Cols = &cols - } + return nil + } + xlsx.Cols.Col = flatCols(colData, xlsx.Cols.Col, func(fc, c xlsxCol) xlsxCol { + fc.BestFit = c.BestFit + fc.Collapsed = c.Collapsed + fc.CustomWidth = c.CustomWidth + fc.OutlineLevel = c.OutlineLevel + fc.Phonetic = c.Phonetic + fc.Style = c.Style + fc.Width = c.Width + return fc + }) return nil } - // GetColOutlineLevel provides a function to get outline level of a single // column by given worksheet name and column name. For example, get outline // level of column D in Sheet1: @@ -162,16 +172,16 @@ func (f *File) SetColOutlineLevel(sheet, col string, level uint8) error { xlsx.Cols = &cols return err } - for v := range xlsx.Cols.Col { - if xlsx.Cols.Col[v].Min <= colNum && colNum <= xlsx.Cols.Col[v].Max { - colData = xlsx.Cols.Col[v] - } - } - colData.Min = colNum - colData.Max = colNum - colData.OutlineLevel = level - colData.CustomWidth = true - xlsx.Cols.Col = append(xlsx.Cols.Col, colData) + xlsx.Cols.Col = flatCols(colData, xlsx.Cols.Col, func(fc, c xlsxCol) xlsxCol { + fc.BestFit = c.BestFit + fc.Collapsed = c.Collapsed + fc.CustomWidth = c.CustomWidth + fc.Hidden = c.Hidden + fc.Phonetic = c.Phonetic + fc.Style = c.Style + fc.Width = c.Width + return fc + }) return err } @@ -214,21 +224,21 @@ func (f *File) SetColStyle(sheet, columns string, styleID int) error { if xlsx.Cols == nil { xlsx.Cols = &xlsxCols{} } - var find bool - for idx, col := range xlsx.Cols.Col { - if col.Min == min && col.Max == max { - xlsx.Cols.Col[idx].Style = styleID - find = true - } - } - if !find { - xlsx.Cols.Col = append(xlsx.Cols.Col, xlsxCol{ - Min: min, - Max: max, - Width: 9, - Style: styleID, - }) - } + xlsx.Cols.Col = flatCols(xlsxCol{ + Min: min, + Max: max, + Width: 9, + Style: styleID, + }, xlsx.Cols.Col, func(fc, c xlsxCol) xlsxCol { + fc.BestFit = c.BestFit + fc.Collapsed = c.Collapsed + fc.CustomWidth = c.CustomWidth + fc.Hidden = c.Hidden + fc.OutlineLevel = c.OutlineLevel + fc.Phonetic = c.Phonetic + fc.Width = c.Width + return fc + }) return nil } @@ -261,16 +271,55 @@ func (f *File) SetColWidth(sheet, startcol, endcol string, width float64) error Width: width, CustomWidth: true, } - if xlsx.Cols != nil { - xlsx.Cols.Col = append(xlsx.Cols.Col, col) - } else { + if xlsx.Cols == nil { cols := xlsxCols{} cols.Col = append(cols.Col, col) xlsx.Cols = &cols + return err } + xlsx.Cols.Col = flatCols(col, xlsx.Cols.Col, func(fc, c xlsxCol) xlsxCol { + fc.BestFit = c.BestFit + fc.Collapsed = c.Collapsed + fc.Hidden = c.Hidden + fc.OutlineLevel = c.OutlineLevel + fc.Phonetic = c.Phonetic + fc.Style = c.Style + return fc + }) return err } +// 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{} + for i := col.Min; i <= col.Max; i++ { + c := deepcopy.Copy(col).(xlsxCol) + c.Min, c.Max = i, i + fc = append(fc, c) + } + inFlat := func(colID int, cols []xlsxCol) (int, bool) { + for idx, c := range cols { + if c.Max == colID && c.Min == colID { + return idx, true + } + } + return -1, false + } + for _, column := range cols { + for i := column.Min; i <= column.Max; i++ { + if idx, ok := inFlat(i, fc); ok { + fc[idx] = replacer(fc[idx], column) + continue + } + c := deepcopy.Copy(column).(xlsxCol) + c.Min, c.Max = i, i + fc = append(fc, c) + } + } + return fc +} + // positionObjectPixels calculate the vertices that define the position of a // graphical object within the worksheet in pixels. // diff --git a/col_test.go b/col_test.go index 08fac1ceb4..050c998e6c 100644 --- a/col_test.go +++ b/col_test.go @@ -31,7 +31,7 @@ func TestColumnVisibility(t *testing.T) { assert.Equal(t, false, visible) assert.NoError(t, err) // ...and displaying them back SetColVisible(...true) - assert.NoError(t, f.SetColVisible("Sheet1", "F:V", true)) + assert.NoError(t, f.SetColVisible("Sheet1", "V:F", true)) visible, err = f.GetColVisible("Sheet1", "F") assert.Equal(t, true, visible) assert.NoError(t, err) @@ -53,7 +53,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), "invalid column name \"-1\"") assert.EqualError(t, f.SetColVisible("SheetN", "E", false), "sheet SheetN is not exist") assert.NoError(t, f.SaveAs(filepath.Join("test", "TestColumnVisibility.xlsx"))) }) diff --git a/merge.go b/merge.go index b952a1ede5..f29640ddbd 100644 --- a/merge.go +++ b/merge.go @@ -22,20 +22,17 @@ import ( // If you create a merged cell that overlaps with another existing merged cell, // those merged cells that already exist will be removed. // -// 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 { rect1, err := f.areaRefToCoordinates(hcell + ":" + vcell) diff --git a/picture.go b/picture.go index 213bae9f94..0b91b91f88 100644 --- a/picture.go +++ b/picture.go @@ -462,8 +462,8 @@ func (f *File) GetPicture(sheet, cell string) (string, []byte, error) { return f.getPicture(row, col, drawingXML, drawingRelationships) } -// DeletePicture provides a function to delete chart in XLSX by given -// worksheet and cell name. Note that the image file won't deleted from the +// DeletePicture provides a function to delete charts in XLSX by given +// worksheet and cell name. 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) diff --git a/sheet.go b/sheet.go index 19b90c63a7..a6ff2a1045 100644 --- a/sheet.go +++ b/sheet.go @@ -287,8 +287,8 @@ func (f *File) GetActiveSheetIndex() int { return 0 } -// SetSheetName provides a function to set the worksheet name be given old and -// new worksheet name. Maximum 31 characters are allowed in sheet title and +// 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 // 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. diff --git a/table.go b/table.go index 566238c69d..55901cd93a 100644 --- a/table.go +++ b/table.go @@ -39,8 +39,10 @@ func parseFormatTableSet(formatSet string) (*formatTable, error) { // // 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 at least two lines include string type header. Multiple -// tables coordinate areas can't have an intersection. +// 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. // // table_name: The name of the table, in the same worksheet name of the table should be unique // diff --git a/xmlWorksheet.go b/xmlWorksheet.go index ed304ccbee..46253e61a6 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -278,15 +278,15 @@ type xlsxCols struct { // 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"` + Collapsed bool `xml:"collapsed,attr,omitempty"` CustomWidth bool `xml:"customWidth,attr,omitempty"` - Hidden bool `xml:"hidden,attr"` + 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"` - Width float64 `xml:"width,attr"` + Style int `xml:"style,attr,omitempty"` + Width float64 `xml:"width,attr,omitempty"` } // xlsxDimension directly maps the dimension element in the namespace From 023dba726510a4a7a97838ac9a8f4292a90aa227 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 13 Feb 2020 00:00:42 +0800 Subject: [PATCH 195/957] Fix #576, serialize by fields order on stream flush --- CODE_OF_CONDUCT.md | 6 +++--- stream.go | 29 ++++++++++++----------------- stream_test.go | 7 +++++++ 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index a84b47ff95..572b5612e0 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -40,7 +40,7 @@ Project maintainers who do not follow or enforce the Code of Conduct in good fai ## Attribution -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] +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] -[homepage]: http://contributor-covenant.org -[version]: http://contributor-covenant.org/version/1/4/ +[homepage]: https://www.contributor-covenant.org +[version]: https://www.contributor-covenant.org/version/2/0/code_of_conduct diff --git a/stream.go b/stream.go index 83986229c0..c854d8bde1 100644 --- a/stream.go +++ b/stream.go @@ -27,6 +27,7 @@ type StreamWriter struct { File *File Sheet string SheetID int + worksheet *xlsxWorksheet rawData bufferedWriter tableParts string } @@ -77,15 +78,15 @@ func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) { Sheet: sheet, SheetID: sheetID, } - - ws, err := f.workSheetReader(sheet) + var err error + sw.worksheet, err = f.workSheetReader(sheet) if err != nil { return nil, err } sw.rawData.WriteString(XMLHeader + ``) - return sw, nil + return sw, err } // AddTable creates an Excel table for the StreamWriter using the given @@ -373,7 +374,9 @@ func writeCell(buf *bufferedWriter, c xlsxC) { // Flush ending the streaming writing process. func (sw *StreamWriter) Flush() error { sw.rawData.WriteString(``) + bulkAppendFields(&sw.rawData, sw.worksheet, 7, 37) sw.rawData.WriteString(sw.tableParts) + bulkAppendFields(&sw.rawData, sw.worksheet, 39, 39) sw.rawData.WriteString(``) if err := sw.rawData.Flush(); err != nil { return err @@ -392,23 +395,15 @@ func (sw *StreamWriter) Flush() error { return nil } -// bulkAppendOtherFields bulk-appends fields in a worksheet, skipping the -// specified field names. -func bulkAppendOtherFields(w io.Writer, ws *xlsxWorksheet, skip ...string) { - skipMap := make(map[string]struct{}) - for _, name := range skip { - skipMap[name] = struct{}{} - } - +// bulkAppendFields bulk-appends fields in a worksheet by specified field +// names order range. +func bulkAppendFields(w io.Writer, ws *xlsxWorksheet, from, to int) { s := reflect.ValueOf(ws).Elem() - typeOfT := s.Type() enc := xml.NewEncoder(w) for i := 0; i < s.NumField(); i++ { - f := s.Field(i) - if _, ok := skipMap[typeOfT.Field(i).Name]; ok { - continue + if from <= i && i <= to { + enc.Encode(s.Field(i).Interface()) } - enc.Encode(f.Interface()) } } diff --git a/stream_test.go b/stream_test.go index 8c5e7ea425..d89dad845a 100644 --- a/stream_test.go +++ b/stream_test.go @@ -92,6 +92,13 @@ func TestStreamWriter(t *testing.T) { _, err = streamWriter.rawData.Reader() assert.NoError(t, err) assert.NoError(t, os.Remove(streamWriter.rawData.tmp.Name())) + + // Test unsupport charset + file = NewFile() + delete(file.Sheet, "xl/worksheets/sheet1.xml") + file.XLSX["xl/worksheets/sheet1.xml"] = MacintoshCyrillicCharset + streamWriter, err = file.NewStreamWriter("Sheet1") + assert.EqualError(t, err, "xml decode error: XML syntax error on line 1: invalid UTF-8") } func TestStreamTable(t *testing.T) { From 52f1eee7c487a086756bda857bb6390f8b4a0ffe Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 15 Feb 2020 16:34:47 +0800 Subject: [PATCH 196/957] Fix #578, escape character in the formula --- xmlWorksheet.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 46253e61a6..dda1b78aab 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -475,7 +475,7 @@ func (c *xlsxC) hasValue() bool { // http://schemas.openxmlformats.org/spreadsheetml/2006/main - currently I have // not checked it for completeness - it does as much as I need. type xlsxF struct { - Content string `xml:",innerxml"` + Content string `xml:",chardata"` T string `xml:"t,attr,omitempty"` // Formula type Ref string `xml:"ref,attr,omitempty"` // Shared formula ref Si string `xml:"si,attr,omitempty"` // Shared formula index From ad883caa0f77dfc016ae99bd5fbb606953eb99a0 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 19 Feb 2020 00:08:10 +0800 Subject: [PATCH 197/957] Resolve #580, revert commit https://github.com/360EntSecGroup-Skylar/excelize/commit/5ca7231ed408ac264f509ff52b5d28ff4fbda757 --- README.md | 45 +++++++++++++++++++++++++++++---------------- README_zh.md | 45 +++++++++++++++++++++++++++++---------------- cell_test.go | 2 +- chart.go | 20 ++++++++++++++------ excelize.go | 6 +++--- excelize_test.go | 2 +- picture.go | 21 +++++++++++---------- pivotTable.go | 4 ++-- rows.go | 16 ++++++++-------- sheet.go | 2 +- sheet_test.go | 12 ++++++------ sheetpr_test.go | 8 ++++---- sheetview_test.go | 24 ++++++++++++------------ sparkline_test.go | 4 ++-- stream.go | 12 ++++++------ styles.go | 16 ++++++++-------- 16 files changed, 137 insertions(+), 102 deletions(-) diff --git a/README.md b/README.md index d7f696cc51..c81efd1b0a 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,11 @@ Here is a minimal example usage that will create XLSX file. ```go package main -import "github.com/360EntSecGroup-Skylar/excelize" +import ( + "fmt" + + "github.com/360EntSecGroup-Skylar/excelize" +) func main() { f := excelize.NewFile() @@ -44,7 +48,7 @@ func main() { f.SetActiveSheet(index) // Save xlsx file by the given path. if err := f.SaveAs("Book1.xlsx"); err != nil { - println(err.Error()) + fmt.Println(err) } } ``` @@ -56,28 +60,32 @@ The following constitutes the bare to read a XLSX document. ```go package main -import "github.com/360EntSecGroup-Skylar/excelize" +import ( + "fmt" + + "github.com/360EntSecGroup-Skylar/excelize" +) func main() { f, err := excelize.OpenFile("Book1.xlsx") if err != nil { - println(err.Error()) + fmt.Println(err) return } // Get value from cell by given worksheet name and axis. cell, err := f.GetCellValue("Sheet1", "B2") if err != nil { - println(err.Error()) + fmt.Println(err) return } - println(cell) + fmt.Println(cell) // Get all the rows in the Sheet1. rows, err := f.GetRows("Sheet1") for _, row := range rows { for _, colCell := range row { - print(colCell, "\t") + fmt.Print(colCell, "\t") } - println() + fmt.Println() } } ``` @@ -91,7 +99,11 @@ With Excelize chart generation and management is as easy as a few lines of code. ```go package main -import "github.com/360EntSecGroup-Skylar/excelize" +import ( + "fmt" + + "github.com/360EntSecGroup-Skylar/excelize" +) func main() { categories := map[string]string{"A2": "Small", "A3": "Normal", "A4": "Large", "B1": "Apple", "C1": "Orange", "D1": "Pear"} @@ -104,12 +116,12 @@ func main() { 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"}}`); err != nil { - println(err.Error()) + fmt.Println(err) return } // Save xlsx file by the given path. if err := f.SaveAs("Book1.xlsx"); err != nil { - println(err.Error()) + fmt.Println(err) } } ``` @@ -120,6 +132,7 @@ func main() { package main import ( + "fmt" _ "image/gif" _ "image/jpeg" _ "image/png" @@ -130,24 +143,24 @@ import ( func main() { f, err := excelize.OpenFile("Book1.xlsx") if err != nil { - println(err.Error()) + fmt.Println(err) return } // Insert a picture. if err := f.AddPicture("Sheet1", "A2", "image.png", ""); err != nil { - println(err.Error()) + fmt.Println(err) } // Insert a picture to worksheet with scaling. if err := f.AddPicture("Sheet1", "D2", "image.jpg", `{"x_scale": 0.5, "y_scale": 0.5}`); err != nil { - println(err.Error()) + 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 { - println(err.Error()) + fmt.Println(err) } // Save the xlsx file with the origin path. if err = f.Save(); err != nil { - println(err.Error()) + fmt.Println(err) } } ``` diff --git a/README_zh.md b/README_zh.md index c1ee83e3eb..f75eec5d7e 100644 --- a/README_zh.md +++ b/README_zh.md @@ -30,7 +30,11 @@ go get github.com/360EntSecGroup-Skylar/excelize ```go package main -import "github.com/360EntSecGroup-Skylar/excelize" +import ( + "fmt" + + "github.com/360EntSecGroup-Skylar/excelize" +) func main() { f := excelize.NewFile() @@ -43,7 +47,7 @@ func main() { f.SetActiveSheet(index) // 根据指定路径保存文件 if err := f.SaveAs("Book1.xlsx"); err != nil { - println(err.Error()) + fmt.Println(err) } } ``` @@ -55,28 +59,32 @@ func main() { ```go package main -import "github.com/360EntSecGroup-Skylar/excelize" +import ( + "fmt" + + "github.com/360EntSecGroup-Skylar/excelize" +) func main() { f, err := excelize.OpenFile("Book1.xlsx") if err != nil { - println(err.Error()) + fmt.Println(err) return } // 获取工作表中指定单元格的值 cell, err := f.GetCellValue("Sheet1", "B2") if err != nil { - println(err.Error()) + fmt.Println(err) return } - println(cell) + fmt.Println(cell) // 获取 Sheet1 上所有单元格 rows, err := f.GetRows("Sheet1") for _, row := range rows { for _, colCell := range row { - print(colCell, "\t") + fmt.Print(colCell, "\t") } - println() + fmt.Println() } } ``` @@ -90,7 +98,11 @@ func main() { ```go package main -import "github.com/360EntSecGroup-Skylar/excelize" +import ( + "fmt" + + "github.com/360EntSecGroup-Skylar/excelize" +) func main() { categories := map[string]string{"A2": "Small", "A3": "Normal", "A4": "Large", "B1": "Apple", "C1": "Orange", "D1": "Pear"} @@ -103,12 +115,12 @@ func main() { 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"}}`); err != nil { - println(err.Error()) + fmt.Println(err) return } // 根据指定路径保存文件 if err := f.SaveAs("Book1.xlsx"); err != nil { - println(err.Error()) + fmt.Println(err) } } ``` @@ -119,6 +131,7 @@ func main() { package main import ( + "fmt" _ "image/gif" _ "image/jpeg" _ "image/png" @@ -129,24 +142,24 @@ import ( func main() { f, err := excelize.OpenFile("Book1.xlsx") if err != nil { - println(err.Error()) + fmt.Println(err) return } // 插入图片 if err := f.AddPicture("Sheet1", "A2", "image.png", ""); err != nil { - println(err.Error()) + fmt.Println(err) } // 在工作表中插入图片,并设置图片的缩放比例 if err := f.AddPicture("Sheet1", "D2", "image.jpg", `{"x_scale": 0.5, "y_scale": 0.5}`); err != nil { - println(err.Error()) + 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 { - println(err.Error()) + fmt.Println(err) } // 保存文件 if err = f.Save(); err != nil { - println(err.Error()) + fmt.Println(err) } } ``` diff --git a/cell_test.go b/cell_test.go index 60f8751a1a..1efbc5a871 100644 --- a/cell_test.go +++ b/cell_test.go @@ -110,7 +110,7 @@ func ExampleFile_SetCellFloat() { f := NewFile() var x = 3.14159265 if err := f.SetCellFloat("Sheet1", "A1", x, 2, 64); err != nil { - println(err.Error()) + fmt.Println(err) } val, _ := f.GetCellValue("Sheet1", "A1") fmt.Println(val) diff --git a/chart.go b/chart.go index 227cdee6e7..69c2c950bb 100644 --- a/chart.go +++ b/chart.go @@ -503,7 +503,11 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // // package main // -// import "github.com/360EntSecGroup-Skylar/excelize" +// import ( +// "fmt" +// +// "github.com/360EntSecGroup-Skylar/excelize" +// ) // // func main() { // categories := map[string]string{"A2": "Small", "A3": "Normal", "A4": "Large", "B1": "Apple", "C1": "Orange", "D1": "Pear"} @@ -516,12 +520,12 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // 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"},"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 { -// println(err.Error()) +// fmt.Println(err) // return // } // // Save xlsx file by the given path. // if err := f.SaveAs("Book1.xlsx"); err != nil { -// println(err.Error()) +// fmt.Println(err) // } // } // @@ -697,7 +701,11 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // // package main // -// import "github.com/360EntSecGroup-Skylar/excelize" +// import ( +// "fmt" +// +// "github.com/360EntSecGroup-Skylar/excelize" +// ) // // func main() { // categories := map[string]string{"A2": "Small", "A3": "Normal", "A4": "Large", "B1": "Apple", "C1": "Orange", "D1": "Pear"} @@ -710,12 +718,12 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // 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},"legend":{"position":"left","show_legend_key":false},"title":{"name":"Clustered Column - 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}}`, `{"type":"line","series":[{"name":"Sheet1!$A$4","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$4:$D$4"}],"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}}`); err != nil { -// println(err.Error()) +// fmt.Println(err) // return // } // // Save xlsx file by the given path. // if err := f.SaveAs("Book1.xlsx"); err != nil { -// println(err.Error()) +// fmt.Println(err) // } // } // diff --git a/excelize.go b/excelize.go index 9832c6a304..e12e769889 100644 --- a/excelize.go +++ b/excelize.go @@ -294,13 +294,13 @@ func (f *File) UpdateLinkedValue() error { // functions and/or macros. The file extension should be .xlsm. For example: // // if err := f.SetSheetPrOptions("Sheet1", excelize.CodeName("Sheet1")); err != nil { -// println(err.Error()) +// fmt.Println(err) // } // if err := f.AddVBAProject("vbaProject.bin"); err != nil { -// println(err.Error()) +// fmt.Println(err) // } // if err := f.SaveAs("macros.xlsm"); err != nil { -// println(err.Error()) +// fmt.Println(err) // } // func (f *File) AddVBAProject(bin string) error { diff --git a/excelize_test.go b/excelize_test.go index c7f5cad747..b78aac8eb3 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -1259,7 +1259,7 @@ func fillCells(f *File, sheet string, colCount, rowCount int) { for row := 1; row <= rowCount; row++ { cell, _ := CoordinatesToCellName(col, row) if err := f.SetCellStr(sheet, cell, cell); err != nil { - println(err.Error()) + fmt.Println(err) } } } diff --git a/picture.go b/picture.go index 0b91b91f88..3e24ce3a8a 100644 --- a/picture.go +++ b/picture.go @@ -59,18 +59,18 @@ func parseFormatPictureSet(formatSet string) (*formatPicture, error) { // f := excelize.NewFile() // // Insert a picture. // if err := f.AddPicture("Sheet1", "A2", "image.jpg", ""); err != nil { -// println(err.Error()) +// 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 { -// println(err.Error()) +// 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/360EntSecGroup-Skylar/excelize", "hyperlink_type": "External", "print_obj": true, "lock_aspect_ratio": false, "locked": false, "positioning": "oneCell"}`); err != nil { -// println(err.Error()) +// fmt.Println(err) // } // if err := f.SaveAs("Book1.xlsx"); err != nil { -// println(err.Error()) +// fmt.Println(err) // } // } // @@ -104,6 +104,7 @@ func (f *File) AddPicture(sheet, cell, picture, format string) error { // package main // // import ( +// "fmt" // _ "image/jpeg" // "io/ioutil" // @@ -115,13 +116,13 @@ func (f *File) AddPicture(sheet, cell, picture, format string) error { // // file, err := ioutil.ReadFile("image.jpg") // if err != nil { -// println(err.Error()) +// fmt.Println(err) // } // if err := f.AddPictureFromBytes("Sheet1", "A2", "", "Excel Logo", ".jpg", file); err != nil { -// println(err.Error()) +// fmt.Println(err) // } // if err := f.SaveAs("Book1.xlsx"); err != nil { -// println(err.Error()) +// fmt.Println(err) // } // } // @@ -424,16 +425,16 @@ func (f *File) getSheetRelationshipsTargetByID(sheet, rID string) string { // // f, err := excelize.OpenFile("Book1.xlsx") // if err != nil { -// println(err.Error()) +// fmt.Println(err) // return // } // file, raw, err := f.GetPicture("Sheet1", "A2") // if err != nil { -// println(err.Error()) +// fmt.Println(err) // return // } // if err := ioutil.WriteFile(file, raw, 0644); err != nil { -// println(err.Error()) +// fmt.Println(err) // } // func (f *File) GetPicture(sheet, cell string) (string, []byte, error) { diff --git a/pivotTable.go b/pivotTable.go index 70681cae8e..ee0d94e230 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -63,10 +63,10 @@ type PivotTableOption struct { // Columns: []string{"Type"}, // Data: []string{"Sales"}, // }); err != nil { -// println(err.Error()) +// fmt.Println(err) // } // if err := f.SaveAs("Book1.xlsx"); err != nil { -// println(err.Error()) +// fmt.Println(err) // } // } // diff --git a/rows.go b/rows.go index 40972ae5a7..23f3a2c162 100644 --- a/rows.go +++ b/rows.go @@ -25,18 +25,18 @@ import ( // // rows, err := f.Rows("Sheet1") // if err != nil { -// println(err.Error()) +// fmt.Println(err) // return // } // for rows.Next() { // row, err := rows.Columns() // if err != nil { -// println(err.Error()) +// fmt.Println(err) // } // for _, colCell := range row { -// print(colCell, "\t") +// fmt.Print(colCell, "\t") // } -// println() +// fmt.Println() // } // func (f *File) GetRows(sheet string) ([][]string, error) { @@ -152,18 +152,18 @@ func (err ErrSheetNotExist) Error() string { // // rows, err := f.Rows("Sheet1") // if err != nil { -// println(err.Error()) +// fmt.Println(err) // return // } // for rows.Next() { // row, err := rows.Columns() // if err != nil { -// println(err.Error()) +// fmt.Println(err) // } // for _, colCell := range row { -// print(colCell, "\t") +// fmt.Print(colCell, "\t") // } -// println() +// fmt.Println() // } // func (f *File) Rows(sheet string) (*Rows, error) { diff --git a/sheet.go b/sheet.go index a6ff2a1045..48671c02dc 100644 --- a/sheet.go +++ b/sheet.go @@ -344,7 +344,7 @@ func (f *File) GetSheetIndex(name string) int { // return // } // for index, name := range f.GetSheetMap() { -// println(index, name) +// fmt.Println(index, name) // } // func (f *File) GetSheetMap() map[int]string { diff --git a/sheet_test.go b/sheet_test.go index a03066a7fd..69c8f22f12 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -19,7 +19,7 @@ func ExampleFile_SetPageLayout() { "Sheet1", excelize.PageLayoutOrientation(excelize.OrientationLandscape), ); err != nil { - println(err.Error()) + fmt.Println(err) } if err := f.SetPageLayout( "Sheet1", @@ -27,7 +27,7 @@ func ExampleFile_SetPageLayout() { excelize.FitToHeight(2), excelize.FitToWidth(2), ); err != nil { - println(err.Error()) + fmt.Println(err) } // Output: } @@ -41,17 +41,17 @@ func ExampleFile_GetPageLayout() { fitToWidth excelize.FitToWidth ) if err := f.GetPageLayout("Sheet1", &orientation); err != nil { - println(err.Error()) + fmt.Println(err) } if err := f.GetPageLayout("Sheet1", &paperSize); err != nil { - println(err.Error()) + fmt.Println(err) } if err := f.GetPageLayout("Sheet1", &fitToHeight); err != nil { - println(err.Error()) + fmt.Println(err) } if err := f.GetPageLayout("Sheet1", &fitToWidth); err != nil { - println(err.Error()) + fmt.Println(err) } fmt.Println("Defaults:") fmt.Printf("- orientation: %q\n", orientation) diff --git a/sheetpr_test.go b/sheetpr_test.go index 6a35a6ecc1..25b67d753e 100644 --- a/sheetpr_test.go +++ b/sheetpr_test.go @@ -40,7 +40,7 @@ func ExampleFile_SetSheetPrOptions() { excelize.AutoPageBreaks(true), excelize.OutlineSummaryBelow(false), ); err != nil { - println(err.Error()) + fmt.Println(err) } // Output: } @@ -66,7 +66,7 @@ func ExampleFile_GetSheetPrOptions() { &autoPageBreaks, &outlineSummaryBelow, ); err != nil { - println(err.Error()) + fmt.Println(err) } fmt.Println("Defaults:") fmt.Printf("- codeName: %q\n", codeName) @@ -189,7 +189,7 @@ func ExampleFile_SetPageMargins() { excelize.PageMarginRight(1.0), excelize.PageMarginTop(1.0), ); err != nil { - println(err.Error()) + fmt.Println(err) } // Output: } @@ -215,7 +215,7 @@ func ExampleFile_GetPageMargins() { &marginRight, &marginTop, ); err != nil { - println(err.Error()) + fmt.Println(err) } fmt.Println("Defaults:") fmt.Println("- marginBottom:", marginBottom) diff --git a/sheetview_test.go b/sheetview_test.go index 8412002660..d999875030 100644 --- a/sheetview_test.go +++ b/sheetview_test.go @@ -47,7 +47,7 @@ func ExampleFile_SetSheetViewOptions() { excelize.ZoomScale(80), excelize.TopLeftCell("C3"), ); err != nil { - println(err.Error()) + fmt.Println(err) } var zoomScale excelize.ZoomScale @@ -55,22 +55,22 @@ func ExampleFile_SetSheetViewOptions() { fmt.Println("- zoomScale: 80") if err := f.SetSheetViewOptions(sheet, 0, excelize.ZoomScale(500)); err != nil { - println(err.Error()) + fmt.Println(err) } if err := f.GetSheetViewOptions(sheet, 0, &zoomScale); err != nil { - println(err.Error()) + fmt.Println(err) } fmt.Println("Used out of range value:") fmt.Println("- zoomScale:", zoomScale) if err := f.SetSheetViewOptions(sheet, 0, excelize.ZoomScale(123)); err != nil { - println(err.Error()) + fmt.Println(err) } if err := f.GetSheetViewOptions(sheet, 0, &zoomScale); err != nil { - println(err.Error()) + fmt.Println(err) } fmt.Println("Used correct value:") @@ -111,7 +111,7 @@ func ExampleFile_GetSheetViewOptions() { &zoomScale, &topLeftCell, ); err != nil { - println(err.Error()) + fmt.Println(err) } fmt.Println("Default:") @@ -125,27 +125,27 @@ func ExampleFile_GetSheetViewOptions() { fmt.Println("- topLeftCell:", `"`+topLeftCell+`"`) if err := f.SetSheetViewOptions(sheet, 0, excelize.TopLeftCell("B2")); err != nil { - println(err.Error()) + fmt.Println(err) } if err := f.GetSheetViewOptions(sheet, 0, &topLeftCell); err != nil { - println(err.Error()) + fmt.Println(err) } if err := f.SetSheetViewOptions(sheet, 0, excelize.ShowGridLines(false)); err != nil { - println(err.Error()) + fmt.Println(err) } if err := f.GetSheetViewOptions(sheet, 0, &showGridLines); err != nil { - println(err.Error()) + fmt.Println(err) } if err := f.SetSheetViewOptions(sheet, 0, excelize.ShowZeros(false)); err != nil { - println(err.Error()) + fmt.Println(err) } if err := f.GetSheetViewOptions(sheet, 0, &showZeros); err != nil { - println(err.Error()) + fmt.Println(err) } fmt.Println("After change:") diff --git a/sparkline_test.go b/sparkline_test.go index 45bf386e56..4b059ab982 100644 --- a/sparkline_test.go +++ b/sparkline_test.go @@ -298,12 +298,12 @@ func prepareSparklineDataset() *File { f.NewSheet("Sheet3") for row, data := range sheet2 { if err := f.SetSheetRow("Sheet2", fmt.Sprintf("A%d", row+1), &data); err != nil { - println(err.Error()) + fmt.Println(err) } } for row, data := range sheet3 { if err := f.SetSheetRow("Sheet3", fmt.Sprintf("A%d", row+1), &data); err != nil { - println(err.Error()) + fmt.Println(err) } } return f diff --git a/stream.go b/stream.go index c854d8bde1..98cf82892e 100644 --- a/stream.go +++ b/stream.go @@ -42,14 +42,14 @@ type StreamWriter struct { // file := excelize.NewFile() // streamWriter, err := file.NewStreamWriter("Sheet1") // if err != nil { -// println(err.Error()) +// fmt.Println(err) // } // styleID, err := file.NewStyle(`{"font":{"color":"#777777"}}`) // if err != nil { -// println(err.Error()) +// fmt.Println(err) // } // if err := streamWriter.SetRow("A1", []interface{}{excelize.Cell{StyleID: styleID, Value: "Data"}}); err != nil { -// println(err.Error()) +// fmt.Println(err) // } // for rowID := 2; rowID <= 102400; rowID++ { // row := make([]interface{}, 50) @@ -58,14 +58,14 @@ type StreamWriter struct { // } // cell, _ := excelize.CoordinatesToCellName(1, rowID) // if err := streamWriter.SetRow(cell, row); err != nil { -// println(err.Error()) +// fmt.Println(err) // } // } // if err := streamWriter.Flush(); err != nil { -// println(err.Error()) +// fmt.Println(err) // } // if err := file.SaveAs("Book1.xlsx"); err != nil { -// println(err.Error()) +// fmt.Println(err) // } // func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) { diff --git a/styles.go b/styles.go index ad3e825542..272d7280a7 100644 --- a/styles.go +++ b/styles.go @@ -2321,7 +2321,7 @@ func (f *File) GetCellStyle(sheet, axis string) (int, error) { // // style, err := f.NewStyle(`{"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 { -// println(err.Error()) +// fmt.Println(err) // } // err = f.SetCellStyle("Sheet1", "H9", "H9", style) // @@ -2330,7 +2330,7 @@ func (f *File) GetCellStyle(sheet, axis string) (int, error) { // // style, err := f.NewStyle(`{"fill":{"type":"gradient","color":["#FFFFFF","#E0EBF5"],"shading":1}}`) // if err != nil { -// println(err.Error()) +// fmt.Println(err) // } // err = f.SetCellStyle("Sheet1", "H9", "H9", style) // @@ -2338,7 +2338,7 @@ func (f *File) GetCellStyle(sheet, axis string) (int, error) { // // style, err := f.NewStyle(`{"fill":{"type":"pattern","color":["#E0EBF5"],"pattern":1}}`) // if err != nil { -// println(err.Error()) +// fmt.Println(err) // } // err = f.SetCellStyle("Sheet1", "H9", "H9", style) // @@ -2346,7 +2346,7 @@ func (f *File) GetCellStyle(sheet, axis string) (int, error) { // // 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":"","wrap_text":true}}`) // if err != nil { -// println(err.Error()) +// fmt.Println(err) // } // err = f.SetCellStyle("Sheet1", "H9", "H9", style) // @@ -2357,7 +2357,7 @@ func (f *File) GetCellStyle(sheet, axis string) (int, error) { // f.SetCellValue("Sheet1", "H9", 42920.5) // style, err := f.NewStyle(`{"number_format": 22}`) // if err != nil { -// println(err.Error()) +// fmt.Println(err) // } // err = f.SetCellStyle("Sheet1", "H9", "H9", style) // @@ -2365,7 +2365,7 @@ func (f *File) GetCellStyle(sheet, axis string) (int, error) { // // style, err := f.NewStyle(`{"font":{"bold":true,"italic":true,"family":"Times New Roman","size":36,"color":"#777777"}}`) // if err != nil { -// println(err.Error()) +// fmt.Println(err) // } // err = f.SetCellStyle("Sheet1", "H9", "H9", style) // @@ -2373,7 +2373,7 @@ func (f *File) GetCellStyle(sheet, axis string) (int, error) { // // style, err := f.NewStyle(`{"protection":{"hidden":true, "locked":true}}`) // if err != nil { -// println(err.Error()) +// fmt.Println(err) // } // err = f.SetCellStyle("Sheet1", "H9", "H9", style) // @@ -2507,7 +2507,7 @@ func (f *File) SetCellStyle(sheet, hcell, vcell string, styleID int) error { // // format, err = f.NewConditionalStyle(`{"font":{"color":"#9A0511"},"fill":{"type":"pattern","color":["#FEC7CE"],"pattern":1}}`) // if err != nil { -// println(err.Error()) +// fmt.Println(err) // } // f.SetConditionalFormat("Sheet1", "A1:A10", fmt.Sprintf(`[{"type":"cell","criteria":">","format":%d,"value":"6"}]`, format)) // From 6dcb7013eeeb8902be97c564c7a5a05dddcb06b8 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 21 Feb 2020 23:07:43 +0800 Subject: [PATCH 198/957] Resolve #582, support to set date field subtotal and names for pivot table - typo fixed and update do.dev badge in the README. --- README.md | 2 +- README_zh.md | 2 +- pivotTable.go | 58 +++++++++++++++++++++++++++++++++++++++------- pivotTable_test.go | 25 ++++++++++++++++++++ 4 files changed, 77 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index c81efd1b0a..fa1dda9ca9 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Build Status Code Coverage Go Report Card - GoDoc + go.dev Licenses Donate

diff --git a/README_zh.md b/README_zh.md index f75eec5d7e..44ab9b5838 100644 --- a/README_zh.md +++ b/README_zh.md @@ -4,7 +4,7 @@ Build Status Code Coverage Go Report Card - GoDoc + go.dev Licenses Donate

diff --git a/pivotTable.go b/pivotTable.go index ee0d94e230..696dfe7876 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -20,6 +20,8 @@ import ( // PivotTableOption directly maps the format settings of the pivot table. type PivotTableOption struct { DataRange string + DataSubtotal string + DataFieldName string PivotTableRange string Rows []string Columns []string @@ -28,9 +30,29 @@ type PivotTableOption struct { } // AddPivotTable provides the method to add pivot table by given pivot table -// options. 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: +// options. +// +// DataSubtotal specifies the aggregation function that applies to this data +// field. The default value is sum. The possible values for this attribute +// are: +// +// Average +// Count +// CountNums +// Max +// Min +// Product +// StdDev +// StdDevp +// Sum +// Var +// Varp +// +// DataFieldName specifies the name of the data field. Maximum 255 characters +// are allowed in data field name, excess characters will be truncated. +// +// 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 // @@ -62,6 +84,8 @@ type PivotTableOption struct { // Rows: []string{"Month", "Year"}, // Columns: []string{"Type"}, // Data: []string{"Sales"}, +// DataSubtotal: "Sum", +// DataFieldName: "Summarize as Sum", // }); err != nil { // fmt.Println(err) // } @@ -278,9 +302,9 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op if err != nil { return err } - for _, filedIdx := range rowFieldsIndex { + for _, fieldIdx := range rowFieldsIndex { pt.RowFields.Field = append(pt.RowFields.Field, &xlsxField{ - X: filedIdx, + X: fieldIdx, }) } @@ -297,9 +321,15 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op if err != nil { return err } + dataFieldName := opt.DataFieldName + if len(dataFieldName) > 255 { + dataFieldName = dataFieldName[0:255] + } for _, dataField := range dataFieldsIndex { pt.DataFields.DataField = append(pt.DataFields.DataField, &xlsxDataField{ - Fld: dataField, + Name: dataFieldName, + Fld: dataField, + Subtotal: f.getFieldsSubtotal(opt), }) } @@ -336,9 +366,9 @@ func (f *File) addPivotColFields(pt *xlsxPivotTableDefinition, opt *PivotTableOp if err != nil { return err } - for _, filedIdx := range colFieldsIndex { + for _, fieldIdx := range colFieldsIndex { pt.ColFields.Field = append(pt.ColFields.Field, &xlsxField{ - X: filedIdx, + X: fieldIdx, }) } @@ -430,6 +460,18 @@ func (f *File) getPivotFieldsIndex(fields []string, opt *PivotTableOption) ([]in return pivotFieldsIndex, nil } +// getFieldsSubtotal prepare data subtotal by given fields and pivot option. +func (f *File) getFieldsSubtotal(opt *PivotTableOption) (subtotal string) { + subtotal = "sum" + for _, enum := range []string{"average", "count", "countNums", "max", "min", "product", "stdDev", "stdDevp", "sum", "var", "varp"} { + if strings.ToLower(enum) == strings.ToLower(opt.DataSubtotal) { + subtotal = enum + return + } + } + return +} + // addWorkbookPivotCache add the association ID of the pivot cache in xl/workbook.xml. func (f *File) addWorkbookPivotCache(RID int) int { wb := f.workbookReader() diff --git a/pivotTable_test.go b/pivotTable_test.go index 5d841d8976..e40dbd60c8 100644 --- a/pivotTable_test.go +++ b/pivotTable_test.go @@ -4,6 +4,7 @@ import ( "fmt" "math/rand" "path/filepath" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -30,6 +31,8 @@ func TestAddPivotTable(t *testing.T) { Rows: []string{"Month", "Year"}, Columns: []string{"Type"}, Data: []string{"Sales"}, + DataSubtotal: "Sum", + DataFieldName: "Summarize by Sum", })) // Use different order of coordinate tests assert.NoError(t, f.AddPivotTable(&PivotTableOption{ @@ -38,6 +41,8 @@ func TestAddPivotTable(t *testing.T) { Rows: []string{"Month", "Year"}, Columns: []string{"Type"}, Data: []string{"Sales"}, + DataSubtotal: "Average", + DataFieldName: "Summarize by Average", })) assert.NoError(t, f.AddPivotTable(&PivotTableOption{ @@ -46,6 +51,8 @@ func TestAddPivotTable(t *testing.T) { Rows: []string{"Month", "Year"}, Columns: []string{"Region"}, Data: []string{"Sales"}, + DataSubtotal: "Count", + DataFieldName: "Summarize by Count", })) assert.NoError(t, f.AddPivotTable(&PivotTableOption{ DataRange: "Sheet1!$A$1:$E$31", @@ -53,12 +60,16 @@ func TestAddPivotTable(t *testing.T) { Rows: []string{"Month"}, Columns: []string{"Region", "Year"}, Data: []string{"Sales"}, + DataSubtotal: "CountNums", + DataFieldName: "Summarize by CountNums", })) assert.NoError(t, f.AddPivotTable(&PivotTableOption{ DataRange: "Sheet1!$A$1:$E$31", PivotTableRange: "Sheet1!$AE$2:$AG$33", Rows: []string{"Month", "Year"}, Data: []string{"Sales"}, + DataSubtotal: "Max", + DataFieldName: "Summarize by Max", })) f.NewSheet("Sheet2") assert.NoError(t, f.AddPivotTable(&PivotTableOption{ @@ -67,6 +78,8 @@ func TestAddPivotTable(t *testing.T) { Rows: []string{"Month"}, Columns: []string{"Region", "Type", "Year"}, Data: []string{"Sales"}, + DataSubtotal: "Min", + DataFieldName: "Summarize by Min", })) assert.NoError(t, f.AddPivotTable(&PivotTableOption{ DataRange: "Sheet1!$A$1:$E$31", @@ -74,6 +87,8 @@ func TestAddPivotTable(t *testing.T) { Rows: []string{"Month", "Type"}, Columns: []string{"Region", "Year"}, Data: []string{"Sales"}, + DataSubtotal: "Product", + DataFieldName: "Summarize by Product", })) // Test empty pivot table options @@ -135,6 +150,16 @@ func TestAddPivotTable(t *testing.T) { Data: []string{"Sales"}, }), `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{ + DataRange: "Sheet1!$A$1:$E$31", + PivotTableRange: "Sheet1!$G$2:$M$34", + Rows: []string{"Month", "Year"}, + Columns: []string{"Type"}, + Data: []string{"Sales"}, + DataSubtotal: "-", + DataFieldName: strings.Repeat("s", 256), + })) // Test adjust range with invalid range _, _, err := f.adjustRange("") From 8b20ea1685cdb010be8f95ffc047fa44e1a0e90a Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 25 Feb 2020 00:19:22 +0800 Subject: [PATCH 199/957] Fix #586, duplicate row with merged cells --- rows.go | 34 ++++++++++++++++++++++++++++ rows_test.go | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/rows.go b/rows.go index 23f3a2c162..0684b18125 100644 --- a/rows.go +++ b/rows.go @@ -519,6 +519,40 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) error { } else { xlsx.SheetData.Row = append(xlsx.SheetData.Row, rowCopy) } + return f.duplicateMergeCells(sheet, xlsx, row, row2) +} + +// duplicateMergeCells merge cells in the destination row if there are single +// row merged cells in the copied row. +func (f *File) duplicateMergeCells(sheet string, xlsx *xlsxWorksheet, row, row2 int) error { + if xlsx.MergeCells == nil { + return nil + } + if row > row2 { + row++ + } + for _, rng := range xlsx.MergeCells.Cells { + coordinates, err := f.areaRefToCoordinates(rng.Ref) + if err != nil { + return err + } + if coordinates[1] < row2 && row2 < coordinates[3] { + return nil + } + } + for i := 0; i < len(xlsx.MergeCells.Cells); i++ { + areaData := xlsx.MergeCells.Cells[i] + coordinates, _ := f.areaRefToCoordinates(areaData.Ref) + x1, y1, x2, y2 := coordinates[0], coordinates[1], coordinates[2], coordinates[3] + if y1 == y2 && y1 == row { + from, _ := CoordinatesToCellName(x1, row2) + to, _ := CoordinatesToCellName(x2, row2) + if err := f.MergeCell(sheet, from, to); err != nil { + return err + } + i++ + } + } return nil } diff --git a/rows_test.go b/rows_test.go index 9377d5e37b..a5ee428721 100644 --- a/rows_test.go +++ b/rows_test.go @@ -693,6 +693,55 @@ 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)) + } + 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) { + xlsx := newFileWithDefaults() + + assert.NoError(t, xlsx.DuplicateRowTo(sheet, 2, 1)) + assert.NoError(t, xlsx.DuplicateRowTo(sheet, 1, 8)) + + if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.InsertBeforeWithMergeCells"))) { + t.FailNow() + } + + expect := []MergeCell{ + {"B3:C3", "B2 Value"}, + {"C7:C10", ""}, + {"B1:C1", "B2 Value"}, + } + + mergeCells, err := xlsx.GetMergeCells(sheet) + assert.NoError(t, err) + for idx, val := range expect { + if !assert.Equal(t, val, mergeCells[idx]) { + t.FailNow() + } + } + }) +} + func TestDuplicateRowInvalidRownum(t *testing.T) { const sheet = "Sheet1" outFile := filepath.Join("test", "TestDuplicateRowInvalidRownum.%s.xlsx") @@ -753,6 +802,21 @@ func TestDuplicateRowInvalidRownum(t *testing.T) { } } +func TestDuplicateRowTo(t *testing.T) { + f := File{} + assert.EqualError(t, f.DuplicateRowTo("SheetN", 1, 2), "sheet SheetN is not exist") +} + +func TestDuplicateMergeCells(t *testing.T) { + f := File{} + xlsx := &xlsxWorksheet{MergeCells: &xlsxMergeCells{ + Cells: []*xlsxMergeCell{&xlsxMergeCell{Ref: "A1:-"}}, + }} + assert.EqualError(t, f.duplicateMergeCells("Sheet1", xlsx, 0, 0), `cannot convert cell "-" to coordinates: invalid cell name "-"`) + xlsx.MergeCells.Cells[0].Ref = "A1:B1" + assert.EqualError(t, f.duplicateMergeCells("SheetN", xlsx, 1, 2), "sheet SheetN is not exist") +} + func TestGetValueFrom(t *testing.T) { c := &xlsxC{T: "inlineStr"} f := NewFile() From 821a5d86725eb80b3f9e806d91eca5859497c2fa Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 26 Feb 2020 18:53:50 +0800 Subject: [PATCH 200/957] AddPivotTable API changed: new structure PivotTableField to hold pivot table fields for better scalability --- pivotTable.go | 119 +++++++++++++++++++++++++++++------------- pivotTable_test.go | 127 +++++++++++++++++++++------------------------ rows_test.go | 2 +- 3 files changed, 144 insertions(+), 104 deletions(-) diff --git a/pivotTable.go b/pivotTable.go index 696dfe7876..b7dc859999 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -20,19 +20,15 @@ import ( // PivotTableOption directly maps the format settings of the pivot table. type PivotTableOption struct { DataRange string - DataSubtotal string - DataFieldName string PivotTableRange string - Rows []string - Columns []string - Data []string - Page []string + Page []PivotTableField + Rows []PivotTableField + Columns []PivotTableField + Data []PivotTableField } -// AddPivotTable provides the method to add pivot table by given pivot table -// options. -// -// DataSubtotal specifies the aggregation function that applies to this data +// PivotTableField directly maps the field settings of the pivot table. +// Subtotal specifies the aggregation function that applies to this data // field. The default value is sum. The possible values for this attribute // are: // @@ -48,8 +44,16 @@ type PivotTableOption struct { // Var // Varp // -// DataFieldName specifies the name of the data field. Maximum 255 characters +// 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 { + Data string + Name string + Subtotal string +} + +// AddPivotTable provides the method to add pivot table by given pivot table +// options. // // 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: @@ -81,11 +85,9 @@ type PivotTableOption struct { // if err := f.AddPivotTable(&excelize.PivotTableOption{ // DataRange: "Sheet1!$A$1:$E$31", // PivotTableRange: "Sheet1!$G$2:$M$34", -// Rows: []string{"Month", "Year"}, -// Columns: []string{"Type"}, -// Data: []string{"Sales"}, -// DataSubtotal: "Sum", -// DataFieldName: "Summarize as Sum", +// Rows: []excelize.PivotTableField{{Data: "Month"}, {Data: "Year"}}, +// Columns: []excelize.PivotTableField{{Data: "Type"}}, +// Data: []excelize.PivotTableField{{Data: "Sales", Name: "Summarize", Subtotal: "Sum"}}, // }); err != nil { // fmt.Println(err) // } @@ -186,6 +188,8 @@ func (f *File) adjustRange(rangeStr string) (string, []int, error) { return rng[0], []int{x1, y1, x2, y2}, nil } +// getPivotFieldsOrder provides a function to get order list of pivot table +// fields. func (f *File) getPivotFieldsOrder(dataRange string) ([]string, error) { order := []string{} // data range has been checked @@ -321,15 +325,13 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op if err != nil { return err } - dataFieldName := opt.DataFieldName - if len(dataFieldName) > 255 { - dataFieldName = dataFieldName[0:255] - } - for _, dataField := range dataFieldsIndex { + dataFieldsSubtotals := f.getPivotTableFieldsSubtotal(opt.Data) + dataFieldsName := f.getPivotTableFieldsName(opt.Data) + for idx, dataField := range dataFieldsIndex { pt.DataFields.DataField = append(pt.DataFields.DataField, &xlsxDataField{ - Name: dataFieldName, + Name: dataFieldsName[idx], Fld: dataField, - Subtotal: f.getFieldsSubtotal(opt), + Subtotal: dataFieldsSubtotals[idx], }) } @@ -352,6 +354,18 @@ func inStrSlice(a []string, x string) int { return -1 } +// inPivotTableField provides a method to check if an element is present in +// pivot table fields list, and return the index of its location, otherwise +// return -1. +func inPivotTableField(a []PivotTableField, x string) int { + for idx, n := range a { + if x == n.Data { + return idx + } + } + return -1 +} + // addPivotColFields create pivot column fields by given pivot table // definition and option. func (f *File) addPivotColFields(pt *xlsxPivotTableDefinition, opt *PivotTableOption) error { @@ -385,9 +399,10 @@ func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opt *PivotTableOptio return err } for _, name := range order { - if inStrSlice(opt.Rows, name) != -1 { + if inPivotTableField(opt.Rows, name) != -1 { pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, &xlsxPivotField{ Axis: "axisRow", + Name: f.getPivotTableFieldName(name, opt.Rows), Items: &xlsxItems{ Count: 1, Item: []*xlsxItem{ @@ -397,9 +412,10 @@ func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opt *PivotTableOptio }) continue } - if inStrSlice(opt.Columns, name) != -1 { + if inPivotTableField(opt.Columns, name) != -1 { pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, &xlsxPivotField{ Axis: "axisCol", + Name: f.getPivotTableFieldName(name, opt.Columns), Items: &xlsxItems{ Count: 1, Item: []*xlsxItem{ @@ -409,7 +425,7 @@ func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opt *PivotTableOptio }) continue } - if inStrSlice(opt.Data, name) != -1 { + if inPivotTableField(opt.Data, name) != -1 { pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, &xlsxPivotField{ DataField: true, }) @@ -446,30 +462,61 @@ 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 []string, opt *PivotTableOption) ([]int, error) { +func (f *File) getPivotFieldsIndex(fields []PivotTableField, opt *PivotTableOption) ([]int, error) { pivotFieldsIndex := []int{} orders, err := f.getPivotFieldsOrder(opt.DataRange) if err != nil { return pivotFieldsIndex, err } for _, field := range fields { - if pos := inStrSlice(orders, field); pos != -1 { + if pos := inStrSlice(orders, field.Data); pos != -1 { pivotFieldsIndex = append(pivotFieldsIndex, pos) } } return pivotFieldsIndex, nil } -// getFieldsSubtotal prepare data subtotal by given fields and pivot option. -func (f *File) getFieldsSubtotal(opt *PivotTableOption) (subtotal string) { - subtotal = "sum" - for _, enum := range []string{"average", "count", "countNums", "max", "min", "product", "stdDev", "stdDevp", "sum", "var", "varp"} { - if strings.ToLower(enum) == strings.ToLower(opt.DataSubtotal) { - subtotal = enum - return +// getPivotTableFieldsSubtotal prepare fields subtotal by given pivot table fields. +func (f *File) getPivotTableFieldsSubtotal(fields []PivotTableField) []string { + field := make([]string, len(fields)) + enums := []string{"average", "count", "countNums", "max", "min", "product", "stdDev", "stdDevp", "sum", "var", "varp"} + inEnums := func(enums []string, val string) string { + for _, enum := range enums { + if strings.ToLower(enum) == strings.ToLower(val) { + return enum + } + } + return "sum" + } + for idx, fld := range fields { + field[idx] = inEnums(enums, fld.Subtotal) + } + return field +} + +// getPivotTableFieldsName prepare fields name list by given pivot table +// fields. +func (f *File) getPivotTableFieldsName(fields []PivotTableField) []string { + field := make([]string, len(fields)) + for idx, fld := range fields { + if len(fld.Name) > 255 { + field[idx] = fld.Name[0:255] + continue + } + field[idx] = fld.Name + } + return field +} + +// getPivotTableFieldName prepare field name by given pivot table fields. +func (f *File) getPivotTableFieldName(name string, fields []PivotTableField) string { + fieldsName := f.getPivotTableFieldsName(fields) + for idx, field := range fields { + if field.Data == name { + return fieldsName[idx] } } - return + return "" } // addWorkbookPivotCache add the association ID of the pivot cache in xl/workbook.xml. diff --git a/pivotTable_test.go b/pivotTable_test.go index e40dbd60c8..4379538523 100644 --- a/pivotTable_test.go +++ b/pivotTable_test.go @@ -28,67 +28,53 @@ func TestAddPivotTable(t *testing.T) { assert.NoError(t, f.AddPivotTable(&PivotTableOption{ DataRange: "Sheet1!$A$1:$E$31", PivotTableRange: "Sheet1!$G$2:$M$34", - Rows: []string{"Month", "Year"}, - Columns: []string{"Type"}, - Data: []string{"Sales"}, - DataSubtotal: "Sum", - DataFieldName: "Summarize by Sum", + Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}}, + Columns: []PivotTableField{{Data: "Type"}}, + Data: []PivotTableField{{Data: "Sales", Subtotal: "Sum", Name: "Summarize by Sum"}}, })) // Use different order of coordinate tests assert.NoError(t, f.AddPivotTable(&PivotTableOption{ DataRange: "Sheet1!$A$1:$E$31", PivotTableRange: "Sheet1!$U$34:$O$2", - Rows: []string{"Month", "Year"}, - Columns: []string{"Type"}, - Data: []string{"Sales"}, - DataSubtotal: "Average", - DataFieldName: "Summarize by Average", + Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}}, + Columns: []PivotTableField{{Data: "Type"}}, + Data: []PivotTableField{{Data: "Sales", Subtotal: "Average", Name: "Summarize by Average"}}, })) assert.NoError(t, f.AddPivotTable(&PivotTableOption{ DataRange: "Sheet1!$A$1:$E$31", PivotTableRange: "Sheet1!$W$2:$AC$34", - Rows: []string{"Month", "Year"}, - Columns: []string{"Region"}, - Data: []string{"Sales"}, - DataSubtotal: "Count", - DataFieldName: "Summarize by Count", + Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}}, + Columns: []PivotTableField{{Data: "Region"}}, + Data: []PivotTableField{{Data: "Sales", Subtotal: "Count", Name: "Summarize by Count"}}, })) assert.NoError(t, f.AddPivotTable(&PivotTableOption{ DataRange: "Sheet1!$A$1:$E$31", PivotTableRange: "Sheet1!$G$37:$W$50", - Rows: []string{"Month"}, - Columns: []string{"Region", "Year"}, - Data: []string{"Sales"}, - DataSubtotal: "CountNums", - DataFieldName: "Summarize by CountNums", + Rows: []PivotTableField{{Data: "Month"}}, + Columns: []PivotTableField{{Data: "Region"}, {Data: "Year"}}, + Data: []PivotTableField{{Data: "Sales", Subtotal: "CountNums", Name: "Summarize by CountNums"}}, })) assert.NoError(t, f.AddPivotTable(&PivotTableOption{ DataRange: "Sheet1!$A$1:$E$31", PivotTableRange: "Sheet1!$AE$2:$AG$33", - Rows: []string{"Month", "Year"}, - Data: []string{"Sales"}, - DataSubtotal: "Max", - DataFieldName: "Summarize by Max", + Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}}, + Data: []PivotTableField{{Data: "Sales", Subtotal: "Max", Name: "Summarize by Max"}}, })) f.NewSheet("Sheet2") assert.NoError(t, f.AddPivotTable(&PivotTableOption{ DataRange: "Sheet1!$A$1:$E$31", PivotTableRange: "Sheet2!$A$1:$AR$15", - Rows: []string{"Month"}, - Columns: []string{"Region", "Type", "Year"}, - Data: []string{"Sales"}, - DataSubtotal: "Min", - DataFieldName: "Summarize by Min", + Rows: []PivotTableField{{Data: "Month"}}, + Columns: []PivotTableField{{Data: "Region"}, {Data: "Type"}, {Data: "Year"}}, + Data: []PivotTableField{{Data: "Sales", Subtotal: "Min", Name: "Summarize by Min"}}, })) assert.NoError(t, f.AddPivotTable(&PivotTableOption{ DataRange: "Sheet1!$A$1:$E$31", PivotTableRange: "Sheet2!$A$18:$AR$54", - Rows: []string{"Month", "Type"}, - Columns: []string{"Region", "Year"}, - Data: []string{"Sales"}, - DataSubtotal: "Product", - DataFieldName: "Summarize by Product", + Rows: []PivotTableField{{Data: "Month"}, {Data: "Type"}}, + Columns: []PivotTableField{{Data: "Region"}, {Data: "Year"}}, + Data: []PivotTableField{{Data: "Sales", Subtotal: "Product", Name: "Summarize by Product"}}, })) // Test empty pivot table options @@ -97,68 +83,66 @@ func TestAddPivotTable(t *testing.T) { assert.EqualError(t, f.AddPivotTable(&PivotTableOption{ DataRange: "Sheet1!$A$1:$A$1", PivotTableRange: "Sheet1!$U$34:$O$2", - Rows: []string{"Month", "Year"}, - Columns: []string{"Type"}, - Data: []string{"Sales"}, + Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}}, + Columns: []PivotTableField{{Data: "Type"}}, + 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{ DataRange: "$A$1:$E$31", PivotTableRange: "Sheet1!$U$34:$O$2", - Rows: []string{"Month", "Year"}, - Columns: []string{"Type"}, - Data: []string{"Sales"}, + Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}}, + Columns: []PivotTableField{{Data: "Type"}}, + 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{ DataRange: "SheetN!$A$1:$E$31", PivotTableRange: "Sheet1!$U$34:$O$2", - Rows: []string{"Month", "Year"}, - Columns: []string{"Type"}, - Data: []string{"Sales"}, + Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}}, + Columns: []PivotTableField{{Data: "Type"}}, + Data: []PivotTableField{{Data: "Sales"}}, }), "sheet SheetN is 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", PivotTableRange: "$U$34:$O$2", - Rows: []string{"Month", "Year"}, - Columns: []string{"Type"}, - Data: []string{"Sales"}, + Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}}, + Columns: []PivotTableField{{Data: "Type"}}, + 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{ DataRange: "Sheet1!$A$1:$E$31", PivotTableRange: "SheetN!$U$34:$O$2", - Rows: []string{"Month", "Year"}, - Columns: []string{"Type"}, - Data: []string{"Sales"}, + Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}}, + Columns: []PivotTableField{{Data: "Type"}}, + Data: []PivotTableField{{Data: "Sales"}}, }), "sheet SheetN is not exist") // Test not exists worksheet in data range assert.EqualError(t, f.AddPivotTable(&PivotTableOption{ DataRange: "SheetN!$A$1:$E$31", PivotTableRange: "Sheet1!$U$34:$O$2", - Rows: []string{"Month", "Year"}, - Columns: []string{"Type"}, - Data: []string{"Sales"}, + Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}}, + Columns: []PivotTableField{{Data: "Type"}}, + Data: []PivotTableField{{Data: "Sales"}}, }), "sheet SheetN is not exist") // Test invalid row number in data range assert.EqualError(t, f.AddPivotTable(&PivotTableOption{ DataRange: "Sheet1!$A$0:$E$31", PivotTableRange: "Sheet1!$U$34:$O$2", - Rows: []string{"Month", "Year"}, - Columns: []string{"Type"}, - Data: []string{"Sales"}, + Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}}, + Columns: []PivotTableField{{Data: "Type"}}, + Data: []PivotTableField{{Data: "Sales"}}, }), `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{ DataRange: "Sheet1!$A$1:$E$31", PivotTableRange: "Sheet1!$G$2:$M$34", - Rows: []string{"Month", "Year"}, - Columns: []string{"Type"}, - Data: []string{"Sales"}, - DataSubtotal: "-", - DataFieldName: strings.Repeat("s", 256), + Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}}, + Columns: []PivotTableField{{Data: "Type"}}, + Data: []PivotTableField{{Data: "Sales", Subtotal: "-", Name: strings.Repeat("s", 256)}}, })) // Test adjust range with invalid range @@ -173,9 +157,9 @@ func TestAddPivotTable(t *testing.T) { assert.EqualError(t, f.addPivotCache(0, "", &PivotTableOption{ DataRange: "$A$1:$E$31", PivotTableRange: "Sheet1!$U$34:$O$2", - Rows: []string{"Month", "Year"}, - Columns: []string{"Type"}, - Data: []string{"Sales"}, + Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}}, + Columns: []PivotTableField{{Data: "Type"}}, + Data: []PivotTableField{{Data: "Sales"}}, }, nil), "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") @@ -185,11 +169,20 @@ func TestAddPivotTable(t *testing.T) { assert.EqualError(t, f.addPivotFields(nil, &PivotTableOption{ DataRange: "$A$1:$E$31", PivotTableRange: "Sheet1!$U$34:$O$2", - Rows: []string{"Month", "Year"}, - Columns: []string{"Type"}, - Data: []string{"Sales"}, + Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}}, + Columns: []PivotTableField{{Data: "Type"}}, + Data: []PivotTableField{{Data: "Sales"}}, }), `parameter 'DataRange' parsing error: parameter is invalid`) // Test get pivot fields index with empty data range - _, err = f.getPivotFieldsIndex([]string{}, &PivotTableOption{}) + _, err = f.getPivotFieldsIndex([]PivotTableField{}, &PivotTableOption{}) assert.EqualError(t, err, `parameter 'DataRange' parsing error: parameter is required`) } + +func TestInStrSlice(t *testing.T) { + assert.EqualValues(t, -1, inStrSlice([]string{}, "")) +} + +func TestGetPivotTableFieldName(t *testing.T) { + f := NewFile() + f.getPivotTableFieldName("-", []PivotTableField{}) +} diff --git a/rows_test.go b/rows_test.go index a5ee428721..a53b0a9350 100644 --- a/rows_test.go +++ b/rows_test.go @@ -810,7 +810,7 @@ func TestDuplicateRowTo(t *testing.T) { func TestDuplicateMergeCells(t *testing.T) { f := File{} xlsx := &xlsxWorksheet{MergeCells: &xlsxMergeCells{ - Cells: []*xlsxMergeCell{&xlsxMergeCell{Ref: "A1:-"}}, + Cells: []*xlsxMergeCell{{Ref: "A1:-"}}, }} assert.EqualError(t, f.duplicateMergeCells("Sheet1", xlsx, 0, 0), `cannot convert cell "-" to coordinates: invalid cell name "-"`) xlsx.MergeCells.Cells[0].Ref = "A1:B1" From 386a42dfa25f4ce5d5daf95e87ab65c528dbdd38 Mon Sep 17 00:00:00 2001 From: xxb-at-julichina <57735034+xxb-at-julichina@users.noreply.github.com> Date: Fri, 28 Feb 2020 15:53:04 +0800 Subject: [PATCH 201/957] Update rows.go --- rows.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rows.go b/rows.go index 0684b18125..e00a627ed2 100644 --- a/rows.go +++ b/rows.go @@ -238,7 +238,8 @@ func (f *File) SetRowHeight(sheet string, row int, height float64) error { // name and row index. func (f *File) getRowHeight(sheet string, row int) int { xlsx, _ := f.workSheetReader(sheet) - for _, v := range xlsx.SheetData.Row { + for i := range xlsx.SheetData.Row { + v := &xlsx.SheetData.Row[i] if v.R == row+1 && v.Ht != 0 { return int(convertRowHeightToPixels(v.Ht)) } From 1d87da57ecf5e13203b6441dd97160885981545e Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 1 Mar 2020 00:34:41 +0800 Subject: [PATCH 202/957] Resolve #492, init support for insert and remove page break --- .travis.yml | 1 + sheet.go | 109 ++++++++++++++++++++++++++++++++++++++++++++++++ sheet_test.go | 37 ++++++++++++++++ xmlWorksheet.go | 6 +-- 4 files changed, 150 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1cb1d496e6..d94d5d81f0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,7 @@ go: - 1.11.x - 1.12.x - 1.13.x + - 1.14.x os: - linux diff --git a/sheet.go b/sheet.go index 48671c02dc..08b0e96f3d 100644 --- a/sheet.go +++ b/sheet.go @@ -1437,6 +1437,115 @@ func (f *File) UngroupSheets() 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 axis, 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 + var rowBrk, colBrk = -1, -1 + if ws, err = f.workSheetReader(sheet); err != nil { + return + } + if col, row, err = CellNameToCoordinates(cell); err != nil { + return + } + col-- + row-- + if col == row && col == 0 { + return + } + if ws.RowBreaks == nil { + ws.RowBreaks = &xlsxBreaks{} + } + if ws.ColBreaks == nil { + ws.ColBreaks = &xlsxBreaks{} + } + + for idx, brk := range ws.RowBreaks.Brk { + if brk.ID == row { + rowBrk = idx + } + } + for idx, brk := range ws.ColBreaks.Brk { + if brk.ID == col { + colBrk = idx + } + } + + if row != 0 && rowBrk == -1 { + ws.RowBreaks.Brk = append(ws.RowBreaks.Brk, &xlsxBrk{ + ID: row, + Max: 16383, + Man: true, + }) + ws.RowBreaks.ManualBreakCount++ + } + if col != 0 && colBrk == -1 { + ws.ColBreaks.Brk = append(ws.ColBreaks.Brk, &xlsxBrk{ + ID: col, + Max: 1048575, + Man: true, + }) + ws.ColBreaks.ManualBreakCount++ + } + ws.RowBreaks.Count = len(ws.RowBreaks.Brk) + ws.ColBreaks.Count = len(ws.ColBreaks.Brk) + return +} + +// RemovePageBreak remove a page break by given worksheet name and axis. +func (f *File) RemovePageBreak(sheet, cell string) (err error) { + var ws *xlsxWorksheet + var row, col int + if ws, err = f.workSheetReader(sheet); err != nil { + return + } + if col, row, err = CellNameToCoordinates(cell); err != nil { + return + } + col-- + row-- + if col == row && col == 0 { + return + } + removeBrk := func(ID int, brks []*xlsxBrk) []*xlsxBrk { + for i, brk := range brks { + if brk.ID == ID { + brks = append(brks[:i], brks[i+1:]...) + } + } + return brks + } + if ws.RowBreaks == nil || ws.ColBreaks == nil { + return + } + rowBrks := len(ws.RowBreaks.Brk) + colBrks := len(ws.ColBreaks.Brk) + if rowBrks > 0 && rowBrks == colBrks { + ws.RowBreaks.Brk = removeBrk(row, ws.RowBreaks.Brk) + ws.ColBreaks.Brk = removeBrk(col, ws.ColBreaks.Brk) + ws.RowBreaks.Count = len(ws.RowBreaks.Brk) + ws.ColBreaks.Count = len(ws.ColBreaks.Brk) + ws.RowBreaks.ManualBreakCount-- + ws.ColBreaks.ManualBreakCount-- + return + } + if rowBrks > 0 && rowBrks > colBrks { + ws.RowBreaks.Brk = removeBrk(row, ws.RowBreaks.Brk) + ws.RowBreaks.Count = len(ws.RowBreaks.Brk) + ws.RowBreaks.ManualBreakCount-- + return + } + if colBrks > 0 && colBrks > rowBrks { + ws.ColBreaks.Brk = removeBrk(col, ws.ColBreaks.Brk) + ws.ColBreaks.Count = len(ws.ColBreaks.Brk) + ws.ColBreaks.ManualBreakCount-- + } + return +} + // 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 { diff --git a/sheet_test.go b/sheet_test.go index 69c8f22f12..38d86e64e2 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -264,6 +264,43 @@ func TestUngroupSheets(t *testing.T) { assert.NoError(t, f.UngroupSheets()) } +func TestInsertPageBreak(t *testing.T) { + f := excelize.NewFile() + assert.NoError(t, f.InsertPageBreak("Sheet1", "A1")) + assert.NoError(t, f.InsertPageBreak("Sheet1", "B2")) + assert.NoError(t, f.InsertPageBreak("Sheet1", "C3")) + assert.NoError(t, f.InsertPageBreak("Sheet1", "C3")) + assert.EqualError(t, f.InsertPageBreak("Sheet1", "A"), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.EqualError(t, f.InsertPageBreak("SheetN", "C3"), "sheet SheetN is not exist") + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestInsertPageBreak.xlsx"))) +} + +func TestRemovePageBreak(t *testing.T) { + f := excelize.NewFile() + assert.NoError(t, f.RemovePageBreak("Sheet1", "A2")) + + assert.NoError(t, f.InsertPageBreak("Sheet1", "A2")) + assert.NoError(t, f.InsertPageBreak("Sheet1", "B2")) + assert.NoError(t, f.RemovePageBreak("Sheet1", "A1")) + assert.NoError(t, f.RemovePageBreak("Sheet1", "B2")) + + assert.NoError(t, f.InsertPageBreak("Sheet1", "C3")) + assert.NoError(t, f.RemovePageBreak("Sheet1", "C3")) + + assert.NoError(t, f.InsertPageBreak("Sheet1", "A3")) + assert.NoError(t, f.RemovePageBreak("Sheet1", "B3")) + assert.NoError(t, f.RemovePageBreak("Sheet1", "A3")) + + f.NewSheet("Sheet2") + assert.NoError(t, f.InsertPageBreak("Sheet2", "B2")) + assert.NoError(t, f.InsertPageBreak("Sheet2", "C2")) + assert.NoError(t, f.RemovePageBreak("Sheet2", "B2")) + + assert.EqualError(t, f.RemovePageBreak("Sheet1", "A"), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.EqualError(t, f.RemovePageBreak("SheetN", "C3"), "sheet SheetN is not exist") + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestRemovePageBreak.xlsx"))) +} + func TestGetSheetName(t *testing.T) { f, _ := excelize.OpenFile(filepath.Join("test", "Book1.xlsx")) assert.Equal(t, "Sheet1", f.GetSheetName(1)) diff --git a/xmlWorksheet.go b/xmlWorksheet.go index dda1b78aab..aa33819c1f 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -357,9 +357,9 @@ type xlsxBrk struct { // xlsxBreaks directly maps a collection of the row or column breaks. type xlsxBreaks struct { - Brk *xlsxBrk `xml:"brk"` - Count int `xml:"count,attr,omitempty"` - ManualBreakCount int `xml:"manualBreakCount,attr,omitempty"` + Brk []*xlsxBrk `xml:"brk"` + Count int `xml:"count,attr,omitempty"` + ManualBreakCount int `xml:"manualBreakCount,attr,omitempty"` } // xlsxCustomSheetView directly maps the customSheetView element. From 1e3c85024d3bbc650c2f6a85fb075804af74720b Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 3 Mar 2020 00:15:03 +0800 Subject: [PATCH 203/957] Resolve #571, init remove conditional format support --- README.md | 2 +- README_zh.md | 2 +- styles.go | 16 ++++++++++++++++ styles_test.go | 16 ++++++++++++++++ 4 files changed, 34 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fa1dda9ca9..821bbd7686 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ ## Introduction Excelize is a library written in pure Go providing a set of functions that allow you to write to and read from XLSX files. Supports reading and writing XLSX file generated by Microsoft Excel™ 2007 and later. -Supports saving a file without losing original charts of XLSX. This library needs Go version 1.10 or later. The full API docs can be seen using go's built-in documentation tool, or online at [godoc.org](https://godoc.org/github.com/360EntSecGroup-Skylar/excelize) and [docs reference](https://xuri.me/excelize/). +Supports saving a file without losing original charts of XLSX. This library needs Go version 1.10 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/360EntSecGroup-Skylar/excelize/v2?tab=doc) and [docs reference](https://xuri.me/excelize/). ## Basic Usage diff --git a/README_zh.md b/README_zh.md index 44ab9b5838..18db28ffa7 100644 --- a/README_zh.md +++ b/README_zh.md @@ -13,7 +13,7 @@ ## 简介 -Excelize 是 Go 语言编写的用于操作 Office Excel 文档类库,基于 ECMA-376 Office OpenXML 标准。可以使用它来读取、写入由 Microsoft Excel™ 2007 及以上版本创建的 XLSX 文档。相比较其他的开源类库,Excelize 支持写入原本带有图片(表)、透视表和切片器等复杂样式的文档,还支持向 Excel 文档中插入图片与图表,并且在保存后不会丢失文档原有样式,可以应用于各类报表系统中。使用本类库要求使用的 Go 语言为 1.10 或更高版本,完整的 API 使用文档请访问 [godoc.org](https://godoc.org/github.com/360EntSecGroup-Skylar/excelize) 或查看 [参考文档](https://xuri.me/excelize/)。 +Excelize 是 Go 语言编写的用于操作 Office Excel 文档类库,基于 ECMA-376 Office OpenXML 标准。可以使用它来读取、写入由 Microsoft Excel™ 2007 及以上版本创建的 XLSX 文档。相比较其他的开源类库,Excelize 支持写入原本带有图片(表)、透视表和切片器等复杂样式的文档,还支持向 Excel 文档中插入图片与图表,并且在保存后不会丢失文档原有样式,可以应用于各类报表系统中。使用本类库要求使用的 Go 语言为 1.10 或更高版本,完整的 API 使用文档请访问 [go.dev](https://pkg.go.dev/github.com/360EntSecGroup-Skylar/excelize/v2?tab=doc) 或查看 [参考文档](https://xuri.me/excelize/)。 ## 快速上手 diff --git a/styles.go b/styles.go index 272d7280a7..caf2732ba2 100644 --- a/styles.go +++ b/styles.go @@ -2676,6 +2676,22 @@ func (f *File) SetConditionalFormat(sheet, area, formatSet string) error { return err } +// UnsetConditionalFormat provides a function to unset the conditional format +// by given worksheet name and range. +func (f *File) UnsetConditionalFormat(sheet, area string) error { + ws, err := f.workSheetReader(sheet) + if err != nil { + return err + } + for i, cf := range ws.ConditionalFormatting { + if cf.SQRef == area { + ws.ConditionalFormatting = append(ws.ConditionalFormatting[:i], ws.ConditionalFormatting[i+1:]...) + return nil + } + } + return nil +} + // 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. diff --git a/styles_test.go b/styles_test.go index a53670045d..4e9b41167b 100644 --- a/styles_test.go +++ b/styles_test.go @@ -1,6 +1,8 @@ package excelize import ( + "fmt" + "path/filepath" "testing" "github.com/stretchr/testify/assert" @@ -166,6 +168,20 @@ func TestSetConditionalFormat(t *testing.T) { } } +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}}`) + 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. + assert.EqualError(t, f.UnsetConditionalFormat("SheetN", "A1:A10"), "sheet SheetN is not exist") + // Save xlsx file by the given path. + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestUnsetConditionalFormat.xlsx"))) +} + func TestNewStyle(t *testing.T) { f := NewFile() styleID, err := f.NewStyle(`{"font":{"bold":true,"italic":true,"family":"Times New Roman","size":36,"color":"#777777"}}`) From 83eedce70de7a1ddeb3a4446f86b13bc6ff0b5ec Mon Sep 17 00:00:00 2001 From: Vaibhav Nayak Date: Tue, 3 Mar 2020 17:01:02 +0530 Subject: [PATCH 204/957] Export ExcelDateToTime function to convert excel date to time Signed-off-by: Vaibhav Nayak --- date.go | 8 ++++++++ date_test.go | 32 +++++++++++++++++++++++--------- errors.go | 4 ++++ errors_test.go | 4 ++++ 4 files changed, 39 insertions(+), 9 deletions(-) diff --git a/date.go b/date.go index dad39b5253..172c32c58d 100644 --- a/date.go +++ b/date.go @@ -172,3 +172,11 @@ func timeFromExcelTime(excelTime float64, date1904 bool) time.Time { durationPart := time.Duration(dayNanoSeconds * floatPart) return date.Add(durationDays).Add(durationPart) } + +// 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) + } + return timeFromExcelTime(excelDate, use1904Format), nil +} diff --git a/date_test.go b/date_test.go index 2885af07a6..ee01356efc 100644 --- a/date_test.go +++ b/date_test.go @@ -28,6 +28,14 @@ var trueExpectedDateList = []dateTest{ {401769.00000000000, time.Date(3000, time.January, 1, 0, 0, 0, 0, time.UTC)}, } +var excelTimeInputList = []dateTest{ + {0.0, time.Date(1899, 12, 30, 0, 0, 0, 0, time.UTC)}, + {60.0, time.Date(1900, 2, 28, 0, 0, 0, 0, time.UTC)}, + {61.0, time.Date(1900, 3, 1, 0, 0, 0, 0, time.UTC)}, + {41275.0, time.Date(2013, 1, 1, 0, 0, 0, 0, time.UTC)}, + {401769.0, time.Date(3000, 1, 1, 0, 0, 0, 0, time.UTC)}, +} + func TestTimeToExcelTime(t *testing.T) { for i, test := range trueExpectedDateList { t.Run(fmt.Sprintf("TestData%d", i+1), func(t *testing.T) { @@ -53,15 +61,7 @@ func TestTimeToExcelTime_Timezone(t *testing.T) { } func TestTimeFromExcelTime(t *testing.T) { - trueExpectedInputList := []dateTest{ - {0.0, time.Date(1899, 12, 30, 0, 0, 0, 0, time.UTC)}, - {60.0, time.Date(1900, 2, 28, 0, 0, 0, 0, time.UTC)}, - {61.0, time.Date(1900, 3, 1, 0, 0, 0, 0, time.UTC)}, - {41275.0, time.Date(2013, 1, 1, 0, 0, 0, 0, time.UTC)}, - {401769.0, time.Date(3000, 1, 1, 0, 0, 0, 0, time.UTC)}, - } - - for i, test := range trueExpectedInputList { + for i, test := range excelTimeInputList { t.Run(fmt.Sprintf("TestData%d", i+1), func(t *testing.T) { assert.Equal(t, test.GoValue, timeFromExcelTime(test.ExcelValue, false)) }) @@ -73,3 +73,17 @@ func TestTimeFromExcelTime_1904(t *testing.T) { timeFromExcelTime(61, true) timeFromExcelTime(62, true) } + +func TestExcelDateToTime(t *testing.T) { + // Check normal case + for i, test := range excelTimeInputList { + t.Run(fmt.Sprintf("TestData%d", i+1), func(t *testing.T) { + timeValue, err := ExcelDateToTime(test.ExcelValue, false) + assert.Equal(t, test.GoValue, timeValue) + assert.NoError(t, err) + }) + } + // Check error case + _, err := ExcelDateToTime(-1, false) + assert.EqualError(t, err, "invalid date value -1.000000, negative values are not supported supported") +} diff --git a/errors.go b/errors.go index 456049743e..5576ecdb68 100644 --- a/errors.go +++ b/errors.go @@ -22,3 +22,7 @@ func newInvalidRowNumberError(row int) error { func newInvalidCellNameError(cell string) error { return fmt.Errorf("invalid cell name %q", cell) } + +func newInvalidExcelDateError(dateValue float64) error { + return fmt.Errorf("invalid date value %f, negative values are not supported supported", dateValue) +} diff --git a/errors_test.go b/errors_test.go index 89d241c7ea..207e80aabb 100644 --- a/errors_test.go +++ b/errors_test.go @@ -19,3 +19,7 @@ func TestNewInvalidCellNameError(t *testing.T) { assert.EqualError(t, newInvalidCellNameError("A"), "invalid cell name \"A\"") assert.EqualError(t, newInvalidCellNameError(""), "invalid cell name \"\"") } + +func TestNewInvalidExcelDateError(t *testing.T) { + assert.EqualError(t, newInvalidExcelDateError(-1), "invalid date value -1.000000, negative values are not supported supported") +} From 2ccb8f62edd5d1ce039e663591964b9066fd2f4e Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 9 Mar 2020 00:08:47 +0800 Subject: [PATCH 205/957] Remove calculated properties to make recalculate formulas in some spreadsheet applications, such as Kingsoft WPS --- excelize.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/excelize.go b/excelize.go index e12e769889..0962122537 100644 --- a/excelize.go +++ b/excelize.go @@ -273,6 +273,9 @@ func replaceStyleRelationshipsNameSpaceBytes(contentMarshal []byte) []byte { //
// func (f *File) UpdateLinkedValue() error { + wb := f.workbookReader() + // recalculate formulas + wb.CalcPr = nil for _, name := range f.GetSheetMap() { xlsx, err := f.workSheetReader(name) if err != nil { From 9e2318cefa4ebaa7bf6b1dbc95b30ad7a32366b1 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 10 Mar 2020 00:04:23 +0800 Subject: [PATCH 206/957] Resolve #470, export Style structs to allow create the style for cells by given JSON or structure --- excelize.go | 2 +- excelize_test.go | 40 ++++++++++- shape.go | 2 +- styles.go | 176 +++++++++++++++++++++++++---------------------- styles_test.go | 23 +++++++ xmlChart.go | 2 +- xmlDrawing.go | 4 +- xmlStyles.go | 84 ++++++++++++---------- 8 files changed, 207 insertions(+), 126 deletions(-) diff --git a/excelize.go b/excelize.go index 0962122537..795120d8ba 100644 --- a/excelize.go +++ b/excelize.go @@ -137,7 +137,7 @@ func (f *File) setDefaultTimeStyle(sheet, axis string, format int) error { return err } if s == 0 { - style, _ := f.NewStyle(`{"number_format": ` + strconv.Itoa(format) + `}`) + style, _ := f.NewStyle(&Style{NumFmt: format}) _ = f.SetCellStyle(sheet, axis, axis, style) } return err diff --git a/excelize_test.go b/excelize_test.go index b78aac8eb3..1ce4fe95a1 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -580,7 +580,45 @@ func TestSetCellStyleBorder(t *testing.T) { assert.NoError(t, f.SetCellStyle("Sheet1", "M28", "K24", style)) // Test set border and solid style pattern fill for a single cell. - style, err = f.NewStyle(`{"border":[{"type":"left","color":"0000FF","style":8},{"type":"top","color":"00FF00","style":9},{"type":"bottom","color":"FFFF00","style":10},{"type":"right","color":"FF0000","style":11},{"type":"diagonalDown","color":"A020F0","style":12},{"type":"diagonalUp","color":"A020F0","style":13}],"fill":{"type":"pattern","color":["#E0EBF5"],"pattern":1}}`) + style, err = f.NewStyle(&Style{ + Border: []Border{ + { + Type: "left", + Color: "0000FF", + Style: 8, + }, + { + Type: "top", + Color: "00FF00", + Style: 9, + }, + { + Type: "bottom", + Color: "FFFF00", + Style: 10, + }, + { + Type: "right", + Color: "FF0000", + Style: 11, + }, + { + Type: "diagonalDown", + Color: "A020F0", + Style: 12, + }, + { + Type: "diagonalUp", + Color: "A020F0", + Style: 13, + }, + }, + Fill: Fill{ + Type: "pattern", + Color: []string{"#E0EBF5"}, + Pattern: 1, + }, + }) if !assert.NoError(t, err) { t.FailNow() } diff --git a/shape.go b/shape.go index e9bdb42ea4..0455b220d2 100644 --- a/shape.go +++ b/shape.go @@ -378,7 +378,7 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, formatSet *format if len(formatSet.Paragraph) < 1 { formatSet.Paragraph = []formatShapeParagraph{ { - Font: formatFont{ + Font: Font{ Bold: false, Italic: false, Underline: "none", diff --git a/styles.go b/styles.go index caf2732ba2..175a17c42b 100644 --- a/styles.go +++ b/styles.go @@ -1024,16 +1024,16 @@ func (f *File) styleSheetWriter() { // parseFormatStyleSet provides a function to parse the format settings of the // cells and conditional formats. -func parseFormatStyleSet(style string) (*formatStyle, error) { - format := formatStyle{ +func parseFormatStyleSet(style string) (*Style, error) { + format := Style{ DecimalPlaces: 2, } err := json.Unmarshal([]byte(style), &format) return &format, err } -// NewStyle provides a function to create style for cells by given style -// format. Note that the color field uses RGB color code. +// NewStyle provides a function to create the style for cells by given JSON or +// structure. Note that the color field uses RGB color code. // // The following shows the border styles sorted by excelize index number: // @@ -1888,18 +1888,26 @@ func parseFormatStyleSet(style string) (*formatStyle, error) { // // f := excelize.NewFile() // f.SetCellValue("Sheet1", "A6", 42920.5) -// style, err := f.NewStyle(`{"custom_number_format": "[$-380A]dddd\\,\\ dd\" de \"mmmm\" de \"yyyy;@"}`) +// 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 string) (int, error) { +func (f *File) NewStyle(style interface{}) (int, error) { + var fs *Style + var err error var cellXfsID, fontID, borderID, fillID int - s := f.stylesReader() - fs, err := parseFormatStyleSet(style) - if err != nil { - return cellXfsID, err + switch v := style.(type) { + case string: + fs, err = parseFormatStyleSet(v) + if err != nil { + return cellXfsID, err + } + case *Style: + fs = v } + s := f.stylesReader() numFmtID := setNumFmt(s, fs) if fs.Font != nil { @@ -1978,34 +1986,34 @@ func (f *File) readDefaultFont() *xlsxFont { // setFont provides a function to add font style by given cell format // settings. -func (f *File) setFont(formatStyle *formatStyle) *xlsxFont { +func (f *File) setFont(style *Style) *xlsxFont { fontUnderlineType := map[string]string{"single": "single", "double": "double"} - if formatStyle.Font.Size < 1 { - formatStyle.Font.Size = 11 + if style.Font.Size < 1 { + style.Font.Size = 11 } - if formatStyle.Font.Color == "" { - formatStyle.Font.Color = "#000000" + if style.Font.Color == "" { + style.Font.Color = "#000000" } fnt := xlsxFont{ - Sz: &attrValFloat{Val: float64Ptr(formatStyle.Font.Size)}, - Color: &xlsxColor{RGB: getPaletteColor(formatStyle.Font.Color)}, - Name: &attrValString{Val: stringPtr(formatStyle.Font.Family)}, + 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)}, } - if formatStyle.Font.Bold { - fnt.B = &formatStyle.Font.Bold + if style.Font.Bold { + fnt.B = &style.Font.Bold } - if formatStyle.Font.Italic { - fnt.I = &formatStyle.Font.Italic + if style.Font.Italic { + fnt.I = &style.Font.Italic } if *fnt.Name.Val == "" { *fnt.Name.Val = f.GetDefaultFont() } - if formatStyle.Font.Strike { + if style.Font.Strike { strike := true fnt.Strike = &strike } - val, ok := fontUnderlineType[formatStyle.Font.Underline] + val, ok := fontUnderlineType[style.Font.Underline] if ok { fnt.U = &attrValString{Val: stringPtr(val)} } @@ -2014,36 +2022,36 @@ func (f *File) setFont(formatStyle *formatStyle) *xlsxFont { // setNumFmt provides a function to check if number format code in the range // of built-in values. -func setNumFmt(style *xlsxStyleSheet, formatStyle *formatStyle) int { +func setNumFmt(styleSheet *xlsxStyleSheet, style *Style) int { dp := "0." numFmtID := 164 // Default custom number format code from 164. - if formatStyle.DecimalPlaces < 0 || formatStyle.DecimalPlaces > 30 { - formatStyle.DecimalPlaces = 2 + if style.DecimalPlaces < 0 || style.DecimalPlaces > 30 { + style.DecimalPlaces = 2 } - for i := 0; i < formatStyle.DecimalPlaces; i++ { + for i := 0; i < style.DecimalPlaces; i++ { dp += "0" } - if formatStyle.CustomNumFmt != nil { - return setCustomNumFmt(style, formatStyle) + if style.CustomNumFmt != nil { + return setCustomNumFmt(styleSheet, style) } - _, ok := builtInNumFmt[formatStyle.NumFmt] + _, ok := builtInNumFmt[style.NumFmt] if !ok { - fc, currency := currencyNumFmt[formatStyle.NumFmt] + fc, currency := currencyNumFmt[style.NumFmt] if !currency { - return setLangNumFmt(style, formatStyle) + return setLangNumFmt(styleSheet, style) } fc = strings.Replace(fc, "0.00", dp, -1) - if formatStyle.NegRed { + if style.NegRed { fc = fc + ";[Red]" + fc } - if style.NumFmts != nil { - numFmtID = style.NumFmts.NumFmt[len(style.NumFmts.NumFmt)-1].NumFmtID + 1 + if styleSheet.NumFmts != nil { + numFmtID = styleSheet.NumFmts.NumFmt[len(styleSheet.NumFmts.NumFmt)-1].NumFmtID + 1 nf := xlsxNumFmt{ FormatCode: fc, NumFmtID: numFmtID, } - style.NumFmts.NumFmt = append(style.NumFmts.NumFmt, &nf) - style.NumFmts.Count++ + styleSheet.NumFmts.NumFmt = append(styleSheet.NumFmts.NumFmt, &nf) + styleSheet.NumFmts.Count++ } else { nf := xlsxNumFmt{ FormatCode: fc, @@ -2053,61 +2061,61 @@ func setNumFmt(style *xlsxStyleSheet, formatStyle *formatStyle) int { NumFmt: []*xlsxNumFmt{&nf}, Count: 1, } - style.NumFmts = &numFmts + styleSheet.NumFmts = &numFmts } return numFmtID } - return formatStyle.NumFmt + return style.NumFmt } // setCustomNumFmt provides a function to set custom number format code. -func setCustomNumFmt(style *xlsxStyleSheet, formatStyle *formatStyle) int { - nf := xlsxNumFmt{FormatCode: *formatStyle.CustomNumFmt} - if style.NumFmts != nil { - nf.NumFmtID = style.NumFmts.NumFmt[len(style.NumFmts.NumFmt)-1].NumFmtID + 1 - style.NumFmts.NumFmt = append(style.NumFmts.NumFmt, &nf) - style.NumFmts.Count++ +func setCustomNumFmt(styleSheet *xlsxStyleSheet, style *Style) int { + nf := xlsxNumFmt{FormatCode: *style.CustomNumFmt} + 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 = 164 numFmts := xlsxNumFmts{ NumFmt: []*xlsxNumFmt{&nf}, Count: 1, } - style.NumFmts = &numFmts + styleSheet.NumFmts = &numFmts } return nf.NumFmtID } // setLangNumFmt provides a function to set number format code with language. -func setLangNumFmt(style *xlsxStyleSheet, formatStyle *formatStyle) int { - numFmts, ok := langNumFmt[formatStyle.Lang] +func setLangNumFmt(styleSheet *xlsxStyleSheet, style *Style) int { + numFmts, ok := langNumFmt[style.Lang] if !ok { return 0 } var fc string - fc, ok = numFmts[formatStyle.NumFmt] + fc, ok = numFmts[style.NumFmt] if !ok { return 0 } nf := xlsxNumFmt{FormatCode: fc} - if style.NumFmts != nil { - nf.NumFmtID = style.NumFmts.NumFmt[len(style.NumFmts.NumFmt)-1].NumFmtID + 1 - style.NumFmts.NumFmt = append(style.NumFmts.NumFmt, &nf) - style.NumFmts.Count++ + 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 = formatStyle.NumFmt + nf.NumFmtID = style.NumFmt numFmts := xlsxNumFmts{ NumFmt: []*xlsxNumFmt{&nf}, Count: 1, } - style.NumFmts = &numFmts + styleSheet.NumFmts = &numFmts } return nf.NumFmtID } // setFills provides a function to add fill elements in the styles.xml by // given cell format settings. -func setFills(formatStyle *formatStyle, fg bool) *xlsxFill { +func setFills(style *Style, fg bool) *xlsxFill { var patterns = []string{ "none", "solid", @@ -2138,15 +2146,15 @@ func setFills(formatStyle *formatStyle, fg bool) *xlsxFill { } var fill xlsxFill - switch formatStyle.Fill.Type { + switch style.Fill.Type { case "gradient": - if len(formatStyle.Fill.Color) != 2 { + if len(style.Fill.Color) != 2 { break } var gradient xlsxGradientFill - switch formatStyle.Fill.Shading { + switch style.Fill.Shading { case 0, 1, 2, 3: - gradient.Degree = variants[formatStyle.Fill.Shading] + gradient.Degree = variants[style.Fill.Shading] case 4: gradient.Type = "path" case 5: @@ -2159,7 +2167,7 @@ func setFills(formatStyle *formatStyle, fg bool) *xlsxFill { break } var stops []*xlsxGradientFillStop - for index, color := range formatStyle.Fill.Color { + for index, color := range style.Fill.Color { var stop xlsxGradientFillStop stop.Position = float64(index) stop.Color.RGB = getPaletteColor(color) @@ -2168,18 +2176,18 @@ func setFills(formatStyle *formatStyle, fg bool) *xlsxFill { gradient.Stop = stops fill.GradientFill = &gradient case "pattern": - if formatStyle.Fill.Pattern > 18 || formatStyle.Fill.Pattern < 0 { + if style.Fill.Pattern > 18 || style.Fill.Pattern < 0 { break } - if len(formatStyle.Fill.Color) < 1 { + if len(style.Fill.Color) < 1 { break } var pattern xlsxPatternFill - pattern.PatternType = patterns[formatStyle.Fill.Pattern] + pattern.PatternType = patterns[style.Fill.Pattern] if fg { - pattern.FgColor.RGB = getPaletteColor(formatStyle.Fill.Color[0]) + pattern.FgColor.RGB = getPaletteColor(style.Fill.Color[0]) } else { - pattern.BgColor.RGB = getPaletteColor(formatStyle.Fill.Color[0]) + pattern.BgColor.RGB = getPaletteColor(style.Fill.Color[0]) } fill.PatternFill = &pattern default: @@ -2192,36 +2200,36 @@ func setFills(formatStyle *formatStyle, fg bool) *xlsxFill { // text alignment in cells. There are a variety of choices for how text is // aligned both horizontally and vertically, as well as indentation settings, // and so on. -func setAlignment(formatStyle *formatStyle) *xlsxAlignment { +func setAlignment(style *Style) *xlsxAlignment { var alignment xlsxAlignment - if formatStyle.Alignment != nil { - alignment.Horizontal = formatStyle.Alignment.Horizontal - alignment.Indent = formatStyle.Alignment.Indent - alignment.JustifyLastLine = formatStyle.Alignment.JustifyLastLine - alignment.ReadingOrder = formatStyle.Alignment.ReadingOrder - alignment.RelativeIndent = formatStyle.Alignment.RelativeIndent - alignment.ShrinkToFit = formatStyle.Alignment.ShrinkToFit - alignment.TextRotation = formatStyle.Alignment.TextRotation - alignment.Vertical = formatStyle.Alignment.Vertical - alignment.WrapText = formatStyle.Alignment.WrapText + if style.Alignment != nil { + alignment.Horizontal = style.Alignment.Horizontal + alignment.Indent = style.Alignment.Indent + alignment.JustifyLastLine = style.Alignment.JustifyLastLine + alignment.ReadingOrder = style.Alignment.ReadingOrder + alignment.RelativeIndent = style.Alignment.RelativeIndent + alignment.ShrinkToFit = style.Alignment.ShrinkToFit + alignment.TextRotation = style.Alignment.TextRotation + alignment.Vertical = style.Alignment.Vertical + alignment.WrapText = style.Alignment.WrapText } return &alignment } // setProtection provides a function to set protection properties associated // with the cell. -func setProtection(formatStyle *formatStyle) *xlsxProtection { +func setProtection(style *Style) *xlsxProtection { var protection xlsxProtection - if formatStyle.Protection != nil { - protection.Hidden = formatStyle.Protection.Hidden - protection.Locked = formatStyle.Protection.Locked + if style.Protection != nil { + protection.Hidden = style.Protection.Hidden + protection.Locked = style.Protection.Locked } return &protection } // setBorders provides a function to add border elements in the styles.xml by // given borders format settings. -func setBorders(formatStyle *formatStyle) *xlsxBorder { +func setBorders(style *Style) *xlsxBorder { var styles = []string{ "none", "thin", @@ -2240,7 +2248,7 @@ func setBorders(formatStyle *formatStyle) *xlsxBorder { } var border xlsxBorder - for _, v := range formatStyle.Border { + for _, v := range style.Border { if 0 <= v.Style && v.Style < 14 { var color xlsxColor color.RGB = getPaletteColor(v.Color) diff --git a/styles_test.go b/styles_test.go index 4e9b41167b..5a9a77155f 100644 --- a/styles_test.go +++ b/styles_test.go @@ -191,6 +191,8 @@ func TestNewStyle(t *testing.T) { font := styles.Fonts.Font[fontID] assert.Contains(t, *font.Name.Val, "Times New Roman", "Stored font should contain font name") assert.Equal(t, 2, styles.CellXfs.Count, "Should have 2 styles") + _, err = f.NewStyle(&Style{}) + assert.NoError(t, err) } func TestGetDefaultFont(t *testing.T) { @@ -207,3 +209,24 @@ func TestSetDefaultFont(t *testing.T) { assert.Equal(t, s, "Ariel", "Default font should change to Ariel") assert.Equal(t, *styles.CellStyles.CellStyle[0].CustomBuiltIn, true) } + +func TestStylesReader(t *testing.T) { + f := NewFile() + // Test read styles with unsupport charset. + f.Styles = nil + f.XLSX["xl/styles.xml"] = MacintoshCyrillicCharset + assert.EqualValues(t, new(xlsxStyleSheet), f.stylesReader()) +} + +func TestThemeReader(t *testing.T) { + f := NewFile() + // Test read theme with unsupport charset. + f.XLSX["xl/theme/theme1.xml"] = MacintoshCyrillicCharset + assert.EqualValues(t, new(xlsxTheme), f.themeReader()) +} + +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") +} diff --git a/xmlChart.go b/xmlChart.go index 8d24552481..03b47a1f83 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -604,7 +604,7 @@ type formatChart struct { type formatChartLegend struct { None bool `json:"none"` DeleteSeries []int `json:"delete_series"` - Font formatFont `json:"font"` + Font Font `json:"font"` Layout formatLayout `json:"layout"` Position string `json:"position"` ShowLegendEntry bool `json:"show_legend_entry"` diff --git a/xmlDrawing.go b/xmlDrawing.go index 5bb5977519..2bad16a0bf 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -417,8 +417,8 @@ type formatShape struct { // formatShapeParagraph directly maps the format settings of the paragraph in // the shape. type formatShapeParagraph struct { - Font formatFont `json:"font"` - Text string `json:"text"` + Font Font `json:"font"` + Text string `json:"text"` } // formatShapeColor directly maps the color settings of the shape. diff --git a/xmlStyles.go b/xmlStyles.go index 0313008e0b..d6aa4f9f61 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -313,8 +313,28 @@ type xlsxStyleColors struct { Color string `xml:",innerxml"` } -// formatFont directly maps the styles settings of the fonts. -type formatFont 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"` +} + +// Border directly maps the border settings of the cells. +type Border struct { + Type string `json:"type"` + Color string `json:"color"` + Style int `json:"style"` +} + +// Font directly maps the font settings of the fonts. +type Font struct { Bold bool `json:"bold"` Italic bool `json:"italic"` Underline string `json:"underline"` @@ -324,38 +344,30 @@ type formatFont struct { Color string `json:"color"` } -// formatStyle directly maps the styles settings of the cells. -type formatStyle struct { - Border []struct { - Type string `json:"type"` - Color string `json:"color"` - Style int `json:"style"` - } `json:"border"` - Fill struct { - Type string `json:"type"` - Pattern int `json:"pattern"` - Color []string `json:"color"` - Shading int `json:"shading"` - } `json:"fill"` - Font *formatFont `json:"font"` - 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"` - } `json:"alignment"` - Protection *struct { - Hidden bool `json:"hidden"` - Locked bool `json:"locked"` - } `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"` +// 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"` +} + +// Protection directly maps the protection settings of the cells. +type Protection struct { + Hidden bool `json:"hidden"` + Locked bool `json:"locked"` +} + +// 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"` } From 6ab5b991e47e7fa9e9370da93404adaf04cba34a Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 13 Mar 2020 00:48:16 +0800 Subject: [PATCH 207/957] Resolve #348, support delete Data Validation --- datavalidation.go | 34 +++++++++++++++++++++++++++++----- datavalidation_test.go | 17 +++++++++++++++++ test/images/chart.png | Bin 190484 -> 137555 bytes 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/datavalidation.go b/datavalidation.go index 8b95b407fc..1aeb1dca3e 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -228,14 +228,38 @@ func convDataValidationOperatior(o DataValidationOperator) string { // err = f.AddDataValidation("Sheet1", dvRange) // func (f *File) AddDataValidation(sheet string, dv *DataValidation) error { - xlsx, err := f.workSheetReader(sheet) + ws, err := f.workSheetReader(sheet) if err != nil { return err } - if nil == xlsx.DataValidations { - xlsx.DataValidations = new(xlsxDataValidations) + if nil == ws.DataValidations { + ws.DataValidations = new(xlsxDataValidations) } - xlsx.DataValidations.DataValidation = append(xlsx.DataValidations.DataValidation, dv) - xlsx.DataValidations.Count = len(xlsx.DataValidations.DataValidation) + ws.DataValidations.DataValidation = append(ws.DataValidations.DataValidation, dv) + ws.DataValidations.Count = len(ws.DataValidations.DataValidation) return err } + +// DeleteDataValidation delete data validation by given worksheet name and +// reference sequence. +func (f *File) DeleteDataValidation(sheet, sqref string) error { + ws, err := f.workSheetReader(sheet) + if err != nil { + return err + } + if ws.DataValidations == nil { + return nil + } + dv := ws.DataValidations + for i := 0; i < len(dv.DataValidation); i++ { + if dv.DataValidation[i].Sqref == sqref { + dv.DataValidation = append(dv.DataValidation[:i], dv.DataValidation[i+1:]...) + i-- + } + } + dv.Count = len(dv.DataValidation) + if dv.Count == 0 { + ws.DataValidations = nil + } + return nil +} diff --git a/datavalidation_test.go b/datavalidation_test.go index c245df3315..d70b874885 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -85,3 +85,20 @@ func TestDataValidationError(t *testing.T) { f = NewFile() assert.EqualError(t, f.AddDataValidation("SheetN", nil), "sheet SheetN is not exist") } + +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)) + + assert.NoError(t, f.DeleteDataValidation("Sheet1", "A1:B2")) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDeleteDataValidation.xlsx"))) + + // Test delete data validation on no exists worksheet. + assert.EqualError(t, f.DeleteDataValidation("SheetN", "A1:B2"), "sheet SheetN is not exist") +} diff --git a/test/images/chart.png b/test/images/chart.png index 9fcd28a3de177f2baa1943e415d24158059b90b8..dc30051564a72e53f4dd23e47dbf6c449ace5dad 100644 GIT binary patch literal 137555 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

=O4@^xf# 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

# Excelize diff --git a/README_zh.md b/README_zh.md index deba22ac68..f54cf01d6f 100644 --- a/README_zh.md +++ b/README_zh.md @@ -6,7 +6,7 @@ Go Report Card go.dev Licenses - Donate + Donate

# Excelize diff --git a/calc.go b/calc.go index bc3b6e973d..577cfaae77 100644 --- a/calc.go +++ b/calc.go @@ -42,6 +42,14 @@ const ( formulaErrorGETTINGDATA = "#GETTING_DATA" ) +// Numeric precision correct numeric values as legacy Excel application +// https://en.wikipedia.org/wiki/Numeric_precision_in_Microsoft_Excel In the +// top figure the fraction 1/9000 in Excel is displayed. Although this number +// has a decimal representation that is an infinite string of ones, Excel +// displays only the leading 15 figures. In the second line, the number one +// is added to the fraction, and again Excel displays only 15 figures. +const numericPrecision = 1000000000000000 + // cellRef defines the structure of a cell reference. type cellRef struct { Col int @@ -141,6 +149,13 @@ func (f *File) CalcCellValue(sheet, cell string) (result string, err error) { return } result = token.TValue + if len(result) > 16 { + num, e := roundPrecision(result) + if e != nil { + return result, err + } + result = strings.ToUpper(num) + } return } diff --git a/calc_test.go b/calc_test.go index 9bf6e085ea..6998b778bc 100644 --- a/calc_test.go +++ b/calc_test.go @@ -54,42 +54,42 @@ func TestCalcCellValue(t *testing.T) { "=ABS(2-4.5)": "2.5", // ACOS "=ACOS(-1)": "3.141592653589793", - "=ACOS(0)": "1.5707963267948966", + "=ACOS(0)": "1.570796326794897", // ACOSH "=ACOSH(1)": "0", "=ACOSH(2.5)": "1.566799236972411", - "=ACOSH(5)": "2.2924316695611777", + "=ACOSH(5)": "2.292431669561178", // ACOT - "=_xlfn.ACOT(1)": "0.7853981633974483", + "=_xlfn.ACOT(1)": "0.785398163397448", "=_xlfn.ACOT(-2)": "2.677945044588987", - "=_xlfn.ACOT(0)": "1.5707963267948966", + "=_xlfn.ACOT(0)": "1.570796326794897", // ACOTH - "=_xlfn.ACOTH(-5)": "-0.2027325540540822", - "=_xlfn.ACOTH(1.1)": "1.5222612188617113", - "=_xlfn.ACOTH(2)": "0.5493061443340548", + "=_xlfn.ACOTH(-5)": "-0.202732554054082", + "=_xlfn.ACOTH(1.1)": "1.522261218861711", + "=_xlfn.ACOTH(2)": "0.549306144334055", // ARABIC `=_xlfn.ARABIC("IV")`: "4", `=_xlfn.ARABIC("-IV")`: "-4", `=_xlfn.ARABIC("MCXX")`: "1120", `=_xlfn.ARABIC("")`: "0", // ASIN - "=ASIN(-1)": "-1.5707963267948966", + "=ASIN(-1)": "-1.570796326794897", "=ASIN(0)": "0", // ASINH "=ASINH(0)": "0", - "=ASINH(-0.5)": "-0.48121182505960347", - "=ASINH(2)": "1.4436354751788103", + "=ASINH(-0.5)": "-0.481211825059604", + "=ASINH(2)": "1.44363547517881", // ATAN - "=ATAN(-1)": "-0.7853981633974483", + "=ATAN(-1)": "-0.785398163397448", "=ATAN(0)": "0", - "=ATAN(1)": "0.7853981633974483", + "=ATAN(1)": "0.785398163397448", // ATANH - "=ATANH(-0.8)": "-1.0986122886681098", + "=ATANH(-0.8)": "-1.09861228866811", "=ATANH(0)": "0", - "=ATANH(0.5)": "0.5493061443340548", + "=ATANH(0.5)": "0.549306144334055", // ATAN2 - "=ATAN2(1,1)": "0.7853981633974483", - "=ATAN2(1,-1)": "-0.7853981633974483", + "=ATAN2(1,1)": "0.785398163397448", + "=ATAN2(1,-1)": "-0.785398163397448", "=ATAN2(4,0)": "0", // BASE "=BASE(12,2)": "1100", @@ -145,17 +145,17 @@ func TestCalcCellValue(t *testing.T) { "=COS(0)": "1", // COSH "=COSH(0)": "1", - "=COSH(0.5)": "1.1276259652063807", - "=COSH(-2)": "3.7621956910836314", + "=COSH(0.5)": "1.127625965206381", + "=COSH(-2)": "3.762195691083632", // _xlfn.COT - "=_xlfn.COT(0.785398163397448)": "0.9999999999999992", + "=_xlfn.COT(0.785398163397448)": "0.999999999999999", // _xlfn.COTH - "=_xlfn.COTH(-3.14159265358979)": "-0.9962720762207499", + "=_xlfn.COTH(-3.14159265358979)": "-0.99627207622075", // _xlfn.CSC - "=_xlfn.CSC(-6)": "3.5788995472544056", + "=_xlfn.CSC(-6)": "3.578899547254406", "=_xlfn.CSC(1.5707963267949)": "1", // _xlfn.CSCH - "=_xlfn.CSCH(-3.14159265358979)": "-0.08658953753004724", + "=_xlfn.CSCH(-3.14159265358979)": "-0.086589537530047", // _xlfn.DECIMAL `=_xlfn.DECIMAL("1100",2)`: "12", `=_xlfn.DECIMAL("186A0",16)`: "100000", @@ -174,9 +174,9 @@ func TestCalcCellValue(t *testing.T) { "=EVEN(-4)": "-4", // EXP "=EXP(100)": "2.6881171418161356E+43", - "=EXP(0.1)": "1.1051709180756477", + "=EXP(0.1)": "1.105170918075648", "=EXP(0)": "1", - "=EXP(-5)": "0.006737946999085467", + "=EXP(-5)": "0.006737946999085", // FACT "=FACT(3)": "6", "=FACT(6)": "720", @@ -247,23 +247,23 @@ func TestCalcCellValue(t *testing.T) { // LN "=LN(1)": "0", "=LN(100)": "4.605170185988092", - "=LN(0.5)": "-0.6931471805599453", + "=LN(0.5)": "-0.693147180559945", // LOG "=LOG(64,2)": "6", "=LOG(100)": "2", "=LOG(4,0.5)": "-2", - "=LOG(500)": "2.6989700043360183", + "=LOG(500)": "2.698970004336019", // LOG10 "=LOG10(100)": "2", "=LOG10(1000)": "3", "=LOG10(0.001)": "-3", - "=LOG10(25)": "1.3979400086720375", + "=LOG10(25)": "1.397940008672038", // MOD "=MOD(6,4)": "2", "=MOD(6,3)": "0", "=MOD(6,2.5)": "1", - "=MOD(6,1.333)": "0.6680000000000001", - "=MOD(-10.23,1)": "0.7699999999999996", + "=MOD(6,1.333)": "0.668", + "=MOD(-10.23,1)": "0.77", // MROUND "=MROUND(333.7,0.5)": "333.5", "=MROUND(333.8,1)": "334", @@ -298,7 +298,7 @@ func TestCalcCellValue(t *testing.T) { "=QUOTIENT(4.5,3.1)": "1", "=QUOTIENT(-10,3)": "-3", // RADIANS - "=RADIANS(50)": "0.8726646259971648", + "=RADIANS(50)": "0.872664625997165", "=RADIANS(-180)": "-3.141592653589793", "=RADIANS(180)": "3.141592653589793", "=RADIANS(360)": "6.283185307179586", @@ -323,13 +323,13 @@ func TestCalcCellValue(t *testing.T) { "=ROUND(991,-1)": "990", // ROUNDDOWN "=ROUNDDOWN(99.999,1)": "99.9", - "=ROUNDDOWN(99.999,2)": "99.99000000000002", + "=ROUNDDOWN(99.999,2)": "99.99000000000001", "=ROUNDDOWN(99.999,0)": "99", "=ROUNDDOWN(99.999,-1)": "90", - "=ROUNDDOWN(-99.999,2)": "-99.99000000000002", + "=ROUNDDOWN(-99.999,2)": "-99.99000000000001", "=ROUNDDOWN(-99.999,-1)": "-90", // ROUNDUP - "=ROUNDUP(11.111,1)": "11.200000000000001", + "=ROUNDUP(11.111,1)": "11.200000000000003", "=ROUNDUP(11.111,2)": "11.120000000000003", "=ROUNDUP(11.111,0)": "12", "=ROUNDUP(11.111,-1)": "20", @@ -339,7 +339,7 @@ func TestCalcCellValue(t *testing.T) { "=_xlfn.SEC(-3.14159265358979)": "-1", "=_xlfn.SEC(0)": "1", // SECH - "=_xlfn.SECH(-3.14159265358979)": "0.0862667383340547", + "=_xlfn.SECH(-3.14159265358979)": "0.086266738334055", "=_xlfn.SECH(0)": "1", // SIGN "=SIGN(9.5)": "1", @@ -348,17 +348,17 @@ func TestCalcCellValue(t *testing.T) { "=SIGN(0.00000001)": "1", "=SIGN(6-7)": "-1", // SIN - "=SIN(0.785398163)": "0.7071067809055092", + "=SIN(0.785398163)": "0.707106780905509", // SINH "=SINH(0)": "0", - "=SINH(0.5)": "0.5210953054937474", + "=SINH(0.5)": "0.521095305493747", "=SINH(-2)": "-3.626860407847019", // SQRT "=SQRT(4)": "2", `=SQRT("")`: "0", // SQRTPI "=SQRTPI(5)": "3.963327297606011", - "=SQRTPI(0.2)": "0.7926654595212022", + "=SQRTPI(0.2)": "0.792665459521202", "=SQRTPI(100)": "17.72453850905516", "=SQRTPI(0)": "0", // SUM @@ -399,8 +399,8 @@ func TestCalcCellValue(t *testing.T) { "=TAN(0)": "0", // TANH "=TANH(0)": "0", - "=TANH(0.5)": "0.46211715726000974", - "=TANH(-2)": "-0.9640275800758169", + "=TANH(0.5)": "0.46211715726001", + "=TANH(-2)": "-0.964027580075817", // TRUNC "=TRUNC(99.999,1)": "99.9", "=TRUNC(99.999,2)": "99.99", @@ -794,14 +794,14 @@ func TestCalcCellValue(t *testing.T) { // PRODUCT "=PRODUCT(Sheet1!A1:Sheet1!A1:A2,A2)": "4", // SUM - "=A1/A3": "0.3333333333333333", + "=A1/A3": "0.333333333333333", "=SUM(A1:A2)": "3", "=SUM(Sheet1!A1,A2)": "3", "=(-2-SUM(-4+A2))*5": "0", "=SUM(Sheet1!A1:Sheet1!A1:A2,A2)": "5", "=SUM(A1,A2,A3)*SUM(2,3)": "30", - "=1+SUM(SUM(A1+A2/A3)*(2-3),2)": "1.3333333333333335", - "=A1/A2/SUM(A1:A2:B1)": "0.041666666666666664", + "=1+SUM(SUM(A1+A2/A3)*(2-3),2)": "1.333333333333334", + "=A1/A2/SUM(A1:A2:B1)": "0.041666666666667", "=A1/A2/SUM(A1:A2:B1)*A3": "0.125", } for formula, expected := range referenceCalc { diff --git a/rows.go b/rows.go index 4f93ed1162..591d7e91a4 100644 --- a/rows.go +++ b/rows.go @@ -345,20 +345,11 @@ func (c *xlsxC) getValueFrom(f *File, d *xlsxSST) (string, error) { } return f.formattedValue(c.S, c.V), nil default: - // correct numeric values as legacy Excel app - // https://en.wikipedia.org/wiki/Numeric_precision_in_Microsoft_Excel - // In the top figure the fraction 1/9000 in Excel is displayed. - // Although this number has a decimal representation that is an infinite string of ones, - // Excel displays only the leading 15 figures. In the second line, the number one is added to the fraction, and again Excel displays only 15 figures. - const precision = 1000000000000000 if len(c.V) > 16 { - num, err := strconv.ParseFloat(c.V, 64) + val, err := roundPrecision(c.V) if err != nil { return "", err } - - num = math.Round(num*precision) / precision - val := fmt.Sprintf("%g", num) if val != c.V { return f.formattedValue(c.S, val), nil } @@ -367,6 +358,16 @@ func (c *xlsxC) getValueFrom(f *File, d *xlsxSST) (string, error) { } } +// roundPrecision round precision for numeric. +func roundPrecision(value string) (result string, err error) { + var num float64 + if num, err = strconv.ParseFloat(value, 64); err != nil { + return + } + result = fmt.Sprintf("%g", math.Round(num*numericPrecision)/numericPrecision) + return +} + // 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: // From 13e0ed2a69af0ff0ba20e50fb23dd4909a39b69d Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 23 Nov 2020 00:01:06 +0800 Subject: [PATCH 303/957] Fixed #735, refresh active tab after delete sheet --- cell.go | 9 ++++----- sheet.go | 12 +++--------- sheet_test.go | 11 ++++++++++- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/cell.go b/cell.go index 019f300dfb..1edfd24b25 100644 --- a/cell.go +++ b/cell.go @@ -765,21 +765,20 @@ func (f *File) formattedValue(s int, v string) string { if s >= len(styleSheet.CellXfs.Xf) { return v } - numFmtId := *styleSheet.CellXfs.Xf[s].NumFmtID - ok := builtInNumFmtFunc[numFmtId] + numFmtID := *styleSheet.CellXfs.Xf[s].NumFmtID + ok := builtInNumFmtFunc[numFmtID] if ok != nil { - return ok(v, builtInNumFmt[numFmtId]) + return ok(v, builtInNumFmt[numFmtID]) } if styleSheet == nil || styleSheet.NumFmts == nil { return v } for _, xlsxFmt := range styleSheet.NumFmts.NumFmt { - if xlsxFmt.NumFmtID == numFmtId { + if xlsxFmt.NumFmtID == numFmtID { format := strings.ToLower(xlsxFmt.FormatCode) if strings.Contains(format, "y") || strings.Contains(format, "m") || strings.Contains(strings.Replace(format, "red", "", -1), "d") || strings.Contains(format, "h") { return parseTime(v, format) } - return v } } diff --git a/sheet.go b/sheet.go index 82eaae9591..82c6a6998e 100644 --- a/sheet.go +++ b/sheet.go @@ -317,7 +317,7 @@ func (f *File) GetActiveSheetIndex() (index int) { return } -// getActiveSheetID provides a function to get active sheet index of the +// 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() @@ -499,6 +499,7 @@ func (f *File) DeleteSheet(name string) { sheetName := trimSheetName(name) wb := f.workbookReader() wbRels := f.relsReader(f.getWorkbookRelsPath()) + activeSheetName := f.GetSheetName(f.GetActiveSheetIndex()) for idx, sheet := range wb.Sheets.Sheet { if sheet.Name == sheetName { wb.Sheets.Sheet = append(wb.Sheets.Sheet[:idx], wb.Sheets.Sheet[idx+1:]...) @@ -526,14 +527,7 @@ func (f *File) DeleteSheet(name string) { f.SheetCount-- } } - if wb.BookViews != nil { - for idx, bookView := range wb.BookViews.WorkBookView { - if bookView.ActiveTab >= f.SheetCount { - wb.BookViews.WorkBookView[idx].ActiveTab-- - } - } - } - f.SetActiveSheet(len(f.GetSheetMap())) + f.SetActiveSheet(f.GetSheetIndex(activeSheetName)) } // deleteSheetFromWorkbookRels provides a function to remove worksheet diff --git a/sheet_test.go b/sheet_test.go index bfe0ce3fcc..d1c8f642bb 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -359,6 +359,15 @@ func TestGetWorkbookRelsPath(t *testing.T) { assert.Equal(t, "_rels/workbook.xml.rels", f.getWorkbookRelsPath()) } +func TestDeleteSheet(t *testing.T) { + f := NewFile() + f.SetActiveSheet(f.NewSheet("Sheet2")) + f.NewSheet("Sheet3") + f.DeleteSheet("Sheet1") + assert.Equal(t, "Sheet2", f.GetSheetName(f.GetActiveSheetIndex())) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDeleteSheet.xlsx"))) +} + func BenchmarkNewSheet(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { @@ -380,9 +389,9 @@ func BenchmarkFile_SaveAs(b *testing.B) { for pb.Next() { newSheetWithSave() } - }) } + func newSheetWithSave() { file := NewFile() file.NewSheet("sheet1") From 95d8920c8e99684eb4e5068e38d3e7e5d0409d4e Mon Sep 17 00:00:00 2001 From: jacentsao Date: Thu, 10 Dec 2020 13:37:34 +0800 Subject: [PATCH 304/957] support range validation for decimal (#739) Co-authored-by: jacen_cao --- datavalidation.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/datavalidation.go b/datavalidation.go index f76f9b3cc4..4cfb1257a2 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -120,9 +120,9 @@ func (dd *DataValidation) SetDropList(keys []string) error { } // SetRange provides function to set data validation range in drop list. -func (dd *DataValidation) SetRange(f1, f2 int, t DataValidationType, o DataValidationOperator) error { - formula1 := fmt.Sprintf("%d", f1) - formula2 := fmt.Sprintf("%d", f2) +func (dd *DataValidation) SetRange(f1, f2 float64, t DataValidationType, o DataValidationOperator) error { + formula1 := fmt.Sprintf("%f", f1) + formula2 := fmt.Sprintf("%f", f2) if dataValidationFormulaStrLen+21 < len(dd.Formula1) || dataValidationFormulaStrLen+21 < len(dd.Formula2) { return fmt.Errorf(dataValidationFormulaStrLenErr) } From 61057c58d34c78232ad0a5c1702ea9fa25a7641a Mon Sep 17 00:00:00 2001 From: Artem Kustikov Date: Sat, 12 Dec 2020 11:17:00 +0300 Subject: [PATCH 305/957] Number format read fix (#741) * fix UT-generated file names to be ignored * fix cell value load with invalid number format ID * fix PR issues --- .gitignore | 4 ++++ cell.go | 7 ++++++- cell_test.go | 18 +++++++++++++++--- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 685d2bfdc5..ce92812619 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,10 @@ ~$*.xlsx test/Test*.xlsx test/Test*.xlsm +# generated files +test/BadEncrypt.xlsx +test/BadWorkbook.SaveAsEmptyStruct.xlsx +test/*.png *.out *.test .idea diff --git a/cell.go b/cell.go index 1edfd24b25..22adefd454 100644 --- a/cell.go +++ b/cell.go @@ -762,10 +762,15 @@ func (f *File) formattedValue(s int, v string) string { return v } styleSheet := f.stylesReader() + if s >= len(styleSheet.CellXfs.Xf) { return v } - numFmtID := *styleSheet.CellXfs.Xf[s].NumFmtID + var numFmtID int + if styleSheet.CellXfs.Xf[s].NumFmtID != nil { + numFmtID = *styleSheet.CellXfs.Xf[s].NumFmtID + } + ok := builtInNumFmtFunc[numFmtID] if ok != nil { return ok(v, builtInNumFmt[numFmtID]) diff --git a/cell_test.go b/cell_test.go index c934876edb..8d3f77482d 100644 --- a/cell_test.go +++ b/cell_test.go @@ -301,7 +301,7 @@ func TestSetCellRichText(t *testing.T) { assert.EqualError(t, f.SetCellRichText("Sheet1", "A", richTextRun), `cannot convert cell "A" to coordinates: invalid cell name "A"`) } -func TestFormattedValue(t *testing.T) { +func TestFormattedValue2(t *testing.T) { f := NewFile() v := f.formattedValue(0, "43528") assert.Equal(t, "43528", v) @@ -320,12 +320,24 @@ func TestFormattedValue(t *testing.T) { assert.Equal(t, "03/04/2019", v) // formatted value with no built-in number format ID - assert.NoError(t, err) - f.Styles.NumFmts = nil numFmtID := 5 f.Styles.CellXfs.Xf = append(f.Styles.CellXfs.Xf, xlsxXf{ NumFmtID: &numFmtID, }) + v = f.formattedValue(2, "43528") + assert.Equal(t, "43528", v) + + // formatted value with invalid number format ID + f.Styles.CellXfs.Xf = append(f.Styles.CellXfs.Xf, xlsxXf{ + NumFmtID: nil, + }) + v = f.formattedValue(3, "43528") + + // 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") assert.Equal(t, "43528", v) } From ad79505173302fdd7619288b793497052e25a148 Mon Sep 17 00:00:00 2001 From: Zhang Zhipeng <414326615@qq.com> Date: Mon, 14 Dec 2020 09:56:42 +0800 Subject: [PATCH 306/957] new formula func CLEAN and TRIM, change import path to v2 (#747) --- README.md | 8 ++++---- README_zh.md | 8 ++++---- calc.go | 29 +++++++++++++++++++++++++++++ calc_test.go | 14 ++++++++++++++ cell.go | 2 +- chart.go | 4 ++-- picture.go | 4 ++-- pivotTable.go | 2 +- 8 files changed, 57 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 6afcc7e729..4dbf532c01 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ package main import ( "fmt" - "github.com/360EntSecGroup-Skylar/excelize" + "github.com/360EntSecGroup-Skylar/excelize/v2" ) func main() { @@ -68,7 +68,7 @@ package main import ( "fmt" - "github.com/360EntSecGroup-Skylar/excelize" + "github.com/360EntSecGroup-Skylar/excelize/v2" ) func main() { @@ -107,7 +107,7 @@ package main import ( "fmt" - "github.com/360EntSecGroup-Skylar/excelize" + "github.com/360EntSecGroup-Skylar/excelize/v2" ) func main() { @@ -142,7 +142,7 @@ import ( _ "image/jpeg" _ "image/png" - "github.com/360EntSecGroup-Skylar/excelize" + "github.com/360EntSecGroup-Skylar/excelize/v2" ) func main() { diff --git a/README_zh.md b/README_zh.md index f54cf01d6f..25b2fbf482 100644 --- a/README_zh.md +++ b/README_zh.md @@ -39,7 +39,7 @@ package main import ( "fmt" - "github.com/360EntSecGroup-Skylar/excelize" + "github.com/360EntSecGroup-Skylar/excelize/v2" ) func main() { @@ -68,7 +68,7 @@ package main import ( "fmt" - "github.com/360EntSecGroup-Skylar/excelize" + "github.com/360EntSecGroup-Skylar/excelize/v2" ) func main() { @@ -107,7 +107,7 @@ package main import ( "fmt" - "github.com/360EntSecGroup-Skylar/excelize" + "github.com/360EntSecGroup-Skylar/excelize/v2" ) func main() { @@ -142,7 +142,7 @@ import ( _ "image/jpeg" _ "image/png" - "github.com/360EntSecGroup-Skylar/excelize" + "github.com/360EntSecGroup-Skylar/excelize/v2" ) func main() { diff --git a/calc.go b/calc.go index 577cfaae77..7da24938af 100644 --- a/calc.go +++ b/calc.go @@ -3365,3 +3365,32 @@ func makeDate(y int, m time.Month, d int) int64 { func daysBetween(startDate, endDate int64) float64 { return float64(int(0.5 + float64((endDate-startDate)/86400))) } + +// Text Functions + +// CLEAN removes all non-printable characters from a supplied text string. +func (fn *formulaFuncs) CLEAN(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("CLEAN requires 1 argument") + return + } + b := bytes.Buffer{} + for _, c := range argsList.Front().Value.(formulaArg).String { + if c > 31 { + b.WriteRune(c) + } + } + result = b.String() + return +} + +// TRIM removes extra spaces (i.e. all spaces except for single spaces between +// words or characters) from a supplied text string. +func (fn *formulaFuncs) TRIM(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("TRIM requires 1 argument") + return + } + result = strings.TrimSpace(argsList.Front().Value.(formulaArg).String) + return +} diff --git a/calc_test.go b/calc_test.go index 6998b778bc..f928797cac 100644 --- a/calc_test.go +++ b/calc_test.go @@ -463,6 +463,13 @@ func TestCalcCellValue(t *testing.T) { // DATE "=DATE(2020,10,21)": "2020-10-21 00:00:00 +0000 UTC", "=DATE(1900,1,1)": "1899-12-31 00:00:00 +0000 UTC", + // Text Functions + // CLEAN + "=CLEAN(\"\u0009clean text\")": "clean text", + "=CLEAN(0)": "0", + // TRIM + "=TRIM(\" trim text \")": "trim text", + "=TRIM(0)": "0", } for formula, expected := range mathCalc { f := prepareData() @@ -779,6 +786,13 @@ func TestCalcCellValue(t *testing.T) { `=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", + // Text Functions + // CLEAN + "=CLEAN()": "CLEAN requires 1 argument", + "=CLEAN(1,2)": "CLEAN requires 1 argument", + // TRIM + "=TRIM()": "TRIM requires 1 argument", + "=TRIM(1,2)": "TRIM requires 1 argument", } for formula, expected := range mathCalcError { f := prepareData() diff --git a/cell.go b/cell.go index 22adefd454..4dd28306d7 100644 --- a/cell.go +++ b/cell.go @@ -502,7 +502,7 @@ func (f *File) SetCellHyperLink(sheet, axis, link, linkType string) error { // import ( // "fmt" // -// "github.com/360EntSecGroup-Skylar/excelize" +// "github.com/360EntSecGroup-Skylar/excelize/v2" // ) // // func main() { diff --git a/chart.go b/chart.go index 57f7838b2a..32c8d715ae 100644 --- a/chart.go +++ b/chart.go @@ -510,7 +510,7 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // import ( // "fmt" // -// "github.com/360EntSecGroup-Skylar/excelize" +// "github.com/360EntSecGroup-Skylar/excelize/v2" // ) // // func main() { @@ -708,7 +708,7 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // import ( // "fmt" // -// "github.com/360EntSecGroup-Skylar/excelize" +// "github.com/360EntSecGroup-Skylar/excelize/v2" // ) // // func main() { diff --git a/picture.go b/picture.go index 6adfa718af..1a9cac1075 100644 --- a/picture.go +++ b/picture.go @@ -55,7 +55,7 @@ func parseFormatPictureSet(formatSet string) (*formatPicture, error) { // _ "image/jpeg" // _ "image/png" // -// "github.com/360EntSecGroup-Skylar/excelize" +// "github.com/360EntSecGroup-Skylar/excelize/v2" // ) // // func main() { @@ -111,7 +111,7 @@ func (f *File) AddPicture(sheet, cell, picture, format string) error { // _ "image/jpeg" // "io/ioutil" // -// "github.com/360EntSecGroup-Skylar/excelize" +// "github.com/360EntSecGroup-Skylar/excelize/v2" // ) // // func main() { diff --git a/pivotTable.go b/pivotTable.go index 96e362783e..bffda17334 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -80,7 +80,7 @@ type PivotTableField struct { // "fmt" // "math/rand" // -// "github.com/360EntSecGroup-Skylar/excelize" +// "github.com/360EntSecGroup-Skylar/excelize/v2" // ) // // func main() { From 77978ac68d3808060e58df41ebede4b9f3631641 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 14 Dec 2020 20:56:51 +0800 Subject: [PATCH 307/957] This closes #657 and closes #748, AddChart support custom marker symbol and symbol size, fix AddPicture auto fit failure with multi merged cells --- chart.go | 20 ++++++++++++++++++-- chart_test.go | 2 +- drawing.go | 11 +++++++++-- go.mod | 2 ++ go.sum | 13 +++++++++++++ picture.go | 3 +++ picture_test.go | 1 + xmlChart.go | 2 +- 8 files changed, 48 insertions(+), 6 deletions(-) diff --git a/chart.go b/chart.go index 32c8d715ae..bdb7f5b761 100644 --- a/chart.go +++ b/chart.go @@ -600,6 +600,7 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // 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 // @@ -609,6 +610,21 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // // 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. // +// 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 +// // Set properties of the chart legend. The options that can be set are: // // position @@ -638,7 +654,7 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // // gap: Specifies that blank values shall be left as a gap. // -// sapn: Specifies that blank values shall be spanned with a line. +// span: Specifies that blank values shall be spanned with a line. // // zero: Specifies that blank values shall be treated as zero. // @@ -721,7 +737,7 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // 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},"legend":{"position":"left","show_legend_key":false},"title":{"name":"Clustered Column - 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}}`, `{"type":"line","series":[{"name":"Sheet1!$A$4","categories":"Sheet1!$B$1:$D$1","values":"Sheet1!$B$4:$D$4"}],"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}}`); err != nil { +// 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},"legend":{"position":"left","show_legend_key":false},"title":{"name":"Clustered Column - 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}}`, `{"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":"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}}`); err != nil { // fmt.Println(err) // return // } diff --git a/chart_test.go b/chart_test.go index 67d5683d1b..4a7000ae1b 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"}`)) - 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"},{"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}},{"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 42eb420e95..0db5d0e749 100644 --- a/drawing.go +++ b/drawing.go @@ -841,10 +841,17 @@ 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 { + defaultSymbol := map[string]*attrValString{Scatter: &attrValString{Val: stringPtr("circle")}} marker := &cMarker{ - Symbol: &attrValString{Val: stringPtr("circle")}, + Symbol: defaultSymbol[formatSet.Type], Size: &attrValInt{Val: intPtr(5)}, } + if symbol := stringPtr(formatSet.Series[i].Marker.Symbol); *symbol != "" { + marker.Symbol = &attrValString{Val: symbol} + } + if size := intPtr(formatSet.Series[i].Marker.Size); *size != 0 { + marker.Size = &attrValInt{Val: size} + } if i < 6 { marker.SpPr = &cSpPr{ SolidFill: &aSolidFill{ @@ -862,7 +869,7 @@ func (f *File) drawChartSeriesMarker(i int, formatSet *formatChart) *cMarker { }, } } - chartSeriesMarker := map[string]*cMarker{Scatter: marker} + chartSeriesMarker := map[string]*cMarker{Scatter: marker, Line: marker} return chartSeriesMarker[formatSet.Type] } diff --git a/go.mod b/go.mod index 773f0a3914..9637ba0e3c 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,10 @@ go 1.11 require ( github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 github.com/richardlehane/mscfb v1.0.3 + github.com/stretchr/testify v1.6.1 github.com/xuri/efp v0.0.0-20201016154823-031c29024257 golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee + golang.org/x/image v0.0.0-20201208152932-35266b937fa6 golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0 golang.org/x/text v0.3.3 ) diff --git a/go.sum b/go.sum index 17e16a5a58..24aa2255ed 100644 --- a/go.sum +++ b/go.sum @@ -1,15 +1,25 @@ +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/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.3 h1:rD8TBkYWkObWO0oLDFCbwMeZ4KoalxQy+QgniCj3nKI= github.com/richardlehane/mscfb v1.0.3/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/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/xuri/efp v0.0.0-20201016154823-031c29024257 h1:6ldmGEJXtsRMwdR2KuS3esk9wjVJNvgk05/YY2XmOj0= github.com/xuri/efp v0.0.0-20201016154823-031c29024257/go.mod h1:uBiSUepVYMhGTfDeBKKasV4GpgBlzJ46gXUBAqV8qLk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee h1:4yd7jl+vXjalO5ztz6Vc1VADv+S/80LGJmyl1ROJ2AI= golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/image v0.0.0-20201208152932-35266b937fa6 h1:nfeHNc1nAqecKCy2FCy4HY+soOOe5sDLJ/gZLbx6GYI= +golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0 h1:5kGOVHlq0euqwzgTC9Vu15p6fV1Wi0ArVi8da2urnVg= golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= @@ -20,3 +30,6 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +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= diff --git a/picture.go b/picture.go index 1a9cac1075..77898fce6c 100644 --- a/picture.go +++ b/picture.go @@ -605,6 +605,9 @@ func (f *File) drawingResize(sheet string, cell string, width, height float64, f } cellWidth, cellHeight := f.getColWidth(sheet, c), f.getRowHeight(sheet, r) for _, mergeCell := range mergeCells { + if inMergeCell { + continue + } if inMergeCell, err = f.checkCellInArea(cell, mergeCell[0]); err != nil { return } diff --git a/picture_test.go b/picture_test.go index f6f716efde..58f7a81525 100644 --- a/picture_test.go +++ b/picture_test.go @@ -53,6 +53,7 @@ func TestAddPicture(t *testing.T) { f.NewSheet("AddPicture") 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}`)) diff --git a/xmlChart.go b/xmlChart.go index c95393a334..fffdddd018 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -623,7 +623,7 @@ type formatChartSeries struct { Width float64 `json:"width"` } `json:"line"` Marker struct { - Type string `json:"type"` + Symbol string `json:"symbol"` Size int `json:"size"` Width float64 `json:"width"` Border struct { From 576bfffbe6add78e719fc4fab851f40f5779a4d3 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 22 Dec 2020 08:47:46 +0800 Subject: [PATCH 308/957] This closes #752, fix incorrectly merged cells on duplicate row, and new formula function: LOWER, PROPER, UPPER --- README.md | 43 +++++++++-- README_zh.md | 43 +++++++++-- calc.go | 211 ++++++++++++++++++++++++++++++++++----------------- calc_test.go | 24 ++++++ rows.go | 1 - rows_test.go | 26 +++---- 6 files changed, 254 insertions(+), 94 deletions(-) diff --git a/README.md b/README.md index 4dbf532c01..7c67092c3e 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ func main() { ### Add chart to spreadsheet file -With Excelize chart generation and management is as easy as a few lines of code. You can build charts based off data in your worksheet or generate charts without any data in your worksheet at all. +With Excelize chart generation and management is as easy as a few lines of code. You can build charts based on data in your worksheet or generate charts without any data in your worksheet at all.

Excelize

@@ -111,8 +111,10 @@ 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} + 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) @@ -120,7 +122,29 @@ func main() { 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"}}`); err != nil { + 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" + } + }`); err != nil { fmt.Println(err) return } @@ -156,11 +180,18 @@ func main() { fmt.Println(err) } // Insert a picture to worksheet with scaling. - if err := f.AddPicture("Sheet1", "D2", "image.jpg", `{"x_scale": 0.5, "y_scale": 0.5}`); err != nil { + if err := f.AddPicture("Sheet1", "D2", "image.jpg", + `{"x_scale": 0.5, "y_scale": 0.5}`); 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", `{ + "x_offset": 15, + "y_offset": 10, + "print_obj": true, + "lock_aspect_ratio": false, + "locked": false + }`); err != nil { fmt.Println(err) } // Save the spreadsheet with the origin path. diff --git a/README_zh.md b/README_zh.md index 25b2fbf482..daafd1d89f 100644 --- a/README_zh.md +++ b/README_zh.md @@ -99,7 +99,7 @@ func main() { 使用 Excelize 生成图表十分简单,仅需几行代码。您可以根据工作表中的已有数据构建图表,或向工作表中添加数据并创建图表。 -

Excelize

+

使用 Excelize 在 Excel 电子表格文档中创建图表

```go package main @@ -111,8 +111,10 @@ 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} + 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) @@ -120,7 +122,29 @@ func main() { 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"}}`); err != nil { + 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" + } + }`); err != nil { fmt.Println(err) return } @@ -156,11 +180,18 @@ func main() { fmt.Println(err) } // 在工作表中插入图片,并设置图片的缩放比例 - if err := f.AddPicture("Sheet1", "D2", "image.jpg", `{"x_scale": 0.5, "y_scale": 0.5}`); err != nil { + if err := f.AddPicture("Sheet1", "D2", "image.jpg", + `{"x_scale": 0.5, "y_scale": 0.5}`); 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", `{ + "x_offset": 15, + "y_offset": 10, + "print_obj": true, + "lock_aspect_ratio": false, + "locked": false + }`); err != nil { fmt.Println(err) } // 保存文件 diff --git a/calc.go b/calc.go index 7da24938af..d2bab1dc70 100644 --- a/calc.go +++ b/calc.go @@ -24,6 +24,7 @@ import ( "strconv" "strings" "time" + "unicode" "github.com/xuri/efp" ) @@ -123,14 +124,15 @@ var tokenPriority = map[string]int{ // Supported formulas: // // ABS, ACOS, ACOSH, ACOT, ACOTH, AND, ARABIC, ASIN, ASINH, ATAN2, ATANH, -// BASE, CEILING, CEILING.MATH, CEILING.PRECISE, COMBIN, COMBINA, COS, -// COSH, COT, COTH, COUNTA, CSC, CSCH, DATE, DECIMAL, DEGREES, EVEN, EXP, -// FACT, FACTDOUBLE, FLOOR, FLOOR.MATH, FLOOR.PRECISE, GCD, INT, ISBLANK, -// ISERR, ISERROR, ISEVEN, ISNA, ISNONTEXT, ISNUMBER, ISO.CEILING, ISODD, -// LCM, LN, LOG, LOG10, MDETERM, MEDIAN, MOD, MROUND, MULTINOMIAL, MUNIT, -// NA, ODD, OR, PI, POWER, PRODUCT, QUOTIENT, RADIANS, RAND, RANDBETWEEN, -// ROUND, ROUNDDOWN, ROUNDUP, SEC, SECH, SIGN, SIN, SINH, SQRT, SQRTPI, -// SUM, SUMIF, SUMSQ, TAN, TANH, TRUNC +// BASE, CEILING, CEILING.MATH, CEILING.PRECISE, CLEAN, COMBIN, COMBINA, +// COS, COSH, COT, COTH, COUNTA, CSC, CSCH, DATE, DECIMAL, DEGREES, EVEN, +// EXP, FACT, FACTDOUBLE, FLOOR, FLOOR.MATH, FLOOR.PRECISE, GCD, INT, +// ISBLANK, ISERR, ISERROR, ISEVEN, ISNA, ISNONTEXT, ISNUMBER, ISO.CEILING, +// ISODD, LCM, LN, LOG, LOG10, LOWER, MDETERM, MEDIAN, MOD, MROUND, +// MULTINOMIAL, MUNIT, NA, ODD, OR, PI, POWER, PRODUCT, PROPER, QUOTIENT, +// RADIANS, RAND, RANDBETWEEN, ROUND, ROUNDDOWN, ROUNDUP, SEC, SECH, SIGN, +// SIN, SINH, SQRT, SQRTPI, SUM, SUMIF, SUMSQ, TAN, TANH, TRIM, TRUNC, +// UPPER // func (f *File) CalcCellValue(sheet, cell string) (result string, err error) { var ( @@ -869,7 +871,7 @@ func formulaCriteriaEval(val string, criteria *formulaCriteria) (result bool, er // 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) (result string, err error) { if argsList.Len() != 1 { @@ -889,7 +891,7 @@ func (fn *formulaFuncs) ABS(argsList *list.List) (result string, err error) { // 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) (result string, err error) { if argsList.Len() != 1 { @@ -908,7 +910,7 @@ func (fn *formulaFuncs) ACOS(argsList *list.List) (result string, err error) { // 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) (result string, err error) { if argsList.Len() != 1 { @@ -928,7 +930,7 @@ func (fn *formulaFuncs) ACOSH(argsList *list.List) (result string, err error) { // 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) (result string, err error) { if argsList.Len() != 1 { @@ -947,7 +949,7 @@ func (fn *formulaFuncs) ACOT(argsList *list.List) (result string, err error) { // 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) (result string, err error) { if argsList.Len() != 1 { @@ -966,7 +968,7 @@ func (fn *formulaFuncs) ACOTH(argsList *list.List) (result string, err error) { // 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) (result string, err error) { if argsList.Len() != 1 { @@ -1004,7 +1006,7 @@ func (fn *formulaFuncs) ARABIC(argsList *list.List) (result string, err error) { // 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) (result string, err error) { if argsList.Len() != 1 { @@ -1023,7 +1025,7 @@ func (fn *formulaFuncs) ASIN(argsList *list.List) (result string, err error) { // 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) (result string, err error) { if argsList.Len() != 1 { @@ -1043,7 +1045,7 @@ func (fn *formulaFuncs) ASINH(argsList *list.List) (result string, err error) { // 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) (result string, err error) { if argsList.Len() != 1 { @@ -1062,7 +1064,7 @@ func (fn *formulaFuncs) ATAN(argsList *list.List) (result string, err error) { // 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) (result string, err error) { if argsList.Len() != 1 { @@ -1082,7 +1084,7 @@ func (fn *formulaFuncs) ATANH(argsList *list.List) (result string, err error) { // 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) (result string, err error) { if argsList.Len() != 2 { @@ -1105,7 +1107,7 @@ func (fn *formulaFuncs) ATAN2(argsList *list.List) (result string, err error) { // 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) (result string, err error) { if argsList.Len() < 2 { @@ -1147,7 +1149,7 @@ func (fn *formulaFuncs) BASE(argsList *list.List) (result string, err error) { // 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) (result string, err error) { if argsList.Len() == 0 { @@ -1191,7 +1193,7 @@ func (fn *formulaFuncs) CEILING(argsList *list.List) (result string, err error) // CEILINGMATH 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) CEILINGMATH(argsList *list.List) (result string, err error) { if argsList.Len() == 0 { @@ -1242,7 +1244,7 @@ func (fn *formulaFuncs) CEILINGMATH(argsList *list.List) (result string, err err // 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) CEILINGPRECISE(argsList *list.List) (result string, err error) { if argsList.Len() == 0 { @@ -1289,7 +1291,7 @@ func (fn *formulaFuncs) CEILINGPRECISE(argsList *list.List) (result string, err // 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) (result string, err error) { if argsList.Len() != 2 { @@ -1324,7 +1326,7 @@ func (fn *formulaFuncs) COMBIN(argsList *list.List) (result string, err error) { // 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) (result string, err error) { if argsList.Len() != 2 { @@ -1364,7 +1366,7 @@ func (fn *formulaFuncs) COMBINA(argsList *list.List) (result string, err error) // 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) (result string, err error) { if argsList.Len() != 1 { @@ -1383,7 +1385,7 @@ func (fn *formulaFuncs) COS(argsList *list.List) (result string, err error) { // 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) (result string, err error) { if argsList.Len() != 1 { @@ -1402,7 +1404,7 @@ func (fn *formulaFuncs) COSH(argsList *list.List) (result string, err error) { // 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) (result string, err error) { if argsList.Len() != 1 { @@ -1425,7 +1427,7 @@ func (fn *formulaFuncs) COT(argsList *list.List) (result string, err error) { // 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) (result string, err error) { if argsList.Len() != 1 { @@ -1448,7 +1450,7 @@ func (fn *formulaFuncs) COTH(argsList *list.List) (result string, err error) { // 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) (result string, err error) { if argsList.Len() != 1 { @@ -1471,7 +1473,7 @@ func (fn *formulaFuncs) CSC(argsList *list.List) (result string, err error) { // 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) (result string, err error) { if argsList.Len() != 1 { @@ -1494,7 +1496,7 @@ func (fn *formulaFuncs) CSCH(argsList *list.List) (result string, err error) { // 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) (result string, err error) { if argsList.Len() != 2 { @@ -1522,7 +1524,7 @@ func (fn *formulaFuncs) DECIMAL(argsList *list.List) (result string, err error) // DEGREES function converts radians into degrees. The syntax of the function // is: // -// DEGREES(angle) +// DEGREES(angle) // func (fn *formulaFuncs) DEGREES(argsList *list.List) (result string, err error) { if argsList.Len() != 1 { @@ -1546,7 +1548,7 @@ func (fn *formulaFuncs) DEGREES(argsList *list.List) (result string, err error) // 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) (result string, err error) { if argsList.Len() != 1 { @@ -1575,7 +1577,7 @@ func (fn *formulaFuncs) EVEN(argsList *list.List) (result string, err error) { // 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) (result string, err error) { if argsList.Len() != 1 { @@ -1603,7 +1605,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) (result string, err error) { if argsList.Len() != 1 { @@ -1625,7 +1627,7 @@ func (fn *formulaFuncs) FACT(argsList *list.List) (result string, err error) { // 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) (result string, err error) { if argsList.Len() != 1 { @@ -1651,7 +1653,7 @@ func (fn *formulaFuncs) FACTDOUBLE(argsList *list.List) (result string, err erro // 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) (result string, err error) { if argsList.Len() != 2 { @@ -1685,7 +1687,7 @@ func (fn *formulaFuncs) FLOOR(argsList *list.List) (result string, err error) { // FLOORMATH 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) FLOORMATH(argsList *list.List) (result string, err error) { if argsList.Len() == 0 { @@ -1731,7 +1733,7 @@ func (fn *formulaFuncs) FLOORMATH(argsList *list.List) (result string, err error // FLOORPRECISE 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) FLOORPRECISE(argsList *list.List) (result string, err error) { if argsList.Len() == 0 { @@ -1797,7 +1799,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) (result string, err error) { if argsList.Len() == 0 { @@ -1842,7 +1844,7 @@ func (fn *formulaFuncs) GCD(argsList *list.List) (result string, err error) { // 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) (result string, err error) { if argsList.Len() != 1 { @@ -1866,7 +1868,7 @@ func (fn *formulaFuncs) INT(argsList *list.List) (result string, err error) { // 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) ISOCEILING(argsList *list.List) (result string, err error) { if argsList.Len() == 0 { @@ -1923,7 +1925,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) (result string, err error) { if argsList.Len() == 0 { @@ -1968,7 +1970,7 @@ func (fn *formulaFuncs) LCM(argsList *list.List) (result string, err error) { // 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) (result string, err error) { if argsList.Len() != 1 { @@ -1987,7 +1989,7 @@ func (fn *formulaFuncs) LN(argsList *list.List) (result string, err error) { // 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) (result string, err error) { if argsList.Len() == 0 { @@ -2028,7 +2030,7 @@ func (fn *formulaFuncs) LOG(argsList *list.List) (result string, err error) { // 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) (result string, err error) { if argsList.Len() != 1 { @@ -2082,7 +2084,7 @@ func det(sqMtx [][]float64) float64 { // 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 string, err error) { var num float64 @@ -2113,7 +2115,7 @@ func (fn *formulaFuncs) MDETERM(argsList *list.List) (result string, err error) // 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) (result string, err error) { if argsList.Len() != 2 { @@ -2144,7 +2146,7 @@ func (fn *formulaFuncs) MOD(argsList *list.List) (result string, err error) { // MROUND function rounds a supplied number up or down to the nearest multiple // of a given number. The syntax of the function is: // -// MOD(number,multiple) +// MROUND(number,multiple) // func (fn *formulaFuncs) MROUND(argsList *list.List) (result string, err error) { if argsList.Len() != 2 { @@ -2852,7 +2854,7 @@ func (fn *formulaFuncs) SUMIF(argsList *list.List) (result string, err error) { // 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) (result string, err error) { var val, sq float64 @@ -2928,7 +2930,7 @@ func (fn *formulaFuncs) TANH(argsList *list.List) (result string, err error) { // 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) (result string, err error) { if argsList.Len() == 0 { @@ -2967,7 +2969,7 @@ func (fn *formulaFuncs) TRUNC(argsList *list.List) (result string, err error) { // 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) (result string, err error) { var count int @@ -2995,7 +2997,7 @@ func (fn *formulaFuncs) COUNTA(argsList *list.List) (result string, err error) { // 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) (result string, err error) { if argsList.Len() == 0 { @@ -3044,7 +3046,7 @@ func (fn *formulaFuncs) MEDIAN(argsList *list.List) (result string, err error) { // returns TRUE; Otherwise the function returns FALSE. The syntax of the // function is: // -// ISBLANK(value) +// ISBLANK(value) // func (fn *formulaFuncs) ISBLANK(argsList *list.List) (result string, err error) { if argsList.Len() != 1 { @@ -3069,7 +3071,7 @@ func (fn *formulaFuncs) ISBLANK(argsList *list.List) (result string, err error) // 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) (result string, err error) { if argsList.Len() != 1 { @@ -3092,7 +3094,7 @@ func (fn *formulaFuncs) ISERR(argsList *list.List) (result string, err error) { // 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) (result string, err error) { if argsList.Len() != 1 { @@ -3115,7 +3117,7 @@ func (fn *formulaFuncs) ISERROR(argsList *list.List) (result string, err error) // 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) (result string, err error) { if argsList.Len() != 1 { @@ -3142,7 +3144,7 @@ func (fn *formulaFuncs) ISEVEN(argsList *list.List) (result string, err error) { // 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) (result string, err error) { if argsList.Len() != 1 { @@ -3161,7 +3163,7 @@ func (fn *formulaFuncs) ISNA(argsList *list.List) (result string, err error) { // 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) (result string, err error) { if argsList.Len() != 1 { @@ -3180,7 +3182,7 @@ func (fn *formulaFuncs) ISNONTEXT(argsList *list.List) (result string, err error // 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) (result string, err error) { if argsList.Len() != 1 { @@ -3202,7 +3204,7 @@ func (fn *formulaFuncs) ISNUMBER(argsList *list.List) (result string, err error) // 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) (result string, err error) { if argsList.Len() != 1 { @@ -3229,7 +3231,7 @@ func (fn *formulaFuncs) ISODD(argsList *list.List) (result string, err error) { // 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) (result string, err error) { if argsList.Len() != 0 { @@ -3243,7 +3245,10 @@ func (fn *formulaFuncs) NA(argsList *list.List) (result string, err error) { // Logical Functions // AND function tests a number of supplied conditions and returns TRUE or -// FALSE. +// FALSE. The syntax of the function is: +// +// AND(logical_test1,[logical_test2],...) +// func (fn *formulaFuncs) AND(argsList *list.List) (result string, err error) { if argsList.Len() == 0 { err = errors.New("AND requires at least 1 argument") @@ -3284,7 +3289,10 @@ func (fn *formulaFuncs) AND(argsList *list.List) (result string, err error) { } // OR function tests a number of supplied conditions and returns either TRUE -// or FALSE. +// or FALSE. The syntax of the function is: +// +// OR(logical_test1,[logical_test2],...) +// func (fn *formulaFuncs) OR(argsList *list.List) (result string, err error) { if argsList.Len() == 0 { err = errors.New("OR requires at least 1 argument") @@ -3326,7 +3334,11 @@ func (fn *formulaFuncs) OR(argsList *list.List) (result string, err error) { // Date and Time Functions -// DATE returns a date, from a user-supplied year, month and day. +// DATE returns a date, from a user-supplied year, month and day. The syntax +// of the function is: +// +// DATE(year,month,day) +// func (fn *formulaFuncs) DATE(argsList *list.List) (result string, err error) { if argsList.Len() != 3 { err = errors.New("DATE requires 3 number arguments") @@ -3368,7 +3380,11 @@ func daysBetween(startDate, endDate int64) float64 { // Text Functions -// CLEAN removes all non-printable characters from a supplied text string. +// CLEAN removes all non-printable characters from a supplied text string. The +// syntax of the function is: +// +// CLEAN(text) +// func (fn *formulaFuncs) CLEAN(argsList *list.List) (result string, err error) { if argsList.Len() != 1 { err = errors.New("CLEAN requires 1 argument") @@ -3385,7 +3401,11 @@ func (fn *formulaFuncs) CLEAN(argsList *list.List) (result string, err error) { } // TRIM removes extra spaces (i.e. all spaces except for single spaces between -// words or characters) from a supplied text string. +// words or characters) from a supplied text string. The syntax of the +// function is: +// +// TRIM(text) +// func (fn *formulaFuncs) TRIM(argsList *list.List) (result string, err error) { if argsList.Len() != 1 { err = errors.New("TRIM requires 1 argument") @@ -3394,3 +3414,58 @@ func (fn *formulaFuncs) TRIM(argsList *list.List) (result string, err error) { result = strings.TrimSpace(argsList.Front().Value.(formulaArg).String) return } + +// LOWER converts all characters in a supplied text string to lower case. The +// syntax of the function is: +// +// LOWER(text) +// +func (fn *formulaFuncs) LOWER(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("LOWER requires 1 argument") + return + } + result = strings.ToLower(argsList.Front().Value.(formulaArg).String) + return +} + +// PROPER converts all characters in a supplied text string to proper case +// (i.e. all letters that do not immediately follow another letter are set to +// upper case and all other characters are lower case). The syntax of the +// function is: +// +// PROPER(text) +// +func (fn *formulaFuncs) PROPER(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("PROPER requires 1 argument") + return + } + buf := bytes.Buffer{} + isLetter := false + for _, char := range argsList.Front().Value.(formulaArg).String { + if !isLetter && unicode.IsLetter(char) { + buf.WriteRune(unicode.ToUpper(char)) + } else { + buf.WriteRune(unicode.ToLower(char)) + } + isLetter = unicode.IsLetter(char) + } + + result = buf.String() + return +} + +// UPPER converts all characters in a supplied text string to upper case. The +// syntax of the function is: +// +// UPPER(text) +// +func (fn *formulaFuncs) UPPER(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("UPPER requires 1 argument") + return + } + result = strings.ToUpper(argsList.Front().Value.(formulaArg).String) + return +} diff --git a/calc_test.go b/calc_test.go index f928797cac..ea60a50043 100644 --- a/calc_test.go +++ b/calc_test.go @@ -470,6 +470,21 @@ func TestCalcCellValue(t *testing.T) { // TRIM "=TRIM(\" trim text \")": "trim text", "=TRIM(0)": "0", + // LOWER + "=LOWER(\"test\")": "test", + "=LOWER(\"TEST\")": "test", + "=LOWER(\"Test\")": "test", + "=LOWER(\"TEST 123\")": "test 123", + // PROPER + "=PROPER(\"this is a test sentence\")": "This Is A Test Sentence", + "=PROPER(\"THIS IS A TEST SENTENCE\")": "This Is A Test Sentence", + "=PROPER(\"123tEST teXT\")": "123Test Text", + "=PROPER(\"Mr. SMITH's address\")": "Mr. Smith'S Address", + // UPPER + "=UPPER(\"test\")": "TEST", + "=UPPER(\"TEST\")": "TEST", + "=UPPER(\"Test\")": "TEST", + "=UPPER(\"TEST 123\")": "TEST 123", } for formula, expected := range mathCalc { f := prepareData() @@ -793,6 +808,15 @@ func TestCalcCellValue(t *testing.T) { // TRIM "=TRIM()": "TRIM requires 1 argument", "=TRIM(1,2)": "TRIM requires 1 argument", + // LOWER + "=LOWER()": "LOWER requires 1 argument", + "=LOWER(1,2)": "LOWER requires 1 argument", + // UPPER + "=UPPER()": "UPPER requires 1 argument", + "=UPPER(1,2)": "UPPER requires 1 argument", + // PROPER + "=PROPER()": "PROPER requires 1 argument", + "=PROPER(1,2)": "PROPER requires 1 argument", } for formula, expected := range mathCalcError { f := prepareData() diff --git a/rows.go b/rows.go index 591d7e91a4..1e29d8f8e2 100644 --- a/rows.go +++ b/rows.go @@ -602,7 +602,6 @@ func (f *File) duplicateMergeCells(sheet string, ws *xlsxWorksheet, row, row2 in if err := f.MergeCell(sheet, from, to); err != nil { return err } - i++ } } return nil diff --git a/rows_test.go b/rows_test.go index e49b28a36a..02b00daf8e 100644 --- a/rows_test.go +++ b/rows_test.go @@ -323,7 +323,7 @@ func TestDuplicateRowFromSingleRow(t *testing.T) { assert.NoError(t, f.SetCellStr(sheet, "B1", cells["B1"])) assert.NoError(t, f.DuplicateRow(sheet, 1)) - if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.FromSingleRow_1"))) { + if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "FromSingleRow_1"))) { t.FailNow() } expect := map[string]string{ @@ -339,7 +339,7 @@ func TestDuplicateRowFromSingleRow(t *testing.T) { } assert.NoError(t, f.DuplicateRow(sheet, 2)) - if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.FromSingleRow_2"))) { + if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "FromSingleRow_2"))) { t.FailNow() } expect = map[string]string{ @@ -380,7 +380,7 @@ func TestDuplicateRowUpdateDuplicatedRows(t *testing.T) { assert.NoError(t, f.SetCellStr(sheet, "A2", cells["A2"])) assert.NoError(t, f.SetCellStr(sheet, "B2", cells["B2"])) - if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.UpdateDuplicatedRows"))) { + if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "UpdateDuplicatedRows"))) { t.FailNow() } expect := map[string]string{ @@ -423,7 +423,7 @@ func TestDuplicateRowFirstOfMultipleRows(t *testing.T) { assert.NoError(t, f.DuplicateRow(sheet, 1)) - if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.FirstOfMultipleRows"))) { + if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "FirstOfMultipleRows"))) { t.FailNow() } expect := map[string]string{ @@ -451,7 +451,7 @@ func TestDuplicateRowZeroWithNoRows(t *testing.T) { assert.EqualError(t, f.DuplicateRow(sheet, 0), "invalid row number 0") - if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.ZeroWithNoRows"))) { + if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "ZeroWithNoRows"))) { t.FailNow() } @@ -493,7 +493,7 @@ func TestDuplicateRowMiddleRowOfEmptyFile(t *testing.T) { assert.NoError(t, f.DuplicateRow(sheet, 99)) - if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.MiddleRowOfEmptyFile"))) { + if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "MiddleRowOfEmptyFile"))) { t.FailNow() } expect := map[string]string{ @@ -537,7 +537,7 @@ func TestDuplicateRowWithLargeOffsetToMiddleOfData(t *testing.T) { assert.NoError(t, f.DuplicateRowTo(sheet, 1, 3)) - if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.WithLargeOffsetToMiddleOfData"))) { + if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "WithLargeOffsetToMiddleOfData"))) { t.FailNow() } expect := map[string]string{ @@ -582,7 +582,7 @@ func TestDuplicateRowWithLargeOffsetToEmptyRows(t *testing.T) { assert.NoError(t, f.DuplicateRowTo(sheet, 1, 7)) - if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.WithLargeOffsetToEmptyRows"))) { + if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "WithLargeOffsetToEmptyRows"))) { t.FailNow() } expect := map[string]string{ @@ -627,7 +627,7 @@ func TestDuplicateRowInsertBefore(t *testing.T) { assert.NoError(t, f.DuplicateRowTo(sheet, 2, 1)) - if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.InsertBefore"))) { + if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "InsertBefore"))) { t.FailNow() } @@ -673,7 +673,7 @@ func TestDuplicateRowInsertBeforeWithLargeOffset(t *testing.T) { assert.NoError(t, f.DuplicateRowTo(sheet, 3, 1)) - if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.InsertBeforeWithLargeOffset"))) { + if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "InsertBeforeWithLargeOffset"))) { t.FailNow() } @@ -722,7 +722,7 @@ func TestDuplicateRowInsertBeforeWithMergeCells(t *testing.T) { assert.NoError(t, f.DuplicateRowTo(sheet, 2, 1)) assert.NoError(t, f.DuplicateRowTo(sheet, 1, 8)) - if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.InsertBeforeWithMergeCells"))) { + if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "InsertBeforeWithMergeCells"))) { t.FailNow() } @@ -742,9 +742,9 @@ func TestDuplicateRowInsertBeforeWithMergeCells(t *testing.T) { }) } -func TestDuplicateRowInvalidRownum(t *testing.T) { +func TestDuplicateRowInvalidRowNum(t *testing.T) { const sheet = "Sheet1" - outFile := filepath.Join("test", "TestDuplicateRowInvalidRownum.%s.xlsx") + outFile := filepath.Join("test", "TestDuplicateRow.InvalidRowNum.%s.xlsx") cells := map[string]string{ "A1": "A1 Value", From 71829c520235b733870563f30dceef9ef4dbbb98 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 27 Dec 2020 00:18:54 +0800 Subject: [PATCH 309/957] AddChart support disable legend of the chart --- README.md | 3 +- README_zh.md | 3 +- chart.go | 148 +++++++++++++++++++++++++++++++++++++++++++++++--- chart_test.go | 16 +++--- drawing.go | 3 + 5 files changed, 156 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 7c67092c3e..b6c14332f0 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,8 @@ import ( func main() { categories := map[string]string{ - "A2": "Small", "A3": "Normal", "A4": "Large", "B1": "Apple", "C1": "Orange", "D1": "Pear"} + "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() diff --git a/README_zh.md b/README_zh.md index daafd1d89f..1118367dd4 100644 --- a/README_zh.md +++ b/README_zh.md @@ -112,7 +112,8 @@ import ( func main() { categories := map[string]string{ - "A2": "Small", "A3": "Normal", "A4": "Large", "B1": "Apple", "C1": "Orange", "D1": "Pear"} + "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() diff --git a/chart.go b/chart.go index bdb7f5b761..9d44c50e75 100644 --- a/chart.go +++ b/chart.go @@ -514,8 +514,11 @@ func parseFormatChartSet(formatSet string) (*formatChart, 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} +// 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) @@ -523,7 +526,54 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // 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"},"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 { +// 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 // } @@ -627,10 +677,13 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // // Set properties of the chart legend. The options that can be set are: // +// none // position // show_legend_key // -// position: Set the position of the chart legend. The default legend position is right. The available positions are: +// 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 @@ -728,8 +781,11 @@ func parseFormatChartSet(formatSet string) (*formatChart, 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} +// 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) @@ -737,7 +793,85 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // 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},"legend":{"position":"left","show_legend_key":false},"title":{"name":"Clustered Column - 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}}`, `{"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":"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}}`); err != nil { +// 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 // } diff --git a/chart_test.go b/chart_test.go index 4a7000ae1b..9bbc06d141 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 is 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":{"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.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"}`)) 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"}`)) @@ -179,14 +179,14 @@ func TestAddChart(t *testing.T) { 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}}`)) // combo chart f.NewSheet("Combo Charts") - clusteredColumnCombo := map[string][]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"}, + 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"}, } - for axis, props := range clusteredColumnCombo { - assert.NoError(t, f.AddChart("Combo Charts", axis, 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[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]))) + 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]))) } stackedAreaCombo := map[string][]string{ "A16": {"line", "Stacked Area - Line Chart"}, diff --git a/drawing.go b/drawing.go index 0db5d0e749..de9905e7d5 100644 --- a/drawing.go +++ b/drawing.go @@ -236,6 +236,9 @@ func (f *File) addChart(formatSet *formatChart, comboCharts []*formatChart) { Bubble: f.drawBaseChart, Bubble3D: f.drawBaseChart, } + if formatSet.Legend.None { + xlsxChartSpace.Chart.Legend = nil + } addChart := func(c, p *cPlotArea) { immutable, mutable := reflect.ValueOf(c).Elem(), reflect.ValueOf(p).Elem() for i := 0; i < mutable.NumField(); i++ { From 22dc6ff33c24e25c0281401272c852f81c10a9f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BE=9A=E5=B0=9A?= Date: Tue, 5 Jan 2021 13:41:00 +0800 Subject: [PATCH 310/957] Update the tips message for Go modules user --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b6c14332f0..44fc57a6f8 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Excelize is a library written in pure Go providing a set of functions that allow go get github.com/360EntSecGroup-Skylar/excelize ``` -- If your package management with [Go Modules](https://blog.golang.org/using-go-modules), please install with following command. +- If your packages are managed using [Go Modules](https://blog.golang.org/using-go-modules), please install with following command. ```bash go get github.com/360EntSecGroup-Skylar/excelize/v2 From a26675517e6326a7e3d3391f9de79d5efeb8bb90 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 8 Jan 2021 23:57:13 +0800 Subject: [PATCH 311/957] This closes #756, not set the empty string for the cell when SetCellValue with nil --- cell.go | 2 +- cell_test.go | 2 +- drawing.go | 2 +- xmlChart.go | 8 ++++---- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cell.go b/cell.go index 4dd28306d7..3e635659fa 100644 --- a/cell.go +++ b/cell.go @@ -93,7 +93,7 @@ func (f *File) SetCellValue(sheet, axis string, value interface{}) error { case bool: err = f.SetCellBool(sheet, axis, v) case nil: - err = f.SetCellStr(sheet, axis, "") + break default: err = f.SetCellStr(sheet, axis, fmt.Sprint(value)) } diff --git a/cell_test.go b/cell_test.go index 8d3f77482d..2122ecac0e 100644 --- a/cell_test.go +++ b/cell_test.go @@ -331,7 +331,7 @@ func TestFormattedValue2(t *testing.T) { f.Styles.CellXfs.Xf = append(f.Styles.CellXfs.Xf, xlsxXf{ NumFmtID: nil, }) - v = f.formattedValue(3, "43528") + _ = f.formattedValue(3, "43528") // formatted value with empty number format f.Styles.NumFmts = nil diff --git a/drawing.go b/drawing.go index de9905e7d5..f0eb7e9571 100644 --- a/drawing.go +++ b/drawing.go @@ -844,7 +844,7 @@ 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 { - defaultSymbol := map[string]*attrValString{Scatter: &attrValString{Val: stringPtr("circle")}} + defaultSymbol := map[string]*attrValString{Scatter: {Val: stringPtr("circle")}} marker := &cMarker{ Symbol: defaultSymbol[formatSet.Type], Size: &attrValInt{Val: intPtr(5)}, diff --git a/xmlChart.go b/xmlChart.go index fffdddd018..ee2ad29425 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -138,25 +138,25 @@ type aSchemeClr struct { } // attrValInt directly maps the val element with integer data type as an -// attribute。 +// attribute. type attrValInt struct { Val *int `xml:"val,attr"` } // attrValFloat directly maps the val element with float64 data type as an -// attribute。 +// attribute. type attrValFloat struct { Val *float64 `xml:"val,attr"` } // attrValBool directly maps the val element with boolean data type as an -// attribute。 +// attribute. type attrValBool struct { Val *bool `xml:"val,attr"` } // attrValString directly maps the val element with string data type as an -// attribute。 +// attribute. type attrValString struct { Val *string `xml:"val,attr"` } From 054bb9f0612ab7bd5836c3817d105a9b0c9e6059 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 16 Jan 2021 21:51:23 +0800 Subject: [PATCH 312/957] Support to adjust print scaling of the worksheet --- sheet.go | 20 ++++++++++++++++++++ sheet_test.go | 9 ++++++++- xmlWorksheet.go | 2 +- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/sheet.go b/sheet.go index 82c6a6998e..9f71de47ad 100644 --- a/sheet.go +++ b/sheet.go @@ -1144,6 +1144,10 @@ type ( FitToHeight int // FitToWidth specified 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 + // overridden when fitToWidth and/or fitToHeight are in use. + PageLayoutScale uint ) const ( @@ -1215,6 +1219,22 @@ func (p *FitToWidth) getPageLayout(ps *xlsxPageSetUp) { *p = FitToWidth(ps.FitToWidth) } +// setPageLayout provides a method to set the scale for the worksheet. +func (p PageLayoutScale) setPageLayout(ps *xlsxPageSetUp) { + if 10 <= uint(p) && uint(p) <= 400 { + ps.Scale = uint(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: diff --git a/sheet_test.go b/sheet_test.go index d1c8f642bb..701d8244b2 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -25,6 +25,7 @@ func ExampleFile_SetPageLayout() { PageLayoutPaperSize(10), FitToHeight(2), FitToWidth(2), + PageLayoutScale(50), ); err != nil { fmt.Println(err) } @@ -38,6 +39,7 @@ func ExampleFile_GetPageLayout() { paperSize PageLayoutPaperSize fitToHeight FitToHeight fitToWidth FitToWidth + scale PageLayoutScale ) if err := f.GetPageLayout("Sheet1", &orientation); err != nil { fmt.Println(err) @@ -48,21 +50,25 @@ func ExampleFile_GetPageLayout() { 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("- 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: // - orientation: "portrait" // - paper size: 1 // - fit to height: 1 // - fit to width: 1 + // - scale: 100 } func TestNewSheet(t *testing.T) { @@ -101,6 +107,7 @@ func TestPageLayoutOption(t *testing.T) { {new(PageLayoutPaperSize), PageLayoutPaperSize(10)}, {new(FitToHeight), FitToHeight(2)}, {new(FitToWidth), FitToWidth(2)}, + {new(PageLayoutScale), PageLayoutScale(50)}, } for i, test := range testData { diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 26c8facedd..1f680d2b87 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -121,7 +121,7 @@ type xlsxPageSetUp struct { PaperHeight string `xml:"paperHeight,attr,omitempty"` PaperSize int `xml:"paperSize,attr,omitempty"` PaperWidth string `xml:"paperWidth,attr,omitempty"` - Scale int `xml:"scale,attr,omitempty"` + Scale uint `xml:"scale,attr,omitempty"` UseFirstPageNumber bool `xml:"useFirstPageNumber,attr,omitempty"` UsePrinterDefaults bool `xml:"usePrinterDefaults,attr,omitempty"` VerticalDPI int `xml:"verticalDpi,attr,omitempty"` From b260485f29038ca8df9993edb1c021672b3df7e4 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 17 Jan 2021 01:06:08 +0800 Subject: [PATCH 313/957] support to set print black and white and specified the first printed page number --- sheet.go | 56 ++++++++++++++++++++++++++++++++++++++++++++----- sheet_test.go | 32 ++++++++++++++++++---------- xmlWorksheet.go | 8 +++---- 3 files changed, 76 insertions(+), 20 deletions(-) diff --git a/sheet.go b/sheet.go index 9f71de47ad..4e56943975 100644 --- a/sheet.go +++ b/sheet.go @@ -1135,14 +1135,19 @@ type PageLayoutOptionPtr interface { } type ( + // BlackAndWhite specified print black and white. + BlackAndWhite bool + // FirstPageNumber specified 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 defines the paper size of the worksheet. PageLayoutPaperSize int - // FitToHeight specified number of vertical pages to fit on + // FitToHeight specified number of vertical pages to fit on. FitToHeight int - // FitToWidth specified number of horizontal pages to fit on + // FitToWidth specified 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 @@ -1157,6 +1162,41 @@ const ( 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 < uint(p) { + ps.FirstPageNumber = uint(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.FirstPageNumber == 0 || !ps.UseFirstPageNumber { + *p = 1 + return + } + *p = FirstPageNumber(ps.FirstPageNumber) +} + // setPageLayout provides a method to set the orientation for the worksheet. func (o PageLayoutOrientation) setPageLayout(ps *xlsxPageSetUp) { ps.Orientation = string(o) @@ -1238,8 +1278,14 @@ func (p *PageLayoutScale) getPageLayout(ps *xlsxPageSetUp) { // SetPageLayout provides a function to sets worksheet page layout. // // Available options: -// PageLayoutOrientation(string) -// PageLayoutPaperSize(int) +// +// 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: // diff --git a/sheet_test.go b/sheet_test.go index 701d8244b2..d68327e93c 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -13,15 +13,11 @@ import ( func ExampleFile_SetPageLayout() { f := NewFile() - if err := f.SetPageLayout( "Sheet1", + BlackAndWhite(true), + FirstPageNumber(2), PageLayoutOrientation(OrientationLandscape), - ); err != nil { - fmt.Println(err) - } - if err := f.SetPageLayout( - "Sheet1", PageLayoutPaperSize(10), FitToHeight(2), FitToWidth(2), @@ -35,12 +31,20 @@ func ExampleFile_SetPageLayout() { func ExampleFile_GetPageLayout() { f := NewFile() var ( - orientation PageLayoutOrientation - paperSize PageLayoutPaperSize - fitToHeight FitToHeight - fitToWidth FitToWidth - scale PageLayoutScale + 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) } @@ -57,6 +61,8 @@ func ExampleFile_GetPageLayout() { 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) @@ -64,6 +70,8 @@ func ExampleFile_GetPageLayout() { 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 @@ -103,6 +111,8 @@ func TestPageLayoutOption(t *testing.T) { container PageLayoutOptionPtr nonDefault PageLayoutOption }{ + {new(BlackAndWhite), BlackAndWhite(true)}, + {new(FirstPageNumber), FirstPageNumber(2)}, {new(PageLayoutOrientation), PageLayoutOrientation(OrientationLandscape)}, {new(PageLayoutPaperSize), PageLayoutPaperSize(10)}, {new(FitToHeight), FitToHeight(2)}, diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 1f680d2b87..72a470ffc1 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -108,13 +108,13 @@ type xlsxPageSetUp struct { XMLName xml.Name `xml:"pageSetup"` BlackAndWhite bool `xml:"blackAndWhite,attr,omitempty"` CellComments string `xml:"cellComments,attr,omitempty"` - Copies int `xml:"copies,attr,omitempty"` + Copies uint `xml:"copies,attr,omitempty"` Draft bool `xml:"draft,attr,omitempty"` Errors string `xml:"errors,attr,omitempty"` - FirstPageNumber int `xml:"firstPageNumber,attr,omitempty"` + FirstPageNumber uint `xml:"firstPageNumber,attr,omitempty"` FitToHeight int `xml:"fitToHeight,attr,omitempty"` FitToWidth int `xml:"fitToWidth,attr,omitempty"` - HorizontalDPI int `xml:"horizontalDpi,attr,omitempty"` + HorizontalDPI uint `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"` @@ -124,7 +124,7 @@ type xlsxPageSetUp struct { Scale uint `xml:"scale,attr,omitempty"` UseFirstPageNumber bool `xml:"useFirstPageNumber,attr,omitempty"` UsePrinterDefaults bool `xml:"usePrinterDefaults,attr,omitempty"` - VerticalDPI int `xml:"verticalDpi,attr,omitempty"` + VerticalDPI uint `xml:"verticalDpi,attr,omitempty"` } // xlsxPrintOptions directly maps the printOptions element in the namespace From 1bc5302007e04b83ed542fee993e9a79aea9e370 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 20 Jan 2021 00:14:21 +0800 Subject: [PATCH 314/957] Fixed #764, add a condition for round precision --- cell_test.go | 5 +++-- rows.go | 4 +++- sheet.go | 6 +++--- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/cell_test.go b/cell_test.go index 2122ecac0e..93e9f4c98b 100644 --- a/cell_test.go +++ b/cell_test.go @@ -124,8 +124,9 @@ func TestSetCellValues(t *testing.T) { err = f.SetCellValue("Sheet1", "A1", time.Date(1600, time.December, 31, 0, 0, 0, 0, time.UTC)) assert.NoError(t, err) - _, err = f.GetCellValue("Sheet1", "A1") - assert.EqualError(t, err, `strconv.ParseFloat: parsing "1600-12-31T00:00:00Z": invalid syntax`) + v, err = f.GetCellValue("Sheet1", "A1") + assert.NoError(t, err) + assert.Equal(t, v, "1600-12-31T00:00:00Z") } func TestSetCellBool(t *testing.T) { diff --git a/rows.go b/rows.go index 1e29d8f8e2..44c4b643a4 100644 --- a/rows.go +++ b/rows.go @@ -20,6 +20,7 @@ import ( "log" "math" "strconv" + "strings" "github.com/mohae/deepcopy" ) @@ -345,7 +346,8 @@ func (c *xlsxC) getValueFrom(f *File, d *xlsxSST) (string, error) { } return f.formattedValue(c.S, c.V), nil default: - if len(c.V) > 16 { + splited := strings.Split(c.V, ".") + if len(splited) == 2 && len(splited[1]) > 15 { val, err := roundPrecision(c.V) if err != nil { return "", err diff --git a/sheet.go b/sheet.go index 4e56943975..9b80395ef8 100644 --- a/sheet.go +++ b/sheet.go @@ -1137,7 +1137,7 @@ type PageLayoutOptionPtr interface { type ( // BlackAndWhite specified print black and white. BlackAndWhite bool - // FirstPageNumber specified first printed page number. If no value is + // 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 @@ -1145,9 +1145,9 @@ type ( PageLayoutOrientation string // PageLayoutPaperSize defines the paper size of the worksheet. PageLayoutPaperSize int - // FitToHeight specified number of vertical pages to fit on. + // FitToHeight specified the number of vertical pages to fit on. FitToHeight int - // FitToWidth specified number of horizontal pages to fit on. + // 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 From 58ecf81630e693646d4e70d5f7f33dcb8dafaeb0 Mon Sep 17 00:00:00 2001 From: Ray Date: Wed, 20 Jan 2021 14:40:33 +0800 Subject: [PATCH 315/957] Update excelize.go (#765) miss a char as 'l' in the excel. --- excelize.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/excelize.go b/excelize.go index bcae48103b..a38a74566c 100644 --- a/excelize.go +++ b/excelize.go @@ -4,7 +4,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX / XLSM / XLTM files. Supports reading and writing -// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports +// 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.10 or later. From e568319bbca22011690a54c5f5741da2b50d0c7c Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 23 Jan 2021 00:09:13 +0800 Subject: [PATCH 316/957] Fixed #766, change order, and added fields of workbook fields --- xmlWorkbook.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/xmlWorkbook.go b/xmlWorkbook.go index 89cacd9253..b25165bf04 100644 --- a/xmlWorkbook.go +++ b/xmlWorkbook.go @@ -32,18 +32,26 @@ type xlsxRelationship struct { // subclause references. type xlsxWorkbook struct { XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main workbook"` + Conformance string `xml:"conformance,attr,omitempty"` FileVersion *xlsxFileVersion `xml:"fileVersion"` + FileSharing *xlsxExtLst `xml:"fileSharing"` WorkbookPr *xlsxWorkbookPr `xml:"workbookPr"` WorkbookProtection *xlsxWorkbookProtection `xml:"workbookProtection"` BookViews *xlsxBookViews `xml:"bookViews"` Sheets xlsxSheets `xml:"sheets"` + FunctionGroups *xlsxExtLst `xml:"functionGroups"` ExternalReferences *xlsxExternalReferences `xml:"externalReferences"` DefinedNames *xlsxDefinedNames `xml:"definedNames"` CalcPr *xlsxCalcPr `xml:"calcPr"` + OleSize *xlsxExtLst `xml:"oleSize"` CustomWorkbookViews *xlsxCustomWorkbookViews `xml:"customWorkbookViews"` PivotCaches *xlsxPivotCaches `xml:"pivotCaches"` - ExtLst *xlsxExtLst `xml:"extLst"` + SmartTagPr *xlsxExtLst `xml:"smartTagPr"` + SmartTagTypes *xlsxExtLst `xml:"smartTagTypes"` + WebPublishing *xlsxExtLst `xml:"webPublishing"` FileRecoveryPr *xlsxFileRecoveryPr `xml:"fileRecoveryPr"` + WebPublishObjects *xlsxExtLst `xml:"webPublishObjects"` + ExtLst *xlsxExtLst `xml:"extLst"` } // xlsxFileRecoveryPr maps sheet recovery information. This element defines From b84bd1abc06457f6383013b8a600fc8c95eed2ed Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 27 Jan 2021 13:51:47 +0800 Subject: [PATCH 317/957] new formula fn: IF, LEN; not equal operator support and faster numeric precision process --- calc.go | 70 ++++++++++++++++++++++++++++++++++++++++++++++++---- calc_test.go | 24 +++++++++++++++--- lib.go | 27 ++++++++++++++++++-- rows.go | 10 +++----- rows_test.go | 11 ++++++--- 5 files changed, 121 insertions(+), 21 deletions(-) diff --git a/calc.go b/calc.go index d2bab1dc70..5b975f5247 100644 --- a/calc.go +++ b/calc.go @@ -110,6 +110,7 @@ var tokenPriority = map[string]int{ "+": 3, "-": 3, "=": 2, + "<>": 2, "<": 2, "<=": 2, ">": 2, @@ -151,11 +152,9 @@ func (f *File) CalcCellValue(sheet, cell string) (result string, err error) { return } result = token.TValue - if len(result) > 16 { - num, e := roundPrecision(result) - if e != nil { - return result, err - } + isNum, precision := isNumeric(result) + if isNum && precision > 15 { + num, _ := roundPrecision(result) result = strings.ToUpper(num) } return @@ -353,6 +352,12 @@ func calcEq(rOpd, lOpd string, opdStack *Stack) error { return nil } +// calcNEq evaluate not equal arithmetic operations. +func calcNEq(rOpd, lOpd string, opdStack *Stack) error { + opdStack.Push(efp.Token{TValue: strings.ToUpper(strconv.FormatBool(rOpd != lOpd)), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + return nil +} + // calcL evaluate less than arithmetic operations. func calcL(rOpd, lOpd string, opdStack *Stack) error { lOpdVal, err := strconv.ParseFloat(lOpd, 64) @@ -498,6 +503,7 @@ func calculate(opdStack *Stack, opt efp.Token) error { "/": calcDiv, "+": calcAdd, "=": calcEq, + "<>": calcNEq, "<": calcL, "<=": calcLe, ">": calcG, @@ -3400,6 +3406,20 @@ func (fn *formulaFuncs) CLEAN(argsList *list.List) (result string, err error) { return } +// LEN returns the length of a supplied text string. The syntax of the +// function is: +// +// LEN(text) +// +func (fn *formulaFuncs) LEN(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("LEN requires 1 string argument") + return + } + result = strconv.Itoa(len(argsList.Front().Value.(formulaArg).String)) + return +} + // TRIM removes extra spaces (i.e. all spaces except for single spaces between // words or characters) from a supplied text string. The syntax of the // function is: @@ -3469,3 +3489,43 @@ func (fn *formulaFuncs) UPPER(argsList *list.List) (result string, err error) { result = strings.ToUpper(argsList.Front().Value.(formulaArg).String) return } + +// Conditional Functions + +// IF function tests a supplied condition and returns one result if the +// 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 ) +// +func (fn *formulaFuncs) IF(argsList *list.List) (result string, err error) { + if argsList.Len() == 0 { + err = errors.New("IF requires at least 1 argument") + return + } + if argsList.Len() > 3 { + err = errors.New("IF accepts at most 3 arguments") + return + } + token := argsList.Front().Value.(formulaArg) + var cond bool + switch token.Type { + case ArgString: + if cond, err = strconv.ParseBool(token.String); err != nil { + err = errors.New(formulaErrorVALUE) + return + } + if argsList.Len() == 1 { + result = strings.ToUpper(strconv.FormatBool(cond)) + return + } + if cond { + result = argsList.Front().Next().Value.(formulaArg).String + return + } + if argsList.Len() == 3 { + result = argsList.Back().Value.(formulaArg).String + } + } + return +} diff --git a/calc_test.go b/calc_test.go index ea60a50043..c999540380 100644 --- a/calc_test.go +++ b/calc_test.go @@ -323,13 +323,13 @@ func TestCalcCellValue(t *testing.T) { "=ROUND(991,-1)": "990", // ROUNDDOWN "=ROUNDDOWN(99.999,1)": "99.9", - "=ROUNDDOWN(99.999,2)": "99.99000000000001", + "=ROUNDDOWN(99.999,2)": "99.99000000000002", "=ROUNDDOWN(99.999,0)": "99", "=ROUNDDOWN(99.999,-1)": "90", - "=ROUNDDOWN(-99.999,2)": "-99.99000000000001", + "=ROUNDDOWN(-99.999,2)": "-99.99000000000002", "=ROUNDDOWN(-99.999,-1)": "-90", - // ROUNDUP - "=ROUNDUP(11.111,1)": "11.200000000000003", + // ROUNDUP` + "=ROUNDUP(11.111,1)": "11.200000000000001", "=ROUNDUP(11.111,2)": "11.120000000000003", "=ROUNDUP(11.111,0)": "12", "=ROUNDUP(11.111,-1)": "20", @@ -467,6 +467,9 @@ func TestCalcCellValue(t *testing.T) { // CLEAN "=CLEAN(\"\u0009clean text\")": "clean text", "=CLEAN(0)": "0", + // LEN + "=LEN(\"\")": "0", + "=LEN(D1)": "5", // TRIM "=TRIM(\" trim text \")": "trim text", "=TRIM(0)": "0", @@ -485,6 +488,12 @@ func TestCalcCellValue(t *testing.T) { "=UPPER(\"TEST\")": "TEST", "=UPPER(\"Test\")": "TEST", "=UPPER(\"TEST 123\")": "TEST 123", + // Conditional Functions + // IF + "=IF(1=1)": "TRUE", + "=IF(1<>1)": "FALSE", + "=IF(5<0, \"negative\", \"positive\")": "positive", + "=IF(-2<0, \"negative\", \"positive\")": "negative", } for formula, expected := range mathCalc { f := prepareData() @@ -805,6 +814,8 @@ func TestCalcCellValue(t *testing.T) { // CLEAN "=CLEAN()": "CLEAN requires 1 argument", "=CLEAN(1,2)": "CLEAN requires 1 argument", + // LEN + "=LEN()": "LEN requires 1 string argument", // TRIM "=TRIM()": "TRIM requires 1 argument", "=TRIM(1,2)": "TRIM requires 1 argument", @@ -817,6 +828,11 @@ func TestCalcCellValue(t *testing.T) { // PROPER "=PROPER()": "PROPER requires 1 argument", "=PROPER(1,2)": "PROPER 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)": "#VALUE!", } for formula, expected := range mathCalcError { f := prepareData() diff --git a/lib.go b/lib.go index c89d69f0af..3f135128c2 100644 --- a/lib.go +++ b/lib.go @@ -403,8 +403,8 @@ func (f *File) addNameSpaces(path string, ns xml.Attr) { } } -// setIgnorableNameSpace provides a function to set XML namespace as ignorable by the given -// attribute. +// setIgnorableNameSpace provides a function to set XML namespace as ignorable +// by the given attribute. func (f *File) setIgnorableNameSpace(path string, index int, ns xml.Attr) { ignorableNS := []string{"c14", "cdr14", "a14", "pic14", "x14", "xdr14", "x14ac", "dsp", "mso14", "dgm14", "x15", "x12ac", "x15ac", "xr", "xr2", "xr3", "xr4", "xr5", "xr6", "xr7", "xr8", "xr9", "xr10", "xr11", "xr12", "xr13", "xr14", "xr15", "x15", "x16", "x16r2", "mo", "mx", "mv", "o", "v"} if inStrSlice(strings.Fields(f.xmlAttr[path][index].Value), ns.Name.Local) == -1 && inStrSlice(ignorableNS, ns.Name.Local) != -1 { @@ -418,6 +418,29 @@ func (f *File) addSheetNameSpace(sheet string, ns xml.Attr) { f.addNameSpaces(name, ns) } +// isNumeric determines whether an expression is a valid numeric type and get +// the precision for the numeric. +func isNumeric(s string) (bool, int) { + dot := false + p := 0 + for i, v := range s { + if v == '.' { + if dot { + return false, 0 + } + dot = true + } else if v < '0' || v > '9' { + if i == 0 && v == '-' { + continue + } + return false, 0 + } else if dot { + p++ + } + } + return true, p +} + // Stack defined an abstract data type that serves as a collection of elements. type Stack struct { list *list.List diff --git a/rows.go b/rows.go index 44c4b643a4..97cf2f592f 100644 --- a/rows.go +++ b/rows.go @@ -20,7 +20,6 @@ import ( "log" "math" "strconv" - "strings" "github.com/mohae/deepcopy" ) @@ -346,12 +345,9 @@ func (c *xlsxC) getValueFrom(f *File, d *xlsxSST) (string, error) { } return f.formattedValue(c.S, c.V), nil default: - splited := strings.Split(c.V, ".") - if len(splited) == 2 && len(splited[1]) > 15 { - val, err := roundPrecision(c.V) - if err != nil { - return "", err - } + isNum, precision := isNumeric(c.V) + if isNum && precision > 15 { + val, _ := roundPrecision(c.V) if val != c.V { return f.formattedValue(c.S, val), nil } diff --git a/rows_test.go b/rows_test.go index 02b00daf8e..73931aa3f5 100644 --- a/rows_test.go +++ b/rows_test.go @@ -835,9 +835,14 @@ func TestGetValueFromNumber(t *testing.T) { assert.Equal(t, "2.22", val) c = &xlsxC{T: "n", V: "2.220000ddsf0000000002-r"} - _, err = c.getValueFrom(f, d) - assert.NotNil(t, err) - assert.Equal(t, "strconv.ParseFloat: parsing \"2.220000ddsf0000000002-r\": invalid syntax", err.Error()) + val, err = c.getValueFrom(f, d) + assert.NoError(t, err) + assert.Equal(t, "2.220000ddsf0000000002-r", val) + + c = &xlsxC{T: "n", V: "2.2."} + val, err = c.getValueFrom(f, d) + assert.NoError(t, err) + assert.Equal(t, "2.2.", val) } func TestErrSheetNotExistError(t *testing.T) { From 219add2f0e4ae591141330648d410b60f5c0dbcf Mon Sep 17 00:00:00 2001 From: Eagle Xiang Date: Thu, 28 Jan 2021 21:13:23 +0800 Subject: [PATCH 318/957] =?UTF-8?q?value=20fields=20xlsxPatternFill.FgColo?= =?UTF-8?q?r=20&=20xlsxPatternFill.BgColor=20cause=20=E2=80=A6=20(#770)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * value fields xlsxPatternFill.FgColor & xlsxPatternFill.BgColor cause ineffective omitempty tags * remove useless omitempty tag on xlsxPatternFill.FgColor and xlsxPatternFill.BgColor --- styles.go | 6 ++++++ xmlStyles.go | 6 +++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/styles.go b/styles.go index c2dc7fa989..9fd0f1850e 100644 --- a/styles.go +++ b/styles.go @@ -2432,8 +2432,14 @@ func newFills(style *Style, fg bool) *xlsxFill { var pattern xlsxPatternFill pattern.PatternType = patterns[style.Fill.Pattern] if fg { + if pattern.FgColor == nil { + pattern.FgColor = new(xlsxColor) + } pattern.FgColor.RGB = getPaletteColor(style.Fill.Color[0]) } else { + if pattern.BgColor == nil { + pattern.BgColor = new(xlsxColor) + } pattern.BgColor.RGB = getPaletteColor(style.Fill.Color[0]) } fill.PatternFill = &pattern diff --git a/xmlStyles.go b/xmlStyles.go index 2884800097..db85b15965 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -120,9 +120,9 @@ type xlsxFill struct { // For cell fills with patterns specified, then the cell fill color is // specified by the bgColor element. type xlsxPatternFill struct { - PatternType string `xml:"patternType,attr,omitempty"` - FgColor xlsxColor `xml:"fgColor,omitempty"` - BgColor xlsxColor `xml:"bgColor,omitempty"` + PatternType string `xml:"patternType,attr,omitempty"` + FgColor *xlsxColor `xml:"fgColor"` + BgColor *xlsxColor `xml:"bgColor"` } // xlsxGradientFill defines a gradient-style cell fill. Gradient cell fills can From dd77cfe44c0d5481adb3af9a8e67b31d450a99e0 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 30 Jan 2021 00:11:01 +0800 Subject: [PATCH 319/957] refactor formula calculation framework, add new function CHOOSE, and update dependencies module --- calc.go | 1728 +++++++++++++++++++++++--------------------------- calc_test.go | 227 +++---- go.mod | 8 +- go.sum | 24 +- 4 files changed, 943 insertions(+), 1044 deletions(-) diff --git a/calc.go b/calc.go index 5b975f5247..0a653288b7 100644 --- a/calc.go +++ b/calc.go @@ -88,15 +88,36 @@ type ArgType byte // Formula argument types enumeration. const ( ArgUnknown ArgType = iota + ArgNumber ArgString + ArgList ArgMatrix + ArgError + ArgEmpty ) // formulaArg is the argument of a formula or function. type formulaArg struct { - String string - Matrix [][]formulaArg - Type ArgType + Number float64 + String string + List []formulaArg + Matrix [][]formulaArg + Boolean bool + Error string + Type ArgType +} + +// Value returns a string data type of the formula argument. +func (fa formulaArg) Value() (value string) { + switch fa.Type { + case ArgNumber: + return fmt.Sprintf("%g", fa.Number) + case ArgString: + return fa.String + case ArgError: + return fa.Error + } + return } // formulaFuncs is the type of the formula functions. @@ -124,15 +145,94 @@ var tokenPriority = map[string]int{ // // Supported formulas: // -// ABS, ACOS, ACOSH, ACOT, ACOTH, AND, ARABIC, ASIN, ASINH, ATAN2, ATANH, -// BASE, CEILING, CEILING.MATH, CEILING.PRECISE, CLEAN, COMBIN, COMBINA, -// COS, COSH, COT, COTH, COUNTA, CSC, CSCH, DATE, DECIMAL, DEGREES, EVEN, -// EXP, FACT, FACTDOUBLE, FLOOR, FLOOR.MATH, FLOOR.PRECISE, GCD, INT, -// ISBLANK, ISERR, ISERROR, ISEVEN, ISNA, ISNONTEXT, ISNUMBER, ISO.CEILING, -// ISODD, LCM, LN, LOG, LOG10, LOWER, MDETERM, MEDIAN, MOD, MROUND, -// MULTINOMIAL, MUNIT, NA, ODD, OR, PI, POWER, PRODUCT, PROPER, QUOTIENT, -// RADIANS, RAND, RANDBETWEEN, ROUND, ROUNDDOWN, ROUNDUP, SEC, SECH, SIGN, -// SIN, SINH, SQRT, SQRTPI, SUM, SUMIF, SUMSQ, TAN, TANH, TRIM, TRUNC, +// ABS +// ACOS +// ACOSH +// ACOT +// ACOTH +// AND +// ARABIC +// ASIN +// ASINH +// ATAN2 +// ATANH +// BASE +// CEILING +// CEILING.MATH +// CEILING.PRECISE +// CHOOSE +// CLEAN +// COMBIN +// COMBINA +// COS +// COSH +// COT +// COTH +// COUNTA +// CSC +// CSCH +// DATE +// DECIMAL +// DEGREES +// EVEN +// EXP +// FACT +// FACTDOUBLE +// FLOOR +// FLOOR.MATH +// FLOOR.PRECISE +// GCD +// IF +// INT +// ISBLANK +// ISERR +// ISERROR +// ISEVEN +// ISNA +// ISNONTEXT +// ISNUMBER +// ISODD +// ISO.CEILING +// LCM +// LEN +// LN +// LOG +// LOG10 +// LOWER +// MDETERM +// MEDIAN +// MOD +// MROUND +// MULTINOMIAL +// MUNIT +// NA +// ODD +// OR +// PI +// POWER +// PRODUCT +// PROPER +// QUOTIENT +// RADIANS +// RAND +// RANDBETWEEN +// ROUND +// ROUNDDOWN +// ROUNDUP +// SEC +// SECH +// SIGN +// SIN +// SINH +// SQRT +// SQRTPI +// SUM +// SUMIF +// SUMSQ +// TAN +// TANH +// TRIM +// TRUNC // UPPER // func (f *File) CalcCellValue(sheet, cell string) (result string, err error) { @@ -172,6 +272,21 @@ func getPriority(token efp.Token) (pri int) { return } +// newNumberFormulaArg constructs a number formula argument. +func newNumberFormulaArg(n float64) formulaArg { + return formulaArg{Type: ArgNumber, Number: n} +} + +// newStringFormulaArg constructs a string formula argument. +func newStringFormulaArg(s string) formulaArg { + return formulaArg{Type: ArgString, String: s} +} + +// newErrorFormulaArg create an error formula argument of a given type with a specified error message. +func newErrorFormulaArg(formulaError, msg string) formulaArg { + return formulaArg{Type: ArgError, String: formulaError, Error: msg} +} + // evalInfixExp evaluate syntax analysis by given infix expression after // lexical analysis. Evaluate an infix expression containing formulas by // stacks: @@ -302,18 +417,18 @@ func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error) }) } // call formula function to evaluate - result, err := callFuncByName(&formulaFuncs{}, strings.NewReplacer( + arg := callFuncByName(&formulaFuncs{}, strings.NewReplacer( "_xlfn", "", ".", "").Replace(opfStack.Peek().(efp.Token).TValue), []reflect.Value{reflect.ValueOf(argsList)}) - if err != nil { - return efp.Token{}, err + if arg.Type == ArgError { + return efp.Token{}, errors.New(arg.Value()) } argsList.Init() opfStack.Pop() if opfStack.Len() > 0 { // still in function stack - opfdStack.Push(efp.Token{TValue: result, TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + opfdStack.Push(efp.Token{TValue: arg.Value(), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) } else { - opdStack.Push(efp.Token{TValue: result, TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + opdStack.Push(efp.Token{TValue: arg.Value(), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) } } } @@ -573,6 +688,7 @@ func isOperatorPrefixToken(token efp.Token) bool { return false } +// getDefinedNameRefTo convert defined name to reference range. func (f *File) getDefinedNameRefTo(definedNameName string, currentSheet string) (refTo string) { for _, definedName := range f.GetDefinedName() { if definedName.Name == definedNameName { @@ -775,22 +891,17 @@ func (f *File) rangeResolver(cellRefs, cellRanges *list.List) (arg formulaArg, e // callFuncByName calls the no error or only error return function with // reflect by given receiver, name and parameters. -func callFuncByName(receiver interface{}, name string, params []reflect.Value) (result string, err error) { +func callFuncByName(receiver interface{}, name string, params []reflect.Value) (arg formulaArg) { function := reflect.ValueOf(receiver).MethodByName(name) if function.IsValid() { rt := function.Call(params) if len(rt) == 0 { return } - if !rt[1].IsNil() { - err = rt[1].Interface().(error) - return - } - result = rt[0].Interface().(string) + arg = rt[0].Interface().(formulaArg) return } - err = fmt.Errorf("not support %s function", name) - return + return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("not support %s function", name)) } // formulaCriteriaParser parse formula criteria. @@ -879,18 +990,15 @@ func formulaCriteriaEval(val string, criteria *formulaCriteria) (result bool, er // // ABS(number) // -func (fn *formulaFuncs) ABS(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) ABS(argsList *list.List) formulaArg { if argsList.Len() != 1 { - err = errors.New("ABS requires 1 numeric argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "ABS requires 1 numeric argument") } - var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + val, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } - result = fmt.Sprintf("%g", math.Abs(val)) - return + return newNumberFormulaArg(math.Abs(val)) } // ACOS function calculates the arccosine (i.e. the inverse cosine) of a given @@ -899,18 +1007,15 @@ func (fn *formulaFuncs) ABS(argsList *list.List) (result string, err error) { // // ACOS(number) // -func (fn *formulaFuncs) ACOS(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) ACOS(argsList *list.List) formulaArg { if argsList.Len() != 1 { - err = errors.New("ACOS requires 1 numeric argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "ACOS requires 1 numeric argument") } - var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + val, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } - result = fmt.Sprintf("%g", math.Acos(val)) - return + return newNumberFormulaArg(math.Acos(val)) } // ACOSH function calculates the inverse hyperbolic cosine of a supplied number. @@ -918,18 +1023,15 @@ func (fn *formulaFuncs) ACOS(argsList *list.List) (result string, err error) { // // ACOSH(number) // -func (fn *formulaFuncs) ACOSH(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) ACOSH(argsList *list.List) formulaArg { if argsList.Len() != 1 { - err = errors.New("ACOSH requires 1 numeric argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "ACOSH requires 1 numeric argument") } - var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + val, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } - result = fmt.Sprintf("%g", math.Acosh(val)) - return + return newNumberFormulaArg(math.Acosh(val)) } // ACOT function calculates the arccotangent (i.e. the inverse cotangent) of a @@ -938,18 +1040,15 @@ func (fn *formulaFuncs) ACOSH(argsList *list.List) (result string, err error) { // // ACOT(number) // -func (fn *formulaFuncs) ACOT(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) ACOT(argsList *list.List) formulaArg { if argsList.Len() != 1 { - err = errors.New("ACOT requires 1 numeric argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "ACOT requires 1 numeric argument") } - var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + val, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } - result = fmt.Sprintf("%g", math.Pi/2-math.Atan(val)) - return + return newNumberFormulaArg(math.Pi/2 - math.Atan(val)) } // ACOTH function calculates the hyperbolic arccotangent (coth) of a supplied @@ -957,18 +1056,15 @@ func (fn *formulaFuncs) ACOT(argsList *list.List) (result string, err error) { // // ACOTH(number) // -func (fn *formulaFuncs) ACOTH(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) ACOTH(argsList *list.List) formulaArg { if argsList.Len() != 1 { - err = errors.New("ACOTH requires 1 numeric argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "ACOTH requires 1 numeric argument") } - var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + val, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } - result = fmt.Sprintf("%g", math.Atanh(1/val)) - return + return newNumberFormulaArg(math.Atanh(1 / val)) } // ARABIC function converts a Roman numeral into an Arabic numeral. The syntax @@ -976,10 +1072,9 @@ func (fn *formulaFuncs) ACOTH(argsList *list.List) (result string, err error) { // // ARABIC(text) // -func (fn *formulaFuncs) ARABIC(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) ARABIC(argsList *list.List) formulaArg { if argsList.Len() != 1 { - err = errors.New("ARABIC requires 1 numeric argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "ARABIC requires 1 numeric argument") } charMap := map[rune]float64{'I': 1, 'V': 5, 'X': 10, 'L': 50, 'C': 100, 'D': 500, 'M': 1000} val, last, prefix := 0.0, 0.0, 1.0 @@ -993,19 +1088,16 @@ func (fn *formulaFuncs) ARABIC(argsList *list.List) (result string, err error) { val += digit switch { case last == digit && (last == 5 || last == 50 || last == 500): - result = formulaErrorVALUE - return + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) case 2*last == digit: - result = formulaErrorVALUE - return + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) } if last < digit { val -= 2 * last } last = digit } - result = fmt.Sprintf("%g", prefix*val) - return + return newNumberFormulaArg(prefix * val) } // ASIN function calculates the arcsine (i.e. the inverse sine) of a given @@ -1014,18 +1106,15 @@ func (fn *formulaFuncs) ARABIC(argsList *list.List) (result string, err error) { // // ASIN(number) // -func (fn *formulaFuncs) ASIN(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) ASIN(argsList *list.List) formulaArg { if argsList.Len() != 1 { - err = errors.New("ASIN requires 1 numeric argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "ASIN requires 1 numeric argument") } - var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + val, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } - result = fmt.Sprintf("%g", math.Asin(val)) - return + return newNumberFormulaArg(math.Asin(val)) } // ASINH function calculates the inverse hyperbolic sine of a supplied number. @@ -1033,18 +1122,15 @@ func (fn *formulaFuncs) ASIN(argsList *list.List) (result string, err error) { // // ASINH(number) // -func (fn *formulaFuncs) ASINH(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) ASINH(argsList *list.List) formulaArg { if argsList.Len() != 1 { - err = errors.New("ASINH requires 1 numeric argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "ASINH requires 1 numeric argument") } - var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + val, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } - result = fmt.Sprintf("%g", math.Asinh(val)) - return + return newNumberFormulaArg(math.Asinh(val)) } // ATAN function calculates the arctangent (i.e. the inverse tangent) of a @@ -1053,18 +1139,15 @@ func (fn *formulaFuncs) ASINH(argsList *list.List) (result string, err error) { // // ATAN(number) // -func (fn *formulaFuncs) ATAN(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) ATAN(argsList *list.List) formulaArg { if argsList.Len() != 1 { - err = errors.New("ATAN requires 1 numeric argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "ATAN requires 1 numeric argument") } - var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + val, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } - result = fmt.Sprintf("%g", math.Atan(val)) - return + return newNumberFormulaArg(math.Atan(val)) } // ATANH function calculates the inverse hyperbolic tangent of a supplied @@ -1072,18 +1155,15 @@ func (fn *formulaFuncs) ATAN(argsList *list.List) (result string, err error) { // // ATANH(number) // -func (fn *formulaFuncs) ATANH(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) ATANH(argsList *list.List) formulaArg { if argsList.Len() != 1 { - err = errors.New("ATANH requires 1 numeric argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "ATANH requires 1 numeric argument") } - var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + val, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } - result = fmt.Sprintf("%g", math.Atanh(val)) - return + return newNumberFormulaArg(math.Atanh(val)) } // ATAN2 function calculates the arctangent (i.e. the inverse tangent) of a @@ -1092,22 +1172,19 @@ func (fn *formulaFuncs) ATANH(argsList *list.List) (result string, err error) { // // ATAN2(x_num,y_num) // -func (fn *formulaFuncs) ATAN2(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) ATAN2(argsList *list.List) formulaArg { if argsList.Len() != 2 { - err = errors.New("ATAN2 requires 2 numeric arguments") - return + return newErrorFormulaArg(formulaErrorVALUE, "ATAN2 requires 2 numeric arguments") } - var x, y float64 - if x, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + x, err := strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } - if y, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + y, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } - result = fmt.Sprintf("%g", math.Atan2(x, y)) - return + return newNumberFormulaArg(math.Atan2(x, y)) } // BASE function converts a number into a supplied base (radix), and returns a @@ -1115,41 +1192,35 @@ func (fn *formulaFuncs) ATAN2(argsList *list.List) (result string, err error) { // // BASE(number,radix,[min_length]) // -func (fn *formulaFuncs) BASE(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) BASE(argsList *list.List) formulaArg { if argsList.Len() < 2 { - err = errors.New("BASE requires at least 2 arguments") - return + return newErrorFormulaArg(formulaErrorVALUE, "BASE requires at least 2 arguments") } if argsList.Len() > 3 { - err = errors.New("BASE allows at most 3 arguments") - return + return newErrorFormulaArg(formulaErrorVALUE, "BASE allows at most 3 arguments") } var number float64 var radix, minLength int + var err error if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } if radix, err = strconv.Atoi(argsList.Front().Next().Value.(formulaArg).String); err != nil { - err = errors.New(formulaErrorVALUE) - return + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } if radix < 2 || radix > 36 { - err = errors.New("radix must be an integer >= 2 and <= 36") - return + 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 { - err = errors.New(formulaErrorVALUE) - return + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } } - result = strconv.FormatInt(int64(number), radix) + result := strconv.FormatInt(int64(number), radix) if len(result) < minLength { result = strings.Repeat("0", minLength-len(result)) + result } - result = strings.ToUpper(result) - return + return newStringFormulaArg(strings.ToUpper(result)) } // CEILING function rounds a supplied number away from zero, to the nearest @@ -1157,43 +1228,38 @@ func (fn *formulaFuncs) BASE(argsList *list.List) (result string, err error) { // // CEILING(number,significance) // -func (fn *formulaFuncs) CEILING(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) CEILING(argsList *list.List) formulaArg { if argsList.Len() == 0 { - err = errors.New("CEILING requires at least 1 argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "CEILING requires at least 1 argument") } if argsList.Len() > 2 { - err = errors.New("CEILING allows at most 2 arguments") - return + return newErrorFormulaArg(formulaErrorVALUE, "CEILING allows at most 2 arguments") } number, significance, res := 0.0, 1.0, 0.0 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + var err error + number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } if number < 0 { significance = -1 } if argsList.Len() > 1 { if significance, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } } if significance < 0 && number > 0 { - err = errors.New("negative sig to CEILING invalid") - return + return newErrorFormulaArg(formulaErrorVALUE, "negative sig to CEILING invalid") } if argsList.Len() == 1 { - result = fmt.Sprintf("%g", math.Ceil(number)) - return + return newNumberFormulaArg(math.Ceil(number)) } number, res = math.Modf(number / significance) if res > 0 { number++ } - result = fmt.Sprintf("%g", number*significance) - return + return newNumberFormulaArg(number * significance) } // CEILINGMATH function rounds a supplied number up to a supplied multiple of @@ -1201,37 +1267,32 @@ func (fn *formulaFuncs) CEILING(argsList *list.List) (result string, err error) // // CEILING.MATH(number,[significance],[mode]) // -func (fn *formulaFuncs) CEILINGMATH(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) CEILINGMATH(argsList *list.List) formulaArg { if argsList.Len() == 0 { - err = errors.New("CEILING.MATH requires at least 1 argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "CEILING.MATH requires at least 1 argument") } if argsList.Len() > 3 { - err = errors.New("CEILING.MATH allows at most 3 arguments") - return + return newErrorFormulaArg(formulaErrorVALUE, "CEILING.MATH allows at most 3 arguments") } number, significance, mode := 0.0, 1.0, 1.0 + var err error if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } if number < 0 { significance = -1 } if argsList.Len() > 1 { if significance, err = strconv.ParseFloat(argsList.Front().Next().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } } if argsList.Len() == 1 { - result = fmt.Sprintf("%g", math.Ceil(number)) - return + return newNumberFormulaArg(math.Ceil(number)) } if argsList.Len() > 2 { if mode, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } } val, res := math.Modf(number / significance) @@ -1242,8 +1303,7 @@ func (fn *formulaFuncs) CEILINGMATH(argsList *list.List) (result string, err err val-- } } - result = fmt.Sprintf("%g", val*significance) - return + return newNumberFormulaArg(val * significance) } // CEILINGPRECISE function rounds a supplied number up (regardless of the @@ -1252,36 +1312,33 @@ func (fn *formulaFuncs) CEILINGMATH(argsList *list.List) (result string, err err // // CEILING.PRECISE(number,[significance]) // -func (fn *formulaFuncs) CEILINGPRECISE(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) CEILINGPRECISE(argsList *list.List) formulaArg { if argsList.Len() == 0 { - err = errors.New("CEILING.PRECISE requires at least 1 argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "CEILING.PRECISE requires at least 1 argument") } if argsList.Len() > 2 { - err = errors.New("CEILING.PRECISE allows at most 2 arguments") - return + return newErrorFormulaArg(formulaErrorVALUE, "CEILING.PRECISE allows at most 2 arguments") } number, significance := 0.0, 1.0 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + var err error + number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } if number < 0 { significance = -1 } if argsList.Len() == 1 { - result = fmt.Sprintf("%g", math.Ceil(number)) - return + return newNumberFormulaArg(math.Ceil(number)) } if argsList.Len() > 1 { if significance, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) - return + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } significance = math.Abs(significance) if significance == 0 { - result = "0" - return + return newStringFormulaArg("0") } } val, res := math.Modf(number / significance) @@ -1290,8 +1347,7 @@ func (fn *formulaFuncs) CEILINGPRECISE(argsList *list.List) (result string, err val++ } } - result = fmt.Sprintf("%g", val*significance) - return + return newNumberFormulaArg(val * significance) } // COMBIN function calculates the number of combinations (in any order) of a @@ -1299,34 +1355,29 @@ func (fn *formulaFuncs) CEILINGPRECISE(argsList *list.List) (result string, err // // COMBIN(number,number_chosen) // -func (fn *formulaFuncs) COMBIN(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) COMBIN(argsList *list.List) formulaArg { if argsList.Len() != 2 { - err = errors.New("COMBIN requires 2 argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "COMBIN requires 2 argument") } number, chosen, val := 0.0, 0.0, 1.0 + var err error if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } if chosen, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } number, chosen = math.Trunc(number), math.Trunc(chosen) if chosen > number { - err = errors.New("COMBIN requires number >= number_chosen") - return + return newErrorFormulaArg(formulaErrorVALUE, "COMBIN requires number >= number_chosen") } if chosen == number || chosen == 0 { - result = "1" - return + return newStringFormulaArg("1") } for c := float64(1); c <= chosen; c++ { val *= (number + 1 - c) / c } - result = fmt.Sprintf("%g", math.Ceil(val)) - return + return newNumberFormulaArg(math.Ceil(val)) } // COMBINA function calculates the number of combinations, with repetitions, @@ -1334,28 +1385,26 @@ func (fn *formulaFuncs) COMBIN(argsList *list.List) (result string, err error) { // // COMBINA(number,number_chosen) // -func (fn *formulaFuncs) COMBINA(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) COMBINA(argsList *list.List) formulaArg { if argsList.Len() != 2 { - err = errors.New("COMBINA requires 2 argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "COMBINA requires 2 argument") } var number, chosen float64 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + var err error + number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } - if chosen, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + chosen, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } number, chosen = math.Trunc(number), math.Trunc(chosen) if number < chosen { - err = errors.New("COMBINA requires number > number_chosen") - return + return newErrorFormulaArg(formulaErrorVALUE, "COMBINA requires number > number_chosen") } if number == 0 { - result = "0" - return + return newStringFormulaArg("0") } args := list.New() args.PushBack(formulaArg{ @@ -1374,18 +1423,15 @@ func (fn *formulaFuncs) COMBINA(argsList *list.List) (result string, err error) // // COS(number) // -func (fn *formulaFuncs) COS(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) COS(argsList *list.List) formulaArg { if argsList.Len() != 1 { - err = errors.New("COS requires 1 numeric argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "COS requires 1 numeric argument") } - var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + val, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } - result = fmt.Sprintf("%g", math.Cos(val)) - return + return newNumberFormulaArg(math.Cos(val)) } // COSH function calculates the hyperbolic cosine (cosh) of a supplied number. @@ -1393,18 +1439,15 @@ func (fn *formulaFuncs) COS(argsList *list.List) (result string, err error) { // // COSH(number) // -func (fn *formulaFuncs) COSH(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) COSH(argsList *list.List) formulaArg { if argsList.Len() != 1 { - err = errors.New("COSH requires 1 numeric argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "COSH requires 1 numeric argument") } - var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + val, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } - result = fmt.Sprintf("%g", math.Cosh(val)) - return + return newNumberFormulaArg(math.Cosh(val)) } // COT function calculates the cotangent of a given angle. The syntax of the @@ -1412,22 +1455,18 @@ func (fn *formulaFuncs) COSH(argsList *list.List) (result string, err error) { // // COT(number) // -func (fn *formulaFuncs) COT(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) COT(argsList *list.List) formulaArg { if argsList.Len() != 1 { - err = errors.New("COT requires 1 numeric argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "COT requires 1 numeric argument") } - var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + val, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } if val == 0 { - err = errors.New(formulaErrorDIV) - return + return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV) } - result = fmt.Sprintf("%g", math.Tan(val)) - return + return newNumberFormulaArg(math.Tan(val)) } // COTH function calculates the hyperbolic cotangent (coth) of a supplied @@ -1435,22 +1474,18 @@ func (fn *formulaFuncs) COT(argsList *list.List) (result string, err error) { // // COTH(number) // -func (fn *formulaFuncs) COTH(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) COTH(argsList *list.List) formulaArg { if argsList.Len() != 1 { - err = errors.New("COTH requires 1 numeric argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "COTH requires 1 numeric argument") } - var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + val, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } if val == 0 { - err = errors.New(formulaErrorDIV) - return + return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV) } - result = fmt.Sprintf("%g", math.Tanh(val)) - return + return newNumberFormulaArg(math.Tanh(val)) } // CSC function calculates the cosecant of a given angle. The syntax of the @@ -1458,22 +1493,18 @@ func (fn *formulaFuncs) COTH(argsList *list.List) (result string, err error) { // // CSC(number) // -func (fn *formulaFuncs) CSC(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) CSC(argsList *list.List) formulaArg { if argsList.Len() != 1 { - err = errors.New("CSC requires 1 numeric argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "CSC requires 1 numeric argument") } - var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + val, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } if val == 0 { - err = errors.New(formulaErrorDIV) - return + return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV) } - result = fmt.Sprintf("%g", 1/math.Sin(val)) - return + return newNumberFormulaArg(1 / math.Sin(val)) } // CSCH function calculates the hyperbolic cosecant (csch) of a supplied @@ -1481,22 +1512,18 @@ func (fn *formulaFuncs) CSC(argsList *list.List) (result string, err error) { // // CSCH(number) // -func (fn *formulaFuncs) CSCH(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) CSCH(argsList *list.List) formulaArg { if argsList.Len() != 1 { - err = errors.New("CSCH requires 1 numeric argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "CSCH requires 1 numeric argument") } - var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + val, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } if val == 0 { - err = errors.New(formulaErrorDIV) - return + return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV) } - result = fmt.Sprintf("%g", 1/math.Sinh(val)) - return + return newNumberFormulaArg(1 / math.Sinh(val)) } // DECIMAL function converts a text representation of a number in a specified @@ -1504,27 +1531,25 @@ func (fn *formulaFuncs) CSCH(argsList *list.List) (result string, err error) { // // DECIMAL(text,radix) // -func (fn *formulaFuncs) DECIMAL(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) DECIMAL(argsList *list.List) formulaArg { if argsList.Len() != 2 { - err = errors.New("DECIMAL requires 2 numeric arguments") - return + return newErrorFormulaArg(formulaErrorVALUE, "DECIMAL requires 2 numeric arguments") } var text = argsList.Front().Value.(formulaArg).String var radix int - if radix, err = strconv.Atoi(argsList.Back().Value.(formulaArg).String); err != nil { - err = errors.New(formulaErrorVALUE) - return + var err error + radix, err = strconv.Atoi(argsList.Back().Value.(formulaArg).String) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } if len(text) > 2 && (strings.HasPrefix(text, "0x") || strings.HasPrefix(text, "0X")) { text = text[2:] } val, err := strconv.ParseInt(text, radix, 64) if err != nil { - err = errors.New(formulaErrorVALUE) - return + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } - result = fmt.Sprintf("%g", float64(val)) - return + return newNumberFormulaArg(float64(val)) } // DEGREES function converts radians into degrees. The syntax of the function @@ -1532,22 +1557,18 @@ func (fn *formulaFuncs) DECIMAL(argsList *list.List) (result string, err error) // // DEGREES(angle) // -func (fn *formulaFuncs) DEGREES(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) DEGREES(argsList *list.List) formulaArg { if argsList.Len() != 1 { - err = errors.New("DEGREES requires 1 numeric argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "DEGREES requires 1 numeric argument") } - var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + val, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } if val == 0 { - err = errors.New(formulaErrorDIV) - return + return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV) } - result = fmt.Sprintf("%g", 180.0/math.Pi*val) - return + return newNumberFormulaArg(180.0 / math.Pi * val) } // EVEN function rounds a supplied number away from zero (i.e. rounds a @@ -1556,15 +1577,13 @@ func (fn *formulaFuncs) DEGREES(argsList *list.List) (result string, err error) // // EVEN(number) // -func (fn *formulaFuncs) EVEN(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) EVEN(argsList *list.List) formulaArg { if argsList.Len() != 1 { - err = errors.New("EVEN requires 1 numeric argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "EVEN requires 1 numeric argument") } - var number float64 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + number, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } sign := math.Signbit(number) m, frac := math.Modf(number / 2) @@ -1576,8 +1595,7 @@ func (fn *formulaFuncs) EVEN(argsList *list.List) (result string, err error) { val -= 2 } } - result = fmt.Sprintf("%g", val) - return + return newNumberFormulaArg(val) } // EXP function calculates the value of the mathematical constant e, raised to @@ -1585,18 +1603,15 @@ func (fn *formulaFuncs) EVEN(argsList *list.List) (result string, err error) { // // EXP(number) // -func (fn *formulaFuncs) EXP(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) EXP(argsList *list.List) formulaArg { if argsList.Len() != 1 { - err = errors.New("EXP requires 1 numeric argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "EXP requires 1 numeric argument") } - var number float64 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + number, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } - result = strings.ToUpper(fmt.Sprintf("%g", math.Exp(number))) - return + return newStringFormulaArg(strings.ToUpper(fmt.Sprintf("%g", math.Exp(number)))) } // fact returns the factorial of a supplied number. @@ -1613,21 +1628,18 @@ func fact(number float64) float64 { // // FACT(number) // -func (fn *formulaFuncs) FACT(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) FACT(argsList *list.List) formulaArg { if argsList.Len() != 1 { - err = errors.New("FACT requires 1 numeric argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "FACT requires 1 numeric argument") } - var number float64 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + number, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } if number < 0 { - err = errors.New(formulaErrorNUM) + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) } - result = strings.ToUpper(fmt.Sprintf("%g", fact(number))) - return + return newStringFormulaArg(strings.ToUpper(fmt.Sprintf("%g", fact(number)))) } // FACTDOUBLE function returns the double factorial of a supplied number. The @@ -1635,25 +1647,22 @@ func (fn *formulaFuncs) FACT(argsList *list.List) (result string, err error) { // // FACTDOUBLE(number) // -func (fn *formulaFuncs) FACTDOUBLE(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) FACTDOUBLE(argsList *list.List) formulaArg { if argsList.Len() != 1 { - err = errors.New("FACTDOUBLE requires 1 numeric argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "FACTDOUBLE requires 1 numeric argument") } - number, val := 0.0, 1.0 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + val := 1.0 + number, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } if number < 0 { - err = errors.New(formulaErrorNUM) - return + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) } for i := math.Trunc(number); i > 1; i -= 2 { val *= i } - result = strings.ToUpper(fmt.Sprintf("%g", val)) - return + return newStringFormulaArg(strings.ToUpper(fmt.Sprintf("%g", val))) } // FLOOR function rounds a supplied number towards zero to the nearest @@ -1661,23 +1670,22 @@ func (fn *formulaFuncs) FACTDOUBLE(argsList *list.List) (result string, err erro // // FLOOR(number,significance) // -func (fn *formulaFuncs) FLOOR(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) FLOOR(argsList *list.List) formulaArg { if argsList.Len() != 2 { - err = errors.New("FLOOR requires 2 numeric arguments") - return + return newErrorFormulaArg(formulaErrorVALUE, "FLOOR requires 2 numeric arguments") } var number, significance float64 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + var err error + number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } - if significance, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + significance, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } if significance < 0 && number >= 0 { - err = errors.New(formulaErrorNUM) - return + return newErrorFormulaArg(formulaErrorNUM, "invalid arguments to FLOOR") } val := number val, res := math.Modf(val / significance) @@ -1686,8 +1694,7 @@ func (fn *formulaFuncs) FLOOR(argsList *list.List) (result string, err error) { val-- } } - result = strings.ToUpper(fmt.Sprintf("%g", val*significance)) - return + return newStringFormulaArg(strings.ToUpper(fmt.Sprintf("%g", val*significance))) } // FLOORMATH function rounds a supplied number down to a supplied multiple of @@ -1695,45 +1702,40 @@ func (fn *formulaFuncs) FLOOR(argsList *list.List) (result string, err error) { // // FLOOR.MATH(number,[significance],[mode]) // -func (fn *formulaFuncs) FLOORMATH(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) FLOORMATH(argsList *list.List) formulaArg { if argsList.Len() == 0 { - err = errors.New("FLOOR.MATH requires at least 1 argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "FLOOR.MATH requires at least 1 argument") } if argsList.Len() > 3 { - err = errors.New("FLOOR.MATH allows at most 3 arguments") - return + return newErrorFormulaArg(formulaErrorVALUE, "FLOOR.MATH allows at most 3 arguments") } number, significance, mode := 0.0, 1.0, 1.0 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + var err error + number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } if number < 0 { significance = -1 } if argsList.Len() > 1 { if significance, err = strconv.ParseFloat(argsList.Front().Next().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } } if argsList.Len() == 1 { - result = fmt.Sprintf("%g", math.Floor(number)) - return + return newNumberFormulaArg(math.Floor(number)) } if argsList.Len() > 2 { if mode, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } } val, res := math.Modf(number / significance) if res != 0 && number < 0 && mode > 0 { val-- } - result = fmt.Sprintf("%g", val*significance) - return + return newNumberFormulaArg(val * significance) } // FLOORPRECISE function rounds a supplied number down to a supplied multiple @@ -1741,36 +1743,32 @@ func (fn *formulaFuncs) FLOORMATH(argsList *list.List) (result string, err error // // FLOOR.PRECISE(number,[significance]) // -func (fn *formulaFuncs) FLOORPRECISE(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) FLOORPRECISE(argsList *list.List) formulaArg { if argsList.Len() == 0 { - err = errors.New("FLOOR.PRECISE requires at least 1 argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "FLOOR.PRECISE requires at least 1 argument") } if argsList.Len() > 2 { - err = errors.New("FLOOR.PRECISE allows at most 2 arguments") - return + return newErrorFormulaArg(formulaErrorVALUE, "FLOOR.PRECISE allows at most 2 arguments") } var number, significance float64 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + var err error + number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } if number < 0 { significance = -1 } if argsList.Len() == 1 { - result = fmt.Sprintf("%g", math.Floor(number)) - return + return newNumberFormulaArg(math.Floor(number)) } if argsList.Len() > 1 { if significance, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } significance = math.Abs(significance) if significance == 0 { - result = "0" - return + return newStringFormulaArg("0") } } val, res := math.Modf(number / significance) @@ -1779,8 +1777,7 @@ func (fn *formulaFuncs) FLOORPRECISE(argsList *list.List) (result string, err er val-- } } - result = fmt.Sprintf("%g", val*significance) - return + return newNumberFormulaArg(val * significance) } // gcd returns the greatest common divisor of two supplied integers. @@ -1807,14 +1804,14 @@ func gcd(x, y float64) float64 { // // GCD(number1,[number2],...) // -func (fn *formulaFuncs) GCD(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) GCD(argsList *list.List) formulaArg { if argsList.Len() == 0 { - err = errors.New("GCD requires at least 1 argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "GCD requires at least 1 argument") } var ( val float64 nums = []float64{} + err error ) for arg := argsList.Front(); arg != nil; arg = arg.Next() { token := arg.Value.(formulaArg).String @@ -1822,29 +1819,24 @@ func (fn *formulaFuncs) GCD(argsList *list.List) (result string, err error) { continue } if val, err = strconv.ParseFloat(token, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } nums = append(nums, val) } if nums[0] < 0 { - err = errors.New("GCD only accepts positive arguments") - return + return newErrorFormulaArg(formulaErrorVALUE, "GCD only accepts positive arguments") } if len(nums) == 1 { - result = fmt.Sprintf("%g", nums[0]) - return + return newNumberFormulaArg(nums[0]) } cd := nums[0] for i := 1; i < len(nums); i++ { if nums[i] < 0 { - err = errors.New("GCD only accepts positive arguments") - return + return newErrorFormulaArg(formulaErrorVALUE, "GCD only accepts positive arguments") } cd = gcd(cd, nums[i]) } - result = fmt.Sprintf("%g", cd) - return + return newNumberFormulaArg(cd) } // INT function truncates a supplied number down to the closest integer. The @@ -1852,22 +1844,19 @@ func (fn *formulaFuncs) GCD(argsList *list.List) (result string, err error) { // // INT(number) // -func (fn *formulaFuncs) INT(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) INT(argsList *list.List) formulaArg { if argsList.Len() != 1 { - err = errors.New("INT requires 1 numeric argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "INT requires 1 numeric argument") } - var number float64 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + number, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } val, frac := math.Modf(number) if frac < 0 { val-- } - result = fmt.Sprintf("%g", val) - return + return newNumberFormulaArg(val) } // ISOCEILING function rounds a supplied number up (regardless of the number's @@ -1876,36 +1865,31 @@ func (fn *formulaFuncs) INT(argsList *list.List) (result string, err error) { // // ISO.CEILING(number,[significance]) // -func (fn *formulaFuncs) ISOCEILING(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) ISOCEILING(argsList *list.List) formulaArg { if argsList.Len() == 0 { - err = errors.New("ISO.CEILING requires at least 1 argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "ISO.CEILING requires at least 1 argument") } if argsList.Len() > 2 { - err = errors.New("ISO.CEILING allows at most 2 arguments") - return + return newErrorFormulaArg(formulaErrorVALUE, "ISO.CEILING allows at most 2 arguments") } var number, significance float64 + var err error if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } if number < 0 { significance = -1 } if argsList.Len() == 1 { - result = fmt.Sprintf("%g", math.Ceil(number)) - return + return newNumberFormulaArg(math.Ceil(number)) } if argsList.Len() > 1 { if significance, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } significance = math.Abs(significance) if significance == 0 { - result = "0" - return + return newStringFormulaArg("0") } } val, res := math.Modf(number / significance) @@ -1914,8 +1898,7 @@ func (fn *formulaFuncs) ISOCEILING(argsList *list.List) (result string, err erro val++ } } - result = fmt.Sprintf("%g", val*significance) - return + return newNumberFormulaArg(val * significance) } // lcm returns the least common multiple of two supplied integers. @@ -1933,14 +1916,14 @@ func lcm(a, b float64) float64 { // // LCM(number1,[number2],...) // -func (fn *formulaFuncs) LCM(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) LCM(argsList *list.List) formulaArg { if argsList.Len() == 0 { - err = errors.New("LCM requires at least 1 argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "LCM requires at least 1 argument") } var ( val float64 nums = []float64{} + err error ) for arg := argsList.Front(); arg != nil; arg = arg.Next() { token := arg.Value.(formulaArg).String @@ -1948,29 +1931,24 @@ func (fn *formulaFuncs) LCM(argsList *list.List) (result string, err error) { continue } if val, err = strconv.ParseFloat(token, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } nums = append(nums, val) } if nums[0] < 0 { - err = errors.New("LCM only accepts positive arguments") - return + return newErrorFormulaArg(formulaErrorVALUE, "LCM only accepts positive arguments") } if len(nums) == 1 { - result = fmt.Sprintf("%g", nums[0]) - return + return newNumberFormulaArg(nums[0]) } cm := nums[0] for i := 1; i < len(nums); i++ { if nums[i] < 0 { - err = errors.New("LCM only accepts positive arguments") - return + return newErrorFormulaArg(formulaErrorVALUE, "LCM only accepts positive arguments") } cm = lcm(cm, nums[i]) } - result = fmt.Sprintf("%g", cm) - return + return newNumberFormulaArg(cm) } // LN function calculates the natural logarithm of a given number. The syntax @@ -1978,18 +1956,15 @@ func (fn *formulaFuncs) LCM(argsList *list.List) (result string, err error) { // // LN(number) // -func (fn *formulaFuncs) LN(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) LN(argsList *list.List) formulaArg { if argsList.Len() != 1 { - err = errors.New("LN requires 1 numeric argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "LN requires 1 numeric argument") } - var number float64 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + number, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } - result = fmt.Sprintf("%g", math.Log(number)) - return + return newNumberFormulaArg(math.Log(number)) } // LOG function calculates the logarithm of a given number, to a supplied @@ -1997,40 +1972,34 @@ func (fn *formulaFuncs) LN(argsList *list.List) (result string, err error) { // // LOG(number,[base]) // -func (fn *formulaFuncs) LOG(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) LOG(argsList *list.List) formulaArg { if argsList.Len() == 0 { - err = errors.New("LOG requires at least 1 argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "LOG requires at least 1 argument") } if argsList.Len() > 2 { - err = errors.New("LOG allows at most 2 arguments") - return + return newErrorFormulaArg(formulaErrorVALUE, "LOG allows at most 2 arguments") } number, base := 0.0, 10.0 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + var err error + number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } if argsList.Len() > 1 { if base, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } } if number == 0 { - err = errors.New(formulaErrorNUM) - return + return newErrorFormulaArg(formulaErrorNUM, formulaErrorDIV) } if base == 0 { - err = errors.New(formulaErrorNUM) - return + return newErrorFormulaArg(formulaErrorNUM, formulaErrorDIV) } if base == 1 { - err = errors.New(formulaErrorDIV) - return + return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV) } - result = fmt.Sprintf("%g", math.Log(number)/math.Log(base)) - return + return newNumberFormulaArg(math.Log(number) / math.Log(base)) } // LOG10 function calculates the base 10 logarithm of a given number. The @@ -2038,20 +2007,19 @@ func (fn *formulaFuncs) LOG(argsList *list.List) (result string, err error) { // // LOG10(number) // -func (fn *formulaFuncs) LOG10(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) LOG10(argsList *list.List) formulaArg { if argsList.Len() != 1 { - err = errors.New("LOG10 requires 1 numeric argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "LOG10 requires 1 numeric argument") } - var number float64 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + number, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } - result = fmt.Sprintf("%g", math.Log10(number)) - return + return newNumberFormulaArg(math.Log10(number)) } +// 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{} for i := range sqMtx { @@ -2092,30 +2060,31 @@ func det(sqMtx [][]float64) float64 { // // MDETERM(array) // -func (fn *formulaFuncs) MDETERM(argsList *list.List) (result string, err error) { - var num float64 - var numMtx = [][]float64{} - var strMtx = argsList.Front().Value.(formulaArg).Matrix +func (fn *formulaFuncs) MDETERM(argsList *list.List) (result formulaArg) { + var ( + num float64 + numMtx = [][]float64{} + err error + strMtx = argsList.Front().Value.(formulaArg).Matrix + ) if argsList.Len() < 1 { return } var rows = len(strMtx) for _, row := range argsList.Front().Value.(formulaArg).Matrix { if len(row) != rows { - err = errors.New(formulaErrorVALUE) - return + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) } numRow := []float64{} for _, ele := range row { if num, err = strconv.ParseFloat(ele.String, 64); err != nil { - return + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } numRow = append(numRow, num) } numMtx = append(numMtx, numRow) } - result = fmt.Sprintf("%g", det(numMtx)) - return + return newNumberFormulaArg(det(numMtx)) } // MOD function returns the remainder of a division between two supplied @@ -2123,30 +2092,28 @@ func (fn *formulaFuncs) MDETERM(argsList *list.List) (result string, err error) // // MOD(number,divisor) // -func (fn *formulaFuncs) MOD(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) MOD(argsList *list.List) formulaArg { if argsList.Len() != 2 { - err = errors.New("MOD requires 2 numeric arguments") - return + return newErrorFormulaArg(formulaErrorVALUE, "MOD requires 2 numeric arguments") } var number, divisor float64 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + var err error + number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } - if divisor, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + divisor, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } if divisor == 0 { - err = errors.New(formulaErrorDIV) - return + return newErrorFormulaArg(formulaErrorDIV, "MOD divide by zero") } trunc, rem := math.Modf(number / divisor) if rem < 0 { trunc-- } - result = fmt.Sprintf("%g", number-divisor*trunc) - return + return newNumberFormulaArg(number - divisor*trunc) } // MROUND function rounds a supplied number up or down to the nearest multiple @@ -2154,35 +2121,32 @@ func (fn *formulaFuncs) MOD(argsList *list.List) (result string, err error) { // // MROUND(number,multiple) // -func (fn *formulaFuncs) MROUND(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) MROUND(argsList *list.List) formulaArg { if argsList.Len() != 2 { - err = errors.New("MROUND requires 2 numeric arguments") - return + return newErrorFormulaArg(formulaErrorVALUE, "MROUND requires 2 numeric arguments") } var number, multiple float64 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + var err error + number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } - if multiple, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + multiple, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } if multiple == 0 { - err = errors.New(formulaErrorNUM) - return + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) } if multiple < 0 && number > 0 || multiple > 0 && number < 0 { - err = errors.New(formulaErrorNUM) - return + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) } number, res := math.Modf(number / multiple) if math.Trunc(res+0.5) > 0 { number++ } - result = fmt.Sprintf("%g", number*multiple) - return + return newNumberFormulaArg(number * multiple) } // MULTINOMIAL function calculates the ratio of the factorial of a sum of @@ -2191,22 +2155,21 @@ func (fn *formulaFuncs) MROUND(argsList *list.List) (result string, err error) { // // MULTINOMIAL(number1,[number2],...) // -func (fn *formulaFuncs) MULTINOMIAL(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) MULTINOMIAL(argsList *list.List) formulaArg { val, num, denom := 0.0, 0.0, 1.0 + var err error for arg := argsList.Front(); arg != nil; arg = arg.Next() { token := arg.Value.(formulaArg) if token.String == "" { continue } if val, err = strconv.ParseFloat(token.String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } num += val denom *= fact(val) } - result = fmt.Sprintf("%g", fact(num)/denom) - return + return newNumberFormulaArg(fact(num) / denom) } // MUNIT function returns the unit matrix for a specified dimension. The @@ -2214,15 +2177,13 @@ func (fn *formulaFuncs) MULTINOMIAL(argsList *list.List) (result string, err err // // MUNIT(dimension) // -func (fn *formulaFuncs) MUNIT(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) MUNIT(argsList *list.List) (result formulaArg) { if argsList.Len() != 1 { - err = errors.New("MUNIT requires 1 numeric argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "MUNIT requires 1 numeric argument") } - var dimension int - if dimension, err = strconv.Atoi(argsList.Front().Value.(formulaArg).String); err != nil { - err = errors.New(formulaErrorVALUE) - return + dimension, err := strconv.Atoi(argsList.Front().Value.(formulaArg).String) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } matrix := make([][]float64, 0, dimension) for i := 0; i < dimension; i++ { @@ -2245,19 +2206,16 @@ func (fn *formulaFuncs) MUNIT(argsList *list.List) (result string, err error) { // // ODD(number) // -func (fn *formulaFuncs) ODD(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) ODD(argsList *list.List) formulaArg { if argsList.Len() != 1 { - err = errors.New("ODD requires 1 numeric argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "ODD requires 1 numeric argument") } - var number float64 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + number, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } if number == 0 { - result = "1" - return + return newStringFormulaArg("1") } sign := math.Signbit(number) m, frac := math.Modf((number - 1) / 2) @@ -2269,8 +2227,7 @@ func (fn *formulaFuncs) ODD(argsList *list.List) (result string, err error) { val -= 2 } } - result = fmt.Sprintf("%g", val) - return + return newNumberFormulaArg(val) } // PI function returns the value of the mathematical constant π (pi), accurate @@ -2278,13 +2235,11 @@ func (fn *formulaFuncs) ODD(argsList *list.List) (result string, err error) { // // PI() // -func (fn *formulaFuncs) PI(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) PI(argsList *list.List) formulaArg { if argsList.Len() != 0 { - err = errors.New("PI accepts no arguments") - return + return newErrorFormulaArg(formulaErrorVALUE, "PI accepts no arguments") } - result = fmt.Sprintf("%g", math.Pi) - return + return newNumberFormulaArg(math.Pi) } // POWER function calculates a given number, raised to a supplied power. @@ -2292,30 +2247,27 @@ func (fn *formulaFuncs) PI(argsList *list.List) (result string, err error) { // // POWER(number,power) // -func (fn *formulaFuncs) POWER(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) POWER(argsList *list.List) formulaArg { if argsList.Len() != 2 { - err = errors.New("POWER requires 2 numeric arguments") - return + return newErrorFormulaArg(formulaErrorVALUE, "POWER requires 2 numeric arguments") } var x, y float64 - if x, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + var err error + x, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } - if y, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + y, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } if x == 0 && y == 0 { - err = errors.New(formulaErrorNUM) - return + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) } if x == 0 && y < 0 { - err = errors.New(formulaErrorDIV) - return + return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV) } - result = fmt.Sprintf("%g", math.Pow(x, y)) - return + return newNumberFormulaArg(math.Pow(x, y)) } // PRODUCT function returns the product (multiplication) of a supplied set of @@ -2323,8 +2275,9 @@ func (fn *formulaFuncs) POWER(argsList *list.List) (result string, err error) { // // PRODUCT(number1,[number2],...) // -func (fn *formulaFuncs) PRODUCT(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) PRODUCT(argsList *list.List) formulaArg { val, product := 0.0, 1.0 + var err error for arg := argsList.Front(); arg != nil; arg = arg.Next() { token := arg.Value.(formulaArg) switch token.Type { @@ -2335,8 +2288,7 @@ func (fn *formulaFuncs) PRODUCT(argsList *list.List) (result string, err error) continue } if val, err = strconv.ParseFloat(token.String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } product = product * val case ArgMatrix: @@ -2346,16 +2298,14 @@ func (fn *formulaFuncs) PRODUCT(argsList *list.List) (result string, err error) continue } if val, err = strconv.ParseFloat(value.String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } product = product * val } } } } - result = fmt.Sprintf("%g", product) - return + return newNumberFormulaArg(product) } // QUOTIENT function returns the integer portion of a division between two @@ -2363,44 +2313,39 @@ func (fn *formulaFuncs) PRODUCT(argsList *list.List) (result string, err error) // // QUOTIENT(numerator,denominator) // -func (fn *formulaFuncs) QUOTIENT(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) QUOTIENT(argsList *list.List) formulaArg { if argsList.Len() != 2 { - err = errors.New("QUOTIENT requires 2 numeric arguments") - return + return newErrorFormulaArg(formulaErrorVALUE, "QUOTIENT requires 2 numeric arguments") } var x, y float64 - if x, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + var err error + x, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } - if y, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + y, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } if y == 0 { - err = errors.New(formulaErrorDIV) - return + return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV) } - result = fmt.Sprintf("%g", math.Trunc(x/y)) - return + return newNumberFormulaArg(math.Trunc(x / y)) } // RADIANS function converts radians into degrees. The syntax of the function is: // // RADIANS(angle) // -func (fn *formulaFuncs) RADIANS(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) RADIANS(argsList *list.List) formulaArg { if argsList.Len() != 1 { - err = errors.New("RADIANS requires 1 numeric argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "RADIANS requires 1 numeric argument") } - var angle float64 - if angle, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + angle, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } - result = fmt.Sprintf("%g", math.Pi/180.0*angle) - return + return newNumberFormulaArg(math.Pi / 180.0 * angle) } // RAND function generates a random real number between 0 and 1. The syntax of @@ -2408,13 +2353,11 @@ func (fn *formulaFuncs) RADIANS(argsList *list.List) (result string, err error) // // RAND() // -func (fn *formulaFuncs) RAND(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) RAND(argsList *list.List) formulaArg { if argsList.Len() != 0 { - err = errors.New("RAND accepts no arguments") - return + return newErrorFormulaArg(formulaErrorVALUE, "RAND accepts no arguments") } - result = fmt.Sprintf("%g", rand.New(rand.NewSource(time.Now().UnixNano())).Float64()) - return + return newNumberFormulaArg(rand.New(rand.NewSource(time.Now().UnixNano())).Float64()) } // RANDBETWEEN function generates a random integer between two supplied @@ -2422,26 +2365,24 @@ func (fn *formulaFuncs) RAND(argsList *list.List) (result string, err error) { // // RANDBETWEEN(bottom,top) // -func (fn *formulaFuncs) RANDBETWEEN(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) RANDBETWEEN(argsList *list.List) formulaArg { if argsList.Len() != 2 { - err = errors.New("RANDBETWEEN requires 2 numeric arguments") - return + return newErrorFormulaArg(formulaErrorVALUE, "RANDBETWEEN requires 2 numeric arguments") } var bottom, top int64 - if bottom, err = strconv.ParseInt(argsList.Front().Value.(formulaArg).String, 10, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + var err error + bottom, err = strconv.ParseInt(argsList.Front().Value.(formulaArg).String, 10, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } - if top, err = strconv.ParseInt(argsList.Back().Value.(formulaArg).String, 10, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + top, err = strconv.ParseInt(argsList.Back().Value.(formulaArg).String, 10, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } if top < bottom { - err = errors.New(formulaErrorNUM) - return + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) } - result = fmt.Sprintf("%g", float64(rand.New(rand.NewSource(time.Now().UnixNano())).Int63n(top-bottom+1)+bottom)) - return + return newNumberFormulaArg(float64(rand.New(rand.NewSource(time.Now().UnixNano())).Int63n(top-bottom+1) + bottom)) } // romanNumerals defined a numeral system that originated in ancient Rome and @@ -2464,25 +2405,23 @@ var romanTable = [][]romanNumerals{{{1000, "M"}, {900, "CM"}, {500, "D"}, {400, // // ROMAN(number,[form]) // -func (fn *formulaFuncs) ROMAN(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) ROMAN(argsList *list.List) formulaArg { if argsList.Len() == 0 { - err = errors.New("ROMAN requires at least 1 argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "ROMAN requires at least 1 argument") } if argsList.Len() > 2 { - err = errors.New("ROMAN allows at most 2 arguments") - return + return newErrorFormulaArg(formulaErrorVALUE, "ROMAN allows at most 2 arguments") } var number float64 var form int - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + var err error + number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } if argsList.Len() > 1 { if form, err = strconv.Atoi(argsList.Back().Value.(formulaArg).String); err != nil { - err = errors.New(formulaErrorVALUE) - return + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } if form < 0 { form = 0 @@ -2509,8 +2448,7 @@ func (fn *formulaFuncs) ROMAN(argsList *list.List) (result string, err error) { val -= r.n } } - result = buf.String() - return + return newStringFormulaArg(buf.String()) } type roundMode byte @@ -2554,22 +2492,21 @@ func (fn *formulaFuncs) round(number, digits float64, mode roundMode) float64 { // // ROUND(number,num_digits) // -func (fn *formulaFuncs) ROUND(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) ROUND(argsList *list.List) formulaArg { if argsList.Len() != 2 { - err = errors.New("ROUND requires 2 numeric arguments") - return + return newErrorFormulaArg(formulaErrorVALUE, "ROUND requires 2 numeric arguments") } var number, digits float64 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + var err error + number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } - if digits, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + digits, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } - result = fmt.Sprintf("%g", fn.round(number, digits, closest)) - return + return newNumberFormulaArg(fn.round(number, digits, closest)) } // ROUNDDOWN function rounds a supplied number down towards zero, to a @@ -2577,22 +2514,21 @@ func (fn *formulaFuncs) ROUND(argsList *list.List) (result string, err error) { // // ROUNDDOWN(number,num_digits) // -func (fn *formulaFuncs) ROUNDDOWN(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) ROUNDDOWN(argsList *list.List) formulaArg { if argsList.Len() != 2 { - err = errors.New("ROUNDDOWN requires 2 numeric arguments") - return + return newErrorFormulaArg(formulaErrorVALUE, "ROUNDDOWN requires 2 numeric arguments") } var number, digits float64 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + var err error + number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } - if digits, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + digits, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } - result = fmt.Sprintf("%g", fn.round(number, digits, down)) - return + return newNumberFormulaArg(fn.round(number, digits, down)) } // ROUNDUP function rounds a supplied number up, away from zero, to a @@ -2600,22 +2536,21 @@ func (fn *formulaFuncs) ROUNDDOWN(argsList *list.List) (result string, err error // // ROUNDUP(number,num_digits) // -func (fn *formulaFuncs) ROUNDUP(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) ROUNDUP(argsList *list.List) formulaArg { if argsList.Len() != 2 { - err = errors.New("ROUNDUP requires 2 numeric arguments") - return + return newErrorFormulaArg(formulaErrorVALUE, "ROUNDUP requires 2 numeric arguments") } var number, digits float64 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + var err error + number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } - if digits, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + digits, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } - result = fmt.Sprintf("%g", fn.round(number, digits, up)) - return + return newNumberFormulaArg(fn.round(number, digits, up)) } // SEC function calculates the secant of a given angle. The syntax of the @@ -2623,18 +2558,15 @@ func (fn *formulaFuncs) ROUNDUP(argsList *list.List) (result string, err error) // // SEC(number) // -func (fn *formulaFuncs) SEC(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) SEC(argsList *list.List) formulaArg { if argsList.Len() != 1 { - err = errors.New("SEC requires 1 numeric argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "SEC requires 1 numeric argument") } - var number float64 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + number, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } - result = fmt.Sprintf("%g", math.Cos(number)) - return + return newNumberFormulaArg(math.Cos(number)) } // SECH function calculates the hyperbolic secant (sech) of a supplied angle. @@ -2642,18 +2574,15 @@ func (fn *formulaFuncs) SEC(argsList *list.List) (result string, err error) { // // SECH(number) // -func (fn *formulaFuncs) SECH(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) SECH(argsList *list.List) formulaArg { if argsList.Len() != 1 { - err = errors.New("SECH requires 1 numeric argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "SECH requires 1 numeric argument") } - var number float64 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + number, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } - result = fmt.Sprintf("%g", 1/math.Cosh(number)) - return + return newNumberFormulaArg(1 / math.Cosh(number)) } // SIGN function returns the arithmetic sign (+1, -1 or 0) of a supplied @@ -2663,26 +2592,21 @@ func (fn *formulaFuncs) SECH(argsList *list.List) (result string, err error) { // // SIGN(number) // -func (fn *formulaFuncs) SIGN(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) SIGN(argsList *list.List) formulaArg { if argsList.Len() != 1 { - err = errors.New("SIGN requires 1 numeric argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "SIGN requires 1 numeric argument") } - var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + val, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } if val < 0 { - result = "-1" - return + return newStringFormulaArg("-1") } if val > 0 { - result = "1" - return + return newStringFormulaArg("1") } - result = "0" - return + return newStringFormulaArg("0") } // SIN function calculates the sine of a given angle. The syntax of the @@ -2690,18 +2614,15 @@ func (fn *formulaFuncs) SIGN(argsList *list.List) (result string, err error) { // // SIN(number) // -func (fn *formulaFuncs) SIN(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) SIN(argsList *list.List) formulaArg { if argsList.Len() != 1 { - err = errors.New("SIN requires 1 numeric argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "SIN requires 1 numeric argument") } - var number float64 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + number, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } - result = fmt.Sprintf("%g", math.Sin(number)) - return + return newNumberFormulaArg(math.Sin(number)) } // SINH function calculates the hyperbolic sine (sinh) of a supplied number. @@ -2709,18 +2630,15 @@ func (fn *formulaFuncs) SIN(argsList *list.List) (result string, err error) { // // SINH(number) // -func (fn *formulaFuncs) SINH(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) SINH(argsList *list.List) formulaArg { if argsList.Len() != 1 { - err = errors.New("SINH requires 1 numeric argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "SINH requires 1 numeric argument") } - var number float64 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + number, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } - result = fmt.Sprintf("%g", math.Sinh(number)) - return + return newNumberFormulaArg(math.Sinh(number)) } // SQRT function calculates the positive square root of a supplied number. The @@ -2728,27 +2646,23 @@ func (fn *formulaFuncs) SINH(argsList *list.List) (result string, err error) { // // SQRT(number) // -func (fn *formulaFuncs) SQRT(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) SQRT(argsList *list.List) formulaArg { if argsList.Len() != 1 { - err = errors.New("SQRT requires 1 numeric argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "SQRT requires 1 numeric argument") } var res float64 var value = argsList.Front().Value.(formulaArg).String if value == "" { - result = "0" - return + return newStringFormulaArg("0") } - if res, err = strconv.ParseFloat(value, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + res, err := strconv.ParseFloat(value, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } if res < 0 { - err = errors.New(formulaErrorNUM) - return + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) } - result = fmt.Sprintf("%g", math.Sqrt(res)) - return + return newNumberFormulaArg(math.Sqrt(res)) } // SQRTPI function returns the square root of a supplied number multiplied by @@ -2756,18 +2670,15 @@ func (fn *formulaFuncs) SQRT(argsList *list.List) (result string, err error) { // // SQRTPI(number) // -func (fn *formulaFuncs) SQRTPI(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) SQRTPI(argsList *list.List) formulaArg { if argsList.Len() != 1 { - err = errors.New("SQRTPI requires 1 numeric argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "SQRTPI requires 1 numeric argument") } - var number float64 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + number, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } - result = fmt.Sprintf("%g", math.Sqrt(number*math.Pi)) - return + return newNumberFormulaArg(math.Sqrt(number * math.Pi)) } // SUM function adds together a supplied set of numbers and returns the sum of @@ -2775,8 +2686,11 @@ func (fn *formulaFuncs) SQRTPI(argsList *list.List) (result string, err error) { // // SUM(number1,[number2],...) // -func (fn *formulaFuncs) SUM(argsList *list.List) (result string, err error) { - var val, sum float64 +func (fn *formulaFuncs) SUM(argsList *list.List) formulaArg { + var ( + val, sum float64 + err error + ) for arg := argsList.Front(); arg != nil; arg = arg.Next() { token := arg.Value.(formulaArg) switch token.Type { @@ -2787,8 +2701,7 @@ func (fn *formulaFuncs) SUM(argsList *list.List) (result string, err error) { continue } if val, err = strconv.ParseFloat(token.String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } sum += val case ArgMatrix: @@ -2798,16 +2711,14 @@ func (fn *formulaFuncs) SUM(argsList *list.List) (result string, err error) { continue } if val, err = strconv.ParseFloat(value.String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } sum += val } } } } - result = fmt.Sprintf("%g", sum) - return + return newNumberFormulaArg(sum) } // SUMIF function finds the values in a supplied array, that satisfy a given @@ -2816,10 +2727,9 @@ func (fn *formulaFuncs) SUM(argsList *list.List) (result string, err error) { // // SUMIF(range,criteria,[sum_range]) // -func (fn *formulaFuncs) SUMIF(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) SUMIF(argsList *list.List) formulaArg { if argsList.Len() < 2 { - err = errors.New("SUMIF requires at least 2 argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "SUMIF requires at least 2 argument") } var criteria = formulaCriteriaParser(argsList.Front().Next().Value.(formulaArg).String) var rangeMtx = argsList.Front().Value.(formulaArg).Matrix @@ -2828,6 +2738,7 @@ func (fn *formulaFuncs) SUMIF(argsList *list.List) (result string, err error) { sumRange = argsList.Back().Value.(formulaArg).Matrix } var sum, val float64 + var err error for rowIdx, row := range rangeMtx { for colIdx, col := range row { var ok bool @@ -2836,7 +2747,7 @@ func (fn *formulaFuncs) SUMIF(argsList *list.List) (result string, err error) { continue } if ok, err = formulaCriteriaEval(fromVal, criteria); err != nil { - return + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } if ok { if argsList.Len() == 3 { @@ -2846,15 +2757,13 @@ func (fn *formulaFuncs) SUMIF(argsList *list.List) (result string, err error) { fromVal = sumRange[rowIdx][colIdx].String } if val, err = strconv.ParseFloat(fromVal, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } sum += val } } } - result = fmt.Sprintf("%g", sum) - return + return newNumberFormulaArg(sum) } // SUMSQ function returns the sum of squares of a supplied set of values. The @@ -2862,8 +2771,9 @@ func (fn *formulaFuncs) SUMIF(argsList *list.List) (result string, err error) { // // SUMSQ(number1,[number2],...) // -func (fn *formulaFuncs) SUMSQ(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) SUMSQ(argsList *list.List) formulaArg { var val, sq float64 + var err error for arg := argsList.Front(); arg != nil; arg = arg.Next() { token := arg.Value.(formulaArg) switch token.Type { @@ -2872,8 +2782,7 @@ func (fn *formulaFuncs) SUMSQ(argsList *list.List) (result string, err error) { continue } if val, err = strconv.ParseFloat(token.String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } sq += val * val case ArgMatrix: @@ -2883,16 +2792,14 @@ func (fn *formulaFuncs) SUMSQ(argsList *list.List) (result string, err error) { continue } if val, err = strconv.ParseFloat(value.String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } sq += val * val } } } } - result = fmt.Sprintf("%g", sq) - return + return newNumberFormulaArg(sq) } // TAN function calculates the tangent of a given angle. The syntax of the @@ -2900,18 +2807,15 @@ func (fn *formulaFuncs) SUMSQ(argsList *list.List) (result string, err error) { // // TAN(number) // -func (fn *formulaFuncs) TAN(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) TAN(argsList *list.List) formulaArg { if argsList.Len() != 1 { - err = errors.New("TAN requires 1 numeric argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "TAN requires 1 numeric argument") } - var number float64 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + number, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } - result = fmt.Sprintf("%g", math.Tan(number)) - return + return newNumberFormulaArg(math.Tan(number)) } // TANH function calculates the hyperbolic tangent (tanh) of a supplied @@ -2919,18 +2823,15 @@ func (fn *formulaFuncs) TAN(argsList *list.List) (result string, err error) { // // TANH(number) // -func (fn *formulaFuncs) TANH(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) TANH(argsList *list.List) formulaArg { if argsList.Len() != 1 { - err = errors.New("TANH requires 1 numeric argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "TANH requires 1 numeric argument") } - var number float64 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + number, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } - result = fmt.Sprintf("%g", math.Tanh(number)) - return + return newNumberFormulaArg(math.Tanh(number)) } // TRUNC function truncates a supplied number to a specified number of decimal @@ -2938,20 +2839,19 @@ func (fn *formulaFuncs) TANH(argsList *list.List) (result string, err error) { // // TRUNC(number,[number_digits]) // -func (fn *formulaFuncs) TRUNC(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) TRUNC(argsList *list.List) formulaArg { if argsList.Len() == 0 { - err = errors.New("TRUNC requires at least 1 argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "TRUNC requires at least 1 argument") } var number, digits, adjust, rtrim float64 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + var err error + number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } if argsList.Len() > 1 { if digits, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } digits = math.Floor(digits) } @@ -2959,15 +2859,13 @@ func (fn *formulaFuncs) TRUNC(argsList *list.List) (result string, err error) { x := int((math.Abs(number) - math.Abs(float64(int(number)))) * adjust) if x != 0 { if rtrim, err = strconv.ParseFloat(strings.TrimRight(strconv.Itoa(x), "0"), 64); err != nil { - return + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } } if (digits > 0) && (rtrim < adjust/10) { - result = fmt.Sprintf("%g", number) - return + return newNumberFormulaArg(number) } - result = fmt.Sprintf("%g", float64(int(number*adjust))/adjust) - return + return newNumberFormulaArg(float64(int(number*adjust)) / adjust) } // Statistical functions @@ -2977,7 +2875,7 @@ func (fn *formulaFuncs) TRUNC(argsList *list.List) (result string, err error) { // // COUNTA(value1,[value2],...) // -func (fn *formulaFuncs) COUNTA(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) COUNTA(argsList *list.List) formulaArg { var count int for token := argsList.Front(); token != nil; token = token.Next() { arg := token.Value.(formulaArg) @@ -2996,8 +2894,7 @@ func (fn *formulaFuncs) COUNTA(argsList *list.List) (result string, err error) { } } } - result = fmt.Sprintf("%d", count) - return + return newStringFormulaArg(fmt.Sprintf("%d", count)) } // MEDIAN function returns the statistical median (the middle value) of a list @@ -3005,20 +2902,19 @@ func (fn *formulaFuncs) COUNTA(argsList *list.List) (result string, err error) { // // MEDIAN(number1,[number2],...) // -func (fn *formulaFuncs) MEDIAN(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) MEDIAN(argsList *list.List) formulaArg { if argsList.Len() == 0 { - err = errors.New("MEDIAN requires at least 1 argument") - return + 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() { arg := token.Value.(formulaArg) switch arg.Type { case ArgString: if digits, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } values = append(values, digits) case ArgMatrix: @@ -3028,8 +2924,7 @@ func (fn *formulaFuncs) MEDIAN(argsList *list.List) (result string, err error) { continue } if digits, err = strconv.ParseFloat(value.String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } values = append(values, digits) } @@ -3042,8 +2937,7 @@ func (fn *formulaFuncs) MEDIAN(argsList *list.List) (result string, err error) { } else { median = values[len(values)/2] } - result = fmt.Sprintf("%g", median) - return + return newNumberFormulaArg(median) } // Information functions @@ -3054,13 +2948,12 @@ func (fn *formulaFuncs) MEDIAN(argsList *list.List) (result string, err error) { // // ISBLANK(value) // -func (fn *formulaFuncs) ISBLANK(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) ISBLANK(argsList *list.List) formulaArg { if argsList.Len() != 1 { - err = errors.New("ISBLANK requires 1 argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "ISBLANK requires 1 argument") } token := argsList.Front().Value.(formulaArg) - result = "FALSE" + result := "FALSE" switch token.Type { case ArgUnknown: result = "TRUE" @@ -3069,7 +2962,7 @@ func (fn *formulaFuncs) ISBLANK(argsList *list.List) (result string, err error) result = "TRUE" } } - return + return newStringFormulaArg(result) } // ISERR function tests if an initial supplied expression (or value) returns @@ -3079,13 +2972,12 @@ func (fn *formulaFuncs) ISBLANK(argsList *list.List) (result string, err error) // // ISERR(value) // -func (fn *formulaFuncs) ISERR(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) ISERR(argsList *list.List) formulaArg { if argsList.Len() != 1 { - err = errors.New("ISERR requires 1 argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "ISERR requires 1 argument") } token := argsList.Front().Value.(formulaArg) - result = "FALSE" + result := "FALSE" if token.Type == ArgString { for _, errType := range []string{formulaErrorDIV, formulaErrorNAME, formulaErrorNUM, formulaErrorVALUE, formulaErrorREF, formulaErrorNULL, formulaErrorSPILL, formulaErrorCALC, formulaErrorGETTINGDATA} { if errType == token.String { @@ -3093,7 +2985,7 @@ func (fn *formulaFuncs) ISERR(argsList *list.List) (result string, err error) { } } } - return + return newStringFormulaArg(result) } // ISERROR function tests if an initial supplied expression (or value) returns @@ -3102,13 +2994,12 @@ func (fn *formulaFuncs) ISERR(argsList *list.List) (result string, err error) { // // ISERROR(value) // -func (fn *formulaFuncs) ISERROR(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) ISERROR(argsList *list.List) formulaArg { if argsList.Len() != 1 { - err = errors.New("ISERROR requires 1 argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "ISERROR requires 1 argument") } token := argsList.Front().Value.(formulaArg) - result = "FALSE" + result := "FALSE" if token.Type == ArgString { for _, errType := range []string{formulaErrorDIV, formulaErrorNAME, formulaErrorNA, formulaErrorNUM, formulaErrorVALUE, formulaErrorREF, formulaErrorNULL, formulaErrorSPILL, formulaErrorCALC, formulaErrorGETTINGDATA} { if errType == token.String { @@ -3116,7 +3007,7 @@ func (fn *formulaFuncs) ISERROR(argsList *list.List) (result string, err error) } } } - return + return newStringFormulaArg(result) } // ISEVEN function tests if a supplied number (or numeric expression) @@ -3125,25 +3016,25 @@ func (fn *formulaFuncs) ISERROR(argsList *list.List) (result string, err error) // // ISEVEN(value) // -func (fn *formulaFuncs) ISEVEN(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) ISEVEN(argsList *list.List) formulaArg { if argsList.Len() != 1 { - err = errors.New("ISEVEN requires 1 argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "ISEVEN requires 1 argument") } - token := argsList.Front().Value.(formulaArg) - result = "FALSE" - var numeric int + 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 { - err = errors.New(formulaErrorVALUE) - return + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } if numeric == numeric/2*2 { - result = "TRUE" - return + return newStringFormulaArg("TRUE") } } - return + return newStringFormulaArg(result) } // ISNA function tests if an initial supplied expression (or value) returns @@ -3152,17 +3043,16 @@ func (fn *formulaFuncs) ISEVEN(argsList *list.List) (result string, err error) { // // ISNA(value) // -func (fn *formulaFuncs) ISNA(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) ISNA(argsList *list.List) formulaArg { if argsList.Len() != 1 { - err = errors.New("ISNA requires 1 argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "ISNA requires 1 argument") } token := argsList.Front().Value.(formulaArg) - result = "FALSE" + result := "FALSE" if token.Type == ArgString && token.String == formulaErrorNA { result = "TRUE" } - return + return newStringFormulaArg(result) } // ISNONTEXT function function tests if a supplied value is text. If not, the @@ -3171,17 +3061,16 @@ func (fn *formulaFuncs) ISNA(argsList *list.List) (result string, err error) { // // ISNONTEXT(value) // -func (fn *formulaFuncs) ISNONTEXT(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) ISNONTEXT(argsList *list.List) formulaArg { if argsList.Len() != 1 { - err = errors.New("ISNONTEXT requires 1 argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "ISNONTEXT requires 1 argument") } token := argsList.Front().Value.(formulaArg) - result = "TRUE" + result := "TRUE" if token.Type == ArgString && token.String != "" { result = "FALSE" } - return + return newStringFormulaArg(result) } // ISNUMBER function function tests if a supplied value is a number. If so, @@ -3190,20 +3079,18 @@ func (fn *formulaFuncs) ISNONTEXT(argsList *list.List) (result string, err error // // ISNUMBER(value) // -func (fn *formulaFuncs) ISNUMBER(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) ISNUMBER(argsList *list.List) formulaArg { if argsList.Len() != 1 { - err = errors.New("ISNUMBER requires 1 argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "ISNUMBER requires 1 argument") } token := argsList.Front().Value.(formulaArg) - result = "FALSE" + result := "FALSE" if token.Type == ArgString && token.String != "" { - if _, err = strconv.Atoi(token.String); err == nil { + if _, err := strconv.Atoi(token.String); err == nil { result = "TRUE" } - err = nil } - return + return newStringFormulaArg(result) } // ISODD function tests if a supplied number (or numeric expression) evaluates @@ -3212,25 +3099,25 @@ func (fn *formulaFuncs) ISNUMBER(argsList *list.List) (result string, err error) // // ISODD(value) // -func (fn *formulaFuncs) ISODD(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) ISODD(argsList *list.List) formulaArg { if argsList.Len() != 1 { - err = errors.New("ISODD requires 1 argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "ISODD requires 1 argument") } - token := argsList.Front().Value.(formulaArg) - result = "FALSE" - var numeric int + 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 { - err = errors.New(formulaErrorVALUE) - return + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } if numeric != numeric/2*2 { - result = "TRUE" - return + return newStringFormulaArg("TRUE") } } - return + return newStringFormulaArg(result) } // NA function returns the Excel #N/A error. This error message has the @@ -3239,13 +3126,11 @@ func (fn *formulaFuncs) ISODD(argsList *list.List) (result string, err error) { // // NA() // -func (fn *formulaFuncs) NA(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) NA(argsList *list.List) formulaArg { if argsList.Len() != 0 { - err = errors.New("NA accepts no arguments") - return + return newErrorFormulaArg(formulaErrorVALUE, "NA accepts no arguments") } - result = formulaErrorNA - return + return newStringFormulaArg(formulaErrorNA) } // Logical Functions @@ -3255,17 +3140,18 @@ func (fn *formulaFuncs) NA(argsList *list.List) (result string, err error) { // // AND(logical_test1,[logical_test2],...) // -func (fn *formulaFuncs) AND(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) AND(argsList *list.List) formulaArg { if argsList.Len() == 0 { - err = errors.New("AND requires at least 1 argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "AND requires at least 1 argument") } if argsList.Len() > 30 { - err = errors.New("AND accepts at most 30 arguments") - return + return newErrorFormulaArg(formulaErrorVALUE, "AND accepts at most 30 arguments") } - var and = true - var val float64 + var ( + and = true + val float64 + err error + ) for arg := argsList.Front(); arg != nil; arg = arg.Next() { token := arg.Value.(formulaArg) switch token.Type { @@ -3276,22 +3162,18 @@ func (fn *formulaFuncs) AND(argsList *list.List) (result string, err error) { continue } if token.String == "FALSE" { - result = token.String - return + return newStringFormulaArg(token.String) } if val, err = strconv.ParseFloat(token.String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } and = and && (val != 0) case ArgMatrix: // TODO - err = errors.New(formulaErrorVALUE) - return + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) } } - result = strings.ToUpper(strconv.FormatBool(and)) - return + return newStringFormulaArg(strings.ToUpper(strconv.FormatBool(and))) } // OR function tests a number of supplied conditions and returns either TRUE @@ -3299,17 +3181,18 @@ func (fn *formulaFuncs) AND(argsList *list.List) (result string, err error) { // // OR(logical_test1,[logical_test2],...) // -func (fn *formulaFuncs) OR(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) OR(argsList *list.List) formulaArg { if argsList.Len() == 0 { - err = errors.New("OR requires at least 1 argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "OR requires at least 1 argument") } if argsList.Len() > 30 { - err = errors.New("OR accepts at most 30 arguments") - return + return newErrorFormulaArg(formulaErrorVALUE, "OR accepts at most 30 arguments") } - var or bool - var val float64 + var ( + or bool + val float64 + err error + ) for arg := argsList.Front(); arg != nil; arg = arg.Next() { token := arg.Value.(formulaArg) switch token.Type { @@ -3324,18 +3207,15 @@ func (fn *formulaFuncs) OR(argsList *list.List) (result string, err error) { continue } if val, err = strconv.ParseFloat(token.String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } or = val != 0 case ArgMatrix: // TODO - err = errors.New(formulaErrorVALUE) - return + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) } } - result = strings.ToUpper(strconv.FormatBool(or)) - return + return newStringFormulaArg(strings.ToUpper(strconv.FormatBool(or))) } // Date and Time Functions @@ -3345,27 +3225,23 @@ func (fn *formulaFuncs) OR(argsList *list.List) (result string, err error) { // // DATE(year,month,day) // -func (fn *formulaFuncs) DATE(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) DATE(argsList *list.List) formulaArg { if argsList.Len() != 3 { - err = errors.New("DATE requires 3 number arguments") - return + return newErrorFormulaArg(formulaErrorVALUE, "DATE requires 3 number arguments") } var year, month, day int + var err error if year, err = strconv.Atoi(argsList.Front().Value.(formulaArg).String); err != nil { - err = errors.New("DATE requires 3 number arguments") - return + return newErrorFormulaArg(formulaErrorVALUE, "DATE requires 3 number arguments") } if month, err = strconv.Atoi(argsList.Front().Next().Value.(formulaArg).String); err != nil { - err = errors.New("DATE requires 3 number arguments") - return + return newErrorFormulaArg(formulaErrorVALUE, "DATE requires 3 number arguments") } if day, err = strconv.Atoi(argsList.Back().Value.(formulaArg).String); err != nil { - err = errors.New("DATE requires 3 number arguments") - return + return newErrorFormulaArg(formulaErrorVALUE, "DATE requires 3 number arguments") } d := makeDate(year, time.Month(month), day) - result = timeFromExcelTime(daysBetween(excelMinTime1900.Unix(), d)+1, false).String() - return + return newStringFormulaArg(timeFromExcelTime(daysBetween(excelMinTime1900.Unix(), d)+1, false).String()) } // makeDate return date as a Unix time, the number of seconds elapsed since @@ -3391,10 +3267,9 @@ func daysBetween(startDate, endDate int64) float64 { // // CLEAN(text) // -func (fn *formulaFuncs) CLEAN(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) CLEAN(argsList *list.List) formulaArg { if argsList.Len() != 1 { - err = errors.New("CLEAN requires 1 argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "CLEAN requires 1 argument") } b := bytes.Buffer{} for _, c := range argsList.Front().Value.(formulaArg).String { @@ -3402,8 +3277,7 @@ func (fn *formulaFuncs) CLEAN(argsList *list.List) (result string, err error) { b.WriteRune(c) } } - result = b.String() - return + return newStringFormulaArg(b.String()) } // LEN returns the length of a supplied text string. The syntax of the @@ -3411,13 +3285,11 @@ func (fn *formulaFuncs) CLEAN(argsList *list.List) (result string, err error) { // // LEN(text) // -func (fn *formulaFuncs) LEN(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) LEN(argsList *list.List) formulaArg { if argsList.Len() != 1 { - err = errors.New("LEN requires 1 string argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "LEN requires 1 string argument") } - result = strconv.Itoa(len(argsList.Front().Value.(formulaArg).String)) - return + return newStringFormulaArg(strconv.Itoa(len(argsList.Front().Value.(formulaArg).String))) } // TRIM removes extra spaces (i.e. all spaces except for single spaces between @@ -3426,13 +3298,11 @@ func (fn *formulaFuncs) LEN(argsList *list.List) (result string, err error) { // // TRIM(text) // -func (fn *formulaFuncs) TRIM(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) TRIM(argsList *list.List) formulaArg { if argsList.Len() != 1 { - err = errors.New("TRIM requires 1 argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "TRIM requires 1 argument") } - result = strings.TrimSpace(argsList.Front().Value.(formulaArg).String) - return + return newStringFormulaArg(strings.TrimSpace(argsList.Front().Value.(formulaArg).String)) } // LOWER converts all characters in a supplied text string to lower case. The @@ -3440,13 +3310,11 @@ func (fn *formulaFuncs) TRIM(argsList *list.List) (result string, err error) { // // LOWER(text) // -func (fn *formulaFuncs) LOWER(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) LOWER(argsList *list.List) formulaArg { if argsList.Len() != 1 { - err = errors.New("LOWER requires 1 argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "LOWER requires 1 argument") } - result = strings.ToLower(argsList.Front().Value.(formulaArg).String) - return + return newStringFormulaArg(strings.ToLower(argsList.Front().Value.(formulaArg).String)) } // PROPER converts all characters in a supplied text string to proper case @@ -3456,10 +3324,9 @@ func (fn *formulaFuncs) LOWER(argsList *list.List) (result string, err error) { // // PROPER(text) // -func (fn *formulaFuncs) PROPER(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) PROPER(argsList *list.List) formulaArg { if argsList.Len() != 1 { - err = errors.New("PROPER requires 1 argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "PROPER requires 1 argument") } buf := bytes.Buffer{} isLetter := false @@ -3471,9 +3338,7 @@ func (fn *formulaFuncs) PROPER(argsList *list.List) (result string, err error) { } isLetter = unicode.IsLetter(char) } - - result = buf.String() - return + return newStringFormulaArg(buf.String()) } // UPPER converts all characters in a supplied text string to upper case. The @@ -3481,13 +3346,11 @@ func (fn *formulaFuncs) PROPER(argsList *list.List) (result string, err error) { // // UPPER(text) // -func (fn *formulaFuncs) UPPER(argsList *list.List) (result string, err error) { +func (fn *formulaFuncs) UPPER(argsList *list.List) formulaArg { if argsList.Len() != 1 { - err = errors.New("UPPER requires 1 argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "UPPER requires 1 argument") } - result = strings.ToUpper(argsList.Front().Value.(formulaArg).String) - return + return newStringFormulaArg(strings.ToUpper(argsList.Front().Value.(formulaArg).String)) } // Conditional Functions @@ -3496,36 +3359,61 @@ func (fn *formulaFuncs) UPPER(argsList *list.List) (result string, err error) { // 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) (result string, err error) { +func (fn *formulaFuncs) IF(argsList *list.List) formulaArg { if argsList.Len() == 0 { - err = errors.New("IF requires at least 1 argument") - return + return newErrorFormulaArg(formulaErrorVALUE, "IF requires at least 1 argument") } if argsList.Len() > 3 { - err = errors.New("IF accepts at most 3 arguments") - return + return newErrorFormulaArg(formulaErrorVALUE, "IF accepts at most 3 arguments") } token := argsList.Front().Value.(formulaArg) - var cond bool + var ( + cond bool + err error + result string + ) switch token.Type { case ArgString: if cond, err = strconv.ParseBool(token.String); err != nil { - err = errors.New(formulaErrorVALUE) - return + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } if argsList.Len() == 1 { - result = strings.ToUpper(strconv.FormatBool(cond)) - return + return newStringFormulaArg(strings.ToUpper(strconv.FormatBool(cond))) } if cond { - result = argsList.Front().Next().Value.(formulaArg).String - return + return newStringFormulaArg(argsList.Front().Next().Value.(formulaArg).String) } if argsList.Len() == 3 { result = argsList.Back().Value.(formulaArg).String } } - return + return newStringFormulaArg(result) +} + +// Excel Lookup and Reference Functions + +// 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],...) +// +// TODO: resolve range choose. +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) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, "CHOOSE requires first argument of type number") + } + if argsList.Len() <= idx { + return newErrorFormulaArg(formulaErrorVALUE, "index_num should be <= to the number of values") + } + arg := argsList.Front() + for i := 0; i < idx; i++ { + arg = arg.Next() + } + return newStringFormulaArg(arg.Value.(formulaArg).String) } diff --git a/calc_test.go b/calc_test.go index c999540380..d890043061 100644 --- a/calc_test.go +++ b/calc_test.go @@ -494,6 +494,10 @@ func TestCalcCellValue(t *testing.T) { "=IF(1<>1)": "FALSE", "=IF(5<0, \"negative\", \"positive\")": "positive", "=IF(-2<0, \"negative\", \"positive\")": "negative", + // Excel Lookup and Reference Functions + // CHOOSE + "=CHOOSE(4,\"red\",\"blue\",\"green\",\"brown\")": "brown", + "=CHOOSE(1,\"red\",\"blue\",\"green\",\"brown\")": "red", } for formula, expected := range mathCalc { f := prepareData() @@ -505,248 +509,248 @@ func TestCalcCellValue(t *testing.T) { mathCalcError := map[string]string{ // ABS "=ABS()": "ABS requires 1 numeric argument", - `=ABS("X")`: "#VALUE!", + `=ABS("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", "=ABS(~)": `cannot convert cell "~" to coordinates: invalid cell name "~"`, // ACOS "=ACOS()": "ACOS requires 1 numeric argument", - `=ACOS("X")`: "#VALUE!", + `=ACOS("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", // ACOSH "=ACOSH()": "ACOSH requires 1 numeric argument", - `=ACOSH("X")`: "#VALUE!", + `=ACOSH("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", // _xlfn.ACOT "=_xlfn.ACOT()": "ACOT requires 1 numeric argument", - `=_xlfn.ACOT("X")`: "#VALUE!", + `=_xlfn.ACOT("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", // _xlfn.ACOTH "=_xlfn.ACOTH()": "ACOTH requires 1 numeric argument", - `=_xlfn.ACOTH("X")`: "#VALUE!", + `=_xlfn.ACOTH("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", // _xlfn.ARABIC "=_xlfn.ARABIC()": "ARABIC requires 1 numeric argument", // ASIN "=ASIN()": "ASIN requires 1 numeric argument", - `=ASIN("X")`: "#VALUE!", + `=ASIN("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", // ASINH "=ASINH()": "ASINH requires 1 numeric argument", - `=ASINH("X")`: "#VALUE!", + `=ASINH("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", // ATAN "=ATAN()": "ATAN requires 1 numeric argument", - `=ATAN("X")`: "#VALUE!", + `=ATAN("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", // ATANH "=ATANH()": "ATANH requires 1 numeric argument", - `=ATANH("X")`: "#VALUE!", + `=ATANH("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", // ATAN2 "=ATAN2()": "ATAN2 requires 2 numeric arguments", - `=ATAN2("X",0)`: "#VALUE!", - `=ATAN2(0,"X")`: "#VALUE!", + `=ATAN2("X",0)`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + `=ATAN2(0,"X")`: "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)`: "#VALUE!", - `=BASE(1,"X")`: "#VALUE!", - `=BASE(1,2,"X")`: "#VALUE!", + `=BASE("X",2)`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + `=BASE(1,"X")`: "strconv.Atoi: parsing \"X\": invalid syntax", + `=BASE(1,2,"X")`: "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)`: "#VALUE!", - `=CEILING(0,"X")`: "#VALUE!", + `=CEILING("X",0)`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + `=CEILING(0,"X")`: "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")`: "#VALUE!", - `=_xlfn.CEILING.MATH(1,"X")`: "#VALUE!", - `=_xlfn.CEILING.MATH(1,2,"X")`: "#VALUE!", + `=_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.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)`: "#VALUE!", + `=_xlfn.CEILING.PRECISE("X",2)`: "strconv.ParseFloat: parsing \"X\": invalid syntax", `=_xlfn.CEILING.PRECISE(1,"X")`: "#VALUE!", // COMBIN "=COMBIN()": "COMBIN requires 2 argument", "=COMBIN(-1,1)": "COMBIN requires number >= number_chosen", - `=COMBIN("X",1)`: "#VALUE!", - `=COMBIN(-1,"X")`: "#VALUE!", + `=COMBIN("X",1)`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + `=COMBIN(-1,"X")`: "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)`: "#VALUE!", - `=_xlfn.COMBINA(-1,"X")`: "#VALUE!", + `=_xlfn.COMBINA("X",1)`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + `=_xlfn.COMBINA(-1,"X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", // COS "=COS()": "COS requires 1 numeric argument", - `=COS("X")`: "#VALUE!", + `=COS("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", // COSH "=COSH()": "COSH requires 1 numeric argument", - `=COSH("X")`: "#VALUE!", + `=COSH("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", // _xlfn.COT "=COT()": "COT requires 1 numeric argument", - `=COT("X")`: "#VALUE!", + `=COT("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", "=COT(0)": "#DIV/0!", // _xlfn.COTH "=COTH()": "COTH requires 1 numeric argument", - `=COTH("X")`: "#VALUE!", + `=COTH("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", "=COTH(0)": "#DIV/0!", // _xlfn.CSC "=_xlfn.CSC()": "CSC requires 1 numeric argument", - `=_xlfn.CSC("X")`: "#VALUE!", + `=_xlfn.CSC("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", "=_xlfn.CSC(0)": "#DIV/0!", // _xlfn.CSCH "=_xlfn.CSCH()": "CSCH requires 1 numeric argument", - `=_xlfn.CSCH("X")`: "#VALUE!", + `=_xlfn.CSCH("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", "=_xlfn.CSCH(0)": "#DIV/0!", // _xlfn.DECIMAL "=_xlfn.DECIMAL()": "DECIMAL requires 2 numeric arguments", - `=_xlfn.DECIMAL("X", 2)`: "#VALUE!", - `=_xlfn.DECIMAL(2000, "X")`: "#VALUE!", + `=_xlfn.DECIMAL("X", 2)`: "strconv.ParseInt: parsing \"X\": invalid syntax", + `=_xlfn.DECIMAL(2000, "X")`: "strconv.Atoi: parsing \"X\": invalid syntax", // DEGREES "=DEGREES()": "DEGREES requires 1 numeric argument", - `=DEGREES("X")`: "#VALUE!", + `=DEGREES("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", "=DEGREES(0)": "#DIV/0!", // EVEN "=EVEN()": "EVEN requires 1 numeric argument", - `=EVEN("X")`: "#VALUE!", + `=EVEN("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", // EXP "=EXP()": "EXP requires 1 numeric argument", - `=EXP("X")`: "#VALUE!", + `=EXP("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", // FACT "=FACT()": "FACT requires 1 numeric argument", - `=FACT("X")`: "#VALUE!", + `=FACT("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", "=FACT(-1)": "#NUM!", // FACTDOUBLE "=FACTDOUBLE()": "FACTDOUBLE requires 1 numeric argument", - `=FACTDOUBLE("X")`: "#VALUE!", + `=FACTDOUBLE("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", "=FACTDOUBLE(-1)": "#NUM!", // FLOOR "=FLOOR()": "FLOOR requires 2 numeric arguments", - `=FLOOR("X",-1)`: "#VALUE!", - `=FLOOR(1,"X")`: "#VALUE!", - "=FLOOR(1,-1)": "#NUM!", + `=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", // _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)`: "#VALUE!", - `=_xlfn.FLOOR.MATH(1,"X",3)`: "#VALUE!", - `=_xlfn.FLOOR.MATH(1,2,"X")`: "#VALUE!", + `=_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.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)`: "#VALUE!", - `=_xlfn.FLOOR.PRECISE(1,"X")`: "#VALUE!", + `=_xlfn.FLOOR.PRECISE("X",2)`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + `=_xlfn.FLOOR.PRECISE(1,"X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", // GCD "=GCD()": "GCD requires at least 1 argument", "=GCD(-1)": "GCD only accepts positive arguments", "=GCD(1,-1)": "GCD only accepts positive arguments", - `=GCD("X")`: "#VALUE!", + `=GCD("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", // INT "=INT()": "INT requires 1 numeric argument", - `=INT("X")`: "#VALUE!", + `=INT("X")`: "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)`: "#VALUE!", - `=ISO.CEILING(1,"X")`: "#VALUE!", + `=ISO.CEILING("X",2)`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + `=ISO.CEILING(1,"X")`: "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")`: "#VALUE!", + `=LCM("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", // LN "=LN()": "LN requires 1 numeric argument", - `=LN("X")`: "#VALUE!", + `=LN("X")`: "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)`: "#VALUE!", - `=LOG(1,"X")`: "#VALUE!", - "=LOG(0,0)": "#NUM!", - "=LOG(1,0)": "#NUM!", + `=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!", // LOG10 "=LOG10()": "LOG10 requires 1 numeric argument", - `=LOG10("X")`: "#VALUE!", + `=LOG10("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", // MOD "=MOD()": "MOD requires 2 numeric arguments", - "=MOD(6,0)": "#DIV/0!", - `=MOD("X",0)`: "#VALUE!", - `=MOD(6,"X")`: "#VALUE!", + "=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", // MROUND "=MROUND()": "MROUND requires 2 numeric arguments", "=MROUND(1,0)": "#NUM!", "=MROUND(1,-1)": "#NUM!", - `=MROUND("X",0)`: "#VALUE!", - `=MROUND(1,"X")`: "#VALUE!", + `=MROUND("X",0)`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + `=MROUND(1,"X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", // MULTINOMIAL - `=MULTINOMIAL("X")`: "#VALUE!", + `=MULTINOMIAL("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", // _xlfn.MUNIT - "=_xlfn.MUNIT()": "MUNIT requires 1 numeric argument", // not support currently - `=_xlfn.MUNIT("X")`: "#VALUE!", // not support currently + "=_xlfn.MUNIT()": "MUNIT requires 1 numeric argument", // not support currently + `=_xlfn.MUNIT("X")`: "strconv.Atoi: parsing \"X\": invalid syntax", // not support currently // ODD "=ODD()": "ODD requires 1 numeric argument", - `=ODD("X")`: "#VALUE!", + `=ODD("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", // PI "=PI(1)": "PI accepts no arguments", // POWER - `=POWER("X",1)`: "#VALUE!", - `=POWER(1,"X")`: "#VALUE!", + `=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", // PRODUCT - `=PRODUCT("X")`: "#VALUE!", + `=PRODUCT("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", // QUOTIENT - `=QUOTIENT("X",1)`: "#VALUE!", - `=QUOTIENT(1,"X")`: "#VALUE!", + `=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", // RADIANS - `=RADIANS("X")`: "#VALUE!", + `=RADIANS("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", "=RADIANS()": "RADIANS requires 1 numeric argument", // RAND "=RAND(1)": "RAND accepts no arguments", // RANDBETWEEN - `=RANDBETWEEN("X",1)`: "#VALUE!", - `=RANDBETWEEN(1,"X")`: "#VALUE!", + `=RANDBETWEEN("X",1)`: "strconv.ParseInt: parsing \"X\": invalid syntax", + `=RANDBETWEEN(1,"X")`: "strconv.ParseInt: parsing \"X\": invalid syntax", "=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")`: "#VALUE!", - `=ROMAN("X",1)`: "#VALUE!", + `=ROMAN("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + `=ROMAN("X",1)`: "strconv.ParseFloat: parsing \"X\": invalid syntax", // ROUND "=ROUND()": "ROUND requires 2 numeric arguments", - `=ROUND("X",1)`: "#VALUE!", - `=ROUND(1,"X")`: "#VALUE!", + `=ROUND("X",1)`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + `=ROUND(1,"X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", // ROUNDDOWN "=ROUNDDOWN()": "ROUNDDOWN requires 2 numeric arguments", - `=ROUNDDOWN("X",1)`: "#VALUE!", - `=ROUNDDOWN(1,"X")`: "#VALUE!", + `=ROUNDDOWN("X",1)`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + `=ROUNDDOWN(1,"X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", // ROUNDUP "=ROUNDUP()": "ROUNDUP requires 2 numeric arguments", - `=ROUNDUP("X",1)`: "#VALUE!", - `=ROUNDUP(1,"X")`: "#VALUE!", + `=ROUNDUP("X",1)`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + `=ROUNDUP(1,"X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", // SEC "=_xlfn.SEC()": "SEC requires 1 numeric argument", - `=_xlfn.SEC("X")`: "#VALUE!", + `=_xlfn.SEC("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", // _xlfn.SECH "=_xlfn.SECH()": "SECH requires 1 numeric argument", - `=_xlfn.SECH("X")`: "#VALUE!", + `=_xlfn.SECH("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", // SIGN "=SIGN()": "SIGN requires 1 numeric argument", - `=SIGN("X")`: "#VALUE!", + `=SIGN("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", // SIN "=SIN()": "SIN requires 1 numeric argument", - `=SIN("X")`: "#VALUE!", + `=SIN("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", // SINH "=SINH()": "SINH requires 1 numeric argument", - `=SINH("X")`: "#VALUE!", + `=SINH("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", // SQRT "=SQRT()": "SQRT requires 1 numeric argument", - `=SQRT("X")`: "#VALUE!", + `=SQRT("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", "=SQRT(-1)": "#NUM!", // SQRTPI "=SQRTPI()": "SQRTPI requires 1 numeric argument", - `=SQRTPI("X")`: "#VALUE!", + `=SQRTPI("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", // SUM "=SUM((": "formula not valid", "=SUM(-)": "formula not valid", @@ -754,21 +758,21 @@ func TestCalcCellValue(t *testing.T) { "=SUM(1-)": "formula not valid", "=SUM(1*)": "formula not valid", "=SUM(1/)": "formula not valid", - `=SUM("X")`: "#VALUE!", + `=SUM("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", // SUMIF "=SUMIF()": "SUMIF requires at least 2 argument", // SUMSQ - `=SUMSQ("X")`: "#VALUE!", + `=SUMSQ("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", // TAN "=TAN()": "TAN requires 1 numeric argument", - `=TAN("X")`: "#VALUE!", + `=TAN("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", // TANH "=TANH()": "TANH requires 1 numeric argument", - `=TANH("X")`: "#VALUE!", + `=TANH("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", // TRUNC "=TRUNC()": "TRUNC requires at least 1 argument", - `=TRUNC("X")`: "#VALUE!", - `=TRUNC(1,"X")`: "#VALUE!", + `=TRUNC("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + `=TRUNC(1,"X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", // Statistical Functions // MEDIAN "=MEDIAN()": "MEDIAN requires at least 1 argument", @@ -781,7 +785,7 @@ func TestCalcCellValue(t *testing.T) { "=ISERROR()": "ISERROR requires 1 argument", // ISEVEN "=ISEVEN()": "ISEVEN requires 1 argument", - `=ISEVEN("text")`: "#VALUE!", + `=ISEVEN("text")`: "strconv.Atoi: parsing \"text\": invalid syntax", // ISNA "=ISNA()": "ISNA requires 1 argument", // ISNONTEXT @@ -790,17 +794,17 @@ func TestCalcCellValue(t *testing.T) { "=ISNUMBER()": "ISNUMBER requires 1 argument", // ISODD "=ISODD()": "ISODD requires 1 argument", - `=ISODD("text")`: "#VALUE!", + `=ISODD("text")`: "strconv.Atoi: parsing \"text\": invalid syntax", // NA "=NA(1)": "NA accepts no arguments", // Logical Functions // AND - `=AND("text")`: "#VALUE!", + `=AND("text")`: "strconv.ParseFloat: parsing \"text\": invalid syntax", `=AND(A1:B1)`: "#VALUE!", "=AND()": "AND requires at least 1 argument", "=AND(1" + strings.Repeat(",1", 30) + ")": "AND accepts at most 30 arguments", // OR - `=OR("text")`: "#VALUE!", + `=OR("text")`: "strconv.ParseFloat: parsing \"text\": invalid syntax", `=OR(A1:B1)`: "#VALUE!", "=OR()": "OR requires at least 1 argument", "=OR(1" + strings.Repeat(",1", 30) + ")": "OR accepts at most 30 arguments", @@ -832,13 +836,18 @@ func TestCalcCellValue(t *testing.T) { // IF "=IF()": "IF requires at least 1 argument", "=IF(0,1,2,3)": "IF accepts at most 3 arguments", - "=IF(D1,1,2)": "#VALUE!", + "=IF(D1,1,2)": "strconv.ParseBool: parsing \"Month\": invalid syntax", + // Excel Lookup and Reference Functions + // 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", } for formula, expected := range mathCalcError { f := prepareData() assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) result, err := f.CalcCellValue("Sheet1", "C1") - assert.EqualError(t, err, expected) + assert.EqualError(t, err, expected, formula) assert.Equal(t, "", result, formula) } @@ -976,9 +985,9 @@ func TestISBLANK(t *testing.T) { Type: ArgUnknown, }) fn := formulaFuncs{} - result, err := fn.ISBLANK(argsList) - assert.Equal(t, result, "TRUE") - assert.NoError(t, err) + result := fn.ISBLANK(argsList) + assert.Equal(t, result.String, "TRUE") + assert.Empty(t, result.Error) } func TestAND(t *testing.T) { @@ -987,9 +996,9 @@ func TestAND(t *testing.T) { Type: ArgUnknown, }) fn := formulaFuncs{} - result, err := fn.AND(argsList) - assert.Equal(t, result, "TRUE") - assert.NoError(t, err) + result := fn.AND(argsList) + assert.Equal(t, result.String, "TRUE") + assert.Empty(t, result.Error) } func TestOR(t *testing.T) { @@ -998,9 +1007,9 @@ func TestOR(t *testing.T) { Type: ArgUnknown, }) fn := formulaFuncs{} - result, err := fn.OR(argsList) - assert.Equal(t, result, "FALSE") - assert.NoError(t, err) + result := fn.OR(argsList) + assert.Equal(t, result.String, "FALSE") + assert.Empty(t, result.Error) } func TestDet(t *testing.T) { diff --git a/go.mod b/go.mod index 9637ba0e3c..41babe1c5e 100644 --- a/go.mod +++ b/go.mod @@ -6,9 +6,9 @@ require ( github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 github.com/richardlehane/mscfb v1.0.3 github.com/stretchr/testify v1.6.1 - github.com/xuri/efp v0.0.0-20201016154823-031c29024257 - golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee + github.com/xuri/efp v0.0.0-20210128032744-13be4fd5dcb5 + golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad golang.org/x/image v0.0.0-20201208152932-35266b937fa6 - golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0 - golang.org/x/text v0.3.3 + golang.org/x/net v0.0.0-20210119194325-5f4716e94777 + golang.org/x/text v0.3.5 ) diff --git a/go.sum b/go.sum index 24aa2255ed..75323d6276 100644 --- a/go.sum +++ b/go.sum @@ -8,28 +8,30 @@ github.com/richardlehane/mscfb v1.0.3 h1:rD8TBkYWkObWO0oLDFCbwMeZ4KoalxQy+QgniCj github.com/richardlehane/mscfb v1.0.3/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/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/xuri/efp v0.0.0-20201016154823-031c29024257 h1:6ldmGEJXtsRMwdR2KuS3esk9wjVJNvgk05/YY2XmOj0= -github.com/xuri/efp v0.0.0-20201016154823-031c29024257/go.mod h1:uBiSUepVYMhGTfDeBKKasV4GpgBlzJ46gXUBAqV8qLk= +github.com/xuri/efp v0.0.0-20210128032744-13be4fd5dcb5 h1:hO7we8DcWAkmZX/Voqa04Tuo84SbeXIJbdgMj92hIpA= +github.com/xuri/efp v0.0.0-20210128032744-13be4fd5dcb5/go.mod h1:uBiSUepVYMhGTfDeBKKasV4GpgBlzJ46gXUBAqV8qLk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee h1:4yd7jl+vXjalO5ztz6Vc1VADv+S/80LGJmyl1ROJ2AI= -golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY= +golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/image v0.0.0-20201208152932-35266b937fa6 h1:nfeHNc1nAqecKCy2FCy4HY+soOOe5sDLJ/gZLbx6GYI= golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0 h1:5kGOVHlq0euqwzgTC9Vu15p6fV1Wi0ArVi8da2urnVg= -golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 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= From 76c72e0a3060a7231f4ffb1437a2d86c7715e656 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 31 Jan 2021 01:28:40 +0800 Subject: [PATCH 320/957] Nested formula function support cell references as arguments --- calc.go | 144 +++++++++++++++++++++++++++++++++++---------------- calc_test.go | 76 +++++++++++++++------------ 2 files changed, 142 insertions(+), 78 deletions(-) diff --git a/calc.go b/calc.go index 0a653288b7..ecd5f0cdcc 100644 --- a/calc.go +++ b/calc.go @@ -111,6 +111,12 @@ type formulaArg struct { func (fa formulaArg) Value() (value string) { switch fa.Type { case ArgNumber: + if fa.Boolean { + if fa.Number == 0 { + return "FALSE" + } + return "TRUE" + } return fmt.Sprintf("%g", fa.Number) case ArgString: return fa.String @@ -120,6 +126,22 @@ func (fa formulaArg) Value() (value string) { return } +// ToNumber returns a formula argument with number data type. +func (fa formulaArg) ToNumber() formulaArg { + var n float64 + var err error + switch fa.Type { + case ArgString: + n, err = strconv.ParseFloat(fa.String, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + } + case ArgNumber: + n = fa.Number + } + return newNumberFormulaArg(n) +} + // formulaFuncs is the type of the formula functions. type formulaFuncs struct{} @@ -274,6 +296,9 @@ func getPriority(token efp.Token) (pri int) { // newNumberFormulaArg constructs a number formula argument. func newNumberFormulaArg(n float64) formulaArg { + if math.IsNaN(n) { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } return formulaArg{Type: ArgNumber, Number: n} } @@ -282,6 +307,20 @@ func newStringFormulaArg(s string) formulaArg { return formulaArg{Type: ArgString, String: s} } +// newMatrixFormulaArg constructs a matrix formula argument. +func newMatrixFormulaArg(m [][]formulaArg) formulaArg { + return formulaArg{Type: ArgMatrix, Matrix: m} +} + +// newBoolFormulaArg constructs a boolean formula argument. +func newBoolFormulaArg(b bool) formulaArg { + var n float64 + if b { + n = 1 + } + return formulaArg{Type: ArgNumber, Number: n, Boolean: true} +} + // newErrorFormulaArg create an error formula argument of a given type with a specified error message. func newErrorFormulaArg(formulaError, msg string) formulaArg { return formulaArg{Type: ArgError, String: formulaError, Error: msg} @@ -426,7 +465,12 @@ func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error) argsList.Init() opfStack.Pop() if opfStack.Len() > 0 { // still in function stack - opfdStack.Push(efp.Token{TValue: arg.Value(), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + if nextToken.TType == efp.TokenTypeOperatorInfix { + // mathematics calculate in formula function + opfdStack.Push(efp.Token{TValue: arg.Value(), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + } else { + argsList.PushBack(arg) + } } else { opdStack.Push(efp.Token{TValue: arg.Value(), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) } @@ -994,11 +1038,11 @@ func (fn *formulaFuncs) ABS(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "ABS requires 1 numeric argument") } - val, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + arg := argsList.Front().Value.(formulaArg).ToNumber() + if arg.Type == ArgError { + return arg } - return newNumberFormulaArg(math.Abs(val)) + return newNumberFormulaArg(math.Abs(arg.Number)) } // ACOS function calculates the arccosine (i.e. the inverse cosine) of a given @@ -1011,11 +1055,11 @@ func (fn *formulaFuncs) ACOS(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "ACOS requires 1 numeric argument") } - val, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + arg := argsList.Front().Value.(formulaArg).ToNumber() + if arg.Type == ArgError { + return arg } - return newNumberFormulaArg(math.Acos(val)) + return newNumberFormulaArg(math.Acos(arg.Number)) } // ACOSH function calculates the inverse hyperbolic cosine of a supplied number. @@ -1027,11 +1071,11 @@ func (fn *formulaFuncs) ACOSH(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "ACOSH requires 1 numeric argument") } - val, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + arg := argsList.Front().Value.(formulaArg).ToNumber() + if arg.Type == ArgError { + return arg } - return newNumberFormulaArg(math.Acosh(val)) + return newNumberFormulaArg(math.Acosh(arg.Number)) } // ACOT function calculates the arccotangent (i.e. the inverse cotangent) of a @@ -1044,11 +1088,11 @@ func (fn *formulaFuncs) ACOT(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "ACOT requires 1 numeric argument") } - val, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + arg := argsList.Front().Value.(formulaArg).ToNumber() + if arg.Type == ArgError { + return arg } - return newNumberFormulaArg(math.Pi/2 - math.Atan(val)) + return newNumberFormulaArg(math.Pi/2 - math.Atan(arg.Number)) } // ACOTH function calculates the hyperbolic arccotangent (coth) of a supplied @@ -1060,11 +1104,11 @@ func (fn *formulaFuncs) ACOTH(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "ACOTH requires 1 numeric argument") } - val, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + arg := argsList.Front().Value.(formulaArg).ToNumber() + if arg.Type == ArgError { + return arg } - return newNumberFormulaArg(math.Atanh(1 / val)) + return newNumberFormulaArg(math.Atanh(1 / arg.Number)) } // ARABIC function converts a Roman numeral into an Arabic numeral. The syntax @@ -1110,11 +1154,11 @@ func (fn *formulaFuncs) ASIN(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "ASIN requires 1 numeric argument") } - val, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + arg := argsList.Front().Value.(formulaArg).ToNumber() + if arg.Type == ArgError { + return arg } - return newNumberFormulaArg(math.Asin(val)) + return newNumberFormulaArg(math.Asin(arg.Number)) } // ASINH function calculates the inverse hyperbolic sine of a supplied number. @@ -1126,11 +1170,11 @@ func (fn *formulaFuncs) ASINH(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "ASINH requires 1 numeric argument") } - val, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + arg := argsList.Front().Value.(formulaArg).ToNumber() + if arg.Type == ArgError { + return arg } - return newNumberFormulaArg(math.Asinh(val)) + return newNumberFormulaArg(math.Asinh(arg.Number)) } // ATAN function calculates the arctangent (i.e. the inverse tangent) of a @@ -1143,11 +1187,11 @@ func (fn *formulaFuncs) ATAN(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "ATAN requires 1 numeric argument") } - val, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + arg := argsList.Front().Value.(formulaArg).ToNumber() + if arg.Type == ArgError { + return arg } - return newNumberFormulaArg(math.Atan(val)) + return newNumberFormulaArg(math.Atan(arg.Number)) } // ATANH function calculates the inverse hyperbolic tangent of a supplied @@ -1159,11 +1203,11 @@ func (fn *formulaFuncs) ATANH(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "ATANH requires 1 numeric argument") } - val, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + arg := argsList.Front().Value.(formulaArg).ToNumber() + if arg.Type == ArgError { + return arg } - return newNumberFormulaArg(math.Atanh(val)) + return newNumberFormulaArg(math.Atanh(arg.Number)) } // ATAN2 function calculates the arctangent (i.e. the inverse tangent) of a @@ -2185,19 +2229,19 @@ func (fn *formulaFuncs) MUNIT(argsList *list.List) (result formulaArg) { if err != nil { return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } - matrix := make([][]float64, 0, dimension) + matrix := make([][]formulaArg, 0, dimension) for i := 0; i < dimension; i++ { - row := make([]float64, dimension) + row := make([]formulaArg, dimension) for j := 0; j < dimension; j++ { if i == j { - row[j] = float64(1.0) + row[j] = newNumberFormulaArg(float64(1.0)) } else { - row[j] = float64(0.0) + row[j] = newNumberFormulaArg(float64(0.0)) } } matrix = append(matrix, row) } - return + return newMatrixFormulaArg(matrix) } // ODD function ounds a supplied number away from zero (i.e. rounds a positive @@ -2704,6 +2748,8 @@ func (fn *formulaFuncs) SUM(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } sum += val + case ArgNumber: + sum += token.Number case ArgMatrix: for _, row := range token.Matrix { for _, value := range row { @@ -3173,7 +3219,7 @@ func (fn *formulaFuncs) AND(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) } } - return newStringFormulaArg(strings.ToUpper(strconv.FormatBool(and))) + return newBoolFormulaArg(and) } // OR function tests a number of supplied conditions and returns either TRUE @@ -3380,7 +3426,7 @@ func (fn *formulaFuncs) IF(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } if argsList.Len() == 1 { - return newStringFormulaArg(strings.ToUpper(strconv.FormatBool(cond))) + return newBoolFormulaArg(cond) } if cond { return newStringFormulaArg(argsList.Front().Next().Value.(formulaArg).String) @@ -3399,7 +3445,6 @@ func (fn *formulaFuncs) IF(argsList *list.List) formulaArg { // // CHOOSE(index_num,value1,[value2],...) // -// TODO: resolve range choose. func (fn *formulaFuncs) CHOOSE(argsList *list.List) formulaArg { if argsList.Len() < 2 { return newErrorFormulaArg(formulaErrorVALUE, "CHOOSE requires 2 arguments") @@ -3415,5 +3460,12 @@ func (fn *formulaFuncs) CHOOSE(argsList *list.List) formulaArg { for i := 0; i < idx; i++ { arg = arg.Next() } - return newStringFormulaArg(arg.Value.(formulaArg).String) + var result formulaArg + switch arg.Value.(formulaArg).Type { + case ArgString: + result = newStringFormulaArg(arg.Value.(formulaArg).String) + case ArgMatrix: + result = newMatrixFormulaArg(arg.Value.(formulaArg).Matrix) + } + return result } diff --git a/calc_test.go b/calc_test.go index d890043061..ad3366ea76 100644 --- a/calc_test.go +++ b/calc_test.go @@ -47,46 +47,55 @@ func TestCalcCellValue(t *testing.T) { "=2>=3": "FALSE", "=1&2": "12", // ABS - "=ABS(-1)": "1", - "=ABS(-6.5)": "6.5", - "=ABS(6.5)": "6.5", - "=ABS(0)": "0", - "=ABS(2-4.5)": "2.5", + "=ABS(-1)": "1", + "=ABS(-6.5)": "6.5", + "=ABS(6.5)": "6.5", + "=ABS(0)": "0", + "=ABS(2-4.5)": "2.5", + "=ABS(ABS(-1))": "1", // ACOS - "=ACOS(-1)": "3.141592653589793", - "=ACOS(0)": "1.570796326794897", + "=ACOS(-1)": "3.141592653589793", + "=ACOS(0)": "1.570796326794897", + "=ACOS(ABS(0))": "1.570796326794897", // ACOSH - "=ACOSH(1)": "0", - "=ACOSH(2.5)": "1.566799236972411", - "=ACOSH(5)": "2.292431669561178", + "=ACOSH(1)": "0", + "=ACOSH(2.5)": "1.566799236972411", + "=ACOSH(5)": "2.292431669561178", + "=ACOSH(ACOSH(5))": "1.471383321536679", // ACOT - "=_xlfn.ACOT(1)": "0.785398163397448", - "=_xlfn.ACOT(-2)": "2.677945044588987", - "=_xlfn.ACOT(0)": "1.570796326794897", + "=_xlfn.ACOT(1)": "0.785398163397448", + "=_xlfn.ACOT(-2)": "2.677945044588987", + "=_xlfn.ACOT(0)": "1.570796326794897", + "=_xlfn.ACOT(_xlfn.ACOT(0))": "0.566911504941009", // ACOTH - "=_xlfn.ACOTH(-5)": "-0.202732554054082", - "=_xlfn.ACOTH(1.1)": "1.522261218861711", - "=_xlfn.ACOTH(2)": "0.549306144334055", + "=_xlfn.ACOTH(-5)": "-0.202732554054082", + "=_xlfn.ACOTH(1.1)": "1.522261218861711", + "=_xlfn.ACOTH(2)": "0.549306144334055", + "=_xlfn.ACOTH(ABS(-2))": "0.549306144334055", // ARABIC `=_xlfn.ARABIC("IV")`: "4", `=_xlfn.ARABIC("-IV")`: "-4", `=_xlfn.ARABIC("MCXX")`: "1120", `=_xlfn.ARABIC("")`: "0", // ASIN - "=ASIN(-1)": "-1.570796326794897", - "=ASIN(0)": "0", + "=ASIN(-1)": "-1.570796326794897", + "=ASIN(0)": "0", + "=ASIN(ASIN(0))": "0", // ASINH - "=ASINH(0)": "0", - "=ASINH(-0.5)": "-0.481211825059604", - "=ASINH(2)": "1.44363547517881", + "=ASINH(0)": "0", + "=ASINH(-0.5)": "-0.481211825059604", + "=ASINH(2)": "1.44363547517881", + "=ASINH(ASINH(0))": "0", // ATAN - "=ATAN(-1)": "-0.785398163397448", - "=ATAN(0)": "0", - "=ATAN(1)": "0.785398163397448", + "=ATAN(-1)": "-0.785398163397448", + "=ATAN(0)": "0", + "=ATAN(1)": "0.785398163397448", + "=ATAN(ATAN(0))": "0", // ATANH - "=ATANH(-0.8)": "-1.09861228866811", - "=ATANH(0)": "0", - "=ATANH(0.5)": "0.549306144334055", + "=ATANH(-0.8)": "-1.09861228866811", + "=ATANH(0)": "0", + "=ATANH(0.5)": "0.549306144334055", + "=ATANH(ATANH(0))": "0", // ATAN2 "=ATAN2(1,1)": "0.785398163397448", "=ATAN2(1,-1)": "-0.785398163397448", @@ -277,7 +286,7 @@ func TestCalcCellValue(t *testing.T) { "=MULTINOMIAL(3,1,2,5)": "27720", `=MULTINOMIAL("",3,1,2,5)`: "27720", // _xlfn.MUNIT - "=_xlfn.MUNIT(4)": "", // not support currently + "=_xlfn.MUNIT(4)": "", // ODD "=ODD(22)": "23", "=ODD(1.22)": "3", @@ -498,6 +507,7 @@ func TestCalcCellValue(t *testing.T) { // CHOOSE "=CHOOSE(4,\"red\",\"blue\",\"green\",\"brown\")": "brown", "=CHOOSE(1,\"red\",\"blue\",\"green\",\"brown\")": "red", + "=SUM(CHOOSE(A2,A1,B1:B2,A1:A3,A1:A4))": "9", } for formula, expected := range mathCalc { f := prepareData() @@ -512,8 +522,9 @@ func TestCalcCellValue(t *testing.T) { `=ABS("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", "=ABS(~)": `cannot convert cell "~" to coordinates: invalid cell name "~"`, // ACOS - "=ACOS()": "ACOS requires 1 numeric argument", - `=ACOS("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + "=ACOS()": "ACOS requires 1 numeric argument", + `=ACOS("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + "=ACOS(ACOS(0))": "#NUM!", // ACOSH "=ACOSH()": "ACOSH requires 1 numeric argument", `=ACOSH("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", @@ -521,8 +532,9 @@ func TestCalcCellValue(t *testing.T) { "=_xlfn.ACOT()": "ACOT requires 1 numeric argument", `=_xlfn.ACOT("X")`: "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()": "ACOTH requires 1 numeric argument", + `=_xlfn.ACOTH("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + "=_xlfn.ACOTH(_xlfn.ACOTH(2))": "#NUM!", // _xlfn.ARABIC "=_xlfn.ARABIC()": "ARABIC requires 1 numeric argument", // ASIN From 4ac32278ff3e8951307274ceb4dd0043d606d5a0 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 31 Jan 2021 13:15:10 +0800 Subject: [PATCH 321/957] Fix hyperbolic cotangent calculation incorrect and unit test --- calc.go | 179 +++++++++++++++++++++++++++------------------------ calc_test.go | 120 ++++++++++++++++++---------------- 2 files changed, 162 insertions(+), 137 deletions(-) diff --git a/calc.go b/calc.go index ecd5f0cdcc..eed0f5d162 100644 --- a/calc.go +++ b/calc.go @@ -344,8 +344,7 @@ func newErrorFormulaArg(formulaError, msg string) formulaArg { // func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error) { var err error - opdStack, optStack, opfStack, opfdStack, opftStack := NewStack(), NewStack(), NewStack(), NewStack(), NewStack() - argsList := list.New() + opdStack, optStack, opfStack, opfdStack, opftStack, argsStack := NewStack(), NewStack(), NewStack(), NewStack(), NewStack(), NewStack() for i := 0; i < len(tokens); i++ { token := tokens[i] @@ -359,6 +358,7 @@ func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error) // function start if token.TType == efp.TokenTypeFunction && token.TSubType == efp.TokenSubTypeStart { opfStack.Push(token) + argsStack.Push(list.New().Init()) continue } @@ -396,7 +396,7 @@ func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error) if result.Type == ArgUnknown { return efp.Token{}, errors.New(formulaErrorVALUE) } - argsList.PushBack(result) + argsStack.Peek().(*list.List).PushBack(result) continue } } @@ -417,7 +417,7 @@ func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error) opftStack.Pop() } if !opfdStack.Empty() { - argsList.PushBack(formulaArg{ + argsStack.Peek().(*list.List).PushBack(formulaArg{ String: opfdStack.Pop().(efp.Token).TValue, Type: ArgString, }) @@ -431,7 +431,7 @@ func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error) // current token is text if token.TType == efp.TokenTypeOperand && token.TSubType == efp.TokenSubTypeText { - argsList.PushBack(formulaArg{ + argsStack.Peek().(*list.List).PushBack(formulaArg{ String: token.TValue, Type: ArgString, }) @@ -450,26 +450,26 @@ func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error) // push opfd to args if opfdStack.Len() > 0 { - argsList.PushBack(formulaArg{ + argsStack.Peek().(*list.List).PushBack(formulaArg{ String: opfdStack.Pop().(efp.Token).TValue, Type: ArgString, }) } - // call formula function to evaluate + arg := callFuncByName(&formulaFuncs{}, strings.NewReplacer( "_xlfn", "", ".", "").Replace(opfStack.Peek().(efp.Token).TValue), - []reflect.Value{reflect.ValueOf(argsList)}) + []reflect.Value{reflect.ValueOf(argsStack.Peek().(*list.List))}) if arg.Type == ArgError { return efp.Token{}, errors.New(arg.Value()) } - argsList.Init() + argsStack.Pop() opfStack.Pop() if opfStack.Len() > 0 { // still in function stack if nextToken.TType == efp.TokenTypeOperatorInfix { // mathematics calculate in formula function opfdStack.Push(efp.Token{TValue: arg.Value(), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) } else { - argsList.PushBack(arg) + argsStack.Peek().(*list.List).PushBack(arg) } } else { opdStack.Push(efp.Token{TValue: arg.Value(), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) @@ -1220,15 +1220,15 @@ func (fn *formulaFuncs) ATAN2(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "ATAN2 requires 2 numeric arguments") } - x, err := strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + x := argsList.Back().Value.(formulaArg).ToNumber() + if x.Type == ArgError { + return x } - y, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + y := argsList.Front().Value.(formulaArg).ToNumber() + if y.Type == ArgError { + return y } - return newNumberFormulaArg(math.Atan2(x, y)) + return newNumberFormulaArg(math.Atan2(x.Number, y.Number)) } // BASE function converts a number into a supplied base (radix), and returns a @@ -1243,16 +1243,17 @@ func (fn *formulaFuncs) BASE(argsList *list.List) formulaArg { if argsList.Len() > 3 { return newErrorFormulaArg(formulaErrorVALUE, "BASE allows at most 3 arguments") } - var number float64 - var radix, minLength int + var minLength int var err error - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + number := argsList.Front().Value.(formulaArg).ToNumber() + if number.Type == ArgError { + return number } - if radix, err = strconv.Atoi(argsList.Front().Next().Value.(formulaArg).String); err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + radix := argsList.Front().Next().Value.(formulaArg).ToNumber() + if radix.Type == ArgError { + return radix } - if radix < 2 || radix > 36 { + if int(radix.Number) < 2 || int(radix.Number) > 36 { return newErrorFormulaArg(formulaErrorVALUE, "radix must be an integer >= 2 and <= 36") } if argsList.Len() > 2 { @@ -1260,7 +1261,7 @@ func (fn *formulaFuncs) BASE(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } } - result := strconv.FormatInt(int64(number), radix) + result := strconv.FormatInt(int64(number.Number), int(radix.Number)) if len(result) < minLength { result = strings.Repeat("0", minLength-len(result)) + result } @@ -1280,18 +1281,20 @@ func (fn *formulaFuncs) CEILING(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorVALUE, "CEILING allows at most 2 arguments") } number, significance, res := 0.0, 1.0, 0.0 - var err error - number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + n := argsList.Front().Value.(formulaArg).ToNumber() + if n.Type == ArgError { + return n } + number = n.Number if number < 0 { significance = -1 } if argsList.Len() > 1 { - if significance, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + s := argsList.Back().Value.(formulaArg).ToNumber() + if s.Type == ArgError { + return s } + significance = s.Number } if significance < 0 && number > 0 { return newErrorFormulaArg(formulaErrorVALUE, "negative sig to CEILING invalid") @@ -1319,25 +1322,30 @@ func (fn *formulaFuncs) CEILINGMATH(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorVALUE, "CEILING.MATH allows at most 3 arguments") } number, significance, mode := 0.0, 1.0, 1.0 - var err error - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + n := argsList.Front().Value.(formulaArg).ToNumber() + if n.Type == ArgError { + return n } + number = n.Number if number < 0 { significance = -1 } if argsList.Len() > 1 { - if significance, err = strconv.ParseFloat(argsList.Front().Next().Value.(formulaArg).String, 64); err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + s := argsList.Front().Next().Value.(formulaArg).ToNumber() + if s.Type == ArgError { + return s } + significance = s.Number } if argsList.Len() == 1 { return newNumberFormulaArg(math.Ceil(number)) } if argsList.Len() > 2 { - if mode, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + m := argsList.Back().Value.(formulaArg).ToNumber() + if m.Type == ArgError { + return m } + mode = m.Number } val, res := math.Modf(number / significance) if res != 0 { @@ -1364,11 +1372,11 @@ func (fn *formulaFuncs) CEILINGPRECISE(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorVALUE, "CEILING.PRECISE allows at most 2 arguments") } number, significance := 0.0, 1.0 - var err error - number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + n := argsList.Front().Value.(formulaArg).ToNumber() + if n.Type == ArgError { + return n } + number = n.Number if number < 0 { significance = -1 } @@ -1376,13 +1384,14 @@ func (fn *formulaFuncs) CEILINGPRECISE(argsList *list.List) formulaArg { return newNumberFormulaArg(math.Ceil(number)) } if argsList.Len() > 1 { - if significance, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + s := argsList.Back().Value.(formulaArg).ToNumber() + if s.Type == ArgError { + return s } + significance = s.Number significance = math.Abs(significance) if significance == 0 { - return newStringFormulaArg("0") + return newNumberFormulaArg(significance) } } val, res := math.Modf(number / significance) @@ -1404,19 +1413,22 @@ func (fn *formulaFuncs) COMBIN(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorVALUE, "COMBIN requires 2 argument") } number, chosen, val := 0.0, 0.0, 1.0 - var err error - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + n := argsList.Front().Value.(formulaArg).ToNumber() + if n.Type == ArgError { + return n } - if chosen, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + number = n.Number + c := argsList.Back().Value.(formulaArg).ToNumber() + if c.Type == ArgError { + return c } + chosen = c.Number number, chosen = math.Trunc(number), math.Trunc(chosen) if chosen > number { return newErrorFormulaArg(formulaErrorVALUE, "COMBIN requires number >= number_chosen") } if chosen == number || chosen == 0 { - return newStringFormulaArg("1") + return newNumberFormulaArg(1) } for c := float64(1); c <= chosen; c++ { val *= (number + 1 - c) / c @@ -1434,21 +1446,22 @@ func (fn *formulaFuncs) COMBINA(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorVALUE, "COMBINA requires 2 argument") } var number, chosen float64 - var err error - number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + n := argsList.Front().Value.(formulaArg).ToNumber() + if n.Type == ArgError { + return n } - chosen, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + number = n.Number + c := argsList.Back().Value.(formulaArg).ToNumber() + if c.Type == ArgError { + return c } + chosen = c.Number number, chosen = math.Trunc(number), math.Trunc(chosen) if number < chosen { return newErrorFormulaArg(formulaErrorVALUE, "COMBINA requires number > number_chosen") } if number == 0 { - return newStringFormulaArg("0") + return newNumberFormulaArg(number) } args := list.New() args.PushBack(formulaArg{ @@ -1471,11 +1484,11 @@ func (fn *formulaFuncs) COS(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "COS requires 1 numeric argument") } - val, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + val := argsList.Front().Value.(formulaArg).ToNumber() + if val.Type == ArgError { + return val } - return newNumberFormulaArg(math.Cos(val)) + return newNumberFormulaArg(math.Cos(val.Number)) } // COSH function calculates the hyperbolic cosine (cosh) of a supplied number. @@ -1487,11 +1500,11 @@ func (fn *formulaFuncs) COSH(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "COSH requires 1 numeric argument") } - val, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + val := argsList.Front().Value.(formulaArg).ToNumber() + if val.Type == ArgError { + return val } - return newNumberFormulaArg(math.Cosh(val)) + return newNumberFormulaArg(math.Cosh(val.Number)) } // COT function calculates the cotangent of a given angle. The syntax of the @@ -1503,14 +1516,14 @@ func (fn *formulaFuncs) COT(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "COT requires 1 numeric argument") } - val, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + val := argsList.Front().Value.(formulaArg).ToNumber() + if val.Type == ArgError { + return val } - if val == 0 { + if val.Number == 0 { return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV) } - return newNumberFormulaArg(math.Tan(val)) + return newNumberFormulaArg(1 / math.Tan(val.Number)) } // COTH function calculates the hyperbolic cotangent (coth) of a supplied @@ -1522,14 +1535,14 @@ func (fn *formulaFuncs) COTH(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "COTH requires 1 numeric argument") } - val, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + val := argsList.Front().Value.(formulaArg).ToNumber() + if val.Type == ArgError { + return val } - if val == 0 { + if val.Number == 0 { return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV) } - return newNumberFormulaArg(math.Tanh(val)) + return newNumberFormulaArg((math.Exp(val.Number) + math.Exp(-val.Number)) / (math.Exp(val.Number) - math.Exp(-val.Number))) } // CSC function calculates the cosecant of a given angle. The syntax of the @@ -1541,14 +1554,14 @@ func (fn *formulaFuncs) CSC(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "CSC requires 1 numeric argument") } - val, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + val := argsList.Front().Value.(formulaArg).ToNumber() + if val.Type == ArgError { + return val } - if val == 0 { + if val.Number == 0 { return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV) } - return newNumberFormulaArg(1 / math.Sin(val)) + return newNumberFormulaArg(1 / math.Sin(val.Number)) } // CSCH function calculates the hyperbolic cosecant (csch) of a supplied diff --git a/calc_test.go b/calc_test.go index ad3366ea76..d0b1c64f4a 100644 --- a/calc_test.go +++ b/calc_test.go @@ -97,72 +97,84 @@ func TestCalcCellValue(t *testing.T) { "=ATANH(0.5)": "0.549306144334055", "=ATANH(ATANH(0))": "0", // ATAN2 - "=ATAN2(1,1)": "0.785398163397448", - "=ATAN2(1,-1)": "-0.785398163397448", - "=ATAN2(4,0)": "0", + "=ATAN2(1,1)": "0.785398163397448", + "=ATAN2(1,-1)": "-0.785398163397448", + "=ATAN2(4,0)": "0", + "=ATAN2(4,ATAN2(4,0))": "0", // BASE - "=BASE(12,2)": "1100", - "=BASE(12,2,8)": "00001100", - "=BASE(100000,16)": "186A0", + "=BASE(12,2)": "1100", + "=BASE(12,2,8)": "00001100", + "=BASE(100000,16)": "186A0", + "=BASE(BASE(12,2),16)": "44C", // CEILING - "=CEILING(22.25,0.1)": "22.3", - "=CEILING(22.25,0.5)": "22.5", - "=CEILING(22.25,1)": "23", - "=CEILING(22.25,10)": "30", - "=CEILING(22.25,20)": "40", - "=CEILING(-22.25,-0.1)": "-22.3", - "=CEILING(-22.25,-1)": "-23", - "=CEILING(-22.25,-5)": "-25", - "=CEILING(22.25)": "23", + "=CEILING(22.25,0.1)": "22.3", + "=CEILING(22.25,0.5)": "22.5", + "=CEILING(22.25,1)": "23", + "=CEILING(22.25,10)": "30", + "=CEILING(22.25,20)": "40", + "=CEILING(-22.25,-0.1)": "-22.3", + "=CEILING(-22.25,-1)": "-23", + "=CEILING(-22.25,-5)": "-25", + "=CEILING(22.25)": "23", + "=CEILING(CEILING(22.25,0.1),0.1)": "22.3", // _xlfn.CEILING.MATH - "=_xlfn.CEILING.MATH(15.25,1)": "16", - "=_xlfn.CEILING.MATH(15.25,0.1)": "15.3", - "=_xlfn.CEILING.MATH(15.25,5)": "20", - "=_xlfn.CEILING.MATH(-15.25,1)": "-15", - "=_xlfn.CEILING.MATH(-15.25,1,1)": "-15", // should be 16 - "=_xlfn.CEILING.MATH(-15.25,10)": "-10", - "=_xlfn.CEILING.MATH(-15.25)": "-15", - "=_xlfn.CEILING.MATH(-15.25,-5,-1)": "-10", + "=_xlfn.CEILING.MATH(15.25,1)": "16", + "=_xlfn.CEILING.MATH(15.25,0.1)": "15.3", + "=_xlfn.CEILING.MATH(15.25,5)": "20", + "=_xlfn.CEILING.MATH(-15.25,1)": "-15", + "=_xlfn.CEILING.MATH(-15.25,1,1)": "-15", // should be 16 + "=_xlfn.CEILING.MATH(-15.25,10)": "-10", + "=_xlfn.CEILING.MATH(-15.25)": "-15", + "=_xlfn.CEILING.MATH(-15.25,-5,-1)": "-10", + "=_xlfn.CEILING.MATH(_xlfn.CEILING.MATH(15.25,1),1)": "16", // _xlfn.CEILING.PRECISE - "=_xlfn.CEILING.PRECISE(22.25,0.1)": "22.3", - "=_xlfn.CEILING.PRECISE(22.25,0.5)": "22.5", - "=_xlfn.CEILING.PRECISE(22.25,1)": "23", - "=_xlfn.CEILING.PRECISE(22.25)": "23", - "=_xlfn.CEILING.PRECISE(22.25,10)": "30", - "=_xlfn.CEILING.PRECISE(22.25,0)": "0", - "=_xlfn.CEILING.PRECISE(-22.25,1)": "-22", - "=_xlfn.CEILING.PRECISE(-22.25,-1)": "-22", - "=_xlfn.CEILING.PRECISE(-22.25,5)": "-20", + "=_xlfn.CEILING.PRECISE(22.25,0.1)": "22.3", + "=_xlfn.CEILING.PRECISE(22.25,0.5)": "22.5", + "=_xlfn.CEILING.PRECISE(22.25,1)": "23", + "=_xlfn.CEILING.PRECISE(22.25)": "23", + "=_xlfn.CEILING.PRECISE(22.25,10)": "30", + "=_xlfn.CEILING.PRECISE(22.25,0)": "0", + "=_xlfn.CEILING.PRECISE(-22.25,1)": "-22", + "=_xlfn.CEILING.PRECISE(-22.25,-1)": "-22", + "=_xlfn.CEILING.PRECISE(-22.25,5)": "-20", + "=_xlfn.CEILING.PRECISE(_xlfn.CEILING.PRECISE(22.25,0.1),5)": "25", // COMBIN - "=COMBIN(6,1)": "6", - "=COMBIN(6,2)": "15", - "=COMBIN(6,3)": "20", - "=COMBIN(6,4)": "15", - "=COMBIN(6,5)": "6", - "=COMBIN(6,6)": "1", - "=COMBIN(0,0)": "1", + "=COMBIN(6,1)": "6", + "=COMBIN(6,2)": "15", + "=COMBIN(6,3)": "20", + "=COMBIN(6,4)": "15", + "=COMBIN(6,5)": "6", + "=COMBIN(6,6)": "1", + "=COMBIN(0,0)": "1", + "=COMBIN(6,COMBIN(0,0))": "6", // _xlfn.COMBINA - "=_xlfn.COMBINA(6,1)": "6", - "=_xlfn.COMBINA(6,2)": "21", - "=_xlfn.COMBINA(6,3)": "56", - "=_xlfn.COMBINA(6,4)": "126", - "=_xlfn.COMBINA(6,5)": "252", - "=_xlfn.COMBINA(6,6)": "462", - "=_xlfn.COMBINA(0,0)": "0", + "=_xlfn.COMBINA(6,1)": "6", + "=_xlfn.COMBINA(6,2)": "21", + "=_xlfn.COMBINA(6,3)": "56", + "=_xlfn.COMBINA(6,4)": "126", + "=_xlfn.COMBINA(6,5)": "252", + "=_xlfn.COMBINA(6,6)": "462", + "=_xlfn.COMBINA(0,0)": "0", + "=_xlfn.COMBINA(0,_xlfn.COMBINA(0,0))": "0", // COS "=COS(0.785398163)": "0.707106781467586", "=COS(0)": "1", + "=COS(COS(0))": "0.54030230586814", // COSH - "=COSH(0)": "1", - "=COSH(0.5)": "1.127625965206381", - "=COSH(-2)": "3.762195691083632", + "=COSH(0)": "1", + "=COSH(0.5)": "1.127625965206381", + "=COSH(-2)": "3.762195691083632", + "=COSH(COSH(0))": "1.543080634815244", // _xlfn.COT - "=_xlfn.COT(0.785398163397448)": "0.999999999999999", + "=_xlfn.COT(0.785398163397448)": "1.000000000000001", + "=_xlfn.COT(_xlfn.COT(0.45))": "-0.545473116787229", // _xlfn.COTH - "=_xlfn.COTH(-3.14159265358979)": "-0.99627207622075", + "=_xlfn.COTH(-3.14159265358979)": "-1.003741873197322", + "=_xlfn.COTH(_xlfn.COTH(1))": "1.156014018113954", // _xlfn.CSC "=_xlfn.CSC(-6)": "3.578899547254406", "=_xlfn.CSC(1.5707963267949)": "1", + "=_xlfn.CSC(_xlfn.CSC(1))": "1.077851840310882", // _xlfn.CSCH "=_xlfn.CSCH(-3.14159265358979)": "-0.086589537530047", // _xlfn.DECIMAL @@ -558,7 +570,7 @@ func TestCalcCellValue(t *testing.T) { "=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.Atoi: parsing \"X\": invalid syntax", + `=BASE(1,"X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", `=BASE(1,2,"X")`: "strconv.Atoi: parsing \"X\": invalid syntax", // CEILING "=CEILING()": "CEILING requires at least 1 argument", @@ -576,7 +588,7 @@ func TestCalcCellValue(t *testing.T) { "=_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")`: "#VALUE!", + `=_xlfn.CEILING.PRECISE(1,"X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", // COMBIN "=COMBIN()": "COMBIN requires 2 argument", "=COMBIN(-1,1)": "COMBIN requires number >= number_chosen", @@ -1009,7 +1021,7 @@ func TestAND(t *testing.T) { }) fn := formulaFuncs{} result := fn.AND(argsList) - assert.Equal(t, result.String, "TRUE") + assert.Equal(t, result.String, "") assert.Empty(t, result.Error) } From db7b4ee36200df4b4838c2111e81808016b4f6ef Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 1 Feb 2021 00:07:51 +0800 Subject: [PATCH 322/957] update formula functions test --- calc.go | 583 ++++++++++++++++++++++++++------------------------- calc_test.go | 323 +++++++++++++++------------- 2 files changed, 483 insertions(+), 423 deletions(-) diff --git a/calc.go b/calc.go index eed0f5d162..cb7d2f88d0 100644 --- a/calc.go +++ b/calc.go @@ -455,7 +455,7 @@ func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error) Type: ArgString, }) } - + // call formula function to evaluate arg := callFuncByName(&formulaFuncs{}, strings.NewReplacer( "_xlfn", "", ".", "").Replace(opfStack.Peek().(efp.Token).TValue), []reflect.Value{reflect.ValueOf(argsStack.Peek().(*list.List))}) @@ -1573,14 +1573,14 @@ func (fn *formulaFuncs) CSCH(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "CSCH requires 1 numeric argument") } - val, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + val := argsList.Front().Value.(formulaArg).ToNumber() + if val.Type == ArgError { + return val } - if val == 0 { + if val.Number == 0 { return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV) } - return newNumberFormulaArg(1 / math.Sinh(val)) + return newNumberFormulaArg(1 / math.Sinh(val.Number)) } // DECIMAL function converts a text representation of a number in a specified @@ -1618,14 +1618,14 @@ func (fn *formulaFuncs) DEGREES(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "DEGREES requires 1 numeric argument") } - val, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + val := argsList.Front().Value.(formulaArg).ToNumber() + if val.Type == ArgError { + return val } - if val == 0 { + if val.Number == 0 { return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV) } - return newNumberFormulaArg(180.0 / math.Pi * val) + return newNumberFormulaArg(180.0 / math.Pi * val.Number) } // EVEN function rounds a supplied number away from zero (i.e. rounds a @@ -1638,12 +1638,12 @@ func (fn *formulaFuncs) EVEN(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "EVEN requires 1 numeric argument") } - number, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + number := argsList.Front().Value.(formulaArg).ToNumber() + if number.Type == ArgError { + return number } - sign := math.Signbit(number) - m, frac := math.Modf(number / 2) + sign := math.Signbit(number.Number) + m, frac := math.Modf(number.Number / 2) val := m * 2 if frac != 0 { if !sign { @@ -1664,11 +1664,11 @@ func (fn *formulaFuncs) EXP(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "EXP requires 1 numeric argument") } - number, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + number := argsList.Front().Value.(formulaArg).ToNumber() + if number.Type == ArgError { + return number } - return newStringFormulaArg(strings.ToUpper(fmt.Sprintf("%g", math.Exp(number)))) + return newStringFormulaArg(strings.ToUpper(fmt.Sprintf("%g", math.Exp(number.Number)))) } // fact returns the factorial of a supplied number. @@ -1689,14 +1689,14 @@ func (fn *formulaFuncs) FACT(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "FACT requires 1 numeric argument") } - number, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + number := argsList.Front().Value.(formulaArg).ToNumber() + if number.Type == ArgError { + return number } - if number < 0 { + if number.Number < 0 { return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) } - return newStringFormulaArg(strings.ToUpper(fmt.Sprintf("%g", fact(number)))) + return newStringFormulaArg(strings.ToUpper(fmt.Sprintf("%g", fact(number.Number)))) } // FACTDOUBLE function returns the double factorial of a supplied number. The @@ -1709,14 +1709,14 @@ func (fn *formulaFuncs) FACTDOUBLE(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorVALUE, "FACTDOUBLE requires 1 numeric argument") } val := 1.0 - number, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + number := argsList.Front().Value.(formulaArg).ToNumber() + if number.Type == ArgError { + return number } - if number < 0 { + if number.Number < 0 { return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) } - for i := math.Trunc(number); i > 1; i -= 2 { + for i := math.Trunc(number.Number); i > 1; i -= 2 { val *= i } return newStringFormulaArg(strings.ToUpper(fmt.Sprintf("%g", val))) @@ -1731,27 +1731,25 @@ func (fn *formulaFuncs) FLOOR(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "FLOOR requires 2 numeric arguments") } - var number, significance float64 - var err error - number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + number := argsList.Front().Value.(formulaArg).ToNumber() + if number.Type == ArgError { + return number } - significance, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + significance := argsList.Back().Value.(formulaArg).ToNumber() + if significance.Type == ArgError { + return significance } - if significance < 0 && number >= 0 { + if significance.Number < 0 && number.Number >= 0 { return newErrorFormulaArg(formulaErrorNUM, "invalid arguments to FLOOR") } - val := number - val, res := math.Modf(val / significance) + val := number.Number + val, res := math.Modf(val / significance.Number) if res != 0 { - if number < 0 && res < 0 { + if number.Number < 0 && res < 0 { val-- } } - return newStringFormulaArg(strings.ToUpper(fmt.Sprintf("%g", val*significance))) + return newStringFormulaArg(strings.ToUpper(fmt.Sprintf("%g", val*significance.Number))) } // FLOORMATH function rounds a supplied number down to a supplied multiple of @@ -1766,30 +1764,33 @@ func (fn *formulaFuncs) FLOORMATH(argsList *list.List) formulaArg { if argsList.Len() > 3 { return newErrorFormulaArg(formulaErrorVALUE, "FLOOR.MATH allows at most 3 arguments") } - number, significance, mode := 0.0, 1.0, 1.0 - var err error - number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + significance, mode := 1.0, 1.0 + number := argsList.Front().Value.(formulaArg).ToNumber() + if number.Type == ArgError { + return number } - if number < 0 { + if number.Number < 0 { significance = -1 } if argsList.Len() > 1 { - if significance, err = strconv.ParseFloat(argsList.Front().Next().Value.(formulaArg).String, 64); err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + s := argsList.Front().Next().Value.(formulaArg).ToNumber() + if s.Type == ArgError { + return s } + significance = s.Number } if argsList.Len() == 1 { - return newNumberFormulaArg(math.Floor(number)) + return newNumberFormulaArg(math.Floor(number.Number)) } if argsList.Len() > 2 { - if mode, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + m := argsList.Back().Value.(formulaArg).ToNumber() + if m.Type == ArgError { + return m } + mode = m.Number } - val, res := math.Modf(number / significance) - if res != 0 && number < 0 && mode > 0 { + val, res := math.Modf(number.Number / significance) + if res != 0 && number.Number < 0 && mode > 0 { val-- } return newNumberFormulaArg(val * significance) @@ -1807,30 +1808,31 @@ func (fn *formulaFuncs) FLOORPRECISE(argsList *list.List) formulaArg { if argsList.Len() > 2 { return newErrorFormulaArg(formulaErrorVALUE, "FLOOR.PRECISE allows at most 2 arguments") } - var number, significance float64 - var err error - number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + var significance float64 + number := argsList.Front().Value.(formulaArg).ToNumber() + if number.Type == ArgError { + return number } - if number < 0 { + if number.Number < 0 { significance = -1 } if argsList.Len() == 1 { - return newNumberFormulaArg(math.Floor(number)) + return newNumberFormulaArg(math.Floor(number.Number)) } if argsList.Len() > 1 { - if significance, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + s := argsList.Back().Value.(formulaArg).ToNumber() + if s.Type == ArgError { + return s } + significance = s.Number significance = math.Abs(significance) if significance == 0 { - return newStringFormulaArg("0") + return newNumberFormulaArg(significance) } } - val, res := math.Modf(number / significance) + val, res := math.Modf(number.Number / significance) if res != 0 { - if number < 0 { + if number.Number < 0 { val-- } } @@ -1871,12 +1873,19 @@ func (fn *formulaFuncs) GCD(argsList *list.List) formulaArg { err error ) for arg := argsList.Front(); arg != nil; arg = arg.Next() { - token := arg.Value.(formulaArg).String - if token == "" { - continue - } - if val, err = strconv.ParseFloat(token, 64); err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + 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()) + } + break + case ArgNumber: + val = token.Number + break } nums = append(nums, val) } @@ -1905,11 +1914,11 @@ func (fn *formulaFuncs) INT(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "INT requires 1 numeric argument") } - number, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + number := argsList.Front().Value.(formulaArg).ToNumber() + if number.Type == ArgError { + return number } - val, frac := math.Modf(number) + val, frac := math.Modf(number.Number) if frac < 0 { val-- } @@ -1929,29 +1938,31 @@ func (fn *formulaFuncs) ISOCEILING(argsList *list.List) formulaArg { if argsList.Len() > 2 { return newErrorFormulaArg(formulaErrorVALUE, "ISO.CEILING allows at most 2 arguments") } - var number, significance float64 - var err error - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + var significance float64 + number := argsList.Front().Value.(formulaArg).ToNumber() + if number.Type == ArgError { + return number } - if number < 0 { + if number.Number < 0 { significance = -1 } if argsList.Len() == 1 { - return newNumberFormulaArg(math.Ceil(number)) + return newNumberFormulaArg(math.Ceil(number.Number)) } if argsList.Len() > 1 { - if significance, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + s := argsList.Back().Value.(formulaArg).ToNumber() + if s.Type == ArgError { + return s } + significance = s.Number significance = math.Abs(significance) if significance == 0 { - return newStringFormulaArg("0") + return newNumberFormulaArg(significance) } } - val, res := math.Modf(number / significance) + val, res := math.Modf(number.Number / significance) if res != 0 { - if number > 0 { + if number.Number > 0 { val++ } } @@ -1983,12 +1994,19 @@ func (fn *formulaFuncs) LCM(argsList *list.List) formulaArg { err error ) for arg := argsList.Front(); arg != nil; arg = arg.Next() { - token := arg.Value.(formulaArg).String - if token == "" { - continue - } - if val, err = strconv.ParseFloat(token, 64); err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + 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()) + } + break + case ArgNumber: + val = token.Number + break } nums = append(nums, val) } @@ -2017,11 +2035,11 @@ func (fn *formulaFuncs) LN(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "LN requires 1 numeric argument") } - number, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + number := argsList.Front().Value.(formulaArg).ToNumber() + if number.Type == ArgError { + return number } - return newNumberFormulaArg(math.Log(number)) + return newNumberFormulaArg(math.Log(number.Number)) } // LOG function calculates the logarithm of a given number, to a supplied @@ -2036,18 +2054,19 @@ func (fn *formulaFuncs) LOG(argsList *list.List) formulaArg { if argsList.Len() > 2 { return newErrorFormulaArg(formulaErrorVALUE, "LOG allows at most 2 arguments") } - number, base := 0.0, 10.0 - var err error - number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + base := 10.0 + number := argsList.Front().Value.(formulaArg).ToNumber() + if number.Type == ArgError { + return number } if argsList.Len() > 1 { - if base, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + b := argsList.Back().Value.(formulaArg).ToNumber() + if b.Type == ArgError { + return b } + base = b.Number } - if number == 0 { + if number.Number == 0 { return newErrorFormulaArg(formulaErrorNUM, formulaErrorDIV) } if base == 0 { @@ -2056,7 +2075,7 @@ func (fn *formulaFuncs) LOG(argsList *list.List) formulaArg { if base == 1 { return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV) } - return newNumberFormulaArg(math.Log(number) / math.Log(base)) + return newNumberFormulaArg(math.Log(number.Number) / math.Log(base)) } // LOG10 function calculates the base 10 logarithm of a given number. The @@ -2068,11 +2087,11 @@ func (fn *formulaFuncs) LOG10(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "LOG10 requires 1 numeric argument") } - number, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + number := argsList.Front().Value.(formulaArg).ToNumber() + if number.Type == ArgError { + return number } - return newNumberFormulaArg(math.Log10(number)) + return newNumberFormulaArg(math.Log10(number.Number)) } // minor function implement a minor of a matrix A is the determinant of some @@ -2153,24 +2172,22 @@ func (fn *formulaFuncs) MOD(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "MOD requires 2 numeric arguments") } - var number, divisor float64 - var err error - number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + number := argsList.Front().Value.(formulaArg).ToNumber() + if number.Type == ArgError { + return number } - divisor, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + divisor := argsList.Back().Value.(formulaArg).ToNumber() + if divisor.Type == ArgError { + return divisor } - if divisor == 0 { + if divisor.Number == 0 { return newErrorFormulaArg(formulaErrorDIV, "MOD divide by zero") } - trunc, rem := math.Modf(number / divisor) + trunc, rem := math.Modf(number.Number / divisor.Number) if rem < 0 { trunc-- } - return newNumberFormulaArg(number - divisor*trunc) + return newNumberFormulaArg(number.Number - divisor.Number*trunc) } // MROUND function rounds a supplied number up or down to the nearest multiple @@ -2182,28 +2199,26 @@ func (fn *formulaFuncs) MROUND(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "MROUND requires 2 numeric arguments") } - var number, multiple float64 - var err error - number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + n := argsList.Front().Value.(formulaArg).ToNumber() + if n.Type == ArgError { + return n } - multiple, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + multiple := argsList.Back().Value.(formulaArg).ToNumber() + if multiple.Type == ArgError { + return multiple } - if multiple == 0 { + if multiple.Number == 0 { return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) } - if multiple < 0 && number > 0 || - multiple > 0 && number < 0 { + if multiple.Number < 0 && n.Number > 0 || + multiple.Number > 0 && n.Number < 0 { return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) } - number, res := math.Modf(number / multiple) + number, res := math.Modf(n.Number / multiple.Number) if math.Trunc(res+0.5) > 0 { number++ } - return newNumberFormulaArg(number * multiple) + return newNumberFormulaArg(number * multiple.Number) } // MULTINOMIAL function calculates the ratio of the factorial of a sum of @@ -2217,11 +2232,18 @@ func (fn *formulaFuncs) MULTINOMIAL(argsList *list.List) formulaArg { var err error for arg := argsList.Front(); arg != nil; arg = arg.Next() { token := arg.Value.(formulaArg) - if token.String == "" { - continue - } - if val, err = strconv.ParseFloat(token.String, 64); err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + switch token.Type { + case ArgString: + if token.String == "" { + continue + } + if val, err = strconv.ParseFloat(token.String, 64); err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + } + break + case ArgNumber: + val = token.Number + break } num += val denom *= fact(val) @@ -2238,18 +2260,18 @@ func (fn *formulaFuncs) MUNIT(argsList *list.List) (result formulaArg) { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "MUNIT requires 1 numeric argument") } - dimension, err := strconv.Atoi(argsList.Front().Value.(formulaArg).String) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + dimension := argsList.Back().Value.(formulaArg).ToNumber() + if dimension.Type == ArgError { + return dimension } - matrix := make([][]formulaArg, 0, dimension) - for i := 0; i < dimension; i++ { - row := make([]formulaArg, dimension) - for j := 0; j < dimension; j++ { + matrix := make([][]formulaArg, 0, int(dimension.Number)) + for i := 0; i < int(dimension.Number); i++ { + row := make([]formulaArg, int(dimension.Number)) + for j := 0; j < int(dimension.Number); j++ { if i == j { - row[j] = newNumberFormulaArg(float64(1.0)) + row[j] = newNumberFormulaArg(1.0) } else { - row[j] = newNumberFormulaArg(float64(0.0)) + row[j] = newNumberFormulaArg(0.0) } } matrix = append(matrix, row) @@ -2267,15 +2289,15 @@ func (fn *formulaFuncs) ODD(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "ODD requires 1 numeric argument") } - number, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + number := argsList.Back().Value.(formulaArg).ToNumber() + if number.Type == ArgError { + return number } - if number == 0 { - return newStringFormulaArg("1") + if number.Number == 0 { + return newNumberFormulaArg(1) } - sign := math.Signbit(number) - m, frac := math.Modf((number - 1) / 2) + sign := math.Signbit(number.Number) + m, frac := math.Modf((number.Number - 1) / 2) val := m*2 + 1 if frac != 0 { if !sign { @@ -2308,23 +2330,21 @@ func (fn *formulaFuncs) POWER(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "POWER requires 2 numeric arguments") } - var x, y float64 - var err error - x, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + x := argsList.Front().Value.(formulaArg).ToNumber() + if x.Type == ArgError { + return x } - y, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + y := argsList.Back().Value.(formulaArg).ToNumber() + if y.Type == ArgError { + return y } - if x == 0 && y == 0 { + if x.Number == 0 && y.Number == 0 { return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) } - if x == 0 && y < 0 { + if x.Number == 0 && y.Number < 0 { return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV) } - return newNumberFormulaArg(math.Pow(x, y)) + return newNumberFormulaArg(math.Pow(x.Number, y.Number)) } // PRODUCT function returns the product (multiplication) of a supplied set of @@ -2348,6 +2368,10 @@ func (fn *formulaFuncs) PRODUCT(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } product = product * val + break + case ArgNumber: + product = product * token.Number + break case ArgMatrix: for _, row := range token.Matrix { for _, value := range row { @@ -2374,20 +2398,18 @@ func (fn *formulaFuncs) QUOTIENT(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "QUOTIENT requires 2 numeric arguments") } - var x, y float64 - var err error - x, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + x := argsList.Front().Value.(formulaArg).ToNumber() + if x.Type == ArgError { + return x } - y, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + y := argsList.Back().Value.(formulaArg).ToNumber() + if y.Type == ArgError { + return y } - if y == 0 { + if y.Number == 0 { return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV) } - return newNumberFormulaArg(math.Trunc(x / y)) + return newNumberFormulaArg(math.Trunc(x.Number / y.Number)) } // RADIANS function converts radians into degrees. The syntax of the function is: @@ -2398,11 +2420,11 @@ func (fn *formulaFuncs) RADIANS(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "RADIANS requires 1 numeric argument") } - angle, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + angle := argsList.Front().Value.(formulaArg).ToNumber() + if angle.Type == ArgError { + return angle } - return newNumberFormulaArg(math.Pi / 180.0 * angle) + return newNumberFormulaArg(math.Pi / 180.0 * angle.Number) } // RAND function generates a random real number between 0 and 1. The syntax of @@ -2426,20 +2448,18 @@ func (fn *formulaFuncs) RANDBETWEEN(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "RANDBETWEEN requires 2 numeric arguments") } - var bottom, top int64 - var err error - bottom, err = strconv.ParseInt(argsList.Front().Value.(formulaArg).String, 10, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + bottom := argsList.Front().Value.(formulaArg).ToNumber() + if bottom.Type == ArgError { + return bottom } - top, err = strconv.ParseInt(argsList.Back().Value.(formulaArg).String, 10, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + top := argsList.Back().Value.(formulaArg).ToNumber() + if top.Type == ArgError { + return top } - if top < bottom { + if top.Number < bottom.Number { return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) } - return newNumberFormulaArg(float64(rand.New(rand.NewSource(time.Now().UnixNano())).Int63n(top-bottom+1) + bottom)) + return newNumberFormulaArg(float64(rand.New(rand.NewSource(time.Now().UnixNano())).Int63n(int64(top.Number-bottom.Number+1)) + int64(bottom.Number))) } // romanNumerals defined a numeral system that originated in ancient Rome and @@ -2469,17 +2489,17 @@ func (fn *formulaFuncs) ROMAN(argsList *list.List) formulaArg { if argsList.Len() > 2 { return newErrorFormulaArg(formulaErrorVALUE, "ROMAN allows at most 2 arguments") } - var number float64 var form int - var err error - number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + number := argsList.Front().Value.(formulaArg).ToNumber() + if number.Type == ArgError { + return number } if argsList.Len() > 1 { - if form, err = strconv.Atoi(argsList.Back().Value.(formulaArg).String); err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + f := argsList.Back().Value.(formulaArg).ToNumber() + if f.Type == ArgError { + return f } + form = int(f.Number) if form < 0 { form = 0 } else if form > 4 { @@ -2497,7 +2517,7 @@ func (fn *formulaFuncs) ROMAN(argsList *list.List) formulaArg { case 4: decimalTable = romanTable[4] } - val := math.Trunc(number) + val := math.Trunc(number.Number) buf := bytes.Buffer{} for _, r := range decimalTable { for val >= r.n { @@ -2553,17 +2573,15 @@ func (fn *formulaFuncs) ROUND(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "ROUND requires 2 numeric arguments") } - var number, digits float64 - var err error - number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + number := argsList.Front().Value.(formulaArg).ToNumber() + if number.Type == ArgError { + return number } - digits, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + digits := argsList.Back().Value.(formulaArg).ToNumber() + if digits.Type == ArgError { + return digits } - return newNumberFormulaArg(fn.round(number, digits, closest)) + return newNumberFormulaArg(fn.round(number.Number, digits.Number, closest)) } // ROUNDDOWN function rounds a supplied number down towards zero, to a @@ -2575,17 +2593,15 @@ func (fn *formulaFuncs) ROUNDDOWN(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "ROUNDDOWN requires 2 numeric arguments") } - var number, digits float64 - var err error - number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + number := argsList.Front().Value.(formulaArg).ToNumber() + if number.Type == ArgError { + return number } - digits, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + digits := argsList.Back().Value.(formulaArg).ToNumber() + if digits.Type == ArgError { + return digits } - return newNumberFormulaArg(fn.round(number, digits, down)) + return newNumberFormulaArg(fn.round(number.Number, digits.Number, down)) } // ROUNDUP function rounds a supplied number up, away from zero, to a @@ -2597,17 +2613,15 @@ func (fn *formulaFuncs) ROUNDUP(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "ROUNDUP requires 2 numeric arguments") } - var number, digits float64 - var err error - number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + number := argsList.Front().Value.(formulaArg).ToNumber() + if number.Type == ArgError { + return number } - digits, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + digits := argsList.Back().Value.(formulaArg).ToNumber() + if digits.Type == ArgError { + return digits } - return newNumberFormulaArg(fn.round(number, digits, up)) + return newNumberFormulaArg(fn.round(number.Number, digits.Number, up)) } // SEC function calculates the secant of a given angle. The syntax of the @@ -2619,11 +2633,11 @@ func (fn *formulaFuncs) SEC(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "SEC requires 1 numeric argument") } - number, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + number := argsList.Front().Value.(formulaArg).ToNumber() + if number.Type == ArgError { + return number } - return newNumberFormulaArg(math.Cos(number)) + return newNumberFormulaArg(math.Cos(number.Number)) } // SECH function calculates the hyperbolic secant (sech) of a supplied angle. @@ -2635,11 +2649,11 @@ func (fn *formulaFuncs) SECH(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "SECH requires 1 numeric argument") } - number, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + number := argsList.Front().Value.(formulaArg).ToNumber() + if number.Type == ArgError { + return number } - return newNumberFormulaArg(1 / math.Cosh(number)) + return newNumberFormulaArg(1 / math.Cosh(number.Number)) } // SIGN function returns the arithmetic sign (+1, -1 or 0) of a supplied @@ -2653,17 +2667,17 @@ func (fn *formulaFuncs) SIGN(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "SIGN requires 1 numeric argument") } - val, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + val := argsList.Front().Value.(formulaArg).ToNumber() + if val.Type == ArgError { + return val } - if val < 0 { - return newStringFormulaArg("-1") + if val.Number < 0 { + return newNumberFormulaArg(-1) } - if val > 0 { - return newStringFormulaArg("1") + if val.Number > 0 { + return newNumberFormulaArg(1) } - return newStringFormulaArg("0") + return newNumberFormulaArg(0) } // SIN function calculates the sine of a given angle. The syntax of the @@ -2675,11 +2689,11 @@ func (fn *formulaFuncs) SIN(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "SIN requires 1 numeric argument") } - number, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + number := argsList.Front().Value.(formulaArg).ToNumber() + if number.Type == ArgError { + return number } - return newNumberFormulaArg(math.Sin(number)) + return newNumberFormulaArg(math.Sin(number.Number)) } // SINH function calculates the hyperbolic sine (sinh) of a supplied number. @@ -2691,11 +2705,11 @@ func (fn *formulaFuncs) SINH(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "SINH requires 1 numeric argument") } - number, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + number := argsList.Front().Value.(formulaArg).ToNumber() + if number.Type == ArgError { + return number } - return newNumberFormulaArg(math.Sinh(number)) + return newNumberFormulaArg(math.Sinh(number.Number)) } // SQRT function calculates the positive square root of a supplied number. The @@ -2707,19 +2721,14 @@ func (fn *formulaFuncs) SQRT(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "SQRT requires 1 numeric argument") } - var res float64 - var value = argsList.Front().Value.(formulaArg).String - if value == "" { - return newStringFormulaArg("0") + value := argsList.Front().Value.(formulaArg).ToNumber() + if value.Type == ArgError { + return value } - res, err := strconv.ParseFloat(value, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) - } - if res < 0 { + if value.Number < 0 { return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) } - return newNumberFormulaArg(math.Sqrt(res)) + return newNumberFormulaArg(math.Sqrt(value.Number)) } // SQRTPI function returns the square root of a supplied number multiplied by @@ -2731,11 +2740,11 @@ func (fn *formulaFuncs) SQRTPI(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "SQRTPI requires 1 numeric argument") } - number, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + number := argsList.Front().Value.(formulaArg).ToNumber() + if number.Type == ArgError { + return number } - return newNumberFormulaArg(math.Sqrt(number * math.Pi)) + return newNumberFormulaArg(math.Sqrt(number.Number * math.Pi)) } // SUM function adds together a supplied set of numbers and returns the sum of @@ -2844,6 +2853,10 @@ func (fn *formulaFuncs) SUMSQ(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } sq += val * val + break + case ArgNumber: + sq += token.Number + break case ArgMatrix: for _, row := range token.Matrix { for _, value := range row { @@ -2870,11 +2883,11 @@ func (fn *formulaFuncs) TAN(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "TAN requires 1 numeric argument") } - number, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + number := argsList.Front().Value.(formulaArg).ToNumber() + if number.Type == ArgError { + return number } - return newNumberFormulaArg(math.Tan(number)) + return newNumberFormulaArg(math.Tan(number.Number)) } // TANH function calculates the hyperbolic tangent (tanh) of a supplied @@ -2886,11 +2899,11 @@ func (fn *formulaFuncs) TANH(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "TANH requires 1 numeric argument") } - number, err := strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + number := argsList.Front().Value.(formulaArg).ToNumber() + if number.Type == ArgError { + return number } - return newNumberFormulaArg(math.Tanh(number)) + return newNumberFormulaArg(math.Tanh(number.Number)) } // TRUNC function truncates a supplied number to a specified number of decimal @@ -2902,29 +2915,31 @@ func (fn *formulaFuncs) TRUNC(argsList *list.List) formulaArg { if argsList.Len() == 0 { return newErrorFormulaArg(formulaErrorVALUE, "TRUNC requires at least 1 argument") } - var number, digits, adjust, rtrim float64 + var digits, adjust, rtrim float64 var err error - number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64) - if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + number := argsList.Front().Value.(formulaArg).ToNumber() + if number.Type == ArgError { + return number } if argsList.Len() > 1 { - if digits, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + d := argsList.Back().Value.(formulaArg).ToNumber() + if d.Type == ArgError { + return d } + digits = d.Number digits = math.Floor(digits) } adjust = math.Pow(10, digits) - x := int((math.Abs(number) - math.Abs(float64(int(number)))) * adjust) + x := int((math.Abs(number.Number) - math.Abs(float64(int(number.Number)))) * adjust) if x != 0 { if rtrim, err = strconv.ParseFloat(strings.TrimRight(strconv.Itoa(x), "0"), 64); err != nil { return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } } if (digits > 0) && (rtrim < adjust/10) { - return newNumberFormulaArg(number) + return newNumberFormulaArg(number.Number) } - return newNumberFormulaArg(float64(int(number*adjust)) / adjust) + return newNumberFormulaArg(float64(int(number.Number*adjust)) / adjust) } // Statistical functions @@ -2976,6 +2991,10 @@ func (fn *formulaFuncs) MEDIAN(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } values = append(values, digits) + break + case ArgNumber: + values = append(values, arg.Number) + break case ArgMatrix: for _, row := range arg.Matrix { for _, value := range row { diff --git a/calc_test.go b/calc_test.go index d0b1c64f4a..d3621c6ac0 100644 --- a/calc_test.go +++ b/calc_test.go @@ -177,6 +177,7 @@ func TestCalcCellValue(t *testing.T) { "=_xlfn.CSC(_xlfn.CSC(1))": "1.077851840310882", // _xlfn.CSCH "=_xlfn.CSCH(-3.14159265358979)": "-0.086589537530047", + "=_xlfn.CSCH(_xlfn.CSCH(1))": "1.044510103955183", // _xlfn.DECIMAL `=_xlfn.DECIMAL("1100",2)`: "12", `=_xlfn.DECIMAL("186A0",16)`: "100000", @@ -184,8 +185,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(1)": "57.29577951308232", + "=DEGREES(2.5)": "143.2394487827058", + "=DEGREES(DEGREES(1))": "3282.806350011744", // EVEN "=EVEN(23)": "24", "=EVEN(2.22)": "4", @@ -193,47 +195,54 @@ func TestCalcCellValue(t *testing.T) { "=EVEN(-0.3)": "-2", "=EVEN(-11)": "-12", "=EVEN(-4)": "-4", + "=EVEN((0))": "0", // EXP - "=EXP(100)": "2.6881171418161356E+43", - "=EXP(0.1)": "1.105170918075648", - "=EXP(0)": "1", - "=EXP(-5)": "0.006737946999085", + "=EXP(100)": "2.6881171418161356E+43", + "=EXP(0.1)": "1.105170918075648", + "=EXP(0)": "1", + "=EXP(-5)": "0.006737946999085", + "=EXP(EXP(0))": "2.718281828459045", // FACT - "=FACT(3)": "6", - "=FACT(6)": "720", - "=FACT(10)": "3.6288E+06", + "=FACT(3)": "6", + "=FACT(6)": "720", + "=FACT(10)": "3.6288E+06", + "=FACT(FACT(3))": "720", // FACTDOUBLE - "=FACTDOUBLE(5)": "15", - "=FACTDOUBLE(8)": "384", - "=FACTDOUBLE(13)": "135135", + "=FACTDOUBLE(5)": "15", + "=FACTDOUBLE(8)": "384", + "=FACTDOUBLE(13)": "135135", + "=FACTDOUBLE(FACTDOUBLE(1))": "1", // FLOOR - "=FLOOR(26.75,0.1)": "26.700000000000003", - "=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,-1)": "-26", - "=FLOOR(-26.75,-5)": "-25", + "=FLOOR(26.75,0.1)": "26.700000000000003", + "=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,-1)": "-26", + "=FLOOR(-26.75,-5)": "-25", + "=FLOOR(FLOOR(26.75,1),1)": "26", // _xlfn.FLOOR.MATH - "=_xlfn.FLOOR.MATH(58.55)": "58", - "=_xlfn.FLOOR.MATH(58.55,0.1)": "58.5", - "=_xlfn.FLOOR.MATH(58.55,5)": "55", - "=_xlfn.FLOOR.MATH(58.55,1,1)": "58", - "=_xlfn.FLOOR.MATH(-58.55,1)": "-59", - "=_xlfn.FLOOR.MATH(-58.55,1,-1)": "-58", - "=_xlfn.FLOOR.MATH(-58.55,1,1)": "-59", // should be -58 - "=_xlfn.FLOOR.MATH(-58.55,10)": "-60", + "=_xlfn.FLOOR.MATH(58.55)": "58", + "=_xlfn.FLOOR.MATH(58.55,0.1)": "58.5", + "=_xlfn.FLOOR.MATH(58.55,5)": "55", + "=_xlfn.FLOOR.MATH(58.55,1,1)": "58", + "=_xlfn.FLOOR.MATH(-58.55,1)": "-59", + "=_xlfn.FLOOR.MATH(-58.55,1,-1)": "-58", + "=_xlfn.FLOOR.MATH(-58.55,1,1)": "-59", // should be -58 + "=_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.5)": "26.5", - "=_xlfn.FLOOR.PRECISE(26.75,1)": "26", - "=_xlfn.FLOOR.PRECISE(26.75)": "26", - "=_xlfn.FLOOR.PRECISE(26.75,10)": "20", - "=_xlfn.FLOOR.PRECISE(26.75,0)": "0", - "=_xlfn.FLOOR.PRECISE(-26.75,1)": "-27", - "=_xlfn.FLOOR.PRECISE(-26.75,-1)": "-27", - "=_xlfn.FLOOR.PRECISE(-26.75,-5)": "-30", + "=_xlfn.FLOOR.PRECISE(26.75,0.1)": "26.700000000000003", + "=_xlfn.FLOOR.PRECISE(26.75,0.5)": "26.5", + "=_xlfn.FLOOR.PRECISE(26.75,1)": "26", + "=_xlfn.FLOOR.PRECISE(26.75)": "26", + "=_xlfn.FLOOR.PRECISE(26.75,10)": "20", + "=_xlfn.FLOOR.PRECISE(26.75,0)": "0", + "=_xlfn.FLOOR.PRECISE(-26.75,1)": "-27", + "=_xlfn.FLOOR.PRECISE(-26.75,-1)": "-27", + "=_xlfn.FLOOR.PRECISE(-26.75,-5)": "-30", + "=_xlfn.FLOOR.PRECISE(_xlfn.FLOOR.PRECISE(26.75),-5)": "25", // GCD "=GCD(0)": "0", `=GCD("",1)`: "1", @@ -242,61 +251,71 @@ func TestCalcCellValue(t *testing.T) { "=GCD(15,10,25)": "5", "=GCD(0,8,12)": "4", "=GCD(7,2)": "1", + "=GCD(1,GCD(1))": "1", // INT "=INT(100.9)": "100", "=INT(5.22)": "5", "=INT(5.99)": "5", "=INT(-6.1)": "-7", "=INT(-100.9)": "-101", + "=INT(INT(0))": "0", // ISO.CEILING - "=ISO.CEILING(22.25)": "23", - "=ISO.CEILING(22.25,1)": "23", - "=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,5)": "-20", - "=ISO.CEILING(-22.25,0)": "0", + "=ISO.CEILING(22.25)": "23", + "=ISO.CEILING(22.25,1)": "23", + "=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,5)": "-20", + "=ISO.CEILING(-22.25,0)": "0", + "=ISO.CEILING(1,ISO.CEILING(1,0))": "0", // LCM - "=LCM(1,5)": "5", - "=LCM(15,10,25)": "150", - "=LCM(1,8,12)": "24", - "=LCM(7,2)": "14", - "=LCM(7)": "7", - `=LCM("",1)`: "1", - `=LCM(0,0)`: "0", + "=LCM(1,5)": "5", + "=LCM(15,10,25)": "150", + "=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", // LN - "=LN(1)": "0", - "=LN(100)": "4.605170185988092", - "=LN(0.5)": "-0.693147180559945", + "=LN(1)": "0", + "=LN(100)": "4.605170185988092", + "=LN(0.5)": "-0.693147180559945", + "=LN(LN(100))": "1.527179625807901", // LOG - "=LOG(64,2)": "6", - "=LOG(100)": "2", - "=LOG(4,0.5)": "-2", - "=LOG(500)": "2.698970004336019", + "=LOG(64,2)": "6", + "=LOG(100)": "2", + "=LOG(4,0.5)": "-2", + "=LOG(500)": "2.698970004336019", + "=LOG(LOG(100))": "0.301029995663981", // LOG10 - "=LOG10(100)": "2", - "=LOG10(1000)": "3", - "=LOG10(0.001)": "-3", - "=LOG10(25)": "1.397940008672038", + "=LOG10(100)": "2", + "=LOG10(1000)": "3", + "=LOG10(0.001)": "-3", + "=LOG10(25)": "1.397940008672038", + "=LOG10(LOG10(100))": "0.301029995663981", // MOD - "=MOD(6,4)": "2", - "=MOD(6,3)": "0", - "=MOD(6,2.5)": "1", - "=MOD(6,1.333)": "0.668", - "=MOD(-10.23,1)": "0.77", + "=MOD(6,4)": "2", + "=MOD(6,3)": "0", + "=MOD(6,2.5)": "1", + "=MOD(6,1.333)": "0.668", + "=MOD(-10.23,1)": "0.77", + "=MOD(MOD(1,1),1)": "0", // MROUND - "=MROUND(333.7,0.5)": "333.5", - "=MROUND(333.8,1)": "334", - "=MROUND(333.3,2)": "334", - "=MROUND(555.3,400)": "400", - "=MROUND(555,1000)": "1000", - "=MROUND(-555.7,-1)": "-556", - "=MROUND(-555.4,-1)": "-555", - "=MROUND(-1555,-1000)": "-2000", + "=MROUND(333.7,0.5)": "333.5", + "=MROUND(333.8,1)": "334", + "=MROUND(333.3,2)": "334", + "=MROUND(555.3,400)": "400", + "=MROUND(555,1000)": "1000", + "=MROUND(-555.7,-1)": "-556", + "=MROUND(-555.4,-1)": "-555", + "=MROUND(-1555,-1000)": "-2000", + "=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("",3,1,2,5)`: "27720", + "=MULTINOMIAL(MULTINOMIAL(1))": "1", // _xlfn.MUNIT "=_xlfn.MUNIT(4)": "", // ODD @@ -307,81 +326,96 @@ func TestCalcCellValue(t *testing.T) { "=ODD(-1.3)": "-3", "=ODD(-10)": "-11", "=ODD(-3)": "-3", + "=ODD(ODD(1))": "1", // PI "=PI()": "3.141592653589793", // POWER - "=POWER(4,2)": "16", + "=POWER(4,2)": "16", + "=POWER(4,POWER(1,1))": "4", // PRODUCT - "=PRODUCT(3,6)": "18", - `=PRODUCT("",3,6)`: "18", + "=PRODUCT(3,6)": "18", + `=PRODUCT("",3,6)`: "18", + `=PRODUCT(PRODUCT(1),3,6)`: "18", // QUOTIENT - "=QUOTIENT(5,2)": "2", - "=QUOTIENT(4.5,3.1)": "1", - "=QUOTIENT(-10,3)": "-3", + "=QUOTIENT(5,2)": "2", + "=QUOTIENT(4.5,3.1)": "1", + "=QUOTIENT(-10,3)": "-3", + "=QUOTIENT(QUOTIENT(1,2),3)": "0", // RADIANS - "=RADIANS(50)": "0.872664625997165", - "=RADIANS(-180)": "-3.141592653589793", - "=RADIANS(180)": "3.141592653589793", - "=RADIANS(360)": "6.283185307179586", + "=RADIANS(50)": "0.872664625997165", + "=RADIANS(-180)": "-3.141592653589793", + "=RADIANS(180)": "3.141592653589793", + "=RADIANS(360)": "6.283185307179586", + "=RADIANS(RADIANS(360))": "0.109662271123215", // ROMAN - "=ROMAN(499,0)": "CDXCIX", - "=ROMAN(1999,0)": "MCMXCIX", - "=ROMAN(1999,1)": "MLMVLIV", - "=ROMAN(1999,2)": "MXMIX", - "=ROMAN(1999,3)": "MVMIV", - "=ROMAN(1999,4)": "MIM", - "=ROMAN(1999,-1)": "MCMXCIX", - "=ROMAN(1999,5)": "MIM", + "=ROMAN(499,0)": "CDXCIX", + "=ROMAN(1999,0)": "MCMXCIX", + "=ROMAN(1999,1)": "MLMVLIV", + "=ROMAN(1999,2)": "MXMIX", + "=ROMAN(1999,3)": "MVMIV", + "=ROMAN(1999,4)": "MIM", + "=ROMAN(1999,-1)": "MCMXCIX", + "=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(99.5,0)": "100", - "=ROUND(-6.3,0)": "-6", - "=ROUND(-100.5,0)": "-101", - "=ROUND(-22.45,1)": "-22.5", - "=ROUND(999,-1)": "1000", - "=ROUND(991,-1)": "990", + "=ROUND(100.319,1)": "100.30000000000001", + "=ROUND(5.28,1)": "5.300000000000001", + "=ROUND(5.9999,3)": "6.000000000000002", + "=ROUND(99.5,0)": "100", + "=ROUND(-6.3,0)": "-6", + "=ROUND(-100.5,0)": "-101", + "=ROUND(-22.45,1)": "-22.5", + "=ROUND(999,-1)": "1000", + "=ROUND(991,-1)": "990", + "=ROUND(ROUND(100,1),-1)": "100", // ROUNDDOWN - "=ROUNDDOWN(99.999,1)": "99.9", - "=ROUNDDOWN(99.999,2)": "99.99000000000002", - "=ROUNDDOWN(99.999,0)": "99", - "=ROUNDDOWN(99.999,-1)": "90", - "=ROUNDDOWN(-99.999,2)": "-99.99000000000002", - "=ROUNDDOWN(-99.999,-1)": "-90", + "=ROUNDDOWN(99.999,1)": "99.9", + "=ROUNDDOWN(99.999,2)": "99.99000000000002", + "=ROUNDDOWN(99.999,0)": "99", + "=ROUNDDOWN(99.999,-1)": "90", + "=ROUNDDOWN(-99.999,2)": "-99.99000000000002", + "=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,0)": "12", - "=ROUNDUP(11.111,-1)": "20", - "=ROUNDUP(-11.111,2)": "-11.120000000000003", - "=ROUNDUP(-11.111,-1)": "-20", + "=ROUNDUP(11.111,1)": "11.200000000000001", + "=ROUNDUP(11.111,2)": "11.120000000000003", + "=ROUNDUP(11.111,0)": "12", + "=ROUNDUP(11.111,-1)": "20", + "=ROUNDUP(-11.111,2)": "-11.120000000000003", + "=ROUNDUP(-11.111,-1)": "-20", + "=ROUNDUP(ROUNDUP(100,1),-1)": "100", // SEC "=_xlfn.SEC(-3.14159265358979)": "-1", "=_xlfn.SEC(0)": "1", + "=_xlfn.SEC(_xlfn.SEC(0))": "0.54030230586814", // SECH "=_xlfn.SECH(-3.14159265358979)": "0.086266738334055", "=_xlfn.SECH(0)": "1", + "=_xlfn.SECH(_xlfn.SECH(0))": "0.648054273663886", // SIGN "=SIGN(9.5)": "1", "=SIGN(-9.5)": "-1", "=SIGN(0)": "0", "=SIGN(0.00000001)": "1", "=SIGN(6-7)": "-1", + "=SIGN(SIGN(-1))": "-1", // SIN "=SIN(0.785398163)": "0.707106780905509", + "=SIN(SIN(1))": "0.745624141665558", // SINH - "=SINH(0)": "0", - "=SINH(0.5)": "0.521095305493747", - "=SINH(-2)": "-3.626860407847019", + "=SINH(0)": "0", + "=SINH(0.5)": "0.521095305493747", + "=SINH(-2)": "-3.626860407847019", + "=SINH(SINH(0))": "0", // SQRT - "=SQRT(4)": "2", - `=SQRT("")`: "0", + "=SQRT(4)": "2", + "=SQRT(SQRT(16))": "2", // SQRTPI - "=SQRTPI(5)": "3.963327297606011", - "=SQRTPI(0.2)": "0.792665459521202", - "=SQRTPI(100)": "17.72453850905516", - "=SQRTPI(0)": "0", + "=SQRTPI(5)": "3.963327297606011", + "=SQRTPI(0.2)": "0.792665459521202", + "=SQRTPI(100)": "17.72453850905516", + "=SQRTPI(0)": "0", + "=SQRTPI(SQRTPI(0))": "0", // SUM "=SUM(1,2)": "3", `=SUM("",1,2)`: "3", @@ -415,27 +449,33 @@ func TestCalcCellValue(t *testing.T) { "=SUMSQ(A1:A4)": "14", "=SUMSQ(A1,B1,A2,B2,6)": "82", `=SUMSQ("",A1,B1,A2,B2,6)`: "82", + `=SUMSQ(1,SUMSQ(1))`: "2", // TAN "=TAN(1.047197551)": "1.732050806782486", "=TAN(0)": "0", + "=TAN(TAN(0))": "0", // TANH - "=TANH(0)": "0", - "=TANH(0.5)": "0.46211715726001", - "=TANH(-2)": "-0.964027580075817", + "=TANH(0)": "0", + "=TANH(0.5)": "0.46211715726001", + "=TANH(-2)": "-0.964027580075817", + "=TANH(TANH(0))": "0", // TRUNC - "=TRUNC(99.999,1)": "99.9", - "=TRUNC(99.999,2)": "99.99", - "=TRUNC(99.999)": "99", - "=TRUNC(99.999,-1)": "90", - "=TRUNC(-99.999,2)": "-99.99", - "=TRUNC(-99.999,-1)": "-90", + "=TRUNC(99.999,1)": "99.9", + "=TRUNC(99.999,2)": "99.99", + "=TRUNC(99.999)": "99", + "=TRUNC(99.999,-1)": "90", + "=TRUNC(-99.999,2)": "-99.99", + "=TRUNC(-99.999,-1)": "-90", + "=TRUNC(TRUNC(1),-1)": "0", // Statistical Functions // COUNTA `=COUNTA()`: "0", `=COUNTA(A1:A5,B2:B5,"text",1,2)`: "8", + `=COUNTA(COUNTA(1))`: "1", // MEDIAN - "=MEDIAN(A1:A5,12)": "2", - "=MEDIAN(A1:A5)": "1.5", + "=MEDIAN(A1:A5,12)": "2", + "=MEDIAN(A1:A5)": "1.5", + "=MEDIAN(A1:A5,MEDIAN(A1:A5,12))": "2", // Information Functions // ISBLANK "=ISBLANK(A1)": "FALSE", @@ -706,8 +746,8 @@ func TestCalcCellValue(t *testing.T) { // MULTINOMIAL `=MULTINOMIAL("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", // _xlfn.MUNIT - "=_xlfn.MUNIT()": "MUNIT requires 1 numeric argument", // not support currently - `=_xlfn.MUNIT("X")`: "strconv.Atoi: parsing \"X\": invalid syntax", // not support currently + "=_xlfn.MUNIT()": "MUNIT requires 1 numeric argument", // not support currently + `=_xlfn.MUNIT("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", // not support currently // ODD "=ODD()": "ODD requires 1 numeric argument", `=ODD("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", @@ -732,8 +772,8 @@ func TestCalcCellValue(t *testing.T) { // RAND "=RAND(1)": "RAND accepts no arguments", // RANDBETWEEN - `=RANDBETWEEN("X",1)`: "strconv.ParseInt: parsing \"X\": invalid syntax", - `=RANDBETWEEN(1,"X")`: "strconv.ParseInt: parsing \"X\": invalid syntax", + `=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!", // ROMAN @@ -770,6 +810,7 @@ func TestCalcCellValue(t *testing.T) { `=SINH("X")`: "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!", // SQRTPI From 1f329e8f968014e26351a729ba7e6e3c846e96db Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 2 Feb 2021 22:23:16 +0800 Subject: [PATCH 323/957] This closes #774, closes #775 and closes #776 - correct adjust calculation chain in duplicate rows - correct adjust defined name in the workbook when delete worksheet - use absolute reference in the auto filters defined name to make it compatible with OpenOffice - API `CoordinatesToCellName` have a new optional param to specify if using an absolute reference format - Fix cyclomatic complexity issue of internal function `newFills` and `parseToken` --- adjust.go | 12 +++-- adjust_test.go | 4 +- calc.go | 116 ++++++++++++++++++++++++++++++------------------- lib.go | 13 ++++-- sheet.go | 20 ++++++++- sheet_test.go | 13 ++++++ styles.go | 4 +- table.go | 6 +-- 8 files changed, 127 insertions(+), 61 deletions(-) diff --git a/adjust.go b/adjust.go index f1ae5360f3..c391cb1c48 100644 --- a/adjust.go +++ b/adjust.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2021 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. // @@ -39,6 +39,7 @@ func (f *File) adjustHelper(sheet string, dir adjustDirection, num, offset int) if err != nil { return err } + sheetID := f.getSheetID(sheet) if dir == rows { f.adjustRowDimensions(ws, num, offset) } else { @@ -51,7 +52,7 @@ func (f *File) adjustHelper(sheet string, dir adjustDirection, num, offset int) if err = f.adjustAutoFilter(ws, dir, num, offset); err != nil { return err } - if err = f.adjustCalcChain(dir, num, offset); err != nil { + if err = f.adjustCalcChain(dir, num, offset, sheetID); err != nil { return err } checkSheet(ws) @@ -197,7 +198,7 @@ func (f *File) adjustAutoFilterHelper(dir adjustDirection, coordinates []int, nu // areaRefToCoordinates provides a function to convert area reference to a // pair of coordinates. func (f *File) areaRefToCoordinates(ref string) ([]int, error) { - rng := strings.Split(ref, ":") + rng := strings.Split(strings.Replace(ref, "$", "", -1), ":") return areaRangeToCoordinates(rng[0], rng[1]) } @@ -310,11 +311,14 @@ func (f *File) deleteMergeCell(ws *xlsxWorksheet, idx int) { // adjustCalcChain provides a function to update the calculation chain when // inserting or deleting rows or columns. -func (f *File) adjustCalcChain(dir adjustDirection, num, offset int) error { +func (f *File) adjustCalcChain(dir adjustDirection, num, offset, sheetID int) error { if f.CalcChain == nil { return nil } for index, c := range f.CalcChain.C { + if c.I != sheetID { + continue + } colNum, rowNum, err := CellNameToCoordinates(c.R) if err != nil { return err diff --git a/adjust_test.go b/adjust_test.go index 3997bd9242..bdbaebea18 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -98,13 +98,13 @@ func TestAdjustCalcChain(t *testing.T) { f := NewFile() f.CalcChain = &xlsxCalcChain{ C: []xlsxCalcChainC{ - {R: "B2"}, + {R: "B2", I: 2}, {R: "B2", I: 1}, }, } assert.NoError(t, f.InsertCol("Sheet1", "A")) assert.NoError(t, f.InsertRow("Sheet1", 1)) - f.CalcChain.C[0].R = "invalid coordinates" + f.CalcChain.C[1].R = "invalid coordinates" assert.EqualError(t, f.InsertCol("Sheet1", "A"), `cannot convert cell "invalid coordinates" to coordinates: invalid cell name "invalid coordinates"`) f.CalcChain = nil assert.NoError(t, f.InsertCol("Sheet1", "A")) diff --git a/calc.go b/calc.go index cb7d2f88d0..fd039187e9 100644 --- a/calc.go +++ b/calc.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2021 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. // @@ -288,7 +288,7 @@ func getPriority(token efp.Token) (pri int) { if token.TValue == "-" && token.TType == efp.TokenTypeOperatorPrefix { pri = 6 } - if token.TSubType == efp.TokenSubTypeStart && token.TType == efp.TokenTypeSubexpression { // ( + if isBeginParenthesesToken(token) { // ( pri = 0 } return @@ -356,7 +356,7 @@ func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error) } // function start - if token.TType == efp.TokenTypeFunction && token.TSubType == efp.TokenSubTypeStart { + if isFunctionStartToken(token) { opfStack.Push(token) argsStack.Push(list.New().Init()) continue @@ -436,44 +436,8 @@ func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error) Type: ArgString, }) } - - // current token is function stop - if token.TType == efp.TokenTypeFunction && token.TSubType == efp.TokenSubTypeStop { - for !opftStack.Empty() { - // calculate trigger - topOpt := opftStack.Peek().(efp.Token) - if err := calculate(opfdStack, topOpt); err != nil { - return efp.Token{}, err - } - opftStack.Pop() - } - - // push opfd to args - if opfdStack.Len() > 0 { - argsStack.Peek().(*list.List).PushBack(formulaArg{ - String: opfdStack.Pop().(efp.Token).TValue, - Type: ArgString, - }) - } - // call formula function to evaluate - arg := callFuncByName(&formulaFuncs{}, strings.NewReplacer( - "_xlfn", "", ".", "").Replace(opfStack.Peek().(efp.Token).TValue), - []reflect.Value{reflect.ValueOf(argsStack.Peek().(*list.List))}) - if arg.Type == ArgError { - return efp.Token{}, errors.New(arg.Value()) - } - argsStack.Pop() - opfStack.Pop() - if opfStack.Len() > 0 { // still in function stack - if nextToken.TType == efp.TokenTypeOperatorInfix { - // mathematics calculate in formula function - opfdStack.Push(efp.Token{TValue: arg.Value(), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) - } else { - argsStack.Peek().(*list.List).PushBack(arg) - } - } else { - opdStack.Push(efp.Token{TValue: arg.Value(), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) - } + if err = evalInfixExpFunc(token, nextToken, opfStack, opdStack, opftStack, opfdStack, argsStack); err != nil { + return efp.Token{}, err } } } @@ -490,6 +454,50 @@ func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error) return opdStack.Peek().(efp.Token), err } +// evalInfixExpFunc evaluate formula function in the infix expression. +func evalInfixExpFunc(token, nextToken efp.Token, opfStack, opdStack, opftStack, opfdStack, argsStack *Stack) error { + if !isFunctionStopToken(token) { + return nil + } + // current token is function stop + for !opftStack.Empty() { + // calculate trigger + topOpt := opftStack.Peek().(efp.Token) + if err := calculate(opfdStack, topOpt); err != nil { + return err + } + opftStack.Pop() + } + + // push opfd to args + if opfdStack.Len() > 0 { + argsStack.Peek().(*list.List).PushBack(formulaArg{ + String: opfdStack.Pop().(efp.Token).TValue, + Type: ArgString, + }) + } + // call formula function to evaluate + arg := callFuncByName(&formulaFuncs{}, strings.NewReplacer( + "_xlfn", "", ".", "").Replace(opfStack.Peek().(efp.Token).TValue), + []reflect.Value{reflect.ValueOf(argsStack.Peek().(*list.List))}) + if arg.Type == ArgError { + return errors.New(arg.Value()) + } + argsStack.Pop() + opfStack.Pop() + if opfStack.Len() > 0 { // still in function stack + if nextToken.TType == efp.TokenTypeOperatorInfix { + // mathematics calculate in formula function + opfdStack.Push(efp.Token{TValue: arg.Value(), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + } else { + argsStack.Peek().(*list.List).PushBack(arg) + } + } else { + opdStack.Push(efp.Token{TValue: arg.Value(), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + } + return nil +} + // calcPow evaluate exponentiation arithmetic operations. func calcPow(rOpd, lOpd string, opdStack *Stack) error { lOpdVal, err := strconv.ParseFloat(lOpd, 64) @@ -722,6 +730,26 @@ func (f *File) parseOperatorPrefixToken(optStack, opdStack *Stack, token efp.Tok return } +// isFunctionStartToken determine if the token is function stop. +func isFunctionStartToken(token efp.Token) bool { + return token.TType == efp.TokenTypeFunction && token.TSubType == efp.TokenSubTypeStart +} + +// isFunctionStopToken determine if the token is function stop. +func isFunctionStopToken(token efp.Token) bool { + return token.TType == efp.TokenTypeFunction && token.TSubType == efp.TokenSubTypeStop +} + +// isBeginParenthesesToken determine if the token is begin parentheses: (. +func isBeginParenthesesToken(token efp.Token) bool { + return token.TType == efp.TokenTypeSubexpression && token.TSubType == efp.TokenSubTypeStart +} + +// isEndParenthesesToken determine if the token is end parentheses: ). +func isEndParenthesesToken(token efp.Token) bool { + return token.TType == efp.TokenTypeSubexpression && token.TSubType == efp.TokenSubTypeStop +} + // isOperatorPrefixToken determine if the token is parse operator prefix // token. func isOperatorPrefixToken(token efp.Token) bool { @@ -771,11 +799,11 @@ func (f *File) parseToken(sheet string, token efp.Token, opdStack, optStack *Sta return err } } - if token.TType == efp.TokenTypeSubexpression && token.TSubType == efp.TokenSubTypeStart { // ( + if isBeginParenthesesToken(token) { // ( optStack.Push(token) } - if token.TType == efp.TokenTypeSubexpression && token.TSubType == efp.TokenSubTypeStop { // ) - for optStack.Peek().(efp.Token).TSubType != efp.TokenSubTypeStart && optStack.Peek().(efp.Token).TType != efp.TokenTypeSubexpression { // != ( + if isEndParenthesesToken(token) { // ) + for !isBeginParenthesesToken(optStack.Peek().(efp.Token)) { // != ( topOpt := optStack.Peek().(efp.Token) if err := calculate(opdStack, topOpt); err != nil { return err diff --git a/lib.go b/lib.go index 3f135128c2..0ebe4683d1 100644 --- a/lib.go +++ b/lib.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2021 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. // @@ -201,13 +201,20 @@ func CellNameToCoordinates(cell string) (int, int, error) { // Example: // // excelize.CoordinatesToCellName(1, 1) // returns "A1", nil +// excelize.CoordinatesToCellName(1, 1, true) // returns "$A$1", nil // -func CoordinatesToCellName(col, row int) (string, error) { +func CoordinatesToCellName(col, row int, abs ...bool) (string, error) { if col < 1 || row < 1 { return "", fmt.Errorf("invalid cell coordinates [%d, %d]", col, row) } + sign := "" + for _, a := range abs { + if a { + sign = "$" + } + } colname, err := ColumnNumberToName(col) - return colname + strconv.Itoa(row), err + return sign + colname + sign + strconv.Itoa(row), err } // boolPtr returns a pointer to a bool with the given value. diff --git a/sheet.go b/sheet.go index 9b80395ef8..bb94f6a1fc 100644 --- a/sheet.go +++ b/sheet.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2021 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. // @@ -500,6 +500,22 @@ func (f *File) DeleteSheet(name string) { wb := f.workbookReader() wbRels := f.relsReader(f.getWorkbookRelsPath()) activeSheetName := f.GetSheetName(f.GetActiveSheetIndex()) + deleteSheetID := f.getSheetID(name) + // Delete and adjust defined names + if wb.DefinedNames != nil { + for idx := 0; idx < len(wb.DefinedNames.DefinedName); idx++ { + dn := wb.DefinedNames.DefinedName[idx] + if dn.LocalSheetID != nil { + sheetID := *dn.LocalSheetID + 1 + if sheetID == deleteSheetID { + wb.DefinedNames.DefinedName = append(wb.DefinedNames.DefinedName[:idx], wb.DefinedNames.DefinedName[idx+1:]...) + idx-- + } else if sheetID > deleteSheetID { + wb.DefinedNames.DefinedName[idx].LocalSheetID = intPtr(*dn.LocalSheetID - 1) + } + } + } + } for idx, sheet := range wb.Sheets.Sheet { if sheet.Name == sheetName { wb.Sheets.Sheet = append(wb.Sheets.Sheet[:idx], wb.Sheets.Sheet[idx+1:]...) @@ -517,7 +533,7 @@ func (f *File) DeleteSheet(name string) { } target := f.deleteSheetFromWorkbookRels(sheet.ID) f.deleteSheetFromContentTypes(target) - f.deleteCalcChain(sheet.SheetID, "") // Delete CalcChain + f.deleteCalcChain(sheet.SheetID, "") delete(f.sheetMap, sheetName) delete(f.XLSX, sheetXML) delete(f.XLSX, rels) diff --git a/sheet_test.go b/sheet_test.go index d68327e93c..f218da73ea 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -383,6 +383,19 @@ func TestDeleteSheet(t *testing.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") + 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", "")) + f.DeleteSheet("Sheet2") + f.DeleteSheet("Sheet1") + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDeleteSheet2.xlsx"))) } func BenchmarkNewSheet(b *testing.B) { diff --git a/styles.go b/styles.go index 9fd0f1850e..851332b7cc 100644 --- a/styles.go +++ b/styles.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2021 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. // @@ -2410,8 +2410,6 @@ func newFills(style *Style, fg bool) *xlsxFill { gradient.Left = 0.5 gradient.Right = 0.5 gradient.Top = 0.5 - default: - break } var stops []*xlsxGradientFillStop for index, color := range style.Fill.Color { diff --git a/table.go b/table.go index 59f1cfecdd..8775929464 100644 --- a/table.go +++ b/table.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2021 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. // @@ -287,8 +287,8 @@ func (f *File) AutoFilter(sheet, hcell, vcell, format string) error { } formatSet, _ := parseAutoFilterSet(format) - cellStart, _ := CoordinatesToCellName(hcol, hrow) - cellEnd, _ := CoordinatesToCellName(vcol, vrow) + cellStart, _ := CoordinatesToCellName(hcol, hrow, true) + cellEnd, _ := CoordinatesToCellName(vcol, vrow, true) ref, filterDB := cellStart+":"+cellEnd, "_xlnm._FilterDatabase" wb := f.workbookReader() sheetID := f.GetSheetIndex(sheet) From 66d85dae1367c106acd373baa5087e4bd712e3f9 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 4 Feb 2021 23:57:07 +0800 Subject: [PATCH 324/957] init new formula function: VLOOKUP --- calc.go | 256 ++++++++++++++++++++++++++++++++++++++++++++++++++- calc_test.go | 45 ++++++++- 2 files changed, 294 insertions(+), 7 deletions(-) diff --git a/calc.go b/calc.go index fd039187e9..1d10f629a8 100644 --- a/calc.go +++ b/calc.go @@ -74,6 +74,7 @@ const ( criteriaG criteriaBeg criteriaEnd + criteriaErr ) // formulaCriteria defined formula criteria parser result. @@ -142,6 +143,24 @@ func (fa formulaArg) ToNumber() formulaArg { return newNumberFormulaArg(n) } +// ToBool returns a formula argument with boolean data type. +func (fa formulaArg) ToBool() formulaArg { + var b bool + var err error + switch fa.Type { + case ArgString: + b, err = strconv.ParseBool(fa.String) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + } + case ArgNumber: + if fa.Boolean && fa.Number == 1 { + b = true + } + } + return newBoolFormulaArg(b) +} + // formulaFuncs is the type of the formula functions. type formulaFuncs struct{} @@ -312,6 +331,11 @@ func newMatrixFormulaArg(m [][]formulaArg) formulaArg { return formulaArg{Type: ArgMatrix, Matrix: m} } +// newListFormulaArg create a list formula argument. +func newListFormulaArg(l []formulaArg) formulaArg { + return formulaArg{Type: ArgList, List: l} +} + // newBoolFormulaArg constructs a boolean formula argument. func newBoolFormulaArg(b bool) formulaArg { var n float64 @@ -321,11 +345,17 @@ func newBoolFormulaArg(b bool) formulaArg { return formulaArg{Type: ArgNumber, Number: n, Boolean: true} } -// newErrorFormulaArg create an error formula argument of a given type with a specified error message. +// newErrorFormulaArg create an error formula argument of a given type with a +// specified error message. func newErrorFormulaArg(formulaError, msg string) formulaArg { return formulaArg{Type: ArgError, String: formulaError, Error: msg} } +// newEmptyFormulaArg create an empty formula argument. +func newEmptyFormulaArg() formulaArg { + return formulaArg{Type: ArgEmpty} +} + // evalInfixExp evaluate syntax analysis by given infix expression after // lexical analysis. Evaluate an infix expression containing formulas by // stacks: @@ -428,6 +458,12 @@ func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error) // current token is logical if token.TType == efp.OperatorsInfix && token.TSubType == efp.TokenSubTypeLogical { } + if token.TType == efp.TokenTypeOperand && token.TSubType == efp.TokenSubTypeLogical { + argsStack.Peek().(*list.List).PushBack(formulaArg{ + String: token.TValue, + Type: ArgString, + }) + } // current token is text if token.TType == efp.TokenTypeOperand && token.TSubType == efp.TokenSubTypeText { @@ -841,6 +877,15 @@ func (f *File) parseReference(sheet, reference string) (arg formulaArg, err erro continue } if cr.Col, cr.Row, err = CellNameToCoordinates(tokens[0]); err != nil { + if cr.Col, err = ColumnNameToNumber(tokens[0]); err != nil { + return + } + 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(cellRefs, cellRanges) return } e := refs.Back() @@ -3189,14 +3234,13 @@ func (fn *formulaFuncs) ISNUMBER(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "ISNUMBER requires 1 argument") } - token := argsList.Front().Value.(formulaArg) - result := "FALSE" + token, result := argsList.Front().Value.(formulaArg), false if token.Type == ArgString && token.String != "" { if _, err := strconv.Atoi(token.String); err == nil { - result = "TRUE" + result = true } } - return newStringFormulaArg(result) + return newBoolFormulaArg(result) } // ISODD function tests if a supplied number (or numeric expression) evaluates @@ -3529,3 +3573,205 @@ func (fn *formulaFuncs) CHOOSE(argsList *list.List) formulaArg { } return result } + +// deepMatchRune finds whether the text deep matches/satisfies the pattern +// string. +func deepMatchRune(str, pattern []rune, simple bool) bool { + for len(pattern) > 0 { + switch pattern[0] { + default: + if len(str) == 0 || str[0] != pattern[0] { + return false + } + case '?': + if len(str) == 0 && !simple { + return false + } + case '*': + return deepMatchRune(str, pattern[1:], simple) || + (len(str) > 0 && deepMatchRune(str[1:], pattern, simple)) + } + str = str[1:] + pattern = pattern[1:] + } + return len(str) == 0 && len(pattern) == 0 +} + +// matchPattern finds whether the text matches or satisfies the pattern +// string. The pattern supports '*' and '?' wildcards in the pattern string. +func matchPattern(pattern, name string) (matched bool) { + if pattern == "" { + return name == pattern + } + if pattern == "*" { + return true + } + rname := make([]rune, 0, len(name)) + rpattern := make([]rune, 0, len(pattern)) + for _, r := range name { + rname = append(rname, r) + } + for _, r := range pattern { + rpattern = append(rpattern, r) + } + simple := false // Does extended wildcard '*' and '?' match. + return deepMatchRune(rname, rpattern, simple) +} + +// 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 formulaArg, caseSensitive, exactMatch bool) byte { + if lhs.Type != rhs.Type { + return criteriaErr + } + switch lhs.Type { + case ArgNumber: + if lhs.Number == rhs.Number { + return criteriaEq + } + if lhs.Number < rhs.Number { + return criteriaL + } + return criteriaG + case ArgString: + ls := lhs.String + rs := rhs.String + if !caseSensitive { + ls = strings.ToLower(ls) + rs = strings.ToLower(rs) + } + if exactMatch { + match := matchPattern(rs, ls) + if match { + return criteriaEq + } + return criteriaG + } + return byte(strings.Compare(ls, rs)) + case ArgEmpty: + return criteriaEq + case ArgList: + return compareFormulaArgList(lhs, rhs, caseSensitive, exactMatch) + case ArgMatrix: + return compareFormulaArgMatrix(lhs, rhs, caseSensitive, exactMatch) + } + return criteriaErr +} + +// compareFormulaArgList compares the left-hand sides and the right-hand sides +// list type formula arguments. +func compareFormulaArgList(lhs, rhs formulaArg, caseSensitive, exactMatch bool) byte { + if len(lhs.List) < len(rhs.List) { + return criteriaL + } + if len(lhs.List) > len(rhs.List) { + return criteriaG + } + for arg := range lhs.List { + criteria := compareFormulaArg(lhs.List[arg], rhs.List[arg], caseSensitive, exactMatch) + if criteria != criteriaEq { + return criteria + } + } + return criteriaEq +} + +// compareFormulaArgMatrix compares the left-hand sides and the right-hand sides +// matrix type formula arguments. +func compareFormulaArgMatrix(lhs, rhs formulaArg, caseSensitive, exactMatch bool) byte { + if len(lhs.Matrix) < len(rhs.Matrix) { + return criteriaL + } + if len(lhs.Matrix) > len(rhs.Matrix) { + return criteriaG + } + for i := range lhs.Matrix { + left := lhs.Matrix[i] + right := lhs.Matrix[i] + if len(left) < len(right) { + return criteriaL + } + if len(left) > len(right) { + return criteriaG + } + for arg := range left { + criteria := compareFormulaArg(left[arg], right[arg], caseSensitive, exactMatch) + if criteria != criteriaEq { + return criteria + } + } + } + return criteriaEq +} + +// VLOOKUP function 'looks up' a given value in the left-hand column of a +// 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]) +// +func (fn *formulaFuncs) VLOOKUP(argsList *list.List) formulaArg { + if argsList.Len() < 3 { + return newErrorFormulaArg(formulaErrorVALUE, "VLOOKUP requires at least 3 arguments") + } + if argsList.Len() > 4 { + return newErrorFormulaArg(formulaErrorVALUE, "VLOOKUP requires at most 4 arguments") + } + lookupValue := argsList.Front().Value.(formulaArg) + tableArray := argsList.Front().Next().Value.(formulaArg) + if tableArray.Type != ArgMatrix { + return newErrorFormulaArg(formulaErrorVALUE, "VLOOKUP requires second argument of table array") + } + colIdx := argsList.Front().Next().Next().Value.(formulaArg).ToNumber() + if colIdx.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, "VLOOKUP requires numeric col argument") + } + col, matchIdx, wasExact, exactMatch := int(colIdx.Number)-1, -1, false, false + if argsList.Len() == 4 { + rangeLookup := argsList.Back().Value.(formulaArg).ToBool() + if rangeLookup.Type == ArgError { + return newErrorFormulaArg(formulaErrorVALUE, rangeLookup.Error) + } + if rangeLookup.Number == 0 { + exactMatch = true + } + } +start: + for idx, mtx := range tableArray.Matrix { + if len(mtx) == 0 { + continue + } + lhs := mtx[0] + switch lookupValue.Type { + case ArgNumber: + if !lookupValue.Boolean { + lhs = mtx[0].ToNumber() + if lhs.Type == ArgError { + lhs = mtx[0] + } + } + case ArgMatrix: + lhs = tableArray + } + switch compareFormulaArg(lhs, lookupValue, false, exactMatch) { + case criteriaL: + matchIdx = idx + case criteriaEq: + matchIdx = idx + wasExact = true + break start + } + } + if matchIdx == -1 { + return newErrorFormulaArg(formulaErrorNA, "VLOOKUP no result found") + } + mtx := tableArray.Matrix[matchIdx] + if col < 0 || col >= len(mtx) { + return newErrorFormulaArg(formulaErrorNA, "VLOOKUP has invalid column index") + } + if wasExact || !exactMatch { + return mtx[col] + } + return newErrorFormulaArg(formulaErrorNA, "VLOOKUP no result found") +} diff --git a/calc_test.go b/calc_test.go index d3621c6ac0..881077c873 100644 --- a/calc_test.go +++ b/calc_test.go @@ -560,19 +560,26 @@ func TestCalcCellValue(t *testing.T) { "=CHOOSE(4,\"red\",\"blue\",\"green\",\"brown\")": "brown", "=CHOOSE(1,\"red\",\"blue\",\"green\",\"brown\")": "red", "=SUM(CHOOSE(A2,A1,B1:B2,A1:A3,A1:A4))": "9", + // VLOOKUP + "=VLOOKUP(D2,D:D,1,FALSE)": "Jan", + "=VLOOKUP(D2,D:D,1,TRUE)": "Month", // should be Feb + "=VLOOKUP(INT(36693),F2:F2,1,FALSE)": "36693", + "=VLOOKUP(INT(F2),F3:F9,1)": "32080", + "=VLOOKUP(MUNIT(3),MUNIT(2),1)": "0", // should be 1 + "=VLOOKUP(MUNIT(3),MUNIT(3),1)": "1", } for formula, expected := range mathCalc { f := prepareData() assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) result, err := f.CalcCellValue("Sheet1", "C1") - assert.NoError(t, err) + assert.NoError(t, err, formula) assert.Equal(t, expected, result, formula) } mathCalcError := map[string]string{ // ABS "=ABS()": "ABS requires 1 numeric argument", `=ABS("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", - "=ABS(~)": `cannot convert cell "~" to coordinates: invalid cell name "~"`, + "=ABS(~)": `invalid column name "~"`, // ACOS "=ACOS()": "ACOS requires 1 numeric argument", `=ACOS("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", @@ -907,6 +914,19 @@ func TestCalcCellValue(t *testing.T) { "=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", + // 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,D:D,1,2)": "strconv.ParseBool: parsing \"2\": invalid syntax", + "=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(A1:B2,B2:B3,1)": "VLOOKUP no result found", } for formula, expected := range mathCalcError { f := prepareData() @@ -1085,3 +1105,24 @@ func TestDet(t *testing.T) { {4, 5, 6, 7}, }), float64(0)) } + +func TestCompareFormulaArg(t *testing.T) { + assert.Equal(t, compareFormulaArg(newEmptyFormulaArg(), newEmptyFormulaArg(), false, false), criteriaEq) + lhs := newListFormulaArg([]formulaArg{newEmptyFormulaArg()}) + rhs := newListFormulaArg([]formulaArg{newEmptyFormulaArg(), newEmptyFormulaArg()}) + assert.Equal(t, compareFormulaArg(lhs, rhs, false, false), criteriaL) + assert.Equal(t, compareFormulaArg(rhs, lhs, false, false), criteriaG) + + lhs = newListFormulaArg([]formulaArg{newBoolFormulaArg(true)}) + rhs = newListFormulaArg([]formulaArg{newBoolFormulaArg(true)}) + assert.Equal(t, compareFormulaArg(lhs, rhs, false, false), criteriaEq) + + assert.Equal(t, compareFormulaArg(formulaArg{Type: ArgUnknown}, formulaArg{Type: ArgUnknown}, false, false), criteriaErr) +} + +func TestMatchPattern(t *testing.T) { + assert.True(t, matchPattern("", "")) + assert.True(t, matchPattern("file/*", "file/abc/bcd/def")) + assert.True(t, matchPattern("*", "")) + assert.False(t, matchPattern("file/?", "file/abc/bcd/def")) +} From 2fb135bc94bbb0c487563d166fd24786fab7280a Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 5 Feb 2021 22:52:31 +0800 Subject: [PATCH 325/957] handle end element event in the worksheet row/column iterator XML SAX parser --- col.go | 107 ++++++++++++++++++++++++++++++--------------------- col_test.go | 14 ++++++- rows.go | 107 ++++++++++++++++++++++++++++++--------------------- rows_test.go | 4 ++ 4 files changed, 144 insertions(+), 88 deletions(-) diff --git a/col.go b/col.go index 5d9122910e..9d46733e09 100644 --- a/col.go +++ b/col.go @@ -97,20 +97,20 @@ func (cols *Cols) Rows() ([]string, error) { if token == nil { break } - switch startElement := token.(type) { + switch xmlElement := token.(type) { case xml.StartElement: - inElement = startElement.Name.Local + inElement = xmlElement.Name.Local if inElement == "row" { cellCol = 0 cellRow++ - attrR, _ := attrValToInt("r", startElement.Attr) + attrR, _ := attrValToInt("r", xmlElement.Attr) if attrR != 0 { cellRow = attrR } } if inElement == "c" { cellCol++ - for _, attr := range startElement.Attr { + for _, attr := range xmlElement.Attr { if attr.Name.Local == "r" { if cellCol, cellRow, err = CellNameToCoordinates(attr.Value); err != nil { return rows, err @@ -123,14 +123,59 @@ func (cols *Cols) Rows() ([]string, error) { } if cellCol == cols.curCol { colCell := xlsxC{} - _ = decoder.DecodeElement(&colCell, &startElement) + _ = decoder.DecodeElement(&colCell, &xmlElement) val, _ := colCell.getValueFrom(cols.f, d) rows = append(rows, val) } } + case xml.EndElement: + if xmlElement.Name.Local == "sheetData" { + return rows, err + } + } + } + return rows, err +} + +// columnXMLIterator defined runtime use field for the worksheet column SAX parser. +type columnXMLIterator struct { + err error + inElement string + cols Cols + cellCol, curRow, row int +} + +// columnXMLHandler parse the column XML element of the worksheet. +func columnXMLHandler(colIterator *columnXMLIterator, xmlElement *xml.StartElement) { + colIterator.err = nil + inElement := xmlElement.Name.Local + if inElement == "row" { + colIterator.row++ + for _, attr := range xmlElement.Attr { + if attr.Name.Local == "r" { + if colIterator.curRow, colIterator.err = strconv.Atoi(attr.Value); colIterator.err != nil { + return + } + colIterator.row = colIterator.curRow + } + } + colIterator.cols.totalRow = colIterator.row + colIterator.cellCol = 0 + } + if inElement == "c" { + colIterator.cellCol++ + for _, attr := range xmlElement.Attr { + if attr.Name.Local == "r" { + if colIterator.cellCol, _, colIterator.err = CellNameToCoordinates(attr.Value); colIterator.err != nil { + return + } + } + } + if colIterator.cellCol > colIterator.cols.totalCol { + colIterator.cols.totalCol = colIterator.cellCol } } - return rows, nil + return } // Cols returns a columns iterator, used for streaming reading data for a @@ -161,53 +206,29 @@ func (f *File) Cols(sheet string) (*Cols, error) { output, _ := xml.Marshal(f.Sheet[name]) f.saveFileList(name, f.replaceNameSpaceBytes(name, output)) } - var ( - inElement string - cols Cols - cellCol, curRow, row int - err error - ) - cols.sheetXML = f.readXML(name) - decoder := f.xmlNewDecoder(bytes.NewReader(cols.sheetXML)) + var colIterator columnXMLIterator + colIterator.cols.sheetXML = f.readXML(name) + decoder := f.xmlNewDecoder(bytes.NewReader(colIterator.cols.sheetXML)) for { token, _ := decoder.Token() if token == nil { break } - switch startElement := token.(type) { + switch xmlElement := token.(type) { case xml.StartElement: - inElement = startElement.Name.Local - if inElement == "row" { - row++ - for _, attr := range startElement.Attr { - if attr.Name.Local == "r" { - if curRow, err = strconv.Atoi(attr.Value); err != nil { - return &cols, err - } - row = curRow - } - } - cols.totalRow = row - cellCol = 0 + columnXMLHandler(&colIterator, &xmlElement) + if colIterator.err != nil { + return &colIterator.cols, colIterator.err } - if inElement == "c" { - cellCol++ - for _, attr := range startElement.Attr { - if attr.Name.Local == "r" { - if cellCol, _, err = CellNameToCoordinates(attr.Value); err != nil { - return &cols, err - } - } - } - if cellCol > cols.totalCol { - cols.totalCol = cellCol - } + case xml.EndElement: + if xmlElement.Name.Local == "sheetData" { + colIterator.cols.f = f + colIterator.cols.sheet = trimSheetName(sheet) + return &colIterator.cols, nil } } } - cols.f = f - cols.sheet = trimSheetName(sheet) - return &cols, nil + return &colIterator.cols, nil } // GetColVisible provides a function to get visible of a single column by given diff --git a/col_test.go b/col_test.go index 532f42855e..97c4b7ffb8 100644 --- a/col_test.go +++ b/col_test.go @@ -148,10 +148,20 @@ func TestColsRows(t *testing.T) { }, } - cols.stashCol, cols.curCol = 0, 1 + f = NewFile() + f.XLSX["xl/worksheets/sheet1.xml"] = nil + cols, err = f.Cols("Sheet1") + if !assert.NoError(t, err) { + t.FailNow() + } + f = NewFile() cols, err = f.Cols("Sheet1") + if !assert.NoError(t, err) { + t.FailNow() + } + _, err = cols.Rows() assert.NoError(t, err) - + cols.stashCol, cols.curCol = 0, 1 // Test if token is nil cols.sheetXML = nil _, err = cols.Rows() diff --git a/rows.go b/rows.go index 97cf2f592f..702d8f52d4 100644 --- a/rows.go +++ b/rows.go @@ -78,60 +78,48 @@ func (rows *Rows) Error() error { // Columns return the current row's column values. func (rows *Rows) Columns() ([]string, error) { - var ( - err error - inElement string - attrR, cellCol, row int - columns []string - ) - + var rowIterator rowXMLIterator if rows.stashRow >= rows.curRow { - return columns, err + return rowIterator.columns, rowIterator.err } - - d := rows.f.sharedStringsReader() + rowIterator.rows = rows + rowIterator.d = rows.f.sharedStringsReader() for { token, _ := rows.decoder.Token() if token == nil { break } - switch startElement := token.(type) { + switch xmlElement := token.(type) { case xml.StartElement: - inElement = startElement.Name.Local - if inElement == "row" { - row++ - if attrR, err = attrValToInt("r", startElement.Attr); attrR != 0 { - row = attrR + rowIterator.inElement = xmlElement.Name.Local + if rowIterator.inElement == "row" { + rowIterator.row++ + if rowIterator.attrR, rowIterator.err = attrValToInt("r", xmlElement.Attr); rowIterator.attrR != 0 { + rowIterator.row = rowIterator.attrR } - if row > rows.curRow { - rows.stashRow = row - 1 - return columns, err + if rowIterator.row > rowIterator.rows.curRow { + rowIterator.rows.stashRow = rowIterator.row - 1 + return rowIterator.columns, rowIterator.err } } - if inElement == "c" { - cellCol++ - colCell := xlsxC{} - _ = rows.decoder.DecodeElement(&colCell, &startElement) - if colCell.R != "" { - if cellCol, _, err = CellNameToCoordinates(colCell.R); err != nil { - return columns, err - } - } - blank := cellCol - len(columns) - val, _ := colCell.getValueFrom(rows.f, d) - columns = append(appendSpace(blank, columns), val) + rowXMLHandler(&rowIterator, &xmlElement) + if rowIterator.err != nil { + return rowIterator.columns, rowIterator.err } case xml.EndElement: - inElement = startElement.Name.Local - if row == 0 { - row = rows.curRow + rowIterator.inElement = xmlElement.Name.Local + if rowIterator.row == 0 { + rowIterator.row = rowIterator.rows.curRow } - if inElement == "row" && row+1 < rows.curRow { - return columns, err + if rowIterator.inElement == "row" && rowIterator.row+1 < rowIterator.rows.curRow { + return rowIterator.columns, rowIterator.err + } + if rowIterator.inElement == "sheetData" { + return rowIterator.columns, rowIterator.err } } } - return columns, err + return rowIterator.columns, rowIterator.err } // appendSpace append blank characters to slice by given length and source slice. @@ -151,6 +139,35 @@ func (err ErrSheetNotExist) Error() string { return fmt.Sprintf("sheet %s is not exist", string(err.SheetName)) } +// rowXMLIterator defined runtime use field for the worksheet row SAX parser. +type rowXMLIterator struct { + err error + inElement string + attrR, cellCol, row int + columns []string + rows *Rows + d *xlsxSST +} + +// rowXMLHandler parse the row XML element of the worksheet. +func rowXMLHandler(rowIterator *rowXMLIterator, xmlElement *xml.StartElement) { + rowIterator.err = nil + if rowIterator.inElement == "c" { + rowIterator.cellCol++ + colCell := xlsxC{} + _ = rowIterator.rows.decoder.DecodeElement(&colCell, xmlElement) + if colCell.R != "" { + if rowIterator.cellCol, _, rowIterator.err = CellNameToCoordinates(colCell.R); rowIterator.err != nil { + return + } + } + blank := rowIterator.cellCol - len(rowIterator.columns) + val, _ := colCell.getValueFrom(rowIterator.rows.f, rowIterator.d) + rowIterator.columns = append(appendSpace(blank, rowIterator.columns), val) + } + return +} + // Rows returns a rows iterator, used for streaming reading data for a // worksheet with a large data. For example: // @@ -192,12 +209,12 @@ func (f *File) Rows(sheet string) (*Rows, error) { if token == nil { break } - switch startElement := token.(type) { + switch xmlElement := token.(type) { case xml.StartElement: - inElement = startElement.Name.Local + inElement = xmlElement.Name.Local if inElement == "row" { row++ - for _, attr := range startElement.Attr { + for _, attr := range xmlElement.Attr { if attr.Name.Local == "r" { row, err = strconv.Atoi(attr.Value) if err != nil { @@ -207,12 +224,16 @@ func (f *File) Rows(sheet string) (*Rows, error) { } rows.totalRow = row } + case xml.EndElement: + if xmlElement.Name.Local == "sheetData" { + rows.f = f + rows.sheet = name + rows.decoder = f.xmlNewDecoder(bytes.NewReader(f.readXML(name))) + return &rows, nil + } default: } } - rows.f = f - rows.sheet = name - rows.decoder = f.xmlNewDecoder(bytes.NewReader(f.readXML(name))) return &rows, nil } diff --git a/rows_test.go b/rows_test.go index 73931aa3f5..0e250f6037 100644 --- a/rows_test.go +++ b/rows_test.go @@ -46,6 +46,10 @@ func TestRows(t *testing.T) { f.XLSX["xl/worksheets/sheet1.xml"] = []byte(`1B`) _, err = f.Rows("Sheet1") assert.EqualError(t, err, `strconv.Atoi: parsing "A": invalid syntax`) + + f.XLSX["xl/worksheets/sheet1.xml"] = nil + _, err = f.Rows("Sheet1") + assert.NoError(t, err) } func TestRowsIterator(t *testing.T) { From 30549c5e90884789fff6599d6e773d1ca56ba962 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 8 Feb 2021 18:05:15 +0800 Subject: [PATCH 326/957] fix custom row height check issue --- rows.go | 2 +- rows_test.go | 12 ++++++++++++ sheet.go | 10 +++++----- sheetview.go | 2 ++ stream.go | 5 +++-- 5 files changed, 23 insertions(+), 8 deletions(-) diff --git a/rows.go b/rows.go index 702d8f52d4..75bea47f6b 100644 --- a/rows.go +++ b/rows.go @@ -290,7 +290,7 @@ func (f *File) GetRowHeight(sheet string, row int) (float64, error) { if err != nil { return ht, err } - if ws.SheetFormatPr != nil { + if ws.SheetFormatPr != nil && ws.SheetFormatPr.CustomHeight { ht = ws.SheetFormatPr.DefaultRowHeight } if row > len(ws.SheetData.Row) { diff --git a/rows_test.go b/rows_test.go index 0e250f6037..01804986ca 100644 --- a/rows_test.go +++ b/rows_test.go @@ -126,6 +126,18 @@ func TestRowHeight(t *testing.T) { _, err = f.GetRowHeight("SheetN", 3) assert.EqualError(t, err, "sheet SheetN is not exist") + // Test get row height with custom default row height. + assert.NoError(t, f.SetSheetFormatPr(sheet1, + DefaultRowHeight(30.0), + CustomHeight(true), + )) + height, err = f.GetRowHeight(sheet1, 100) + assert.NoError(t, err) + assert.Equal(t, 30.0, height) + + // Test set row height with custom default row height with prepare XML. + assert.NoError(t, f.SetCellValue(sheet1, "A10", "A10")) + err = f.SaveAs(filepath.Join("test", "TestRowHeight.xlsx")) if !assert.NoError(t, err) { t.FailNow() diff --git a/sheet.go b/sheet.go index bb94f6a1fc..0d6f5d1203 100644 --- a/sheet.go +++ b/sheet.go @@ -860,18 +860,18 @@ func (f *File) searchSheet(name, value string, regSearch bool) (result []string, } break } - switch startElement := token.(type) { + switch xmlElement := token.(type) { case xml.StartElement: - inElement = startElement.Name.Local + inElement = xmlElement.Name.Local if inElement == "row" { - row, err = attrValToInt("r", startElement.Attr) + row, err = attrValToInt("r", xmlElement.Attr) if err != nil { return } } if inElement == "c" { colCell := xlsxC{} - _ = decoder.DecodeElement(&colCell, &startElement) + _ = decoder.DecodeElement(&colCell, &xmlElement) val, _ := colCell.getValueFrom(f, d) if regSearch { regex := regexp.MustCompile(value) @@ -1745,7 +1745,7 @@ func prepareSheetXML(ws *xlsxWorksheet, col int, row int) { sizeHint := 0 var ht float64 var customHeight bool - if ws.SheetFormatPr != nil { + if ws.SheetFormatPr != nil && ws.SheetFormatPr.CustomHeight { ht = ws.SheetFormatPr.DefaultRowHeight customHeight = true } diff --git a/sheetview.go b/sheetview.go index a942fb42d7..0a8fd5c141 100644 --- a/sheetview.go +++ b/sheetview.go @@ -169,6 +169,7 @@ func (f *File) getSheetView(sheet string, viewIndex int) (*xlsxSheetView, error) // ShowRowColHeaders(bool) // ZoomScale(float64) // TopLeftCell(string) +// ShowZeros(bool) // // Example: // @@ -198,6 +199,7 @@ func (f *File) SetSheetViewOptions(name string, viewIndex int, opts ...SheetView // ShowRowColHeaders(bool) // ZoomScale(float64) // TopLeftCell(string) +// ShowZeros(bool) // // Example: // diff --git a/stream.go b/stream.go index bbb1ec197b..e2f7935626 100644 --- a/stream.go +++ b/stream.go @@ -37,8 +37,9 @@ 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. For -// example, set data for worksheet of size 102400 rows x 50 columns with +// 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. +// For example, set data for worksheet of size 102400 rows x 50 columns with // numbers and style: // // file := excelize.NewFile() From 23c73ab527731f9d414e81f7ea15e2ae1a72a290 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 9 Feb 2021 00:12:53 +0800 Subject: [PATCH 327/957] init new formula function: HLOOKUP --- calc.go | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++-- calc_test.go | 25 +++++++++++++++++++-- 2 files changed, 83 insertions(+), 4 deletions(-) diff --git a/calc.go b/calc.go index 1d10f629a8..df2230e374 100644 --- a/calc.go +++ b/calc.go @@ -2214,11 +2214,12 @@ func (fn *formulaFuncs) MDETERM(argsList *list.List) (result formulaArg) { num float64 numMtx = [][]float64{} err error - strMtx = argsList.Front().Value.(formulaArg).Matrix + strMtx [][]formulaArg ) if argsList.Len() < 1 { - return + return newErrorFormulaArg(formulaErrorVALUE, "MDETERM requires at least 1 argument") } + strMtx = argsList.Front().Value.(formulaArg).Matrix var rows = len(strMtx) for _, row := range argsList.Front().Value.(formulaArg).Matrix { if len(row) != rows { @@ -3705,6 +3706,63 @@ func compareFormulaArgMatrix(lhs, rhs formulaArg, caseSensitive, exactMatch bool return criteriaEq } +// HLOOKUP function 'looks up' a given value in the top row of a data array +// (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]) +// +func (fn *formulaFuncs) HLOOKUP(argsList *list.List) formulaArg { + if argsList.Len() < 3 { + return newErrorFormulaArg(formulaErrorVALUE, "HLOOKUP requires at least 3 arguments") + } + if argsList.Len() > 4 { + return newErrorFormulaArg(formulaErrorVALUE, "HLOOKUP requires at most 4 arguments") + } + lookupValue := argsList.Front().Value.(formulaArg) + tableArray := argsList.Front().Next().Value.(formulaArg) + if tableArray.Type != ArgMatrix { + return newErrorFormulaArg(formulaErrorVALUE, "HLOOKUP requires second argument of table array") + } + rowArg := argsList.Front().Next().Next().Value.(formulaArg).ToNumber() + if rowArg.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, "HLOOKUP requires numeric row argument") + } + rowIdx, matchIdx, wasExact, exactMatch := int(rowArg.Number)-1, -1, false, false + if argsList.Len() == 4 { + rangeLookup := argsList.Back().Value.(formulaArg).ToBool() + if rangeLookup.Type == ArgError { + return newErrorFormulaArg(formulaErrorVALUE, rangeLookup.Error) + } + if rangeLookup.Number == 0 { + exactMatch = true + } + } + row := tableArray.Matrix[0] +start: + for idx, mtx := range row { + switch compareFormulaArg(mtx, lookupValue, false, exactMatch) { + case criteriaL: + matchIdx = idx + case criteriaEq: + matchIdx = idx + wasExact = true + break start + } + } + if matchIdx == -1 { + return newErrorFormulaArg(formulaErrorNA, "HLOOKUP no result found") + } + if rowIdx < 0 || rowIdx >= len(tableArray.Matrix) { + return newErrorFormulaArg(formulaErrorNA, "HLOOKUP has invalid row index") + } + row = tableArray.Matrix[rowIdx] + if wasExact || !exactMatch { + return row[matchIdx] + } + return newErrorFormulaArg(formulaErrorNA, "HLOOKUP no result found") +} + // VLOOKUP function 'looks up' a given value in the left-hand column of a // data array (or table), and returns the corresponding value from another // column of the array. The syntax of the function is: diff --git a/calc_test.go b/calc_test.go index 881077c873..c72d78ba98 100644 --- a/calc_test.go +++ b/calc_test.go @@ -450,6 +450,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", // TAN "=TAN(1.047197551)": "1.732050806782486", "=TAN(0)": "0", @@ -560,6 +561,9 @@ func TestCalcCellValue(t *testing.T) { "=CHOOSE(4,\"red\",\"blue\",\"green\",\"brown\")": "brown", "=CHOOSE(1,\"red\",\"blue\",\"green\",\"brown\")": "red", "=SUM(CHOOSE(A2,A1,B1:B2,A1:A3,A1:A4))": "9", + // HLOOKUP + "=HLOOKUP(D2,D2:D8,1,FALSE)": "Jan", + "=HLOOKUP(F3,F3:F8,3,FALSE)": "34440", // should be Feb // VLOOKUP "=VLOOKUP(D2,D:D,1,FALSE)": "Jan", "=VLOOKUP(D2,D:D,1,TRUE)": "Month", // should be Feb @@ -739,6 +743,8 @@ func TestCalcCellValue(t *testing.T) { // LOG10 "=LOG10()": "LOG10 requires 1 numeric argument", `=LOG10("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + // MDETERM + "MDETERM()": "MDETERM requires at least 1 argument", // MOD "=MOD()": "MOD requires 2 numeric arguments", "=MOD(6,0)": "MOD divide by zero", @@ -834,7 +840,8 @@ func TestCalcCellValue(t *testing.T) { // SUMIF "=SUMIF()": "SUMIF requires at least 2 argument", // SUMSQ - `=SUMSQ("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + `=SUMSQ("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + "=SUMSQ(C1:D2)": "strconv.ParseFloat: parsing \"Month\": invalid syntax", // TAN "=TAN()": "TAN requires 1 numeric argument", `=TAN("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", @@ -847,7 +854,8 @@ func TestCalcCellValue(t *testing.T) { `=TRUNC(1,"X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", // Statistical Functions // MEDIAN - "=MEDIAN()": "MEDIAN requires at least 1 argument", + "=MEDIAN()": "MEDIAN requires at least 1 argument", + "=MEDIAN(D1:D2)": "strconv.ParseFloat: parsing \"Month\": invalid syntax", // Information Functions // ISBLANK "=ISBLANK(A1,A2)": "ISBLANK requires 1 argument", @@ -914,6 +922,19 @@ func TestCalcCellValue(t *testing.T) { "=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", + // 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)": "strconv.ParseBool: parsing \"2\": invalid syntax", + "=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", // VLOOKUP "=VLOOKUP()": "VLOOKUP requires at least 3 arguments", "=VLOOKUP(D2,D1,1,FALSE)": "VLOOKUP requires second argument of table array", From 3783d1d01b458a56a4de6aba4a2d10605bfbc876 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 10 Feb 2021 00:04:13 +0800 Subject: [PATCH 328/957] This closes #782, fix unmerge all cells cause corrupted file --- merge.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/merge.go b/merge.go index ec7815f31e..c50eaa3492 100644 --- a/merge.go +++ b/merge.go @@ -148,6 +148,9 @@ func (f *File) UnmergeCell(sheet string, hcell, vcell string) error { } ws.MergeCells.Cells = ws.MergeCells.Cells[:i] ws.MergeCells.Count = len(ws.MergeCells.Cells) + if ws.MergeCells.Count == 0 { + ws.MergeCells = nil + } return nil } From 3648335d7f45d5cf204c32345f47e8938fe86bfb Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 11 Feb 2021 00:07:46 +0800 Subject: [PATCH 329/957] This improves compatibility for worksheet relative XML path and multi rules auto filter --- sheet.go | 18 +++++++++--------- stream_test.go | 2 +- table.go | 16 ++++++++-------- xmlTable.go | 8 ++++---- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/sheet.go b/sheet.go index 0d6f5d1203..b0e29712ad 100644 --- a/sheet.go +++ b/sheet.go @@ -447,17 +447,17 @@ 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 { - content := f.workbookReader() - rels := f.relsReader(f.getWorkbookRelsPath()) maps := map[string]string{} - for _, v := range content.Sheets.Sheet { - for _, rel := range rels.Relationships { + for _, v := range f.workbookReader().Sheets.Sheet { + for _, rel := range f.relsReader(f.getWorkbookRelsPath()).Relationships { if rel.ID == v.ID { - // Construct a target XML as xl/worksheets/sheet%d by split path, compatible with different types of relative paths in workbook.xml.rels, for example: worksheets/sheet%d.xml and /xl/worksheets/sheet%d.xml - pathInfo := strings.Split(rel.Target, "/") - pathInfoLen := len(pathInfo) - if pathInfoLen > 1 { - maps[v.Name] = fmt.Sprintf("xl/%s", strings.Join(pathInfo[pathInfoLen-2:], "/")) + // Construct a target XML as xl/worksheets/sheet%d by split + // path, compatible with different types of relative paths in + // workbook.xml.rels, for example: worksheets/sheet%d.xml + // and /xl/worksheets/sheet%d.xml + path := filepath.ToSlash(strings.TrimPrefix(filepath.Clean(fmt.Sprintf("%s/%s", filepath.Dir(f.getWorkbookPath()), rel.Target)), "/")) + if _, ok := f.XLSX[path]; ok { + maps[v.Name] = path } } } diff --git a/stream_test.go b/stream_test.go index 4f1812efd6..ec7bd08420 100644 --- a/stream_test.go +++ b/stream_test.go @@ -76,7 +76,7 @@ func TestStreamWriter(t *testing.T) { file = NewFile() streamWriter, err = file.NewStreamWriter("Sheet1") assert.NoError(t, err) - for rowID := 10; rowID <= 51200; rowID++ { + for rowID := 10; rowID <= 25600; rowID++ { row := make([]interface{}, 50) for colID := 0; colID < 50; colID++ { row[colID] = rand.Intn(640000) diff --git a/table.go b/table.go index 8775929464..ba8de25c80 100644 --- a/table.go +++ b/table.go @@ -348,9 +348,9 @@ func (f *File) autoFilter(sheet, ref string, refRange, col int, formatSet *forma return fmt.Errorf("incorrect index of column '%s'", formatSet.Column) } - filter.FilterColumn = &xlsxFilterColumn{ + filter.FilterColumn = append(filter.FilterColumn, &xlsxFilterColumn{ ColID: offset, - } + }) re := regexp.MustCompile(`"(?:[^"]|"")*"|\S+`) token := re.FindAllString(formatSet.Expression, -1) if len(token) != 3 && len(token) != 7 { @@ -372,14 +372,14 @@ func (f *File) writeAutoFilter(filter *xlsxAutoFilter, exp []int, tokens []strin // Single equality. var filters []*xlsxFilter filters = append(filters, &xlsxFilter{Val: tokens[0]}) - filter.FilterColumn.Filters = &xlsxFilters{Filter: filters} + 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{} for _, v := range tokens { filters = append(filters, &xlsxFilter{Val: v}) } - filter.FilterColumn.Filters = &xlsxFilters{Filter: filters} + filter.FilterColumn[0].Filters = &xlsxFilters{Filter: filters} } else { // Non default custom filter. expRel := map[int]int{0: 0, 1: 2} @@ -387,7 +387,7 @@ func (f *File) writeAutoFilter(filter *xlsxAutoFilter, exp []int, tokens []strin for k, v := range tokens { f.writeCustomFilter(filter, exp[expRel[k]], v) if k == 1 { - filter.FilterColumn.CustomFilters.And = andRel[exp[k]] + filter.FilterColumn[0].CustomFilters.And = andRel[exp[k]] } } } @@ -408,12 +408,12 @@ func (f *File) writeCustomFilter(filter *xlsxAutoFilter, operator int, val strin Operator: operators[operator], Val: val, } - if filter.FilterColumn.CustomFilters != nil { - filter.FilterColumn.CustomFilters.CustomFilter = append(filter.FilterColumn.CustomFilters.CustomFilter, &customFilter) + if filter.FilterColumn[0].CustomFilters != nil { + filter.FilterColumn[0].CustomFilters.CustomFilter = append(filter.FilterColumn[0].CustomFilters.CustomFilter, &customFilter) } else { customFilters := []*xlsxCustomFilter{} customFilters = append(customFilters, &customFilter) - filter.FilterColumn.CustomFilters = &xlsxCustomFilters{CustomFilter: customFilters} + filter.FilterColumn[0].CustomFilters = &xlsxCustomFilters{CustomFilter: customFilters} } } diff --git a/xmlTable.go b/xmlTable.go index 22d191e349..c48720bd73 100644 --- a/xmlTable.go +++ b/xmlTable.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2021 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. // @@ -46,9 +46,9 @@ type xlsxTable struct { // applied column by column to a table of data in the worksheet. This collection // expresses AutoFilter settings. type xlsxAutoFilter struct { - XMLName xml.Name `xml:"autoFilter"` - Ref string `xml:"ref,attr"` - FilterColumn *xlsxFilterColumn `xml:"filterColumn"` + XMLName xml.Name `xml:"autoFilter"` + Ref string `xml:"ref,attr"` + FilterColumn []*xlsxFilterColumn `xml:"filterColumn"` } // xlsxFilterColumn directly maps the filterColumn element. The filterColumn From ec45d67e59318ad876b38d6ef96402732b601071 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 13 Feb 2021 00:07:52 +0800 Subject: [PATCH 330/957] binary search in range lookup and new formula function: LOOKUP --- LICENSE | 2 +- calc.go | 215 ++++++++++++++++++++++++++++++++++++++++++--------- calc_test.go | 187 +++++++++++++++++++++++++++++++------------- 3 files changed, 315 insertions(+), 89 deletions(-) diff --git a/LICENSE b/LICENSE index e0f34bbc6a..17591f25c4 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 2016-2020 The excelize Authors. +Copyright (c) 2016-2021 The excelize Authors. All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/calc.go b/calc.go index df2230e374..450a78887f 100644 --- a/calc.go +++ b/calc.go @@ -223,6 +223,7 @@ var tokenPriority = map[string]int{ // FLOOR.MATH // FLOOR.PRECISE // GCD +// HLOOKUP // IF // INT // ISBLANK @@ -239,6 +240,7 @@ var tokenPriority = map[string]int{ // LN // LOG // LOG10 +// LOOKUP // LOWER // MDETERM // MEDIAN @@ -275,6 +277,7 @@ var tokenPriority = map[string]int{ // TRIM // TRUNC // UPPER +// VLOOKUP // func (f *File) CalcCellValue(sheet, cell string) (result string, err error) { var ( @@ -2335,8 +2338,8 @@ func (fn *formulaFuncs) MUNIT(argsList *list.List) (result formulaArg) { return newErrorFormulaArg(formulaErrorVALUE, "MUNIT requires 1 numeric argument") } dimension := argsList.Back().Value.(formulaArg).ToNumber() - if dimension.Type == ArgError { - return dimension + if dimension.Type == ArgError || dimension.Number < 0 { + return newErrorFormulaArg(formulaErrorVALUE, dimension.Error) } matrix := make([][]formulaArg, 0, int(dimension.Number)) for i := 0; i < int(dimension.Number); i++ { @@ -3607,8 +3610,7 @@ func matchPattern(pattern, name string) (matched bool) { if pattern == "*" { return true } - rname := make([]rune, 0, len(name)) - rpattern := 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) } @@ -3636,11 +3638,9 @@ func compareFormulaArg(lhs, rhs formulaArg, caseSensitive, exactMatch bool) byte } return criteriaG case ArgString: - ls := lhs.String - rs := rhs.String + ls, rs := lhs.String, rhs.String if !caseSensitive { - ls = strings.ToLower(ls) - rs = strings.ToLower(rs) + ls, rs = strings.ToLower(ls), strings.ToLower(rs) } if exactMatch { match := matchPattern(rs, ls) @@ -3649,7 +3649,15 @@ func compareFormulaArg(lhs, rhs formulaArg, caseSensitive, exactMatch bool) byte } return criteriaG } - return byte(strings.Compare(ls, rs)) + switch strings.Compare(ls, rs) { + case 1: + return criteriaG + case -1: + return criteriaL + case 0: + return criteriaEq + } + return criteriaErr case ArgEmpty: return criteriaEq case ArgList: @@ -3739,16 +3747,29 @@ func (fn *formulaFuncs) HLOOKUP(argsList *list.List) formulaArg { } } row := tableArray.Matrix[0] -start: - for idx, mtx := range row { - switch compareFormulaArg(mtx, lookupValue, false, exactMatch) { - case criteriaL: - matchIdx = idx - case criteriaEq: - matchIdx = idx - wasExact = true - break start + if exactMatch || len(tableArray.Matrix) == TotalRows { + start: + for idx, mtx := range row { + lhs := mtx + switch lookupValue.Type { + case ArgNumber: + if !lookupValue.Boolean { + lhs = mtx.ToNumber() + if lhs.Type == ArgError { + lhs = mtx + } + } + case ArgMatrix: + lhs = tableArray + } + if compareFormulaArg(lhs, lookupValue, false, exactMatch) == criteriaEq { + matchIdx = idx + wasExact = true + break start + } } + } else { + matchIdx, wasExact = hlookupBinarySearch(row, lookupValue) } if matchIdx == -1 { return newErrorFormulaArg(formulaErrorNA, "HLOOKUP no result found") @@ -3795,11 +3816,51 @@ func (fn *formulaFuncs) VLOOKUP(argsList *list.List) formulaArg { exactMatch = true } } -start: - for idx, mtx := range tableArray.Matrix { - if len(mtx) == 0 { - continue + if exactMatch || len(tableArray.Matrix) == TotalRows { + start: + for idx, mtx := range tableArray.Matrix { + lhs := mtx[0] + switch lookupValue.Type { + case ArgNumber: + if !lookupValue.Boolean { + lhs = mtx[0].ToNumber() + if lhs.Type == ArgError { + lhs = mtx[0] + } + } + case ArgMatrix: + lhs = tableArray + } + if compareFormulaArg(lhs, lookupValue, false, exactMatch) == criteriaEq { + matchIdx = idx + wasExact = true + break start + } } + } else { + matchIdx, wasExact = vlookupBinarySearch(tableArray, lookupValue) + } + if matchIdx == -1 { + return newErrorFormulaArg(formulaErrorNA, "VLOOKUP no result found") + } + mtx := tableArray.Matrix[matchIdx] + if col < 0 || col >= len(mtx) { + return newErrorFormulaArg(formulaErrorNA, "VLOOKUP has invalid column index") + } + if wasExact || !exactMatch { + return mtx[col] + } + return newErrorFormulaArg(formulaErrorNA, "VLOOKUP no result found") +} + +// vlookupBinarySearch finds the position of a target value when range lookup +// is TRUE, if the data of table array can't guarantee be sorted, it will +// return wrong result. +func vlookupBinarySearch(tableArray, lookupValue formulaArg) (matchIdx int, wasExact bool) { + var low, high, lastMatchIdx int = 0, len(tableArray.Matrix) - 1, -1 + for low <= high { + var mid int = low + (high-low)/2 + mtx := tableArray.Matrix[mid] lhs := mtx[0] switch lookupValue.Type { case ArgNumber: @@ -3812,24 +3873,106 @@ start: case ArgMatrix: lhs = tableArray } - switch compareFormulaArg(lhs, lookupValue, false, exactMatch) { - case criteriaL: - matchIdx = idx - case criteriaEq: + result := compareFormulaArg(lhs, lookupValue, false, false) + if result == criteriaEq { + matchIdx, wasExact = mid, true + return + } else if result == criteriaG { + high = mid - 1 + } else if result == criteriaL { + matchIdx, low = mid, mid+1 + if lhs.Value() != "" { + lastMatchIdx = matchIdx + } + } else { + return -1, false + } + } + matchIdx, wasExact = lastMatchIdx, true + return +} + +// vlookupBinarySearch finds the position of a target value when range lookup +// is TRUE, if the data of table array can't guarantee be sorted, it will +// return wrong result. +func hlookupBinarySearch(row []formulaArg, lookupValue formulaArg) (matchIdx int, wasExact bool) { + var low, high, lastMatchIdx int = 0, len(row) - 1, -1 + for low <= high { + var mid int = low + (high-low)/2 + mtx := row[mid] + result := compareFormulaArg(mtx, lookupValue, false, false) + if result == criteriaEq { + matchIdx, wasExact = mid, true + return + } else if result == criteriaG { + high = mid - 1 + } else if result == criteriaL { + low, lastMatchIdx = mid+1, mid + } else { + return -1, false + } + } + matchIdx, wasExact = lastMatchIdx, true + return +} + +// LOOKUP function performs an approximate match lookup in a one-column or +// 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]) +// +func (fn *formulaFuncs) LOOKUP(argsList *list.List) formulaArg { + if argsList.Len() < 2 { + return newErrorFormulaArg(formulaErrorVALUE, "LOOKUP requires at least 2 arguments") + } + if argsList.Len() > 3 { + return newErrorFormulaArg(formulaErrorVALUE, "LOOKUP requires at most 3 arguments") + } + lookupValue := argsList.Front().Value.(formulaArg) + lookupVector := argsList.Front().Next().Value.(formulaArg) + if lookupVector.Type != ArgMatrix && lookupVector.Type != ArgList { + return newErrorFormulaArg(formulaErrorVALUE, "LOOKUP requires second argument of table array") + } + cols, matchIdx := lookupCol(lookupVector), -1 + for idx, col := range cols { + lhs := lookupValue + switch col.Type { + case ArgNumber: + lhs = lhs.ToNumber() + if !col.Boolean { + if lhs.Type == ArgError { + lhs = lookupValue + } + } + } + if compareFormulaArg(lhs, col, false, false) == criteriaEq { matchIdx = idx - wasExact = true - break start + break } } - if matchIdx == -1 { - return newErrorFormulaArg(formulaErrorNA, "VLOOKUP no result found") + column := cols + if argsList.Len() == 3 { + column = lookupCol(argsList.Back().Value.(formulaArg)) } - mtx := tableArray.Matrix[matchIdx] - if col < 0 || col >= len(mtx) { - return newErrorFormulaArg(formulaErrorNA, "VLOOKUP has invalid column index") + if matchIdx < 0 || matchIdx >= len(column) { + return newErrorFormulaArg(formulaErrorNA, "LOOKUP no result found") } - if wasExact || !exactMatch { - return mtx[col] + return column[matchIdx] +} + +// lookupCol extract columns for LOOKUP. +func lookupCol(arr formulaArg) []formulaArg { + col := arr.List + if arr.Type == ArgMatrix { + col = nil + for _, r := range arr.Matrix { + if len(r) > 0 { + col = append(col, r[0]) + continue + } + col = append(col, newEmptyFormulaArg()) + } } - return newErrorFormulaArg(formulaErrorNA, "VLOOKUP no result found") + return col } diff --git a/calc_test.go b/calc_test.go index c72d78ba98..b71d822b83 100644 --- a/calc_test.go +++ b/calc_test.go @@ -10,6 +10,17 @@ import ( "github.com/xuri/efp" ) +func prepareCalcData(cellData [][]interface{}) *File { + f := NewFile() + for r, row := range cellData { + for c, value := range row { + cell, _ := CoordinatesToCellName(c+1, r+1) + f.SetCellValue("Sheet1", cell, value) + } + } + return f +} + func TestCalcCellValue(t *testing.T) { cellData := [][]interface{}{ {1, 4, nil, "Month", "Team", "Sales"}, @@ -22,17 +33,6 @@ func TestCalcCellValue(t *testing.T) { {nil, nil, nil, "Feb", "South 1", 32080}, {nil, nil, nil, "Feb", "South 2", 45500}, } - prepareData := func() *File { - f := NewFile() - for r, row := range cellData { - for c, value := range row { - cell, _ := CoordinatesToCellName(c+1, r+1) - assert.NoError(t, f.SetCellValue("Sheet1", cell, value)) - } - } - return f - } - mathCalc := map[string]string{ "=2^3": "8", "=1=1": "TRUE", @@ -562,18 +562,28 @@ func TestCalcCellValue(t *testing.T) { "=CHOOSE(1,\"red\",\"blue\",\"green\",\"brown\")": "red", "=SUM(CHOOSE(A2,A1,B1:B2,A1:A3,A1:A4))": "9", // HLOOKUP - "=HLOOKUP(D2,D2:D8,1,FALSE)": "Jan", - "=HLOOKUP(F3,F3:F8,3,FALSE)": "34440", // should be Feb + "=HLOOKUP(D2,D2:D8,1,FALSE)": "Jan", + "=HLOOKUP(F3,F3:F8,3,FALSE)": "34440", + "=HLOOKUP(INT(F3),F3:F8,3,FALSE)": "34440", + "=HLOOKUP(MUNIT(1),MUNIT(1),1,FALSE)": "1", // VLOOKUP - "=VLOOKUP(D2,D:D,1,FALSE)": "Jan", - "=VLOOKUP(D2,D:D,1,TRUE)": "Month", // should be Feb - "=VLOOKUP(INT(36693),F2:F2,1,FALSE)": "36693", - "=VLOOKUP(INT(F2),F3:F9,1)": "32080", - "=VLOOKUP(MUNIT(3),MUNIT(2),1)": "0", // should be 1 - "=VLOOKUP(MUNIT(3),MUNIT(3),1)": "1", + "=VLOOKUP(D2,D:D,1,FALSE)": "Jan", + "=VLOOKUP(D2,D1:D10,1)": "Jan", + "=VLOOKUP(D2,D1:D11,1)": "Feb", + "=VLOOKUP(D2,D1:D10,1,FALSE)": "Jan", + "=VLOOKUP(INT(36693),F2:F2,1,FALSE)": "36693", + "=VLOOKUP(INT(F2),F3:F9,1)": "32080", + "=VLOOKUP(INT(F2),F3:F9,1,TRUE)": "32080", + "=VLOOKUP(MUNIT(3),MUNIT(3),1)": "0", + "=VLOOKUP(A1,A3:B5,1)": "0", + "=VLOOKUP(MUNIT(1),MUNIT(1),1,FALSE)": "1", + // LOOKUP + "=LOOKUP(F8,F8:F9,F8:F9)": "32080", + "=LOOKUP(F8,F8:F9,D8:D9)": "Feb", + "=LOOKUP(1,MUNIT(1),MUNIT(1))": "1", } for formula, expected := range mathCalc { - f := prepareData() + f := prepareCalcData(cellData) assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) result, err := f.CalcCellValue("Sheet1", "C1") assert.NoError(t, err, formula) @@ -759,8 +769,9 @@ func TestCalcCellValue(t *testing.T) { // MULTINOMIAL `=MULTINOMIAL("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", // _xlfn.MUNIT - "=_xlfn.MUNIT()": "MUNIT requires 1 numeric argument", // not support currently - `=_xlfn.MUNIT("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", // not support currently + "=_xlfn.MUNIT()": "MUNIT requires 1 numeric argument", + `=_xlfn.MUNIT("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + "=_xlfn.MUNIT(-1)": "", // ODD "=ODD()": "ODD requires 1 numeric argument", `=ODD("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", @@ -947,10 +958,15 @@ func TestCalcCellValue(t *testing.T) { "=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(A1:B2,B2:B3,1)": "VLOOKUP no result found", + "=VLOOKUP(1,G1:H2,1,FALSE)": "VLOOKUP no result found", + // 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(D1,MUNIT(1),MUNIT(1))": "LOOKUP no result found", } for formula, expected := range mathCalcError { - f := prepareData() + f := prepareCalcData(cellData) assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) result, err := f.CalcCellValue("Sheet1", "C1") assert.EqualError(t, err, expected, formula) @@ -974,7 +990,7 @@ func TestCalcCellValue(t *testing.T) { "=A1/A2/SUM(A1:A2:B1)*A3": "0.125", } for formula, expected := range referenceCalc { - f := prepareData() + f := prepareCalcData(cellData) assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) result, err := f.CalcCellValue("Sheet1", "C1") assert.NoError(t, err) @@ -988,7 +1004,7 @@ func TestCalcCellValue(t *testing.T) { "=1+SUM(SUM(A1+A2/A4)*(2-3),2)": "#DIV/0!", } for formula, expected := range referenceCalcError { - f := prepareData() + f := prepareCalcData(cellData) assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) result, err := f.CalcCellValue("Sheet1", "C1") assert.EqualError(t, err, expected) @@ -1000,23 +1016,23 @@ func TestCalcCellValue(t *testing.T) { "=RANDBETWEEN(1,2)", } for _, formula := range volatileFuncs { - f := prepareData() + f := prepareCalcData(cellData) assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) _, err := f.CalcCellValue("Sheet1", "C1") assert.NoError(t, err) } // Test get calculated cell value on not formula cell. - f := prepareData() + 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. - f = prepareData() + f = prepareCalcData(cellData) _, err = f.CalcCellValue("SheetN", "A1") assert.EqualError(t, err, "sheet SheetN is not exist") // Test get calculated cell value with not support formula. - f = prepareData() + f = prepareCalcData(cellData) assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "=UNSUPPORT(A1)")) _, err = f.CalcCellValue("Sheet1", "A1") assert.EqualError(t, err, "not support UNSUPPORT function") @@ -1036,24 +1052,13 @@ func TestCalculate(t *testing.T) { assert.EqualError(t, calculate(opd, opt), err) } -func TestCalcCellValueWithDefinedName(t *testing.T) { +func TestCalcWithDefinedName(t *testing.T) { cellData := [][]interface{}{ {"A1 value", "B1 value", nil}, } - prepareData := func() *File { - f := NewFile() - for r, row := range cellData { - for c, value := range row { - cell, _ := CoordinatesToCellName(c+1, r+1) - assert.NoError(t, f.SetCellValue("Sheet1", cell, value)) - } - } - assert.NoError(t, f.SetDefinedName(&DefinedName{Name: "defined_name1", RefersTo: "Sheet1!A1", Scope: "Workbook"})) - assert.NoError(t, f.SetDefinedName(&DefinedName{Name: "defined_name1", RefersTo: "Sheet1!B1", Scope: "Sheet1"})) - - return f - } - f := prepareData() + f := prepareCalcData(cellData) + assert.NoError(t, f.SetDefinedName(&DefinedName{Name: "defined_name1", RefersTo: "Sheet1!A1", Scope: "Workbook"})) + assert.NoError(t, f.SetDefinedName(&DefinedName{Name: "defined_name1", RefersTo: "Sheet1!B1", Scope: "Sheet1"})) assert.NoError(t, f.SetCellFormula("Sheet1", "C1", "=defined_name1")) result, err := f.CalcCellValue("Sheet1", "C1") assert.NoError(t, err) @@ -1061,7 +1066,7 @@ func TestCalcCellValueWithDefinedName(t *testing.T) { assert.Equal(t, "B1 value", result, "=defined_name1") } -func TestCalcPow(t *testing.T) { +func TestCalcArithmeticOperations(t *testing.T) { err := `strconv.ParseFloat: parsing "text": invalid syntax` assert.EqualError(t, calcPow("1", "text", nil), err) assert.EqualError(t, calcPow("text", "1", nil), err) @@ -1085,7 +1090,7 @@ func TestCalcPow(t *testing.T) { assert.EqualError(t, calcDiv("text", "1", nil), err) } -func TestISBLANK(t *testing.T) { +func TestCalcISBLANK(t *testing.T) { argsList := list.New() argsList.PushBack(formulaArg{ Type: ArgUnknown, @@ -1096,7 +1101,7 @@ func TestISBLANK(t *testing.T) { assert.Empty(t, result.Error) } -func TestAND(t *testing.T) { +func TestCalcAND(t *testing.T) { argsList := list.New() argsList.PushBack(formulaArg{ Type: ArgUnknown, @@ -1107,7 +1112,7 @@ func TestAND(t *testing.T) { assert.Empty(t, result.Error) } -func TestOR(t *testing.T) { +func TestCalcOR(t *testing.T) { argsList := list.New() argsList.PushBack(formulaArg{ Type: ArgUnknown, @@ -1118,7 +1123,7 @@ func TestOR(t *testing.T) { assert.Empty(t, result.Error) } -func TestDet(t *testing.T) { +func TestCalcDet(t *testing.T) { assert.Equal(t, det([][]float64{ {1, 2, 3, 4}, {2, 3, 4, 5}, @@ -1127,7 +1132,12 @@ func TestDet(t *testing.T) { }), float64(0)) } -func TestCompareFormulaArg(t *testing.T) { +func TestCalcToBool(t *testing.T) { + b := newBoolFormulaArg(true).ToBool() + assert.Equal(t, b.Boolean, true) + assert.Equal(t, b.Number, 1.0) +} +func TestCalcCompareFormulaArg(t *testing.T) { assert.Equal(t, compareFormulaArg(newEmptyFormulaArg(), newEmptyFormulaArg(), false, false), criteriaEq) lhs := newListFormulaArg([]formulaArg{newEmptyFormulaArg()}) rhs := newListFormulaArg([]formulaArg{newEmptyFormulaArg(), newEmptyFormulaArg()}) @@ -1141,9 +1151,82 @@ func TestCompareFormulaArg(t *testing.T) { assert.Equal(t, compareFormulaArg(formulaArg{Type: ArgUnknown}, formulaArg{Type: ArgUnknown}, false, false), criteriaErr) } -func TestMatchPattern(t *testing.T) { +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("file/?", "file/abc/bcd/def")) } + +func TestCalcVLOOKUP(t *testing.T) { + cellData := [][]interface{}{ + {nil, nil, nil, nil, nil, nil}, + {nil, "Score", "Grade", nil, nil, nil}, + {nil, 0, "F", nil, "Score", 85}, + {nil, 60, "D", nil, "Grade"}, + {nil, 70, "C", nil, nil, nil}, + {nil, 80, "b", nil, nil, nil}, + {nil, 90, "A", nil, nil, nil}, + {nil, 85, "B", nil, nil, nil}, + {nil, nil, nil, nil, nil, nil}, + } + f := prepareCalcData(cellData) + calc := map[string]string{ + "=VLOOKUP(F3,B3:C8,2)": "b", + "=VLOOKUP(F3,B3:C8,2,TRUE)": "b", + "=VLOOKUP(F3,B3:C8,2,FALSE)": "B", + } + for formula, expected := range calc { + assert.NoError(t, f.SetCellFormula("Sheet1", "F4", formula)) + result, err := f.CalcCellValue("Sheet1", "F4") + 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", + } + 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) + } +} + +func TestCalcHLOOKUP(t *testing.T) { + cellData := [][]interface{}{ + {"Example Result Table"}, + {nil, "A", "B", "C", "E", "F"}, + {"Math", .58, .9, .67, .76, .8}, + {"French", .61, .71, .59, .59, .76}, + {"Physics", .75, .45, .39, .52, .69}, + {"Biology", .39, .55, .77, .61, .45}, + {}, + {"Individual Student Score"}, + {"Student:", "Biology Score:"}, + {"E"}, + } + f := prepareCalcData(cellData) + formulaList := map[string]string{ + "=HLOOKUP(A10,A2:F6,5,FALSE)": "0.61", + "=HLOOKUP(D3,D3:D3,1,TRUE)": "0.67", + "=HLOOKUP(F3,D3:F3,1,TRUE)": "0.8", + "=HLOOKUP(A5,A2:F2,1,TRUE)": "F", + "=HLOOKUP(\"D\",A2:F2,1,TRUE)": "C", + } + for formula, expected := range formulaList { + assert.NoError(t, f.SetCellFormula("Sheet1", "B10", formula)) + result, err := f.CalcCellValue("Sheet1", "B10") + 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", + } + 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) + } +} From ca6b1577a7d1508e61e7084ee67ab0dd759b305e Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 14 Feb 2021 00:02:29 +0800 Subject: [PATCH 331/957] add ten formula functions: ENCODEURL, EXACT, FALSE, IFERROR, ISTEXT, LENB, NOT, REPT, SHEET, TRUE --- calc.go | 238 ++++++++++++++++++++++++++++++++++++++++++++------- calc_test.go | 60 +++++++++++++ 2 files changed, 267 insertions(+), 31 deletions(-) diff --git a/calc.go b/calc.go index 450a78887f..b684a77603 100644 --- a/calc.go +++ b/calc.go @@ -18,6 +18,7 @@ import ( "fmt" "math" "math/rand" + "net/url" "reflect" "regexp" "sort" @@ -99,13 +100,15 @@ const ( // formulaArg is the argument of a formula or function. type formulaArg struct { - Number float64 - String string - List []formulaArg - Matrix [][]formulaArg - Boolean bool - Error string - Type ArgType + f *File + SheetName string + Number float64 + String string + List []formulaArg + Matrix [][]formulaArg + Boolean bool + Error string + Type ArgType } // Value returns a string data type of the formula argument. @@ -162,7 +165,10 @@ func (fa formulaArg) ToBool() formulaArg { } // formulaFuncs is the type of the formula functions. -type formulaFuncs struct{} +type formulaFuncs struct { + f *File + sheet string +} // tokenPriority defined basic arithmetic operator priority. var tokenPriority = map[string]int{ @@ -184,7 +190,7 @@ var tokenPriority = map[string]int{ // feature is currently in working processing. Array formula, table formula // and some other formulas are not supported currently. // -// Supported formulas: +// Supported formula functions: // // ABS // ACOS @@ -215,16 +221,20 @@ var tokenPriority = map[string]int{ // DATE // DECIMAL // DEGREES +// ENCODEURL // EVEN +// EXACT // EXP // FACT // FACTDOUBLE +// FALSE // FLOOR // FLOOR.MATH // FLOOR.PRECISE // GCD // HLOOKUP // IF +// IFERROR // INT // ISBLANK // ISERR @@ -234,9 +244,11 @@ var tokenPriority = map[string]int{ // ISNONTEXT // ISNUMBER // ISODD +// ISTEXT // ISO.CEILING // LCM // LEN +// LENB // LN // LOG // LOG10 @@ -249,6 +261,7 @@ var tokenPriority = map[string]int{ // MULTINOMIAL // MUNIT // NA +// NOT // ODD // OR // PI @@ -259,11 +272,13 @@ var tokenPriority = map[string]int{ // RADIANS // RAND // RANDBETWEEN +// REPT // ROUND // ROUNDDOWN // ROUNDUP // SEC // SECH +// SHEET // SIGN // SIN // SINH @@ -275,6 +290,7 @@ var tokenPriority = map[string]int{ // TAN // TANH // TRIM +// TRUE // TRUNC // UPPER // VLOOKUP @@ -445,15 +461,12 @@ func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error) // calculate trigger topOpt := opftStack.Peek().(efp.Token) if err := calculate(opfdStack, topOpt); err != nil { - return efp.Token{}, err + argsStack.Peek().(*list.List).PushFront(newErrorFormulaArg(formulaErrorVALUE, err.Error())) } opftStack.Pop() } if !opfdStack.Empty() { - argsStack.Peek().(*list.List).PushBack(formulaArg{ - String: opfdStack.Pop().(efp.Token).TValue, - Type: ArgString, - }) + argsStack.Peek().(*list.List).PushBack(newStringFormulaArg(opfdStack.Pop().(efp.Token).TValue)) } continue } @@ -462,20 +475,14 @@ func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error) if token.TType == efp.OperatorsInfix && token.TSubType == efp.TokenSubTypeLogical { } if token.TType == efp.TokenTypeOperand && token.TSubType == efp.TokenSubTypeLogical { - argsStack.Peek().(*list.List).PushBack(formulaArg{ - String: token.TValue, - Type: ArgString, - }) + argsStack.Peek().(*list.List).PushBack(newStringFormulaArg(token.TValue)) } // current token is text if token.TType == efp.TokenTypeOperand && token.TSubType == efp.TokenSubTypeText { - argsStack.Peek().(*list.List).PushBack(formulaArg{ - String: token.TValue, - Type: ArgString, - }) + argsStack.Peek().(*list.List).PushBack(newStringFormulaArg(token.TValue)) } - if err = evalInfixExpFunc(token, nextToken, opfStack, opdStack, opftStack, opfdStack, argsStack); err != nil { + if err = f.evalInfixExpFunc(sheet, token, nextToken, opfStack, opdStack, opftStack, opfdStack, argsStack); err != nil { return efp.Token{}, err } } @@ -494,7 +501,7 @@ func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error) } // evalInfixExpFunc evaluate formula function in the infix expression. -func evalInfixExpFunc(token, nextToken efp.Token, opfStack, opdStack, opftStack, opfdStack, argsStack *Stack) error { +func (f *File) evalInfixExpFunc(sheet string, token, nextToken efp.Token, opfStack, opdStack, opftStack, opfdStack, argsStack *Stack) error { if !isFunctionStopToken(token) { return nil } @@ -510,16 +517,13 @@ func evalInfixExpFunc(token, nextToken efp.Token, opfStack, opdStack, opftStack, // push opfd to args if opfdStack.Len() > 0 { - argsStack.Peek().(*list.List).PushBack(formulaArg{ - String: opfdStack.Pop().(efp.Token).TValue, - Type: ArgString, - }) + argsStack.Peek().(*list.List).PushBack(newStringFormulaArg(opfdStack.Pop().(efp.Token).TValue)) } // call formula function to evaluate - arg := callFuncByName(&formulaFuncs{}, strings.NewReplacer( + arg := callFuncByName(&formulaFuncs{f: f, sheet: sheet}, strings.NewReplacer( "_xlfn", "", ".", "").Replace(opfStack.Peek().(efp.Token).TValue), []reflect.Value{reflect.ValueOf(argsStack.Peek().(*list.List))}) - if arg.Type == ArgError { + if arg.Type == ArgError && opfStack.Len() == 1 { return errors.New(arg.Value()) } argsStack.Pop() @@ -793,7 +797,7 @@ func isEndParenthesesToken(token efp.Token) bool { // token. func isOperatorPrefixToken(token efp.Token) bool { _, ok := tokenPriority[token.TValue] - if (token.TValue == "-" && token.TType == efp.TokenTypeOperatorPrefix) || ok { + if (token.TValue == "-" && token.TType == efp.TokenTypeOperatorPrefix) || (ok && token.TType == efp.TokenTypeOperatorInfix) { return true } return false @@ -3274,6 +3278,22 @@ func (fn *formulaFuncs) ISODD(argsList *list.List) formulaArg { return newStringFormulaArg(result) } +// 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) +// +func (fn *formulaFuncs) ISTEXT(argsList *list.List) formulaArg { + if argsList.Len() != 1 { + return newErrorFormulaArg(formulaErrorVALUE, "ISTEXT requires 1 argument") + } + token := argsList.Front().Value.(formulaArg) + if token.ToNumber().Type != ArgError { + return newBoolFormulaArg(false) + } + return newBoolFormulaArg(token.Type == ArgString) +} + // NA function returns the Excel #N/A error. This error message has the // 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: @@ -3287,6 +3307,18 @@ func (fn *formulaFuncs) NA(argsList *list.List) formulaArg { return newStringFormulaArg(formulaErrorNA) } +// SHEET function returns the Sheet number for a specified reference. The +// syntax of the function is: +// +// SHEET() +// +func (fn *formulaFuncs) SHEET(argsList *list.List) formulaArg { + if argsList.Len() != 0 { + return newErrorFormulaArg(formulaErrorVALUE, "SHEET accepts no arguments") + } + return newNumberFormulaArg(float64(fn.f.GetSheetIndex(fn.sheet) + 1)) +} + // Logical Functions // AND function tests a number of supplied conditions and returns TRUE or @@ -3330,6 +3362,64 @@ func (fn *formulaFuncs) AND(argsList *list.List) formulaArg { return newBoolFormulaArg(and) } +// FALSE function function returns the logical value FALSE. The syntax of the +// function is: +// +// FALSE() +// +func (fn *formulaFuncs) FALSE(argsList *list.List) formulaArg { + if argsList.Len() != 0 { + return newErrorFormulaArg(formulaErrorVALUE, "FALSE takes no arguments") + } + return newBoolFormulaArg(false) +} + +// 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) +// +func (fn *formulaFuncs) IFERROR(argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, "IFERROR requires 2 arguments") + } + value := argsList.Front().Value.(formulaArg) + if value.Type != ArgError { + if value.Type == ArgEmpty { + return newNumberFormulaArg(0) + } + return value + } + return argsList.Back().Value.(formulaArg) +} + +// NOT function returns the opposite to a supplied logical value. The syntax +// of the function is: +// +// NOT(logical) +// +func (fn *formulaFuncs) NOT(argsList *list.List) formulaArg { + if argsList.Len() != 1 { + return newErrorFormulaArg(formulaErrorVALUE, "NOT requires 1 argument") + } + token := argsList.Front().Value.(formulaArg) + switch token.Type { + case ArgString, ArgList: + if strings.ToUpper(token.String) == "TRUE" { + return newBoolFormulaArg(false) + } + if strings.ToUpper(token.String) == "FALSE" { + return newBoolFormulaArg(true) + } + case ArgNumber: + return newBoolFormulaArg(!(token.Number != 0)) + case ArgError: + + return token + } + return newErrorFormulaArg(formulaErrorVALUE, "NOT expects 1 boolean or numeric argument") +} + // OR function tests a number of supplied conditions and returns either TRUE // or FALSE. The syntax of the function is: // @@ -3372,6 +3462,18 @@ func (fn *formulaFuncs) OR(argsList *list.List) formulaArg { return newStringFormulaArg(strings.ToUpper(strconv.FormatBool(or))) } +// TRUE function returns the logical value TRUE. The syntax of the function +// is: +// +// TRUE() +// +func (fn *formulaFuncs) TRUE(argsList *list.List) formulaArg { + if argsList.Len() != 0 { + return newErrorFormulaArg(formulaErrorVALUE, "TRUE takes no arguments") + } + return newBoolFormulaArg(true) +} + // Date and Time Functions // DATE returns a date, from a user-supplied year, month and day. The syntax @@ -3434,6 +3536,21 @@ func (fn *formulaFuncs) CLEAN(argsList *list.List) formulaArg { return newStringFormulaArg(b.String()) } +// EXACT function tests if two supplied text strings or values are exactly +// 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) +// +func (fn *formulaFuncs) EXACT(argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, "EXACT requires 2 arguments") + } + text1 := argsList.Front().Value.(formulaArg).Value() + text2 := argsList.Back().Value.(formulaArg).Value() + return newBoolFormulaArg(text1 == text2) +} + // LEN returns the length of a supplied text string. The syntax of the // function is: // @@ -3446,6 +3563,22 @@ func (fn *formulaFuncs) LEN(argsList *list.List) formulaArg { return newStringFormulaArg(strconv.Itoa(len(argsList.Front().Value.(formulaArg).String))) } +// LENB returns the number of bytes used to represent the characters in a text +// string. LENB counts 2 bytes per character only when a DBCS language is set +// as the default language. Otherwise LENB behaves the same as LEN, counting +// 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") + } + return newStringFormulaArg(strconv.Itoa(len(argsList.Front().Value.(formulaArg).String))) +} + // TRIM removes extra spaces (i.e. all spaces except for single spaces between // words or characters) from a supplied text string. The syntax of the // function is: @@ -3495,6 +3628,36 @@ func (fn *formulaFuncs) PROPER(argsList *list.List) formulaArg { return newStringFormulaArg(buf.String()) } +// REPT function returns a supplied text string, repeated a specified number +// of times. The syntax of the function is: +// +// REPT(text,number_times) +// +func (fn *formulaFuncs) REPT(argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, "REPT requires 2 arguments") + } + text := argsList.Front().Value.(formulaArg) + if text.Type != ArgString { + return newErrorFormulaArg(formulaErrorVALUE, "REPT requires first argument to be a string") + } + times := argsList.Back().Value.(formulaArg).ToNumber() + if times.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, "REPT requires second argument to be a number") + } + if times.Number < 0 { + return newErrorFormulaArg(formulaErrorVALUE, "REPT requires second argument to be >= 0") + } + if times.Number == 0 { + return newStringFormulaArg("") + } + buf := bytes.Buffer{} + for i := 0; i < int(times.Number); i++ { + buf.WriteString(text.String) + } + return newStringFormulaArg(buf.String()) +} + // UPPER converts all characters in a supplied text string to upper case. The // syntax of the function is: // @@ -3976,3 +4139,16 @@ func lookupCol(arr formulaArg) []formulaArg { } return col } + +// Web Functions + +// ENCODEURL function returns a URL-encoded string, replacing certain non-alphanumeric characters with the percentage symbol (%) and a hexadecimal number. The syntax of the function is: +// +// ENCODEURL(url) +// +func (fn *formulaFuncs) ENCODEURL(argsList *list.List) formulaArg { + if argsList.Len() != 1 { + return newErrorFormulaArg(formulaErrorVALUE, "ENCODEURL requires 1 argument") + } + return newStringFormulaArg(strings.Replace(url.QueryEscape(argsList.Front().Value.(formulaArg).Value()), "+", "%20", -1)) +} diff --git a/calc_test.go b/calc_test.go index b71d822b83..437a7b508a 100644 --- a/calc_test.go +++ b/calc_test.go @@ -504,8 +504,13 @@ func TestCalcCellValue(t *testing.T) { // ISODD "=ISODD(A1)": "TRUE", "=ISODD(A2)": "FALSE", + // ISTEXT + "=ISTEXT(D1)": "TRUE", + "=ISTEXT(A1)": "FALSE", // NA "=NA()": "#N/A", + // SHEET + "SHEET()": "1", // Logical Functions // AND "=AND(0)": "FALSE", @@ -516,11 +521,24 @@ func TestCalcCellValue(t *testing.T) { "=AND(1<2)": "TRUE", "=AND(1>2,2<3,2>0,3>1)": "FALSE", "=AND(1=1),1=1": "TRUE", + // FALSE + "=FALSE()": "FALSE", + // IFERROR + "=IFERROR(1/2,0)": "0.5", + "=IFERROR(ISERROR(),0)": "0", + "=IFERROR(1/0,0)": "0", + // NOT + "=NOT(FALSE())": "TRUE", + "=NOT(\"false\")": "TRUE", + "=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", + // TRUE + "=TRUE()": "TRUE", // Date and Time Functions // DATE "=DATE(2020,10,21)": "2020-10-21 00:00:00 +0000 UTC", @@ -529,9 +547,16 @@ func TestCalcCellValue(t *testing.T) { // CLEAN "=CLEAN(\"\u0009clean text\")": "clean text", "=CLEAN(0)": "0", + // EXACT + "=EXACT(1,\"1\")": "TRUE", + "=EXACT(1,1)": "TRUE", + "=EXACT(\"A\",\"a\")": "FALSE", // LEN "=LEN(\"\")": "0", "=LEN(D1)": "5", + // LENB + "=LENB(\"\")": "0", + "=LENB(D1)": "5", // TRIM "=TRIM(\" trim text \")": "trim text", "=TRIM(0)": "0", @@ -545,6 +570,10 @@ func TestCalcCellValue(t *testing.T) { "=PROPER(\"THIS IS A TEST SENTENCE\")": "This Is A Test Sentence", "=PROPER(\"123tEST teXT\")": "123Test Text", "=PROPER(\"Mr. SMITH's address\")": "Mr. Smith'S Address", + // REPT + "=REPT(\"*\",0)": "", + "=REPT(\"*\",1)": "*", + "=REPT(\"**\",2)": "****", // UPPER "=UPPER(\"test\")": "TEST", "=UPPER(\"TEST\")": "TEST", @@ -581,6 +610,9 @@ func TestCalcCellValue(t *testing.T) { "=LOOKUP(F8,F8:F9,F8:F9)": "32080", "=LOOKUP(F8,F8:F9,D8:D9)": "Feb", "=LOOKUP(1,MUNIT(1),MUNIT(1))": "1", + // Web Functions + // ENCODEURL + "=ENCODEURL(\"https://xuri.me/excelize/en/?q=Save As\")": "https%3A%2F%2Fxuri.me%2Fexcelize%2Fen%2F%3Fq%3DSave%20As", } for formula, expected := range mathCalc { f := prepareCalcData(cellData) @@ -590,6 +622,7 @@ func TestCalcCellValue(t *testing.T) { assert.Equal(t, expected, result, formula) } mathCalcError := map[string]string{ + "=1/0": "#DIV/0!", // ABS "=ABS()": "ABS requires 1 numeric argument", `=ABS("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", @@ -886,19 +919,33 @@ func TestCalcCellValue(t *testing.T) { // ISODD "=ISODD()": "ISODD requires 1 argument", `=ISODD("text")`: "strconv.Atoi: parsing \"text\": invalid syntax", + // ISTEXT + "=ISTEXT()": "ISTEXT requires 1 argument", // NA "=NA(1)": "NA accepts no arguments", + // SHEET + "=SHEET(1)": "SHEET accepts no arguments", // Logical Functions // AND `=AND("text")`: "strconv.ParseFloat: parsing \"text\": invalid syntax", `=AND(A1:B1)`: "#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", + // IFERROR + "=IFERROR()": "IFERROR requires 2 arguments", + // NOT + "=NOT()": "NOT requires 1 argument", + "=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()": "OR requires at least 1 argument", "=OR(1" + strings.Repeat(",1", 30) + ")": "OR accepts at most 30 arguments", + // TRUE + "=TRUE(A1)": "TRUE takes no arguments", // Date and Time Functions // DATE "=DATE()": "DATE requires 3 number arguments", @@ -909,8 +956,13 @@ func TestCalcCellValue(t *testing.T) { // CLEAN "=CLEAN()": "CLEAN requires 1 argument", "=CLEAN(1,2)": "CLEAN requires 1 argument", + // EXACT + "=EXACT()": "EXACT requires 2 arguments", + "=EXACT(1,2,3)": "EXACT requires 2 arguments", // LEN "=LEN()": "LEN requires 1 string argument", + // LENB + "=LENB()": "LENB requires 1 string argument", // TRIM "=TRIM()": "TRIM requires 1 argument", "=TRIM(1,2)": "TRIM requires 1 argument", @@ -923,6 +975,11 @@ func TestCalcCellValue(t *testing.T) { // PROPER "=PROPER()": "PROPER requires 1 argument", "=PROPER(1,2)": "PROPER requires 1 argument", + // 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", // Conditional Functions // IF "=IF()": "IF requires at least 1 argument", @@ -964,6 +1021,9 @@ func TestCalcCellValue(t *testing.T) { "=LOOKUP(D2,D1,D2)": "LOOKUP requires second argument of table array", "=LOOKUP(D2,D1,D2,FALSE)": "LOOKUP requires at most 3 arguments", "=LOOKUP(D1,MUNIT(1),MUNIT(1))": "LOOKUP no result found", + // Web Functions + // ENCODEURL + "=ENCODEURL()": "ENCODEURL requires 1 argument", } for formula, expected := range mathCalcError { f := prepareCalcData(cellData) From 36b7990d6ba1036823abf7a01ec8cf74509d4910 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 15 Feb 2021 00:09:35 +0800 Subject: [PATCH 332/957] lint issue fixed and new formula function: ATAN, AVERAGE, AVERAGEA, CONCAT, CONCATENATE, COUNT, COUNTBLANK, MAX --- calc.go | 335 ++++++++++++++++++++++++++++++++++++++++++----- calc_test.go | 101 ++++++++++++-- cell_test.go | 11 +- col.go | 5 +- col_test.go | 6 +- crypt.go | 26 ++-- drawing.go | 4 +- excelize_test.go | 6 +- file.go | 2 +- file_test.go | 2 +- lib.go | 2 +- picture.go | 4 +- pivotTable.go | 4 +- rows.go | 4 +- sheet.go | 4 +- sheet_test.go | 6 +- stream.go | 32 ++--- stream_test.go | 4 +- styles.go | 18 +-- table.go | 11 +- 20 files changed, 462 insertions(+), 125 deletions(-) diff --git a/calc.go b/calc.go index b684a77603..33c504d25e 100644 --- a/calc.go +++ b/calc.go @@ -100,7 +100,6 @@ const ( // formulaArg is the argument of a formula or function. type formulaArg struct { - f *File SheetName string Number float64 String string @@ -164,6 +163,21 @@ func (fa formulaArg) ToBool() formulaArg { return newBoolFormulaArg(b) } +// ToList returns a formula argument with array data type. +func (fa formulaArg) ToList() []formulaArg { + if fa.Type == ArgMatrix { + list := []formulaArg{} + for _, row := range fa.Matrix { + list = append(list, row...) + } + return list + } + if fa.Type == ArgList { + return fa.List + } + return nil +} + // formulaFuncs is the type of the formula functions. type formulaFuncs struct { f *File @@ -201,8 +215,11 @@ var tokenPriority = map[string]int{ // ARABIC // ASIN // ASINH +// ATAN // ATAN2 // ATANH +// AVERAGE +// AVERAGEA // BASE // CEILING // CEILING.MATH @@ -211,11 +228,15 @@ var tokenPriority = map[string]int{ // CLEAN // COMBIN // COMBINA +// CONCAT +// CONCATENATE // COS // COSH // COT // COTH +// COUNT // COUNTA +// COUNTBLANK // CSC // CSCH // DATE @@ -254,6 +275,7 @@ var tokenPriority = map[string]int{ // LOG10 // LOOKUP // LOWER +// MAX // MDETERM // MEDIAN // MOD @@ -322,7 +344,7 @@ func (f *File) CalcCellValue(sheet, cell string) (result string, err error) { // getPriority calculate arithmetic operator priority. func getPriority(token efp.Token) (pri int) { - pri, _ = tokenPriority[token.TValue] + pri = tokenPriority[token.TValue] if token.TValue == "-" && token.TType == efp.TokenTypeOperatorPrefix { pri = 6 } @@ -962,7 +984,7 @@ func (f *File) rangeResolver(cellRefs, cellRanges *list.List) (arg formulaArg, e err = errors.New(formulaErrorVALUE) } rng := []int{cr.From.Col, cr.From.Row, cr.To.Col, cr.To.Row} - sortCoordinates(rng) + _ = sortCoordinates(rng) cr.From.Col, cr.From.Row, cr.To.Col, cr.To.Row = rng[0], rng[1], rng[2], rng[3] prepareValueRange(cr, valueRange) if cr.From.Sheet != "" { @@ -1208,7 +1230,7 @@ func (fn *formulaFuncs) ARABIC(argsList *list.List) formulaArg { prefix = -1 continue } - digit, _ = charMap[char] + digit = charMap[char] val += digit switch { case last == digit && (last == 5 || last == 50 || last == 500): @@ -1950,22 +1972,18 @@ func (fn *formulaFuncs) GCD(argsList *list.List) formulaArg { var ( val float64 nums = []float64{} - err error ) 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 == ArgError { + return num } - break + val = num.Number case ArgNumber: val = token.Number - break } nums = append(nums, val) } @@ -2083,10 +2101,8 @@ func (fn *formulaFuncs) LCM(argsList *list.List) formulaArg { if val, err = strconv.ParseFloat(token.String, 64); err != nil { return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } - break case ArgNumber: val = token.Number - break } nums = append(nums, val) } @@ -2321,10 +2337,8 @@ func (fn *formulaFuncs) MULTINOMIAL(argsList *list.List) formulaArg { if val, err = strconv.ParseFloat(token.String, 64); err != nil { return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } - break case ArgNumber: val = token.Number - break } num += val denom *= fact(val) @@ -2449,10 +2463,8 @@ func (fn *formulaFuncs) PRODUCT(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } product = product * val - break case ArgNumber: product = product * token.Number - break case ArgMatrix: for _, row := range token.Matrix { for _, value := range row { @@ -2934,10 +2946,8 @@ func (fn *formulaFuncs) SUMSQ(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } sq += val * val - break case ArgNumber: sq += token.Number - break case ArgMatrix: for _, row := range token.Matrix { for _, value := range row { @@ -3023,7 +3033,98 @@ func (fn *formulaFuncs) TRUNC(argsList *list.List) formulaArg { return newNumberFormulaArg(float64(int(number.Number*adjust)) / adjust) } -// Statistical functions +// Statistical Functions + +// AVERAGE function returns the arithmetic mean of a list of supplied numbers. +// The syntax of the function is: +// +// AVERAGE(number1,[number2],...) +// +func (fn *formulaFuncs) AVERAGE(argsList *list.List) formulaArg { + args := []formulaArg{} + for arg := argsList.Front(); arg != nil; arg = arg.Next() { + args = append(args, arg.Value.(formulaArg)) + } + count, sum := fn.countSum(false, args) + if count == 0 { + return newErrorFormulaArg(formulaErrorDIV, "AVERAGE divide by zero") + } + return newNumberFormulaArg(sum / count) +} + +// 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],...) +// +func (fn *formulaFuncs) AVERAGEA(argsList *list.List) formulaArg { + args := []formulaArg{} + for arg := argsList.Front(); arg != nil; arg = arg.Next() { + args = append(args, arg.Value.(formulaArg)) + } + count, sum := fn.countSum(true, args) + if count == 0 { + return newErrorFormulaArg(formulaErrorDIV, "AVERAGEA divide by zero") + } + return newNumberFormulaArg(sum / count) +} + +// countSum get count and sum for a formula arguments array. +func (fn *formulaFuncs) countSum(countText bool, args []formulaArg) (count, sum float64) { + for _, arg := range args { + switch arg.Type { + case ArgNumber: + if countText || !arg.Boolean { + sum += arg.Number + count++ + } + case ArgString: + num := arg.ToNumber() + if countText && num.Type == ArgError && arg.String != "" { + count++ + } + if num.Type == ArgNumber { + sum += num.Number + count++ + } + case ArgList, ArgMatrix: + cnt, summary := fn.countSum(countText, arg.ToList()) + sum += summary + count += cnt + } + } + return +} + +// COUNT function returns the count of numeric values in a supplied set of +// cells or values. This count includes both numbers and dates. The syntax of +// the function is: +// +// COUNT(value1,[value2],...) +// +func (fn *formulaFuncs) COUNT(argsList *list.List) formulaArg { + var count int + for token := argsList.Front(); token != nil; token = token.Next() { + arg := token.Value.(formulaArg) + switch arg.Type { + case ArgString: + if arg.ToNumber().Type != ArgError { + count++ + } + case ArgNumber: + count++ + case ArgMatrix: + for _, row := range arg.Matrix { + for _, value := range row { + if value.ToNumber().Type != ArgError { + count++ + } + } + } + } + } + return newNumberFormulaArg(float64(count)) +} // COUNTA function returns the number of non-blanks within a supplied set of // cells or values. The syntax of the function is: @@ -3039,17 +3140,135 @@ func (fn *formulaFuncs) COUNTA(argsList *list.List) formulaArg { if arg.String != "" { count++ } + case ArgNumber: + count++ case ArgMatrix: - for _, row := range arg.Matrix { - for _, value := range row { - if value.String != "" { + for _, row := range arg.ToList() { + switch row.Type { + case ArgString: + if row.String != "" { count++ } + case ArgNumber: + count++ + } + } + } + } + return newNumberFormulaArg(float64(count)) +} + +// COUNTBLANK function returns the number of blank cells in a supplied range. +// The syntax of the function is: +// +// COUNTBLANK(range) +// +func (fn *formulaFuncs) COUNTBLANK(argsList *list.List) formulaArg { + if argsList.Len() != 1 { + return newErrorFormulaArg(formulaErrorVALUE, "COUNTBLANK requires 1 argument") + } + var count int + token := argsList.Front().Value.(formulaArg) + switch token.Type { + case ArgString: + if token.String == "" { + count++ + } + case ArgList, ArgMatrix: + for _, row := range token.ToList() { + switch row.Type { + case ArgString: + if row.String == "" { + count++ + } + case ArgEmpty: + count++ + } + } + case ArgEmpty: + count++ + } + return newNumberFormulaArg(float64(count)) +} + +// MAX function returns the largest value from a supplied set of numeric +// values. The syntax of the function is: +// +// MAX(number1,[number2],...) +// +func (fn *formulaFuncs) MAX(argsList *list.List) formulaArg { + if argsList.Len() == 0 { + return newErrorFormulaArg(formulaErrorVALUE, "MAX requires at least 1 argument") + } + return fn.max(false, argsList) +} + +// MAXA function returns the largest value from a supplied set of numeric values, while counting text and the logical value FALSE as the value 0 and counting the logical value TRUE as the value 1. The syntax of the function is: +// +// MAXA(number1,[number2],...) +// +func (fn *formulaFuncs) MAXA(argsList *list.List) formulaArg { + if argsList.Len() == 0 { + return newErrorFormulaArg(formulaErrorVALUE, "MAXA requires at least 1 argument") + } + return fn.max(true, argsList) +} + +// max is an implementation of the formula function MAX and MAXA. +func (fn *formulaFuncs) max(maxa bool, argsList *list.List) formulaArg { + max := -math.MaxFloat64 + for token := argsList.Front(); token != nil; token = token.Next() { + arg := token.Value.(formulaArg) + switch arg.Type { + case ArgString: + if !maxa && (arg.Value() == "TRUE" || arg.Value() == "FALSE") { + continue + } else { + num := arg.ToBool() + if num.Type == ArgNumber && num.Number > max { + max = num.Number + continue + } + } + num := arg.ToNumber() + if num.Type != ArgError && num.Number > max { + max = num.Number + } + case ArgNumber: + if arg.Number > max { + max = arg.Number + } + case ArgList, ArgMatrix: + 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 + } } } + case ArgError: + return arg } } - return newStringFormulaArg(fmt.Sprintf("%d", count)) + if max == -math.MaxFloat64 { + max = 0 + } + return newNumberFormulaArg(max) } // MEDIAN function returns the statistical median (the middle value) of a list @@ -3068,14 +3287,13 @@ func (fn *formulaFuncs) MEDIAN(argsList *list.List) formulaArg { arg := token.Value.(formulaArg) switch arg.Type { case ArgString: - if digits, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + num := arg.ToNumber() + if num.Type == ArgError { + return newErrorFormulaArg(formulaErrorVALUE, num.Error) } - values = append(values, digits) - break + values = append(values, num.Number) case ArgNumber: values = append(values, arg.Number) - break case ArgMatrix: for _, row := range arg.Matrix { for _, value := range row { @@ -3099,7 +3317,7 @@ func (fn *formulaFuncs) MEDIAN(argsList *list.List) formulaArg { return newNumberFormulaArg(median) } -// Information functions +// Information Functions // ISBLANK function tests if a specified cell is blank (empty) and if so, // returns TRUE; Otherwise the function returns FALSE. The syntax of the @@ -3137,7 +3355,7 @@ func (fn *formulaFuncs) ISERR(argsList *list.List) formulaArg { } token := argsList.Front().Value.(formulaArg) result := "FALSE" - if token.Type == ArgString { + if token.Type == ArgError { for _, errType := range []string{formulaErrorDIV, formulaErrorNAME, formulaErrorNUM, formulaErrorVALUE, formulaErrorREF, formulaErrorNULL, formulaErrorSPILL, formulaErrorCALC, formulaErrorGETTINGDATA} { if errType == token.String { result = "TRUE" @@ -3159,7 +3377,7 @@ func (fn *formulaFuncs) ISERROR(argsList *list.List) formulaArg { } token := argsList.Front().Value.(formulaArg) result := "FALSE" - if token.Type == ArgString { + if token.Type == ArgError { for _, errType := range []string{formulaErrorDIV, formulaErrorNAME, formulaErrorNA, formulaErrorNUM, formulaErrorVALUE, formulaErrorREF, formulaErrorNULL, formulaErrorSPILL, formulaErrorCALC, formulaErrorGETTINGDATA} { if errType == token.String { result = "TRUE" @@ -3208,7 +3426,7 @@ func (fn *formulaFuncs) ISNA(argsList *list.List) formulaArg { } token := argsList.Front().Value.(formulaArg) result := "FALSE" - if token.Type == ArgString && token.String == formulaErrorNA { + if token.Type == ArgError && token.String == formulaErrorNA { result = "TRUE" } return newStringFormulaArg(result) @@ -3304,7 +3522,7 @@ func (fn *formulaFuncs) NA(argsList *list.List) formulaArg { if argsList.Len() != 0 { return newErrorFormulaArg(formulaErrorVALUE, "NA accepts no arguments") } - return newStringFormulaArg(formulaErrorNA) + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) } // SHEET function returns the Sheet number for a specified reference. The @@ -3536,6 +3754,49 @@ func (fn *formulaFuncs) CLEAN(argsList *list.List) formulaArg { return newStringFormulaArg(b.String()) } +// CONCAT function joins together a series of supplied text strings into one +// combined text string. +// +// CONCAT(text1,[text2],...) +// +func (fn *formulaFuncs) CONCAT(argsList *list.List) formulaArg { + return fn.concat("CONCAT", argsList) +} + +// CONCATENATE function joins together a series of supplied text strings into +// one combined text string. +// +// CONCATENATE(text1,[text2],...) +// +func (fn *formulaFuncs) CONCATENATE(argsList *list.List) formulaArg { + return fn.concat("CONCATENATE", argsList) +} + +// concat is an implementation of the formula function CONCAT and CONCATENATE. +func (fn *formulaFuncs) concat(name string, argsList *list.List) formulaArg { + 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()) + } + default: + return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires arguments to be strings", name)) + } + } + return newStringFormulaArg(buf.String()) +} + // EXACT function tests if two supplied text strings or values are exactly // equal and if so, returns TRUE; Otherwise, the function returns FALSE. The // function is case-sensitive. The syntax of the function is: @@ -4142,7 +4403,9 @@ func lookupCol(arr formulaArg) []formulaArg { // Web Functions -// ENCODEURL function returns a URL-encoded string, replacing certain non-alphanumeric characters with the percentage symbol (%) and a hexadecimal number. The syntax of the function is: +// ENCODEURL function returns a URL-encoded string, replacing certain +// non-alphanumeric characters with the percentage symbol (%) and a +// hexadecimal number. The syntax of the function is: // // ENCODEURL(url) // diff --git a/calc_test.go b/calc_test.go index 437a7b508a..ef028a91d0 100644 --- a/calc_test.go +++ b/calc_test.go @@ -15,7 +15,7 @@ func prepareCalcData(cellData [][]interface{}) *File { for r, row := range cellData { for c, value := range row { cell, _ := CoordinatesToCellName(c+1, r+1) - f.SetCellValue("Sheet1", cell, value) + _ = f.SetCellValue("Sheet1", cell, value) } } return f @@ -245,7 +245,6 @@ func TestCalcCellValue(t *testing.T) { "=_xlfn.FLOOR.PRECISE(_xlfn.FLOOR.PRECISE(26.75),-5)": "25", // GCD "=GCD(0)": "0", - `=GCD("",1)`: "1", "=GCD(1,0)": "1", "=GCD(1,5)": "1", "=GCD(15,10,25)": "5", @@ -469,10 +468,43 @@ func TestCalcCellValue(t *testing.T) { "=TRUNC(-99.999,-1)": "-90", "=TRUNC(TRUNC(1),-1)": "0", // Statistical Functions + // AVERAGE + "=AVERAGE(INT(1))": "1", + "=AVERAGE(A1)": "1", + "=AVERAGE(A1:A2)": "1.5", + "=AVERAGE(D2:F9)": "38014.125", + // AVERAGEA + "=AVERAGEA(INT(1))": "1", + "=AVERAGEA(A1)": "1", + "=AVERAGEA(A1:A2)": "1.5", + "=AVERAGEA(D2:F9)": "12671.375", + // COUNT + "=COUNT()": "0", + "=COUNT(E1:F2,\"text\",1,INT(2))": "3", // COUNTA - `=COUNTA()`: "0", - `=COUNTA(A1:A5,B2:B5,"text",1,2)`: "8", - `=COUNTA(COUNTA(1))`: "1", + "=COUNTA()": "0", + "=COUNTA(A1:A5,B2:B5,\"text\",1,INT(2))": "8", + "=COUNTA(COUNTA(1),MUNIT(1))": "2", + // COUNTBLANK + "=COUNTBLANK(MUNIT(1))": "0", + "=COUNTBLANK(1)": "0", + "=COUNTBLANK(B1:C1)": "1", + "=COUNTBLANK(C1)": "1", + // MAX + "=MAX(1)": "1", + "=MAX(TRUE())": "1", + "=MAX(0.5,TRUE())": "1", + "=MAX(FALSE())": "0", + "=MAX(MUNIT(2))": "1", + "=MAX(INT(1))": "1", + // MAXA + "=MAXA(1)": "1", + "=MAXA(TRUE())": "1", + "=MAXA(0.5,TRUE())": "1", + "=MAXA(FALSE())": "0", + "=MAXA(MUNIT(2))": "1", + "=MAXA(INT(1))": "1", + "=MAXA(A1:B4,MUNIT(1),INT(0),1,E1:F2,\"\")": "36693", // MEDIAN "=MEDIAN(A1:A5,12)": "2", "=MEDIAN(A1:A5)": "1.5", @@ -482,8 +514,9 @@ func TestCalcCellValue(t *testing.T) { "=ISBLANK(A1)": "FALSE", "=ISBLANK(A5)": "TRUE", // ISERR - "=ISERR(A1)": "FALSE", - "=ISERR(NA())": "FALSE", + "=ISERR(A1)": "FALSE", + "=ISERR(NA())": "FALSE", + "=ISERR(POWER(0,-1)))": "TRUE", // ISERROR "=ISERROR(A1)": "FALSE", "=ISERROR(NA())": "TRUE", @@ -497,7 +530,7 @@ func TestCalcCellValue(t *testing.T) { "=ISNONTEXT(A1)": "FALSE", "=ISNONTEXT(A5)": "TRUE", `=ISNONTEXT("Excelize")`: "FALSE", - "=ISNONTEXT(NA())": "FALSE", + "=ISNONTEXT(NA())": "TRUE", // ISNUMBER "=ISNUMBER(A1)": "TRUE", "=ISNUMBER(D1)": "FALSE", @@ -507,8 +540,6 @@ func TestCalcCellValue(t *testing.T) { // ISTEXT "=ISTEXT(D1)": "TRUE", "=ISTEXT(A1)": "FALSE", - // NA - "=NA()": "#N/A", // SHEET "SHEET()": "1", // Logical Functions @@ -547,6 +578,10 @@ func TestCalcCellValue(t *testing.T) { // CLEAN "=CLEAN(\"\u0009clean text\")": "clean text", "=CLEAN(0)": "0", + // CONCAT + "=CONCAT(TRUE(),1,FALSE(),\"0\",INT(2))": "TRUE1FALSE02", + // CONCATENATE + "=CONCATENATE(TRUE(),1,FALSE(),\"0\",INT(2))": "TRUE1FALSE02", // EXACT "=EXACT(1,\"1\")": "TRUE", "=EXACT(1,1)": "TRUE", @@ -756,6 +791,7 @@ func TestCalcCellValue(t *testing.T) { `=_xlfn.FLOOR.PRECISE(1,"X")`: "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", @@ -897,8 +933,22 @@ func TestCalcCellValue(t *testing.T) { `=TRUNC("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", `=TRUNC(1,"X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", // Statistical Functions + // AVERAGE + "=AVERAGE(H1)": "AVERAGE divide by zero", + // AVERAGE + "=AVERAGEA(H1)": "AVERAGEA divide by zero", + // COUNTBLANK + "=COUNTBLANK()": "COUNTBLANK requires 1 argument", + "=COUNTBLANK(1,2)": "COUNTBLANK requires 1 argument", + // MAX + "=MAX()": "MAX requires at least 1 argument", + "=MAX(NA())": "#N/A", + // MAXA + "=MAXA()": "MAXA requires at least 1 argument", + "=MAXA(NA())": "#N/A", // MEDIAN "=MEDIAN()": "MEDIAN requires at least 1 argument", + "=MEDIAN(\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", "=MEDIAN(D1:D2)": "strconv.ParseFloat: parsing \"Month\": invalid syntax", // Information Functions // ISBLANK @@ -922,6 +972,7 @@ func TestCalcCellValue(t *testing.T) { // ISTEXT "=ISTEXT()": "ISTEXT requires 1 argument", // NA + "=NA()": "#N/A", "=NA(1)": "NA accepts no arguments", // SHEET "=SHEET(1)": "SHEET accepts no arguments", @@ -956,6 +1007,10 @@ func TestCalcCellValue(t *testing.T) { // CLEAN "=CLEAN()": "CLEAN requires 1 argument", "=CLEAN(1,2)": "CLEAN requires 1 argument", + // CONCAT + "=CONCAT(MUNIT(2))": "CONCAT requires arguments to be strings", + // CONCATENATE + "=CONCATENATE(MUNIT(2))": "CONCATENATE requires arguments to be strings", // EXACT "=EXACT()": "EXACT requires 2 arguments", "=EXACT(1,2,3)": "EXACT requires 2 arguments", @@ -1197,6 +1252,13 @@ func TestCalcToBool(t *testing.T) { assert.Equal(t, b.Boolean, true) assert.Equal(t, b.Number, 1.0) } + +func TestCalcToList(t *testing.T) { + assert.Equal(t, []formulaArg(nil), newEmptyFormulaArg().ToList()) + formulaList := []formulaArg{newEmptyFormulaArg()} + assert.Equal(t, formulaList, newListFormulaArg(formulaList).ToList()) +} + func TestCalcCompareFormulaArg(t *testing.T) { assert.Equal(t, compareFormulaArg(newEmptyFormulaArg(), newEmptyFormulaArg(), false, false), criteriaEq) lhs := newListFormulaArg([]formulaArg{newEmptyFormulaArg()}) @@ -1253,6 +1315,25 @@ func TestCalcVLOOKUP(t *testing.T) { } } +func TestCalcMAX(t *testing.T) { + cellData := [][]interface{}{ + {0.5, "TRUE"}, + } + f := prepareCalcData(cellData) + formulaList := map[string]string{ + "=MAX(0.5,B1)": "0.5", + "=MAX(A1:B1)": "0.5", + "=MAXA(A1:B1)": "1", + "=MAXA(0.5,B1)": "1", + } + for formula, expected := range formulaList { + assert.NoError(t, f.SetCellFormula("Sheet1", "B10", formula)) + result, err := f.CalcCellValue("Sheet1", "B10") + assert.NoError(t, err, formula) + assert.Equal(t, expected, result, formula) + } +} + func TestCalcHLOOKUP(t *testing.T) { cellData := [][]interface{}{ {"Example Result Table"}, diff --git a/cell_test.go b/cell_test.go index 93e9f4c98b..c3c20f7388 100644 --- a/cell_test.go +++ b/cell_test.go @@ -16,12 +16,13 @@ func TestConcurrency(t *testing.T) { wg := new(sync.WaitGroup) for i := 1; i <= 5; i++ { wg.Add(1) - go func(val int) { - f.SetCellValue("Sheet1", fmt.Sprintf("A%d", val), val) - f.SetCellValue("Sheet1", fmt.Sprintf("B%d", val), strconv.Itoa(val)) - f.GetCellValue("Sheet1", fmt.Sprintf("A%d", val)) + go func(val int, t *testing.T) { + assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("A%d", val), val)) + assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("B%d", val), strconv.Itoa(val))) + _, err := f.GetCellValue("Sheet1", fmt.Sprintf("A%d", val)) + assert.NoError(t, err) wg.Done() - }(i) + }(i, t) } wg.Wait() val, err := f.GetCellValue("Sheet1", "A1") diff --git a/col.go b/col.go index 9d46733e09..0980596aef 100644 --- a/col.go +++ b/col.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2021 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. // @@ -35,7 +35,6 @@ type Cols struct { err error curCol, totalCol, stashCol, totalRow int sheet string - cols []xlsxCols f *File sheetXML []byte } @@ -140,7 +139,6 @@ func (cols *Cols) Rows() ([]string, error) { // columnXMLIterator defined runtime use field for the worksheet column SAX parser. type columnXMLIterator struct { err error - inElement string cols Cols cellCol, curRow, row int } @@ -175,7 +173,6 @@ func columnXMLHandler(colIterator *columnXMLIterator, xmlElement *xml.StartEleme colIterator.cols.totalCol = colIterator.cellCol } } - return } // Cols returns a columns iterator, used for streaming reading data for a diff --git a/col_test.go b/col_test.go index 97c4b7ffb8..706f90aee0 100644 --- a/col_test.go +++ b/col_test.go @@ -138,7 +138,7 @@ func TestColsRows(t *testing.T) { f := NewFile() f.NewSheet("Sheet1") - cols, err := f.Cols("Sheet1") + _, err := f.Cols("Sheet1") assert.NoError(t, err) assert.NoError(t, f.SetCellValue("Sheet1", "A1", 1)) @@ -150,12 +150,12 @@ func TestColsRows(t *testing.T) { f = NewFile() f.XLSX["xl/worksheets/sheet1.xml"] = nil - cols, err = f.Cols("Sheet1") + _, err = f.Cols("Sheet1") if !assert.NoError(t, err) { t.FailNow() } f = NewFile() - cols, err = f.Cols("Sheet1") + cols, err := f.Cols("Sheet1") if !assert.NoError(t, err) { t.FailNow() } diff --git a/crypt.go b/crypt.go index 8ae8332945..64eadd6a7d 100644 --- a/crypt.go +++ b/crypt.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2021 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. // @@ -42,14 +42,7 @@ var ( packageOffset = 8 // First 8 bytes are the size of the stream packageEncryptionChunkSize = 4096 iterCount = 50000 - cryptoIdentifier = []byte{ // checking protect workbook by [MS-OFFCRYPTO] - v20181211 3.1 FeatureIdentifier - 0x3c, 0x00, 0x00, 0x00, 0x4d, 0x00, 0x69, 0x00, 0x63, 0x00, 0x72, 0x00, 0x6f, 0x00, 0x73, 0x00, - 0x6f, 0x00, 0x66, 0x00, 0x74, 0x00, 0x2e, 0x00, 0x43, 0x00, 0x6f, 0x00, 0x6e, 0x00, 0x74, 0x00, - 0x61, 0x00, 0x69, 0x00, 0x6e, 0x00, 0x65, 0x00, 0x72, 0x00, 0x2e, 0x00, 0x44, 0x00, 0x61, 0x00, - 0x74, 0x00, 0x61, 0x00, 0x53, 0x00, 0x70, 0x00, 0x61, 0x00, 0x63, 0x00, 0x65, 0x00, 0x73, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - } - oleIdentifier = []byte{ + oleIdentifier = []byte{ 0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1, } ) @@ -153,7 +146,6 @@ func Decrypt(raw []byte, opt *Options) (packageBuf []byte, err error) { return standardDecrypt(encryptionInfoBuf, encryptedPackageBuf, opt) default: err = errors.New("unsupport encryption mechanism") - break } return } @@ -209,11 +201,11 @@ func Encrypt(raw []byte, opt *Options) (packageBuf []byte, err error) { return } // Use the package key and the IV to encrypt the HMAC key. - encryptedHmacKey, err := crypt(true, encryptionInfo.KeyData.CipherAlgorithm, encryptionInfo.KeyData.CipherChaining, packageKey, hmacKeyIV, hmacKey) + 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) + _, _ = h.Write(buf) } hmacValue := h.Sum(nil) // Generate an initialization vector for encrypting the resulting HMAC value. @@ -222,7 +214,7 @@ func Encrypt(raw []byte, opt *Options) (packageBuf []byte, err error) { return } // Encrypt the value. - encryptedHmacValue, err := crypt(true, encryptionInfo.KeyData.CipherAlgorithm, encryptionInfo.KeyData.CipherChaining, packageKey, hmacValueIV, hmacValue) + 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) @@ -235,7 +227,7 @@ func Encrypt(raw []byte, opt *Options) (packageBuf []byte, err error) { return } // Encrypt the package key with the encryption key. - encryptedKeyValue, err := crypt(true, encryptionInfo.KeyEncryptors.KeyEncryptor[0].EncryptedKey.CipherAlgorithm, encryptionInfo.KeyEncryptors.KeyEncryptor[0].EncryptedKey.CipherChaining, key, keyEncryptors, packageKey) + 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 @@ -412,7 +404,7 @@ func standardConvertPasswdToKey(header StandardEncryptionHeader, verifier Standa // standardXORBytes perform XOR operations for two bytes slice. func standardXORBytes(a, b []byte) []byte { - r := make([][2]byte, len(a), len(a)) + r := make([][2]byte, len(a)) for i, e := range a { r[i] = [2]byte{e, b[i]} } @@ -447,7 +439,7 @@ func agileDecrypt(encryptionInfoBuf, encryptedPackageBuf []byte, opt *Options) ( if err != nil { return } - packageKey, err := crypt(false, encryptedKey.CipherAlgorithm, encryptedKey.CipherChaining, key, saltValue, encryptedKeyValue) + packageKey, _ := crypt(false, encryptedKey.CipherAlgorithm, encryptedKey.CipherChaining, key, saltValue, encryptedKeyValue) // Use the package key to decrypt the package. return cryptPackage(false, packageKey, encryptedPackageBuf, encryptionInfo) } @@ -503,7 +495,7 @@ func hashing(hashAlgorithm string, buffer ...[]byte) (key []byte) { return key } for _, buf := range buffer { - handler.Write(buf) + _, _ = handler.Write(buf) } key = handler.Sum(nil) return key diff --git a/drawing.go b/drawing.go index f0eb7e9571..2518651e26 100644 --- a/drawing.go +++ b/drawing.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2021 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. // @@ -51,7 +51,6 @@ func (f *File) prepareChartSheetDrawing(cs *xlsxChartsheet, drawingID int, sheet cs.Drawing = &xlsxDrawing{ RID: "rId" + strconv.Itoa(rID), } - return } // addChart provides a function to create chart as xl/charts/chart%d.xml by @@ -1272,7 +1271,6 @@ func (f *File) addSheetDrawingChart(drawingXML string, rID int, formatSet *forma } content.AbsoluteAnchor = append(content.AbsoluteAnchor, &absoluteAnchor) f.Drawings[drawingXML] = content - return } // deleteDrawing provides a function to delete chart graphic frame by given by diff --git a/excelize_test.go b/excelize_test.go index 1b4887275b..8bce6d1350 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -542,10 +542,10 @@ func TestWriteArrayFormula(t *testing.T) { assert.NoError(t, f.SetCellFormula("Sheet1", avgCell, fmt.Sprintf("ROUND(AVERAGEIF(%s,%s,%s),0)", assocRange, nameCell, valRange))) ref := stdevCell + ":" + stdevCell - t := STCellFormulaTypeArray + arr := STCellFormulaTypeArray // Use an array formula for standard deviation - f.SetCellFormula("Sheet1", stdevCell, fmt.Sprintf("ROUND(STDEVP(IF(%s=%s,%s)),0)", assocRange, nameCell, valRange), - FormulaOpts{}, FormulaOpts{Type: &t}, FormulaOpts{Ref: &ref}) + assert.NoError(t, f.SetCellFormula("Sheet1", stdevCell, fmt.Sprintf("ROUND(STDEVP(IF(%s=%s,%s)),0)", assocRange, nameCell, valRange), + FormulaOpts{}, FormulaOpts{Type: &arr}, FormulaOpts{Ref: &ref})) } assert.NoError(t, f.SaveAs(filepath.Join("test", "TestWriteArrayFormula.xlsx"))) diff --git a/file.go b/file.go index 9adc8597da..582099e336 100644 --- a/file.go +++ b/file.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2021 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. // diff --git a/file_test.go b/file_test.go index 0f979b8df9..656271fe85 100644 --- a/file_test.go +++ b/file_test.go @@ -35,7 +35,7 @@ func BenchmarkWrite(b *testing.B) { func TestWriteTo(t *testing.T) { f := File{} buf := bytes.Buffer{} - f.XLSX = make(map[string][]byte, 0) + f.XLSX = make(map[string][]byte) f.XLSX["/d/"] = []byte("s") _, err := f.WriteTo(bufio.NewWriter(&buf)) assert.EqualError(t, err, "zip: write to directory") diff --git a/lib.go b/lib.go index 0ebe4683d1..b6ea32139d 100644 --- a/lib.go +++ b/lib.go @@ -421,7 +421,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.sheetMap[trimSheetName(sheet)] f.addNameSpaces(name, ns) } diff --git a/picture.go b/picture.go index 77898fce6c..e46d37e246 100644 --- a/picture.go +++ b/picture.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2021 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. // @@ -613,7 +613,7 @@ func (f *File) drawingResize(sheet string, cell string, width, height float64, f } if inMergeCell { rng, _ = areaRangeToCoordinates(mergeCell.GetStartAxis(), mergeCell.GetEndAxis()) - sortCoordinates(rng) + _ = sortCoordinates(rng) } } if inMergeCell { diff --git a/pivotTable.go b/pivotTable.go index bffda17334..ff21ac1d06 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2021 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. // @@ -615,7 +615,7 @@ func (f *File) getPivotTableFieldsSubtotal(fields []PivotTableField) []string { enums := []string{"average", "count", "countNums", "max", "min", "product", "stdDev", "stdDevp", "sum", "var", "varp"} inEnums := func(enums []string, val string) string { for _, enum := range enums { - if strings.ToLower(enum) == strings.ToLower(val) { + if strings.EqualFold(enum, val) { return enum } } diff --git a/rows.go b/rows.go index 75bea47f6b..7b4f998e64 100644 --- a/rows.go +++ b/rows.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2021 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. // @@ -60,7 +60,6 @@ type Rows struct { err error curRow, totalRow, stashRow int sheet string - rows []xlsxRow f *File decoder *xml.Decoder } @@ -165,7 +164,6 @@ func rowXMLHandler(rowIterator *rowXMLIterator, xmlElement *xml.StartElement) { val, _ := colCell.getValueFrom(rowIterator.rows.f, rowIterator.d) rowIterator.columns = append(appendSpace(blank, rowIterator.columns), val) } - return } // Rows returns a rows iterator, used for streaming reading data for a diff --git a/sheet.go b/sheet.go index b0e29712ad..26c0081ec9 100644 --- a/sheet.go +++ b/sheet.go @@ -610,8 +610,8 @@ func (f *File) copySheet(from, to int) error { if ok { f.XLSX[toRels] = f.XLSX[fromRels] } - fromSheetXMLPath, _ := f.sheetMap[trimSheetName(fromSheet)] - fromSheetAttr, _ := f.xmlAttr[fromSheetXMLPath] + fromSheetXMLPath := f.sheetMap[trimSheetName(fromSheet)] + fromSheetAttr := f.xmlAttr[fromSheetXMLPath] f.xmlAttr[path] = fromSheetAttr return err } diff --git a/sheet_test.go b/sheet_test.go index f218da73ea..a72147203b 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -409,7 +409,7 @@ 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 } @@ -426,7 +426,7 @@ 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() + _ = file.Save() } diff --git a/stream.go b/stream.go index e2f7935626..f5fda9d700 100644 --- a/stream.go +++ b/stream.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2021 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. // @@ -93,9 +93,9 @@ func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) { } f.streams[sheetXML] = sw - sw.rawData.WriteString(XMLHeader + ``) + _, _ = sw.rawData.WriteString(``) return sw, err } @@ -184,7 +184,7 @@ func (sw *StreamWriter) AddTable(hcell, vcell, format string) error { tableXML := strings.Replace(sheetRelationshipsTableXML, "..", "xl", -1) // 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, "") @@ -296,12 +296,12 @@ func (sw *StreamWriter) SetRow(axis string, values []interface{}) error { val = v.Value } if err = setCellValFunc(&c, val); err != nil { - sw.rawData.WriteString(``) + _, _ = sw.rawData.WriteString(``) return err } writeCell(&sw.rawData, c) } - sw.rawData.WriteString(``) + _, _ = sw.rawData.WriteString(``) return sw.rawData.Sync() } @@ -361,7 +361,7 @@ func setCellIntFunc(c *xlsxC, val interface{}) (err error) { } func writeCell(buf *bufferedWriter, c xlsxC) { - buf.WriteString(``) + _, _ = buf.WriteString(`>`) if c.V != "" { - buf.WriteString(``) - xml.EscapeText(buf, []byte(c.V)) - buf.WriteString(``) + _, _ = buf.WriteString(``) + _ = xml.EscapeText(buf, []byte(c.V)) + _, _ = buf.WriteString(``) } - buf.WriteString(``) + _, _ = buf.WriteString(``) } // Flush ending the streaming writing process. func (sw *StreamWriter) Flush() error { - sw.rawData.WriteString(``) + _, _ = sw.rawData.WriteString(``) bulkAppendFields(&sw.rawData, sw.worksheet, 8, 38) - sw.rawData.WriteString(sw.tableParts) + _, _ = sw.rawData.WriteString(sw.tableParts) bulkAppendFields(&sw.rawData, sw.worksheet, 40, 40) - sw.rawData.WriteString(``) + _, _ = sw.rawData.WriteString(``) if err := sw.rawData.Flush(); err != nil { return err } @@ -407,7 +407,7 @@ func bulkAppendFields(w io.Writer, ws *xlsxWorksheet, from, to int) { enc := xml.NewEncoder(w) for i := 0; i < s.NumField(); i++ { if from <= i && i <= to { - enc.Encode(s.Field(i).Interface()) + _ = enc.Encode(s.Field(i).Interface()) } } } diff --git a/stream_test.go b/stream_test.go index ec7bd08420..7c6eb9bb16 100644 --- a/stream_test.go +++ b/stream_test.go @@ -26,7 +26,7 @@ func BenchmarkStreamWriter(b *testing.B) { streamWriter, _ := file.NewStreamWriter("Sheet1") for rowID := 10; rowID <= 110; rowID++ { cell, _ := CoordinatesToCellName(1, rowID) - streamWriter.SetRow(cell, row) + _ = streamWriter.SetRow(cell, row) } } @@ -98,7 +98,7 @@ func TestStreamWriter(t *testing.T) { file = NewFile() delete(file.Sheet, "xl/worksheets/sheet1.xml") file.XLSX["xl/worksheets/sheet1.xml"] = MacintoshCyrillicCharset - streamWriter, err = file.NewStreamWriter("Sheet1") + _, err = file.NewStreamWriter("Sheet1") assert.EqualError(t, err, "xml decode error: XML syntax error on line 1: invalid UTF-8") } diff --git a/styles.go b/styles.go index 851332b7cc..06215f36bc 100644 --- a/styles.go +++ b/styles.go @@ -2043,33 +2043,33 @@ var getXfIDFuncs = map[string]func(int, xlsxXf, *Style) bool{ }, "font": func(fontID int, xf xlsxXf, style *Style) bool { if style.Font == nil { - return (xf.FontID == nil || *xf.FontID == 0) && (xf.ApplyFont == nil || *xf.ApplyFont == false) + return (xf.FontID == nil || *xf.FontID == 0) && (xf.ApplyFont == nil || !*xf.ApplyFont) } - return xf.FontID != nil && *xf.FontID == fontID && xf.ApplyFont != nil && *xf.ApplyFont == true + return xf.FontID != nil && *xf.FontID == fontID && xf.ApplyFont != nil && *xf.ApplyFont }, "fill": func(fillID int, xf xlsxXf, style *Style) bool { if style.Fill.Type == "" { - return (xf.FillID == nil || *xf.FillID == 0) && (xf.ApplyFill == nil || *xf.ApplyFill == false) + return (xf.FillID == nil || *xf.FillID == 0) && (xf.ApplyFill == nil || !*xf.ApplyFill) } - return xf.FillID != nil && *xf.FillID == fillID && xf.ApplyFill != nil && *xf.ApplyFill == true + return xf.FillID != nil && *xf.FillID == fillID && xf.ApplyFill != nil && *xf.ApplyFill }, "border": func(borderID int, xf xlsxXf, style *Style) bool { if len(style.Border) == 0 { - return (xf.BorderID == nil || *xf.BorderID == 0) && (xf.ApplyBorder == nil || *xf.ApplyBorder == false) + return (xf.BorderID == nil || *xf.BorderID == 0) && (xf.ApplyBorder == nil || !*xf.ApplyBorder) } - return xf.BorderID != nil && *xf.BorderID == borderID && xf.ApplyBorder != nil && *xf.ApplyBorder == true + return xf.BorderID != nil && *xf.BorderID == borderID && xf.ApplyBorder != nil && *xf.ApplyBorder }, "alignment": func(ID int, xf xlsxXf, style *Style) bool { if style.Alignment == nil { - return xf.ApplyAlignment == nil || *xf.ApplyAlignment == false + return xf.ApplyAlignment == nil || !*xf.ApplyAlignment } return reflect.DeepEqual(xf.Alignment, newAlignment(style)) }, "protection": func(ID int, xf xlsxXf, style *Style) bool { if style.Protection == nil { - return xf.ApplyProtection == nil || *xf.ApplyProtection == false + return xf.ApplyProtection == nil || !*xf.ApplyProtection } - return reflect.DeepEqual(xf.Protection, newProtection(style)) && xf.ApplyProtection != nil && *xf.ApplyProtection == true + return reflect.DeepEqual(xf.Protection, newProtection(style)) && xf.ApplyProtection != nil && *xf.ApplyProtection }, } diff --git a/table.go b/table.go index ba8de25c80..8862b574b6 100644 --- a/table.go +++ b/table.go @@ -39,7 +39,14 @@ func parseFormatTableSet(formatSet string) (*formatTable, error) { // // 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 @@ -153,7 +160,7 @@ func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, formatSet } if name == "" { name = "Column" + strconv.Itoa(idx) - f.SetCellStr(sheet, cell, name) + _ = f.SetCellStr(sheet, cell, name) } tableColumn = append(tableColumn, &xlsxTableColumn{ ID: idx, From bddea1262b9219df224d19b24928d8da78a2f8c0 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 16 Feb 2021 00:02:14 +0800 Subject: [PATCH 333/957] This closes #785, support to change tab color; new formula function: FISHER, FISHERINV, GAMMA, GAMMALN, MIN, MINA, PERMUT --- calc.go | 278 ++++++++++++++++++++++++++++++++++++++++++++++-- calc_test.go | 83 +++++++++++++-- drawing.go | 3 +- sheetpr.go | 26 +++++ sheetpr_test.go | 9 ++ 5 files changed, 380 insertions(+), 19 deletions(-) diff --git a/calc.go b/calc.go index 33c504d25e..72ed876b52 100644 --- a/calc.go +++ b/calc.go @@ -249,9 +249,13 @@ var tokenPriority = map[string]int{ // FACT // FACTDOUBLE // FALSE +// FISHER +// FISHERINV // FLOOR // FLOOR.MATH // FLOOR.PRECISE +// GAMMA +// GAMMALN // GCD // HLOOKUP // IF @@ -278,6 +282,8 @@ var tokenPriority = map[string]int{ // MAX // MDETERM // MEDIAN +// MIN +// MINA // MOD // MROUND // MULTINOMIAL @@ -286,6 +292,7 @@ var tokenPriority = map[string]int{ // NOT // ODD // OR +// PERMUT // PI // POWER // PRODUCT @@ -295,6 +302,7 @@ var tokenPriority = map[string]int{ // RAND // RANDBETWEEN // REPT +// ROMAN // ROUND // ROUNDDOWN // ROUNDUP @@ -1798,7 +1806,7 @@ func (fn *formulaFuncs) FACT(argsList *list.List) formulaArg { if number.Number < 0 { return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) } - return newStringFormulaArg(strings.ToUpper(fmt.Sprintf("%g", fact(number.Number)))) + return newNumberFormulaArg(fact(number.Number)) } // FACTDOUBLE function returns the double factorial of a supplied number. The @@ -2552,7 +2560,8 @@ func (fn *formulaFuncs) RANDBETWEEN(argsList *list.List) formulaArg { if top.Number < bottom.Number { return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) } - return newNumberFormulaArg(float64(rand.New(rand.NewSource(time.Now().UnixNano())).Int63n(int64(top.Number-bottom.Number+1)) + int64(bottom.Number))) + num := rand.New(rand.NewSource(time.Now().UnixNano())).Int63n(int64(top.Number - bottom.Number + 1)) + return newNumberFormulaArg(float64(num + int64(bottom.Number))) } // romanNumerals defined a numeral system that originated in ancient Rome and @@ -2563,11 +2572,34 @@ type romanNumerals struct { s string } -var romanTable = [][]romanNumerals{{{1000, "M"}, {900, "CM"}, {500, "D"}, {400, "CD"}, {100, "C"}, {90, "XC"}, {50, "L"}, {40, "XL"}, {10, "X"}, {9, "IX"}, {5, "V"}, {4, "IV"}, {1, "I"}}, - {{1000, "M"}, {950, "LM"}, {900, "CM"}, {500, "D"}, {450, "LD"}, {400, "CD"}, {100, "C"}, {95, "VC"}, {90, "XC"}, {50, "L"}, {45, "VL"}, {40, "XL"}, {10, "X"}, {9, "IX"}, {5, "V"}, {4, "IV"}, {1, "I"}}, - {{1000, "M"}, {990, "XM"}, {950, "LM"}, {900, "CM"}, {500, "D"}, {490, "XD"}, {450, "LD"}, {400, "CD"}, {100, "C"}, {99, "IC"}, {90, "XC"}, {50, "L"}, {45, "VL"}, {40, "XL"}, {10, "X"}, {9, "IX"}, {5, "V"}, {4, "IV"}, {1, "I"}}, - {{1000, "M"}, {995, "VM"}, {990, "XM"}, {950, "LM"}, {900, "CM"}, {500, "D"}, {495, "VD"}, {490, "XD"}, {450, "LD"}, {400, "CD"}, {100, "C"}, {99, "IC"}, {90, "XC"}, {50, "L"}, {45, "VL"}, {40, "XL"}, {10, "X"}, {9, "IX"}, {5, "V"}, {4, "IV"}, {1, "I"}}, - {{1000, "M"}, {999, "IM"}, {995, "VM"}, {990, "XM"}, {950, "LM"}, {900, "CM"}, {500, "D"}, {499, "ID"}, {495, "VD"}, {490, "XD"}, {450, "LD"}, {400, "CD"}, {100, "C"}, {99, "IC"}, {90, "XC"}, {50, "L"}, {45, "VL"}, {40, "XL"}, {10, "X"}, {9, "IX"}, {5, "V"}, {4, "IV"}, {1, "I"}}} +var romanTable = [][]romanNumerals{ + { + {1000, "M"}, {900, "CM"}, {500, "D"}, {400, "CD"}, {100, "C"}, {90, "XC"}, + {50, "L"}, {40, "XL"}, {10, "X"}, {9, "IX"}, {5, "V"}, {4, "IV"}, {1, "I"}, + }, + { + {1000, "M"}, {950, "LM"}, {900, "CM"}, {500, "D"}, {450, "LD"}, {400, "CD"}, + {100, "C"}, {95, "VC"}, {90, "XC"}, {50, "L"}, {45, "VL"}, {40, "XL"}, + {10, "X"}, {9, "IX"}, {5, "V"}, {4, "IV"}, {1, "I"}, + }, + { + {1000, "M"}, {990, "XM"}, {950, "LM"}, {900, "CM"}, {500, "D"}, {490, "XD"}, + {450, "LD"}, {400, "CD"}, {100, "C"}, {99, "IC"}, {90, "XC"}, {50, "L"}, + {45, "VL"}, {40, "XL"}, {10, "X"}, {9, "IX"}, {5, "V"}, {4, "IV"}, {1, "I"}, + }, + { + {1000, "M"}, {995, "VM"}, {990, "XM"}, {950, "LM"}, {900, "CM"}, {500, "D"}, + {495, "VD"}, {490, "XD"}, {450, "LD"}, {400, "CD"}, {100, "C"}, {99, "IC"}, + {90, "XC"}, {50, "L"}, {45, "VL"}, {40, "XL"}, {10, "X"}, {9, "IX"}, + {5, "V"}, {4, "IV"}, {1, "I"}, + }, + { + {1000, "M"}, {999, "IM"}, {995, "VM"}, {990, "XM"}, {950, "LM"}, {900, "CM"}, + {500, "D"}, {499, "ID"}, {495, "VD"}, {490, "XD"}, {450, "LD"}, {400, "CD"}, + {100, "C"}, {99, "IC"}, {90, "XC"}, {50, "L"}, {45, "VL"}, {40, "XL"}, + {10, "X"}, {9, "IX"}, {5, "V"}, {4, "IV"}, {1, "I"}, + }, +} // ROMAN function converts an arabic number to Roman. I.e. for a supplied // integer, the function returns a text string depicting the roman numeral @@ -3191,6 +3223,112 @@ func (fn *formulaFuncs) COUNTBLANK(argsList *list.List) formulaArg { return newNumberFormulaArg(float64(count)) } +// FISHER function calculates the Fisher Transformation for a supplied value. +// The syntax of the function is: +// +// FISHER(x) +// +func (fn *formulaFuncs) FISHER(argsList *list.List) formulaArg { + if argsList.Len() != 1 { + return newErrorFormulaArg(formulaErrorVALUE, "FISHER requires 1 numeric argument") + } + token := argsList.Front().Value.(formulaArg) + switch token.Type { + case ArgString: + arg := token.ToNumber() + if arg.Type == ArgNumber { + if arg.Number <= -1 || arg.Number >= 1 { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + } + return newNumberFormulaArg(0.5 * math.Log((1+arg.Number)/(1-arg.Number))) + } + case ArgNumber: + if token.Number <= -1 || token.Number >= 1 { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + } + return newNumberFormulaArg(0.5 * math.Log((1+token.Number)/(1-token.Number))) + } + return newErrorFormulaArg(formulaErrorVALUE, "FISHER requires 1 numeric argument") +} + +// 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) +// +func (fn *formulaFuncs) FISHERINV(argsList *list.List) formulaArg { + if argsList.Len() != 1 { + return newErrorFormulaArg(formulaErrorVALUE, "FISHERINV requires 1 numeric argument") + } + token := argsList.Front().Value.(formulaArg) + switch token.Type { + case ArgString: + arg := token.ToNumber() + if arg.Type == ArgNumber { + return newNumberFormulaArg((math.Exp(2*arg.Number) - 1) / (math.Exp(2*arg.Number) + 1)) + } + case ArgNumber: + return newNumberFormulaArg((math.Exp(2*token.Number) - 1) / (math.Exp(2*token.Number) + 1)) + } + return newErrorFormulaArg(formulaErrorVALUE, "FISHERINV requires 1 numeric argument") +} + +// GAMMA function returns the value of the Gamma Function, Γ(n), for a +// specified number, n. The syntax of the function is: +// +// GAMMA(number) +// +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)) + } + return newErrorFormulaArg(formulaErrorVALUE, "GAMMA requires 1 numeric argument") +} + +// GAMMALN function returns the natural logarithm of the Gamma Function, Γ +// (n). The syntax of the function is: +// +// GAMMALN(x) +// +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))) + } + return newErrorFormulaArg(formulaErrorVALUE, "GAMMALN requires 1 numeric argument") +} + // MAX function returns the largest value from a supplied set of numeric // values. The syntax of the function is: // @@ -3203,7 +3341,10 @@ func (fn *formulaFuncs) MAX(argsList *list.List) formulaArg { return fn.max(false, argsList) } -// MAXA function returns the largest value from a supplied set of numeric values, while counting text and the logical value FALSE as the value 0 and counting the logical value TRUE as the value 1. The syntax of the function is: +// MAXA function returns the largest value from a supplied set of numeric +// values, while counting text and the logical value FALSE as the value 0 and +// counting the logical value TRUE as the value 1. The syntax of the function +// is: // // MAXA(number1,[number2],...) // @@ -3317,6 +3458,112 @@ func (fn *formulaFuncs) MEDIAN(argsList *list.List) formulaArg { return newNumberFormulaArg(median) } +// MIN function returns the smallest value from a supplied set of numeric +// values. The syntax of the function is: +// +// MIN(number1,[number2],...) +// +func (fn *formulaFuncs) MIN(argsList *list.List) formulaArg { + if argsList.Len() == 0 { + return newErrorFormulaArg(formulaErrorVALUE, "MIN requires at least 1 argument") + } + return fn.min(false, argsList) +} + +// MINA function returns the smallest value from a supplied set of numeric +// values, while counting text and the logical value FALSE as the value 0 and +// counting the logical value TRUE as the value 1. The syntax of the function +// is: +// +// MINA(number1,[number2],...) +// +func (fn *formulaFuncs) MINA(argsList *list.List) formulaArg { + if argsList.Len() == 0 { + return newErrorFormulaArg(formulaErrorVALUE, "MINA requires at least 1 argument") + } + return fn.min(true, argsList) +} + +// min is an implementation of the formula function MIN and MINA. +func (fn *formulaFuncs) min(mina bool, argsList *list.List) formulaArg { + min := math.MaxFloat64 + for token := argsList.Front(); token != nil; token = token.Next() { + arg := token.Value.(formulaArg) + switch arg.Type { + case ArgString: + if !mina && (arg.Value() == "TRUE" || arg.Value() == "FALSE") { + continue + } else { + num := arg.ToBool() + if num.Type == ArgNumber && num.Number < min { + min = num.Number + continue + } + } + num := arg.ToNumber() + if num.Type != ArgError && num.Number < min { + min = num.Number + } + case ArgNumber: + if arg.Number < min { + min = arg.Number + } + case ArgList, ArgMatrix: + 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 + } + } + } + case ArgError: + return arg + } + } + if min == math.MaxFloat64 { + min = 0 + } + return newNumberFormulaArg(min) +} + +// 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) +// +func (fn *formulaFuncs) PERMUT(argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, "PERMUT requires 2 numeric arguments") + } + number := argsList.Front().Value.(formulaArg).ToNumber() + chosen := argsList.Back().Value.(formulaArg).ToNumber() + if number.Type != ArgNumber { + return number + } + if chosen.Type != ArgNumber { + return chosen + } + if number.Number < chosen.Number { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + } + return newNumberFormulaArg(math.Round(fact(number.Number) / fact(number.Number-chosen.Number))) +} + // Information Functions // ISBLANK function tests if a specified cell is blank (empty) and if so, @@ -3356,7 +3603,11 @@ func (fn *formulaFuncs) ISERR(argsList *list.List) formulaArg { token := argsList.Front().Value.(formulaArg) result := "FALSE" if token.Type == ArgError { - for _, errType := range []string{formulaErrorDIV, formulaErrorNAME, formulaErrorNUM, formulaErrorVALUE, formulaErrorREF, formulaErrorNULL, formulaErrorSPILL, formulaErrorCALC, formulaErrorGETTINGDATA} { + for _, errType := range []string{ + formulaErrorDIV, formulaErrorNAME, formulaErrorNUM, + formulaErrorVALUE, formulaErrorREF, formulaErrorNULL, + formulaErrorSPILL, formulaErrorCALC, formulaErrorGETTINGDATA, + } { if errType == token.String { result = "TRUE" } @@ -3378,7 +3629,11 @@ func (fn *formulaFuncs) ISERROR(argsList *list.List) formulaArg { token := argsList.Front().Value.(formulaArg) result := "FALSE" if token.Type == ArgError { - for _, errType := range []string{formulaErrorDIV, formulaErrorNAME, formulaErrorNA, formulaErrorNUM, formulaErrorVALUE, formulaErrorREF, formulaErrorNULL, formulaErrorSPILL, formulaErrorCALC, formulaErrorGETTINGDATA} { + for _, errType := range []string{ + formulaErrorDIV, formulaErrorNAME, formulaErrorNA, formulaErrorNUM, + formulaErrorVALUE, formulaErrorREF, formulaErrorNULL, formulaErrorSPILL, + formulaErrorCALC, formulaErrorGETTINGDATA, + } { if errType == token.String { result = "TRUE" } @@ -4413,5 +4668,6 @@ func (fn *formulaFuncs) ENCODEURL(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "ENCODEURL requires 1 argument") } - return newStringFormulaArg(strings.Replace(url.QueryEscape(argsList.Front().Value.(formulaArg).Value()), "+", "%20", -1)) + token := argsList.Front().Value.(formulaArg).Value() + return newStringFormulaArg(strings.Replace(url.QueryEscape(token), "+", "%20", -1)) } diff --git a/calc_test.go b/calc_test.go index ef028a91d0..1f9fea68bb 100644 --- a/calc_test.go +++ b/calc_test.go @@ -205,7 +205,7 @@ func TestCalcCellValue(t *testing.T) { // FACT "=FACT(3)": "6", "=FACT(6)": "720", - "=FACT(10)": "3.6288E+06", + "=FACT(10)": "3.6288e+06", "=FACT(FACT(3))": "720", // FACTDOUBLE "=FACTDOUBLE(5)": "15", @@ -490,6 +490,23 @@ func TestCalcCellValue(t *testing.T) { "=COUNTBLANK(1)": "0", "=COUNTBLANK(B1:C1)": "1", "=COUNTBLANK(C1)": "1", + // FISHER + "=FISHER(-0.9)": "-1.47221948958322", + "=FISHER(-0.25)": "-0.255412811882995", + "=FISHER(0.8)": "1.09861228866811", + "=FISHER(INT(0))": "0", + // FISHERINV + "=FISHERINV(-0.2)": "-0.197375320224904", + "=FISHERINV(INT(0))": "0", + "=FISHERINV(2.8)": "0.992631520201128", + // GAMMA + "=GAMMA(0.1)": "9.513507698668732", + "=GAMMA(INT(1))": "1", + "=GAMMA(1.5)": "0.886226925452758", + "=GAMMA(5.5)": "52.34277778455352", + // GAMMALN + "=GAMMALN(4.5)": "2.453736570842443", + "=GAMMALN(INT(1))": "0", // MAX "=MAX(1)": "1", "=MAX(TRUE())": "1", @@ -509,6 +526,25 @@ func TestCalcCellValue(t *testing.T) { "=MEDIAN(A1:A5,12)": "2", "=MEDIAN(A1:A5)": "1.5", "=MEDIAN(A1:A5,MEDIAN(A1:A5,12))": "2", + // MIN + "=MIN(1)": "1", + "=MIN(TRUE())": "1", + "=MIN(0.5,FALSE())": "0", + "=MIN(FALSE())": "0", + "=MIN(MUNIT(2))": "0", + "=MIN(INT(1))": "1", + // MINA + "=MINA(1)": "1", + "=MINA(TRUE())": "1", + "=MINA(0.5,FALSE())": "0", + "=MINA(FALSE())": "0", + "=MINA(MUNIT(2))": "0", + "=MINA(INT(1))": "1", + "=MINA(A1:B4,MUNIT(1),INT(0),1,E1:F2,\"\")": "0", + // PERMUT + "=PERMUT(6,6)": "720", + "=PERMUT(7,6)": "5040", + "=PERMUT(10,6)": "151200", // Information Functions // ISBLANK "=ISBLANK(A1)": "FALSE", @@ -940,6 +976,24 @@ func TestCalcCellValue(t *testing.T) { // COUNTBLANK "=COUNTBLANK()": "COUNTBLANK requires 1 argument", "=COUNTBLANK(1,2)": "COUNTBLANK requires 1 argument", + // FISHER + "=FISHER()": "FISHER requires 1 numeric argument", + "=FISHER(2)": "#N/A", + "=FISHER(INT(-2)))": "#N/A", + "=FISHER(F1)": "FISHER requires 1 numeric argument", + // FISHERINV + "=FISHERINV()": "FISHERINV requires 1 numeric argument", + "=FISHERINV(F1)": "FISHERINV requires 1 numeric argument", + // GAMMA + "=GAMMA()": "GAMMA requires 1 numeric argument", + "=GAMMA(F1)": "GAMMA requires 1 numeric argument", + "=GAMMA(0)": "#N/A", + "=GAMMA(INT(0))": "#N/A", + // GAMMALN + "=GAMMALN()": "GAMMALN requires 1 numeric argument", + "=GAMMALN(F1)": "GAMMALN requires 1 numeric argument", + "=GAMMALN(0)": "#N/A", + "=GAMMALN(INT(0))": "#N/A", // MAX "=MAX()": "MAX requires at least 1 argument", "=MAX(NA())": "#N/A", @@ -950,6 +1004,17 @@ func TestCalcCellValue(t *testing.T) { "=MEDIAN()": "MEDIAN requires at least 1 argument", "=MEDIAN(\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", "=MEDIAN(D1:D2)": "strconv.ParseFloat: parsing \"Month\": invalid syntax", + // MIN + "=MIN()": "MIN requires at least 1 argument", + "=MIN(NA())": "#N/A", + // MINA + "=MINA()": "MINA requires at least 1 argument", + "=MINA(NA())": "#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", // Information Functions // ISBLANK "=ISBLANK(A1,A2)": "ISBLANK requires 1 argument", @@ -1315,16 +1380,20 @@ func TestCalcVLOOKUP(t *testing.T) { } } -func TestCalcMAX(t *testing.T) { +func TestCalcMAXMIN(t *testing.T) { cellData := [][]interface{}{ - {0.5, "TRUE"}, + {0.5, "TRUE", -0.5, "FALSE"}, } f := prepareCalcData(cellData) formulaList := map[string]string{ - "=MAX(0.5,B1)": "0.5", - "=MAX(A1:B1)": "0.5", - "=MAXA(A1:B1)": "1", - "=MAXA(0.5,B1)": "1", + "=MAX(0.5,B1)": "0.5", + "=MAX(A1:B1)": "0.5", + "=MAXA(A1:B1)": "1", + "=MAXA(0.5,B1)": "1", + "=MIN(-0.5,D1)": "-0.5", + "=MIN(C1:D1)": "-0.5", + "=MINA(C1:D1)": "-0.5", + "=MINA(-0.5,D1)": "-0.5", } for formula, expected := range formulaList { assert.NoError(t, f.SetCellFormula("Sheet1", "B10", formula)) diff --git a/drawing.go b/drawing.go index 2518651e26..632b914050 100644 --- a/drawing.go +++ b/drawing.go @@ -939,7 +939,8 @@ func (f *File) drawChartDLbls(formatSet *formatChart) *cDLbls { // given format sets. func (f *File) drawChartSeriesDLbls(formatSet *formatChart) *cDLbls { dLbls := f.drawChartDLbls(formatSet) - chartSeriesDLbls := map[string]*cDLbls{Scatter: nil, Surface3D: nil, WireframeSurface3D: nil, Contour: nil, WireframeContour: nil, Bubble: nil, Bubble3D: nil} + chartSeriesDLbls := map[string]*cDLbls{ + Scatter: nil, Surface3D: nil, WireframeSurface3D: nil, Contour: nil, WireframeContour: nil, Bubble: nil, Bubble3D: nil} if _, ok := chartSeriesDLbls[formatSet.Type]; ok { return nil } diff --git a/sheetpr.go b/sheetpr.go index ee3b23c504..52586d99b7 100644 --- a/sheetpr.go +++ b/sheetpr.go @@ -11,6 +11,8 @@ package excelize +import "strings" + // SheetPrOption is an option of a view of a worksheet. See SetSheetPrOptions(). type SheetPrOption interface { setSheetPrOption(view *xlsxSheetPr) @@ -31,6 +33,8 @@ type ( Published bool // FitToPage is a SheetPrOption FitToPage bool + // TabColor is a SheetPrOption + TabColor string // AutoPageBreaks is a SheetPrOption AutoPageBreaks bool // OutlineSummaryBelow is an outlinePr, within SheetPr option @@ -125,6 +129,28 @@ func (o *FitToPage) getSheetPrOption(pr *xlsxSheetPr) { *o = FitToPage(pr.PageSetUpPr.FitToPage) } +// setSheetPrOption implements the SheetPrOption interface and specifies a +// stable name of the sheet. +func (o TabColor) setSheetPrOption(pr *xlsxSheetPr) { + if pr.TabColor == nil { + if string(o) == "" { + return + } + 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 *TabColor) getSheetPrOption(pr *xlsxSheetPr) { + if pr == nil || pr.TabColor == nil { + *o = "" + return + } + *o = TabColor(strings.TrimPrefix(pr.TabColor.RGB, "FF")) +} + // setSheetPrOption implements the SheetPrOption interface. func (o AutoPageBreaks) setSheetPrOption(pr *xlsxSheetPr) { if pr.PageSetUpPr == nil { diff --git a/sheetpr_test.go b/sheetpr_test.go index 29bd99efda..42e2e0d1ed 100644 --- a/sheetpr_test.go +++ b/sheetpr_test.go @@ -13,6 +13,7 @@ var _ = []SheetPrOption{ EnableFormatConditionsCalculation(false), Published(false), FitToPage(true), + TabColor("#FFFF00"), AutoPageBreaks(true), OutlineSummaryBelow(true), } @@ -22,6 +23,7 @@ var _ = []SheetPrOptionPtr{ (*EnableFormatConditionsCalculation)(nil), (*Published)(nil), (*FitToPage)(nil), + (*TabColor)(nil), (*AutoPageBreaks)(nil), (*OutlineSummaryBelow)(nil), } @@ -35,6 +37,7 @@ func ExampleFile_SetSheetPrOptions() { EnableFormatConditionsCalculation(false), Published(false), FitToPage(true), + TabColor("#FFFF00"), AutoPageBreaks(true), OutlineSummaryBelow(false), ); err != nil { @@ -52,6 +55,7 @@ func ExampleFile_GetSheetPrOptions() { enableFormatConditionsCalculation EnableFormatConditionsCalculation published Published fitToPage FitToPage + tabColor TabColor autoPageBreaks AutoPageBreaks outlineSummaryBelow OutlineSummaryBelow ) @@ -61,6 +65,7 @@ func ExampleFile_GetSheetPrOptions() { &enableFormatConditionsCalculation, &published, &fitToPage, + &tabColor, &autoPageBreaks, &outlineSummaryBelow, ); err != nil { @@ -71,6 +76,7 @@ func ExampleFile_GetSheetPrOptions() { fmt.Println("- enableFormatConditionsCalculation:", enableFormatConditionsCalculation) fmt.Println("- published:", published) fmt.Println("- fitToPage:", fitToPage) + fmt.Printf("- tabColor: %q\n", tabColor) fmt.Println("- autoPageBreaks:", autoPageBreaks) fmt.Println("- outlineSummaryBelow:", outlineSummaryBelow) // Output: @@ -79,6 +85,7 @@ func ExampleFile_GetSheetPrOptions() { // - enableFormatConditionsCalculation: true // - published: true // - fitToPage: false + // - tabColor: "" // - autoPageBreaks: false // - outlineSummaryBelow: true } @@ -94,6 +101,7 @@ func TestSheetPrOptions(t *testing.T) { {new(EnableFormatConditionsCalculation), EnableFormatConditionsCalculation(false)}, {new(Published), Published(false)}, {new(FitToPage), FitToPage(true)}, + {new(TabColor), TabColor("FFFF00")}, {new(AutoPageBreaks), AutoPageBreaks(true)}, {new(OutlineSummaryBelow), OutlineSummaryBelow(false)}, } @@ -147,6 +155,7 @@ func TestSheetPrOptions(t *testing.T) { func TestSetSheetrOptions(t *testing.T) { f := NewFile() + assert.NoError(t, f.SetSheetPrOptions("Sheet1", TabColor(""))) // Test SetSheetrOptions on not exists worksheet. assert.EqualError(t, f.SetSheetPrOptions("SheetN"), "sheet SheetN is not exist") } From b3493c54168c0f05b8623775640304128fd472e9 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 17 Feb 2021 00:51:06 +0800 Subject: [PATCH 334/957] #65 fn: KURT, STDEV, STDEVA --- calc.go | 164 ++++++++++++++++++++++++++++++++++++++++++++++----- calc_test.go | 59 +++++++++++++----- 2 files changed, 191 insertions(+), 32 deletions(-) diff --git a/calc.go b/calc.go index 72ed876b52..ca44bf5ceb 100644 --- a/calc.go +++ b/calc.go @@ -271,6 +271,7 @@ var tokenPriority = map[string]int{ // ISODD // ISTEXT // ISO.CEILING +// KURT // LCM // LEN // LENB @@ -314,6 +315,8 @@ var tokenPriority = map[string]int{ // SINH // SQRT // SQRTPI +// STDEV +// STDEVA // SUM // SUMIF // SUMSQ @@ -2872,41 +2875,118 @@ func (fn *formulaFuncs) SQRTPI(argsList *list.List) formulaArg { return newNumberFormulaArg(math.Sqrt(number.Number * math.Pi)) } +// STDEV function calculates the sample standard deviation of a supplied set +// of values. The syntax of the function is: +// +// STDEV(number1,[number2],...) +// +func (fn *formulaFuncs) STDEV(argsList *list.List) formulaArg { + if argsList.Len() < 1 { + return newErrorFormulaArg(formulaErrorVALUE, "STDEV requires at least 1 argument") + } + return fn.stdev(false, argsList) +} + +// STDEVA function estimates standard deviation based on a sample. The +// 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],...) +// +func (fn *formulaFuncs) STDEVA(argsList *list.List) formulaArg { + if argsList.Len() < 1 { + return newErrorFormulaArg(formulaErrorVALUE, "STDEVA requires at least 1 argument") + } + return fn.stdev(true, argsList) +} + +// stdev is an implementation of the formula function STDEV and STDEVA. +func (fn *formulaFuncs) stdev(stdeva bool, argsList *list.List) formulaArg { + pow := func(result, count float64, n, m formulaArg) (float64, float64) { + if result == -1 { + result = math.Pow((n.Number - m.Number), 2) + } else { + result += math.Pow((n.Number - m.Number), 2) + } + count++ + return result, count + } + count, result := -1.0, -1.0 + var mean formulaArg + if stdeva { + mean = fn.AVERAGEA(argsList) + } else { + mean = fn.AVERAGE(argsList) + } + for arg := argsList.Front(); arg != nil; arg = arg.Next() { + token := arg.Value.(formulaArg) + switch token.Type { + case ArgString, ArgNumber: + if !stdeva && (token.Value() == "TRUE" || token.Value() == "FALSE") { + continue + } else if stdeva && (token.Value() == "TRUE" || token.Value() == "FALSE") { + num := token.ToBool() + if num.Type == ArgNumber { + result, count = pow(result, count, num, mean) + continue + } + } else { + num := token.ToNumber() + if num.Type == ArgNumber { + result, count = pow(result, count, num, mean) + } + } + case ArgList, ArgMatrix: + for _, row := range token.ToList() { + if row.Type == ArgNumber || row.Type == ArgString { + if !stdeva && (row.Value() == "TRUE" || row.Value() == "FALSE") { + continue + } else if stdeva && (row.Value() == "TRUE" || row.Value() == "FALSE") { + num := row.ToBool() + if num.Type == ArgNumber { + result, count = pow(result, count, num, mean) + continue + } + } else { + num := row.ToNumber() + if num.Type == ArgNumber { + result, count = pow(result, count, num, mean) + } + } + } + } + } + } + if count > 0 && result >= 0 { + return newNumberFormulaArg(math.Sqrt(result / count)) + } + return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV) +} + // 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],...) // func (fn *formulaFuncs) SUM(argsList *list.List) formulaArg { - var ( - val, sum float64 - err error - ) + var sum float64 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 - } - if val, err = strconv.ParseFloat(token.String, 64); err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + if num := token.ToNumber(); num.Type == ArgNumber { + sum += num.Number } - sum += val case ArgNumber: sum += token.Number case ArgMatrix: for _, row := range token.Matrix { for _, value := range row { - if value.String == "" { - continue - } - if val, err = strconv.ParseFloat(value.String, 64); err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + if num := value.ToNumber(); num.Type == ArgNumber { + sum += num.Number } - sum += val } } } @@ -3111,6 +3191,16 @@ func (fn *formulaFuncs) countSum(countText bool, args []formulaArg) (count, sum count++ } case ArgString: + if !countText && (arg.Value() == "TRUE" || arg.Value() == "FALSE") { + continue + } else if countText && (arg.Value() == "TRUE" || arg.Value() == "FALSE") { + num := arg.ToBool() + if num.Type == ArgNumber { + count++ + sum += num.Number + continue + } + } num := arg.ToNumber() if countText && num.Type == ArgError && arg.String != "" { count++ @@ -3329,6 +3419,48 @@ func (fn *formulaFuncs) GAMMALN(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorVALUE, "GAMMALN requires 1 numeric argument") } +// KURT function calculates the kurtosis of a supplied set of values. The +// syntax of the function is: +// +// KURT(number1,[number2],...) +// +func (fn *formulaFuncs) KURT(argsList *list.List) formulaArg { + if argsList.Len() < 1 { + return newErrorFormulaArg(formulaErrorVALUE, "KURT requires at least 1 argument") + } + mean, stdev := fn.AVERAGE(argsList), fn.STDEV(argsList) + if stdev.Number > 0 { + count, summer := 0.0, 0.0 + for arg := argsList.Front(); arg != nil; arg = arg.Next() { + token := arg.Value.(formulaArg) + switch token.Type { + case ArgString, ArgNumber: + num := token.ToNumber() + if num.Type == ArgError { + continue + } + summer += math.Pow((num.Number-mean.Number)/stdev.Number, 4) + count++ + case ArgList, ArgMatrix: + for _, row := range token.ToList() { + if row.Type == ArgNumber || row.Type == ArgString { + num := row.ToNumber() + if num.Type == ArgError { + continue + } + summer += math.Pow((num.Number-mean.Number)/stdev.Number, 4) + count++ + } + } + } + } + if count > 3 { + return newNumberFormulaArg(summer*(count*(count+1)/((count-1)*(count-2)*(count-3))) - (3 * math.Pow(count-1, 2) / ((count - 2) * (count - 3)))) + } + } + return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV) +} + // MAX function returns the largest value from a supplied set of numeric // values. The syntax of the function is: // diff --git a/calc_test.go b/calc_test.go index 1f9fea68bb..5b406bcc3f 100644 --- a/calc_test.go +++ b/calc_test.go @@ -415,6 +415,15 @@ func TestCalcCellValue(t *testing.T) { "=SQRTPI(100)": "17.72453850905516", "=SQRTPI(0)": "0", "=SQRTPI(SQRTPI(0))": "0", + // STDEV + "=STDEV(F2:F9)": "10724.978287523809", + "=STDEV(MUNIT(2))": "0.577350269189626", + "=STDEV(0,INT(0))": "0", + "=STDEV(INT(1),INT(1))": "0", + // STDEVA + "=STDEVA(F2:F9)": "10724.978287523809", + "=STDEVA(MUNIT(2))": "0.577350269189626", + "=STDEVA(0,INT(0))": "0", // SUM "=SUM(1,2)": "3", `=SUM("",1,2)`: "3", @@ -507,6 +516,10 @@ func TestCalcCellValue(t *testing.T) { // GAMMALN "=GAMMALN(4.5)": "2.453736570842443", "=GAMMALN(INT(1))": "0", + // KURT + "=KURT(F1:F9)": "-1.033503502551368", + "=KURT(F1,F2:F9)": "-1.033503502551368", + "=KURT(INT(1),MUNIT(2))": "-3.333333333333336", // MAX "=MAX(1)": "1", "=MAX(TRUE())": "1", @@ -945,14 +958,19 @@ func TestCalcCellValue(t *testing.T) { // SQRTPI "=SQRTPI()": "SQRTPI requires 1 numeric argument", `=SQRTPI("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + // STDEV + "=STDEV()": "STDEV requires at least 1 argument", + "=STDEV(E2:E9)": "#DIV/0!", + // STDEVA + "=STDEVA()": "STDEVA requires at least 1 argument", + "=STDEVA(E2:E9)": "#DIV/0!", // SUM - "=SUM((": "formula not valid", - "=SUM(-)": "formula not valid", - "=SUM(1+)": "formula not valid", - "=SUM(1-)": "formula not valid", - "=SUM(1*)": "formula not valid", - "=SUM(1/)": "formula not valid", - `=SUM("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + "=SUM((": "formula not valid", + "=SUM(-)": "formula not valid", + "=SUM(1+)": "formula not valid", + "=SUM(1-)": "formula not valid", + "=SUM(1*)": "formula not valid", + "=SUM(1/)": "formula not valid", // SUMIF "=SUMIF()": "SUMIF requires at least 2 argument", // SUMSQ @@ -994,6 +1012,9 @@ func TestCalcCellValue(t *testing.T) { "=GAMMALN(F1)": "GAMMALN requires 1 numeric argument", "=GAMMALN(0)": "#N/A", "=GAMMALN(INT(0))": "#N/A", + // KURT + "=KURT()": "KURT requires at least 1 argument", + "=KURT(F1,INT(1))": "#DIV/0!", // MAX "=MAX()": "MAX requires at least 1 argument", "=MAX(NA())": "#N/A", @@ -1168,6 +1189,8 @@ func TestCalcCellValue(t *testing.T) { "=1+SUM(SUM(A1+A2/A3)*(2-3),2)": "1.333333333333334", "=A1/A2/SUM(A1:A2:B1)": "0.041666666666667", "=A1/A2/SUM(A1:A2:B1)*A3": "0.125", + "=SUM(B1:D1)": "4", + "=SUM(\"X\")": "0", } for formula, expected := range referenceCalc { f := prepareCalcData(cellData) @@ -1380,20 +1403,24 @@ func TestCalcVLOOKUP(t *testing.T) { } } -func TestCalcMAXMIN(t *testing.T) { +func TestCalcBoolean(t *testing.T) { cellData := [][]interface{}{ {0.5, "TRUE", -0.5, "FALSE"}, } f := prepareCalcData(cellData) formulaList := map[string]string{ - "=MAX(0.5,B1)": "0.5", - "=MAX(A1:B1)": "0.5", - "=MAXA(A1:B1)": "1", - "=MAXA(0.5,B1)": "1", - "=MIN(-0.5,D1)": "-0.5", - "=MIN(C1:D1)": "-0.5", - "=MINA(C1:D1)": "-0.5", - "=MINA(-0.5,D1)": "-0.5", + "=AVERAGEA(A1:C1)": "0.333333333333333", + "=MAX(0.5,B1)": "0.5", + "=MAX(A1:B1)": "0.5", + "=MAXA(A1:B1)": "1", + "=MAXA(0.5,B1)": "1", + "=MIN(-0.5,D1)": "-0.5", + "=MIN(C1:D1)": "-0.5", + "=MINA(C1:D1)": "-0.5", + "=MINA(-0.5,D1)": "-0.5", + "=STDEV(A1:C1)": "0.707106781186548", + "=STDEV(A1,B1,C1)": "0.707106781186548", + "=STDEVA(A1:C1,B1)": "0.707106781186548", } for formula, expected := range formulaList { assert.NoError(t, f.SetCellFormula("Sheet1", "B10", formula)) From ae6f56b9531d6dce437a719523c1a9c67b97f776 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 18 Feb 2021 08:13:22 +0800 Subject: [PATCH 335/957] #65 fn: DEC2BIN, DEC2HEX, DEC2OCT --- .travis.yml | 1 + calc.go | 101 ++++++++++++++++++++++++++++++++++++++++++++++++++- calc_test.go | 44 ++++++++++++++++++++++ 3 files changed, 144 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index cd22ebb80c..f302eededc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,7 @@ go: - 1.13.x - 1.14.x - 1.15.x + - 1.16.x os: - linux diff --git a/calc.go b/calc.go index ca44bf5ceb..a11315a166 100644 --- a/calc.go +++ b/calc.go @@ -26,6 +26,7 @@ import ( "strings" "time" "unicode" + "unsafe" "github.com/xuri/efp" ) @@ -240,6 +241,9 @@ var tokenPriority = map[string]int{ // CSC // CSCH // DATE +// DEC2BIN +// DEC2HEX +// DEC2OCT // DECIMAL // DEGREES // ENCODEURL @@ -1140,7 +1144,100 @@ func formulaCriteriaEval(val string, criteria *formulaCriteria) (result bool, er return } -// Math and Trigonometric functions +// Engineering Functions + +// DEC2BIN function converts a decimal number into a Binary (Base 2) number. +// The syntax of the function is: +// +// DEC2BIN(number,[places]) +// +func (fn *formulaFuncs) DEC2BIN(argsList *list.List) formulaArg { + if argsList.Len() < 1 { + return newErrorFormulaArg(formulaErrorVALUE, "DEC2BIN requires at least 1 argument") + } + if argsList.Len() > 2 { + return newErrorFormulaArg(formulaErrorVALUE, "DEC2BIN allows at most 2 arguments") + } + return fn.dec2x("DEC2BIN", argsList) +} + +// DEC2HEX function converts a decimal number into a Hexadecimal (Base 16) +// number. The syntax of the function is: +// +// DEC2HEX(number,[places]) +// +func (fn *formulaFuncs) DEC2HEX(argsList *list.List) formulaArg { + if argsList.Len() < 1 { + return newErrorFormulaArg(formulaErrorVALUE, "DEC2HEX requires at least 1 argument") + } + if argsList.Len() > 2 { + return newErrorFormulaArg(formulaErrorVALUE, "DEC2HEX allows at most 2 arguments") + } + return fn.dec2x("DEC2HEX", argsList) +} + +// DEC2OCT function converts a decimal number into an Octal (Base 8) number. +// The syntax of the function is: +// +// DEC2OCT(number,[places]) +// +func (fn *formulaFuncs) DEC2OCT(argsList *list.List) formulaArg { + if argsList.Len() < 1 { + return newErrorFormulaArg(formulaErrorVALUE, "DEC2OCT requires at least 1 argument") + } + if argsList.Len() > 2 { + return newErrorFormulaArg(formulaErrorVALUE, "DEC2OCT allows at most 2 arguments") + } + return fn.dec2x("DEC2OCT", argsList) +} + +// dec2x is an implementation of the formula function DEC2BIN, DEC2HEX and DEC2OCT. +func (fn *formulaFuncs) dec2x(name string, argsList *list.List) formulaArg { + decimal := argsList.Front().Value.(formulaArg).ToNumber() + if decimal.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, decimal.Error) + } + maxLimitMap := map[string]float64{ + "DEC2BIN": 511, + "DEC2HEX": 549755813887, + "DEC2OCT": 536870911, + } + minLimitMap := map[string]float64{ + "DEC2BIN": -512, + "DEC2HEX": -549755813888, + "DEC2OCT": -536870912, + } + baseMap := map[string]int{ + "DEC2BIN": 2, + "DEC2HEX": 16, + "DEC2OCT": 8, + } + maxLimit := maxLimitMap[name] + minLimit := minLimitMap[name] + base := baseMap[name] + if decimal.Number < minLimit || decimal.Number > maxLimit { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + n := int64(decimal.Number) + binary := strconv.FormatUint(*(*uint64)(unsafe.Pointer(&n)), base) + if argsList.Len() == 2 { + places := argsList.Back().Value.(formulaArg).ToNumber() + if places.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, places.Error) + } + binaryPlaces := len(binary) + if places.Number < 0 || places.Number > 10 || binaryPlaces > int(places.Number) { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + return newStringFormulaArg(strings.ToUpper(fmt.Sprintf("%s%s", strings.Repeat("0", int(places.Number)-binaryPlaces), binary))) + } + if decimal.Number < 0 && len(binary) > 10 { + return newStringFormulaArg(strings.ToUpper(binary[len(binary)-10:])) + } + return newStringFormulaArg(strings.ToUpper(binary)) +} + +// Math and Trigonometric Functions // ABS function returns the absolute value of any supplied number. The syntax // of the function is: @@ -4357,7 +4454,7 @@ func (fn *formulaFuncs) IF(argsList *list.List) formulaArg { return newStringFormulaArg(result) } -// Excel Lookup and Reference Functions +// Lookup and Reference Functions // CHOOSE function returns a value from an array, that corresponds to a // supplied index number (position). The syntax of the function is: diff --git a/calc_test.go b/calc_test.go index 5b406bcc3f..28ffb6cfb3 100644 --- a/calc_test.go +++ b/calc_test.go @@ -46,6 +46,25 @@ func TestCalcCellValue(t *testing.T) { "=2>=1": "TRUE", "=2>=3": "FALSE", "=1&2": "12", + // Engineering Functions + // DEC2BIN + "=DEC2BIN(2)": "10", + "=DEC2BIN(3)": "11", + "=DEC2BIN(2,10)": "0000000010", + "=DEC2BIN(-2)": "1111111110", + "=DEC2BIN(6)": "110", + // DEC2HEX + "=DEC2HEX(10)": "A", + "=DEC2HEX(31)": "1F", + "=DEC2HEX(16,10)": "0000000010", + "=DEC2HEX(-16)": "FFFFFFFFF0", + "=DEC2HEX(273)": "111", + // DEC2OCT + "=DEC2OCT(8)": "10", + "=DEC2OCT(18)": "22", + "=DEC2OCT(8,10)": "0000000010", + "=DEC2OCT(-8)": "7777777770", + "=DEC2OCT(237)": "355", // ABS "=ABS(-1)": "1", "=ABS(-6.5)": "6.5", @@ -707,6 +726,31 @@ func TestCalcCellValue(t *testing.T) { } mathCalcError := map[string]string{ "=1/0": "#DIV/0!", + // Engineering Functions + // 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!", + // 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!", + // 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!", // ABS "=ABS()": "ABS requires 1 numeric argument", `=ABS("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", From 9154d500cf50621e15bf4a2bb9f6b5045d7b72d2 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 19 Feb 2021 00:03:51 +0800 Subject: [PATCH 336/957] ref: #756, set cell as blank when SetCellValue with nil #756, new formula fn: BITAND, BITLSHIFT, BITOR, BITRSHIFT, BITXOR --- calc.go | 102 +++++++++++++++++++++++++++++++++++++++++---------- calc_test.go | 53 ++++++++++++++++++++++++++ cell.go | 9 +++-- 3 files changed, 141 insertions(+), 23 deletions(-) diff --git a/calc.go b/calc.go index a11315a166..f49477fdb8 100644 --- a/calc.go +++ b/calc.go @@ -222,6 +222,11 @@ var tokenPriority = map[string]int{ // AVERAGE // AVERAGEA // BASE +// BITAND +// BITLSHIFT +// BITOR +// BITRSHIFT +// BITXOR // CEILING // CEILING.MATH // CEILING.PRECISE @@ -1146,18 +1151,82 @@ func formulaCriteriaEval(val string, criteria *formulaCriteria) (result bool, er // Engineering Functions +// BITAND function returns the bitwise 'AND' for two supplied integers. The +// syntax of the function is: +// +// BITAND(number1,number2) +// +func (fn *formulaFuncs) BITAND(argsList *list.List) formulaArg { + return fn.bitwise("BITAND", argsList) +} + +// BITLSHIFT function returns a supplied integer, shifted left by a specified +// number of bits. The syntax of the function is: +// +// BITLSHIFT(number1,shift_amount) +// +func (fn *formulaFuncs) BITLSHIFT(argsList *list.List) formulaArg { + return fn.bitwise("BITLSHIFT", argsList) +} + +// BITOR function returns the bitwise 'OR' for two supplied integers. The +// syntax of the function is: +// +// BITOR(number1,number2) +// +func (fn *formulaFuncs) BITOR(argsList *list.List) formulaArg { + return fn.bitwise("BITOR", argsList) +} + +// BITRSHIFT function returns a supplied integer, shifted right by a specified +// number of bits. The syntax of the function is: +// +// BITRSHIFT(number1,shift_amount) +// +func (fn *formulaFuncs) BITRSHIFT(argsList *list.List) formulaArg { + return fn.bitwise("BITRSHIFT", argsList) +} + +// BITXOR function returns the bitwise 'XOR' (exclusive 'OR') for two supplied +// integers. The syntax of the function is: +// +// BITXOR(number1,number2) +// +func (fn *formulaFuncs) BITXOR(argsList *list.List) formulaArg { + return fn.bitwise("BITXOR", argsList) +} + +// bitwise is an implementation of the formula function BITAND, BITLSHIFT, +// BITOR, BITRSHIFT and BITXOR. +func (fn *formulaFuncs) bitwise(name string, argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires 2 numeric arguments", name)) + } + num1, num2 := argsList.Front().Value.(formulaArg).ToNumber(), argsList.Back().Value.(formulaArg).ToNumber() + if num1.Type != ArgNumber || num2.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + max := math.Pow(2, 48) - 1 + if num1.Number < 0 || num1.Number > max || num2.Number < 0 || num2.Number > max { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + bitwiseFuncMap := map[string]func(a, b int) int{ + "BITAND": func(a, b int) int { return a & b }, + "BITLSHIFT": func(a, b int) int { return a << uint(b) }, + "BITOR": func(a, b int) int { return a | b }, + "BITRSHIFT": func(a, b int) int { return a >> uint(b) }, + "BITXOR": func(a, b int) int { return a ^ b }, + } + bitwiseFunc, _ := bitwiseFuncMap[name] + return newNumberFormulaArg(float64(bitwiseFunc(int(num1.Number), int(num2.Number)))) +} + // DEC2BIN function converts a decimal number into a Binary (Base 2) number. // The syntax of the function is: // // DEC2BIN(number,[places]) // func (fn *formulaFuncs) DEC2BIN(argsList *list.List) formulaArg { - if argsList.Len() < 1 { - return newErrorFormulaArg(formulaErrorVALUE, "DEC2BIN requires at least 1 argument") - } - if argsList.Len() > 2 { - return newErrorFormulaArg(formulaErrorVALUE, "DEC2BIN allows at most 2 arguments") - } return fn.dec2x("DEC2BIN", argsList) } @@ -1167,12 +1236,6 @@ func (fn *formulaFuncs) DEC2BIN(argsList *list.List) formulaArg { // DEC2HEX(number,[places]) // func (fn *formulaFuncs) DEC2HEX(argsList *list.List) formulaArg { - if argsList.Len() < 1 { - return newErrorFormulaArg(formulaErrorVALUE, "DEC2HEX requires at least 1 argument") - } - if argsList.Len() > 2 { - return newErrorFormulaArg(formulaErrorVALUE, "DEC2HEX allows at most 2 arguments") - } return fn.dec2x("DEC2HEX", argsList) } @@ -1182,17 +1245,18 @@ func (fn *formulaFuncs) DEC2HEX(argsList *list.List) formulaArg { // DEC2OCT(number,[places]) // func (fn *formulaFuncs) DEC2OCT(argsList *list.List) formulaArg { - if argsList.Len() < 1 { - return newErrorFormulaArg(formulaErrorVALUE, "DEC2OCT requires at least 1 argument") - } - if argsList.Len() > 2 { - return newErrorFormulaArg(formulaErrorVALUE, "DEC2OCT allows at most 2 arguments") - } return fn.dec2x("DEC2OCT", argsList) } -// dec2x is an implementation of the formula function DEC2BIN, DEC2HEX and DEC2OCT. +// dec2x is an implementation of the formula function DEC2BIN, DEC2HEX and +// DEC2OCT. func (fn *formulaFuncs) dec2x(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() > 2 { + return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s allows at most 2 arguments", name)) + } decimal := argsList.Front().Value.(formulaArg).ToNumber() if decimal.Type != ArgNumber { return newErrorFormulaArg(formulaErrorVALUE, decimal.Error) diff --git a/calc_test.go b/calc_test.go index 28ffb6cfb3..04dce37014 100644 --- a/calc_test.go +++ b/calc_test.go @@ -47,6 +47,19 @@ func TestCalcCellValue(t *testing.T) { "=2>=3": "FALSE", "=1&2": "12", // Engineering Functions + // BITAND + "=BITAND(13,14)": "12", + // BITLSHIFT + "=BITLSHIFT(5,2)": "20", + "=BITLSHIFT(3,5)": "96", + // BITOR + "=BITOR(9,12)": "13", + // BITRSHIFT + "=BITRSHIFT(20,2)": "5", + "=BITRSHIFT(52,4)": "3", + // BITXOR + "=BITXOR(5,6)": "3", + "=BITXOR(9,12)": "5", // DEC2BIN "=DEC2BIN(2)": "10", "=DEC2BIN(3)": "11", @@ -727,6 +740,46 @@ func TestCalcCellValue(t *testing.T) { mathCalcError := map[string]string{ "=1/0": "#DIV/0!", // Engineering Functions + // 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!", + // 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!", + // 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!", + // 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!", + // 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!", // DEC2BIN "=DEC2BIN()": "DEC2BIN requires at least 1 argument", "=DEC2BIN(1,1,1)": "DEC2BIN allows at most 2 arguments", diff --git a/cell.go b/cell.go index 3e635659fa..4617b76098 100644 --- a/cell.go +++ b/cell.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2021 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. // @@ -44,8 +44,9 @@ func (f *File) GetCellValue(sheet, axis string) (string, error) { } // SetCellValue provides a function to set value of a cell. The specified -// coordinates should not be in the first row of the table. The following -// shows the supported data types: +// 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 @@ -93,7 +94,7 @@ func (f *File) SetCellValue(sheet, axis string, value interface{}) error { case bool: err = f.SetCellBool(sheet, axis, v) case nil: - break + err = f.SetCellDefault(sheet, axis, "") default: err = f.SetCellStr(sheet, axis, fmt.Sprint(value)) } From 283339534741d3f0ff01c2ed2adc7c87445edf07 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 21 Feb 2021 00:21:45 +0800 Subject: [PATCH 337/957] This closes #787, avoid duplicate rich text string items, new formula fn: BIN2DEC, BIN2HEX, BIN2OCT, HEX2BIN, HEX2DEC, HEX2OCT, OCT2BIN, OCT2DEC, OCT2HEX --- calc.go | 279 ++++++++++++++++++++++++++++++++++++++++++++++++++- calc_test.go | 111 ++++++++++++++++++++ cell.go | 14 ++- 3 files changed, 398 insertions(+), 6 deletions(-) diff --git a/calc.go b/calc.go index f49477fdb8..629aacf973 100644 --- a/calc.go +++ b/calc.go @@ -222,6 +222,9 @@ var tokenPriority = map[string]int{ // AVERAGE // AVERAGEA // BASE +// BIN2DEC +// BIN2HEX +// BIN2OCT // BITAND // BITLSHIFT // BITOR @@ -266,6 +269,9 @@ var tokenPriority = map[string]int{ // GAMMA // GAMMALN // GCD +// HEX2BIN +// HEX2DEC +// HEX2OCT // HLOOKUP // IF // IFERROR @@ -300,6 +306,9 @@ var tokenPriority = map[string]int{ // MUNIT // NA // NOT +// OCT2BIN +// OCT2DEC +// OCT2HEX // ODD // OR // PERMUT @@ -1151,6 +1160,99 @@ func formulaCriteriaEval(val string, criteria *formulaCriteria) (result bool, er // Engineering Functions +// BIN2DEC function converts a Binary (a base-2 number) into a decimal number. +// The syntax of the function is: +// +// BIN2DEC(number) +// +func (fn *formulaFuncs) BIN2DEC(argsList *list.List) formulaArg { + if argsList.Len() != 1 { + return newErrorFormulaArg(formulaErrorVALUE, "BIN2DEC requires 1 numeric argument") + } + token := argsList.Front().Value.(formulaArg) + number := token.ToNumber() + if number.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, number.Error) + } + return fn.bin2dec(token.Value()) +} + +// BIN2HEX function converts a Binary (Base 2) number into a Hexadecimal +// (Base 16) number. The syntax of the function is: +// +// BIN2HEX(number,[places]) +// +func (fn *formulaFuncs) BIN2HEX(argsList *list.List) formulaArg { + if argsList.Len() < 1 { + return newErrorFormulaArg(formulaErrorVALUE, "BIN2HEX requires at least 1 argument") + } + if argsList.Len() > 2 { + return newErrorFormulaArg(formulaErrorVALUE, "BIN2HEX allows at most 2 arguments") + } + token := argsList.Front().Value.(formulaArg) + number := token.ToNumber() + if number.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, number.Error) + } + decimal, newList := fn.bin2dec(token.Value()), list.New() + if decimal.Type != ArgNumber { + return decimal + } + newList.PushBack(decimal) + if argsList.Len() == 2 { + newList.PushBack(argsList.Back().Value.(formulaArg)) + } + return fn.dec2x("BIN2HEX", newList) +} + +// BIN2OCT function converts a Binary (Base 2) number into an Octal (Base 8) +// number. The syntax of the function is: +// +// BIN2OCT(number,[places]) +// +func (fn *formulaFuncs) BIN2OCT(argsList *list.List) formulaArg { + if argsList.Len() < 1 { + return newErrorFormulaArg(formulaErrorVALUE, "BIN2OCT requires at least 1 argument") + } + if argsList.Len() > 2 { + return newErrorFormulaArg(formulaErrorVALUE, "BIN2OCT allows at most 2 arguments") + } + token := argsList.Front().Value.(formulaArg) + number := token.ToNumber() + if number.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, number.Error) + } + decimal, newList := fn.bin2dec(token.Value()), list.New() + if decimal.Type != ArgNumber { + return decimal + } + newList.PushBack(decimal) + if argsList.Len() == 2 { + newList.PushBack(argsList.Back().Value.(formulaArg)) + } + return fn.dec2x("BIN2OCT", newList) +} + +// bin2dec is an implementation of the formula function BIN2DEC. +func (fn *formulaFuncs) bin2dec(number string) formulaArg { + decimal, length := 0.0, len(number) + for i := length; i > 0; i-- { + s := string(number[length-i]) + if 10 == i && s == "1" { + decimal += math.Pow(-2.0, float64(i-1)) + continue + } + if s == "1" { + decimal += math.Pow(2.0, float64(i-1)) + continue + } + if s != "0" { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + } + return newNumberFormulaArg(decimal) +} + // BITAND function returns the bitwise 'AND' for two supplied integers. The // syntax of the function is: // @@ -1263,21 +1365,38 @@ func (fn *formulaFuncs) dec2x(name string, argsList *list.List) formulaArg { } maxLimitMap := map[string]float64{ "DEC2BIN": 511, + "HEX2BIN": 511, + "OCT2BIN": 511, + "BIN2HEX": 549755813887, "DEC2HEX": 549755813887, + "OCT2HEX": 549755813887, + "BIN2OCT": 536870911, "DEC2OCT": 536870911, + "HEX2OCT": 536870911, } minLimitMap := map[string]float64{ "DEC2BIN": -512, + "HEX2BIN": -512, + "OCT2BIN": -512, + "BIN2HEX": -549755813888, "DEC2HEX": -549755813888, + "OCT2HEX": -549755813888, + "BIN2OCT": -536870912, "DEC2OCT": -536870912, + "HEX2OCT": -536870912, } baseMap := map[string]int{ "DEC2BIN": 2, + "HEX2BIN": 2, + "OCT2BIN": 2, + "BIN2HEX": 16, "DEC2HEX": 16, + "OCT2HEX": 16, + "BIN2OCT": 8, "DEC2OCT": 8, + "HEX2OCT": 8, } - maxLimit := maxLimitMap[name] - minLimit := minLimitMap[name] + maxLimit, minLimit := maxLimitMap[name], minLimitMap[name] base := baseMap[name] if decimal.Number < minLimit || decimal.Number > maxLimit { return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) @@ -1301,6 +1420,162 @@ func (fn *formulaFuncs) dec2x(name string, argsList *list.List) formulaArg { return newStringFormulaArg(strings.ToUpper(binary)) } +// HEX2BIN function converts a Hexadecimal (Base 16) number into a Binary +// (Base 2) number. The syntax of the function is: +// +// HEX2BIN(number,[places]) +// +func (fn *formulaFuncs) HEX2BIN(argsList *list.List) formulaArg { + if argsList.Len() < 1 { + return newErrorFormulaArg(formulaErrorVALUE, "HEX2BIN requires at least 1 argument") + } + if argsList.Len() > 2 { + return newErrorFormulaArg(formulaErrorVALUE, "HEX2BIN allows at most 2 arguments") + } + decimal, newList := fn.hex2dec(argsList.Front().Value.(formulaArg).Value()), list.New() + if decimal.Type != ArgNumber { + return decimal + } + newList.PushBack(decimal) + if argsList.Len() == 2 { + newList.PushBack(argsList.Back().Value.(formulaArg)) + } + return fn.dec2x("HEX2BIN", newList) +} + +// HEX2DEC function converts a hexadecimal (a base-16 number) into a decimal +// number. The syntax of the function is: +// +// HEX2DEC(number) +// +func (fn *formulaFuncs) HEX2DEC(argsList *list.List) formulaArg { + if argsList.Len() != 1 { + return newErrorFormulaArg(formulaErrorVALUE, "HEX2DEC requires 1 numeric argument") + } + return fn.hex2dec(argsList.Front().Value.(formulaArg).Value()) +} + +// HEX2OCT function converts a Hexadecimal (Base 16) number into an Octal +// (Base 8) number. The syntax of the function is: +// +// HEX2OCT(number,[places]) +// +func (fn *formulaFuncs) HEX2OCT(argsList *list.List) formulaArg { + if argsList.Len() < 1 { + return newErrorFormulaArg(formulaErrorVALUE, "HEX2OCT requires at least 1 argument") + } + if argsList.Len() > 2 { + return newErrorFormulaArg(formulaErrorVALUE, "HEX2OCT allows at most 2 arguments") + } + decimal, newList := fn.hex2dec(argsList.Front().Value.(formulaArg).Value()), list.New() + if decimal.Type != ArgNumber { + return decimal + } + newList.PushBack(decimal) + if argsList.Len() == 2 { + newList.PushBack(argsList.Back().Value.(formulaArg)) + } + return fn.dec2x("HEX2OCT", newList) +} + +// hex2dec is an implementation of the formula function HEX2DEC. +func (fn *formulaFuncs) hex2dec(number string) formulaArg { + decimal, length := 0.0, len(number) + for i := length; i > 0; i-- { + num, err := strconv.ParseInt(string(number[length-i]), 16, 64) + if err != nil { + return newErrorFormulaArg(formulaErrorNUM, err.Error()) + } + if 10 == i && string(number[length-i]) == "F" { + decimal += math.Pow(-16.0, float64(i-1)) + continue + } + decimal += float64(num) * math.Pow(16.0, float64(i-1)) + } + return newNumberFormulaArg(decimal) +} + +// OCT2BIN function converts an Octal (Base 8) number into a Binary (Base 2) +// number. The syntax of the function is: +// +// OCT2BIN(number,[places]) +// +func (fn *formulaFuncs) OCT2BIN(argsList *list.List) formulaArg { + if argsList.Len() < 1 { + return newErrorFormulaArg(formulaErrorVALUE, "OCT2BIN requires at least 1 argument") + } + if argsList.Len() > 2 { + return newErrorFormulaArg(formulaErrorVALUE, "OCT2BIN allows at most 2 arguments") + } + token := argsList.Front().Value.(formulaArg) + number := token.ToNumber() + if number.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, number.Error) + } + decimal, newList := fn.oct2dec(token.Value()), list.New() + newList.PushBack(decimal) + if argsList.Len() == 2 { + newList.PushBack(argsList.Back().Value.(formulaArg)) + } + return fn.dec2x("OCT2BIN", newList) +} + +// OCT2DEC function converts an Octal (a base-8 number) into a decimal number. +// The syntax of the function is: +// +// OCT2DEC(number) +// +func (fn *formulaFuncs) OCT2DEC(argsList *list.List) formulaArg { + if argsList.Len() != 1 { + return newErrorFormulaArg(formulaErrorVALUE, "OCT2DEC requires 1 numeric argument") + } + token := argsList.Front().Value.(formulaArg) + number := token.ToNumber() + if number.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, number.Error) + } + return fn.oct2dec(token.Value()) +} + +// OCT2HEX function converts an Octal (Base 8) number into a Hexadecimal +// (Base 16) number. The syntax of the function is: +// +// OCT2HEX(number,[places]) +// +func (fn *formulaFuncs) OCT2HEX(argsList *list.List) formulaArg { + if argsList.Len() < 1 { + return newErrorFormulaArg(formulaErrorVALUE, "OCT2HEX requires at least 1 argument") + } + if argsList.Len() > 2 { + return newErrorFormulaArg(formulaErrorVALUE, "OCT2HEX allows at most 2 arguments") + } + token := argsList.Front().Value.(formulaArg) + number := token.ToNumber() + if number.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, number.Error) + } + decimal, newList := fn.oct2dec(token.Value()), list.New() + newList.PushBack(decimal) + if argsList.Len() == 2 { + newList.PushBack(argsList.Back().Value.(formulaArg)) + } + return fn.dec2x("OCT2HEX", newList) +} + +// oct2dec is an implementation of the formula function OCT2DEC. +func (fn *formulaFuncs) oct2dec(number string) formulaArg { + decimal, length := 0.0, len(number) + for i := length; i > 0; i-- { + num, _ := strconv.Atoi(string(number[length-i])) + if 10 == i && string(number[length-i]) == "7" { + decimal += math.Pow(-8.0, float64(i-1)) + continue + } + decimal += float64(num) * math.Pow(8.0, float64(i-1)) + } + return newNumberFormulaArg(decimal) +} + // Math and Trigonometric Functions // ABS function returns the absolute value of any supplied number. The syntax diff --git a/calc_test.go b/calc_test.go index 04dce37014..49af52385b 100644 --- a/calc_test.go +++ b/calc_test.go @@ -47,6 +47,24 @@ func TestCalcCellValue(t *testing.T) { "=2>=3": "FALSE", "=1&2": "12", // Engineering Functions + // BIN2DEC + "=BIN2DEC(\"10\")": "2", + "=BIN2DEC(\"11\")": "3", + "=BIN2DEC(\"0000000010\")": "2", + "=BIN2DEC(\"1111111110\")": "-2", + "=BIN2DEC(\"110\")": "6", + // BIN2HEX + "=BIN2HEX(\"10\")": "2", + "=BIN2HEX(\"0000000001\")": "1", + "=BIN2HEX(\"10\",10)": "0000000002", + "=BIN2HEX(\"1111111110\")": "FFFFFFFFFE", + "=BIN2HEX(\"11101\")": "1D", + // BIN2OCT + "=BIN2OCT(\"101\")": "5", + "=BIN2OCT(\"0000000001\")": "1", + "=BIN2OCT(\"10\",10)": "0000000002", + "=BIN2OCT(\"1111111110\")": "7777777776", + "=BIN2OCT(\"1110\")": "16", // BITAND "=BITAND(13,14)": "12", // BITLSHIFT @@ -78,6 +96,44 @@ func TestCalcCellValue(t *testing.T) { "=DEC2OCT(8,10)": "0000000010", "=DEC2OCT(-8)": "7777777770", "=DEC2OCT(237)": "355", + // HEX2BIN + "=HEX2BIN(\"2\")": "10", + "=HEX2BIN(\"0000000001\")": "1", + "=HEX2BIN(\"2\",10)": "0000000010", + "=HEX2BIN(\"F0\")": "11110000", + "=HEX2BIN(\"1D\")": "11101", + // HEX2DEC + "=HEX2DEC(\"A\")": "10", + "=HEX2DEC(\"1F\")": "31", + "=HEX2DEC(\"0000000010\")": "16", + "=HEX2DEC(\"FFFFFFFFF0\")": "-16", + "=HEX2DEC(\"111\")": "273", + "=HEX2DEC(\"\")": "0", + // HEX2OCT + "=HEX2OCT(\"A\")": "12", + "=HEX2OCT(\"000000000F\")": "17", + "=HEX2OCT(\"8\",10)": "0000000010", + "=HEX2OCT(\"FFFFFFFFF8\")": "7777777770", + "=HEX2OCT(\"1F3\")": "763", + // OCT2BIN + "=OCT2BIN(\"5\")": "101", + "=OCT2BIN(\"0000000001\")": "1", + "=OCT2BIN(\"2\",10)": "0000000010", + "=OCT2BIN(\"7777777770\")": "1111111000", + "=OCT2BIN(\"16\")": "1110", + // OCT2DEC + "=OCT2DEC(\"10\")": "8", + "=OCT2DEC(\"22\")": "18", + "=OCT2DEC(\"0000000010\")": "8", + "=OCT2DEC(\"7777777770\")": "-8", + "=OCT2DEC(\"355\")": "237", + // OCT2HEX + "=OCT2HEX(\"10\")": "8", + "=OCT2HEX(\"0000000007\")": "7", + "=OCT2HEX(\"10\",10)": "0000000008", + "=OCT2HEX(\"7777777770\")": "FFFFFFFFF8", + "=OCT2HEX(\"763\")": "1F3", + // Math and Trigonometric Functions // ABS "=ABS(-1)": "1", "=ABS(-6.5)": "6.5", @@ -740,6 +796,25 @@ func TestCalcCellValue(t *testing.T) { mathCalcError := map[string]string{ "=1/0": "#DIV/0!", // Engineering Functions + // BIN2DEC + "=BIN2DEC()": "BIN2DEC requires 1 numeric argument", + "=BIN2DEC(\"\")": "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!", + // 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!", // BITAND "=BITAND()": "BITAND requires 2 numeric arguments", "=BITAND(-1,2)": "#NUM!", @@ -804,6 +879,42 @@ func TestCalcCellValue(t *testing.T) { "=DEC2OCT(-536870912 ,10)": "#NUM!", "=DEC2OCT(1,-1)": "#NUM!", "=DEC2OCT(8,1)": "#NUM!", + // 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!", + // HEX2DEC + "=HEX2DEC()": "HEX2DEC requires 1 numeric argument", + "=HEX2DEC(\"X\")": "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!", + // 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!", + // OCT2DEC + "=OCT2DEC()": "OCT2DEC requires 1 numeric argument", + "=OCT2DEC(\"\")": "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!", + // Math and Trigonometric Functions // ABS "=ABS()": "ABS requires 1 numeric argument", `=ABS("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", diff --git a/cell.go b/cell.go index 4617b76098..ae468e963b 100644 --- a/cell.go +++ b/cell.go @@ -33,9 +33,9 @@ const ( ) // GetCellValue provides a function to get formatted value from cell by given -// worksheet name and axis in XLSX 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. +// 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. func (f *File) GetCellValue(sheet, axis string) (string, error) { return f.getCellStringFunc(sheet, axis, func(x *xlsxWorksheet, c *xlsxC) (string, bool, error) { val, err := c.getValueFrom(f, f.sharedStringsReader()) @@ -43,7 +43,7 @@ func (f *File) GetCellValue(sheet, axis string) (string, error) { }) } -// SetCellValue provides a function to set value of a cell. The specified +// 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: @@ -645,6 +645,12 @@ func (f *File) SetCellRichText(sheet, cell string, runs []RichTextRun) error { textRuns = append(textRuns, run) } si.R = textRuns + for idx, strItem := range sst.SI { + if reflect.DeepEqual(strItem, si) { + cellData.T, cellData.V = "s", strconv.Itoa(idx) + return err + } + } sst.SI = append(sst.SI, si) sst.Count++ sst.UniqueCount++ From bbb8ebfa8cc648ec09094d16eddebb03240baecf Mon Sep 17 00:00:00 2001 From: tonnyzhang <450024933@qq.com> Date: Mon, 22 Feb 2021 06:04:13 -0600 Subject: [PATCH 338/957] add GetCellRichText method and test (#789) --- cell.go | 55 ++++++++++++++++++++++++++++++++++++++++++--- cell_test.go | 54 ++++++++++++++++++++++++++++++++++++++++++++ comment.go | 3 ++- xmlSharedStrings.go | 14 ++++++------ 4 files changed, 115 insertions(+), 11 deletions(-) diff --git a/cell.go b/cell.go index ae468e963b..ea4e1d0cee 100644 --- a/cell.go +++ b/cell.go @@ -494,6 +494,54 @@ func (f *File) SetCellHyperLink(sheet, axis, link, linkType string) error { 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, sheet, cell) + if err != nil { + return + } + siIdx, err := strconv.Atoi(cellData.V) + if nil != err { + return + } + sst := f.sharedStringsReader() + if len(sst.SI) <= siIdx || siIdx < 0 { + return + } + si := sst.SI[siIdx] + for _, v := range si.R { + run := RichTextRun{ + Text: v.T.Val, + } + if nil != v.RPr { + font := Font{} + font.Bold = v.RPr.B != nil + font.Italic = v.RPr.I != nil + if nil != v.RPr.U { + font.Underline = *v.RPr.U.Val + } + if nil != v.RPr.RFont { + font.Family = *v.RPr.RFont.Val + } + if nil != v.RPr.Sz { + font.Size = *v.RPr.Sz.Val + } + font.Strike = v.RPr.Strike != nil + if nil != v.RPr.Color { + font.Color = strings.TrimPrefix(v.RPr.Color.RGB, "FF") + } + run.Font = &font + } + runs = append(runs, run) + } + return +} + // 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: @@ -619,14 +667,15 @@ func (f *File) SetCellRichText(sheet, cell string, runs []RichTextRun) error { fnt := textRun.Font if fnt != nil { rpr := xlsxRPr{} + trueVal := "" if fnt.Bold { - rpr.B = " " + rpr.B = &trueVal } if fnt.Italic { - rpr.I = " " + rpr.I = &trueVal } if fnt.Strike { - rpr.Strike = " " + rpr.Strike = &trueVal } if fnt.Underline != "" { rpr.U = &attrValString{Val: &fnt.Underline} diff --git a/cell_test.go b/cell_test.go index c3c20f7388..026189c0a3 100644 --- a/cell_test.go +++ b/cell_test.go @@ -3,7 +3,9 @@ package excelize import ( "fmt" "path/filepath" + "reflect" "strconv" + "strings" "sync" "testing" "time" @@ -221,7 +223,59 @@ func TestOverflowNumericCell(t *testing.T) { // GOARCH=amd64 - all ok; GOARCH=386 - actual: "-2147483648" assert.Equal(t, "8595602512225", val, "A1 should be 8595602512225") } +func TestGetCellRichText(t *testing.T) { + f := NewFile() + runsSource := []RichTextRun{ + { + Text: "a\n", + }, + { + Text: "b", + Font: &Font{ + Underline: "single", + Color: "ff0000", + Bold: true, + Italic: true, + Family: "Times New Roman", + Size: 100, + Strike: true, + }, + }, + } + assert.NoError(t, f.SetCellRichText("Sheet1", "A1", runsSource)) + + runs, err := f.GetCellRichText("Sheet1", "A1") + assert.NoError(t, err) + + assert.Equal(t, runsSource[0].Text, runs[0].Text) + assert.Nil(t, runs[0].Font) + assert.NotNil(t, runs[1].Font) + + 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 + f.Sheet["xl/worksheets/sheet1.xml"].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 + f.Sheet["xl/worksheets/sheet1.xml"].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 + f.Sheet["xl/worksheets/sheet1.xml"].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 + _, err = f.GetCellRichText("SheetN", "A1") + assert.EqualError(t, err, "sheet SheetN is not exist") + // Test set cell rich text with illegal cell coordinates + _, err = f.GetCellRichText("Sheet1", "A") + assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`) +} func TestSetCellRichText(t *testing.T) { f := NewFile() assert.NoError(t, f.SetRowHeight("Sheet1", 1, 35)) diff --git a/comment.go b/comment.go index 1ef387790f..c8979430ba 100644 --- a/comment.go +++ b/comment.go @@ -253,6 +253,7 @@ func (f *File) addComment(commentsXML, cell string, formatSet *formatComment) { } } defaultFont := f.GetDefaultFont() + bold := "" cmt := xlsxComment{ Ref: cell, AuthorID: 0, @@ -260,7 +261,7 @@ func (f *File) addComment(commentsXML, cell string, formatSet *formatComment) { R: []xlsxR{ { RPr: &xlsxRPr{ - B: " ", + B: &bold, Sz: &attrValFloat{Val: float64Ptr(9)}, Color: &xlsxColor{ Indexed: 81, diff --git a/xmlSharedStrings.go b/xmlSharedStrings.go index f59119f5ee..c9f311b959 100644 --- a/xmlSharedStrings.go +++ b/xmlSharedStrings.go @@ -86,13 +86,13 @@ type xlsxRPr struct { RFont *attrValString `xml:"rFont"` Charset *attrValInt `xml:"charset"` Family *attrValInt `xml:"family"` - B string `xml:"b,omitempty"` - I string `xml:"i,omitempty"` - Strike string `xml:"strike,omitempty"` - Outline string `xml:"outline,omitempty"` - Shadow string `xml:"shadow,omitempty"` - Condense string `xml:"condense,omitempty"` - Extend string `xml:"extend,omitempty"` + B *string `xml:"b"` + I *string `xml:"i"` + Strike *string `xml:"strike"` + Outline *string `xml:"outline"` + Shadow *string `xml:"shadow"` + Condense *string `xml:"condense"` + Extend *string `xml:"extend"` Color *xlsxColor `xml:"color"` Sz *attrValFloat `xml:"sz"` U *attrValString `xml:"u"` From d84050921eddf5c4221f8723477be2ac93f56ecc Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 23 Feb 2021 00:05:19 +0800 Subject: [PATCH 339/957] check empty rich text run properties; new formula fn: LEFT, LEFTB, RIGHT, RIGHTB --- calc.go | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 44 +++++++++++++++++++++++++++++++++ cell.go | 6 ++--- sheet.go | 2 ++ 4 files changed, 119 insertions(+), 3 deletions(-) diff --git a/calc.go b/calc.go index 629aacf973..a2efbdbbac 100644 --- a/calc.go +++ b/calc.go @@ -288,6 +288,8 @@ var tokenPriority = map[string]int{ // ISO.CEILING // KURT // LCM +// LEFT +// LEFTB // LEN // LENB // LN @@ -321,6 +323,8 @@ var tokenPriority = map[string]int{ // RAND // RANDBETWEEN // REPT +// RIGHT +// RIGHTB // ROMAN // ROUND // ROUNDDOWN @@ -4635,6 +4639,54 @@ func (fn *formulaFuncs) EXACT(argsList *list.List) formulaArg { return newBoolFormulaArg(text1 == text2) } +// 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]) +// +func (fn *formulaFuncs) LEFT(argsList *list.List) formulaArg { + return fn.leftRight("LEFT", argsList) +} + +// 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]) +// +func (fn *formulaFuncs) LEFTB(argsList *list.List) formulaArg { + return fn.leftRight("LEFTB", argsList) +} + +// leftRight is an implementation of the formula function LEFT, LEFTB, RIGHT, +// RIGHTB. TODO: support DBCS include Japanese, Chinese (Simplified), Chinese +// (Traditional), and Korean. +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)) + } + if argsList.Len() > 2 { + return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s allows at most 2 arguments", name)) + } + text, numChars := argsList.Front().Value.(formulaArg).Value(), 1 + if argsList.Len() == 2 { + numArg := argsList.Back().Value.(formulaArg).ToNumber() + if numArg.Type != ArgNumber { + return numArg + } + if numArg.Number < 0 { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + numChars = int(numArg.Number) + } + if len(text) > numChars { + if name == "LEFT" || name == "LEFTB" { + return newStringFormulaArg(text[:numChars]) + } + return newStringFormulaArg(text[len(text)-numChars:]) + } + return newStringFormulaArg(text) +} + // LEN returns the length of a supplied text string. The syntax of the // function is: // @@ -4742,6 +4794,24 @@ func (fn *formulaFuncs) REPT(argsList *list.List) formulaArg { return newStringFormulaArg(buf.String()) } +// 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]) +// +func (fn *formulaFuncs) RIGHT(argsList *list.List) formulaArg { + return fn.leftRight("RIGHT", argsList) +} + +// 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]) +// +func (fn *formulaFuncs) RIGHTB(argsList *list.List) formulaArg { + return fn.leftRight("RIGHTB", argsList) +} + // UPPER converts all characters in a supplied text string to upper case. The // syntax of the function is: // diff --git a/calc_test.go b/calc_test.go index 49af52385b..1b04df8224 100644 --- a/calc_test.go +++ b/calc_test.go @@ -723,6 +723,18 @@ func TestCalcCellValue(t *testing.T) { "=EXACT(1,\"1\")": "TRUE", "=EXACT(1,1)": "TRUE", "=EXACT(\"A\",\"a\")": "FALSE", + // LEFT + "=LEFT(\"Original Text\")": "O", + "=LEFT(\"Original Text\",4)": "Orig", + "=LEFT(\"Original Text\",0)": "", + "=LEFT(\"Original Text\",13)": "Original Text", + "=LEFT(\"Original Text\",20)": "Original Text", + // LEFTB + "=LEFTB(\"Original Text\")": "O", + "=LEFTB(\"Original Text\",4)": "Orig", + "=LEFTB(\"Original Text\",0)": "", + "=LEFTB(\"Original Text\",13)": "Original Text", + "=LEFTB(\"Original Text\",20)": "Original Text", // LEN "=LEN(\"\")": "0", "=LEN(D1)": "5", @@ -746,6 +758,18 @@ func TestCalcCellValue(t *testing.T) { "=REPT(\"*\",0)": "", "=REPT(\"*\",1)": "*", "=REPT(\"**\",2)": "****", + // RIGHT + "=RIGHT(\"Original Text\")": "t", + "=RIGHT(\"Original Text\",4)": "Text", + "=RIGHT(\"Original Text\",0)": "", + "=RIGHT(\"Original Text\",13)": "Original Text", + "=RIGHT(\"Original Text\",20)": "Original Text", + // RIGHTB + "=RIGHTB(\"Original Text\")": "t", + "=RIGHTB(\"Original Text\",4)": "Text", + "=RIGHTB(\"Original Text\",0)": "", + "=RIGHTB(\"Original Text\",13)": "Original Text", + "=RIGHTB(\"Original Text\",20)": "Original Text", // UPPER "=UPPER(\"test\")": "TEST", "=UPPER(\"TEST\")": "TEST", @@ -1308,6 +1332,16 @@ func TestCalcCellValue(t *testing.T) { // EXACT "=EXACT()": "EXACT requires 2 arguments", "=EXACT(1,2,3)": "EXACT requires 2 arguments", + // 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!", + // 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!", // LEN "=LEN()": "LEN requires 1 string argument", // LENB @@ -1329,6 +1363,16 @@ func TestCalcCellValue(t *testing.T) { "=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", + // 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!", + // 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!", // Conditional Functions // IF "=IF()": "IF requires at least 1 argument", diff --git a/cell.go b/cell.go index ea4e1d0cee..892a9067e1 100644 --- a/cell.go +++ b/cell.go @@ -522,13 +522,13 @@ func (f *File) GetCellRichText(sheet, cell string) (runs []RichTextRun, err erro font := Font{} font.Bold = v.RPr.B != nil font.Italic = v.RPr.I != nil - if nil != v.RPr.U { + if v.RPr.U != nil && v.RPr.U.Val != nil { font.Underline = *v.RPr.U.Val } - if nil != v.RPr.RFont { + if v.RPr.RFont != nil && v.RPr.RFont.Val != nil { font.Family = *v.RPr.RFont.Val } - if nil != v.RPr.Sz { + if v.RPr.Sz != nil && v.RPr.Sz.Val != nil { font.Size = *v.RPr.Sz.Val } font.Strike = v.RPr.Strike != nil diff --git a/sheet.go b/sheet.go index 26c0081ec9..d4362b1296 100644 --- a/sheet.go +++ b/sheet.go @@ -1762,6 +1762,7 @@ func prepareSheetXML(ws *xlsxWorksheet, col int, row int) { fillColumns(rowData, col, row) } +// fillColumns fill cells in the column of the row as contiguous. func fillColumns(rowData *xlsxRow, col, row int) { cellCount := len(rowData.C) if cellCount < col { @@ -1772,6 +1773,7 @@ func fillColumns(rowData *xlsxRow, col, row int) { } } +// makeContiguousColumns make columns in specific rows as contiguous. func makeContiguousColumns(ws *xlsxWorksheet, fromRow, toRow, colCount int) { for ; fromRow < toRow; fromRow++ { rowData := &ws.SheetData.Row[fromRow-1] From 5a0d885315521a4e703f9de401e2dda834285d5f Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 24 Feb 2021 00:37:44 +0800 Subject: [PATCH 340/957] handle default underline type on get rich text; #65 fn: CODE, COLUMN, FIND, FINDB --- calc.go | 132 +++++++++++++++++++++++++++++++++++++++++++++------ calc_test.go | 46 ++++++++++++++++++ cell.go | 9 ++-- 3 files changed, 169 insertions(+), 18 deletions(-) diff --git a/calc.go b/calc.go index a2efbdbbac..c1e0b44a3b 100644 --- a/calc.go +++ b/calc.go @@ -101,14 +101,15 @@ const ( // formulaArg is the argument of a formula or function. type formulaArg struct { - SheetName string - Number float64 - String string - List []formulaArg - Matrix [][]formulaArg - Boolean bool - Error string - Type ArgType + SheetName string + Number float64 + String string + List []formulaArg + Matrix [][]formulaArg + Boolean bool + Error string + Type ArgType + cellRefs, cellRanges *list.List } // Value returns a string data type of the formula argument. @@ -181,8 +182,8 @@ func (fa formulaArg) ToList() []formulaArg { // formulaFuncs is the type of the formula functions. type formulaFuncs struct { - f *File - sheet string + f *File + sheet, cell string } // tokenPriority defined basic arithmetic operator priority. @@ -235,6 +236,8 @@ var tokenPriority = map[string]int{ // CEILING.PRECISE // CHOOSE // CLEAN +// CODE +// COLUMN // COMBIN // COMBINA // CONCAT @@ -261,6 +264,8 @@ var tokenPriority = map[string]int{ // FACT // FACTDOUBLE // FALSE +// FIND +// FINDB // FISHER // FISHERINV // FLOOR @@ -363,7 +368,7 @@ func (f *File) CalcCellValue(sheet, cell string) (result string, err error) { if tokens == nil { return } - if token, err = f.evalInfixExp(sheet, tokens); err != nil { + if token, err = f.evalInfixExp(sheet, cell, tokens); err != nil { return } result = token.TValue @@ -446,7 +451,7 @@ func newEmptyFormulaArg() formulaArg { // // TODO: handle subtypes: Nothing, Text, Logical, Error, Concatenation, Intersection, Union // -func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error) { +func (f *File) evalInfixExp(sheet, cell string, tokens []efp.Token) (efp.Token, error) { var err error opdStack, optStack, opfStack, opfdStack, opftStack, argsStack := NewStack(), NewStack(), NewStack(), NewStack(), NewStack(), NewStack() for i := 0; i < len(tokens); i++ { @@ -537,7 +542,7 @@ func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error) if token.TType == efp.TokenTypeOperand && token.TSubType == efp.TokenSubTypeText { argsStack.Peek().(*list.List).PushBack(newStringFormulaArg(token.TValue)) } - if err = f.evalInfixExpFunc(sheet, token, nextToken, opfStack, opdStack, opftStack, opfdStack, argsStack); err != nil { + if err = f.evalInfixExpFunc(sheet, cell, token, nextToken, opfStack, opdStack, opftStack, opfdStack, argsStack); err != nil { return efp.Token{}, err } } @@ -556,7 +561,7 @@ func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error) } // evalInfixExpFunc evaluate formula function in the infix expression. -func (f *File) evalInfixExpFunc(sheet string, token, nextToken efp.Token, opfStack, opdStack, opftStack, opfdStack, argsStack *Stack) error { +func (f *File) evalInfixExpFunc(sheet, cell string, token, nextToken efp.Token, opfStack, opdStack, opftStack, opfdStack, argsStack *Stack) error { if !isFunctionStopToken(token) { return nil } @@ -575,7 +580,7 @@ func (f *File) evalInfixExpFunc(sheet string, token, nextToken efp.Token, opfSta argsStack.Peek().(*list.List).PushBack(newStringFormulaArg(opfdStack.Pop().(efp.Token).TValue)) } // call formula function to evaluate - arg := callFuncByName(&formulaFuncs{f: f, sheet: sheet}, strings.NewReplacer( + arg := callFuncByName(&formulaFuncs{f: f, sheet: sheet, cell: cell}, strings.NewReplacer( "_xlfn", "", ".", "").Replace(opfStack.Peek().(efp.Token).TValue), []reflect.Value{reflect.ValueOf(argsStack.Peek().(*list.List))}) if arg.Type == ArgError && opfStack.Len() == 1 { @@ -1007,6 +1012,7 @@ func prepareValueRef(cr cellRef, valueRange []int) { // 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) { + arg.cellRefs, arg.cellRanges = cellRefs, cellRanges // value range order: from row, to row, from column, to column valueRange := []int{0, 0, 0, 0} var sheet string @@ -4581,6 +4587,23 @@ func (fn *formulaFuncs) CLEAN(argsList *list.List) formulaArg { return newStringFormulaArg(b.String()) } +// CODE function converts the first character of a supplied text string into +// the associated numeric character set code used by your computer. The +// syntax of the function is: +// +// CODE(text) +// +func (fn *formulaFuncs) CODE(argsList *list.List) formulaArg { + if argsList.Len() != 1 { + return newErrorFormulaArg(formulaErrorVALUE, "CODE requires 1 argument") + } + text := argsList.Front().Value.(formulaArg).Value() + if len(text) == 0 { + return newNumberFormulaArg(0) + } + return newNumberFormulaArg(float64(text[0])) +} + // CONCAT function joins together a series of supplied text strings into one // combined text string. // @@ -4639,6 +4662,63 @@ func (fn *formulaFuncs) EXACT(argsList *list.List) formulaArg { return newBoolFormulaArg(text1 == text2) } +// FIND function returns the position of a specified character or sub-string +// within a supplied text string. The function is case-sensitive. The syntax +// of the function is: +// +// FIND(find_text,within_text,[start_num]) +// +func (fn *formulaFuncs) FIND(argsList *list.List) formulaArg { + return fn.find("FIND", argsList) +} + +// FINDB counts each double-byte character as 2 when you have enabled the +// editing of a language that supports DBCS and then set it as the default +// language. Otherwise, FINDB counts each character as 1. The syntax of the +// function is: +// +// FINDB(find_text,within_text,[start_num]) +// +func (fn *formulaFuncs) FINDB(argsList *list.List) formulaArg { + return fn.find("FINDB", argsList) +} + +// find is an implementation of the formula function FIND and FINDB. +func (fn *formulaFuncs) find(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)) + } + findText := argsList.Front().Value.(formulaArg).Value() + withinText := argsList.Front().Next().Value.(formulaArg).Value() + startNum, result := 1, 1 + if argsList.Len() == 3 { + numArg := argsList.Back().Value.(formulaArg).ToNumber() + if numArg.Type != ArgNumber { + return numArg + } + if numArg.Number < 0 { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + startNum = int(numArg.Number) + } + if findText == "" { + return newNumberFormulaArg(float64(startNum)) + } + for idx := range withinText { + if result < startNum { + result++ + } + if strings.Index(withinText[idx:], findText) == 0 { + return newNumberFormulaArg(float64(result)) + } + result++ + } + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) +} + // LEFT function returns a specified number of characters from the start of a // supplied text string. The syntax of the function is: // @@ -5031,6 +5111,28 @@ func compareFormulaArgMatrix(lhs, rhs formulaArg, caseSensitive, exactMatch bool return criteriaEq } +// 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]) +// +func (fn *formulaFuncs) COLUMN(argsList *list.List) formulaArg { + if argsList.Len() > 1 { + return newErrorFormulaArg(formulaErrorVALUE, "COLUMN requires at most 1 argument") + } + if argsList.Len() == 1 { + if argsList.Front().Value.(formulaArg).cellRanges != nil && argsList.Front().Value.(formulaArg).cellRanges.Len() > 0 { + return newNumberFormulaArg(float64(argsList.Front().Value.(formulaArg).cellRanges.Front().Value.(cellRange).From.Col)) + } + if argsList.Front().Value.(formulaArg).cellRefs != nil && argsList.Front().Value.(formulaArg).cellRefs.Len() > 0 { + return newNumberFormulaArg(float64(argsList.Front().Value.(formulaArg).cellRefs.Front().Value.(cellRef).Col)) + } + return newErrorFormulaArg(formulaErrorVALUE, "invalid reference") + } + col, _, _ := CellNameToCoordinates(fn.cell) + return newNumberFormulaArg(float64(col)) +} + // HLOOKUP function 'looks up' a given value in the top row of a data array // (or table), and returns the corresponding value from another row of the // array. The syntax of the function is: diff --git a/calc_test.go b/calc_test.go index 1b04df8224..8a07eeffd6 100644 --- a/calc_test.go +++ b/calc_test.go @@ -715,6 +715,12 @@ func TestCalcCellValue(t *testing.T) { // CLEAN "=CLEAN(\"\u0009clean text\")": "clean text", "=CLEAN(0)": "0", + // CODE + "=CODE(\"Alpha\")": "65", + "=CODE(\"alpha\")": "97", + "=CODE(\"?\")": "63", + "=CODE(\"3\")": "51", + "=CODE(\"\")": "0", // CONCAT "=CONCAT(TRUE(),1,FALSE(),\"0\",INT(2))": "TRUE1FALSE02", // CONCATENATE @@ -723,6 +729,20 @@ func TestCalcCellValue(t *testing.T) { "=EXACT(1,\"1\")": "TRUE", "=EXACT(1,1)": "TRUE", "=EXACT(\"A\",\"a\")": "FALSE", + // FIND + "=FIND(\"T\",\"Original Text\")": "10", + "=FIND(\"t\",\"Original Text\")": "13", + "=FIND(\"i\",\"Original Text\")": "3", + "=FIND(\"i\",\"Original Text\",4)": "5", + "=FIND(\"\",\"Original Text\")": "1", + "=FIND(\"\",\"Original Text\",2)": "2", + // FINDB + "=FINDB(\"T\",\"Original Text\")": "10", + "=FINDB(\"t\",\"Original Text\")": "13", + "=FINDB(\"i\",\"Original Text\")": "3", + "=FINDB(\"i\",\"Original Text\",4)": "5", + "=FINDB(\"\",\"Original Text\")": "1", + "=FINDB(\"\",\"Original Text\",2)": "2", // LEFT "=LEFT(\"Original Text\")": "O", "=LEFT(\"Original Text\",4)": "Orig", @@ -786,6 +806,12 @@ func TestCalcCellValue(t *testing.T) { "=CHOOSE(4,\"red\",\"blue\",\"green\",\"brown\")": "brown", "=CHOOSE(1,\"red\",\"blue\",\"green\",\"brown\")": "red", "=SUM(CHOOSE(A2,A1,B1:B2,A1:A3,A1:A4))": "9", + // COLUMN + "=COLUMN()": "3", + "=COLUMN(Sheet1!A1)": "1", + "=COLUMN(Sheet1!A1:B1:C1)": "1", + "=COLUMN(Sheet1!F1:G1)": "6", + "=COLUMN(H1)": "8", // HLOOKUP "=HLOOKUP(D2,D2:D8,1,FALSE)": "Jan", "=HLOOKUP(F3,F3:F8,3,FALSE)": "34440", @@ -1325,6 +1351,9 @@ func TestCalcCellValue(t *testing.T) { // CLEAN "=CLEAN()": "CLEAN requires 1 argument", "=CLEAN(1,2)": "CLEAN requires 1 argument", + // CODE + "=CODE()": "CODE requires 1 argument", + "=CODE(1,2)": "CODE requires 1 argument", // CONCAT "=CONCAT(MUNIT(2))": "CONCAT requires arguments to be strings", // CONCATENATE @@ -1332,6 +1361,18 @@ func TestCalcCellValue(t *testing.T) { // EXACT "=EXACT()": "EXACT requires 2 arguments", "=EXACT(1,2,3)": "EXACT requires 2 arguments", + // 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", + // 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", // LEFT "=LEFT()": "LEFT requires at least 1 argument", "=LEFT(\"\",2,3)": "LEFT allows at most 2 arguments", @@ -1383,6 +1424,11 @@ func TestCalcCellValue(t *testing.T) { "=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", + // COLUMN + "=COLUMN(1,2)": "COLUMN requires at most 1 argument", + "=COLUMN(\"\")": "invalid reference", + "=COLUMN(Sheet1)": "invalid column name \"Sheet1\"", + "=COLUMN(Sheet1!A1!B1)": "invalid column name \"Sheet1\"", // HLOOKUP "=HLOOKUP()": "HLOOKUP requires at least 3 arguments", "=HLOOKUP(D2,D1,1,FALSE)": "HLOOKUP requires second argument of table array", diff --git a/cell.go b/cell.go index 892a9067e1..912ee6ab25 100644 --- a/cell.go +++ b/cell.go @@ -519,11 +519,14 @@ func (f *File) GetCellRichText(sheet, cell string) (runs []RichTextRun, err erro Text: v.T.Val, } if nil != v.RPr { - font := Font{} + font := Font{Underline: "none"} font.Bold = v.RPr.B != nil font.Italic = v.RPr.I != nil - if v.RPr.U != nil && v.RPr.U.Val != nil { - font.Underline = *v.RPr.U.Val + 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 From afe2ebc26143330a15b4396b9be6ca04797a5e8e Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 27 Feb 2021 00:03:46 +0800 Subject: [PATCH 341/957] This improves compatibility for absolute XML path, Windows-style directory separator and inline namespace; --- calc.go | 352 +++++++++++++++++++++++++++++++++++++++++++++--- calc_test.go | 131 ++++++++++++++++-- lib.go | 6 +- sheet.go | 9 +- test/Book1.xlsx | Bin 20750 -> 20753 bytes 5 files changed, 467 insertions(+), 31 deletions(-) diff --git a/calc.go b/calc.go index c1e0b44a3b..906a0bbecc 100644 --- a/calc.go +++ b/calc.go @@ -234,10 +234,12 @@ var tokenPriority = map[string]int{ // CEILING // CEILING.MATH // CEILING.PRECISE +// CHAR // CHOOSE // CLEAN // CODE // COLUMN +// COLUMNS // COMBIN // COMBINA // CONCAT @@ -305,6 +307,8 @@ var tokenPriority = map[string]int{ // MAX // MDETERM // MEDIAN +// MID +// MIDB // MIN // MINA // MOD @@ -327,6 +331,8 @@ var tokenPriority = map[string]int{ // RADIANS // RAND // RANDBETWEEN +// REPLACE +// REPLACEB // REPT // RIGHT // RIGHTB @@ -334,6 +340,8 @@ var tokenPriority = map[string]int{ // ROUND // ROUNDDOWN // ROUNDUP +// ROW +// ROWS // SEC // SECH // SHEET @@ -352,6 +360,8 @@ var tokenPriority = map[string]int{ // TRIM // TRUE // TRUNC +// UNICHAR +// UNICODE // UPPER // VLOOKUP // @@ -932,8 +942,17 @@ func (f *File) parseReference(sheet, reference string) (arg formulaArg, err erro cr := cellRef{} if len(tokens) == 2 { // have a worksheet name cr.Sheet = tokens[0] + // cast to cell coordinates if cr.Col, cr.Row, err = CellNameToCoordinates(tokens[1]); err != nil { - return + // 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 = TotalColumns + } } if refs.Len() > 0 { e := refs.Back() @@ -943,9 +962,16 @@ func (f *File) parseReference(sheet, reference string) (arg formulaArg, err erro refs.PushBack(cr) continue } + // cast to cell coordinates if cr.Col, cr.Row, err = CellNameToCoordinates(tokens[0]); err != nil { + // cast to column if cr.Col, err = ColumnNameToNumber(tokens[0]); err != nil { - return + // cast to row + if cr.Row, err = strconv.Atoi(tokens[0]); err != nil { + err = newInvalidColumnNameError(tokens[0]) + return + } + cr.Col = TotalColumns } cellRanges.PushBack(cellRange{ From: cellRef{Sheet: sheet, Col: cr.Col, Row: 1}, @@ -1329,7 +1355,7 @@ func (fn *formulaFuncs) bitwise(name string, argsList *list.List) formulaArg { "BITRSHIFT": func(a, b int) int { return a >> uint(b) }, "BITXOR": func(a, b int) int { return a ^ b }, } - bitwiseFunc, _ := bitwiseFuncMap[name] + bitwiseFunc := bitwiseFuncMap[name] return newNumberFormulaArg(float64(bitwiseFunc(int(num1.Number), int(num2.Number)))) } @@ -4569,6 +4595,26 @@ func daysBetween(startDate, endDate int64) float64 { // Text Functions +// CHAR function returns the character relating to a supplied character set +// number (from 1 to 255). syntax of the function is: +// +// CHAR(number) +// +func (fn *formulaFuncs) CHAR(argsList *list.List) formulaArg { + if argsList.Len() != 1 { + return newErrorFormulaArg(formulaErrorVALUE, "CHAR requires 1 argument") + } + arg := argsList.Front().Value.(formulaArg).ToNumber() + if arg.Type != ArgNumber { + return arg + } + num := int(arg.Number) + if num < 0 || num > 255 { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + return newStringFormulaArg(fmt.Sprintf("%c", num)) +} + // CLEAN removes all non-printable characters from a supplied text string. The // syntax of the function is: // @@ -4594,12 +4640,20 @@ func (fn *formulaFuncs) CLEAN(argsList *list.List) formulaArg { // CODE(text) // func (fn *formulaFuncs) CODE(argsList *list.List) formulaArg { + return fn.code("CODE", argsList) +} + +// code is an implementation of the formula function CODE and UNICODE. +func (fn *formulaFuncs) code(name string, argsList *list.List) formulaArg { if argsList.Len() != 1 { - return newErrorFormulaArg(formulaErrorVALUE, "CODE requires 1 argument") + return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires 1 argument", name)) } text := argsList.Front().Value.(formulaArg).Value() if len(text) == 0 { - return newNumberFormulaArg(0) + if name == "CODE" { + return newNumberFormulaArg(0) + } + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) } return newNumberFormulaArg(float64(text[0])) } @@ -4795,19 +4849,6 @@ func (fn *formulaFuncs) LENB(argsList *list.List) formulaArg { return newStringFormulaArg(strconv.Itoa(len(argsList.Front().Value.(formulaArg).String))) } -// TRIM removes extra spaces (i.e. all spaces except for single spaces between -// words or characters) from a supplied text string. The syntax of the -// function is: -// -// TRIM(text) -// -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)) -} - // LOWER converts all characters in a supplied text string to lower case. The // syntax of the function is: // @@ -4820,6 +4861,56 @@ func (fn *formulaFuncs) LOWER(argsList *list.List) formulaArg { return newStringFormulaArg(strings.ToLower(argsList.Front().Value.(formulaArg).String)) } +// 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) +// +func (fn *formulaFuncs) MID(argsList *list.List) formulaArg { + return fn.mid("MID", argsList) +} + +// MIDB returns a specific number of characters from a text string, starting +// 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) +// +func (fn *formulaFuncs) MIDB(argsList *list.List) formulaArg { + return fn.mid("MIDB", argsList) +} + +// mid is an implementation of the formula function MID and MIDB. TODO: +// support DBCS include Japanese, Chinese (Simplified), Chinese +// (Traditional), and Korean. +func (fn *formulaFuncs) mid(name string, argsList *list.List) formulaArg { + if argsList.Len() != 3 { + return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires 3 arguments", name)) + } + text := argsList.Front().Value.(formulaArg).Value() + startNumArg, numCharsArg := argsList.Front().Next().Value.(formulaArg).ToNumber(), argsList.Front().Next().Next().Value.(formulaArg).ToNumber() + if startNumArg.Type != ArgNumber { + return startNumArg + } + if numCharsArg.Type != ArgNumber { + return numCharsArg + } + startNum := int(startNumArg.Number) + if startNum < 0 { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + 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]) +} + // PROPER converts all characters in a supplied text string to proper case // (i.e. all letters that do not immediately follow another letter are set to // upper case and all other characters are lower case). The syntax of the @@ -4844,6 +4935,54 @@ func (fn *formulaFuncs) PROPER(argsList *list.List) formulaArg { return newStringFormulaArg(buf.String()) } +// 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) +// +func (fn *formulaFuncs) REPLACE(argsList *list.List) formulaArg { + return fn.replace("REPLACE", argsList) +} + +// 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) +// +func (fn *formulaFuncs) REPLACEB(argsList *list.List) formulaArg { + return fn.replace("REPLACEB", argsList) +} + +// replace is an implementation of the formula function REPLACE and REPLACEB. +// TODO: support DBCS include Japanese, Chinese (Simplified), Chinese +// (Traditional), and Korean. +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() + startNumArg, numCharsArg := argsList.Front().Next().Value.(formulaArg).ToNumber(), argsList.Front().Next().Next().Value.(formulaArg).ToNumber() + if startNumArg.Type != ArgNumber { + return startNumArg + } + if numCharsArg.Type != ArgNumber { + return numCharsArg + } + oldTextLen, startIdx := len(oldText), int(startNumArg.Number) + if startIdx > oldTextLen { + startIdx = oldTextLen + 1 + } + endIdx := startIdx + int(numCharsArg.Number) + if endIdx > oldTextLen { + endIdx = oldTextLen + 1 + } + if startIdx < 1 || endIdx < 1 { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + result := oldText[:startIdx-1] + newText + oldText[endIdx-1:] + return newStringFormulaArg(result) +} + // REPT function returns a supplied text string, repeated a specified number // of times. The syntax of the function is: // @@ -4892,6 +5031,47 @@ func (fn *formulaFuncs) RIGHTB(argsList *list.List) formulaArg { return fn.leftRight("RIGHTB", argsList) } +// TRIM removes extra spaces (i.e. all spaces except for single spaces between +// words or characters) from a supplied text string. The syntax of the +// function is: +// +// TRIM(text) +// +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)) +} + +// UNICHAR returns the Unicode character that is referenced by the given +// numeric value. The syntax of the function is: +// +// UNICHAR(number) +// +func (fn *formulaFuncs) UNICHAR(argsList *list.List) formulaArg { + if argsList.Len() != 1 { + return newErrorFormulaArg(formulaErrorVALUE, "UNICHAR requires 1 argument") + } + numArg := argsList.Front().Value.(formulaArg).ToNumber() + if numArg.Type != ArgNumber { + return numArg + } + if numArg.Number <= 0 || numArg.Number > 55295 { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + return newStringFormulaArg(string(rune(numArg.Number))) +} + +// UNICODE function returns the code point for the first character of a +// supplied text string. The syntax of the function is: +// +// UNICODE(text) +// +func (fn *formulaFuncs) UNICODE(argsList *list.List) formulaArg { + return fn.code("UNICODE", argsList) +} + // UPPER converts all characters in a supplied text string to upper case. The // syntax of the function is: // @@ -5133,6 +5313,63 @@ func (fn *formulaFuncs) COLUMN(argsList *list.List) formulaArg { return newNumberFormulaArg(float64(col)) } +// 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) +// +func (fn *formulaFuncs) COLUMNS(argsList *list.List) formulaArg { + if argsList.Len() != 1 { + return newErrorFormulaArg(formulaErrorVALUE, "COLUMNS requires 1 argument") + } + var 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.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 { + cr := argsList.Front().Value.(formulaArg).cellRefs + for refs := cr.Front(); refs != nil; refs = refs.Next() { + 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 + } + } + } + if max == TotalColumns { + return newNumberFormulaArg(float64(TotalColumns)) + } + result := max - min + 1 + if max == min { + if min == 0 { + return newErrorFormulaArg(formulaErrorVALUE, "invalid reference") + } + return newNumberFormulaArg(float64(1)) + } + return newNumberFormulaArg(float64(result)) +} + // HLOOKUP function 'looks up' a given value in the top row of a data array // (or table), and returns the corresponding value from another row of the // array. The syntax of the function is: @@ -5396,6 +5633,85 @@ func lookupCol(arr formulaArg) []formulaArg { return col } +// 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]) +// +func (fn *formulaFuncs) ROW(argsList *list.List) formulaArg { + if argsList.Len() > 1 { + return newErrorFormulaArg(formulaErrorVALUE, "ROW requires at most 1 argument") + } + if argsList.Len() == 1 { + if argsList.Front().Value.(formulaArg).cellRanges != nil && argsList.Front().Value.(formulaArg).cellRanges.Len() > 0 { + return newNumberFormulaArg(float64(argsList.Front().Value.(formulaArg).cellRanges.Front().Value.(cellRange).From.Row)) + } + if argsList.Front().Value.(formulaArg).cellRefs != nil && argsList.Front().Value.(formulaArg).cellRefs.Len() > 0 { + return newNumberFormulaArg(float64(argsList.Front().Value.(formulaArg).cellRefs.Front().Value.(cellRef).Row)) + } + return newErrorFormulaArg(formulaErrorVALUE, "invalid reference") + } + _, row, _ := CellNameToCoordinates(fn.cell) + return newNumberFormulaArg(float64(row)) +} + +// 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) +// +func (fn *formulaFuncs) ROWS(argsList *list.List) formulaArg { + if argsList.Len() != 1 { + return newErrorFormulaArg(formulaErrorVALUE, "ROWS requires 1 argument") + } + var 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 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 { + cr := argsList.Front().Value.(formulaArg).cellRefs + for refs := cr.Front(); refs != nil; refs = refs.Next() { + 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 + } + } + } + if max == TotalRows { + return newStringFormulaArg(strconv.Itoa(TotalRows)) + } + result := max - min + 1 + if max == min { + if min == 0 { + return newErrorFormulaArg(formulaErrorVALUE, "invalid reference") + } + return newNumberFormulaArg(float64(1)) + } + return newStringFormulaArg(strconv.Itoa(result)) +} + // Web Functions // ENCODEURL function returns a URL-encoded string, replacing certain diff --git a/calc_test.go b/calc_test.go index 8a07eeffd6..c5293125ee 100644 --- a/calc_test.go +++ b/calc_test.go @@ -712,6 +712,11 @@ func TestCalcCellValue(t *testing.T) { "=DATE(2020,10,21)": "2020-10-21 00:00:00 +0000 UTC", "=DATE(1900,1,1)": "1899-12-31 00:00:00 +0000 UTC", // Text Functions + // CHAR + "=CHAR(65)": "A", + "=CHAR(97)": "a", + "=CHAR(63)": "?", + "=CHAR(51)": "3", // CLEAN "=CLEAN(\"\u0009clean text\")": "clean text", "=CLEAN(0)": "0", @@ -761,19 +766,38 @@ func TestCalcCellValue(t *testing.T) { // LENB "=LENB(\"\")": "0", "=LENB(D1)": "5", - // TRIM - "=TRIM(\" trim text \")": "trim text", - "=TRIM(0)": "0", // LOWER "=LOWER(\"test\")": "test", "=LOWER(\"TEST\")": "test", "=LOWER(\"Test\")": "test", "=LOWER(\"TEST 123\")": "test 123", + // MID + "=MID(\"Original Text\",7,1)": "a", + "=MID(\"Original Text\",4,7)": "ginal T", + "=MID(\"255 years\",3,1)": "5", + "=MID(\"text\",3,6)": "xt", + "=MID(\"text\",6,0)": "", + // MIDB + "=MIDB(\"Original Text\",7,1)": "a", + "=MIDB(\"Original Text\",4,7)": "ginal T", + "=MIDB(\"255 years\",3,1)": "5", + "=MIDB(\"text\",3,6)": "xt", + "=MIDB(\"text\",6,0)": "", // PROPER "=PROPER(\"this is a test sentence\")": "This Is A Test Sentence", "=PROPER(\"THIS IS A TEST SENTENCE\")": "This Is A Test Sentence", "=PROPER(\"123tEST teXT\")": "123Test Text", "=PROPER(\"Mr. SMITH's address\")": "Mr. Smith'S Address", + // REPLACE + "=REPLACE(\"test string\",7,3,\"X\")": "test sXng", + "=REPLACE(\"second test string\",8,4,\"XXX\")": "second XXX string", + "=REPLACE(\"text\",5,0,\" and char\")": "text and char", + "=REPLACE(\"text\",1,20,\"char and \")": "char and ", + // REPLACEB + "=REPLACEB(\"test string\",7,3,\"X\")": "test sXng", + "=REPLACEB(\"second test string\",8,4,\"XXX\")": "second XXX string", + "=REPLACEB(\"text\",5,0,\" and char\")": "text and char", + "=REPLACEB(\"text\",1,20,\"char and \")": "char and ", // REPT "=REPT(\"*\",0)": "", "=REPT(\"*\",1)": "*", @@ -790,6 +814,19 @@ func TestCalcCellValue(t *testing.T) { "=RIGHTB(\"Original Text\",0)": "", "=RIGHTB(\"Original Text\",13)": "Original Text", "=RIGHTB(\"Original Text\",20)": "Original Text", + // TRIM + "=TRIM(\" trim text \")": "trim text", + "=TRIM(0)": "0", + // UNICHAR + "=UNICHAR(65)": "A", + "=UNICHAR(97)": "a", + "=UNICHAR(63)": "?", + "=UNICHAR(51)": "3", + // UNICODE + "=UNICODE(\"Alpha\")": "65", + "=UNICODE(\"alpha\")": "97", + "=UNICODE(\"?\")": "63", + "=UNICODE(\"3\")": "51", // UPPER "=UPPER(\"test\")": "TEST", "=UPPER(\"TEST\")": "TEST", @@ -812,6 +849,15 @@ func TestCalcCellValue(t *testing.T) { "=COLUMN(Sheet1!A1:B1:C1)": "1", "=COLUMN(Sheet1!F1:G1)": "6", "=COLUMN(H1)": "8", + // COLUMNS + "=COLUMNS(B1)": "1", + "=COLUMNS(1:1)": "16384", + "=COLUMNS(Sheet1!1:1)": "16384", + "=COLUMNS(B1:E5)": "4", + "=COLUMNS(Sheet1!E5:H7:B1)": "7", + "=COLUMNS(E5:H7:B1:C1:Z1:C1:B1)": "25", + "=COLUMNS(E5:B1)": "4", + "=COLUMNS(EM38:HZ81)": "92", // HLOOKUP "=HLOOKUP(D2,D2:D8,1,FALSE)": "Jan", "=HLOOKUP(F3,F3:F8,3,FALSE)": "34440", @@ -832,6 +878,21 @@ func TestCalcCellValue(t *testing.T) { "=LOOKUP(F8,F8:F9,F8:F9)": "32080", "=LOOKUP(F8,F8:F9,D8:D9)": "Feb", "=LOOKUP(1,MUNIT(1),MUNIT(1))": "1", + // ROW + "=ROW()": "1", + "=ROW(Sheet1!A1)": "1", + "=ROW(Sheet1!A1:B2:C3)": "1", + "=ROW(Sheet1!F5:G6)": "5", + "=ROW(A8)": "8", + // ROWS + "=ROWS(B1)": "1", + "=ROWS(B:B)": "1048576", + "=ROWS(Sheet1!B:B)": "1048576", + "=ROWS(B1:E5)": "5", + "=ROWS(Sheet1!E5:H7:B1)": "7", + "=ROWS(E5:H8:B2:C3:Z26:C3:B2)": "25", + "=ROWS(E5:B1)": "5", + "=ROWS(EM38:HZ81)": "44", // Web Functions // ENCODEURL "=ENCODEURL(\"https://xuri.me/excelize/en/?q=Save As\")": "https%3A%2F%2Fxuri.me%2Fexcelize%2Fen%2F%3Fq%3DSave%20As", @@ -1348,6 +1409,11 @@ func TestCalcCellValue(t *testing.T) { `=DATE(2020,"text",21)`: "DATE requires 3 number arguments", `=DATE(2020,10,"text")`: "DATE requires 3 number arguments", // Text Functions + // CHAR + "=CHAR()": "CHAR requires 1 argument", + "=CHAR(-1)": "#VALUE!", + "=CHAR(256)": "#VALUE!", + "=CHAR(\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", // CLEAN "=CLEAN()": "CLEAN requires 1 argument", "=CLEAN(1,2)": "CLEAN requires 1 argument", @@ -1387,18 +1453,32 @@ func TestCalcCellValue(t *testing.T) { "=LEN()": "LEN requires 1 string argument", // LENB "=LENB()": "LENB requires 1 string argument", - // TRIM - "=TRIM()": "TRIM requires 1 argument", - "=TRIM(1,2)": "TRIM requires 1 argument", // LOWER "=LOWER()": "LOWER requires 1 argument", "=LOWER(1,2)": "LOWER requires 1 argument", - // UPPER - "=UPPER()": "UPPER requires 1 argument", - "=UPPER(1,2)": "UPPER 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", + // MIDB + "=MIDB()": "MIDB requires 3 arguments", + "=MIDB(\"\",-1,1)": "#VALUE!", + "=MIDB(\"\",\"\",1)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=MIDB(\"\",1,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", // PROPER "=PROPER()": "PROPER requires 1 argument", "=PROPER(1,2)": "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", + // 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", // REPT "=REPT()": "REPT requires 2 arguments", "=REPT(INT(0),2)": "REPT requires first argument to be a string", @@ -1414,6 +1494,20 @@ func TestCalcCellValue(t *testing.T) { "=RIGHTB(\"\",2,3)": "RIGHTB allows at most 2 arguments", "=RIGHTB(\"\",\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", "=RIGHTB(\"\",-1)": "#VALUE!", + // TRIM + "=TRIM()": "TRIM requires 1 argument", + "=TRIM(1,2)": "TRIM requires 1 argument", + // UNICHAR + "=UNICHAR()": "UNICHAR requires 1 argument", + "=UNICHAR(\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=UNICHAR(55296)": "#VALUE!", + "=UNICHAR(0)": "#VALUE!", + // UNICODE + "=UNICODE()": "UNICODE requires 1 argument", + "=UNICODE(\"\")": "#VALUE!", + // UPPER + "=UPPER()": "UPPER requires 1 argument", + "=UPPER(1,2)": "UPPER requires 1 argument", // Conditional Functions // IF "=IF()": "IF requires at least 1 argument", @@ -1429,6 +1523,13 @@ func TestCalcCellValue(t *testing.T) { "=COLUMN(\"\")": "invalid reference", "=COLUMN(Sheet1)": "invalid column name \"Sheet1\"", "=COLUMN(Sheet1!A1!B1)": "invalid column name \"Sheet1\"", + // COLUMNS + "=COLUMNS()": "COLUMNS requires 1 argument", + "=COLUMNS(1)": "invalid reference", + "=COLUMNS(\"\")": "invalid reference", + "=COLUMNS(Sheet1)": "invalid column name \"Sheet1\"", + "=COLUMNS(Sheet1!A1!B1)": "invalid column name \"Sheet1\"", + "=COLUMNS(Sheet1!Sheet1)": "invalid column name \"Sheet1\"", // HLOOKUP "=HLOOKUP()": "HLOOKUP requires at least 3 arguments", "=HLOOKUP(D2,D1,1,FALSE)": "HLOOKUP requires second argument of table array", @@ -1460,6 +1561,18 @@ func TestCalcCellValue(t *testing.T) { "=LOOKUP(D2,D1,D2)": "LOOKUP requires second argument of table array", "=LOOKUP(D2,D1,D2,FALSE)": "LOOKUP requires at most 3 arguments", "=LOOKUP(D1,MUNIT(1),MUNIT(1))": "LOOKUP no result found", + // ROW + "=ROW(1,2)": "ROW requires at most 1 argument", + "=ROW(\"\")": "invalid reference", + "=ROW(Sheet1)": "invalid column name \"Sheet1\"", + "=ROW(Sheet1!A1!B1)": "invalid column name \"Sheet1\"", + // ROWS + "=ROWS()": "ROWS requires 1 argument", + "=ROWS(1)": "invalid reference", + "=ROWS(\"\")": "invalid reference", + "=ROWS(Sheet1)": "invalid column name \"Sheet1\"", + "=ROWS(Sheet1!A1!B1)": "invalid column name \"Sheet1\"", + "=ROWS(Sheet1!Sheet1)": "invalid column name \"Sheet1\"", // Web Functions // ENCODEURL "=ENCODEURL()": "ENCODEURL requires 1 argument", diff --git a/lib.go b/lib.go index b6ea32139d..32ef615fb1 100644 --- a/lib.go +++ b/lib.go @@ -33,14 +33,14 @@ func ReadZipReader(r *zip.Reader) (map[string][]byte, int, error) { fileList := make(map[string][]byte, len(r.File)) worksheets := 0 for _, v := range r.File { - fileName := v.Name - if partName, ok := docPart[strings.ToLower(v.Name)]; ok { + fileName := strings.Replace(v.Name, "\\", "/", -1) + if partName, ok := docPart[strings.ToLower(fileName)]; ok { fileName = partName } if fileList[fileName], err = readFile(v); err != nil { return nil, 0, err } - if strings.HasPrefix(v.Name, "xl/worksheets/sheet") { + if strings.HasPrefix(fileName, "xl/worksheets/sheet") { worksheets++ } } diff --git a/sheet.go b/sheet.go index d4362b1296..087364475c 100644 --- a/sheet.go +++ b/sheet.go @@ -157,6 +157,9 @@ func (f *File) workSheetWriter() { for k, v := range sheet.SheetData.Row { f.Sheet[p].SheetData.Row[k].C = trimCell(v.C) } + if sheet.SheetPr != nil || sheet.Drawing != nil || sheet.Hyperlinks != nil || sheet.Picture != nil || sheet.TableParts != nil { + f.addNameSpaces(p, SourceRelationship) + } // reusing buffer _ = encoder.Encode(sheet) f.saveFileList(p, replaceRelationshipsBytes(f.replaceNameSpaceBytes(p, buffer.Bytes()))) @@ -455,7 +458,11 @@ func (f *File) getSheetMap() map[string]string { // path, compatible with different types of relative paths in // workbook.xml.rels, for example: worksheets/sheet%d.xml // and /xl/worksheets/sheet%d.xml - path := filepath.ToSlash(strings.TrimPrefix(filepath.Clean(fmt.Sprintf("%s/%s", filepath.Dir(f.getWorkbookPath()), rel.Target)), "/")) + path := filepath.ToSlash(strings.TrimPrefix( + strings.Replace(filepath.Clean(fmt.Sprintf("%s/%s", filepath.Dir(f.getWorkbookPath()), rel.Target)), "\\", "/", -1), "/")) + if strings.HasPrefix(rel.Target, "/") { + path = filepath.ToSlash(strings.TrimPrefix(strings.Replace(filepath.Clean(rel.Target), "\\", "/", -1), "/")) + } if _, ok := f.XLSX[path]; ok { maps[v.Name] = path } diff --git a/test/Book1.xlsx b/test/Book1.xlsx index d5a059121b7d591b7fab2dc10f7ed6202255dc28..64c9e70977cf2fbbd8ba08a29cd005f16d3d79b3 100644 GIT binary patch delta 1178 zcmeBM#5i#gBVT|wGm8iV2L}hk>djF>8~Iq7SU`-;;!N_45SA75K`84#OC5wYxsgq) ze(U73euoVNTHf2VZY_VL@`q!g)n0`qaSLr4Qu80J=*zvyZIa)=Fle??hegX|)8w~j z)YtEcde46Pp24zv){LQnOwP;qyo*(Tkovo#l3izFNmSJPfCC+z*Uqv=+b*0srG3&% z=OoLcs*G(KcZ%mtIq10T5PQkbWnT*?Oi6uHF(Ip7-TFys$f!%z6Lzec0T8 zp@t#Ao1G(_RX$DZ8v_GFATt9{067FV?_n2)2GSmO)yY3N-h)GN@ zEFZ4TjshPbBAZRcwV>Ke#T7O;OK?HMe4=D2D@>34W=(l{7D%l4CP&PK z-h4>)9#njZ`UXf`0__&woUL`217?o!=6K_MP*sYib<7CyZ5Ad_@pqO`w{B*0kbtHM zC8u_{P0E`uIQK%u6WtC$%~1EUg&3CMWeQ;|^s<65?t5ADf>IVJ?7%@cS>4-O9whRI zed5nkz`*QhWMGg7(g?6|a-+AN0!U32^G~zWKnYAWCpacQx9114Z(HaVd(TMr-nX9~H3mxYa~R zOfHlVpZv=QV*O+;Un!o|F(uRMf%2B>4Y z%H$0ys*`#BpgLszq?nedPTs33r5)hS$Rq-EJS>YY(q&wm&BVa)m4$&p8pXLYC)fFD zLCjp^C&lPH`M4j{#&< fzpVp?@Gou#1_=~rhq(bYNlpF{AiyT&1(E>(>I$74 delta 1131 zcmZWoZAep57(VyzQ`5EDYpN+3!+*_ zg$y+{m9%s=Qz~plnPr(u8j)05L=csZlGH4_^PC~t{d1q^JTP-nh-H2 z4p$0yuBBT@Wd&pC&FR4UyoBFjC; zKJVi-57js3h#$Qfyz@&Gbatn+DQbO6@9Ru!V*1_qRkhmWgO?uKiqqGL%3|bwwU-}v zZ996grDR+F+6DHGibnVT)^}g*trPxLn0W9$J}mYF4xI!B^!N4}fC~vo!BEn&{Sv5!Zz)wCJXrTxjTw}pvnx)e)!$%z~jf2-Ma^&r0X)*j_ zi9u9hrN}BZysxGr7!yHC*_^)1Rvk&)$Rq(6=K`>NUiwma%Bn&BW-CP)VYd~3+r!d0 z7(k=Fg?^9ZdQai{MF0fO>mY$vdIjo`MN>oxJVWCSPMRWzb#O)(BwiQ@dRXZM3}8Z$ yGm4=IDNJJUR`MB&tW1O>Ob~Q3OJ;{Fy50UQj}0)z1_+)PU^g6QBKR_+xA+gaKWY;I From 7ef1892f320879670c4e9a8536a70281d782625e Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 2 Mar 2021 22:34:09 +0800 Subject: [PATCH 342/957] #65 fn: LARGE, SMALL, SUBSTITUTE, refactor ARABIC --- calc.go | 168 +++++++++++++++++++++++++++++++++++++++++++-------- calc_test.go | 40 ++++++++++-- 2 files changed, 178 insertions(+), 30 deletions(-) diff --git a/calc.go b/calc.go index 906a0bbecc..8b78e28530 100644 --- a/calc.go +++ b/calc.go @@ -167,15 +167,17 @@ func (fa formulaArg) ToBool() formulaArg { // ToList returns a formula argument with array data type. func (fa formulaArg) ToList() []formulaArg { - if fa.Type == ArgMatrix { + switch fa.Type { + case ArgMatrix: list := []formulaArg{} for _, row := range fa.Matrix { list = append(list, row...) } return list - } - if fa.Type == ArgList { + case ArgList: return fa.List + case ArgNumber, ArgString, ArgError, ArgUnknown: + return []formulaArg{fa} } return nil } @@ -294,6 +296,7 @@ var tokenPriority = map[string]int{ // ISTEXT // ISO.CEILING // KURT +// LARGE // LCM // LEFT // LEFTB @@ -348,10 +351,12 @@ var tokenPriority = map[string]int{ // SIGN // SIN // SINH +// SMALL // SQRT // SQRTPI // STDEV // STDEVA +// SUBSTITUTE // SUM // SUMIF // SUMSQ @@ -454,10 +459,7 @@ func newEmptyFormulaArg() formulaArg { // opf - Operation formula // opfd - Operand of the operation formula // opft - Operator of the operation formula -// -// Evaluate arguments of the operation formula by list: -// -// args - Arguments of the operation formula +// args - Arguments list of the operation formula // // TODO: handle subtypes: Nothing, Text, Logical, Error, Concatenation, Intersection, Union // @@ -1705,28 +1707,48 @@ func (fn *formulaFuncs) ARABIC(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "ARABIC requires 1 numeric argument") } - charMap := map[rune]float64{'I': 1, 'V': 5, 'X': 10, 'L': 50, 'C': 100, 'D': 500, 'M': 1000} - val, last, prefix := 0.0, 0.0, 1.0 - for _, char := range argsList.Front().Value.(formulaArg).String { - digit := 0.0 - if char == '-' { - prefix = -1 + text := argsList.Front().Value.(formulaArg).Value() + if len(text) > 255 { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + text = strings.ToUpper(text) + number, actualStart, index, isNegative := 0, 0, len(text)-1, false + startIndex, subtractNumber, currentPartValue, currentCharValue, prevCharValue := 0, 0, 0, 0, -1 + for index >= 0 && text[index] == ' ' { + index-- + } + for actualStart <= index && text[actualStart] == ' ' { + actualStart++ + } + if actualStart <= index && text[actualStart] == '-' { + isNegative = true + actualStart++ + } + charMap := map[rune]int{'I': 1, 'V': 5, 'X': 10, 'L': 50, 'C': 100, 'D': 500, 'M': 1000} + for index >= actualStart { + startIndex = index + startChar := text[startIndex] + index-- + for index >= actualStart && (text[index]|' ') == startChar { + index-- + } + currentCharValue = charMap[rune(startChar)] + currentPartValue = (startIndex - index) * currentCharValue + if currentCharValue >= prevCharValue { + number += currentPartValue - subtractNumber + prevCharValue = currentCharValue + subtractNumber = 0 continue } - digit = charMap[char] - val += digit - switch { - case last == digit && (last == 5 || last == 50 || last == 500): - return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) - case 2*last == digit: - return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) - } - if last < digit { - val -= 2 * last - } - last = digit + subtractNumber += currentPartValue + } + if subtractNumber != 0 { + number -= subtractNumber + } + if isNegative { + number = -number } - return newNumberFormulaArg(prefix * val) + return newNumberFormulaArg(float64(number)) } // ASIN function calculates the arcsine (i.e. the inverse sine) of a given @@ -3933,6 +3955,45 @@ func (fn *formulaFuncs) KURT(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV) } +// kth is an implementation of the formula function LARGE and SMALL. +func (fn *formulaFuncs) kth(name string, argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires 2 arguments", name)) + } + array := argsList.Front().Value.(formulaArg).ToList() + kArg := argsList.Back().Value.(formulaArg).ToNumber() + if kArg.Type != ArgNumber { + return kArg + } + k := int(kArg.Number) + if k < 1 { + return newErrorFormulaArg(formulaErrorNUM, "k should be > 0") + } + data := []float64{} + for _, arg := range array { + if numArg := arg.ToNumber(); numArg.Type == ArgNumber { + data = append(data, numArg.Number) + } + } + if len(data) < k { + return newErrorFormulaArg(formulaErrorNUM, "k should be <= length of array") + } + sort.Float64s(data) + if name == "LARGE" { + return newNumberFormulaArg(data[len(data)-k]) + } + return newNumberFormulaArg(data[k-1]) +} + +// LARGE function returns the k'th largest value from an array of numeric +// values. The syntax of the function is: +// +// LARGE(array,k) +// +func (fn *formulaFuncs) LARGE(argsList *list.List) formulaArg { + return fn.kth("LARGE", argsList) +} + // MAX function returns the largest value from a supplied set of numeric // values. The syntax of the function is: // @@ -4168,6 +4229,15 @@ func (fn *formulaFuncs) PERMUT(argsList *list.List) formulaArg { return newNumberFormulaArg(math.Round(fact(number.Number) / fact(number.Number-chosen.Number))) } +// SMALL function returns the k'th smallest value from an array of numeric +// values. The syntax of the function is: +// +// SMALL(array,k) +// +func (fn *formulaFuncs) SMALL(argsList *list.List) formulaArg { + return fn.kth("SMALL", argsList) +} + // Information Functions // ISBLANK function tests if a specified cell is blank (empty) and if so, @@ -5031,6 +5101,52 @@ func (fn *formulaFuncs) RIGHTB(argsList *list.List) formulaArg { return fn.leftRight("RIGHTB", argsList) } +// 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]) +// +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 + if argsList.Len() == 3 { + return newStringFormulaArg(strings.Replace(text.Value(), oldText.Value(), newText.Value(), -1)) + } + instanceNumArg := argsList.Back().Value.(formulaArg).ToNumber() + if instanceNumArg.Type != ArgNumber { + return instanceNumArg + } + instanceNum = int(instanceNumArg.Number) + if instanceNum < 1 { + return newErrorFormulaArg(formulaErrorVALUE, "instance_num should be > 0") + } + str, oldTextLen, count, chars, pos := text.Value(), len(oldText.Value()), instanceNum, 0, -1 + for { + count-- + index := strings.Index(str, oldText.Value()) + if index == -1 { + pos = -1 + break + } else { + pos = index + chars + if count == 0 { + break + } + idx := oldTextLen + index + chars += idx + str = str[idx:] + } + } + if pos == -1 { + return newStringFormulaArg(text.Value()) + } + pre, post := text.Value()[:pos], text.Value()[pos+oldTextLen:] + return newStringFormulaArg(pre + newText.Value() + post) +} + // TRIM removes extra spaces (i.e. all spaces except for single spaces between // words or characters) from a supplied text string. The syntax of the // function is: diff --git a/calc_test.go b/calc_test.go index c5293125ee..29da244d04 100644 --- a/calc_test.go +++ b/calc_test.go @@ -161,10 +161,11 @@ func TestCalcCellValue(t *testing.T) { "=_xlfn.ACOTH(2)": "0.549306144334055", "=_xlfn.ACOTH(ABS(-2))": "0.549306144334055", // ARABIC - `=_xlfn.ARABIC("IV")`: "4", - `=_xlfn.ARABIC("-IV")`: "-4", - `=_xlfn.ARABIC("MCXX")`: "1120", - `=_xlfn.ARABIC("")`: "0", + "=_xlfn.ARABIC(\"IV\")": "4", + "=_xlfn.ARABIC(\"-IV\")": "-4", + "=_xlfn.ARABIC(\"MCXX\")": "1120", + "=_xlfn.ARABIC(\"\")": "0", + "=_xlfn.ARABIC(\" ll lc \")": "-50", // ASIN "=ASIN(-1)": "-1.570796326794897", "=ASIN(0)": "0", @@ -608,6 +609,11 @@ func TestCalcCellValue(t *testing.T) { "=KURT(F1:F9)": "-1.033503502551368", "=KURT(F1,F2:F9)": "-1.033503502551368", "=KURT(INT(1),MUNIT(2))": "-3.333333333333336", + // LARGE + "=LARGE(A1:A5,1)": "3", + "=LARGE(A1:B5,2)": "4", + "=LARGE(A1,1)": "1", + "=LARGE(A1:F2,1)": "36693", // MAX "=MAX(1)": "1", "=MAX(TRUE())": "1", @@ -646,6 +652,11 @@ func TestCalcCellValue(t *testing.T) { "=PERMUT(6,6)": "720", "=PERMUT(7,6)": "5040", "=PERMUT(10,6)": "151200", + // SMALL + "=SMALL(A1:A5,1)": "0", + "=SMALL(A1:B5,2)": "1", + "=SMALL(A1,1)": "1", + "=SMALL(A1:F2,1)": "1", // Information Functions // ISBLANK "=ISBLANK(A1)": "FALSE", @@ -814,6 +825,12 @@ func TestCalcCellValue(t *testing.T) { "=RIGHTB(\"Original Text\",0)": "", "=RIGHTB(\"Original Text\",13)": "Original Text", "=RIGHTB(\"Original Text\",20)": "Original Text", + // SUBSTITUTE + "=SUBSTITUTE(\"abab\",\"a\",\"X\")": "XbXb", + "=SUBSTITUTE(\"abab\",\"a\",\"X\",2)": "abXb", + "=SUBSTITUTE(\"abab\",\"x\",\"X\",2)": "abab", + "=SUBSTITUTE(\"John is 5 years old\",\"John\",\"Jack\")": "Jack is 5 years old", + "=SUBSTITUTE(\"John is 5 years old\",\"5\",\"6\")": "John is 6 years old", // TRIM "=TRIM(\" trim text \")": "trim text", "=TRIM(0)": "0", @@ -1046,6 +1063,7 @@ func TestCalcCellValue(t *testing.T) { "=_xlfn.ACOTH(_xlfn.ACOTH(2))": "#NUM!", // _xlfn.ARABIC "=_xlfn.ARABIC()": "ARABIC requires 1 numeric argument", + "=_xlfn.ARABIC(\"" + strings.Repeat("I", 256) + "\")": "#VALUE!", // ASIN "=ASIN()": "ASIN requires 1 numeric argument", `=ASIN("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", @@ -1334,6 +1352,11 @@ func TestCalcCellValue(t *testing.T) { // KURT "=KURT()": "KURT requires at least 1 argument", "=KURT(F1,INT(1))": "#DIV/0!", + // 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", // MAX "=MAX()": "MAX requires at least 1 argument", "=MAX(NA())": "#N/A", @@ -1355,6 +1378,11 @@ func TestCalcCellValue(t *testing.T) { "=PERMUT(\"\",0)": "strconv.ParseFloat: parsing \"\": invalid syntax", "=PERMUT(0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", "=PERMUT(6,8)": "#N/A", + // 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", // Information Functions // ISBLANK "=ISBLANK(A1,A2)": "ISBLANK requires 1 argument", @@ -1494,6 +1522,10 @@ func TestCalcCellValue(t *testing.T) { "=RIGHTB(\"\",2,3)": "RIGHTB allows at most 2 arguments", "=RIGHTB(\"\",\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", "=RIGHTB(\"\",-1)": "#VALUE!", + // SUBSTITUTE + "=SUBSTITUTE()": "SUBSTITUTE requires 3 or 4 arguments", + "=SUBSTITUTE(\"\",\"\",\"\",\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=SUBSTITUTE(\"\",\"\",\"\",0)": "instance_num should be > 0", // TRIM "=TRIM()": "TRIM requires 1 argument", "=TRIM(1,2)": "TRIM requires 1 argument", From a12dfd3ce6402f22baeb6415af271d8545c74f71 Mon Sep 17 00:00:00 2001 From: James Allen <24575899+jrdallen97@users.noreply.github.com> Date: Wed, 3 Mar 2021 12:30:31 +0000 Subject: [PATCH 343/957] Add support for setting hyperlink display & tooltip (closes #790) (#794) --- cell.go | 18 +++++++++++++++++- excelize_test.go | 7 +++++++ xmlWorksheet.go | 1 + 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/cell.go b/cell.go index 912ee6ab25..2567f198fb 100644 --- a/cell.go +++ b/cell.go @@ -431,6 +431,13 @@ func (f *File) GetCellHyperLink(sheet, axis string) (bool, string, error) { return false, "", err } +// HyperlinkOpts can be passed to SetCellHyperlink to set optional hyperlink +// attributes (e.g. display value) +type HyperlinkOpts struct { + Display *string + Tooltip *string +} + // 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 @@ -446,7 +453,7 @@ func (f *File) GetCellHyperLink(sheet, axis string) (bool, string, error) { // // err := f.SetCellHyperLink("Sheet1", "A3", "Sheet1!A40", "Location") // -func (f *File) SetCellHyperLink(sheet, axis, link, linkType string) error { +func (f *File) SetCellHyperLink(sheet, axis, link, linkType string, opts ...HyperlinkOpts) error { // Check for correct cell name if _, _, err := SplitCellName(axis); err != nil { return err @@ -490,6 +497,15 @@ func (f *File) SetCellHyperLink(sheet, axis, link, linkType string) error { return fmt.Errorf("invalid link type %q", linkType) } + for _, o := range opts { + if o.Display != nil { + linkData.Display = *o.Display + } + if o.Tooltip != nil { + linkData.Tooltip = *o.Tooltip + } + } + ws.Hyperlinks.Hyperlink = append(ws.Hyperlinks.Hyperlink, linkData) return nil } diff --git a/excelize_test.go b/excelize_test.go index 8bce6d1350..0d0d587421 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -326,6 +326,13 @@ func TestSetCellHyperLink(t *testing.T) { assert.NoError(t, f.SetCellHyperLink("Sheet2", "C1", "https://github.com/360EntSecGroup-Skylar/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. + display := "Display value" + tooltip := "Hover text" + assert.NoError(t, f.SetCellHyperLink("Sheet2", "D7", "Sheet1!D9", "Location", HyperlinkOpts{ + Display: &display, + Tooltip: &tooltip, + })) assert.EqualError(t, f.SetCellHyperLink("Sheet2", "C3", "Sheet1!D8", ""), `invalid link type ""`) diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 72a470ffc1..b31eec1bf2 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -604,6 +604,7 @@ type xlsxHyperlink struct { Ref string `xml:"ref,attr"` Location string `xml:"location,attr,omitempty"` Display string `xml:"display,attr,omitempty"` + Tooltip string `xml:"tooltip,attr,omitempty"` RID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr,omitempty"` } From dbe88d723ef8ab3d2fbb1b94aa4c5b2486b0e679 Mon Sep 17 00:00:00 2001 From: yuki2006 Date: Thu, 4 Mar 2021 10:23:45 +0900 Subject: [PATCH 344/957] Fix UpdateLinkedValue which returns an error when has graph sheet (#793) * Fixed UpdateLinkedValue which returns an error when there is a graph sheet Signed-off-by: yuuki.ono * fix refactoring from review Signed-off-by: yuuki.ono --- chart_test.go | 2 ++ excelize.go | 3 +++ 2 files changed, 5 insertions(+) diff --git a/chart_test.go b/chart_test.go index 9bbc06d141..6ee0a0f0e0 100644 --- a/chart_test.go +++ b/chart_test.go @@ -236,6 +236,8 @@ func TestAddChartSheet(t *testing.T) { // 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"))) } diff --git a/excelize.go b/excelize.go index a38a74566c..f45dcc031f 100644 --- a/excelize.go +++ b/excelize.go @@ -314,6 +314,9 @@ func (f *File) UpdateLinkedValue() error { for _, name := range f.GetSheetList() { xlsx, err := f.workSheetReader(name) if err != nil { + if err.Error() == fmt.Sprintf("sheet %s is chart sheet", trimSheetName(name)) { + continue + } return err } for indexR := range xlsx.SheetData.Row { From 71bd5e19598db4dae85d983d1f79251451cb2d9a Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 5 Mar 2021 00:40:37 +0800 Subject: [PATCH 345/957] fix incorrect default column from GetColWidth --- col.go | 74 +++++++++++++++++++++++------------------------------ col_test.go | 4 +-- 2 files changed, 34 insertions(+), 44 deletions(-) diff --git a/col.go b/col.go index 0980596aef..ab95a0b4b9 100644 --- a/col.go +++ b/col.go @@ -24,6 +24,7 @@ import ( // Define the default cell size and EMU unit of measurement. const ( + defaultColWidth float64 = 9.140625 defaultColWidthPixels float64 = 64 defaultRowHeight float64 = 15 defaultRowHeightPixels float64 = 20 @@ -270,32 +271,18 @@ 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 { - var max int - - colsTab := strings.Split(columns, ":") - min, err := ColumnNameToNumber(colsTab[0]) + start, end, err := f.parseColRange(columns) if err != nil { return err } - if len(colsTab) == 2 { - max, err = ColumnNameToNumber(colsTab[1]) - if err != nil { - return err - } - } else { - max = min - } - if max < min { - min, max = max, min - } ws, err := f.workSheetReader(sheet) if err != nil { return err } colData := xlsxCol{ - Min: min, - Max: max, - Width: 9, // default width + Min: start, + Max: end, + Width: defaultColWidth, // default width Hidden: !visible, CustomWidth: true, } @@ -346,6 +333,25 @@ func (f *File) GetColOutlineLevel(sheet, col string) (uint8, error) { return level, err } +// parseColRange parse and convert column range with column name to the column number. +func (f *File) parseColRange(columns string) (start, end int, err error) { + colsTab := strings.Split(columns, ":") + start, err = ColumnNameToNumber(colsTab[0]) + if err != nil { + return + } + end = start + if len(colsTab) == 2 { + if end, err = ColumnNameToNumber(colsTab[1]); err != nil { + return + } + } + if end < start { + start, end = end, start + } + return +} + // SetColOutlineLevel provides a function to set outline level of a single // 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: @@ -401,37 +407,21 @@ 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 { - ws, err := f.workSheetReader(sheet) + start, end, err := f.parseColRange(columns) if err != nil { return err } - var c1, c2 string - var min, max int - cols := strings.Split(columns, ":") - c1 = cols[0] - min, err = ColumnNameToNumber(c1) + ws, err := f.workSheetReader(sheet) if err != nil { return err } - if len(cols) == 2 { - c2 = cols[1] - max, err = ColumnNameToNumber(c2) - if err != nil { - return err - } - } else { - max = min - } - if max < min { - min, max = max, min - } if ws.Cols == nil { ws.Cols = &xlsxCols{} } ws.Cols.Col = flatCols(xlsxCol{ - Min: min, - Max: max, - Width: 9, + Min: start, + Max: end, + Width: defaultColWidth, Style: styleID, }, ws.Cols.Col, func(fc, c xlsxCol) xlsxCol { fc.BestFit = c.BestFit @@ -638,11 +628,11 @@ func (f *File) getColWidth(sheet string, col int) int { func (f *File) GetColWidth(sheet, col string) (float64, error) { colNum, err := ColumnNameToNumber(col) if err != nil { - return defaultColWidthPixels, err + return defaultColWidth, err } ws, err := f.workSheetReader(sheet) if err != nil { - return defaultColWidthPixels, err + return defaultColWidth, err } if ws.Cols != nil { var width float64 @@ -656,7 +646,7 @@ func (f *File) GetColWidth(sheet, col string) (float64, error) { } } // Optimisation for when the column widths haven't changed. - return defaultColWidthPixels, err + return defaultColWidth, err } // InsertCol provides a function to insert a new column before given column diff --git a/col_test.go b/col_test.go index 706f90aee0..add1c116a4 100644 --- a/col_test.go +++ b/col_test.go @@ -310,12 +310,12 @@ func TestColWidth(t *testing.T) { assert.Equal(t, float64(12), width) assert.NoError(t, err) width, err = f.GetColWidth("Sheet1", "C") - assert.Equal(t, float64(64), width) + assert.Equal(t, defaultColWidth, width) assert.NoError(t, err) // Test set and get column width with illegal cell coordinates. width, err = f.GetColWidth("Sheet1", "*") - assert.Equal(t, float64(64), width) + assert.Equal(t, defaultColWidth, width) assert.EqualError(t, err, `invalid column name "*"`) assert.EqualError(t, f.SetColWidth("Sheet1", "*", "B", 1), `invalid column name "*"`) assert.EqualError(t, f.SetColWidth("Sheet1", "A", "*", 1), `invalid column name "*"`) From 7e12b560ce40fc756fa5347d25a64ea48f9710ac Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 7 Mar 2021 15:02:04 +0800 Subject: [PATCH 346/957] #625, support setting formula for cell in streaming API --- chart.go | 6 +++--- stream.go | 31 ++++++++++++++++++++++++++++++- stream_test.go | 4 ++-- xmlChartSheet.go | 40 ++++++++++++++++++++-------------------- 4 files changed, 55 insertions(+), 26 deletions(-) diff --git a/chart.go b/chart.go index 9d44c50e75..6b7053eba6 100644 --- a/chart.go +++ b/chart.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2021 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. // @@ -923,8 +923,8 @@ func (f *File) AddChartSheet(sheet, format string, combo ...string) error { return err } cs := xlsxChartsheet{ - SheetViews: []*xlsxChartsheetViews{{ - SheetView: []*xlsxChartsheetView{{ZoomScaleAttr: 100, ZoomToFitAttr: true}}}, + SheetViews: &xlsxChartsheetViews{ + SheetView: []*xlsxChartsheetView{{ZoomScaleAttr: 100, ZoomToFitAttr: true}}, }, } f.SheetCount++ diff --git a/stream.go b/stream.go index f5fda9d700..7aaf7b4295 100644 --- a/stream.go +++ b/stream.go @@ -71,6 +71,13 @@ type StreamWriter struct { // 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)"}}); +// func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) { sheetID := f.getSheetID(sheet) if sheetID == -1 { @@ -106,7 +113,14 @@ func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) { // // 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. @@ -266,6 +280,7 @@ func getRowElement(token xml.Token, hrow int) (startElement xml.StartElement, ok // a value. type Cell struct { StyleID int + Formula string Value interface{} } @@ -291,9 +306,11 @@ func (sw *StreamWriter) SetRow(axis string, values []interface{}) error { if v, ok := val.(Cell); ok { c.S = v.StyleID val = v.Value + setCellFormula(&c, v.Formula) } else if v, ok := val.(*Cell); ok && v != nil { c.S = v.StyleID val = v.Value + setCellFormula(&c, v.Formula) } if err = setCellValFunc(&c, val); err != nil { _, _ = sw.rawData.WriteString(``) @@ -305,6 +322,13 @@ func (sw *StreamWriter) SetRow(axis string, values []interface{}) error { return sw.rawData.Sync() } +// setCellFormula provides a function to set formula of a cell. +func setCellFormula(c *xlsxC, formula string) { + if formula != "" { + c.F = &xlsxF{Content: formula} + } +} + // setCellValFunc provides a function to set value of a cell. func setCellValFunc(c *xlsxC, val interface{}) (err error) { switch val := val.(type) { @@ -373,6 +397,11 @@ func writeCell(buf *bufferedWriter, c xlsxC) { fmt.Fprintf(buf, ` t="%s"`, c.T) } _, _ = buf.WriteString(`>`) + if c.F != nil { + _, _ = buf.WriteString(``) + _ = xml.EscapeText(buf, []byte(c.F.Content)) + _, _ = buf.WriteString(``) + } if c.V != "" { _, _ = buf.WriteString(``) _ = xml.EscapeText(buf, []byte(c.V)) diff --git a/stream_test.go b/stream_test.go index 7c6eb9bb16..c5febfc3c5 100644 --- a/stream_test.go +++ b/stream_test.go @@ -55,8 +55,8 @@ func TestStreamWriter(t *testing.T) { // Test set cell with style. styleID, err := file.NewStyle(`{"font":{"color":"#777777"}}`) assert.NoError(t, err) - assert.NoError(t, streamWriter.SetRow("A4", []interface{}{Cell{StyleID: styleID}})) - assert.NoError(t, streamWriter.SetRow("A5", []interface{}{&Cell{StyleID: styleID, Value: "cell"}})) + assert.NoError(t, streamWriter.SetRow("A4", []interface{}{Cell{StyleID: styleID}, Cell{Formula: "SUM(A10,B10)"}})) + assert.NoError(t, streamWriter.SetRow("A5", []interface{}{&Cell{StyleID: styleID, Value: "cell"}, &Cell{Formula: "SUM(A10,B10)"}})) assert.EqualError(t, streamWriter.SetRow("A6", []interface{}{time.Now()}), "only UTC time expected") for rowID := 10; rowID <= 51200; rowID++ { diff --git a/xmlChartSheet.go b/xmlChartSheet.go index 30a06931fa..bd3d0630e2 100644 --- a/xmlChartSheet.go +++ b/xmlChartSheet.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2021 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. // @@ -16,27 +16,27 @@ import "encoding/xml" // xlsxChartsheet directly maps the chartsheet element of Chartsheet Parts in // a SpreadsheetML document. type xlsxChartsheet struct { - XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main chartsheet"` - SheetPr []*xlsxChartsheetPr `xml:"sheetPr"` - SheetViews []*xlsxChartsheetViews `xml:"sheetViews"` - SheetProtection []*xlsxChartsheetProtection `xml:"sheetProtection"` - CustomSheetViews []*xlsxCustomChartsheetViews `xml:"customSheetViews"` - PageMargins *xlsxPageMargins `xml:"pageMargins"` - PageSetup []*xlsxPageSetUp `xml:"pageSetup"` - HeaderFooter *xlsxHeaderFooter `xml:"headerFooter"` - Drawing *xlsxDrawing `xml:"drawing"` - DrawingHF []*xlsxDrawingHF `xml:"drawingHF"` - Picture []*xlsxPicture `xml:"picture"` - WebPublishItems []*xlsxInnerXML `xml:"webPublishItems"` - ExtLst []*xlsxExtLst `xml:"extLst"` + XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main chartsheet"` + SheetPr *xlsxChartsheetPr `xml:"sheetPr"` + SheetViews *xlsxChartsheetViews `xml:"sheetViews"` + SheetProtection *xlsxChartsheetProtection `xml:"sheetProtection"` + CustomSheetViews *xlsxCustomChartsheetViews `xml:"customSheetViews"` + PageMargins *xlsxPageMargins `xml:"pageMargins"` + PageSetup *xlsxPageSetUp `xml:"pageSetup"` + HeaderFooter *xlsxHeaderFooter `xml:"headerFooter"` + Drawing *xlsxDrawing `xml:"drawing"` + DrawingHF *xlsxDrawingHF `xml:"drawingHF"` + Picture *xlsxPicture `xml:"picture"` + WebPublishItems *xlsxInnerXML `xml:"webPublishItems"` + ExtLst *xlsxExtLst `xml:"extLst"` } // 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 *xlsxTabColor `xml:"tabColor"` } // xlsxChartsheetViews specifies chart sheet views. @@ -71,13 +71,13 @@ type xlsxChartsheetProtection struct { // xlsxCustomChartsheetViews collection of custom Chart Sheet View // information. type xlsxCustomChartsheetViews struct { - XMLName xml.Name `xml:"customChartsheetViews"` + XMLName xml.Name `xml:"customSheetViews"` CustomSheetView []*xlsxCustomChartsheetView `xml:"customSheetView"` } // xlsxCustomChartsheetView defines custom view properties for chart sheets. type xlsxCustomChartsheetView struct { - XMLName xml.Name `xml:"customChartsheetView"` + XMLName xml.Name `xml:"customSheetView"` GUIDAttr string `xml:"guid,attr"` ScaleAttr uint32 `xml:"scale,attr,omitempty"` StateAttr string `xml:"state,attr,omitempty"` From b83a36a8aead4b76c2c4025283590e1afd7e500a Mon Sep 17 00:00:00 2001 From: jinhyuk-kim-ca <71794373+jinhyuk-kim-ca@users.noreply.github.com> Date: Sat, 13 Mar 2021 00:22:28 -0500 Subject: [PATCH 347/957] support ShowError option in Pivot table (#802) --- pivotTable.go | 2 ++ pivotTable_test.go | 1 + xmlPivotTable.go | 4 ++-- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/pivotTable.go b/pivotTable.go index ff21ac1d06..42a94732fc 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -34,6 +34,7 @@ type PivotTableOption struct { PageOverThenDown bool MergeItem bool CompactData bool + ShowError bool ShowRowHeaders bool ShowColHeaders bool ShowRowStripes bool @@ -308,6 +309,7 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op PageOverThenDown: &opt.PageOverThenDown, MergeItem: &opt.MergeItem, CompactData: &opt.CompactData, + ShowError: &opt.ShowError, DataCaption: "Values", Location: &xlsxLocation{ Ref: hcell + ":" + vcell, diff --git a/pivotTable_test.go b/pivotTable_test.go index 42103f3ed9..40d58d4885 100644 --- a/pivotTable_test.go +++ b/pivotTable_test.go @@ -38,6 +38,7 @@ func TestAddPivotTable(t *testing.T) { ShowRowHeaders: true, ShowColHeaders: true, ShowLastColumn: true, + ShowError: true, })) // Use different order of coordinate tests assert.NoError(t, f.AddPivotTable(&PivotTableOption{ diff --git a/xmlPivotTable.go b/xmlPivotTable.go index dc8b76538c..9c4be50780 100644 --- a/xmlPivotTable.go +++ b/xmlPivotTable.go @@ -31,7 +31,7 @@ type xlsxPivotTableDefinition struct { DataCaption string `xml:"dataCaption,attr"` GrandTotalCaption string `xml:"grandTotalCaption,attr,omitempty"` ErrorCaption string `xml:"errorCaption,attr,omitempty"` - ShowError bool `xml:"showError,attr,omitempty"` + ShowError *bool `xml:"showError,attr"` MissingCaption string `xml:"missingCaption,attr,omitempty"` ShowMissing bool `xml:"showMissing,attr,omitempty"` PageStyle string `xml:"pageStyle,attr,omitempty"` @@ -48,7 +48,7 @@ type xlsxPivotTableDefinition struct { VisualTotals bool `xml:"visualTotals,attr,omitempty"` ShowMultipleLabel bool `xml:"showMultipleLabel,attr,omitempty"` ShowDataDropDown bool `xml:"showDataDropDown,attr,omitempty"` - ShowDrill *bool `xml:"showDrill,attr,omitempty"` + ShowDrill *bool `xml:"showDrill,attr"` PrintDrill bool `xml:"printDrill,attr,omitempty"` ShowMemberPropertyTips bool `xml:"showMemberPropertyTips,attr,omitempty"` ShowDataTips bool `xml:"showDataTips,attr,omitempty"` From 2350866d460c883fbd0b3a403a62b943a5f6aca5 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 14 Mar 2021 22:29:18 +0800 Subject: [PATCH 348/957] #65 fn: NOW and TODAY, and update dependencies --- calc.go | 29 +++++++++++++++++++++++++++++ calc_test.go | 6 ++++++ go.mod | 6 +++--- go.sum | 12 ++++++------ 4 files changed, 44 insertions(+), 9 deletions(-) diff --git a/calc.go b/calc.go index 8b78e28530..ef8d88c4ad 100644 --- a/calc.go +++ b/calc.go @@ -320,6 +320,7 @@ var tokenPriority = map[string]int{ // MUNIT // NA // NOT +// NOW // OCT2BIN // OCT2DEC // OCT2HEX @@ -362,6 +363,7 @@ var tokenPriority = map[string]int{ // SUMSQ // TAN // TANH +// TODAY // TRIM // TRUE // TRUNC @@ -4647,6 +4649,33 @@ func (fn *formulaFuncs) DATE(argsList *list.List) formulaArg { return newStringFormulaArg(timeFromExcelTime(daysBetween(excelMinTime1900.Unix(), d)+1, false).String()) } +// NOW function returns the current date and time. The function receives no arguments and therefore. The syntax of the function is: +// +// NOW() +// +func (fn *formulaFuncs) NOW(argsList *list.List) formulaArg { + if argsList.Len() != 0 { + return newErrorFormulaArg(formulaErrorVALUE, "NOW accepts no arguments") + } + now := time.Now() + _, offset := now.Zone() + return newNumberFormulaArg(25569.0 + float64(now.Unix()+int64(offset))/86400) +} + +// TODAY function returns the current date. The function has no arguments and +// therefore. The syntax of the function is: +// +// TODAY() +// +func (fn *formulaFuncs) TODAY(argsList *list.List) formulaArg { + if argsList.Len() != 0 { + return newErrorFormulaArg(formulaErrorVALUE, "TODAY accepts no arguments") + } + now := time.Now() + _, offset := now.Zone() + return newNumberFormulaArg(daysBetween(excelMinTime1900.Unix(), now.Unix()+int64(offset)) + 1) +} + // makeDate return date as a Unix time, the number of seconds elapsed since // January 1, 1970 UTC. func makeDate(y int, m time.Month, d int) int64 { diff --git a/calc_test.go b/calc_test.go index 29da244d04..6b7093e578 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1436,6 +1436,10 @@ func TestCalcCellValue(t *testing.T) { `=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", + // NOW + "=NOW(A1)": "NOW accepts no arguments", + // TODAY + "=TODAY(A1)": "TODAY accepts no arguments", // Text Functions // CHAR "=CHAR()": "CHAR requires 1 argument", @@ -1658,8 +1662,10 @@ func TestCalcCellValue(t *testing.T) { } volatileFuncs := []string{ + "=NOW()", "=RAND()", "=RANDBETWEEN(1,2)", + "=TODAY()", } for _, formula := range volatileFuncs { f := prepareCalcData(cellData) diff --git a/go.mod b/go.mod index 41babe1c5e..4318ff8581 100644 --- a/go.mod +++ b/go.mod @@ -6,9 +6,9 @@ require ( github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 github.com/richardlehane/mscfb v1.0.3 github.com/stretchr/testify v1.6.1 - github.com/xuri/efp v0.0.0-20210128032744-13be4fd5dcb5 - golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad + github.com/xuri/efp v0.0.0-20210311002341-9c6784cb2d17 + golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 golang.org/x/image v0.0.0-20201208152932-35266b937fa6 - golang.org/x/net v0.0.0-20210119194325-5f4716e94777 + golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 golang.org/x/text v0.3.5 ) diff --git a/go.sum b/go.sum index 75323d6276..ee79f1c478 100644 --- a/go.sum +++ b/go.sum @@ -11,16 +11,16 @@ 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.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/xuri/efp v0.0.0-20210128032744-13be4fd5dcb5 h1:hO7we8DcWAkmZX/Voqa04Tuo84SbeXIJbdgMj92hIpA= -github.com/xuri/efp v0.0.0-20210128032744-13be4fd5dcb5/go.mod h1:uBiSUepVYMhGTfDeBKKasV4GpgBlzJ46gXUBAqV8qLk= +github.com/xuri/efp v0.0.0-20210311002341-9c6784cb2d17 h1:Ou4I7pYPQBk/qE9K2y31rawl/ftLHbTJJAFYJPVSyQo= +github.com/xuri/efp v0.0.0-20210311002341-9c6784cb2d17/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY= -golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g= +golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/image v0.0.0-20201208152932-35266b937fa6 h1:nfeHNc1nAqecKCy2FCy4HY+soOOe5sDLJ/gZLbx6GYI= golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= From 9af00b9b98daa2beca7bbf7805b88da4963a8cd1 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 15 Mar 2021 23:56:36 +0800 Subject: [PATCH 349/957] This closes #804, fixes can't add timelines and slicers for a pivot table in generated spreadsheet --- pivotTable.go | 35 +++++++++++++++++++++-------------- xmlDrawing.go | 7 ++++++- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/pivotTable.go b/pivotTable.go index 42a94732fc..9df8c6467e 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -70,7 +70,8 @@ type PivotTableField struct { } // AddPivotTable provides the method to add pivot table by given pivot table -// options. +// 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: @@ -243,8 +244,11 @@ func (f *File) addPivotCache(pivotCacheID int, pivotCacheXML string, opt *PivotT hcell, _ := CoordinatesToCellName(coordinates[0], coordinates[1]) vcell, _ := CoordinatesToCellName(coordinates[2], coordinates[3]) pc := xlsxPivotCacheDefinition{ - SaveData: false, - RefreshOnLoad: true, + SaveData: false, + RefreshOnLoad: true, + CreatedVersion: pivotTableVersion, + RefreshedVersion: pivotTableVersion, + MinRefreshableVersion: pivotTableVersion, CacheSource: &xlsxCacheSource{ Type: "worksheet", WorksheetSource: &xlsxWorksheetSource{ @@ -300,17 +304,20 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op return opt.PivotTableStyleName } pt := xlsxPivotTableDefinition{ - Name: fmt.Sprintf("Pivot Table%d", pivotTableID), - CacheID: cacheID, - RowGrandTotals: &opt.RowGrandTotals, - ColGrandTotals: &opt.ColGrandTotals, - ShowDrill: &opt.ShowDrill, - UseAutoFormatting: &opt.UseAutoFormatting, - PageOverThenDown: &opt.PageOverThenDown, - MergeItem: &opt.MergeItem, - CompactData: &opt.CompactData, - ShowError: &opt.ShowError, - DataCaption: "Values", + Name: fmt.Sprintf("Pivot Table%d", pivotTableID), + CacheID: cacheID, + RowGrandTotals: &opt.RowGrandTotals, + ColGrandTotals: &opt.ColGrandTotals, + UpdatedVersion: pivotTableVersion, + MinRefreshableVersion: pivotTableVersion, + ShowDrill: &opt.ShowDrill, + UseAutoFormatting: &opt.UseAutoFormatting, + PageOverThenDown: &opt.PageOverThenDown, + MergeItem: &opt.MergeItem, + CreatedVersion: pivotTableVersion, + CompactData: &opt.CompactData, + ShowError: &opt.ShowError, + DataCaption: "Values", Location: &xlsxLocation{ Ref: hcell + ":" + vcell, FirstDataCol: 1, diff --git a/xmlDrawing.go b/xmlDrawing.go index d2a59e189d..76d7e17c18 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2021 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. // @@ -100,6 +100,11 @@ const ( TotalColumns = 16384 TotalSheetHyperlinks = 65529 TotalCellChars = 32767 + // 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 + // PivotTables created in Excel 2007 or a newer version of Excel. + pivotTableVersion = 3 ) var supportImageTypes = map[string]string{".gif": ".gif", ".jpg": ".jpeg", ".jpeg": ".jpeg", ".png": ".png", ".tif": ".tiff", ".tiff": ".tiff"} From d3227393efb037bc13a4b5f7b150715f22761b4d Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 19 Mar 2021 23:23:05 +0800 Subject: [PATCH 350/957] #65 fn: DATEDIF --- calc.go | 90 +++++++++++++++++++++++++++++++++++++++++++++------- calc_test.go | 19 +++++++++++ 2 files changed, 98 insertions(+), 11 deletions(-) diff --git a/calc.go b/calc.go index ef8d88c4ad..55290563c0 100644 --- a/calc.go +++ b/calc.go @@ -256,6 +256,7 @@ var tokenPriority = map[string]int{ // CSC // CSCH // DATE +// DATEDIF // DEC2BIN // DEC2HEX // DEC2OCT @@ -4634,22 +4635,89 @@ func (fn *formulaFuncs) DATE(argsList *list.List) formulaArg { if argsList.Len() != 3 { return newErrorFormulaArg(formulaErrorVALUE, "DATE requires 3 number arguments") } - var year, month, day int - var err error - if year, err = strconv.Atoi(argsList.Front().Value.(formulaArg).String); err != nil { - return newErrorFormulaArg(formulaErrorVALUE, "DATE requires 3 number arguments") - } - if month, err = strconv.Atoi(argsList.Front().Next().Value.(formulaArg).String); err != nil { - return newErrorFormulaArg(formulaErrorVALUE, "DATE requires 3 number arguments") - } - if day, err = strconv.Atoi(argsList.Back().Value.(formulaArg).String); err != nil { + year := argsList.Front().Value.(formulaArg).ToNumber() + month := argsList.Front().Next().Value.(formulaArg).ToNumber() + day := argsList.Back().Value.(formulaArg).ToNumber() + if year.Type != ArgNumber || month.Type != ArgNumber || day.Type != ArgNumber { return newErrorFormulaArg(formulaErrorVALUE, "DATE requires 3 number arguments") } - d := makeDate(year, time.Month(month), day) + d := makeDate(int(year.Number), time.Month(month.Number), int(day.Number)) return newStringFormulaArg(timeFromExcelTime(daysBetween(excelMinTime1900.Unix(), d)+1, false).String()) } -// NOW function returns the current date and time. The function receives no arguments and therefore. The syntax of the function is: +// 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) +// +func (fn *formulaFuncs) DATEDIF(argsList *list.List) formulaArg { + if argsList.Len() != 3 { + return newErrorFormulaArg(formulaErrorVALUE, "DATEDIF requires 3 number arguments") + } + startArg, endArg := argsList.Front().Value.(formulaArg).ToNumber(), argsList.Front().Next().Value.(formulaArg).ToNumber() + if startArg.Type != ArgNumber || endArg.Type != ArgNumber { + return startArg + } + if startArg.Number > endArg.Number { + return newErrorFormulaArg(formulaErrorNUM, "start_date > end_date") + } + if startArg.Number == endArg.Number { + return newNumberFormulaArg(0) + } + unit := strings.ToLower(argsList.Back().Value.(formulaArg).Value()) + startDate, endDate := timeFromExcelTime(startArg.Number, false), timeFromExcelTime(endArg.Number, false) + sy, smm, sd := startDate.Date() + ey, emm, ed := endDate.Date() + sm, em, diff := int(smm), int(emm), 0.0 + switch unit { + case "d": + return newNumberFormulaArg(endArg.Number - startArg.Number) + case "y": + diff = float64(ey - sy) + if em < sm || (em == sm && ed < sd) { + diff-- + } + case "m": + ydiff := ey - sy + mdiff := em - sm + if ed < sd { + mdiff-- + } + if mdiff < 0 { + ydiff-- + mdiff += 12 + } + diff = float64(ydiff*12 + mdiff) + case "md": + smMD := em + if ed < sd { + smMD-- + } + diff = endArg.Number - daysBetween(excelMinTime1900.Unix(), makeDate(ey, time.Month(smMD), sd)) - 1 + case "ym": + diff = float64(em - sm) + if ed < sd { + diff-- + } + if diff < 0 { + diff += 12 + } + case "yd": + syYD := sy + if em < sm || (em == sm && ed < sd) { + syYD++ + } + s := daysBetween(excelMinTime1900.Unix(), makeDate(syYD, time.Month(em), ed)) + e := daysBetween(excelMinTime1900.Unix(), makeDate(sy, time.Month(sm), sd)) + diff = s - e + default: + return newErrorFormulaArg(formulaErrorVALUE, "DATEDIF has invalid unit") + } + return newNumberFormulaArg(diff) +} + +// NOW function returns the current date and time. The function receives no +// arguments and therefore. The syntax of the function is: // // NOW() // diff --git a/calc_test.go b/calc_test.go index 6b7093e578..6fc61b072b 100644 --- a/calc_test.go +++ b/calc_test.go @@ -722,6 +722,20 @@ func TestCalcCellValue(t *testing.T) { // DATE "=DATE(2020,10,21)": "2020-10-21 00:00:00 +0000 UTC", "=DATE(1900,1,1)": "1899-12-31 00:00:00 +0000 UTC", + // DATEDIF + "=DATEDIF(43101,43101,\"D\")": "0", + "=DATEDIF(43101,43891,\"d\")": "790", + "=DATEDIF(43101,43891,\"Y\")": "2", + "=DATEDIF(42156,44242,\"y\")": "5", + "=DATEDIF(43101,43891,\"M\")": "26", + "=DATEDIF(42171,44242,\"m\")": "67", + "=DATEDIF(42156,44454,\"MD\")": "14", + "=DATEDIF(42171,44242,\"md\")": "30", + "=DATEDIF(43101,43891,\"YM\")": "2", + "=DATEDIF(42171,44242,\"ym\")": "7", + "=DATEDIF(43101,43891,\"YD\")": "59", + "=DATEDIF(36526,73110,\"YD\")": "60", + "=DATEDIF(42171,44242,\"yd\")": "244", // Text Functions // CHAR "=CHAR(65)": "A", @@ -1436,6 +1450,11 @@ func TestCalcCellValue(t *testing.T) { `=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", + // 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", // NOW "=NOW(A1)": "NOW accepts no arguments", // TODAY From 874d59cee02b5fab36e7cf5e2595c473f65f6c9d Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 21 Mar 2021 23:45:36 +0800 Subject: [PATCH 351/957] related issue #65 fn: FIXED --- calc.go | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 15 +++++++++++++++ go.mod | 6 +++--- go.sum | 17 +++++++---------- 4 files changed, 77 insertions(+), 13 deletions(-) diff --git a/calc.go b/calc.go index 55290563c0..66978bd736 100644 --- a/calc.go +++ b/calc.go @@ -29,6 +29,8 @@ import ( "unsafe" "github.com/xuri/efp" + "golang.org/x/text/language" + "golang.org/x/text/message" ) // Excel formula errors @@ -273,6 +275,7 @@ var tokenPriority = map[string]int{ // FINDB // FISHER // FISHERINV +// FIXED // FLOOR // FLOOR.MATH // FLOOR.PRECISE @@ -4883,6 +4886,55 @@ func (fn *formulaFuncs) EXACT(argsList *list.List) formulaArg { return newBoolFormulaArg(text1 == text2) } +// 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]) +// +func (fn *formulaFuncs) FIXED(argsList *list.List) formulaArg { + if argsList.Len() < 1 { + return newErrorFormulaArg(formulaErrorVALUE, "FIXED requires at least 1 argument") + } + if argsList.Len() > 3 { + return newErrorFormulaArg(formulaErrorVALUE, "FIXED allows at most 3 arguments") + } + numArg := argsList.Front().Value.(formulaArg).ToNumber() + if numArg.Type != ArgNumber { + return numArg + } + precision, decimals, noCommas := 0, 0, false + s := strings.Split(argsList.Front().Value.(formulaArg).Value(), ".") + if argsList.Len() == 1 && len(s) == 2 { + precision = len(s[1]) + decimals = len(s[1]) + } + if argsList.Len() >= 2 { + decimalsArg := argsList.Front().Next().Value.(formulaArg).ToNumber() + if decimalsArg.Type != ArgNumber { + return decimalsArg + } + decimals = int(decimalsArg.Number) + } + if argsList.Len() == 3 { + noCommasArg := argsList.Back().Value.(formulaArg).ToBool() + if noCommasArg.Type == ArgError { + return noCommasArg + } + noCommas = noCommasArg.Boolean + } + n := math.Pow(10, float64(decimals)) + r := numArg.Number * n + fixed := float64(int(r+math.Copysign(0.5, r))) / n + if decimals > 0 { + precision = decimals + } + if noCommas { + return newStringFormulaArg(fmt.Sprintf(fmt.Sprintf("%%.%df", precision), fixed)) + } + p := message.NewPrinter(language.English) + return newStringFormulaArg(p.Sprintf(fmt.Sprintf("%%.%df", precision), fixed)) +} + // FIND function returns the position of a specified character or sub-string // within a supplied text string. The function is case-sensitive. The syntax // of the function is: diff --git a/calc_test.go b/calc_test.go index 6fc61b072b..d6a15c9170 100644 --- a/calc_test.go +++ b/calc_test.go @@ -759,6 +759,15 @@ func TestCalcCellValue(t *testing.T) { "=EXACT(1,\"1\")": "TRUE", "=EXACT(1,1)": "TRUE", "=EXACT(\"A\",\"a\")": "FALSE", + // FIXED + "=FIXED(5123.591)": "5,123.591", + "=FIXED(5123.591,1)": "5,123.6", + "=FIXED(5123.591,0)": "5,124", + "=FIXED(5123.591,-1)": "5,120", + "=FIXED(5123.591,-2)": "5,100", + "=FIXED(5123.591,-3,TRUE)": "5000", + "=FIXED(5123.591,-5)": "0", + "=FIXED(-77262.23973,-5)": "-100,000", // FIND "=FIND(\"T\",\"Original Text\")": "10", "=FIND(\"t\",\"Original Text\")": "13", @@ -1478,6 +1487,12 @@ func TestCalcCellValue(t *testing.T) { // EXACT "=EXACT()": "EXACT requires 2 arguments", "=EXACT(1,2,3)": "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", // FIND "=FIND()": "FIND requires at least 2 arguments", "=FIND(1,2,3,4)": "FIND allows at most 3 arguments", diff --git a/go.mod b/go.mod index 4318ff8581..01e837ac3c 100644 --- a/go.mod +++ b/go.mod @@ -7,8 +7,8 @@ require ( github.com/richardlehane/mscfb v1.0.3 github.com/stretchr/testify v1.6.1 github.com/xuri/efp v0.0.0-20210311002341-9c6784cb2d17 - golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 - golang.org/x/image v0.0.0-20201208152932-35266b937fa6 - golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 + golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670 + golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb + golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4 golang.org/x/text v0.3.5 ) diff --git a/go.sum b/go.sum index ee79f1c478..4dfc806ef7 100644 --- a/go.sum +++ b/go.sum @@ -13,18 +13,15 @@ github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/xuri/efp v0.0.0-20210311002341-9c6784cb2d17 h1:Ou4I7pYPQBk/qE9K2y31rawl/ftLHbTJJAFYJPVSyQo= github.com/xuri/efp v0.0.0-20210311002341-9c6784cb2d17/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g= -golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/image v0.0.0-20201208152932-35266b937fa6 h1:nfeHNc1nAqecKCy2FCy4HY+soOOe5sDLJ/gZLbx6GYI= -golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= +golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670 h1:gzMM0EjIYiRmJI3+jBdFuoynZlpxa2JQZsolKu09BXo= +golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb h1:fqpd0EBDzlHRCjiphRR5Zo/RSWWQlWv34418dnEixWk= +golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4 h1:b0LrWgu8+q7z4J+0Y3Umo5q1dL7NXBkKBWkaVkAq17E= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 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= From d08a6d243761a6214f8fef3181b689251bb72de0 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 21 Mar 2021 19:39:36 -0400 Subject: [PATCH 352/957] updated SetDefinedName's localSheetId attr to use sheetIndex Excelize 2.3.2 OUT: ``` ap-T-QP-11!$2:$5 R-T-QP-11!$2:$13 ``` MS Excel 2010 out ``` 'ap-T-QP-11'!$2:$5 'R-T-QP-11'!$2:$13 ``` Compare localSheetId it uses sheet index instead of sheet's sheetId --- sheet.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/sheet.go b/sheet.go index 087364475c..5a9fd46ed1 100644 --- a/sheet.go +++ b/sheet.go @@ -1487,9 +1487,8 @@ func (f *File) SetDefinedName(definedName *DefinedName) error { Data: definedName.RefersTo, } if definedName.Scope != "" { - if sheetID := f.getSheetID(definedName.Scope); sheetID != 0 { - sheetID-- - d.LocalSheetID = &sheetID + if sheetIndex := f.GetSheetIndex(definedName.Scope); sheetIndex >= 0 { + d.LocalSheetID = &sheetIndex } } if wb.DefinedNames != nil { From ab2c1c8fe1ef787c1f692bcf26c7bbe0cca2c5b3 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 24 Mar 2021 08:24:59 +0800 Subject: [PATCH 353/957] #64 fn: NORM.DIST, NORMDIST, NORM.INV, NORMINV, NORM.S.DIST, NORMSDIST, NORM.S.INV, and NORMSINV --- calc.go | 241 +++++++++++++++++++++++++++++++++++++++++++++++---- calc_test.go | 48 ++++++++++ go.mod | 4 +- go.sum | 8 +- 4 files changed, 279 insertions(+), 22 deletions(-) diff --git a/calc.go b/calc.go index 66978bd736..8be52c76a8 100644 --- a/calc.go +++ b/calc.go @@ -323,6 +323,14 @@ var tokenPriority = map[string]int{ // MULTINOMIAL // MUNIT // NA +// NORM.DIST +// NORMDIST +// NORM.INV +// NORMINV +// NORM.S.DIST +// NORMSDIST +// NORM.S.INV +// NORMSINV // NOT // NOW // OCT2BIN @@ -599,7 +607,7 @@ func (f *File) evalInfixExpFunc(sheet, cell string, token, nextToken efp.Token, } // call formula function to evaluate arg := callFuncByName(&formulaFuncs{f: f, sheet: sheet, cell: cell}, strings.NewReplacer( - "_xlfn", "", ".", "").Replace(opfStack.Peek().(efp.Token).TValue), + "_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()) @@ -1922,12 +1930,12 @@ func (fn *formulaFuncs) CEILING(argsList *list.List) formulaArg { return newNumberFormulaArg(number * significance) } -// CEILINGMATH function rounds a supplied number up to a supplied multiple of -// significance. The syntax of the function is: +// CEILINGdotMATH function rounds a supplied number up to a supplied multiple +// of significance. The syntax of the function is: // // CEILING.MATH(number,[significance],[mode]) // -func (fn *formulaFuncs) CEILINGMATH(argsList *list.List) formulaArg { +func (fn *formulaFuncs) CEILINGdotMATH(argsList *list.List) formulaArg { if argsList.Len() == 0 { return newErrorFormulaArg(formulaErrorVALUE, "CEILING.MATH requires at least 1 argument") } @@ -1971,13 +1979,13 @@ func (fn *formulaFuncs) CEILINGMATH(argsList *list.List) formulaArg { return newNumberFormulaArg(val * significance) } -// CEILINGPRECISE function rounds a supplied number up (regardless of the +// CEILINGdotPRECISE function rounds a supplied number up (regardless of the // number's sign), to the nearest multiple of a given number. The syntax of // the function is: // // CEILING.PRECISE(number,[significance]) // -func (fn *formulaFuncs) CEILINGPRECISE(argsList *list.List) formulaArg { +func (fn *formulaFuncs) CEILINGdotPRECISE(argsList *list.List) formulaArg { if argsList.Len() == 0 { return newErrorFormulaArg(formulaErrorVALUE, "CEILING.PRECISE requires at least 1 argument") } @@ -2365,12 +2373,12 @@ func (fn *formulaFuncs) FLOOR(argsList *list.List) formulaArg { return newStringFormulaArg(strings.ToUpper(fmt.Sprintf("%g", val*significance.Number))) } -// FLOORMATH function rounds a supplied number down to a supplied multiple of -// significance. The syntax of the function is: +// FLOORdotMATH function rounds a supplied number down to a supplied multiple +// of significance. The syntax of the function is: // // FLOOR.MATH(number,[significance],[mode]) // -func (fn *formulaFuncs) FLOORMATH(argsList *list.List) formulaArg { +func (fn *formulaFuncs) FLOORdotMATH(argsList *list.List) formulaArg { if argsList.Len() == 0 { return newErrorFormulaArg(formulaErrorVALUE, "FLOOR.MATH requires at least 1 argument") } @@ -2409,12 +2417,12 @@ func (fn *formulaFuncs) FLOORMATH(argsList *list.List) formulaArg { return newNumberFormulaArg(val * significance) } -// FLOORPRECISE function rounds a supplied number down to a supplied multiple -// of significance. The syntax of the function is: +// FLOORdotPRECISE function rounds a supplied number down to a supplied +// multiple of significance. The syntax of the function is: // // FLOOR.PRECISE(number,[significance]) // -func (fn *formulaFuncs) FLOORPRECISE(argsList *list.List) formulaArg { +func (fn *formulaFuncs) FLOORdotPRECISE(argsList *list.List) formulaArg { if argsList.Len() == 0 { return newErrorFormulaArg(formulaErrorVALUE, "FLOOR.PRECISE requires at least 1 argument") } @@ -2534,13 +2542,13 @@ func (fn *formulaFuncs) INT(argsList *list.List) formulaArg { return newNumberFormulaArg(val) } -// ISOCEILING function rounds a supplied number up (regardless of the number's -// sign), to the nearest multiple of a supplied significance. The syntax of -// the function is: +// ISOdotCEILING function rounds a supplied number up (regardless of the +// number's sign), to the nearest multiple of a supplied significance. The +// syntax of the function is: // // ISO.CEILING(number,[significance]) // -func (fn *formulaFuncs) ISOCEILING(argsList *list.List) formulaArg { +func (fn *formulaFuncs) ISOdotCEILING(argsList *list.List) formulaArg { if argsList.Len() == 0 { return newErrorFormulaArg(formulaErrorVALUE, "ISO.CEILING requires at least 1 argument") } @@ -3961,6 +3969,207 @@ func (fn *formulaFuncs) KURT(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV) } +// 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: +// +// 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") + } + return fn.NORMDIST(argsList) +} + +// NORMDIST 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: +// +// 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") + } + 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 stdDev.Number < 0 { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + } + if cumulative.Number == 1 { + return newNumberFormulaArg(0.5 * (1 + math.Erf((x.Number-mean.Number)/(stdDev.Number*math.Sqrt(2))))) + } + return newNumberFormulaArg((1 / (math.Sqrt(2*math.Pi) * stdDev.Number)) * math.Exp(0-(math.Pow(x.Number-mean.Number, 2)/(2*(stdDev.Number*stdDev.Number))))) +} + +// NORMdotINV function calculates the inverse of the Cumulative Normal +// 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) +// +func (fn *formulaFuncs) NORMdotINV(argsList *list.List) formulaArg { + if argsList.Len() != 3 { + return newErrorFormulaArg(formulaErrorVALUE, "NORM.INV requires 3 arguments") + } + return fn.NORMINV(argsList) +} + +// NORMINV function calculates the inverse of the Cumulative Normal +// 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) +// +func (fn *formulaFuncs) NORMINV(argsList *list.List) formulaArg { + if argsList.Len() != 3 { + return newErrorFormulaArg(formulaErrorVALUE, "NORMINV requires 3 arguments") + } + var prob, mean, stdDev formulaArg + if prob = argsList.Front().Value.(formulaArg).ToNumber(); prob.Type != ArgNumber { + return prob + } + 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 prob.Number < 0 || prob.Number > 1 { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + } + if stdDev.Number < 0 { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + } + inv, err := norminv(prob.Number) + if err != nil { + return newErrorFormulaArg(err.Error(), err.Error()) + } + return newNumberFormulaArg(inv*stdDev.Number + mean.Number) +} + +// NORMdotSdotDIST function calculates the Standard Normal Cumulative +// Distribution Function for a supplied value. The syntax of the function +// is: +// +// 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") + } + args := list.New().Init() + args.PushBack(argsList.Front().Value.(formulaArg)) + args.PushBack(formulaArg{Type: ArgNumber, Number: 0}) + args.PushBack(formulaArg{Type: ArgNumber, Number: 1}) + args.PushBack(argsList.Back().Value.(formulaArg)) + return fn.NORMDIST(args) +} + +// NORMSDIST function calculates the Standard Normal Cumulative Distribution +// Function for a supplied value. The syntax of the function is: +// +// NORMSDIST(z) +// +func (fn *formulaFuncs) NORMSDIST(argsList *list.List) formulaArg { + if argsList.Len() != 1 { + return newErrorFormulaArg(formulaErrorVALUE, "NORMSDIST requires 1 numeric argument") + } + args := list.New().Init() + args.PushBack(argsList.Front().Value.(formulaArg)) + args.PushBack(formulaArg{Type: ArgNumber, Number: 0}) + args.PushBack(formulaArg{Type: ArgNumber, Number: 1}) + args.PushBack(formulaArg{Type: ArgNumber, Number: 1, Boolean: true}) + return fn.NORMDIST(args) +} + +// NORMSINV function calculates the inverse of the Standard Normal Cumulative +// Distribution Function for a supplied probability value. The syntax of the +// function is: +// +// NORMSINV(probability) +// +func (fn *formulaFuncs) NORMSINV(argsList *list.List) formulaArg { + if argsList.Len() != 1 { + return newErrorFormulaArg(formulaErrorVALUE, "NORMSINV requires 1 numeric argument") + } + args := list.New().Init() + args.PushBack(argsList.Front().Value.(formulaArg)) + args.PushBack(formulaArg{Type: ArgNumber, Number: 0}) + args.PushBack(formulaArg{Type: ArgNumber, Number: 1}) + return fn.NORMINV(args) +} + +// NORMdotSdotINV function calculates the inverse of the Standard Normal +// Cumulative Distribution Function for a supplied probability value. The +// syntax of the function is: +// +// 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") + } + args := list.New().Init() + args.PushBack(argsList.Front().Value.(formulaArg)) + args.PushBack(formulaArg{Type: ArgNumber, Number: 0}) + args.PushBack(formulaArg{Type: ArgNumber, Number: 1}) + return fn.NORMINV(args) +} + +// norminv returns the inverse of the normal cumulative distribution for the +// specified value. +func norminv(p float64) (float64, error) { + a := map[int]float64{ + 1: -3.969683028665376e+01, 2: 2.209460984245205e+02, 3: -2.759285104469687e+02, + 4: 1.383577518672690e+02, 5: -3.066479806614716e+01, 6: 2.506628277459239e+00, + } + b := map[int]float64{ + 1: -5.447609879822406e+01, 2: 1.615858368580409e+02, 3: -1.556989798598866e+02, + 4: 6.680131188771972e+01, 5: -1.328068155288572e+01, + } + c := map[int]float64{ + 1: -7.784894002430293e-03, 2: -3.223964580411365e-01, 3: -2.400758277161838e+00, + 4: -2.549732539343734e+00, 5: 4.374664141464968e+00, 6: 2.938163982698783e+00, + } + d := map[int]float64{ + 1: 7.784695709041462e-03, 2: 3.224671290700398e-01, 3: 2.445134137142996e+00, + 4: 3.754408661907416e+00, + } + pLow := 0.02425 // Use lower region approx. below this + pHigh := 1 - pLow // Use upper region approx. above this + if 0 < p && p < pLow { + // Rational approximation for lower region. + q := math.Sqrt(-2 * math.Log(p)) + return (((((c[1]*q+c[2])*q+c[3])*q+c[4])*q+c[5])*q + c[6]) / + ((((d[1]*q+d[2])*q+d[3])*q+d[4])*q + 1), nil + } else if pLow <= p && p <= pHigh { + // 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 + } else if pHigh < p && p < 1 { + // Rational approximation for upper region. + q := math.Sqrt(-2 * math.Log(1-p)) + return -(((((c[1]*q+c[2])*q+c[3])*q+c[4])*q+c[5])*q + c[6]) / + ((((d[1]*q+d[2])*q+d[3])*q+d[4])*q + 1), nil + } + return 0, errors.New(formulaErrorNUM) +} + // kth is an implementation of the formula function LARGE and SMALL. func (fn *formulaFuncs) kth(name string, argsList *list.List) formulaArg { if argsList.Len() != 2 { diff --git a/calc_test.go b/calc_test.go index d6a15c9170..f8397e77ad 100644 --- a/calc_test.go +++ b/calc_test.go @@ -609,6 +609,27 @@ func TestCalcCellValue(t *testing.T) { "=KURT(F1:F9)": "-1.033503502551368", "=KURT(F1,F2:F9)": "-1.033503502551368", "=KURT(INT(1),MUNIT(2))": "-3.333333333333336", + // NORM.DIST + "=NORM.DIST(0.8,1,0.3,TRUE)": "0.252492537546923", + "=NORM.DIST(50,40,20,FALSE)": "0.017603266338215", + // NORMDIST + "=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", + // NORMINV + "=NORMINV(0.6,5,2)": "5.506694205719997", + "=NORMINV(0.99,40,1.5)": "43.489521811582044", + "=NORMINV(0.02,40,1.5)": "36.91937663649545", + // NORM.S.DIST + "=NORM.S.DIST(0.8,TRUE)": "0.788144601416603", + // NORMSDIST + "=NORMSDIST(1.333333)": "0.908788725604095", + "=NORMSDIST(0)": "0.5", + // NORM.S.INV + "=NORM.S.INV(0.25)": "-0.674489750223423", + // NORMSINV + "=NORMSINV(0.25)": "-0.674489750223423", // LARGE "=LARGE(A1:A5,1)": "3", "=LARGE(A1:B5,2)": "4", @@ -1375,6 +1396,33 @@ func TestCalcCellValue(t *testing.T) { // KURT "=KURT()": "KURT requires at least 1 argument", "=KURT(F1,INT(1))": "#DIV/0!", + // NORM.DIST + "=NORM.DIST()": "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", + // NORM.INV + "=NORM.INV()": "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!", + // NORM.S.DIST + "=NORM.S.DIST()": "NORM.S.DIST requires 2 numeric arguments", + // NORMSDIST + "=NORMSDIST()": "NORMSDIST requires 1 numeric argument", + // NORM.S.INV + "=NORM.S.INV()": "NORM.S.INV requires 1 numeric argument", + // NORMSINV + "=NORMSINV()": "NORMSINV requires 1 numeric argument", // LARGE "=LARGE()": "LARGE requires 2 arguments", "=LARGE(A1:A5,0)": "k should be > 0", diff --git a/go.mod b/go.mod index 01e837ac3c..8beb465fa4 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.3 github.com/stretchr/testify v1.6.1 - github.com/xuri/efp v0.0.0-20210311002341-9c6784cb2d17 - golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670 + github.com/xuri/efp v0.0.0-20210322160811-ab561f5b45e3 + golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4 golang.org/x/text v0.3.5 diff --git a/go.sum b/go.sum index 4dfc806ef7..5b2a1a3521 100644 --- a/go.sum +++ b/go.sum @@ -11,10 +11,10 @@ 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.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/xuri/efp v0.0.0-20210311002341-9c6784cb2d17 h1:Ou4I7pYPQBk/qE9K2y31rawl/ftLHbTJJAFYJPVSyQo= -github.com/xuri/efp v0.0.0-20210311002341-9c6784cb2d17/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= -golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670 h1:gzMM0EjIYiRmJI3+jBdFuoynZlpxa2JQZsolKu09BXo= -golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +github.com/xuri/efp v0.0.0-20210322160811-ab561f5b45e3 h1:EpI0bqf/eX9SdZDwlMmahKM+CDBgNbsXMhsN28XrM8o= +github.com/xuri/efp v0.0.0-20210322160811-ab561f5b45e3/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb h1:fqpd0EBDzlHRCjiphRR5Zo/RSWWQlWv34418dnEixWk= golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= From e1abdb0e5a0ecae47dd0116c9165f4ad6e492856 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 25 Mar 2021 00:05:02 +0800 Subject: [PATCH 354/957] This closes #809, and add new fn: HARMEAN --- calc.go | 35 +++++++++++++++++++++++++++++++++++ calc_test.go | 7 +++++++ styles.go | 4 ++-- xmlStyles.go | 4 ++-- 4 files changed, 46 insertions(+), 4 deletions(-) diff --git a/calc.go b/calc.go index 8be52c76a8..04d0f8b965 100644 --- a/calc.go +++ b/calc.go @@ -282,6 +282,7 @@ var tokenPriority = map[string]int{ // GAMMA // GAMMALN // GCD +// HARMEAN // HEX2BIN // HEX2DEC // HEX2OCT @@ -3927,6 +3928,40 @@ func (fn *formulaFuncs) GAMMALN(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorVALUE, "GAMMALN requires 1 numeric argument") } +// HARMEAN function calculates the harmonic mean of a supplied set of values. +// The syntax of the function is: +// +// HARMEAN(number1,[number2],...) +// +func (fn *formulaFuncs) HARMEAN(argsList *list.List) formulaArg { + if argsList.Len() < 1 { + return newErrorFormulaArg(formulaErrorVALUE, "HARMEAN requires at least 1 argument") + } + if min := fn.MIN(argsList); min.Number < 0 { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + } + number, val, cnt := 0.0, 0.0, 0.0 + for token := argsList.Front(); token != nil; token = token.Next() { + arg := token.Value.(formulaArg) + switch arg.Type { + case ArgString: + num := arg.ToNumber() + if num.Type != ArgNumber { + continue + } + number = num.Number + case ArgNumber: + number = arg.Number + } + if number <= 0 { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + } + val += (1 / number) + cnt++ + } + return newNumberFormulaArg(1 / (val / cnt)) +} + // 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 f8397e77ad..65f7ce143a 100644 --- a/calc_test.go +++ b/calc_test.go @@ -605,6 +605,9 @@ func TestCalcCellValue(t *testing.T) { // GAMMALN "=GAMMALN(4.5)": "2.453736570842443", "=GAMMALN(INT(1))": "0", + // HARMEAN + "=HARMEAN(2.5,3,0.5,1,3)": "1.229508196721312", + "=HARMEAN(\"2.5\",3,0.5,1,INT(3),\"\")": "1.229508196721312", // KURT "=KURT(F1:F9)": "-1.033503502551368", "=KURT(F1,F2:F9)": "-1.033503502551368", @@ -1393,6 +1396,10 @@ func TestCalcCellValue(t *testing.T) { "=GAMMALN(F1)": "GAMMALN requires 1 numeric argument", "=GAMMALN(0)": "#N/A", "=GAMMALN(INT(0))": "#N/A", + // HARMEAN + "=HARMEAN()": "HARMEAN requires at least 1 argument", + "=HARMEAN(-1)": "#N/A", + "=HARMEAN(0)": "#N/A", // KURT "=KURT()": "KURT requires at least 1 argument", "=KURT(F1,INT(1))": "#DIV/0!", diff --git a/styles.go b/styles.go index 06215f36bc..9e37239380 100644 --- a/styles.go +++ b/styles.go @@ -2472,8 +2472,8 @@ func newAlignment(style *Style) *xlsxAlignment { func newProtection(style *Style) *xlsxProtection { var protection xlsxProtection if style.Protection != nil { - protection.Hidden = style.Protection.Hidden - protection.Locked = style.Protection.Locked + protection.Hidden = &style.Protection.Hidden + protection.Locked = &style.Protection.Locked } return &protection } diff --git a/xmlStyles.go b/xmlStyles.go index db85b15965..0670b59450 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -49,8 +49,8 @@ type xlsxAlignment struct { // set. The cell protection properties do not take effect unless the sheet has // been protected. type xlsxProtection struct { - Hidden bool `xml:"hidden,attr"` - Locked bool `xml:"locked,attr"` + Hidden *bool `xml:"hidden,attr"` + Locked *bool `xml:"locked,attr"` } // xlsxLine expresses a single set of cell border. From 3903c106a4391d408fb9f72713038f7e4e6367de Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 26 Mar 2021 00:17:29 +0800 Subject: [PATCH 355/957] #65, new fn: POISSON.DIST, POISSON, SKEW, and STDEV.S --- calc.go | 100 +++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 26 ++++++++++++++ 2 files changed, 126 insertions(+) diff --git a/calc.go b/calc.go index 04d0f8b965..631d52f119 100644 --- a/calc.go +++ b/calc.go @@ -341,6 +341,8 @@ var tokenPriority = map[string]int{ // OR // PERMUT // PI +// POISSON.DIST +// POISSON // POWER // PRODUCT // PROPER @@ -365,10 +367,12 @@ var tokenPriority = map[string]int{ // SIGN // SIN // SINH +// SKEW // SMALL // SQRT // SQRTPI // STDEV +// STDEV.S // STDEVA // SUBSTITUTE // SUM @@ -3396,6 +3400,18 @@ func (fn *formulaFuncs) STDEV(argsList *list.List) formulaArg { return fn.stdev(false, argsList) } +// STDEVdotS function calculates the sample standard deviation of a supplied +// set of values. The syntax of the function is: +// +// 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") + } + return fn.stdev(false, argsList) +} + // STDEVA function estimates standard deviation based on a sample. The // standard deviation is a measure of how widely values are dispersed from // the average value (the mean). The syntax of the function is: @@ -3472,6 +3488,53 @@ func (fn *formulaFuncs) stdev(stdeva bool, argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV) } +// POISSONdotDIST function calculates the Poisson Probability Mass Function or +// the Cumulative Poisson Probability Function for a supplied set of +// parameters. The syntax of the function is: +// +// 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") + } + return fn.POISSON(argsList) +} + +// POISSON function calculates the Poisson Probability Mass Function or the +// Cumulative Poisson Probability Function for a supplied set of parameters. +// The syntax of the function is: +// +// POISSON(x,mean,cumulative) +// +func (fn *formulaFuncs) POISSON(argsList *list.List) formulaArg { + if argsList.Len() != 3 { + return newErrorFormulaArg(formulaErrorVALUE, "POISSON requires 3 arguments") + } + var x, mean, 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 cumulative = argsList.Back().Value.(formulaArg).ToBool(); cumulative.Type == ArgError { + return cumulative + } + if x.Number < 0 || mean.Number <= 0 { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + } + if cumulative.Number == 1 { + summer := 0.0 + floor := math.Floor(x.Number) + for i := 0; i <= int(floor); i++ { + summer += math.Pow(mean.Number, float64(i)) / fact(float64(i)) + } + return newNumberFormulaArg(math.Exp(0-mean.Number) * summer) + } + return newNumberFormulaArg(math.Exp(0-mean.Number) * math.Pow(mean.Number, x.Number) / fact(x.Number)) +} + // SUM function adds together a supplied set of numbers and returns the sum of // these values. The syntax of the function is: // @@ -4479,6 +4542,43 @@ func (fn *formulaFuncs) PERMUT(argsList *list.List) formulaArg { return newNumberFormulaArg(math.Round(fact(number.Number) / fact(number.Number-chosen.Number))) } +// 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 { + if argsList.Len() < 1 { + return newErrorFormulaArg(formulaErrorVALUE, "SKEW requires at least 1 argument") + } + 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 { + case ArgNumber, ArgString: + num := token.ToNumber() + if num.Type == ArgError { + return num + } + 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 { + continue + } + summer += math.Pow((numArg.Number-mean.Number)/stdDev.Number, 3) + count++ + } + } + } + if count > 2 { + return newNumberFormulaArg(summer * (count / ((count - 1) * (count - 2)))) + } + return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV) +} + // 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 65f7ce143a..c18683c63a 100644 --- a/calc_test.go +++ b/calc_test.go @@ -509,10 +509,18 @@ func TestCalcCellValue(t *testing.T) { "=STDEV(MUNIT(2))": "0.577350269189626", "=STDEV(0,INT(0))": "0", "=STDEV(INT(1),INT(1))": "0", + // STDEV.S + "=STDEV.S(F2:F9)": "10724.978287523809", // STDEVA "=STDEVA(F2:F9)": "10724.978287523809", "=STDEVA(MUNIT(2))": "0.577350269189626", "=STDEVA(0,INT(0))": "0", + // POISSON.DIST + "=POISSON.DIST(20,25,FALSE)": "0.051917468608491", + "=POISSON.DIST(35,40,TRUE)": "0.242414197690103", + // POISSON + "=POISSON(20,25,FALSE)": "0.051917468608491", + "=POISSON(35,40,TRUE)": "0.242414197690103", // SUM "=SUM(1,2)": "3", `=SUM("",1,2)`: "3", @@ -676,6 +684,10 @@ func TestCalcCellValue(t *testing.T) { "=PERMUT(6,6)": "720", "=PERMUT(7,6)": "5040", "=PERMUT(10,6)": "151200", + // SKEW + "=SKEW(1,2,3,4,3)": "-0.404796008910937", + "=SKEW(A1:B2)": "0", + "=SKEW(A1:D3)": "0", // SMALL "=SMALL(A1:A5,1)": "0", "=SMALL(A1:B5,2)": "1", @@ -1345,9 +1357,19 @@ func TestCalcCellValue(t *testing.T) { // STDEV "=STDEV()": "STDEV requires at least 1 argument", "=STDEV(E2:E9)": "#DIV/0!", + // STDEV.S + "=STDEV.S()": "STDEV.S requires at least 1 argument", // STDEVA "=STDEVA()": "STDEVA requires at least 1 argument", "=STDEVA(E2:E9)": "#DIV/0!", + // POISSON.DIST + "=POISSON.DIST()": "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", // SUM "=SUM((": "formula not valid", "=SUM(-)": "formula not valid", @@ -1456,6 +1478,10 @@ func TestCalcCellValue(t *testing.T) { "=PERMUT(\"\",0)": "strconv.ParseFloat: parsing \"\": invalid syntax", "=PERMUT(0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", "=PERMUT(6,8)": "#N/A", + // SKEW + "=SKEW()": "SKEW requires at least 1 argument", + "=SKEW(\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=SKEW(0)": "#DIV/0!", // SMALL "=SMALL()": "SMALL requires 2 arguments", "=SMALL(A1:A5,0)": "k should be > 0", From 6d7bd7cd8aaeba3a0969b6f035f0a5a53209a184 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 27 Mar 2021 00:08:55 +0800 Subject: [PATCH 356/957] #65 fn: PERCENTILE and PERMUTATIONA --- calc.go | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 17 +++++++++++++ 2 files changed, 84 insertions(+) diff --git a/calc.go b/calc.go index 631d52f119..698c51dcbb 100644 --- a/calc.go +++ b/calc.go @@ -339,7 +339,9 @@ var tokenPriority = map[string]int{ // OCT2HEX // ODD // OR +// PERCENTILE // PERMUT +// PERMUTATIONA // PI // POISSON.DIST // POISSON @@ -4519,6 +4521,46 @@ func (fn *formulaFuncs) min(mina bool, argsList *list.List) formulaArg { return newNumberFormulaArg(min) } +// PERCENTILE 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. The syntax of the function is: +// +// PERCENTILE(array,k) +// +func (fn *formulaFuncs) PERCENTILE(argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, "PERCENTILE requires 2 arguments") + } + array := argsList.Front().Value.(formulaArg).ToList() + k := argsList.Back().Value.(formulaArg).ToNumber() + if k.Type != ArgNumber { + return k + } + if k.Number < 0 || k.Number > 1 { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + } + numbers := []float64{} + for _, arg := range array { + if arg.Type == ArgError { + return arg + } + num := arg.ToNumber() + if num.Type == ArgNumber { + numbers = append(numbers, num.Number) + } + } + cnt := len(numbers) + sort.Float64s(numbers) + idx := k.Number * (float64(cnt) - 1) + base := math.Floor(idx) + if idx == base { + return newNumberFormulaArg(numbers[int(idx)]) + } + next := base + 1 + proportion := idx - base + return newNumberFormulaArg(numbers[int(base)] + ((numbers[int(next)] - numbers[int(base)]) * proportion)) +} + // PERMUT function calculates the number of permutations of a specified number // of objects from a set of objects. The syntax of the function is: // @@ -4542,6 +4584,31 @@ func (fn *formulaFuncs) PERMUT(argsList *list.List) formulaArg { return newNumberFormulaArg(math.Round(fact(number.Number) / fact(number.Number-chosen.Number))) } +// PERMUTATIONA function calculates the number of permutations, with +// repetitions, of a specified number of objects from a set. The syntax of +// the function is: +// +// PERMUTATIONA(number,number_chosen) +// +func (fn *formulaFuncs) PERMUTATIONA(argsList *list.List) formulaArg { + if argsList.Len() < 1 { + return newErrorFormulaArg(formulaErrorVALUE, "PERMUTATIONA requires 2 numeric arguments") + } + number := argsList.Front().Value.(formulaArg).ToNumber() + chosen := argsList.Back().Value.(formulaArg).ToNumber() + if number.Type != ArgNumber { + return number + } + if chosen.Type != ArgNumber { + return chosen + } + num, numChosen := math.Floor(number.Number), math.Floor(chosen.Number) + if num < 0 || numChosen < 0 { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + } + return newNumberFormulaArg(math.Pow(num, numChosen)) +} + // 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 c18683c63a..1253ae07d8 100644 --- a/calc_test.go +++ b/calc_test.go @@ -680,10 +680,16 @@ func TestCalcCellValue(t *testing.T) { "=MINA(MUNIT(2))": "0", "=MINA(INT(1))": "1", "=MINA(A1:B4,MUNIT(1),INT(0),1,E1:F2,\"\")": "0", + // PERCENTILE + "=PERCENTILE(A1:A4,0.2)": "0.6", + "=PERCENTILE(0,0)": "0", // PERMUT "=PERMUT(6,6)": "720", "=PERMUT(7,6)": "5040", "=PERMUT(10,6)": "151200", + // PERMUTATIONA + "=PERMUTATIONA(6,6)": "46656", + "=PERMUTATIONA(7,6)": "117649", // SKEW "=SKEW(1,2,3,4,3)": "-0.404796008910937", "=SKEW(A1:B2)": "0", @@ -1473,11 +1479,22 @@ func TestCalcCellValue(t *testing.T) { // MINA "=MINA()": "MINA requires at least 1 argument", "=MINA(NA())": "#N/A", + // PERCENTILE + "=PERCENTILE()": "PERCENTILE requires 2 arguments", + "=PERCENTILE(0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=PERCENTILE(0,-1)": "#N/A", + "=PERCENTILE(NA(),1)": "#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", + // 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", // SKEW "=SKEW()": "SKEW requires at least 1 argument", "=SKEW(\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", From 2af96c07149e2b79a06375bdc735c0a8c1f6646e Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 30 Mar 2021 23:02:22 +0800 Subject: [PATCH 357/957] #65 fn: N, PERCENTILE.INC and T typo fixed --- adjust.go | 2 +- calc.go | 60 +++++++++++++++++++++++++++++++++++++++++- calc_test.go | 21 ++++++++++++++- calcchain.go | 4 +-- cell.go | 2 +- chart.go | 2 +- col.go | 2 +- comment.go | 4 +-- comment_test.go | 2 +- crypt_test.go | 2 +- datavalidation.go | 4 +-- datavalidation_test.go | 2 +- date.go | 4 +-- docProps.go | 4 +-- docProps_test.go | 14 +++++----- drawing.go | 2 +- drawing_test.go | 12 +++++---- errors.go | 4 +-- excelize.go | 2 +- excelize_test.go | 8 +++--- file.go | 2 +- lib.go | 2 +- merge.go | 4 +-- picture.go | 2 +- picture_test.go | 2 +- pivotTable.go | 2 +- rows.go | 2 +- shape.go | 4 +-- sheet.go | 2 +- sheetpr.go | 4 +-- sheetview.go | 4 +-- sparkline.go | 4 +-- sparkline_test.go | 2 +- stream.go | 2 +- stream_test.go | 2 +- styles.go | 2 +- styles_test.go | 4 +-- table.go | 2 +- templates.go | 4 +-- vmlDrawing.go | 4 +-- xmlApp.go | 4 +-- xmlCalcChain.go | 4 +-- xmlChart.go | 4 +-- xmlComments.go | 4 +-- xmlContentTypes.go | 4 +-- xmlCore.go | 4 +-- xmlDecodeDrawing.go | 4 +-- xmlDrawing.go | 2 +- xmlPivotCache.go | 4 +-- xmlPivotTable.go | 4 +-- xmlSharedStrings.go | 4 +-- xmlStyles.go | 4 +-- xmlTable.go | 2 +- xmlTheme.go | 4 +-- xmlWorkbook.go | 4 +-- xmlWorksheet.go | 4 +-- 56 files changed, 177 insertions(+), 96 deletions(-) diff --git a/adjust.go b/adjust.go index c391cb1c48..3694fb661a 100644 --- a/adjust.go +++ b/adjust.go @@ -4,7 +4,7 @@ // // Package excelize providing a set of functions that allow you to write to // and read from XLSX / XLSM / XLTM files. Supports reading and writing -// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports +// 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.10 or later. diff --git a/calc.go b/calc.go index 698c51dcbb..1610f5e3dd 100644 --- a/calc.go +++ b/calc.go @@ -4,7 +4,7 @@ // // Package excelize providing a set of functions that allow you to write to // and read from XLSX / XLSM / XLTM files. Supports reading and writing -// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports +// 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.10 or later. @@ -323,6 +323,7 @@ var tokenPriority = map[string]int{ // MROUND // MULTINOMIAL // MUNIT +// N // NA // NORM.DIST // NORMDIST @@ -339,6 +340,7 @@ var tokenPriority = map[string]int{ // OCT2HEX // ODD // OR +// PERCENTILE.INC // PERCENTILE // PERMUT // PERMUTATIONA @@ -380,6 +382,7 @@ var tokenPriority = map[string]int{ // SUM // SUMIF // SUMSQ +// T // TAN // TANH // TODAY @@ -4521,6 +4524,19 @@ func (fn *formulaFuncs) min(mina bool, argsList *list.List) formulaArg { return newNumberFormulaArg(min) } +// PERCENTILEdotINC 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. The syntax of the function is: +// +// PERCENTILE.INC(array,k) +// +func (fn *formulaFuncs) PERCENTILEdotINC(argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, "PERCENTILE.INC requires 2 arguments") + } + return fn.PERCENTILE(argsList) +} + // PERCENTILE 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. The syntax of the function is: @@ -4858,6 +4874,28 @@ func (fn *formulaFuncs) ISTEXT(argsList *list.List) formulaArg { return newBoolFormulaArg(token.Type == ArgString) } +// N function converts data into a numeric value. The syntax of the function +// is: +// +// N(value) +// +func (fn *formulaFuncs) N(argsList *list.List) formulaArg { + if argsList.Len() != 1 { + return newErrorFormulaArg(formulaErrorVALUE, "N requires 1 argument") + } + token, num := argsList.Front().Value.(formulaArg), 0.0 + if token.Type == ArgError { + return token + } + if arg := token.ToNumber(); arg.Type == ArgNumber { + num = arg.Number + } + if token.Value() == "TRUE" { + num = 1 + } + return newNumberFormulaArg(num) +} + // NA function returns the Excel #N/A error. This error message has the // 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: @@ -4883,6 +4921,26 @@ func (fn *formulaFuncs) SHEET(argsList *list.List) formulaArg { return newNumberFormulaArg(float64(fn.f.GetSheetIndex(fn.sheet) + 1)) } +// T function tests if a supplied value is text and if so, returns the +// supplied text; Otherwise, the function returns an empty text string. The +// syntax of the function is: +// +// T(value) +// +func (fn *formulaFuncs) T(argsList *list.List) formulaArg { + if argsList.Len() != 1 { + return newErrorFormulaArg(formulaErrorVALUE, "T requires 1 argument") + } + token := argsList.Front().Value.(formulaArg) + if token.Type == ArgError { + return token + } + if token.Type == ArgNumber { + return newStringFormulaArg("") + } + return newStringFormulaArg(token.Value()) +} + // Logical Functions // AND function tests a number of supplied conditions and returns TRUE or diff --git a/calc_test.go b/calc_test.go index 1253ae07d8..935b3c2ed9 100644 --- a/calc_test.go +++ b/calc_test.go @@ -680,6 +680,8 @@ func TestCalcCellValue(t *testing.T) { "=MINA(MUNIT(2))": "0", "=MINA(INT(1))": "1", "=MINA(A1:B4,MUNIT(1),INT(0),1,E1:F2,\"\")": "0", + // PERCENTILE.INC + "=PERCENTILE.INC(A1:A4,0.2)": "0.6", // PERCENTILE "=PERCENTILE(A1:A4,0.2)": "0.6", "=PERCENTILE(0,0)": "0", @@ -730,8 +732,17 @@ func TestCalcCellValue(t *testing.T) { // ISTEXT "=ISTEXT(D1)": "TRUE", "=ISTEXT(A1)": "FALSE", + // N + "=N(10)": "10", + "=N(\"10\")": "10", + "=N(\"x\")": "0", + "=N(TRUE)": "1", + "=N(FALSE)": "0", // SHEET - "SHEET()": "1", + "=SHEET()": "1", + // T + "=T(\"text\")": "text", + "=T(N(10))": "", // Logical Functions // AND "=AND(0)": "FALSE", @@ -1479,6 +1490,8 @@ func TestCalcCellValue(t *testing.T) { // MINA "=MINA()": "MINA requires at least 1 argument", "=MINA(NA())": "#N/A", + // PERCENTILE.INC + "=PERCENTILE.INC()": "PERCENTILE.INC requires 2 arguments", // PERCENTILE "=PERCENTILE()": "PERCENTILE requires 2 arguments", "=PERCENTILE(0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", @@ -1525,11 +1538,17 @@ func TestCalcCellValue(t *testing.T) { `=ISODD("text")`: "strconv.Atoi: parsing \"text\": invalid syntax", // ISTEXT "=ISTEXT()": "ISTEXT requires 1 argument", + // N + "=N()": "N requires 1 argument", + "=N(NA())": "#N/A", // NA "=NA()": "#N/A", "=NA(1)": "NA accepts no arguments", // SHEET "=SHEET(1)": "SHEET accepts no arguments", + // T + "=T()": "T requires 1 argument", + "=T(NA())": "#N/A", // Logical Functions // AND `=AND("text")`: "strconv.ParseFloat: parsing \"text\": invalid syntax", diff --git a/calcchain.go b/calcchain.go index 03505079b0..fdc4d3eb53 100644 --- a/calcchain.go +++ b/calcchain.go @@ -1,10 +1,10 @@ -// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2021 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. // // Package excelize providing a set of functions that allow you to write to // and read from XLSX / XLSM / XLTM files. Supports reading and writing -// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports +// 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.10 or later. diff --git a/cell.go b/cell.go index 2567f198fb..fd8772fd5a 100644 --- a/cell.go +++ b/cell.go @@ -4,7 +4,7 @@ // // Package excelize providing a set of functions that allow you to write to // and read from XLSX / XLSM / XLTM files. Supports reading and writing -// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports +// 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.10 or later. diff --git a/chart.go b/chart.go index 6b7053eba6..d22cdb04c7 100644 --- a/chart.go +++ b/chart.go @@ -4,7 +4,7 @@ // // Package excelize providing a set of functions that allow you to write to // and read from XLSX / XLSM / XLTM files. Supports reading and writing -// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports +// 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.10 or later. diff --git a/col.go b/col.go index ab95a0b4b9..40ef45ff99 100644 --- a/col.go +++ b/col.go @@ -4,7 +4,7 @@ // // Package excelize providing a set of functions that allow you to write to // and read from XLSX / XLSM / XLTM files. Supports reading and writing -// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports +// 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.10 or later. diff --git a/comment.go b/comment.go index c8979430ba..7e6d31fabc 100644 --- a/comment.go +++ b/comment.go @@ -1,10 +1,10 @@ -// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2021 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. // // Package excelize providing a set of functions that allow you to write to // and read from XLSX / XLSM / XLTM files. Supports reading and writing -// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports +// 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.10 or later. diff --git a/comment_test.go b/comment_test.go index 955d4e87c4..80ed0d003a 100644 --- a/comment_test.go +++ b/comment_test.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2021 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. // diff --git a/crypt_test.go b/crypt_test.go index 6f712c1944..2e35001cb7 100644 --- a/crypt_test.go +++ b/crypt_test.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2021 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. // diff --git a/datavalidation.go b/datavalidation.go index 4cfb1257a2..7d7de0aa26 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -1,10 +1,10 @@ -// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2021 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. // // Package excelize providing a set of functions that allow you to write to // and read from XLSX / XLSM / XLTM files. Supports reading and writing -// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports +// 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.10 or later. diff --git a/datavalidation_test.go b/datavalidation_test.go index d70b874885..758267db53 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2021 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. // diff --git a/date.go b/date.go index 34c8989cb5..702f9fe098 100644 --- a/date.go +++ b/date.go @@ -1,10 +1,10 @@ -// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2021 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. // // Package excelize providing a set of functions that allow you to write to // and read from XLSX / XLSM / XLTM files. Supports reading and writing -// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports +// 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.10 or later. diff --git a/docProps.go b/docProps.go index 03604c9348..f110cd8119 100644 --- a/docProps.go +++ b/docProps.go @@ -1,10 +1,10 @@ -// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2021 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. // // Package excelize providing a set of functions that allow you to write to // and read from XLSX / XLSM / XLTM files. Supports reading and writing -// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports +// 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.10 or later. diff --git a/docProps_test.go b/docProps_test.go index ef930aefc4..071e7efc1e 100644 --- a/docProps_test.go +++ b/docProps_test.go @@ -1,11 +1,13 @@ -// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2021 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. // // Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.10 or later. +// and read from XLSX / XLSM / XLTM 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.10 or later. package excelize @@ -43,7 +45,7 @@ func TestSetDocProps(t *testing.T) { f.XLSX["docProps/core.xml"] = nil assert.NoError(t, f.SetDocProps(&DocProperties{})) - // Test unsupport charset + // Test unsupported charset f = NewFile() f.XLSX["docProps/core.xml"] = MacintoshCyrillicCharset assert.EqualError(t, f.SetDocProps(&DocProperties{}), "xml decode error: XML syntax error on line 1: invalid UTF-8") @@ -61,7 +63,7 @@ func TestGetDocProps(t *testing.T) { _, err = f.GetDocProps() assert.NoError(t, err) - // Test unsupport charset + // Test unsupported charset f = NewFile() f.XLSX["docProps/core.xml"] = MacintoshCyrillicCharset _, err = f.GetDocProps() diff --git a/drawing.go b/drawing.go index 632b914050..9ee0b07533 100644 --- a/drawing.go +++ b/drawing.go @@ -4,7 +4,7 @@ // // Package excelize providing a set of functions that allow you to write to // and read from XLSX / XLSM / XLTM files. Supports reading and writing -// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports +// 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.10 or later. diff --git a/drawing_test.go b/drawing_test.go index 0a380eda39..3c01705083 100644 --- a/drawing_test.go +++ b/drawing_test.go @@ -1,11 +1,13 @@ -// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2021 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. // // Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.10 or later. +// and read from XLSX / XLSM / XLTM 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.10 or later. package excelize @@ -22,6 +24,6 @@ func TestDrawingParser(t *testing.T) { } // Test with one cell anchor f.drawingParser("wsDr") - // Test with unsupport charset + // Test with unsupported charset f.drawingParser("charset") } diff --git a/errors.go b/errors.go index a31c93ab43..62b1312366 100644 --- a/errors.go +++ b/errors.go @@ -1,10 +1,10 @@ -// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2021 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. // // Package excelize providing a set of functions that allow you to write to // and read from XLSX / XLSM / XLTM files. Supports reading and writing -// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports +// 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.10 or later. diff --git a/excelize.go b/excelize.go index f45dcc031f..1d4cf586df 100644 --- a/excelize.go +++ b/excelize.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2021 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. diff --git a/excelize_test.go b/excelize_test.go index 0d0d587421..f663ae0e4b 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -1195,7 +1195,7 @@ func TestAddVBAProject(t *testing.T) { } func TestContentTypesReader(t *testing.T) { - // Test unsupport charset. + // Test unsupported charset. f := NewFile() f.ContentTypes = nil f.XLSX["[Content_Types].xml"] = MacintoshCyrillicCharset @@ -1203,7 +1203,7 @@ func TestContentTypesReader(t *testing.T) { } func TestWorkbookReader(t *testing.T) { - // Test unsupport charset. + // Test unsupported charset. f := NewFile() f.WorkBook = nil f.XLSX["xl/workbook.xml"] = MacintoshCyrillicCharset @@ -1211,7 +1211,7 @@ func TestWorkbookReader(t *testing.T) { } func TestWorkSheetReader(t *testing.T) { - // Test unsupport charset. + // Test unsupported charset. f := NewFile() delete(f.Sheet, "xl/worksheets/sheet1.xml") f.XLSX["xl/worksheets/sheet1.xml"] = MacintoshCyrillicCharset @@ -1228,7 +1228,7 @@ func TestWorkSheetReader(t *testing.T) { } func TestRelsReader(t *testing.T) { - // Test unsupport charset. + // Test unsupported charset. f := NewFile() rels := "xl/_rels/workbook.xml.rels" f.Relationships[rels] = nil diff --git a/file.go b/file.go index 582099e336..8be71c5388 100644 --- a/file.go +++ b/file.go @@ -4,7 +4,7 @@ // // Package excelize providing a set of functions that allow you to write to // and read from XLSX / XLSM / XLTM files. Supports reading and writing -// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports +// 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.10 or later. diff --git a/lib.go b/lib.go index 32ef615fb1..10e8c771fb 100644 --- a/lib.go +++ b/lib.go @@ -4,7 +4,7 @@ // // Package excelize providing a set of functions that allow you to write to // and read from XLSX / XLSM / XLTM files. Supports reading and writing -// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports +// 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.10 or later. diff --git a/merge.go b/merge.go index c50eaa3492..76ea0690ef 100644 --- a/merge.go +++ b/merge.go @@ -1,10 +1,10 @@ -// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2021 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. // // Package excelize providing a set of functions that allow you to write to // and read from XLSX / XLSM / XLTM files. Supports reading and writing -// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports +// 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.10 or later. diff --git a/picture.go b/picture.go index e46d37e246..02f922900c 100644 --- a/picture.go +++ b/picture.go @@ -4,7 +4,7 @@ // // Package excelize providing a set of functions that allow you to write to // and read from XLSX / XLSM / XLTM files. Supports reading and writing -// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports +// 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.10 or later. diff --git a/picture_test.go b/picture_test.go index 58f7a81525..28d8aa806a 100644 --- a/picture_test.go +++ b/picture_test.go @@ -80,7 +80,7 @@ func TestAddPictureErrors(t *testing.T) { assert.True(t, os.IsNotExist(err), "Expected os.IsNotExist(err) == true") } - // Test add picture to worksheet with unsupport file type. + // Test add picture to worksheet with unsupported file type. err = xlsx.AddPicture("Sheet1", "G21", filepath.Join("test", "Book1.xlsx"), "") assert.EqualError(t, err, "unsupported image extension") diff --git a/pivotTable.go b/pivotTable.go index 9df8c6467e..0dae4d16b2 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -4,7 +4,7 @@ // // Package excelize providing a set of functions that allow you to write to // and read from XLSX / XLSM / XLTM files. Supports reading and writing -// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports +// 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.10 or later. diff --git a/rows.go b/rows.go index 7b4f998e64..a354e2aed6 100644 --- a/rows.go +++ b/rows.go @@ -4,7 +4,7 @@ // // Package excelize providing a set of functions that allow you to write to // and read from XLSX / XLSM / XLTM files. Supports reading and writing -// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports +// 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.10 or later. diff --git a/shape.go b/shape.go index 2cc72abece..5409a20145 100644 --- a/shape.go +++ b/shape.go @@ -1,10 +1,10 @@ -// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2021 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. // // Package excelize providing a set of functions that allow you to write to // and read from XLSX / XLSM / XLTM files. Supports reading and writing -// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports +// 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.10 or later. diff --git a/sheet.go b/sheet.go index 5a9fd46ed1..7b7a94618f 100644 --- a/sheet.go +++ b/sheet.go @@ -4,7 +4,7 @@ // // Package excelize providing a set of functions that allow you to write to // and read from XLSX / XLSM / XLTM files. Supports reading and writing -// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports +// 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.10 or later. diff --git a/sheetpr.go b/sheetpr.go index 52586d99b7..2ea2394014 100644 --- a/sheetpr.go +++ b/sheetpr.go @@ -1,10 +1,10 @@ -// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2021 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. // // Package excelize providing a set of functions that allow you to write to // and read from XLSX / XLSM / XLTM files. Supports reading and writing -// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports +// 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.10 or later. diff --git a/sheetview.go b/sheetview.go index 0a8fd5c141..ad216b934d 100644 --- a/sheetview.go +++ b/sheetview.go @@ -1,10 +1,10 @@ -// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2021 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. // // Package excelize providing a set of functions that allow you to write to // and read from XLSX / XLSM / XLTM files. Supports reading and writing -// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports +// 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.10 or later. diff --git a/sparkline.go b/sparkline.go index b42207cbec..bf24843d0e 100644 --- a/sparkline.go +++ b/sparkline.go @@ -1,10 +1,10 @@ -// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2021 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. // // Package excelize providing a set of functions that allow you to write to // and read from XLSX / XLSM / XLTM files. Supports reading and writing -// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports +// 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.10 or later. diff --git a/sparkline_test.go b/sparkline_test.go index 45d3d727a2..eac982466d 100644 --- a/sparkline_test.go +++ b/sparkline_test.go @@ -270,7 +270,7 @@ func TestAddSparkline(t *testing.T) { } func TestAppendSparkline(t *testing.T) { - // Test unsupport charset. + // Test unsupported charset. f := NewFile() ws, err := f.workSheetReader("Sheet1") assert.NoError(t, err) diff --git a/stream.go b/stream.go index 7aaf7b4295..2500aa8b96 100644 --- a/stream.go +++ b/stream.go @@ -4,7 +4,7 @@ // // Package excelize providing a set of functions that allow you to write to // and read from XLSX / XLSM / XLTM files. Supports reading and writing -// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports +// 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.10 or later. diff --git a/stream_test.go b/stream_test.go index c5febfc3c5..322eea94a6 100644 --- a/stream_test.go +++ b/stream_test.go @@ -94,7 +94,7 @@ func TestStreamWriter(t *testing.T) { assert.NoError(t, streamWriter.rawData.tmp.Close()) assert.NoError(t, os.Remove(streamWriter.rawData.tmp.Name())) - // Test unsupport charset + // Test unsupported charset file = NewFile() delete(file.Sheet, "xl/worksheets/sheet1.xml") file.XLSX["xl/worksheets/sheet1.xml"] = MacintoshCyrillicCharset diff --git a/styles.go b/styles.go index 9e37239380..c2489f28d5 100644 --- a/styles.go +++ b/styles.go @@ -4,7 +4,7 @@ // // Package excelize providing a set of functions that allow you to write to // and read from XLSX / XLSM / XLTM files. Supports reading and writing -// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports +// 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.10 or later. diff --git a/styles_test.go b/styles_test.go index 02a48cc2b0..d3fde0c288 100644 --- a/styles_test.go +++ b/styles_test.go @@ -259,7 +259,7 @@ func TestSetDefaultFont(t *testing.T) { func TestStylesReader(t *testing.T) { f := NewFile() - // Test read styles with unsupport charset. + // Test read styles with unsupported charset. f.Styles = nil f.XLSX["xl/styles.xml"] = MacintoshCyrillicCharset assert.EqualValues(t, new(xlsxStyleSheet), f.stylesReader()) @@ -267,7 +267,7 @@ func TestStylesReader(t *testing.T) { func TestThemeReader(t *testing.T) { f := NewFile() - // Test read theme with unsupport charset. + // Test read theme with unsupported charset. f.XLSX["xl/theme/theme1.xml"] = MacintoshCyrillicCharset assert.EqualValues(t, new(xlsxTheme), f.themeReader()) } diff --git a/table.go b/table.go index 8862b574b6..7b5eaacf04 100644 --- a/table.go +++ b/table.go @@ -4,7 +4,7 @@ // // Package excelize providing a set of functions that allow you to write to // and read from XLSX / XLSM / XLTM files. Supports reading and writing -// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports +// 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.10 or later. diff --git a/templates.go b/templates.go index 5721150ce8..7985282704 100644 --- a/templates.go +++ b/templates.go @@ -1,10 +1,10 @@ -// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2021 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. // // Package excelize providing a set of functions that allow you to write to // and read from XLSX / XLSM / XLTM files. Supports reading and writing -// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports +// 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.10 or later. diff --git a/vmlDrawing.go b/vmlDrawing.go index 185df28890..5da188a89d 100644 --- a/vmlDrawing.go +++ b/vmlDrawing.go @@ -1,10 +1,10 @@ -// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2021 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. // // Package excelize providing a set of functions that allow you to write to // and read from XLSX / XLSM / XLTM files. Supports reading and writing -// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports +// 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.10 or later. diff --git a/xmlApp.go b/xmlApp.go index 1d51095254..0146c544a7 100644 --- a/xmlApp.go +++ b/xmlApp.go @@ -1,10 +1,10 @@ -// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2021 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. // // Package excelize providing a set of functions that allow you to write to // and read from XLSX / XLSM / XLTM files. Supports reading and writing -// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports +// 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.10 or later. diff --git a/xmlCalcChain.go b/xmlCalcChain.go index 401bb5e307..8af9f5b43a 100644 --- a/xmlCalcChain.go +++ b/xmlCalcChain.go @@ -1,10 +1,10 @@ -// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2021 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. // // Package excelize providing a set of functions that allow you to write to // and read from XLSX / XLSM / XLTM files. Supports reading and writing -// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports +// 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.10 or later. diff --git a/xmlChart.go b/xmlChart.go index ee2ad29425..453b5d8df1 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -1,10 +1,10 @@ -// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2021 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. // // Package excelize providing a set of functions that allow you to write to // and read from XLSX / XLSM / XLTM files. Supports reading and writing -// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports +// 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.10 or later. diff --git a/xmlComments.go b/xmlComments.go index f2b03a1e7a..5573ddb0bd 100644 --- a/xmlComments.go +++ b/xmlComments.go @@ -1,10 +1,10 @@ -// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2021 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. // // Package excelize providing a set of functions that allow you to write to // and read from XLSX / XLSM / XLTM files. Supports reading and writing -// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports +// 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.10 or later. diff --git a/xmlContentTypes.go b/xmlContentTypes.go index 458b117311..0edbcaf64b 100644 --- a/xmlContentTypes.go +++ b/xmlContentTypes.go @@ -1,10 +1,10 @@ -// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2021 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. // // Package excelize providing a set of functions that allow you to write to // and read from XLSX / XLSM / XLTM files. Supports reading and writing -// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports +// 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.10 or later. diff --git a/xmlCore.go b/xmlCore.go index 9d7fc4551d..32cf916c3f 100644 --- a/xmlCore.go +++ b/xmlCore.go @@ -1,10 +1,10 @@ -// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2021 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. // // Package excelize providing a set of functions that allow you to write to // and read from XLSX / XLSM / XLTM files. Supports reading and writing -// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports +// 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.10 or later. diff --git a/xmlDecodeDrawing.go b/xmlDecodeDrawing.go index 8aa22dbfa6..9176a99c7f 100644 --- a/xmlDecodeDrawing.go +++ b/xmlDecodeDrawing.go @@ -1,10 +1,10 @@ -// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2021 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. // // Package excelize providing a set of functions that allow you to write to // and read from XLSX / XLSM / XLTM files. Supports reading and writing -// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports +// 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.10 or later. diff --git a/xmlDrawing.go b/xmlDrawing.go index 76d7e17c18..73291c7e25 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -4,7 +4,7 @@ // // Package excelize providing a set of functions that allow you to write to // and read from XLSX / XLSM / XLTM files. Supports reading and writing -// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports +// 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.10 or later. diff --git a/xmlPivotCache.go b/xmlPivotCache.go index 58d977af3a..4dd42d8fbb 100644 --- a/xmlPivotCache.go +++ b/xmlPivotCache.go @@ -1,10 +1,10 @@ -// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2021 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. // // Package excelize providing a set of functions that allow you to write to // and read from XLSX / XLSM / XLTM files. Supports reading and writing -// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports +// 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.10 or later. diff --git a/xmlPivotTable.go b/xmlPivotTable.go index 9c4be50780..e187abad38 100644 --- a/xmlPivotTable.go +++ b/xmlPivotTable.go @@ -1,10 +1,10 @@ -// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2021 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. // // Package excelize providing a set of functions that allow you to write to // and read from XLSX / XLSM / XLTM files. Supports reading and writing -// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports +// 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.10 or later. diff --git a/xmlSharedStrings.go b/xmlSharedStrings.go index c9f311b959..3c8bc1ed47 100644 --- a/xmlSharedStrings.go +++ b/xmlSharedStrings.go @@ -1,10 +1,10 @@ -// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2021 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. // // Package excelize providing a set of functions that allow you to write to // and read from XLSX / XLSM / XLTM files. Supports reading and writing -// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports +// 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.10 or later. diff --git a/xmlStyles.go b/xmlStyles.go index 0670b59450..08f780eedc 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -1,10 +1,10 @@ -// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2021 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. // // Package excelize providing a set of functions that allow you to write to // and read from XLSX / XLSM / XLTM files. Supports reading and writing -// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports +// 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.10 or later. diff --git a/xmlTable.go b/xmlTable.go index c48720bd73..9770e1b53d 100644 --- a/xmlTable.go +++ b/xmlTable.go @@ -4,7 +4,7 @@ // // Package excelize providing a set of functions that allow you to write to // and read from XLSX / XLSM / XLTM files. Supports reading and writing -// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports +// 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.10 or later. diff --git a/xmlTheme.go b/xmlTheme.go index e3588dc5b6..822e1ba386 100644 --- a/xmlTheme.go +++ b/xmlTheme.go @@ -1,10 +1,10 @@ -// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2021 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. // // Package excelize providing a set of functions that allow you to write to // and read from XLSX / XLSM / XLTM files. Supports reading and writing -// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports +// 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.10 or later. diff --git a/xmlWorkbook.go b/xmlWorkbook.go index b25165bf04..dd127f8437 100644 --- a/xmlWorkbook.go +++ b/xmlWorkbook.go @@ -1,10 +1,10 @@ -// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2021 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. // // Package excelize providing a set of functions that allow you to write to // and read from XLSX / XLSM / XLTM files. Supports reading and writing -// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports +// 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.10 or later. diff --git a/xmlWorksheet.go b/xmlWorksheet.go index b31eec1bf2..9079331a6e 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -1,10 +1,10 @@ -// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2021 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. // // Package excelize providing a set of functions that allow you to write to // and read from XLSX / XLSM / XLTM files. Supports reading and writing -// spreadsheet documents generated by Microsoft Exce™ 2007 and later. Supports +// 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.10 or later. From 9d4bf88b4700eeb5c50d4cc6efb0a2d73e5e8b7f Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 31 Mar 2021 22:01:02 +0800 Subject: [PATCH 358/957] #65 fn: QUARTILE and QUARTILE.INC --- calc.go | 36 ++++++++++++++++++++++++++++++++++++ calc_test.go | 11 +++++++++++ 2 files changed, 47 insertions(+) diff --git a/calc.go b/calc.go index 1610f5e3dd..8f3bfb01ff 100644 --- a/calc.go +++ b/calc.go @@ -350,6 +350,8 @@ var tokenPriority = map[string]int{ // POWER // PRODUCT // PROPER +// QUARTILE +// QUARTILE.INC // QUOTIENT // RADIANS // RAND @@ -4625,6 +4627,40 @@ func (fn *formulaFuncs) PERMUTATIONA(argsList *list.List) formulaArg { return newNumberFormulaArg(math.Pow(num, numChosen)) } +// QUARTILE function returns a requested quartile of a supplied range of +// values. The syntax of the function is: +// +// QUARTILE(array,quart) +// +func (fn *formulaFuncs) QUARTILE(argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, "QUARTILE requires 2 arguments") + } + quart := argsList.Back().Value.(formulaArg).ToNumber() + if quart.Type != ArgNumber { + return quart + } + if quart.Number < 0 || quart.Number > 4 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + args := list.New().Init() + args.PushBack(argsList.Front().Value.(formulaArg)) + args.PushBack(newNumberFormulaArg(quart.Number / 4)) + return fn.PERCENTILE(args) +} + +// QUARTILEdotINC function returns a requested quartile of a supplied range of +// values. The syntax of the function is: +// +// QUARTILE.INC(array,quart) +// +func (fn *formulaFuncs) QUARTILEdotINC(argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, "QUARTILE.INC requires 2 arguments") + } + return fn.QUARTILE(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 935b3c2ed9..bce0c0fc03 100644 --- a/calc_test.go +++ b/calc_test.go @@ -692,6 +692,10 @@ func TestCalcCellValue(t *testing.T) { // PERMUTATIONA "=PERMUTATIONA(6,6)": "46656", "=PERMUTATIONA(7,6)": "117649", + // QUARTILE + "=QUARTILE(A1:A4,2)": "1.5", + // QUARTILE.INC + "=QUARTILE.INC(A1:A4,0)": "0", // SKEW "=SKEW(1,2,3,4,3)": "-0.404796008910937", "=SKEW(A1:B2)": "0", @@ -1508,6 +1512,13 @@ func TestCalcCellValue(t *testing.T) { "=PERMUTATIONA(0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", "=PERMUTATIONA(-1,0)": "#N/A", "=PERMUTATIONA(0,-1)": "#N/A", + // 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.INC + "=QUARTILE.INC()": "QUARTILE.INC requires 2 arguments", // SKEW "=SKEW()": "SKEW requires at least 1 argument", "=SKEW(\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", From a002a2417e9cc9d8dc3d405d2d96f7dd24710082 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 1 Apr 2021 21:52:43 +0800 Subject: [PATCH 359/957] #65 fn: VAR.P and VARP and fix Yoda conditions issue --- calc.go | 47 ++++++++++++++++++++++++++++++++++++++++++++--- calc_test.go | 10 ++++++++++ crypt.go | 2 -- 3 files changed, 54 insertions(+), 5 deletions(-) diff --git a/calc.go b/calc.go index 8f3bfb01ff..9396fa2673 100644 --- a/calc.go +++ b/calc.go @@ -394,6 +394,8 @@ var tokenPriority = map[string]int{ // UNICHAR // UNICODE // UPPER +// VAR.P +// VARP // VLOOKUP // func (f *File) CalcCellValue(sheet, cell string) (result string, err error) { @@ -1302,7 +1304,7 @@ func (fn *formulaFuncs) bin2dec(number string) formulaArg { decimal, length := 0.0, len(number) for i := length; i > 0; i-- { s := string(number[length-i]) - if 10 == i && s == "1" { + if i == 10 && s == "1" { decimal += math.Pow(-2.0, float64(i-1)) continue } @@ -1550,7 +1552,7 @@ func (fn *formulaFuncs) hex2dec(number string) formulaArg { if err != nil { return newErrorFormulaArg(formulaErrorNUM, err.Error()) } - if 10 == i && string(number[length-i]) == "F" { + if i == 10 && string(number[length-i]) == "F" { decimal += math.Pow(-16.0, float64(i-1)) continue } @@ -1631,7 +1633,7 @@ func (fn *formulaFuncs) oct2dec(number string) formulaArg { decimal, length := 0.0, len(number) for i := length; i > 0; i-- { num, _ := strconv.Atoi(string(number[length-i])) - if 10 == i && string(number[length-i]) == "7" { + if i == 10 && string(number[length-i]) == "7" { decimal += math.Pow(-8.0, float64(i-1)) continue } @@ -4707,6 +4709,45 @@ func (fn *formulaFuncs) SMALL(argsList *list.List) formulaArg { return fn.kth("SMALL", argsList) } +// VARP function returns the Variance of a given set of values. The syntax of +// the function is: +// +// VARP(number1,[number2],...) +// +func (fn *formulaFuncs) VARP(argsList *list.List) formulaArg { + if argsList.Len() < 1 { + return newErrorFormulaArg(formulaErrorVALUE, "VARP requires at least 1 argument") + } + summerA, summerB, count := 0.0, 0.0, 0.0 + for arg := argsList.Front(); arg != nil; arg = arg.Next() { + for _, token := range arg.Value.(formulaArg).ToList() { + if num := token.ToNumber(); num.Type == ArgNumber { + summerA += (num.Number * num.Number) + summerB += num.Number + count++ + } + } + } + if count > 0 { + summerA *= count + summerB *= summerB + return newNumberFormulaArg((summerA - summerB) / (count * count)) + } + return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV) +} + +// VARdotP function returns the Variance of a given set of values. The syntax +// of the function is: +// +// VAR.P(number1,[number2],...) +// +func (fn *formulaFuncs) VARdotP(argsList *list.List) formulaArg { + if argsList.Len() < 1 { + return newErrorFormulaArg(formulaErrorVALUE, "VAR.P requires at least 1 argument") + } + return fn.VARP(argsList) +} + // Information Functions // ISBLANK function tests if a specified cell is blank (empty) and if so, diff --git a/calc_test.go b/calc_test.go index bce0c0fc03..c4f1924ced 100644 --- a/calc_test.go +++ b/calc_test.go @@ -705,6 +705,10 @@ func TestCalcCellValue(t *testing.T) { "=SMALL(A1:B5,2)": "1", "=SMALL(A1,1)": "1", "=SMALL(A1:F2,1)": "1", + // VARP + "=VARP(A1:A5)": "1.25", + // VAR.P + "=VAR.P(A1:A5)": "1.25", // Information Functions // ISBLANK "=ISBLANK(A1)": "FALSE", @@ -1528,6 +1532,12 @@ func TestCalcCellValue(t *testing.T) { "=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", + // VARP + "=VARP()": "VARP requires at least 1 argument", + "=VARP(\"\")": "#DIV/0!", + // VAR.P + "=VAR.P()": "VAR.P requires at least 1 argument", + "=VAR.P(\"\")": "#DIV/0!", // Information Functions // ISBLANK "=ISBLANK(A1,A2)": "ISBLANK requires 1 argument", diff --git a/crypt.go b/crypt.go index 64eadd6a7d..5ecdf33481 100644 --- a/crypt.go +++ b/crypt.go @@ -278,14 +278,12 @@ func extractPart(doc *mscfb.Reader) (encryptionInfoBuf, encryptedPackageBuf []by i, _ := doc.Read(buf) if i > 0 { encryptionInfoBuf = buf - break } case "EncryptedPackage": buf := make([]byte, entry.Size) i, _ := doc.Read(buf) if i > 0 { encryptedPackageBuf = buf - break } } } From 6e812a27c6e74a141e301c0f19484743ea437c52 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 2 Apr 2021 22:41:33 +0800 Subject: [PATCH 360/957] #65 fn: BESSELI and BESSELJ --- calc.go | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 13 ++++++++++++ 2 files changed, 73 insertions(+) diff --git a/calc.go b/calc.go index 9396fa2673..3098e48ed0 100644 --- a/calc.go +++ b/calc.go @@ -227,6 +227,8 @@ var tokenPriority = map[string]int{ // AVERAGE // AVERAGEA // BASE +// BESSELI +// BESSELJ // BIN2DEC // BIN2HEX // BIN2OCT @@ -1226,6 +1228,64 @@ func formulaCriteriaEval(val string, criteria *formulaCriteria) (result bool, er // Engineering Functions +// BESSELI function the modified Bessel function, which is equivalent to the +// Bessel function evaluated for purely imaginary arguments. The syntax of +// the Besseli function is: +// +// BESSELI(x,n) +// +func (fn *formulaFuncs) BESSELI(argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, "BESSELI requires 2 numeric arguments") + } + return fn.bassel(argsList, true) +} + +// 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) +// +func (fn *formulaFuncs) BESSELJ(argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, "BESSELJ requires 2 numeric arguments") + } + return fn.bassel(argsList, false) +} + +// bassel is an implementation of the formula function BESSELI and BESSELJ. +func (fn *formulaFuncs) bassel(argsList *list.List, modfied bool) formulaArg { + x, n := argsList.Front().Value.(formulaArg).ToNumber(), argsList.Back().Value.(formulaArg).ToNumber() + if x.Type != ArgNumber { + return x + } + if n.Type != ArgNumber { + return n + } + max, x1 := 100, x.Number*0.5 + x2 := x1 * x1 + x1 = math.Pow(x1, n.Number) + n1, n2, n3, n4, add := fact(n.Number), 1.0, 0.0, n.Number, false + result := x1 / n1 + t := result * 0.9 + for result != t && max != 0 { + x1 *= x2 + n3++ + n1 *= n3 + n4++ + n2 *= n4 + t = result + if modfied || add { + result += (x1 / n1 / n2) + } else { + result -= (x1 / n1 / n2) + } + max-- + add = !add + } + return newNumberFormulaArg(result) +} + // BIN2DEC function converts a Binary (a base-2 number) into a decimal number. // The syntax of the function is: // diff --git a/calc_test.go b/calc_test.go index c4f1924ced..04f95bc403 100644 --- a/calc_test.go +++ b/calc_test.go @@ -47,6 +47,11 @@ func TestCalcCellValue(t *testing.T) { "=2>=3": "FALSE", "=1&2": "12", // Engineering Functions + // BESSELI + "=BESSELI(4.5,1)": "15.389222753735925", + "=BESSELI(32,1)": "5.502845511211247e+12", + // BESSELJ + "=BESSELJ(1.9,2)": "0.329925727692387", // BIN2DEC "=BIN2DEC(\"10\")": "2", "=BIN2DEC(\"11\")": "3", @@ -1008,6 +1013,14 @@ func TestCalcCellValue(t *testing.T) { mathCalcError := map[string]string{ "=1/0": "#DIV/0!", // Engineering Functions + // BESSELI + "=BESSELI()": "BESSELI requires 2 numeric arguments", + "=BESSELI(\"\",0)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=BESSELI(0,\"\")": "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", // BIN2DEC "=BIN2DEC()": "BIN2DEC requires 1 numeric argument", "=BIN2DEC(\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", From 89c262fc1d525f74bb0e2fb61ae7a3d10d07a12a Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 3 Apr 2021 22:56:02 +0800 Subject: [PATCH 361/957] Fixed #813, streaming data writer result missing after call normal API #65 formula function: COMPLEX --- calc.go | 30 ++++++++++++++++++++++++++++++ calc_test.go | 13 +++++++++++++ file.go | 3 +++ 3 files changed, 46 insertions(+) diff --git a/calc.go b/calc.go index 3098e48ed0..ae22c02a68 100644 --- a/calc.go +++ b/calc.go @@ -248,6 +248,7 @@ var tokenPriority = map[string]int{ // COLUMNS // COMBIN // COMBINA +// COMPLEX // CONCAT // CONCATENATE // COS @@ -1449,6 +1450,35 @@ func (fn *formulaFuncs) bitwise(name string, argsList *list.List) formulaArg { return newNumberFormulaArg(float64(bitwiseFunc(int(num1.Number), int(num2.Number)))) } +// COMPLEX function takes two arguments, representing the real and the +// 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]) +// +func (fn *formulaFuncs) COMPLEX(argsList *list.List) formulaArg { + if argsList.Len() < 2 { + return newErrorFormulaArg(formulaErrorVALUE, "COMPLEX requires at least 2 arguments") + } + 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 + } + if i.Type != ArgNumber { + return i + } + if argsList.Len() == 3 { + if suffix = strings.ToLower(argsList.Back().Value.(formulaArg).Value()); suffix != "i" && suffix != "j" { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + } + r := strings.NewReplacer("(", "", ")", "", "0+", "", "+0i", "", "0+0i", "0", "i", suffix) + return newStringFormulaArg(r.Replace(fmt.Sprint(complex(real.Number, i.Number)))) +} + // 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 04f95bc403..2abeec00d8 100644 --- a/calc_test.go +++ b/calc_test.go @@ -83,6 +83,13 @@ func TestCalcCellValue(t *testing.T) { // BITXOR "=BITXOR(5,6)": "3", "=BITXOR(9,12)": "5", + // COMPLEX + "=COMPLEX(5,2)": "5+2i", + "=COMPLEX(5,-9)": "5-9i", + "=COMPLEX(-1,2,\"j\")": "-1+2j", + "=COMPLEX(10,-5,\"i\")": "10-5i", + "=COMPLEX(0,5)": "5i", + "=COMPLEX(3,0)": "3", // DEC2BIN "=DEC2BIN(2)": "10", "=DEC2BIN(3)": "11", @@ -1080,6 +1087,12 @@ func TestCalcCellValue(t *testing.T) { "=BITXOR(\"\",-1)": "#NUM!", "=BITXOR(1,\"\")": "#NUM!", "=BITXOR(1,2^48)": "#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", // DEC2BIN "=DEC2BIN()": "DEC2BIN requires at least 1 argument", "=DEC2BIN(1,1,1)": "DEC2BIN allows at most 2 arguments", diff --git a/file.go b/file.go index 8be71c5388..ab7464595d 100644 --- a/file.go +++ b/file.go @@ -131,6 +131,9 @@ func (f *File) WriteToBuffer() (*bytes.Buffer, error) { } for path, content := range f.XLSX { + if _, ok := f.streams[path]; ok { + continue + } fi, err := zw.Create(path) if err != nil { zw.Close() From f8f699a172bd0219a8736b19091dd3eaa9eb3b25 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 4 Apr 2021 15:29:43 +0800 Subject: [PATCH 362/957] Go 1.15 and later required, #65 fn: IMABS, IMCOS, IMCOSH, IMCOT, IMCSC, IMCSCH, IMEXP, IMLN and IMLOG10 --- .travis.yml | 4 - README.md | 2 +- README_zh.md | 2 +- adjust.go | 2 +- calc.go | 206 ++++++++++++++++++++++++++++++++++++++++- calc_test.go | 71 ++++++++++++++ calcchain.go | 2 +- cell.go | 2 +- chart.go | 2 +- col.go | 2 +- comment.go | 2 +- comment_test.go | 2 +- crypt.go | 2 +- crypt_test.go | 2 +- datavalidation.go | 2 +- datavalidation_test.go | 2 +- date.go | 2 +- docProps.go | 2 +- docProps_test.go | 2 +- drawing.go | 2 +- drawing_test.go | 2 +- errors.go | 2 +- excelize.go | 2 +- file.go | 2 +- go.mod | 2 +- lib.go | 2 +- merge.go | 2 +- picture.go | 2 +- pivotTable.go | 2 +- rows.go | 2 +- shape.go | 2 +- sheet.go | 2 +- sheetpr.go | 2 +- sheetview.go | 2 +- sparkline.go | 2 +- stream.go | 2 +- styles.go | 2 +- table.go | 2 +- templates.go | 2 +- vmlDrawing.go | 2 +- xmlApp.go | 2 +- xmlCalcChain.go | 2 +- xmlChart.go | 2 +- xmlChartSheet.go | 2 +- xmlComments.go | 2 +- xmlContentTypes.go | 2 +- xmlCore.go | 2 +- xmlDecodeDrawing.go | 2 +- xmlDrawing.go | 2 +- xmlPivotCache.go | 2 +- xmlPivotTable.go | 2 +- xmlSharedStrings.go | 2 +- xmlStyles.go | 2 +- xmlTable.go | 2 +- xmlTheme.go | 2 +- xmlWorkbook.go | 2 +- xmlWorksheet.go | 2 +- 57 files changed, 328 insertions(+), 61 deletions(-) diff --git a/.travis.yml b/.travis.yml index f302eededc..34ce8ee3e8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,10 +4,6 @@ install: - go get -d -t -v ./... && go build -v ./... go: - - 1.11.x - - 1.12.x - - 1.13.x - - 1.14.x - 1.15.x - 1.16.x diff --git a/README.md b/README.md index 44fc57a6f8..ce7cf3d704 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 XLSX / XLSM / XLTM 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.10 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/360EntSecGroup-Skylar/excelize/v2?tab=doc) 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 XLSX / XLSM / XLTM 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/360EntSecGroup-Skylar/excelize/v2?tab=doc) and [docs reference](https://xuri.me/excelize/). ## Basic Usage diff --git a/README_zh.md b/README_zh.md index 1118367dd4..cf9888b30d 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 及以上版本创建的电子表格文档。支持 XLSX / XLSM / XLTM 等多种文档格式,高度兼容带有样式、图片(表)、透视表、切片器等复杂组件的文档,并提供流式读写 API,用于处理包含大规模数据的工作簿。可应用于各类报表平台、云计算、边缘计算等系统。使用本类库要求使用的 Go 语言为 1.10 或更高版本,完整的 API 使用文档请访问 [go.dev](https://pkg.go.dev/github.com/360EntSecGroup-Skylar/excelize/v2?tab=doc) 或查看 [参考文档](https://xuri.me/excelize/)。 +Excelize 是 Go 语言编写的用于操作 Office Excel 文档基础库,基于 ECMA-376,ISO/IEC 29500 国际标准。可以使用它来读取、写入由 Microsoft Excel™ 2007 及以上版本创建的电子表格文档。支持 XLSX / XLSM / XLTM 等多种文档格式,高度兼容带有样式、图片(表)、透视表、切片器等复杂组件的文档,并提供流式读写 API,用于处理包含大规模数据的工作簿。可应用于各类报表平台、云计算、边缘计算等系统。使用本类库要求使用的 Go 语言为 1.15 或更高版本,完整的 API 使用文档请访问 [go.dev](https://pkg.go.dev/github.com/360EntSecGroup-Skylar/excelize/v2?tab=doc) 或查看 [参考文档](https://xuri.me/excelize/)。 ## 快速上手 diff --git a/adjust.go b/adjust.go index 3694fb661a..e06d4f62ab 100644 --- a/adjust.go +++ b/adjust.go @@ -7,7 +7,7 @@ // 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.10 or later. +// library needs Go version 1.15 or later. package excelize diff --git a/calc.go b/calc.go index ae22c02a68..9cb67a1f11 100644 --- a/calc.go +++ b/calc.go @@ -7,7 +7,7 @@ // 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.10 or later. +// library needs Go version 1.15 or later. package excelize @@ -17,6 +17,7 @@ import ( "errors" "fmt" "math" + "math/cmplx" "math/rand" "net/url" "reflect" @@ -292,6 +293,15 @@ var tokenPriority = map[string]int{ // HLOOKUP // IF // IFERROR +// IMABS +// IMCOS +// IMCOSH +// IMCOT +// IMCSC +// IMCSCH +// IMEXP +// IMLN +// IMLOG10 // INT // ISBLANK // ISERR @@ -1475,8 +1485,38 @@ func (fn *formulaFuncs) COMPLEX(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) } } - r := strings.NewReplacer("(", "", ")", "", "0+", "", "+0i", "", "0+0i", "0", "i", suffix) - return newStringFormulaArg(r.Replace(fmt.Sprint(complex(real.Number, i.Number)))) + return newStringFormulaArg(cmplx2str(fmt.Sprint(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)" { + return "0" + } + c = strings.TrimPrefix(c, "(") + c = strings.TrimPrefix(c, "+0+") + c = strings.TrimPrefix(c, "-0+") + c = strings.TrimSuffix(c, ")") + c = strings.TrimPrefix(c, "0+") + if strings.HasPrefix(c, "0-") { + c = "-" + strings.TrimPrefix(c, "0-") + } + c = strings.TrimPrefix(c, "0+") + 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) + return c +} + +// str2cmplx convert complex number string characters. +func str2cmplx(c string) string { + c = strings.Replace(c, "j", "i", -1) + if c == "i" { + c = "1i" + } + c = strings.NewReplacer("+i", "+1i", "-i", "-1i").Replace(c) + return c } // DEC2BIN function converts a decimal number into a Binary (Base 2) number. @@ -1651,6 +1691,166 @@ func (fn *formulaFuncs) hex2dec(number string) formulaArg { return newNumberFormulaArg(decimal) } +// IMABS function returns the absolute value (the modulus) of a complex +// number. The syntax of the function is: +// +// IMABS(inumber) +// +func (fn *formulaFuncs) IMABS(argsList *list.List) formulaArg { + if argsList.Len() != 1 { + return newErrorFormulaArg(formulaErrorVALUE, "IMABS requires 1 argument") + } + inumber, err := strconv.ParseComplex(strings.Replace(argsList.Front().Value.(formulaArg).Value(), "j", "i", -1), 128) + if err != nil { + return newErrorFormulaArg(formulaErrorNUM, err.Error()) + } + return newNumberFormulaArg(cmplx.Abs(inumber)) +} + +// IMCOS function returns the cosine of a supplied complex number. The syntax +// of the function is: +// +// IMCOS(inumber) +// +func (fn *formulaFuncs) IMCOS(argsList *list.List) formulaArg { + if argsList.Len() != 1 { + return newErrorFormulaArg(formulaErrorVALUE, "IMCOS requires 1 argument") + } + inumber, err := strconv.ParseComplex(strings.Replace(argsList.Front().Value.(formulaArg).Value(), "j", "i", -1), 128) + if err != nil { + return newErrorFormulaArg(formulaErrorNUM, err.Error()) + } + return newStringFormulaArg(cmplx2str(fmt.Sprint(cmplx.Cos(inumber)), "i")) +} + +// IMCOSH function returns the hyperbolic cosine of a supplied complex number. The syntax +// of the function is: +// +// IMCOSH(inumber) +// +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) + if err != nil { + return newErrorFormulaArg(formulaErrorNUM, err.Error()) + } + return newStringFormulaArg(cmplx2str(fmt.Sprint(cmplx.Cosh(inumber)), "i")) +} + +// IMCOT function returns the cotangent of a supplied complex number. The syntax +// of the function is: +// +// IMCOT(inumber) +// +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) + if err != nil { + return newErrorFormulaArg(formulaErrorNUM, err.Error()) + } + return newStringFormulaArg(cmplx2str(fmt.Sprint(cmplx.Cot(inumber)), "i")) +} + +// IMCSC function returns the cosecant of a supplied complex number. The syntax +// of the function is: +// +// IMCSC(inumber) +// +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) + if err != nil { + return newErrorFormulaArg(formulaErrorNUM, err.Error()) + } + num := 1 / cmplx.Sin(inumber) + if cmplx.IsInf(num) { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + return newStringFormulaArg(cmplx2str(fmt.Sprint(num), "i")) +} + +// IMCSCH function returns the hyperbolic cosecant of a supplied complex +// number. The syntax of the function is: +// +// IMCSCH(inumber) +// +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) + if err != nil { + return newErrorFormulaArg(formulaErrorNUM, err.Error()) + } + num := 1 / cmplx.Sinh(inumber) + if cmplx.IsInf(num) { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + return newStringFormulaArg(cmplx2str(fmt.Sprint(num), "i")) +} + +// IMEXP function returns the exponential of a supplied complex number. The +// syntax of the function is: +// +// IMEXP(inumber) +// +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) + if err != nil { + return newErrorFormulaArg(formulaErrorNUM, err.Error()) + } + return newStringFormulaArg(cmplx2str(fmt.Sprint(cmplx.Exp(inumber)), "i")) +} + +// IMLN function returns the natural logarithm of a supplied complex number. +// The syntax of the function is: +// +// IMLN(inumber) +// +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) + if err != nil { + return newErrorFormulaArg(formulaErrorNUM, err.Error()) + } + num := cmplx.Log(inumber) + if cmplx.IsInf(num) { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + return newStringFormulaArg(cmplx2str(fmt.Sprint(num), "i")) +} + +// IMLOG10 function returns the common (base 10) logarithm of a supplied +// complex number. The syntax of the function is: +// +// IMLOG10(inumber) +// +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) + if err != nil { + return newErrorFormulaArg(formulaErrorNUM, err.Error()) + } + num := cmplx.Log10(inumber) + if cmplx.IsInf(num) { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + return newStringFormulaArg(cmplx2str(fmt.Sprint(num), "i")) +} + // OCT2BIN function converts an Octal (Base 8) number into a Binary (Base 2) // number. The syntax of the function is: // diff --git a/calc_test.go b/calc_test.go index 2abeec00d8..5fbc6a3963 100644 --- a/calc_test.go +++ b/calc_test.go @@ -90,6 +90,9 @@ func TestCalcCellValue(t *testing.T) { "=COMPLEX(10,-5,\"i\")": "10-5i", "=COMPLEX(0,5)": "5i", "=COMPLEX(3,0)": "3", + "=COMPLEX(0,-2)": "-2i", + "=COMPLEX(0,0)": "0", + "=COMPLEX(0,-1,\"j\")": "-j", // DEC2BIN "=DEC2BIN(2)": "10", "=DEC2BIN(3)": "11", @@ -127,6 +130,43 @@ func TestCalcCellValue(t *testing.T) { "=HEX2OCT(\"8\",10)": "0000000010", "=HEX2OCT(\"FFFFFFFFF8\")": "7777777770", "=HEX2OCT(\"1F3\")": "763", + // IMABS + "=IMABS(\"2j\")": "2", + "=IMABS(\"-1+2i\")": "2.23606797749979", + "=IMABS(COMPLEX(-1,2,\"j\"))": "2.23606797749979", + // IMCOS + "=IMCOS(0)": "1", + "=IMCOS(0.5)": "0.877582561890373", + "=IMCOS(\"3+0.5i\")": "-1.1163412445261518-0.0735369737112366i", + // IMCOSH + "=IMCOSH(0.5)": "1.127625965206381", + "=IMCOSH(\"3+0.5i\")": "8.835204606500994+4.802825082743033i", + "=IMCOSH(\"2-i\")": "2.0327230070196656-3.0518977991518i", + "=IMCOSH(COMPLEX(1,-1))": "0.8337300251311491-0.9888977057628651i", + // IMCOT + "=IMCOT(0.5)": "1.830487721712452", + "=IMCOT(\"3+0.5i\")": "-0.4793455787473728-2.016092521506228i", + "=IMCOT(\"2-i\")": "-0.171383612909185+0.8213297974938518i", + "=IMCOT(COMPLEX(1,-1))": "0.21762156185440268+0.868014142895925i", + // IMCSC + "=IMCSC(\"j\")": "-0.8509181282393216i", + // IMCSCH + "=IMCSCH(COMPLEX(1,-1))": "0.30393100162842646+0.6215180171704284i", + // IMEXP + "=IMEXP(0)": "1", + "=IMEXP(0.5)": "1.648721270700128", + "=IMEXP(\"1-2i\")": "-1.1312043837568135-2.4717266720048183i", + "=IMEXP(COMPLEX(1,-1))": "1.4686939399158851-2.2873552871788423i", + // 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", + // 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", // OCT2BIN "=OCT2BIN(\"5\")": "101", "=OCT2BIN(\"0000000001\")": "1", @@ -1135,6 +1175,37 @@ func TestCalcCellValue(t *testing.T) { "=HEX2OCT(1,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", "=HEX2OCT(-513,10)": "strconv.ParseInt: parsing \"-\": invalid syntax", "=HEX2OCT(1,-1)": "#NUM!", + // IMABS + "=IMABS()": "IMABS requires 1 argument", + "=IMABS(\"\")": "strconv.ParseComplex: parsing \"\": invalid syntax", + // IMCOS + "=IMCOS()": "IMCOS requires 1 argument", + "=IMCOS(\"\")": "strconv.ParseComplex: parsing \"\": invalid syntax", + // IMCOSH + "=IMCOSH()": "IMCOSH requires 1 argument", + "=IMCOSH(\"\")": "strconv.ParseComplex: parsing \"\": invalid syntax", + // IMCOT + "=IMCOT()": "IMCOT requires 1 argument", + "=IMCOT(\"\")": "strconv.ParseComplex: parsing \"\": invalid syntax", + // IMCSC + "=IMCSC()": "IMCSC requires 1 argument", + "=IMCSC(\"\")": "strconv.ParseComplex: parsing \"\": invalid syntax", + "=IMCSC(0)": "#NUM!", + // IMCSCH + "=IMCSCH()": "IMCSCH requires 1 argument", + "=IMCSCH(\"\")": "strconv.ParseComplex: parsing \"\": invalid syntax", + "=IMCSCH(0)": "#NUM!", + // IMEXP + "=IMEXP()": "IMEXP requires 1 argument", + "=IMEXP(\"\")": "strconv.ParseComplex: parsing \"\": invalid syntax", + // IMLN + "=IMLN()": "IMLN requires 1 argument", + "=IMLN(\"\")": "strconv.ParseComplex: parsing \"\": invalid syntax", + "=IMLN(0)": "#NUM!", + // IMLOG10 + "=IMLOG10()": "IMLOG10 requires 1 argument", + "=IMLOG10(\"\")": "strconv.ParseComplex: parsing \"\": invalid syntax", + "=IMLOG10(0)": "#NUM!", // OCT2BIN "=OCT2BIN()": "OCT2BIN requires at least 1 argument", "=OCT2BIN(1,1,1)": "OCT2BIN allows at most 2 arguments", diff --git a/calcchain.go b/calcchain.go index fdc4d3eb53..ea3080f2c2 100644 --- a/calcchain.go +++ b/calcchain.go @@ -7,7 +7,7 @@ // 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.10 or later. +// library needs Go version 1.15 or later. package excelize diff --git a/cell.go b/cell.go index fd8772fd5a..27d24d98d9 100644 --- a/cell.go +++ b/cell.go @@ -7,7 +7,7 @@ // 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.10 or later. +// library needs Go version 1.15 or later. package excelize diff --git a/chart.go b/chart.go index d22cdb04c7..1e2e04655d 100644 --- a/chart.go +++ b/chart.go @@ -7,7 +7,7 @@ // 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.10 or later. +// library needs Go version 1.15 or later. package excelize diff --git a/col.go b/col.go index 40ef45ff99..09a172a11d 100644 --- a/col.go +++ b/col.go @@ -7,7 +7,7 @@ // 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.10 or later. +// library needs Go version 1.15 or later. package excelize diff --git a/comment.go b/comment.go index 7e6d31fabc..3c48d61844 100644 --- a/comment.go +++ b/comment.go @@ -7,7 +7,7 @@ // 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.10 or later. +// library needs Go version 1.15 or later. package excelize diff --git a/comment_test.go b/comment_test.go index 80ed0d003a..ee8b8266bf 100644 --- a/comment_test.go +++ b/comment_test.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.10 or later. +// charts of XLSX. This library needs Go version 1.15 or later. package excelize diff --git a/crypt.go b/crypt.go index 5ecdf33481..88abd0e306 100644 --- a/crypt.go +++ b/crypt.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.10 or later. +// charts of XLSX. This library needs Go version 1.15 or later. package excelize diff --git a/crypt_test.go b/crypt_test.go index 2e35001cb7..6a882e5d73 100644 --- a/crypt_test.go +++ b/crypt_test.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.10 or later. +// charts of XLSX. This library needs Go version 1.15 or later. package excelize diff --git a/datavalidation.go b/datavalidation.go index 7d7de0aa26..0f8508bac2 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -7,7 +7,7 @@ // 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.10 or later. +// library needs Go version 1.15 or later. package excelize diff --git a/datavalidation_test.go b/datavalidation_test.go index 758267db53..5aea5832ec 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -5,7 +5,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.10 or later. +// charts of XLSX. This library needs Go version 1.15 or later. package excelize diff --git a/date.go b/date.go index 702f9fe098..3e50e3d3c3 100644 --- a/date.go +++ b/date.go @@ -7,7 +7,7 @@ // 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.10 or later. +// library needs Go version 1.15 or later. package excelize diff --git a/docProps.go b/docProps.go index f110cd8119..bf294f292e 100644 --- a/docProps.go +++ b/docProps.go @@ -7,7 +7,7 @@ // 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.10 or later. +// library needs Go version 1.15 or later. package excelize diff --git a/docProps_test.go b/docProps_test.go index 071e7efc1e..0cb6f71fed 100644 --- a/docProps_test.go +++ b/docProps_test.go @@ -7,7 +7,7 @@ // 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.10 or later. +// library needs Go version 1.15 or later. package excelize diff --git a/drawing.go b/drawing.go index 9ee0b07533..0e5d9480e3 100644 --- a/drawing.go +++ b/drawing.go @@ -7,7 +7,7 @@ // 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.10 or later. +// library needs Go version 1.15 or later. package excelize diff --git a/drawing_test.go b/drawing_test.go index 3c01705083..1ee8fae3d8 100644 --- a/drawing_test.go +++ b/drawing_test.go @@ -7,7 +7,7 @@ // 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.10 or later. +// library needs Go version 1.15 or later. package excelize diff --git a/errors.go b/errors.go index 62b1312366..0ab2642f1b 100644 --- a/errors.go +++ b/errors.go @@ -7,7 +7,7 @@ // 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.10 or later. +// library needs Go version 1.15 or later. package excelize diff --git a/excelize.go b/excelize.go index 1d4cf586df..6b3d4062ca 100644 --- a/excelize.go +++ b/excelize.go @@ -7,7 +7,7 @@ // 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.10 or later. +// library needs Go version 1.15 or later. // // See https://xuri.me/excelize for more information about this package. package excelize diff --git a/file.go b/file.go index ab7464595d..8a37aefe1b 100644 --- a/file.go +++ b/file.go @@ -7,7 +7,7 @@ // 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.10 or later. +// library needs Go version 1.15 or later. package excelize diff --git a/go.mod b/go.mod index 8beb465fa4..0984d3a984 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/360EntSecGroup-Skylar/excelize/v2 -go 1.11 +go 1.15 require ( github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 diff --git a/lib.go b/lib.go index 10e8c771fb..0c1938aa33 100644 --- a/lib.go +++ b/lib.go @@ -7,7 +7,7 @@ // 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.10 or later. +// library needs Go version 1.15 or later. package excelize diff --git a/merge.go b/merge.go index 76ea0690ef..1f8974ed3d 100644 --- a/merge.go +++ b/merge.go @@ -7,7 +7,7 @@ // 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.10 or later. +// library needs Go version 1.15 or later. package excelize diff --git a/picture.go b/picture.go index 02f922900c..de7e0f859f 100644 --- a/picture.go +++ b/picture.go @@ -7,7 +7,7 @@ // 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.10 or later. +// library needs Go version 1.15 or later. package excelize diff --git a/pivotTable.go b/pivotTable.go index 0dae4d16b2..11c2b31b67 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -7,7 +7,7 @@ // 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.10 or later. +// library needs Go version 1.15 or later. package excelize diff --git a/rows.go b/rows.go index a354e2aed6..76a8f67b71 100644 --- a/rows.go +++ b/rows.go @@ -7,7 +7,7 @@ // 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.10 or later. +// library needs Go version 1.15 or later. package excelize diff --git a/shape.go b/shape.go index 5409a20145..f7e2ef3d54 100644 --- a/shape.go +++ b/shape.go @@ -7,7 +7,7 @@ // 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.10 or later. +// library needs Go version 1.15 or later. package excelize diff --git a/sheet.go b/sheet.go index 7b7a94618f..8bbbc82742 100644 --- a/sheet.go +++ b/sheet.go @@ -7,7 +7,7 @@ // 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.10 or later. +// library needs Go version 1.15 or later. package excelize diff --git a/sheetpr.go b/sheetpr.go index 2ea2394014..8bc4bfe247 100644 --- a/sheetpr.go +++ b/sheetpr.go @@ -7,7 +7,7 @@ // 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.10 or later. +// library needs Go version 1.15 or later. package excelize diff --git a/sheetview.go b/sheetview.go index ad216b934d..91df04c1ab 100644 --- a/sheetview.go +++ b/sheetview.go @@ -7,7 +7,7 @@ // 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.10 or later. +// library needs Go version 1.15 or later. package excelize diff --git a/sparkline.go b/sparkline.go index bf24843d0e..150c0eaece 100644 --- a/sparkline.go +++ b/sparkline.go @@ -7,7 +7,7 @@ // 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.10 or later. +// library needs Go version 1.15 or later. package excelize diff --git a/stream.go b/stream.go index 2500aa8b96..b0e5b533c9 100644 --- a/stream.go +++ b/stream.go @@ -7,7 +7,7 @@ // 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.10 or later. +// library needs Go version 1.15 or later. package excelize diff --git a/styles.go b/styles.go index c2489f28d5..d58281d79a 100644 --- a/styles.go +++ b/styles.go @@ -7,7 +7,7 @@ // 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.10 or later. +// library needs Go version 1.15 or later. package excelize diff --git a/table.go b/table.go index 7b5eaacf04..973a416f21 100644 --- a/table.go +++ b/table.go @@ -7,7 +7,7 @@ // 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.10 or later. +// library needs Go version 1.15 or later. package excelize diff --git a/templates.go b/templates.go index 7985282704..56588c4514 100644 --- a/templates.go +++ b/templates.go @@ -7,7 +7,7 @@ // 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.10 or later. +// library needs Go version 1.15 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 5da188a89d..58166fa6d3 100644 --- a/vmlDrawing.go +++ b/vmlDrawing.go @@ -7,7 +7,7 @@ // 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.10 or later. +// library needs Go version 1.15 or later. package excelize diff --git a/xmlApp.go b/xmlApp.go index 0146c544a7..fdb600845d 100644 --- a/xmlApp.go +++ b/xmlApp.go @@ -7,7 +7,7 @@ // 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.10 or later. +// library needs Go version 1.15 or later. package excelize diff --git a/xmlCalcChain.go b/xmlCalcChain.go index 8af9f5b43a..dfbb074e41 100644 --- a/xmlCalcChain.go +++ b/xmlCalcChain.go @@ -7,7 +7,7 @@ // 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.10 or later. +// library needs Go version 1.15 or later. package excelize diff --git a/xmlChart.go b/xmlChart.go index 453b5d8df1..637d954112 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -7,7 +7,7 @@ // 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.10 or later. +// library needs Go version 1.15 or later. package excelize diff --git a/xmlChartSheet.go b/xmlChartSheet.go index bd3d0630e2..4ef2deddf5 100644 --- a/xmlChartSheet.go +++ b/xmlChartSheet.go @@ -7,7 +7,7 @@ // Package excelize providing a set of functions that allow you to write to // and read from XLSX files. Support reads and writes XLSX file generated by // Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.10 or later. +// charts of XLSX. This library needs Go version 1.15 or later. package excelize diff --git a/xmlComments.go b/xmlComments.go index 5573ddb0bd..e39fb24354 100644 --- a/xmlComments.go +++ b/xmlComments.go @@ -7,7 +7,7 @@ // 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.10 or later. +// library needs Go version 1.15 or later. package excelize diff --git a/xmlContentTypes.go b/xmlContentTypes.go index 0edbcaf64b..f429ef6bb9 100644 --- a/xmlContentTypes.go +++ b/xmlContentTypes.go @@ -7,7 +7,7 @@ // 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.10 or later. +// library needs Go version 1.15 or later. package excelize diff --git a/xmlCore.go b/xmlCore.go index 32cf916c3f..8ed8f30471 100644 --- a/xmlCore.go +++ b/xmlCore.go @@ -7,7 +7,7 @@ // 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.10 or later. +// library needs Go version 1.15 or later. package excelize diff --git a/xmlDecodeDrawing.go b/xmlDecodeDrawing.go index 9176a99c7f..da333ef984 100644 --- a/xmlDecodeDrawing.go +++ b/xmlDecodeDrawing.go @@ -7,7 +7,7 @@ // 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.10 or later. +// library needs Go version 1.15 or later. package excelize diff --git a/xmlDrawing.go b/xmlDrawing.go index 73291c7e25..a18c5886bf 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -7,7 +7,7 @@ // 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.10 or later. +// library needs Go version 1.15 or later. package excelize diff --git a/xmlPivotCache.go b/xmlPivotCache.go index 4dd42d8fbb..2812cf415b 100644 --- a/xmlPivotCache.go +++ b/xmlPivotCache.go @@ -7,7 +7,7 @@ // 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.10 or later. +// library needs Go version 1.15 or later. package excelize diff --git a/xmlPivotTable.go b/xmlPivotTable.go index e187abad38..53249910f4 100644 --- a/xmlPivotTable.go +++ b/xmlPivotTable.go @@ -7,7 +7,7 @@ // 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.10 or later. +// library needs Go version 1.15 or later. package excelize diff --git a/xmlSharedStrings.go b/xmlSharedStrings.go index 3c8bc1ed47..7cb23fd57d 100644 --- a/xmlSharedStrings.go +++ b/xmlSharedStrings.go @@ -7,7 +7,7 @@ // 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.10 or later. +// library needs Go version 1.15 or later. package excelize diff --git a/xmlStyles.go b/xmlStyles.go index 08f780eedc..46604dc463 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -7,7 +7,7 @@ // 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.10 or later. +// library needs Go version 1.15 or later. package excelize diff --git a/xmlTable.go b/xmlTable.go index 9770e1b53d..cb343bd304 100644 --- a/xmlTable.go +++ b/xmlTable.go @@ -7,7 +7,7 @@ // 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.10 or later. +// library needs Go version 1.15 or later. package excelize diff --git a/xmlTheme.go b/xmlTheme.go index 822e1ba386..ad557384f1 100644 --- a/xmlTheme.go +++ b/xmlTheme.go @@ -7,7 +7,7 @@ // 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.10 or later. +// library needs Go version 1.15 or later. package excelize diff --git a/xmlWorkbook.go b/xmlWorkbook.go index dd127f8437..7151c6fdee 100644 --- a/xmlWorkbook.go +++ b/xmlWorkbook.go @@ -7,7 +7,7 @@ // 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.10 or later. +// library needs Go version 1.15 or later. package excelize diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 9079331a6e..edf57373a2 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -7,7 +7,7 @@ // 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.10 or later. +// library needs Go version 1.15 or later. package excelize From 3345e89b96fa058273da732be48d01cff4f69960 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 5 Apr 2021 13:00:34 +0800 Subject: [PATCH 363/957] #65 fn: IMSIN, IMSINH, IMSQRT, IMSUB, IMSUM, IMTAN --- calc.go | 116 ++++++++++++++++++++++++++++++++++++++++++++++++++- calc_test.go | 49 ++++++++++++++++++++++ 2 files changed, 163 insertions(+), 2 deletions(-) diff --git a/calc.go b/calc.go index 9cb67a1f11..521529fbaa 100644 --- a/calc.go +++ b/calc.go @@ -302,6 +302,12 @@ var tokenPriority = map[string]int{ // IMEXP // IMLN // IMLOG10 +// IMSIN +// IMSINH +// IMSQRT +// IMSUB +// IMSUM +// IMTAN // INT // ISBLANK // ISERR @@ -1490,7 +1496,7 @@ func (fn *formulaFuncs) COMPLEX(argsList *list.List) formulaArg { // cmplx2str replace complex number string characters. func cmplx2str(c, suffix string) string { - if c == "(0+0i)" || c == "(0-0i)" { + if c == "(0+0i)" || c == "(-0+0i)" || c == "(0-0i)" || c == "(-0-0i)" { return "0" } c = strings.TrimPrefix(c, "(") @@ -1504,7 +1510,7 @@ func cmplx2str(c, suffix string) string { c = strings.TrimPrefix(c, "0+") c = strings.TrimSuffix(c, "+0i") c = strings.TrimSuffix(c, "-0i") - c = strings.NewReplacer("+1i", "i", "-1i", "-i").Replace(c) + c = strings.NewReplacer("+1i", "+i", "-1i", "-i").Replace(c) c = strings.Replace(c, "i", suffix, -1) return c } @@ -1851,6 +1857,112 @@ func (fn *formulaFuncs) IMLOG10(argsList *list.List) formulaArg { return newStringFormulaArg(cmplx2str(fmt.Sprint(num), "i")) } +// IMSIN function returns the Sine of a supplied complex number. The syntax of +// the function is: +// +// IMSIN(inumber) +// +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) + if err != nil { + return newErrorFormulaArg(formulaErrorNUM, err.Error()) + } + return newStringFormulaArg(cmplx2str(fmt.Sprint(cmplx.Sin(inumber)), "i")) +} + +// IMSINH function returns the hyperbolic sine of a supplied complex number. +// The syntax of the function is: +// +// IMSINH(inumber) +// +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) + if err != nil { + return newErrorFormulaArg(formulaErrorNUM, err.Error()) + } + return newStringFormulaArg(cmplx2str(fmt.Sprint(cmplx.Sinh(inumber)), "i")) +} + +// IMSQRT function returns the square root of a supplied complex number. The +// syntax of the function is: +// +// IMSQRT(inumber) +// +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) + if err != nil { + return newErrorFormulaArg(formulaErrorNUM, err.Error()) + } + return newStringFormulaArg(cmplx2str(fmt.Sprint(cmplx.Sqrt(inumber)), "i")) +} + +// IMSUB function calculates the difference between two complex numbers +// (i.e. subtracts one complex number from another). The syntax of the +// function is: +// +// IMSUB(inumber1,inumber2) +// +func (fn *formulaFuncs) IMSUB(argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, "IMSUB requires 2 arguments") + } + i1, err := strconv.ParseComplex(str2cmplx(argsList.Front().Value.(formulaArg).Value()), 128) + if err != nil { + return newErrorFormulaArg(formulaErrorNUM, err.Error()) + } + i2, err := strconv.ParseComplex(str2cmplx(argsList.Back().Value.(formulaArg).Value()), 128) + if err != nil { + return newErrorFormulaArg(formulaErrorNUM, err.Error()) + } + return newStringFormulaArg(cmplx2str(fmt.Sprint(i1-i2), "i")) +} + +// IMSUM function calculates the sum of two or more complex numbers. The +// syntax of the function is: +// +// IMSUM(inumber1,inumber2,...) +// +func (fn *formulaFuncs) IMSUM(argsList *list.List) formulaArg { + if argsList.Len() < 1 { + return newErrorFormulaArg(formulaErrorVALUE, "IMSUM requires at least 1 argument") + } + var result complex128 + for arg := argsList.Front(); arg != nil; arg = arg.Next() { + token := arg.Value.(formulaArg) + num, err := strconv.ParseComplex(str2cmplx(token.Value()), 128) + if err != nil { + return newErrorFormulaArg(formulaErrorNUM, err.Error()) + } + result += num + } + return newStringFormulaArg(cmplx2str(fmt.Sprint(result), "i")) +} + +// IMTAN function returns the tangent of a supplied complex number. The syntax +// of the function is: +// +// IMTAN(inumber) +// +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) + if err != nil { + return newErrorFormulaArg(formulaErrorNUM, err.Error()) + } + return newStringFormulaArg(cmplx2str(fmt.Sprint(cmplx.Tan(inumber)), "i")) +} + // OCT2BIN function converts an Octal (Base 8) number into a Binary (Base 2) // number. The syntax of the function is: // diff --git a/calc_test.go b/calc_test.go index 5fbc6a3963..590432d7f5 100644 --- a/calc_test.go +++ b/calc_test.go @@ -167,6 +167,36 @@ func TestCalcCellValue(t *testing.T) { "=IMLOG10(\"3+0.5i\")": "0.48307086636951624+0.07172315929479262i", "=IMLOG10(\"2-i\")": "0.34948500216800943-0.20135959813668655i", "=IMLOG10(COMPLEX(1,-1))": "0.1505149978319906-0.3410940884604603i", + // 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", + // 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", + // IMSQRT + "=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(\"-2-4i\")": "1.1117859405028423-1.7989074399478673i", + // IMSUB + "=IMSUB(\"5+i\",\"1+4i\")": "4-3i", + "=IMSUB(\"9+2i\",6)": "3+2i", + "=IMSUB(COMPLEX(5,2),COMPLEX(0,1))": "5+i", + // IMSUM + "=IMSUM(\"1-i\",\"5+10i\",2)": "8+9i", + "=IMSUM(COMPLEX(5,2),COMPLEX(0,1))": "5+3i", + // IMTAN + "=IMTAN(-0)": "0", + "=IMTAN(0.5)": "0.546302489843791", + "=IMTAN(\"3+0.5i\")": "-0.11162105077158344+0.46946999342588536i", + "=IMTAN(\"2-i\")": "-0.24345820118572523-1.16673625724092i", + "=IMTAN(COMPLEX(1,-1))": "0.2717525853195117-1.0839233273386948i", // OCT2BIN "=OCT2BIN(\"5\")": "101", "=OCT2BIN(\"0000000001\")": "1", @@ -1206,6 +1236,25 @@ func TestCalcCellValue(t *testing.T) { "=IMLOG10()": "IMLOG10 requires 1 argument", "=IMLOG10(\"\")": "strconv.ParseComplex: parsing \"\": invalid syntax", "=IMLOG10(0)": "#NUM!", + // IMSIN + "=IMSIN()": "IMSIN requires 1 argument", + "=IMSIN(\"\")": "strconv.ParseComplex: parsing \"\": invalid syntax", + // IMSINH + "=IMSINH()": "IMSINH requires 1 argument", + "=IMSINH(\"\")": "strconv.ParseComplex: parsing \"\": invalid syntax", + // IMSQRT + "=IMSQRT()": "IMSQRT requires 1 argument", + "=IMSQRT(\"\")": "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", + // IMSUM + "=IMSUM()": "IMSUM requires at least 1 argument", + "=IMSUM(\"\")": "strconv.ParseComplex: parsing \"\": invalid syntax", + // IMTAN + "=IMTAN()": "IMTAN requires 1 argument", + "=IMTAN(\"\")": "strconv.ParseComplex: parsing \"\": invalid syntax", // OCT2BIN "=OCT2BIN()": "OCT2BIN requires at least 1 argument", "=OCT2BIN(1,1,1)": "OCT2BIN allows at most 2 arguments", From 99963f89c70234a804521f6897354a82e996142c Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 7 Apr 2021 00:01:03 +0800 Subject: [PATCH 364/957] #65 fn: IMLOG2, IMPOWER, IMPRODUCT, IMREAL, IMSEC, and IMSECH --- calc.go | 140 +++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 57 +++++++++++++++++++++ 2 files changed, 197 insertions(+) diff --git a/calc.go b/calc.go index 521529fbaa..4d2bdccf01 100644 --- a/calc.go +++ b/calc.go @@ -302,6 +302,12 @@ var tokenPriority = map[string]int{ // IMEXP // IMLN // IMLOG10 +// IMLOG2 +// IMPOWER +// IMPRODUCT +// IMREAL +// IMSEC +// IMSECH // IMSIN // IMSINH // IMSQRT @@ -1857,6 +1863,140 @@ func (fn *formulaFuncs) IMLOG10(argsList *list.List) formulaArg { return newStringFormulaArg(cmplx2str(fmt.Sprint(num), "i")) } +// IMLOG2 function calculates the base 2 logarithm of a supplied complex +// number. The syntax of the function is: +// +// IMLOG2(inumber) +// +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) + if err != nil { + return newErrorFormulaArg(formulaErrorNUM, err.Error()) + } + num := cmplx.Log(inumber) + if cmplx.IsInf(num) { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + return newStringFormulaArg(cmplx2str(fmt.Sprint(num/cmplx.Log(2)), "i")) +} + +// IMPOWER function returns a supplied complex number, raised to a given +// power. The syntax of the function is: +// +// IMPOWER(inumber,number) +// +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) + if err != nil { + return newErrorFormulaArg(formulaErrorNUM, err.Error()) + } + number, err := strconv.ParseComplex(str2cmplx(argsList.Back().Value.(formulaArg).Value()), 128) + if err != nil { + return newErrorFormulaArg(formulaErrorNUM, err.Error()) + } + if inumber == 0 && number == 0 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + num := cmplx.Pow(inumber, number) + if cmplx.IsInf(num) { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + return newStringFormulaArg(cmplx2str(fmt.Sprint(num), "i")) +} + +// IMPRODUCT function calculates the product of two or more complex numbers. +// The syntax of the function is: +// +// IMPRODUCT(number1,[number2],...) +// +func (fn *formulaFuncs) IMPRODUCT(argsList *list.List) formulaArg { + product := complex128(1) + for arg := argsList.Front(); arg != nil; arg = arg.Next() { + token := arg.Value.(formulaArg) + switch token.Type { + case ArgString: + if token.Value() == "" { + continue + } + val, err := strconv.ParseComplex(str2cmplx(token.Value()), 128) + if err != nil { + return newErrorFormulaArg(formulaErrorNUM, err.Error()) + } + product = product * val + case ArgNumber: + product = product * complex(token.Number, 0) + case ArgMatrix: + for _, row := range token.Matrix { + for _, value := range row { + if value.Value() == "" { + continue + } + val, err := strconv.ParseComplex(str2cmplx(value.Value()), 128) + if err != nil { + return newErrorFormulaArg(formulaErrorNUM, err.Error()) + } + product = product * val + } + } + } + } + return newStringFormulaArg(cmplx2str(fmt.Sprint(product), "i")) +} + +// IMREAL function returns the real coefficient of a supplied complex number. +// The syntax of the function is: +// +// IMREAL(inumber) +// +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) + if err != nil { + return newErrorFormulaArg(formulaErrorNUM, err.Error()) + } + return newStringFormulaArg(cmplx2str(fmt.Sprint(real(inumber)), "i")) +} + +// IMSEC function returns the secant of a supplied complex number. The syntax +// of the function is: +// +// IMSEC(inumber) +// +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) + if err != nil { + return newErrorFormulaArg(formulaErrorNUM, err.Error()) + } + return newStringFormulaArg(cmplx2str(fmt.Sprint(1/cmplx.Cos(inumber)), "i")) +} + +// IMSECH function returns the hyperbolic secant of a supplied complex number. +// The syntax of the function is: +// +// IMSECH(inumber) +// +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) + if err != nil { + return newErrorFormulaArg(formulaErrorNUM, err.Error()) + } + return newStringFormulaArg(cmplx2str(fmt.Sprint(1/cmplx.Cosh(inumber)), "i")) +} + // IMSIN function returns the Sine of a supplied complex number. The syntax of // the function is: // diff --git a/calc_test.go b/calc_test.go index 590432d7f5..afbf880e6b 100644 --- a/calc_test.go +++ b/calc_test.go @@ -167,6 +167,22 @@ func TestCalcCellValue(t *testing.T) { "=IMLOG10(\"3+0.5i\")": "0.48307086636951624+0.07172315929479262i", "=IMLOG10(\"2-i\")": "0.34948500216800943-0.20135959813668655i", "=IMLOG10(COMPLEX(1,-1))": "0.1505149978319906-0.3410940884604603i", + // IMREAL + "=IMREAL(\"5+2i\")": "5", + "=IMREAL(\"2+2i\")": "2", + "=IMREAL(6)": "6", + "=IMREAL(\"3i\")": "0", + "=IMREAL(COMPLEX(4,1))": "4", + // IMSEC + "=IMSEC(0.5)": "1.139493927324549", + "=IMSEC(\"3+0.5i\")": "-0.8919131797403304+0.05875317818173977i", + "=IMSEC(\"2-i\")": "-0.4131493442669401-0.687527438655479i", + "=IMSEC(COMPLEX(1,-1))": "0.49833703055518686-0.5910838417210451i", + // 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", // IMSIN "=IMSIN(0.5)": "0.479425538604203", "=IMSIN(\"3+0.5i\")": "0.15913058529843999-0.5158804424525267i", @@ -465,6 +481,23 @@ func TestCalcCellValue(t *testing.T) { "=LOG10(0.001)": "-3", "=LOG10(25)": "1.397940008672038", "=LOG10(LOG10(100))": "0.301029995663981", + // IMLOG2 + "=IMLOG2(\"5+2i\")": "2.4289904975637864+0.5489546632866347i", + "=IMLOG2(\"2-i\")": "1.1609640474436813-0.6689021062254881i", + "=IMLOG2(6)": "2.584962500721156", + "=IMLOG2(\"3i\")": "1.584962500721156+2.266180070913597i", + "=IMLOG2(\"4+i\")": "2.04373142062517+0.3534295024167349i", + // IMPOWER + "=IMPOWER(\"2-i\",2)": "3.000000000000001-4i", + "=IMPOWER(\"2-i\",3)": "2.0000000000000018-11.000000000000002i", + "=IMPOWER(9,0.5)": "3", + "=IMPOWER(\"2+4i\",-2)": "-0.029999999999999985-0.039999999999999994i", + // IMPRODUCT + "=IMPRODUCT(3,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", // MOD "=MOD(6,4)": "2", "=MOD(6,3)": "0", @@ -1236,6 +1269,28 @@ func TestCalcCellValue(t *testing.T) { "=IMLOG10()": "IMLOG10 requires 1 argument", "=IMLOG10(\"\")": "strconv.ParseComplex: parsing \"\": invalid syntax", "=IMLOG10(0)": "#NUM!", + // IMLOG2 + "=IMLOG2()": "IMLOG2 requires 1 argument", + "=IMLOG2(\"\")": "strconv.ParseComplex: parsing \"\": invalid syntax", + "=IMLOG2(0)": "#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!", + // IMPRODUCT + "=IMPRODUCT(\"x\")": "strconv.ParseComplex: parsing \"x\": invalid syntax", + "=IMPRODUCT(A1:D1)": "strconv.ParseComplex: parsing \"Month\": invalid syntax", + // IMREAL + "=IMREAL()": "IMREAL requires 1 argument", + "=IMREAL(\"\")": "strconv.ParseComplex: parsing \"\": invalid syntax", + // IMSEC + "=IMSEC()": "IMSEC requires 1 argument", + "=IMSEC(\"\")": "strconv.ParseComplex: parsing \"\": invalid syntax", + // IMSECH + "=IMSECH()": "IMSECH requires 1 argument", + "=IMSECH(\"\")": "strconv.ParseComplex: parsing \"\": invalid syntax", // IMSIN "=IMSIN()": "IMSIN requires 1 argument", "=IMSIN(\"\")": "strconv.ParseComplex: parsing \"\": invalid syntax", @@ -1944,6 +1999,8 @@ func TestCalcCellValue(t *testing.T) { "=MDETERM(A1:B2)": "-3", // PRODUCT "=PRODUCT(Sheet1!A1:Sheet1!A1:A2,A2)": "4", + // IMPRODUCT + "=IMPRODUCT(Sheet1!A1:Sheet1!A1:A2,A2)": "4", // SUM "=A1/A3": "0.333333333333333", "=SUM(A1:A2)": "3", From 737b7839a25d530d6a1908fc6c4c33e1c047cdd6 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 8 Apr 2021 00:50:59 +0800 Subject: [PATCH 365/957] Fixed #819, read empty after streaming data writing #65 fn: IMAGINARY, IMARGUMENT, IMCONJUGATE and IMDIV --- calc.go | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++-- calc_test.go | 34 ++++++++++++++++++++++ lib.go | 3 ++ 3 files changed, 115 insertions(+), 2 deletions(-) diff --git a/calc.go b/calc.go index 4d2bdccf01..e3e7c4d700 100644 --- a/calc.go +++ b/calc.go @@ -294,11 +294,15 @@ var tokenPriority = map[string]int{ // IF // IFERROR // IMABS +// IMAGINARY +// IMARGUMENT +// IMCONJUGATE // IMCOS // IMCOSH // IMCOT // IMCSC // IMCSCH +// IMDIV // IMEXP // IMLN // IMLOG10 @@ -1712,13 +1716,61 @@ func (fn *formulaFuncs) IMABS(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "IMABS requires 1 argument") } - inumber, err := strconv.ParseComplex(strings.Replace(argsList.Front().Value.(formulaArg).Value(), "j", "i", -1), 128) + inumber, err := strconv.ParseComplex(str2cmplx(argsList.Front().Value.(formulaArg).Value()), 128) if err != nil { return newErrorFormulaArg(formulaErrorNUM, err.Error()) } return newNumberFormulaArg(cmplx.Abs(inumber)) } +// IMAGINARY function returns the imaginary coefficient of a supplied complex +// number. The syntax of the function is: +// +// IMAGINARY(inumber) +// +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) + if err != nil { + return newErrorFormulaArg(formulaErrorNUM, err.Error()) + } + return newNumberFormulaArg(imag(inumber)) +} + +// IMARGUMENT function returns the phase (also called the argument) of a +// supplied complex number. The syntax of the function is: +// +// IMARGUMENT(inumber) +// +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) + if err != nil { + return newErrorFormulaArg(formulaErrorNUM, err.Error()) + } + return newNumberFormulaArg(cmplx.Phase(inumber)) +} + +// IMCONJUGATE function returns the complex conjugate of a supplied complex +// number. The syntax of the function is: +// +// IMCONJUGATE(inumber) +// +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) + if err != nil { + return newErrorFormulaArg(formulaErrorNUM, err.Error()) + } + return newStringFormulaArg(cmplx2str(fmt.Sprint(cmplx.Conj(inumber)), "i")) +} + // IMCOS function returns the cosine of a supplied complex number. The syntax // of the function is: // @@ -1728,7 +1780,7 @@ func (fn *formulaFuncs) IMCOS(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "IMCOS requires 1 argument") } - inumber, err := strconv.ParseComplex(strings.Replace(argsList.Front().Value.(formulaArg).Value(), "j", "i", -1), 128) + inumber, err := strconv.ParseComplex(str2cmplx(argsList.Front().Value.(formulaArg).Value()), 128) if err != nil { return newErrorFormulaArg(formulaErrorNUM, err.Error()) } @@ -1807,6 +1859,30 @@ func (fn *formulaFuncs) IMCSCH(argsList *list.List) formulaArg { return newStringFormulaArg(cmplx2str(fmt.Sprint(num), "i")) } +// 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) +// +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) + if err != nil { + return newErrorFormulaArg(formulaErrorNUM, err.Error()) + } + inumber2, err := strconv.ParseComplex(str2cmplx(argsList.Back().Value.(formulaArg).Value()), 128) + if err != nil { + return newErrorFormulaArg(formulaErrorNUM, err.Error()) + } + num := inumber1 / inumber2 + if cmplx.IsInf(num) { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + return newStringFormulaArg(cmplx2str(fmt.Sprint(num), "i")) +} + // IMEXP function returns the exponential of a supplied complex number. The // syntax of the function is: // diff --git a/calc_test.go b/calc_test.go index afbf880e6b..26f8875441 100644 --- a/calc_test.go +++ b/calc_test.go @@ -134,6 +134,22 @@ func TestCalcCellValue(t *testing.T) { "=IMABS(\"2j\")": "2", "=IMABS(\"-1+2i\")": "2.23606797749979", "=IMABS(COMPLEX(-1,2,\"j\"))": "2.23606797749979", + // IMAGINARY + "=IMAGINARY(\"5+2i\")": "2", + "=IMAGINARY(\"2-i\")": "-1", + "=IMAGINARY(6)": "0", + "=IMAGINARY(\"3i\")": "3", + "=IMAGINARY(\"4+i\")": "1", + // IMARGUMENT + "=IMARGUMENT(\"5+2i\")": "0.380506377112365", + "=IMARGUMENT(\"2-i\")": "-0.463647609000806", + "=IMARGUMENT(6)": "0", + // IMCONJUGATE + "=IMCONJUGATE(\"5+2i\")": "5-2i", + "=IMCONJUGATE(\"2-i\")": "2+i", + "=IMCONJUGATE(6)": "6", + "=IMCONJUGATE(\"3i\")": "-3i", + "=IMCONJUGATE(\"4+i\")": "4-i", // IMCOS "=IMCOS(0)": "1", "=IMCOS(0.5)": "0.877582561890373", @@ -152,6 +168,10 @@ func TestCalcCellValue(t *testing.T) { "=IMCSC(\"j\")": "-0.8509181282393216i", // IMCSCH "=IMCSCH(COMPLEX(1,-1))": "0.30393100162842646+0.6215180171704284i", + // IMDIV + "=IMDIV(\"5+2i\",\"1+i\")": "3.5-1.5i", + "=IMDIV(\"2+2i\",\"2+i\")": "1.2+0.4i", + "=IMDIV(COMPLEX(5,2),COMPLEX(0,1))": "2-5i", // IMEXP "=IMEXP(0)": "1", "=IMEXP(0.5)": "1.648721270700128", @@ -1241,6 +1261,15 @@ func TestCalcCellValue(t *testing.T) { // IMABS "=IMABS()": "IMABS requires 1 argument", "=IMABS(\"\")": "strconv.ParseComplex: parsing \"\": invalid syntax", + // IMAGINARY + "=IMAGINARY()": "IMAGINARY requires 1 argument", + "=IMAGINARY(\"\")": "strconv.ParseComplex: parsing \"\": invalid syntax", + // IMARGUMENT + "=IMARGUMENT()": "IMARGUMENT requires 1 argument", + "=IMARGUMENT(\"\")": "strconv.ParseComplex: parsing \"\": invalid syntax", + // IMCONJUGATE + "=IMCONJUGATE()": "IMCONJUGATE requires 1 argument", + "=IMCONJUGATE(\"\")": "strconv.ParseComplex: parsing \"\": invalid syntax", // IMCOS "=IMCOS()": "IMCOS requires 1 argument", "=IMCOS(\"\")": "strconv.ParseComplex: parsing \"\": invalid syntax", @@ -1258,6 +1287,11 @@ func TestCalcCellValue(t *testing.T) { "=IMCSCH()": "IMCSCH requires 1 argument", "=IMCSCH(\"\")": "strconv.ParseComplex: parsing \"\": invalid syntax", "=IMCSCH(0)": "#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!", // IMEXP "=IMEXP()": "IMEXP requires 1 argument", "=IMEXP(\"\")": "strconv.ParseComplex: parsing \"\": invalid syntax", diff --git a/lib.go b/lib.go index 0c1938aa33..26d402a10a 100644 --- a/lib.go +++ b/lib.go @@ -52,6 +52,9 @@ func (f *File) readXML(name string) []byte { if content, ok := f.XLSX[name]; ok { return content } + if content, ok := f.streams[name]; ok { + return content.rawData.buf.Bytes() + } return []byte{} } From a8197485b5ca94f18f454eaae34af74500bd4dc3 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 9 Apr 2021 00:29:47 +0800 Subject: [PATCH 366/957] #65 fn: IPMT, PMT and PPMT --- calc.go | 134 +++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 44 +++++++++++++++++ 2 files changed, 178 insertions(+) diff --git a/calc.go b/calc.go index e3e7c4d700..3b67ef90bb 100644 --- a/calc.go +++ b/calc.go @@ -319,6 +319,7 @@ var tokenPriority = map[string]int{ // IMSUM // IMTAN // INT +// IPMT // ISBLANK // ISERR // ISERROR @@ -374,9 +375,11 @@ var tokenPriority = map[string]int{ // PERMUT // PERMUTATIONA // PI +// PMT // POISSON.DIST // POISSON // POWER +// PPMT // PRODUCT // PROPER // QUARTILE @@ -7156,3 +7159,134 @@ func (fn *formulaFuncs) ENCODEURL(argsList *list.List) formulaArg { token := argsList.Front().Value.(formulaArg).Value() return newStringFormulaArg(strings.Replace(url.QueryEscape(token), "+", "%20", -1)) } + +// Financial Functions + +// IPMT function calculates the interest payment, during a specific period of a +// 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]) +// +func (fn *formulaFuncs) IPMT(argsList *list.List) formulaArg { + return fn.ipmt("IPMT", argsList) +} + +// ipmt is an implementation of the formula function IPMT and PPMT. +func (fn *formulaFuncs) ipmt(name string, argsList *list.List) formulaArg { + if argsList.Len() < 4 { + return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires at least 4 arguments", name)) + } + if argsList.Len() > 6 { + return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s allows at most 6 arguments", name)) + } + rate := argsList.Front().Value.(formulaArg).ToNumber() + if rate.Type != ArgNumber { + return rate + } + per := argsList.Front().Next().Value.(formulaArg).ToNumber() + if per.Type != ArgNumber { + return per + } + nper := argsList.Front().Next().Next().Value.(formulaArg).ToNumber() + if nper.Type != ArgNumber { + return nper + } + pv := argsList.Front().Next().Next().Next().Value.(formulaArg).ToNumber() + if pv.Type != ArgNumber { + return pv + } + fv, typ := newNumberFormulaArg(0), newNumberFormulaArg(0) + if argsList.Len() >= 5 { + if fv = argsList.Front().Next().Next().Next().Next().Value.(formulaArg).ToNumber(); fv.Type != ArgNumber { + return fv + } + } + if argsList.Len() == 6 { + if typ = argsList.Back().Value.(formulaArg).ToNumber(); typ.Type != ArgNumber { + return typ + } + } + if typ.Number != 0 && typ.Number != 1 { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + } + if per.Number <= 0 || per.Number > nper.Number { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + } + args := list.New().Init() + args.PushBack(rate) + args.PushBack(nper) + args.PushBack(pv) + args.PushBack(fv) + args.PushBack(typ) + pmt, capital, interest, principal := fn.PMT(args), pv.Number, 0.0, 0.0 + for i := 1; i <= int(per.Number); i++ { + if typ.Number != 0 && i == 1 { + interest = 0 + } else { + interest = -capital * rate.Number + } + principal = pmt.Number - interest + capital += principal + } + if name == "IPMT" { + return newNumberFormulaArg(interest) + } + return newNumberFormulaArg(principal) +} + +// PMT function calculates the constant periodic payment required to pay off +// (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]) +// +func (fn *formulaFuncs) PMT(argsList *list.List) formulaArg { + if argsList.Len() < 3 { + return newErrorFormulaArg(formulaErrorVALUE, "PMT requires at least 3 arguments") + } + if argsList.Len() > 5 { + return newErrorFormulaArg(formulaErrorVALUE, "PMT allows at most 5 arguments") + } + rate := argsList.Front().Value.(formulaArg).ToNumber() + if rate.Type != ArgNumber { + return rate + } + nper := argsList.Front().Next().Value.(formulaArg).ToNumber() + if nper.Type != ArgNumber { + return nper + } + pv := argsList.Front().Next().Next().Value.(formulaArg).ToNumber() + if pv.Type != ArgNumber { + return pv + } + fv, typ := newNumberFormulaArg(0), newNumberFormulaArg(0) + if argsList.Len() >= 4 { + if fv = argsList.Front().Next().Next().Next().Value.(formulaArg).ToNumber(); fv.Type != ArgNumber { + return fv + } + } + if argsList.Len() == 5 { + if typ = argsList.Back().Value.(formulaArg).ToNumber(); typ.Type != ArgNumber { + return typ + } + } + if typ.Number != 0 && typ.Number != 1 { + 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) + return newNumberFormulaArg(p) + } + return newNumberFormulaArg((-pv.Number - fv.Number) / nper.Number) +} + +// PPMT function calculates the payment on the principal, during a specific +// 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]) +// +func (fn *formulaFuncs) PPMT(argsList *list.List) formulaArg { + return fn.ipmt("PPMT", argsList) +} diff --git a/calc_test.go b/calc_test.go index 26f8875441..a3d9117c09 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1132,6 +1132,16 @@ func TestCalcCellValue(t *testing.T) { // Web Functions // ENCODEURL "=ENCODEURL(\"https://xuri.me/excelize/en/?q=Save As\")": "https%3A%2F%2Fxuri.me%2Fexcelize%2Fen%2F%3Fq%3DSave%20As", + // Financial Functions + // IPMT + "=IPMT(0.05/12,2,60,50000)": "-205.26988187971995", + "=IPMT(0.035/4,2,8,0,5000,1)": "5.257455237829077", + // PMT + "=PMT(0,8,0,5000,1)": "-625", + "=PMT(0.035/4,8,0,5000,1)": "-600.8520271804658", + // PPMT + "=PPMT(0.05/12,2,60,50000)": "-738.2918003208238", + "=PPMT(0.035/4,2,8,0,5000,1)": "-606.1094824182949", } for formula, expected := range mathCalc { f := prepareCalcData(cellData) @@ -2019,6 +2029,40 @@ func TestCalcCellValue(t *testing.T) { // Web Functions // ENCODEURL "=ENCODEURL()": "ENCODEURL requires 1 argument", + // Financial Functions + // 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", + // 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", + // 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", } for formula, expected := range mathCalcError { f := prepareCalcData(cellData) From 1559dd31be6f394ed78129d763c4677cc26bb51c Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 10 Apr 2021 00:15:39 +0800 Subject: [PATCH 367/957] Support specifies that each data marker in the series has a different color --- chart.go | 3 +++ drawing.go | 12 ++++++------ xmlChart.go | 19 ++++++++++--------- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/chart.go b/chart.go index 1e2e04655d..3ac460b8ac 100644 --- a/chart.go +++ b/chart.go @@ -494,6 +494,7 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { Title: formatChartTitle{ Name: " ", }, + VaryColors: true, ShowBlanksAs: "gap", } err := json.Unmarshal([]byte(formatSet), &format) @@ -711,6 +712,8 @@ func parseFormatChartSet(formatSet string) (*formatChart, 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. +// // 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: diff --git a/drawing.go b/drawing.go index 0e5d9480e3..8b517f9068 100644 --- a/drawing.go +++ b/drawing.go @@ -271,7 +271,7 @@ func (f *File) drawBaseChart(formatSet *formatChart) *cPlotArea { Val: stringPtr("clustered"), }, VaryColors: &attrValBool{ - Val: boolPtr(true), + Val: boolPtr(formatSet.VaryColors), }, Ser: f.drawChartSeries(formatSet), Shape: f.drawChartShape(formatSet), @@ -515,7 +515,7 @@ func (f *File) drawDoughnutChart(formatSet *formatChart) *cPlotArea { return &cPlotArea{ DoughnutChart: &cCharts{ VaryColors: &attrValBool{ - Val: boolPtr(true), + Val: boolPtr(formatSet.VaryColors), }, Ser: f.drawChartSeries(formatSet), HoleSize: &attrValInt{Val: intPtr(75)}, @@ -555,7 +555,7 @@ func (f *File) drawPieChart(formatSet *formatChart) *cPlotArea { return &cPlotArea{ PieChart: &cCharts{ VaryColors: &attrValBool{ - Val: boolPtr(true), + Val: boolPtr(formatSet.VaryColors), }, Ser: f.drawChartSeries(formatSet), }, @@ -568,7 +568,7 @@ func (f *File) drawPie3DChart(formatSet *formatChart) *cPlotArea { return &cPlotArea{ Pie3DChart: &cCharts{ VaryColors: &attrValBool{ - Val: boolPtr(true), + Val: boolPtr(formatSet.VaryColors), }, Ser: f.drawChartSeries(formatSet), }, @@ -584,7 +584,7 @@ func (f *File) drawPieOfPieChart(formatSet *formatChart) *cPlotArea { Val: stringPtr("pie"), }, VaryColors: &attrValBool{ - Val: boolPtr(true), + Val: boolPtr(formatSet.VaryColors), }, Ser: f.drawChartSeries(formatSet), SerLines: &attrValString{}, @@ -601,7 +601,7 @@ func (f *File) drawBarOfPieChart(formatSet *formatChart) *cPlotArea { Val: stringPtr("bar"), }, VaryColors: &attrValBool{ - Val: boolPtr(true), + Val: boolPtr(formatSet.VaryColors), }, Ser: f.drawChartSeries(formatSet), SerLines: &attrValString{}, diff --git a/xmlChart.go b/xmlChart.go index 637d954112..85a2c5c040 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -553,15 +553,16 @@ type formatChartDimension struct { // 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"` - XAxis formatChartAxis `json:"x_axis"` - YAxis formatChartAxis `json:"y_axis"` - Chartarea 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"` + Chartarea struct { Border struct { None bool `json:"none"` } `json:"border"` From a13b21fe07e7d19a40529837b7148bc0261b9ae7 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 11 Apr 2021 00:03:25 +0800 Subject: [PATCH 368/957] fixed the negative values series missing chart color, #65 fn: CUMIPMT and CUMPRINC --- calc.go | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 28 ++++++++++++++++++++ drawing.go | 21 ++++++++------- 3 files changed, 114 insertions(+), 10 deletions(-) diff --git a/calc.go b/calc.go index 3b67ef90bb..aca69da664 100644 --- a/calc.go +++ b/calc.go @@ -261,6 +261,8 @@ var tokenPriority = map[string]int{ // COUNTBLANK // CSC // CSCH +// CUMIPMT +// CUMPRINC // DATE // DATEDIF // DEC2BIN @@ -7162,6 +7164,79 @@ func (fn *formulaFuncs) ENCODEURL(argsList *list.List) formulaArg { // Financial Functions +// 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) +// +func (fn *formulaFuncs) CUMIPMT(argsList *list.List) formulaArg { + return fn.cumip("CUMIPMT", argsList) +} + +// CUMPRINC function calculates the cumulative payment on the principal of a +// loan or investment, between two specified periods. The syntax of the +// function is: +// +// CUMPRINC(rate,nper,pv,start_period,end_period,type) +// +func (fn *formulaFuncs) CUMPRINC(argsList *list.List) formulaArg { + return fn.cumip("CUMPRINC", argsList) +} + +// cumip is an implementation of the formula function CUMIPMT and CUMPRINC. +func (fn *formulaFuncs) cumip(name string, argsList *list.List) formulaArg { + if argsList.Len() != 6 { + return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires 6 arguments", name)) + } + rate := argsList.Front().Value.(formulaArg).ToNumber() + if rate.Type != ArgNumber { + return rate + } + nper := argsList.Front().Next().Value.(formulaArg).ToNumber() + if nper.Type != ArgNumber { + return nper + } + pv := argsList.Front().Next().Next().Value.(formulaArg).ToNumber() + if pv.Type != ArgNumber { + return pv + } + start := argsList.Back().Prev().Prev().Value.(formulaArg).ToNumber() + if start.Type != ArgNumber { + return start + } + end := argsList.Back().Prev().Value.(formulaArg).ToNumber() + if end.Type != ArgNumber { + return end + } + typ := argsList.Back().Value.(formulaArg).ToNumber() + if typ.Type != ArgNumber { + return typ + } + if typ.Number != 0 && typ.Number != 1 { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + } + if start.Number < 1 || start.Number > end.Number { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + } + num, ipmt := 0.0, newNumberFormulaArg(0) + for per := start.Number; per <= end.Number; per++ { + args := list.New().Init() + args.PushBack(rate) + args.PushBack(newNumberFormulaArg(per)) + args.PushBack(nper) + args.PushBack(pv) + args.PushBack(newNumberFormulaArg(0)) + args.PushBack(typ) + if name == "CUMIPMT" { + ipmt = fn.IPMT(args) + } else { + ipmt = fn.PPMT(args) + } + num += ipmt.Number + } + return newNumberFormulaArg(num) +} + // IPMT function calculates the interest payment, during a specific period of a // loan or investment that is paid in constant periodic payments, with a // constant interest rate. The syntax of the function is: diff --git a/calc_test.go b/calc_test.go index a3d9117c09..d105b1448e 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1133,6 +1133,12 @@ func TestCalcCellValue(t *testing.T) { // ENCODEURL "=ENCODEURL(\"https://xuri.me/excelize/en/?q=Save As\")": "https%3A%2F%2Fxuri.me%2Fexcelize%2Fen%2F%3Fq%3DSave%20As", // Financial Functions + // CUMIPMT + "=CUMIPMT(0.05/12,60,50000,1,12,0)": "-2294.97753732664", + "=CUMIPMT(0.05/12,60,50000,13,24,0)": "-1833.1000665738893", + // CUMPRINC + "=CUMPRINC(0.05/12,60,50000,1,12,0)": "-9027.762649079885", + "=CUMPRINC(0.05/12,60,50000,13,24,0)": "-9489.640119832635", // IPMT "=IPMT(0.05/12,2,60,50000)": "-205.26988187971995", "=IPMT(0.035/4,2,8,0,5000,1)": "5.257455237829077", @@ -2030,6 +2036,28 @@ func TestCalcCellValue(t *testing.T) { // ENCODEURL "=ENCODEURL()": "ENCODEURL requires 1 argument", // Financial Functions + // 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", + // 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", // IPMT "=IPMT()": "IPMT requires at least 4 arguments", "=IPMT(0,0,0,0,0,0,0)": "IPMT allows at most 6 arguments", diff --git a/drawing.go b/drawing.go index 8b517f9068..1d9a63b257 100644 --- a/drawing.go +++ b/drawing.go @@ -744,16 +744,17 @@ func (f *File) drawChartSeries(formatSet *formatChart) *[]cSer { F: formatSet.Series[k].Name, }, }, - SpPr: f.drawChartSeriesSpPr(k, formatSet), - Marker: f.drawChartSeriesMarker(k, formatSet), - DPt: f.drawChartSeriesDPt(k, formatSet), - DLbls: f.drawChartSeriesDLbls(formatSet), - Cat: f.drawChartSeriesCat(formatSet.Series[k], formatSet), - 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), + SpPr: f.drawChartSeriesSpPr(k, formatSet), + Marker: f.drawChartSeriesMarker(k, formatSet), + DPt: f.drawChartSeriesDPt(k, formatSet), + DLbls: f.drawChartSeriesDLbls(formatSet), + InvertIfNegative: &attrValBool{Val: boolPtr(false)}, + Cat: f.drawChartSeriesCat(formatSet.Series[k], formatSet), + 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), }) } return &ser From 68dd5b345c666bc76aedfe1ae396b1fa04238ba6 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 13 Apr 2021 00:01:13 +0800 Subject: [PATCH 369/957] #65 fn: DB, DDB, DOLLARDE, DOLLARFR, EFFECT, ISPMT and NOMINAL --- calc.go | 246 +++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 74 ++++++++++++++++ 2 files changed, 320 insertions(+) diff --git a/calc.go b/calc.go index aca69da664..a4b60132d6 100644 --- a/calc.go +++ b/calc.go @@ -265,11 +265,16 @@ var tokenPriority = map[string]int{ // CUMPRINC // DATE // DATEDIF +// DB +// DDB // DEC2BIN // DEC2HEX // DEC2OCT // DECIMAL // DEGREES +// DOLLARDE +// DOLLARFR +// EFFECT // ENCODEURL // EVEN // EXACT @@ -332,6 +337,7 @@ var tokenPriority = map[string]int{ // ISODD // ISTEXT // ISO.CEILING +// ISPMT // KURT // LARGE // LCM @@ -357,6 +363,7 @@ var tokenPriority = map[string]int{ // MUNIT // N // NA +// NOMINAL // NORM.DIST // NORMDIST // NORM.INV @@ -7237,6 +7244,185 @@ func (fn *formulaFuncs) cumip(name string, argsList *list.List) formulaArg { return newNumberFormulaArg(num) } +// DB function calculates the depreciation of an asset, using the Fixed +// Declining Balance Method, for each period of the asset's lifetime. The +// syntax of the function is: +// +// 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") + } + if argsList.Len() > 5 { + return newErrorFormulaArg(formulaErrorVALUE, "DB allows at most 5 arguments") + } + cost := argsList.Front().Value.(formulaArg).ToNumber() + if cost.Type != ArgNumber { + return cost + } + salvage := argsList.Front().Next().Value.(formulaArg).ToNumber() + if salvage.Type != ArgNumber { + return salvage + } + life := argsList.Front().Next().Next().Value.(formulaArg).ToNumber() + if life.Type != ArgNumber { + return life + } + period := argsList.Front().Next().Next().Next().Value.(formulaArg).ToNumber() + if period.Type != ArgNumber { + return period + } + month := newNumberFormulaArg(12) + if argsList.Len() == 5 { + if month = argsList.Back().Value.(formulaArg).ToNumber(); month.Type != ArgNumber { + return month + } + } + if cost.Number == 0 { + return newNumberFormulaArg(0) + } + if (cost.Number <= 0) || ((salvage.Number / cost.Number) < 0) || (life.Number <= 0) || (period.Number < 1) || (month.Number < 1) { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + } + dr := 1 - math.Pow(salvage.Number/cost.Number, 1/life.Number) + dr = math.Round(dr*1000) / 1000 + pd, depreciation := 0.0, 0.0 + for per := 1; per <= int(period.Number); per++ { + if per == 1 { + depreciation = cost.Number * dr * month.Number / 12 + } else if per == int(life.Number+1) { + depreciation = (cost.Number - pd) * dr * (12 - month.Number) / 12 + } else { + depreciation = (cost.Number - pd) * dr + } + pd += depreciation + } + return newNumberFormulaArg(depreciation) +} + +// DDB function calculates the depreciation of an asset, using the Double +// Declining Balance Method, or another specified depreciation rate. The +// syntax of the function is: +// +// 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") + } + if argsList.Len() > 5 { + return newErrorFormulaArg(formulaErrorVALUE, "DDB allows at most 5 arguments") + } + cost := argsList.Front().Value.(formulaArg).ToNumber() + if cost.Type != ArgNumber { + return cost + } + salvage := argsList.Front().Next().Value.(formulaArg).ToNumber() + if salvage.Type != ArgNumber { + return salvage + } + life := argsList.Front().Next().Next().Value.(formulaArg).ToNumber() + if life.Type != ArgNumber { + return life + } + period := argsList.Front().Next().Next().Next().Value.(formulaArg).ToNumber() + if period.Type != ArgNumber { + return period + } + factor := newNumberFormulaArg(2) + if argsList.Len() == 5 { + if factor = argsList.Back().Value.(formulaArg).ToNumber(); factor.Type != ArgNumber { + return factor + } + } + if cost.Number == 0 { + return newNumberFormulaArg(0) + } + if (cost.Number <= 0) || ((salvage.Number / cost.Number) < 0) || (life.Number <= 0) || (period.Number < 1) || (factor.Number <= 0.0) || (period.Number > life.Number) { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + } + 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)) + pd += depreciation + } + return newNumberFormulaArg(depreciation) +} + +// 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) +// +func (fn *formulaFuncs) DOLLARDE(argsList *list.List) formulaArg { + return fn.dollar("DOLLARDE", argsList) +} + +// DOLLARFR function converts a dollar value in decimal notation, into a +// dollar value that is expressed in fractional notation. The syntax of the +// function is: +// +// DOLLARFR(decimal_dollar,fraction) +// +func (fn *formulaFuncs) DOLLARFR(argsList *list.List) formulaArg { + return fn.dollar("DOLLARFR", argsList) +} + +// dollar is an implementation of the formula function DOLLARDE and DOLLARFR. +func (fn *formulaFuncs) dollar(name string, argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires 2 arguments", name)) + } + dollar := argsList.Front().Value.(formulaArg).ToNumber() + if dollar.Type != ArgNumber { + return dollar + } + frac := argsList.Back().Value.(formulaArg).ToNumber() + if frac.Type != ArgNumber { + return frac + } + if frac.Number < 0 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + if frac.Number == 0 { + return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV) + } + cents := math.Mod(dollar.Number, 1) + if name == "DOLLARDE" { + cents /= frac.Number + cents *= math.Pow(10, math.Ceil(math.Log10(frac.Number))) + } else { + cents *= frac.Number + cents *= math.Pow(10, -math.Ceil(math.Log10(frac.Number))) + } + return newNumberFormulaArg(math.Floor(dollar.Number) + cents) +} + +// EFFECT function returns the effective annual interest rate for a given +// nominal interest rate and number of compounding periods per year. The +// syntax of the function is: +// +// EFFECT(nominal_rate,npery) +// +func (fn *formulaFuncs) EFFECT(argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, "EFFECT requires 2 arguments") + } + rate := argsList.Front().Value.(formulaArg).ToNumber() + if rate.Type != ArgNumber { + return rate + } + npery := argsList.Back().Value.(formulaArg).ToNumber() + if npery.Type != ArgNumber { + return npery + } + if rate.Number <= 0 || npery.Number < 1 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + return newNumberFormulaArg(math.Pow((1+rate.Number/npery.Number), npery.Number) - 1) +} + // IPMT function calculates the interest payment, during a specific period of a // loan or investment that is paid in constant periodic payments, with a // constant interest rate. The syntax of the function is: @@ -7310,6 +7496,66 @@ func (fn *formulaFuncs) ipmt(name string, argsList *list.List) formulaArg { return newNumberFormulaArg(principal) } +// 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) +// +func (fn *formulaFuncs) ISPMT(argsList *list.List) formulaArg { + if argsList.Len() != 4 { + return newErrorFormulaArg(formulaErrorVALUE, "ISPMT requires 4 arguments") + } + rate := argsList.Front().Value.(formulaArg).ToNumber() + if rate.Type != ArgNumber { + return rate + } + per := argsList.Front().Next().Value.(formulaArg).ToNumber() + if per.Type != ArgNumber { + return per + } + nper := argsList.Back().Prev().Value.(formulaArg).ToNumber() + if nper.Type != ArgNumber { + return nper + } + pv := argsList.Back().Value.(formulaArg).ToNumber() + if pv.Type != ArgNumber { + return pv + } + pr, payment, num := pv.Number, pv.Number/nper.Number, 0.0 + for i := 0; i <= int(per.Number); i++ { + num = rate.Number * pr * -1 + pr -= payment + if i == int(nper.Number) { + num = 0 + } + } + return newNumberFormulaArg(num) +} + +// NOMINAL function returns the nominal interest rate for a given effective +// interest rate and number of compounding periods per year. The syntax of +// the function is: +// +// NOMINAL(effect_rate,npery) +// +func (fn *formulaFuncs) NOMINAL(argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, "NOMINAL requires 2 arguments") + } + rate := argsList.Front().Value.(formulaArg).ToNumber() + if rate.Type != ArgNumber { + return rate + } + npery := argsList.Back().Value.(formulaArg).ToNumber() + if npery.Type != ArgNumber { + return npery + } + if rate.Number <= 0 || npery.Number < 1 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + return newNumberFormulaArg(npery.Number * (math.Pow(rate.Number+1, 1/npery.Number) - 1)) +} + // PMT function calculates the constant periodic payment required to pay off // (or partially pay off) a loan or investment, with a constant interest // rate, over a specified period. The syntax of the function is: diff --git a/calc_test.go b/calc_test.go index d105b1448e..74cc627ddb 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1139,9 +1139,35 @@ func TestCalcCellValue(t *testing.T) { // CUMPRINC "=CUMPRINC(0.05/12,60,50000,1,12,0)": "-9027.762649079885", "=CUMPRINC(0.05/12,60,50000,13,24,0)": "-9489.640119832635", + // 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", + // DDB + "=DDB(0,1000,5,1)": "0", + "=DDB(10000,1000,5,1)": "4000", + "=DDB(10000,1000,5,2)": "2400", + "=DDB(10000,1000,5,3)": "1440", + "=DDB(10000,1000,5,4)": "864", + "=DDB(10000,1000,5,5)": "296", + // DOLLARDE + "=DOLLARDE(1.01,16)": "1.0625", + // DOLLARFR + "=DOLLARFR(1.0625,16)": "1.01", + // EFFECT + "=EFFECT(0.1,4)": "0.103812890625", + "=EFFECT(0.025,2)": "0.02515625", // IPMT "=IPMT(0.05/12,2,60,50000)": "-205.26988187971995", "=IPMT(0.035/4,2,8,0,5000,1)": "5.257455237829077", + // 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", + // NOMINAL + "=NOMINAL(0.025,12)": "0.024718035238113", // PMT "=PMT(0,8,0,5000,1)": "-625", "=PMT(0.035/4,8,0,5000,1)": "-600.8520271804658", @@ -2058,6 +2084,42 @@ func TestCalcCellValue(t *testing.T) { "=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", + // 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", + // 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", + // 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!", + // 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!", + // 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!", // IPMT "=IPMT()": "IPMT requires at least 4 arguments", "=IPMT(0,0,0,0,0,0,0)": "IPMT allows at most 6 arguments", @@ -2070,6 +2132,18 @@ func TestCalcCellValue(t *testing.T) { "=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", + // 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", + // 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!", // PMT "=PMT()": "PMT requires at least 3 arguments", "=PMT(0,0,0,0,0,0)": "PMT allows at most 5 arguments", From 471f4f8d2ba13a7ee50039aef0bc3df16c329818 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 14 Apr 2021 00:39:18 +0800 Subject: [PATCH 370/957] #65 fn: FV, FVSCHEDULE, NPER, NPV and PDURATION --- calc.go | 180 +++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 47 ++++++++++++++ 2 files changed, 227 insertions(+) diff --git a/calc.go b/calc.go index a4b60132d6..a1a0dcb458 100644 --- a/calc.go +++ b/calc.go @@ -290,6 +290,8 @@ var tokenPriority = map[string]int{ // FLOOR // FLOOR.MATH // FLOOR.PRECISE +// FV +// FVSCHEDULE // GAMMA // GAMMALN // GCD @@ -374,11 +376,14 @@ var tokenPriority = map[string]int{ // NORMSINV // NOT // NOW +// NPER +// NPV // OCT2BIN // OCT2DEC // OCT2HEX // ODD // OR +// PDURATION // PERCENTILE.INC // PERCENTILE // PERMUT @@ -7423,6 +7428,78 @@ func (fn *formulaFuncs) EFFECT(argsList *list.List) formulaArg { return newNumberFormulaArg(math.Pow((1+rate.Number/npery.Number), npery.Number) - 1) } +// FV function calculates the Future Value of an investment with periodic +// constant payments and a constant interest rate. The syntax of the function +// is: +// +// 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") + } + if argsList.Len() > 5 { + return newErrorFormulaArg(formulaErrorVALUE, "FV allows at most 5 arguments") + } + rate := argsList.Front().Value.(formulaArg).ToNumber() + if rate.Type != ArgNumber { + return rate + } + nper := argsList.Front().Next().Value.(formulaArg).ToNumber() + if nper.Type != ArgNumber { + return nper + } + pmt := argsList.Front().Next().Next().Value.(formulaArg).ToNumber() + if pmt.Type != ArgNumber { + return pmt + } + pv, typ := newNumberFormulaArg(0), newNumberFormulaArg(0) + if argsList.Len() >= 4 { + if pv = argsList.Front().Next().Next().Next().Value.(formulaArg).ToNumber(); pv.Type != ArgNumber { + return pv + } + } + if argsList.Len() == 5 { + if typ = argsList.Back().Value.(formulaArg).ToNumber(); typ.Type != ArgNumber { + return typ + } + } + if typ.Number != 0 && typ.Number != 1 { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + } + if rate.Number != 0 { + return newNumberFormulaArg(-pv.Number*math.Pow(1+rate.Number, nper.Number) - pmt.Number*(1+rate.Number*typ.Number)*(math.Pow(1+rate.Number, nper.Number)-1)/rate.Number) + } + return newNumberFormulaArg(-pv.Number - pmt.Number*nper.Number) +} + +// FVSCHEDULE function calculates the Future Value of an investment with a +// variable interest rate. The syntax of the function is: +// +// FVSCHEDULE(principal,schedule) +// +func (fn *formulaFuncs) FVSCHEDULE(argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, "FVSCHEDULE requires 2 arguments") + } + pri := argsList.Front().Value.(formulaArg).ToNumber() + if pri.Type != ArgNumber { + return pri + } + principal := pri.Number + for _, arg := range argsList.Back().Value.(formulaArg).ToList() { + if arg.Value() == "" { + continue + } + rate := arg.ToNumber() + if rate.Type != ArgNumber { + return rate + } + principal *= (1 + rate.Number) + } + return newNumberFormulaArg(principal) +} + // IPMT function calculates the interest payment, during a specific period of a // loan or investment that is paid in constant periodic payments, with a // constant interest rate. The syntax of the function is: @@ -7556,6 +7633,109 @@ func (fn *formulaFuncs) NOMINAL(argsList *list.List) formulaArg { return newNumberFormulaArg(npery.Number * (math.Pow(rate.Number+1, 1/npery.Number) - 1)) } +// NPER function calculates the number of periods required to pay off a loan, +// for a constant periodic payment and a constant interest rate. The syntax +// of the function is: +// +// 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") + } + if argsList.Len() > 5 { + return newErrorFormulaArg(formulaErrorVALUE, "NPER allows at most 5 arguments") + } + rate := argsList.Front().Value.(formulaArg).ToNumber() + if rate.Type != ArgNumber { + return rate + } + pmt := argsList.Front().Next().Value.(formulaArg).ToNumber() + if pmt.Type != ArgNumber { + return pmt + } + pv := argsList.Front().Next().Next().Value.(formulaArg).ToNumber() + if pv.Type != ArgNumber { + return pv + } + fv, typ := newNumberFormulaArg(0), newNumberFormulaArg(0) + if argsList.Len() >= 4 { + if fv = argsList.Front().Next().Next().Next().Value.(formulaArg).ToNumber(); fv.Type != ArgNumber { + return fv + } + } + if argsList.Len() == 5 { + if typ = argsList.Back().Value.(formulaArg).ToNumber(); typ.Type != ArgNumber { + return typ + } + } + if typ.Number != 0 && typ.Number != 1 { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + } + if pmt.Number == 0 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + if rate.Number != 0 { + p := math.Log((pmt.Number*(1+rate.Number*typ.Number)/rate.Number-fv.Number)/(pv.Number+pmt.Number*(1+rate.Number*typ.Number)/rate.Number)) / math.Log(1+rate.Number) + return newNumberFormulaArg(p) + } + return newNumberFormulaArg((-pv.Number - fv.Number) / pmt.Number) +} + +// NPV function calculates the Net Present Value of an investment, based on a +// supplied discount rate, and a series of future payments and income. The +// syntax of the function is: +// +// 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") + } + rate := argsList.Front().Value.(formulaArg).ToNumber() + if rate.Type != ArgNumber { + return rate + } + val, i := 0.0, 1 + for arg := argsList.Front().Next(); arg != nil; arg = arg.Next() { + num := arg.Value.(formulaArg).ToNumber() + if num.Type != ArgNumber { + continue + } + val += num.Number / math.Pow(1+rate.Number, float64(i)) + i++ + } + return newNumberFormulaArg(val) +} + +// PDURATION function calculates the number of periods required for an +// investment to reach a specified future value. The syntax of the function +// is: +// +// PDURATION(rate,pv,fv) +// +func (fn *formulaFuncs) PDURATION(argsList *list.List) formulaArg { + if argsList.Len() != 3 { + return newErrorFormulaArg(formulaErrorVALUE, "PDURATION requires 3 arguments") + } + rate := argsList.Front().Value.(formulaArg).ToNumber() + if rate.Type != ArgNumber { + return rate + } + pv := argsList.Front().Next().Value.(formulaArg).ToNumber() + if pv.Type != ArgNumber { + return pv + } + fv := argsList.Back().Value.(formulaArg).ToNumber() + if fv.Type != ArgNumber { + return fv + } + if rate.Number <= 0 || pv.Number <= 0 || fv.Number <= 0 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + return newNumberFormulaArg((math.Log(fv.Number) - math.Log(pv.Number)) / math.Log(1+rate.Number)) +} + // PMT function calculates the constant periodic payment required to pay off // (or partially pay off) a loan or investment, with a constant interest // rate, over a specified period. The syntax of the function is: diff --git a/calc_test.go b/calc_test.go index 74cc627ddb..e80e8b943b 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1159,6 +1159,13 @@ func TestCalcCellValue(t *testing.T) { // 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,16,-2000)": "32000", + // FVSCHEDULE + "=FVSCHEDULE(10000,A1:A5)": "240000", + "=FVSCHEDULE(10000,0.5)": "15000", // IPMT "=IPMT(0.05/12,2,60,50000)": "-205.26988187971995", "=IPMT(0.035/4,2,8,0,5000,1)": "5.257455237829077", @@ -1168,6 +1175,14 @@ func TestCalcCellValue(t *testing.T) { "=ISPMT(0.05/12,2,1,50000)": "208.33333333333334", // NOMINAL "=NOMINAL(0.025,12)": "0.024718035238113", + // NPER + "=NPER(0.04,-6000,50000)": "10.338035071507665", + "=NPER(0,-6000,50000)": "8.333333333333334", + "=NPER(0.06/4,-2000,60000,30000,1)": "52.794773709274764", + // NPV + "=NPV(0.02,-5000,\"\",800)": "-4133.025759323337", + // PDURATION + "=PDURATION(0.04,10000,15000)": "10.33803507150765", // PMT "=PMT(0,8,0,5000,1)": "-625", "=PMT(0.035/4,8,0,5000,1)": "-600.8520271804658", @@ -2120,6 +2135,19 @@ func TestCalcCellValue(t *testing.T) { "=EFFECT(0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", "=EFFECT(0,0)": "#NUM!", "=EFFECT(1,0)": "#NUM!", + // 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", + // FVSCHEDULE + "=FVSCHEDULE()": "FVSCHEDULE requires 2 arguments", + "=FVSCHEDULE(\"\",0)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=FVSCHEDULE(0,\"x\")": "strconv.ParseFloat: parsing \"x\": invalid syntax", // IPMT "=IPMT()": "IPMT requires at least 4 arguments", "=IPMT(0,0,0,0,0,0,0)": "IPMT allows at most 6 arguments", @@ -2144,6 +2172,25 @@ func TestCalcCellValue(t *testing.T) { "=NOMINAL(0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", "=NOMINAL(0,0)": "#NUM!", "=NOMINAL(1,0)": "#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", + // NPV + "=NPV()": "NPV requires at least 2 arguments", + "=NPV(\"\",0)": "strconv.ParseFloat: parsing \"\": invalid syntax", + // 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!", // PMT "=PMT()": "PMT requires at least 3 arguments", "=PMT(0,0,0,0,0,0)": "PMT allows at most 5 arguments", From 80d832022f8633b2829eb3586f35866d393f1959 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 16 Apr 2021 14:45:45 +0000 Subject: [PATCH 371/957] #65 fn: IRR nad MIRR --- calc.go | 109 +++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 55 ++++++++++++++++++++++++++ 2 files changed, 164 insertions(+) diff --git a/calc.go b/calc.go index a1a0dcb458..ee81dcf461 100644 --- a/calc.go +++ b/calc.go @@ -55,6 +55,8 @@ const ( // displays only the leading 15 figures. In the second line, the number one // is added to the fraction, and again Excel displays only 15 figures. const numericPrecision = 1000000000000000 +const maxFinancialIterations = 128 +const financialPercision = 1.0e-08 // cellRef defines the structure of a cell reference. type cellRef struct { @@ -329,6 +331,7 @@ var tokenPriority = map[string]int{ // IMTAN // INT // IPMT +// IRR // ISBLANK // ISERR // ISERROR @@ -359,6 +362,7 @@ var tokenPriority = map[string]int{ // MIDB // MIN // MINA +// MIRR // MOD // MROUND // MULTINOMIAL @@ -7573,6 +7577,76 @@ func (fn *formulaFuncs) ipmt(name string, argsList *list.List) formulaArg { return newNumberFormulaArg(principal) } +// IRR function returns the Internal Rate of Return for a supplied series of +// 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]) +// +func (fn *formulaFuncs) IRR(argsList *list.List) formulaArg { + if argsList.Len() < 1 { + return newErrorFormulaArg(formulaErrorVALUE, "IRR requires at least 1 argument") + } + if argsList.Len() > 2 { + return newErrorFormulaArg(formulaErrorVALUE, "IRR allows at most 2 arguments") + } + values, guess := argsList.Front().Value.(formulaArg).ToList(), newNumberFormulaArg(0.1) + if argsList.Len() > 1 { + if guess = argsList.Back().Value.(formulaArg).ToNumber(); guess.Type != ArgNumber { + return guess + } + } + x1, x2 := newNumberFormulaArg(0), guess + args := list.New().Init() + args.PushBack(x1) + for _, v := range values { + args.PushBack(v) + } + f1 := fn.NPV(args) + args.Front().Value = x2 + f2 := fn.NPV(args) + for i := 0; i < maxFinancialIterations; i++ { + if f1.Number*f2.Number < 0 { + break + } + 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) + continue + } + x2.Number += 1.6 * (x2.Number - x1.Number) + args.Front().Value = x2 + f2 = fn.NPV(args) + } + if f1.Number*f2.Number > 0 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + args.Front().Value = x1 + f := fn.NPV(args) + var rtb, dx, xMid, fMid float64 + if f.Number < 0 { + rtb = x1.Number + dx = x2.Number - x1.Number + } else { + rtb = x2.Number + dx = x1.Number - x2.Number + } + for i := 0; i < maxFinancialIterations; i++ { + dx *= 0.5 + xMid = rtb + dx + args.Front().Value = newNumberFormulaArg(xMid) + fMid = fn.NPV(args).Number + if fMid <= 0 { + rtb = xMid + } + if math.Abs(fMid) < financialPercision || math.Abs(dx) < financialPercision { + break + } + } + return newNumberFormulaArg(xMid) +} + // ISPMT function calculates the interest paid during a specific period of a // loan or investment. The syntax of the function is: // @@ -7609,6 +7683,41 @@ func (fn *formulaFuncs) ISPMT(argsList *list.List) formulaArg { return newNumberFormulaArg(num) } +// MIRR function returns the Modified Internal Rate of Return for a supplied +// series of periodic cash flows (i.e. a set of values, which includes an +// initial investment value and a series of net income values). The syntax of +// the function is: +// +// 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") + } + values := argsList.Front().Value.(formulaArg).ToList() + financeRate := argsList.Front().Next().Value.(formulaArg).ToNumber() + if financeRate.Type != ArgNumber { + return financeRate + } + reinvestRate := argsList.Back().Value.(formulaArg).ToNumber() + if reinvestRate.Type != ArgNumber { + return reinvestRate + } + n, fr, rr, npvPos, npvNeg := len(values), 1+financeRate.Number, 1+reinvestRate.Number, 0.0, 0.0 + for i, v := range values { + val := v.ToNumber() + if val.Number >= 0 { + npvPos += val.Number / math.Pow(float64(rr), float64(i)) + continue + } + npvNeg += val.Number / math.Pow(float64(fr), float64(i)) + } + if npvNeg == 0 || npvPos == 0 || reinvestRate.Number <= -1 { + return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV) + } + return newNumberFormulaArg(math.Pow(-npvPos*math.Pow(rr, float64(n))/(npvNeg*rr), 1/(float64(n)-1)) - 1) +} + // NOMINAL function returns the nominal interest rate for a given effective // interest rate and number of compounding periods per year. The syntax of // the function is: diff --git a/calc_test.go b/calc_test.go index e80e8b943b..a6d5f97443 100644 --- a/calc_test.go +++ b/calc_test.go @@ -2518,3 +2518,58 @@ func TestCalcHLOOKUP(t *testing.T) { 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) + formulaList := map[string]string{ + "=IRR(A1:A4)": "-0.136189509034157", + "=IRR(A1:A6)": "0.130575760006905", + "=IRR(A1:A4,-0.1)": "-0.136189514994621", + } + 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) + } + 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!", + } + 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) + } +} + +func TestCalcMIRR(t *testing.T) { + cellData := [][]interface{}{{-100}, {18}, {22.5}, {28}, {35.5}, {45}} + f := prepareCalcData(cellData) + formulaList := map[string]string{ + "=MIRR(A1:A5,0.055,0.05)": "0.025376365108071", + "=MIRR(A1:A6,0.055,0.05)": "0.1000268752662", + } + 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) + } + 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!", + } + 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) + } +} From d42834f3a82cebe6b54fd67b1f7f50582ea287dc Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 18 Apr 2021 16:00:34 +0000 Subject: [PATCH 372/957] update dependencies module and bump version 2.4.0 --- go.mod | 6 +++--- go.sum | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/go.mod b/go.mod index 0984d3a984..692eaa331f 100644 --- a/go.mod +++ b/go.mod @@ -7,8 +7,8 @@ require ( github.com/richardlehane/mscfb v1.0.3 github.com/stretchr/testify v1.6.1 github.com/xuri/efp v0.0.0-20210322160811-ab561f5b45e3 - golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 + golang.org/x/crypto v0.0.0-20210415154028-4f45737414dc golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb - golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4 - golang.org/x/text v0.3.5 + golang.org/x/net v0.0.0-20210415231046-e915ea6b2b7d + golang.org/x/text v0.3.6 ) diff --git a/go.sum b/go.sum index 5b2a1a3521..a709fdfe20 100644 --- a/go.sum +++ b/go.sum @@ -13,20 +13,20 @@ github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/xuri/efp v0.0.0-20210322160811-ab561f5b45e3 h1:EpI0bqf/eX9SdZDwlMmahKM+CDBgNbsXMhsN28XrM8o= github.com/xuri/efp v0.0.0-20210322160811-ab561f5b45e3/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= -golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w= -golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210415154028-4f45737414dc h1:+q90ECDSAQirdykUN6sPEiBXBsp8Csjcca8Oy7bgLTA= +golang.org/x/crypto v0.0.0-20210415154028-4f45737414dc/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb h1:fqpd0EBDzlHRCjiphRR5Zo/RSWWQlWv34418dnEixWk= golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4 h1:b0LrWgu8+q7z4J+0Y3Umo5q1dL7NXBkKBWkaVkAq17E= -golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210415231046-e915ea6b2b7d h1:BgJvlyh+UqCUaPlscHJ+PN8GcpfrFdr7NHjd1JL0+Gs= +golang.org/x/net v0.0.0-20210415231046-e915ea6b2b7d/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 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.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 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= From f5a20fa03f2abd9edfa2f9da66680e987fffae79 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 20 Apr 2021 15:20:46 +0000 Subject: [PATCH 373/957] Fixed #823, 12/24 hours time format parsing error --- cell_test.go | 2 +- styles.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cell_test.go b/cell_test.go index 026189c0a3..51ee956473 100644 --- a/cell_test.go +++ b/cell_test.go @@ -121,7 +121,7 @@ func TestSetCellValues(t *testing.T) { v, err := f.GetCellValue("Sheet1", "A1") assert.NoError(t, err) - assert.Equal(t, v, "12/31/10 12:00") + assert.Equal(t, v, "12/31/10 00:00") // 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)) diff --git a/styles.go b/styles.go index d58281d79a..15234fd21c 100644 --- a/styles.go +++ b/styles.go @@ -48,7 +48,7 @@ var builtInNumFmt = map[int]string{ 19: "h:mm:ss am/pm", 20: "h:mm", 21: "h:mm:ss", - 22: "m/d/yy h:mm", + 22: "m/d/yy hh:mm", 37: "#,##0 ;(#,##0)", 38: "#,##0 ;[red](#,##0)", 39: "#,##0.00;(#,##0.00)", From e5c5ecc379434b7ef4ffcbb9dcf58526cafda66a Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 26 Apr 2021 22:51:35 +0800 Subject: [PATCH 374/957] Fixed #825, improves compatibility for comments with absolute XML path --- comment.go | 9 ++++++++- comment_test.go | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/comment.go b/comment.go index 3c48d61844..705b957dc6 100644 --- a/comment.go +++ b/comment.go @@ -39,7 +39,14 @@ func parseFormatCommentsSet(formatSet string) (*formatComment, error) { func (f *File) GetComments() (comments map[string][]Comment) { comments = map[string][]Comment{} for n, path := range f.sheetMap { - if d := f.commentsReader("xl" + strings.TrimPrefix(f.getSheetComments(filepath.Base(path)), "..")); d != nil { + target := f.getSheetComments(filepath.Base(path)) + if target == "" { + continue + } + if !filepath.IsAbs(target) { + target = "xl" + strings.TrimPrefix(target, "..") + } + if d := f.commentsReader(strings.TrimPrefix(target, "/")); d != nil { sheetComments := []Comment{} for _, comment := range d.CommentList.Comment { sheetComment := Comment{} diff --git a/comment_test.go b/comment_test.go index ee8b8266bf..19b705f463 100644 --- a/comment_test.go +++ b/comment_test.go @@ -40,6 +40,7 @@ func TestAddComments(t *testing.T) { comments := f.GetComments() assert.EqualValues(t, 2, len(comments["Sheet1"])) assert.EqualValues(t, 1, len(comments["Sheet2"])) + assert.EqualValues(t, len(NewFile().GetComments()), 0) } func TestDecodeVMLDrawingReader(t *testing.T) { From a13ef5545ec79108477910346ae4cab82ab8bbda Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 27 Apr 2021 04:46:51 +0000 Subject: [PATCH 375/957] This closes #825, closes #829, closes #830, fix issue when get and add comments on multi authors --- comment.go | 21 ++++++++++----------- xmlComments.go | 4 ++-- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/comment.go b/comment.go index 705b957dc6..b05f308069 100644 --- a/comment.go +++ b/comment.go @@ -43,15 +43,15 @@ func (f *File) GetComments() (comments map[string][]Comment) { if target == "" { continue } - if !filepath.IsAbs(target) { + if !strings.HasPrefix(target, "/") { target = "xl" + strings.TrimPrefix(target, "..") } if d := f.commentsReader(strings.TrimPrefix(target, "/")); d != nil { sheetComments := []Comment{} for _, comment := range d.CommentList.Comment { sheetComment := Comment{} - if comment.AuthorID < len(d.Authors) { - sheetComment.Author = d.Authors[comment.AuthorID].Author + if comment.AuthorID < len(d.Authors.Author) { + sheetComment.Author = d.Authors.Author[comment.AuthorID] } sheetComment.Ref = comment.Ref sheetComment.AuthorID = comment.AuthorID @@ -250,20 +250,19 @@ func (f *File) addComment(commentsXML, cell string, formatSet *formatComment) { t = t[0:32512] } comments := f.commentsReader(commentsXML) + authorID := 0 if comments == nil { - comments = &xlsxComments{ - Authors: []xlsxAuthor{ - { - Author: formatSet.Author, - }, - }, - } + comments = &xlsxComments{Authors: xlsxAuthor{Author: []string{formatSet.Author}}} + } + if inStrSlice(comments.Authors.Author, formatSet.Author) == -1 { + comments.Authors.Author = append(comments.Authors.Author, formatSet.Author) + authorID = len(comments.Authors.Author) - 1 } defaultFont := f.GetDefaultFont() bold := "" cmt := xlsxComment{ Ref: cell, - AuthorID: 0, + AuthorID: authorID, Text: xlsxText{ R: []xlsxR{ { diff --git a/xmlComments.go b/xmlComments.go index e39fb24354..7965c863e8 100644 --- a/xmlComments.go +++ b/xmlComments.go @@ -24,7 +24,7 @@ import "encoding/xml" // something special about the cell. type xlsxComments struct { XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main comments"` - Authors []xlsxAuthor `xml:"authors"` + Authors xlsxAuthor `xml:"authors"` CommentList xlsxCommentList `xml:"commentList"` } @@ -33,7 +33,7 @@ type xlsxComments struct { // have an author. The maximum length of the author string is an implementation // detail, but a good guideline is 255 chars. type xlsxAuthor struct { - Author string `xml:"author"` + Author []string `xml:"author"` } // xlsxCommentList (List of Comments) directly maps the xlsxCommentList element. From af5e87dbcf5a89201072f5ca07d532258ece278f Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 28 Apr 2021 02:04:36 +0000 Subject: [PATCH 376/957] #826, support merge cell in streaming mode --- stream.go | 34 +++++++++++++++++++++++++++------- stream_test.go | 12 ++++++++++++ 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/stream.go b/stream.go index b0e5b533c9..fdada74da6 100644 --- a/stream.go +++ b/stream.go @@ -26,12 +26,14 @@ import ( // StreamWriter defined the type of stream writer. type StreamWriter struct { - File *File - Sheet string - SheetID int - worksheet *xlsxWorksheet - rawData bufferedWriter - tableParts string + File *File + Sheet string + SheetID int + worksheet *xlsxWorksheet + rawData bufferedWriter + mergeCellsCount int + mergeCells string + tableParts string } // NewStreamWriter return stream writer struct by given worksheet name for @@ -322,6 +324,19 @@ func (sw *StreamWriter) SetRow(axis string, values []interface{}) error { return sw.rawData.Sync() } +// MergeCell provides a function to merge cells by a given coordinate area for +// 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) + if err != nil { + return err + } + sw.mergeCellsCount++ + sw.mergeCells += fmt.Sprintf(``, hcell, vcell) + return nil +} + // setCellFormula provides a function to set formula of a cell. func setCellFormula(c *xlsxC, formula string) { if formula != "" { @@ -413,7 +428,12 @@ func writeCell(buf *bufferedWriter, c xlsxC) { // Flush ending the streaming writing process. func (sw *StreamWriter) Flush() error { _, _ = sw.rawData.WriteString(``) - bulkAppendFields(&sw.rawData, sw.worksheet, 8, 38) + bulkAppendFields(&sw.rawData, sw.worksheet, 8, 15) + if sw.mergeCellsCount > 0 { + sw.mergeCells = fmt.Sprintf(`%s`, sw.mergeCellsCount, sw.mergeCells) + } + _, _ = sw.rawData.WriteString(sw.mergeCells) + bulkAppendFields(&sw.rawData, sw.worksheet, 17, 38) _, _ = sw.rawData.WriteString(sw.tableParts) bulkAppendFields(&sw.rawData, sw.worksheet, 40, 40) _, _ = sw.rawData.WriteString(``) diff --git a/stream_test.go b/stream_test.go index 322eea94a6..26732d80aa 100644 --- a/stream_test.go +++ b/stream_test.go @@ -134,6 +134,18 @@ func TestStreamTable(t *testing.T) { assert.EqualError(t, streamWriter.AddTable("A1", "B", `{}`), `cannot convert cell "B" to coordinates: invalid cell name "B"`) } +func TestStreamMergeCells(t *testing.T) { + file := NewFile() + streamWriter, err := file.NewStreamWriter("Sheet1") + assert.NoError(t, err) + assert.NoError(t, streamWriter.MergeCell("A1", "D1")) + // Test merge cells with illegal cell coordinates. + assert.EqualError(t, streamWriter.MergeCell("A", "D1"), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.NoError(t, streamWriter.Flush()) + // Save spreadsheet by the given path. + assert.NoError(t, file.SaveAs(filepath.Join("test", "TestStreamMergeCells.xlsx"))) +} + func TestNewStreamWriter(t *testing.T) { // Test error exceptions file := NewFile() From 7e429c5b464b53f305e94cc355f14ba9e1d9849c Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 30 Apr 2021 00:14:42 +0800 Subject: [PATCH 377/957] Fixe issue generated file corrupted caused by incorrect default XML namespace attributes --- README.md | 2 +- README_zh.md | 2 +- excelize_test.go | 2 +- lib.go | 3 +++ lib_test.go | 6 ++++++ stream.go | 10 +++++----- stream_test.go | 12 +++++++++++- styles.go | 7 +++---- table.go | 2 +- xmlDrawing.go | 1 + xmlStyles.go | 14 +++++++------- 11 files changed, 40 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index ce7cf3d704..3e6728f7da 100644 --- a/README.md +++ b/README.md @@ -204,7 +204,7 @@ func main() { ## Contributing -Contributions are welcome! Open a pull request to fix a bug, or open an issue to discuss a new feature or change. XML is compliant with [part 1 of the 5th edition of the ECMA-376 Standard for Office Open XML](http://www.ecma-international.org/publications/standards/Ecma-376.htm). +Contributions are welcome! Open a pull request to fix a bug, or open an issue to discuss a new feature or change. XML is compliant with [part 1 of the 5th edition of the ECMA-376 Standard for Office Open XML](https://www.ecma-international.org/publications-and-standards/standards/ecma-376/). ## Licenses diff --git a/README_zh.md b/README_zh.md index cf9888b30d..4e0a499276 100644 --- a/README_zh.md +++ b/README_zh.md @@ -204,7 +204,7 @@ func main() { ## 社区合作 -欢迎您为此项目贡献代码,提出建议或问题、修复 Bug 以及参与讨论对新功能的想法。 XML 符合标准: [part 1 of the 5th edition of the ECMA-376 Standard for Office Open XML](http://www.ecma-international.org/publications/standards/Ecma-376.htm)。 +欢迎您为此项目贡献代码,提出建议或问题、修复 Bug 以及参与讨论对新功能的想法。 XML 符合标准: [part 1 of the 5th edition of the ECMA-376 Standard for Office Open XML](https://www.ecma-international.org/publications-and-standards/standards/ecma-376/)。 ## 开源许可 diff --git a/excelize_test.go b/excelize_test.go index f663ae0e4b..0dcfacbe23 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -54,7 +54,7 @@ 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", 32769))) + 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. f.SetSheetName("Maximum 31 characters allowed i", "[Rename]:\\/?* Maximum 31 characters allowed in sheet title.") diff --git a/lib.go b/lib.go index 26d402a10a..3a9e807fff 100644 --- a/lib.go +++ b/lib.go @@ -349,6 +349,9 @@ func genXMLNamespace(attr []xml.Attr) string { var rootElement string for _, v := range attr { if lastSpace := getXMLNamespace(v.Name.Space, attr); lastSpace != "" { + if lastSpace == NameSpaceXML { + lastSpace = "xml" + } rootElement += fmt.Sprintf("%s:%s=\"%s\" ", lastSpace, v.Name.Local, v.Value) continue } diff --git a/lib_test.go b/lib_test.go index f3e9b3e529..10d7c3aef1 100644 --- a/lib_test.go +++ b/lib_test.go @@ -228,3 +228,9 @@ func TestStack(t *testing.T) { assert.Equal(t, s.Peek(), nil) assert.Equal(t, s.Pop(), nil) } + +func TestGenXMLNamespace(t *testing.T) { + assert.Equal(t, genXMLNamespace([]xml.Attr{ + {Name: xml.Name{Space: NameSpaceXML, Local: "space"}, Value: "preserve"}, + }), `xml:space="preserve">`) +} diff --git a/stream.go b/stream.go index fdada74da6..57bf4a207c 100644 --- a/stream.go +++ b/stream.go @@ -40,8 +40,9 @@ type StreamWriter struct { // 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. -// For example, set data for worksheet of size 102400 rows x 50 columns with +// 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: // // file := excelize.NewFile() @@ -111,7 +112,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: // -// err := sw.AddTable("A1", "D5", ``) +// err := sw.AddTable("A1", "D5", "") // // Create a table of F2:H6 with format set: // @@ -500,8 +501,7 @@ func (bw *bufferedWriter) Reader() (io.Reader, error) { // buffer has grown large enough. Any error will be returned. func (bw *bufferedWriter) Sync() (err error) { // Try to use local storage - const chunk = 1 << 24 - if bw.buf.Len() < chunk { + if bw.buf.Len() < StreamChunkSize { return nil } if bw.tmp == nil { diff --git a/stream_test.go b/stream_test.go index 26732d80aa..d36883ab75 100644 --- a/stream_test.go +++ b/stream_test.go @@ -40,7 +40,7 @@ func TestStreamWriter(t *testing.T) { // Test max characters in a cell. row := make([]interface{}, 1) - row[0] = strings.Repeat("c", 32769) + 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. @@ -100,6 +100,16 @@ func TestStreamWriter(t *testing.T) { file.XLSX["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") + + // Test read cell. + file = NewFile() + streamWriter, err = file.NewStreamWriter("Sheet1") + assert.NoError(t, err) + assert.NoError(t, streamWriter.SetRow("A1", []interface{}{Cell{StyleID: styleID, Value: "Data"}})) + assert.NoError(t, streamWriter.Flush()) + cellValue, err := file.GetCellValue("Sheet1", "A1") + assert.NoError(t, err) + assert.Equal(t, "Data", cellValue) } func TestStreamTable(t *testing.T) { diff --git a/styles.go b/styles.go index 15234fd21c..4369f29cbf 100644 --- a/styles.go +++ b/styles.go @@ -2187,17 +2187,16 @@ func (f *File) newFont(style *Style) *xlsxFont { Family: &attrValInt{Val: intPtr(2)}, } if style.Font.Bold { - fnt.B = &style.Font.Bold + fnt.B = &attrValBool{Val: &style.Font.Bold} } if style.Font.Italic { - fnt.I = &style.Font.Italic + fnt.I = &attrValBool{Val: &style.Font.Italic} } if *fnt.Name.Val == "" { *fnt.Name.Val = f.GetDefaultFont() } if style.Font.Strike { - strike := true - fnt.Strike = &strike + fnt.Strike = &attrValBool{Val: &style.Font.Strike} } val, ok := fontUnderlineType[style.Font.Underline] if ok { diff --git a/table.go b/table.go index 973a416f21..12ef41a8c0 100644 --- a/table.go +++ b/table.go @@ -35,7 +35,7 @@ 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: // diff --git a/xmlDrawing.go b/xmlDrawing.go index a18c5886bf..4b51b635d5 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -91,6 +91,7 @@ const ( // Excel specifications and limits const ( + StreamChunkSize = 1 << 24 MaxFontFamilyLength = 31 MaxFontSize = 409 MaxFileNameLength = 207 diff --git a/xmlStyles.go b/xmlStyles.go index 46604dc463..92e4e6abc8 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -83,13 +83,13 @@ type xlsxFonts struct { // xlsxFont directly maps the font element. This element defines the // properties for one of the fonts used in this workbook. type xlsxFont struct { - B *bool `xml:"b,omitempty"` - I *bool `xml:"i,omitempty"` - Strike *bool `xml:"strike,omitempty"` - Outline *bool `xml:"outline,omitempty"` - Shadow *bool `xml:"shadow,omitempty"` - Condense *bool `xml:"condense,omitempty"` - Extend *bool `xml:"extend,omitempty"` + B *attrValBool `xml:"b,omitempty"` + I *attrValBool `xml:"i,omitempty"` + Strike *attrValBool `xml:"strike,omitempty"` + Outline *attrValBool `xml:"outline,omitempty"` + Shadow *attrValBool `xml:"shadow,omitempty"` + Condense *attrValBool `xml:"condense,omitempty"` + Extend *attrValBool `xml:"extend,omitempty"` U *attrValString `xml:"u"` Sz *attrValFloat `xml:"sz"` Color *xlsxColor `xml:"color"` From 438fd4b3f958dc3edbbe4915e866ac639a3135f1 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 6 May 2021 22:09:12 +0800 Subject: [PATCH 378/957] This closes #834, fix invalid file path and duplicate namespace when re-creating worksheet --- sheet.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/sheet.go b/sheet.go index 8bbbc82742..de46e90009 100644 --- a/sheet.go +++ b/sheet.go @@ -214,7 +214,7 @@ func (f *File) setSheet(index int, name string) { path := "xl/worksheets/sheet" + strconv.Itoa(index) + ".xml" f.sheetMap[trimSheetName(name)] = path f.Sheet[path] = &ws - f.xmlAttr[path] = append(f.xmlAttr[path], NameSpaceSpreadSheet) + f.xmlAttr[path] = []xml.Attr{NameSpaceSpreadSheet} } // setWorkbook update workbook property of the spreadsheet. Maximum 31 @@ -530,11 +530,8 @@ func (f *File) DeleteSheet(name string) { if wbRels != nil { for _, rel := range wbRels.Relationships { if rel.ID == sheet.ID { - sheetXML = fmt.Sprintf("xl/%s", rel.Target) - pathInfo := strings.Split(rel.Target, "/") - if len(pathInfo) == 2 { - rels = fmt.Sprintf("xl/%s/_rels/%s.rels", pathInfo[0], pathInfo[1]) - } + sheetXML = rel.Target + rels = "xl/worksheets/_rels/" + strings.TrimPrefix(f.sheetMap[sheetName], "xl/worksheets/") + ".rels" } } } @@ -569,9 +566,12 @@ 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) { + if !strings.HasPrefix(target, "/") { + target = "/xl/" + target + } content := f.contentTypesReader() for k, v := range content.Overrides { - if v.PartName == "/xl/"+target { + if v.PartName == target { content.Overrides = append(content.Overrides[:k], content.Overrides[k+1:]...) } } From 0e0237e62dcdbdd9ef5686d37171b1d5a61fac2b Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 7 May 2021 23:08:58 +0800 Subject: [PATCH 379/957] compatibility with non-standard page setup attributes --- sheet.go | 8 ++++---- xmlWorksheet.go | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/sheet.go b/sheet.go index de46e90009..7541f43c44 100644 --- a/sheet.go +++ b/sheet.go @@ -1204,8 +1204,8 @@ func (p *BlackAndWhite) getPageLayout(ps *xlsxPageSetUp) { // setPageLayout provides a method to set the first printed page number for // the worksheet. func (p FirstPageNumber) setPageLayout(ps *xlsxPageSetUp) { - if 0 < uint(p) { - ps.FirstPageNumber = uint(p) + if 0 < int(p) { + ps.FirstPageNumber = int(p) ps.UseFirstPageNumber = true } } @@ -1284,8 +1284,8 @@ func (p *FitToWidth) getPageLayout(ps *xlsxPageSetUp) { // setPageLayout provides a method to set the scale for the worksheet. func (p PageLayoutScale) setPageLayout(ps *xlsxPageSetUp) { - if 10 <= uint(p) && uint(p) <= 400 { - ps.Scale = uint(p) + if 10 <= int(p) && int(p) <= 400 { + ps.Scale = int(p) } } diff --git a/xmlWorksheet.go b/xmlWorksheet.go index edf57373a2..438e900c7e 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -108,23 +108,23 @@ type xlsxPageSetUp struct { XMLName xml.Name `xml:"pageSetup"` BlackAndWhite bool `xml:"blackAndWhite,attr,omitempty"` CellComments string `xml:"cellComments,attr,omitempty"` - Copies uint `xml:"copies,attr,omitempty"` + Copies int `xml:"copies,attr,omitempty"` Draft bool `xml:"draft,attr,omitempty"` Errors string `xml:"errors,attr,omitempty"` - FirstPageNumber uint `xml:"firstPageNumber,attr,omitempty"` + FirstPageNumber int `xml:"firstPageNumber,attr,omitempty"` FitToHeight int `xml:"fitToHeight,attr,omitempty"` FitToWidth int `xml:"fitToWidth,attr,omitempty"` - HorizontalDPI uint `xml:"horizontalDpi,attr,omitempty"` + HorizontalDPI int `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"` PaperHeight string `xml:"paperHeight,attr,omitempty"` PaperSize int `xml:"paperSize,attr,omitempty"` PaperWidth string `xml:"paperWidth,attr,omitempty"` - Scale uint `xml:"scale,attr,omitempty"` + Scale int `xml:"scale,attr,omitempty"` UseFirstPageNumber bool `xml:"useFirstPageNumber,attr,omitempty"` UsePrinterDefaults bool `xml:"usePrinterDefaults,attr,omitempty"` - VerticalDPI uint `xml:"verticalDpi,attr,omitempty"` + VerticalDPI int `xml:"verticalDpi,attr,omitempty"` } // xlsxPrintOptions directly maps the printOptions element in the namespace From 423bc26d1f87db55bab5704afebf4509269bbc7e Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 9 May 2021 14:20:17 +0800 Subject: [PATCH 380/957] #65 fn: BESSELK and BESSELY --- calc.go | 165 +++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 22 +++++++ 2 files changed, 187 insertions(+) diff --git a/calc.go b/calc.go index ee81dcf461..b65a3c49a3 100644 --- a/calc.go +++ b/calc.go @@ -232,6 +232,8 @@ var tokenPriority = map[string]int{ // BASE // BESSELI // BESSELJ +// BESSELK +// BESSELY // BIN2DEC // BIN2HEX // BIN2OCT @@ -1334,6 +1336,169 @@ func (fn *formulaFuncs) bassel(argsList *list.List, modfied bool) formulaArg { return newNumberFormulaArg(result) } +// BESSELK function calculates the modified Bessel functions, Kn(x), which are +// also known as the hyperbolic Bessel Functions. These are the equivalent of +// the Bessel functions, evaluated for purely imaginary arguments. The syntax +// of the function is: +// +// BESSELK(x,n) +// +func (fn *formulaFuncs) BESSELK(argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, "BESSELK requires 2 numeric arguments") + } + x, n := argsList.Front().Value.(formulaArg).ToNumber(), argsList.Back().Value.(formulaArg).ToNumber() + if x.Type != ArgNumber { + return x + } + if n.Type != ArgNumber { + return n + } + if x.Number <= 0 || n.Number < 0 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + var result float64 + switch math.Floor(n.Number) { + case 0: + result = fn.besselK0(x) + case 1: + result = fn.besselK1(x) + default: + result = fn.besselK2(x, n) + } + return newNumberFormulaArg(result) +} + +// besselK0 is an implementation of the formula function BESSELK. +func (fn *formulaFuncs) besselK0(x formulaArg) float64 { + var y float64 + if x.Number <= 2 { + n2 := x.Number * 0.5 + y = n2 * n2 + args := list.New() + args.PushBack(x) + args.PushBack(newNumberFormulaArg(0)) + return -math.Log(n2)*fn.BESSELI(args).Number + + (-0.57721566 + y*(0.42278420+y*(0.23069756+y*(0.3488590e-1+y*(0.262698e-2+y* + (0.10750e-3+y*0.74e-5)))))) + } + y = 2 / x.Number + return math.Exp(-x.Number) / math.Sqrt(x.Number) * + (1.25331414 + y*(-0.7832358e-1+y*(0.2189568e-1+y*(-0.1062446e-1+y* + (0.587872e-2+y*(-0.251540e-2+y*0.53208e-3)))))) +} + +// besselK1 is an implementation of the formula function BESSELK. +func (fn *formulaFuncs) besselK1(x formulaArg) float64 { + var n2, y float64 + if x.Number <= 2 { + n2 = x.Number * 0.5 + y = n2 * n2 + args := list.New() + args.PushBack(x) + args.PushBack(newNumberFormulaArg(1)) + return math.Log(n2)*fn.BESSELI(args).Number + + (1+y*(0.15443144+y*(-0.67278579+y*(-0.18156897+y*(-0.1919402e-1+y*(-0.110404e-2+y*(-0.4686e-4)))))))/x.Number + } + y = 2 / x.Number + return math.Exp(-x.Number) / math.Sqrt(x.Number) * + (1.25331414 + y*(0.23498619+y*(-0.3655620e-1+y*(0.1504268e-1+y*(-0.780353e-2+y* + (0.325614e-2+y*(-0.68245e-3))))))) +} + +// besselK2 is an implementation of the formula function BESSELK. +func (fn *formulaFuncs) besselK2(x, n formulaArg) float64 { + tox, bkm, bk, bkp := 2/x.Number, fn.besselK0(x), fn.besselK1(x), 0.0 + for i := 1.0; i < n.Number; i++ { + bkp = bkm + i*tox*bk + bkm = bk + bk = bkp + } + return bk +} + +// BESSELY function returns the Bessel function, Yn(x), (also known as the +// Weber function or the Neumann function), for a specified order and value +// of x. The syntax of the function is: +// +// BESSELY(x,n) +// +func (fn *formulaFuncs) BESSELY(argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, "BESSELY requires 2 numeric arguments") + } + x, n := argsList.Front().Value.(formulaArg).ToNumber(), argsList.Back().Value.(formulaArg).ToNumber() + if x.Type != ArgNumber { + return x + } + if n.Type != ArgNumber { + return n + } + if x.Number <= 0 || n.Number < 0 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + var result float64 + switch math.Floor(n.Number) { + case 0: + result = fn.besselY0(x) + case 1: + result = fn.besselY1(x) + default: + result = fn.besselY2(x, n) + } + return newNumberFormulaArg(result) +} + +// besselY0 is an implementation of the formula function BESSELY. +func (fn *formulaFuncs) besselY0(x formulaArg) float64 { + var y float64 + if x.Number < 8 { + y = x.Number * x.Number + f1 := -2957821389.0 + y*(7062834065.0+y*(-512359803.6+y*(10879881.29+y* + (-86327.92757+y*228.4622733)))) + f2 := 40076544269.0 + y*(745249964.8+y*(7189466.438+y* + (47447.26470+y*(226.1030244+y)))) + args := list.New() + args.PushBack(x) + args.PushBack(newNumberFormulaArg(0)) + return f1/f2 + 0.636619772*fn.BESSELJ(args).Number*math.Log(x.Number) + } + z := 8.0 / x.Number + y = z * z + xx := x.Number - 0.785398164 + f1 := 1 + y*(-0.1098628627e-2+y*(0.2734510407e-4+y*(-0.2073370639e-5+y*0.2093887211e-6))) + f2 := -0.1562499995e-1 + y*(0.1430488765e-3+y*(-0.6911147651e-5+y*(0.7621095161e-6+y* + (-0.934945152e-7)))) + return math.Sqrt(0.636619772/x.Number) * (math.Sin(xx)*f1 + z*math.Cos(xx)*f2) +} + +// besselY1 is an implementation of the formula function BESSELY. +func (fn *formulaFuncs) besselY1(x formulaArg) float64 { + if x.Number < 8 { + y := x.Number * x.Number + f1 := x.Number * (-0.4900604943e13 + y*(0.1275274390e13+y*(-0.5153438139e11+y* + (0.7349264551e9+y*(-0.4237922726e7+y*0.8511937935e4))))) + f2 := 0.2499580570e14 + y*(0.4244419664e12+y*(0.3733650367e10+y*(0.2245904002e8+y* + (0.1020426050e6+y*(0.3549632885e3+y))))) + args := list.New() + args.PushBack(x) + args.PushBack(newNumberFormulaArg(1)) + return f1/f2 + 0.636619772*(fn.BESSELJ(args).Number*math.Log(x.Number)-1/x.Number) + } + return math.Sqrt(0.636619772/x.Number) * math.Sin(x.Number-2.356194491) +} + +// besselY2 is an implementation of the formula function BESSELY. +func (fn *formulaFuncs) besselY2(x, n formulaArg) float64 { + tox, bym, by, byp := 2/x.Number, fn.besselY0(x), fn.besselY1(x), 0.0 + for i := 1.0; i < n.Number; i++ { + byp = i*tox*by - bym + bym = by + by = byp + } + return by +} + // BIN2DEC function converts a Binary (a base-2 number) into a decimal number. // The syntax of the function is: // diff --git a/calc_test.go b/calc_test.go index a6d5f97443..d5d54392ca 100644 --- a/calc_test.go +++ b/calc_test.go @@ -52,6 +52,16 @@ func TestCalcCellValue(t *testing.T) { "=BESSELI(32,1)": "5.502845511211247e+12", // BESSELJ "=BESSELJ(1.9,2)": "0.329925727692387", + // BESSELK + "=BESSELK(0.05,0)": "3.114234034289662", + "=BESSELK(0.05,1)": "19.90967432724863", + "=BESSELK(0.05,2)": "799.501207124235", + "=BESSELK(3,2)": "0.061510458561912", + // BESSELY + "=BESSELY(0.05,0)": "-1.979311006841528", + "=BESSELY(0.05,1)": "-12.789855163794034", + "=BESSELY(0.05,2)": "-509.61489554491976", + "=BESSELY(9,2)": "-0.229082087487741", // BIN2DEC "=BIN2DEC(\"10\")": "2", "=BIN2DEC(\"11\")": "3", @@ -1208,6 +1218,18 @@ func TestCalcCellValue(t *testing.T) { "=BESSELJ()": "BESSELJ requires 2 numeric arguments", "=BESSELJ(\"\",0)": "strconv.ParseFloat: parsing \"\": invalid syntax", "=BESSELJ(0,\"\")": "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!", + // 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!", // BIN2DEC "=BIN2DEC()": "BIN2DEC requires 1 numeric argument", "=BIN2DEC(\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", From be12cc27f1d774154b17763c071e1dc6f91eab8c Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 10 May 2021 00:09:24 +0800 Subject: [PATCH 381/957] This closes #652, new SetColWidth API, support set column width in stream writing mode, and export error message --- adjust.go | 5 ++--- adjust_test.go | 4 ++-- calc.go | 8 ++++---- calc_test.go | 12 ++++++------ cell.go | 2 +- chart.go | 2 +- chart_test.go | 2 +- col.go | 5 ++--- col_test.go | 6 +++--- date.go | 3 +-- date_test.go | 2 +- errors.go | 46 +++++++++++++++++++++++++++++++++++++++++++++- excelize.go | 3 +-- excelize_test.go | 10 +++++----- file.go | 3 +-- lib.go | 4 ++-- lib_test.go | 4 ++-- picture.go | 5 ++--- picture_test.go | 4 ++-- rows.go | 5 ++--- rows_test.go | 2 +- sheet.go | 2 +- stream.go | 40 +++++++++++++++++++++++++++++++++++++--- stream_test.go | 17 ++++++++++++++++- 24 files changed, 141 insertions(+), 55 deletions(-) diff --git a/adjust.go b/adjust.go index e06d4f62ab..28b62ccb12 100644 --- a/adjust.go +++ b/adjust.go @@ -12,7 +12,6 @@ package excelize import ( - "errors" "strings" ) @@ -219,7 +218,7 @@ func areaRangeToCoordinates(firstCell, lastCell string) ([]int, error) { // correct C1:B3 to B1:C3. func sortCoordinates(coordinates []int) error { if len(coordinates) != 4 { - return errors.New("coordinates length must be 4") + return ErrCoordinates } if coordinates[2] < coordinates[0] { coordinates[2], coordinates[0] = coordinates[0], coordinates[2] @@ -234,7 +233,7 @@ func sortCoordinates(coordinates []int) error { // to area reference. func (f *File) coordinatesToAreaRef(coordinates []int) (string, error) { if len(coordinates) != 4 { - return "", errors.New("coordinates length must be 4") + return "", ErrCoordinates } firstCell, err := CoordinatesToCellName(coordinates[0], coordinates[1]) if err != nil { diff --git a/adjust_test.go b/adjust_test.go index bdbaebea18..0d63ed62ef 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -113,7 +113,7 @@ func TestAdjustCalcChain(t *testing.T) { func TestCoordinatesToAreaRef(t *testing.T) { f := NewFile() _, err := f.coordinatesToAreaRef([]int{}) - assert.EqualError(t, err, "coordinates length must be 4") + assert.EqualError(t, err, ErrCoordinates.Error()) _, err = f.coordinatesToAreaRef([]int{1, -1, 1, 1}) assert.EqualError(t, err, "invalid cell coordinates [1, -1]") _, err = f.coordinatesToAreaRef([]int{1, 1, 1, -1}) @@ -124,5 +124,5 @@ func TestCoordinatesToAreaRef(t *testing.T) { } func TestSortCoordinates(t *testing.T) { - assert.EqualError(t, sortCoordinates(make([]int, 3)), "coordinates length must be 4") + assert.EqualError(t, sortCoordinates(make([]int, 3)), ErrCoordinates.Error()) } diff --git a/calc.go b/calc.go index b65a3c49a3..573abf2b47 100644 --- a/calc.go +++ b/calc.go @@ -647,7 +647,7 @@ func (f *File) evalInfixExp(sheet, cell string, tokens []efp.Token) (efp.Token, optStack.Pop() } if opdStack.Len() == 0 { - return efp.Token{}, errors.New("formula not valid") + return efp.Token{}, ErrInvalidFormula } return opdStack.Peek().(efp.Token), err } @@ -849,7 +849,7 @@ func calcDiv(rOpd, lOpd string, opdStack *Stack) error { func calculate(opdStack *Stack, opt efp.Token) error { if opt.TValue == "-" && opt.TType == efp.TokenTypeOperatorPrefix { if opdStack.Len() < 1 { - return errors.New("formula not valid") + return ErrInvalidFormula } opd := opdStack.Pop().(efp.Token) opdVal, err := strconv.ParseFloat(opd.TValue, 64) @@ -874,7 +874,7 @@ func calculate(opdStack *Stack, opt efp.Token) error { } if opt.TValue == "-" && opt.TType == efp.TokenTypeOperatorInfix { if opdStack.Len() < 2 { - return errors.New("formula not valid") + return ErrInvalidFormula } rOpd := opdStack.Pop().(efp.Token) lOpd := opdStack.Pop().(efp.Token) @@ -885,7 +885,7 @@ func calculate(opdStack *Stack, opt efp.Token) error { fn, ok := tokenCalcFunc[opt.TValue] if ok { if opdStack.Len() < 2 { - return errors.New("formula not valid") + return ErrInvalidFormula } rOpd := opdStack.Pop().(efp.Token) lOpd := opdStack.Pop().(efp.Token) diff --git a/calc_test.go b/calc_test.go index d5d54392ca..23568ff5c3 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1710,12 +1710,12 @@ func TestCalcCellValue(t *testing.T) { "=POISSON(0,0,\"\")": "strconv.ParseBool: parsing \"\": invalid syntax", "=POISSON(0,-1,TRUE)": "#N/A", // SUM - "=SUM((": "formula not valid", - "=SUM(-)": "formula not valid", - "=SUM(1+)": "formula not valid", - "=SUM(1-)": "formula not valid", - "=SUM(1*)": "formula not valid", - "=SUM(1/)": "formula not valid", + "=SUM((": ErrInvalidFormula.Error(), + "=SUM(-)": ErrInvalidFormula.Error(), + "=SUM(1+)": ErrInvalidFormula.Error(), + "=SUM(1-)": ErrInvalidFormula.Error(), + "=SUM(1*)": ErrInvalidFormula.Error(), + "=SUM(1/)": ErrInvalidFormula.Error(), // SUMIF "=SUMIF()": "SUMIF requires at least 2 argument", // SUMSQ diff --git a/cell.go b/cell.go index 27d24d98d9..f94b81e9d5 100644 --- a/cell.go +++ b/cell.go @@ -475,7 +475,7 @@ func (f *File) SetCellHyperLink(sheet, axis, link, linkType string, opts ...Hype } if len(ws.Hyperlinks.Hyperlink) > TotalSheetHyperlinks { - return errors.New("over maximum limit hyperlinks in a worksheet") + return ErrTotalSheetHyperlinks } switch linkType { diff --git a/chart.go b/chart.go index 3ac460b8ac..23ea77614a 100644 --- a/chart.go +++ b/chart.go @@ -919,7 +919,7 @@ func (f *File) AddChart(sheet, cell, format string, combo ...string) error { func (f *File) AddChartSheet(sheet, format string, combo ...string) error { // Check if the worksheet already exists if f.GetSheetIndex(sheet) != -1 { - return errors.New("the same name worksheet already exists") + return ErrExistsWorksheet } formatSet, comboCharts, err := f.getFormatChart(format, combo) if err != nil { diff --git a/chart_test.go b/chart_test.go index 6ee0a0f0e0..657230b103 100644 --- a/chart_test.go +++ b/chart_test.go @@ -232,7 +232,7 @@ func TestAddChartSheet(t *testing.T) { // Test cell value on chartsheet assert.EqualError(t, f.SetCellValue("Chart1", "A1", true), "sheet Chart1 is chart sheet") // 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"}}`), "the same name worksheet already exists") + 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()) // 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") diff --git a/col.go b/col.go index 09a172a11d..e1ac5a5ab1 100644 --- a/col.go +++ b/col.go @@ -14,7 +14,6 @@ package excelize import ( "bytes" "encoding/xml" - "errors" "math" "strconv" "strings" @@ -360,7 +359,7 @@ func (f *File) parseColRange(columns string) (start, end int, err error) { // func (f *File) SetColOutlineLevel(sheet, col string, level uint8) error { if level > 7 || level < 1 { - return errors.New("invalid outline level") + return ErrOutlineLevel } colNum, err := ColumnNameToNumber(col) if err != nil { @@ -452,7 +451,7 @@ func (f *File) SetColWidth(sheet, startcol, endcol string, width float64) error return err } if width > MaxColumnWidth { - return errors.New("the width of the column must be smaller than or equal to 255 characters") + return ErrColumnWidth } if min > max { min, max = max, min diff --git a/col_test.go b/col_test.go index add1c116a4..6ab5e5769f 100644 --- a/col_test.go +++ b/col_test.go @@ -246,12 +246,12 @@ func TestOutlineLevel(t *testing.T) { assert.EqualError(t, err, "sheet Shee2 is not exist") assert.NoError(t, f.SetColWidth("Sheet2", "A", "D", 13)) - assert.EqualError(t, f.SetColWidth("Sheet2", "A", "D", MaxColumnWidth+1), "the width of the column must be smaller than or equal to 255 characters") + assert.EqualError(t, f.SetColWidth("Sheet2", "A", "D", MaxColumnWidth+1), ErrColumnWidth.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), "invalid outline level") - assert.EqualError(t, f.SetRowOutlineLevel("Sheet1", 2, 8), "invalid outline level") + 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") // Test get row outline level on not exists worksheet. diff --git a/date.go b/date.go index 3e50e3d3c3..9ef5caf5c0 100644 --- a/date.go +++ b/date.go @@ -12,7 +12,6 @@ package excelize import ( - "errors" "math" "time" ) @@ -35,7 +34,7 @@ func timeToExcelTime(t time.Time) (float64, error) { // Because for example 1900-01-01 00:00:00 +0300 MSK converts to 1900-01-01 00:00:00 +0230 LMT // probably due to daylight saving. if t.Location() != time.UTC { - return 0.0, errors.New("only UTC time expected") + return 0.0, ErrToExcelTime } if t.Before(excelMinTime1900) { diff --git a/date_test.go b/date_test.go index 79462c5995..ecc96ba9c2 100644 --- a/date_test.go +++ b/date_test.go @@ -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)) - assert.EqualError(t, err, "only UTC time expected") + assert.EqualError(t, err, ErrToExcelTime.Error()) }) } } diff --git a/errors.go b/errors.go index 0ab2642f1b..a0c61c8dd0 100644 --- a/errors.go +++ b/errors.go @@ -11,7 +11,10 @@ package excelize -import "fmt" +import ( + "errors" + "fmt" +) func newInvalidColumnNameError(col string) error { return fmt.Errorf("invalid column name %q", col) @@ -28,3 +31,44 @@ func newInvalidCellNameError(cell string) error { func newInvalidExcelDateError(dateValue float64) error { return fmt.Errorf("invalid date value %f, negative values are not supported supported", dateValue) } + +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") + // ErrColumnNumber defined the error message on receive an invalid column + // number. + ErrColumnNumber = errors.New("column number exceeds maximum limit") + // ErrColumnWidth defined the error message on receive an invalid column + // width. + ErrColumnWidth = errors.New("the width of the column must be smaller than or equal to 255 characters") + // ErrOutlineLevel defined the error message on receive an invalid outline + // level number. + ErrOutlineLevel = errors.New("invalid outline level") + // 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") + // ErrTotalSheetHyperlinks defined the error message on hyperlinks count + // overflow. + ErrTotalSheetHyperlinks = errors.New("over maximum limit hyperlinks in a worksheet") + // ErrInvalidFormula defined the error message on receive an invalid + // formula. + 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") + // ErrToExcelTime defined the error message on receive a not UTC time. + ErrToExcelTime = errors.New("only UTC time expected") + // 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") + // ErrImgExt defined the error message on receive an unsupported image + // extension. + ErrImgExt = errors.New("unsupported image extension") + // ErrMaxFileNameLength defined the error message on receive the file name + // length overflow. + ErrMaxFileNameLength = errors.New("file name length exceeds maximum limit") +) diff --git a/excelize.go b/excelize.go index 6b3d4062ca..940acf1bda 100644 --- a/excelize.go +++ b/excelize.go @@ -16,7 +16,6 @@ import ( "archive/zip" "bytes" "encoding/xml" - "errors" "fmt" "io" "io/ioutil" @@ -351,7 +350,7 @@ func (f *File) AddVBAProject(bin string) error { return fmt.Errorf("stat %s: no such file or directory", bin) } if path.Ext(bin) != ".bin" { - return errors.New("unsupported VBA project extension") + return ErrAddVBAProject } f.setContentTypePartVBAProjectExtensions() wb := f.relsReader(f.getWorkbookRelsPath()) diff --git a/excelize_test.go b/excelize_test.go index 0dcfacbe23..0c42178a92 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -144,7 +144,7 @@ func TestOpenFile(t *testing.T) { assert.NoError(t, f.SetCellValue("Sheet2", "G2", nil)) - assert.EqualError(t, f.SetCellValue("Sheet2", "G4", time.Now()), "only UTC time expected") + assert.EqualError(t, f.SetCellValue("Sheet2", "G4", time.Now()), ErrToExcelTime.Error()) assert.NoError(t, f.SetCellValue("Sheet2", "G4", time.Now().UTC())) // 02:46:40 @@ -166,7 +166,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")), "file name length exceeds maximum limit") + assert.EqualError(t, f.SaveAs(filepath.Join("test", strings.Repeat("c", 199), ".xlsx")), ErrMaxFileNameLength.Error()) } func TestSaveFile(t *testing.T) { @@ -344,7 +344,7 @@ func TestSetCellHyperLink(t *testing.T) { _, err = f.workSheetReader("Sheet1") assert.NoError(t, err) f.Sheet["xl/worksheets/sheet1.xml"].Hyperlinks = &xlsxHyperlinks{Hyperlink: make([]xlsxHyperlink, 65530)} - assert.EqualError(t, f.SetCellHyperLink("Sheet1", "A65531", "https://github.com/360EntSecGroup-Skylar/excelize", "External"), "over maximum limit hyperlinks in a worksheet") + assert.EqualError(t, f.SetCellHyperLink("Sheet1", "A65531", "https://github.com/360EntSecGroup-Skylar/excelize", "External"), ErrTotalSheetHyperlinks.Error()) f = NewFile() _, err = f.workSheetReader("Sheet1") @@ -449,7 +449,7 @@ func TestSetSheetBackgroundErrors(t *testing.T) { } err = f.SetSheetBackground("Sheet2", filepath.Join("test", "Book1.xlsx")) - assert.EqualError(t, err, "unsupported image extension") + assert.EqualError(t, err, ErrImgExt.Error()) } // TestWriteArrayFormula tests the extended options of SetCellFormula by writing an array function @@ -1187,7 +1187,7 @@ func TestAddVBAProject(t *testing.T) { f := NewFile() assert.NoError(t, f.SetSheetPrOptions("Sheet1", CodeName("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")), "unsupported VBA project extension") + 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"))) diff --git a/file.go b/file.go index 8a37aefe1b..36b1a425a9 100644 --- a/file.go +++ b/file.go @@ -14,7 +14,6 @@ package excelize import ( "archive/zip" "bytes" - "errors" "fmt" "io" "os" @@ -66,7 +65,7 @@ func (f *File) Save() error { // provided path. func (f *File) SaveAs(name string, opt ...Options) error { if len(name) > MaxFileNameLength { - return errors.New("file name length exceeds maximum limit") + return ErrMaxFileNameLength } file, err := os.OpenFile(name, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0666) if err != nil { diff --git a/lib.go b/lib.go index 3a9e807fff..99083b15d0 100644 --- a/lib.go +++ b/lib.go @@ -149,7 +149,7 @@ func ColumnNameToNumber(name string) (int, error) { multi *= 26 } if col > TotalColumns { - return -1, fmt.Errorf("column number exceeds maximum limit") + return -1, ErrColumnNumber } return col, nil } @@ -166,7 +166,7 @@ func ColumnNumberToName(num int) (string, error) { return "", fmt.Errorf("incorrect column number %d", num) } if num > TotalColumns { - return "", fmt.Errorf("column number exceeds maximum limit") + return "", ErrColumnNumber } var col string for num > 0 { diff --git a/lib_test.go b/lib_test.go index 10d7c3aef1..eb0a2893ec 100644 --- a/lib_test.go +++ b/lib_test.go @@ -73,7 +73,7 @@ func TestColumnNameToNumber_Error(t *testing.T) { } } _, err := ColumnNameToNumber("XFE") - assert.EqualError(t, err, "column number exceeds maximum limit") + assert.EqualError(t, err, ErrColumnNumber.Error()) } func TestColumnNumberToName_OK(t *testing.T) { @@ -98,7 +98,7 @@ func TestColumnNumberToName_Error(t *testing.T) { } _, err = ColumnNumberToName(TotalColumns + 1) - assert.EqualError(t, err, "column number exceeds maximum limit") + assert.EqualError(t, err, ErrColumnNumber.Error()) } func TestSplitCellName_OK(t *testing.T) { diff --git a/picture.go b/picture.go index de7e0f859f..92e01065c0 100644 --- a/picture.go +++ b/picture.go @@ -15,7 +15,6 @@ import ( "bytes" "encoding/json" "encoding/xml" - "errors" "fmt" "image" "io" @@ -93,7 +92,7 @@ func (f *File) AddPicture(sheet, cell, picture, format string) error { } ext, ok := supportImageTypes[path.Ext(picture)] if !ok { - return errors.New("unsupported image extension") + return ErrImgExt } file, _ := ioutil.ReadFile(picture) _, name := filepath.Split(picture) @@ -134,7 +133,7 @@ func (f *File) AddPictureFromBytes(sheet, cell, format, name, extension string, var hyperlinkType string ext, ok := supportImageTypes[extension] if !ok { - return errors.New("unsupported image extension") + return ErrImgExt } formatSet, err := parseFormatPictureSet(format) if err != nil { diff --git a/picture_test.go b/picture_test.go index 28d8aa806a..be917b84ef 100644 --- a/picture_test.go +++ b/picture_test.go @@ -82,10 +82,10 @@ func TestAddPictureErrors(t *testing.T) { // Test add picture to worksheet with unsupported file type. err = xlsx.AddPicture("Sheet1", "G21", filepath.Join("test", "Book1.xlsx"), "") - assert.EqualError(t, err, "unsupported image extension") + assert.EqualError(t, err, ErrImgExt.Error()) err = xlsx.AddPictureFromBytes("Sheet1", "G21", "", "Excel Logo", "jpg", make([]byte, 1)) - assert.EqualError(t, err, "unsupported image extension") + assert.EqualError(t, err, ErrImgExt.Error()) // Test add picture to worksheet with invalid file data. err = xlsx.AddPictureFromBytes("Sheet1", "G21", "", "Excel Logo", ".jpg", make([]byte, 1)) diff --git a/rows.go b/rows.go index 76a8f67b71..451dbe9f97 100644 --- a/rows.go +++ b/rows.go @@ -14,7 +14,6 @@ package excelize import ( "bytes" "encoding/xml" - "errors" "fmt" "io" "log" @@ -245,7 +244,7 @@ func (f *File) SetRowHeight(sheet string, row int, height float64) error { return newInvalidRowNumberError(row) } if height > MaxRowHeight { - return errors.New("the height of the row must be smaller than or equal to 409 points") + return ErrMaxRowHeight } ws, err := f.workSheetReader(sheet) if err != nil { @@ -436,7 +435,7 @@ func (f *File) SetRowOutlineLevel(sheet string, row int, level uint8) error { return newInvalidRowNumberError(row) } if level > 7 || level < 1 { - return errors.New("invalid outline level") + return ErrOutlineLevel } ws, err := f.workSheetReader(sheet) if err != nil { diff --git a/rows_test.go b/rows_test.go index 01804986ca..5e7fd37cd4 100644 --- a/rows_test.go +++ b/rows_test.go @@ -109,7 +109,7 @@ func TestRowHeight(t *testing.T) { assert.Equal(t, 111.0, height) // Test set row height overflow max row height limit. - assert.EqualError(t, f.SetRowHeight(sheet1, 4, MaxRowHeight+1), "the height of the row must be smaller than or equal to 409 points") + assert.EqualError(t, f.SetRowHeight(sheet1, 4, MaxRowHeight+1), ErrMaxRowHeight.Error()) // Test get row height that rows index over exists rows. height, err = f.GetRowHeight(sheet1, 5) diff --git a/sheet.go b/sheet.go index 7541f43c44..97ef57e952 100644 --- a/sheet.go +++ b/sheet.go @@ -482,7 +482,7 @@ func (f *File) SetSheetBackground(sheet, picture string) error { } ext, ok := supportImageTypes[path.Ext(picture)] if !ok { - return errors.New("unsupported image extension") + return ErrImgExt } file, _ := ioutil.ReadFile(picture) name := f.addMedia(file, ext) diff --git a/stream.go b/stream.go index 57bf4a207c..f12b201fc0 100644 --- a/stream.go +++ b/stream.go @@ -29,6 +29,8 @@ type StreamWriter struct { File *File Sheet string SheetID int + sheetWritten bool + cols string worksheet *xlsxWorksheet rawData bufferedWriter mergeCellsCount int @@ -104,8 +106,7 @@ func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) { f.streams[sheetXML] = sw _, _ = sw.rawData.WriteString(XMLHeader + ``) + bulkAppendFields(&sw.rawData, sw.worksheet, 2, 5) return sw, err } @@ -298,7 +299,13 @@ func (sw *StreamWriter) SetRow(axis string, values []interface{}) error { if err != nil { return err } - + if !sw.sheetWritten { + if len(sw.cols) > 0 { + sw.rawData.WriteString("" + sw.cols + "") + } + _, _ = sw.rawData.WriteString(``) + sw.sheetWritten = true + } fmt.Fprintf(&sw.rawData, ``, row) for i, val := range values { axis, err := CoordinatesToCellName(col+i, row) @@ -325,6 +332,33 @@ func (sw *StreamWriter) SetRow(axis string, values []interface{}) error { return sw.rawData.Sync() } +// SetColWidth provides a function to set the width of a single column or +// multiple columns for the the StreamWriter. Note that you must call +// the 'SetColWidth' function before the 'SetRow' function. For example set +// the width column B:C as 20: +// +// err := streamWriter.SetColWidth(2, 3, 20) +// +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 { + return ErrColumnNumber + } + if width > MaxColumnWidth { + return ErrColumnWidth + } + if min > max { + min, max = max, min + } + sw.cols += fmt.Sprintf(``, min, max, width) + return nil +} + // MergeCell provides a function to merge cells by a given coordinate area for // the StreamWriter. Don't create a merged cell that overlaps with another // existing merged cell. diff --git a/stream_test.go b/stream_test.go index d36883ab75..0834a2dc1e 100644 --- a/stream_test.go +++ b/stream_test.go @@ -57,7 +57,7 @@ func TestStreamWriter(t *testing.T) { assert.NoError(t, err) assert.NoError(t, streamWriter.SetRow("A4", []interface{}{Cell{StyleID: styleID}, Cell{Formula: "SUM(A10,B10)"}})) assert.NoError(t, streamWriter.SetRow("A5", []interface{}{&Cell{StyleID: styleID, Value: "cell"}, &Cell{Formula: "SUM(A10,B10)"}})) - assert.EqualError(t, streamWriter.SetRow("A6", []interface{}{time.Now()}), "only UTC time expected") + assert.EqualError(t, streamWriter.SetRow("A6", []interface{}{time.Now()}), ErrToExcelTime.Error()) for rowID := 10; rowID <= 51200; rowID++ { row := make([]interface{}, 50) @@ -68,6 +68,9 @@ func TestStreamWriter(t *testing.T) { assert.NoError(t, streamWriter.SetRow(cell, row)) } + // Test set cell column overflow. + assert.EqualError(t, streamWriter.SetRow("XFD1", []interface{}{"A", "B", "C"}), ErrColumnNumber.Error()) + assert.NoError(t, streamWriter.Flush()) // Save spreadsheet by the given path. assert.NoError(t, file.SaveAs(filepath.Join("test", "TestStreamWriter.xlsx"))) @@ -112,6 +115,18 @@ func TestStreamWriter(t *testing.T) { assert.Equal(t, "Data", cellValue) } +func TestStreamSetColWidth(t *testing.T) { + file := NewFile() + 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.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()) +} + func TestStreamTable(t *testing.T) { file := NewFile() streamWriter, err := file.NewStreamWriter("Sheet1") From a1e1db1e6f2faa8286afb1e9291c51fa084b66f7 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 14 May 2021 00:09:23 +0800 Subject: [PATCH 382/957] This closes #838, fix wrong worksheet XML path of the stream writer in some case --- stream.go | 16 ++++++++++------ stream_test.go | 6 +++--- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/stream.go b/stream.go index f12b201fc0..e5fa237329 100644 --- a/stream.go +++ b/stream.go @@ -99,11 +99,11 @@ func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) { return nil, err } - sheetXML := fmt.Sprintf("xl/worksheets/sheet%d.xml", sw.SheetID) + sheetPath := f.sheetMap[trimSheetName(sheet)] if f.streams == nil { f.streams = make(map[string]*StreamWriter) } - f.streams[sheetXML] = sw + f.streams[sheetPath] = sw _, _ = sw.rawData.WriteString(XMLHeader + ``) + sw.sheetWritten = true + } _, _ = sw.rawData.WriteString(``) bulkAppendFields(&sw.rawData, sw.worksheet, 8, 15) if sw.mergeCellsCount > 0 { @@ -476,10 +480,10 @@ func (sw *StreamWriter) Flush() error { return err } - sheetXML := fmt.Sprintf("xl/worksheets/sheet%d.xml", sw.SheetID) - delete(sw.File.Sheet, sheetXML) - delete(sw.File.checked, sheetXML) - delete(sw.File.XLSX, sheetXML) + sheetPath := sw.File.sheetMap[trimSheetName(sw.Sheet)] + delete(sw.File.Sheet, sheetPath) + delete(sw.File.checked, sheetPath) + delete(sw.File.XLSX, sheetPath) return nil } diff --git a/stream_test.go b/stream_test.go index 0834a2dc1e..391c99d03d 100644 --- a/stream_test.go +++ b/stream_test.go @@ -68,13 +68,13 @@ func TestStreamWriter(t *testing.T) { assert.NoError(t, streamWriter.SetRow(cell, row)) } - // Test set cell column overflow. - assert.EqualError(t, streamWriter.SetRow("XFD1", []interface{}{"A", "B", "C"}), ErrColumnNumber.Error()) - assert.NoError(t, streamWriter.Flush()) // Save spreadsheet by the given path. 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()) + // Test close temporary file error. file = NewFile() streamWriter, err = file.NewStreamWriter("Sheet1") From 37342f6d81b27adbcdc6cf6cf9ccdc68f92cdb46 Mon Sep 17 00:00:00 2001 From: ice Date: Fri, 14 May 2021 13:01:08 +0800 Subject: [PATCH 383/957] "15" is the correct 24 hours time format in go (#839) * "15" is the correct 24 hours time format in go * fix number format convert issue and remove the `dateTimeFormatsCache` --- styles.go | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/styles.go b/styles.go index 4369f29cbf..a701a69e4f 100644 --- a/styles.go +++ b/styles.go @@ -934,8 +934,6 @@ func formatToE(v string, format string) string { return fmt.Sprintf("%.e", f) } -var dateTimeFormatsCache = map[string]string{} - // parseTime provides a function to returns a string parsed using time.Time. // Replace Excel placeholders with Go time placeholders. For example, replace // yyyy with 2006. These are in a specific order, due to the fact that m is @@ -963,11 +961,6 @@ func parseTime(v string, format string) string { return v } - goFmt, found := dateTimeFormatsCache[format] - if found { - return val.Format(goFmt) - } - goFmt = format if strings.Contains(goFmt, "[") { @@ -1023,9 +1016,17 @@ func parseTime(v string, format string) string { goFmt = strings.Replace(goFmt, "H", "3", 1) } else { goFmt = strings.Replace(goFmt, "hh", "15", 1) - goFmt = strings.Replace(goFmt, "h", "3", 1) + if val.Hour() < 12 { + goFmt = strings.Replace(goFmt, "h", "3", 1) + } else { + goFmt = strings.Replace(goFmt, "h", "15", 1) + } goFmt = strings.Replace(goFmt, "HH", "15", 1) - goFmt = strings.Replace(goFmt, "H", "3", 1) + if val.Hour() < 12 { + goFmt = strings.Replace(goFmt, "H", "3", 1) + } else { + goFmt = strings.Replace(goFmt, "H", "15", 1) + } } for _, repl := range replacements { @@ -1045,9 +1046,6 @@ func parseTime(v string, format string) string { goFmt = strings.Replace(goFmt, "[3]", "3", 1) goFmt = strings.Replace(goFmt, "[15]", "15", 1) } - - dateTimeFormatsCache[format] = goFmt - return val.Format(goFmt) } From c8c62c2d2a7da3f8e03ad081a9227bcb47f38c45 Mon Sep 17 00:00:00 2001 From: ice Date: Sat, 15 May 2021 23:42:52 +0800 Subject: [PATCH 384/957] * This closes #841, fix incorrect build number format in PR #839 --- styles.go | 23 ++++++++++++++--------- styles_test.go | 8 ++++++++ 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/styles.go b/styles.go index a701a69e4f..1ba9f08b1e 100644 --- a/styles.go +++ b/styles.go @@ -46,8 +46,8 @@ var builtInNumFmt = map[int]string{ 17: "mmm-yy", 18: "h:mm am/pm", 19: "h:mm:ss am/pm", - 20: "h:mm", - 21: "h:mm:ss", + 20: "hh:mm", + 21: "hh:mm:ss", 22: "m/d/yy hh:mm", 37: "#,##0 ;(#,##0)", 38: "#,##0 ;[red](#,##0)", @@ -1009,6 +1009,10 @@ func parseTime(v string, format string) string { } // It is the presence of the "am/pm" indicator that determines if this is // a 12 hour or 24 hours time format, not the number of 'h' characters. + var padding bool + if val.Hour() == 0 && !strings.Contains(format, "hh") && !strings.Contains(format, "HH") { + padding = true + } if is12HourTime(format) { goFmt = strings.Replace(goFmt, "hh", "3", 1) goFmt = strings.Replace(goFmt, "h", "3", 1) @@ -1016,15 +1020,12 @@ func parseTime(v string, format string) string { goFmt = strings.Replace(goFmt, "H", "3", 1) } else { goFmt = strings.Replace(goFmt, "hh", "15", 1) - if val.Hour() < 12 { - goFmt = strings.Replace(goFmt, "h", "3", 1) - } else { - goFmt = strings.Replace(goFmt, "h", "15", 1) - } goFmt = strings.Replace(goFmt, "HH", "15", 1) - if val.Hour() < 12 { + if 0 < val.Hour() && val.Hour() < 12 { + goFmt = strings.Replace(goFmt, "h", "3", 1) goFmt = strings.Replace(goFmt, "H", "3", 1) } else { + goFmt = strings.Replace(goFmt, "h", "15", 1) goFmt = strings.Replace(goFmt, "H", "15", 1) } } @@ -1046,7 +1047,11 @@ func parseTime(v string, format string) string { goFmt = strings.Replace(goFmt, "[3]", "3", 1) goFmt = strings.Replace(goFmt, "[15]", "15", 1) } - return val.Format(goFmt) + s := val.Format(goFmt) + if padding { + s = strings.Replace(s, "00:", "0:", 1) + } + return s } // is12HourTime checks whether an Excel time format string is a 12 hours form. diff --git a/styles_test.go b/styles_test.go index d3fde0c288..e2eed1d7f9 100644 --- a/styles_test.go +++ b/styles_test.go @@ -293,6 +293,14 @@ func TestParseTime(t *testing.T) { assert.Equal(t, "2019-03-04 05:05:42", parseTime("43528.2123", "YYYY-MM-DD hh:mm:ss")) assert.Equal(t, "2019-03-04 05:05:42", parseTime("43528.2123", "YYYY-MM-DD hh:mm:ss;YYYY-MM-DD hh:mm:ss")) assert.Equal(t, "3/4/2019 5:5:42", parseTime("43528.2123", "M/D/YYYY h:m:s")) + assert.Equal(t, "3/4/2019 0:5:42", parseTime("43528.003958333335", "m/d/yyyy h:m:s")) + assert.Equal(t, "3/4/2019 0:05:42", parseTime("43528.003958333335", "M/D/YYYY h:mm:s")) + assert.Equal(t, "0:05", parseTime("43528.003958333335", "h:mm")) + assert.Equal(t, "0:0", parseTime("6.9444444444444444E-5", "h:m")) + assert.Equal(t, "0:00", parseTime("6.9444444444444444E-5", "h:mm")) + assert.Equal(t, "0:0", parseTime("6.9444444444444444E-5", "h:m")) + assert.Equal(t, "12:1", parseTime("0.50070601851851848", "h:m")) + assert.Equal(t, "23:30", parseTime("0.97952546296296295", "h:m")) assert.Equal(t, "March", parseTime("43528", "mmmm")) assert.Equal(t, "Monday", parseTime("43528", "dddd")) } From 5bf3ea61547df2a36757f14bfba47bf22b747079 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 16 May 2021 00:03:09 +0800 Subject: [PATCH 385/957] This closes #842, avoid empty rows in the tail of the worksheet --- README.md | 2 +- README_zh.md | 2 +- rows.go | 12 +++++++++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3e6728f7da..5beea06671 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 XLSX / XLSM / XLTM 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/360EntSecGroup-Skylar/excelize/v2?tab=doc) 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 XLSX / XLSM / 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/360EntSecGroup-Skylar/excelize/v2?tab=doc) and [docs reference](https://xuri.me/excelize/). ## Basic Usage diff --git a/README_zh.md b/README_zh.md index 4e0a499276..b8c02011da 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 及以上版本创建的电子表格文档。支持 XLSX / XLSM / XLTM 等多种文档格式,高度兼容带有样式、图片(表)、透视表、切片器等复杂组件的文档,并提供流式读写 API,用于处理包含大规模数据的工作簿。可应用于各类报表平台、云计算、边缘计算等系统。使用本类库要求使用的 Go 语言为 1.15 或更高版本,完整的 API 使用文档请访问 [go.dev](https://pkg.go.dev/github.com/360EntSecGroup-Skylar/excelize/v2?tab=doc) 或查看 [参考文档](https://xuri.me/excelize/)。 +Excelize 是 Go 语言编写的用于操作 Office Excel 文档基础库,基于 ECMA-376,ISO/IEC 29500 国际标准。可以使用它来读取、写入由 Microsoft Excel™ 2007 及以上版本创建的电子表格文档。支持 XLSX / XLSM / XLTM / XLTX 等多种文档格式,高度兼容带有样式、图片(表)、透视表、切片器等复杂组件的文档,并提供流式读写 API,用于处理包含大规模数据的工作簿。可应用于各类报表平台、云计算、边缘计算等系统。使用本类库要求使用的 Go 语言为 1.15 或更高版本,完整的 API 使用文档请访问 [go.dev](https://pkg.go.dev/github.com/360EntSecGroup-Skylar/excelize/v2?tab=doc) 或查看 [参考文档](https://xuri.me/excelize/)。 ## 快速上手 diff --git a/rows.go b/rows.go index 451dbe9f97..41476629dd 100644 --- a/rows.go +++ b/rows.go @@ -43,15 +43,19 @@ func (f *File) GetRows(sheet string) ([][]string, error) { if err != nil { return nil, err } - results := make([][]string, 0, 64) + results, cur, max := make([][]string, 0, 64), 0, 0 for rows.Next() { + cur++ row, err := rows.Columns() if err != nil { break } results = append(results, row) + if len(row) > 0 { + max = cur + } } - return results, nil + return results[:max], nil } // Rows defines an iterator to a sheet. @@ -161,7 +165,9 @@ func rowXMLHandler(rowIterator *rowXMLIterator, xmlElement *xml.StartElement) { } blank := rowIterator.cellCol - len(rowIterator.columns) val, _ := colCell.getValueFrom(rowIterator.rows.f, rowIterator.d) - rowIterator.columns = append(appendSpace(blank, rowIterator.columns), val) + if val != "" { + rowIterator.columns = append(appendSpace(blank, rowIterator.columns), val) + } } } From 2f74ec171d8c42367666014fa661eab26f088e68 Mon Sep 17 00:00:00 2001 From: william Date: Mon, 24 May 2021 15:27:36 +0800 Subject: [PATCH 386/957] fix the bug when there was no count attribute in sharedStrings file --- rows.go | 3 +++ xmlWorksheet.go | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/rows.go b/rows.go index 41476629dd..d8750eb14a 100644 --- a/rows.go +++ b/rows.go @@ -322,6 +322,9 @@ func (f *File) sharedStringsReader() *xlsxSST { Decode(&sharedStrings); err != nil && err != io.EOF { log.Printf("xml decode error: %s", err) } + if sharedStrings.Count == 0 { + sharedStrings.Count = len(sharedStrings.SI) + } if sharedStrings.UniqueCount == 0 { sharedStrings.UniqueCount = sharedStrings.Count } diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 438e900c7e..82ac39547c 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -248,9 +248,9 @@ type xlsxSheetPr struct { // adjust the direction of grouper controls. type xlsxOutlinePr struct { ApplyStyles *bool `xml:"applyStyles,attr"` - SummaryBelow bool `xml:"summaryBelow,attr,omitempty"` - SummaryRight bool `xml:"summaryRight,attr,omitempty"` - ShowOutlineSymbols bool `xml:"showOutlineSymbols,attr,omitempty"` + SummaryBelow bool `xml:"summaryBelow,attr"` + SummaryRight bool `xml:"summaryRight,attr"` + ShowOutlineSymbols bool `xml:"showOutlineSymbols,attr"` } // xlsxPageSetUpPr expresses page setup properties of the worksheet. From faa50c3326f05833e560653f368b80cb2cf0fac0 Mon Sep 17 00:00:00 2001 From: si9ma Date: Thu, 27 May 2021 13:30:48 +0800 Subject: [PATCH 387/957] feat: add disable option for chart xAxis and yAxis --- drawing.go | 6 +++--- xmlChart.go | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/drawing.go b/drawing.go index 1d9a63b257..83ec85bb22 100644 --- a/drawing.go +++ b/drawing.go @@ -966,7 +966,7 @@ func (f *File) drawPlotAreaCatAx(formatSet *formatChart) []*cAxs { Max: max, Min: min, }, - Delete: &attrValBool{Val: boolPtr(false)}, + Delete: &attrValBool{Val: boolPtr(formatSet.XAxis.None)}, AxPos: &attrValString{Val: stringPtr(catAxPos[formatSet.XAxis.ReverseOrder])}, NumFmt: &cNumFmt{ FormatCode: "General", @@ -1020,7 +1020,7 @@ func (f *File) drawPlotAreaValAx(formatSet *formatChart) []*cAxs { Max: max, Min: min, }, - Delete: &attrValBool{Val: boolPtr(false)}, + Delete: &attrValBool{Val: boolPtr(formatSet.YAxis.None)}, AxPos: &attrValString{Val: stringPtr(valAxPos[formatSet.YAxis.ReverseOrder])}, NumFmt: &cNumFmt{ FormatCode: chartValAxNumFmtFormatCode[formatSet.Type], @@ -1069,7 +1069,7 @@ func (f *File) drawPlotAreaSerAx(formatSet *formatChart) []*cAxs { Max: max, Min: min, }, - Delete: &attrValBool{Val: boolPtr(false)}, + Delete: &attrValBool{Val: boolPtr(formatSet.YAxis.None)}, AxPos: &attrValString{Val: stringPtr(catAxPos[formatSet.XAxis.ReverseOrder])}, TickLblPos: &attrValString{Val: stringPtr("nextTo")}, SpPr: f.drawPlotAreaSpPr(), diff --git a/xmlChart.go b/xmlChart.go index 85a2c5c040..a838f51d62 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -520,6 +520,7 @@ 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"` From 58f9287559b26ec44a9a68c4351efa88327be51d Mon Sep 17 00:00:00 2001 From: Alluuu <22728104+Alluuu@users.noreply.github.com> Date: Fri, 4 Jun 2021 18:06:58 +0300 Subject: [PATCH 388/957] This closes #409 Remove UTC timezone requirement from date.go (#853) According to issue #409 There is absolutely no reason for the timezone to be in UTC, and converting the local times to UTC while keeping values is hacky at least. Excel has no understanding of timezones, hence the user of this library should know what timezone their values are supposed to be, by following the timezone within their timeTime structs. --- date.go | 7 ------- date_test.go | 2 +- excelize_test.go | 2 +- stream_test.go | 2 +- 4 files changed, 3 insertions(+), 10 deletions(-) diff --git a/date.go b/date.go index 9ef5caf5c0..0531b6c757 100644 --- a/date.go +++ b/date.go @@ -30,13 +30,6 @@ var ( func timeToExcelTime(t time.Time) (float64, error) { // TODO in future this should probably also handle date1904 and like TimeFromExcelTime - // Force user to explicit convet passed value to UTC time. - // Because for example 1900-01-01 00:00:00 +0300 MSK converts to 1900-01-01 00:00:00 +0230 LMT - // probably due to daylight saving. - if t.Location() != time.UTC { - return 0.0, ErrToExcelTime - } - if t.Before(excelMinTime1900) { return 0.0, nil } diff --git a/date_test.go b/date_test.go index ecc96ba9c2..38898b0124 100644 --- a/date_test.go +++ b/date_test.go @@ -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)) - assert.EqualError(t, err, ErrToExcelTime.Error()) + assert.NoError(t, err) }) } } diff --git a/excelize_test.go b/excelize_test.go index 0c42178a92..ba7b528e87 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -144,7 +144,7 @@ func TestOpenFile(t *testing.T) { assert.NoError(t, f.SetCellValue("Sheet2", "G2", nil)) - assert.EqualError(t, f.SetCellValue("Sheet2", "G4", time.Now()), ErrToExcelTime.Error()) + assert.NoError(t, f.SetCellValue("Sheet2", "G4", time.Now())) assert.NoError(t, f.SetCellValue("Sheet2", "G4", time.Now().UTC())) // 02:46:40 diff --git a/stream_test.go b/stream_test.go index 391c99d03d..cf133f14aa 100644 --- a/stream_test.go +++ b/stream_test.go @@ -57,7 +57,7 @@ func TestStreamWriter(t *testing.T) { assert.NoError(t, err) assert.NoError(t, streamWriter.SetRow("A4", []interface{}{Cell{StyleID: styleID}, Cell{Formula: "SUM(A10,B10)"}})) assert.NoError(t, streamWriter.SetRow("A5", []interface{}{&Cell{StyleID: styleID, Value: "cell"}, &Cell{Formula: "SUM(A10,B10)"}})) - assert.EqualError(t, streamWriter.SetRow("A6", []interface{}{time.Now()}), ErrToExcelTime.Error()) + assert.NoError(t, streamWriter.SetRow("A6", []interface{}{time.Now()})) for rowID := 10; rowID <= 51200; rowID++ { row := make([]interface{}, 50) From d932e62a127da177769ec3f35aae985ca0a516db Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 5 Jun 2021 00:06:14 +0800 Subject: [PATCH 389/957] This closes #855, fix missing formula cell when getting rows value --- rows.go | 2 +- rows_test.go | 8 +++++++- stream.go | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/rows.go b/rows.go index d8750eb14a..70a59d83dc 100644 --- a/rows.go +++ b/rows.go @@ -165,7 +165,7 @@ func rowXMLHandler(rowIterator *rowXMLIterator, xmlElement *xml.StartElement) { } blank := rowIterator.cellCol - len(rowIterator.columns) val, _ := colCell.getValueFrom(rowIterator.rows.f, rowIterator.d) - if val != "" { + if val != "" || colCell.F != nil { rowIterator.columns = append(appendSpace(blank, rowIterator.columns), val) } } diff --git a/rows_test.go b/rows_test.go index 5e7fd37cd4..585fe59aed 100644 --- a/rows_test.go +++ b/rows_test.go @@ -138,12 +138,18 @@ 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") + assert.NoError(t, f.SetCellValue("Sheet2", "A2", true)) + height, err = f.GetRowHeight("Sheet2", 1) + assert.NoError(t, err) + assert.Equal(t, 15.0, height) + err = f.SaveAs(filepath.Join("test", "TestRowHeight.xlsx")) if !assert.NoError(t, err) { t.FailNow() } - convertColWidthToPixels(0) + assert.Equal(t, 0.0, convertColWidthToPixels(0)) } func TestColumns(t *testing.T) { diff --git a/stream.go b/stream.go index e5fa237329..054dd8d189 100644 --- a/stream.go +++ b/stream.go @@ -333,7 +333,7 @@ func (sw *StreamWriter) SetRow(axis string, values []interface{}) error { } // SetColWidth provides a function to set the width of a single column or -// multiple columns for the the StreamWriter. Note that you must call +// multiple columns for the StreamWriter. Note that you must call // the 'SetColWidth' function before the 'SetRow' function. For example set // the width column B:C as 20: // From 2c90b3f53559076af661e3aebabfad9643a77c40 Mon Sep 17 00:00:00 2001 From: jaby Date: Mon, 7 Jun 2021 12:49:20 +0200 Subject: [PATCH 390/957] fixes #856 (#857) --- calc.go | 4 ++++ calc_test.go | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/calc.go b/calc.go index 573abf2b47..146573cdb9 100644 --- a/calc.go +++ b/calc.go @@ -590,6 +590,10 @@ func (f *File) evalInfixExp(sheet, cell string, tokens []efp.Token) (efp.Token, } if nextToken.TType == efp.TokenTypeArgument || nextToken.TType == efp.TokenTypeFunction { // parse reference: reference or range at here + refTo := f.getDefinedNameRefTo(token.TValue, sheet) + if refTo != "" { + token.TValue = refTo + } result, err := f.parseReference(sheet, token.TValue) if err != nil { return efp.Token{TValue: formulaErrorNAME}, err diff --git a/calc_test.go b/calc_test.go index 23568ff5c3..22b90e69d2 100644 --- a/calc_test.go +++ b/calc_test.go @@ -2335,11 +2335,18 @@ func TestCalcWithDefinedName(t *testing.T) { f := prepareCalcData(cellData) assert.NoError(t, f.SetDefinedName(&DefinedName{Name: "defined_name1", RefersTo: "Sheet1!A1", Scope: "Workbook"})) assert.NoError(t, f.SetDefinedName(&DefinedName{Name: "defined_name1", RefersTo: "Sheet1!B1", Scope: "Sheet1"})) + assert.NoError(t, f.SetCellFormula("Sheet1", "C1", "=defined_name1")) result, err := f.CalcCellValue("Sheet1", "C1") assert.NoError(t, err) // DefinedName with scope WorkSheet takes precedence over DefinedName with scope Workbook, so we should get B1 value assert.Equal(t, "B1 value", result, "=defined_name1") + + assert.NoError(t, f.SetCellFormula("Sheet1", "C1", "=CONCATENATE(\"<\",defined_name1,\">\")")) + result, err = f.CalcCellValue("Sheet1", "C1") + assert.NoError(t, err) + assert.Equal(t, "", result, "=defined_name1") + } func TestCalcArithmeticOperations(t *testing.T) { From bafe087a61ca85b16fc69fda280f03eb2d7551dc Mon Sep 17 00:00:00 2001 From: jaby Date: Tue, 8 Jun 2021 13:02:34 +0200 Subject: [PATCH 391/957] This closes #858 (#859) * fixes https://github.com/360EntSecGroup-Skylar/excelize/issues/858 * fixes https://github.com/360EntSecGroup-Skylar/excelize/issues/858 Co-authored-by: dvelderp --- calc.go | 8 +------- calc_test.go | 30 ++++++++++++++++++------------ 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/calc.go b/calc.go index 146573cdb9..8ceceeca50 100644 --- a/calc.go +++ b/calc.go @@ -628,16 +628,10 @@ func (f *File) evalInfixExp(sheet, cell string, tokens []efp.Token) (efp.Token, } // current token is logical - if token.TType == efp.OperatorsInfix && token.TSubType == efp.TokenSubTypeLogical { - } if token.TType == efp.TokenTypeOperand && token.TSubType == efp.TokenSubTypeLogical { argsStack.Peek().(*list.List).PushBack(newStringFormulaArg(token.TValue)) } - // current token is text - if token.TType == efp.TokenTypeOperand && token.TSubType == efp.TokenSubTypeText { - argsStack.Peek().(*list.List).PushBack(newStringFormulaArg(token.TValue)) - } if err = f.evalInfixExpFunc(sheet, cell, token, nextToken, opfStack, opdStack, opftStack, opfdStack, argsStack); err != nil { return efp.Token{}, err } @@ -1012,7 +1006,7 @@ func (f *File) parseToken(sheet string, token efp.Token, opdStack, optStack *Sta optStack.Pop() } // opd - if token.TType == efp.TokenTypeOperand && token.TSubType == efp.TokenSubTypeNumber { + if token.TType == efp.TokenTypeOperand && (token.TSubType == efp.TokenSubTypeNumber || token.TSubType == efp.TokenSubTypeText) { opdStack.Push(token) } return nil diff --git a/calc_test.go b/calc_test.go index 22b90e69d2..20505fcba4 100644 --- a/calc_test.go +++ b/calc_test.go @@ -34,18 +34,20 @@ func TestCalcCellValue(t *testing.T) { {nil, nil, nil, "Feb", "South 2", 45500}, } mathCalc := map[string]string{ - "=2^3": "8", - "=1=1": "TRUE", - "=1=2": "FALSE", - "=1<2": "TRUE", - "=3<2": "FALSE", - "=2<=3": "TRUE", - "=2<=1": "FALSE", - "=2>1": "TRUE", - "=2>3": "FALSE", - "=2>=1": "TRUE", - "=2>=3": "FALSE", - "=1&2": "12", + "=2^3": "8", + "=1=1": "TRUE", + "=1=2": "FALSE", + "=1<2": "TRUE", + "=3<2": "FALSE", + "=2<=3": "TRUE", + "=2<=1": "FALSE", + "=2>1": "TRUE", + "=2>3": "FALSE", + "=2>=1": "TRUE", + "=2>=3": "FALSE", + "=1&2": "12", + `="A"="A"`: "TRUE", + `="A"<>"A"`: "FALSE", // Engineering Functions // BESSELI "=BESSELI(4.5,1)": "15.389222753735925", @@ -1084,6 +1086,10 @@ func TestCalcCellValue(t *testing.T) { "=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", // Excel Lookup and Reference Functions // CHOOSE "=CHOOSE(4,\"red\",\"blue\",\"green\",\"brown\")": "brown", From 38162539b61b2b429b0498198ed58e1ef53d284b Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 9 Jun 2021 14:42:20 +0800 Subject: [PATCH 392/957] Create go.yml --- .github/workflows/go.yml | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .github/workflows/go.yml diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 0000000000..13913aa1cb --- /dev/null +++ b/.github/workflows/go.yml @@ -0,0 +1,38 @@ +on: [push, pull_request] +name: build +jobs: + + test: + strategy: + matrix: + go-version: [1.15.x, 1.16.x] + os: [ubuntu-latest, macos-latest, windows-latest] + targetplatform: [x86, x64] + + runs-on: ${{ matrix.os }} + + steps: + + - name: Install Go + uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go-version }} + + - name: Checkout code + uses: actions/checkout@v2 + + - name: Get dependencies + run: | + env GO111MODULE=on go vet ./... + - name: Build + run: go build -v . + + - name: Test + run: env GO111MODULE=on go test -v -race ./... -coverprofile=coverage.txt -covermode=atomic + + - name: Codecov + uses: codecov/codecov-action@v1 + with: + file: coverage.txt + flags: unittests + name: codecov-umbrella From 83e12cc4e5242904366a51f642596acbc9f9dd56 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 11 Jun 2021 22:48:37 +0800 Subject: [PATCH 393/957] support escaped string literal basic string and use GitHub Action instead of TravisCI - Note that: travis-ci.org will shutdown on June 15th, 2021, and I don't have enough permission to migrate this project to travis-ci.com currently --- .travis.yml | 25 ------------------------ README.md | 6 +++++- README_zh.md | 6 +++++- chart.go | 4 ++++ lib.go | 46 +++++++++++++++++++++++++++++++++++++++++++++ lib_test.go | 19 +++++++++++++++++++ xmlSharedStrings.go | 4 ++-- 7 files changed, 81 insertions(+), 29 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 34ce8ee3e8..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,25 +0,0 @@ -language: go - -install: - - go get -d -t -v ./... && go build -v ./... - -go: - - 1.15.x - - 1.16.x - -os: - - linux - - osx - - windows - -env: - jobs: - - GOARCH=amd64 - - GOARCH=386 - -script: - - env GO111MODULE=on go vet ./... - - env GO111MODULE=on go test -v -race ./... -coverprofile=coverage.txt -covermode=atomic - -after_success: - - bash <(curl -s https://codecov.io/bash) diff --git a/README.md b/README.md index 5beea06671..972d01331f 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@

Excelize logo

- Build Status + Build Status Code Coverage Go Report Card go.dev @@ -86,6 +86,10 @@ func main() { fmt.Println(cell) // Get all the rows in the 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") diff --git a/README_zh.md b/README_zh.md index b8c02011da..4e1f56f4bb 100644 --- a/README_zh.md +++ b/README_zh.md @@ -1,7 +1,7 @@

Excelize logo

- Build Status + Build Status Code Coverage Go Report Card go.dev @@ -86,6 +86,10 @@ func main() { fmt.Println(cell) // 获取 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") diff --git a/chart.go b/chart.go index 23ea77614a..9bc4b20d60 100644 --- a/chart.go +++ b/chart.go @@ -739,6 +739,7 @@ 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 @@ -748,6 +749,7 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // // The properties of y_axis that can be set are: // +// none // major_grid_lines // minor_grid_lines // major_unit @@ -755,6 +757,8 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // maximum // minimum // +// none: Disable axes. +// // major_grid_lines: Specifies major gridlines. // // minor_grid_lines: Specifies minor gridlines. diff --git a/lib.go b/lib.go index 99083b15d0..e221d17ca2 100644 --- a/lib.go +++ b/lib.go @@ -18,6 +18,7 @@ import ( "encoding/xml" "fmt" "io" + "regexp" "strconv" "strings" ) @@ -454,6 +455,51 @@ func isNumeric(s string) (bool, int) { return true, p } +// 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 +// 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 +// hexadecimal character in the character's value. For example: The Unicode +// character 8 is not permitted in an XML 1.0 document, so it shall be +// escaped as _x0008_. To store the literal form of an escape sequence, the +// initial underscore shall itself be escaped (i.e. stored as _x005F_). For +// example: The string literal _x0008_ would be stored as _x005F_x0008_. +func bstrUnmarshal(s string) (result string) { + m := regexp.MustCompile(`_x[a-zA-Z0-9]{4}_`) + escapeExp := regexp.MustCompile(`x[a-zA-Z0-9]{4}_`) + matches := m.FindAllStringSubmatchIndex(s, -1) + var cursor int + for _, match := range matches { + result += s[cursor:match[0]] + if s[match[0]:match[1]] == "_x005F_" { + if len(s) > match[1]+6 && !escapeExp.MatchString(s[match[1]:match[1]+6]) { + result += s[match[0]:match[1]] + cursor = match[1] + continue + } + if len(s) > match[1]+5 && s[match[1]:match[1]+5] == "x005F" { + result += "_" + cursor = match[1] + continue + } + if escapeExp.MatchString(s[match[0]:match[1]]) { + result += "_" + cursor = match[1] + continue + } + } + if escapeExp.MatchString(s[match[0]:match[1]]) { + cursor = match[1] + } + } + if cursor < len(s) { + result += s[cursor:] + } + return result +} + // Stack defined an abstract data type that serves as a collection of elements. type Stack struct { list *list.List diff --git a/lib_test.go b/lib_test.go index eb0a2893ec..bd28c7e411 100644 --- a/lib_test.go +++ b/lib_test.go @@ -234,3 +234,22 @@ func TestGenXMLNamespace(t *testing.T) { {Name: xml.Name{Space: NameSpaceXML, Local: "space"}, Value: "preserve"}, }), `xml:space="preserve">`) } + +func TestBstrUnmarshal(t *testing.T) { + bstrs := map[string]string{ + "*": "*", + "*_x0008_": "*", + "_x0008_*": "*", + "*_x0008_*": "**", + "*_x005F__x0008_*": "*_x005F_*", + "*_x005F_x0001_*": "*_x0001_*", + "*_x005F_x005F_x005F_x0006_*": "*_x005F_x0006_*", + "_x005F__x0008_******": "_x005F_******", + "******_x005F__x0008_": "******_x005F_", + "******_x005F__x0008_******": "******_x005F_******", + "*_x005F_x005F__x0008_*": "*_x005F_*", + } + for bstr, expected := range bstrs { + assert.Equal(t, expected, bstrUnmarshal(bstr)) + } +} diff --git a/xmlSharedStrings.go b/xmlSharedStrings.go index 7cb23fd57d..816c931383 100644 --- a/xmlSharedStrings.go +++ b/xmlSharedStrings.go @@ -53,10 +53,10 @@ func (x xlsxSI) String() string { rows.WriteString(s.T.Val) } } - return rows.String() + return bstrUnmarshal(rows.String()) } if x.T != nil { - return x.T.Val + return bstrUnmarshal(x.T.Val) } return "" } From 5faa36430cd77ae256a79cb52a7808d540da50da Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 12 Jun 2021 08:49:18 +0800 Subject: [PATCH 394/957] skip XML control character in the escape literal string, and update dependencies --- go.mod | 4 ++-- go.sum | 10 +++++----- lib.go | 31 +++++++++++++++++++++---------- lib_test.go | 2 ++ 4 files changed, 30 insertions(+), 17 deletions(-) diff --git a/go.mod b/go.mod index 692eaa331f..78ae93c412 100644 --- a/go.mod +++ b/go.mod @@ -7,8 +7,8 @@ require ( github.com/richardlehane/mscfb v1.0.3 github.com/stretchr/testify v1.6.1 github.com/xuri/efp v0.0.0-20210322160811-ab561f5b45e3 - golang.org/x/crypto v0.0.0-20210415154028-4f45737414dc + golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb - golang.org/x/net v0.0.0-20210415231046-e915ea6b2b7d + golang.org/x/net v0.0.0-20210610132358-84b48f89b13b golang.org/x/text v0.3.6 ) diff --git a/go.sum b/go.sum index a709fdfe20..309a85b514 100644 --- a/go.sum +++ b/go.sum @@ -13,15 +13,15 @@ github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/xuri/efp v0.0.0-20210322160811-ab561f5b45e3 h1:EpI0bqf/eX9SdZDwlMmahKM+CDBgNbsXMhsN28XrM8o= github.com/xuri/efp v0.0.0-20210322160811-ab561f5b45e3/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= -golang.org/x/crypto v0.0.0-20210415154028-4f45737414dc h1:+q90ECDSAQirdykUN6sPEiBXBsp8Csjcca8Oy7bgLTA= -golang.org/x/crypto v0.0.0-20210415154028-4f45737414dc/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a h1:kr2P4QFmQr29mSLA43kwrOcgcReGTfbE9N577tCTuBc= +golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb h1:fqpd0EBDzlHRCjiphRR5Zo/RSWWQlWv34418dnEixWk= golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210415231046-e915ea6b2b7d h1:BgJvlyh+UqCUaPlscHJ+PN8GcpfrFdr7NHjd1JL0+Gs= -golang.org/x/net v0.0.0-20210415231046-e915ea6b2b7d/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= +golang.org/x/net v0.0.0-20210610132358-84b48f89b13b h1:k+E048sYJHyVnsr1GDrRZWQ32D2C7lWs9JRc0bel53A= +golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 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= diff --git a/lib.go b/lib.go index e221d17ca2..eb8ced6075 100644 --- a/lib.go +++ b/lib.go @@ -21,6 +21,7 @@ import ( "regexp" "strconv" "strings" + "unicode" ) // ReadZipReader can be used to read the spreadsheet in memory without touching the @@ -467,34 +468,44 @@ func isNumeric(s string) (bool, int) { // initial underscore shall itself be escaped (i.e. stored as _x005F_). For // example: The string literal _x0008_ would be stored as _x005F_x0008_. func bstrUnmarshal(s string) (result string) { - m := regexp.MustCompile(`_x[a-zA-Z0-9]{4}_`) + bstrExp := regexp.MustCompile(`_x[a-zA-Z0-9]{4}_`) escapeExp := regexp.MustCompile(`x[a-zA-Z0-9]{4}_`) - matches := m.FindAllStringSubmatchIndex(s, -1) - var cursor int + matches, l, cursor := bstrExp.FindAllStringSubmatchIndex(s, -1), len(s), 0 for _, match := range matches { result += s[cursor:match[0]] - if s[match[0]:match[1]] == "_x005F_" { - if len(s) > match[1]+6 && !escapeExp.MatchString(s[match[1]:match[1]+6]) { - result += s[match[0]:match[1]] + subStr := s[match[0]:match[1]] + if subStr == "_x005F_" { + if l > match[1]+6 && !escapeExp.MatchString(s[match[1]:match[1]+6]) { + result += subStr cursor = match[1] continue } - if len(s) > match[1]+5 && s[match[1]:match[1]+5] == "x005F" { + if l > match[1]+5 && s[match[1]:match[1]+5] == "x005F" { result += "_" cursor = match[1] continue } - if escapeExp.MatchString(s[match[0]:match[1]]) { + if escapeExp.MatchString(subStr) { result += "_" cursor = match[1] continue } } - if escapeExp.MatchString(s[match[0]:match[1]]) { + if bstrExp.MatchString(subStr) { + x, _ := strconv.Unquote(`"\u` + s[match[0]+2:match[1]-1] + `"`) + hasRune := false + for _, c := range string(x) { + if unicode.IsControl(c) { + hasRune = true + } + } + if !hasRune { + result += string(x) + } cursor = match[1] } } - if cursor < len(s) { + if cursor < l { result += s[cursor:] } return result diff --git a/lib_test.go b/lib_test.go index bd28c7e411..58c6ed9fbd 100644 --- a/lib_test.go +++ b/lib_test.go @@ -237,10 +237,12 @@ func TestGenXMLNamespace(t *testing.T) { func TestBstrUnmarshal(t *testing.T) { bstrs := map[string]string{ + "*_x0000_": "*", "*": "*", "*_x0008_": "*", "_x0008_*": "*", "*_x0008_*": "**", + "*_x4F60__x597D_": "*你好", "*_x005F__x0008_*": "*_x005F_*", "*_x005F_x0001_*": "*_x0001_*", "*_x005F_x005F_x005F_x0006_*": "*_x005F_x0006_*", From e354db69b0bdd02357c221af9ef09695dd8fd122 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 13 Jun 2021 11:23:52 +0800 Subject: [PATCH 395/957] string pattern match context check instead of regex lookahead assertion --- lib.go | 31 ++++++++++++++++--------------- lib_test.go | 7 +++++-- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/lib.go b/lib.go index eb8ced6075..baa62bf5e9 100644 --- a/lib.go +++ b/lib.go @@ -475,34 +475,35 @@ func bstrUnmarshal(s string) (result string) { result += s[cursor:match[0]] subStr := s[match[0]:match[1]] if subStr == "_x005F_" { + cursor = match[1] if l > match[1]+6 && !escapeExp.MatchString(s[match[1]:match[1]+6]) { result += subStr - cursor = match[1] - continue - } - if l > match[1]+5 && s[match[1]:match[1]+5] == "x005F" { - result += "_" - cursor = match[1] - continue - } - if escapeExp.MatchString(subStr) { - result += "_" - cursor = match[1] continue } + result += "_" + continue } if bstrExp.MatchString(subStr) { - x, _ := strconv.Unquote(`"\u` + s[match[0]+2:match[1]-1] + `"`) + cursor = match[1] + v, err := strconv.Unquote(`"\u` + s[match[0]+2:match[1]-1] + `"`) + if err != nil { + if l > match[1]+6 && escapeExp.MatchString(s[match[1]:match[1]+6]) { + result += subStr[:6] + cursor = match[1] + 6 + continue + } + result += subStr + continue + } hasRune := false - for _, c := range string(x) { + for _, c := range v { if unicode.IsControl(c) { hasRune = true } } if !hasRune { - result += string(x) + result += v } - cursor = match[1] } } if cursor < l { diff --git a/lib_test.go b/lib_test.go index 58c6ed9fbd..ad209467cf 100644 --- a/lib_test.go +++ b/lib_test.go @@ -237,19 +237,22 @@ func TestGenXMLNamespace(t *testing.T) { func TestBstrUnmarshal(t *testing.T) { bstrs := map[string]string{ - "*_x0000_": "*", "*": "*", + "*_x0000_": "*", "*_x0008_": "*", "_x0008_*": "*", "*_x0008_*": "**", "*_x4F60__x597D_": "*你好", + "*_xG000_": "*_xG000_", + "*_xG05F_x0001_*": "*_xG05F*", "*_x005F__x0008_*": "*_x005F_*", "*_x005F_x0001_*": "*_x0001_*", + "*_x005f_x005F__x0008_*": "*_x005F_*", + "*_x005F_x005F_xG05F_x0006_*": "*_x005F_xG05F*", "*_x005F_x005F_x005F_x0006_*": "*_x005F_x0006_*", "_x005F__x0008_******": "_x005F_******", "******_x005F__x0008_": "******_x005F_", "******_x005F__x0008_******": "******_x005F_******", - "*_x005F_x005F__x0008_*": "*_x005F_*", } for bstr, expected := range bstrs { assert.Equal(t, expected, bstrUnmarshal(bstr)) From bffb5d6b41f409258a2f0c126a04127f31e28a06 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 13 Jun 2021 14:38:01 +0800 Subject: [PATCH 396/957] make the caller of `getRowHeight` function adapt row number change, update comment: use rows number instead of rows index. --- col.go | 4 ++-- picture.go | 4 ++-- rows.go | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/col.go b/col.go index e1ac5a5ab1..2e3190c120 100644 --- a/col.go +++ b/col.go @@ -592,9 +592,9 @@ func (f *File) positionObjectPixels(sheet string, col, row, x1, y1, width, heigh } // Subtract the underlying cell heights to find end cell of the object. - for height >= f.getRowHeight(sheet, rowEnd) { - height -= f.getRowHeight(sheet, rowEnd) + for height >= f.getRowHeight(sheet, rowEnd+1) { rowEnd++ + height -= f.getRowHeight(sheet, rowEnd) } // The end vertices are whatever is left from the width and height. diff --git a/picture.go b/picture.go index 92e01065c0..052ec83918 100644 --- a/picture.go +++ b/picture.go @@ -618,10 +618,10 @@ func (f *File) drawingResize(sheet string, cell string, width, height float64, f if inMergeCell { cellWidth, cellHeight = 0, 0 c, r = rng[0], rng[1] - for col := rng[0] - 1; col < rng[2]; col++ { + for col := rng[0]; col <= rng[2]; col++ { cellWidth += f.getColWidth(sheet, col) } - for row := rng[1] - 1; row < rng[3]; row++ { + for row := rng[1]; row <= rng[3]; row++ { cellHeight += f.getRowHeight(sheet, row) } } diff --git a/rows.go b/rows.go index 70a59d83dc..6c0e8163e8 100644 --- a/rows.go +++ b/rows.go @@ -266,7 +266,7 @@ func (f *File) SetRowHeight(sheet string, row int, height float64) error { } // getRowHeight provides a function to get row height in pixels by given sheet -// name and row index. +// name and row number. func (f *File) getRowHeight(sheet string, row int) int { ws, _ := f.workSheetReader(sheet) for i := range ws.SheetData.Row { @@ -280,7 +280,7 @@ func (f *File) getRowHeight(sheet string, row int) int { } // GetRowHeight provides a function to get row height by given worksheet name -// and row index. For example, get the height of the first row in Sheet1: +// and row number. For example, get the height of the first row in Sheet1: // // height, err := f.GetRowHeight("Sheet1", 1) // From c62ced7ca7a6cf715f62bd10981560a809c723dd Mon Sep 17 00:00:00 2001 From: strong <372045127@qq.com> Date: Sun, 13 Jun 2021 14:42:09 +0800 Subject: [PATCH 397/957] fix getRowHeight actually get the height of the next row (#860) --- rows.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rows.go b/rows.go index 6c0e8163e8..6360f4e7ab 100644 --- a/rows.go +++ b/rows.go @@ -271,7 +271,7 @@ func (f *File) getRowHeight(sheet string, row int) int { ws, _ := f.workSheetReader(sheet) for i := range ws.SheetData.Row { v := &ws.SheetData.Row[i] - if v.R == row+1 && v.Ht != 0 { + if v.R == row && v.Ht != 0 { return int(convertRowHeightToPixels(v.Ht)) } } From 2cfcf9eb5ff2f332dad0c6adead53ef0500001db Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 16 Jun 2021 15:03:50 +0000 Subject: [PATCH 398/957] encode the escaped string literal which not permitted in an XML 1.0 document --- cell.go | 3 ++- lib.go | 46 ++++++++++++++++++++++++++++++++++++++++++---- lib_test.go | 13 +++++++++++++ 3 files changed, 57 insertions(+), 5 deletions(-) diff --git a/cell.go b/cell.go index f94b81e9d5..4dec093526 100644 --- a/cell.go +++ b/cell.go @@ -288,6 +288,7 @@ func (f *File) setSharedString(val string) int { } sst.Count++ sst.UniqueCount++ + val = bstrMarshal(val) t := xlsxT{Val: val} // Leading and ending space(s) character detection. if len(val) > 0 && (val[0] == 32 || val[len(val)-1] == 32) { @@ -315,7 +316,7 @@ func setCellStr(value string) (t string, v string, ns xml.Attr) { } } t = "str" - v = value + v = bstrMarshal(value) return } diff --git a/lib.go b/lib.go index baa62bf5e9..5c9bbf6eb2 100644 --- a/lib.go +++ b/lib.go @@ -456,6 +456,11 @@ func isNumeric(s string) (bool, int) { return true, p } +var ( + bstrExp = regexp.MustCompile(`_x[a-zA-Z\d]{4}_`) + bstrEscapeExp = regexp.MustCompile(`x[a-zA-Z\d]{4}_`) +) + // 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 @@ -468,15 +473,13 @@ func isNumeric(s string) (bool, int) { // initial underscore shall itself be escaped (i.e. stored as _x005F_). For // example: The string literal _x0008_ would be stored as _x005F_x0008_. func bstrUnmarshal(s string) (result string) { - bstrExp := regexp.MustCompile(`_x[a-zA-Z0-9]{4}_`) - escapeExp := regexp.MustCompile(`x[a-zA-Z0-9]{4}_`) matches, l, cursor := bstrExp.FindAllStringSubmatchIndex(s, -1), len(s), 0 for _, match := range matches { result += s[cursor:match[0]] subStr := s[match[0]:match[1]] if subStr == "_x005F_" { cursor = match[1] - if l > match[1]+6 && !escapeExp.MatchString(s[match[1]:match[1]+6]) { + if l > match[1]+6 && !bstrEscapeExp.MatchString(s[match[1]:match[1]+6]) { result += subStr continue } @@ -487,7 +490,7 @@ func bstrUnmarshal(s string) (result string) { cursor = match[1] v, err := strconv.Unquote(`"\u` + s[match[0]+2:match[1]-1] + `"`) if err != nil { - if l > match[1]+6 && escapeExp.MatchString(s[match[1]:match[1]+6]) { + if l > match[1]+6 && bstrEscapeExp.MatchString(s[match[1]:match[1]+6]) { result += subStr[:6] cursor = match[1] + 6 continue @@ -512,6 +515,41 @@ func bstrUnmarshal(s string) (result string) { return result } +// bstrMarshal encode the escaped string literal which not permitted in an XML +// 1.0 document. +func bstrMarshal(s string) (result string) { + matches, l, cursor := bstrExp.FindAllStringSubmatchIndex(s, -1), len(s), 0 + for _, match := range matches { + result += s[cursor:match[0]] + subStr := s[match[0]:match[1]] + if subStr == "_x005F_" { + cursor = match[1] + if match[1]+6 <= l && bstrEscapeExp.MatchString(s[match[1]:match[1]+6]) { + _, err := strconv.Unquote(`"\u` + s[match[1]+1:match[1]+5] + `"`) + if err == nil { + result += subStr + "x005F" + subStr + continue + } + } + result += subStr + "x005F_" + continue + } + if bstrExp.MatchString(subStr) { + cursor = match[1] + _, err := strconv.Unquote(`"\u` + s[match[0]+2:match[1]-1] + `"`) + if err == nil { + result += "_x005F" + subStr + continue + } + result += subStr + } + } + if cursor < l { + result += s[cursor:] + } + return result +} + // Stack defined an abstract data type that serves as a collection of elements. type Stack struct { list *list.List diff --git a/lib_test.go b/lib_test.go index ad209467cf..315688fc17 100644 --- a/lib_test.go +++ b/lib_test.go @@ -258,3 +258,16 @@ func TestBstrUnmarshal(t *testing.T) { assert.Equal(t, expected, bstrUnmarshal(bstr)) } } + +func TestBstrMarshal(t *testing.T) { + bstrs := map[string]string{ + "*_xG05F_*": "*_xG05F_*", + "*_x0008_*": "*_x005F_x0008_*", + "*_x005F_*": "*_x005F_x005F_*", + "*_x005F_xG006_*": "*_x005F_x005F_xG006_*", + "*_x005F_x0006_*": "*_x005F_x005F_x005F_x0006_*", + } + for bstr, expected := range bstrs { + assert.Equal(t, expected, bstrMarshal(bstr)) + } +} From 24967a5c25499f92b4e58b8d6f8a92a46a7acc7a Mon Sep 17 00:00:00 2001 From: Zitao <369815332@qq.com> Date: Tue, 22 Jun 2021 14:06:08 +0800 Subject: [PATCH 399/957] feat: stream write to zip directly (#863) --- file.go | 74 ++++++++++++++++++++++++++++++++++------------------ file_test.go | 43 ++++++++++++++++++++++-------- 2 files changed, 80 insertions(+), 37 deletions(-) diff --git a/file.go b/file.go index 36b1a425a9..495718aa30 100644 --- a/file.go +++ b/file.go @@ -87,17 +87,55 @@ func (f *File) Write(w io.Writer) error { // WriteTo implements io.WriterTo to write the file. func (f *File) WriteTo(w io.Writer) (int64, error) { - buf, err := f.WriteToBuffer() - if err != nil { + if f.options != nil && f.options.Password != "" { + buf, err := f.WriteToBuffer() + if err != nil { + return 0, err + } + return buf.WriteTo(w) + } + if err := f.writeDirectToWriter(w); err != nil { return 0, err } - return buf.WriteTo(w) + return 0, nil } -// WriteToBuffer provides a function to get bytes.Buffer from the saved file. +// WriteToBuffer provides a function to get bytes.Buffer from the saved file. And it allocate space in memory. Be careful when the file size is large. func (f *File) WriteToBuffer() (*bytes.Buffer, error) { buf := new(bytes.Buffer) zw := zip.NewWriter(buf) + + if err := f.writeToZip(zw); err != nil { + return buf, zw.Close() + } + + if f.options != nil && f.options.Password != "" { + if err := zw.Close(); err != nil { + return buf, err + } + b, err := Encrypt(buf.Bytes(), f.options) + if err != nil { + return buf, err + } + buf.Reset() + buf.Write(b) + return buf, nil + } + return buf, zw.Close() +} + +// writeDirectToWriter provides a function to write to io.Writer. +func (f *File) writeDirectToWriter(w io.Writer) error { + zw := zip.NewWriter(w) + if err := f.writeToZip(zw); err != nil { + zw.Close() + return err + } + return zw.Close() +} + +// writeToZip provides a function to write to zip.Writer +func (f *File) writeToZip(zw *zip.Writer) error { f.calcChainWriter() f.commentsWriter() f.contentTypesWriter() @@ -112,19 +150,17 @@ func (f *File) WriteToBuffer() (*bytes.Buffer, error) { for path, stream := range f.streams { fi, err := zw.Create(path) if err != nil { - zw.Close() - return buf, err + return err } var from io.Reader from, err = stream.rawData.Reader() if err != nil { stream.rawData.Close() - return buf, err + return err } _, err = io.Copy(fi, from) if err != nil { - zw.Close() - return buf, err + return err } stream.rawData.Close() } @@ -135,27 +171,13 @@ func (f *File) WriteToBuffer() (*bytes.Buffer, error) { } fi, err := zw.Create(path) if err != nil { - zw.Close() - return buf, err + return err } _, err = fi.Write(content) if err != nil { - zw.Close() - return buf, err + return err } } - if f.options != nil && f.options.Password != "" { - if err := zw.Close(); err != nil { - return buf, err - } - b, err := Encrypt(buf.Bytes(), f.options) - if err != nil { - return buf, err - } - buf.Reset() - buf.Write(b) - return buf, nil - } - return buf, zw.Close() + return nil } diff --git a/file_test.go b/file_test.go index 656271fe85..dbbf75a8eb 100644 --- a/file_test.go +++ b/file_test.go @@ -3,6 +3,7 @@ package excelize import ( "bufio" "bytes" + "os" "strings" "testing" @@ -33,16 +34,36 @@ func BenchmarkWrite(b *testing.B) { } func TestWriteTo(t *testing.T) { - f := File{} - buf := bytes.Buffer{} - f.XLSX = make(map[string][]byte) - f.XLSX["/d/"] = []byte("s") - _, err := f.WriteTo(bufio.NewWriter(&buf)) - assert.EqualError(t, err, "zip: write to directory") - delete(f.XLSX, "/d/") + // Test WriteToBuffer err + { + f := File{} + buf := bytes.Buffer{} + f.XLSX = make(map[string][]byte) + f.XLSX["/d/"] = []byte("s") + _, err := f.WriteTo(bufio.NewWriter(&buf)) + assert.EqualError(t, err, "zip: write to directory") + delete(f.XLSX, "/d/") + } // Test file path overflow - const maxUint16 = 1<<16 - 1 - f.XLSX[strings.Repeat("s", maxUint16+1)] = nil - _, err = f.WriteTo(bufio.NewWriter(&buf)) - assert.EqualError(t, err, "zip: FileHeader.Name too long") + { + f := File{} + buf := bytes.Buffer{} + f.XLSX = make(map[string][]byte) + const maxUint16 = 1<<16 - 1 + f.XLSX[strings.Repeat("s", maxUint16+1)] = nil + _, err := f.WriteTo(bufio.NewWriter(&buf)) + assert.EqualError(t, err, "zip: FileHeader.Name too long") + } + // Test StreamsWriter err + { + f := File{} + buf := bytes.Buffer{} + f.XLSX = make(map[string][]byte) + f.XLSX["s"] = nil + f.streams = make(map[string]*StreamWriter) + file, _ := os.Open("123") + f.streams["s"] = &StreamWriter{rawData: bufferedWriter{tmp: file}} + _, err := f.WriteTo(bufio.NewWriter(&buf)) + assert.Nil(t, err) + } } From f27624acddfb51916e028f421568840595dbad67 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 29 Jun 2021 22:26:55 +0800 Subject: [PATCH 400/957] This closes #866, support use the defined name to reference the data range in pivot table options - Fix incorrect scope when getting defined name - Update docs: use column number instead of index on get column width --- calc.go | 13 ++++++++++--- col.go | 4 ++-- pivotTable.go | 38 ++++++++++++++++++++++++++++---------- pivotTable_test.go | 16 +++++++++++----- 4 files changed, 51 insertions(+), 20 deletions(-) diff --git a/calc.go b/calc.go index 8ceceeca50..934ae43927 100644 --- a/calc.go +++ b/calc.go @@ -955,16 +955,23 @@ func isOperatorPrefixToken(token efp.Token) bool { // 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 { - refTo = definedName.RefersTo // worksheet scope takes precedence over scope workbook when both definedNames exist + if definedName.Scope == "Workbook" { + workbookRefTo = definedName.RefersTo + } if definedName.Scope == currentSheet { - break + worksheetRefTo = definedName.RefersTo } } } - return refTo + refTo = workbookRefTo + if worksheetRefTo != "" { + refTo = worksheetRefTo + } + return } // parseToken parse basic arithmetic operator priority and evaluate based on diff --git a/col.go b/col.go index 2e3190c120..2fd90b2fb8 100644 --- a/col.go +++ b/col.go @@ -604,7 +604,7 @@ func (f *File) positionObjectPixels(sheet string, col, row, x1, y1, width, heigh } // getColWidth provides a function to get column width in pixels by given -// sheet name and column index. +// sheet name and column number. func (f *File) getColWidth(sheet string, col int) int { xlsx, _ := f.workSheetReader(sheet) if xlsx.Cols != nil { @@ -623,7 +623,7 @@ func (f *File) getColWidth(sheet string, col int) int { } // GetColWidth provides a function to get column width by given worksheet name -// and column index. +// and column name. func (f *File) GetColWidth(sheet, col string) (float64, error) { colNum, err := ColumnNameToNumber(col) if err != nil { diff --git a/pivotTable.go b/pivotTable.go index 11c2b31b67..3d93260aca 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -21,6 +21,7 @@ import ( // PivotTableOption directly maps the format settings of the pivot table. type PivotTableOption struct { + pivotTableSheetName string DataRange string PivotTableRange string Rows []PivotTableField @@ -164,14 +165,19 @@ func (f *File) parseFormatPivotTableSet(opt *PivotTableOption) (*xlsxWorksheet, if opt == nil { return nil, "", errors.New("parameter is required") } - dataSheetName, _, err := f.adjustRange(opt.DataRange) - if err != nil { - return nil, "", fmt.Errorf("parameter 'DataRange' parsing error: %s", err.Error()) - } pivotTableSheetName, _, err := f.adjustRange(opt.PivotTableRange) if err != nil { return nil, "", fmt.Errorf("parameter 'PivotTableRange' parsing error: %s", err.Error()) } + opt.pivotTableSheetName = pivotTableSheetName + dataRange := f.getDefinedNameRefTo(opt.DataRange, pivotTableSheetName) + if dataRange == "" { + dataRange = opt.DataRange + } + dataSheetName, _, err := f.adjustRange(dataRange) + if err != nil { + return nil, "", fmt.Errorf("parameter 'DataRange' parsing error: %s", err.Error()) + } dataSheet, err := f.workSheetReader(dataSheetName) if err != nil { return dataSheet, "", err @@ -215,8 +221,12 @@ 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(dataRange string) ([]string, error) { +func (f *File) getPivotFieldsOrder(opt *PivotTableOption) ([]string, error) { order := []string{} + dataRange := f.getDefinedNameRefTo(opt.DataRange, opt.pivotTableSheetName) + if dataRange == "" { + dataRange = opt.DataRange + } dataSheet, coordinates, err := f.adjustRange(dataRange) if err != nil { return order, fmt.Errorf("parameter 'DataRange' parsing error: %s", err.Error()) @@ -235,12 +245,18 @@ func (f *File) getPivotFieldsOrder(dataRange string) ([]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 { // validate data range - dataSheet, coordinates, err := f.adjustRange(opt.DataRange) + definedNameRef := true + dataRange := f.getDefinedNameRefTo(opt.DataRange, opt.pivotTableSheetName) + if dataRange == "" { + definedNameRef = false + dataRange = opt.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.DataRange) + order, _ := f.getPivotFieldsOrder(opt) hcell, _ := CoordinatesToCellName(coordinates[0], coordinates[1]) vcell, _ := CoordinatesToCellName(coordinates[2], coordinates[3]) pc := xlsxPivotCacheDefinition{ @@ -258,7 +274,9 @@ func (f *File) addPivotCache(pivotCacheID int, pivotCacheXML string, opt *PivotT }, CacheFields: &xlsxCacheFields{}, } - + if definedNameRef { + pc.CacheSource.WorksheetSource = &xlsxWorksheetSource{Name: opt.DataRange} + } for _, name := range order { defaultRowsSubtotal, rowOk := f.getPivotTableFieldNameDefaultSubtotal(name, opt.Rows) defaultColumnsSubtotal, colOk := f.getPivotTableFieldNameDefaultSubtotal(name, opt.Columns) @@ -509,7 +527,7 @@ 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.DataRange) + order, err := f.getPivotFieldsOrder(opt) if err != nil { return err } @@ -606,7 +624,7 @@ func (f *File) countPivotCache() int { // to a sequential index by given fields and pivot option. func (f *File) getPivotFieldsIndex(fields []PivotTableField, opt *PivotTableOption) ([]int, error) { pivotFieldsIndex := []int{} - orders, err := f.getPivotFieldsOrder(opt.DataRange) + orders, err := f.getPivotFieldsOrder(opt) if err != nil { return pivotFieldsIndex, err } diff --git a/pivotTable_test.go b/pivotTable_test.go index 40d58d4885..7098b3a1ca 100644 --- a/pivotTable_test.go +++ b/pivotTable_test.go @@ -136,10 +136,16 @@ func TestAddPivotTable(t *testing.T) { ShowColHeaders: true, ShowLastColumn: true, })) - //Test Pivot table with many data, many rows, many cols + // Create pivot table with many data, many rows, many cols and defined name + f.SetDefinedName(&DefinedName{ + Name: "dataRange", + RefersTo: "Sheet1!$A$1:$E$31", + Comment: "Pivot Table Data Range", + Scope: "Sheet2", + }) assert.NoError(t, f.AddPivotTable(&PivotTableOption{ - DataRange: "Sheet1!$A$1:$E$31", - PivotTableRange: "Sheet2!$A$56:$AG$90", + DataRange: "dataRange", + PivotTableRange: "Sheet2!$A$57:$AJ$91", Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, Columns: []PivotTableField{{Data: "Region", DefaultSubtotal: true}, {Data: "Type"}}, Data: []PivotTableField{{Data: "Sales", Subtotal: "Sum", Name: "Sum of Sales"}, {Data: "Sales", Subtotal: "Average", Name: "Average of Sales"}}, @@ -223,7 +229,7 @@ func TestAddPivotTable(t *testing.T) { _, _, err := f.adjustRange("") assert.EqualError(t, err, "parameter is required") // Test get pivot fields order with empty data range - _, err = f.getPivotFieldsOrder("") + _, 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") @@ -288,7 +294,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("SheetN!$A$1:$E$31") + _, err := f.getPivotFieldsOrder(&PivotTableOption{DataRange: "SheetN!$A$1:$E$31"}) assert.EqualError(t, err, "sheet SheetN is not exist") } From 5ec61310dc55c9af93d66e6d225f721738416d1f Mon Sep 17 00:00:00 2001 From: vettich Date: Sat, 3 Jul 2021 11:13:26 +0500 Subject: [PATCH 401/957] fix: LocalSheetID in DefinedName should be equal to SheetIndex instead of SheetID (#868) --- sheet.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/sheet.go b/sheet.go index 97ef57e952..555f8e341c 100644 --- a/sheet.go +++ b/sheet.go @@ -507,17 +507,17 @@ func (f *File) DeleteSheet(name string) { wb := f.workbookReader() wbRels := f.relsReader(f.getWorkbookRelsPath()) activeSheetName := f.GetSheetName(f.GetActiveSheetIndex()) - deleteSheetID := f.getSheetID(name) + deleteLocalSheetID := f.GetSheetIndex(name) // Delete and adjust defined names if wb.DefinedNames != nil { for idx := 0; idx < len(wb.DefinedNames.DefinedName); idx++ { dn := wb.DefinedNames.DefinedName[idx] if dn.LocalSheetID != nil { - sheetID := *dn.LocalSheetID + 1 - if sheetID == deleteSheetID { + localSheetID := *dn.LocalSheetID + if localSheetID == deleteLocalSheetID { wb.DefinedNames.DefinedName = append(wb.DefinedNames.DefinedName[:idx], wb.DefinedNames.DefinedName[idx+1:]...) idx-- - } else if sheetID > deleteSheetID { + } else if localSheetID > deleteLocalSheetID { wb.DefinedNames.DefinedName[idx].LocalSheetID = intPtr(*dn.LocalSheetID - 1) } } @@ -1495,7 +1495,7 @@ func (f *File) SetDefinedName(definedName *DefinedName) error { for _, dn := range wb.DefinedNames.DefinedName { var scope string if dn.LocalSheetID != nil { - scope = f.getSheetNameByID(*dn.LocalSheetID + 1) + scope = f.GetSheetName(*dn.LocalSheetID) } if scope == definedName.Scope && dn.Name == definedName.Name { return errors.New("the same name already exists on the scope") @@ -1525,7 +1525,7 @@ func (f *File) DeleteDefinedName(definedName *DefinedName) error { for idx, dn := range wb.DefinedNames.DefinedName { var scope string if dn.LocalSheetID != nil { - scope = f.getSheetNameByID(*dn.LocalSheetID + 1) + scope = f.GetSheetName(*dn.LocalSheetID) } if scope == definedName.Scope && dn.Name == definedName.Name { wb.DefinedNames.DefinedName = append(wb.DefinedNames.DefinedName[:idx], wb.DefinedNames.DefinedName[idx+1:]...) @@ -1550,7 +1550,7 @@ func (f *File) GetDefinedName() []DefinedName { Scope: "Workbook", } if dn.LocalSheetID != nil && *dn.LocalSheetID >= 0 { - definedName.Scope = f.getSheetNameByID(*dn.LocalSheetID + 1) + definedName.Scope = f.GetSheetName(*dn.LocalSheetID) } definedNames = append(definedNames, definedName) } From 0e02329bedf6648259fd219642bb907bdb07fd21 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 4 Jul 2021 12:13:06 +0800 Subject: [PATCH 402/957] This closes #861, support concurrency get cell picture and remove unused internal function `getSheetNameByID` --- cell_test.go | 10 +++++++++- drawing.go | 16 ++++++++-------- drawing_test.go | 3 ++- excelize.go | 10 +++++----- excelize_test.go | 6 +++--- file.go | 7 ++++--- picture.go | 20 +++++++++++--------- shape.go | 2 +- sheet.go | 43 +++++++++++++++---------------------------- 9 files changed, 58 insertions(+), 59 deletions(-) diff --git a/cell_test.go b/cell_test.go index 51ee956473..2282e55a30 100644 --- a/cell_test.go +++ b/cell_test.go @@ -14,15 +14,23 @@ import ( ) func TestConcurrency(t *testing.T) { - f := NewFile() + f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + assert.NoError(t, err) wg := new(sync.WaitGroup) for i := 1; i <= 5; i++ { wg.Add(1) go func(val int, t *testing.T) { + // Concurrency set cell value assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("A%d", val), val)) assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("B%d", val), strconv.Itoa(val))) _, err := f.GetCellValue("Sheet1", fmt.Sprintf("A%d", val)) assert.NoError(t, err) + // Concurrency get cell picture + name, raw, err := f.GetPicture("Sheet1", "A1") + assert.Equal(t, "", name) + assert.Nil(t, raw) + assert.NoError(t, err) + wg.Done() }(i, t) } diff --git a/drawing.go b/drawing.go index 83ec85bb22..58e266931d 100644 --- a/drawing.go +++ b/drawing.go @@ -1146,8 +1146,8 @@ func (f *File) drawingParser(path string) (*xlsxWsDr, int) { err error ok bool ) - - if f.Drawings[path] == nil { + _, ok = f.Drawings.Load(path) + if !ok { content := xlsxWsDr{} content.A = NameSpaceDrawingML.Value content.Xdr = NameSpaceDrawingMLSpreadSheet.Value @@ -1171,10 +1171,10 @@ func (f *File) drawingParser(path string) (*xlsxWsDr, int) { }) } } - f.Drawings[path] = &content + f.Drawings.Store(path, &content) } - wsDr := f.Drawings[path] - return wsDr, len(wsDr.OneCellAnchor) + len(wsDr.TwoCellAnchor) + 2 + wsDr, _ := f.Drawings.Load(path) + return wsDr.(*xlsxWsDr), len(wsDr.(*xlsxWsDr).OneCellAnchor) + len(wsDr.(*xlsxWsDr).TwoCellAnchor) + 2 } // addDrawingChart provides a function to add chart graphic frame by given @@ -1232,7 +1232,7 @@ func (f *File) addDrawingChart(sheet, drawingXML, cell string, width, height, rI FPrintsWithSheet: formatSet.FPrintsWithSheet, } content.TwoCellAnchor = append(content.TwoCellAnchor, &twoCellAnchor) - f.Drawings[drawingXML] = content + f.Drawings.Store(drawingXML, content) return err } @@ -1272,7 +1272,7 @@ func (f *File) addSheetDrawingChart(drawingXML string, rID int, formatSet *forma FPrintsWithSheet: formatSet.FPrintsWithSheet, } content.AbsoluteAnchor = append(content.AbsoluteAnchor, &absoluteAnchor) - f.Drawings[drawingXML] = content + f.Drawings.Store(drawingXML, content) } // deleteDrawing provides a function to delete chart graphic frame by given by @@ -1313,6 +1313,6 @@ func (f *File) deleteDrawing(col, row int, drawingXML, drawingType string) (err } } } - f.Drawings[drawingXML] = wsDr + f.Drawings.Store(drawingXML, wsDr) return err } diff --git a/drawing_test.go b/drawing_test.go index 1ee8fae3d8..3c0b619eee 100644 --- a/drawing_test.go +++ b/drawing_test.go @@ -12,12 +12,13 @@ package excelize import ( + "sync" "testing" ) func TestDrawingParser(t *testing.T) { f := File{ - Drawings: make(map[string]*xlsxWsDr), + Drawings: sync.Map{}, XLSX: map[string][]byte{ "charset": MacintoshCyrillicCharset, "wsDr": []byte(``)}, diff --git a/excelize.go b/excelize.go index 940acf1bda..66cfd00f38 100644 --- a/excelize.go +++ b/excelize.go @@ -39,7 +39,7 @@ type File struct { CalcChain *xlsxCalcChain Comments map[string]*xlsxComments ContentTypes *xlsxTypes - Drawings map[string]*xlsxWsDr + Drawings sync.Map Path string SharedStrings *xlsxSST sharedStringsMap map[string]int @@ -50,7 +50,7 @@ type File struct { DecodeVMLDrawing map[string]*decodeVmlDrawing VMLDrawing map[string]*vmlDrawing WorkBook *xlsxWorkbook - Relationships map[string]*xlsxRelationships + Relationships sync.Map XLSX map[string][]byte CharsetReader charsetTranscoderFn } @@ -93,12 +93,12 @@ func newFile() *File { checked: make(map[string]bool), sheetMap: make(map[string]string), Comments: make(map[string]*xlsxComments), - Drawings: make(map[string]*xlsxWsDr), + Drawings: sync.Map{}, sharedStringsMap: make(map[string]int), Sheet: make(map[string]*xlsxWorksheet), DecodeVMLDrawing: make(map[string]*decodeVmlDrawing), VMLDrawing: make(map[string]*vmlDrawing), - Relationships: make(map[string]*xlsxRelationships), + Relationships: sync.Map{}, CharsetReader: charset.NewReaderLabel, } } @@ -277,7 +277,7 @@ func (f *File) addRels(relPath, relType, target, targetMode string) int { Target: target, TargetMode: targetMode, }) - f.Relationships[relPath] = rels + f.Relationships.Store(relPath, rels) return rID } diff --git a/excelize_test.go b/excelize_test.go index ba7b528e87..e3cfa53396 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -975,7 +975,7 @@ func TestGetActiveSheetIndex(t *testing.T) { func TestRelsWriter(t *testing.T) { f := NewFile() - f.Relationships["xl/worksheets/sheet/rels/sheet1.xml.rel"] = &xlsxRelationships{} + f.Relationships.Store("xl/worksheets/sheet/rels/sheet1.xml.rel", &xlsxRelationships{}) f.relsWriter() } @@ -1231,7 +1231,7 @@ func TestRelsReader(t *testing.T) { // Test unsupported charset. f := NewFile() rels := "xl/_rels/workbook.xml.rels" - f.Relationships[rels] = nil + f.Relationships.Store(rels, nil) f.XLSX[rels] = MacintoshCyrillicCharset f.relsReader(rels) } @@ -1239,7 +1239,7 @@ func TestRelsReader(t *testing.T) { func TestDeleteSheetFromWorkbookRels(t *testing.T) { f := NewFile() rels := "xl/_rels/workbook.xml.rels" - f.Relationships[rels] = nil + f.Relationships.Store(rels, nil) assert.Equal(t, f.deleteSheetFromWorkbookRels("rID"), "") } diff --git a/file.go b/file.go index 495718aa30..fa73ec83ff 100644 --- a/file.go +++ b/file.go @@ -17,6 +17,7 @@ import ( "fmt" "io" "os" + "sync" ) // NewFile provides a function to create new file by default template. For @@ -40,13 +41,13 @@ func NewFile() *File { f.CalcChain = f.calcChainReader() f.Comments = make(map[string]*xlsxComments) f.ContentTypes = f.contentTypesReader() - f.Drawings = make(map[string]*xlsxWsDr) + 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 = make(map[string]*xlsxRelationships) - f.Relationships["xl/_rels/workbook.xml.rels"] = f.relsReader("xl/_rels/workbook.xml.rels") + f.Relationships = sync.Map{} + f.Relationships.Store("xl/_rels/workbook.xml.rels", f.relsReader("xl/_rels/workbook.xml.rels")) f.Sheet["xl/worksheets/sheet1.xml"], _ = f.workSheetReader("Sheet1") f.sheetMap["Sheet1"] = "xl/worksheets/sheet1.xml" f.Theme = f.themeReader() diff --git a/picture.go b/picture.go index 052ec83918..09283c814c 100644 --- a/picture.go +++ b/picture.go @@ -189,7 +189,7 @@ func (f *File) deleteSheetRelationships(sheet, rID string) { sheetRels.Relationships = append(sheetRels.Relationships[:k], sheetRels.Relationships[k+1:]...) } } - f.Relationships[rels] = sheetRels + f.Relationships.Store(rels, sheetRels) } // addSheetLegacyDrawing provides a function to add legacy drawing element to @@ -228,11 +228,12 @@ func (f *File) countDrawings() int { c1++ } } - for rel := range f.Drawings { - if strings.Contains(rel, "xl/drawings/drawing") { + f.Drawings.Range(func(rel, value interface{}) bool { + if strings.Contains(rel.(string), "xl/drawings/drawing") { c2++ } - } + return true + }) if c1 < c2 { return c2 } @@ -296,7 +297,7 @@ func (f *File) addDrawingPicture(sheet, drawingXML, cell, file string, width, he FPrintsWithSheet: formatSet.FPrintsWithSheet, } content.TwoCellAnchor = append(content.TwoCellAnchor, &twoCellAnchor) - f.Drawings[drawingXML] = content + f.Drawings.Store(drawingXML, content) return err } @@ -582,12 +583,13 @@ func (f *File) getDrawingRelationships(rels, rID string) *xlsxRelationship { // drawingsWriter provides a function to save xl/drawings/drawing%d.xml after // serialize structure. func (f *File) drawingsWriter() { - for path, d := range f.Drawings { + f.Drawings.Range(func(path, d interface{}) bool { if d != nil { - v, _ := xml.Marshal(d) - f.saveFileList(path, v) + v, _ := xml.Marshal(d.(*xlsxWsDr)) + f.saveFileList(path.(string), v) } - } + return true + }) } // drawingResize calculate the height and width after resizing. diff --git a/shape.go b/shape.go index f7e2ef3d54..e58d5cf1a4 100644 --- a/shape.go +++ b/shape.go @@ -436,7 +436,7 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, formatSet *format FPrintsWithSheet: formatSet.Format.FPrintsWithSheet, } content.TwoCellAnchor = append(content.TwoCellAnchor, &twoCellAnchor) - f.Drawings[drawingXML] = content + f.Drawings.Store(drawingXML, content) return err } diff --git a/sheet.go b/sheet.go index 555f8e341c..420235c0bd 100644 --- a/sheet.go +++ b/sheet.go @@ -231,15 +231,16 @@ func (f *File) setWorkbook(name string, sheetID, rid int) { // relsWriter provides a function to save relationships after // serialize structure. func (f *File) relsWriter() { - for path, rel := range f.Relationships { + f.Relationships.Range(func(path, rel interface{}) bool { if rel != nil { - output, _ := xml.Marshal(rel) - if strings.HasPrefix(path, "xl/worksheets/sheet/rels/sheet") { - output = f.replaceNameSpaceBytes(path, output) + output, _ := xml.Marshal(rel.(*xlsxRelationships)) + if strings.HasPrefix(path.(string), "xl/worksheets/sheet/rels/sheet") { + output = f.replaceNameSpaceBytes(path.(string), output) } - f.saveFileList(path, replaceRelationshipsBytes(output)) + f.saveFileList(path.(string), replaceRelationshipsBytes(output)) } - } + return true + }) } // setAppXML update docProps/app.xml file of XML. @@ -359,22 +360,6 @@ func (f *File) SetSheetName(oldName, newName string) { } } -// getSheetNameByID provides a function to get worksheet name of the -// spreadsheet by given worksheet ID. If given sheet ID is invalid, will -// return an empty string. -func (f *File) getSheetNameByID(ID int) string { - wb := f.workbookReader() - if wb == nil || ID < 1 { - return "" - } - for _, sheet := range wb.Sheets.Sheet { - if ID == sheet.SheetID { - return sheet.Name - } - } - return "" -} - // GetSheetName provides a function to get the sheet name of the workbook by // the given sheet index. If the given sheet index is invalid, it will return // an empty string. @@ -541,7 +526,7 @@ func (f *File) DeleteSheet(name string) { delete(f.sheetMap, sheetName) delete(f.XLSX, sheetXML) delete(f.XLSX, rels) - delete(f.Relationships, rels) + f.Relationships.Delete(rels) delete(f.Sheet, sheetXML) delete(f.xmlAttr, sheetXML) f.SheetCount-- @@ -1727,8 +1712,8 @@ func (f *File) RemovePageBreak(sheet, cell string) (err error) { // after deserialization of xl/worksheets/_rels/sheet%d.xml.rels. func (f *File) relsReader(path string) *xlsxRelationships { var err error - - if f.Relationships[path] == nil { + rels, _ := f.Relationships.Load(path) + if rels == nil { _, ok := f.XLSX[path] if ok { c := xlsxRelationships{} @@ -1736,11 +1721,13 @@ func (f *File) relsReader(path string) *xlsxRelationships { Decode(&c); err != nil && err != io.EOF { log.Printf("xml decode error: %s", err) } - f.Relationships[path] = &c + f.Relationships.Store(path, &c) } } - - return f.Relationships[path] + if rels, _ = f.Relationships.Load(path); rels != nil { + return rels.(*xlsxRelationships) + } + return nil } // fillSheetData ensures there are enough rows, and columns in the chosen From 544ef18a8cb9949fcb8833c6d2816783c90f3318 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 5 Jul 2021 00:03:56 +0800 Subject: [PATCH 403/957] - Support concurrency iterate rows and columns - Rename exported field `File.XLSX` to `File.Pkg` - Exported error message --- adjust_test.go | 18 +++---------- calcchain.go | 2 +- calcchain_test.go | 2 +- cell.go | 12 +++++---- cell_test.go | 42 +++++++++++++++++++++-------- chart.go | 14 +++++----- chart_test.go | 14 ++++++---- col.go | 7 +++-- col_test.go | 18 ++++++------- comment.go | 22 +++++++-------- comment_test.go | 6 ++--- crypt.go | 11 +++----- docProps_test.go | 8 +++--- drawing.go | 2 +- drawing_test.go | 6 ++--- errors.go | 34 +++++++++++++++++++++++ excelize.go | 67 ++++++++++++++++++++++++---------------------- excelize_test.go | 34 ++++++++++++++--------- file.go | 48 +++++++++++++++++---------------- file_test.go | 15 ++++++----- lib.go | 6 ++--- merge_test.go | 28 ++++++++++++++----- picture.go | 45 ++++++++++++++++++++----------- picture_test.go | 13 +++++---- pivotTable.go | 23 ++++++++-------- pivotTable_test.go | 4 +-- rows.go | 7 +++-- rows_test.go | 14 ++++++---- sheet.go | 55 ++++++++++++++++++------------------- sheet_test.go | 14 ++++++---- sheetpr_test.go | 8 ++++-- sparkline.go | 2 +- sparkline_test.go | 4 ++- stream.go | 6 ++--- stream_test.go | 8 +++--- styles.go | 7 +++-- styles_test.go | 6 ++--- table.go | 7 ++--- 38 files changed, 377 insertions(+), 262 deletions(-) diff --git a/adjust_test.go b/adjust_test.go index 0d63ed62ef..c4af38b13a 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -73,20 +73,10 @@ func TestAdjustAutoFilter(t *testing.T) { func TestAdjustHelper(t *testing.T) { f := NewFile() f.NewSheet("Sheet2") - f.Sheet["xl/worksheets/sheet1.xml"] = &xlsxWorksheet{ - MergeCells: &xlsxMergeCells{ - Cells: []*xlsxMergeCell{ - { - Ref: "A:B1", - }, - }, - }, - } - f.Sheet["xl/worksheets/sheet2.xml"] = &xlsxWorksheet{ - AutoFilter: &xlsxAutoFilter{ - Ref: "A1:B", - }, - } + 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"}}) // testing adjustHelper with illegal cell coordinates. assert.EqualError(t, f.adjustHelper("Sheet1", rows, 0, 0), `cannot convert cell "A" to coordinates: invalid cell name "A"`) assert.EqualError(t, f.adjustHelper("Sheet2", rows, 0, 0), `cannot convert cell "B" to coordinates: invalid cell name "B"`) diff --git a/calcchain.go b/calcchain.go index ea3080f2c2..1b99a04b4f 100644 --- a/calcchain.go +++ b/calcchain.go @@ -54,7 +54,7 @@ func (f *File) deleteCalcChain(index int, axis string) { } if len(calc.C) == 0 { f.CalcChain = nil - delete(f.XLSX, "xl/calcChain.xml") + f.Pkg.Delete("xl/calcChain.xml") content := f.contentTypesReader() for k, v := range content.Overrides { if v.PartName == "/xl/calcChain.xml" { diff --git a/calcchain_test.go b/calcchain_test.go index 842dde16df..4956f60d4c 100644 --- a/calcchain_test.go +++ b/calcchain_test.go @@ -5,7 +5,7 @@ import "testing" func TestCalcChainReader(t *testing.T) { f := NewFile() f.CalcChain = nil - f.XLSX["xl/calcChain.xml"] = MacintoshCyrillicCharset + f.Pkg.Store("xl/calcChain.xml", MacintoshCyrillicCharset) f.calcChainReader() } diff --git a/cell.go b/cell.go index 4dec093526..1d08c8ae9a 100644 --- a/cell.go +++ b/cell.go @@ -13,7 +13,6 @@ package excelize import ( "encoding/xml" - "errors" "fmt" "reflect" "strconv" @@ -187,6 +186,8 @@ func (f *File) SetCellInt(sheet, axis string, value int) error { if err != nil { return err } + ws.Lock() + defer ws.Unlock() cellData.S = f.prepareCellStyle(ws, col, cellData.S) cellData.T, cellData.V = setCellInt(value) return err @@ -262,6 +263,8 @@ func (f *File) SetCellStr(sheet, axis, value string) error { if err != nil { return err } + ws.Lock() + defer ws.Unlock() cellData.S = f.prepareCellStyle(ws, col, cellData.S) cellData.T, cellData.V = f.setCellString(value) return err @@ -742,7 +745,7 @@ func (f *File) SetSheetRow(sheet, axis string, slice interface{}) error { // Make sure 'slice' is a Ptr to Slice v := reflect.ValueOf(slice) if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Slice { - return errors.New("pointer to slice expected") + return ErrParameterInvalid } v = v.Elem() @@ -762,8 +765,6 @@ 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) { - ws.Lock() - defer ws.Unlock() var err error cell, err = f.mergeCellsParser(ws, cell) if err != nil { @@ -775,7 +776,8 @@ func (f *File) prepareCell(ws *xlsxWorksheet, sheet, cell string) (*xlsxC, int, } prepareSheetXML(ws, col, row) - + ws.Lock() + defer ws.Unlock() return &ws.SheetData.Row[row-1].C[col-1], col, row, err } diff --git a/cell_test.go b/cell_test.go index 2282e55a30..e289983f7c 100644 --- a/cell_test.go +++ b/cell_test.go @@ -30,6 +30,20 @@ func TestConcurrency(t *testing.T) { assert.Equal(t, "", name) assert.Nil(t, raw) assert.NoError(t, err) + // Concurrency iterate rows + rows, err := f.Rows("Sheet1") + assert.NoError(t, err) + for rows.Next() { + _, err := rows.Columns() + assert.NoError(t, err) + } + // Concurrency iterate columns + cols, err := f.Cols("Sheet1") + assert.NoError(t, err) + for rows.Next() { + _, err := cols.Rows() + assert.NoError(t, err) + } wg.Done() }(i, t) @@ -149,8 +163,8 @@ func TestGetCellValue(t *testing.T) { // Test get cell value without r attribute of the row. f := NewFile() sheetData := `%s` - delete(f.Sheet, "xl/worksheets/sheet1.xml") - f.XLSX["xl/worksheets/sheet1.xml"] = []byte(fmt.Sprintf(sheetData, `A3A4B4A7B7A8B8`)) + f.Sheet.Delete("xl/worksheets/sheet1.xml") + 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") @@ -164,20 +178,20 @@ func TestGetCellValue(t *testing.T) { cols, err := f.GetCols("Sheet1") assert.Equal(t, [][]string{{"", "", "A3", "A4", "", "", "A7", "A8"}, {"", "", "", "B4", "", "", "B7", "B8"}}, cols) assert.NoError(t, err) - delete(f.Sheet, "xl/worksheets/sheet1.xml") - f.XLSX["xl/worksheets/sheet1.xml"] = []byte(fmt.Sprintf(sheetData, `A2B2`)) + f.Sheet.Delete("xl/worksheets/sheet1.xml") + 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) - delete(f.Sheet, "xl/worksheets/sheet1.xml") - f.XLSX["xl/worksheets/sheet1.xml"] = []byte(fmt.Sprintf(sheetData, `A2B2`)) + f.Sheet.Delete("xl/worksheets/sheet1.xml") + 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) - delete(f.Sheet, "xl/worksheets/sheet1.xml") - f.XLSX["xl/worksheets/sheet1.xml"] = []byte(fmt.Sprintf(sheetData, `A1B1`)) + f.Sheet.Delete("xl/worksheets/sheet1.xml") + 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) @@ -264,17 +278,23 @@ func TestGetCellRichText(t *testing.T) { 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 - f.Sheet["xl/worksheets/sheet1.xml"].SheetData.Row[0].C[0].V = "2" + 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 - f.Sheet["xl/worksheets/sheet1.xml"].SheetData.Row[0].C[0].V = "-1" + 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 - f.Sheet["xl/worksheets/sheet1.xml"].SheetData.Row[0].C[0].V = "x" + 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 diff --git a/chart.go b/chart.go index 9bc4b20d60..52fd54315a 100644 --- a/chart.go +++ b/chart.go @@ -14,7 +14,6 @@ package excelize import ( "encoding/json" "encoding/xml" - "errors" "fmt" "strconv" "strings" @@ -945,7 +944,7 @@ func (f *File) AddChartSheet(sheet, format string, combo ...string) error { sheetID++ path := "xl/chartsheets/sheet" + strconv.Itoa(sheetID) + ".xml" f.sheetMap[trimSheetName(sheet)] = path - f.Sheet[path] = nil + f.Sheet.Store(path, nil) drawingID := f.countDrawings() + 1 chartID := f.countCharts() + 1 drawingXML := "xl/drawings/drawing" + strconv.Itoa(drawingID) + ".xml" @@ -981,12 +980,12 @@ func (f *File) getFormatChart(format string, combo []string) (*formatChart, []*f return formatSet, comboCharts, err } if _, ok := chartValAxNumFmtFormatCode[comboChart.Type]; !ok { - return formatSet, comboCharts, errors.New("unsupported chart type " + comboChart.Type) + return formatSet, comboCharts, newUnsupportChartType(comboChart.Type) } comboCharts = append(comboCharts, comboChart) } if _, ok := chartValAxNumFmtFormatCode[formatSet.Type]; !ok { - return formatSet, comboCharts, errors.New("unsupported chart type " + formatSet.Type) + return formatSet, comboCharts, newUnsupportChartType(formatSet.Type) } return formatSet, comboCharts, err } @@ -1015,11 +1014,12 @@ func (f *File) DeleteChart(sheet, cell string) (err error) { // folder xl/charts. func (f *File) countCharts() int { count := 0 - for k := range f.XLSX { - if strings.Contains(k, "xl/charts/chart") { + f.Pkg.Range(func(k, v interface{}) bool { + if strings.Contains(k.(string), "xl/charts/chart") { count++ } - } + return true + }) return count } diff --git a/chart_test.go b/chart_test.go index 657230b103..8957b93f80 100644 --- a/chart_test.go +++ b/chart_test.go @@ -65,10 +65,10 @@ func TestChartSize(t *testing.T) { anchor decodeTwoCellAnchor ) - content, ok := newFile.XLSX["xl/drawings/drawing1.xml"] + content, ok := newFile.Pkg.Load("xl/drawings/drawing1.xml") assert.True(t, ok, "Can't open the chart") - err = xml.Unmarshal([]byte(content), &workdir) + err = xml.Unmarshal(content.([]byte), &workdir) if !assert.NoError(t, err) { t.FailNow() } @@ -340,11 +340,15 @@ func TestChartWithLogarithmicBase(t *testing.T) { type xmlChartContent []byte xmlCharts := make([]xmlChartContent, expectedChartsCount) expectedChartsLogBase := []float64{0, 10.5, 0, 2, 0, 1000} - var ok bool - + var ( + drawingML interface{} + ok bool + ) for i := 0; i < expectedChartsCount; i++ { chartPath := fmt.Sprintf("xl/charts/chart%d.xml", i+1) - xmlCharts[i], ok = newFile.XLSX[chartPath] + if drawingML, ok = newFile.Pkg.Load(chartPath); ok { + xmlCharts[i] = drawingML.([]byte) + } assert.True(t, ok, "Can't open the %s", chartPath) err = xml.Unmarshal([]byte(xmlCharts[i]), &chartSpaces[i]) diff --git a/col.go b/col.go index 2fd90b2fb8..91ca3da10b 100644 --- a/col.go +++ b/col.go @@ -199,8 +199,11 @@ func (f *File) Cols(sheet string) (*Cols, error) { if !ok { return nil, ErrSheetNotExist{sheet} } - if f.Sheet[name] != nil { - output, _ := xml.Marshal(f.Sheet[name]) + if ws, ok := f.Sheet.Load(name); ok && ws != nil { + worksheet := ws.(*xlsxWorksheet) + worksheet.Lock() + defer worksheet.Unlock() + output, _ := xml.Marshal(worksheet) f.saveFileList(name, f.replaceNameSpaceBytes(name, output)) } var colIterator columnXMLIterator diff --git a/col_test.go b/col_test.go index 6ab5e5769f..8159a11cda 100644 --- a/col_test.go +++ b/col_test.go @@ -48,11 +48,11 @@ func TestCols(t *testing.T) { _, err = f.Rows("Sheet1") assert.NoError(t, err) - f.Sheet["xl/worksheets/sheet1.xml"] = &xlsxWorksheet{ + f.Sheet.Store("xl/worksheets/sheet1.xml", &xlsxWorksheet{ Dimension: &xlsxDimension{ Ref: "C2:C4", }, - } + }) _, err = f.Rows("Sheet1") assert.NoError(t, err) } @@ -110,15 +110,15 @@ func TestGetColsError(t *testing.T) { assert.EqualError(t, err, "sheet SheetN is not exist") f = NewFile() - delete(f.Sheet, "xl/worksheets/sheet1.xml") - f.XLSX["xl/worksheets/sheet1.xml"] = []byte(`B`) + f.Sheet.Delete("xl/worksheets/sheet1.xml") + 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 = NewFile() - delete(f.Sheet, "xl/worksheets/sheet1.xml") - f.XLSX["xl/worksheets/sheet1.xml"] = []byte(`B`) + f.Sheet.Delete("xl/worksheets/sheet1.xml") + f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`B`)) f.checked = nil _, err = f.GetCols("Sheet1") assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`) @@ -142,14 +142,14 @@ func TestColsRows(t *testing.T) { assert.NoError(t, err) assert.NoError(t, f.SetCellValue("Sheet1", "A1", 1)) - f.Sheet["xl/worksheets/sheet1.xml"] = &xlsxWorksheet{ + f.Sheet.Store("xl/worksheets/sheet1.xml", &xlsxWorksheet{ Dimension: &xlsxDimension{ Ref: "A1:A1", }, - } + }) f = NewFile() - f.XLSX["xl/worksheets/sheet1.xml"] = nil + f.Pkg.Store("xl/worksheets/sheet1.xml", nil) _, err = f.Cols("Sheet1") if !assert.NoError(t, err) { t.FailNow() diff --git a/comment.go b/comment.go index b05f308069..306826dc52 100644 --- a/comment.go +++ b/comment.go @@ -299,11 +299,12 @@ func (f *File) addComment(commentsXML, cell string, formatSet *formatComment) { // the folder xl. func (f *File) countComments() int { c1, c2 := 0, 0 - for k := range f.XLSX { - if strings.Contains(k, "xl/comments") { + f.Pkg.Range(func(k, v interface{}) bool { + if strings.Contains(k.(string), "xl/comments") { c1++ } - } + return true + }) for rel := range f.Comments { if strings.Contains(rel, "xl/comments") { c2++ @@ -321,10 +322,10 @@ func (f *File) decodeVMLDrawingReader(path string) *decodeVmlDrawing { var err error if f.DecodeVMLDrawing[path] == nil { - c, ok := f.XLSX[path] - if ok { + c, ok := f.Pkg.Load(path) + if ok && c != nil { f.DecodeVMLDrawing[path] = new(decodeVmlDrawing) - if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(c))). + 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) } @@ -339,7 +340,7 @@ func (f *File) vmlDrawingWriter() { for path, vml := range f.VMLDrawing { if vml != nil { v, _ := xml.Marshal(vml) - f.XLSX[path] = v + f.Pkg.Store(path, v) } } } @@ -348,12 +349,11 @@ func (f *File) vmlDrawingWriter() { // after deserialization of xl/comments%d.xml. func (f *File) commentsReader(path string) *xlsxComments { var err error - if f.Comments[path] == nil { - content, ok := f.XLSX[path] - if ok { + content, ok := f.Pkg.Load(path) + if ok && content != nil { f.Comments[path] = new(xlsxComments) - if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(content))). + 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) } diff --git a/comment_test.go b/comment_test.go index 19b705f463..f1b60dc6c6 100644 --- a/comment_test.go +++ b/comment_test.go @@ -36,7 +36,7 @@ func TestAddComments(t *testing.T) { } f.Comments["xl/comments2.xml"] = nil - f.XLSX["xl/comments2.xml"] = []byte(`Excelize: Excelize: `) + f.Pkg.Store("xl/comments2.xml", []byte(`Excelize: Excelize: `)) comments := f.GetComments() assert.EqualValues(t, 2, len(comments["Sheet1"])) assert.EqualValues(t, 1, len(comments["Sheet2"])) @@ -46,14 +46,14 @@ func TestAddComments(t *testing.T) { func TestDecodeVMLDrawingReader(t *testing.T) { f := NewFile() path := "xl/drawings/vmlDrawing1.xml" - f.XLSX[path] = MacintoshCyrillicCharset + f.Pkg.Store(path, MacintoshCyrillicCharset) f.decodeVMLDrawingReader(path) } func TestCommentsReader(t *testing.T) { f := NewFile() path := "xl/comments1.xml" - f.XLSX[path] = MacintoshCyrillicCharset + f.Pkg.Store(path, MacintoshCyrillicCharset) f.commentsReader(path) } diff --git a/crypt.go b/crypt.go index 88abd0e306..a0096c92e4 100644 --- a/crypt.go +++ b/crypt.go @@ -21,7 +21,6 @@ import ( "encoding/base64" "encoding/binary" "encoding/xml" - "errors" "hash" "math/rand" "reflect" @@ -145,7 +144,7 @@ func Decrypt(raw []byte, opt *Options) (packageBuf []byte, err error) { case "standard": return standardDecrypt(encryptionInfoBuf, encryptedPackageBuf, opt) default: - err = errors.New("unsupport encryption mechanism") + err = ErrUnsupportEncryptMechanism } return } @@ -265,7 +264,7 @@ func Encrypt(raw []byte, opt *Options) (packageBuf []byte, err error) { } // TODO: Create a new CFB. _, _ = encryptedPackage, encryptionInfoBuffer - err = errors.New("not support encryption currently") + err = ErrEncrypt return } @@ -293,7 +292,7 @@ func extractPart(doc *mscfb.Reader) (encryptionInfoBuf, encryptedPackageBuf []by // encryptionMechanism parse password-protected documents created mechanism. func encryptionMechanism(buffer []byte) (mechanism string, err error) { if len(buffer) < 4 { - err = errors.New("unknown encryption mechanism") + err = ErrUnknownEncryptMechanism return } versionMajor, versionMinor := binary.LittleEndian.Uint16(buffer[0:2]), binary.LittleEndian.Uint16(buffer[2:4]) @@ -306,7 +305,7 @@ func encryptionMechanism(buffer []byte) (mechanism string, err error) { } else if (versionMajor == 3 || versionMajor == 4) && versionMinor == 3 { mechanism = "extensible" } - err = errors.New("unsupport encryption mechanism") + err = ErrUnsupportEncryptMechanism return } @@ -470,7 +469,6 @@ func convertPasswdToKey(passwd string, blockKey []byte, encryption Encryption) ( if len(key) < keyBytes { tmp := make([]byte, 0x36) key = append(key, tmp...) - key = tmp } else if len(key) > keyBytes { key = key[:keyBytes] } @@ -599,7 +597,6 @@ func createIV(blockKey interface{}, encryption Encryption) ([]byte, error) { if len(iv) < encryptedKey.BlockSize { tmp := make([]byte, 0x36) iv = append(iv, tmp...) - iv = tmp } else if len(iv) > encryptedKey.BlockSize { iv = iv[0:encryptedKey.BlockSize] } diff --git a/docProps_test.go b/docProps_test.go index 0cb6f71fed..40ae2dc98b 100644 --- a/docProps_test.go +++ b/docProps_test.go @@ -42,12 +42,12 @@ func TestSetDocProps(t *testing.T) { Version: "1.0.0", })) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetDocProps.xlsx"))) - f.XLSX["docProps/core.xml"] = nil + f.Pkg.Store("docProps/core.xml", nil) assert.NoError(t, f.SetDocProps(&DocProperties{})) // Test unsupported charset f = NewFile() - f.XLSX["docProps/core.xml"] = MacintoshCyrillicCharset + f.Pkg.Store("docProps/core.xml", MacintoshCyrillicCharset) assert.EqualError(t, f.SetDocProps(&DocProperties{}), "xml decode error: XML syntax error on line 1: invalid UTF-8") } @@ -59,13 +59,13 @@ func TestGetDocProps(t *testing.T) { props, err := f.GetDocProps() assert.NoError(t, err) assert.Equal(t, props.Creator, "Microsoft Office User") - f.XLSX["docProps/core.xml"] = nil + f.Pkg.Store("docProps/core.xml", nil) _, err = f.GetDocProps() assert.NoError(t, err) // Test unsupported charset f = NewFile() - f.XLSX["docProps/core.xml"] = MacintoshCyrillicCharset + f.Pkg.Store("docProps/core.xml", MacintoshCyrillicCharset) _, err = f.GetDocProps() assert.EqualError(t, err, "xml decode error: XML syntax error on line 1: invalid UTF-8") } diff --git a/drawing.go b/drawing.go index 58e266931d..181bb433ee 100644 --- a/drawing.go +++ b/drawing.go @@ -1151,7 +1151,7 @@ func (f *File) drawingParser(path string) (*xlsxWsDr, int) { content := xlsxWsDr{} content.A = NameSpaceDrawingML.Value content.Xdr = NameSpaceDrawingMLSpreadSheet.Value - if _, ok = f.XLSX[path]; ok { // Append Model + if _, ok = f.Pkg.Load(path); ok { // Append Model decodeWsDr := decodeWsDr{} if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(path)))). Decode(&decodeWsDr); err != nil && err != io.EOF { diff --git a/drawing_test.go b/drawing_test.go index 3c0b619eee..f2413cfec6 100644 --- a/drawing_test.go +++ b/drawing_test.go @@ -19,10 +19,10 @@ import ( func TestDrawingParser(t *testing.T) { f := File{ Drawings: sync.Map{}, - XLSX: map[string][]byte{ - "charset": MacintoshCyrillicCharset, - "wsDr": []byte(``)}, + Pkg: sync.Map{}, } + f.Pkg.Store("charset", MacintoshCyrillicCharset) + f.Pkg.Store("wsDr", []byte(``)) // Test with one cell anchor f.drawingParser("wsDr") // Test with unsupported charset diff --git a/errors.go b/errors.go index a0c61c8dd0..4931198eba 100644 --- a/errors.go +++ b/errors.go @@ -32,6 +32,10 @@ func newInvalidExcelDateError(dateValue float64) error { return fmt.Errorf("invalid date value %f, negative values are not supported supported", dateValue) } +func newUnsupportChartType(chartType string) error { + return fmt.Errorf("unsupported chart type %s", chartType) +} + var ( // ErrStreamSetColWidth defined the error message on set column width in // stream writing mode. @@ -71,4 +75,34 @@ var ( // ErrMaxFileNameLength defined the error message on receive the file name // length overflow. ErrMaxFileNameLength = errors.New("file name 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 unsupport + // encryption mechanism. + ErrUnknownEncryptMechanism = errors.New("unknown encryption mechanism") + // ErrUnsupportEncryptMechanism defined the error message on unsupport + // encryption mechanism. + ErrUnsupportEncryptMechanism = errors.New("unsupport encryption mechanism") + // ErrParameterRequired defined the error message on receive the empty + // parameter. + ErrParameterRequired = errors.New("parameter is required") + // ErrParameterInvalid defined the error message on receive the invalid + // parameter. + ErrParameterInvalid = errors.New("parameter is invalid") + // 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 + // already exists on the scope. + ErrDefinedNameduplicate = errors.New("the same name already exists on the scope") + // 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") + // 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") + // ErrSheetIdx defined the error message on receive the invalid worksheet + // index. + ErrSheetIdx = errors.New("invalid worksheet index") + // ErrGroupSheets defined the error message on group sheets. + ErrGroupSheets = errors.New("group worksheet must contain an active worksheet") ) diff --git a/excelize.go b/excelize.go index 66cfd00f38..bdb71202d9 100644 --- a/excelize.go +++ b/excelize.go @@ -43,7 +43,7 @@ type File struct { Path string SharedStrings *xlsxSST sharedStringsMap map[string]int - Sheet map[string]*xlsxWorksheet + Sheet sync.Map // map[string]*xlsxWorksheet SheetCount int Styles *xlsxStyleSheet Theme *xlsxTheme @@ -51,7 +51,7 @@ type File struct { VMLDrawing map[string]*vmlDrawing WorkBook *xlsxWorkbook Relationships sync.Map - XLSX map[string][]byte + Pkg sync.Map CharsetReader charsetTranscoderFn } @@ -95,7 +95,7 @@ func newFile() *File { Comments: make(map[string]*xlsxComments), Drawings: sync.Map{}, sharedStringsMap: make(map[string]int), - Sheet: make(map[string]*xlsxWorksheet), + Sheet: sync.Map{}, DecodeVMLDrawing: make(map[string]*decodeVmlDrawing), VMLDrawing: make(map[string]*vmlDrawing), Relationships: sync.Map{}, @@ -129,7 +129,10 @@ func OpenReader(r io.Reader, opt ...Options) (*File, error) { if err != nil { return nil, err } - f.SheetCount, f.XLSX = sheetCount, file + f.SheetCount = sheetCount + for k, v := range file { + f.Pkg.Store(k, v) + } f.CalcChain = f.calcChainReader() f.sheetMap = f.getSheetMap() f.Styles = f.stylesReader() @@ -172,40 +175,40 @@ func (f *File) workSheetReader(sheet string) (ws *xlsxWorksheet, err error) { name string ok bool ) - if name, ok = f.sheetMap[trimSheetName(sheet)]; !ok { err = fmt.Errorf("sheet %s is not exist", sheet) return } - if ws = f.Sheet[name]; f.Sheet[name] == nil { - if strings.HasPrefix(name, "xl/chartsheets") { - err = fmt.Errorf("sheet %s is chart sheet", sheet) - return - } - ws = new(xlsxWorksheet) - if _, ok := f.xmlAttr[name]; !ok { - d := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(name)))) - f.xmlAttr[name] = append(f.xmlAttr[name], getRootElement(d)...) - } - if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(name)))). - Decode(ws); err != nil && err != io.EOF { - err = fmt.Errorf("xml decode error: %s", err) + if worksheet, ok := f.Sheet.Load(name); ok && worksheet != nil { + ws = worksheet.(*xlsxWorksheet) + return + } + if strings.HasPrefix(name, "xl/chartsheets") { + err = fmt.Errorf("sheet %s is chart sheet", sheet) + return + } + ws = new(xlsxWorksheet) + if _, ok := f.xmlAttr[name]; !ok { + d := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(name)))) + f.xmlAttr[name] = append(f.xmlAttr[name], getRootElement(d)...) + } + if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(name)))). + Decode(ws); err != nil && err != io.EOF { + err = fmt.Errorf("xml decode error: %s", err) + return + } + err = nil + if f.checked == nil { + f.checked = make(map[string]bool) + } + if ok = f.checked[name]; !ok { + checkSheet(ws) + if err = checkRow(ws); err != nil { return } - err = nil - if f.checked == nil { - f.checked = make(map[string]bool) - } - if ok = f.checked[name]; !ok { - checkSheet(ws) - if err = checkRow(ws); err != nil { - return - } - f.checked[name] = true - } - f.Sheet[name] = ws + f.checked[name] = true } - + f.Sheet.Store(name, ws) return } @@ -375,7 +378,7 @@ func (f *File) AddVBAProject(bin string) error { }) } file, _ := ioutil.ReadFile(bin) - f.XLSX["xl/vbaProject.bin"] = file + f.Pkg.Store("xl/vbaProject.bin", file) return err } diff --git a/excelize_test.go b/excelize_test.go index e3cfa53396..22e39d1267 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -343,13 +343,17 @@ func TestSetCellHyperLink(t *testing.T) { f = NewFile() _, err = f.workSheetReader("Sheet1") assert.NoError(t, err) - f.Sheet["xl/worksheets/sheet1.xml"].Hyperlinks = &xlsxHyperlinks{Hyperlink: make([]xlsxHyperlink, 65530)} + ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") + assert.True(t, ok) + ws.(*xlsxWorksheet).Hyperlinks = &xlsxHyperlinks{Hyperlink: make([]xlsxHyperlink, 65530)} assert.EqualError(t, f.SetCellHyperLink("Sheet1", "A65531", "https://github.com/360EntSecGroup-Skylar/excelize", "External"), ErrTotalSheetHyperlinks.Error()) f = NewFile() _, err = f.workSheetReader("Sheet1") assert.NoError(t, err) - f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:A"}}} + ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml") + assert.True(t, ok) + ws.(*xlsxWorksheet).MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:A"}}} err = f.SetCellHyperLink("Sheet1", "A1", "https://github.com/360EntSecGroup-Skylar/excelize", "External") assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`) } @@ -376,7 +380,9 @@ func TestGetCellHyperLink(t *testing.T) { f = NewFile() _, err = f.workSheetReader("Sheet1") assert.NoError(t, err) - f.Sheet["xl/worksheets/sheet1.xml"].Hyperlinks = &xlsxHyperlinks{ + ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") + assert.True(t, ok) + ws.(*xlsxWorksheet).Hyperlinks = &xlsxHyperlinks{ Hyperlink: []xlsxHyperlink{{Ref: "A1"}}, } link, target, err = f.GetCellHyperLink("Sheet1", "A1") @@ -384,7 +390,9 @@ func TestGetCellHyperLink(t *testing.T) { assert.Equal(t, link, true) assert.Equal(t, target, "") - f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:A"}}} + ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml") + assert.True(t, ok) + ws.(*xlsxWorksheet).MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:A"}}} link, target, err = f.GetCellHyperLink("Sheet1", "A1") assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`) assert.Equal(t, link, false) @@ -1112,8 +1120,8 @@ func TestSetSheetRow(t *testing.T) { assert.EqualError(t, f.SetSheetRow("Sheet1", "", &[]interface{}{"cell", nil, 2}), `cannot convert cell "" to coordinates: invalid cell name ""`) - assert.EqualError(t, f.SetSheetRow("Sheet1", "B27", []interface{}{}), `pointer to slice expected`) - assert.EqualError(t, f.SetSheetRow("Sheet1", "B27", &f), `pointer to slice expected`) + 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"))) } @@ -1198,7 +1206,7 @@ func TestContentTypesReader(t *testing.T) { // Test unsupported charset. f := NewFile() f.ContentTypes = nil - f.XLSX["[Content_Types].xml"] = MacintoshCyrillicCharset + f.Pkg.Store("[Content_Types].xml", MacintoshCyrillicCharset) f.contentTypesReader() } @@ -1206,22 +1214,22 @@ func TestWorkbookReader(t *testing.T) { // Test unsupported charset. f := NewFile() f.WorkBook = nil - f.XLSX["xl/workbook.xml"] = MacintoshCyrillicCharset + f.Pkg.Store("xl/workbook.xml", MacintoshCyrillicCharset) f.workbookReader() } func TestWorkSheetReader(t *testing.T) { // Test unsupported charset. f := NewFile() - delete(f.Sheet, "xl/worksheets/sheet1.xml") - f.XLSX["xl/worksheets/sheet1.xml"] = MacintoshCyrillicCharset + 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") // Test on no checked worksheet. f = NewFile() - delete(f.Sheet, "xl/worksheets/sheet1.xml") - f.XLSX["xl/worksheets/sheet1.xml"] = []byte(``) + f.Sheet.Delete("xl/worksheets/sheet1.xml") + f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(``)) f.checked = nil _, err = f.workSheetReader("Sheet1") assert.NoError(t, err) @@ -1232,7 +1240,7 @@ func TestRelsReader(t *testing.T) { f := NewFile() rels := "xl/_rels/workbook.xml.rels" f.Relationships.Store(rels, nil) - f.XLSX[rels] = MacintoshCyrillicCharset + f.Pkg.Store(rels, MacintoshCyrillicCharset) f.relsReader(rels) } diff --git a/file.go b/file.go index fa73ec83ff..1786e27d17 100644 --- a/file.go +++ b/file.go @@ -26,18 +26,17 @@ import ( // f := NewFile() // func NewFile() *File { - file := make(map[string][]byte) - file["_rels/.rels"] = []byte(XMLHeader + templateRels) - file["docProps/app.xml"] = []byte(XMLHeader + templateDocpropsApp) - file["docProps/core.xml"] = []byte(XMLHeader + templateDocpropsCore) - file["xl/_rels/workbook.xml.rels"] = []byte(XMLHeader + templateWorkbookRels) - file["xl/theme/theme1.xml"] = []byte(XMLHeader + templateTheme) - file["xl/worksheets/sheet1.xml"] = []byte(XMLHeader + templateSheet) - file["xl/styles.xml"] = []byte(XMLHeader + templateStyles) - file["xl/workbook.xml"] = []byte(XMLHeader + templateWorkbook) - file["[Content_Types].xml"] = []byte(XMLHeader + templateContentTypes) f := newFile() - f.SheetCount, f.XLSX = 1, file + f.Pkg.Store("_rels/.rels", []byte(XMLHeader+templateRels)) + f.Pkg.Store("docProps/app.xml", []byte(XMLHeader+templateDocpropsApp)) + f.Pkg.Store("docProps/core.xml", []byte(XMLHeader+templateDocpropsCore)) + f.Pkg.Store("xl/_rels/workbook.xml.rels", []byte(XMLHeader+templateWorkbookRels)) + f.Pkg.Store("xl/theme/theme1.xml", []byte(XMLHeader+templateTheme)) + f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(XMLHeader+templateSheet)) + f.Pkg.Store("xl/styles.xml", []byte(XMLHeader+templateStyles)) + f.Pkg.Store("xl/workbook.xml", []byte(XMLHeader+templateWorkbook)) + f.Pkg.Store("[Content_Types].xml", []byte(XMLHeader+templateContentTypes)) + f.SheetCount = 1 f.CalcChain = f.calcChainReader() f.Comments = make(map[string]*xlsxComments) f.ContentTypes = f.contentTypesReader() @@ -48,8 +47,9 @@ func NewFile() *File { f.WorkBook = f.workbookReader() f.Relationships = sync.Map{} f.Relationships.Store("xl/_rels/workbook.xml.rels", f.relsReader("xl/_rels/workbook.xml.rels")) - f.Sheet["xl/worksheets/sheet1.xml"], _ = f.workSheetReader("Sheet1") f.sheetMap["Sheet1"] = "xl/worksheets/sheet1.xml" + ws, _ := f.workSheetReader("Sheet1") + f.Sheet.Store("xl/worksheets/sheet1.xml", ws) f.Theme = f.themeReader() return f } @@ -165,20 +165,22 @@ func (f *File) writeToZip(zw *zip.Writer) error { } stream.rawData.Close() } - - for path, content := range f.XLSX { - if _, ok := f.streams[path]; ok { - continue - } - fi, err := zw.Create(path) + var err error + f.Pkg.Range(func(path, content interface{}) bool { if err != nil { - return err + return false } - _, err = fi.Write(content) + if _, ok := f.streams[path.(string)]; ok { + return true + } + var fi io.Writer + fi, err = zw.Create(path.(string)) if err != nil { - return err + return false } - } + _, err = fi.Write(content.([]byte)) + return true + }) - return nil + return err } diff --git a/file_test.go b/file_test.go index dbbf75a8eb..d86ce535c4 100644 --- a/file_test.go +++ b/file_test.go @@ -5,6 +5,7 @@ import ( "bytes" "os" "strings" + "sync" "testing" "github.com/stretchr/testify/assert" @@ -38,19 +39,19 @@ func TestWriteTo(t *testing.T) { { f := File{} buf := bytes.Buffer{} - f.XLSX = make(map[string][]byte) - f.XLSX["/d/"] = []byte("s") + f.Pkg = sync.Map{} + f.Pkg.Store("/d/", []byte("s")) _, err := f.WriteTo(bufio.NewWriter(&buf)) assert.EqualError(t, err, "zip: write to directory") - delete(f.XLSX, "/d/") + f.Pkg.Delete("/d/") } // Test file path overflow { f := File{} buf := bytes.Buffer{} - f.XLSX = make(map[string][]byte) + f.Pkg = sync.Map{} const maxUint16 = 1<<16 - 1 - f.XLSX[strings.Repeat("s", maxUint16+1)] = nil + f.Pkg.Store(strings.Repeat("s", maxUint16+1), nil) _, err := f.WriteTo(bufio.NewWriter(&buf)) assert.EqualError(t, err, "zip: FileHeader.Name too long") } @@ -58,8 +59,8 @@ func TestWriteTo(t *testing.T) { { f := File{} buf := bytes.Buffer{} - f.XLSX = make(map[string][]byte) - f.XLSX["s"] = nil + f.Pkg = sync.Map{} + f.Pkg.Store("s", nil) f.streams = make(map[string]*StreamWriter) file, _ := os.Open("123") f.streams["s"] = &StreamWriter{rawData: bufferedWriter{tmp: file}} diff --git a/lib.go b/lib.go index 5c9bbf6eb2..00a67d9658 100644 --- a/lib.go +++ b/lib.go @@ -51,8 +51,8 @@ func ReadZipReader(r *zip.Reader) (map[string][]byte, int, error) { // readXML provides a function to read XML content as string. func (f *File) readXML(name string) []byte { - if content, ok := f.XLSX[name]; ok { - return content + if content, _ := f.Pkg.Load(name); content != nil { + return content.([]byte) } if content, ok := f.streams[name]; ok { return content.rawData.buf.Bytes() @@ -66,7 +66,7 @@ func (f *File) saveFileList(name string, content []byte) { newContent := make([]byte, 0, len(XMLHeader)+len(content)) newContent = append(newContent, []byte(XMLHeader)...) newContent = append(newContent, content...) - f.XLSX[name] = newContent + f.Pkg.Store(name, newContent) } // Read file content as string in a archive file. diff --git a/merge_test.go b/merge_test.go index afe75aac01..cf460dd9a0 100644 --- a/merge_test.go +++ b/merge_test.go @@ -71,13 +71,19 @@ func TestMergeCell(t *testing.T) { f = NewFile() assert.NoError(t, f.MergeCell("Sheet1", "A2", "B3")) - f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{nil, nil}} + ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") + assert.True(t, ok) + ws.(*xlsxWorksheet).MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{nil, nil}} assert.NoError(t, f.MergeCell("Sheet1", "A2", "B3")) - f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A1"}}} + 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.MergeCell("Sheet1", "A2", "B3"), `invalid area "A1"`) - f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:A"}}} + 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.MergeCell("Sheet1", "A2", "B3"), `cannot convert cell "A" to coordinates: invalid cell name "A"`) } @@ -154,16 +160,24 @@ func TestUnmergeCell(t *testing.T) { // Test unmerged area on not exists worksheet. assert.EqualError(t, f.UnmergeCell("SheetN", "A1", "A1"), "sheet SheetN is not exist") - f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = nil + ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") + assert.True(t, ok) + ws.(*xlsxWorksheet).MergeCells = nil assert.NoError(t, f.UnmergeCell("Sheet1", "H7", "B15")) - f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{nil, nil}} + ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml") + assert.True(t, ok) + ws.(*xlsxWorksheet).MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{nil, nil}} assert.NoError(t, f.UnmergeCell("Sheet1", "H15", "B7")) - f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A1"}}} + 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"), `invalid area "A1"`) - f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:A"}}} + 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.UnmergeCell("Sheet1", "A2", "B3"), `cannot convert cell "A" to coordinates: invalid cell name "A"`) } diff --git a/picture.go b/picture.go index 09283c814c..58fa909778 100644 --- a/picture.go +++ b/picture.go @@ -223,11 +223,12 @@ func (f *File) addSheetPicture(sheet string, rID int) { // folder xl/drawings. func (f *File) countDrawings() int { c1, c2 := 0, 0 - for k := range f.XLSX { - if strings.Contains(k, "xl/drawings/drawing") { + f.Pkg.Range(func(k, v interface{}) bool { + if strings.Contains(k.(string), "xl/drawings/drawing") { c1++ } - } + return true + }) f.Drawings.Range(func(rel, value interface{}) bool { if strings.Contains(rel.(string), "xl/drawings/drawing") { c2++ @@ -305,11 +306,12 @@ func (f *File) addDrawingPicture(sheet, drawingXML, cell, file string, width, he // folder xl/media/image. func (f *File) countMedia() int { count := 0 - for k := range f.XLSX { - if strings.Contains(k, "xl/media/image") { + f.Pkg.Range(func(k, v interface{}) bool { + if strings.Contains(k.(string), "xl/media/image") { count++ } - } + return true + }) return count } @@ -318,16 +320,22 @@ func (f *File) countMedia() int { // and drawings that use it will reference the same image. func (f *File) addMedia(file []byte, ext string) string { count := f.countMedia() - for name, existing := range f.XLSX { - if !strings.HasPrefix(name, "xl/media/image") { - continue + var name string + f.Pkg.Range(func(k, existing interface{}) bool { + if !strings.HasPrefix(k.(string), "xl/media/image") { + return true } - if bytes.Equal(file, existing) { - return name + if bytes.Equal(file, existing.([]byte)) { + name = k.(string) + return false } + return true + }) + if name != "" { + return name } media := "xl/media/image" + strconv.Itoa(count+1) + ext - f.XLSX[media] = file + f.Pkg.Store(media, file) return media } @@ -468,8 +476,7 @@ func (f *File) GetPicture(sheet, cell string) (string, []byte, error) { } target := f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID) drawingXML := strings.Replace(target, "..", "xl", -1) - _, ok := f.XLSX[drawingXML] - if !ok { + if _, ok := f.Pkg.Load(drawingXML); !ok { return "", nil, err } drawingRelationships := strings.Replace( @@ -532,7 +539,10 @@ 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 = supportImageTypes[filepath.Ext(drawRel.Target)]; ok { - ret, buf = filepath.Base(drawRel.Target), f.XLSX[strings.Replace(drawRel.Target, "..", "xl", -1)] + ret = filepath.Base(drawRel.Target) + if buffer, _ := f.Pkg.Load(strings.Replace(drawRel.Target, "..", "xl", -1)); buffer != nil { + buf = buffer.([]byte) + } return } } @@ -556,7 +566,10 @@ func (f *File) getPictureFromWsDr(row, col int, drawingRelationships string, wsD if drawRel = f.getDrawingRelationships(drawingRelationships, anchor.Pic.BlipFill.Blip.Embed); drawRel != nil { if _, ok = supportImageTypes[filepath.Ext(drawRel.Target)]; ok { - ret, buf = filepath.Base(drawRel.Target), f.XLSX[strings.Replace(drawRel.Target, "..", "xl", -1)] + ret = filepath.Base(drawRel.Target) + if buffer, _ := f.Pkg.Load(strings.Replace(drawRel.Target, "..", "xl", -1)); buffer != nil { + buf = buffer.([]byte) + } return } } diff --git a/picture_test.go b/picture_test.go index be917b84ef..69873eb0de 100644 --- a/picture_test.go +++ b/picture_test.go @@ -155,7 +155,7 @@ func TestGetPicture(t *testing.T) { assert.Empty(t, raw) f, err = prepareTestBook1() assert.NoError(t, err) - f.XLSX["xl/drawings/drawing1.xml"] = MacintoshCyrillicCharset + 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") } @@ -173,11 +173,12 @@ func TestAddPictureFromBytes(t *testing.T) { 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)) imageCount := 0 - for fileName := range f.XLSX { - if strings.Contains(fileName, "media/image") { + f.Pkg.Range(func(fileName, v interface{}) bool { + if strings.Contains(fileName.(string), "media/image") { imageCount++ } - } + 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") } @@ -205,6 +206,8 @@ func TestDrawingResize(t *testing.T) { // Test calculate drawing resize with invalid coordinates. _, _, _, _, err = f.drawingResize("Sheet1", "", 1, 1, nil) assert.EqualError(t, err, `cannot convert cell "" to coordinates: invalid cell name ""`) - f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:A"}}} + 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}`), `cannot convert cell "A" to coordinates: invalid cell name "A"`) } diff --git a/pivotTable.go b/pivotTable.go index 3d93260aca..05ac78327c 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -13,7 +13,6 @@ package excelize import ( "encoding/xml" - "errors" "fmt" "strconv" "strings" @@ -163,7 +162,7 @@ func (f *File) AddPivotTable(opt *PivotTableOption) error { // properties. func (f *File) parseFormatPivotTableSet(opt *PivotTableOption) (*xlsxWorksheet, string, error) { if opt == nil { - return nil, "", errors.New("parameter is required") + return nil, "", ErrParameterRequired } pivotTableSheetName, _, err := f.adjustRange(opt.PivotTableRange) if err != nil { @@ -192,11 +191,11 @@ func (f *File) parseFormatPivotTableSet(opt *PivotTableOption) (*xlsxWorksheet, // adjustRange adjust range, for example: adjust Sheet1!$E$31:$A$1 to Sheet1!$A$1:$E$31 func (f *File) adjustRange(rangeStr string) (string, []int, error) { if len(rangeStr) < 1 { - return "", []int{}, errors.New("parameter is required") + return "", []int{}, ErrParameterRequired } rng := strings.Split(rangeStr, "!") if len(rng) != 2 { - return "", []int{}, errors.New("parameter is invalid") + return "", []int{}, ErrParameterInvalid } trimRng := strings.Replace(rng[1], "$", "", -1) coordinates, err := f.areaRefToCoordinates(trimRng) @@ -205,7 +204,7 @@ func (f *File) adjustRange(rangeStr string) (string, []int, error) { } x1, y1, x2, y2 := coordinates[0], coordinates[1], coordinates[2], coordinates[3] if x1 == x2 && y1 == y2 { - return rng[0], []int{}, errors.New("parameter is invalid") + return rng[0], []int{}, ErrParameterInvalid } // Correct the coordinate area, such correct C1:B3 to B1:C3. @@ -600,11 +599,12 @@ func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opt *PivotTableOptio // the folder xl/pivotTables. func (f *File) countPivotTables() int { count := 0 - for k := range f.XLSX { - if strings.Contains(k, "xl/pivotTables/pivotTable") { + f.Pkg.Range(func(k, v interface{}) bool { + if strings.Contains(k.(string), "xl/pivotTables/pivotTable") { count++ } - } + return true + }) return count } @@ -612,11 +612,12 @@ func (f *File) countPivotTables() int { // the folder xl/pivotCache. func (f *File) countPivotCache() int { count := 0 - for k := range f.XLSX { - if strings.Contains(k, "xl/pivotCache/pivotCacheDefinition") { + f.Pkg.Range(func(k, v interface{}) bool { + if strings.Contains(k.(string), "xl/pivotCache/pivotCacheDefinition") { count++ } - } + return true + }) return count } diff --git a/pivotTable_test.go b/pivotTable_test.go index 7098b3a1ca..e746d8df50 100644 --- a/pivotTable_test.go +++ b/pivotTable_test.go @@ -137,12 +137,12 @@ func TestAddPivotTable(t *testing.T) { ShowLastColumn: true, })) // Create pivot table with many data, many rows, many cols and defined name - f.SetDefinedName(&DefinedName{ + assert.NoError(t, f.SetDefinedName(&DefinedName{ Name: "dataRange", RefersTo: "Sheet1!$A$1:$E$31", Comment: "Pivot Table Data Range", Scope: "Sheet2", - }) + })) assert.NoError(t, f.AddPivotTable(&PivotTableOption{ DataRange: "dataRange", PivotTableRange: "Sheet2!$A$57:$AJ$91", diff --git a/rows.go b/rows.go index 6360f4e7ab..229b12d6fb 100644 --- a/rows.go +++ b/rows.go @@ -195,9 +195,12 @@ func (f *File) Rows(sheet string) (*Rows, error) { if !ok { return nil, ErrSheetNotExist{sheet} } - if f.Sheet[name] != nil { + if ws, ok := f.Sheet.Load(name); ok && ws != nil { + worksheet := ws.(*xlsxWorksheet) + worksheet.Lock() + defer worksheet.Unlock() // flush data - output, _ := xml.Marshal(f.Sheet[name]) + output, _ := xml.Marshal(worksheet) f.saveFileList(name, f.replaceNameSpaceBytes(name, output)) } var ( diff --git a/rows_test.go b/rows_test.go index 585fe59aed..069b6680a7 100644 --- a/rows_test.go +++ b/rows_test.go @@ -43,11 +43,13 @@ func TestRows(t *testing.T) { } f = NewFile() - f.XLSX["xl/worksheets/sheet1.xml"] = []byte(`1B`) + f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`1B`)) + f.Sheet.Delete("xl/worksheets/sheet1.xml") + delete(f.checked, "xl/worksheets/sheet1.xml") _, err = f.Rows("Sheet1") assert.EqualError(t, err, `strconv.Atoi: parsing "A": invalid syntax`) - f.XLSX["xl/worksheets/sheet1.xml"] = nil + f.Pkg.Store("xl/worksheets/sheet1.xml", nil) _, err = f.Rows("Sheet1") assert.NoError(t, err) } @@ -187,7 +189,7 @@ func TestColumns(t *testing.T) { func TestSharedStringsReader(t *testing.T) { f := NewFile() - f.XLSX["xl/sharedStrings.xml"] = MacintoshCyrillicCharset + f.Pkg.Store("xl/sharedStrings.xml", MacintoshCyrillicCharset) f.sharedStringsReader() si := xlsxSI{} assert.EqualValues(t, "", si.String()) @@ -874,12 +876,14 @@ func TestErrSheetNotExistError(t *testing.T) { func TestCheckRow(t *testing.T) { f := NewFile() - f.XLSX["xl/worksheets/sheet1.xml"] = []byte(`12345`) + f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`12345`)) _, err := f.GetRows("Sheet1") assert.NoError(t, err) assert.NoError(t, f.SetCellValue("Sheet1", "A1", false)) f = NewFile() - f.XLSX["xl/worksheets/sheet1.xml"] = []byte(`12345`) + f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`12345`)) + f.Sheet.Delete("xl/worksheets/sheet1.xml") + delete(f.checked, "xl/worksheets/sheet1.xml") assert.EqualError(t, f.SetCellValue("Sheet1", "A1", false), `cannot convert cell "-" to coordinates: invalid cell name "-"`) } diff --git a/sheet.go b/sheet.go index 420235c0bd..2b9149504d 100644 --- a/sheet.go +++ b/sheet.go @@ -15,7 +15,6 @@ import ( "bytes" "encoding/json" "encoding/xml" - "errors" "fmt" "io" "io/ioutil" @@ -152,25 +151,27 @@ func (f *File) workSheetWriter() { var arr []byte buffer := bytes.NewBuffer(arr) encoder := xml.NewEncoder(buffer) - for p, sheet := range f.Sheet { - if sheet != nil { + f.Sheet.Range(func(p, ws interface{}) bool { + if ws != nil { + sheet := ws.(*xlsxWorksheet) for k, v := range sheet.SheetData.Row { - f.Sheet[p].SheetData.Row[k].C = trimCell(v.C) + sheet.SheetData.Row[k].C = trimCell(v.C) } if sheet.SheetPr != nil || sheet.Drawing != nil || sheet.Hyperlinks != nil || sheet.Picture != nil || sheet.TableParts != nil { - f.addNameSpaces(p, SourceRelationship) + f.addNameSpaces(p.(string), SourceRelationship) } // reusing buffer _ = encoder.Encode(sheet) - f.saveFileList(p, replaceRelationshipsBytes(f.replaceNameSpaceBytes(p, buffer.Bytes()))) - ok := f.checked[p] + f.saveFileList(p.(string), replaceRelationshipsBytes(f.replaceNameSpaceBytes(p.(string), buffer.Bytes()))) + ok := f.checked[p.(string)] if ok { - delete(f.Sheet, p) - f.checked[p] = false + f.Sheet.Delete(p.(string)) + f.checked[p.(string)] = false } buffer.Reset() } - } + return true + }) } // trimCell provides a function to trim blank cells which created by fillColumns. @@ -213,7 +214,7 @@ func (f *File) setSheet(index int, name string) { } path := "xl/worksheets/sheet" + strconv.Itoa(index) + ".xml" f.sheetMap[trimSheetName(name)] = path - f.Sheet[path] = &ws + f.Sheet.Store(path, &ws) f.xmlAttr[path] = []xml.Attr{NameSpaceSpreadSheet} } @@ -448,7 +449,7 @@ func (f *File) getSheetMap() map[string]string { if strings.HasPrefix(rel.Target, "/") { path = filepath.ToSlash(strings.TrimPrefix(strings.Replace(filepath.Clean(rel.Target), "\\", "/", -1), "/")) } - if _, ok := f.XLSX[path]; ok { + if _, ok := f.Pkg.Load(path); ok { maps[v.Name] = path } } @@ -524,10 +525,10 @@ func (f *File) DeleteSheet(name string) { f.deleteSheetFromContentTypes(target) f.deleteCalcChain(sheet.SheetID, "") delete(f.sheetMap, sheetName) - delete(f.XLSX, sheetXML) - delete(f.XLSX, rels) + f.Pkg.Delete(sheetXML) + f.Pkg.Delete(rels) f.Relationships.Delete(rels) - delete(f.Sheet, sheetXML) + f.Sheet.Delete(sheetXML) delete(f.xmlAttr, sheetXML) f.SheetCount-- } @@ -573,7 +574,7 @@ func (f *File) deleteSheetFromContentTypes(target string) { // func (f *File) CopySheet(from, to int) error { if from < 0 || to < 0 || from == to || f.GetSheetName(from) == "" || f.GetSheetName(to) == "" { - return errors.New("invalid worksheet index") + return ErrSheetIdx } return f.copySheet(from, to) } @@ -595,12 +596,11 @@ func (f *File) copySheet(from, to int) error { worksheet.Drawing = nil worksheet.TableParts = nil worksheet.PageSetUp = nil - f.Sheet[path] = worksheet + f.Sheet.Store(path, worksheet) toRels := "xl/worksheets/_rels/sheet" + toSheetID + ".xml.rels" fromRels := "xl/worksheets/_rels/sheet" + strconv.Itoa(f.getSheetID(fromSheet)) + ".xml.rels" - _, ok := f.XLSX[fromRels] - if ok { - f.XLSX[toRels] = f.XLSX[fromRels] + if rels, ok := f.Pkg.Load(fromRels); ok && rels != nil { + f.Pkg.Store(toRels, rels.([]byte)) } fromSheetXMLPath := f.sheetMap[trimSheetName(fromSheet)] fromSheetAttr := f.xmlAttr[fromSheetXMLPath] @@ -824,9 +824,9 @@ func (f *File) SearchSheet(sheet, value string, reg ...bool) ([]string, error) { if !ok { return result, ErrSheetNotExist{sheet} } - if f.Sheet[name] != nil { + if ws, ok := f.Sheet.Load(name); ok && ws != nil { // flush data - output, _ := xml.Marshal(f.Sheet[name]) + output, _ := xml.Marshal(ws.(*xlsxWorksheet)) f.saveFileList(name, f.replaceNameSpaceBytes(name, output)) } return f.searchSheet(name, value, regSearch) @@ -1483,7 +1483,7 @@ func (f *File) SetDefinedName(definedName *DefinedName) error { scope = f.GetSheetName(*dn.LocalSheetID) } if scope == definedName.Scope && dn.Name == definedName.Name { - return errors.New("the same name already exists on the scope") + return ErrDefinedNameduplicate } } wb.DefinedNames.DefinedName = append(wb.DefinedNames.DefinedName, d) @@ -1518,7 +1518,7 @@ func (f *File) DeleteDefinedName(definedName *DefinedName) error { } } } - return errors.New("no defined name on the scope") + return ErrDefinedNameScope } // GetDefinedName provides a function to get the defined names of the workbook @@ -1558,7 +1558,7 @@ func (f *File) GroupSheets(sheets []string) error { } } if !inActiveSheet { - return errors.New("group worksheet must contain an active worksheet") + return ErrGroupSheets } // check worksheet exists wss := []*xlsxWorksheet{} @@ -1714,8 +1714,7 @@ func (f *File) relsReader(path string) *xlsxRelationships { var err error rels, _ := f.Relationships.Load(path) if rels == nil { - _, ok := f.XLSX[path] - if ok { + if _, ok := f.Pkg.Load(path); ok { c := xlsxRelationships{} if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(path)))). Decode(&c); err != nil && err != io.EOF { @@ -1734,6 +1733,8 @@ func (f *File) relsReader(path string) *xlsxRelationships { // 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() rowCount := len(ws.SheetData.Row) sizeHint := 0 var ht float64 diff --git a/sheet_test.go b/sheet_test.go index a72147203b..268abdca7d 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -347,9 +347,13 @@ func TestSetActiveSheet(t *testing.T) { f.WorkBook.BookViews = nil f.SetActiveSheet(1) f.WorkBook.BookViews = &xlsxBookViews{WorkBookView: []xlsxWorkBookView{}} - f.Sheet["xl/worksheets/sheet1.xml"].SheetViews = &xlsxSheetViews{SheetView: []xlsxSheetView{}} + ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") + assert.True(t, ok) + ws.(*xlsxWorksheet).SheetViews = &xlsxSheetViews{SheetView: []xlsxSheetView{}} f.SetActiveSheet(1) - f.Sheet["xl/worksheets/sheet1.xml"].SheetViews = nil + ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml") + assert.True(t, ok) + ws.(*xlsxWorksheet).SheetViews = nil f.SetActiveSheet(1) f = NewFile() f.SetActiveSheet(-1) @@ -365,14 +369,14 @@ func TestSetSheetName(t *testing.T) { func TestGetWorkbookPath(t *testing.T) { f := NewFile() - delete(f.XLSX, "_rels/.rels") + f.Pkg.Delete("_rels/.rels") assert.Equal(t, "", f.getWorkbookPath()) } func TestGetWorkbookRelsPath(t *testing.T) { f := NewFile() - delete(f.XLSX, "xl/_rels/.rels") - f.XLSX["_rels/.rels"] = []byte(``) + f.Pkg.Delete("xl/_rels/.rels") + f.Pkg.Store("_rels/.rels", []byte(``)) assert.Equal(t, "_rels/workbook.xml.rels", f.getWorkbookRelsPath()) } diff --git a/sheetpr_test.go b/sheetpr_test.go index 42e2e0d1ed..53532e9561 100644 --- a/sheetpr_test.go +++ b/sheetpr_test.go @@ -443,7 +443,9 @@ func TestSheetFormatPrOptions(t *testing.T) { func TestSetSheetFormatPr(t *testing.T) { f := NewFile() assert.NoError(t, f.GetSheetFormatPr("Sheet1")) - f.Sheet["xl/worksheets/sheet1.xml"].SheetFormatPr = nil + ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") + assert.True(t, ok) + 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") @@ -452,7 +454,9 @@ func TestSetSheetFormatPr(t *testing.T) { func TestGetSheetFormatPr(t *testing.T) { f := NewFile() assert.NoError(t, f.GetSheetFormatPr("Sheet1")) - f.Sheet["xl/worksheets/sheet1.xml"].SheetFormatPr = nil + ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") + assert.True(t, ok) + ws.(*xlsxWorksheet).SheetFormatPr = nil var ( baseColWidth BaseColWidth defaultColWidth DefaultColWidth diff --git a/sparkline.go b/sparkline.go index 150c0eaece..5326c60c52 100644 --- a/sparkline.go +++ b/sparkline.go @@ -467,7 +467,7 @@ func (f *File) parseFormatAddSparklineSet(sheet string, opt *SparklineOption) (* return ws, err } if opt == nil { - return ws, errors.New("parameter is required") + return ws, ErrParameterRequired } if len(opt.Location) < 1 { return ws, errors.New("parameter 'Location' is required") diff --git a/sparkline_test.go b/sparkline_test.go index eac982466d..0777ee16a3 100644 --- a/sparkline_test.go +++ b/sparkline_test.go @@ -253,7 +253,9 @@ func TestAddSparkline(t *testing.T) { Style: -1, }), `parameter 'Style' must betweent 0-35`) - f.Sheet["xl/worksheets/sheet1.xml"].ExtLst.Ext = ` + ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") + assert.True(t, ok) + ws.(*xlsxWorksheet).ExtLst.Ext = ` diff --git a/stream.go b/stream.go index 054dd8d189..4a77b5632b 100644 --- a/stream.go +++ b/stream.go @@ -301,7 +301,7 @@ func (sw *StreamWriter) SetRow(axis string, values []interface{}) error { } if !sw.sheetWritten { if len(sw.cols) > 0 { - sw.rawData.WriteString("" + sw.cols + "") + _, _ = sw.rawData.WriteString("" + sw.cols + "") } _, _ = sw.rawData.WriteString(``) sw.sheetWritten = true @@ -481,9 +481,9 @@ func (sw *StreamWriter) Flush() error { } sheetPath := sw.File.sheetMap[trimSheetName(sw.Sheet)] - delete(sw.File.Sheet, sheetPath) + sw.File.Sheet.Delete(sheetPath) delete(sw.File.checked, sheetPath) - delete(sw.File.XLSX, sheetPath) + sw.File.Pkg.Delete(sheetPath) return nil } diff --git a/stream_test.go b/stream_test.go index cf133f14aa..f911ccc81e 100644 --- a/stream_test.go +++ b/stream_test.go @@ -99,8 +99,8 @@ func TestStreamWriter(t *testing.T) { // Test unsupported charset file = NewFile() - delete(file.Sheet, "xl/worksheets/sheet1.xml") - file.XLSX["xl/worksheets/sheet1.xml"] = MacintoshCyrillicCharset + 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") @@ -145,7 +145,9 @@ func TestStreamTable(t *testing.T) { // Verify the table has names. var table xlsxTable - assert.NoError(t, xml.Unmarshal(file.XLSX["xl/tables/table1.xml"], &table)) + val, ok := file.Pkg.Load("xl/tables/table1.xml") + assert.True(t, ok) + assert.NoError(t, xml.Unmarshal(val.([]byte), &table)) assert.Equal(t, "A", table.TableColumns.TableColumn[0].Name) assert.Equal(t, "B", table.TableColumns.TableColumn[1].Name) assert.Equal(t, "C", table.TableColumns.TableColumn[2].Name) diff --git a/styles.go b/styles.go index 1ba9f08b1e..235746c579 100644 --- a/styles.go +++ b/styles.go @@ -15,7 +15,6 @@ import ( "bytes" "encoding/json" "encoding/xml" - "errors" "fmt" "io" "log" @@ -1104,14 +1103,14 @@ func parseFormatStyleSet(style interface{}) (*Style, error) { case *Style: fs = *v default: - err = errors.New("invalid parameter type") + err = ErrParameterInvalid } if fs.Font != nil { if len(fs.Font.Family) > MaxFontFamilyLength { - return &fs, errors.New("the length of the font family name must be smaller than or equal to 31") + return &fs, ErrFontLength } if fs.Font.Size > MaxFontSize { - return &fs, errors.New("font size must be between 1 and 409 points") + return &fs, ErrFontSize } } return &fs, err diff --git a/styles_test.go b/styles_test.go index e2eed1d7f9..5d452f6f28 100644 --- a/styles_test.go +++ b/styles_test.go @@ -201,7 +201,7 @@ func TestNewStyle(t *testing.T) { _, err = f.NewStyle(&Style{}) assert.NoError(t, err) _, err = f.NewStyle(Style{}) - assert.EqualError(t, err, "invalid parameter type") + assert.EqualError(t, err, ErrParameterInvalid.Error()) _, err = f.NewStyle(&Style{Font: &Font{Family: strings.Repeat("s", MaxFontFamilyLength+1)}}) assert.EqualError(t, err, "the length of the font family name must be smaller than or equal to 31") @@ -261,14 +261,14 @@ func TestStylesReader(t *testing.T) { f := NewFile() // Test read styles with unsupported charset. f.Styles = nil - f.XLSX["xl/styles.xml"] = MacintoshCyrillicCharset + f.Pkg.Store("xl/styles.xml", MacintoshCyrillicCharset) assert.EqualValues(t, new(xlsxStyleSheet), f.stylesReader()) } func TestThemeReader(t *testing.T) { f := NewFile() // Test read theme with unsupported charset. - f.XLSX["xl/theme/theme1.xml"] = MacintoshCyrillicCharset + f.Pkg.Store("xl/theme/theme1.xml", MacintoshCyrillicCharset) assert.EqualValues(t, new(xlsxTheme), f.themeReader()) } diff --git a/table.go b/table.go index 12ef41a8c0..620cf20b35 100644 --- a/table.go +++ b/table.go @@ -105,11 +105,12 @@ func (f *File) AddTable(sheet, hcell, vcell, format string) error { // folder xl/tables. func (f *File) countTables() int { count := 0 - for k := range f.XLSX { - if strings.Contains(k, "xl/tables/table") { + f.Pkg.Range(func(k, v interface{}) bool { + if strings.Contains(k.(string), "xl/tables/table") { count++ } - } + return true + }) return count } From b7fece51736977e7d84aca30ecce7f6b3a1251f2 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 6 Jul 2021 00:31:04 +0800 Subject: [PATCH 404/957] Support concurrency add picture --- calcchain.go | 2 ++ cell_test.go | 5 +++++ comment.go | 2 ++ drawing.go | 9 +++++++-- excelize.go | 8 +++++++- picture.go | 16 ++++++++++++++++ rows.go | 2 ++ sheet.go | 8 ++++++++ xmlContentTypes.go | 6 +++++- xmlDrawing.go | 6 +++++- xmlWorkbook.go | 6 +++++- 11 files changed, 64 insertions(+), 6 deletions(-) diff --git a/calcchain.go b/calcchain.go index 1b99a04b4f..671d144707 100644 --- a/calcchain.go +++ b/calcchain.go @@ -56,6 +56,8 @@ func (f *File) deleteCalcChain(index int, axis string) { f.CalcChain = nil f.Pkg.Delete("xl/calcChain.xml") content := f.contentTypesReader() + content.Lock() + defer content.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_test.go b/cell_test.go index e289983f7c..91f4804053 100644 --- a/cell_test.go +++ b/cell_test.go @@ -10,6 +10,8 @@ import ( "testing" "time" + _ "image/jpeg" + "github.com/stretchr/testify/assert" ) @@ -25,6 +27,9 @@ func TestConcurrency(t *testing.T) { assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("B%d", val), strconv.Itoa(val))) _, err := f.GetCellValue("Sheet1", fmt.Sprintf("A%d", val)) assert.NoError(t, err) + // 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/360EntSecGroup-Skylar/excelize", "hyperlink_type": "External", "positioning": "oneCell"}`)) // Concurrency get cell picture name, raw, err := f.GetPicture("Sheet1", "A1") assert.Equal(t, "", name) diff --git a/comment.go b/comment.go index 306826dc52..a89d2bb610 100644 --- a/comment.go +++ b/comment.go @@ -76,6 +76,8 @@ func (f *File) GetComments() (comments map[string][]Comment) { func (f *File) getSheetComments(sheetFile string) string { var rels = "xl/worksheets/_rels/" + sheetFile + ".rels" if sheetRels := f.relsReader(rels); sheetRels != nil { + sheetRels.Lock() + defer sheetRels.Unlock() for _, v := range sheetRels.Relationships { if v.Type == SourceRelationshipComments { return v.Target diff --git a/drawing.go b/drawing.go index 181bb433ee..86d5ca68c5 100644 --- a/drawing.go +++ b/drawing.go @@ -1173,8 +1173,13 @@ func (f *File) drawingParser(path string) (*xlsxWsDr, int) { } f.Drawings.Store(path, &content) } - wsDr, _ := f.Drawings.Load(path) - return wsDr.(*xlsxWsDr), len(wsDr.(*xlsxWsDr).OneCellAnchor) + len(wsDr.(*xlsxWsDr).TwoCellAnchor) + 2 + var wsDr *xlsxWsDr + if drawing, ok := f.Drawings.Load(path); ok && drawing != nil { + wsDr = drawing.(*xlsxWsDr) + } + wsDr.Lock() + defer wsDr.Unlock() + return wsDr, len(wsDr.OneCellAnchor) + len(wsDr.TwoCellAnchor) + 2 } // addDrawingChart provides a function to add chart graphic frame by given diff --git a/excelize.go b/excelize.go index bdb71202d9..0091c820c8 100644 --- a/excelize.go +++ b/excelize.go @@ -43,7 +43,7 @@ type File struct { Path string SharedStrings *xlsxSST sharedStringsMap map[string]int - Sheet sync.Map // map[string]*xlsxWorksheet + Sheet sync.Map SheetCount int Styles *xlsxStyleSheet Theme *xlsxTheme @@ -257,6 +257,8 @@ func (f *File) addRels(relPath, relType, target, targetMode string) int { if rels == nil { rels = &xlsxRelationships{} } + rels.Lock() + defer rels.Unlock() var rID int for idx, rel := range rels.Relationships { ID, _ := strconv.Atoi(strings.TrimPrefix(rel.ID, "rId")) @@ -357,6 +359,8 @@ func (f *File) AddVBAProject(bin string) error { } f.setContentTypePartVBAProjectExtensions() wb := f.relsReader(f.getWorkbookRelsPath()) + wb.Lock() + defer wb.Unlock() var rID int var ok bool for _, rel := range wb.Relationships { @@ -387,6 +391,8 @@ func (f *File) AddVBAProject(bin string) error { func (f *File) setContentTypePartVBAProjectExtensions() { var ok bool content := f.contentTypesReader() + content.Lock() + defer content.Unlock() for _, v := range content.Defaults { if v.Extension == "bin" { ok = true diff --git a/picture.go b/picture.go index 58fa909778..e524224700 100644 --- a/picture.go +++ b/picture.go @@ -184,6 +184,8 @@ func (f *File) deleteSheetRelationships(sheet, rID string) { if sheetRels == nil { sheetRels = &xlsxRelationships{} } + sheetRels.Lock() + defer sheetRels.Unlock() for k, v := range sheetRels.Relationships { if v.ID == rID { sheetRels.Relationships = append(sheetRels.Relationships[:k], sheetRels.Relationships[k+1:]...) @@ -297,6 +299,8 @@ func (f *File) addDrawingPicture(sheet, drawingXML, cell, file string, width, he FLocksWithSheet: formatSet.FLocksWithSheet, FPrintsWithSheet: formatSet.FPrintsWithSheet, } + content.Lock() + defer content.Unlock() content.TwoCellAnchor = append(content.TwoCellAnchor, &twoCellAnchor) f.Drawings.Store(drawingXML, content) return err @@ -344,6 +348,8 @@ func (f *File) addMedia(file []byte, ext string) string { func (f *File) setContentTypePartImageExtensions() { var imageTypes = map[string]bool{"jpeg": false, "png": false, "gif": false, "tiff": false} content := f.contentTypesReader() + content.Lock() + defer content.Unlock() for _, v := range content.Defaults { _, ok := imageTypes[v.Extension] if ok { @@ -365,6 +371,8 @@ func (f *File) setContentTypePartImageExtensions() { func (f *File) setContentTypePartVMLExtensions() { vml := false content := f.contentTypesReader() + content.Lock() + defer content.Unlock() for _, v := range content.Defaults { if v.Extension == "vml" { vml = true @@ -410,6 +418,8 @@ func (f *File) addContentTypePart(index int, contentType string) { s() } content := f.contentTypesReader() + content.Lock() + defer content.Unlock() for _, v := range content.Overrides { if v.PartName == partNames[contentType] { return @@ -434,6 +444,8 @@ func (f *File) getSheetRelationshipsTargetByID(sheet, rID string) string { if sheetRels == nil { sheetRels = &xlsxRelationships{} } + sheetRels.Lock() + defer sheetRels.Unlock() for _, v := range sheetRels.Relationships { if v.ID == rID { return v.Target @@ -560,6 +572,8 @@ func (f *File) getPictureFromWsDr(row, col int, drawingRelationships string, wsD anchor *xdrCellAnchor drawRel *xlsxRelationship ) + wsDr.Lock() + defer wsDr.Unlock() for _, anchor = range wsDr.TwoCellAnchor { if anchor.From != nil && anchor.Pic != nil { if anchor.From.Col == col && anchor.From.Row == row { @@ -584,6 +598,8 @@ func (f *File) getPictureFromWsDr(row, col int, drawingRelationships string, wsD // relationship ID. func (f *File) getDrawingRelationships(rels, rID string) *xlsxRelationship { if drawingRels := f.relsReader(rels); drawingRels != nil { + drawingRels.Lock() + defer drawingRels.Unlock() for _, v := range drawingRels.Relationships { if v.ID == rID { return &v diff --git a/rows.go b/rows.go index 229b12d6fb..a40f4a9a98 100644 --- a/rows.go +++ b/rows.go @@ -272,6 +272,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() for i := range ws.SheetData.Row { v := &ws.SheetData.Row[i] if v.R == row && v.Ht != 0 { diff --git a/sheet.go b/sheet.go index 2b9149504d..3a555402fd 100644 --- a/sheet.go +++ b/sheet.go @@ -93,6 +93,8 @@ func (f *File) contentTypesWriter() { // the spreadsheet. func (f *File) getWorkbookPath() (path string) { if rels := f.relsReader("_rels/.rels"); rels != nil { + rels.Lock() + defer rels.Unlock() for _, rel := range rels.Relationships { if rel.Type == SourceRelationshipOfficeDocument { path = strings.TrimPrefix(rel.Target, "/") @@ -198,6 +200,8 @@ func trimCell(column []xlsxC) []xlsxC { // type of the spreadsheet. func (f *File) setContentTypes(partName, contentType string) { content := f.contentTypesReader() + content.Lock() + defer content.Unlock() content.Overrides = append(content.Overrides, xlsxOverride{ PartName: partName, ContentType: contentType, @@ -540,6 +544,8 @@ func (f *File) DeleteSheet(name string) { // 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 { if v.ID == rID { content.Relationships = append(content.Relationships[:k], content.Relationships[k+1:]...) @@ -556,6 +562,8 @@ func (f *File) deleteSheetFromContentTypes(target string) { target = "/xl/" + target } content := f.contentTypesReader() + content.Lock() + defer content.Unlock() for k, v := range content.Overrides { if v.PartName == target { content.Overrides = append(content.Overrides[:k], content.Overrides[k+1:]...) diff --git a/xmlContentTypes.go b/xmlContentTypes.go index f429ef6bb9..6b6db6391e 100644 --- a/xmlContentTypes.go +++ b/xmlContentTypes.go @@ -11,12 +11,16 @@ package excelize -import "encoding/xml" +import ( + "encoding/xml" + "sync" +) // 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 { + sync.Mutex XMLName xml.Name `xml:"http://schemas.openxmlformats.org/package/2006/content-types Types"` Overrides []xlsxOverride `xml:"Override"` Defaults []xlsxDefault `xml:"Default"` diff --git a/xmlDrawing.go b/xmlDrawing.go index 4b51b635d5..4e35fcf7f6 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -11,7 +11,10 @@ package excelize -import "encoding/xml" +import ( + "encoding/xml" + "sync" +) // Source relationship and namespace list, associated prefixes and schema in which it was // introduced. @@ -303,6 +306,7 @@ type xlsxPoint2D struct { // xlsxWsDr directly maps the root element for a part of this content type shall // wsDr. type xlsxWsDr struct { + sync.Mutex XMLName xml.Name `xml:"xdr:wsDr"` AbsoluteAnchor []*xdrCellAnchor `xml:"xdr:absoluteAnchor"` OneCellAnchor []*xdrCellAnchor `xml:"xdr:oneCellAnchor"` diff --git a/xmlWorkbook.go b/xmlWorkbook.go index 7151c6fdee..0e8839b329 100644 --- a/xmlWorkbook.go +++ b/xmlWorkbook.go @@ -11,10 +11,14 @@ package excelize -import "encoding/xml" +import ( + "encoding/xml" + "sync" +) // xlsxRelationships describe references from parts to other internal resources in the package or to external resources. type xlsxRelationships struct { + sync.Mutex XMLName xml.Name `xml:"http://schemas.openxmlformats.org/package/2006/relationships Relationships"` Relationships []xlsxRelationship `xml:"Relationship"` } From 90d200a10ba4d8c2ae2eff47ad8e1cca0ab28e76 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 7 Jul 2021 00:57:43 +0800 Subject: [PATCH 405/957] Make the functions `SetSheetRow`, `New Style` and `SetCellStyle` concurrency safety --- cell.go | 20 ++++++++++++++++++++ cell_test.go | 13 +++++++++++++ sheet.go | 2 ++ styles.go | 5 ++++- xmlStyles.go | 6 +++++- 5 files changed, 44 insertions(+), 2 deletions(-) diff --git a/cell.go b/cell.go index 1d08c8ae9a..82e93c57f1 100644 --- a/cell.go +++ b/cell.go @@ -139,7 +139,9 @@ func (f *File) setCellTimeFunc(sheet, axis string, value time.Time) error { if err != nil { return err } + ws.Lock() cellData.S = f.prepareCellStyle(ws, col, cellData.S) + ws.Unlock() var isNum bool cellData.T, cellData.V, isNum, err = setCellTime(value) @@ -155,6 +157,8 @@ func (f *File) setCellTimeFunc(sheet, axis string, value time.Time) error { return err } +// 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) { var excelTime float64 excelTime, err = timeToExcelTime(value) @@ -170,6 +174,8 @@ func setCellTime(value time.Time) (t string, b string, isNum bool, err error) { return } +// 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) return @@ -193,6 +199,8 @@ func (f *File) SetCellInt(sheet, axis string, value int) error { return err } +// setCellInt prepares cell type and string type cell value by a given +// integer. func setCellInt(value int) (t string, v string) { v = strconv.Itoa(value) return @@ -209,11 +217,15 @@ func (f *File) SetCellBool(sheet, axis string, value bool) error { if err != nil { return err } + ws.Lock() + defer ws.Unlock() cellData.S = f.prepareCellStyle(ws, col, cellData.S) cellData.T, cellData.V = setCellBool(value) return err } +// setCellBool prepares cell type and string type cell value by a given +// boolean value. func setCellBool(value bool) (t string, v string) { t = "b" if value { @@ -242,11 +254,15 @@ func (f *File) SetCellFloat(sheet, axis string, value float64, prec, bitSize int if err != nil { return err } + ws.Lock() + defer ws.Unlock() cellData.S = f.prepareCellStyle(ws, col, cellData.S) cellData.T, cellData.V = setCellFloat(value, prec, 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) return @@ -334,11 +350,15 @@ func (f *File) SetCellDefault(sheet, axis, value string) error { if err != nil { return err } + ws.Lock() + defer ws.Unlock() cellData.S = f.prepareCellStyle(ws, col, cellData.S) cellData.T, cellData.V = setCellDefault(value) return err } +// setCellDefault prepares cell type and string type cell value by a given +// string. func setCellDefault(value string) (t string, v string) { v = value return diff --git a/cell_test.go b/cell_test.go index 91f4804053..f11c708f74 100644 --- a/cell_test.go +++ b/cell_test.go @@ -25,8 +25,20 @@ func TestConcurrency(t *testing.T) { // Concurrency set cell value assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("A%d", val), val)) assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("B%d", val), strconv.Itoa(val))) + // Concurrency get cell value _, err := f.GetCellValue("Sheet1", fmt.Sprintf("A%d", val)) assert.NoError(t, err) + // Concurrency set rows + 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), + uint(1<<32 - 1), uint8(1<<8 - 1), uint16(1<<16 - 1), uint32(1<<32 - 1), + uint64(1<<32 - 1), true, complex64(5 + 10i)})) + // Concurrency create style + style, err := f.NewStyle(`{"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/360EntSecGroup-Skylar/excelize", "hyperlink_type": "External", "positioning": "oneCell"}`)) @@ -59,6 +71,7 @@ func TestConcurrency(t *testing.T) { t.Error(err) } assert.Equal(t, "1", val) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestConcurrency.xlsx"))) } func TestCheckCellInArea(t *testing.T) { diff --git a/sheet.go b/sheet.go index 3a555402fd..8d3d457dd3 100644 --- a/sheet.go +++ b/sheet.go @@ -1777,6 +1777,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() for ; fromRow < toRow; fromRow++ { rowData := &ws.SheetData.Row[fromRow-1] fillColumns(rowData, colCount, fromRow) diff --git a/styles.go b/styles.go index 235746c579..5b9b200788 100644 --- a/styles.go +++ b/styles.go @@ -1990,6 +1990,8 @@ func (f *File) NewStyle(style interface{}) (int, error) { fs.DecimalPlaces = 2 } s := f.stylesReader() + s.Lock() + defer s.Unlock() // check given style already exist. if cellXfsID = f.getStyleID(s, fs); cellXfsID != -1 { return cellXfsID, err @@ -2693,7 +2695,8 @@ func (f *File) SetCellStyle(sheet, hcell, vcell string, styleID int) error { } prepareSheetXML(ws, vcol, vrow) makeContiguousColumns(ws, hrow, vrow, vcol) - + ws.Lock() + defer ws.Unlock() for r := hrowIdx; r <= vrowIdx; r++ { for k := hcolIdx; k <= vcolIdx; k++ { ws.SheetData.Row[r].C[k].S = styleID diff --git a/xmlStyles.go b/xmlStyles.go index 92e4e6abc8..afdc1700ca 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -11,10 +11,14 @@ package excelize -import "encoding/xml" +import ( + "encoding/xml" + "sync" +) // xlsxStyleSheet is the root element of the Styles part. type xlsxStyleSheet struct { + sync.Mutex XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main styleSheet"` NumFmts *xlsxNumFmts `xml:"numFmts,omitempty"` Fonts *xlsxFonts `xml:"fonts,omitempty"` From 4f0d676eb765472d1fe7a29cacd165b982785bd2 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 8 Jul 2021 00:52:07 +0800 Subject: [PATCH 406/957] Fix missing set each cell's styles when set columns style --- calc.go | 5 +---- col.go | 7 +++++++ col_test.go | 1 + 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/calc.go b/calc.go index 934ae43927..2d10e3b706 100644 --- a/calc.go +++ b/calc.go @@ -947,10 +947,7 @@ func isEndParenthesesToken(token efp.Token) bool { // token. func isOperatorPrefixToken(token efp.Token) bool { _, ok := tokenPriority[token.TValue] - if (token.TValue == "-" && token.TType == efp.TokenTypeOperatorPrefix) || (ok && token.TType == efp.TokenTypeOperatorInfix) { - return true - } - return false + return (token.TValue == "-" && token.TType == efp.TokenTypeOperatorPrefix) || (ok && token.TType == efp.TokenTypeOperatorInfix) } // getDefinedNameRefTo convert defined name to reference range. diff --git a/col.go b/col.go index 91ca3da10b..5171f34e65 100644 --- a/col.go +++ b/col.go @@ -435,6 +435,13 @@ func (f *File) SetColStyle(sheet, columns string, styleID int) error { fc.Width = c.Width return fc }) + if rows := len(ws.SheetData.Row); rows > 0 { + for col := start; col <= end; col++ { + from, _ := CoordinatesToCellName(col, 1) + to, _ := CoordinatesToCellName(col, rows) + f.SetCellStyle(sheet, from, to, styleID) + } + } return nil } diff --git a/col_test.go b/col_test.go index 8159a11cda..58f424baa5 100644 --- a/col_test.go +++ b/col_test.go @@ -287,6 +287,7 @@ func TestOutlineLevel(t *testing.T) { func TestSetColStyle(t *testing.T) { f := NewFile() + assert.NoError(t, f.SetCellValue("Sheet1", "B2", "Hello")) style, err := f.NewStyle(`{"fill":{"type":"pattern","color":["#94d3a2"],"pattern":1}}`) assert.NoError(t, err) // Test set column style on not exists worksheet. From 2ced00d6a82f993094858e60127d4f817ad788e3 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 9 Jul 2021 00:04:58 +0800 Subject: [PATCH 407/957] This closes #872, support re-save the new spreadsheet after `SaveAs` --- excelize_test.go | 1 + file.go | 1 + 2 files changed, 2 insertions(+) diff --git a/excelize_test.go b/excelize_test.go index 22e39d1267..8a5ea276f7 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -307,6 +307,7 @@ func TestNewFile(t *testing.T) { } assert.NoError(t, f.SaveAs(filepath.Join("test", "TestNewFile.xlsx"))) + assert.NoError(t, f.Save()) } func TestAddDrawingVML(t *testing.T) { diff --git a/file.go b/file.go index 1786e27d17..abb03053ba 100644 --- a/file.go +++ b/file.go @@ -68,6 +68,7 @@ func (f *File) SaveAs(name string, opt ...Options) error { if len(name) > MaxFileNameLength { return ErrMaxFileNameLength } + f.Path = name file, err := os.OpenFile(name, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0666) if err != nil { return err From ee8098037dc71028e755ae62dfeb823c0e7b366e Mon Sep 17 00:00:00 2001 From: Deepak S Date: Sat, 10 Jul 2021 09:47:41 +0530 Subject: [PATCH 408/957] Prevent panic when incorrect range is provided as PivotTableRange to (#874) --- adjust.go | 4 ++++ pivotTable_test.go | 3 +++ 2 files changed, 7 insertions(+) diff --git a/adjust.go b/adjust.go index 28b62ccb12..ef7b19a7ad 100644 --- a/adjust.go +++ b/adjust.go @@ -198,6 +198,10 @@ func (f *File) adjustAutoFilterHelper(dir adjustDirection, coordinates []int, nu // pair of coordinates. func (f *File) areaRefToCoordinates(ref string) ([]int, error) { rng := strings.Split(strings.Replace(ref, "$", "", -1), ":") + if len(rng) < 2 { + return nil, ErrParameterInvalid + } + return areaRangeToCoordinates(rng[0], rng[1]) } diff --git a/pivotTable_test.go b/pivotTable_test.go index e746d8df50..bf6bb01eb5 100644 --- a/pivotTable_test.go +++ b/pivotTable_test.go @@ -228,6 +228,9 @@ func TestAddPivotTable(t *testing.T) { // Test adjust range with invalid range _, _, err := f.adjustRange("") assert.EqualError(t, err, "parameter is required") + // Test adjust range with incorrect range + _, _, err = f.adjustRange("sheet1!") + assert.EqualError(t, err, "parameter is invalid") // Test get pivot fields order with empty data range _, err = f.getPivotFieldsOrder(&PivotTableOption{}) assert.EqualError(t, err, `parameter 'DataRange' parsing error: parameter is required`) From b14b74bf560f192f658d66fdbf719190f691bc5f Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 10 Jul 2021 23:47:35 +0800 Subject: [PATCH 409/957] This closes #873, make the sheet names are not case sensitive for `NewSheet`, `GetSheetIndex`, `DeleteSheet` --- sheet.go | 26 +++++++++++++++----------- sheet_test.go | 3 ++- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/sheet.go b/sheet.go index 8d3d457dd3..ac22b88e48 100644 --- a/sheet.go +++ b/sheet.go @@ -33,8 +33,9 @@ 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 when creating a new spreadsheet -// file, the default worksheet named `Sheet1` will be created. +// (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 { // Check if the worksheet already exists index := f.GetSheetIndex(name) @@ -391,12 +392,13 @@ func (f *File) getSheetID(name 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. +// 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 { var idx = -1 for index, sheet := range f.GetSheetList() { - if sheet == trimSheetName(name) { + if strings.EqualFold(sheet, trimSheetName(name)) { idx = index } } @@ -485,10 +487,12 @@ func (f *File) SetSheetBackground(sheet, picture string) error { } // DeleteSheet provides a function to delete worksheet in a workbook by given -// 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 the one worksheet is left. +// 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. func (f *File) DeleteSheet(name string) { if f.SheetCount == 1 || f.GetSheetIndex(name) == -1 { return @@ -514,7 +518,7 @@ func (f *File) DeleteSheet(name string) { } } for idx, sheet := range wb.Sheets.Sheet { - if sheet.Name == sheetName { + if strings.EqualFold(sheet.Name, sheetName) { wb.Sheets.Sheet = append(wb.Sheets.Sheet[:idx], wb.Sheets.Sheet[idx+1:]...) var sheetXML, rels string if wbRels != nil { @@ -528,7 +532,7 @@ func (f *File) DeleteSheet(name string) { target := f.deleteSheetFromWorkbookRels(sheet.ID) f.deleteSheetFromContentTypes(target) f.deleteCalcChain(sheet.SheetID, "") - delete(f.sheetMap, sheetName) + delete(f.sheetMap, sheet.Name) f.Pkg.Delete(sheetXML) f.Pkg.Delete(rels) f.Relationships.Delete(rels) diff --git a/sheet_test.go b/sheet_test.go index 268abdca7d..0a604de947 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -81,7 +81,8 @@ func ExampleFile_GetPageLayout() { func TestNewSheet(t *testing.T) { f := NewFile() - sheetID := f.NewSheet("Sheet2") + f.NewSheet("Sheet2") + sheetID := f.NewSheet("sheet2") f.SetActiveSheet(sheetID) // delete original sheet f.DeleteSheet(f.GetSheetName(f.GetSheetIndex("Sheet1"))) From f62c45fe0c111774fc69a31a42d5f3add10e5095 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 12 Jul 2021 00:02:39 +0800 Subject: [PATCH 410/957] This closes #848 and closes #852, fix reading decimals precision --- rows.go | 2 +- rows_test.go | 30 ++++++++++++++++-------------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/rows.go b/rows.go index a40f4a9a98..fcd3c1ad2e 100644 --- a/rows.go +++ b/rows.go @@ -378,7 +378,7 @@ func (c *xlsxC) getValueFrom(f *File, d *xlsxSST) (string, error) { return f.formattedValue(c.S, c.V), nil default: isNum, precision := isNumeric(c.V) - if isNum && precision > 15 { + if isNum && precision > 10 { val, _ := roundPrecision(c.V) if val != c.V { return f.formattedValue(c.S, val), nil diff --git a/rows_test.go b/rows_test.go index 069b6680a7..e07ecf133b 100644 --- a/rows_test.go +++ b/rows_test.go @@ -851,22 +851,24 @@ func TestGetValueFromInlineStr(t *testing.T) { } func TestGetValueFromNumber(t *testing.T) { - c := &xlsxC{T: "n", V: "2.2200000000000002"} + c := &xlsxC{T: "n"} f := NewFile() d := &xlsxSST{} - val, err := c.getValueFrom(f, d) - assert.NoError(t, err) - assert.Equal(t, "2.22", val) - - c = &xlsxC{T: "n", V: "2.220000ddsf0000000002-r"} - val, err = c.getValueFrom(f, d) - assert.NoError(t, err) - assert.Equal(t, "2.220000ddsf0000000002-r", val) - - c = &xlsxC{T: "n", V: "2.2."} - val, err = c.getValueFrom(f, d) - assert.NoError(t, err) - assert.Equal(t, "2.2.", val) + for input, expected := range map[string]string{ + "2.2.": "2.2.", + "1.1000000000000001": "1.1", + "2.2200000000000002": "2.22", + "28.552": "28.552", + "27.399000000000001": "27.399", + "26.245999999999999": "26.246", + "2422.3000000000002": "2422.3", + "2.220000ddsf0000000002-r": "2.220000ddsf0000000002-r", + } { + c.V = input + val, err := c.getValueFrom(f, d) + assert.NoError(t, err) + assert.Equal(t, expected, val) + } } func TestErrSheetNotExistError(t *testing.T) { From fbcfdeae90b7e755a70c6ceef27346c7d0552937 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 15 Jul 2021 23:24:01 +0800 Subject: [PATCH 411/957] This closes #879, fix delete defined name failed in some case --- sheet.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/sheet.go b/sheet.go index ac22b88e48..05dc2cf142 100644 --- a/sheet.go +++ b/sheet.go @@ -1520,11 +1520,15 @@ func (f *File) DeleteDefinedName(definedName *DefinedName) error { wb := f.workbookReader() if wb.DefinedNames != nil { for idx, dn := range wb.DefinedNames.DefinedName { - var scope string + scope := "Workbook" + deleteScope := definedName.Scope + if deleteScope == "" { + deleteScope = "Workbook" + } if dn.LocalSheetID != nil { scope = f.GetSheetName(*dn.LocalSheetID) } - if scope == definedName.Scope && dn.Name == definedName.Name { + if scope == deleteScope && dn.Name == definedName.Name { wb.DefinedNames.DefinedName = append(wb.DefinedNames.DefinedName[:idx], wb.DefinedNames.DefinedName[idx+1:]...) return nil } From ec0ca8ba50a3a59048c51e360301230f45dfc978 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 16 Jul 2021 00:00:50 +0800 Subject: [PATCH 412/957] This closes #883, fix missing pivot attribute of conditional formatting --- xmlWorksheet.go | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 82ac39547c..d280bfdc37 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -528,6 +528,7 @@ type xlsxPhoneticPr struct { // applied to a particular cell or range. type xlsxConditionalFormatting struct { XMLName xml.Name `xml:"conditionalFormatting"` + Pivot bool `xml:"pivot,attr,omitempty"` SQRef string `xml:"sqref,attr,omitempty"` CfRule []*xlsxCfRule `xml:"cfRule"` } @@ -535,19 +536,19 @@ type xlsxConditionalFormatting struct { // xlsxCfRule (Conditional Formatting Rule) represents a description of a // conditional formatting rule. type xlsxCfRule struct { - AboveAverage *bool `xml:"aboveAverage,attr"` - Bottom bool `xml:"bottom,attr,omitempty"` + Type string `xml:"type,attr,omitempty"` DxfID *int `xml:"dxfId,attr"` - EqualAverage bool `xml:"equalAverage,attr,omitempty"` - Operator string `xml:"operator,attr,omitempty"` - Percent bool `xml:"percent,attr,omitempty"` Priority int `xml:"priority,attr,omitempty"` - Rank int `xml:"rank,attr,omitempty"` - StdDev int `xml:"stdDev,attr,omitempty"` StopIfTrue bool `xml:"stopIfTrue,attr,omitempty"` + AboveAverage *bool `xml:"aboveAverage,attr"` + Percent bool `xml:"percent,attr,omitempty"` + Bottom bool `xml:"bottom,attr,omitempty"` + Operator string `xml:"operator,attr,omitempty"` Text string `xml:"text,attr,omitempty"` TimePeriod string `xml:"timePeriod,attr,omitempty"` - Type string `xml:"type,attr,omitempty"` + Rank int `xml:"rank,attr,omitempty"` + StdDev int `xml:"stdDev,attr,omitempty"` + EqualAverage bool `xml:"equalAverage,attr,omitempty"` Formula []string `xml:"formula,omitempty"` ColorScale *xlsxColorScale `xml:"colorScale"` DataBar *xlsxDataBar `xml:"dataBar"` From 1ec0207fb5fe772e47b257ab2b0c26ff85f94598 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 20 Jul 2021 23:04:50 +0800 Subject: [PATCH 413/957] Fix code security issue --- calc.go | 9 ++++----- lib.go | 5 +---- styles.go | 8 ++++---- 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/calc.go b/calc.go index 2d10e3b706..3bb81b89f4 100644 --- a/calc.go +++ b/calc.go @@ -7401,7 +7401,7 @@ func (fn *formulaFuncs) cumip(name string, argsList *list.List) formulaArg { if start.Number < 1 || start.Number > end.Number { return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) } - num, ipmt := 0.0, newNumberFormulaArg(0) + num := 0.0 for per := start.Number; per <= end.Number; per++ { args := list.New().Init() args.PushBack(rate) @@ -7411,11 +7411,10 @@ func (fn *formulaFuncs) cumip(name string, argsList *list.List) formulaArg { args.PushBack(newNumberFormulaArg(0)) args.PushBack(typ) if name == "CUMIPMT" { - ipmt = fn.IPMT(args) - } else { - ipmt = fn.PPMT(args) + num += fn.IPMT(args).Number + continue } - num += ipmt.Number + num += fn.PPMT(args).Number } return newNumberFormulaArg(num) } diff --git a/lib.go b/lib.go index 00a67d9658..df2af4a903 100644 --- a/lib.go +++ b/lib.go @@ -63,10 +63,7 @@ func (f *File) readXML(name string) []byte { // saveFileList provides a function to update given file content in file list // of XLSX. func (f *File) saveFileList(name string, content []byte) { - newContent := make([]byte, 0, len(XMLHeader)+len(content)) - newContent = append(newContent, []byte(XMLHeader)...) - newContent = append(newContent, content...) - f.Pkg.Store(name, newContent) + f.Pkg.Store(name, append([]byte(XMLHeader), content...)) } // Read file content as string in a archive file. diff --git a/styles.go b/styles.go index 5b9b200788..07ccab1d5b 100644 --- a/styles.go +++ b/styles.go @@ -3130,11 +3130,11 @@ func ThemeColor(baseColor string, tint float64) string { if tint == 0 { return "FF" + baseColor } - r, _ := strconv.ParseInt(baseColor[0:2], 16, 64) - g, _ := strconv.ParseInt(baseColor[2:4], 16, 64) - b, _ := strconv.ParseInt(baseColor[4:6], 16, 64) + r, _ := strconv.ParseUint(baseColor[0:2], 16, 64) + g, _ := strconv.ParseUint(baseColor[2:4], 16, 64) + b, _ := strconv.ParseUint(baseColor[4:6], 16, 64) var h, s, l float64 - if r >= 0 && r <= math.MaxUint8 && g >= 0 && g <= math.MaxUint8 && b >= 0 && b <= math.MaxUint8 { + if r <= math.MaxUint8 && g <= math.MaxUint8 && b <= math.MaxUint8 { h, s, l = RGBToHSL(uint8(r), uint8(g), uint8(b)) } if tint < 0 { From 5ce3fe8cb89f5f278b3857bab8e69c117c2a6027 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 21 Jul 2021 23:24:49 +0800 Subject: [PATCH 414/957] Improvement compatibility with invalid first-page number attribute in the page layout --- sheet.go | 12 +++++++----- xmlWorksheet.go | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/sheet.go b/sheet.go index 05dc2cf142..7a1fff39c6 100644 --- a/sheet.go +++ b/sheet.go @@ -1202,7 +1202,7 @@ func (p *BlackAndWhite) getPageLayout(ps *xlsxPageSetUp) { // the worksheet. func (p FirstPageNumber) setPageLayout(ps *xlsxPageSetUp) { if 0 < int(p) { - ps.FirstPageNumber = int(p) + ps.FirstPageNumber = strconv.Itoa(int(p)) ps.UseFirstPageNumber = true } } @@ -1210,11 +1210,13 @@ func (p FirstPageNumber) setPageLayout(ps *xlsxPageSetUp) { // getPageLayout provides a method to get the first printed page number for // the worksheet. func (p *FirstPageNumber) getPageLayout(ps *xlsxPageSetUp) { - if ps == nil || ps.FirstPageNumber == 0 || !ps.UseFirstPageNumber { - *p = 1 - return + if ps != nil && ps.UseFirstPageNumber { + if number, _ := strconv.Atoi(ps.FirstPageNumber); number != 0 { + *p = FirstPageNumber(number) + return + } } - *p = FirstPageNumber(ps.FirstPageNumber) + *p = 1 } // setPageLayout provides a method to set the orientation for the worksheet. diff --git a/xmlWorksheet.go b/xmlWorksheet.go index d280bfdc37..4499546367 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -111,7 +111,7 @@ type xlsxPageSetUp struct { Copies int `xml:"copies,attr,omitempty"` Draft bool `xml:"draft,attr,omitempty"` Errors string `xml:"errors,attr,omitempty"` - FirstPageNumber int `xml:"firstPageNumber,attr,omitempty"` + FirstPageNumber string `xml:"firstPageNumber,attr,omitempty"` FitToHeight int `xml:"fitToHeight,attr,omitempty"` FitToWidth int `xml:"fitToWidth,attr,omitempty"` HorizontalDPI int `xml:"horizontalDpi,attr,omitempty"` From f9e9e5d2e07b087e2d4fb2487193aea8c240ab0e Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 25 Jul 2021 00:43:07 +0800 Subject: [PATCH 415/957] This closes #882, support set rows height and hidden row by stream writer --- stream.go | 39 ++++++++++++++++++++++++++++++++++++--- stream_test.go | 4 +++- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/stream.go b/stream.go index 4a77b5632b..99390168ac 100644 --- a/stream.go +++ b/stream.go @@ -56,7 +56,8 @@ type StreamWriter struct { // if err != nil { // fmt.Println(err) // } -// if err := streamWriter.SetRow("A1", []interface{}{excelize.Cell{StyleID: styleID, Value: "Data"}}); err != nil { +// 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++ { @@ -288,13 +289,19 @@ type Cell struct { Value interface{} } +// RowOpts define the options for set row. +type RowOpts struct { + Height float64 + Hidden bool +} + // 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. // // 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{}) error { +func (sw *StreamWriter) SetRow(axis string, values []interface{}, opts ...RowOpts) error { col, row, err := CellNameToCoordinates(axis) if err != nil { return err @@ -306,7 +313,11 @@ func (sw *StreamWriter) SetRow(axis string, values []interface{}) error { _, _ = sw.rawData.WriteString(``) sw.sheetWritten = true } - fmt.Fprintf(&sw.rawData, ``, row) + attrs, err := marshalRowAttrs(opts...) + if err != nil { + return err + } + fmt.Fprintf(&sw.rawData, ``, row, attrs) for i, val := range values { axis, err := CoordinatesToCellName(col+i, row) if err != nil { @@ -332,6 +343,28 @@ func (sw *StreamWriter) SetRow(axis string, values []interface{}) error { return sw.rawData.Sync() } +// marshalRowAttrs prepare attributes of the row by given options. +func marshalRowAttrs(opts ...RowOpts) (attrs string, err error) { + var opt *RowOpts + for _, o := range opts { + opt = &o + } + if opt == nil { + return + } + if opt.Height > MaxRowHeight { + err = ErrMaxRowHeight + return + } + if opt.Height > 0 { + attrs += fmt.Sprintf(` ht="%v" customHeight="true"`, opt.Height) + } + if opt.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 f911ccc81e..fda44fbe76 100644 --- a/stream_test.go +++ b/stream_test.go @@ -55,9 +55,11 @@ func TestStreamWriter(t *testing.T) { // Test set cell with style. styleID, err := file.NewStyle(`{"font":{"color":"#777777"}}`) assert.NoError(t, err) - assert.NoError(t, streamWriter.SetRow("A4", []interface{}{Cell{StyleID: styleID}, Cell{Formula: "SUM(A10,B10)"}})) + assert.NoError(t, streamWriter.SetRow("A4", []interface{}{Cell{StyleID: styleID}, Cell{Formula: "SUM(A10,B10)"}}), RowOpts{Height: 45}) 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{Hidden: true})) + assert.EqualError(t, streamWriter.SetRow("A7", nil, RowOpts{Height: MaxRowHeight + 1}), ErrMaxRowHeight.Error()) for rowID := 10; rowID <= 51200; rowID++ { row := make([]interface{}, 50) From e9ae9b45b20a5df7e3aa15afcfac83ecb13394c6 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 28 Jul 2021 00:38:09 +0800 Subject: [PATCH 416/957] change go module import path to github.com/xuri/excelize --- .github/FUNDING.yml | 6 ++++++ CONTRIBUTING.md | 4 ++-- README.md | 22 +++++++++++----------- README_zh.md | 22 +++++++++++----------- cell.go | 4 ++-- cell_test.go | 2 +- chart.go | 4 ++-- col_test.go | 4 ++-- excelize_test.go | 10 +++++----- go.mod | 2 +- merge_test.go | 2 +- picture.go | 6 +++--- picture_test.go | 2 +- pivotTable.go | 2 +- rows_test.go | 4 ++-- 15 files changed, 51 insertions(+), 45 deletions(-) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000000..ff137ebd4c --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,6 @@ +patreon: xuri +open_collective: excelize +ko_fi: xurime +liberapay: xuri +issuehunt: xuri +custom: https://www.paypal.com/paypalme/xuri \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 53c650e55f..89bc60edc4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -31,7 +31,7 @@ A great way to contribute to the project is to send a detailed report when you encounter an issue. We always appreciate a well-written, thorough bug report, and will thank you for it! -Check that [our issue database](https://github.com/360EntSecGroup-Skylar/excelize/issues) +Check that [our issue database](https://github.com/xuri/excelize/issues) doesn't already include that problem or suggestion before submitting an issue. If you find a match, you can use the "subscribe" button to get notified on updates. Do *not* leave random "+1" or "I have this too" comments, as they @@ -55,7 +55,7 @@ This section gives the experienced contributor some tips and guidelines. Not sure if that typo is worth a pull request? Found a bug and know how to fix it? Do it! We will appreciate it. Any significant improvement should be -documented as [a GitHub issue](https://github.com/360EntSecGroup-Skylar/excelize/issues) before +documented as [a GitHub issue](https://github.com/xuri/excelize/issues) before anybody starts working on it. We are always thrilled to receive pull requests. We do our best to process them diff --git a/README.md b/README.md index 972d01331f..3c49173c82 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@

Excelize logo

- Build Status - Code Coverage - Go Report Card - go.dev + Build Status + Code Coverage + Go Report Card + go.dev Licenses Donate

@@ -13,20 +13,20 @@ ## Introduction -Excelize is a library written in pure Go providing a set of functions that allow you to write to and read from XLSX / XLSM / 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/360EntSecGroup-Skylar/excelize/v2?tab=doc) 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 XLSX / XLSM / 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?tab=doc) and [docs reference](https://xuri.me/excelize/). ## Basic Usage ### Installation ```bash -go get github.com/360EntSecGroup-Skylar/excelize +go get github.com/xuri/excelize ``` - If your packages are managed using [Go Modules](https://blog.golang.org/using-go-modules), please install with following command. ```bash -go get github.com/360EntSecGroup-Skylar/excelize/v2 +go get github.com/xuri/excelize/v2 ``` ### Create spreadsheet @@ -39,7 +39,7 @@ package main import ( "fmt" - "github.com/360EntSecGroup-Skylar/excelize/v2" + "github.com/xuri/excelize/v2" ) func main() { @@ -68,7 +68,7 @@ package main import ( "fmt" - "github.com/360EntSecGroup-Skylar/excelize/v2" + "github.com/xuri/excelize/v2" ) func main() { @@ -111,7 +111,7 @@ package main import ( "fmt" - "github.com/360EntSecGroup-Skylar/excelize/v2" + "github.com/xuri/excelize/v2" ) func main() { @@ -171,7 +171,7 @@ import ( _ "image/jpeg" _ "image/png" - "github.com/360EntSecGroup-Skylar/excelize/v2" + "github.com/xuri/excelize/v2" ) func main() { diff --git a/README_zh.md b/README_zh.md index 4e1f56f4bb..6015a44238 100644 --- a/README_zh.md +++ b/README_zh.md @@ -1,10 +1,10 @@

Excelize logo

- Build Status - Code Coverage - Go Report Card - go.dev + Build Status + Code Coverage + Go Report Card + go.dev Licenses Donate

@@ -13,20 +13,20 @@ ## 简介 -Excelize 是 Go 语言编写的用于操作 Office Excel 文档基础库,基于 ECMA-376,ISO/IEC 29500 国际标准。可以使用它来读取、写入由 Microsoft Excel™ 2007 及以上版本创建的电子表格文档。支持 XLSX / XLSM / XLTM / XLTX 等多种文档格式,高度兼容带有样式、图片(表)、透视表、切片器等复杂组件的文档,并提供流式读写 API,用于处理包含大规模数据的工作簿。可应用于各类报表平台、云计算、边缘计算等系统。使用本类库要求使用的 Go 语言为 1.15 或更高版本,完整的 API 使用文档请访问 [go.dev](https://pkg.go.dev/github.com/360EntSecGroup-Skylar/excelize/v2?tab=doc) 或查看 [参考文档](https://xuri.me/excelize/)。 +Excelize 是 Go 语言编写的用于操作 Office Excel 文档基础库,基于 ECMA-376,ISO/IEC 29500 国际标准。可以使用它来读取、写入由 Microsoft Excel™ 2007 及以上版本创建的电子表格文档。支持 XLSX / XLSM / XLTM / XLTX 等多种文档格式,高度兼容带有样式、图片(表)、透视表、切片器等复杂组件的文档,并提供流式读写 API,用于处理包含大规模数据的工作簿。可应用于各类报表平台、云计算、边缘计算等系统。使用本类库要求使用的 Go 语言为 1.15 或更高版本,完整的 API 使用文档请访问 [go.dev](https://pkg.go.dev/github.com/xuri/excelize/v2?tab=doc) 或查看 [参考文档](https://xuri.me/excelize/)。 ## 快速上手 ### 安装 ```bash -go get github.com/360EntSecGroup-Skylar/excelize +go get github.com/xuri/excelize ``` - 如果您使用 [Go Modules](https://blog.golang.org/using-go-modules) 管理软件包,请使用下面的命令来安装最新版本。 ```bash -go get github.com/360EntSecGroup-Skylar/excelize/v2 +go get github.com/xuri/excelize/v2 ``` ### 创建 Excel 文档 @@ -39,7 +39,7 @@ package main import ( "fmt" - "github.com/360EntSecGroup-Skylar/excelize/v2" + "github.com/xuri/excelize/v2" ) func main() { @@ -68,7 +68,7 @@ package main import ( "fmt" - "github.com/360EntSecGroup-Skylar/excelize/v2" + "github.com/xuri/excelize/v2" ) func main() { @@ -111,7 +111,7 @@ package main import ( "fmt" - "github.com/360EntSecGroup-Skylar/excelize/v2" + "github.com/xuri/excelize/v2" ) func main() { @@ -171,7 +171,7 @@ import ( _ "image/jpeg" _ "image/png" - "github.com/360EntSecGroup-Skylar/excelize/v2" + "github.com/xuri/excelize/v2" ) func main() { diff --git a/cell.go b/cell.go index 82e93c57f1..39990d488c 100644 --- a/cell.go +++ b/cell.go @@ -468,7 +468,7 @@ type HyperlinkOpts struct { // in this workbook. Maximum limit hyperlinks in a worksheet is 65530. The // below is example for external link. // -// err := f.SetCellHyperLink("Sheet1", "A3", "https://github.com/360EntSecGroup-Skylar/excelize", "External") +// err := f.SetCellHyperLink("Sheet1", "A3", "https://github.com/xuri/excelize", "External") // // Set underline and font color style for the cell. // style, err := f.NewStyle(`{"font":{"color":"#1265BE","underline":"single"}}`) // err = f.SetCellStyle("Sheet1", "A3", "A3", style) @@ -594,7 +594,7 @@ func (f *File) GetCellRichText(sheet, cell string) (runs []RichTextRun, err erro // import ( // "fmt" // -// "github.com/360EntSecGroup-Skylar/excelize/v2" +// "github.com/xuri/excelize/v2" // ) // // func main() { diff --git a/cell_test.go b/cell_test.go index f11c708f74..ea62869619 100644 --- a/cell_test.go +++ b/cell_test.go @@ -41,7 +41,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"), - `{"x_offset": 10, "y_offset": 10, "hyperlink": "https://github.com/360EntSecGroup-Skylar/excelize", "hyperlink_type": "External", "positioning": "oneCell"}`)) + `{"x_offset": 10, "y_offset": 10, "hyperlink": "https://github.com/xuri/excelize", "hyperlink_type": "External", "positioning": "oneCell"}`)) // Concurrency get cell picture name, raw, err := f.GetPicture("Sheet1", "A1") assert.Equal(t, "", name) diff --git a/chart.go b/chart.go index 52fd54315a..755c160ef8 100644 --- a/chart.go +++ b/chart.go @@ -510,7 +510,7 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // import ( // "fmt" // -// "github.com/360EntSecGroup-Skylar/excelize/v2" +// "github.com/xuri/excelize/v2" // ) // // func main() { @@ -783,7 +783,7 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // import ( // "fmt" // -// "github.com/360EntSecGroup-Skylar/excelize/v2" +// "github.com/xuri/excelize/v2" // ) // // func main() { diff --git a/col_test.go b/col_test.go index 58f424baa5..b19eadf84e 100644 --- a/col_test.go +++ b/col_test.go @@ -338,7 +338,7 @@ func TestInsertCol(t *testing.T) { fillCells(f, sheet1, 10, 10) - assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External")) + 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"}`)) @@ -356,7 +356,7 @@ func TestRemoveCol(t *testing.T) { fillCells(f, sheet1, 10, 15) - assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External")) + assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/xuri/excelize", "External")) assert.NoError(t, f.SetCellHyperLink(sheet1, "C5", "https://github.com", "External")) assert.NoError(t, f.MergeCell(sheet1, "A1", "B1")) diff --git a/excelize_test.go b/excelize_test.go index 8a5ea276f7..cc3a1b2d06 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -322,9 +322,9 @@ func TestSetCellHyperLink(t *testing.T) { t.Log(err) } // Test set cell hyperlink in a work sheet already have hyperlinks. - assert.NoError(t, f.SetCellHyperLink("Sheet1", "B19", "https://github.com/360EntSecGroup-Skylar/excelize", "External")) + 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/360EntSecGroup-Skylar/excelize", "External")) + 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. @@ -347,7 +347,7 @@ func TestSetCellHyperLink(t *testing.T) { ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") assert.True(t, ok) ws.(*xlsxWorksheet).Hyperlinks = &xlsxHyperlinks{Hyperlink: make([]xlsxHyperlink, 65530)} - assert.EqualError(t, f.SetCellHyperLink("Sheet1", "A65531", "https://github.com/360EntSecGroup-Skylar/excelize", "External"), ErrTotalSheetHyperlinks.Error()) + assert.EqualError(t, f.SetCellHyperLink("Sheet1", "A65531", "https://github.com/xuri/excelize", "External"), ErrTotalSheetHyperlinks.Error()) f = NewFile() _, err = f.workSheetReader("Sheet1") @@ -355,7 +355,7 @@ func TestSetCellHyperLink(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"}}} - err = f.SetCellHyperLink("Sheet1", "A1", "https://github.com/360EntSecGroup-Skylar/excelize", "External") + err = f.SetCellHyperLink("Sheet1", "A1", "https://github.com/xuri/excelize", "External") assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`) } @@ -1272,7 +1272,7 @@ func prepareTestBook1() (*File, error) { // 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/360EntSecGroup-Skylar/excelize", "hyperlink_type": "External", "positioning": "oneCell"}`) + `{"x_offset": 10, "y_offset": 10, "hyperlink": "https://github.com/xuri/excelize", "hyperlink_type": "External", "positioning": "oneCell"}`) if err != nil { return nil, err } diff --git a/go.mod b/go.mod index 78ae93c412..877b924655 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/360EntSecGroup-Skylar/excelize/v2 +module github.com/xuri/excelize/v2 go 1.15 diff --git a/merge_test.go b/merge_test.go index cf460dd9a0..41c122cb53 100644 --- a/merge_test.go +++ b/merge_test.go @@ -24,7 +24,7 @@ func TestMergeCell(t *testing.T) { 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.SetCellHyperLink("Sheet1", "J11", "https://github.com/360EntSecGroup-Skylar/excelize", "External")) + 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") assert.Equal(t, "0.5", value) diff --git a/picture.go b/picture.go index e524224700..0531272279 100644 --- a/picture.go +++ b/picture.go @@ -54,7 +54,7 @@ func parseFormatPictureSet(formatSet string) (*formatPicture, error) { // _ "image/jpeg" // _ "image/png" // -// "github.com/360EntSecGroup-Skylar/excelize/v2" +// "github.com/xuri/excelize/v2" // ) // // func main() { @@ -68,7 +68,7 @@ func parseFormatPictureSet(formatSet string) (*formatPicture, error) { // 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/360EntSecGroup-Skylar/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", `{"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 { @@ -110,7 +110,7 @@ func (f *File) AddPicture(sheet, cell, picture, format string) error { // _ "image/jpeg" // "io/ioutil" // -// "github.com/360EntSecGroup-Skylar/excelize/v2" +// "github.com/xuri/excelize/v2" // ) // // func main() { diff --git a/picture_test.go b/picture_test.go index 69873eb0de..913ed3d7e1 100644 --- a/picture_test.go +++ b/picture_test.go @@ -42,7 +42,7 @@ func TestAddPicture(t *testing.T) { `{"x_offset": 140, "y_offset": 120, "hyperlink": "#Sheet2!D8", "hyperlink_type": "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/360EntSecGroup-Skylar/excelize", "hyperlink_type": "External", "positioning": "oneCell"}`)) + `{"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")) assert.NoError(t, err) diff --git a/pivotTable.go b/pivotTable.go index 05ac78327c..f6d6d2dfdf 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -82,7 +82,7 @@ type PivotTableField struct { // "fmt" // "math/rand" // -// "github.com/360EntSecGroup-Skylar/excelize/v2" +// "github.com/xuri/excelize/v2" // ) // // func main() { diff --git a/rows_test.go b/rows_test.go index e07ecf133b..768246936f 100644 --- a/rows_test.go +++ b/rows_test.go @@ -232,7 +232,7 @@ func TestRemoveRow(t *testing.T) { ) fillCells(f, sheet1, colCount, rowCount) - assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External")) + assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/xuri/excelize", "External")) assert.EqualError(t, f.RemoveRow(sheet1, -1), "invalid row number -1") @@ -293,7 +293,7 @@ func TestInsertRow(t *testing.T) { ) fillCells(f, sheet1, colCount, rowCount) - assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External")) + assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/xuri/excelize", "External")) assert.EqualError(t, f.InsertRow(sheet1, -1), "invalid row number -1") From 7dbf88f221f278075d4ff9e153b21236d0826c33 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 29 Jul 2021 00:03:57 +0800 Subject: [PATCH 417/957] This closes #971, closes #972 and closes #974 - Escape XML character in the drop list - Fix incorrect character count limit in the drop list - Fix Excel time parse issue in some case - Fix custom number format month parse issue in some case - Fix corrupted file generated caused by concurrency adding pictures --- col.go | 4 ++-- datavalidation.go | 13 +++++++------ date.go | 27 ++++++++++----------------- sheet.go | 3 ++- styles.go | 2 ++ xmlWorksheet.go | 4 ++-- 6 files changed, 25 insertions(+), 28 deletions(-) diff --git a/col.go b/col.go index 5171f34e65..088fac9919 100644 --- a/col.go +++ b/col.go @@ -439,10 +439,10 @@ func (f *File) SetColStyle(sheet, columns string, styleID int) error { for col := start; col <= end; col++ { from, _ := CoordinatesToCellName(col, 1) to, _ := CoordinatesToCellName(col, rows) - f.SetCellStyle(sheet, from, to, styleID) + err = f.SetCellStyle(sheet, from, to, styleID) } } - return nil + return err } // SetColWidth provides a function to set the width of a single column or diff --git a/datavalidation.go b/datavalidation.go index 0f8508bac2..a95f4d02c0 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -14,6 +14,7 @@ package excelize import ( "fmt" "strings" + "unicode/utf16" ) // DataValidationType defined the type of data validation. @@ -111,10 +112,10 @@ func (dd *DataValidation) SetInput(title, msg string) { // SetDropList data validation list. func (dd *DataValidation) SetDropList(keys []string) error { formula := "\"" + strings.Join(keys, ",") + "\"" - if dataValidationFormulaStrLen < len(formula) { + if dataValidationFormulaStrLen < len(utf16.Encode([]rune(formula))) { return fmt.Errorf(dataValidationFormulaStrLenErr) } - dd.Formula1 = fmt.Sprintf("%s", formula) + dd.Formula1 = formula dd.Type = convDataValidationType(typeList) return nil } @@ -123,12 +124,12 @@ func (dd *DataValidation) SetDropList(keys []string) error { func (dd *DataValidation) SetRange(f1, f2 float64, t DataValidationType, o DataValidationOperator) error { formula1 := fmt.Sprintf("%f", f1) formula2 := fmt.Sprintf("%f", f2) - if dataValidationFormulaStrLen+21 < len(dd.Formula1) || dataValidationFormulaStrLen+21 < len(dd.Formula2) { + if dataValidationFormulaStrLen < len(utf16.Encode([]rune(dd.Formula1))) || dataValidationFormulaStrLen < len(utf16.Encode([]rune(dd.Formula2))) { return fmt.Errorf(dataValidationFormulaStrLenErr) } - dd.Formula1 = fmt.Sprintf("%s", formula1) - dd.Formula2 = fmt.Sprintf("%s", formula2) + dd.Formula1 = formula1 + dd.Formula2 = formula2 dd.Type = convDataValidationType(t) dd.Operator = convDataValidationOperatior(o) return nil @@ -148,7 +149,7 @@ func (dd *DataValidation) SetRange(f1, f2 float64, t DataValidationType, o DataV // func (dd *DataValidation) SetSqrefDropList(sqref string, isCurrentSheet bool) error { if isCurrentSheet { - dd.Formula1 = fmt.Sprintf("%s", sqref) + dd.Formula1 = sqref dd.Type = convDataValidationType(typeList) return nil } diff --git a/date.go b/date.go index 0531b6c757..a5edcf8849 100644 --- a/date.go +++ b/date.go @@ -17,11 +17,14 @@ import ( ) const ( + nanosInADay = float64((24 * time.Hour) / time.Nanosecond) dayNanoseconds = 24 * time.Hour maxDuration = 290 * 364 * dayNanoseconds ) var ( + excel1900Epoc = time.Date(1899, time.December, 30, 0, 0, 0, 0, time.UTC) + excel1904Epoc = time.Date(1904, time.January, 1, 0, 0, 0, 0, time.UTC) excelMinTime1900 = time.Date(1899, time.December, 31, 0, 0, 0, 0, time.UTC) excelBuggyPeriodStart = time.Date(1900, time.March, 1, 0, 0, 0, 0, time.UTC).Add(-time.Nanosecond) ) @@ -131,12 +134,11 @@ func doTheFliegelAndVanFlandernAlgorithm(jd int) (day, month, year int) { // timeFromExcelTime provides a function to convert an excelTime // representation (stored as a floating point number) to a time.Time. func timeFromExcelTime(excelTime float64, date1904 bool) time.Time { - const MDD int64 = 106750 // Max time.Duration Days, aprox. 290 years var date time.Time - var intPart = int64(excelTime) + var wholeDaysPart = int(excelTime) // Excel uses Julian dates prior to March 1st 1900, and Gregorian // thereafter. - if intPart <= 61 { + if wholeDaysPart <= 61 { const OFFSET1900 = 15018.0 const OFFSET1904 = 16480.0 const MJD0 float64 = 2400000.5 @@ -148,23 +150,14 @@ func timeFromExcelTime(excelTime float64, date1904 bool) time.Time { } return date } - var floatPart = excelTime - float64(intPart) - var dayNanoSeconds float64 = 24 * 60 * 60 * 1000 * 1000 * 1000 + var floatPart = excelTime - float64(wholeDaysPart) if date1904 { - date = time.Date(1904, 1, 1, 0, 0, 0, 0, time.UTC) + date = excel1904Epoc } else { - date = time.Date(1899, 12, 30, 0, 0, 0, 0, time.UTC) + date = excel1900Epoc } - - // Duration is limited to aprox. 290 years - for intPart > MDD { - durationDays := time.Duration(MDD) * time.Hour * 24 - date = date.Add(durationDays) - intPart = intPart - MDD - } - durationDays := time.Duration(intPart) * time.Hour * 24 - durationPart := time.Duration(dayNanoSeconds * floatPart) - return date.Add(durationDays).Add(durationPart) + durationPart := time.Duration(nanosInADay * floatPart) + return date.AddDate(0, 0, wholeDaysPart).Add(durationPart) } // ExcelDateToTime converts a float-based excel date representation to a time.Time. diff --git a/sheet.go b/sheet.go index 7a1fff39c6..756eb81c3e 100644 --- a/sheet.go +++ b/sheet.go @@ -72,12 +72,13 @@ func (f *File) contentTypesReader() *xlsxTypes { if f.ContentTypes == nil { f.ContentTypes = new(xlsxTypes) + f.ContentTypes.Lock() + defer f.ContentTypes.Unlock() if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML("[Content_Types].xml")))). Decode(f.ContentTypes); err != nil && err != io.EOF { log.Printf("xml decode error: %s", err) } } - return f.ContentTypes } diff --git a/styles.go b/styles.go index 07ccab1d5b..2a99a4dce5 100644 --- a/styles.go +++ b/styles.go @@ -996,6 +996,7 @@ func parseTime(v string, format string) string { {"mm", "01"}, {"am/pm", "pm"}, {"m/", "1/"}, + {"m", "1"}, {"%%%%", "January"}, {"&&&&", "Monday"}, } @@ -1005,6 +1006,7 @@ func parseTime(v string, format string) string { {"\\ ", " "}, {"\\.", "."}, {"\\", ""}, + {"\"", ""}, } // It is the presence of the "am/pm" indicator that determines if this is // a 12 hour or 24 hours time format, not the number of 'h' characters. diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 4499546367..a54d51b73b 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -436,8 +436,8 @@ type DataValidation struct { ShowInputMessage bool `xml:"showInputMessage,attr,omitempty"` Sqref string `xml:"sqref,attr"` Type string `xml:"type,attr,omitempty"` - Formula1 string `xml:",innerxml"` - Formula2 string `xml:",innerxml"` + Formula1 string `xml:"formula1,omitempty"` + Formula2 string `xml:"formula2,omitempty"` } // xlsxC collection represents a cell in the worksheet. Information about the From 7ac37edfebebc9bee201fad001e2f2f8b780a9a8 Mon Sep 17 00:00:00 2001 From: Arnie97 Date: Sat, 31 Jul 2021 00:31:51 +0800 Subject: [PATCH 418/957] Fix data validation issues (#975) * Fix `SetDropList` to allow XML special characters * This closes #971, allow quotation marks in SetDropList() This patch included a XML entity mapping table instead of xml.EscapeText() to be fully compatible with Microsoft Excel. * This closes #972, allow more than 255 bytes of validation formulas This patch changed the string length calculation unit of data validation formulas from UTF-8 bytes to UTF-16 code units. * Add unit tests for SetDropList() * Fix: allow MaxFloat64 to be used in validation range 17 decimal significant digits should be more than enough to represent every IEEE-754 double-precision float number without losing precision, and numbers in this form will never reach the Excel limitation of 255 UTF-16 code units. --- datavalidation.go | 33 +++++++++++++++++------------- datavalidation_test.go | 46 ++++++++++++++++++++++++++++++++++++------ errors.go | 6 ++++++ xmlWorksheet.go | 4 ++-- 4 files changed, 67 insertions(+), 22 deletions(-) diff --git a/datavalidation.go b/datavalidation.go index a95f4d02c0..04dbe25319 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -13,6 +13,7 @@ package excelize import ( "fmt" + "math" "strings" "unicode/utf16" ) @@ -35,10 +36,8 @@ const ( ) const ( - // dataValidationFormulaStrLen 255 characters+ 2 quotes - dataValidationFormulaStrLen = 257 - // dataValidationFormulaStrLenErr - dataValidationFormulaStrLenErr = "data validation must be 0-255 characters" + // dataValidationFormulaStrLen 255 characters + dataValidationFormulaStrLen = 255 ) // DataValidationErrorStyle defined the style of data validation error alert. @@ -75,6 +74,15 @@ const ( DataValidationOperatorNotEqual ) +// formulaEscaper mimics the Excel escaping rules for data validation, +// which converts `"` to `""` instead of `"`. +var formulaEscaper = strings.NewReplacer( + `&`, `&`, + `<`, `<`, + `>`, `>`, + `"`, `""`, +) + // NewDataValidation return data validation struct. func NewDataValidation(allowBlank bool) *DataValidation { return &DataValidation{ @@ -111,25 +119,22 @@ func (dd *DataValidation) SetInput(title, msg string) { // SetDropList data validation list. func (dd *DataValidation) SetDropList(keys []string) error { - formula := "\"" + strings.Join(keys, ",") + "\"" + formula := strings.Join(keys, ",") if dataValidationFormulaStrLen < len(utf16.Encode([]rune(formula))) { - return fmt.Errorf(dataValidationFormulaStrLenErr) + return ErrDataValidationFormulaLenth } - dd.Formula1 = formula + dd.Formula1 = fmt.Sprintf(`"%s"`, formulaEscaper.Replace(formula)) dd.Type = convDataValidationType(typeList) return nil } // SetRange provides function to set data validation range in drop list. func (dd *DataValidation) SetRange(f1, f2 float64, t DataValidationType, o DataValidationOperator) error { - formula1 := fmt.Sprintf("%f", f1) - formula2 := fmt.Sprintf("%f", f2) - if dataValidationFormulaStrLen < len(utf16.Encode([]rune(dd.Formula1))) || dataValidationFormulaStrLen < len(utf16.Encode([]rune(dd.Formula2))) { - return fmt.Errorf(dataValidationFormulaStrLenErr) + if math.Abs(f1) > math.MaxFloat32 || math.Abs(f2) > math.MaxFloat32 { + return ErrDataValidationRange } - - dd.Formula1 = formula1 - dd.Formula2 = formula2 + dd.Formula1 = fmt.Sprintf("%.17g", f1) + dd.Formula2 = fmt.Sprintf("%.17g", f2) dd.Type = convDataValidationType(t) dd.Operator = convDataValidationOperatior(o) return nil diff --git a/datavalidation_test.go b/datavalidation_test.go index 5aea5832ec..13a0053c3a 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -10,6 +10,7 @@ package excelize import ( + "math" "path/filepath" "strings" "testing" @@ -40,7 +41,20 @@ func TestDataValidation(t *testing.T) { dvRange = NewDataValidation(true) dvRange.Sqref = "A5:B6" - assert.NoError(t, dvRange.SetDropList([]string{"1", "2", "3"})) + for _, listValid := range [][]string{ + {"1", "2", "3"}, + {strings.Repeat("&", 255)}, + {strings.Repeat("\u4E00", 255)}, + {strings.Repeat("\U0001F600", 100), strings.Repeat("\u4E01", 50), "<&>"}, + {`A<`, `B>`, `C"`, "D\t", `E'`, `F`}, + } { + dvRange.Formula1 = "" + assert.NoError(t, dvRange.SetDropList(listValid), + "SetDropList failed for valid input %v", listValid) + assert.NotEqual(t, "", dvRange.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.NoError(t, f.SaveAs(resultFile)) } @@ -62,7 +76,6 @@ func TestDataValidationError(t *testing.T) { assert.EqualError(t, err, "cross-sheet sqref cell are not supported") assert.NoError(t, f.AddDataValidation("Sheet1", dvRange)) - assert.NoError(t, f.SaveAs(resultFile)) dvRange = NewDataValidation(true) err = dvRange.SetDropList(make([]string, 258)) @@ -70,16 +83,37 @@ func TestDataValidationError(t *testing.T) { t.Errorf("data validation error. Formula1 must be empty!") return } - assert.EqualError(t, err, "data validation must be 0-255 characters") + assert.EqualError(t, err, ErrDataValidationFormulaLenth.Error()) assert.NoError(t, dvRange.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorGreaterThan)) dvRange.SetSqref("A9:B10") assert.NoError(t, f.AddDataValidation("Sheet1", dvRange)) - assert.NoError(t, f.SaveAs(resultFile)) // Test width invalid data validation formula. - dvRange.Formula1 = strings.Repeat("s", dataValidationFormulaStrLen+22) - assert.EqualError(t, dvRange.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorGreaterThan), "data validation must be 0-255 characters") + prevFormula1 := dvRange.Formula1 + for _, keys := range [][]string{ + make([]string, 257), + {strings.Repeat("s", 256)}, + {strings.Repeat("\u4E00", 256)}, + {strings.Repeat("\U0001F600", 128)}, + {strings.Repeat("\U0001F600", 127), "s"}, + } { + err = dvRange.SetDropList(keys) + assert.Equal(t, prevFormula1, dvRange.Formula1, + "Formula1 should be unchanged for invalid input %v", keys) + assert.EqualError(t, err, ErrDataValidationFormulaLenth.Error()) + } + assert.NoError(t, f.AddDataValidation("Sheet1", dvRange)) + assert.NoError(t, dvRange.SetRange( + -math.MaxFloat32, math.MaxFloat32, + DataValidationTypeWhole, DataValidationOperatorGreaterThan)) + assert.EqualError(t, dvRange.SetRange( + -math.MaxFloat64, math.MaxFloat32, + DataValidationTypeWhole, DataValidationOperatorGreaterThan), ErrDataValidationRange.Error()) + assert.EqualError(t, dvRange.SetRange( + math.SmallestNonzeroFloat64, math.MaxFloat64, + DataValidationTypeWhole, DataValidationOperatorGreaterThan), ErrDataValidationRange.Error()) + assert.NoError(t, f.SaveAs(resultFile)) // Test add data validation on no exists worksheet. f = NewFile() diff --git a/errors.go b/errors.go index 4931198eba..6b325636c5 100644 --- a/errors.go +++ b/errors.go @@ -105,4 +105,10 @@ var ( ErrSheetIdx = errors.New("invalid worksheet index") // ErrGroupSheets defined the error message on group sheets. ErrGroupSheets = errors.New("group worksheet must contain an active worksheet") + // ErrDataValidationFormulaLenth defined the error message for receiving a + // data validation formula length that exceeds the limit. + ErrDataValidationFormulaLenth = errors.New("data validation must be 0-255 characters") + // ErrDataValidationRange defined the error message on set decimal range + // exceeds limit. + ErrDataValidationRange = errors.New("data validation range exceeds limit") ) diff --git a/xmlWorksheet.go b/xmlWorksheet.go index a54d51b73b..4499546367 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -436,8 +436,8 @@ type DataValidation struct { ShowInputMessage bool `xml:"showInputMessage,attr,omitempty"` Sqref string `xml:"sqref,attr"` Type string `xml:"type,attr,omitempty"` - Formula1 string `xml:"formula1,omitempty"` - Formula2 string `xml:"formula2,omitempty"` + Formula1 string `xml:",innerxml"` + Formula2 string `xml:",innerxml"` } // xlsxC collection represents a cell in the worksheet. Information about the From eaf9781e7e51d5aacf238c9cb39a249097abed33 Mon Sep 17 00:00:00 2001 From: Arnie97 Date: Sat, 31 Jul 2021 14:20:29 +0800 Subject: [PATCH 419/957] Improve compatibility for SetRichText (#976) - support escaped string literal - maximum character limit added - fix missing preserve character in some case Co-authored-by: xuri --- cell.go | 34 +++++++++++++++++----------------- cell_test.go | 3 +++ errors.go | 5 ++++- 3 files changed, 24 insertions(+), 18 deletions(-) diff --git a/cell.go b/cell.go index 39990d488c..ad94038d82 100644 --- a/cell.go +++ b/cell.go @@ -307,16 +307,8 @@ func (f *File) setSharedString(val string) int { } sst.Count++ sst.UniqueCount++ - val = bstrMarshal(val) t := xlsxT{Val: val} - // Leading and ending space(s) character detection. - if len(val) > 0 && (val[0] == 32 || val[len(val)-1] == 32) { - ns := xml.Attr{ - Name: xml.Name{Space: NameSpaceXML, Local: "space"}, - Value: "preserve", - } - t.Space = ns - } + _, val, t.Space = setCellStr(val) sst.SI = append(sst.SI, xlsxSI{T: &t}) f.sharedStringsMap[val] = sst.UniqueCount - 1 return sst.UniqueCount - 1 @@ -327,11 +319,16 @@ func setCellStr(value string) (t string, v string, ns xml.Attr) { if len(value) > TotalCellChars { value = value[0:TotalCellChars] } - // Leading and ending space(s) character detection. - if len(value) > 0 && (value[0] == 32 || value[len(value)-1] == 32) { - ns = xml.Attr{ - Name: xml.Name{Space: NameSpaceXML, Local: "space"}, - Value: "preserve", + if len(value) > 0 { + prefix, suffix := value[0], value[len(value)-1] + for _, ascii := range []byte{10, 13, 32} { + if prefix == ascii || suffix == ascii { + ns = xml.Attr{ + Name: xml.Name{Space: NameSpaceXML, Local: "space"}, + Value: "preserve", + } + break + } } } t = "str" @@ -702,11 +699,14 @@ func (f *File) SetCellRichText(sheet, cell string, runs []RichTextRun) error { si := xlsxSI{} sst := f.sharedStringsReader() textRuns := []xlsxR{} + totalCellChars := 0 for _, textRun := range runs { - run := xlsxR{T: &xlsxT{Val: textRun.Text}} - if strings.ContainsAny(textRun.Text, "\r\n ") { - run.T.Space = xml.Attr{Name: xml.Name{Space: NameSpaceXML, Local: "space"}, Value: "preserve"} + 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 { rpr := xlsxRPr{} diff --git a/cell_test.go b/cell_test.go index ea62869619..3954438ebe 100644 --- a/cell_test.go +++ b/cell_test.go @@ -401,6 +401,9 @@ func TestSetCellRichText(t *testing.T) { assert.EqualError(t, f.SetCellRichText("SheetN", "A1", richTextRun), "sheet SheetN is not exist") // Test set cell rich text with illegal cell coordinates assert.EqualError(t, f.SetCellRichText("Sheet1", "A", richTextRun), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + richTextRun = []RichTextRun{{Text: strings.Repeat("s", TotalCellChars+1)}} + // 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) { diff --git a/errors.go b/errors.go index 6b325636c5..0edb697c21 100644 --- a/errors.go +++ b/errors.go @@ -45,7 +45,7 @@ var ( ErrColumnNumber = errors.New("column number exceeds maximum limit") // ErrColumnWidth defined the error message on receive an invalid column // width. - ErrColumnWidth = errors.New("the width of the column must be smaller than or equal to 255 characters") + ErrColumnWidth = fmt.Errorf("the width of the column must be smaller 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") @@ -111,4 +111,7 @@ var ( // ErrDataValidationRange defined the error message on set decimal range // exceeds limit. ErrDataValidationRange = errors.New("data validation range exceeds limit") + // ErrCellCharsLength defined the error message for receiving a cell + // characters length that exceeds the limit. + ErrCellCharsLength = fmt.Errorf("cell value must be 0-%d characters", TotalCellChars) ) From 933159f9391f9be1b41b51e85885722124f8a7aa Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 2 Aug 2021 00:00:26 +0800 Subject: [PATCH 420/957] Update dependencies module and bump version 2.4.1 --- README.md | 4 ++-- README_zh.md | 4 ++-- go.mod | 4 ++-- go.sum | 9 +++++---- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 3c49173c82..ac2b6c3ab4 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Build Status Code Coverage Go Report Card - go.dev + go.dev Licenses Donate

@@ -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 XLSX / XLSM / 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?tab=doc) 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 XLSX / XLSM / 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/). ## Basic Usage diff --git a/README_zh.md b/README_zh.md index 6015a44238..359192bc2e 100644 --- a/README_zh.md +++ b/README_zh.md @@ -4,7 +4,7 @@ Build Status Code Coverage Go Report Card - go.dev + go.dev Licenses Donate

@@ -13,7 +13,7 @@ ## 简介 -Excelize 是 Go 语言编写的用于操作 Office Excel 文档基础库,基于 ECMA-376,ISO/IEC 29500 国际标准。可以使用它来读取、写入由 Microsoft Excel™ 2007 及以上版本创建的电子表格文档。支持 XLSX / XLSM / XLTM / XLTX 等多种文档格式,高度兼容带有样式、图片(表)、透视表、切片器等复杂组件的文档,并提供流式读写 API,用于处理包含大规模数据的工作簿。可应用于各类报表平台、云计算、边缘计算等系统。使用本类库要求使用的 Go 语言为 1.15 或更高版本,完整的 API 使用文档请访问 [go.dev](https://pkg.go.dev/github.com/xuri/excelize/v2?tab=doc) 或查看 [参考文档](https://xuri.me/excelize/)。 +Excelize 是 Go 语言编写的用于操作 Office Excel 文档基础库,基于 ECMA-376,ISO/IEC 29500 国际标准。可以使用它来读取、写入由 Microsoft Excel™ 2007 及以上版本创建的电子表格文档。支持 XLSX / XLSM / XLTM / XLTX 等多种文档格式,高度兼容带有样式、图片(表)、透视表、切片器等复杂组件的文档,并提供流式读写 API,用于处理包含大规模数据的工作簿。可应用于各类报表平台、云计算、边缘计算等系统。使用本类库要求使用的 Go 语言为 1.15 或更高版本,完整的 API 使用文档请访问 [go.dev](https://pkg.go.dev/github.com/xuri/excelize/v2) 或查看 [参考文档](https://xuri.me/excelize/)。 ## 快速上手 diff --git a/go.mod b/go.mod index 877b924655..41f53a205d 100644 --- a/go.mod +++ b/go.mod @@ -7,8 +7,8 @@ require ( github.com/richardlehane/mscfb v1.0.3 github.com/stretchr/testify v1.6.1 github.com/xuri/efp v0.0.0-20210322160811-ab561f5b45e3 - golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a + golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb - golang.org/x/net v0.0.0-20210610132358-84b48f89b13b + golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985 golang.org/x/text v0.3.6 ) diff --git a/go.sum b/go.sum index 309a85b514..53c304719c 100644 --- a/go.sum +++ b/go.sum @@ -13,15 +13,16 @@ github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/xuri/efp v0.0.0-20210322160811-ab561f5b45e3 h1:EpI0bqf/eX9SdZDwlMmahKM+CDBgNbsXMhsN28XrM8o= github.com/xuri/efp v0.0.0-20210322160811-ab561f5b45e3/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= -golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a h1:kr2P4QFmQr29mSLA43kwrOcgcReGTfbE9N577tCTuBc= -golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb h1:fqpd0EBDzlHRCjiphRR5Zo/RSWWQlWv34418dnEixWk= golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210610132358-84b48f89b13b h1:k+E048sYJHyVnsr1GDrRZWQ32D2C7lWs9JRc0bel53A= -golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985 h1:4CSI6oo7cOjJKajidEljs9h+uP0rRZBPPPhcCbj5mw8= +golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 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/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 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= From cf9fbafdd805874267a0f5d27fd1c720b148ec91 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 6 Aug 2021 22:44:43 +0800 Subject: [PATCH 421/957] This closes #979, fix the data validation deletion issue and tidy the internal function in the source code --- adjust.go | 60 ----------------------- adjust_test.go | 17 ------- datavalidation.go | 51 ++++++++++++++++++- datavalidation_test.go | 28 ++++++++++- lib.go | 108 +++++++++++++++++++++++++++++++++++++++++ lib_test.go | 21 ++++++++ pivotTable.go | 11 ----- pivotTable_test.go | 4 -- 8 files changed, 206 insertions(+), 94 deletions(-) diff --git a/adjust.go b/adjust.go index ef7b19a7ad..1fe66634fe 100644 --- a/adjust.go +++ b/adjust.go @@ -11,10 +11,6 @@ package excelize -import ( - "strings" -) - type adjustDirection bool const ( @@ -194,62 +190,6 @@ func (f *File) adjustAutoFilterHelper(dir adjustDirection, coordinates []int, nu return coordinates } -// areaRefToCoordinates provides a function to convert area reference to a -// pair of coordinates. -func (f *File) areaRefToCoordinates(ref string) ([]int, error) { - rng := strings.Split(strings.Replace(ref, "$", "", -1), ":") - if len(rng) < 2 { - return nil, ErrParameterInvalid - } - - return areaRangeToCoordinates(rng[0], rng[1]) -} - -// areaRangeToCoordinates provides a function to convert cell range to a -// pair of coordinates. -func areaRangeToCoordinates(firstCell, lastCell string) ([]int, error) { - coordinates := make([]int, 4) - var err error - coordinates[0], coordinates[1], err = CellNameToCoordinates(firstCell) - if err != nil { - return coordinates, err - } - coordinates[2], coordinates[3], err = CellNameToCoordinates(lastCell) - return coordinates, err -} - -// sortCoordinates provides a function to correct the coordinate area, such -// correct C1:B3 to B1:C3. -func sortCoordinates(coordinates []int) error { - if len(coordinates) != 4 { - return ErrCoordinates - } - if coordinates[2] < coordinates[0] { - coordinates[2], coordinates[0] = coordinates[0], coordinates[2] - } - if coordinates[3] < coordinates[1] { - coordinates[3], coordinates[1] = coordinates[1], coordinates[3] - } - return nil -} - -// coordinatesToAreaRef provides a function to convert a pair of coordinates -// to area reference. -func (f *File) coordinatesToAreaRef(coordinates []int) (string, error) { - if len(coordinates) != 4 { - return "", ErrCoordinates - } - firstCell, err := CoordinatesToCellName(coordinates[0], coordinates[1]) - if err != nil { - return "", err - } - lastCell, err := CoordinatesToCellName(coordinates[2], coordinates[3]) - if err != nil { - return "", err - } - return firstCell + ":" + lastCell, err -} - // adjustMergeCells provides a function to update merged cells when inserting // or deleting rows or columns. func (f *File) adjustMergeCells(ws *xlsxWorksheet, dir adjustDirection, num, offset int) error { diff --git a/adjust_test.go b/adjust_test.go index c4af38b13a..ced091df03 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -99,20 +99,3 @@ func TestAdjustCalcChain(t *testing.T) { f.CalcChain = nil assert.NoError(t, f.InsertCol("Sheet1", "A")) } - -func TestCoordinatesToAreaRef(t *testing.T) { - f := NewFile() - _, 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]") - _, err = f.coordinatesToAreaRef([]int{1, 1, 1, -1}) - assert.EqualError(t, err, "invalid cell coordinates [1, -1]") - ref, err := f.coordinatesToAreaRef([]int{1, 1, 1, 1}) - assert.NoError(t, err) - assert.EqualValues(t, ref, "A1:A1") -} - -func TestSortCoordinates(t *testing.T) { - assert.EqualError(t, sortCoordinates(make([]int, 3)), ErrCoordinates.Error()) -} diff --git a/datavalidation.go b/datavalidation.go index 04dbe25319..d44d2b8f48 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -258,9 +258,30 @@ func (f *File) DeleteDataValidation(sheet, sqref string) error { if ws.DataValidations == nil { return nil } + delCells, err := f.flatSqref(sqref) + if err != nil { + return err + } dv := ws.DataValidations for i := 0; i < len(dv.DataValidation); i++ { - if dv.DataValidation[i].Sqref == sqref { + applySqref := []string{} + colCells, err := f.flatSqref(dv.DataValidation[i].Sqref) + if err != nil { + return err + } + for col, cells := range delCells { + for _, cell := range cells { + idx := inCoordinates(colCells[col], cell) + if idx != -1 { + colCells[col] = append(colCells[col][:idx], colCells[col][idx+1:]...) + } + } + } + for _, col := range colCells { + applySqref = append(applySqref, f.squashSqref(col)...) + } + dv.DataValidation[i].Sqref = strings.Join(applySqref, " ") + if len(applySqref) == 0 { dv.DataValidation = append(dv.DataValidation[:i], dv.DataValidation[i+1:]...) i-- } @@ -271,3 +292,31 @@ func (f *File) DeleteDataValidation(sheet, sqref string) error { } return nil } + +// squashSqref generates cell reference sequence by given cells coordinates list. +func (f *File) squashSqref(cells [][]int) []string { + if len(cells) == 1 { + cell, _ := CoordinatesToCellName(cells[0][0], cells[0][1]) + return []string{cell} + } else if len(cells) == 0 { + return []string{} + } + l, r, res := 0, 0, []string{} + 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]...)) + if l == r { + curr, _ = CoordinatesToCellName(cells[l][0], cells[l][1]) + } + res = append(res, curr) + l, r = i, i + } else { + r++ + } + } + curr, _ := f.coordinatesToAreaRef(append(cells[l], cells[r]...)) + if l == r { + curr, _ = CoordinatesToCellName(cells[l][0], cells[l][1]) + } + return append(res, curr) +} diff --git a/datavalidation_test.go b/datavalidation_test.go index 13a0053c3a..f0afe5f143 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -129,10 +129,36 @@ func TestDeleteDataValidation(t *testing.T) { assert.NoError(t, dvRange.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorBetween)) dvRange.SetInput("input title", "input body") assert.NoError(t, f.AddDataValidation("Sheet1", dvRange)) - assert.NoError(t, f.DeleteDataValidation("Sheet1", "A1:B2")) + + dvRange.Sqref = "A1" + assert.NoError(t, f.AddDataValidation("Sheet1", dvRange)) + 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)) + 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)) + 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)) + assert.EqualError(t, f.DeleteDataValidation("Sheet1", "A1"), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + + assert.EqualError(t, f.DeleteDataValidation("Sheet1", "A1:A"), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") + assert.True(t, ok) + ws.(*xlsxWorksheet).DataValidations.DataValidation[0].Sqref = "A1:A" + assert.EqualError(t, f.DeleteDataValidation("Sheet1", "A1:B2"), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + // Test delete data validation on no exists worksheet. assert.EqualError(t, f.DeleteDataValidation("SheetN", "A1:B2"), "sheet SheetN is not exist") } diff --git a/lib.go b/lib.go index df2af4a903..7db14c4cd7 100644 --- a/lib.go +++ b/lib.go @@ -219,6 +219,114 @@ 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 +// pair of coordinates. +func (f *File) areaRefToCoordinates(ref string) ([]int, error) { + rng := strings.Split(strings.Replace(ref, "$", "", -1), ":") + if len(rng) < 2 { + return nil, ErrParameterInvalid + } + + return areaRangeToCoordinates(rng[0], rng[1]) +} + +// areaRangeToCoordinates provides a function to convert cell range to a +// pair of coordinates. +func areaRangeToCoordinates(firstCell, lastCell string) ([]int, error) { + coordinates := make([]int, 4) + var err error + coordinates[0], coordinates[1], err = CellNameToCoordinates(firstCell) + if err != nil { + return coordinates, err + } + coordinates[2], coordinates[3], err = CellNameToCoordinates(lastCell) + return coordinates, err +} + +// sortCoordinates provides a function to correct the coordinate area, such +// correct C1:B3 to B1:C3. +func sortCoordinates(coordinates []int) error { + if len(coordinates) != 4 { + return ErrCoordinates + } + if coordinates[2] < coordinates[0] { + coordinates[2], coordinates[0] = coordinates[0], coordinates[2] + } + if coordinates[3] < coordinates[1] { + coordinates[3], coordinates[1] = coordinates[1], coordinates[3] + } + return nil +} + +// coordinatesToAreaRef provides a function to convert a pair of coordinates +// to area reference. +func (f *File) coordinatesToAreaRef(coordinates []int) (string, error) { + if len(coordinates) != 4 { + return "", ErrCoordinates + } + firstCell, err := CoordinatesToCellName(coordinates[0], coordinates[1]) + if err != nil { + return "", err + } + lastCell, err := CoordinatesToCellName(coordinates[2], coordinates[3]) + if err != nil { + return "", err + } + return firstCell + ":" + lastCell, err +} + +// flatSqref convert reference sequence to cell coordinates list. +func (f *File) flatSqref(sqref string) (cells map[int][][]int, err error) { + var coordinates []int + cells = make(map[int][][]int) + for _, ref := range strings.Fields(sqref) { + rng := strings.Split(ref, ":") + switch len(rng) { + case 1: + var col, row int + col, row, err = CellNameToCoordinates(rng[0]) + if err != nil { + return + } + cells[col] = append(cells[col], []int{col, row}) + case 2: + if coordinates, err = f.areaRefToCoordinates(ref); err != nil { + return + } + _ = sortCoordinates(coordinates) + for c := coordinates[0]; c <= coordinates[2]; c++ { + for r := coordinates[1]; r <= coordinates[3]; r++ { + cells[c] = append(cells[c], []int{c, r}) + } + } + } + } + return +} + +// inCoordinates provides a method to check if an coordinate is present in +// coordinates array, and return the index of its location, otherwise +// return -1. +func inCoordinates(a [][]int, x []int) int { + for idx, n := range a { + if x[0] == n[0] && x[1] == n[1] { + return idx + } + } + return -1 +} + +// inStrSlice provides a method to check if an element is present in an array, +// and return the index of its location, otherwise return -1. +func inStrSlice(a []string, x string) int { + for idx, n := range a { + if x == n { + return idx + } + } + return -1 +} + // boolPtr returns a pointer to a bool with the given value. func boolPtr(b bool) *bool { return &b } diff --git a/lib_test.go b/lib_test.go index 315688fc17..2e0e5066dc 100644 --- a/lib_test.go +++ b/lib_test.go @@ -211,6 +211,27 @@ func TestCoordinatesToCellName_Error(t *testing.T) { } } +func TestCoordinatesToAreaRef(t *testing.T) { + f := NewFile() + _, 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]") + _, err = f.coordinatesToAreaRef([]int{1, 1, 1, -1}) + assert.EqualError(t, err, "invalid cell coordinates [1, -1]") + ref, err := f.coordinatesToAreaRef([]int{1, 1, 1, 1}) + assert.NoError(t, err) + assert.EqualValues(t, ref, "A1:A1") +} + +func TestSortCoordinates(t *testing.T) { + assert.EqualError(t, sortCoordinates(make([]int, 3)), ErrCoordinates.Error()) +} + +func TestInStrSlice(t *testing.T) { + assert.EqualValues(t, -1, inStrSlice([]string{}, "")) +} + func TestBytesReplace(t *testing.T) { s := []byte{0x01} assert.EqualValues(t, s, bytesReplace(s, []byte{}, []byte{}, 0)) diff --git a/pivotTable.go b/pivotTable.go index f6d6d2dfdf..07cf84cf71 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -459,17 +459,6 @@ func (f *File) addPivotDataFields(pt *xlsxPivotTableDefinition, opt *PivotTableO return err } -// inStrSlice provides a method to check if an element is present in an array, -// and return the index of its location, otherwise return -1. -func inStrSlice(a []string, x string) int { - for idx, n := range a { - if x == n { - return idx - } - } - return -1 -} - // inPivotTableField provides a method to check if an element is present in // pivot table fields list, and return the index of its location, otherwise // return -1. diff --git a/pivotTable_test.go b/pivotTable_test.go index bf6bb01eb5..dbb82523c7 100644 --- a/pivotTable_test.go +++ b/pivotTable_test.go @@ -301,10 +301,6 @@ func TestGetPivotFieldsOrder(t *testing.T) { assert.EqualError(t, err, "sheet SheetN is not exist") } -func TestInStrSlice(t *testing.T) { - assert.EqualValues(t, -1, inStrSlice([]string{}, "")) -} - func TestGetPivotTableFieldName(t *testing.T) { f := NewFile() f.getPivotTableFieldName("-", []PivotTableField{}) From c49e7aab306437f0e721620af4a24364edf4d601 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 9 Aug 2021 22:22:43 +0800 Subject: [PATCH 422/957] Reduce cyclomatic complexities for the formula calculate function and update documentation for the API: `MergeCell` and `GetCellValue` --- calc.go | 281 ++++++++++++++++++++++++++++++--------------------- cell.go | 3 +- lib_test.go | 4 + merge.go | 4 +- sparkline.go | 7 +- 5 files changed, 178 insertions(+), 121 deletions(-) diff --git a/calc.go b/calc.go index 3bb81b89f4..18605db417 100644 --- a/calc.go +++ b/calc.go @@ -4240,17 +4240,42 @@ func (fn *formulaFuncs) STDEVA(argsList *list.List) formulaArg { return fn.stdev(true, argsList) } -// stdev is an implementation of the formula function STDEV and STDEVA. -func (fn *formulaFuncs) stdev(stdeva bool, argsList *list.List) formulaArg { - pow := func(result, count float64, n, m formulaArg) (float64, float64) { - if result == -1 { - result = math.Pow((n.Number - m.Number), 2) - } else { - result += math.Pow((n.Number - m.Number), 2) +// 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) + } else { + result += math.Pow((n.Number - m.Number), 2) + } + count++ + return result, count +} + +// calcStdev is part of the implementation stdev. +func calcStdev(stdeva bool, result, count float64, mean, token formulaArg) (float64, float64) { + for _, row := range token.ToList() { + if row.Type == ArgNumber || row.Type == ArgString { + if !stdeva && (row.Value() == "TRUE" || row.Value() == "FALSE") { + continue + } else if stdeva && (row.Value() == "TRUE" || row.Value() == "FALSE") { + num := row.ToBool() + if num.Type == ArgNumber { + result, count = calcStdevPow(result, count, num, mean) + continue + } + } else { + num := row.ToNumber() + if num.Type == ArgNumber { + result, count = calcStdevPow(result, count, num, mean) + } + } } - count++ - return result, count } + return result, count +} + +// stdev is an implementation of the formula function STDEV and STDEVA. +func (fn *formulaFuncs) stdev(stdeva bool, argsList *list.List) formulaArg { count, result := -1.0, -1.0 var mean formulaArg if stdeva { @@ -4267,34 +4292,17 @@ func (fn *formulaFuncs) stdev(stdeva bool, argsList *list.List) formulaArg { } else if stdeva && (token.Value() == "TRUE" || token.Value() == "FALSE") { num := token.ToBool() if num.Type == ArgNumber { - result, count = pow(result, count, num, mean) + result, count = calcStdevPow(result, count, num, mean) continue } } else { num := token.ToNumber() if num.Type == ArgNumber { - result, count = pow(result, count, num, mean) + result, count = calcStdevPow(result, count, num, mean) } } case ArgList, ArgMatrix: - for _, row := range token.ToList() { - if row.Type == ArgNumber || row.Type == ArgString { - if !stdeva && (row.Value() == "TRUE" || row.Value() == "FALSE") { - continue - } else if stdeva && (row.Value() == "TRUE" || row.Value() == "FALSE") { - num := row.ToBool() - if num.Type == ArgNumber { - result, count = pow(result, count, num, mean) - continue - } - } else { - num := row.ToNumber() - if num.Type == ArgNumber { - result, count = pow(result, count, num, mean) - } - } - } - } + result, count = calcStdev(stdeva, result, count, mean, token) } } if count > 0 && result >= 0 { @@ -4568,6 +4576,18 @@ func (fn *formulaFuncs) AVERAGEA(argsList *list.List) formulaArg { return newNumberFormulaArg(sum / count) } +// calcStringCountSum is part of the implementation countSum. +func calcStringCountSum(countText bool, count, sum float64, num, arg formulaArg) (float64, float64) { + if countText && num.Type == ArgError && arg.String != "" { + count++ + } + if num.Type == ArgNumber { + sum += num.Number + count++ + } + return count, sum +} + // countSum get count and sum for a formula arguments array. func (fn *formulaFuncs) countSum(countText bool, args []formulaArg) (count, sum float64) { for _, arg := range args { @@ -4589,13 +4609,7 @@ func (fn *formulaFuncs) countSum(countText bool, args []formulaArg) (count, sum } } num := arg.ToNumber() - if countText && num.Type == ArgError && arg.String != "" { - count++ - } - if num.Type == ArgNumber { - sum += num.Number - count++ - } + count, sum = calcStringCountSum(countText, count, sum, num, arg) case ArgList, ArgMatrix: cnt, summary := fn.countSum(countText, arg.ToList()) sum += summary @@ -5148,6 +5162,33 @@ func (fn *formulaFuncs) MAXA(argsList *list.List) formulaArg { return fn.max(true, argsList) } +// 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 + } + } + } + return max +} + // max is an implementation of the formula function MAX and MAXA. func (fn *formulaFuncs) max(maxa bool, argsList *list.List) formulaArg { max := -math.MaxFloat64 @@ -5173,28 +5214,7 @@ func (fn *formulaFuncs) max(maxa bool, argsList *list.List) formulaArg { max = arg.Number } case ArgList, ArgMatrix: - 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 - } - } - } + max = calcListMatrixMax(maxa, max, arg) case ArgError: return arg } @@ -5277,6 +5297,33 @@ func (fn *formulaFuncs) MINA(argsList *list.List) formulaArg { return fn.min(true, argsList) } +// 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 + } + } + } + return min +} + // min is an implementation of the formula function MIN and MINA. func (fn *formulaFuncs) min(mina bool, argsList *list.List) formulaArg { min := math.MaxFloat64 @@ -5302,28 +5349,7 @@ func (fn *formulaFuncs) min(mina bool, argsList *list.List) formulaArg { min = arg.Number } case ArgList, ArgMatrix: - 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 - } - } - } + min = calcListMatrixMin(mina, min, arg) case ArgError: return arg } @@ -6930,16 +6956,9 @@ func (fn *formulaFuncs) COLUMN(argsList *list.List) formulaArg { return newNumberFormulaArg(float64(col)) } -// 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) -// -func (fn *formulaFuncs) COLUMNS(argsList *list.List) formulaArg { - if argsList.Len() != 1 { - return newErrorFormulaArg(formulaErrorVALUE, "COLUMNS requires 1 argument") - } - var min, max int +// calcColumnsMinMax calculation min and max value for given formula arguments +// sequence of the formula function COLUMNS. +func calcColumnsMinMax(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() { @@ -6974,6 +6993,19 @@ func (fn *formulaFuncs) COLUMNS(argsList *list.List) formulaArg { } } } + return +} + +// 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) +// +func (fn *formulaFuncs) COLUMNS(argsList *list.List) formulaArg { + if argsList.Len() != 1 { + return newErrorFormulaArg(formulaErrorVALUE, "COLUMNS requires 1 argument") + } + min, max := calcColumnsMinMax(argsList) if max == TotalColumns { return newNumberFormulaArg(float64(TotalColumns)) } @@ -7272,16 +7304,9 @@ func (fn *formulaFuncs) ROW(argsList *list.List) formulaArg { return newNumberFormulaArg(float64(row)) } -// 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) -// -func (fn *formulaFuncs) ROWS(argsList *list.List) formulaArg { - if argsList.Len() != 1 { - return newErrorFormulaArg(formulaErrorVALUE, "ROWS requires 1 argument") - } - var min, max int +// 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() { @@ -7316,6 +7341,19 @@ func (fn *formulaFuncs) ROWS(argsList *list.List) formulaArg { } } } + 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: +// +// ROWS(array) +// +func (fn *formulaFuncs) ROWS(argsList *list.List) formulaArg { + if argsList.Len() != 1 { + return newErrorFormulaArg(formulaErrorVALUE, "ROWS requires 1 argument") + } + min, max := calcRowsMinMax(argsList) if max == TotalRows { return newStringFormulaArg(strconv.Itoa(TotalRows)) } @@ -7419,6 +7457,11 @@ func (fn *formulaFuncs) cumip(name string, argsList *list.List) formulaArg { return newNumberFormulaArg(num) } +// 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) +} + // DB function calculates the depreciation of an asset, using the Fixed // Declining Balance Method, for each period of the asset's lifetime. The // syntax of the function is: @@ -7457,7 +7500,7 @@ func (fn *formulaFuncs) DB(argsList *list.List) formulaArg { if cost.Number == 0 { return newNumberFormulaArg(0) } - if (cost.Number <= 0) || ((salvage.Number / cost.Number) < 0) || (life.Number <= 0) || (period.Number < 1) || (month.Number < 1) { + if calcDbArgsCompare(cost, salvage, life, period) || (month.Number < 1) { return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) } dr := 1 - math.Pow(salvage.Number/cost.Number, 1/life.Number) @@ -7514,7 +7557,7 @@ func (fn *formulaFuncs) DDB(argsList *list.List) formulaArg { if cost.Number == 0 { return newNumberFormulaArg(0) } - if (cost.Number <= 0) || ((salvage.Number / cost.Number) < 0) || (life.Number <= 0) || (period.Number < 1) || (factor.Number <= 0.0) || (period.Number > life.Number) { + if calcDbArgsCompare(cost, salvage, life, period) || (factor.Number <= 0.0) || (period.Number > life.Number) { return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) } pd, depreciation := 0.0, 0.0 @@ -7680,6 +7723,24 @@ func (fn *formulaFuncs) IPMT(argsList *list.List) formulaArg { return fn.ipmt("IPMT", argsList) } +// calcIpmt is part of the implementation ipmt. +func calcIpmt(name string, typ, per, pmt, pv, rate formulaArg) formulaArg { + capital, interest, principal := pv.Number, 0.0, 0.0 + for i := 1; i <= int(per.Number); i++ { + if typ.Number != 0 && i == 1 { + interest = 0 + } else { + interest = -capital * rate.Number + } + principal = pmt.Number - interest + capital += principal + } + if name == "IPMT" { + return newNumberFormulaArg(interest) + } + return newNumberFormulaArg(principal) +} + // ipmt is an implementation of the formula function IPMT and PPMT. func (fn *formulaFuncs) ipmt(name string, argsList *list.List) formulaArg { if argsList.Len() < 4 { @@ -7727,20 +7788,8 @@ func (fn *formulaFuncs) ipmt(name string, argsList *list.List) formulaArg { args.PushBack(pv) args.PushBack(fv) args.PushBack(typ) - pmt, capital, interest, principal := fn.PMT(args), pv.Number, 0.0, 0.0 - for i := 1; i <= int(per.Number); i++ { - if typ.Number != 0 && i == 1 { - interest = 0 - } else { - interest = -capital * rate.Number - } - principal = pmt.Number - interest - capital += principal - } - if name == "IPMT" { - return newNumberFormulaArg(interest) - } - return newNumberFormulaArg(principal) + pmt := fn.PMT(args) + return calcIpmt(name, typ, per, pmt, pv, rate) } // IRR function returns the Internal Rate of Return for a supplied series of diff --git a/cell.go b/cell.go index ad94038d82..f44e877a6b 100644 --- a/cell.go +++ b/cell.go @@ -34,7 +34,8 @@ const ( // 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. +// returned, along with the raw value of the cell. All cells value will be +// same in a merged range. func (f *File) GetCellValue(sheet, axis string) (string, error) { return f.getCellStringFunc(sheet, axis, func(x *xlsxWorksheet, c *xlsxC) (string, bool, error) { val, err := c.getValueFrom(f, f.sharedStringsReader()) diff --git a/lib_test.go b/lib_test.go index 2e0e5066dc..025bc85018 100644 --- a/lib_test.go +++ b/lib_test.go @@ -237,6 +237,10 @@ func TestBytesReplace(t *testing.T) { assert.EqualValues(t, s, bytesReplace(s, []byte{}, []byte{}, 0)) } +func TestGetRootElement(t *testing.T) { + assert.Equal(t, 0, len(getRootElement(xml.NewDecoder(strings.NewReader(""))))) +} + func TestSetIgnorableNameSpace(t *testing.T) { f := NewFile() f.xmlAttr["xml_path"] = []xml.Attr{{}} diff --git a/merge.go b/merge.go index 1f8974ed3d..7769b89104 100644 --- a/merge.go +++ b/merge.go @@ -17,7 +17,9 @@ import ( ) // MergeCell provides a function to merge cells by given coordinate area and -// sheet name. For example create a merged cell of D3:E9 on Sheet1: +// 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: // // err := f.MergeCell("Sheet1", "D3", "E9") // diff --git a/sparkline.go b/sparkline.go index 5326c60c52..917383d9e4 100644 --- a/sparkline.go +++ b/sparkline.go @@ -524,10 +524,11 @@ func (f *File) appendSparkline(ws *xlsxWorksheet, group *xlsxX14SparklineGroup, if sparklineGroupBytes, err = xml.Marshal(group); err != nil { return } - groups = &xlsxX14SparklineGroups{ - XMLNSXM: NameSpaceSpreadSheetExcel2006Main.Value, - Content: decodeSparklineGroups.Content + string(sparklineGroupBytes), + if groups == nil { + groups = &xlsxX14SparklineGroups{} } + groups.XMLNSXM = NameSpaceSpreadSheetExcel2006Main.Value + groups.Content = decodeSparklineGroups.Content + string(sparklineGroupBytes) if sparklineGroupsBytes, err = xml.Marshal(groups); err != nil { return } From 43a057b1eaeb810495618d70c2dc2d3e5df1fea4 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 12 Aug 2021 00:02:27 +0800 Subject: [PATCH 423/957] This closes #986, fix set data validation drop list failed in some cases Update documentation for `GetCellValue` and simplify code --- cell.go | 4 ++-- datavalidation.go | 2 +- picture.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cell.go b/cell.go index f44e877a6b..9200d13e44 100644 --- a/cell.go +++ b/cell.go @@ -34,8 +34,8 @@ const ( // 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 value will be -// same in a merged range. +// returned, along with the raw value of the cell. All cells' values will be +// the same in a merged range. func (f *File) GetCellValue(sheet, axis string) (string, error) { return f.getCellStringFunc(sheet, axis, func(x *xlsxWorksheet, c *xlsxC) (string, bool, error) { val, err := c.getValueFrom(f, f.sharedStringsReader()) diff --git a/datavalidation.go b/datavalidation.go index d44d2b8f48..e182ebe108 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -154,7 +154,7 @@ func (dd *DataValidation) SetRange(f1, f2 float64, t DataValidationType, o DataV // func (dd *DataValidation) SetSqrefDropList(sqref string, isCurrentSheet bool) error { if isCurrentSheet { - dd.Formula1 = sqref + dd.Formula1 = fmt.Sprintf("%s", sqref) dd.Type = convDataValidationType(typeList) return nil } diff --git a/picture.go b/picture.go index 0531272279..d22a7081a5 100644 --- a/picture.go +++ b/picture.go @@ -622,7 +622,7 @@ func (f *File) drawingsWriter() { } // drawingResize calculate the height and width after resizing. -func (f *File) drawingResize(sheet string, cell string, width, height float64, formatSet *formatPicture) (w, h, c, r int, err error) { +func (f *File) drawingResize(sheet, cell string, width, height float64, formatSet *formatPicture) (w, h, c, r int, err error) { var mergeCells []MergeCell mergeCells, err = f.GetMergeCells(sheet) if err != nil { From 61d0ed1ff26fbe47b4bfdc6adbc6db09743beb3a Mon Sep 17 00:00:00 2001 From: bailantaotao Date: Thu, 12 Aug 2021 14:53:59 +0800 Subject: [PATCH 424/957] This closes #987: support nested calc for if formula (#988) --- calc.go | 21 +++++++++++++++++---- calc_test.go | 2 ++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/calc.go b/calc.go index 18605db417..cd7fa971fc 100644 --- a/calc.go +++ b/calc.go @@ -6746,7 +6746,7 @@ func (fn *formulaFuncs) IF(argsList *list.List) formulaArg { var ( cond bool err error - result string + result formulaArg ) switch token.Type { case ArgString: @@ -6757,13 +6757,26 @@ func (fn *formulaFuncs) IF(argsList *list.List) formulaArg { return newBoolFormulaArg(cond) } if cond { - return newStringFormulaArg(argsList.Front().Next().Value.(formulaArg).String) + value := argsList.Front().Next().Value.(formulaArg) + switch value.Type { + case ArgNumber: + result = value.ToNumber() + default: + result = newStringFormulaArg(value.String) + } + return result } if argsList.Len() == 3 { - result = argsList.Back().Value.(formulaArg).String + value := argsList.Back().Value.(formulaArg) + switch value.Type { + case ArgNumber: + result = value.ToNumber() + default: + result = newStringFormulaArg(value.String) + } } } - return newStringFormulaArg(result) + return result } // Lookup and Reference Functions diff --git a/calc_test.go b/calc_test.go index 20505fcba4..54bdc0156f 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1090,6 +1090,8 @@ func TestCalcCellValue(t *testing.T) { `=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", // Excel Lookup and Reference Functions // CHOOSE "=CHOOSE(4,\"red\",\"blue\",\"green\",\"brown\")": "brown", From f6f14f507ee1adf4883cb1b12f27932a63afb286 Mon Sep 17 00:00:00 2001 From: three Date: Fri, 13 Aug 2021 01:32:44 +0800 Subject: [PATCH 425/957] Speed up merge cells --- adjust.go | 8 +- adjust_test.go | 4 + calc.go | 12 +-- cell.go | 31 +++++-- col.go | 6 +- lib.go | 5 +- merge.go | 217 ++++++++++++++++++++++++++++++-------------- merge_test.go | 32 +++++-- picture.go | 2 + pivotTable.go | 2 +- rows.go | 4 +- sheet.go | 3 + xmlSharedStrings.go | 22 +---- xmlWorksheet.go | 7 +- 14 files changed, 226 insertions(+), 129 deletions(-) diff --git a/adjust.go b/adjust.go index 1fe66634fe..9f2176feb6 100644 --- a/adjust.go +++ b/adjust.go @@ -145,7 +145,7 @@ func (f *File) adjustAutoFilter(ws *xlsxWorksheet, dir adjustDirection, num, off return nil } - coordinates, err := f.areaRefToCoordinates(ws.AutoFilter.Ref) + coordinates, err := areaRefToCoordinates(ws.AutoFilter.Ref) if err != nil { return err } @@ -199,7 +199,7 @@ 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 := f.areaRefToCoordinates(areaData.Ref) + coordinates, err := areaRefToCoordinates(areaData.Ref) if err != nil { return err } @@ -219,7 +219,7 @@ func (f *File) adjustMergeCells(ws *xlsxWorksheet, dir adjustDirection, num, off x1 = f.adjustMergeCellsHelper(x1, num, offset) x2 = f.adjustMergeCellsHelper(x2, num, offset) } - if x1 == x2 && y1 == y2 { + if x1 == x2 && y1 == y2 && i >= 0 { f.deleteMergeCell(ws, i) i-- } @@ -234,7 +234,7 @@ func (f *File) adjustMergeCells(ws *xlsxWorksheet, dir adjustDirection, num, off // compare and calculate cell axis by the given pivot, operation axis and // offset. func (f *File) adjustMergeCellsHelper(pivot, num, offset int) int { - if pivot >= num { + if pivot > num { pivot += offset if pivot < 1 { return 1 diff --git a/adjust_test.go b/adjust_test.go index ced091df03..f56f7631ba 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -84,6 +84,10 @@ func TestAdjustHelper(t *testing.T) { assert.EqualError(t, f.adjustHelper("SheetN", rows, 0, 0), "sheet SheetN is not exist") } +func TestAdjustMergeCellsHelper(t *testing.T) { + assert.Equal(t, 1, NewFile().adjustMergeCellsHelper(1, 0, -2)) +} + func TestAdjustCalcChain(t *testing.T) { f := NewFile() f.CalcChain = &xlsxCalcChain{ diff --git a/calc.go b/calc.go index cd7fa971fc..a03520bef1 100644 --- a/calc.go +++ b/calc.go @@ -5103,11 +5103,11 @@ func (fn *formulaFuncs) kth(name string, argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires 2 arguments", name)) } array := argsList.Front().Value.(formulaArg).ToList() - kArg := argsList.Back().Value.(formulaArg).ToNumber() - if kArg.Type != ArgNumber { - return kArg + argK := argsList.Back().Value.(formulaArg).ToNumber() + if argK.Type != ArgNumber { + return argK } - k := int(kArg.Number) + k := int(argK.Number) if k < 1 { return newErrorFormulaArg(formulaErrorNUM, "k should be > 0") } @@ -7177,7 +7177,7 @@ func (fn *formulaFuncs) VLOOKUP(argsList *list.List) formulaArg { func vlookupBinarySearch(tableArray, lookupValue formulaArg) (matchIdx int, wasExact bool) { var low, high, lastMatchIdx int = 0, len(tableArray.Matrix) - 1, -1 for low <= high { - var mid int = low + (high-low)/2 + mid := low + (high-low)/2 mtx := tableArray.Matrix[mid] lhs := mtx[0] switch lookupValue.Type { @@ -7216,7 +7216,7 @@ func vlookupBinarySearch(tableArray, lookupValue formulaArg) (matchIdx int, wasE func hlookupBinarySearch(row []formulaArg, lookupValue formulaArg) (matchIdx int, wasExact bool) { var low, high, lastMatchIdx int = 0, len(row) - 1, -1 for low <= high { - var mid int = low + (high-low)/2 + mid := low + (high-low)/2 mtx := row[mid] result := compareFormulaArg(mtx, lookupValue, false, false) if result == criteriaEq { diff --git a/cell.go b/cell.go index 9200d13e44..6ad5f44457 100644 --- a/cell.go +++ b/cell.go @@ -101,6 +101,28 @@ func (f *File) SetCellValue(sheet, axis string, value interface{}) error { return err } +// 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) + } + } + return bstrUnmarshal(rows.String()) + } + if x.T != nil { + return bstrUnmarshal(x.T.Val) + } + return "" +} + +// hasValue determine if cell non-blank value. +func (c *xlsxC) hasValue() bool { + return c.S != 0 || c.V != "" || c.F != nil || c.T != "" +} + // setCellIntFunc is a wrapper of SetCellInt. func (f *File) setCellIntFunc(sheet, axis string, value interface{}) error { var err error @@ -431,13 +453,11 @@ func (f *File) GetCellHyperLink(sheet, axis string) (bool, string, error) { if _, _, err := SplitCellName(axis); err != nil { return false, "", err } - ws, err := f.workSheetReader(sheet) if err != nil { return false, "", err } - axis, err = f.mergeCellsParser(ws, axis) - if err != nil { + if axis, err = f.mergeCellsParser(ws, axis); err != nil { return false, "", err } if ws.Hyperlinks != nil { @@ -485,8 +505,7 @@ func (f *File) SetCellHyperLink(sheet, axis, link, linkType string, opts ...Hype if err != nil { return err } - axis, err = f.mergeCellsParser(ws, axis) - if err != nil { + if axis, err = f.mergeCellsParser(ws, axis); err != nil { return err } @@ -932,7 +951,7 @@ func (f *File) checkCellInArea(cell, area string) (bool, error) { if len(rng) != 2 { return false, err } - coordinates, err := f.areaRefToCoordinates(area) + coordinates, err := areaRefToCoordinates(area) if err != nil { return false, err } diff --git a/col.go b/col.go index 088fac9919..7fbeebab33 100644 --- a/col.go +++ b/col.go @@ -616,10 +616,10 @@ func (f *File) positionObjectPixels(sheet string, col, row, x1, y1, width, heigh // getColWidth provides a function to get column width in pixels by given // sheet name and column number. func (f *File) getColWidth(sheet string, col int) int { - xlsx, _ := f.workSheetReader(sheet) - if xlsx.Cols != nil { + ws, _ := f.workSheetReader(sheet) + if ws.Cols != nil { var width float64 - for _, v := range xlsx.Cols.Col { + for _, v := range ws.Cols.Col { if v.Min <= col && col <= v.Max { width = v.Width } diff --git a/lib.go b/lib.go index 7db14c4cd7..912f7381e8 100644 --- a/lib.go +++ b/lib.go @@ -221,12 +221,11 @@ func CoordinatesToCellName(col, row int, abs ...bool) (string, error) { // areaRefToCoordinates provides a function to convert area reference to a // pair of coordinates. -func (f *File) areaRefToCoordinates(ref string) ([]int, error) { +func areaRefToCoordinates(ref string) ([]int, error) { rng := strings.Split(strings.Replace(ref, "$", "", -1), ":") if len(rng) < 2 { return nil, ErrParameterInvalid } - return areaRangeToCoordinates(rng[0], rng[1]) } @@ -290,7 +289,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 = f.areaRefToCoordinates(ref); err != nil { + if coordinates, err = areaRefToCoordinates(ref); err != nil { return } _ = sortCoordinates(coordinates) diff --git a/merge.go b/merge.go index 7769b89104..1cd8acd92c 100644 --- a/merge.go +++ b/merge.go @@ -11,10 +11,16 @@ package excelize -import ( - "fmt" - "strings" -) +import "strings" + +// Rect gets merged cell rectangle coordinates sequence. +func (mc *xlsxMergeCell) Rect() ([]int, error) { + var err error + if mc.rect == nil { + mc.rect, err = areaRefToCoordinates(mc.Ref) + } + return mc.rect, err +} // MergeCell provides a function to merge cells by given coordinate area and // sheet name. Merging cells only keeps the upper-left cell value, and @@ -24,7 +30,9 @@ import ( // 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. +// 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) // +------------------------+ @@ -39,15 +47,15 @@ import ( // +------------------------+ // func (f *File) MergeCell(sheet, hcell, vcell string) error { - rect1, err := f.areaRefToCoordinates(hcell + ":" + vcell) + rect, err := areaRefToCoordinates(hcell + ":" + vcell) if err != nil { return err } // Correct the coordinate area, such correct C1:B3 to B1:C3. - _ = sortCoordinates(rect1) + _ = sortCoordinates(rect) - hcell, _ = CoordinatesToCellName(rect1[0], rect1[1]) - vcell, _ = CoordinatesToCellName(rect1[2], rect1[3]) + hcell, _ = CoordinatesToCellName(rect[0], rect[1]) + vcell, _ = CoordinatesToCellName(rect[2], rect[3]) ws, err := f.workSheetReader(sheet) if err != nil { @@ -55,49 +63,9 @@ func (f *File) MergeCell(sheet, hcell, vcell string) error { } ref := hcell + ":" + vcell if ws.MergeCells != nil { - for i := 0; i < len(ws.MergeCells.Cells); i++ { - cellData := ws.MergeCells.Cells[i] - if cellData == nil { - continue - } - cc := strings.Split(cellData.Ref, ":") - if len(cc) != 2 { - return fmt.Errorf("invalid area %q", cellData.Ref) - } - - rect2, err := f.areaRefToCoordinates(cellData.Ref) - if err != nil { - return err - } - - // Delete the merged cells of the overlapping area. - if isOverlap(rect1, rect2) { - ws.MergeCells.Cells = append(ws.MergeCells.Cells[:i], ws.MergeCells.Cells[i+1:]...) - i-- - - if rect1[0] > rect2[0] { - rect1[0], rect2[0] = rect2[0], rect1[0] - } - - if rect1[2] < rect2[2] { - rect1[2], rect2[2] = rect2[2], rect1[2] - } - - if rect1[1] > rect2[1] { - rect1[1], rect2[1] = rect2[1], rect1[1] - } - - if rect1[3] < rect2[3] { - rect1[3], rect2[3] = rect2[3], rect1[3] - } - hcell, _ = CoordinatesToCellName(rect1[0], rect1[1]) - vcell, _ = CoordinatesToCellName(rect1[2], rect1[3]) - ref = hcell + ":" + vcell - } - } - ws.MergeCells.Cells = append(ws.MergeCells.Cells, &xlsxMergeCell{Ref: ref}) + ws.MergeCells.Cells = append(ws.MergeCells.Cells, &xlsxMergeCell{Ref: ref, rect: rect}) } else { - ws.MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: ref}}} + ws.MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: ref, rect: rect}}} } ws.MergeCells.Count = len(ws.MergeCells.Cells) return err @@ -114,7 +82,7 @@ func (f *File) UnmergeCell(sheet string, hcell, vcell string) error { if err != nil { return err } - rect1, err := f.areaRefToCoordinates(hcell + ":" + vcell) + rect1, err := areaRefToCoordinates(hcell + ":" + vcell) if err != nil { return err } @@ -126,26 +94,19 @@ func (f *File) UnmergeCell(sheet string, hcell, vcell string) error { if ws.MergeCells == nil { return nil } - + if err = f.mergeOverlapCells(ws); err != nil { + return err + } i := 0 - for _, cellData := range ws.MergeCells.Cells { - if cellData == nil { + for _, mergeCell := range ws.MergeCells.Cells { + if mergeCell == nil { continue } - cc := strings.Split(cellData.Ref, ":") - if len(cc) != 2 { - return fmt.Errorf("invalid area %q", cellData.Ref) - } - - rect2, err := f.areaRefToCoordinates(cellData.Ref) - if err != nil { - return err - } - + rect2, _ := areaRefToCoordinates(mergeCell.Ref) if isOverlap(rect1, rect2) { continue } - ws.MergeCells.Cells[i] = cellData + ws.MergeCells.Cells[i] = mergeCell i++ } ws.MergeCells.Cells = ws.MergeCells.Cells[:i] @@ -165,8 +126,10 @@ func (f *File) GetMergeCells(sheet string) ([]MergeCell, error) { return mergeCells, err } if ws.MergeCells != nil { + if err = f.mergeOverlapCells(ws); err != nil { + return mergeCells, err + } 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] @@ -174,10 +137,128 @@ func (f *File) GetMergeCells(sheet string) ([]MergeCell, error) { mergeCells = append(mergeCells, []string{ref, val}) } } - return mergeCells, err } +// overlapRange calculate overlap range of merged cells, and returns max +// column and rows of the range. +func overlapRange(ws *xlsxWorksheet) (row, col int, err error) { + var rect []int + for _, mergeCell := range ws.MergeCells.Cells { + if mergeCell == nil { + continue + } + if rect, err = mergeCell.Rect(); err != nil { + return + } + x1, y1, x2, y2 := rect[0], rect[1], rect[2], rect[3] + if x1 > col { + col = x1 + } + if x2 > col { + col = x2 + } + if y1 > row { + row = y1 + } + if y2 > row { + row = y2 + } + } + return +} + +// flatMergedCells convert merged cells range reference to cell-matrix. +func flatMergedCells(ws *xlsxWorksheet, matrix [][]*xlsxMergeCell) error { + for i, cell := range ws.MergeCells.Cells { + rect, err := cell.Rect() + if err != nil { + return err + } + x1, y1, x2, y2 := rect[0]-1, rect[1]-1, rect[2]-1, rect[3]-1 + var overlapCells []*xlsxMergeCell + for x := x1; x <= x2; x++ { + for y := y1; y <= y2; y++ { + if matrix[x][y] != nil { + overlapCells = append(overlapCells, matrix[x][y]) + } + matrix[x][y] = cell + } + } + if len(overlapCells) != 0 { + newCell := cell + for _, overlapCell := range overlapCells { + newCell = mergeCell(cell, overlapCell) + } + newRect, _ := newCell.Rect() + x1, y1, x2, y2 := newRect[0]-1, newRect[1]-1, newRect[2]-1, newRect[3]-1 + for x := x1; x <= x2; x++ { + for y := y1; y <= y2; y++ { + matrix[x][y] = newCell + } + } + ws.MergeCells.Cells[i] = newCell + } + } + return nil +} + +// mergeOverlapCells merge overlap cells. +func (f *File) mergeOverlapCells(ws *xlsxWorksheet) error { + rows, cols, err := overlapRange(ws) + if err != nil { + return err + } + if rows == 0 || cols == 0 { + return nil + } + matrix := make([][]*xlsxMergeCell, cols) + for i := range matrix { + matrix[i] = make([]*xlsxMergeCell, rows) + } + _ = flatMergedCells(ws, matrix) + mergeCells := ws.MergeCells.Cells[:0] + for _, cell := range ws.MergeCells.Cells { + rect, _ := cell.Rect() + x1, y1, x2, y2 := rect[0]-1, rect[1]-1, rect[2]-1, rect[3]-1 + if matrix[x1][y1] == cell { + mergeCells = append(mergeCells, cell) + for x := x1; x <= x2; x++ { + for y := y1; y <= y2; y++ { + matrix[x][y] = nil + } + } + } + } + ws.MergeCells.Count, ws.MergeCells.Cells = len(mergeCells), mergeCells + return nil +} + +// mergeCell merge two cells. +func mergeCell(cell1, cell2 *xlsxMergeCell) *xlsxMergeCell { + rect1, _ := cell1.Rect() + rect2, _ := cell2.Rect() + + if rect1[0] > rect2[0] { + rect1[0], rect2[0] = rect2[0], rect1[0] + } + + if rect1[2] < rect2[2] { + rect1[2], rect2[2] = rect2[2], rect1[2] + } + + if rect1[1] > rect2[1] { + rect1[1], rect2[1] = rect2[1], rect1[1] + } + + if rect1[3] < rect2[3] { + rect1[3], rect2[3] = rect2[3], rect1[3] + } + hcell, _ := CoordinatesToCellName(rect1[0], rect1[1]) + vcell, _ := CoordinatesToCellName(rect1[2], rect1[3]) + return &xlsxMergeCell{rect: rect1, Ref: hcell + ":" + vcell} +} + // MergeCell define a merged cell data. // It consists of the following structure. // example: []string{"D4:E10", "cell value"} diff --git a/merge_test.go b/merge_test.go index 41c122cb53..a370126574 100644 --- a/merge_test.go +++ b/merge_test.go @@ -27,7 +27,7 @@ func TestMergeCell(t *testing.T) { 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") - assert.Equal(t, "0.5", value) + assert.Equal(t, "100", value) assert.NoError(t, err) value, err = f.GetCellValue("Sheet2", "A6") // Merged cell ref is single coordinate. assert.Equal(t, "", value) @@ -75,16 +75,24 @@ func TestMergeCell(t *testing.T) { assert.True(t, ok) ws.(*xlsxWorksheet).MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{nil, nil}} assert.NoError(t, f.MergeCell("Sheet1", "A2", "B3")) +} - 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.MergeCell("Sheet1", "A2", "B3"), `invalid area "A1"`) +func TestMergeCellOverlap(t *testing.T) { + f := NewFile() + assert.NoError(t, f.MergeCell("Sheet1", "A1", "C2")) + assert.NoError(t, f.MergeCell("Sheet1", "B2", "D3")) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestMergeCellOverlap.xlsx"))) - 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.MergeCell("Sheet1", "A2", "B3"), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + f, err := OpenFile(filepath.Join("test", "TestMergeCellOverlap.xlsx")) + if !assert.NoError(t, err) { + t.FailNow() + } + mc, err := f.GetMergeCells("Sheet1") + assert.NoError(t, err) + assert.Equal(t, 1, len(mc)) + assert.Equal(t, "A1", mc[0].GetStartAxis()) + assert.Equal(t, "D3", mc[0].GetEndAxis()) + assert.Equal(t, "", mc[0].GetCellValue()) } func TestGetMergeCells(t *testing.T) { @@ -173,11 +181,15 @@ 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"), `invalid area "A1"`) + assert.EqualError(t, f.UnmergeCell("Sheet1", "A2", "B3"), "parameter is invalid") 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.UnmergeCell("Sheet1", "A2", "B3"), `cannot convert cell "A" to coordinates: invalid cell name "A"`) +} +func TestFlatMergedCells(t *testing.T) { + ws := &xlsxWorksheet{MergeCells: &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A1"}}}} + assert.EqualError(t, flatMergedCells(ws, [][]*xlsxMergeCell{}), "parameter is invalid") } diff --git a/picture.go b/picture.go index d22a7081a5..c37899e0ad 100644 --- a/picture.go +++ b/picture.go @@ -148,6 +148,7 @@ func (f *File) AddPictureFromBytes(sheet, cell, format, name, extension string, if err != nil { return err } + ws.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" @@ -162,6 +163,7 @@ func (f *File) AddPictureFromBytes(sheet, cell, format, name, extension string, } drawingHyperlinkRID = f.addRels(drawingRels, SourceRelationshipHyperLink, formatSet.Hyperlink, hyperlinkType) } + ws.Unlock() err = f.addDrawingPicture(sheet, drawingXML, cell, name, img.Width, img.Height, drawingRID, drawingHyperlinkRID, formatSet) if err != nil { return err diff --git a/pivotTable.go b/pivotTable.go index 07cf84cf71..0b0e5af6d5 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -198,7 +198,7 @@ func (f *File) adjustRange(rangeStr string) (string, []int, error) { return "", []int{}, ErrParameterInvalid } trimRng := strings.Replace(rng[1], "$", "", -1) - coordinates, err := f.areaRefToCoordinates(trimRng) + coordinates, err := areaRefToCoordinates(trimRng) if err != nil { return rng[0], []int{}, err } diff --git a/rows.go b/rows.go index fcd3c1ad2e..5fa2cb45ec 100644 --- a/rows.go +++ b/rows.go @@ -614,7 +614,7 @@ func (f *File) duplicateMergeCells(sheet string, ws *xlsxWorksheet, row, row2 in row++ } for _, rng := range ws.MergeCells.Cells { - coordinates, err := f.areaRefToCoordinates(rng.Ref) + coordinates, err := areaRefToCoordinates(rng.Ref) if err != nil { return err } @@ -624,7 +624,7 @@ 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, _ := f.areaRefToCoordinates(areaData.Ref) + coordinates, _ := areaRefToCoordinates(areaData.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/sheet.go b/sheet.go index 756eb81c3e..1c4b355937 100644 --- a/sheet.go +++ b/sheet.go @@ -158,6 +158,9 @@ func (f *File) workSheetWriter() { f.Sheet.Range(func(p, ws interface{}) bool { if ws != nil { sheet := ws.(*xlsxWorksheet) + if sheet.MergeCells != nil && len(sheet.MergeCells.Cells) > 0 { + _ = f.mergeOverlapCells(sheet) + } for k, v := range sheet.SheetData.Row { sheet.SheetData.Row[k].C = trimCell(v.C) } diff --git a/xmlSharedStrings.go b/xmlSharedStrings.go index 816c931383..e505d262dc 100644 --- a/xmlSharedStrings.go +++ b/xmlSharedStrings.go @@ -11,10 +11,7 @@ package excelize -import ( - "encoding/xml" - "strings" -) +import "encoding/xml" // xlsxSST directly maps the sst element from the namespace // http://schemas.openxmlformats.org/spreadsheetml/2006/main. String values may @@ -44,23 +41,6 @@ type xlsxSI struct { PhoneticPr *xlsxPhoneticPr `xml:"phoneticPr"` } -// 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) - } - } - return bstrUnmarshal(rows.String()) - } - if x.T != nil { - return bstrUnmarshal(x.T.Val) - } - return "" -} - // xlsxR represents a run of rich text. A rich text run is a region of text // that share a common set of properties, such as formatting properties. The // properties are defined in the rPr element, and the text displayed to the diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 4499546367..697504e6ce 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -399,7 +399,8 @@ type xlsxCustomSheetView struct { // xlsxMergeCell directly maps the mergeCell element. A single merged cell. type xlsxMergeCell struct { - Ref string `xml:"ref,attr,omitempty"` + Ref string `xml:"ref,attr,omitempty"` + rect []int } // xlsxMergeCells directly maps the mergeCells element. This collection @@ -468,10 +469,6 @@ type xlsxC struct { IS *xlsxSI `xml:"is"` } -func (c *xlsxC) hasValue() bool { - return c.S != 0 || c.V != "" || c.F != nil || c.T != "" -} - // xlsxF represents a formula for the cell. The formula expression is // contained in the character node of this element. type xlsxF struct { From 48c16de8bf74df0fa94a30d29e2e7e3446d48433 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 15 Aug 2021 00:06:40 +0800 Subject: [PATCH 426/957] Improve security and simplify code - Make variable name more semantic - Reduce cyclomatic complexities for the formula calculate function - Support specified unzip size limit on open file options, avoid zip bombs vulnerability attack - Typo fix for documentation and error message --- calc.go | 59 +++++++++++++++++++++++++----------------- comment_test.go | 8 +++--- crypt.go | 10 ++++--- crypt_test.go | 8 +++--- datavalidation_test.go | 8 +++--- date_test.go | 2 +- errors.go | 12 ++++++++- errors_test.go | 2 +- excelize.go | 44 +++++++++++++++++++------------ excelize_test.go | 15 ++++++----- file.go | 16 +++++++----- file_test.go | 12 +++------ lib.go | 28 ++++++++++++-------- merge_test.go | 6 ++--- picture.go | 14 +++++----- picture_test.go | 10 +++---- sheet.go | 8 +++--- sheetpr.go | 10 +++---- stream.go | 4 +-- xmlChartSheet.go | 8 +++--- xmlDrawing.go | 1 + 21 files changed, 165 insertions(+), 120 deletions(-) diff --git a/calc.go b/calc.go index a03520bef1..eb6ff7597b 100644 --- a/calc.go +++ b/calc.go @@ -6026,6 +6026,39 @@ func (fn *formulaFuncs) DATE(argsList *list.List) formulaArg { return newStringFormulaArg(timeFromExcelTime(daysBetween(excelMinTime1900.Unix(), d)+1, false).String()) } +// calcDateDif is an implementation of the formula function DATEDIF, +// calculation difference between two dates. +func calcDateDif(unit string, diff float64, seq []int, startArg, endArg formulaArg) float64 { + ey, sy, em, sm, ed, sd := seq[0], seq[1], seq[2], seq[3], seq[4], seq[5] + switch unit { + case "d": + diff = endArg.Number - startArg.Number + case "md": + smMD := em + if ed < sd { + smMD-- + } + diff = endArg.Number - daysBetween(excelMinTime1900.Unix(), makeDate(ey, time.Month(smMD), sd)) - 1 + case "ym": + diff = float64(em - sm) + if ed < sd { + diff-- + } + if diff < 0 { + diff += 12 + } + case "yd": + syYD := sy + if em < sm || (em == sm && ed < sd) { + syYD++ + } + s := daysBetween(excelMinTime1900.Unix(), makeDate(syYD, time.Month(em), ed)) + e := daysBetween(excelMinTime1900.Unix(), makeDate(sy, time.Month(sm), sd)) + diff = s - e + } + return diff +} + // DATEDIF function calculates the number of days, months, or years between // two dates. The syntax of the function is: // @@ -6051,8 +6084,6 @@ func (fn *formulaFuncs) DATEDIF(argsList *list.List) formulaArg { ey, emm, ed := endDate.Date() sm, em, diff := int(smm), int(emm), 0.0 switch unit { - case "d": - return newNumberFormulaArg(endArg.Number - startArg.Number) case "y": diff = float64(ey - sy) if em < sm || (em == sm && ed < sd) { @@ -6069,28 +6100,8 @@ func (fn *formulaFuncs) DATEDIF(argsList *list.List) formulaArg { mdiff += 12 } diff = float64(ydiff*12 + mdiff) - case "md": - smMD := em - if ed < sd { - smMD-- - } - diff = endArg.Number - daysBetween(excelMinTime1900.Unix(), makeDate(ey, time.Month(smMD), sd)) - 1 - case "ym": - diff = float64(em - sm) - if ed < sd { - diff-- - } - if diff < 0 { - diff += 12 - } - case "yd": - syYD := sy - if em < sm || (em == sm && ed < sd) { - syYD++ - } - s := daysBetween(excelMinTime1900.Unix(), makeDate(syYD, time.Month(em), ed)) - e := daysBetween(excelMinTime1900.Unix(), makeDate(sy, time.Month(sm), sd)) - diff = s - e + case "d", "md", "ym", "yd": + diff = calcDateDif(unit, diff, []int{ey, sy, em, sm, ed, sd}, startArg, endArg) default: return newErrorFormulaArg(formulaErrorVALUE, "DATEDIF has invalid unit") } diff --git a/comment_test.go b/comment_test.go index f1b60dc6c6..fb36d29065 100644 --- a/comment_test.go +++ b/comment_test.go @@ -3,9 +3,11 @@ // the LICENSE file. // // Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.15 or later. +// and read from XLSX / XLSM / XLTM 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. package excelize diff --git a/crypt.go b/crypt.go index a0096c92e4..24ac7eccf5 100644 --- a/crypt.go +++ b/crypt.go @@ -3,9 +3,11 @@ // the LICENSE file. // // Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.15 or later. +// and read from XLSX / XLSM / XLTM 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. package excelize @@ -15,6 +17,7 @@ import ( "crypto/cipher" "crypto/hmac" "crypto/md5" + "crypto/rand" "crypto/sha1" "crypto/sha256" "crypto/sha512" @@ -22,7 +25,6 @@ import ( "encoding/binary" "encoding/xml" "hash" - "math/rand" "reflect" "strings" diff --git a/crypt_test.go b/crypt_test.go index 6a882e5d73..68ff5b85a8 100644 --- a/crypt_test.go +++ b/crypt_test.go @@ -3,9 +3,11 @@ // the LICENSE file. // // Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.15 or later. +// and read from XLSX / XLSM / XLTM 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. package excelize diff --git a/datavalidation_test.go b/datavalidation_test.go index f0afe5f143..0cb5929849 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -3,9 +3,11 @@ // the LICENSE file. // // Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.15 or later. +// and read from XLSX / XLSM / XLTM 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. package excelize diff --git a/date_test.go b/date_test.go index 38898b0124..2addc4ab99 100644 --- a/date_test.go +++ b/date_test.go @@ -85,5 +85,5 @@ func TestExcelDateToTime(t *testing.T) { } // Check error case _, err := ExcelDateToTime(-1, false) - assert.EqualError(t, err, "invalid date value -1.000000, negative values are not supported supported") + assert.EqualError(t, err, "invalid date value -1.000000, negative values are not supported") } diff --git a/errors.go b/errors.go index 0edb697c21..aee442009e 100644 --- a/errors.go +++ b/errors.go @@ -16,26 +16,36 @@ import ( "fmt" ) +// newInvalidColumnNameError defined the error message on receiving the invalid column name. func newInvalidColumnNameError(col string) error { return fmt.Errorf("invalid column name %q", col) } +// newInvalidRowNumberError defined the error message on receiving the invalid row number. func newInvalidRowNumberError(row int) error { return fmt.Errorf("invalid row number %d", row) } +// newInvalidCellNameError defined the error message on receiving the invalid cell name. func newInvalidCellNameError(cell string) error { return fmt.Errorf("invalid cell name %q", cell) } +// newInvalidExcelDateError defined the error message on receiving the data with negative values. func newInvalidExcelDateError(dateValue float64) error { - return fmt.Errorf("invalid date value %f, negative values are not supported supported", dateValue) + return fmt.Errorf("invalid date value %f, negative values are not supported", dateValue) } +// newUnsupportChartType defined the error message on receiving the chart type are unsupported. func newUnsupportChartType(chartType string) error { return fmt.Errorf("unsupported chart type %s", chartType) } +// newUnzipSizeLimitError defined the error message on unzip size exceeds the limit. +func newUnzipSizeLimitError(unzipSizeLimit int64) error { + return fmt.Errorf("unzip size exceeds the %d bytes limit", unzipSizeLimit) +} + var ( // ErrStreamSetColWidth defined the error message on set column width in // stream writing mode. diff --git a/errors_test.go b/errors_test.go index 207e80aabb..971802f79f 100644 --- a/errors_test.go +++ b/errors_test.go @@ -21,5 +21,5 @@ func TestNewInvalidCellNameError(t *testing.T) { } func TestNewInvalidExcelDateError(t *testing.T) { - assert.EqualError(t, newInvalidExcelDateError(-1), "invalid date value -1.000000, negative values are not supported supported") + assert.EqualError(t, newInvalidExcelDateError(-1), "invalid date value -1.000000, negative values are not supported") } diff --git a/excelize.go b/excelize.go index 0091c820c8..fafa57fa3b 100644 --- a/excelize.go +++ b/excelize.go @@ -21,6 +21,7 @@ import ( "io/ioutil" "os" "path" + "path/filepath" "strconv" "strings" "sync" @@ -59,21 +60,27 @@ type charsetTranscoderFn func(charset string, input io.Reader) (rdr io.Reader, e // Options define the options for open spreadsheet. type Options struct { - Password string + Password string + UnzipSizeLimit int64 } -// OpenFile take the name of an spreadsheet file and returns a populated spreadsheet file struct -// for it. For example, open spreadsheet with password protection: +// OpenFile take the name of an spreadsheet file and returns a populated +// 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 // } // -// Note that the excelize just support decrypt and not support encrypt currently, the spreadsheet -// saved by Save and SaveAs will be without password unprotected. +// Note that the excelize just support decrypt and not support encrypt +// currently, the spreadsheet saved by Save and SaveAs will be without +// password unprotected. +// +// UnzipSizeLimit specified the unzip size limit in bytes on open the +// spreadsheet, the default size limit is 16GB. func OpenFile(filename string, opt ...Options) (*File, error) { - file, err := os.Open(filename) + file, err := os.Open(filepath.Clean(filename)) if err != nil { return nil, err } @@ -89,6 +96,7 @@ func OpenFile(filename string, opt ...Options) (*File, error) { // newFile is object builder func newFile() *File { return &File{ + options: &Options{UnzipSizeLimit: UnzipSizeLimit}, xmlAttr: make(map[string][]xml.Attr), checked: make(map[string]bool), sheetMap: make(map[string]string), @@ -111,10 +119,13 @@ func OpenReader(r io.Reader, opt ...Options) (*File, error) { return nil, err } f := newFile() - if bytes.Contains(b, oleIdentifier) && len(opt) > 0 { - for _, o := range opt { - f.options = &o + for i := range opt { + f.options = &opt[i] + if f.options.UnzipSizeLimit == 0 { + f.options.UnzipSizeLimit = UnzipSizeLimit } + } + if bytes.Contains(b, oleIdentifier) { b, err = Decrypt(b, f.options) if err != nil { return nil, fmt.Errorf("decrypted file failed") @@ -124,8 +135,7 @@ func OpenReader(r io.Reader, opt ...Options) (*File, error) { if err != nil { return nil, err } - - file, sheetCount, err := ReadZipReader(zr) + file, sheetCount, err := ReadZipReader(zr, f.options) if err != nil { return nil, err } @@ -316,18 +326,18 @@ func (f *File) UpdateLinkedValue() error { // recalculate formulas wb.CalcPr = nil for _, name := range f.GetSheetList() { - xlsx, err := f.workSheetReader(name) + ws, err := f.workSheetReader(name) if err != nil { if err.Error() == fmt.Sprintf("sheet %s is chart sheet", trimSheetName(name)) { continue } return err } - for indexR := range xlsx.SheetData.Row { - for indexC, col := range xlsx.SheetData.Row[indexR].C { + for indexR := range ws.SheetData.Row { + for indexC, col := range ws.SheetData.Row[indexR].C { if col.F != nil && col.V != "" { - xlsx.SheetData.Row[indexR].C[indexC].V = "" - xlsx.SheetData.Row[indexR].C[indexC].T = "" + ws.SheetData.Row[indexR].C[indexC].V = "" + ws.SheetData.Row[indexR].C[indexC].T = "" } } } @@ -381,7 +391,7 @@ func (f *File) AddVBAProject(bin string) error { Type: SourceRelationshipVBAProject, }) } - file, _ := ioutil.ReadFile(bin) + file, _ := ioutil.ReadFile(filepath.Clean(bin)) f.Pkg.Store("xl/vbaProject.bin", file) return err } diff --git a/excelize_test.go b/excelize_test.go index cc3a1b2d06..918279bfa4 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -184,13 +184,9 @@ func TestSaveFile(t *testing.T) { func TestSaveAsWrongPath(t *testing.T) { f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) - if assert.NoError(t, err) { - // Test write file to not exist directory. - err = f.SaveAs("") - if assert.Error(t, err) { - assert.True(t, os.IsNotExist(err), "Error: %v: Expected os.IsNotExists(err) == true", err) - } - } + assert.NoError(t, err) + // Test write file to not exist directory. + assert.EqualError(t, f.SaveAs(""), "open .: is a directory") } func TestCharsetTranscoder(t *testing.T) { @@ -204,6 +200,10 @@ func TestOpenReader(t *testing.T) { _, err = OpenReader(bytes.NewReader(oleIdentifier), Options{Password: "password"}) assert.EqualError(t, err, "decrypted file failed") + // 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. f, err := OpenFile(filepath.Join("test", "encryptSHA1.xlsx"), Options{Password: "password"}) assert.NoError(t, err) @@ -1226,6 +1226,7 @@ func TestWorkSheetReader(t *testing.T) { 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") // Test on no checked worksheet. f = NewFile() diff --git a/file.go b/file.go index abb03053ba..bfb6abfb13 100644 --- a/file.go +++ b/file.go @@ -17,6 +17,7 @@ import ( "fmt" "io" "os" + "path/filepath" "sync" ) @@ -69,14 +70,14 @@ func (f *File) SaveAs(name string, opt ...Options) error { return ErrMaxFileNameLength } f.Path = name - file, err := os.OpenFile(name, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0666) + file, err := os.OpenFile(filepath.Clean(name), os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0600) if err != nil { return err } defer file.Close() f.options = nil - for _, o := range opt { - f.options = &o + for i := range opt { + f.options = &opt[i] } return f.Write(file) } @@ -102,7 +103,8 @@ func (f *File) WriteTo(w io.Writer) (int64, error) { return 0, nil } -// WriteToBuffer provides a function to get bytes.Buffer from the saved file. And it allocate space in memory. Be careful when the file size is large. +// WriteToBuffer provides a function to get bytes.Buffer from the saved file, +// and it allocates space in memory. Be careful when the file size is large. func (f *File) WriteToBuffer() (*bytes.Buffer, error) { buf := new(bytes.Buffer) zw := zip.NewWriter(buf) @@ -130,7 +132,7 @@ func (f *File) WriteToBuffer() (*bytes.Buffer, error) { func (f *File) writeDirectToWriter(w io.Writer) error { zw := zip.NewWriter(w) if err := f.writeToZip(zw); err != nil { - zw.Close() + _ = zw.Close() return err } return zw.Close() @@ -157,14 +159,14 @@ func (f *File) writeToZip(zw *zip.Writer) error { var from io.Reader from, err = stream.rawData.Reader() if err != nil { - stream.rawData.Close() + _ = stream.rawData.Close() return err } _, err = io.Copy(fi, from) if err != nil { return err } - stream.rawData.Close() + _ = stream.rawData.Close() } var err error f.Pkg.Range(func(path, content interface{}) bool { diff --git a/file_test.go b/file_test.go index d86ce535c4..956ff92ebd 100644 --- a/file_test.go +++ b/file_test.go @@ -37,9 +37,7 @@ func BenchmarkWrite(b *testing.B) { func TestWriteTo(t *testing.T) { // Test WriteToBuffer err { - f := File{} - buf := bytes.Buffer{} - f.Pkg = sync.Map{} + f, buf := File{Pkg: sync.Map{}}, bytes.Buffer{} f.Pkg.Store("/d/", []byte("s")) _, err := f.WriteTo(bufio.NewWriter(&buf)) assert.EqualError(t, err, "zip: write to directory") @@ -47,9 +45,7 @@ func TestWriteTo(t *testing.T) { } // Test file path overflow { - f := File{} - buf := bytes.Buffer{} - f.Pkg = sync.Map{} + f, buf := File{Pkg: sync.Map{}}, bytes.Buffer{} const maxUint16 = 1<<16 - 1 f.Pkg.Store(strings.Repeat("s", maxUint16+1), nil) _, err := f.WriteTo(bufio.NewWriter(&buf)) @@ -57,9 +53,7 @@ func TestWriteTo(t *testing.T) { } // Test StreamsWriter err { - f := File{} - buf := bytes.Buffer{} - f.Pkg = sync.Map{} + f, buf := File{Pkg: sync.Map{}}, bytes.Buffer{} f.Pkg.Store("s", nil) f.streams = make(map[string]*StreamWriter) file, _ := os.Open("123") diff --git a/lib.go b/lib.go index 912f7381e8..712576de21 100644 --- a/lib.go +++ b/lib.go @@ -26,15 +26,22 @@ import ( // ReadZipReader can be used to read the spreadsheet in memory without touching the // filesystem. -func ReadZipReader(r *zip.Reader) (map[string][]byte, int, error) { - var err error - var docPart = map[string]string{ - "[content_types].xml": "[Content_Types].xml", - "xl/sharedstrings.xml": "xl/sharedStrings.xml", - } - fileList := make(map[string][]byte, len(r.File)) - worksheets := 0 +func ReadZipReader(r *zip.Reader, o *Options) (map[string][]byte, int, error) { + var ( + err error + docPart = map[string]string{ + "[content_types].xml": "[Content_Types].xml", + "xl/sharedstrings.xml": "xl/sharedStrings.xml", + } + fileList = make(map[string][]byte, len(r.File)) + worksheets int + unzipSize int64 + ) for _, v := range r.File { + unzipSize += v.FileInfo().Size() + if unzipSize > o.UnzipSizeLimit { + return fileList, worksheets, newUnzipSizeLimitError(o.UnzipSizeLimit) + } fileName := strings.Replace(v.Name, "\\", "/", -1) if partName, ok := docPart[strings.ToLower(fileName)]; ok { fileName = partName @@ -61,7 +68,7 @@ func (f *File) readXML(name string) []byte { } // saveFileList provides a function to update given file content in file list -// of XLSX. +// of spreadsheet. func (f *File) saveFileList(name string, content []byte) { f.Pkg.Store(name, append([]byte(XMLHeader), content...)) } @@ -75,8 +82,7 @@ func readFile(file *zip.File) ([]byte, error) { dat := make([]byte, 0, file.FileInfo().Size()) buff := bytes.NewBuffer(dat) _, _ = io.Copy(buff, rc) - rc.Close() - return buff.Bytes(), nil + return buff.Bytes(), rc.Close() } // SplitCellName splits cell name to column name and row number. diff --git a/merge_test.go b/merge_test.go index a370126574..02d92fbdef 100644 --- a/merge_test.go +++ b/merge_test.go @@ -148,16 +148,16 @@ func TestUnmergeCell(t *testing.T) { } sheet1 := f.GetSheetName(0) - xlsx, err := f.workSheetReader(sheet1) + sheet, err := f.workSheetReader(sheet1) assert.NoError(t, err) - mergeCellNum := len(xlsx.MergeCells.Cells) + mergeCellNum := len(sheet.MergeCells.Cells) assert.EqualError(t, f.UnmergeCell("Sheet1", "A", "A"), `cannot convert cell "A" to coordinates: invalid cell name "A"`) // unmerge the mergecell that contains A1 assert.NoError(t, f.UnmergeCell(sheet1, "A1", "A1")) - if len(xlsx.MergeCells.Cells) != mergeCellNum-1 { + if len(sheet.MergeCells.Cells) != mergeCellNum-1 { t.FailNow() } diff --git a/picture.go b/picture.go index c37899e0ad..e3601ddde2 100644 --- a/picture.go +++ b/picture.go @@ -94,7 +94,7 @@ func (f *File) AddPicture(sheet, cell, picture, format string) error { if !ok { return ErrImgExt } - file, _ := ioutil.ReadFile(picture) + file, _ := ioutil.ReadFile(filepath.Clean(picture)) _, name := filepath.Split(picture) return f.AddPictureFromBytes(sheet, cell, format, name, ext, file) } @@ -199,8 +199,8 @@ func (f *File) deleteSheetRelationships(sheet, rID string) { // addSheetLegacyDrawing provides a function to add legacy drawing element to // xl/worksheets/sheet%d.xml by given worksheet name and relationship index. func (f *File) addSheetLegacyDrawing(sheet string, rID int) { - xlsx, _ := f.workSheetReader(sheet) - xlsx.LegacyDrawing = &xlsxLegacyDrawing{ + ws, _ := f.workSheetReader(sheet) + ws.LegacyDrawing = &xlsxLegacyDrawing{ RID: "rId" + strconv.Itoa(rID), } } @@ -208,8 +208,8 @@ func (f *File) addSheetLegacyDrawing(sheet string, rID int) { // addSheetDrawing provides a function to add drawing element to // xl/worksheets/sheet%d.xml by given worksheet name and relationship index. func (f *File) addSheetDrawing(sheet string, rID int) { - xlsx, _ := f.workSheetReader(sheet) - xlsx.Drawing = &xlsxDrawing{ + ws, _ := f.workSheetReader(sheet) + ws.Drawing = &xlsxDrawing{ RID: "rId" + strconv.Itoa(rID), } } @@ -217,8 +217,8 @@ 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) { - xlsx, _ := f.workSheetReader(sheet) - xlsx.Picture = &xlsxPicture{ + ws, _ := f.workSheetReader(sheet) + ws.Picture = &xlsxPicture{ RID: "rId" + strconv.Itoa(rID), } } diff --git a/picture_test.go b/picture_test.go index 913ed3d7e1..3e12f5f9db 100644 --- a/picture_test.go +++ b/picture_test.go @@ -71,24 +71,24 @@ func TestAddPicture(t *testing.T) { } func TestAddPictureErrors(t *testing.T) { - xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) assert.NoError(t, err) // Test add picture to worksheet with invalid file path. - err = xlsx.AddPicture("Sheet1", "G21", filepath.Join("test", "not_exists_dir", "not_exists.icon"), "") + 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") } // Test add picture to worksheet with unsupported file type. - err = xlsx.AddPicture("Sheet1", "G21", filepath.Join("test", "Book1.xlsx"), "") + err = f.AddPicture("Sheet1", "G21", filepath.Join("test", "Book1.xlsx"), "") assert.EqualError(t, err, ErrImgExt.Error()) - err = xlsx.AddPictureFromBytes("Sheet1", "G21", "", "Excel Logo", "jpg", make([]byte, 1)) + err = f.AddPictureFromBytes("Sheet1", "G21", "", "Excel Logo", "jpg", make([]byte, 1)) assert.EqualError(t, err, ErrImgExt.Error()) // Test add picture to worksheet with invalid file data. - err = xlsx.AddPictureFromBytes("Sheet1", "G21", "", "Excel Logo", ".jpg", make([]byte, 1)) + err = f.AddPictureFromBytes("Sheet1", "G21", "", "Excel Logo", ".jpg", make([]byte, 1)) assert.EqualError(t, err, "image: unknown format") } diff --git a/sheet.go b/sheet.go index 1c4b355937..7e15bbe5d0 100644 --- a/sheet.go +++ b/sheet.go @@ -480,7 +480,7 @@ func (f *File) SetSheetBackground(sheet, picture string) error { if !ok { return ErrImgExt } - file, _ := ioutil.ReadFile(picture) + file, _ := ioutil.ReadFile(filepath.Clean(picture)) name := f.addMedia(file, ext) sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(f.sheetMap[trimSheetName(sheet)], "xl/worksheets/") + ".rels" rID := f.addRels(sheetRels, SourceRelationshipImage, strings.Replace(name, "xl", "..", 1), "") @@ -655,13 +655,13 @@ func (f *File) SetSheetVisible(name string, visible bool) error { } } for k, v := range content.Sheets.Sheet { - xlsx, err := f.workSheetReader(v.Name) + ws, err := f.workSheetReader(v.Name) if err != nil { return err } tabSelected := false - if len(xlsx.SheetViews.SheetView) > 0 { - tabSelected = xlsx.SheetViews.SheetView[0].TabSelected + if len(ws.SheetViews.SheetView) > 0 { + tabSelected = ws.SheetViews.SheetView[0].TabSelected } if v.Name == name && count > 1 && !tabSelected { content.Sheets.Sheet[k].State = "hidden" diff --git a/sheetpr.go b/sheetpr.go index 8bc4bfe247..6f46040cc5 100644 --- a/sheetpr.go +++ b/sheetpr.go @@ -182,14 +182,14 @@ func (o *AutoPageBreaks) getSheetPrOption(pr *xlsxSheetPr) { // AutoPageBreaks(bool) // OutlineSummaryBelow(bool) func (f *File) SetSheetPrOptions(name string, opts ...SheetPrOption) error { - sheet, err := f.workSheetReader(name) + ws, err := f.workSheetReader(name) if err != nil { return err } - pr := sheet.SheetPr + pr := ws.SheetPr if pr == nil { pr = new(xlsxSheetPr) - sheet.SheetPr = pr + ws.SheetPr = pr } for _, opt := range opts { @@ -208,11 +208,11 @@ func (f *File) SetSheetPrOptions(name string, opts ...SheetPrOption) error { // AutoPageBreaks(bool) // OutlineSummaryBelow(bool) func (f *File) GetSheetPrOptions(name string, opts ...SheetPrOptionPtr) error { - sheet, err := f.workSheetReader(name) + ws, err := f.workSheetReader(name) if err != nil { return err } - pr := sheet.SheetPr + pr := ws.SheetPr for _, opt := range opts { opt.getSheetPrOption(pr) diff --git a/stream.go b/stream.go index 99390168ac..125b58ce40 100644 --- a/stream.go +++ b/stream.go @@ -346,8 +346,8 @@ 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 - for _, o := range opts { - opt = &o + for i := range opts { + opt = &opts[i] } if opt == nil { return diff --git a/xmlChartSheet.go b/xmlChartSheet.go index 4ef2deddf5..fcc34432c4 100644 --- a/xmlChartSheet.go +++ b/xmlChartSheet.go @@ -5,9 +5,11 @@ // struct code generated by github.com/xuri/xgen // // Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.15 or later. +// and read from XLSX / XLSM / XLTM 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. package excelize diff --git a/xmlDrawing.go b/xmlDrawing.go index 4e35fcf7f6..b49ae9d9ef 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -94,6 +94,7 @@ const ( // Excel specifications and limits const ( + UnzipSizeLimit = 1000 << 24 StreamChunkSize = 1 << 24 MaxFontFamilyLength = 31 MaxFontSize = 409 From b02f864eab5edb2155601b9dd640f99fbd442cb3 Mon Sep 17 00:00:00 2001 From: raochq <31030448+raochq@users.noreply.github.com> Date: Sun, 15 Aug 2021 01:19:49 +0800 Subject: [PATCH 427/957] This closes #844, support get shared formula --- cell.go | 78 ++++++++++++++++++++++++++++++++++++++++++++++-- cell_test.go | 22 ++++++++++++++ excelize_test.go | 2 +- lib.go | 5 ++-- 4 files changed, 100 insertions(+), 7 deletions(-) diff --git a/cell.go b/cell.go index 6ad5f44457..d44a5527a1 100644 --- a/cell.go +++ b/cell.go @@ -392,7 +392,7 @@ func (f *File) GetCellFormula(sheet, axis string) (string, error) { return "", false, nil } if c.F.T == STCellFormulaTypeShared { - return getSharedForumula(x, c.F.Si), true, nil + return getSharedForumula(x, c.F.Si, c.R), true, nil } return c.F.Content, true, nil }) @@ -977,6 +977,48 @@ func isOverlap(rect1, rect2 []int) bool { cellInRef([]int{rect2[2], rect2[3]}, rect1) } +// parseSharedFormula generate dynamic part of shared formula for target cell +// by given column and rows distance and origin shared formula. +func parseSharedFormula(dCol, dRow int, orig []byte) (res string, start int) { + var ( + end int + stringLiteral bool + ) + for end = 0; end < len(orig); end++ { + c := orig[end] + if c == '"' { + stringLiteral = !stringLiteral + } + if stringLiteral { + continue // Skip characters in quotes + } + if c >= 'A' && c <= 'Z' || c == '$' { + res += string(orig[start:end]) + start = end + end++ + foundNum := false + for ; end < len(orig); end++ { + idc := orig[end] + if idc >= '0' && idc <= '9' || idc == '$' { + foundNum = true + } else if idc >= 'A' && idc <= 'Z' { + if foundNum { + break + } + } else { + break + } + } + if foundNum { + cellID := string(orig[start:end]) + res += shiftCell(cellID, dCol, dRow) + start = end + } + } + } + return +} + // getSharedForumula find a cell contains the same formula as another cell, // the "shared" value can be used for the t attribute and the si attribute can // be used to refer to the cell containing the formula. Two formulas are @@ -985,13 +1027,43 @@ func isOverlap(rect1, rect2 []int) bool { // // Note that this function not validate ref tag to check the cell if or not in // allow area, and always return origin shared formula. -func getSharedForumula(ws *xlsxWorksheet, si string) string { +func getSharedForumula(ws *xlsxWorksheet, si string, axis 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 == si { - return c.F.Content + col, row, _ := CellNameToCoordinates(axis) + sharedCol, sharedRow, _ := CellNameToCoordinates(c.R) + dCol := col - sharedCol + dRow := row - sharedRow + orig := []byte(c.F.Content) + res, start := parseSharedFormula(dCol, dRow, orig) + if start < len(orig) { + res += string(orig[start:]) + } + return res } } } return "" } + +// shiftCell returns the cell shifted according to dCol and dRow taking into +// consideration of absolute references with dollar sign ($) +func shiftCell(cellID string, dCol, dRow int) string { + fCol, fRow, _ := CellNameToCoordinates(cellID) + signCol, signRow := "", "" + if strings.Index(cellID, "$") == 0 { + signCol = "$" + } else { + // Shift column + fCol += dCol + } + if strings.LastIndex(cellID, "$") > 0 { + signRow = "$" + } else { + // Shift row + fRow += dRow + } + colName, _ := ColumnNumberToName(fCol) + return signCol + colName + signRow + strconv.Itoa(fRow) +} diff --git a/cell_test.go b/cell_test.go index 3954438ebe..0af0097109 100644 --- a/cell_test.go +++ b/cell_test.go @@ -226,6 +226,28 @@ func TestGetCellFormula(t *testing.T) { assert.NoError(t, f.SetCellValue("Sheet1", "A1", true)) _, err = f.GetCellFormula("Sheet1", "A1") assert.NoError(t, err) + + // Test get cell shared formula + f = NewFile() + sheetData := `12*A12%s34567` + + for sharedFormula, expected := range map[string]string{ + `2*A2`: `2*A3`, + `2*A1A`: `2*A2A`, + `2*$A$2+LEN("")`: `2*$A$2+LEN("")`, + } { + f.Sheet.Delete("xl/worksheets/sheet1.xml") + f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, sharedFormula))) + formula, err := f.GetCellFormula("Sheet1", "B3") + assert.NoError(t, err) + assert.Equal(t, expected, formula) + } + + f.Sheet.Delete("xl/worksheets/sheet1.xml") + f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(``)) + formula, err := f.GetCellFormula("Sheet1", "B2") + assert.NoError(t, err) + assert.Equal(t, "", formula) } func ExampleFile_SetCellFloat() { diff --git a/excelize_test.go b/excelize_test.go index 918279bfa4..f33c3d5818 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -83,7 +83,7 @@ func TestOpenFile(t *testing.T) { assert.NoError(t, err) _, err = f.GetCellFormula("Sheet2", "I11") assert.NoError(t, err) - getSharedForumula(&xlsxWorksheet{}, "") + getSharedForumula(&xlsxWorksheet{}, "", "") // Test read cell value with given illegal rows number. _, err = f.GetCellValue("Sheet2", "a-1") diff --git a/lib.go b/lib.go index 712576de21..81f1e53bfb 100644 --- a/lib.go +++ b/lib.go @@ -93,13 +93,12 @@ func readFile(file *zip.File) ([]byte, error) { // func SplitCellName(cell string) (string, int, error) { alpha := func(r rune) bool { - return ('A' <= r && r <= 'Z') || ('a' <= r && r <= 'z') + return ('A' <= r && r <= 'Z') || ('a' <= r && r <= 'z') || (r == 36) } - if strings.IndexFunc(cell, alpha) == 0 { i := strings.LastIndexFunc(cell, alpha) if i >= 0 && i < len(cell)-1 { - col, rowstr := cell[:i+1], cell[i+1:] + col, rowstr := strings.ReplaceAll(cell[:i+1], "$", ""), cell[i+1:] if row, err := strconv.Atoi(rowstr); err == nil && row > 0 { return col, row, nil } From a55f354eb3d0c6c1b9a543ff8ff98227aa6063a6 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 17 Aug 2021 00:01:44 +0800 Subject: [PATCH 428/957] This closes #989, closes #990 New API: `SetRowStyle` support for set style for the rows Update documentation for the `GetRows`, `SetCellStyle` and `SetColStyle` --- col.go | 4 +++- errors.go | 7 +++++++ lib.go | 2 +- rows.go | 42 ++++++++++++++++++++++++++++++++++++++++-- rows_test.go | 12 ++++++++++++ styles.go | 3 ++- 6 files changed, 65 insertions(+), 5 deletions(-) diff --git a/col.go b/col.go index 7fbeebab33..d2eba8b02e 100644 --- a/col.go +++ b/col.go @@ -398,7 +398,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. +// name, columns range and style ID. Note that this will overwrite the +// existing styles for the cell, it won't append or merge style with existing +// styles. // // For example set style of column H on Sheet1: // diff --git a/errors.go b/errors.go index aee442009e..4cfd12e91a 100644 --- a/errors.go +++ b/errors.go @@ -46,6 +46,11 @@ func newUnzipSizeLimitError(unzipSizeLimit int64) error { return fmt.Errorf("unzip size exceeds the %d bytes limit", unzipSizeLimit) } +// 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) +} + var ( // ErrStreamSetColWidth defined the error message on set column width in // stream writing mode. @@ -76,6 +81,8 @@ var ( ErrAddVBAProject = errors.New("unsupported VBA project extension") // ErrToExcelTime defined the error message on receive a not UTC time. ErrToExcelTime = errors.New("only UTC time expected") + // 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 // height. ErrMaxRowHeight = errors.New("the height of the row must be smaller than or equal to 409 points") diff --git a/lib.go b/lib.go index 81f1e53bfb..424d3f9b8c 100644 --- a/lib.go +++ b/lib.go @@ -196,7 +196,7 @@ func CellNameToCoordinates(cell string) (int, int, error) { return -1, -1, fmt.Errorf(msg, cell, err) } if row > TotalRows { - return -1, -1, fmt.Errorf("row number exceeds maximum limit") + return -1, -1, ErrMaxRows } col, err := ColumnNameToNumber(colname) return col, row, err diff --git a/rows.go b/rows.go index 5fa2cb45ec..fb03bba59e 100644 --- a/rows.go +++ b/rows.go @@ -23,8 +23,9 @@ import ( "github.com/mohae/deepcopy" ) -// GetRows return all the rows in a sheet by given worksheet name (case -// sensitive). For example: +// GetRows return all the rows in a sheet by given worksheet name +// (case sensitive). GetRows fetched the rows with value or formula cells, +// the tail continuously empty cell will be skipped. For example: // // rows, err := f.GetRows("Sheet1") // if err != nil { @@ -719,6 +720,43 @@ func checkRow(ws *xlsxWorksheet) error { return nil } +// SetRowStyle provides a function to set style of rows by given worksheet +// name, row range and style ID. Note that this will overwrite the existing +// styles for the cell, it won't append or merge style with existing styles. +// +// For example set style of row 1 on Sheet1: +// +// err = f.SetRowStyle("Sheet1", 1, style) +// +// Set style of rows 1 to 10 on Sheet1: +// +// err = f.SetRowStyle("Sheet1", 1, 10, style) +// +func (f *File) SetRowStyle(sheet string, start, end, styleID int) error { + if end < start { + start, end = end, start + } + if start < 1 { + return newInvalidRowNumberError(start) + } + if end > TotalRows { + return ErrMaxRows + } + if styleID < 0 { + return newInvalidStyleID(styleID) + } + ws, err := f.workSheetReader(sheet) + if err != nil { + return err + } + prepareSheetXML(ws, 0, end) + for row := start - 1; row < end; row++ { + ws.SheetData.Row[row].S = styleID + ws.SheetData.Row[row].CustomFormat = true + } + return nil +} + // convertRowHeightToPixels provides a function to convert the height of a // 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. diff --git a/rows_test.go b/rows_test.go index 768246936f..a54e755df5 100644 --- a/rows_test.go +++ b/rows_test.go @@ -889,6 +889,18 @@ func TestCheckRow(t *testing.T) { assert.EqualError(t, f.SetCellValue("Sheet1", "A1", false), `cannot convert cell "-" to coordinates: invalid cell name "-"`) } +func TestSetRowStyle(t *testing.T) { + f := NewFile() + styleID, err := f.NewStyle(`{"fill":{"type":"pattern","color":["#E0EBF5"],"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()) + 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.NoError(t, f.SaveAs(filepath.Join("test", "TestSetRowStyle.xlsx"))) +} + func TestNumberFormats(t *testing.T) { f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if !assert.NoError(t, err) { diff --git a/styles.go b/styles.go index 2a99a4dce5..d925ea9a42 100644 --- a/styles.go +++ b/styles.go @@ -2603,7 +2603,8 @@ 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. +// area, this 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 dca03c6230e596560ea58ca1edb27581cdd59aaa Mon Sep 17 00:00:00 2001 From: Stani Date: Thu, 19 Aug 2021 16:12:37 +0200 Subject: [PATCH 429/957] This closes #994, fix LOOKUP function for Array form --- calc.go | 18 +++++++++++++----- calc_test.go | 4 ++++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/calc.go b/calc.go index eb6ff7597b..f52ed538e8 100644 --- a/calc.go +++ b/calc.go @@ -7263,7 +7263,11 @@ func (fn *formulaFuncs) LOOKUP(argsList *list.List) formulaArg { if lookupVector.Type != ArgMatrix && lookupVector.Type != ArgList { return newErrorFormulaArg(formulaErrorVALUE, "LOOKUP requires second argument of table array") } - cols, matchIdx := lookupCol(lookupVector), -1 + arrayForm := lookupVector.Type == ArgMatrix + if arrayForm && len(lookupVector.Matrix) == 0 { + return newErrorFormulaArg(formulaErrorVALUE, "LOOKUP requires not empty range as second argument") + } + cols, matchIdx := lookupCol(lookupVector, 0), -1 for idx, col := range cols { lhs := lookupValue switch col.Type { @@ -7280,9 +7284,13 @@ func (fn *formulaFuncs) LOOKUP(argsList *list.List) formulaArg { break } } - column := cols + var column []formulaArg if argsList.Len() == 3 { - column = lookupCol(argsList.Back().Value.(formulaArg)) + column = lookupCol(argsList.Back().Value.(formulaArg), 0) + } else if arrayForm && len(lookupVector.Matrix[0]) > 1 { + column = lookupCol(lookupVector, 1) + } else { + column = cols } if matchIdx < 0 || matchIdx >= len(column) { return newErrorFormulaArg(formulaErrorNA, "LOOKUP no result found") @@ -7291,13 +7299,13 @@ func (fn *formulaFuncs) LOOKUP(argsList *list.List) formulaArg { } // lookupCol extract columns for LOOKUP. -func lookupCol(arr formulaArg) []formulaArg { +func lookupCol(arr formulaArg, idx int) []formulaArg { col := arr.List if arr.Type == ArgMatrix { col = nil for _, r := range arr.Matrix { if len(r) > 0 { - col = append(col, r[0]) + col = append(col, r[idx]) continue } col = append(col, newEmptyFormulaArg()) diff --git a/calc_test.go b/calc_test.go index 54bdc0156f..ffcdb4dbda 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1131,6 +1131,9 @@ func TestCalcCellValue(t *testing.T) { // LOOKUP "=LOOKUP(F8,F8:F9,F8:F9)": "32080", "=LOOKUP(F8,F8:F9,D8:D9)": "Feb", + "=LOOKUP(E3,E2:E5,F2:F5)": "22100", + "=LOOKUP(E3,E2:F5)": "22100", + "=LOOKUP(1,MUNIT(1))": "1", "=LOOKUP(1,MUNIT(1),MUNIT(1))": "1", // ROW "=ROW()": "1", @@ -2090,6 +2093,7 @@ func TestCalcCellValue(t *testing.T) { "=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", // ROW "=ROW(1,2)": "ROW requires at most 1 argument", From 935af2e356ff60c88761db1fc9a6be8f8c67a4f5 Mon Sep 17 00:00:00 2001 From: Stani Date: Sat, 21 Aug 2021 05:50:49 +0200 Subject: [PATCH 430/957] This closes #1002, new fn: DAY ref #65 Co-authored-by: Stani Michiels Co-authored-by: xuri --- calc.go | 385 +++++++++++++++++++++++++++++++++++++++++++++------ calc_test.go | 57 ++++++++ date.go | 45 ++++++ 3 files changed, 447 insertions(+), 40 deletions(-) diff --git a/calc.go b/calc.go index f52ed538e8..a434709266 100644 --- a/calc.go +++ b/calc.go @@ -34,29 +34,111 @@ import ( "golang.org/x/text/message" ) -// Excel formula errors const ( + // Excel formula errors formulaErrorDIV = "#DIV/0!" formulaErrorNAME = "#NAME?" formulaErrorNA = "#N/A" formulaErrorNUM = "#NUM!" formulaErrorVALUE = "#VALUE!" formulaErrorREF = "#REF!" - formulaErrorNULL = "#NULL" + formulaErrorNULL = "#NULL!" formulaErrorSPILL = "#SPILL!" formulaErrorCALC = "#CALC!" formulaErrorGETTINGDATA = "#GETTING_DATA" + // formula criteria condition enumeration. + _ byte = iota + criteriaEq + criteriaLe + criteriaGe + criteriaL + criteriaG + criteriaBeg + criteriaEnd + criteriaErr + // Numeric precision correct numeric values as legacy Excel application + // https://en.wikipedia.org/wiki/Numeric_precision_in_Microsoft_Excel In the + // top figure the fraction 1/9000 in Excel is displayed. Although this number + // has a decimal representation that is an infinite string of ones, Excel + // displays only the leading 15 figures. In the second line, the number one + // is added to the fraction, and again Excel displays only 15 figures. + numericPrecision = 1000000000000000 + maxFinancialIterations = 128 + financialPercision = 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])+)` + df2 = monthRe + ` (([0-9])+), (([0-9])+)` + df3 = `(([0-9])+)-(([0-9])+)-(([0-9])+)` + df4 = `(([0-9])+)-` + monthRe + `-(([0-9])+)` + datePrefix = `^((` + df1 + `|` + df2 + `|` + df3 + `|` + df4 + `) )?` + tfhh = `(([0-9])+) (am|pm)` + tfhhmm = `(([0-9])+):(([0-9])+)( (am|pm))?` + tfmmss = `(([0-9])+):(([0-9])+\.([0-9])+)( (am|pm))?` + tfhhmmss = `(([0-9])+):(([0-9])+):(([0-9])+(\.([0-9])+)?)( (am|pm))?` + timeSuffix = `( (` + tfhh + `|` + tfhhmm + `|` + tfmmss + `|` + tfhhmmss + `))?$` ) -// Numeric precision correct numeric values as legacy Excel application -// https://en.wikipedia.org/wiki/Numeric_precision_in_Microsoft_Excel In the -// top figure the fraction 1/9000 in Excel is displayed. Although this number -// has a decimal representation that is an infinite string of ones, Excel -// displays only the leading 15 figures. In the second line, the number one -// is added to the fraction, and again Excel displays only 15 figures. -const numericPrecision = 1000000000000000 -const maxFinancialIterations = 128 -const financialPercision = 1.0e-08 +var ( + // tokenPriority defined basic arithmetic operator priority. + tokenPriority = map[string]int{ + "^": 5, + "*": 4, + "/": 4, + "+": 3, + "-": 3, + "=": 2, + "<>": 2, + "<": 2, + "<=": 2, + ">": 2, + ">=": 2, + "&": 1, + } + month2num = map[string]int{ + "january": 1, + "february": 2, + "march": 3, + "april": 4, + "may": 5, + "june": 6, + "july": 7, + "august": 8, + "septemper": 9, + "october": 10, + "november": 11, + "december": 12, + "jan": 1, + "feb": 2, + "mar": 3, + "apr": 4, + "jun": 6, + "jul": 7, + "aug": 8, + "sep": 9, + "oct": 10, + "nov": 11, + "dec": 12, + } + dateFormats = map[string]*regexp.Regexp{ + "mm/dd/yy": regexp.MustCompile(`^` + df1 + timeSuffix), + "mm dd, yy": regexp.MustCompile(`^` + df2 + timeSuffix), + "yy-mm-dd": regexp.MustCompile(`^` + df3 + timeSuffix), + "yy-mmStr-dd": regexp.MustCompile(`^` + df4 + timeSuffix), + } + timeFormats = map[string]*regexp.Regexp{ + "hh": regexp.MustCompile(datePrefix + tfhh + `$`), + "hh:mm": regexp.MustCompile(datePrefix + tfhhmm + `$`), + "mm:ss": regexp.MustCompile(datePrefix + tfmmss + `$`), + "hh:mm:ss": regexp.MustCompile(datePrefix + tfhhmmss + `$`), + } + dateOnlyFormats = []*regexp.Regexp{ + regexp.MustCompile(`^` + df1 + `$`), + regexp.MustCompile(`^` + df2 + `$`), + regexp.MustCompile(`^` + df3 + `$`), + regexp.MustCompile(`^` + df4 + `$`), + } +) // cellRef defines the structure of a cell reference. type cellRef struct { @@ -71,19 +153,6 @@ type cellRange struct { To cellRef } -// formula criteria condition enumeration. -const ( - _ byte = iota - criteriaEq - criteriaLe - criteriaGe - criteriaL - criteriaG - criteriaBeg - criteriaEnd - criteriaErr -) - // formulaCriteria defined formula criteria parser result. type formulaCriteria struct { Type byte @@ -193,22 +262,6 @@ type formulaFuncs struct { sheet, cell string } -// tokenPriority defined basic arithmetic operator priority. -var tokenPriority = map[string]int{ - "^": 5, - "*": 4, - "/": 4, - "+": 3, - "-": 3, - "=": 2, - "<>": 2, - "<": 2, - "<=": 2, - ">": 2, - ">=": 2, - "&": 1, -} - // 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. @@ -269,6 +322,7 @@ var tokenPriority = map[string]int{ // CUMPRINC // DATE // DATEDIF +// DAY // DB // DDB // DEC2BIN @@ -6108,6 +6162,257 @@ func (fn *formulaFuncs) DATEDIF(argsList *list.List) formulaArg { return newNumberFormulaArg(diff) } +// 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 { + return true + } + } + return false +} + +// 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]) + 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 { + return + } + 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 { + return + } + 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 { + return + } + if m, err = strconv.Atoi(submatch[2]); err != nil { + return + } + 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{} + for key, tf := range timeFormats { + submatch = tf.FindStringSubmatch(str) + if len(submatch) > 1 { + pattern = key + break + } + } + if pattern == "" { + return 0, 0, 0, false, false, newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + dateIsEmpty := submatch[1] == "" + submatch = submatch[49:] + var ( + 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){ + "hh": strToTimePatternHandler1, + "hh:mm": strToTimePatternHandler2, + "mm:ss": strToTimePatternHandler3, + "hh:mm:ss": strToTimePatternHandler4, + }[pattern]; ok { + if hours, minutes, seconds, err = handler(submatch); err != nil { + return 0, 0, 0, false, false, newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + } + if minutes >= 60 { + return 0, 0, 0, false, false, newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + if am || pm { + if hours > 12 || seconds >= 60 { + return 0, 0, 0, false, false, newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } else if hours == 12 { + hours = 0 + } + } else if hours >= 24 || seconds >= 10000 { + return 0, 0, 0, false, false, newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + return hours, minutes, seconds, pm, dateIsEmpty, newEmptyFormulaArg() +} + +// strToDatePatternHandler1 parse and convert the given string in pattern +// mm/dd/yy to the date. +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 { + return 0, 0, 0, false, err + } + if day, err = strconv.Atoi(submatch[3]); err != nil { + return 0, 0, 0, false, err + } + 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 +} + +// strToDatePatternHandler2 parse and convert the given string in pattern mm +// dd, yy to the date. +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 { + return 0, 0, 0, false, err + } + 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 +} + +// strToDatePatternHandler3 parse and convert the given string in pattern +// yy-mm-dd to the date. +func strToDatePatternHandler3(submatch []string) (int, int, int, bool, error) { + var year, month, day int + v1, err := strconv.Atoi(submatch[1]) + if err != nil { + return 0, 0, 0, false, err + } + v2, err := strconv.Atoi(submatch[3]) + if err != nil { + return 0, 0, 0, false, err + } + v3, err := strconv.Atoi(submatch[5]) + if err != nil { + return 0, 0, 0, false, err + } + if v1 >= 1900 && v1 < 10000 { + year = v1 + month = v2 + day = v3 + } else if v1 > 0 && v1 < 13 { + month = v1 + day = v2 + year = v3 + } else { + return 0, 0, 0, false, ErrParameterInvalid + } + 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) { + var year, month, day int + var err error + 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 { + return 0, 0, 0, false, 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{} + for key, df := range dateFormats { + submatch = df.FindStringSubmatch(str) + if len(submatch) > 1 { + pattern = key + break + } + } + if pattern == "" { + return 0, 0, 0, false, newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + var ( + timeIsEmpty bool + year, month, day int + err error + ) + if handler, ok := map[string]func(subsubmatch []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 { + return 0, 0, 0, false, newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + } + if !validateDate(year, month, day) { + return 0, 0, 0, false, newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + return year, month, day, timeIsEmpty, newEmptyFormulaArg() +} + +// DAY function returns the day of a date, represented by a serial number. The +// day is given as an integer ranging from 1 to 31. The syntax of the +// function is: +// +// DAY(serial_number) +// +func (fn *formulaFuncs) DAY(argsList *list.List) formulaArg { + if argsList.Len() != 1 { + return newErrorFormulaArg(formulaErrorVALUE, "DAY requires exactly 1 argument") + } + arg := argsList.Front().Value.(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 + } + } + _, _, day, _, err := strToDate(dateString) + if err.Type == ArgError { + return err + } + return newNumberFormulaArg(float64(day)) + } + if num.Number < 0 { + return newErrorFormulaArg(formulaErrorNUM, "DAY only accepts positive argument") + } + if num.Number <= 60 { + return newNumberFormulaArg(math.Mod(num.Number, 31.0)) + } + return newNumberFormulaArg(float64(timeFromExcelTime(num.Number, false).Day())) +} + // NOW function returns the current date and time. The function receives no // arguments and therefore. The syntax of the function is: // diff --git a/calc_test.go b/calc_test.go index ffcdb4dbda..7c107f38a6 100644 --- a/calc_test.go +++ b/calc_test.go @@ -944,6 +944,24 @@ func TestCalcCellValue(t *testing.T) { "=DATEDIF(43101,43891,\"YD\")": "59", "=DATEDIF(36526,73110,\"YD\")": "60", "=DATEDIF(42171,44242,\"yd\")": "244", + // DAY + "=DAY(0)": "0", + "=DAY(INT(7))": "7", + "=DAY(\"35\")": "4", + "=DAY(42171)": "16", + "=DAY(\"2-28-1900\")": "28", + "=DAY(\"31-May-2015\")": "31", + "=DAY(\"01/03/2019 12:14:16\")": "3", + "=DAY(\"January 25, 2020 01 AM\")": "25", + "=DAY(\"January 25, 2020 01:03 AM\")": "25", + "=DAY(\"January 25, 2020 12:00:00 AM\")": "25", + "=DAY(\"1900-1-1\")": "1", + "=DAY(\"12-1-1900\")": "1", + "=DAY(\"3-January-1900\")": "3", + "=DAY(\"3-February-2000\")": "3", + "=DAY(\"3-February-2008\")": "3", + "=DAY(\"01/25/20\")": "25", + "=DAY(\"01/25/31\")": "25", // Text Functions // CHAR "=CHAR(65)": "A", @@ -1927,6 +1945,40 @@ func TestCalcCellValue(t *testing.T) { "=DATEDIF(\"\",\"\",\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", "=DATEDIF(43891,43101,\"Y\")": "start_date > end_date", "=DATEDIF(43101,43891,\"x\")": "DATEDIF has invalid unit", + // 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!", // NOW "=NOW(A1)": "NOW accepts no arguments", // TODAY @@ -2614,3 +2666,8 @@ func TestCalcMIRR(t *testing.T) { assert.Equal(t, "", result, formula) } } + +func TestStrToDate(t *testing.T) { + _, _, _, _, err := strToDate("") + assert.Equal(t, formulaErrorVALUE, err.Error) +} diff --git a/date.go b/date.go index a5edcf8849..b8c26e008a 100644 --- a/date.go +++ b/date.go @@ -23,6 +23,7 @@ const ( ) var ( + daysInMonth = []int{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} excel1900Epoc = time.Date(1899, time.December, 30, 0, 0, 0, 0, time.UTC) excel1904Epoc = time.Date(1904, time.January, 1, 0, 0, 0, 0, time.UTC) excelMinTime1900 = time.Date(1899, time.December, 31, 0, 0, 0, 0, time.UTC) @@ -167,3 +168,47 @@ func ExcelDateToTime(excelDate float64, use1904Format bool) (time.Time, error) { } return timeFromExcelTime(excelDate, use1904Format), nil } + +// isLeapYear determine if leap year for a given year. +func isLeapYear(y int) bool { + if y == y/400*400 { + return true + } + if y == y/100*100 { + return false + } + return y == y/4*4 +} + +// getDaysInMonth provides a function to get the days by a given year and +// month number. +func getDaysInMonth(y, m int) int { + if m == 2 && isLeapYear(y) { + return 29 + } + return daysInMonth[m-1] +} + +// validateDate provides a function to validate if a valid date by a given +// year, month, and day number. +func validateDate(y, m, d int) bool { + if m < 1 || m > 12 { + return false + } + if d < 1 { + return false + } + return d <= getDaysInMonth(y, m) +} + +// formatYear converts the given year number into a 4-digit format. +func formatYear(y int) int { + if y < 1900 { + if y < 30 { + y += 2000 + } else { + y += 1900 + } + } + return y +} From f280c03345dc2a207ac319182da182a0f0fbb963 Mon Sep 17 00:00:00 2001 From: Stani Date: Sat, 21 Aug 2021 05:58:15 +0200 Subject: [PATCH 431/957] This closes #997, fix LOOKUP function to find nearest match (#1001) --- calc.go | 15 +++++++++++++-- calc_test.go | 2 ++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/calc.go b/calc.go index a434709266..9ff0535848 100644 --- a/calc.go +++ b/calc.go @@ -7572,7 +7572,7 @@ func (fn *formulaFuncs) LOOKUP(argsList *list.List) formulaArg { if arrayForm && len(lookupVector.Matrix) == 0 { return newErrorFormulaArg(formulaErrorVALUE, "LOOKUP requires not empty range as second argument") } - cols, matchIdx := lookupCol(lookupVector, 0), -1 + cols, matchIdx, ok := lookupCol(lookupVector, 0), -1, false for idx, col := range cols { lhs := lookupValue switch col.Type { @@ -7584,10 +7584,21 @@ func (fn *formulaFuncs) LOOKUP(argsList *list.List) formulaArg { } } } - if compareFormulaArg(lhs, col, false, false) == criteriaEq { + compare := compareFormulaArg(lhs, col, false, false) + // Find exact match + if compare == criteriaEq { matchIdx = idx break } + // Find 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 { + matchIdx = idx - 1 + } + } + if ok && matchIdx == -1 { + matchIdx = len(cols) - 1 } var column []formulaArg if argsList.Len() == 3 { diff --git a/calc_test.go b/calc_test.go index 7c107f38a6..8deab93419 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1151,6 +1151,8 @@ func TestCalcCellValue(t *testing.T) { "=LOOKUP(F8,F8:F9,D8:D9)": "Feb", "=LOOKUP(E3,E2:E5,F2:F5)": "22100", "=LOOKUP(E3,E2:F5)": "22100", + "=LOOKUP(F3+1,F3:F4,F3:F4)": "22100", + "=LOOKUP(F4+1,F3:F4,F3:F4)": "53321", "=LOOKUP(1,MUNIT(1))": "1", "=LOOKUP(1,MUNIT(1),MUNIT(1))": "1", // ROW From 4d716fa7edde76ed838aefcd8c5945bcfe3ba8f3 Mon Sep 17 00:00:00 2001 From: Stani Date: Sat, 21 Aug 2021 09:46:43 +0200 Subject: [PATCH 432/957] * This closes #1004, new fn: MONTH ref #65 --- calc.go | 32 ++++++++++++++++++++++++++++++++ calc_test.go | 9 +++++++++ 2 files changed, 41 insertions(+) diff --git a/calc.go b/calc.go index 9ff0535848..9f3e25fa42 100644 --- a/calc.go +++ b/calc.go @@ -420,6 +420,7 @@ type formulaFuncs struct { // MINA // MIRR // MOD +// MONTH // MROUND // MULTINOMIAL // MUNIT @@ -6413,6 +6414,37 @@ func (fn *formulaFuncs) DAY(argsList *list.List) formulaArg { return newNumberFormulaArg(float64(timeFromExcelTime(num.Number, false).Day())) } +// MONTH function returns the month of a date represented by a serial number. +// The month is given as an integer, ranging from 1 (January) to 12 +// (December). The syntax of the function is: +// +// MONTH(serial_number) +// +func (fn *formulaFuncs) MONTH(argsList *list.List) formulaArg { + if argsList.Len() != 1 { + return newErrorFormulaArg(formulaErrorVALUE, "MONTH requires exactly 1 argument") + } + arg := argsList.Front().Value.(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 + } + } + _, month, _, _, err := strToDate(dateString) + if err.Type == ArgError { + return err + } + return newNumberFormulaArg(float64(month)) + } + if num.Number < 0 { + return newErrorFormulaArg(formulaErrorNUM, "MONTH only accepts positive argument") + } + return newNumberFormulaArg(float64(timeFromExcelTime(num.Number, false).Month())) +} + // NOW function returns the current date and time. The function receives no // arguments and therefore. The syntax of the function is: // diff --git a/calc_test.go b/calc_test.go index 8deab93419..cea95210dd 100644 --- a/calc_test.go +++ b/calc_test.go @@ -962,6 +962,9 @@ func TestCalcCellValue(t *testing.T) { "=DAY(\"3-February-2008\")": "3", "=DAY(\"01/25/20\")": "25", "=DAY(\"01/25/31\")": "25", + // MONTH + "=MONTH(42171)": "6", + "=MONTH(\"31-May-2015\")": "5", // Text Functions // CHAR "=CHAR(65)": "A", @@ -1981,6 +1984,12 @@ func TestCalcCellValue(t *testing.T) { "=DAY(\"3-January-9223372036854775808\")": "#VALUE!", "=DAY(\"9223372036854775808-January-1900\")": "#VALUE!", "=DAY(\"0-January-1900\")": "#VALUE!", + // MONTH + "=MONTH()": "MONTH requires exactly 1 argument", + "=MONTH(43891,43101)": "MONTH requires exactly 1 argument", + "=MONTH(-1)": "MONTH only accepts positive argument", + "=MONTH(\"text\")": "#VALUE!", + "=MONTH(\"January 25, 100\")": "#VALUE!", // NOW "=NOW(A1)": "NOW accepts no arguments", // TODAY From 9b55f4f9f0b839934eb8113d2092c60a1a5b64b8 Mon Sep 17 00:00:00 2001 From: Stani Date: Sat, 21 Aug 2021 14:15:53 +0200 Subject: [PATCH 433/957] This closes #1006, new fn: MONTH ref #65 Co-authored-by: xuri --- calc.go | 31 +++++++++++++++++++++++++++++++ calc_test.go | 15 ++++++++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/calc.go b/calc.go index 9f3e25fa42..e59d344bfb 100644 --- a/calc.go +++ b/calc.go @@ -504,6 +504,7 @@ type formulaFuncs struct { // VAR.P // VARP // VLOOKUP +// YEAR // func (f *File) CalcCellValue(sheet, cell string) (result string, err error) { var ( @@ -6445,6 +6446,36 @@ func (fn *formulaFuncs) MONTH(argsList *list.List) formulaArg { return newNumberFormulaArg(float64(timeFromExcelTime(num.Number, false).Month())) } +// YEAR function returns an integer representing the year of a supplied date. +// The syntax of the function is: +// +// YEAR(serial_number) +// +func (fn *formulaFuncs) YEAR(argsList *list.List) formulaArg { + if argsList.Len() != 1 { + return newErrorFormulaArg(formulaErrorVALUE, "YEAR requires exactly 1 argument") + } + arg := argsList.Front().Value.(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 + } + } + year, _, _, _, err := strToDate(dateString) + if err.Type == ArgError { + return err + } + return newNumberFormulaArg(float64(year)) + } + if num.Number < 0 { + return newErrorFormulaArg(formulaErrorNUM, "YEAR only accepts positive argument") + } + return newNumberFormulaArg(float64(timeFromExcelTime(num.Number, false).Year())) +} + // NOW function returns the current date and time. The function receives no // arguments and therefore. The syntax of the function is: // diff --git a/calc_test.go b/calc_test.go index cea95210dd..e5a6e1e09d 100644 --- a/calc_test.go +++ b/calc_test.go @@ -965,6 +965,13 @@ func TestCalcCellValue(t *testing.T) { // MONTH "=MONTH(42171)": "6", "=MONTH(\"31-May-2015\")": "5", + // YEAR + "=YEAR(15)": "1900", + "=YEAR(\"15\")": "1900", + "=YEAR(2048)": "1905", + "=YEAR(42171)": "2015", + "=YEAR(\"29-May-2015\")": "2015", + "=YEAR(\"05/03/1984\")": "1984", // Text Functions // CHAR "=CHAR(65)": "A", @@ -1986,10 +1993,16 @@ func TestCalcCellValue(t *testing.T) { "=DAY(\"0-January-1900\")": "#VALUE!", // MONTH "=MONTH()": "MONTH requires exactly 1 argument", - "=MONTH(43891,43101)": "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!", + // 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!", // NOW "=NOW(A1)": "NOW accepts no arguments", // TODAY From a2d449708cf72928394b4bc4aea41c0c6a606fa2 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 22 Aug 2021 13:36:56 +0800 Subject: [PATCH 434/957] - This fix panic and incorrect cell read on some case - Make unit test on Go 1.7 - API documentation updated --- .github/workflows/go.yml | 2 +- cell_test.go | 11 +++++++++++ col.go | 4 ++-- excelize.go | 2 +- rows.go | 16 ++++++++++------ styles.go | 4 ++-- 6 files changed, 27 insertions(+), 12 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 13913aa1cb..320e3dae1c 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] + go-version: [1.15.x, 1.16.x, 1.17.x] os: [ubuntu-latest, macos-latest, windows-latest] targetplatform: [x86, x64] diff --git a/cell_test.go b/cell_test.go index 0af0097109..91dc4fdcfd 100644 --- a/cell_test.go +++ b/cell_test.go @@ -181,6 +181,7 @@ func TestGetCellValue(t *testing.T) { // Test get cell value without r attribute of the row. f := NewFile() sheetData := `%s` + f.Sheet.Delete("xl/worksheets/sheet1.xml") f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `A3A4B4A7B7A8B8`))) f.checked = nil @@ -196,24 +197,34 @@ func TestGetCellValue(t *testing.T) { cols, err := f.GetCols("Sheet1") assert.Equal(t, [][]string{{"", "", "A3", "A4", "", "", "A7", "A8"}, {"", "", "", "B4", "", "", "B7", "B8"}}, cols) assert.NoError(t, err) + f.Sheet.Delete("xl/worksheets/sheet1.xml") 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.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.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.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) } func TestGetCellFormula(t *testing.T) { diff --git a/col.go b/col.go index d2eba8b02e..1e0c333430 100644 --- a/col.go +++ b/col.go @@ -399,8 +399,8 @@ 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 cell, it won't append or merge style with existing -// styles. +// existing styles for the columns, it won't append or merge style with +// existing styles. // // For example set style of column H on Sheet1: // diff --git a/excelize.go b/excelize.go index fafa57fa3b..6e4e4d9912 100644 --- a/excelize.go +++ b/excelize.go @@ -238,7 +238,7 @@ func checkSheet(ws *xlsxWorksheet) { sheetData := xlsxSheetData{Row: make([]xlsxRow, row)} row = 0 for _, r := range ws.SheetData.Row { - if r.R == row { + if r.R == row && row > 0 { sheetData.Row[r.R-1].C = append(sheetData.Row[r.R-1].C, r.C...) continue } diff --git a/rows.go b/rows.go index fb03bba59e..bfd7d13f0b 100644 --- a/rows.go +++ b/rows.go @@ -24,8 +24,12 @@ import ( ) // GetRows return all the rows in a sheet by given worksheet name -// (case sensitive). GetRows fetched the rows with value or formula cells, -// the tail continuously empty cell will be skipped. For example: +// (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: // // rows, err := f.GetRows("Sheet1") // if err != nil { @@ -111,7 +115,7 @@ func (rows *Rows) Columns() ([]string, error) { } case xml.EndElement: rowIterator.inElement = xmlElement.Name.Local - if rowIterator.row == 0 { + if rowIterator.row == 0 && rowIterator.rows.curRow > 1 { rowIterator.row = rowIterator.rows.curRow } if rowIterator.inElement == "row" && rowIterator.row+1 < rowIterator.rows.curRow { @@ -720,9 +724,9 @@ func checkRow(ws *xlsxWorksheet) error { return nil } -// SetRowStyle provides a function to set style of rows by given worksheet -// name, row range and style ID. Note that this will overwrite the existing -// styles for the cell, it won't append or merge style with existing styles. +// 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. // // For example set style of row 1 on Sheet1: // diff --git a/styles.go b/styles.go index d925ea9a42..31fa7fb349 100644 --- a/styles.go +++ b/styles.go @@ -2603,8 +2603,8 @@ 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, this will overwrite the existing styles for the cell, it won't -// append or merge style with existing styles. +// 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 cd030d4aa81582e8bc04d029c0be6e42eff9ea47 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 23 Aug 2021 00:15:43 +0800 Subject: [PATCH 435/957] Improve compatibility with row element with r="0" attribute --- cell_test.go | 17 +++++++++++++++++ excelize.go | 32 ++++++++++++++++++++++++++++++-- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/cell_test.go b/cell_test.go index 91dc4fdcfd..7a08560ed2 100644 --- a/cell_test.go +++ b/cell_test.go @@ -225,6 +225,23 @@ func TestGetCellValue(t *testing.T) { 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.checked = nil + cell, err = f.GetCellValue("Sheet1", "H6") + assert.Equal(t, "H6", cell) + assert.NoError(t, err) + rows, err = f.GetRows("Sheet1") + assert.Equal(t, [][]string{ + {"A6", "B6", "C6"}, + nil, + {"100", "B3"}, + {"", "", "", "", "", "F4"}, + nil, + {"", "", "", "", "", "", "", "H6"}, + }, rows) + assert.NoError(t, err) } func TestGetCellFormula(t *testing.T) { diff --git a/excelize.go b/excelize.go index 6e4e4d9912..11ddf921c3 100644 --- a/excelize.go +++ b/excelize.go @@ -226,7 +226,13 @@ func (f *File) workSheetReader(sheet string) (ws *xlsxWorksheet, err error) { // continuous in a worksheet of XML. func checkSheet(ws *xlsxWorksheet) { var row int - for _, r := range ws.SheetData.Row { + var r0 xlsxRow + for i, r := range ws.SheetData.Row { + if i == 0 && r.R == 0 { + r0 = r + ws.SheetData.Row = ws.SheetData.Row[1:] + continue + } if r.R != 0 && r.R > row { row = r.R continue @@ -254,7 +260,29 @@ func checkSheet(ws *xlsxWorksheet) { for i := 1; i <= row; i++ { sheetData.Row[i-1].R = i } - ws.SheetData = sheetData + checkSheetR0(ws, &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) { + for _, cell := range r0.C { + if col, row, err := CellNameToCoordinates(cell.R); err == nil { + rows, rowIdx := len(sheetData.Row), row-1 + for r := rows; r < row; r++ { + sheetData.Row = append(sheetData.Row, xlsxRow{R: r + 1}) + } + columns, colIdx := len(sheetData.Row[rowIdx].C), col-1 + for c := columns; c < col; c++ { + sheetData.Row[rowIdx].C = append(sheetData.Row[rowIdx].C, xlsxC{}) + } + if !sheetData.Row[rowIdx].C[colIdx].hasValue() { + sheetData.Row[rowIdx].C[colIdx] = cell + } + } + } + ws.SheetData = *sheetData } // addRels provides a function to add relationships by given XML path, From 7d9b9275bd14556bfcaab7f1d3690b1e54ab75e8 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 26 Aug 2021 00:48:18 +0800 Subject: [PATCH 436/957] This closes #1012, support specify the formula in the data validation range, and update the documentation for the `AddPicture` --- datavalidation.go | 36 ++++++++++++++++++++++++++++++------ datavalidation_test.go | 11 +++++++++++ picture.go | 42 +++++++++++++++++++++++++++++++++++------- 3 files changed, 76 insertions(+), 13 deletions(-) diff --git a/datavalidation.go b/datavalidation.go index e182ebe108..047a53c37e 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -128,13 +128,37 @@ func (dd *DataValidation) SetDropList(keys []string) error { return nil } -// SetRange provides function to set data validation range in drop list. -func (dd *DataValidation) SetRange(f1, f2 float64, t DataValidationType, o DataValidationOperator) error { - if math.Abs(f1) > math.MaxFloat32 || math.Abs(f2) > math.MaxFloat32 { - return ErrDataValidationRange +// 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 { + var formula1, formula2 string + switch v := f1.(type) { + case int: + formula1 = fmt.Sprintf("%d", int(v)) + case float64: + if math.Abs(float64(v)) > math.MaxFloat32 { + return ErrDataValidationRange + } + formula1 = fmt.Sprintf("%.17g", float64(v)) + case string: + formula1 = fmt.Sprintf("%s", string(v)) + default: + return ErrParameterInvalid + } + switch v := f2.(type) { + case int: + formula2 = fmt.Sprintf("%d", int(v)) + case float64: + if math.Abs(float64(v)) > math.MaxFloat32 { + return ErrDataValidationRange + } + formula2 = fmt.Sprintf("%.17g", float64(v)) + case string: + formula2 = fmt.Sprintf("%s", string(v)) + default: + return ErrParameterInvalid } - dd.Formula1 = fmt.Sprintf("%.17g", f1) - dd.Formula2 = fmt.Sprintf("%.17g", f2) + dd.Formula1, dd.Formula2 = formula1, formula2 dd.Type = convDataValidationType(t) dd.Operator = convDataValidationOperatior(o) return nil diff --git a/datavalidation_test.go b/datavalidation_test.go index 0cb5929849..5986375f86 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -41,6 +41,15 @@ func TestDataValidation(t *testing.T) { assert.NoError(t, f.AddDataValidation("Sheet1", dvRange)) assert.NoError(t, f.SaveAs(resultFile)) + f.NewSheet("Sheet2") + 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)) + dvRange = NewDataValidation(true) dvRange.Sqref = "A5:B6" for _, listValid := range [][]string{ @@ -86,6 +95,8 @@ func TestDataValidationError(t *testing.T) { return } assert.EqualError(t, err, ErrDataValidationFormulaLenth.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") diff --git a/picture.go b/picture.go index e3601ddde2..5f3a375880 100644 --- a/picture.go +++ b/picture.go @@ -76,14 +76,42 @@ func parseFormatPictureSet(formatSet string) (*formatPicture, error) { // } // } // -// LinkType defines two types of hyperlink "External" for web site or -// "Location" for moving to one of cell in this workbook. When the -// "hyperlink_type" is "Location", coordinates need to start with "#". +// The optional parameter "autofit" specifies if 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_type" 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 a +// 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 +// when the worksheet is printed, the default value of that is 'true'. +// +// The optional parameter "lock_aspect_ratio" 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 +// an object has no effect unless the sheet is protected. +// +// The optional parameter "x_offset" 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 default value of that is 1.0 which presents 100%. +// +// The optional parameter "y_offset" 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 default value of that is 1.0 which presents 100%. // -// Positioning defines two types of the position of a picture 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, default -// positioning is move and size with cells. func (f *File) AddPicture(sheet, cell, picture, format string) error { var err error // Check picture exists first. From c3d1d7ddddd02d9d8d39204dd891250222bb9ee4 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 28 Aug 2021 09:23:44 +0800 Subject: [PATCH 437/957] Preserve XML control character in bstrUnmarshal result --- lib.go | 15 +-------------- lib_test.go | 18 +++++++++--------- 2 files changed, 10 insertions(+), 23 deletions(-) diff --git a/lib.go b/lib.go index 424d3f9b8c..31b64a5951 100644 --- a/lib.go +++ b/lib.go @@ -21,7 +21,6 @@ import ( "regexp" "strconv" "strings" - "unicode" ) // ReadZipReader can be used to read the spreadsheet in memory without touching the @@ -588,10 +587,6 @@ func bstrUnmarshal(s string) (result string) { subStr := s[match[0]:match[1]] if subStr == "_x005F_" { cursor = match[1] - if l > match[1]+6 && !bstrEscapeExp.MatchString(s[match[1]:match[1]+6]) { - result += subStr - continue - } result += "_" continue } @@ -607,15 +602,7 @@ func bstrUnmarshal(s string) (result string) { result += subStr continue } - hasRune := false - for _, c := range v { - if unicode.IsControl(c) { - hasRune = true - } - } - if !hasRune { - result += v - } + result += v } } if cursor < l { diff --git a/lib_test.go b/lib_test.go index 025bc85018..556ed91659 100644 --- a/lib_test.go +++ b/lib_test.go @@ -263,21 +263,21 @@ func TestGenXMLNamespace(t *testing.T) { func TestBstrUnmarshal(t *testing.T) { bstrs := map[string]string{ "*": "*", - "*_x0000_": "*", - "*_x0008_": "*", - "_x0008_*": "*", - "*_x0008_*": "**", + "*_x0000_": "*\x00", + "*_x0008_": "*\b", + "_x0008_*": "\b*", + "*_x0008_*": "*\b*", "*_x4F60__x597D_": "*你好", "*_xG000_": "*_xG000_", "*_xG05F_x0001_*": "*_xG05F*", - "*_x005F__x0008_*": "*_x005F_*", + "*_x005F__x0008_*": "*_\b*", "*_x005F_x0001_*": "*_x0001_*", - "*_x005f_x005F__x0008_*": "*_x005F_*", + "*_x005f_x005F__x0008_*": "*_x005F_\b*", "*_x005F_x005F_xG05F_x0006_*": "*_x005F_xG05F*", "*_x005F_x005F_x005F_x0006_*": "*_x005F_x0006_*", - "_x005F__x0008_******": "_x005F_******", - "******_x005F__x0008_": "******_x005F_", - "******_x005F__x0008_******": "******_x005F_******", + "_x005F__x0008_******": "_\b******", + "******_x005F__x0008_": "******_\b", + "******_x005F__x0008_******": "******_\b******", } for bstr, expected := range bstrs { assert.Equal(t, expected, bstrUnmarshal(bstr)) From 5e1fbd6bf703b9e3ff0eba48c7ee861b99778cba Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 29 Aug 2021 00:03:44 +0800 Subject: [PATCH 438/957] This closes #1008, added new formula functions MATCH and XOR, related issue #65 --- calc.go | 250 +++++++++++++++++++++++++++++++++++++++------------ calc_test.go | 56 ++++++++++++ 2 files changed, 249 insertions(+), 57 deletions(-) diff --git a/calc.go b/calc.go index e59d344bfb..1fdaf6ea27 100644 --- a/calc.go +++ b/calc.go @@ -53,9 +53,8 @@ const ( criteriaGe criteriaL criteriaG - criteriaBeg - criteriaEnd criteriaErr + criteriaRegexp // Numeric precision correct numeric values as legacy Excel application // https://en.wikipedia.org/wiki/Numeric_precision_in_Microsoft_Excel In the // top figure the fraction 1/9000 in Excel is displayed. Although this number @@ -411,6 +410,7 @@ type formulaFuncs struct { // LOG10 // LOOKUP // LOWER +// MATCH // MAX // MDETERM // MEDIAN @@ -504,6 +504,7 @@ type formulaFuncs struct { // VAR.P // VARP // VLOOKUP +// XOR // YEAR // func (f *File) CalcCellValue(sheet, cell string) (result string, err error) { @@ -1285,16 +1286,13 @@ func formulaCriteriaParser(exp string) (fc *formulaCriteria) { fc.Type, fc.Condition = criteriaG, match[1] return } + if strings.Contains(exp, "?") { + exp = strings.ReplaceAll(exp, "?", ".") + } if strings.Contains(exp, "*") { - if strings.HasPrefix(exp, "*") { - fc.Type, fc.Condition = criteriaEnd, strings.TrimPrefix(exp, "*") - } - if strings.HasSuffix(exp, "*") { - fc.Type, fc.Condition = criteriaBeg, strings.TrimSuffix(exp, "*") - } - return + exp = strings.ReplaceAll(exp, "*", ".*") } - fc.Type, fc.Condition = criteriaEq, exp + fc.Type, fc.Condition = criteriaRegexp, exp return } @@ -1326,10 +1324,8 @@ func formulaCriteriaEval(val string, criteria *formulaCriteria) (result bool, er case criteriaG: value, expected, e = prepareValue(val, criteria.Condition) return value > expected && e == nil, err - case criteriaBeg: - return strings.HasPrefix(val, criteria.Condition), err - case criteriaEnd: - return strings.HasSuffix(val, criteria.Condition), err + case criteriaRegexp: + return regexp.MatchString(criteria.Condition, val) } return } @@ -6061,6 +6057,65 @@ func (fn *formulaFuncs) TRUE(argsList *list.List) formulaArg { return newBoolFormulaArg(true) } +// calcXor checking if numeric cell exists and count it by given arguments +// sequence for the formula function XOR. +func calcXor(argsList *list.List) formulaArg { + count, ok := 0, false + for arg := argsList.Front(); arg != nil; arg = arg.Next() { + token := arg.Value.(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 { + count++ + } + case ArgMatrix: + for _, value := range token.ToList() { + if num := value.ToNumber(); num.Type == ArgNumber { + ok = true + if num.Number != 0 { + count++ + } + } + } + } + } + if !ok { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + return newBoolFormulaArg(count%2 != 0) +} + +// XOR function returns the Exclusive Or logical operation for one or more +// supplied conditions. I.e. the Xor function returns TRUE if an odd number +// of the supplied conditions evaluate to TRUE, and FALSE otherwise. The +// syntax of the function is: +// +// 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") + } + return calcXor(argsList) +} + // Date and Time Functions // DATE returns a date, from a user-supplied year, month and day. The syntax @@ -7411,38 +7466,58 @@ func (fn *formulaFuncs) COLUMNS(argsList *list.List) formulaArg { return newNumberFormulaArg(float64(result)) } -// HLOOKUP function 'looks up' a given value in the top row of a data array -// (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]) -// -func (fn *formulaFuncs) HLOOKUP(argsList *list.List) formulaArg { +// checkHVLookupArgs checking arguments, prepare extract mode, lookup value, +// and data for the formula functions HLOOKUP and VLOOKUP. +func checkHVLookupArgs(name string, argsList *list.List) (idx, matchIdx int, wasExact, exactMatch bool, lookupValue, tableArray, errArg formulaArg) { + unit := map[string]string{ + "HLOOKUP": "row", + "VLOOKUP": "col", + }[name] if argsList.Len() < 3 { - return newErrorFormulaArg(formulaErrorVALUE, "HLOOKUP requires at least 3 arguments") + errArg = newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires at least 3 arguments", name)) + return } if argsList.Len() > 4 { - return newErrorFormulaArg(formulaErrorVALUE, "HLOOKUP requires at most 4 arguments") + errArg = newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires at most 4 arguments", name)) + return } - lookupValue := argsList.Front().Value.(formulaArg) - tableArray := argsList.Front().Next().Value.(formulaArg) + lookupValue = argsList.Front().Value.(formulaArg) + tableArray = argsList.Front().Next().Value.(formulaArg) if tableArray.Type != ArgMatrix { - return newErrorFormulaArg(formulaErrorVALUE, "HLOOKUP requires second argument of table array") + errArg = newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires second argument of table array", name)) + return } - rowArg := argsList.Front().Next().Next().Value.(formulaArg).ToNumber() - if rowArg.Type != ArgNumber { - return newErrorFormulaArg(formulaErrorVALUE, "HLOOKUP requires numeric row argument") + arg := argsList.Front().Next().Next().Value.(formulaArg).ToNumber() + if arg.Type != ArgNumber { + errArg = newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires numeric %s argument", name, unit)) + return } - rowIdx, matchIdx, wasExact, exactMatch := int(rowArg.Number)-1, -1, false, false + idx, matchIdx = int(arg.Number)-1, -1 if argsList.Len() == 4 { rangeLookup := argsList.Back().Value.(formulaArg).ToBool() if rangeLookup.Type == ArgError { - return newErrorFormulaArg(formulaErrorVALUE, rangeLookup.Error) + errArg = newErrorFormulaArg(formulaErrorVALUE, rangeLookup.Error) + return } if rangeLookup.Number == 0 { exactMatch = true } } + return +} + +// HLOOKUP function 'looks up' a given value in the top row of a data array +// (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]) +// +func (fn *formulaFuncs) HLOOKUP(argsList *list.List) formulaArg { + rowIdx, matchIdx, wasExact, exactMatch, + lookupValue, tableArray, errArg := checkHVLookupArgs("HLOOKUP", argsList) + if errArg.Type == ArgError { + return errArg + } row := tableArray.Matrix[0] if exactMatch || len(tableArray.Matrix) == TotalRows { start: @@ -7481,6 +7556,87 @@ func (fn *formulaFuncs) HLOOKUP(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorNA, "HLOOKUP no result found") } +// 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 { + switch matchType { + case 0: + for i, arg := range lookupArray { + if ok, _ := formulaCriteriaEval(arg.Value(), criteria); ok { + return newNumberFormulaArg(float64(i + 1)) + } + } + case -1: + for i, arg := range lookupArray { + if ok, _ := formulaCriteriaEval(arg.Value(), criteria); ok { + return newNumberFormulaArg(float64(i + 1)) + } + if ok, _ := formulaCriteriaEval(arg.Value(), &formulaCriteria{ + Type: criteriaL, Condition: criteria.Condition, + }); ok { + if i == 0 { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + } + return newNumberFormulaArg(float64(i)) + } + } + case 1: + for i, arg := range lookupArray { + if ok, _ := formulaCriteriaEval(arg.Value(), criteria); ok { + return newNumberFormulaArg(float64(i + 1)) + } + if ok, _ := formulaCriteriaEval(arg.Value(), &formulaCriteria{ + Type: criteriaG, Condition: criteria.Condition, + }); ok { + if i == 0 { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + } + return newNumberFormulaArg(float64(i)) + } + } + } + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) +} + +// MATCH function looks up a value in an array, and returns the position of +// the value within the array. The user can specify that the function should +// only return a result if an exact match is found, or that the function +// 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]) +// +func (fn *formulaFuncs) MATCH(argsList *list.List) formulaArg { + if argsList.Len() != 2 && argsList.Len() != 3 { + return newErrorFormulaArg(formulaErrorVALUE, "MATCH requires 1 or 2 arguments") + } + var ( + matchType = 1 + lookupArray []formulaArg + lookupArrayArg = argsList.Front().Next().Value.(formulaArg) + lookupArrayErr = "MATCH arguments lookup_array should be one-dimensional array" + ) + if argsList.Len() == 3 { + matchTypeArg := argsList.Back().Value.(formulaArg).ToNumber() + if matchTypeArg.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, "MATCH requires numeric match_type argument") + } + if matchTypeArg.Number == -1 || matchTypeArg.Number == 0 { + matchType = int(matchTypeArg.Number) + } + } + switch lookupArrayArg.Type { + case ArgMatrix: + if len(lookupArrayArg.Matrix[0]) != 1 { + return newErrorFormulaArg(formulaErrorNA, lookupArrayErr) + } + lookupArray = lookupArrayArg.ToList() + default: + return newErrorFormulaArg(formulaErrorNA, lookupArrayErr) + } + return calcMatch(matchType, formulaCriteriaParser(argsList.Front().Value.(formulaArg).String), lookupArray) +} + // VLOOKUP function 'looks up' a given value in the left-hand column of a // data array (or table), and returns the corresponding value from another // column of the array. The syntax of the function is: @@ -7488,30 +7644,10 @@ func (fn *formulaFuncs) HLOOKUP(argsList *list.List) formulaArg { // VLOOKUP(lookup_value,table_array,col_index_num,[range_lookup]) // func (fn *formulaFuncs) VLOOKUP(argsList *list.List) formulaArg { - if argsList.Len() < 3 { - return newErrorFormulaArg(formulaErrorVALUE, "VLOOKUP requires at least 3 arguments") - } - if argsList.Len() > 4 { - return newErrorFormulaArg(formulaErrorVALUE, "VLOOKUP requires at most 4 arguments") - } - lookupValue := argsList.Front().Value.(formulaArg) - tableArray := argsList.Front().Next().Value.(formulaArg) - if tableArray.Type != ArgMatrix { - return newErrorFormulaArg(formulaErrorVALUE, "VLOOKUP requires second argument of table array") - } - colIdx := argsList.Front().Next().Next().Value.(formulaArg).ToNumber() - if colIdx.Type != ArgNumber { - return newErrorFormulaArg(formulaErrorVALUE, "VLOOKUP requires numeric col argument") - } - col, matchIdx, wasExact, exactMatch := int(colIdx.Number)-1, -1, false, false - if argsList.Len() == 4 { - rangeLookup := argsList.Back().Value.(formulaArg).ToBool() - if rangeLookup.Type == ArgError { - return newErrorFormulaArg(formulaErrorVALUE, rangeLookup.Error) - } - if rangeLookup.Number == 0 { - exactMatch = true - } + colIdx, matchIdx, wasExact, exactMatch, + lookupValue, tableArray, errArg := checkHVLookupArgs("VLOOKUP", argsList) + if errArg.Type == ArgError { + return errArg } if exactMatch || len(tableArray.Matrix) == TotalRows { start: @@ -7541,11 +7677,11 @@ func (fn *formulaFuncs) VLOOKUP(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorNA, "VLOOKUP no result found") } mtx := tableArray.Matrix[matchIdx] - if col < 0 || col >= len(mtx) { + if colIdx < 0 || colIdx >= len(mtx) { return newErrorFormulaArg(formulaErrorNA, "VLOOKUP has invalid column index") } if wasExact || !exactMatch { - return mtx[col] + return mtx[colIdx] } return newErrorFormulaArg(formulaErrorNA, "VLOOKUP no result found") } diff --git a/calc_test.go b/calc_test.go index e5a6e1e09d..d526b34064 100644 --- a/calc_test.go +++ b/calc_test.go @@ -926,6 +926,10 @@ func TestCalcCellValue(t *testing.T) { "=OR(1=2,2=3)": "FALSE", // TRUE "=TRUE()": "TRUE", + // XOR + "=XOR(1>0,2>0)": "FALSE", + "=XOR(1>0,0>1)": "TRUE", + "=XOR(1>0,0>1,INT(0),INT(1),A1:A4,2)": "FALSE", // Date and Time Functions // DATE "=DATE(2020,10,21)": "2020-10-21 00:00:00 +0000 UTC", @@ -1946,6 +1950,10 @@ func TestCalcCellValue(t *testing.T) { "=OR(1" + strings.Repeat(",1", 30) + ")": "OR accepts at most 30 arguments", // TRUE "=TRUE(A1)": "TRUE takes no arguments", + // XOR + "=XOR()": "XOR requires at least 1 argument", + "=XOR(\"text\")": "#VALUE!", + "=XOR(XOR(\"text\"))": "#VALUE!", // Date and Time Functions // DATE "=DATE()": "DATE requires 3 number arguments", @@ -2152,6 +2160,12 @@ func TestCalcCellValue(t *testing.T) { "=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", + // 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", // VLOOKUP "=VLOOKUP()": "VLOOKUP requires at least 3 arguments", "=VLOOKUP(D2,D1,1,FALSE)": "VLOOKUP requires second argument of table array", @@ -2691,6 +2705,48 @@ func TestCalcMIRR(t *testing.T) { } } +func TestCalcMATCH(t *testing.T) { + f := NewFile() + for cell, row := range map[string][]interface{}{ + "A1": {"cccc", 7, 4, 16}, + "A2": {"dddd", 2, 6, 11}, + "A3": {"aaaa", 4, 7, 10}, + "A4": {"bbbb", 1, 10, 7}, + "A5": {"eeee", 8, 11, 6}, + "A6": {nil, 11, 16, 4}, + } { + assert.NoError(t, f.SetSheetRow("Sheet1", cell, &row)) + } + formulaList := map[string]string{ + "=MATCH(\"aaaa\",A1:A6,0)": "3", + "=MATCH(\"*b\",A1:A5,0)": "4", + "=MATCH(\"?eee\",A1:A5,0)": "5", + "=MATCH(\"?*?e\",A1:A5,0)": "5", + "=MATCH(\"aaaa\",A1:A6,1)": "3", + "=MATCH(10,B1:B6)": "5", + "=MATCH(8,C1:C6,1)": "3", + "=MATCH(6,B1:B6,-1)": "1", + "=MATCH(10,D1:D6,-1)": "3", + } + 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{ + "=MATCH(3,C1:C6,1)": "#N/A", + "=MATCH(5,C1:C6,-1)": "#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) + } + assert.Equal(t, newErrorFormulaArg(formulaErrorNA, formulaErrorNA), calcMatch(2, nil, []formulaArg{})) +} + func TestStrToDate(t *testing.T) { _, _, _, _, err := strToDate("") assert.Equal(t, formulaErrorVALUE, err.Error) From 2616aa88cb2b1e45c03ada60093f4dfe7fabfb87 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 3 Sep 2021 22:51:56 +0800 Subject: [PATCH 439/957] Add set shared formula support and documentation for the `SetCellFormula` --- cell.go | 94 ++++++++++++++++++++++++++++++++++++++++++++---- cell_test.go | 45 +++++++++++++++++++++++ excelize_test.go | 28 +-------------- merge_test.go | 4 +-- xmlWorksheet.go | 2 +- 5 files changed, 136 insertions(+), 37 deletions(-) diff --git a/cell.go b/cell.go index d44a5527a1..d176991330 100644 --- a/cell.go +++ b/cell.go @@ -391,8 +391,8 @@ func (f *File) GetCellFormula(sheet, axis string) (string, error) { if c.F == nil { return "", false, nil } - if c.F.T == STCellFormulaTypeShared { - return getSharedForumula(x, c.F.Si, c.R), true, nil + if c.F.T == STCellFormulaTypeShared && c.F.Si != nil { + return getSharedForumula(x, *c.F.Si, c.R), true, nil } return c.F.Content, true, nil }) @@ -404,8 +404,50 @@ type FormulaOpts struct { Ref *string // Shared formula ref } -// SetCellFormula provides a function to set cell formula by given string and -// worksheet name. +// 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. +// +// Example 1, set normal formula "=SUM(A1,B1)" for the cell "A3" on "Sheet1": +// +// 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}") +// +// 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\"}") +// +// 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}) +// +// 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}) +// +// +// 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}) +// func (f *File) SetCellFormula(sheet, axis, formula string, opts ...FormulaOpts) error { ws, err := f.workSheetReader(sheet) if err != nil { @@ -430,8 +472,12 @@ func (f *File) SetCellFormula(sheet, axis, formula string, opts ...FormulaOpts) for _, o := range opts { if o.Type != nil { cellData.F.T = *o.Type + if cellData.F.T == STCellFormulaTypeShared { + if err = ws.setSharedFormula(*o.Ref); err != nil { + return err + } + } } - if o.Ref != nil { cellData.F.Ref = *o.Ref } @@ -440,6 +486,40 @@ func (f *File) SetCellFormula(sheet, axis, formula string, opts ...FormulaOpts) return err } +// setSharedFormula set shared formula for the cells. +func (ws *xlsxWorksheet) setSharedFormula(ref string) error { + coordinates, err := areaRefToCoordinates(ref) + if err != nil { + return err + } + _ = sortCoordinates(coordinates) + cnt := ws.countSharedFormula() + for c := coordinates[0]; c <= coordinates[2]; c++ { + for r := coordinates[1]; r <= coordinates[3]; r++ { + prepareSheetXML(ws, c, r) + cell := &ws.SheetData.Row[r-1].C[c-1] + if cell.F == nil { + cell.F = &xlsxF{} + } + cell.F.T = STCellFormulaTypeShared + cell.F.Si = &cnt + } + } + return err +} + +// countSharedFormula count shared formula in the given worksheet. +func (ws *xlsxWorksheet) countSharedFormula() (count int) { + for _, row := range ws.SheetData.Row { + for _, cell := range row.C { + if cell.F != nil && cell.F.Si != nil && *cell.F.Si+1 > count { + count = *cell.F.Si + 1 + } + } + } + return +} + // GetCellHyperLink provides a function to get cell hyperlink by given // worksheet name and axis. Boolean type value link will be ture if the cell // has a hyperlink and the target is the address of the hyperlink. Otherwise, @@ -1027,10 +1107,10 @@ func parseSharedFormula(dCol, dRow int, orig []byte) (res string, start int) { // // Note that this function not validate ref tag to check the cell if or not in // allow area, and always return origin shared formula. -func getSharedForumula(ws *xlsxWorksheet, si string, axis string) string { +func getSharedForumula(ws *xlsxWorksheet, si int, axis 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 == si { + if c.F != nil && c.F.Ref != "" && c.F.T == STCellFormulaTypeShared && c.F.Si != nil && *c.F.Si == si { col, row, _ := CellNameToCoordinates(axis) sharedCol, sharedRow, _ := CellNameToCoordinates(c.R) dCol := col - sharedCol diff --git a/cell_test.go b/cell_test.go index 7a08560ed2..ad784362dc 100644 --- a/cell_test.go +++ b/cell_test.go @@ -313,6 +313,51 @@ func TestOverflowNumericCell(t *testing.T) { // GOARCH=amd64 - all ok; GOARCH=386 - actual: "-2147483648" assert.Equal(t, "8595602512225", val, "A1 should be 8595602512225") } + +func TestSetCellFormula(t *testing.T) { + f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + if !assert.NoError(t, err) { + t.FailNow() + } + + 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. + assert.EqualError(t, f.SetCellFormula("Sheet1", "C", "SUM(Sheet2!D2,Sheet2!D9)"), `cannot convert cell "C" to coordinates: invalid cell name "C"`) + + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellFormula1.xlsx"))) + + f, err = OpenFile(filepath.Join("test", "CalcChain.xlsx")) + if !assert.NoError(t, err) { + t.FailNow() + } + // 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. + assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "")) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellFormula3.xlsx"))) + + // 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})) + } + formulaType, ref := STCellFormulaTypeShared, "C1:C5" + assert.NoError(t, f.SetCellFormula("Sheet1", "C1", "=A1+B1", FormulaOpts{Ref: &ref, Type: &formulaType})) + sharedFormulaSpreadsheet := filepath.Join("test", "TestSetCellFormula4.xlsx") + assert.NoError(t, f.SaveAs(sharedFormulaSpreadsheet)) + + f, err = OpenFile(sharedFormulaSpreadsheet) + assert.NoError(t, err) + ref = "D1:D5" + assert.NoError(t, f.SetCellFormula("Sheet1", "D1", "=A1+C1", FormulaOpts{Ref: &ref, Type: &formulaType})) + ref = "" + 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"))) +} + func TestGetCellRichText(t *testing.T) { f := NewFile() diff --git a/excelize_test.go b/excelize_test.go index f33c3d5818..02abce5fd7 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -83,7 +83,7 @@ func TestOpenFile(t *testing.T) { assert.NoError(t, err) _, err = f.GetCellFormula("Sheet2", "I11") assert.NoError(t, err) - getSharedForumula(&xlsxWorksheet{}, "", "") + getSharedForumula(&xlsxWorksheet{}, 0, "") // Test read cell value with given illegal rows number. _, err = f.GetCellValue("Sheet2", "a-1") @@ -401,32 +401,6 @@ func TestGetCellHyperLink(t *testing.T) { } -func TestSetCellFormula(t *testing.T) { - f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } - - 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. - assert.EqualError(t, f.SetCellFormula("Sheet1", "C", "SUM(Sheet2!D2,Sheet2!D9)"), `cannot convert cell "C" to coordinates: invalid cell name "C"`) - - assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellFormula1.xlsx"))) - - f, err = OpenFile(filepath.Join("test", "CalcChain.xlsx")) - if !assert.NoError(t, err) { - t.FailNow() - } - // 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. - assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "")) - assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellFormula3.xlsx"))) -} - func TestSetSheetBackground(t *testing.T) { f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if !assert.NoError(t, err) { diff --git a/merge_test.go b/merge_test.go index 02d92fbdef..88fe4f9cd5 100644 --- a/merge_test.go +++ b/merge_test.go @@ -181,7 +181,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"), "parameter is invalid") + assert.EqualError(t, f.UnmergeCell("Sheet1", "A2", "B3"), ErrParameterInvalid.Error()) ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml") assert.True(t, ok) @@ -191,5 +191,5 @@ func TestUnmergeCell(t *testing.T) { func TestFlatMergedCells(t *testing.T) { ws := &xlsxWorksheet{MergeCells: &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A1"}}}} - assert.EqualError(t, flatMergedCells(ws, [][]*xlsxMergeCell{}), "parameter is invalid") + assert.EqualError(t, flatMergedCells(ws, [][]*xlsxMergeCell{}), ErrParameterInvalid.Error()) } diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 697504e6ce..a4aef4ce77 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -475,7 +475,7 @@ type xlsxF struct { Content string `xml:",chardata"` T string `xml:"t,attr,omitempty"` // Formula type Ref string `xml:"ref,attr,omitempty"` // Shared formula ref - Si string `xml:"si,attr,omitempty"` // Shared formula index + Si *int `xml:"si,attr"` // Shared formula index } // xlsxSheetProtection collection expresses the sheet protection options to From 32b23ef42d3ecb393e102c5f63ab5125db354435 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 5 Sep 2021 11:59:50 +0800 Subject: [PATCH 440/957] This closes #998 - Support text comparison in the formula, also ref #65 - `GetCellValue`, `GetRows`, `GetCols`, `Rows` and `Cols` support to specify read cell with raw value, ref #621 - Add missing properties for the cell formula - Update the unit test for the `CalcCellValue` --- calc.go | 92 +++++++++++++++++++++---------------------------- calc_test.go | 26 +++++++++----- cell.go | 47 ++++++++++++++++++++++--- cell_test.go | 24 +++++++++---- col.go | 10 +++--- excelize.go | 23 +++++++++---- rows.go | 30 ++++++++-------- rows_test.go | 4 +-- sheet.go | 2 +- xmlWorksheet.go | 13 +++++-- 10 files changed, 170 insertions(+), 101 deletions(-) diff --git a/calc.go b/calc.go index 1fdaf6ea27..5661d7d507 100644 --- a/calc.go +++ b/calc.go @@ -777,57 +777,25 @@ func calcNEq(rOpd, lOpd string, opdStack *Stack) error { // calcL evaluate less than arithmetic operations. func calcL(rOpd, lOpd string, opdStack *Stack) error { - lOpdVal, err := strconv.ParseFloat(lOpd, 64) - if err != nil { - return err - } - rOpdVal, err := strconv.ParseFloat(rOpd, 64) - if err != nil { - return err - } - opdStack.Push(efp.Token{TValue: strings.ToUpper(strconv.FormatBool(rOpdVal > lOpdVal)), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + opdStack.Push(efp.Token{TValue: strings.ToUpper(strconv.FormatBool(strings.Compare(lOpd, rOpd) == -1)), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) return nil } // calcLe evaluate less than or equal arithmetic operations. func calcLe(rOpd, lOpd string, opdStack *Stack) error { - lOpdVal, err := strconv.ParseFloat(lOpd, 64) - if err != nil { - return err - } - rOpdVal, err := strconv.ParseFloat(rOpd, 64) - if err != nil { - return err - } - opdStack.Push(efp.Token{TValue: strings.ToUpper(strconv.FormatBool(rOpdVal >= lOpdVal)), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + opdStack.Push(efp.Token{TValue: strings.ToUpper(strconv.FormatBool(strings.Compare(lOpd, rOpd) != 1)), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) return nil } // calcG evaluate greater than or equal arithmetic operations. func calcG(rOpd, lOpd string, opdStack *Stack) error { - lOpdVal, err := strconv.ParseFloat(lOpd, 64) - if err != nil { - return err - } - rOpdVal, err := strconv.ParseFloat(rOpd, 64) - if err != nil { - return err - } - opdStack.Push(efp.Token{TValue: strings.ToUpper(strconv.FormatBool(rOpdVal < lOpdVal)), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + opdStack.Push(efp.Token{TValue: strings.ToUpper(strconv.FormatBool(strings.Compare(lOpd, rOpd) == 1)), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) return nil } // calcGe evaluate greater than or equal arithmetic operations. func calcGe(rOpd, lOpd string, opdStack *Stack) error { - lOpdVal, err := strconv.ParseFloat(lOpd, 64) - if err != nil { - return err - } - rOpdVal, err := strconv.ParseFloat(rOpd, 64) - if err != nil { - return err - } - opdStack.Push(efp.Token{TValue: strings.ToUpper(strconv.FormatBool(rOpdVal <= lOpdVal)), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + opdStack.Push(efp.Token{TValue: strings.ToUpper(strconv.FormatBool(strings.Compare(lOpd, rOpd) != -1)), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) return nil } @@ -1214,7 +1182,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); err != nil { + if value, err = f.GetCellValue(sheet, cell, Options{RawCellValue: true}); err != nil { return } matrixRow = append(matrixRow, formulaArg{ @@ -1233,7 +1201,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); err != nil { + if arg.String, err = f.GetCellValue(cr.Sheet, cell, Options{RawCellValue: true}); err != nil { return } arg.Type = ArgString @@ -7749,28 +7717,33 @@ func hlookupBinarySearch(row []formulaArg, lookupValue formulaArg) (matchIdx int return } -// LOOKUP function performs an approximate match lookup in a one-column or -// 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]) -// -func (fn *formulaFuncs) LOOKUP(argsList *list.List) formulaArg { +// checkLookupArgs checking arguments, prepare lookup value, and data for the +// formula function LOOKUP. +func checkLookupArgs(argsList *list.List) (arrayForm bool, lookupValue, lookupVector, errArg formulaArg) { if argsList.Len() < 2 { - return newErrorFormulaArg(formulaErrorVALUE, "LOOKUP requires at least 2 arguments") + errArg = newErrorFormulaArg(formulaErrorVALUE, "LOOKUP requires at least 2 arguments") + return } if argsList.Len() > 3 { - return newErrorFormulaArg(formulaErrorVALUE, "LOOKUP requires at most 3 arguments") + errArg = newErrorFormulaArg(formulaErrorVALUE, "LOOKUP requires at most 3 arguments") + return } - lookupValue := argsList.Front().Value.(formulaArg) - lookupVector := argsList.Front().Next().Value.(formulaArg) + lookupValue = argsList.Front().Value.(formulaArg) + lookupVector = argsList.Front().Next().Value.(formulaArg) if lookupVector.Type != ArgMatrix && lookupVector.Type != ArgList { - return newErrorFormulaArg(formulaErrorVALUE, "LOOKUP requires second argument of table array") + errArg = newErrorFormulaArg(formulaErrorVALUE, "LOOKUP requires second argument of table array") + return } - arrayForm := lookupVector.Type == ArgMatrix + arrayForm = lookupVector.Type == ArgMatrix if arrayForm && len(lookupVector.Matrix) == 0 { - return newErrorFormulaArg(formulaErrorVALUE, "LOOKUP requires not empty range as second argument") + errArg = newErrorFormulaArg(formulaErrorVALUE, "LOOKUP requires not empty range as second argument") } + return +} + +// iterateLookupArgs iterate arguments to extract columns and calculate match +// index for the formula function LOOKUP. +func iterateLookupArgs(lookupValue, lookupVector formulaArg) ([]formulaArg, int, bool) { cols, matchIdx, ok := lookupCol(lookupVector, 0), -1, false for idx, col := range cols { lhs := lookupValue @@ -7796,6 +7769,21 @@ func (fn *formulaFuncs) LOOKUP(argsList *list.List) formulaArg { matchIdx = idx - 1 } } + return cols, matchIdx, ok +} + +// LOOKUP function performs an approximate match lookup in a one-column or +// 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]) +// +func (fn *formulaFuncs) LOOKUP(argsList *list.List) formulaArg { + arrayForm, lookupValue, lookupVector, errArg := checkLookupArgs(argsList) + if errArg.Type == ArgError { + return errArg + } + cols, matchIdx, ok := iterateLookupArgs(lookupValue, lookupVector) if ok && matchIdx == -1 { matchIdx = len(cols) - 1 } diff --git a/calc_test.go b/calc_test.go index d526b34064..4c32983178 100644 --- a/calc_test.go +++ b/calc_test.go @@ -2452,17 +2452,27 @@ func TestCalcWithDefinedName(t *testing.T) { } func TestCalcArithmeticOperations(t *testing.T) { + opdStack := NewStack() + for _, test := range [][]string{{"1", "text", "FALSE"}, {"text", "1", "TRUE"}} { + assert.NoError(t, calcL(test[0], test[1], opdStack)) + assert.Equal(t, test[2], opdStack.Peek().(efp.Token).TValue) + opdStack.Empty() + assert.NoError(t, calcLe(test[0], test[1], opdStack)) + assert.Equal(t, test[2], opdStack.Peek().(efp.Token).TValue) + opdStack.Empty() + } + for _, test := range [][]string{{"1", "text", "TRUE"}, {"text", "1", "FALSE"}} { + assert.NoError(t, calcG(test[0], test[1], opdStack)) + assert.Equal(t, test[2], opdStack.Peek().(efp.Token).TValue) + opdStack.Empty() + assert.NoError(t, calcGe(test[0], test[1], opdStack)) + assert.Equal(t, test[2], opdStack.Peek().(efp.Token).TValue) + opdStack.Empty() + } + err := `strconv.ParseFloat: parsing "text": invalid syntax` assert.EqualError(t, calcPow("1", "text", nil), err) assert.EqualError(t, calcPow("text", "1", nil), err) - assert.EqualError(t, calcL("1", "text", nil), err) - assert.EqualError(t, calcL("text", "1", nil), err) - assert.EqualError(t, calcLe("1", "text", nil), err) - assert.EqualError(t, calcLe("text", "1", nil), err) - assert.EqualError(t, calcG("1", "text", nil), err) - assert.EqualError(t, calcG("text", "1", nil), err) - assert.EqualError(t, calcGe("1", "text", nil), err) - assert.EqualError(t, calcGe("text", "1", nil), err) assert.EqualError(t, calcAdd("1", "text", nil), err) assert.EqualError(t, calcAdd("text", "1", nil), err) assert.EqualError(t, calcAdd("1", "text", nil), err) diff --git a/cell.go b/cell.go index d176991330..59d3bbd751 100644 --- a/cell.go +++ b/cell.go @@ -36,9 +36,9 @@ const ( // 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. -func (f *File) GetCellValue(sheet, axis string) (string, error) { +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()) + val, err := c.getValueFrom(f, f.sharedStringsReader(), parseOptions(opts...).RawCellValue) return val, true, err }) } @@ -440,7 +440,6 @@ type FormulaOpts struct { // 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: // @@ -448,6 +447,41 @@ type FormulaOpts struct { // 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" +// +// "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 { @@ -471,6 +505,9 @@ func (f *File) SetCellFormula(sheet, axis, formula string, opts ...FormulaOpts) for _, o := range opts { if o.Type != nil { + if *o.Type == STCellFormulaTypeDataTable { + return err + } cellData.F.T = *o.Type if cellData.F.T == STCellFormulaTypeShared { if err = ws.setSharedFormula(*o.Ref); err != nil { @@ -955,8 +992,8 @@ func (f *File) getCellStringFunc(sheet, axis 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) string { - if s == 0 { +func (f *File) formattedValue(s int, v string, raw bool) string { + if s == 0 || raw { return v } styleSheet := f.stylesReader() diff --git a/cell_test.go b/cell_test.go index ad784362dc..d56854b777 100644 --- a/cell_test.go +++ b/cell_test.go @@ -356,6 +356,16 @@ func TestSetCellFormula(t *testing.T) { ref = "" 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. + 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)) + } + assert.NoError(t, f.AddTable("Sheet1", "A1", "C2", `{"table_name":"Table1","table_style":"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"))) } func TestGetCellRichText(t *testing.T) { @@ -503,20 +513,20 @@ func TestSetCellRichText(t *testing.T) { func TestFormattedValue2(t *testing.T) { f := NewFile() - v := f.formattedValue(0, "43528") + v := f.formattedValue(0, "43528", false) assert.Equal(t, "43528", v) - v = f.formattedValue(15, "43528") + v = f.formattedValue(15, "43528", false) assert.Equal(t, "43528", v) - v = f.formattedValue(1, "43528") + v = f.formattedValue(1, "43528", false) assert.Equal(t, "43528", v) customNumFmt := "[$-409]MM/DD/YYYY" _, err := f.NewStyle(&Style{ CustomNumFmt: &customNumFmt, }) assert.NoError(t, err) - v = f.formattedValue(1, "43528") + v = f.formattedValue(1, "43528", false) assert.Equal(t, "03/04/2019", v) // formatted value with no built-in number format ID @@ -524,20 +534,20 @@ func TestFormattedValue2(t *testing.T) { f.Styles.CellXfs.Xf = append(f.Styles.CellXfs.Xf, xlsxXf{ NumFmtID: &numFmtID, }) - v = f.formattedValue(2, "43528") + v = f.formattedValue(2, "43528", false) assert.Equal(t, "43528", v) // formatted value with invalid number format ID f.Styles.CellXfs.Xf = append(f.Styles.CellXfs.Xf, xlsxXf{ NumFmtID: nil, }) - _ = f.formattedValue(3, "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") + v = f.formattedValue(1, "43528", false) assert.Equal(t, "43528", v) } diff --git a/col.go b/col.go index 1e0c333430..5ba5caa6cf 100644 --- a/col.go +++ b/col.go @@ -34,6 +34,7 @@ const ( type Cols struct { err error curCol, totalCol, stashCol, totalRow int + rawCellValue bool sheet string f *File sheetXML []byte @@ -54,14 +55,14 @@ type Cols struct { // fmt.Println() // } // -func (f *File) GetCols(sheet string) ([][]string, error) { +func (f *File) GetCols(sheet string, opts ...Options) ([][]string, error) { cols, err := f.Cols(sheet) if err != nil { return nil, err } results := make([][]string, 0, 64) for cols.Next() { - col, _ := cols.Rows() + col, _ := cols.Rows(opts...) results = append(results, col) } return results, nil @@ -79,7 +80,7 @@ func (cols *Cols) Error() error { } // Rows return the current column's row values. -func (cols *Cols) Rows() ([]string, error) { +func (cols *Cols) Rows(opts ...Options) ([]string, error) { var ( err error inElement string @@ -89,6 +90,7 @@ func (cols *Cols) Rows() ([]string, error) { if cols.stashCol >= cols.curCol { return rows, err } + cols.rawCellValue = parseOptions(opts...).RawCellValue d := cols.f.sharedStringsReader() decoder := cols.f.xmlNewDecoder(bytes.NewReader(cols.sheetXML)) for { @@ -123,7 +125,7 @@ func (cols *Cols) Rows() ([]string, error) { if cellCol == cols.curCol { colCell := xlsxC{} _ = decoder.DecodeElement(&colCell, &xmlElement) - val, _ := colCell.getValueFrom(cols.f, d) + val, _ := colCell.getValueFrom(cols.f, d, cols.rawCellValue) rows = append(rows, val) } } diff --git a/excelize.go b/excelize.go index 11ddf921c3..def018bc93 100644 --- a/excelize.go +++ b/excelize.go @@ -58,9 +58,12 @@ type File struct { type charsetTranscoderFn func(charset string, input io.Reader) (rdr io.Reader, err error) -// Options define the options for open spreadsheet. +// Options define the options for open and reading spreadsheet. RawCellValue +// specify if apply the number format for the cell value or get the raw +// value. type Options struct { Password string + RawCellValue bool UnzipSizeLimit int64 } @@ -119,11 +122,9 @@ func OpenReader(r io.Reader, opt ...Options) (*File, error) { return nil, err } f := newFile() - for i := range opt { - f.options = &opt[i] - if f.options.UnzipSizeLimit == 0 { - f.options.UnzipSizeLimit = UnzipSizeLimit - } + f.options = parseOptions(opt...) + if f.options.UnzipSizeLimit == 0 { + f.options.UnzipSizeLimit = UnzipSizeLimit } if bytes.Contains(b, oleIdentifier) { b, err = Decrypt(b, f.options) @@ -150,6 +151,16 @@ func OpenReader(r io.Reader, opt ...Options) (*File, error) { return f, nil } +// 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 + } + return opt +} + // CharsetTranscoder Set user defined codepage transcoder function for open // XLSX from non UTF-8 encoding. func (f *File) CharsetTranscoder(fn charsetTranscoderFn) *File { f.CharsetReader = fn; return f } diff --git a/rows.go b/rows.go index bfd7d13f0b..057cbc8fc4 100644 --- a/rows.go +++ b/rows.go @@ -43,7 +43,7 @@ import ( // fmt.Println() // } // -func (f *File) GetRows(sheet string) ([][]string, error) { +func (f *File) GetRows(sheet string, opts ...Options) ([][]string, error) { rows, err := f.Rows(sheet) if err != nil { return nil, err @@ -51,7 +51,7 @@ func (f *File) GetRows(sheet string) ([][]string, error) { results, cur, max := make([][]string, 0, 64), 0, 0 for rows.Next() { cur++ - row, err := rows.Columns() + row, err := rows.Columns(opts...) if err != nil { break } @@ -67,6 +67,7 @@ func (f *File) GetRows(sheet string) ([][]string, error) { type Rows struct { err error curRow, totalRow, stashRow int + rawCellValue bool sheet string f *File decoder *xml.Decoder @@ -84,11 +85,12 @@ func (rows *Rows) Error() error { } // Columns return the current row's column values. -func (rows *Rows) Columns() ([]string, error) { +func (rows *Rows) Columns(opts ...Options) ([]string, error) { var rowIterator rowXMLIterator if rows.stashRow >= rows.curRow { return rowIterator.columns, rowIterator.err } + rows.rawCellValue = parseOptions(opts...).RawCellValue rowIterator.rows = rows rowIterator.d = rows.f.sharedStringsReader() for { @@ -109,7 +111,7 @@ func (rows *Rows) Columns() ([]string, error) { return rowIterator.columns, rowIterator.err } } - rowXMLHandler(&rowIterator, &xmlElement) + rowXMLHandler(&rowIterator, &xmlElement, rows.rawCellValue) if rowIterator.err != nil { return rowIterator.columns, rowIterator.err } @@ -157,7 +159,7 @@ type rowXMLIterator struct { } // rowXMLHandler parse the row XML element of the worksheet. -func rowXMLHandler(rowIterator *rowXMLIterator, xmlElement *xml.StartElement) { +func rowXMLHandler(rowIterator *rowXMLIterator, xmlElement *xml.StartElement, raw bool) { rowIterator.err = nil if rowIterator.inElement == "c" { rowIterator.cellCol++ @@ -169,7 +171,7 @@ func rowXMLHandler(rowIterator *rowXMLIterator, xmlElement *xml.StartElement) { } } blank := rowIterator.cellCol - len(rowIterator.columns) - val, _ := colCell.getValueFrom(rowIterator.rows.f, rowIterator.d) + val, _ := colCell.getValueFrom(rowIterator.rows.f, rowIterator.d, raw) if val != "" || colCell.F != nil { rowIterator.columns = append(appendSpace(blank, rowIterator.columns), val) } @@ -361,7 +363,7 @@ func (f *File) sharedStringsReader() *xlsxSST { // getValueFrom return a value from a column/row cell, this function is // inteded to be used with for range on rows an argument with the spreadsheet // opened file. -func (c *xlsxC) getValueFrom(f *File, d *xlsxSST) (string, error) { +func (c *xlsxC) getValueFrom(f *File, d *xlsxSST, raw bool) (string, error) { f.Lock() defer f.Unlock() switch c.T { @@ -370,26 +372,26 @@ func (c *xlsxC) getValueFrom(f *File, d *xlsxSST) (string, error) { xlsxSI := 0 xlsxSI, _ = strconv.Atoi(c.V) if len(d.SI) > xlsxSI { - return f.formattedValue(c.S, d.SI[xlsxSI].String()), nil + return f.formattedValue(c.S, d.SI[xlsxSI].String(), raw), nil } } - return f.formattedValue(c.S, c.V), nil + return f.formattedValue(c.S, c.V, raw), nil case "str": - return f.formattedValue(c.S, c.V), nil + return f.formattedValue(c.S, c.V, raw), nil case "inlineStr": if c.IS != nil { - return f.formattedValue(c.S, c.IS.String()), nil + return f.formattedValue(c.S, c.IS.String(), raw), nil } - return f.formattedValue(c.S, c.V), nil + return f.formattedValue(c.S, c.V, raw), nil default: isNum, precision := isNumeric(c.V) if isNum && precision > 10 { val, _ := roundPrecision(c.V) if val != c.V { - return f.formattedValue(c.S, val), nil + return f.formattedValue(c.S, val, raw), nil } } - return f.formattedValue(c.S, c.V), nil + return f.formattedValue(c.S, c.V, raw), nil } } diff --git a/rows_test.go b/rows_test.go index a54e755df5..c0dc1d8aef 100644 --- a/rows_test.go +++ b/rows_test.go @@ -845,7 +845,7 @@ func TestGetValueFromInlineStr(t *testing.T) { c := &xlsxC{T: "inlineStr"} f := NewFile() d := &xlsxSST{} - val, err := c.getValueFrom(f, d) + val, err := c.getValueFrom(f, d, false) assert.NoError(t, err) assert.Equal(t, "", val) } @@ -865,7 +865,7 @@ func TestGetValueFromNumber(t *testing.T) { "2.220000ddsf0000000002-r": "2.220000ddsf0000000002-r", } { c.V = input - val, err := c.getValueFrom(f, d) + val, err := c.getValueFrom(f, d, false) assert.NoError(t, err) assert.Equal(t, expected, val) } diff --git a/sheet.go b/sheet.go index 7e15bbe5d0..be2e964bca 100644 --- a/sheet.go +++ b/sheet.go @@ -880,7 +880,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) + val, _ := colCell.getValueFrom(f, d, false) if regSearch { regex := regexp.MustCompile(value) if !regex.MatchString(val) { diff --git a/xmlWorksheet.go b/xmlWorksheet.go index a4aef4ce77..217f367d4e 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -473,9 +473,18 @@ type xlsxC struct { // contained in the character node of this element. type xlsxF struct { Content string `xml:",chardata"` - T string `xml:"t,attr,omitempty"` // Formula type + T string `xml:"t,attr,omitempty"` // Formula type + Aca bool `xml:"aca,attr,omitempty"` Ref string `xml:"ref,attr,omitempty"` // Shared formula ref - Si *int `xml:"si,attr"` // Shared formula index + Dt2D bool `xml:"dt2D,attr,omitempty"` + Dtr bool `xml:"dtr,attr,omitempty"` + Del1 bool `xml:"del1,attr,omitempty"` + Del2 bool `xml:"del2,attr,omitempty"` + R1 string `xml:"r1,attr,omitempty"` + R2 string `xml:"r2,attr,omitempty"` + Ca bool `xml:"ca,attr,omitempty"` + Si *int `xml:"si,attr"` // Shared formula index + Bx bool `xml:"bx,attr,omitempty"` } // xlsxSheetProtection collection expresses the sheet protection options to From 684603befa0fbde2ee8db704e37a544f9d92d99d Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 6 Sep 2021 00:01:42 +0800 Subject: [PATCH 441/957] This closes #993, closes #1014 - Fix formula percentages calculated incorrectly - Make UpdateLinkedValue skip macro sheet - Fix conditional format bottom N not working --- calc.go | 14 +++++++++++++- calc_test.go | 2 ++ chart_test.go | 2 +- excelize.go | 6 +++--- styles.go | 1 + 5 files changed, 20 insertions(+), 5 deletions(-) diff --git a/calc.go b/calc.go index 5661d7d507..ef8d0b037f 100644 --- a/calc.go +++ b/calc.go @@ -975,6 +975,11 @@ 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. +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 @@ -1034,8 +1039,15 @@ 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 + } // opd - if token.TType == efp.TokenTypeOperand && (token.TSubType == efp.TokenSubTypeNumber || token.TSubType == efp.TokenSubTypeText) { + if isOperand(token) { opdStack.Push(token) } return nil diff --git a/calc_test.go b/calc_test.go index 4c32983178..cd09e9729a 100644 --- a/calc_test.go +++ b/calc_test.go @@ -46,6 +46,8 @@ func TestCalcCellValue(t *testing.T) { "=2>=1": "TRUE", "=2>=3": "FALSE", "=1&2": "12", + "=15%": "0.15", + "=1+20%": "1.2", `="A"="A"`: "TRUE", `="A"<>"A"`: "FALSE", // Engineering Functions diff --git a/chart_test.go b/chart_test.go index 8957b93f80..d2a3975530 100644 --- a/chart_test.go +++ b/chart_test.go @@ -230,7 +230,7 @@ func TestAddChartSheet(t *testing.T) { f.SetActiveSheet(sheetIdx) // Test cell value on chartsheet - assert.EqualError(t, f.SetCellValue("Chart1", "A1", true), "sheet Chart1 is chart sheet") + 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()) // Test with unsupported chart type diff --git a/excelize.go b/excelize.go index def018bc93..24a1a4e146 100644 --- a/excelize.go +++ b/excelize.go @@ -204,8 +204,8 @@ func (f *File) workSheetReader(sheet string) (ws *xlsxWorksheet, err error) { ws = worksheet.(*xlsxWorksheet) return } - if strings.HasPrefix(name, "xl/chartsheets") { - err = fmt.Errorf("sheet %s is chart sheet", sheet) + if strings.HasPrefix(name, "xl/chartsheets") || strings.HasPrefix(name, "xl/macrosheet") { + err = fmt.Errorf("sheet %s is not a worksheet", sheet) return } ws = new(xlsxWorksheet) @@ -367,7 +367,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 chart sheet", trimSheetName(name)) { + if err.Error() == fmt.Sprintf("sheet %s is not a worksheet", trimSheetName(name)) { continue } return err diff --git a/styles.go b/styles.go index 31fa7fb349..e4f3a276ab 100644 --- a/styles.go +++ b/styles.go @@ -3011,6 +3011,7 @@ func drawCondFmtCellIs(p int, ct string, format *formatConditional) *xlsxCfRule func drawCondFmtTop10(p int, ct string, format *formatConditional) *xlsxCfRule { c := &xlsxCfRule{ Priority: p + 1, + Bottom: format.Type == "bottom", Type: validType[format.Type], Rank: 10, DxfID: &format.Format, From 72d84c0cbdd0ad748dba19e21d4e92ea077110c7 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 8 Sep 2021 22:05:42 +0800 Subject: [PATCH 442/957] This closes #262, support set line width of add the shape --- calc.go | 2 +- shape.go | 34 +++++++++++++++++++++++++- shape_test.go | 68 ++++++++++++++++++++++++++++++++++++++++++++++++--- xmlDrawing.go | 18 ++++++++++++-- 4 files changed, 115 insertions(+), 7 deletions(-) diff --git a/calc.go b/calc.go index ef8d0b037f..d650eca532 100644 --- a/calc.go +++ b/calc.go @@ -948,7 +948,7 @@ func (f *File) parseOperatorPrefixToken(optStack, opdStack *Stack, token efp.Tok return } -// isFunctionStartToken determine if the token is function stop. +// isFunctionStartToken determine if the token is function start. func isFunctionStartToken(token efp.Token) bool { return token.TType == efp.TokenTypeFunction && token.TSubType == efp.TokenSubTypeStart } diff --git a/shape.go b/shape.go index e58d5cf1a4..61322dd981 100644 --- a/shape.go +++ b/shape.go @@ -32,6 +32,7 @@ func parseFormatShapeSet(formatSet string) (*formatShape, error) { XScale: 1.0, YScale: 1.0, }, + Line: formatLine{Width: 1}, } err := json.Unmarshal([]byte(formatSet), &format) return &format, err @@ -42,7 +43,33 @@ 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}`) +// 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: // @@ -378,6 +405,11 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, formatSet *format }, }, } + if formatSet.Line.Width != 1 { + shape.SpPr.Ln = xlsxLineProperties{ + W: f.ptToEMUs(formatSet.Line.Width), + } + } if len(formatSet.Paragraph) < 1 { formatSet.Paragraph = []formatShapeParagraph{ { diff --git a/shape_test.go b/shape_test.go index 61fb443d7d..a02e53da0e 100644 --- a/shape_test.go +++ b/shape_test.go @@ -16,13 +16,75 @@ func TestAddShape(t *testing.T) { 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"}, "paragraph":[{"font":{"bold":true,"italic":true,"family":"Times New Roman","size":36,"color":"#777777","underline":"single"}}], "height": 90}`), "sheet Sheet3 is not exist") + assert.EqualError(t, f.AddShape("Sheet3", "H1", `{ + "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 + }`), "sheet Sheet3 is 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"}},{"text":"Shape","font":{"bold":true,"color":"2980B9"}}]}`), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.EqualError(t, f.AddShape("Sheet1", "A", `{ + "type": "rect", + "paragraph": [ + { + "text": "Rectangle", + "font": + { + "color": "CD5C5C" + } + }, + { + "text": "Shape", + "font": + { + "bold": true, + "color": "2980B9" + } + }] + }`), `cannot convert cell "A" to coordinates: invalid cell name "A"`) 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}`)) + 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 + } + }`)) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddShape2.xlsx"))) } diff --git a/xmlDrawing.go b/xmlDrawing.go index b49ae9d9ef..0bb11ace3b 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -234,14 +234,22 @@ type xlsxBlipFill struct { Stretch xlsxStretch `xml:"a:stretch"` } +// xlsxLineProperties specifies the width of a line in EMUs. This simple type +// has a minimum value of greater than or equal to 0. This simple type has a +// maximum value of less than or equal to 20116800. +type xlsxLineProperties struct { + W int `xml:"w,attr,omitempty"` +} + // xlsxSpPr directly maps the spPr (Shape Properties). This element specifies // the visual shape properties that can be applied to a picture. These are the // same properties that are allowed to describe the visual properties of a shape // but are used here to describe the visual appearance of a picture within a // document. type xlsxSpPr struct { - Xfrm xlsxXfrm `xml:"a:xfrm"` - PrstGeom xlsxPrstGeom `xml:"a:prstGeom"` + Xfrm xlsxXfrm `xml:"a:xfrm"` + PrstGeom xlsxPrstGeom `xml:"a:prstGeom"` + Ln xlsxLineProperties `xml:"a:ln"` } // xlsxPic elements encompass the definition of pictures within the DrawingML @@ -469,6 +477,7 @@ type formatShape struct { Height int `json:"height"` Format formatPicture `json:"format"` Color formatShapeColor `json:"color"` + Line formatLine `json:"line"` Paragraph []formatShapeParagraph `json:"paragraph"` } @@ -485,3 +494,8 @@ type formatShapeColor struct { Fill string `json:"fill"` Effect string `json:"effect"` } + +// formatLine directly maps the line settings of the shape. +type formatLine struct { + Width float64 `json:"width"` +} From dad8f490cc2df664bf1e7c6770ecd89a0c0e7fe4 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 9 Sep 2021 23:43:16 +0800 Subject: [PATCH 443/957] This closes #417 and closes #520, new API `GetCellType` has been added --- calc.go | 2 +- cell.go | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ cell_test.go | 13 +++++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/calc.go b/calc.go index d650eca532..59d97e048b 100644 --- a/calc.go +++ b/calc.go @@ -158,7 +158,7 @@ type formulaCriteria struct { Condition string } -// ArgType is the type if formula argument type. +// ArgType is the type of formula argument type. type ArgType byte // Formula argument types enumeration. diff --git a/cell.go b/cell.go index 59d3bbd751..2d49a5e0ff 100644 --- a/cell.go +++ b/cell.go @@ -20,6 +20,19 @@ import ( "time" ) +// CellType is the type of cell value type. +type CellType byte + +// Cell value types enumeration. +const ( + CellTypeUnset CellType = iota + CellTypeBool + CellTypeDate + CellTypeError + CellTypeNumber + CellTypeString +) + const ( // STCellFormulaTypeArray defined the formula is an array formula. STCellFormulaTypeArray = "array" @@ -31,6 +44,17 @@ const ( STCellFormulaTypeShared = "shared" ) +// cellTypes mapping the cell's data type and enumeration. +var cellTypes = map[string]CellType{ + "b": CellTypeBool, + "d": CellTypeDate, + "n": CellTypeNumber, + "e": CellTypeError, + "s": CellTypeString, + "str": CellTypeString, + "inlineStr": CellTypeString, +} + // 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 @@ -43,6 +67,32 @@ func (f *File) GetCellValue(sheet, axis string, opts ...Options) (string, error) }) } +// 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) { + cellTypes := map[string]CellType{ + "b": CellTypeBool, + "d": CellTypeDate, + "n": CellTypeNumber, + "e": CellTypeError, + "s": CellTypeString, + "str": CellTypeString, + "inlineStr": CellTypeString, + } + var ( + err error + cellTypeStr string + cellType CellType = CellTypeUnset + ) + if cellTypeStr, err = f.getCellStringFunc(sheet, axis, func(x *xlsxWorksheet, c *xlsxC) (string, bool, error) { + return c.T, true, nil + }); err != nil { + return CellTypeUnset, err + } + cellType = cellTypes[cellTypeStr] + 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 diff --git a/cell_test.go b/cell_test.go index d56854b777..5467e430d0 100644 --- a/cell_test.go +++ b/cell_test.go @@ -244,6 +244,19 @@ func TestGetCellValue(t *testing.T) { assert.NoError(t, err) } +func TestGetCellType(t *testing.T) { + f := NewFile() + cellType, err := f.GetCellType("Sheet1", "A1") + assert.NoError(t, err) + assert.Equal(t, CellTypeUnset, cellType) + assert.NoError(t, f.SetCellValue("Sheet1", "A1", "A1")) + cellType, err = f.GetCellType("Sheet1", "A1") + assert.NoError(t, err) + assert.Equal(t, CellTypeString, cellType) + _, err = f.GetCellType("Sheet1", "A") + assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`) +} + func TestGetCellFormula(t *testing.T) { // Test get cell formula on not exist worksheet. f := NewFile() From 52609ba52678642810363cba485e60f370850ddc Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 10 Sep 2021 23:19:59 +0800 Subject: [PATCH 444/957] This closes #1017, fix duplicate image caused by incorrect internal relationships ID calculation --- picture.go | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/picture.go b/picture.go index 5f3a375880..ecd6d94117 100644 --- a/picture.go +++ b/picture.go @@ -253,24 +253,20 @@ func (f *File) addSheetPicture(sheet string, rID int) { // countDrawings provides a function to get drawing files count storage in the // folder xl/drawings. -func (f *File) countDrawings() int { - c1, c2 := 0, 0 +func (f *File) countDrawings() (count int) { f.Pkg.Range(func(k, v interface{}) bool { if strings.Contains(k.(string), "xl/drawings/drawing") { - c1++ + count++ } return true }) f.Drawings.Range(func(rel, value interface{}) bool { if strings.Contains(rel.(string), "xl/drawings/drawing") { - c2++ + count++ } return true }) - if c1 < c2 { - return c2 - } - return c1 + return } // addDrawingPicture provides a function to add picture by given sheet, From 4ef68729f5bf2150b06cbace068412b3dde2d1ae Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 13 Sep 2021 22:40:38 +0800 Subject: [PATCH 445/957] new formula functions: Z.TEST and ZTEST, ref #65 --- calc.go | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 33 ++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) diff --git a/calc.go b/calc.go index 59d97e048b..fa6ec0a8b2 100644 --- a/calc.go +++ b/calc.go @@ -506,6 +506,8 @@ type formulaFuncs struct { // VLOOKUP // XOR // YEAR +// Z.TEST +// ZTEST // func (f *File) CalcCellValue(sheet, cell string) (result string, err error) { var ( @@ -5612,6 +5614,61 @@ func (fn *formulaFuncs) VARdotP(argsList *list.List) formulaArg { return fn.VARP(argsList) } +// ZdotTEST function calculates the one-tailed probability value of the +// Z-Test. The syntax of the function is: +// +// Z.TEST(array,x,[sigma]) +// +func (fn *formulaFuncs) ZdotTEST(argsList *list.List) formulaArg { + argsLen := argsList.Len() + if argsLen < 2 { + return newErrorFormulaArg(formulaErrorVALUE, "Z.TEST requires at least 2 arguments") + } + if argsLen > 3 { + return newErrorFormulaArg(formulaErrorVALUE, "Z.TEST accepts at most 3 arguments") + } + return fn.ZTEST(argsList) +} + +// ZTEST function calculates the one-tailed probability value of the Z-Test. +// The syntax of the function is: +// +// ZTEST(array,x,[sigma]) +// +func (fn *formulaFuncs) ZTEST(argsList *list.List) formulaArg { + argsLen := argsList.Len() + if argsLen < 2 { + return newErrorFormulaArg(formulaErrorVALUE, "ZTEST requires at least 2 arguments") + } + if argsLen > 3 { + return newErrorFormulaArg(formulaErrorVALUE, "ZTEST accepts at most 3 arguments") + } + arrArg, arrArgs := argsList.Front().Value.(formulaArg), list.New() + arrArgs.PushBack(arrArg) + arr := fn.AVERAGE(arrArgs) + if arr.Type == ArgError { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + } + x := argsList.Front().Next().Value.(formulaArg).ToNumber() + if x.Type == ArgError { + return x + } + sigma := argsList.Back().Value.(formulaArg).ToNumber() + if sigma.Type == ArgError { + return sigma + } + if argsLen != 3 { + sigma = fn.STDEV(arrArgs).ToNumber() + } + normsdistArg := list.New() + div := sigma.Number / math.Sqrt(float64(len(arrArg.ToList()))) + if div == 0 { + return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV) + } + normsdistArg.PushBack(newNumberFormulaArg((arr.Number - x.Number) / div)) + return newNumberFormulaArg(1 - fn.NORMSDIST(normsdistArg).Number) +} + // Information Functions // ISBLANK function tests if a specified cell is blank (empty) and if so, diff --git a/calc_test.go b/calc_test.go index cd09e9729a..7018ee92a4 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1899,6 +1899,20 @@ func TestCalcCellValue(t *testing.T) { // VAR.P "=VAR.P()": "VAR.P requires at least 1 argument", "=VAR.P(\"\")": "#DIV/0!", + // 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", + // 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", // Information Functions // ISBLANK "=ISBLANK(A1,A2)": "ISBLANK requires 1 argument", @@ -2759,6 +2773,25 @@ func TestCalcMATCH(t *testing.T) { assert.Equal(t, newErrorFormulaArg(formulaErrorNA, formulaErrorNA), calcMatch(2, nil, []formulaArg{})) } +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})) + formulaList := map[string]string{ + "=Z.TEST(A1:L1,5)": "0.371103278558538", + "=Z.TEST(A1:L1,6)": "0.838129187019751", + "=Z.TEST(A1:L1,5,1)": "0.193238115385616", + "=ZTEST(A1:L1,5)": "0.371103278558538", + "=ZTEST(A1:L1,6)": "0.838129187019751", + "=ZTEST(A1:L1,5,1)": "0.193238115385616", + } + for formula, expected := range formulaList { + assert.NoError(t, f.SetCellFormula("Sheet1", "M1", formula)) + result, err := f.CalcCellValue("Sheet1", "M1") + assert.NoError(t, err, formula) + assert.Equal(t, expected, result, formula) + } +} + func TestStrToDate(t *testing.T) { _, _, _, _, err := strToDate("") assert.Equal(t, formulaErrorVALUE, err.Error) From 1ba3690764e90aa6f7bcb3cb1e095475d40e3d91 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 14 Sep 2021 22:23:12 +0800 Subject: [PATCH 446/957] new formula functions: WEIBULL and WEIBULL.DIST, ref #65 --- calc.go | 42 ++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 40 ++++++++++++++++++++++++++++------------ 2 files changed, 70 insertions(+), 12 deletions(-) diff --git a/calc.go b/calc.go index fa6ec0a8b2..9cbc8ecb33 100644 --- a/calc.go +++ b/calc.go @@ -504,6 +504,8 @@ type formulaFuncs struct { // VAR.P // VARP // VLOOKUP +// WEIBULL +// WEIBULL.DIST // XOR // YEAR // Z.TEST @@ -5614,6 +5616,46 @@ func (fn *formulaFuncs) VARdotP(argsList *list.List) formulaArg { return fn.VARP(argsList) } +// WEIBULL function calculates the Weibull Probability Density Function or the +// Weibull Cumulative Distribution Function for a supplied set of parameters. +// The syntax of the function is: +// +// WEIBULL(x,alpha,beta,cumulative) +// +func (fn *formulaFuncs) WEIBULL(argsList *list.List) formulaArg { + if argsList.Len() != 4 { + return newErrorFormulaArg(formulaErrorVALUE, "WEIBULL requires 4 arguments") + } + x := argsList.Front().Value.(formulaArg).ToNumber() + alpha := argsList.Front().Next().Value.(formulaArg).ToNumber() + beta := argsList.Back().Prev().Value.(formulaArg).ToNumber() + if alpha.Type == ArgNumber && beta.Type == ArgNumber && x.Type == ArgNumber { + if alpha.Number < 0 || alpha.Number <= 0 || beta.Number <= 0 { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + } + 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((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))) + } + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) +} + +// WEIBULLdotDIST function calculates the Weibull Probability Density Function +// or the Weibull Cumulative Distribution Function for a supplied set of +// parameters. The syntax of the function is: +// +// 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") + } + return fn.WEIBULL(argsList) +} + // ZdotTEST function calculates the one-tailed probability value of the // Z-Test. The syntax of the function is: // diff --git a/calc_test.go b/calc_test.go index 7018ee92a4..763ea1f6e3 100644 --- a/calc_test.go +++ b/calc_test.go @@ -858,6 +858,12 @@ func TestCalcCellValue(t *testing.T) { "=VARP(A1:A5)": "1.25", // VAR.P "=VAR.P(A1:A5)": "1.25", + // WEIBULL + "=WEIBULL(1,3,1,FALSE)": "1.103638323514327", + "=WEIBULL(2,5,1.5,TRUE)": "0.985212776817482", + // WEIBULL.DIST + "=WEIBULL.DIST(1,3,1,FALSE)": "1.103638323514327", + "=WEIBULL.DIST(2,5,1.5,TRUE)": "0.985212776817482", // Information Functions // ISBLANK "=ISBLANK(A1)": "FALSE", @@ -1899,20 +1905,30 @@ func TestCalcCellValue(t *testing.T) { // VAR.P "=VAR.P()": "VAR.P requires at least 1 argument", "=VAR.P(\"\")": "#DIV/0!", + // 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.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", // 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)": "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", // 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)": "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", // Information Functions // ISBLANK "=ISBLANK(A1,A2)": "ISBLANK requires 1 argument", From 2add938798cdd1456616869298319528b0c76913 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 17 Sep 2021 00:15:51 +0800 Subject: [PATCH 447/957] - new formula functions: DATEVALUE, ref #65 - fix ineffectual variable assignments - timeout in go test --- .github/workflows/go.yml | 2 +- calc.go | 25 +++++++++++++++++++++++++ calc_test.go | 9 +++++++++ cell.go | 2 +- 4 files changed, 36 insertions(+), 2 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 320e3dae1c..5f674c3aeb 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -28,7 +28,7 @@ jobs: run: go build -v . - name: Test - run: env GO111MODULE=on go test -v -race ./... -coverprofile=coverage.txt -covermode=atomic + run: env GO111MODULE=on go test -v -timeout 30m -race ./... -coverprofile=coverage.txt -covermode=atomic - name: Codecov uses: codecov/codecov-action@v1 diff --git a/calc.go b/calc.go index 9cbc8ecb33..3e402e6b94 100644 --- a/calc.go +++ b/calc.go @@ -321,6 +321,7 @@ type formulaFuncs struct { // CUMPRINC // DATE // DATEDIF +// DATEVALUE // DAY // DB // DDB @@ -6515,6 +6516,30 @@ func strToDate(str string) (int, int, int, bool, formulaArg) { return year, month, day, timeIsEmpty, newEmptyFormulaArg() } +// 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: +// +// DATEVALUE(date_text) +// +func (fn *formulaFuncs) DATEVALUE(argsList *list.List) formulaArg { + if argsList.Len() != 1 { + return newErrorFormulaArg(formulaErrorVALUE, "DATEVALUE requires 1 argument") + } + dateText := argsList.Front().Value.(formulaArg).Value() + if !isDateOnlyFmt(dateText) { + if _, _, _, _, _, err := strToTime(dateText); err.Type == ArgError { + return err + } + } + y, m, d, _, err := strToDate(dateText) + if err.Type == ArgError { + return err + } + return newNumberFormulaArg(daysBetween(excelMinTime1900.Unix(), makeDate(y, time.Month(m), d)) + 1) +} + // DAY function returns the day of a date, represented by a serial number. The // day is given as an integer ranging from 1 to 31. The syntax of the // function is: diff --git a/calc_test.go b/calc_test.go index 763ea1f6e3..1e4d99b82b 100644 --- a/calc_test.go +++ b/calc_test.go @@ -956,6 +956,11 @@ func TestCalcCellValue(t *testing.T) { "=DATEDIF(43101,43891,\"YD\")": "59", "=DATEDIF(36526,73110,\"YD\")": "60", "=DATEDIF(42171,44242,\"yd\")": "244", + // DATEVALUE + "=DATEVALUE(\"01/01/16\")": "42370", + "=DATEVALUE(\"01/01/2016\")": "42370", + "=DATEVALUE(\"01/01/29\")": "47119", + "=DATEVALUE(\"01/01/30\")": "10959", // DAY "=DAY(0)": "0", "=DAY(INT(7))": "7", @@ -1997,6 +2002,10 @@ func TestCalcCellValue(t *testing.T) { "=DATEDIF(\"\",\"\",\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", "=DATEDIF(43891,43101,\"Y\")": "start_date > end_date", "=DATEDIF(43101,43891,\"x\")": "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!", // DAY "=DAY()": "DAY requires exactly 1 argument", "=DAY(-1)": "DAY only accepts positive argument", diff --git a/cell.go b/cell.go index 2d49a5e0ff..902f5b7ce3 100644 --- a/cell.go +++ b/cell.go @@ -82,7 +82,7 @@ func (f *File) GetCellType(sheet, axis string) (CellType, error) { var ( err error cellTypeStr string - cellType CellType = CellTypeUnset + cellType CellType ) if cellTypeStr, err = f.getCellStringFunc(sheet, axis, func(x *xlsxWorksheet, c *xlsxC) (string, bool, error) { return c.T, true, nil From 790c363cceaaa09e91ad579e2d25cb13c1582bba Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 18 Sep 2021 23:20:24 +0800 Subject: [PATCH 448/957] This closes #833, closes #845, and closes #1022, breaking changes - Close spreadsheet and row's iterator required - New options `WorksheetUnzipMemLimit` have been added - Improve streaming reading performance, memory usage decrease about 93.7% --- .gitignore | 1 + README.md | 8 ++++++ README_zh.md | 10 ++++++- cell_test.go | 4 +++ chart_test.go | 2 ++ col.go | 2 +- col_test.go | 5 ++++ crypt_test.go | 1 + docProps_test.go | 2 ++ errors.go | 3 ++ excelize.go | 53 ++++++++++++++++++++++++++---------- excelize_test.go | 28 +++++++++++++++++-- file.go | 12 ++++++++ file_test.go | 7 +++++ lib.go | 71 +++++++++++++++++++++++++++++++++++++++++------- lib_test.go | 43 +++++++++++++++++++++++++++++ merge_test.go | 4 +++ picture.go | 3 ++ picture_test.go | 5 +++- rows.go | 43 +++++++++++++++++++++++++---- rows_test.go | 25 +++++++++++++++++ sheet.go | 8 +++++- sheet_test.go | 3 ++ stream_test.go | 15 ++++++++++ 24 files changed, 322 insertions(+), 36 deletions(-) diff --git a/.gitignore b/.gitignore index ce92812619..68532a7f91 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ test/Test*.xlsm test/BadEncrypt.xlsx test/BadWorkbook.SaveAsEmptyStruct.xlsx test/*.png +test/excelize-* *.out *.test .idea diff --git a/README.md b/README.md index ac2b6c3ab4..6b874697a3 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,10 @@ func main() { } fmt.Println() } + // Close the spreadsheet. + if err = f.Close(); err != nil { + fmt.Println(err) + } } ``` @@ -203,6 +207,10 @@ func main() { if err = f.Save(); err != nil { fmt.Println(err) } + // Close the spreadsheet. + if err = f.Close(); err != nil { + fmt.Println(err) + } } ``` diff --git a/README_zh.md b/README_zh.md index 359192bc2e..3b90eecda9 100644 --- a/README_zh.md +++ b/README_zh.md @@ -96,6 +96,10 @@ func main() { } fmt.Println() } + // 关闭工作簿 + if err = f.Close(); err != nil { + fmt.Println(err) + } } ``` @@ -199,10 +203,14 @@ func main() { }`); err != nil { fmt.Println(err) } - // 保存文件 + // 保存工作簿 if err = f.Save(); err != nil { fmt.Println(err) } + // 关闭工作簿 + if err = f.Close(); err != nil { + fmt.Println(err) + } } ``` diff --git a/cell_test.go b/cell_test.go index 5467e430d0..35eaa9637b 100644 --- a/cell_test.go +++ b/cell_test.go @@ -72,6 +72,7 @@ func TestConcurrency(t *testing.T) { } assert.Equal(t, "1", val) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestConcurrency.xlsx"))) + assert.NoError(t, f.Close()) } func TestCheckCellInArea(t *testing.T) { @@ -325,6 +326,7 @@ func TestOverflowNumericCell(t *testing.T) { assert.NoError(t, err) // GOARCH=amd64 - all ok; GOARCH=386 - actual: "-2147483648" assert.Equal(t, "8595602512225", val, "A1 should be 8595602512225") + assert.NoError(t, f.Close()) } func TestSetCellFormula(t *testing.T) { @@ -340,6 +342,7 @@ func TestSetCellFormula(t *testing.T) { assert.EqualError(t, f.SetCellFormula("Sheet1", "C", "SUM(Sheet2!D2,Sheet2!D9)"), `cannot convert cell "C" to coordinates: invalid cell name "C"`) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellFormula1.xlsx"))) + assert.NoError(t, f.Close()) f, err = OpenFile(filepath.Join("test", "CalcChain.xlsx")) if !assert.NoError(t, err) { @@ -351,6 +354,7 @@ func TestSetCellFormula(t *testing.T) { // 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. f = NewFile() diff --git a/chart_test.go b/chart_test.go index d2a3975530..2cd7131617 100644 --- a/chart_test.go +++ b/chart_test.go @@ -206,6 +206,7 @@ func TestAddChart(t *testing.T) { 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") // 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()) } func TestAddChartSheet(t *testing.T) { @@ -254,6 +255,7 @@ func TestDeleteChart(t *testing.T) { assert.EqualError(t, f.DeleteChart("Sheet1", ""), `cannot convert cell "" to coordinates: invalid cell name ""`) // Test delete chart on no chart worksheet. assert.NoError(t, NewFile().DeleteChart("Sheet1", "A1")) + assert.NoError(t, f.Close()) } func TestChartWithLogarithmicBase(t *testing.T) { diff --git a/col.go b/col.go index 5ba5caa6cf..ffd49dd414 100644 --- a/col.go +++ b/col.go @@ -209,7 +209,7 @@ func (f *File) Cols(sheet string) (*Cols, error) { f.saveFileList(name, f.replaceNameSpaceBytes(name, output)) } var colIterator columnXMLIterator - colIterator.cols.sheetXML = f.readXML(name) + colIterator.cols.sheetXML = f.readBytes(name) decoder := f.xmlNewDecoder(bytes.NewReader(colIterator.cols.sheetXML)) for { token, _ := decoder.Token() diff --git a/col_test.go b/col_test.go index b19eadf84e..213c370a5a 100644 --- a/col_test.go +++ b/col_test.go @@ -39,6 +39,7 @@ func TestCols(t *testing.T) { if !assert.Equal(t, collectedRows, returnedColumns) { t.FailNow() } + assert.NoError(t, f.Close()) f = NewFile() cells := []string{"C2", "C3", "C4"} @@ -75,6 +76,7 @@ func TestColumnsIterator(t *testing.T) { require.True(t, colCount <= expectedNumCol, "colCount is greater than expected") } assert.Equal(t, expectedNumCol, colCount) + assert.NoError(t, f.Close()) f = NewFile() cells := []string{"C2", "C3", "C4", "D2", "D3", "D4"} @@ -99,6 +101,7 @@ func TestColsError(t *testing.T) { } _, err = f.Cols("SheetN") assert.EqualError(t, err, "sheet SheetN is not exist") + assert.NoError(t, f.Close()) } func TestGetColsError(t *testing.T) { @@ -108,6 +111,7 @@ func TestGetColsError(t *testing.T) { } _, err = f.GetCols("SheetN") assert.EqualError(t, err, "sheet SheetN is not exist") + assert.NoError(t, f.Close()) f = NewFile() f.Sheet.Delete("xl/worksheets/sheet1.xml") @@ -283,6 +287,7 @@ func TestOutlineLevel(t *testing.T) { f, err = OpenFile(filepath.Join("test", "Book1.xlsx")) assert.NoError(t, err) assert.NoError(t, f.SetColOutlineLevel("Sheet2", "B", 2)) + assert.NoError(t, f.Close()) } func TestSetColStyle(t *testing.T) { diff --git a/crypt_test.go b/crypt_test.go index 68ff5b85a8..cb0b160b8d 100644 --- a/crypt_test.go +++ b/crypt_test.go @@ -22,6 +22,7 @@ func TestEncrypt(t *testing.T) { 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"}), "not support encryption currently") + assert.NoError(t, f.Close()) } func TestEncryptionMechanism(t *testing.T) { diff --git a/docProps_test.go b/docProps_test.go index 40ae2dc98b..df1b6c6ed7 100644 --- a/docProps_test.go +++ b/docProps_test.go @@ -44,6 +44,7 @@ func TestSetDocProps(t *testing.T) { assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetDocProps.xlsx"))) f.Pkg.Store("docProps/core.xml", nil) assert.NoError(t, f.SetDocProps(&DocProperties{})) + assert.NoError(t, f.Close()) // Test unsupported charset f = NewFile() @@ -62,6 +63,7 @@ func TestGetDocProps(t *testing.T) { f.Pkg.Store("docProps/core.xml", nil) _, err = f.GetDocProps() assert.NoError(t, err) + assert.NoError(t, f.Close()) // Test unsupported charset f = NewFile() diff --git a/errors.go b/errors.go index 4cfd12e91a..9fc0957785 100644 --- a/errors.go +++ b/errors.go @@ -131,4 +131,7 @@ var ( // ErrCellCharsLength defined the error message for receiving a cell // characters length that exceeds the limit. ErrCellCharsLength = fmt.Errorf("cell value must be 0-%d characters", TotalCellChars) + // ErrOptionsUnzipSizeLimit defined the error message for receiving + // invalid UnzipSizeLimit and WorksheetUnzipMemLimit. + ErrOptionsUnzipSizeLimit = errors.New("the value of UnzipSizeLimit should be greater than or equal to WorksheetUnzipMemLimit") ) diff --git a/excelize.go b/excelize.go index 24a1a4e146..c5778c8542 100644 --- a/excelize.go +++ b/excelize.go @@ -37,6 +37,7 @@ type File struct { checked map[string]bool sheetMap map[string]string streams map[string]*StreamWriter + tempFiles sync.Map CalcChain *xlsxCalcChain Comments map[string]*xlsxComments ContentTypes *xlsxTypes @@ -58,13 +59,26 @@ type File struct { type charsetTranscoderFn func(charset string, input io.Reader) (rdr io.Reader, err error) -// Options define the options for open and reading spreadsheet. RawCellValue -// specify if apply the number format for the cell value or get the raw -// value. +// Options define the options for open and reading spreadsheet. +// +// Password specifies the password of the spreadsheet in plain text. +// +// 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 +// spreadsheet, this value should be greater than or equal to +// WorksheetUnzipMemLimit, the default size limit is 16GB. +// +// WorksheetUnzipMemLimit specifies the memory limit on unzipping worksheet in +// bytes, worksheet XML will be extracted to system 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. type Options struct { - Password string - RawCellValue bool - UnzipSizeLimit int64 + Password string + RawCellValue bool + UnzipSizeLimit int64 + WorksheetUnzipMemLimit int64 } // OpenFile take the name of an spreadsheet file and returns a populated @@ -78,10 +92,8 @@ type Options struct { // // Note that the excelize just support decrypt and not support encrypt // currently, the spreadsheet saved by Save and SaveAs will be without -// password unprotected. -// -// UnzipSizeLimit specified the unzip size limit in bytes on open the -// spreadsheet, the default size limit is 16GB. +// 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 { @@ -99,10 +111,11 @@ func OpenFile(filename string, opt ...Options) (*File, error) { // newFile is object builder func newFile() *File { return &File{ - options: &Options{UnzipSizeLimit: UnzipSizeLimit}, + options: &Options{UnzipSizeLimit: UnzipSizeLimit, WorksheetUnzipMemLimit: StreamChunkSize}, xmlAttr: make(map[string][]xml.Attr), checked: make(map[string]bool), sheetMap: make(map[string]string), + tempFiles: sync.Map{}, Comments: make(map[string]*xlsxComments), Drawings: sync.Map{}, sharedStringsMap: make(map[string]int), @@ -125,6 +138,18 @@ func OpenReader(r io.Reader, opt ...Options) (*File, error) { f.options = parseOptions(opt...) if f.options.UnzipSizeLimit == 0 { f.options.UnzipSizeLimit = UnzipSizeLimit + if f.options.WorksheetUnzipMemLimit > f.options.UnzipSizeLimit { + f.options.UnzipSizeLimit = f.options.WorksheetUnzipMemLimit + } + } + if f.options.WorksheetUnzipMemLimit == 0 { + f.options.WorksheetUnzipMemLimit = StreamChunkSize + if f.options.UnzipSizeLimit < f.options.WorksheetUnzipMemLimit { + f.options.WorksheetUnzipMemLimit = f.options.UnzipSizeLimit + } + } + if f.options.WorksheetUnzipMemLimit > f.options.UnzipSizeLimit { + return nil, ErrOptionsUnzipSizeLimit } if bytes.Contains(b, oleIdentifier) { b, err = Decrypt(b, f.options) @@ -136,7 +161,7 @@ func OpenReader(r io.Reader, opt ...Options) (*File, error) { if err != nil { return nil, err } - file, sheetCount, err := ReadZipReader(zr, f.options) + file, sheetCount, err := f.ReadZipReader(zr) if err != nil { return nil, err } @@ -210,10 +235,10 @@ func (f *File) workSheetReader(sheet string) (ws *xlsxWorksheet, err error) { } ws = new(xlsxWorksheet) if _, ok := f.xmlAttr[name]; !ok { - d := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(name)))) + d := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readBytes(name)))) f.xmlAttr[name] = append(f.xmlAttr[name], getRootElement(d)...) } - if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(name)))). + 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) return diff --git a/excelize_test.go b/excelize_test.go index 02abce5fd7..f556d83c8a 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -167,6 +167,7 @@ func TestOpenFile(t *testing.T) { } 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.NoError(t, f.Close()) } func TestSaveFile(t *testing.T) { @@ -175,11 +176,13 @@ func TestSaveFile(t *testing.T) { t.FailNow() } assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSaveFile.xlsx"))) + assert.NoError(t, f.Close()) f, err = OpenFile(filepath.Join("test", "TestSaveFile.xlsx")) if !assert.NoError(t, err) { t.FailNow() } assert.NoError(t, f.Save()) + assert.NoError(t, f.Close()) } func TestSaveAsWrongPath(t *testing.T) { @@ -187,6 +190,7 @@ func TestSaveAsWrongPath(t *testing.T) { assert.NoError(t, err) // Test write file to not exist directory. assert.EqualError(t, f.SaveAs(""), "open .: is a directory") + assert.NoError(t, f.Close()) } func TestCharsetTranscoder(t *testing.T) { @@ -197,7 +201,7 @@ func TestCharsetTranscoder(t *testing.T) { func TestOpenReader(t *testing.T) { _, err := OpenReader(strings.NewReader("")) assert.EqualError(t, err, "zip: not a valid zip file") - _, err = OpenReader(bytes.NewReader(oleIdentifier), Options{Password: "password"}) + _, err = OpenReader(bytes.NewReader(oleIdentifier), Options{Password: "password", WorksheetUnzipMemLimit: UnzipSizeLimit + 1}) assert.EqualError(t, err, "decrypted file failed") // Test open spreadsheet with unzip size limit. @@ -210,6 +214,7 @@ func TestOpenReader(t *testing.T) { val, err := f.GetCellValue("Sheet1", "A1") assert.NoError(t, err) assert.Equal(t, "SECRET", val) + assert.NoError(t, f.Close()) // Test open password protected spreadsheet created by LibreOffice 7.0.0.3. f, err = OpenFile(filepath.Join("test", "encryptAES.xlsx"), Options{Password: "password"}) @@ -217,6 +222,11 @@ func TestOpenReader(t *testing.T) { val, err = f.GetCellValue("Sheet1", "A1") assert.NoError(t, err) assert.Equal(t, "SECRET", val) + assert.NoError(t, f.Close()) + + // Test open spreadsheet with invalid optioins. + _, err = OpenReader(bytes.NewReader(oleIdentifier), Options{UnzipSizeLimit: 1, WorksheetUnzipMemLimit: 2}) + assert.EqualError(t, err, ErrOptionsUnzipSizeLimit.Error()) // Test unexpected EOF. var b bytes.Buffer @@ -266,6 +276,7 @@ func TestBrokenFile(t *testing.T) { f3.GetActiveSheetIndex() f3.SetActiveSheet(1) assert.NoError(t, err) + assert.NoError(t, f3.Close()) }) t.Run("OpenNotExistsFile", func(t *testing.T) { @@ -340,6 +351,7 @@ func TestSetCellHyperLink(t *testing.T) { 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()) f = NewFile() _, err = f.workSheetReader("Sheet1") @@ -377,6 +389,7 @@ func TestGetCellHyperLink(t *testing.T) { link, target, err = f.GetCellHyperLink("Sheet3", "H3") assert.EqualError(t, err, "sheet Sheet3 is not exist") t.Log(link, target) + assert.NoError(t, f.Close()) f = NewFile() _, err = f.workSheetReader("Sheet1") @@ -398,7 +411,6 @@ func TestGetCellHyperLink(t *testing.T) { assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`) assert.Equal(t, link, false) assert.Equal(t, target, "") - } func TestSetSheetBackground(t *testing.T) { @@ -418,6 +430,7 @@ func TestSetSheetBackground(t *testing.T) { } assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetSheetBackground.xlsx"))) + assert.NoError(t, f.Close()) } func TestSetSheetBackgroundErrors(t *testing.T) { @@ -433,6 +446,7 @@ func TestSetSheetBackgroundErrors(t *testing.T) { err = f.SetSheetBackground("Sheet2", filepath.Join("test", "Book1.xlsx")) assert.EqualError(t, err, ErrImgExt.Error()) + assert.NoError(t, f.Close()) } // TestWriteArrayFormula tests the extended options of SetCellFormula by writing an array function @@ -1052,6 +1066,7 @@ func TestConditionalFormat(t *testing.T) { if !assert.NoError(t, err) { t.FailNow() } + assert.NoError(t, f.Close()) } func TestConditionalFormatError(t *testing.T) { @@ -1082,6 +1097,7 @@ func TestSharedStrings(t *testing.T) { t.FailNow() } assert.Equal(t, "Test Weight (Kgs)", rows[0][0]) + assert.NoError(t, f.Close()) } func TestSetSheetRow(t *testing.T) { @@ -1098,6 +1114,7 @@ func TestSetSheetRow(t *testing.T) { 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"))) + assert.NoError(t, f.Close()) } func TestHSL(t *testing.T) { @@ -1155,6 +1172,7 @@ func TestUnprotectSheet(t *testing.T) { assert.NoError(t, f.UnprotectSheet("Sheet1")) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestUnprotectSheet.xlsx"))) + assert.NoError(t, f.Close()) } func TestSetDefaultTimeStyle(t *testing.T) { @@ -1323,7 +1341,11 @@ func fillCells(f *File, sheet string, colCount, rowCount int) { func BenchmarkOpenFile(b *testing.B) { for i := 0; i < b.N; i++ { - if _, err := OpenFile(filepath.Join("test", "Book1.xlsx")); err != nil { + f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + if err != nil { + b.Error(err) + } + if err := f.Close(); err != nil { b.Error(err) } } diff --git a/file.go b/file.go index bfb6abfb13..c0092a2eb4 100644 --- a/file.go +++ b/file.go @@ -82,6 +82,18 @@ func (f *File) SaveAs(name string, opt ...Options) error { return f.Write(file) } +// Close closes and cleanup the open temporary file for the spreadsheet. +func (f *File) Close() error { + var err error + f.tempFiles.Range(func(k, v interface{}) bool { + if err = os.Remove(v.(string)); err != nil { + return false + } + return true + }) + return err +} + // Write provides a function to write to an io.Writer. func (f *File) Write(w io.Writer) error { _, err := f.WriteTo(w) diff --git a/file_test.go b/file_test.go index 956ff92ebd..ee5d322fae 100644 --- a/file_test.go +++ b/file_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func BenchmarkWrite(b *testing.B) { @@ -62,3 +63,9 @@ func TestWriteTo(t *testing.T) { assert.Nil(t, err) } } + +func TestClose(t *testing.T) { + f := NewFile() + f.tempFiles.Store("/d/", "/d/") + require.Error(t, f.Close()) +} diff --git a/lib.go b/lib.go index 31b64a5951..c8e957c92a 100644 --- a/lib.go +++ b/lib.go @@ -18,14 +18,15 @@ import ( "encoding/xml" "fmt" "io" + "io/ioutil" + "os" "regexp" "strconv" "strings" ) -// ReadZipReader can be used to read the spreadsheet in memory without touching the -// filesystem. -func ReadZipReader(r *zip.Reader, o *Options) (map[string][]byte, int, error) { +// ReadZipReader extract spreadsheet with given options. +func (f *File) ReadZipReader(r *zip.Reader) (map[string][]byte, int, error) { var ( err error docPart = map[string]string{ @@ -37,25 +38,49 @@ func ReadZipReader(r *zip.Reader, o *Options) (map[string][]byte, int, error) { unzipSize int64 ) for _, v := range r.File { - unzipSize += v.FileInfo().Size() - if unzipSize > o.UnzipSizeLimit { - return fileList, worksheets, newUnzipSizeLimitError(o.UnzipSizeLimit) + fileSize := v.FileInfo().Size() + unzipSize += fileSize + if unzipSize > f.options.UnzipSizeLimit { + return fileList, worksheets, newUnzipSizeLimitError(f.options.UnzipSizeLimit) } fileName := strings.Replace(v.Name, "\\", "/", -1) if partName, ok := docPart[strings.ToLower(fileName)]; ok { fileName = partName } - if fileList[fileName], err = readFile(v); err != nil { - return nil, 0, err - } if strings.HasPrefix(fileName, "xl/worksheets/sheet") { worksheets++ + if fileSize > f.options.WorksheetUnzipMemLimit && !v.FileInfo().IsDir() { + if tempFile, err := f.unzipToTemp(v); err == nil { + f.tempFiles.Store(fileName, tempFile) + continue + } + } + } + if fileList[fileName], err = readFile(v); err != nil { + return nil, 0, err } } return fileList, worksheets, nil } -// readXML provides a function to read XML content as string. +// 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-") + if err != nil { + return "", err + } + rc, err := zipFile.Open() + if err != nil { + return tmp.Name(), err + } + _, err = io.Copy(tmp, rc) + rc.Close() + tmp.Close() + return tmp.Name(), err +} + +// readXML provides a function to read XML content as bytes. func (f *File) readXML(name string) []byte { if content, _ := f.Pkg.Load(name); content != nil { return content.([]byte) @@ -66,6 +91,32 @@ func (f *File) readXML(name string) []byte { return []byte{} } +// readBytes read file as bytes by given path. +func (f *File) readBytes(name string) []byte { + content := f.readXML(name) + if len(content) != 0 { + return content + } + file, err := f.readTemp(name) + if err != nil { + return content + } + content, _ = ioutil.ReadAll(file) + f.Pkg.Store(name, content) + file.Close() + return content +} + +// readTemp read file from system temporary directory by given path. +func (f *File) readTemp(name string) (file *os.File, err error) { + path, ok := f.tempFiles.Load(name) + if !ok { + return + } + file, err = os.Open(path.(string)) + return +} + // saveFileList provides a function to update given file content in file list // of spreadsheet. func (f *File) saveFileList(name string, content []byte) { diff --git a/lib_test.go b/lib_test.go index 556ed91659..84a52bb1c4 100644 --- a/lib_test.go +++ b/lib_test.go @@ -1,13 +1,18 @@ package excelize import ( + "archive/zip" + "bytes" "encoding/xml" "fmt" + "os" "strconv" "strings" + "sync" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) var validColumns = []struct { @@ -296,3 +301,41 @@ func TestBstrMarshal(t *testing.T) { assert.Equal(t, expected, bstrMarshal(bstr)) } } + +func TestReadBytes(t *testing.T) { + f := &File{tempFiles: sync.Map{}} + sheet := "xl/worksheets/sheet1.xml" + f.tempFiles.Store(sheet, "/d/") + assert.Equal(t, []byte{}, f.readBytes(sheet)) +} + +func TestUnzipToTemp(t *testing.T) { + os.Setenv("TMPDIR", "test") + defer os.Unsetenv("TMPDIR") + assert.NoError(t, os.Chmod(os.TempDir(), 0444)) + f := NewFile() + data := []byte("PK\x03\x040000000PK\x01\x0200000" + + "0000000000000000000\x00" + + "\x00\x00\x00\x00\x00000000000000PK\x01" + + "\x020000000000000000000" + + "00000\v\x00\x00\x00\x00\x00000000000" + + "00000000000000PK\x01\x0200" + + "00000000000000000000" + + "00\v\x00\x00\x00\x00\x00000000000000" + + "00000000000PK\x01\x020000<" + + "0\x00\x0000000000000000\v\x00\v" + + "\x00\x00\x00\x00\x0000000000\x00\x00\x00\x00000" + + "00000000PK\x01\x0200000000" + + "0000000000000000\v\x00\x00\x00" + + "\x00\x0000PK\x05\x06000000\x05\x000000" + + "\v\x00\x00\x00\x00\x00") + z, err := zip.NewReader(bytes.NewReader(data), int64(len(data))) + assert.NoError(t, err) + + _, err = f.unzipToTemp(z.File[0]) + require.Error(t, err) + assert.NoError(t, os.Chmod(os.TempDir(), 0755)) + + _, err = f.unzipToTemp(z.File[0]) + assert.EqualError(t, err, "EOF") +} diff --git a/merge_test.go b/merge_test.go index 88fe4f9cd5..8d9ad41e10 100644 --- a/merge_test.go +++ b/merge_test.go @@ -68,6 +68,7 @@ func TestMergeCell(t *testing.T) { assert.EqualError(t, f.MergeCell("SheetN", "N10", "O11"), "sheet SheetN is not exist") assert.NoError(t, f.SaveAs(filepath.Join("test", "TestMergeCell.xlsx"))) + assert.NoError(t, f.Close()) f = NewFile() assert.NoError(t, f.MergeCell("Sheet1", "A2", "B3")) @@ -93,6 +94,7 @@ func TestMergeCellOverlap(t *testing.T) { assert.Equal(t, "A1", mc[0].GetStartAxis()) assert.Equal(t, "D3", mc[0].GetEndAxis()) assert.Equal(t, "", mc[0].GetCellValue()) + assert.NoError(t, f.Close()) } func TestGetMergeCells(t *testing.T) { @@ -139,6 +141,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.NoError(t, f.Close()) } func TestUnmergeCell(t *testing.T) { @@ -162,6 +165,7 @@ func TestUnmergeCell(t *testing.T) { } assert.NoError(t, f.SaveAs(filepath.Join("test", "TestUnmergeCell.xlsx"))) + assert.NoError(t, f.Close()) f = NewFile() assert.NoError(t, f.MergeCell("Sheet1", "A2", "B3")) diff --git a/picture.go b/picture.go index ecd6d94117..332c6392cb 100644 --- a/picture.go +++ b/picture.go @@ -497,6 +497,9 @@ func (f *File) getSheetRelationshipsTargetByID(sheet, rID string) string { // if err := ioutil.WriteFile(file, raw, 0644); err != nil { // fmt.Println(err) // } +// if err = f.Close(); err != nil { +// fmt.Println(err) +// } // func (f *File) GetPicture(sheet, cell string) (string, []byte, error) { col, row, err := CellNameToCoordinates(cell) diff --git a/picture_test.go b/picture_test.go index 3e12f5f9db..2927976857 100644 --- a/picture_test.go +++ b/picture_test.go @@ -68,6 +68,7 @@ func TestAddPicture(t *testing.T) { // Test write file to given path. assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddPicture.xlsx"))) + assert.NoError(t, f.Close()) } func TestAddPictureErrors(t *testing.T) { @@ -90,6 +91,7 @@ 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.NoError(t, f.Close()) } func TestGetPicture(t *testing.T) { @@ -137,7 +139,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, 0644)) { - t.FailNow() } @@ -146,6 +147,7 @@ func TestGetPicture(t *testing.T) { assert.NoError(t, err) assert.Empty(t, file) assert.Empty(t, raw) + assert.NoError(t, f.Close()) // Test get picture from none drawing worksheet. f = NewFile() @@ -194,6 +196,7 @@ func TestDeletePicture(t *testing.T) { assert.EqualError(t, f.DeletePicture("SheetN", "A1"), "sheet SheetN is not exist") // Test delete picture with invalid coordinates. assert.EqualError(t, f.DeletePicture("Sheet1", ""), `cannot convert cell "" to coordinates: invalid cell name ""`) + assert.NoError(t, f.Close()) // Test delete picture on no chart worksheet. assert.NoError(t, NewFile().DeletePicture("Sheet1", "A1")) } diff --git a/rows.go b/rows.go index 057cbc8fc4..5c7b22df79 100644 --- a/rows.go +++ b/rows.go @@ -18,6 +18,7 @@ import ( "io" "log" "math" + "os" "strconv" "github.com/mohae/deepcopy" @@ -60,7 +61,7 @@ func (f *File) GetRows(sheet string, opts ...Options) ([][]string, error) { max = cur } } - return results[:max], nil + return results[:max], rows.Close() } // Rows defines an iterator to a sheet. @@ -70,6 +71,7 @@ type Rows struct { rawCellValue bool sheet string f *File + tempFile *os.File decoder *xml.Decoder } @@ -84,6 +86,15 @@ func (rows *Rows) Error() error { return rows.err } +// Close closes the open worksheet XML file in the system temporary +// directory. +func (rows *Rows) Close() error { + if rows.tempFile != nil { + return rows.tempFile.Close() + } + return nil +} + // Columns return the current row's column values. func (rows *Rows) Columns(opts ...Options) ([]string, error) { var rowIterator rowXMLIterator @@ -196,6 +207,9 @@ func rowXMLHandler(rowIterator *rowXMLIterator, xmlElement *xml.StartElement, ra // } // fmt.Println() // } +// if err = rows.Close(); err != nil { +// fmt.Println(err) +// } // func (f *File) Rows(sheet string) (*Rows, error) { name, ok := f.sheetMap[trimSheetName(sheet)] @@ -215,8 +229,13 @@ func (f *File) Rows(sheet string) (*Rows, error) { inElement string row int rows Rows + needClose bool + decoder *xml.Decoder + tempFile *os.File ) - decoder := f.xmlNewDecoder(bytes.NewReader(f.readXML(name))) + if needClose, decoder, tempFile, err = f.sheetDecoder(name); needClose && err == nil { + defer tempFile.Close() + } for { token, _ := decoder.Token() if token == nil { @@ -241,15 +260,29 @@ func (f *File) Rows(sheet string) (*Rows, error) { if xmlElement.Name.Local == "sheetData" { rows.f = f rows.sheet = name - rows.decoder = f.xmlNewDecoder(bytes.NewReader(f.readXML(name))) - return &rows, nil + _, rows.decoder, rows.tempFile, err = f.sheetDecoder(name) + return &rows, err } - default: } } return &rows, nil } +// sheetDecoder creates XML decoder by given path in the zip from memory data +// or system temporary file. +func (f *File) sheetDecoder(name string) (bool, *xml.Decoder, *os.File, error) { + var ( + content []byte + err error + tempFile *os.File + ) + if content = f.readXML(name); len(content) > 0 { + return false, f.xmlNewDecoder(bytes.NewReader(content)), tempFile, err + } + tempFile, err = f.readTemp(name) + return true, f.xmlNewDecoder(tempFile), tempFile, err +} + // SetRowHeight provides a function to set the height of a single row. For // example, set the height of the first row in Sheet1: // diff --git a/rows_test.go b/rows_test.go index c0dc1d8aef..0ebe59d47e 100644 --- a/rows_test.go +++ b/rows_test.go @@ -32,6 +32,7 @@ func TestRows(t *testing.T) { if !assert.NoError(t, rows.Error()) { t.FailNow() } + assert.NoError(t, rows.Close()) returnedRows, err := f.GetRows(sheet2) assert.NoError(t, err) @@ -41,6 +42,7 @@ func TestRows(t *testing.T) { if !assert.Equal(t, collectedRows, returnedRows) { t.FailNow() } + assert.NoError(t, f.Close()) f = NewFile() f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`1B`)) @@ -52,6 +54,14 @@ func TestRows(t *testing.T) { f.Pkg.Store("xl/worksheets/sheet1.xml", nil) _, err = f.Rows("Sheet1") assert.NoError(t, err) + + // Test reload the file to memory from system temporary directory. + f, err = OpenFile(filepath.Join("test", "Book1.xlsx"), Options{WorksheetUnzipMemLimit: 1024}) + assert.NoError(t, err) + value, err := f.GetCellValue("Sheet1", "A19") + assert.NoError(t, err) + assert.Equal(t, "Total:", value) + assert.NoError(t, f.Close()) } func TestRowsIterator(t *testing.T) { @@ -70,6 +80,8 @@ func TestRowsIterator(t *testing.T) { require.True(t, rowCount <= expectedNumRow, "rowCount is greater than expected") } assert.Equal(t, expectedNumRow, rowCount) + assert.NoError(t, rows.Close()) + assert.NoError(t, f.Close()) // Valued cell sparse distribution test f = NewFile() @@ -94,6 +106,7 @@ func TestRowsError(t *testing.T) { } _, err = f.Rows("SheetN") assert.EqualError(t, err, "sheet SheetN is not exist") + assert.NoError(t, f.Close()) } func TestRowHeight(t *testing.T) { @@ -871,6 +884,11 @@ func TestGetValueFromNumber(t *testing.T) { } } +func TestRoundPrecision(t *testing.T) { + _, err := roundPrecision("") + assert.EqualError(t, err, "strconv.ParseFloat: parsing \"\": invalid syntax") +} + func TestErrSheetNotExistError(t *testing.T) { err := ErrSheetNotExist{SheetName: "Sheet1"} assert.EqualValues(t, err.Error(), "sheet Sheet1 is not exist") @@ -920,6 +938,7 @@ func TestNumberFormats(t *testing.T) { cells = append(cells, col) } assert.Equal(t, []string{"", "200", "450", "200", "510", "315", "127", "89", "348", "53", "37"}, cells[3]) + assert.NoError(t, f.Close()) } func BenchmarkRows(b *testing.B) { @@ -934,6 +953,12 @@ func BenchmarkRows(b *testing.B) { } } } + if err := rows.Close(); err != nil { + b.Error(err) + } + } + if err := f.Close(); err != nil { + b.Error(err) } } diff --git a/sheet.go b/sheet.go index be2e964bca..e5ea1bdedc 100644 --- a/sheet.go +++ b/sheet.go @@ -419,6 +419,9 @@ func (f *File) GetSheetIndex(name string) int { // for index, name := range f.GetSheetMap() { // fmt.Println(index, name) // } +// if err = f.Close(); err != nil { +// fmt.Println(err) +// } // func (f *File) GetSheetMap() map[int]string { wb := f.workbookReader() @@ -462,6 +465,9 @@ func (f *File) getSheetMap() map[string]string { if _, ok := f.Pkg.Load(path); ok { maps[v.Name] = path } + if _, ok := f.tempFiles.Load(path); ok { + maps[v.Name] = path + } } } } @@ -858,7 +864,7 @@ func (f *File) searchSheet(name, value string, regSearch bool) (result []string, ) d = f.sharedStringsReader() - decoder := f.xmlNewDecoder(bytes.NewReader(f.readXML(name))) + decoder := f.xmlNewDecoder(bytes.NewReader(f.readBytes(name))) for { var token xml.Token token, err = decoder.Token() diff --git a/sheet_test.go b/sheet_test.go index 0a604de947..ef32d79bb0 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -189,6 +189,7 @@ func TestSearchSheet(t *testing.T) { result, err = f.SearchSheet("Sheet1", "[0-9]", true) assert.NoError(t, err) assert.EqualValues(t, expected, result) + assert.NoError(t, f.Close()) // Test search worksheet data after set cell value f = NewFile() @@ -327,6 +328,7 @@ func TestGetSheetName(t *testing.T) { assert.Equal(t, "Sheet2", f.GetSheetName(1)) assert.Equal(t, "", f.GetSheetName(-1)) assert.Equal(t, "", f.GetSheetName(2)) + assert.NoError(t, f.Close()) } func TestGetSheetMap(t *testing.T) { @@ -341,6 +343,7 @@ func TestGetSheetMap(t *testing.T) { assert.Equal(t, expectedMap[idx], name) } assert.Equal(t, len(sheetMap), 2) + assert.NoError(t, f.Close()) } func TestSetActiveSheet(t *testing.T) { diff --git a/stream_test.go b/stream_test.go index fda44fbe76..6cfed07170 100644 --- a/stream_test.go +++ b/stream_test.go @@ -115,6 +115,21 @@ func TestStreamWriter(t *testing.T) { cellValue, err := file.GetCellValue("Sheet1", "A1") assert.NoError(t, err) assert.Equal(t, "Data", cellValue) + + // 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") + assert.NoError(t, err) + cells := 0 + for rows.Next() { + row, err := rows.Columns() + assert.NoError(t, err) + cells += len(row) + } + assert.NoError(t, rows.Close()) + assert.Equal(t, 2559558, cells) + assert.NoError(t, file.Close()) } func TestStreamSetColWidth(t *testing.T) { From c05b9fe8a6e6cadad0de7821bd33fa5cc283c8e4 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 24 Sep 2021 08:19:48 +0800 Subject: [PATCH 449/957] new formula function: DAYS, ref #65 --- calc.go | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 10 ++++++++++ 2 files changed, 63 insertions(+) diff --git a/calc.go b/calc.go index 3e402e6b94..266491f473 100644 --- a/calc.go +++ b/calc.go @@ -323,6 +323,7 @@ type formulaFuncs struct { // DATEDIF // DATEVALUE // DAY +// DAYS // DB // DDB // DEC2BIN @@ -6574,6 +6575,58 @@ func (fn *formulaFuncs) DAY(argsList *list.List) formulaArg { return newNumberFormulaArg(float64(timeFromExcelTime(num.Number, false).Day())) } +// DAYS function returns the number of days between two supplied dates. The +// syntax of the function is: +// +// DAYS(end_date,start_date) +// +func (fn *formulaFuncs) DAYS(argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, "DAYS requires 2 arguments") + } + var end, start float64 + endArg, startArg := argsList.Front().Value.(formulaArg), argsList.Back().Value.(formulaArg) + switch endArg.Type { + case ArgNumber: + end = endArg.Number + case ArgString: + endNum := endArg.ToNumber() + if endNum.Type == ArgNumber { + end = endNum.Number + } else { + args := list.New() + args.PushBack(endArg) + endValue := fn.DATEVALUE(args) + if endValue.Type == ArgError { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + end = endValue.Number + } + default: + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + switch startArg.Type { + case ArgNumber: + start = startArg.Number + case ArgString: + startNum := startArg.ToNumber() + if startNum.Type == ArgNumber { + start = startNum.Number + } else { + args := list.New() + args.PushBack(startArg) + startValue := fn.DATEVALUE(args) + if startValue.Type == ArgError { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + start = startValue.Number + } + default: + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + return newNumberFormulaArg(end - start) +} + // MONTH function returns the month of a date represented by a serial number. // The month is given as an integer, ranging from 1 (January) to 12 // (December). The syntax of the function is: diff --git a/calc_test.go b/calc_test.go index 1e4d99b82b..54629a6e04 100644 --- a/calc_test.go +++ b/calc_test.go @@ -979,6 +979,10 @@ func TestCalcCellValue(t *testing.T) { "=DAY(\"3-February-2008\")": "3", "=DAY(\"01/25/20\")": "25", "=DAY(\"01/25/31\")": "25", + // DAYS + "=DAYS(2,1)": "1", + "=DAYS(INT(2),INT(1))": "1", + "=DAYS(\"02/02/2015\",\"01/01/2015\")": "32", // MONTH "=MONTH(42171)": "6", "=MONTH(\"31-May-2015\")": "5", @@ -2040,6 +2044,12 @@ func TestCalcCellValue(t *testing.T) { "=DAY(\"3-January-9223372036854775808\")": "#VALUE!", "=DAY(\"9223372036854775808-January-1900\")": "#VALUE!", "=DAY(\"0-January-1900\")": "#VALUE!", + // DAYS + "=DAYS()": "DAYS requires 2 arguments", + "=DAYS(\"\",0)": "#VALUE!", + "=DAYS(0,\"\")": "#VALUE!", + "=DAYS(NA(),0)": "#VALUE!", + "=DAYS(0,NA())": "#VALUE!", // MONTH "=MONTH()": "MONTH requires exactly 1 argument", "=MONTH(0,0)": "MONTH requires exactly 1 argument", From 490f3063c2cb35a94d64f6a6859cce7b9dee276d Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 26 Sep 2021 00:07:40 +0800 Subject: [PATCH 450/957] This closes #1026, time parse accuracy issue and typo fixed --- calc.go | 2 +- cell.go | 22 ++++++++++++++++------ cell_test.go | 7 +++++++ rows.go | 7 ------- 4 files changed, 24 insertions(+), 14 deletions(-) diff --git a/calc.go b/calc.go index 266491f473..4303b433be 100644 --- a/calc.go +++ b/calc.go @@ -103,7 +103,7 @@ var ( "june": 6, "july": 7, "august": 8, - "septemper": 9, + "september": 9, "october": 10, "november": 11, "december": 12, diff --git a/cell.go b/cell.go index 902f5b7ce3..b95db9c3fb 100644 --- a/cell.go +++ b/cell.go @@ -1043,13 +1043,18 @@ func (f *File) getCellStringFunc(sheet, axis string, fn func(x *xlsxWorksheet, c // 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 { + precise := v + isNum, precision := isNumeric(v) + if isNum && precision > 10 { + precise, _ = roundPrecision(v) + } if s == 0 || raw { - return v + return precise } styleSheet := f.stylesReader() if s >= len(styleSheet.CellXfs.Xf) { - return v + return precise } var numFmtID int if styleSheet.CellXfs.Xf[s].NumFmtID != nil { @@ -1061,18 +1066,23 @@ func (f *File) formattedValue(s int, v string, raw bool) string { return ok(v, builtInNumFmt[numFmtID]) } if styleSheet == nil || styleSheet.NumFmts == nil { - return v + return precise } for _, xlsxFmt := range styleSheet.NumFmts.NumFmt { if xlsxFmt.NumFmtID == numFmtID { format := strings.ToLower(xlsxFmt.FormatCode) - if strings.Contains(format, "y") || strings.Contains(format, "m") || strings.Contains(strings.Replace(format, "red", "", -1), "d") || strings.Contains(format, "h") { + if isTimeNumFmt(format) { return parseTime(v, format) } - return v + return precise } } - return v + return precise +} + +// isTimeNumFmt determine if the given number format expression is a time number format. +func isTimeNumFmt(format string) bool { + return strings.Contains(format, "y") || strings.Contains(format, "m") || strings.Contains(strings.Replace(format, "red", "", -1), "d") || strings.Contains(format, "h") } // prepareCellStyle provides a function to prepare style index of cell in diff --git a/cell_test.go b/cell_test.go index 35eaa9637b..126e5614f3 100644 --- a/cell_test.go +++ b/cell_test.go @@ -243,6 +243,13 @@ func TestGetCellValue(t *testing.T) { {"", "", "", "", "", "", "", "H6"}, }, rows) assert.NoError(t, err) + + f.Sheet.Delete("xl/worksheets/sheet1.xml") + f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `2422.30000000000022422.300000000000212.49641101.5999999999999275.3999999999999868.90000000000000644385.2083333333365.09999999999999965.11000000000000035.09999999999999965.11099999999999985.11110000000000042422.0123456782422.012345678912.0123456789019641101.5999999999999275.3999999999999868.900000000000006`))) + f.checked = nil + rows, err = f.GetRows("Sheet1") + assert.Equal(t, [][]string{{"2422.3", "2422.3", "12.4", "964", "1101.6", "275.4", "68.9", "44385.20833333333", "5.1", "5.11", "5.1", "5.111", "5.1111", "2422.012345678", "2422.0123456789", "12.012345678901", "964", "1101.6", "275.4", "68.9"}}, rows) + assert.NoError(t, err) } func TestGetCellType(t *testing.T) { diff --git a/rows.go b/rows.go index 5c7b22df79..3d8c247f3c 100644 --- a/rows.go +++ b/rows.go @@ -417,13 +417,6 @@ func (c *xlsxC) getValueFrom(f *File, d *xlsxSST, raw bool) (string, error) { } return f.formattedValue(c.S, c.V, raw), nil default: - isNum, precision := isNumeric(c.V) - if isNum && precision > 10 { - val, _ := roundPrecision(c.V) - if val != c.V { - return f.formattedValue(c.S, val, raw), nil - } - } return f.formattedValue(c.S, c.V, raw), nil } } From 2d8b5b1885b3d5cd14c974df61a3d0d757efd7bd Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 28 Sep 2021 22:02:31 +0800 Subject: [PATCH 451/957] This closes #1027 and closes #1028 * Fix build-in scientific number format failed * An error will be returned if given an invalid custom number format when creating a new style --- errors.go | 2 ++ styles.go | 5 ++++- styles_test.go | 16 ++++++++++++++-- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/errors.go b/errors.go index 9fc0957785..e89fea1b43 100644 --- a/errors.go +++ b/errors.go @@ -112,6 +112,8 @@ var ( // 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") + // ErrCustomNumFmt defined the error message on receive the empty parameter. + 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") diff --git a/styles.go b/styles.go index e4f3a276ab..0ae9e516a4 100644 --- a/styles.go +++ b/styles.go @@ -930,7 +930,7 @@ func formatToE(v string, format string) string { if err != nil { return v } - return fmt.Sprintf("%.e", f) + return fmt.Sprintf("%.2E", f) } // parseTime provides a function to returns a string parsed using time.Time. @@ -1115,6 +1115,9 @@ func parseFormatStyleSet(style interface{}) (*Style, error) { return &fs, ErrFontSize } } + if fs.CustomNumFmt != nil && len(*fs.CustomNumFmt) == 0 { + err = ErrCustomNumFmt + } return &fs, err } diff --git a/styles_test.go b/styles_test.go index 5d452f6f28..a214aaa2f5 100644 --- a/styles_test.go +++ b/styles_test.go @@ -203,10 +203,13 @@ func TestNewStyle(t *testing.T) { _, err = f.NewStyle(Style{}) assert.EqualError(t, err, ErrParameterInvalid.Error()) + var exp string + _, err = f.NewStyle(&Style{CustomNumFmt: &exp}) + assert.EqualError(t, err, ErrCustomNumFmt.Error()) _, err = f.NewStyle(&Style{Font: &Font{Family: strings.Repeat("s", MaxFontFamilyLength+1)}}) - assert.EqualError(t, err, "the length of the font family name must be smaller than or equal to 31") + assert.EqualError(t, err, ErrFontLength.Error()) _, err = f.NewStyle(&Style{Font: &Font{Size: MaxFontSize + 1}}) - assert.EqualError(t, err, "font size must be between 1 and 409 points") + assert.EqualError(t, err, ErrFontSize.Error()) // new numeric custom style fmt := "####;####" @@ -240,6 +243,15 @@ func TestNewStyle(t *testing.T) { nf = f.Styles.CellXfs.Xf[styleID] assert.Equal(t, 32, *nf.NumFmtID) + + // 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)) + assert.NoError(t, f.SetSheetRow("Sheet1", "A1", &[]float64{1.23, 1.234})) + rows, err := f.GetRows("Sheet1") + assert.NoError(t, err) + assert.Equal(t, [][]string{{"1.23E+00", "1.23E+00"}}, rows) } func TestGetDefaultFont(t *testing.T) { From e52e75528260745d87fb0962fe239f54c1b5b390 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 29 Sep 2021 23:08:17 +0800 Subject: [PATCH 452/957] Now support set row style in the stream writer --- stream.go | 17 ++++++++++++++--- stream_test.go | 2 +- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/stream.go b/stream.go index 125b58ce40..65d6b72179 100644 --- a/stream.go +++ b/stream.go @@ -84,6 +84,12 @@ type StreamWriter struct { // 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}); +// func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) { sheetID := f.getSheetID(sheet) if sheetID == -1 { @@ -289,10 +295,12 @@ type Cell struct { Value interface{} } -// RowOpts define the options for set row. +// 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 + Height float64 + Hidden bool + StyleID int } // SetRow writes an array to stream rows by giving a worksheet name, starting @@ -356,6 +364,9 @@ func marshalRowAttrs(opts ...RowOpts) (attrs string, err error) { err = ErrMaxRowHeight return } + if opt.StyleID > 0 { + attrs += fmt.Sprintf(` s="%d" customFormat="true"`, opt.StyleID) + } if opt.Height > 0 { attrs += fmt.Sprintf(` ht="%v" customHeight="true"`, opt.Height) } diff --git a/stream_test.go b/stream_test.go index 6cfed07170..52cee4de6e 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(`{"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}) + 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{Hidden: true})) From 28841af9804243205a9693be0cb501ce2d980302 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 1 Oct 2021 21:43:02 +0800 Subject: [PATCH 453/957] initialize formula function TRANSPOSE, ref #65 and remove unused exported error variable ErrToExcelTime --- calc.go | 33 +++++++++++++++++++++++++++++++++ calc_test.go | 17 +++++++++++++++++ errors.go | 4 +--- 3 files changed, 51 insertions(+), 3 deletions(-) diff --git a/calc.go b/calc.go index 4303b433be..1c6edca055 100644 --- a/calc.go +++ b/calc.go @@ -497,6 +497,7 @@ type formulaFuncs struct { // TAN // TANH // TODAY +// TRANSPOSE // TRIM // TRUE // TRUNC @@ -7794,6 +7795,38 @@ func (fn *formulaFuncs) MATCH(argsList *list.List) formulaArg { return calcMatch(matchType, formulaCriteriaParser(argsList.Front().Value.(formulaArg).String), lookupArray) } +// TRANSPOSE function 'transposes' an array of cells (i.e. the function copies +// a horizontal range of cells into a vertical range and vice versa). The +// syntax of the function is: +// +// TRANSPOSE(array) +// +func (fn *formulaFuncs) TRANSPOSE(argsList *list.List) formulaArg { + if argsList.Len() != 1 { + return newErrorFormulaArg(formulaErrorVALUE, "TRANSPOSE requires 1 argument") + } + args := argsList.Back().Value.(formulaArg).ToList() + rmin, rmax := calcRowsMinMax(argsList) + cmin, cmax := calcColumnsMinMax(argsList) + cols, rows := cmax-cmin+1, rmax-rmin+1 + src := make([][]formulaArg, 0) + for i := 0; i < len(args); i += cols { + src = append(src, args[i:i+cols]) + } + mtx := make([][]formulaArg, cols) + for r, row := range src { + colIdx := r % rows + for c, cell := range row { + rowIdx := c % cols + if len(mtx[rowIdx]) == 0 { + mtx[rowIdx] = make([]formulaArg, rows) + } + mtx[rowIdx][colIdx] = cell + } + } + return newMatrixFormulaArg(mtx) +} + // VLOOKUP function 'looks up' a given value in the left-hand column of a // data array (or table), and returns the corresponding value from another // column of the array. The syntax of the function is: diff --git a/calc_test.go b/calc_test.go index 54629a6e04..a391a19551 100644 --- a/calc_test.go +++ b/calc_test.go @@ -2217,6 +2217,8 @@ func TestCalcCellValue(t *testing.T) { "=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", + // TRANSPOSE + "=TRANSPOSE()": "TRANSPOSE requires 1 argument", // VLOOKUP "=VLOOKUP()": "VLOOKUP requires at least 3 arguments", "=VLOOKUP(D2,D1,1,FALSE)": "VLOOKUP requires second argument of table array", @@ -2611,6 +2613,21 @@ func TestCalcMatchPattern(t *testing.T) { assert.False(t, matchPattern("file/?", "file/abc/bcd/def")) } +func TestCalcTRANSPOSE(t *testing.T) { + cellData := [][]interface{}{ + {"a", "d"}, + {"b", "e"}, + {"c", "f"}, + } + formula := "=TRANSPOSE(A1:A3)" + f := prepareCalcData(cellData) + formulaType, ref := STCellFormulaTypeArray, "D1:F2" + assert.NoError(t, f.SetCellFormula("Sheet1", "D1", formula, + FormulaOpts{Ref: &ref, Type: &formulaType})) + _, err := f.CalcCellValue("Sheet1", "D1") + assert.NoError(t, err, formula) +} + func TestCalcVLOOKUP(t *testing.T) { cellData := [][]interface{}{ {nil, nil, nil, nil, nil, nil}, diff --git a/errors.go b/errors.go index e89fea1b43..56a2280d12 100644 --- a/errors.go +++ b/errors.go @@ -79,8 +79,6 @@ var ( // ErrAddVBAProject defined the error message on add the VBA project in // the workbook. ErrAddVBAProject = errors.New("unsupported VBA project extension") - // ErrToExcelTime defined the error message on receive a not UTC time. - ErrToExcelTime = errors.New("only UTC time expected") // 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 @@ -112,7 +110,7 @@ var ( // 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") - // ErrCustomNumFmt defined the error message on receive the empty parameter. + // 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 // family name overflow. From aa8f6f02bdf933df6cffec6b408276d02ed9e6b0 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 11 Oct 2021 00:08:45 +0800 Subject: [PATCH 454/957] This closes #1029, support specify compact and outline for the pivot table --- pivotTable.go | 47 +++++++++++++++++++++++++++++++---------------- xmlPivotTable.go | 8 ++++---- 2 files changed, 35 insertions(+), 20 deletions(-) diff --git a/pivotTable.go b/pivotTable.go index 0b0e5af6d5..6ce0173b9f 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -19,6 +19,13 @@ import ( ) // PivotTableOption 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 { pivotTableSheetName string DataRange string @@ -63,8 +70,10 @@ 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 } @@ -277,13 +286,13 @@ func (f *File) addPivotCache(pivotCacheID int, pivotCacheXML string, opt *PivotT pc.CacheSource.WorksheetSource = &xlsxWorksheetSource{Name: opt.DataRange} } for _, name := range order { - defaultRowsSubtotal, rowOk := f.getPivotTableFieldNameDefaultSubtotal(name, opt.Rows) - defaultColumnsSubtotal, colOk := f.getPivotTableFieldNameDefaultSubtotal(name, opt.Columns) + rowOptions, rowOk := f.getPivotTableFieldOptions(name, opt.Rows) + columnOptions, colOk := f.getPivotTableFieldOptions(name, opt.Columns) sharedItems := xlsxSharedItems{ Count: 0, } s := xlsxString{} - if (rowOk && !defaultRowsSubtotal) || (colOk && !defaultColumnsSubtotal) { + if (rowOk && !rowOptions.DefaultSubtotal) || (colOk && !columnOptions.DefaultSubtotal) { s = xlsxString{ V: "", } @@ -522,22 +531,24 @@ func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opt *PivotTableOptio x := 0 for _, name := range order { if inPivotTableField(opt.Rows, name) != -1 { - defaultSubtotal, ok := f.getPivotTableFieldNameDefaultSubtotal(name, opt.Rows) + rowOptions, ok := f.getPivotTableFieldOptions(name, opt.Rows) var items []*xlsxItem - if !ok || !defaultSubtotal { + if !ok || !rowOptions.DefaultSubtotal { items = append(items, &xlsxItem{X: &x}) } else { items = append(items, &xlsxItem{T: "default"}) } pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, &xlsxPivotField{ - Axis: "axisRow", - Name: f.getPivotTableFieldName(name, opt.Rows), + Name: f.getPivotTableFieldName(name, opt.Rows), + Axis: "axisRow", + Compact: &rowOptions.Compact, + Outline: &rowOptions.Outline, + DefaultSubtotal: &rowOptions.DefaultSubtotal, Items: &xlsxItems{ Count: len(items), Item: items, }, - DefaultSubtotal: &defaultSubtotal, }) continue } @@ -555,21 +566,23 @@ func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opt *PivotTableOptio continue } if inPivotTableField(opt.Columns, name) != -1 { - defaultSubtotal, ok := f.getPivotTableFieldNameDefaultSubtotal(name, opt.Columns) + columnOptions, ok := f.getPivotTableFieldOptions(name, opt.Columns) var items []*xlsxItem - if !ok || !defaultSubtotal { + if !ok || !columnOptions.DefaultSubtotal { items = append(items, &xlsxItem{X: &x}) } else { items = append(items, &xlsxItem{T: "default"}) } pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, &xlsxPivotField{ - Axis: "axisCol", - Name: f.getPivotTableFieldName(name, opt.Columns), + Name: f.getPivotTableFieldName(name, opt.Columns), + Axis: "axisCol", + Compact: &columnOptions.Compact, + Outline: &columnOptions.Outline, + DefaultSubtotal: &columnOptions.DefaultSubtotal, Items: &xlsxItems{ Count: len(items), Item: items, }, - DefaultSubtotal: &defaultSubtotal, }) continue } @@ -669,13 +682,15 @@ func (f *File) getPivotTableFieldName(name string, fields []PivotTableField) str return "" } -func (f *File) getPivotTableFieldNameDefaultSubtotal(name string, fields []PivotTableField) (bool, bool) { +// getPivotTableFieldOptions return options for specific field by given field name. +func (f *File) getPivotTableFieldOptions(name string, fields []PivotTableField) (options PivotTableField, ok bool) { for _, field := range fields { if field.Data == name { - return field.DefaultSubtotal, true + options, ok = field, true + return } } - return false, false + return } // addWorkbookPivotCache add the association ID of the pivot cache in workbook.xml. diff --git a/xmlPivotTable.go b/xmlPivotTable.go index 53249910f4..529b867a2c 100644 --- a/xmlPivotTable.go +++ b/xmlPivotTable.go @@ -71,8 +71,8 @@ type xlsxPivotTableDefinition struct { ShowEmptyRow bool `xml:"showEmptyRow,attr,omitempty"` ShowEmptyCol bool `xml:"showEmptyCol,attr,omitempty"` ShowHeaders bool `xml:"showHeaders,attr,omitempty"` - Compact bool `xml:"compact,attr"` - Outline bool `xml:"outline,attr"` + Compact *bool `xml:"compact,attr"` + Outline *bool `xml:"outline,attr"` OutlineData bool `xml:"outlineData,attr,omitempty"` CompactData *bool `xml:"compactData,attr,omitempty"` Published bool `xml:"published,attr,omitempty"` @@ -125,10 +125,10 @@ type xlsxPivotField struct { ShowDropDowns bool `xml:"showDropDowns,attr,omitempty"` HiddenLevel bool `xml:"hiddenLevel,attr,omitempty"` UniqueMemberProperty string `xml:"uniqueMemberProperty,attr,omitempty"` - Compact bool `xml:"compact,attr"` + Compact *bool `xml:"compact,attr"` AllDrilled bool `xml:"allDrilled,attr,omitempty"` NumFmtID string `xml:"numFmtId,attr,omitempty"` - Outline bool `xml:"outline,attr"` + Outline *bool `xml:"outline,attr"` SubtotalTop bool `xml:"subtotalTop,attr,omitempty"` DragToRow bool `xml:"dragToRow,attr,omitempty"` DragToCol bool `xml:"dragToCol,attr,omitempty"` From 58fd279dc845ebd9ccd4ba336d7c664824e70e43 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 12 Oct 2021 00:01:11 +0800 Subject: [PATCH 455/957] This closes #1030, fix date rounding error --- date.go | 5 +++-- date_test.go | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/date.go b/date.go index b8c26e008a..c4acd6d575 100644 --- a/date.go +++ b/date.go @@ -20,6 +20,7 @@ const ( nanosInADay = float64((24 * time.Hour) / time.Nanosecond) dayNanoseconds = 24 * time.Hour maxDuration = 290 * 364 * dayNanoseconds + roundEpsilon = 1e-9 ) var ( @@ -151,14 +152,14 @@ func timeFromExcelTime(excelTime float64, date1904 bool) time.Time { } return date } - var floatPart = excelTime - float64(wholeDaysPart) + var floatPart = excelTime - float64(wholeDaysPart) + roundEpsilon if date1904 { date = excel1904Epoc } else { date = excel1900Epoc } durationPart := time.Duration(nanosInADay * floatPart) - return date.AddDate(0, 0, wholeDaysPart).Add(durationPart) + return date.AddDate(0, 0, wholeDaysPart).Add(durationPart).Truncate(time.Second) } // ExcelDateToTime converts a float-based excel date representation to a time.Time. diff --git a/date_test.go b/date_test.go index 2addc4ab99..cc516d572a 100644 --- a/date_test.go +++ b/date_test.go @@ -33,6 +33,7 @@ var excelTimeInputList = []dateTest{ {60.0, time.Date(1900, 2, 28, 0, 0, 0, 0, time.UTC)}, {61.0, time.Date(1900, 3, 1, 0, 0, 0, 0, time.UTC)}, {41275.0, time.Date(2013, 1, 1, 0, 0, 0, 0, time.UTC)}, + {44450.3333333333, time.Date(2021, time.September, 11, 8, 0, 0, 0, time.UTC)}, {401769.0, time.Date(3000, 1, 1, 0, 0, 0, 0, time.UTC)}, } @@ -66,6 +67,19 @@ func TestTimeFromExcelTime(t *testing.T) { assert.Equal(t, test.GoValue, timeFromExcelTime(test.ExcelValue, false)) }) } + for hour := 0; hour < 24; hour++ { + 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) + 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()) + } + } + } } func TestTimeFromExcelTime_1904(t *testing.T) { From de38402f74bea9557188ad2c6f31c3127fd1bbfe Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 15 Oct 2021 21:45:46 +0800 Subject: [PATCH 456/957] This closes #1031, fix small float parsed error in some case - new formula function: YEARFRAC, ref #65 - update the codecov version - remove unused variable --- .github/workflows/go.yml | 2 +- calc.go | 161 +++++++++++++++++++++++++++++++++++++-- calc_test.go | 121 ++++++++++++++++++----------- cell.go | 22 +++--- cell_test.go | 60 ++++++++++++++- rows.go | 20 +++-- rows_test.go | 5 -- 7 files changed, 313 insertions(+), 78 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 5f674c3aeb..b9841144cb 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@v1 + uses: codecov/codecov-action@v2 with: file: coverage.txt flags: unittests diff --git a/calc.go b/calc.go index 1c6edca055..a4ecb66749 100644 --- a/calc.go +++ b/calc.go @@ -55,13 +55,6 @@ const ( criteriaG criteriaErr criteriaRegexp - // Numeric precision correct numeric values as legacy Excel application - // https://en.wikipedia.org/wiki/Numeric_precision_in_Microsoft_Excel In the - // top figure the fraction 1/9000 in Excel is displayed. Although this number - // has a decimal representation that is an infinite string of ones, Excel - // displays only the leading 15 figures. In the second line, the number one - // is added to the fraction, and again Excel displays only 15 figures. - numericPrecision = 1000000000000000 maxFinancialIterations = 128 financialPercision = 1.0e-08 // Date and time format regular expressions @@ -511,6 +504,7 @@ type formulaFuncs struct { // WEIBULL.DIST // XOR // YEAR +// YEARFRAC // Z.TEST // ZTEST // @@ -533,7 +527,7 @@ func (f *File) CalcCellValue(sheet, cell string) (result string, err error) { result = token.TValue isNum, precision := isNumeric(result) if isNum && precision > 15 { - num, _ := roundPrecision(result) + num := roundPrecision(result, -1) result = strings.ToUpper(num) } return @@ -6689,6 +6683,157 @@ func (fn *formulaFuncs) YEAR(argsList *list.List) formulaArg { return newNumberFormulaArg(float64(timeFromExcelTime(num.Number, false).Year())) } +// yearFracBasisCond is an implementation of the yearFracBasis1. +func yearFracBasisCond(sy, sm, sd, ey, em, ed int) bool { + return (isLeapYear(sy) && (sm < 2 || (sm == 2 && sd <= 29))) || (isLeapYear(ey) && (em > 2 || (em == 2 && ed == 29))) +} + +// yearFracBasis0 function returns the fraction of a year that between two +// supplied dates in US (NASD) 30/360 type of day. +func yearFracBasis0(startDate, endDate float64) (dayDiff, daysInYear float64) { + startTime, endTime := timeFromExcelTime(startDate, false), timeFromExcelTime(endDate, false) + sy, smM, sd := startTime.Date() + ey, emM, ed := endTime.Date() + sm, em := int(smM), int(emM) + if sd == 31 { + sd-- + } + if sd == 30 && ed == 31 { + ed-- + } else if leap := isLeapYear(sy); sm == 2 && ((leap && sd == 29) || (!leap && sd == 28)) { + sd = 30 + if leap := isLeapYear(ey); em == 2 && ((leap && ed == 29) || (!leap && ed == 28)) { + ed = 30 + } + } + dayDiff = float64((ey-sy)*360 + (em-sm)*30 + (ed - sd)) + daysInYear = 360 + return +} + +// yearFracBasis1 function returns the fraction of a year that between two +// supplied dates in actual type of day. +func yearFracBasis1(startDate, endDate float64) (dayDiff, daysInYear float64) { + startTime, endTime := timeFromExcelTime(startDate, false), timeFromExcelTime(endDate, false) + sy, smM, sd := startTime.Date() + ey, emM, ed := endTime.Date() + sm, em := int(smM), int(emM) + dayDiff = endDate - startDate + isYearDifferent := sy != ey + if isYearDifferent && (ey != sy+1 || sm < em || (sm == em && sd < ed)) { + dayCount := 0 + for y := sy; y <= ey; y++ { + dayCount += getYearDays(y, 1) + } + daysInYear = float64(dayCount) / float64(ey-sy+1) + } else { + if !isYearDifferent && isLeapYear(sy) { + daysInYear = 366 + } else { + if isYearDifferent && yearFracBasisCond(sy, sm, sd, ey, em, ed) { + daysInYear = 366 + } else { + daysInYear = 365 + } + } + } + return +} + +// yearFracBasis4 function returns the fraction of a year that between two +// supplied dates in European 30/360 type of day. +func yearFracBasis4(startDate, endDate float64) (dayDiff, daysInYear float64) { + startTime, endTime := timeFromExcelTime(startDate, false), timeFromExcelTime(endDate, false) + sy, smM, sd := startTime.Date() + ey, emM, ed := endTime.Date() + sm, em := int(smM), int(emM) + if sd == 31 { + sd-- + } + if ed == 31 { + ed-- + } + dayDiff = float64((ey-sy)*360 + (em-sm)*30 + (ed - sd)) + daysInYear = 360 + return +} + +// yearFrac is an implementation of the formula function YEARFRAC. +func yearFrac(startDate, endDate float64, basis int) formulaArg { + startTime, endTime := timeFromExcelTime(startDate, false), timeFromExcelTime(endDate, false) + if startTime == endTime { + return newNumberFormulaArg(0) + } + var dayDiff, daysInYear float64 + switch basis { + case 0: + dayDiff, daysInYear = yearFracBasis0(startDate, endDate) + case 1: + dayDiff, daysInYear = yearFracBasis1(startDate, endDate) + case 2: + dayDiff = endDate - startDate + daysInYear = 360 + case 3: + dayDiff = endDate - startDate + daysInYear = 365 + case 4: + dayDiff, daysInYear = yearFracBasis4(startDate, endDate) + default: + return newErrorFormulaArg(formulaErrorNUM, "invalid basis") + } + return newNumberFormulaArg(dayDiff / daysInYear) +} + +// getYearDays return days of the year with specifying the type of day count +// basis to be used. +func getYearDays(year, basis int) int { + switch basis { + case 1: + if isLeapYear(year) { + return 366 + } + return 365 + case 3: + return 365 + default: + return 360 + } +} + +// YEARFRAC function returns the fraction of a year that is represented by the +// number of whole days between two supplied dates. The syntax of the +// function is: +// +// 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") + } + var basisArg formulaArg + startArg, endArg := argsList.Front().Value.(formulaArg).ToNumber(), argsList.Front().Next().Value.(formulaArg).ToNumber() + args := list.New().Init() + if startArg.Type != ArgNumber { + args.PushBack(argsList.Front().Value.(formulaArg)) + if startArg = fn.DATEVALUE(args); startArg.Type != ArgNumber { + return startArg + } + } + if endArg.Type != ArgNumber { + args.Init() + args.PushBack(argsList.Front().Next().Value.(formulaArg)) + if endArg = fn.DATEVALUE(args); endArg.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + } + if argsList.Len() == 3 { + if basisArg = argsList.Back().Value.(formulaArg).ToNumber(); basisArg.Type != ArgNumber { + return basisArg + } + } + return yearFrac(startArg.Number, endArg.Number, int(basisArg.Number)) +} + // NOW function returns the current date and time. The function receives no // arguments and therefore. The syntax of the function is: // diff --git a/calc_test.go b/calc_test.go index a391a19551..de34723ef4 100644 --- a/calc_test.go +++ b/calc_test.go @@ -57,12 +57,12 @@ func TestCalcCellValue(t *testing.T) { // BESSELJ "=BESSELJ(1.9,2)": "0.329925727692387", // BESSELK - "=BESSELK(0.05,0)": "3.114234034289662", + "=BESSELK(0.05,0)": "3.11423403428966", "=BESSELK(0.05,1)": "19.90967432724863", "=BESSELK(0.05,2)": "799.501207124235", - "=BESSELK(3,2)": "0.061510458561912", + "=BESSELK(3,2)": "0.0615104585619118", // BESSELY - "=BESSELY(0.05,0)": "-1.979311006841528", + "=BESSELY(0.05,0)": "-1.97931100684153", "=BESSELY(0.05,1)": "-12.789855163794034", "=BESSELY(0.05,2)": "-509.61489554491976", "=BESSELY(9,2)": "-0.229082087487741", @@ -169,7 +169,7 @@ func TestCalcCellValue(t *testing.T) { "=IMCOS(0.5)": "0.877582561890373", "=IMCOS(\"3+0.5i\")": "-1.1163412445261518-0.0735369737112366i", // IMCOSH - "=IMCOSH(0.5)": "1.127625965206381", + "=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", @@ -188,7 +188,7 @@ func TestCalcCellValue(t *testing.T) { "=IMDIV(COMPLEX(5,2),COMPLEX(0,1))": "2-5i", // IMEXP "=IMEXP(0)": "1", - "=IMEXP(0.5)": "1.648721270700128", + "=IMEXP(0.5)": "1.64872127070013", "=IMEXP(\"1-2i\")": "-1.1312043837568135-2.4717266720048183i", "=IMEXP(COMPLEX(1,-1))": "1.4686939399158851-2.2873552871788423i", // IMLN @@ -243,7 +243,7 @@ func TestCalcCellValue(t *testing.T) { "=IMSUM(COMPLEX(5,2),COMPLEX(0,1))": "5+3i", // IMTAN "=IMTAN(-0)": "0", - "=IMTAN(0.5)": "0.546302489843791", + "=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", @@ -275,21 +275,21 @@ func TestCalcCellValue(t *testing.T) { "=ABS(ABS(-1))": "1", // ACOS "=ACOS(-1)": "3.141592653589793", - "=ACOS(0)": "1.570796326794897", - "=ACOS(ABS(0))": "1.570796326794897", + "=ACOS(0)": "1.5707963267949", + "=ACOS(ABS(0))": "1.5707963267949", // ACOSH "=ACOSH(1)": "0", "=ACOSH(2.5)": "1.566799236972411", - "=ACOSH(5)": "2.292431669561178", - "=ACOSH(ACOSH(5))": "1.471383321536679", + "=ACOSH(5)": "2.29243166956118", + "=ACOSH(ACOSH(5))": "1.47138332153668", // ACOT "=_xlfn.ACOT(1)": "0.785398163397448", "=_xlfn.ACOT(-2)": "2.677945044588987", - "=_xlfn.ACOT(0)": "1.570796326794897", + "=_xlfn.ACOT(0)": "1.5707963267949", "=_xlfn.ACOT(_xlfn.ACOT(0))": "0.566911504941009", // ACOTH "=_xlfn.ACOTH(-5)": "-0.202732554054082", - "=_xlfn.ACOTH(1.1)": "1.522261218861711", + "=_xlfn.ACOTH(1.1)": "1.52226121886171", "=_xlfn.ACOTH(2)": "0.549306144334055", "=_xlfn.ACOTH(ABS(-2))": "0.549306144334055", // ARABIC @@ -299,12 +299,12 @@ func TestCalcCellValue(t *testing.T) { "=_xlfn.ARABIC(\"\")": "0", "=_xlfn.ARABIC(\" ll lc \")": "-50", // ASIN - "=ASIN(-1)": "-1.570796326794897", + "=ASIN(-1)": "-1.5707963267949", "=ASIN(0)": "0", "=ASIN(ASIN(0))": "0", // ASINH "=ASINH(0)": "0", - "=ASINH(-0.5)": "-0.481211825059604", + "=ASINH(-0.5)": "-0.481211825059603", "=ASINH(2)": "1.44363547517881", "=ASINH(ASINH(0))": "0", // ATAN @@ -383,22 +383,22 @@ func TestCalcCellValue(t *testing.T) { "=COS(COS(0))": "0.54030230586814", // COSH "=COSH(0)": "1", - "=COSH(0.5)": "1.127625965206381", - "=COSH(-2)": "3.762195691083632", - "=COSH(COSH(0))": "1.543080634815244", + "=COSH(0.5)": "1.12762596520638", + "=COSH(-2)": "3.76219569108363", + "=COSH(COSH(0))": "1.54308063481524", // _xlfn.COT - "=_xlfn.COT(0.785398163397448)": "1.000000000000001", + "=_xlfn.COT(0.785398163397448)": "1", "=_xlfn.COT(_xlfn.COT(0.45))": "-0.545473116787229", // _xlfn.COTH - "=_xlfn.COTH(-3.14159265358979)": "-1.003741873197322", - "=_xlfn.COTH(_xlfn.COTH(1))": "1.156014018113954", + "=_xlfn.COTH(-3.14159265358979)": "-1.00374187319732", + "=_xlfn.COTH(_xlfn.COTH(1))": "1.15601401811395", // _xlfn.CSC - "=_xlfn.CSC(-6)": "3.578899547254406", + "=_xlfn.CSC(-6)": "3.57889954725441", "=_xlfn.CSC(1.5707963267949)": "1", - "=_xlfn.CSC(_xlfn.CSC(1))": "1.077851840310882", + "=_xlfn.CSC(_xlfn.CSC(1))": "1.07785184031088", // _xlfn.CSCH - "=_xlfn.CSCH(-3.14159265358979)": "-0.086589537530047", - "=_xlfn.CSCH(_xlfn.CSCH(1))": "1.044510103955183", + "=_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", @@ -419,9 +419,9 @@ func TestCalcCellValue(t *testing.T) { "=EVEN((0))": "0", // EXP "=EXP(100)": "2.6881171418161356E+43", - "=EXP(0.1)": "1.105170918075648", + "=EXP(0.1)": "1.10517091807565", "=EXP(0)": "1", - "=EXP(-5)": "0.006737946999085", + "=EXP(-5)": "0.00673794699908547", "=EXP(EXP(0))": "2.718281828459045", // FACT "=FACT(3)": "6", @@ -502,18 +502,18 @@ func TestCalcCellValue(t *testing.T) { "=LN(1)": "0", "=LN(100)": "4.605170185988092", "=LN(0.5)": "-0.693147180559945", - "=LN(LN(100))": "1.527179625807901", + "=LN(LN(100))": "1.5271796258079", // LOG "=LOG(64,2)": "6", "=LOG(100)": "2", "=LOG(4,0.5)": "-2", - "=LOG(500)": "2.698970004336019", + "=LOG(500)": "2.69897000433602", "=LOG(LOG(100))": "0.301029995663981", // LOG10 "=LOG10(100)": "2", "=LOG10(1000)": "3", "=LOG10(0.001)": "-3", - "=LOG10(25)": "1.397940008672038", + "=LOG10(25)": "1.39794000867204", "=LOG10(LOG10(100))": "0.301029995663981", // IMLOG2 "=IMLOG2(\"5+2i\")": "2.4289904975637864+0.5489546632866347i", @@ -626,9 +626,9 @@ func TestCalcCellValue(t *testing.T) { "=_xlfn.SEC(0)": "1", "=_xlfn.SEC(_xlfn.SEC(0))": "0.54030230586814", // SECH - "=_xlfn.SECH(-3.14159265358979)": "0.086266738334055", + "=_xlfn.SECH(-3.14159265358979)": "0.0862667383340547", "=_xlfn.SECH(0)": "1", - "=_xlfn.SECH(_xlfn.SECH(0))": "0.648054273663886", + "=_xlfn.SECH(_xlfn.SECH(0))": "0.648054273663885", // SIGN "=SIGN(9.5)": "1", "=SIGN(-9.5)": "-1", @@ -665,10 +665,10 @@ func TestCalcCellValue(t *testing.T) { "=STDEVA(MUNIT(2))": "0.577350269189626", "=STDEVA(0,INT(0))": "0", // POISSON.DIST - "=POISSON.DIST(20,25,FALSE)": "0.051917468608491", + "=POISSON.DIST(20,25,FALSE)": "0.0519174686084913", "=POISSON.DIST(35,40,TRUE)": "0.242414197690103", // POISSON - "=POISSON(20,25,FALSE)": "0.051917468608491", + "=POISSON(20,25,FALSE)": "0.0519174686084913", "=POISSON(35,40,TRUE)": "0.242414197690103", // SUM "=SUM(1,2)": "3", @@ -760,15 +760,15 @@ func TestCalcCellValue(t *testing.T) { "=GAMMA(1.5)": "0.886226925452758", "=GAMMA(5.5)": "52.34277778455352", // GAMMALN - "=GAMMALN(4.5)": "2.453736570842443", + "=GAMMALN(4.5)": "2.45373657084244", "=GAMMALN(INT(1))": "0", // HARMEAN - "=HARMEAN(2.5,3,0.5,1,3)": "1.229508196721312", - "=HARMEAN(\"2.5\",3,0.5,1,INT(3),\"\")": "1.229508196721312", + "=HARMEAN(2.5,3,0.5,1,3)": "1.22950819672131", + "=HARMEAN(\"2.5\",3,0.5,1,INT(3),\"\")": "1.22950819672131", // KURT - "=KURT(F1:F9)": "-1.033503502551368", - "=KURT(F1,F2:F9)": "-1.033503502551368", - "=KURT(INT(1),MUNIT(2))": "-3.333333333333336", + "=KURT(F1:F9)": "-1.03350350255137", + "=KURT(F1,F2:F9)": "-1.03350350255137", + "=KURT(INT(1),MUNIT(2))": "-3.33333333333334", // NORM.DIST "=NORM.DIST(0.8,1,0.3,TRUE)": "0.252492537546923", "=NORM.DIST(50,40,20,FALSE)": "0.017603266338215", @@ -993,6 +993,29 @@ func TestCalcCellValue(t *testing.T) { "=YEAR(42171)": "2015", "=YEAR(\"29-May-2015\")": "2015", "=YEAR(\"05/03/1984\")": "1984", + // YEARFRAC + "=YEARFRAC(42005,42005)": "0", + "=YEARFRAC(42005,42094)": "0.25", + "=YEARFRAC(42005,42094,0)": "0.25", + "=YEARFRAC(42005,42094,1)": "0.243835616438356", + "=YEARFRAC(42005,42094,2)": "0.247222222222222", + "=YEARFRAC(42005,42094,3)": "0.243835616438356", + "=YEARFRAC(42005,42094,4)": "0.247222222222222", + "=YEARFRAC(\"01/01/2015\",\"03/31/2015\")": "0.25", + "=YEARFRAC(\"01/01/2015\",\"03/31/2015\",0)": "0.25", + "=YEARFRAC(\"01/01/2015\",\"03/31/2015\",1)": "0.243835616438356", + "=YEARFRAC(\"01/01/2015\",\"03/31/2015\",2)": "0.247222222222222", + "=YEARFRAC(\"01/01/2015\",\"03/31/2015\",3)": "0.243835616438356", + "=YEARFRAC(\"01/01/2015\",\"03/31/2015\",4)": "0.247222222222222", + "=YEARFRAC(\"01/01/2015\",42094)": "0.25", + "=YEARFRAC(42005,\"03/31/2015\",0)": "0.25", + "=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\", \"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", // Text Functions // CHAR "=CHAR(65)": "A", @@ -1246,7 +1269,7 @@ func TestCalcCellValue(t *testing.T) { "=ISPMT(0.05/12,2,60,50000)": "-201.38888888888886", "=ISPMT(0.05/12,2,1,50000)": "208.33333333333334", // NOMINAL - "=NOMINAL(0.025,12)": "0.024718035238113", + "=NOMINAL(0.025,12)": "0.0247180352381129", // NPER "=NPER(0.04,-6000,50000)": "10.338035071507665", "=NPER(0,-6000,50000)": "8.333333333333334", @@ -2062,6 +2085,12 @@ func TestCalcCellValue(t *testing.T) { "=YEAR(-1)": "YEAR only accepts positive argument", "=YEAR(\"text\")": "#VALUE!", "=YEAR(\"January 25, 100\")": "#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", // NOW "=NOW(A1)": "NOW accepts no arguments", // TODAY @@ -2412,8 +2441,8 @@ func TestCalcCellValue(t *testing.T) { "=(-2-SUM(-4+A2))*5": "0", "=SUM(Sheet1!A1:Sheet1!A1:A2,A2)": "5", "=SUM(A1,A2,A3)*SUM(2,3)": "30", - "=1+SUM(SUM(A1+A2/A3)*(2-3),2)": "1.333333333333334", - "=A1/A2/SUM(A1:A2:B1)": "0.041666666666667", + "=1+SUM(SUM(A1+A2/A3)*(2-3),2)": "1.33333333333333", + "=A1/A2/SUM(A1:A2:B1)": "0.0416666666666667", "=A1/A2/SUM(A1:A2:B1)*A3": "0.125", "=SUM(B1:D1)": "4", "=SUM(\"X\")": "0", @@ -2760,7 +2789,7 @@ func TestCalcMIRR(t *testing.T) { cellData := [][]interface{}{{-100}, {18}, {22.5}, {28}, {35.5}, {45}} f := prepareCalcData(cellData) formulaList := map[string]string{ - "=MIRR(A1:A5,0.055,0.05)": "0.025376365108071", + "=MIRR(A1:A5,0.055,0.05)": "0.0253763651080707", "=MIRR(A1:A6,0.055,0.05)": "0.1000268752662", } for formula, expected := range formulaList { @@ -2848,3 +2877,9 @@ func TestStrToDate(t *testing.T) { _, _, _, _, err := strToDate("") assert.Equal(t, formulaErrorVALUE, err.Error) } + +func TestGetYearDays(t *testing.T) { + for _, data := range [][]int{{2021, 0, 360}, {2000, 1, 366}, {2021, 1, 365}, {2000, 3, 365}} { + assert.Equal(t, data[2], getYearDays(data[0], data[1])) + } +} diff --git a/cell.go b/cell.go index b95db9c3fb..41a9517886 100644 --- a/cell.go +++ b/cell.go @@ -70,15 +70,6 @@ func (f *File) GetCellValue(sheet, axis string, opts ...Options) (string, error) // 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) { - cellTypes := map[string]CellType{ - "b": CellTypeBool, - "d": CellTypeDate, - "n": CellTypeNumber, - "e": CellTypeError, - "s": CellTypeString, - "str": CellTypeString, - "inlineStr": CellTypeString, - } var ( err error cellTypeStr string @@ -1046,9 +1037,16 @@ func (f *File) formattedValue(s int, v string, raw bool) string { precise := v isNum, precision := isNumeric(v) if isNum && precision > 10 { - precise, _ = roundPrecision(v) + precise = roundPrecision(v, -1) + } + if raw { + return v + } + if !isNum { + v = roundPrecision(v, 15) + precise = v } - if s == 0 || raw { + if s == 0 { return precise } styleSheet := f.stylesReader() @@ -1063,7 +1061,7 @@ func (f *File) formattedValue(s int, v string, raw bool) string { ok := builtInNumFmtFunc[numFmtID] if ok != nil { - return ok(v, builtInNumFmt[numFmtID]) + return ok(precise, builtInNumFmt[numFmtID]) } if styleSheet == nil || styleSheet.NumFmts == nil { return precise diff --git a/cell_test.go b/cell_test.go index 126e5614f3..0395ef4c34 100644 --- a/cell_test.go +++ b/cell_test.go @@ -245,10 +245,66 @@ 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.30000000000022422.300000000000212.49641101.5999999999999275.3999999999999868.90000000000000644385.2083333333365.09999999999999965.11000000000000035.09999999999999965.11099999999999985.11110000000000042422.0123456782422.012345678912.0123456789019641101.5999999999999275.3999999999999868.900000000000006`))) + 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 +`))) f.checked = nil rows, err = f.GetRows("Sheet1") - assert.Equal(t, [][]string{{"2422.3", "2422.3", "12.4", "964", "1101.6", "275.4", "68.9", "44385.20833333333", "5.1", "5.11", "5.1", "5.111", "5.1111", "2422.012345678", "2422.0123456789", "12.012345678901", "964", "1101.6", "275.4", "68.9"}}, rows) + assert.Equal(t, [][]string{{ + "2422.3", + "2422.3", + "12.4", + "964", + "1101.6", + "275.4", + "68.9", + "44385.2083333333", + "5.1", + "5.11", + "5.1", + "5.111", + "5.1111", + "2422.012345678", + "2422.0123456789", + "12.012345678901", + "964", + "1101.6", + "275.4", + "68.9", + "0.08888", + "0.00004", + "2422.3", + "1101.6", + "275.4", + "68.9", + "1.1", + }}, rows) assert.NoError(t, err) } diff --git a/rows.go b/rows.go index 3d8c247f3c..3171ab1491 100644 --- a/rows.go +++ b/rows.go @@ -18,6 +18,7 @@ import ( "io" "log" "math" + "math/big" "os" "strconv" @@ -421,14 +422,19 @@ func (c *xlsxC) getValueFrom(f *File, d *xlsxSST, raw bool) (string, error) { } } -// roundPrecision round precision for numeric. -func roundPrecision(value string) (result string, err error) { - var num float64 - if num, err = strconv.ParseFloat(value, 64); err != nil { - return +// 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 decimal.Text('G', 15) + } + return strconv.FormatFloat(flt, 'f', -1, 64) } - result = fmt.Sprintf("%g", math.Round(num*numericPrecision)/numericPrecision) - return + return text } // SetRowVisible provides a function to set visible of a single row by given diff --git a/rows_test.go b/rows_test.go index 0ebe59d47e..19ed866b60 100644 --- a/rows_test.go +++ b/rows_test.go @@ -884,11 +884,6 @@ func TestGetValueFromNumber(t *testing.T) { } } -func TestRoundPrecision(t *testing.T) { - _, err := roundPrecision("") - assert.EqualError(t, err, "strconv.ParseFloat: parsing \"\": invalid syntax") -} - func TestErrSheetNotExistError(t *testing.T) { err := ErrSheetNotExist{SheetName: "Sheet1"} assert.EqualValues(t, err.Error(), "sheet Sheet1 is not exist") From c64ce0f9ac1ffa251d6645d527263e3a9a7f854b Mon Sep 17 00:00:00 2001 From: Tammy Date: Fri, 15 Oct 2021 23:23:26 +0800 Subject: [PATCH 457/957] Update the test case of the formula function `MIRR` --- calc_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/calc_test.go b/calc_test.go index de34723ef4..da95790fde 100644 --- a/calc_test.go +++ b/calc_test.go @@ -2789,7 +2789,6 @@ func TestCalcMIRR(t *testing.T) { cellData := [][]interface{}{{-100}, {18}, {22.5}, {28}, {35.5}, {45}} f := prepareCalcData(cellData) formulaList := map[string]string{ - "=MIRR(A1:A5,0.055,0.05)": "0.0253763651080707", "=MIRR(A1:A6,0.055,0.05)": "0.1000268752662", } for formula, expected := range formulaList { From cf8766df83f03152026c2301ff99eb7121d702ba Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 16 Oct 2021 16:05:50 +0800 Subject: [PATCH 458/957] ref #65, new formula function: TIME --- calc.go | 25 +++++++++++++++++++++++++ calc_test.go | 8 ++++++++ cell_test.go | 10 +++++----- 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/calc.go b/calc.go index a4ecb66749..b8cb645e17 100644 --- a/calc.go +++ b/calc.go @@ -489,6 +489,7 @@ type formulaFuncs struct { // T // TAN // TANH +// TIME // TODAY // TRANSPOSE // TRIM @@ -6848,6 +6849,30 @@ func (fn *formulaFuncs) NOW(argsList *list.List) formulaArg { return newNumberFormulaArg(25569.0 + float64(now.Unix()+int64(offset))/86400) } +// 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 +// function is: +// +// TIME(hour,minute,second) +// +func (fn *formulaFuncs) TIME(argsList *list.List) formulaArg { + if argsList.Len() != 3 { + return newErrorFormulaArg(formulaErrorVALUE, "TIME requires 3 number arguments") + } + h := argsList.Front().Value.(formulaArg).ToNumber() + m := argsList.Front().Next().Value.(formulaArg).ToNumber() + s := argsList.Back().Value.(formulaArg).ToNumber() + if h.Type != ArgNumber || m.Type != ArgNumber || s.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, "TIME requires 3 number arguments") + } + t := (h.Number*3600 + m.Number*60 + s.Number) / 86400 + if t < 0 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + return newNumberFormulaArg(t) +} + // TODAY function returns the current date. The function has no arguments and // therefore. The syntax of the function is: // diff --git a/calc_test.go b/calc_test.go index da95790fde..89f54ea6c6 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1016,6 +1016,10 @@ func TestCalcCellValue(t *testing.T) { "=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", + // TIME + "=TIME(5,44,32)": "0.239259259259259", + "=TIME(\"5\",\"44\",\"32\")": "0.239259259259259", + "=TIME(0,0,73)": "0.000844907407407407", // Text Functions // CHAR "=CHAR(65)": "A", @@ -2093,6 +2097,10 @@ func TestCalcCellValue(t *testing.T) { "=YEARFRAC(42005,42094,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", // NOW "=NOW(A1)": "NOW accepts no arguments", + // TIME + "=TIME()": "TIME requires 3 number arguments", + "=TIME(\"\",0,0)": "TIME requires 3 number arguments", + "=TIME(0,0,-1)": "#NUM!", // TODAY "=TODAY(A1)": "TODAY accepts no arguments", // Text Functions diff --git a/cell_test.go b/cell_test.go index 0395ef4c34..f699c05fb5 100644 --- a/cell_test.go +++ b/cell_test.go @@ -268,11 +268,11 @@ func TestGetCellValue(t *testing.T) { 68.900000000000006 8.8880000000000001E-2 4.0000000000000003E-5 - 2422.3000000000002 - 1101.5999999999999 - 275.39999999999998 - 68.900000000000006 - 1.1000000000000001 + 2422.3000000000002 + 1101.5999999999999 + 275.39999999999998 + 68.900000000000006 + 1.1000000000000001 `))) f.checked = nil rows, err = f.GetRows("Sheet1") From c89b64c53c8314b71374f85fabf29175646e10b2 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 17 Oct 2021 22:51:33 +0800 Subject: [PATCH 459/957] ref #65: new formula functions ACCRINTM and AMORDEGRC --- calc.go | 147 +++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 36 +++++++++++++ 2 files changed, 183 insertions(+) diff --git a/calc.go b/calc.go index b8cb645e17..d63bb3fabe 100644 --- a/calc.go +++ b/calc.go @@ -261,10 +261,12 @@ type formulaFuncs struct { // Supported formula functions: // // ABS +// ACCRINTM // ACOS // ACOSH // ACOT // ACOTH +// AMORDEGRC // AND // ARABIC // ASIN @@ -8312,6 +8314,151 @@ func (fn *formulaFuncs) ENCODEURL(argsList *list.List) formulaArg { // Financial Functions +// ACCRINTM function returns the accrued interest for a security that pays +// interest at maturity. The syntax of the function is: +// +// 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") + } + args := list.New().Init() + args.PushBack(argsList.Front().Value.(formulaArg)) + issue := fn.DATEVALUE(args) + if issue.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + args.Init() + args.PushBack(argsList.Front().Next().Value.(formulaArg)) + settlement := fn.DATEVALUE(args) + if settlement.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + if settlement.Number < issue.Number { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + rate := argsList.Front().Next().Next().Value.(formulaArg).ToNumber() + par := argsList.Front().Next().Next().Next().Value.(formulaArg).ToNumber() + if rate.Type != ArgNumber || par.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + if par.Number <= 0 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + basis := newNumberFormulaArg(0) + if argsList.Len() == 5 { + if basis = argsList.Back().Value.(formulaArg).ToNumber(); basis.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + } + frac := yearFrac(issue.Number, settlement.Number, int(basis.Number)) + if frac.Type != ArgNumber { + return frac + } + return newNumberFormulaArg(frac.Number * rate.Number * par.Number) +} + +// prepareAmorArgs checking and prepare arguments for the formula functions +// AMORDEGRC and AMORLINC. +func (fn *formulaFuncs) prepareAmorArgs(name string, argsList *list.List) formulaArg { + cost := argsList.Front().Value.(formulaArg).ToNumber() + if cost.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires cost to be number argument", name)) + } + if cost.Number < 0 { + return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires cost >= 0", name)) + } + args := list.New().Init() + args.PushBack(argsList.Front().Next().Value.(formulaArg)) + datePurchased := fn.DATEVALUE(args) + if datePurchased.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + args.Init() + args.PushBack(argsList.Front().Next().Next().Value.(formulaArg)) + firstPeriod := fn.DATEVALUE(args) + if firstPeriod.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + if firstPeriod.Number < datePurchased.Number { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + salvage := argsList.Front().Next().Next().Next().Value.(formulaArg).ToNumber() + if salvage.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + if salvage.Number < 0 || salvage.Number > cost.Number { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + period := argsList.Front().Next().Next().Next().Next().Value.(formulaArg).ToNumber() + if period.Type != ArgNumber || period.Number < 0 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + rate := argsList.Front().Next().Next().Next().Next().Next().Value.(formulaArg).ToNumber() + if rate.Type != ArgNumber || rate.Number < 0 { + 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 newListFormulaArg([]formulaArg{cost, datePurchased, firstPeriod, salvage, period, rate, basis}) +} + +// AMORDEGRC function is provided for users of the French accounting system. +// 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]) +// +func (fn *formulaFuncs) AMORDEGRC(argsList *list.List) formulaArg { + if argsList.Len() != 6 && argsList.Len() != 7 { + return newErrorFormulaArg(formulaErrorVALUE, "AMORDEGRC requires 6 or 7 arguments") + } + args := fn.prepareAmorArgs("AMORDEGRC", argsList) + if args.Type != ArgList { + return args + } + cost, datePurchased, firstPeriod, salvage, period, rate, basis := args.List[0], args.List[1], args.List[2], args.List[3], args.List[4], args.List[5], args.List[6] + if rate.Number >= 0.5 { + return newErrorFormulaArg(formulaErrorNUM, "AMORDEGRC requires rate to be < 0.5") + } + assetsLife, amorCoeff := 1/rate.Number, 2.5 + if assetsLife < 3 { + amorCoeff = 1 + } else if assetsLife < 5 { + amorCoeff = 1.5 + } else if assetsLife <= 6 { + amorCoeff = 2 + } + rate.Number *= amorCoeff + frac := yearFrac(datePurchased.Number, firstPeriod.Number, int(basis.Number)) + if frac.Type != ArgNumber { + return frac + } + nRate := float64(int((frac.Number * cost.Number * rate.Number) + 0.5)) + cost.Number -= nRate + rest := cost.Number - salvage.Number + for n := 0; n < int(period.Number); n++ { + nRate = float64(int((cost.Number * rate.Number) + 0.5)) + rest -= nRate + if rest < 0 { + switch int(period.Number) - n { + case 0: + case 1: + return newNumberFormulaArg(float64(int((cost.Number * 0.5) + 0.5))) + default: + return newNumberFormulaArg(0) + } + } + cost.Number -= nRate + } + return newNumberFormulaArg(nRate) +} + // CUMIPMT function calculates the cumulative interest paid on a loan or // investment, between two specified periods. The syntax of the function is: // diff --git a/calc_test.go b/calc_test.go index 89f54ea6c6..6d16ce9ca4 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1232,6 +1232,16 @@ func TestCalcCellValue(t *testing.T) { // ENCODEURL "=ENCODEURL(\"https://xuri.me/excelize/en/?q=Save As\")": "https%3A%2F%2Fxuri.me%2Fexcelize%2Fen%2F%3Fq%3DSave%20As", // Financial Functions + // ACCRINTM + "=ACCRINTM(\"01/01/2012\",\"12/31/2012\",8%,10000)": "800", + "=ACCRINTM(\"01/01/2012\",\"12/31/2012\",8%,10000,3)": "800", + // AMORDEGRC + "=AMORDEGRC(150,\"01/01/2015\",\"09/30/2015\",20,1,20%)": "42", + "=AMORDEGRC(150,\"01/01/2015\",\"09/30/2015\",20,1,20%,4)": "42", + "=AMORDEGRC(150,\"01/01/2015\",\"09/30/2015\",20,1,40%,4)": "42", + "=AMORDEGRC(150,\"01/01/2015\",\"09/30/2015\",20,1,25%,4)": "41", + "=AMORDEGRC(150,\"01/01/2015\",\"09/30/2015\",109,1,25%,4)": "54", + "=AMORDEGRC(150,\"01/01/2015\",\"09/30/2015\",110,2,25%,4)": "0", // CUMIPMT "=CUMIPMT(0.05/12,60,50000,1,12,0)": "-2294.97753732664", "=CUMIPMT(0.05/12,60,50000,13,24,0)": "-1833.1000665738893", @@ -2291,6 +2301,32 @@ func TestCalcCellValue(t *testing.T) { // ENCODEURL "=ENCODEURL()": "ENCODEURL requires 1 argument", // Financial Functions + // 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", + // 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", // CUMIPMT "=CUMIPMT()": "CUMIPMT requires 6 arguments", "=CUMIPMT(0,0,0,0,0,2)": "#N/A", From 620f873186e9f69a05d88f79cfb899b42e584331 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 18 Oct 2021 00:18:56 +0800 Subject: [PATCH 460/957] ref #65: new formula function AMORLINC --- calc.go | 35 +++++++++++++++++++++++++++++++++++ calc_test.go | 21 +++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/calc.go b/calc.go index d63bb3fabe..ba9085e4ee 100644 --- a/calc.go +++ b/calc.go @@ -267,6 +267,7 @@ type formulaFuncs struct { // ACOT // ACOTH // AMORDEGRC +// AMORLINC // AND // ARABIC // ASIN @@ -8459,6 +8460,40 @@ func (fn *formulaFuncs) AMORDEGRC(argsList *list.List) formulaArg { return newNumberFormulaArg(nRate) } +// AMORLINC function is provided for users of the French accounting system. +// 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]) +// +func (fn *formulaFuncs) AMORLINC(argsList *list.List) formulaArg { + if argsList.Len() != 6 && argsList.Len() != 7 { + return newErrorFormulaArg(formulaErrorVALUE, "AMORLINC requires 6 or 7 arguments") + } + args := fn.prepareAmorArgs("AMORLINC", argsList) + if args.Type != ArgList { + return args + } + cost, datePurchased, firstPeriod, salvage, period, rate, basis := args.List[0], args.List[1], args.List[2], args.List[3], args.List[4], args.List[5], args.List[6] + frac := yearFrac(datePurchased.Number, firstPeriod.Number, int(basis.Number)) + if frac.Type != ArgNumber { + return frac + } + rate1 := frac.Number * cost.Number * rate.Number + if period.Number == 0 { + return newNumberFormulaArg(rate1) + } + rate2 := cost.Number * rate.Number + delta := cost.Number - salvage.Number + periods := int((delta - rate1) / rate2) + if int(period.Number) <= periods { + return newNumberFormulaArg(rate2) + } else if int(period.Number)-1 == periods { + return newNumberFormulaArg(delta - rate2*float64(periods) - rate1) + } + return newNumberFormulaArg(0) +} + // CUMIPMT function calculates the cumulative interest paid on a loan or // investment, between two specified periods. The syntax of the function is: // diff --git a/calc_test.go b/calc_test.go index 6d16ce9ca4..abf6afaf9b 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1242,6 +1242,12 @@ func TestCalcCellValue(t *testing.T) { "=AMORDEGRC(150,\"01/01/2015\",\"09/30/2015\",20,1,25%,4)": "41", "=AMORDEGRC(150,\"01/01/2015\",\"09/30/2015\",109,1,25%,4)": "54", "=AMORDEGRC(150,\"01/01/2015\",\"09/30/2015\",110,2,25%,4)": "0", + // AMORLINC + "=AMORLINC(150,\"01/01/2015\",\"09/30/2015\",20,1,20%,4)": "30", + "=AMORLINC(150,\"01/01/2015\",\"09/30/2015\",20,1,0%,4)": "0", + "=AMORLINC(150,\"01/01/2015\",\"09/30/2015\",20,20,15%,4)": "0", + "=AMORLINC(150,\"01/01/2015\",\"09/30/2015\",20,6,15%,4)": "0.6875", + "=AMORLINC(150,\"01/01/2015\",\"09/30/2015\",20,0,15%,4)": "16.8125", // CUMIPMT "=CUMIPMT(0.05/12,60,50000,1,12,0)": "-2294.97753732664", "=CUMIPMT(0.05/12,60,50000,13,24,0)": "-1833.1000665738893", @@ -2327,6 +2333,21 @@ func TestCalcCellValue(t *testing.T) { "=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", + // 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", // CUMIPMT "=CUMIPMT()": "CUMIPMT requires 6 arguments", "=CUMIPMT(0,0,0,0,0,2)": "#N/A", From 5f907b78245a8a2601661e7f6b7ac6abba2d3812 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 19 Oct 2021 00:06:19 +0800 Subject: [PATCH 461/957] ref #65: new formula functions IFNA and IFS and fix string compare result issue in arithmetic operations --- calc.go | 153 +++++++++++++++++++++++++++++++++++++++++---------- calc_test.go | 101 +++++++++++++++++----------------- 2 files changed, 173 insertions(+), 81 deletions(-) diff --git a/calc.go b/calc.go index ba9085e4ee..60ca0cb2e9 100644 --- a/calc.go +++ b/calc.go @@ -357,6 +357,8 @@ type formulaFuncs struct { // HLOOKUP // IF // IFERROR +// IFNA +// IFS // IMABS // IMAGINARY // IMARGUMENT @@ -754,12 +756,12 @@ func (f *File) evalInfixExpFunc(sheet, cell string, token, nextToken efp.Token, } // calcPow evaluate exponentiation arithmetic operations. -func calcPow(rOpd, lOpd string, opdStack *Stack) error { - lOpdVal, err := strconv.ParseFloat(lOpd, 64) +func calcPow(rOpd, lOpd efp.Token, opdStack *Stack) error { + lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64) if err != nil { return err } - rOpdVal, err := strconv.ParseFloat(rOpd, 64) + rOpdVal, err := strconv.ParseFloat(rOpd.TValue, 64) if err != nil { return err } @@ -769,54 +771,106 @@ func calcPow(rOpd, lOpd string, opdStack *Stack) error { } // calcEq evaluate equal arithmetic operations. -func calcEq(rOpd, lOpd string, opdStack *Stack) error { +func calcEq(rOpd, lOpd efp.Token, opdStack *Stack) error { opdStack.Push(efp.Token{TValue: strings.ToUpper(strconv.FormatBool(rOpd == lOpd)), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) return nil } // calcNEq evaluate not equal arithmetic operations. -func calcNEq(rOpd, lOpd string, opdStack *Stack) error { +func calcNEq(rOpd, lOpd efp.Token, opdStack *Stack) error { opdStack.Push(efp.Token{TValue: strings.ToUpper(strconv.FormatBool(rOpd != lOpd)), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) return nil } // calcL evaluate less than arithmetic operations. -func calcL(rOpd, lOpd string, opdStack *Stack) error { - opdStack.Push(efp.Token{TValue: strings.ToUpper(strconv.FormatBool(strings.Compare(lOpd, rOpd) == -1)), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) +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}) + } + 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.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.TSubType == efp.TokenSubTypeText && lOpd.TSubType == efp.TokenSubTypeNumber { + opdStack.Push(efp.Token{TValue: strings.ToUpper(strconv.FormatBool(true)), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + } return nil } // calcLe evaluate less than or equal arithmetic operations. -func calcLe(rOpd, lOpd string, opdStack *Stack) error { - opdStack.Push(efp.Token{TValue: strings.ToUpper(strconv.FormatBool(strings.Compare(lOpd, rOpd) != 1)), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) +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}) + } + 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.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.TSubType == efp.TokenSubTypeText && lOpd.TSubType == efp.TokenSubTypeNumber { + opdStack.Push(efp.Token{TValue: strings.ToUpper(strconv.FormatBool(true)), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + } return nil } // calcG evaluate greater than or equal arithmetic operations. -func calcG(rOpd, lOpd string, opdStack *Stack) error { - opdStack.Push(efp.Token{TValue: strings.ToUpper(strconv.FormatBool(strings.Compare(lOpd, rOpd) == 1)), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) +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}) + } + 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.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.TSubType == efp.TokenSubTypeText && lOpd.TSubType == efp.TokenSubTypeNumber { + opdStack.Push(efp.Token{TValue: strings.ToUpper(strconv.FormatBool(false)), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + } return nil } // calcGe evaluate greater than or equal arithmetic operations. -func calcGe(rOpd, lOpd string, opdStack *Stack) error { - opdStack.Push(efp.Token{TValue: strings.ToUpper(strconv.FormatBool(strings.Compare(lOpd, rOpd) != -1)), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) +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}) + } + 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.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.TSubType == efp.TokenSubTypeText && lOpd.TSubType == efp.TokenSubTypeNumber { + opdStack.Push(efp.Token{TValue: strings.ToUpper(strconv.FormatBool(false)), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + } return nil } // calcSplice evaluate splice '&' operations. -func calcSplice(rOpd, lOpd string, opdStack *Stack) error { - opdStack.Push(efp.Token{TValue: lOpd + rOpd, TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) +func calcSplice(rOpd, lOpd efp.Token, opdStack *Stack) error { + opdStack.Push(efp.Token{TValue: lOpd.TValue + rOpd.TValue, TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) return nil } // calcAdd evaluate addition arithmetic operations. -func calcAdd(rOpd, lOpd string, opdStack *Stack) error { - lOpdVal, err := strconv.ParseFloat(lOpd, 64) +func calcAdd(rOpd, lOpd efp.Token, opdStack *Stack) error { + lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64) if err != nil { return err } - rOpdVal, err := strconv.ParseFloat(rOpd, 64) + rOpdVal, err := strconv.ParseFloat(rOpd.TValue, 64) if err != nil { return err } @@ -826,12 +880,12 @@ func calcAdd(rOpd, lOpd string, opdStack *Stack) error { } // calcSubtract evaluate subtraction arithmetic operations. -func calcSubtract(rOpd, lOpd string, opdStack *Stack) error { - lOpdVal, err := strconv.ParseFloat(lOpd, 64) +func calcSubtract(rOpd, lOpd efp.Token, opdStack *Stack) error { + lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64) if err != nil { return err } - rOpdVal, err := strconv.ParseFloat(rOpd, 64) + rOpdVal, err := strconv.ParseFloat(rOpd.TValue, 64) if err != nil { return err } @@ -841,12 +895,12 @@ func calcSubtract(rOpd, lOpd string, opdStack *Stack) error { } // calcMultiply evaluate multiplication arithmetic operations. -func calcMultiply(rOpd, lOpd string, opdStack *Stack) error { - lOpdVal, err := strconv.ParseFloat(lOpd, 64) +func calcMultiply(rOpd, lOpd efp.Token, opdStack *Stack) error { + lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64) if err != nil { return err } - rOpdVal, err := strconv.ParseFloat(rOpd, 64) + rOpdVal, err := strconv.ParseFloat(rOpd.TValue, 64) if err != nil { return err } @@ -856,12 +910,12 @@ func calcMultiply(rOpd, lOpd string, opdStack *Stack) error { } // calcDiv evaluate division arithmetic operations. -func calcDiv(rOpd, lOpd string, opdStack *Stack) error { - lOpdVal, err := strconv.ParseFloat(lOpd, 64) +func calcDiv(rOpd, lOpd efp.Token, opdStack *Stack) error { + lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64) if err != nil { return err } - rOpdVal, err := strconv.ParseFloat(rOpd, 64) + rOpdVal, err := strconv.ParseFloat(rOpd.TValue, 64) if err != nil { return err } @@ -887,7 +941,7 @@ func calculate(opdStack *Stack, opt efp.Token) error { result := 0 - opdVal opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) } - tokenCalcFunc := map[string]func(rOpd, lOpd string, opdStack *Stack) error{ + tokenCalcFunc := map[string]func(rOpd, lOpd efp.Token, opdStack *Stack) error{ "^": calcPow, "*": calcMultiply, "/": calcDiv, @@ -906,7 +960,7 @@ func calculate(opdStack *Stack, opt efp.Token) error { } rOpd := opdStack.Pop().(efp.Token) lOpd := opdStack.Pop().(efp.Token) - if err := calcSubtract(rOpd.TValue, lOpd.TValue, opdStack); err != nil { + if err := calcSubtract(rOpd, lOpd, opdStack); err != nil { return err } } @@ -917,7 +971,8 @@ func calculate(opdStack *Stack, opt efp.Token) error { } rOpd := opdStack.Pop().(efp.Token) lOpd := opdStack.Pop().(efp.Token) - if err := fn(rOpd.TValue, lOpd.TValue, opdStack); err != nil { + + if err := fn(rOpd, lOpd, opdStack); err != nil { return err } } @@ -6056,6 +6111,44 @@ func (fn *formulaFuncs) IFERROR(argsList *list.List) formulaArg { return argsList.Back().Value.(formulaArg) } +// IFNA function tests if an initial supplied value (or expression) evaluates +// to the Excel #N/A error. If so, the function returns a second supplied +// value; Otherwise the function returns the first supplied value. The syntax +// of the function is: +// +// IFNA(value,value_if_na) +// +func (fn *formulaFuncs) IFNA(argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, "IFNA requires 2 arguments") + } + arg := argsList.Front().Value.(formulaArg) + if arg.Type == ArgError && arg.Value() == formulaErrorNA { + return argsList.Back().Value.(formulaArg) + } + return arg +} + +// IFS function tests a number of supplied conditions and returns the result +// corresponding to the first condition that evaluates to TRUE. If none of +// the supplied conditions evaluate to TRUE, the function returns the #N/A +// error. +// +// 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") + } + for arg := argsList.Front(); arg != nil; arg = arg.Next() { + if arg.Value.(formulaArg).ToBool().Number == 1 { + return arg.Next().Value.(formulaArg) + } + arg = arg.Next() + } + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) +} + // NOT function returns the opposite to a supplied logical value. The syntax // of the function is: // diff --git a/calc_test.go b/calc_test.go index abf6afaf9b..144811c112 100644 --- a/calc_test.go +++ b/calc_test.go @@ -34,22 +34,34 @@ func TestCalcCellValue(t *testing.T) { {nil, nil, nil, "Feb", "South 2", 45500}, } mathCalc := map[string]string{ - "=2^3": "8", - "=1=1": "TRUE", - "=1=2": "FALSE", - "=1<2": "TRUE", - "=3<2": "FALSE", - "=2<=3": "TRUE", - "=2<=1": "FALSE", - "=2>1": "TRUE", - "=2>3": "FALSE", - "=2>=1": "TRUE", - "=2>=3": "FALSE", - "=1&2": "12", - "=15%": "0.15", - "=1+20%": "1.2", - `="A"="A"`: "TRUE", - `="A"<>"A"`: "FALSE", + "=2^3": "8", + "=1=1": "TRUE", + "=1=2": "FALSE", + "=1<2": "TRUE", + "=3<2": "FALSE", + "=1<\"-1\"": "TRUE", + "=\"-1\"<1": "FALSE", + "=\"-1\"<\"-2\"": "TRUE", + "=2<=3": "TRUE", + "=2<=1": "FALSE", + "=1<=\"-1\"": "TRUE", + "=\"-1\"<=1": "FALSE", + "=\"-1\"<=\"-2\"": "TRUE", + "=2>1": "TRUE", + "=2>3": "FALSE", + "=1>\"-1\"": "FALSE", + "=\"-1\">-1": "TRUE", + "=\"-1\">\"-2\"": "FALSE", + "=2>=1": "TRUE", + "=2>=3": "FALSE", + "=1>=\"-1\"": "FALSE", + "=\"-1\">=-1": "TRUE", + "=\"-1\">=\"-2\"": "FALSE", + "=1&2": "12", + "=15%": "0.15", + "=1+20%": "1.2", + `="A"="A"`: "TRUE", + `="A"<>"A"`: "FALSE", // Engineering Functions // BESSELI "=BESSELI(4.5,1)": "15.389222753735925", @@ -922,6 +934,13 @@ func TestCalcCellValue(t *testing.T) { "=IFERROR(1/2,0)": "0.5", "=IFERROR(ISERROR(),0)": "0", "=IFERROR(1/0,0)": "0", + // IFNA + "=IFNA(1,\"not found\")": "1", + "=IFNA(NA(),\"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", + "=IFS(0>1,5/0,0<-1,-5/0,TRUE,0)": "0", // NOT "=NOT(FALSE())": "TRUE", "=NOT(\"false\")": "TRUE", @@ -1313,7 +1332,17 @@ func TestCalcCellValue(t *testing.T) { assert.Equal(t, expected, result, formula) } mathCalcError := map[string]string{ - "=1/0": "#DIV/0!", + "=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", @@ -2023,6 +2052,10 @@ func TestCalcCellValue(t *testing.T) { "=FALSE(A1)": "FALSE takes no arguments", // IFERROR "=IFERROR()": "IFERROR requires 2 arguments", + // IFNA + "=IFNA()": "IFNA requires 2 arguments", + // IFS + "=IFS()": "IFS requires at least 2 arguments", // NOT "=NOT()": "NOT requires 1 argument", "=NOT(NOT())": "NOT requires 1 argument", @@ -2598,40 +2631,6 @@ func TestCalcWithDefinedName(t *testing.T) { } -func TestCalcArithmeticOperations(t *testing.T) { - opdStack := NewStack() - for _, test := range [][]string{{"1", "text", "FALSE"}, {"text", "1", "TRUE"}} { - assert.NoError(t, calcL(test[0], test[1], opdStack)) - assert.Equal(t, test[2], opdStack.Peek().(efp.Token).TValue) - opdStack.Empty() - assert.NoError(t, calcLe(test[0], test[1], opdStack)) - assert.Equal(t, test[2], opdStack.Peek().(efp.Token).TValue) - opdStack.Empty() - } - for _, test := range [][]string{{"1", "text", "TRUE"}, {"text", "1", "FALSE"}} { - assert.NoError(t, calcG(test[0], test[1], opdStack)) - assert.Equal(t, test[2], opdStack.Peek().(efp.Token).TValue) - opdStack.Empty() - assert.NoError(t, calcGe(test[0], test[1], opdStack)) - assert.Equal(t, test[2], opdStack.Peek().(efp.Token).TValue) - opdStack.Empty() - } - - err := `strconv.ParseFloat: parsing "text": invalid syntax` - assert.EqualError(t, calcPow("1", "text", nil), err) - assert.EqualError(t, calcPow("text", "1", nil), err) - assert.EqualError(t, calcAdd("1", "text", nil), err) - assert.EqualError(t, calcAdd("text", "1", nil), err) - assert.EqualError(t, calcAdd("1", "text", nil), err) - assert.EqualError(t, calcAdd("text", "1", nil), err) - assert.EqualError(t, calcSubtract("1", "text", nil), err) - assert.EqualError(t, calcSubtract("text", "1", nil), err) - assert.EqualError(t, calcMultiply("1", "text", nil), err) - assert.EqualError(t, calcMultiply("text", "1", nil), err) - assert.EqualError(t, calcDiv("1", "text", nil), err) - assert.EqualError(t, calcDiv("text", "1", nil), err) -} - func TestCalcISBLANK(t *testing.T) { argsList := list.New() argsList.PushBack(formulaArg{ From 1df7f32cc641c747433b3f58e0de0deb58785bf3 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 20 Oct 2021 00:07:22 +0800 Subject: [PATCH 462/957] ref #65, new formula functions: RRI, SLN and SYD --- calc.go | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 23 +++++++++++++++ 2 files changed, 104 insertions(+) diff --git a/calc.go b/calc.go index 60ca0cb2e9..82817f3bff 100644 --- a/calc.go +++ b/calc.go @@ -474,6 +474,7 @@ type formulaFuncs struct { // ROUNDUP // ROW // ROWS +// RRI // SEC // SECH // SHEET @@ -481,6 +482,7 @@ type formulaFuncs struct { // SIN // SINH // SKEW +// SLN // SMALL // SQRT // SQRTPI @@ -491,6 +493,7 @@ type formulaFuncs struct { // SUM // SUMIF // SUMSQ +// SYD // T // TAN // TANH @@ -9317,3 +9320,81 @@ func (fn *formulaFuncs) PMT(argsList *list.List) formulaArg { func (fn *formulaFuncs) PPMT(argsList *list.List) formulaArg { return fn.ipmt("PPMT", argsList) } + +// RRI function calculates the equivalent interest rate for an investment with +// specified present value, future value and duration. The syntax of the +// function is: +// +// RRI(nper,pv,fv) +// +func (fn *formulaFuncs) RRI(argsList *list.List) formulaArg { + if argsList.Len() != 3 { + return newErrorFormulaArg(formulaErrorVALUE, "RRI requires 3 arguments") + } + nper := argsList.Front().Value.(formulaArg).ToNumber() + pv := argsList.Front().Next().Value.(formulaArg).ToNumber() + fv := argsList.Back().Value.(formulaArg).ToNumber() + if nper.Type != ArgNumber || pv.Type != ArgNumber || fv.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + if nper.Number <= 0 { + return newErrorFormulaArg(formulaErrorNUM, "RRI requires nper argument to be > 0") + } + if pv.Number <= 0 { + return newErrorFormulaArg(formulaErrorNUM, "RRI requires pv argument to be > 0") + } + if fv.Number < 0 { + return newErrorFormulaArg(formulaErrorNUM, "RRI requires fv argument to be >= 0") + } + return newNumberFormulaArg(math.Pow(fv.Number/pv.Number, 1/nper.Number) - 1) +} + +// SLN function calculates the straight line depreciation of an asset for one +// period. The syntax of the function is: +// +// SLN(cost,salvage,life) +// +func (fn *formulaFuncs) SLN(argsList *list.List) formulaArg { + if argsList.Len() != 3 { + return newErrorFormulaArg(formulaErrorVALUE, "SLN requires 3 arguments") + } + cost := argsList.Front().Value.(formulaArg).ToNumber() + salvage := argsList.Front().Next().Value.(formulaArg).ToNumber() + life := argsList.Back().Value.(formulaArg).ToNumber() + if cost.Type != ArgNumber || salvage.Type != ArgNumber || life.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + if life.Number == 0 { + return newErrorFormulaArg(formulaErrorNUM, "SLN requires life argument to be > 0") + } + return newNumberFormulaArg((cost.Number - salvage.Number) / life.Number) +} + +// SYD function calculates the sum-of-years' digits depreciation for a +// specified period in the lifetime of an asset. The syntax of the function +// is: +// +// SYD(cost,salvage,life,per) +// +func (fn *formulaFuncs) SYD(argsList *list.List) formulaArg { + if argsList.Len() != 4 { + return newErrorFormulaArg(formulaErrorVALUE, "SYD requires 4 arguments") + } + cost := argsList.Front().Value.(formulaArg).ToNumber() + salvage := argsList.Front().Next().Value.(formulaArg).ToNumber() + life := argsList.Back().Prev().Value.(formulaArg).ToNumber() + per := argsList.Back().Value.(formulaArg).ToNumber() + if cost.Type != ArgNumber || salvage.Type != ArgNumber || life.Type != ArgNumber || per.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + if life.Number <= 0 { + return newErrorFormulaArg(formulaErrorNUM, "SYD requires life argument to be > 0") + } + if per.Number <= 0 { + return newErrorFormulaArg(formulaErrorNUM, "SYD requires per argument to be > 0") + } + if per.Number > life.Number { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + return newNumberFormulaArg(((cost.Number - salvage.Number) * (life.Number - per.Number + 1) * 2) / (life.Number * (life.Number + 1))) +} diff --git a/calc_test.go b/calc_test.go index 144811c112..fb5876cbce 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1323,6 +1323,13 @@ func TestCalcCellValue(t *testing.T) { // PPMT "=PPMT(0.05/12,2,60,50000)": "-738.2918003208238", "=PPMT(0.035/4,2,8,0,5000,1)": "-606.1094824182949", + // RRI + "=RRI(10,10000,15000)": "0.0413797439924106", + // SLN + "=SLN(10000,1000,5)": "1800", + // SYD + "=SYD(10000,1000,5,1)": "3000", + "=SYD(10000,1000,5,2)": "2400", } for formula, expected := range mathCalc { f := prepareCalcData(cellData) @@ -2516,6 +2523,22 @@ func TestCalcCellValue(t *testing.T) { "=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", + // 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", + // SLN + "=SLN()": "SLN requires 3 arguments", + "=SLN(\"\",\"\",\"\")": "#NUM!", + "=SLN(10000,1000,0)": "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!", } for formula, expected := range mathCalcError { f := prepareCalcData(cellData) From 49e80b9e47a76252d27b1e2863541fc3b49ed488 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 21 Oct 2021 00:33:25 +0800 Subject: [PATCH 463/957] ref #65: new formula functions DISC and INTRATE --- calc.go | 104 +++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 26 +++++++++++++ 2 files changed, 130 insertions(+) diff --git a/calc.go b/calc.go index 82817f3bff..8b53352024 100644 --- a/calc.go +++ b/calc.go @@ -327,6 +327,7 @@ type formulaFuncs struct { // DEC2OCT // DECIMAL // DEGREES +// DISC // DOLLARDE // DOLLARFR // EFFECT @@ -385,6 +386,7 @@ type formulaFuncs struct { // IMSUM // IMTAN // INT +// INTRATE // IPMT // IRR // ISBLANK @@ -8773,6 +8775,57 @@ func (fn *formulaFuncs) DDB(argsList *list.List) formulaArg { return newNumberFormulaArg(depreciation) } +// 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 { + if argsList.Len() != 4 && argsList.Len() != 5 { + return newErrorFormulaArg(formulaErrorVALUE, "DISC requires 4 or 5 arguments") + } + args := list.New().Init() + args.PushBack(argsList.Front().Value.(formulaArg)) + settlement := fn.DATEVALUE(args) + if settlement.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + args.Init() + args.PushBack(argsList.Front().Next().Value.(formulaArg)) + maturity := fn.DATEVALUE(args) + if maturity.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + if maturity.Number <= settlement.Number { + return newErrorFormulaArg(formulaErrorNUM, "DISC requires maturity > settlement") + } + pr := argsList.Front().Next().Next().Value.(formulaArg).ToNumber() + if pr.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + if pr.Number <= 0 { + return newErrorFormulaArg(formulaErrorNUM, "DISC requires pr > 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, "DISC 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 - pr.Number) / redemption.Number / frac.Number) +} + // DOLLARDE function converts a dollar value in fractional notation, into a // dollar value expressed as a decimal. The syntax of the function is: // @@ -8918,6 +8971,57 @@ func (fn *formulaFuncs) FVSCHEDULE(argsList *list.List) formulaArg { return newNumberFormulaArg(principal) } +// INTRATE function calculates the interest rate for a fully invested +// security. The syntax of the function is: +// +// 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 := list.New().Init() + args.PushBack(argsList.Front().Value.(formulaArg)) + settlement := fn.DATEVALUE(args) + if settlement.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + args.Init() + args.PushBack(argsList.Front().Next().Value.(formulaArg)) + maturity := fn.DATEVALUE(args) + if maturity.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + 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) +} + // IPMT function calculates the interest payment, during a specific period of a // loan or investment that is paid in constant periodic payments, with a // constant interest rate. The syntax of the function is: diff --git a/calc_test.go b/calc_test.go index fb5876cbce..7262fa944d 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1286,6 +1286,8 @@ func TestCalcCellValue(t *testing.T) { "=DDB(10000,1000,5,3)": "1440", "=DDB(10000,1000,5,4)": "864", "=DDB(10000,1000,5,5)": "296", + // DISC + "=DISC(\"04/01/2016\",\"03/31/2021\",95,100)": "0.01", // DOLLARDE "=DOLLARDE(1.01,16)": "1.0625", // DOLLARFR @@ -1300,6 +1302,8 @@ func TestCalcCellValue(t *testing.T) { // FVSCHEDULE "=FVSCHEDULE(10000,A1:A5)": "240000", "=FVSCHEDULE(10000,0.5)": "15000", + // 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", @@ -2428,6 +2432,17 @@ func TestCalcCellValue(t *testing.T) { "=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", + // 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", // DOLLARDE "=DOLLARDE()": "DOLLARDE requires 2 arguments", "=DOLLARDE(\"\",0)": "strconv.ParseFloat: parsing \"\": invalid syntax", @@ -2459,6 +2474,17 @@ func TestCalcCellValue(t *testing.T) { "=FVSCHEDULE()": "FVSCHEDULE requires 2 arguments", "=FVSCHEDULE(\"\",0)": "strconv.ParseFloat: parsing \"\": invalid syntax", "=FVSCHEDULE(0,\"x\")": "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", // IPMT "=IPMT()": "IPMT requires at least 4 arguments", "=IPMT(0,0,0,0,0,0,0)": "IPMT allows at most 6 arguments", From f126f635629d1029660f1960184d226f35d7947f Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 22 Oct 2021 00:46:11 +0800 Subject: [PATCH 464/957] ref #65: new formula functions ADDRESS and PRICEDISC --- calc.go | 142 +++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 36 +++++++++++++ 2 files changed, 178 insertions(+) diff --git a/calc.go b/calc.go index 8b53352024..ad06517285 100644 --- a/calc.go +++ b/calc.go @@ -130,6 +130,40 @@ var ( regexp.MustCompile(`^` + df3 + `$`), regexp.MustCompile(`^` + df4 + `$`), } + addressFmtMaps = map[string]func(col, row int) (string, error){ + "1_TRUE": func(col, row int) (string, error) { + return CoordinatesToCellName(col, row, true) + }, + "1_FALSE": func(col, row int) (string, error) { + return fmt.Sprintf("R%dC%d", row, col), nil + }, + "2_TRUE": func(col, row int) (string, error) { + column, err := ColumnNumberToName(col) + if err != nil { + return "", err + } + return fmt.Sprintf("%s$%d", column, row), nil + }, + "2_FALSE": func(col, row int) (string, error) { + return fmt.Sprintf("R%dC[%d]", row, col), nil + }, + "3_TRUE": func(col, row int) (string, error) { + column, err := ColumnNumberToName(col) + if err != nil { + return "", err + } + return fmt.Sprintf("$%s%d", column, row), nil + }, + "3_FALSE": func(col, row int) (string, error) { + return fmt.Sprintf("R[%d]C%d", row, col), nil + }, + "4_TRUE": func(col, row int) (string, error) { + return CoordinatesToCellName(col, row, false) + }, + "4_FALSE": func(col, row int) (string, error) { + return fmt.Sprintf("R[%d]C[%d]", row, col), nil + }, + } ) // cellRef defines the structure of a cell reference. @@ -266,6 +300,7 @@ type formulaFuncs struct { // ACOSH // ACOT // ACOTH +// ADDRESS // AMORDEGRC // AMORLINC // AND @@ -457,6 +492,7 @@ type formulaFuncs struct { // POISSON // POWER // PPMT +// PRICEDISC // PRODUCT // PROPER // QUARTILE @@ -7644,6 +7680,61 @@ func (fn *formulaFuncs) IF(argsList *list.List) formulaArg { // Lookup and Reference Functions +// 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]) +// +func (fn *formulaFuncs) ADDRESS(argsList *list.List) formulaArg { + if argsList.Len() < 2 { + return newErrorFormulaArg(formulaErrorVALUE, "ADDRESS requires at least 2 arguments") + } + if argsList.Len() > 5 { + return newErrorFormulaArg(formulaErrorVALUE, "ADDRESS requires at most 5 arguments") + } + rowNum := argsList.Front().Value.(formulaArg).ToNumber() + if rowNum.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + if rowNum.Number >= TotalRows { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + colNum := argsList.Front().Next().Value.(formulaArg).ToNumber() + if colNum.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + absNum := newNumberFormulaArg(1) + if argsList.Len() >= 3 { + absNum = argsList.Front().Next().Next().Value.(formulaArg).ToNumber() + if absNum.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + } + if absNum.Number < 1 || absNum.Number > 4 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + a1 := newBoolFormulaArg(true) + if argsList.Len() >= 4 { + a1 = argsList.Front().Next().Next().Next().Value.(formulaArg).ToBool() + if a1.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + } + var sheetText string + if argsList.Len() == 5 { + sheetText = trimSheetName(argsList.Back().Value.(formulaArg).Value()) + } + if len(sheetText) > 0 { + sheetText = fmt.Sprintf("%s!", sheetText) + } + formatter := addressFmtMaps[fmt.Sprintf("%d_%s", int(absNum.Number), a1.Value())] + addr, err := formatter(int(colNum.Number), int(colNum.Number)) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + return newStringFormulaArg(fmt.Sprintf("%s%s", sheetText, addr)) +} + // CHOOSE function returns a value from an array, that corresponds to a // supplied index number (position). The syntax of the function is: // @@ -9425,6 +9516,57 @@ func (fn *formulaFuncs) PPMT(argsList *list.List) formulaArg { return fn.ipmt("PPMT", argsList) } +// 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]) +// +func (fn *formulaFuncs) PRICEDISC(argsList *list.List) formulaArg { + if argsList.Len() != 4 && argsList.Len() != 5 { + return newErrorFormulaArg(formulaErrorVALUE, "PRICEDISC requires 4 or 5 arguments") + } + args := list.New().Init() + args.PushBack(argsList.Front().Value.(formulaArg)) + settlement := fn.DATEVALUE(args) + if settlement.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + args.Init() + args.PushBack(argsList.Front().Next().Value.(formulaArg)) + maturity := fn.DATEVALUE(args) + if maturity.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + if maturity.Number <= settlement.Number { + return newErrorFormulaArg(formulaErrorNUM, "PRICEDISC requires maturity > settlement") + } + discount := argsList.Front().Next().Next().Value.(formulaArg).ToNumber() + if discount.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + if discount.Number <= 0 { + return newErrorFormulaArg(formulaErrorNUM, "PRICEDISC requires discount > 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, "PRICEDISC 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 * (1 - discount.Number*frac.Number)) +} + // RRI function calculates the equivalent interest rate for an investment with // specified present value, future value and duration. The syntax of the // function is: diff --git a/calc_test.go b/calc_test.go index 7262fa944d..38b5f5f76e 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1188,6 +1188,17 @@ func TestCalcCellValue(t *testing.T) { `=IF(FALSE,0,ROUND(4/2,0))`: "2", `=IF(TRUE,ROUND(4/2,0),0)`: "2", // Excel Lookup and Reference Functions + // ADDRESS + "=ADDRESS(1,1,1,TRUE)": "$A$1", + "=ADDRESS(1,1,1,FALSE)": "R1C1", + "=ADDRESS(1,1,2,TRUE)": "A$1", + "=ADDRESS(1,1,2,FALSE)": "R1C[1]", + "=ADDRESS(1,1,3,TRUE)": "$A1", + "=ADDRESS(1,1,3,FALSE)": "R[1]C1", + "=ADDRESS(1,1,4,TRUE)": "A1", + "=ADDRESS(1,1,4,FALSE)": "R[1]C[1]", + "=ADDRESS(1,1,4,TRUE,\"\")": "A1", + "=ADDRESS(1,1,4,TRUE,\"Sheet1\")": "Sheet1!A1", // CHOOSE "=CHOOSE(4,\"red\",\"blue\",\"green\",\"brown\")": "brown", "=CHOOSE(1,\"red\",\"blue\",\"green\",\"brown\")": "red", @@ -1327,6 +1338,9 @@ func TestCalcCellValue(t *testing.T) { // PPMT "=PPMT(0.05/12,2,60,50000)": "-738.2918003208238", "=PPMT(0.035/4,2,8,0,5000,1)": "-606.1094824182949", + // PRICEDISC + "=PRICEDISC(\"04/01/2017\",\"03/31/2021\",2.5%,100)": "90", + "=PRICEDISC(\"04/01/2017\",\"03/31/2021\",2.5%,100,3)": "90", // RRI "=RRI(10,10000,15000)": "0.0413797439924106", // SLN @@ -2279,6 +2293,17 @@ func TestCalcCellValue(t *testing.T) { "=IF(0,1,2,3)": "IF accepts at most 3 arguments", "=IF(D1,1,2)": "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!", // CHOOSE "=CHOOSE()": "CHOOSE requires 2 arguments", "=CHOOSE(\"index_num\",0)": "CHOOSE requires first argument of type number", @@ -2549,6 +2574,17 @@ func TestCalcCellValue(t *testing.T) { "=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", + // 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", // RRI "=RRI()": "RRI requires 3 arguments", "=RRI(\"\",\"\",\"\")": "#NUM!", From 71684d966aaddf1cfa178f1c2a1677b0a1106766 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 23 Oct 2021 10:18:06 +0800 Subject: [PATCH 465/957] ref #65: new formula functions AVEDEV and CHIDIST --- calc.go | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 14 ++++++++++++ 2 files changed, 75 insertions(+) diff --git a/calc.go b/calc.go index ad06517285..385503f5bc 100644 --- a/calc.go +++ b/calc.go @@ -310,6 +310,7 @@ type formulaFuncs struct { // ATAN // ATAN2 // ATANH +// AVEDEV // AVERAGE // AVERAGEA // BASE @@ -329,6 +330,7 @@ type formulaFuncs struct { // CEILING.MATH // CEILING.PRECISE // CHAR +// CHIDIST // CHOOSE // CLEAN // CODE @@ -4675,6 +4677,31 @@ func (fn *formulaFuncs) TRUNC(argsList *list.List) formulaArg { // Statistical Functions +// AVEDEV function calculates the average deviation of a supplied set of +// values. The syntax of the function is: +// +// AVEDEV(number1,[number2],...) +// +func (fn *formulaFuncs) AVEDEV(argsList *list.List) formulaArg { + if argsList.Len() == 0 { + return newErrorFormulaArg(formulaErrorVALUE, "AVEDEV requires at least 1 argument") + } + average := fn.AVERAGE(argsList) + if average.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + result, count := 0.0, 0.0 + for arg := argsList.Front(); arg != nil; arg = arg.Next() { + num := arg.Value.(formulaArg).ToNumber() + if num.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + result += math.Abs(num.Number - average.Number) + count++ + } + return newNumberFormulaArg(result / count) +} + // AVERAGE function returns the arithmetic mean of a list of supplied numbers. // The syntax of the function is: // @@ -4709,6 +4736,40 @@ func (fn *formulaFuncs) AVERAGEA(argsList *list.List) formulaArg { return newNumberFormulaArg(sum / count) } +// incompleteGamma is an implementation of the incomplete gamma function. +func incompleteGamma(a, x float64) float64 { + max := 32 + summer := 0.0 + for n := 0; n <= max; n++ { + divisor := a + for i := 1; i <= n; i++ { + divisor *= (a + float64(i)) + } + summer += math.Pow(x, float64(n)) / divisor + } + return math.Pow(x, a) * math.Exp(0-x) * summer +} + +// CHIDIST function calculates the right-tailed probability of the chi-square +// distribution. The syntax of the function is: +// +// CHIDIST(x,degrees_freedom) +// +func (fn *formulaFuncs) CHIDIST(argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, "CHIDIST requires 2 numeric arguments") + } + x := argsList.Front().Value.(formulaArg).ToNumber() + if x.Type != ArgNumber { + return x + } + degress := argsList.Back().Value.(formulaArg).ToNumber() + if degress.Type != ArgNumber { + return degress + } + return newNumberFormulaArg(1 - (incompleteGamma(degress.Number/2, x.Number/2) / math.Gamma(degress.Number/2))) +} + // calcStringCountSum is part of the implementation countSum. func calcStringCountSum(countText bool, count, sum float64, num, arg formulaArg) (float64, float64) { if countText && num.Type == ArgError && arg.String != "" { diff --git a/calc_test.go b/calc_test.go index 38b5f5f76e..24126157eb 100644 --- a/calc_test.go +++ b/calc_test.go @@ -735,6 +735,9 @@ func TestCalcCellValue(t *testing.T) { "=TRUNC(-99.999,-1)": "-90", "=TRUNC(TRUNC(1),-1)": "0", // Statistical Functions + // AVEDEV + "=AVEDEV(1,2)": "0.5", + "=AVERAGE(A1:A4,B1:B4)": "2.5", // AVERAGE "=AVERAGE(INT(1))": "1", "=AVERAGE(A1)": "1", @@ -745,6 +748,9 @@ func TestCalcCellValue(t *testing.T) { "=AVERAGEA(A1)": "1", "=AVERAGEA(A1:A2)": "1.5", "=AVERAGEA(D2:F9)": "12671.375", + // CHIDIST + "=CHIDIST(0.5,3)": "0.918891411654676", + "=CHIDIST(8,3)": "0.0460117056892315", // COUNT "=COUNT()": "0", "=COUNT(E1:F2,\"text\",1,INT(2))": "3", @@ -1891,10 +1897,18 @@ func TestCalcCellValue(t *testing.T) { `=TRUNC("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", `=TRUNC(1,"X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", // Statistical Functions + // AVEDEV + "=AVEDEV()": "AVEDEV requires at least 1 argument", + "=AVEDEV(\"\")": "#VALUE!", + "=AVEDEV(1,\"\")": "#VALUE!", // AVERAGE "=AVERAGE(H1)": "AVERAGE divide by zero", // AVERAGE "=AVERAGEA(H1)": "AVERAGEA divide by zero", + // CHIDIST + "=CHIDIST()": "CHIDIST requires 2 numeric arguments", + "=CHIDIST(\"\",3)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=CHIDIST(0.5,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", // COUNTBLANK "=COUNTBLANK()": "COUNTBLANK requires 1 argument", "=COUNTBLANK(1,2)": "COUNTBLANK requires 1 argument", From 154effdf82df2f662b91361006c46a69c3cd3635 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 24 Oct 2021 12:20:24 +0800 Subject: [PATCH 466/957] ref #65: new formula functions YIELDDISC and YIELDMAT --- calc.go | 113 +++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 28 +++++++++++++ 2 files changed, 141 insertions(+) diff --git a/calc.go b/calc.go index 385503f5bc..51a830e14d 100644 --- a/calc.go +++ b/calc.go @@ -554,6 +554,8 @@ type formulaFuncs struct { // XOR // YEAR // YEARFRAC +// YIELDDISC +// YIELDMAT // Z.TEST // ZTEST // @@ -9705,3 +9707,114 @@ func (fn *formulaFuncs) SYD(argsList *list.List) formulaArg { } return newNumberFormulaArg(((cost.Number - salvage.Number) * (life.Number - per.Number + 1) * 2) / (life.Number * (life.Number + 1))) } + +// YIELDDISC function calculates the annual yield of a discounted security. +// The syntax of the function is: +// +// 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") + } + args := list.New().Init() + args.PushBack(argsList.Front().Value.(formulaArg)) + settlement := fn.DATEVALUE(args) + if settlement.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + args.Init() + args.PushBack(argsList.Front().Next().Value.(formulaArg)) + maturity := fn.DATEVALUE(args) + if maturity.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + pr := argsList.Front().Next().Next().Value.(formulaArg).ToNumber() + if pr.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + if pr.Number <= 0 { + return newErrorFormulaArg(formulaErrorNUM, "YIELDDISC requires pr > 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, "YIELDDISC 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/pr.Number - 1) / frac.Number) +} + +// 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]) +// +func (fn *formulaFuncs) YIELDMAT(argsList *list.List) formulaArg { + if argsList.Len() != 5 && argsList.Len() != 6 { + return newErrorFormulaArg(formulaErrorVALUE, "YIELDMAT requires 5 or 6 arguments") + } + args := list.New().Init() + args.PushBack(argsList.Front().Value.(formulaArg)) + settlement := fn.DATEVALUE(args) + if settlement.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + args.Init() + args.PushBack(argsList.Front().Next().Value.(formulaArg)) + maturity := fn.DATEVALUE(args) + if maturity.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + args.Init() + args.PushBack(argsList.Front().Next().Next().Value.(formulaArg)) + issue := fn.DATEVALUE(args) + if issue.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + if issue.Number >= settlement.Number { + return newErrorFormulaArg(formulaErrorNUM, "YIELDMAT requires settlement > issue") + } + rate := argsList.Front().Next().Next().Next().Value.(formulaArg).ToNumber() + if rate.Type != ArgNumber { + return rate + } + if rate.Number < 0 { + return newErrorFormulaArg(formulaErrorNUM, "YIELDMAT requires rate >= 0") + } + pr := argsList.Front().Next().Next().Next().Next().Value.(formulaArg).ToNumber() + if pr.Type != ArgNumber { + return pr + } + if pr.Number <= 0 { + return newErrorFormulaArg(formulaErrorNUM, "YIELDMAT requires pr > 0") + } + basis := newNumberFormulaArg(0) + if argsList.Len() == 6 { + if basis = argsList.Back().Value.(formulaArg).ToNumber(); basis.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + } + dim := yearFrac(issue.Number, maturity.Number, int(basis.Number)) + if dim.Type != ArgNumber { + return dim + } + dis := yearFrac(issue.Number, settlement.Number, int(basis.Number)) + dsm := yearFrac(settlement.Number, maturity.Number, int(basis.Number)) + result := 1 + dim.Number*rate.Number + result /= pr.Number/100 + dis.Number*rate.Number + result-- + result /= dsm.Number + return newNumberFormulaArg(result) +} diff --git a/calc_test.go b/calc_test.go index 24126157eb..1df622ed0c 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1354,6 +1354,12 @@ func TestCalcCellValue(t *testing.T) { // SYD "=SYD(10000,1000,5,1)": "3000", "=SYD(10000,1000,5,2)": "2400", + // YIELDDISC + "=YIELDDISC(\"01/01/2017\",\"06/30/2017\",97,100)": "0.0622012325059031", + "=YIELDDISC(\"01/01/2017\",\"06/30/2017\",97,100,0)": "0.0622012325059031", + // YIELDMAT + "=YIELDMAT(\"01/01/2017\",\"06/30/2018\",\"06/01/2014\",5.5%,101)": "0.0419422478838651", + "=YIELDMAT(\"01/01/2017\",\"06/30/2018\",\"06/01/2014\",5.5%,101,0)": "0.0419422478838651", } for formula, expected := range mathCalc { f := prepareCalcData(cellData) @@ -2615,6 +2621,28 @@ func TestCalcCellValue(t *testing.T) { "=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!", + // 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", + // 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", } for formula, expected := range mathCalcError { f := prepareCalcData(cellData) From 08087e12333542dddfc7be2c87c3333171ee0ffa Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 26 Oct 2021 00:01:05 +0800 Subject: [PATCH 467/957] ref #65: new formula functions TBILLEQ and TEXTJOIN --- calc.go | 104 +++++++++++++++++++++++++++++++++++++++++++++++---- calc_test.go | 23 ++++++++++++ 2 files changed, 119 insertions(+), 8 deletions(-) diff --git a/calc.go b/calc.go index 51a830e14d..a21be0b5a4 100644 --- a/calc.go +++ b/calc.go @@ -537,6 +537,8 @@ type formulaFuncs struct { // T // TAN // TANH +// TBILLEQ +// TEXTJOIN // TIME // TODAY // TRANSPOSE @@ -7636,6 +7638,64 @@ func (fn *formulaFuncs) SUBSTITUTE(argsList *list.List) formulaArg { return newStringFormulaArg(pre + newText.Value() + post) } +// TEXTJOIN function joins together a series of supplied text strings into one +// 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],...) +// +func (fn *formulaFuncs) TEXTJOIN(argsList *list.List) formulaArg { + if argsList.Len() < 3 { + return newErrorFormulaArg(formulaErrorVALUE, "TEXTJOIN requires at least 3 arguments") + } + if argsList.Len() > 252 { + 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 + } + args, ok := textJoin(argsList.Front().Next().Next(), []string{}, ignoreEmpty.Number != 0) + if ok.Type != ArgNumber { + return ok + } + result := strings.Join(args, delimiter.Value()) + if len(result) > TotalCellChars { + return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("TEXTJOIN function exceeds %d characters", TotalCellChars)) + } + return newStringFormulaArg(result) +} + +// textJoin is an implementation of the formula function TEXTJOIN. +func textJoin(arg *list.Element, arr []string, ignoreEmpty bool) ([]string, formulaArg) { + for arg.Next(); arg != nil; arg = arg.Next() { + switch arg.Value.(formulaArg).Type { + case ArgError: + return arr, arg.Value.(formulaArg) + case ArgString: + val := arg.Value.(formulaArg).Value() + if val != "" || !ignoreEmpty { + arr = append(arr, val) + } + case ArgNumber: + arr = append(arr, arg.Value.(formulaArg).Value()) + case ArgMatrix: + for _, row := range arg.Value.(formulaArg).Matrix { + argList := list.New().Init() + for _, ele := range row { + argList.PushBack(ele) + } + if argList.Len() > 0 { + args, _ := textJoin(argList.Front(), []string{}, ignoreEmpty) + arr = append(arr, args...) + } + } + } + } + return arr, newBoolFormulaArg(true) +} + // TRIM removes extra spaces (i.e. all spaces except for single spaces between // words or characters) from a supplied text string. The syntax of the // function is: @@ -7818,14 +7878,7 @@ func (fn *formulaFuncs) CHOOSE(argsList *list.List) formulaArg { for i := 0; i < idx; i++ { arg = arg.Next() } - var result formulaArg - switch arg.Value.(formulaArg).Type { - case ArgString: - result = newStringFormulaArg(arg.Value.(formulaArg).String) - case ArgMatrix: - result = newMatrixFormulaArg(arg.Value.(formulaArg).Matrix) - } - return result + return arg.Value.(formulaArg) } // deepMatchRune finds whether the text deep matches/satisfies the pattern @@ -9708,6 +9761,41 @@ func (fn *formulaFuncs) SYD(argsList *list.List) formulaArg { return newNumberFormulaArg(((cost.Number - salvage.Number) * (life.Number - per.Number + 1) * 2) / (life.Number * (life.Number + 1))) } +// TBILLEQ function calculates the bond-equivalent yield for a Treasury Bill. +// The syntax of the function is: +// +// TBILLEQ(settlement,maturity,discount) +// +func (fn *formulaFuncs) TBILLEQ(argsList *list.List) formulaArg { + if argsList.Len() != 3 { + return newErrorFormulaArg(formulaErrorVALUE, "TBILLEQ requires 3 arguments") + } + args := list.New().Init() + args.PushBack(argsList.Front().Value.(formulaArg)) + settlement := fn.DATEVALUE(args) + if settlement.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + args.Init() + args.PushBack(argsList.Front().Next().Value.(formulaArg)) + maturity := fn.DATEVALUE(args) + if maturity.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + dsm := maturity.Number - settlement.Number + if dsm > 365 || maturity.Number <= settlement.Number { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + discount := argsList.Back().Value.(formulaArg).ToNumber() + if discount.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + if discount.Number <= 0 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + return newNumberFormulaArg((365 * discount.Number) / (360 - discount.Number*dsm)) +} + // YIELDDISC function calculates the annual yield of a discounted security. // The syntax of the function is: // diff --git a/calc_test.go b/calc_test.go index 1df622ed0c..95b479c62b 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1163,6 +1163,12 @@ func TestCalcCellValue(t *testing.T) { "=SUBSTITUTE(\"abab\",\"x\",\"X\",2)": "abab", "=SUBSTITUTE(\"John is 5 years old\",\"John\",\"Jack\")": "Jack is 5 years old", "=SUBSTITUTE(\"John is 5 years old\",\"5\",\"6\")": "John is 6 years old", + // TEXTJOIN + "=TEXTJOIN(\"-\",TRUE,1,2,3,4)": "1-2-3-4", + "=TEXTJOIN(A4,TRUE,A1:B2)": "1040205", + "=TEXTJOIN(\",\",FALSE,A1:C2)": "1,4,,2,5,", + "=TEXTJOIN(\",\",TRUE,A1:C2)": "1,4,2,5", + "=TEXTJOIN(\",\",TRUE,MUNIT(2))": "1,0,0,1", // TRIM "=TRIM(\" trim text \")": "trim text", "=TRIM(0)": "0", @@ -1354,6 +1360,8 @@ func TestCalcCellValue(t *testing.T) { // SYD "=SYD(10000,1000,5,1)": "3000", "=SYD(10000,1000,5,2)": "2400", + // TBILLEQ + "=TBILLEQ(\"01/01/2017\",\"06/30/2017\",2.5%)": "0.0256680731364276", // YIELDDISC "=YIELDDISC(\"01/01/2017\",\"06/30/2017\",97,100)": "0.0622012325059031", "=YIELDDISC(\"01/01/2017\",\"06/30/2017\",97,100,0)": "0.0622012325059031", @@ -2293,6 +2301,12 @@ func TestCalcCellValue(t *testing.T) { "=SUBSTITUTE()": "SUBSTITUTE requires 3 or 4 arguments", "=SUBSTITUTE(\"\",\"\",\"\",\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", "=SUBSTITUTE(\"\",\"\",\"\",0)": "instance_num should be > 0", + // TEXTJOIN + "=TEXTJOIN()": "TEXTJOIN requires at least 3 arguments", + "=TEXTJOIN(\"\",\"\",1)": "strconv.ParseBool: parsing \"\": invalid syntax", + "=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", // TRIM "=TRIM()": "TRIM requires 1 argument", "=TRIM(1,2)": "TRIM requires 1 argument", @@ -2328,6 +2342,7 @@ func TestCalcCellValue(t *testing.T) { "=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", // COLUMN "=COLUMN(1,2)": "COLUMN requires at most 1 argument", "=COLUMN(\"\")": "invalid reference", @@ -2621,6 +2636,14 @@ func TestCalcCellValue(t *testing.T) { "=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!", + // 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!", // YIELDDISC "=YIELDDISC()": "YIELDDISC requires 4 or 5 arguments", "=YIELDDISC(\"\",\"06/30/2017\",97,100,0)": "#VALUE!", From 2e9635ece8ecf2617b0144451029aeca78edd9d9 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 27 Oct 2021 08:07:21 +0800 Subject: [PATCH 468/957] ref #65: new formula functions TBILLPRICE and TBILLYIELD --- calc.go | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 20 +++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/calc.go b/calc.go index a21be0b5a4..6f8dcb6ae4 100644 --- a/calc.go +++ b/calc.go @@ -538,6 +538,8 @@ type formulaFuncs struct { // TAN // TANH // TBILLEQ +// TBILLPRICE +// TBILLYIELD // TEXTJOIN // TIME // TODAY @@ -9796,6 +9798,76 @@ func (fn *formulaFuncs) TBILLEQ(argsList *list.List) formulaArg { return newNumberFormulaArg((365 * discount.Number) / (360 - discount.Number*dsm)) } +// TBILLPRICE function returns the price, per $100 face value, of a Treasury +// Bill. The syntax of the function is: +// +// TBILLPRICE(settlement,maturity,discount) +// +func (fn *formulaFuncs) TBILLPRICE(argsList *list.List) formulaArg { + if argsList.Len() != 3 { + return newErrorFormulaArg(formulaErrorVALUE, "TBILLPRICE requires 3 arguments") + } + args := list.New().Init() + args.PushBack(argsList.Front().Value.(formulaArg)) + settlement := fn.DATEVALUE(args) + if settlement.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + args.Init() + args.PushBack(argsList.Front().Next().Value.(formulaArg)) + maturity := fn.DATEVALUE(args) + if maturity.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + dsm := maturity.Number - settlement.Number + if dsm > 365 || maturity.Number <= settlement.Number { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + discount := argsList.Back().Value.(formulaArg).ToNumber() + if discount.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + if discount.Number <= 0 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + return newNumberFormulaArg(100 * (1 - discount.Number*dsm/360)) +} + +// TBILLYIELD function calculates the yield of a Treasury Bill. The syntax of +// the function is: +// +// TBILLYIELD(settlement,maturity,pr) +// +func (fn *formulaFuncs) TBILLYIELD(argsList *list.List) formulaArg { + if argsList.Len() != 3 { + return newErrorFormulaArg(formulaErrorVALUE, "TBILLYIELD requires 3 arguments") + } + args := list.New().Init() + args.PushBack(argsList.Front().Value.(formulaArg)) + settlement := fn.DATEVALUE(args) + if settlement.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + args.Init() + args.PushBack(argsList.Front().Next().Value.(formulaArg)) + maturity := fn.DATEVALUE(args) + if maturity.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + dsm := maturity.Number - settlement.Number + if dsm > 365 || maturity.Number <= settlement.Number { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + pr := argsList.Back().Value.(formulaArg).ToNumber() + if pr.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + if pr.Number <= 0 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + return newNumberFormulaArg(((100 - pr.Number) / pr.Number) * (360 / dsm)) +} + // YIELDDISC function calculates the annual yield of a discounted security. // The syntax of the function is: // diff --git a/calc_test.go b/calc_test.go index 95b479c62b..06c7fe123f 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1362,6 +1362,10 @@ func TestCalcCellValue(t *testing.T) { "=SYD(10000,1000,5,2)": "2400", // TBILLEQ "=TBILLEQ(\"01/01/2017\",\"06/30/2017\",2.5%)": "0.0256680731364276", + // TBILLPRICE + "=TBILLPRICE(\"02/01/2017\",\"06/30/2017\",2.75%)": "98.86180555555556", + // TBILLYIELD + "=TBILLYIELD(\"02/01/2017\",\"06/30/2017\",99)": "0.024405125076266", // YIELDDISC "=YIELDDISC(\"01/01/2017\",\"06/30/2017\",97,100)": "0.0622012325059031", "=YIELDDISC(\"01/01/2017\",\"06/30/2017\",97,100,0)": "0.0622012325059031", @@ -2644,6 +2648,22 @@ func TestCalcCellValue(t *testing.T) { "=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!", + // 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!", + // 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!", // YIELDDISC "=YIELDDISC()": "YIELDDISC requires 4 or 5 arguments", "=YIELDDISC(\"\",\"06/30/2017\",97,100,0)": "#VALUE!", From ffc998941c12f84917e15b74e6db7a936d5aab76 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 28 Oct 2021 22:17:33 +0800 Subject: [PATCH 469/957] ref #65: new formula functions SWITCH and TRIMMEAN --- calc.go | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 15 ++++++++++ 2 files changed, 95 insertions(+) diff --git a/calc.go b/calc.go index 6f8dcb6ae4..2c05176f63 100644 --- a/calc.go +++ b/calc.go @@ -533,6 +533,7 @@ type formulaFuncs struct { // SUM // SUMIF // SUMSQ +// SWITCH // SYD // T // TAN @@ -545,6 +546,7 @@ type formulaFuncs struct { // TODAY // TRANSPOSE // TRIM +// TRIMMEAN // TRUE // TRUNC // UNICHAR @@ -5741,6 +5743,49 @@ func (fn *formulaFuncs) SMALL(argsList *list.List) formulaArg { return fn.kth("SMALL", argsList) } +// TRIMMEAN function calculates the trimmed mean (or truncated mean) of a +// supplied set of values. The syntax of the function is: +// +// TRIMMEAN(array,percent) +// +func (fn *formulaFuncs) TRIMMEAN(argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, "TRIMMEAN requires 2 arguments") + } + percent := argsList.Back().Value.(formulaArg).ToNumber() + if percent.Type != ArgNumber { + return percent + } + if percent.Number < 0 || percent.Number >= 1 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + arr := []float64{} + arrArg := argsList.Front().Value.(formulaArg).ToList() + for _, cell := range arrArg { + num := cell.ToNumber() + if num.Type != ArgNumber { + continue + } + arr = append(arr, num.Number) + } + discard := math.Floor(float64(len(arr)) * percent.Number / 2) + sort.Float64s(arr) + for i := 0; i < int(discard); i++ { + if len(arr) > 0 { + arr = arr[1:] + } + if len(arr) > 0 { + arr = arr[:len(arr)-1] + } + } + + args := list.New().Init() + for _, ele := range arr { + args.PushBack(newNumberFormulaArg(ele)) + } + return fn.AVERAGE(args) +} + // VARP function returns the Variance of a given set of values. The syntax of // the function is: // @@ -6326,6 +6371,41 @@ func (fn *formulaFuncs) OR(argsList *list.List) formulaArg { return newStringFormulaArg(strings.ToUpper(strconv.FormatBool(or))) } +// SWITCH function compares a number of supplied values to a supplied test +// expression and returns a result corresponding to the first value that +// matches the test expression. A default value can be supplied, to be +// 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]) +// +func (fn *formulaFuncs) SWITCH(argsList *list.List) formulaArg { + if argsList.Len() < 3 { + return newErrorFormulaArg(formulaErrorVALUE, "SWITCH requires at least 3 arguments") + } + target := argsList.Front().Value.(formulaArg) + argCount := argsList.Len() - 1 + switchCount := int(math.Floor(float64(argCount) / 2)) + hasDefaultClause := argCount%2 != 0 + result := newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + if hasDefaultClause { + result = argsList.Back().Value.(formulaArg) + } + if switchCount > 0 { + arg := argsList.Front() + for i := 0; i < switchCount; i++ { + arg = arg.Next() + if target.Value() == arg.Value.(formulaArg).Value() { + result = arg.Next().Value.(formulaArg) + break + } + arg = arg.Next() + } + } + return result +} + // TRUE function returns the logical value TRUE. The syntax of the function // is: // diff --git a/calc_test.go b/calc_test.go index 06c7fe123f..5661145173 100644 --- a/calc_test.go +++ b/calc_test.go @@ -872,6 +872,9 @@ func TestCalcCellValue(t *testing.T) { "=SMALL(A1:B5,2)": "1", "=SMALL(A1,1)": "1", "=SMALL(A1:F2,1)": "1", + // TRIMMEAN + "=TRIMMEAN(A1:B4,10%)": "2.5", + "=TRIMMEAN(A1:B4,70%)": "2.5", // VARP "=VARP(A1:A5)": "1.25", // VAR.P @@ -957,6 +960,10 @@ func TestCalcCellValue(t *testing.T) { "=OR(0)": "FALSE", "=OR(1=2,2=2)": "TRUE", "=OR(1=2,2=3)": "FALSE", + // SWITCH + "=SWITCH(1,1,\"A\",2,\"B\",3,\"C\",\"N\")": "A", + "=SWITCH(3,1,\"A\",2,\"B\",3,\"C\",\"N\")": "C", + "=SWITCH(4,1,\"A\",2,\"B\",3,\"C\",\"N\")": "N", // TRUE "=TRUE()": "TRUE", // XOR @@ -2037,6 +2044,11 @@ func TestCalcCellValue(t *testing.T) { "=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", + // TRIMMEAN + "=TRIMMEAN()": "TRIMMEAN requires 2 arguments", + "=TRIMMEAN(A1,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=TRIMMEAN(A1,1)": "#NUM!", + "=TRIMMEAN(A1,-1)": "#NUM!", // VARP "=VARP()": "VARP requires at least 1 argument", "=VARP(\"\")": "#DIV/0!", @@ -2122,6 +2134,9 @@ func TestCalcCellValue(t *testing.T) { `=OR(A1:B1)`: "#VALUE!", "=OR()": "OR requires at least 1 argument", "=OR(1" + strings.Repeat(",1", 30) + ")": "OR accepts at most 30 arguments", + // SWITCH + "=SWITCH()": "SWITCH requires at least 3 arguments", + "=SWITCH(0,1,2)": "#N/A", // TRUE "=TRUE(A1)": "TRUE takes no arguments", // XOR From 5c4627a0c2ac631aceda207b55f25df195185517 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 29 Oct 2021 20:05:43 +0800 Subject: [PATCH 470/957] ref #65: new formula functions VALUE and WEEKDAY --- calc.go | 98 ++++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 32 +++++++++++++++++ 2 files changed, 130 insertions(+) diff --git a/calc.go b/calc.go index 2c05176f63..8b13caeae7 100644 --- a/calc.go +++ b/calc.go @@ -17,6 +17,7 @@ import ( "errors" "fmt" "math" + "math/big" "math/cmplx" "math/rand" "net/url" @@ -552,9 +553,11 @@ type formulaFuncs struct { // UNICHAR // UNICODE // UPPER +// VALUE // VAR.P // VARP // VLOOKUP +// WEEKDAY // WEIBULL // WEIBULL.DIST // XOR @@ -7187,6 +7190,63 @@ func daysBetween(startDate, endDate int64) float64 { return float64(int(0.5 + float64((endDate-startDate)/86400))) } +// 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]) +// +func (fn *formulaFuncs) WEEKDAY(argsList *list.List) formulaArg { + if argsList.Len() < 1 { + return newErrorFormulaArg(formulaErrorVALUE, "WEEKDAY requires at least 1 argument") + } + if argsList.Len() > 2 { + return newErrorFormulaArg(formulaErrorVALUE, "WEEKDAY allows at most 2 arguments") + } + + sn := argsList.Front().Value.(formulaArg) + num := sn.ToNumber() + weekday, returnType := 0, 1 + 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 + } + weekday = int(time.Date(y, time.Month(m), d, 0, 0, 0, 0, time.Now().Location()).Weekday()) + } else { + if num.Number < 0 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + weekday = int(timeFromExcelTime(num.Number, false).Weekday()) + } + if argsList.Len() == 2 { + returnTypeArg := argsList.Back().Value.(formulaArg).ToNumber() + if returnTypeArg.Type != ArgNumber { + return returnTypeArg + } + returnType = int(returnTypeArg.Number) + } + if returnType == 2 { + returnType = 11 + } + weekday++ + if returnType == 1 { + return newNumberFormulaArg(float64(weekday)) + } + if returnType == 3 { + return newNumberFormulaArg(float64((weekday + 6 - 1) % 7)) + } + if returnType >= 11 && returnType <= 17 { + return newNumberFormulaArg(float64((weekday+6-(returnType-10))%7 + 1)) + } + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) +} + // Text Functions // CHAR function returns the character relating to a supplied character set @@ -7831,6 +7891,44 @@ func (fn *formulaFuncs) UPPER(argsList *list.List) formulaArg { return newStringFormulaArg(strings.ToUpper(argsList.Front().Value.(formulaArg).String)) } +// VALUE function converts a text string into a numeric value. The syntax of +// the function is: +// +// VALUE(text) +// +func (fn *formulaFuncs) VALUE(argsList *list.List) formulaArg { + if argsList.Len() != 1 { + return newErrorFormulaArg(formulaErrorVALUE, "VALUE requires 1 argument") + } + text := strings.ReplaceAll(argsList.Front().Value.(formulaArg).Value(), ",", "") + percent := 1.0 + if strings.HasSuffix(text, "%") { + percent, text = 0.01, strings.TrimSuffix(text, "%") + } + decimal := big.Float{} + if _, ok := decimal.SetString(text); ok { + value, _ := decimal.Float64() + return newNumberFormulaArg(value * percent) + } + dateValue, timeValue, errTime, errDate := 0.0, 0.0, false, false + if !isDateOnlyFmt(text) { + h, m, s, _, _, err := strToTime(text) + errTime = err.Type == ArgError + if !errTime { + timeValue = (float64(h)*3600 + float64(m)*60 + s) / 86400 + } + } + y, m, d, _, err := strToDate(text) + errDate = err.Type == ArgError + if !errDate { + dateValue = daysBetween(excelMinTime1900.Unix(), makeDate(y, time.Month(m), d)) + 1 + } + if errTime && errDate { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + return newNumberFormulaArg(dateValue + timeValue) +} + // Conditional Functions // IF function tests a supplied condition and returns one result if the diff --git a/calc_test.go b/calc_test.go index 5661145173..af27079d42 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1052,6 +1052,20 @@ func TestCalcCellValue(t *testing.T) { "=TIME(5,44,32)": "0.239259259259259", "=TIME(\"5\",\"44\",\"32\")": "0.239259259259259", "=TIME(0,0,73)": "0.000844907407407407", + // WEEKDAY + "=WEEKDAY(0)": "7", + "=WEEKDAY(47119)": "2", + "=WEEKDAY(\"12/25/2012\")": "3", + "=WEEKDAY(\"12/25/2012\",1)": "3", + "=WEEKDAY(\"12/25/2012\",2)": "2", + "=WEEKDAY(\"12/25/2012\",3)": "1", + "=WEEKDAY(\"12/25/2012\",11)": "2", + "=WEEKDAY(\"12/25/2012\",12)": "1", + "=WEEKDAY(\"12/25/2012\",13)": "7", + "=WEEKDAY(\"12/25/2012\",14)": "6", + "=WEEKDAY(\"12/25/2012\",15)": "5", + "=WEEKDAY(\"12/25/2012\",16)": "4", + "=WEEKDAY(\"12/25/2012\",17)": "3", // Text Functions // CHAR "=CHAR(65)": "A", @@ -1194,6 +1208,13 @@ func TestCalcCellValue(t *testing.T) { "=UPPER(\"TEST\")": "TEST", "=UPPER(\"Test\")": "TEST", "=UPPER(\"TEST 123\")": "TEST 123", + // VALUE + "=VALUE(\"50\")": "50", + "=VALUE(\"1.0E-07\")": "1e-07", + "=VALUE(\"5,000\")": "5000", + "=VALUE(\"20%\")": "0.2", + "=VALUE(\"12:00:00\")": "0.5", + "=VALUE(\"01/02/2006 15:04:05\")": "38719.62783564815", // Conditional Functions // IF "=IF(1=1)": "TRUE", @@ -2224,6 +2245,14 @@ func TestCalcCellValue(t *testing.T) { "=TIME(0,0,-1)": "#NUM!", // TODAY "=TODAY(A1)": "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!", // Text Functions // CHAR "=CHAR()": "CHAR requires 1 argument", @@ -2337,6 +2366,9 @@ func TestCalcCellValue(t *testing.T) { // UNICODE "=UNICODE()": "UNICODE requires 1 argument", "=UNICODE(\"\")": "#VALUE!", + // VALUE + "=VALUE()": "VALUE requires 1 argument", + "=VALUE(\"\")": "#VALUE!", // UPPER "=UPPER()": "UPPER requires 1 argument", "=UPPER(1,2)": "UPPER requires 1 argument", From 8932a0a0c34b67c5a1c1b2ffa757885def4cc5d4 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 30 Oct 2021 14:27:14 +0800 Subject: [PATCH 471/957] ref #65: new formula functions PV, RANK and RANK.EQ --- calc.go | 105 +++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 32 ++++++++++++++++ 2 files changed, 137 insertions(+) diff --git a/calc.go b/calc.go index 8b13caeae7..879a0ad842 100644 --- a/calc.go +++ b/calc.go @@ -498,12 +498,15 @@ type formulaFuncs struct { // PRICEDISC // PRODUCT // PROPER +// PV // QUARTILE // QUARTILE.INC // QUOTIENT // RADIANS // RAND // RANDBETWEEN +// RANK +// RANK.EQ // REPLACE // REPLACEB // REPT @@ -5700,6 +5703,63 @@ func (fn *formulaFuncs) QUARTILEdotINC(argsList *list.List) formulaArg { return fn.QUARTILE(argsList) } +// rank is an implementation of the formula functions RANK and RANK.EQ. +func (fn *formulaFuncs) rank(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 requires at most 3 arguments", name)) + } + num := argsList.Front().Value.(formulaArg).ToNumber() + if num.Type != ArgNumber { + return num + } + arr := []float64{} + for _, arg := range argsList.Front().Next().Value.(formulaArg).ToList() { + n := arg.ToNumber() + if n.Type == ArgNumber { + arr = append(arr, n.Number) + } + } + sort.Float64s(arr) + order := newNumberFormulaArg(0) + if argsList.Len() == 3 { + if order = argsList.Back().Value.(formulaArg).ToNumber(); order.Type != ArgNumber { + return order + } + } + if order.Number == 0 { + sort.Sort(sort.Reverse(sort.Float64Slice(arr))) + } + for idx, n := range arr { + if num.Number == n { + return newNumberFormulaArg(float64(idx + 1)) + } + } + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) +} + +// RANK.EQ 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: +// +// RANK.EQ(number,ref,[order]) +// +func (fn *formulaFuncs) RANKdotEQ(argsList *list.List) formulaArg { + return fn.rank("RANK.EQ", argsList) +} + +// RANK 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: +// +// RANK(number,ref,[order]) +// +func (fn *formulaFuncs) RANK(argsList *list.List) formulaArg { + return fn.rank("RANK", argsList) +} + // SKEW function calculates the skewness of the distribution of a supplied set // of values. The syntax of the function is: // @@ -9863,6 +9923,51 @@ func (fn *formulaFuncs) PRICEDISC(argsList *list.List) formulaArg { return newNumberFormulaArg(redemption.Number * (1 - discount.Number*frac.Number)) } +// 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]) +// +func (fn *formulaFuncs) PV(argsList *list.List) formulaArg { + if argsList.Len() < 3 { + return newErrorFormulaArg(formulaErrorVALUE, "PV requires at least 3 arguments") + } + if argsList.Len() > 5 { + return newErrorFormulaArg(formulaErrorVALUE, "PV allows at most 5 arguments") + } + rate := argsList.Front().Value.(formulaArg).ToNumber() + if rate.Type != ArgNumber { + return rate + } + nper := argsList.Front().Next().Value.(formulaArg).ToNumber() + if nper.Type != ArgNumber { + return nper + } + pmt := argsList.Front().Next().Next().Value.(formulaArg).ToNumber() + if pmt.Type != ArgNumber { + return pmt + } + fv := newNumberFormulaArg(0) + if argsList.Len() >= 4 { + if fv = argsList.Front().Next().Next().Next().Value.(formulaArg).ToNumber(); fv.Type != ArgNumber { + return fv + } + } + t := newNumberFormulaArg(0) + if argsList.Len() == 5 { + if t = argsList.Back().Value.(formulaArg).ToNumber(); t.Type != ArgNumber { + return t + } + if t.Number != 0 { + t.Number = 1 + } + } + if rate.Number == 0 { + return newNumberFormulaArg(-pmt.Number*nper.Number - fv.Number) + } + return newNumberFormulaArg((((1-math.Pow(1+rate.Number, nper.Number))/rate.Number)*pmt.Number*(1+rate.Number*t.Number) - fv.Number) / math.Pow(1+rate.Number, nper.Number)) +} + // RRI function calculates the equivalent interest rate for an investment with // specified present value, future value and duration. The syntax of the // function is: diff --git a/calc_test.go b/calc_test.go index af27079d42..1545c76d87 100644 --- a/calc_test.go +++ b/calc_test.go @@ -863,6 +863,14 @@ func TestCalcCellValue(t *testing.T) { "=QUARTILE(A1:A4,2)": "1.5", // QUARTILE.INC "=QUARTILE.INC(A1:A4,0)": "0", + // RANK + "=RANK(1,A1:B5)": "5", + "=RANK(1,A1:B5,0)": "5", + "=RANK(1,A1:B5,1)": "2", + // RANK.EQ + "=RANK.EQ(1,A1:B5)": "5", + "=RANK.EQ(1,A1:B5,0)": "5", + "=RANK.EQ(1,A1:B5,1)": "2", // SKEW "=SKEW(1,2,3,4,3)": "-0.404796008910937", "=SKEW(A1:B2)": "0", @@ -1381,6 +1389,10 @@ 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", + // PV + "=PV(0,60,1000)": "-60000", + "=PV(5%/12,60,1000)": "-52990.70632392748", + "=PV(10%/4,16,2000,0,1)": "-26762.75545288113", // RRI "=RRI(10,10000,15000)": "0.0413797439924106", // SLN @@ -2056,6 +2068,18 @@ func TestCalcCellValue(t *testing.T) { "=QUARTILE(A1:A4,5)": "#NUM!", // QUARTILE.INC "=QUARTILE.INC()": "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.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", // SKEW "=SKEW()": "SKEW requires at least 1 argument", "=SKEW(\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", @@ -2671,6 +2695,14 @@ func TestCalcCellValue(t *testing.T) { "=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", + // 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", // RRI "=RRI()": "RRI requires 3 arguments", "=RRI(\"\",\"\",\"\")": "#NUM!", From 9cc66948307300b9bac41dc38e39f7e1d7810fd0 Mon Sep 17 00:00:00 2001 From: Jerring <3182730575@qq.com> Date: Sat, 30 Oct 2021 22:14:49 +0800 Subject: [PATCH 472/957] ref #65: new formula functions RATE and RECEIVED (#1045) --- calc.go | 115 +++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 26 ++++++++++++ 2 files changed, 141 insertions(+) diff --git a/calc.go b/calc.go index 879a0ad842..f346fa20c8 100644 --- a/calc.go +++ b/calc.go @@ -507,6 +507,8 @@ type formulaFuncs struct { // RANDBETWEEN // RANK // RANK.EQ +// RATE +// RECEIVED // REPLACE // REPLACEB // REPT @@ -9968,6 +9970,119 @@ func (fn *formulaFuncs) PV(argsList *list.List) formulaArg { return newNumberFormulaArg((((1-math.Pow(1+rate.Number, nper.Number))/rate.Number)*pmt.Number*(1+rate.Number*t.Number) - fv.Number) / math.Pow(1+rate.Number, nper.Number)) } +// RATE function calculates the interest rate required to pay off a specified +// 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]) +// +func (fn *formulaFuncs) RATE(argsList *list.List) formulaArg { + if argsList.Len() < 3 { + return newErrorFormulaArg(formulaErrorVALUE, "RATE requires at least 3 arguments") + } + if argsList.Len() > 6 { + return newErrorFormulaArg(formulaErrorVALUE, "RATE allows at most 6 arguments") + } + nper := argsList.Front().Value.(formulaArg).ToNumber() + if nper.Type != ArgNumber { + return nper + } + pmt := argsList.Front().Next().Value.(formulaArg).ToNumber() + if pmt.Type != ArgNumber { + return pmt + } + pv := argsList.Front().Next().Next().Value.(formulaArg).ToNumber() + if pv.Type != ArgNumber { + return pv + } + fv := newNumberFormulaArg(0) + if argsList.Len() >= 4 { + if fv = argsList.Front().Next().Next().Next().Value.(formulaArg).ToNumber(); fv.Type != ArgNumber { + return fv + } + } + t := newNumberFormulaArg(0) + if argsList.Len() >= 5 { + if t = argsList.Front().Next().Next().Next().Next().Value.(formulaArg).ToNumber(); t.Type != ArgNumber { + return t + } + if t.Number != 0 { + t.Number = 1 + } + } + guess := newNumberFormulaArg(0.1) + if argsList.Len() == 6 { + if guess = argsList.Back().Value.(formulaArg).ToNumber(); guess.Type != ArgNumber { + return guess + } + } + maxIter, iter, close, epsMax, rate := 100, 0, false, 1e-6, guess.Number + for iter < maxIter && !close { + t1 := math.Pow(rate+1, nper.Number) + t2 := math.Pow(rate+1, nper.Number-1) + 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) + f3 := (nper.Number*pmt.Number*t2*rt + p0*t.Number) / rate + delta := f1 / (f2 + f3) + if math.Abs(delta) < epsMax { + close = true + } + iter++ + rate -= delta + } + return newNumberFormulaArg(rate) +} + +// 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]) +// +func (fn *formulaFuncs) RECEIVED(argsList *list.List) formulaArg { + if argsList.Len() < 4 { + return newErrorFormulaArg(formulaErrorVALUE, "RECEIVED requires at least 4 arguments") + } + if argsList.Len() > 5 { + return newErrorFormulaArg(formulaErrorVALUE, "RECEIVED allows at most 5 arguments") + } + args := list.New().Init() + args.PushBack(argsList.Front().Value.(formulaArg)) + settlement := fn.DATEVALUE(args) + if settlement.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + args.Init() + args.PushBack(argsList.Front().Next().Value.(formulaArg)) + maturity := fn.DATEVALUE(args) + if maturity.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + investment := argsList.Front().Next().Next().Value.(formulaArg).ToNumber() + if investment.Type != ArgNumber { + return investment + } + discount := argsList.Front().Next().Next().Next().Value.(formulaArg).ToNumber() + if discount.Type != ArgNumber { + return discount + } + if discount.Number <= 0 { + return newErrorFormulaArg(formulaErrorNUM, "RECEIVED requires discount > 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(investment.Number / (1 - discount.Number*frac.Number)) +} + // RRI function calculates the equivalent interest rate for an investment with // specified present value, future value and duration. The syntax of the // function is: diff --git a/calc_test.go b/calc_test.go index 1545c76d87..ffaec1a82c 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1393,6 +1393,13 @@ func TestCalcCellValue(t *testing.T) { "=PV(0,60,1000)": "-60000", "=PV(5%/12,60,1000)": "-52990.70632392748", "=PV(10%/4,16,2000,0,1)": "-26762.75545288113", + // 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", // RRI "=RRI(10,10000,15000)": "0.0413797439924106", // SLN @@ -2703,6 +2710,25 @@ func TestCalcCellValue(t *testing.T) { "=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", + // 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", + // 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", // RRI "=RRI()": "RRI requires 3 arguments", "=RRI(\"\",\"\",\"\")": "#NUM!", From 3bb2849e419e07d0f582537a3dc48f7a265596a8 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 31 Oct 2021 00:15:17 +0800 Subject: [PATCH 473/957] ref #65: new formula functions DEVSQ and GEOMEAN --- calc.go | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 12 ++++++++++++ 2 files changed, 65 insertions(+) diff --git a/calc.go b/calc.go index f346fa20c8..97ba3736e5 100644 --- a/calc.go +++ b/calc.go @@ -365,6 +365,7 @@ type formulaFuncs struct { // DEC2OCT // DECIMAL // DEGREES +// DEVSQ // DISC // DOLLARDE // DOLLARFR @@ -389,6 +390,7 @@ type formulaFuncs struct { // GAMMA // GAMMALN // GCD +// GEOMEAN // HARMEAN // HEX2BIN // HEX2DEC @@ -4924,6 +4926,36 @@ func (fn *formulaFuncs) COUNTBLANK(argsList *list.List) formulaArg { return newNumberFormulaArg(float64(count)) } +// DEVSQ function calculates the sum of the squared deviations from the sample +// mean. The syntax of the function is: +// +// 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") + } + 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 { + continue + } + count++ + if count == 0 { + result = math.Pow(num.Number-avg.Number, 2) + continue + } + result += math.Pow(num.Number-avg.Number, 2) + } + } + if count == -1 { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + } + return newNumberFormulaArg(result) +} + // FISHER function calculates the Fisher Transformation for a supplied value. // The syntax of the function is: // @@ -5030,6 +5062,27 @@ func (fn *formulaFuncs) GAMMALN(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorVALUE, "GAMMALN requires 1 numeric argument") } +// GEOMEAN function calculates the geometric mean of a supplied set of values. +// The syntax of the function is: +// +// 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") + } + product := fn.PRODUCT(argsList) + if product.Type != ArgNumber { + return product + } + 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 newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) +} + // HARMEAN function calculates the harmonic mean of a supplied set of values. // The syntax of the function is: // diff --git a/calc_test.go b/calc_test.go index ffaec1a82c..6d46154fbe 100644 --- a/calc_test.go +++ b/calc_test.go @@ -763,6 +763,9 @@ func TestCalcCellValue(t *testing.T) { "=COUNTBLANK(1)": "0", "=COUNTBLANK(B1:C1)": "1", "=COUNTBLANK(C1)": "1", + // DEVSQ + "=DEVSQ(1,3,5,2,9,7)": "47.5", + "=DEVSQ(A1:D2)": "10", // FISHER "=FISHER(-0.9)": "-1.47221948958322", "=FISHER(-0.25)": "-0.255412811882995", @@ -780,6 +783,8 @@ func TestCalcCellValue(t *testing.T) { // GAMMALN "=GAMMALN(4.5)": "2.45373657084244", "=GAMMALN(INT(1))": "0", + // GEOMEAN + "=GEOMEAN(2.5,3,0.5,1,3)": "1.6226711115996", // HARMEAN "=HARMEAN(2.5,3,0.5,1,3)": "1.22950819672131", "=HARMEAN(\"2.5\",3,0.5,1,INT(3),\"\")": "1.22950819672131", @@ -1977,6 +1982,9 @@ func TestCalcCellValue(t *testing.T) { // COUNTBLANK "=COUNTBLANK()": "COUNTBLANK requires 1 argument", "=COUNTBLANK(1,2)": "COUNTBLANK requires 1 argument", + // DEVSQ + "=DEVSQ()": "DEVSQ requires at least 1 numeric argument", + "=DEVSQ(D1:D2)": "#N/A", // FISHER "=FISHER()": "FISHER requires 1 numeric argument", "=FISHER(2)": "#N/A", @@ -1995,6 +2003,10 @@ func TestCalcCellValue(t *testing.T) { "=GAMMALN(F1)": "GAMMALN requires 1 numeric argument", "=GAMMALN(0)": "#N/A", "=GAMMALN(INT(0))": "#N/A", + // GEOMEAN + "=GEOMEAN()": "GEOMEAN requires at least 1 numeric argument", + "=GEOMEAN(0)": "#NUM!", + "=GEOMEAN(D1:D2)": "strconv.ParseFloat: parsing \"Month\": invalid syntax", // HARMEAN "=HARMEAN()": "HARMEAN requires at least 1 argument", "=HARMEAN(-1)": "#N/A", From 3447cfecf2402f58e4667b9995dfa21e949b9b69 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 1 Nov 2021 00:13:38 +0800 Subject: [PATCH 474/957] ref #65: new formula functions ACCRINT and ISOWEEKNUM --- calc.go | 138 ++++++++++++++++++++++++++++++++++++++++++++------- calc_test.go | 26 ++++++++++ 2 files changed, 146 insertions(+), 18 deletions(-) diff --git a/calc.go b/calc.go index 97ba3736e5..7d27cb88a2 100644 --- a/calc.go +++ b/calc.go @@ -296,6 +296,7 @@ type formulaFuncs struct { // Supported formula functions: // // ABS +// ACCRINT // ACCRINTM // ACOS // ACOSH @@ -439,6 +440,7 @@ type formulaFuncs struct { // ISODD // ISTEXT // ISO.CEILING +// ISOWEEKNUM // ISPMT // KURT // LARGE @@ -7025,6 +7027,39 @@ func (fn *formulaFuncs) DAYS(argsList *list.List) formulaArg { return newNumberFormulaArg(end - start) } +// ISOWEEKNUM function returns the ISO week number of a supplied date. The +// syntax of the function is: +// +// ISOWEEKNUM(date) +// +func (fn *formulaFuncs) ISOWEEKNUM(argsList *list.List) formulaArg { + if argsList.Len() != 1 { + return newErrorFormulaArg(formulaErrorVALUE, "ISOWEEKNUM requires 1 argument") + } + date := argsList.Front().Value.(formulaArg) + num := date.ToNumber() + weeknum := 0 + 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 + } + _, 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() + } + return newNumberFormulaArg(float64(weeknum)) +} + // MONTH function returns the month of a date represented by a serial number. // The month is given as an integer, ranging from 1 (January) to 12 // (December). The syntax of the function is: @@ -7317,7 +7352,6 @@ func (fn *formulaFuncs) WEEKDAY(argsList *list.List) formulaArg { if argsList.Len() > 2 { return newErrorFormulaArg(formulaErrorVALUE, "WEEKDAY allows at most 2 arguments") } - sn := argsList.Front().Value.(formulaArg) num := sn.ToNumber() weekday, returnType := 0, 1 @@ -8915,6 +8949,69 @@ func (fn *formulaFuncs) ENCODEURL(argsList *list.List) formulaArg { // Financial Functions +// validateFrequency check the number of coupon payments per year if be equal to 1, 2 or 4. +func validateFrequency(freq float64) bool { + return freq == 1 || freq == 2 || freq == 4 +} + +// ACCRINT function returns the accrued interest for a security that pays +// periodic interest. The syntax of the function is: +// +// 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") + } + if argsList.Len() > 8 { + return newErrorFormulaArg(formulaErrorVALUE, "ACCRINT allows at most 8 arguments") + } + args := list.New().Init() + args.PushBack(argsList.Front().Value.(formulaArg)) + issue := fn.DATEVALUE(args) + if issue.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + args.Init() + args.PushBack(argsList.Front().Next().Value.(formulaArg)) + fi := fn.DATEVALUE(args) + if fi.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + args.Init() + args.PushBack(argsList.Front().Next().Next().Value.(formulaArg)) + settlement := fn.DATEVALUE(args) + if settlement.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + rate := argsList.Front().Next().Next().Next().Value.(formulaArg).ToNumber() + par := argsList.Front().Next().Next().Next().Next().Value.(formulaArg).ToNumber() + frequency := argsList.Front().Next().Next().Next().Next().Next().Value.(formulaArg).ToNumber() + if rate.Type != ArgNumber || par.Type != ArgNumber || frequency.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + if !validateFrequency(frequency.Number) { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + basis := newNumberFormulaArg(0) + if argsList.Len() >= 7 { + if basis = argsList.Front().Next().Next().Next().Next().Next().Next().Value.(formulaArg).ToNumber(); basis.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + } + cm := newBoolFormulaArg(true) + if argsList.Len() == 8 { + if cm = argsList.Back().Value.(formulaArg).ToBool(); cm.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + } + frac1 := yearFrac(issue.Number, settlement.Number, int(basis.Number)) + if frac1.Type != ArgNumber { + return frac1 + } + return newNumberFormulaArg(par.Number * rate.Number * frac1.Number) +} + // ACCRINTM function returns the accrued interest for a security that pays // interest at maturity. The syntax of the function is: // @@ -10023,6 +10120,27 @@ func (fn *formulaFuncs) PV(argsList *list.List) formulaArg { return newNumberFormulaArg((((1-math.Pow(1+rate.Number, nper.Number))/rate.Number)*pmt.Number*(1+rate.Number*t.Number) - fv.Number) / math.Pow(1+rate.Number, nper.Number)) } +// 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 { + t1 := math.Pow(rate+1, nper.Number) + t2 := math.Pow(rate+1, nper.Number-1) + 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) + f3 := (nper.Number*pmt.Number*t2*rt + p0*t.Number) / rate + delta := f1 / (f2 + f3) + if math.Abs(delta) < epsMax { + close = true + } + iter++ + rate -= delta + } + return newNumberFormulaArg(rate) +} + // RATE function calculates the interest rate required to pay off a specified // amount of a loan, or to reach a target amount on an investment, over a // given period. The syntax of the function is: @@ -10069,23 +10187,7 @@ func (fn *formulaFuncs) RATE(argsList *list.List) formulaArg { return guess } } - maxIter, iter, close, epsMax, rate := 100, 0, false, 1e-6, guess.Number - for iter < maxIter && !close { - t1 := math.Pow(rate+1, nper.Number) - t2 := math.Pow(rate+1, nper.Number-1) - 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) - f3 := (nper.Number*pmt.Number*t2*rt + p0*t.Number) / rate - delta := f1 / (f2 + f3) - if math.Abs(delta) < epsMax { - close = true - } - iter++ - rate -= delta - } - return newNumberFormulaArg(rate) + return fn.rate(nper, pmt, pv, fv, t, guess, argsList) } // RECEIVED function calculates the amount received at maturity for a fully diff --git a/calc_test.go b/calc_test.go index 6d46154fbe..b241db8fe0 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1028,6 +1028,11 @@ func TestCalcCellValue(t *testing.T) { "=DAYS(2,1)": "1", "=DAYS(INT(2),INT(1))": "1", "=DAYS(\"02/02/2015\",\"01/01/2015\")": "32", + // ISOWEEKNUM + "=ISOWEEKNUM(42370)": "53", + "=ISOWEEKNUM(\"42370\")": "53", + "=ISOWEEKNUM(\"01/01/2005\")": "53", + "=ISOWEEKNUM(\"02/02/2005\")": "5", // MONTH "=MONTH(42171)": "6", "=MONTH(\"31-May-2015\")": "5", @@ -1315,6 +1320,9 @@ func TestCalcCellValue(t *testing.T) { // ENCODEURL "=ENCODEURL(\"https://xuri.me/excelize/en/?q=Save As\")": "https%3A%2F%2Fxuri.me%2Fexcelize%2Fen%2F%3Fq%3DSave%20As", // Financial Functions + // ACCRINT + "=ACCRINT(\"01/01/2012\",\"04/01/2012\",\"12/31/2013\",8%,10000,4,0,TRUE)": "1600", + "=ACCRINT(\"01/01/2012\",\"04/01/2012\",\"12/31/2013\",8%,10000,4,0,FALSE)": "1600", // ACCRINTM "=ACCRINTM(\"01/01/2012\",\"12/31/2012\",8%,10000)": "800", "=ACCRINTM(\"01/01/2012\",\"12/31/2012\",8%,10000,3)": "800", @@ -2262,6 +2270,11 @@ func TestCalcCellValue(t *testing.T) { "=DAYS(0,\"\")": "#VALUE!", "=DAYS(NA(),0)": "#VALUE!", "=DAYS(0,NA())": "#VALUE!", + // ISOWEEKNUM + "=ISOWEEKNUM()": "ISOWEEKNUM requires 1 argument", + "=ISOWEEKNUM(\"\")": "#VALUE!", + "=ISOWEEKNUM(\"January 25, 100\")": "#VALUE!", + "=ISOWEEKNUM(-1)": "#NUM!", // MONTH "=MONTH()": "MONTH requires exactly 1 argument", "=MONTH(0,0)": "MONTH requires exactly 1 argument", @@ -2505,6 +2518,19 @@ func TestCalcCellValue(t *testing.T) { // ENCODEURL "=ENCODEURL()": "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", // ACCRINTM "=ACCRINTM()": "ACCRINTM requires 4 or 5 arguments", "=ACCRINTM(\"\",\"01/01/2012\",8%,10000)": "#VALUE!", From b0eb9ef807afb46b1b4f7932b4e08da1780e4fc1 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 2 Nov 2021 00:02:35 +0800 Subject: [PATCH 475/957] ref #65: new formula functions DELTA and GESTEP --- calc.go | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 21 +++++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/calc.go b/calc.go index 7d27cb88a2..9ba6626095 100644 --- a/calc.go +++ b/calc.go @@ -366,6 +366,7 @@ type formulaFuncs struct { // DEC2OCT // DECIMAL // DEGREES +// DELTA // DEVSQ // DISC // DOLLARDE @@ -392,6 +393,7 @@ type formulaFuncs struct { // GAMMALN // GCD // GEOMEAN +// GESTEP // HARMEAN // HEX2BIN // HEX2DEC @@ -1975,6 +1977,57 @@ func (fn *formulaFuncs) dec2x(name string, argsList *list.List) formulaArg { return newStringFormulaArg(strings.ToUpper(binary)) } +// DELTA function tests two numbers for equality and returns the Kronecker +// 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]) +// +func (fn *formulaFuncs) DELTA(argsList *list.List) formulaArg { + if argsList.Len() < 1 { + return newErrorFormulaArg(formulaErrorVALUE, "DELTA requires at least 1 argument") + } + if argsList.Len() > 2 { + return newErrorFormulaArg(formulaErrorVALUE, "DELTA allows at most 2 arguments") + } + number1 := argsList.Front().Value.(formulaArg).ToNumber() + if number1.Type != ArgNumber { + return number1 + } + number2 := newNumberFormulaArg(0) + if argsList.Len() == 2 { + if number2 = argsList.Back().Value.(formulaArg).ToNumber(); number2.Type != ArgNumber { + return number2 + } + } + return newBoolFormulaArg(number1.Number == number2.Number).ToNumber() +} + +// 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]) +// +func (fn *formulaFuncs) GESTEP(argsList *list.List) formulaArg { + if argsList.Len() < 1 { + return newErrorFormulaArg(formulaErrorVALUE, "GESTEP requires at least 1 argument") + } + if argsList.Len() > 2 { + return newErrorFormulaArg(formulaErrorVALUE, "GESTEP allows at most 2 arguments") + } + number := argsList.Front().Value.(formulaArg).ToNumber() + if number.Type != ArgNumber { + return number + } + step := newNumberFormulaArg(0) + if argsList.Len() == 2 { + if step = argsList.Back().Value.(formulaArg).ToNumber(); step.Type != ArgNumber { + return step + } + } + return newBoolFormulaArg(number.Number >= step.Number).ToNumber() +} + // HEX2BIN function converts a Hexadecimal (Base 16) number into a Binary // (Base 2) number. The syntax of the function is: // diff --git a/calc_test.go b/calc_test.go index b241db8fe0..88c58a602b 100644 --- a/calc_test.go +++ b/calc_test.go @@ -137,6 +137,17 @@ func TestCalcCellValue(t *testing.T) { "=DEC2OCT(8,10)": "0000000010", "=DEC2OCT(-8)": "7777777770", "=DEC2OCT(237)": "355", + // DELTA + "=DELTA(5,4)": "0", + "=DELTA(1.00001,1)": "0", + "=DELTA(1.23,1.23)": "1", + "=DELTA(1)": "0", + "=DELTA(0)": "1", + // GESTEP + "=GESTEP(1.2,0.001)": "1", + "=GESTEP(0.05,0.05)": "1", + "=GESTEP(-0.00001,0)": "0", + "=GESTEP(-0.00001)": "0", // HEX2BIN "=HEX2BIN(\"2\")": "10", "=HEX2BIN(\"0000000001\")": "1", @@ -1562,6 +1573,16 @@ func TestCalcCellValue(t *testing.T) { "=DEC2OCT(-536870912 ,10)": "#NUM!", "=DEC2OCT(1,-1)": "#NUM!", "=DEC2OCT(8,1)": "#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", + // 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", // HEX2BIN "=HEX2BIN()": "HEX2BIN requires at least 1 argument", "=HEX2BIN(1,1,1)": "HEX2BIN allows at most 2 arguments", From 08ad10f68e0c603bac1499a0d7b93b251123a54b Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 3 Nov 2021 00:17:06 +0800 Subject: [PATCH 476/957] ref #65: new formula functions ERF, ERF.PRECISE, ERFC and ERFC.PRECISE --- calc.go | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 29 +++++++++++++++++++ 2 files changed, 107 insertions(+) diff --git a/calc.go b/calc.go index 9ba6626095..eac0f6c109 100644 --- a/calc.go +++ b/calc.go @@ -373,6 +373,10 @@ type formulaFuncs struct { // DOLLARFR // EFFECT // ENCODEURL +// ERF +// ERF.PRECISE +// ERFC +// ERFC.PRECISE // EVEN // EXACT // EXP @@ -2003,6 +2007,80 @@ func (fn *formulaFuncs) DELTA(argsList *list.List) formulaArg { return newBoolFormulaArg(number1.Number == number2.Number).ToNumber() } +// ERF function calculates the Error Function, integrated between two supplied +// limits. The syntax of the function is: +// +// 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") + } + if argsList.Len() > 2 { + return newErrorFormulaArg(formulaErrorVALUE, "ERF allows at most 2 arguments") + } + lower := argsList.Front().Value.(formulaArg).ToNumber() + if lower.Type != ArgNumber { + return lower + } + if argsList.Len() == 2 { + upper := argsList.Back().Value.(formulaArg).ToNumber() + if upper.Type != ArgNumber { + return upper + } + return newNumberFormulaArg(math.Erf(upper.Number) - math.Erf(lower.Number)) + } + return newNumberFormulaArg(math.Erf(lower.Number)) +} + +// 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) +// +func (fn *formulaFuncs) ERFdotPRECISE(argsList *list.List) formulaArg { + if argsList.Len() != 1 { + return newErrorFormulaArg(formulaErrorVALUE, "ERF.PRECISE requires 1 argument") + } + x := argsList.Front().Value.(formulaArg).ToNumber() + if x.Type != ArgNumber { + return x + } + return newNumberFormulaArg(math.Erf(x.Number)) +} + +// erfc is an implementation of the formula functions ERFC and ERFC.PRECISE. +func (fn *formulaFuncs) erfc(name string, argsList *list.List) formulaArg { + if argsList.Len() != 1 { + return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires 1 argument", name)) + } + x := argsList.Front().Value.(formulaArg).ToNumber() + if x.Type != ArgNumber { + return x + } + return newNumberFormulaArg(math.Erfc(x.Number)) +} + +// ERFC function calculates the Complementary Error Function, integrated +// between a supplied lower limit and infinity. The syntax of the function +// is: +// +// ERFC(x) +// +func (fn *formulaFuncs) ERFC(argsList *list.List) formulaArg { + return fn.erfc("ERFC", argsList) +} + +// ERFC.PRECISE function calculates the Complementary Error Function, +// integrated between a supplied lower limit and infinity. The syntax of the +// function is: +// +// ERFC(x) +// +func (fn *formulaFuncs) ERFCdotPRECISE(argsList *list.List) formulaArg { + return fn.erfc("ERFC.PRECISE", argsList) +} + // GESTEP unction tests whether a supplied number is greater than a supplied // step size and returns. The syntax of the function is: // diff --git a/calc_test.go b/calc_test.go index 88c58a602b..b2fd5f4f8e 100644 --- a/calc_test.go +++ b/calc_test.go @@ -143,6 +143,21 @@ func TestCalcCellValue(t *testing.T) { "=DELTA(1.23,1.23)": "1", "=DELTA(1)": "0", "=DELTA(0)": "1", + // ERF + "=ERF(1.5)": "0.966105146475311", + "=ERF(0,1.5)": "0.966105146475311", + "=ERF(1,2)": "0.152621472069238", + // ERF.PRECISE + "=ERF.PRECISE(-1)": "-0.842700792949715", + "=ERF.PRECISE(1.5)": "0.966105146475311", + // ERFC + "=ERFC(0)": "1", + "=ERFC(0.5)": "0.479500122186953", + "=ERFC(-1)": "1.84270079294971", + // ERFC.PRECISE + "=ERFC.PRECISE(0)": "1", + "=ERFC.PRECISE(0.5)": "0.479500122186953", + "=ERFC.PRECISE(-1)": "1.84270079294971", // GESTEP "=GESTEP(1.2,0.001)": "1", "=GESTEP(0.05,0.05)": "1", @@ -1578,6 +1593,20 @@ func TestCalcCellValue(t *testing.T) { "=DELTA(0,0,0)": "DELTA allows at most 2 arguments", "=DELTA(\"\",0)": "strconv.ParseFloat: parsing \"\": invalid syntax", "=DELTA(0,\"\")": "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.PRECISE + "=ERF.PRECISE()": "ERF.PRECISE requires 1 argument", + "=ERF.PRECISE(\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + // ERFC + "=ERFC()": "ERFC requires 1 argument", + "=ERFC(\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + // ERFC.PRECISE + "=ERFC.PRECISE()": "ERFC.PRECISE requires 1 argument", + "=ERFC.PRECISE(\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", // GESTEP "=GESTEP()": "GESTEP requires at least 1 argument", "=GESTEP(0,0,0)": "GESTEP allows at most 2 arguments", From 32548a6cac35caced7d016074c2de4219ad5de01 Mon Sep 17 00:00:00 2001 From: Sean Liang Date: Wed, 3 Nov 2021 15:13:46 +0800 Subject: [PATCH 477/957] return immediately when matched for efficiency (#1049) --- sheet.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/sheet.go b/sheet.go index e5ea1bdedc..dc935135b2 100644 --- a/sheet.go +++ b/sheet.go @@ -325,6 +325,7 @@ func (f *File) GetActiveSheetIndex() (index int) { for idx, sheet := range wb.Sheets.Sheet { if sheet.SheetID == sheetID { index = idx + return } } } @@ -377,6 +378,7 @@ func (f *File) GetSheetName(index int) (name string) { for idx, sheet := range f.GetSheetList() { if idx == index { name = sheet + return } } return @@ -386,13 +388,12 @@ 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(name string) int { - var ID = -1 for sheetID, sheet := range f.GetSheetMap() { if sheet == trimSheetName(name) { - ID = sheetID + return sheetID } } - return ID + return -1 } // GetSheetIndex provides a function to get a sheet index of the workbook by @@ -400,13 +401,12 @@ func (f *File) getSheetID(name string) int { // sheet name is invalid or sheet doesn't exist, it will return an integer // type value -1. func (f *File) GetSheetIndex(name string) int { - var idx = -1 for index, sheet := range f.GetSheetList() { if strings.EqualFold(sheet, trimSheetName(name)) { - idx = index + return index } } - return idx + return -1 } // GetSheetMap provides a function to get worksheets, chart sheets, dialog From e64775fdcc38a9bc882ef32b4d4d491ad63acbdd Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 4 Nov 2021 00:04:45 +0800 Subject: [PATCH 478/957] ref #65: new formula functions STANDARDIZE, STDEV.P and STDEVP --- calc.go | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 20 +++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/calc.go b/calc.go index eac0f6c109..f4f6b0c484 100644 --- a/calc.go +++ b/calc.go @@ -542,9 +542,12 @@ type formulaFuncs struct { // SMALL // SQRT // SQRTPI +// STANDARDIZE // STDEV +// STDEV.P // STDEV.S // STDEVA +// STDEVP // SUBSTITUTE // SUM // SUMIF @@ -5994,6 +5997,64 @@ func (fn *formulaFuncs) SMALL(argsList *list.List) formulaArg { return fn.kth("SMALL", argsList) } +// STANDARDIZE function returns a normalized value of a distribution that is +// characterized by a supplied mean and standard deviation. The syntax of the +// function is: +// +// STANDARDIZE(x,mean,standard_dev) +// +func (fn *formulaFuncs) STANDARDIZE(argsList *list.List) formulaArg { + if argsList.Len() != 3 { + return newErrorFormulaArg(formulaErrorVALUE, "STANDARDIZE requires 3 arguments") + } + x := argsList.Front().Value.(formulaArg).ToNumber() + if x.Type != ArgNumber { + return x + } + mean := argsList.Front().Next().Value.(formulaArg).ToNumber() + if mean.Type != ArgNumber { + return mean + } + stdDev := argsList.Back().Value.(formulaArg).ToNumber() + if stdDev.Type != ArgNumber { + return stdDev + } + if stdDev.Number <= 0 { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + } + return newNumberFormulaArg((x.Number - mean.Number) / stdDev.Number) +} + +// stdevp is an implementation of the formula functions STDEVP and STDEV.P. +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) + if varp.Type != ArgNumber { + return varp + } + return newNumberFormulaArg(math.Sqrt(varp.Number)) +} + +// STDEVP function calculates the standard deviation of a supplied set of +// values. The syntax of the function is: +// +// STDEVP(number1,[number2],...) +// +func (fn *formulaFuncs) STDEVP(argsList *list.List) formulaArg { + return fn.stdevp("STDEVP", argsList) +} + +// STDEVdotP function calculates the standard deviation of a supplied set of +// values. +// +// STDEV.P( number1, [number2], ... ) +// +func (fn *formulaFuncs) STDEVdotP(argsList *list.List) formulaArg { + return fn.stdevp("STDEV.P", 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 b2fd5f4f8e..a2ac719957 100644 --- a/calc_test.go +++ b/calc_test.go @@ -911,6 +911,14 @@ func TestCalcCellValue(t *testing.T) { "=SMALL(A1:B5,2)": "1", "=SMALL(A1,1)": "1", "=SMALL(A1:F2,1)": "1", + // STANDARDIZE + "=STANDARDIZE( 5.5, 5, 2 )": "0.25", + "=STANDARDIZE( 12, 15, 1.5 )": "-2", + "=STANDARDIZE( -2, 0, 5 )": "-0.4", + // STDEVP + "=STDEVP(A1:B2,6,-1)": "2.40947204913349", + // STDEV.P + "=STDEV.P(A1:B2,6,-1)": "2.40947204913349", // TRIMMEAN "=TRIMMEAN(A1:B4,10%)": "2.5", "=TRIMMEAN(A1:B4,70%)": "2.5", @@ -2166,6 +2174,18 @@ func TestCalcCellValue(t *testing.T) { "=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", + // 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", + // STDEVP + "=STDEVP()": "STDEVP requires at least 1 argument", + "=STDEVP(\"\")": "#DIV/0!", + // STDEV.P + "=STDEV.P()": "STDEV.P requires at least 1 argument", + "=STDEV.P(\"\")": "#DIV/0!", // TRIMMEAN "=TRIMMEAN()": "TRIMMEAN requires 2 arguments", "=TRIMMEAN(A1,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", From 60b13affbda954261888a7829c88a32993edb5b2 Mon Sep 17 00:00:00 2001 From: li Date: Fri, 5 Nov 2021 00:01:34 +0800 Subject: [PATCH 479/957] Support get current row/col and total rows/cols in the stream reader (#1054) --- col.go | 30 ++++++++++++++++++++---------- col_test.go | 26 ++++++++++++-------------- rows.go | 28 +++++++++++++++++++--------- rows_test.go | 22 ++++++++++------------ 4 files changed, 61 insertions(+), 45 deletions(-) diff --git a/col.go b/col.go index ffd49dd414..c68820121d 100644 --- a/col.go +++ b/col.go @@ -32,12 +32,22 @@ const ( // Cols defines an iterator to a sheet type Cols struct { - err error - curCol, totalCol, stashCol, totalRow int - rawCellValue bool - sheet string - f *File - sheetXML []byte + err error + curCol, totalCols, totalRows, stashCol int + rawCellValue bool + sheet string + f *File + sheetXML []byte +} + +// CurrentCol returns the column number that represents the current column. +func (cols *Cols) CurrentCol() int { + return cols.curCol +} + +// TotalCols returns the total columns count in the worksheet. +func (cols *Cols) TotalCols() int { + return cols.totalCols } // GetCols return all the columns in a sheet by given worksheet name (case @@ -71,7 +81,7 @@ func (f *File) GetCols(sheet string, opts ...Options) ([][]string, error) { // Next will return true if the next column is found. func (cols *Cols) Next() bool { cols.curCol++ - return cols.curCol <= cols.totalCol + return cols.curCol <= cols.totalCols } // Error will return an error when the error occurs. @@ -159,7 +169,7 @@ func columnXMLHandler(colIterator *columnXMLIterator, xmlElement *xml.StartEleme colIterator.row = colIterator.curRow } } - colIterator.cols.totalRow = colIterator.row + colIterator.cols.totalRows = colIterator.row colIterator.cellCol = 0 } if inElement == "c" { @@ -171,8 +181,8 @@ func columnXMLHandler(colIterator *columnXMLIterator, xmlElement *xml.StartEleme } } } - if colIterator.cellCol > colIterator.cols.totalCol { - colIterator.cols.totalCol = colIterator.cellCol + if colIterator.cellCol > colIterator.cols.totalCols { + colIterator.cols.totalCols = colIterator.cellCol } } } diff --git a/col_test.go b/col_test.go index 213c370a5a..08f0eca69a 100644 --- a/col_test.go +++ b/col_test.go @@ -59,39 +59,37 @@ func TestCols(t *testing.T) { } func TestColumnsIterator(t *testing.T) { - const ( - sheet2 = "Sheet2" - expectedNumCol = 9 - ) - + sheetName, colCount, expectedNumCol := "Sheet2", 0, 9 f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) require.NoError(t, err) - cols, err := f.Cols(sheet2) + cols, err := f.Cols(sheetName) require.NoError(t, err) - var colCount int for cols.Next() { colCount++ + assert.Equal(t, colCount, cols.CurrentCol()) + assert.Equal(t, expectedNumCol, cols.TotalCols()) require.True(t, colCount <= expectedNumCol, "colCount is greater than expected") } assert.Equal(t, expectedNumCol, colCount) assert.NoError(t, f.Close()) - f = NewFile() + f, sheetName, colCount, expectedNumCol = NewFile(), "Sheet1", 0, 4 cells := []string{"C2", "C3", "C4", "D2", "D3", "D4"} for _, cell := range cells { - assert.NoError(t, f.SetCellValue("Sheet1", cell, 1)) + assert.NoError(t, f.SetCellValue(sheetName, cell, 1)) } - cols, err = f.Cols("Sheet1") + cols, err = f.Cols(sheetName) require.NoError(t, err) - colCount = 0 for cols.Next() { colCount++ + assert.Equal(t, colCount, cols.CurrentCol()) + assert.Equal(t, expectedNumCol, cols.TotalCols()) require.True(t, colCount <= 4, "colCount is greater than expected") } - assert.Equal(t, 4, colCount) + assert.Equal(t, expectedNumCol, colCount) } func TestColsError(t *testing.T) { @@ -130,8 +128,8 @@ func TestGetColsError(t *testing.T) { f = NewFile() cols, err := f.Cols("Sheet1") assert.NoError(t, err) - cols.totalRow = 2 - cols.totalCol = 2 + cols.totalRows = 2 + cols.totalCols = 2 cols.curCol = 1 cols.sheetXML = []byte(`A`) _, err = cols.Rows() diff --git a/rows.go b/rows.go index 3171ab1491..1e20f0a1e9 100644 --- a/rows.go +++ b/rows.go @@ -67,19 +67,29 @@ func (f *File) GetRows(sheet string, opts ...Options) ([][]string, error) { // Rows defines an iterator to a sheet. type Rows struct { - err error - curRow, totalRow, stashRow int - rawCellValue bool - sheet string - f *File - tempFile *os.File - decoder *xml.Decoder + err error + curRow, totalRows, stashRow int + rawCellValue bool + sheet string + f *File + tempFile *os.File + decoder *xml.Decoder +} + +// CurrentRow returns the row number that represents the current row. +func (rows *Rows) CurrentRow() int { + return rows.curRow +} + +// TotalRows returns the total rows count in the worksheet. +func (rows *Rows) TotalRows() int { + return rows.totalRows } // Next will return true if find the next row element. func (rows *Rows) Next() bool { rows.curRow++ - return rows.curRow <= rows.totalRow + return rows.curRow <= rows.totalRows } // Error will return the error when the error occurs. @@ -255,7 +265,7 @@ func (f *File) Rows(sheet string) (*Rows, error) { } } } - rows.totalRow = row + rows.totalRows = row } case xml.EndElement: if xmlElement.Name.Local == "sheetData" { diff --git a/rows_test.go b/rows_test.go index 19ed866b60..9fee3d9e45 100644 --- a/rows_test.go +++ b/rows_test.go @@ -65,18 +65,17 @@ func TestRows(t *testing.T) { } func TestRowsIterator(t *testing.T) { - const ( - sheet2 = "Sheet2" - expectedNumRow = 11 - ) + sheetName, rowCount, expectedNumRow := "Sheet2", 0, 11 f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) require.NoError(t, err) - rows, err := f.Rows(sheet2) + rows, err := f.Rows(sheetName) require.NoError(t, err) - var rowCount int + for rows.Next() { rowCount++ + assert.Equal(t, rowCount, rows.CurrentRow()) + assert.Equal(t, expectedNumRow, rows.TotalRows()) require.True(t, rowCount <= expectedNumRow, "rowCount is greater than expected") } assert.Equal(t, expectedNumRow, rowCount) @@ -84,19 +83,18 @@ func TestRowsIterator(t *testing.T) { assert.NoError(t, f.Close()) // Valued cell sparse distribution test - f = NewFile() + f, sheetName, rowCount, expectedNumRow = NewFile(), "Sheet1", 0, 3 cells := []string{"C1", "E1", "A3", "B3", "C3", "D3", "E3"} for _, cell := range cells { - assert.NoError(t, f.SetCellValue("Sheet1", cell, 1)) + assert.NoError(t, f.SetCellValue(sheetName, cell, 1)) } - rows, err = f.Rows("Sheet1") + rows, err = f.Rows(sheetName) require.NoError(t, err) - rowCount = 0 for rows.Next() { rowCount++ - require.True(t, rowCount <= 3, "rowCount is greater than expected") + require.True(t, rowCount <= expectedNumRow, "rowCount is greater than expected") } - assert.Equal(t, 3, rowCount) + assert.Equal(t, expectedNumRow, rowCount) } func TestRowsError(t *testing.T) { From 8d21959da39eba34d04475c3549496c8dfd823da Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 5 Nov 2021 00:03:46 +0800 Subject: [PATCH 480/957] ref #65: new formula functions VAR, VARA and VARPA --- calc.go | 77 ++++++++++++++++++++++++++++++++++++++++++---------- calc_test.go | 24 +++++++++++++--- 2 files changed, 83 insertions(+), 18 deletions(-) diff --git a/calc.go b/calc.go index f4f6b0c484..657670c356 100644 --- a/calc.go +++ b/calc.go @@ -572,8 +572,11 @@ type formulaFuncs struct { // UNICODE // UPPER // VALUE +// VAR // VAR.P +// VARA // VARP +// VARPA // VLOOKUP // WEEKDAY // WEIBULL @@ -6098,43 +6101,89 @@ func (fn *formulaFuncs) TRIMMEAN(argsList *list.List) formulaArg { return fn.AVERAGE(args) } -// VARP function returns the Variance of a given set of values. The syntax of -// the function is: -// -// VARP(number1,[number2],...) -// -func (fn *formulaFuncs) VARP(argsList *list.List) formulaArg { +// vars is an implementation of the formula functions VAR, VARA, VARP, VAR.P +// and VARPA. +func (fn *formulaFuncs) vars(name string, argsList *list.List) formulaArg { if argsList.Len() < 1 { - return newErrorFormulaArg(formulaErrorVALUE, "VARP requires at least 1 argument") + return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires at least 1 argument", name)) } summerA, summerB, count := 0.0, 0.0, 0.0 + minimum := 0.0 + if name == "VAR" || name == "VARA" { + minimum = 1.0 + } for arg := argsList.Front(); arg != nil; arg = arg.Next() { for _, token := range arg.Value.(formulaArg).ToList() { - if num := token.ToNumber(); num.Type == ArgNumber { + num := token.ToNumber() + if token.Value() != "TRUE" && num.Type == ArgNumber { + summerA += (num.Number * num.Number) + summerB += num.Number + count++ + continue + } + num = token.ToBool() + if num.Type == ArgNumber { summerA += (num.Number * num.Number) summerB += num.Number count++ + continue + } + if name == "VARA" || name == "VARPA" { + count++ } } } - if count > 0 { + if count > minimum { summerA *= count summerB *= summerB - return newNumberFormulaArg((summerA - summerB) / (count * count)) + return newNumberFormulaArg((summerA - summerB) / (count * (count - minimum))) } return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV) } +// VAR function returns the sample variance of a supplied set of values. The +// syntax of the function is: +// +// VAR(number1,[number2],...) +// +func (fn *formulaFuncs) VAR(argsList *list.List) formulaArg { + return fn.vars("VAR", argsList) +} + +// VARA function calculates the sample variance of a supplied set of values. +// The syntax of the function is: +// +// VARA(number1,[number2],...) +// +func (fn *formulaFuncs) VARA(argsList *list.List) formulaArg { + return fn.vars("VARA", argsList) +} + +// VARP function returns the Variance of a given set of values. The syntax of +// the function is: +// +// VARP(number1,[number2],...) +// +func (fn *formulaFuncs) VARP(argsList *list.List) formulaArg { + return fn.vars("VARP", argsList) +} + // VARdotP function returns the Variance of a given set of values. The syntax // of the function is: // // VAR.P(number1,[number2],...) // func (fn *formulaFuncs) VARdotP(argsList *list.List) formulaArg { - if argsList.Len() < 1 { - return newErrorFormulaArg(formulaErrorVALUE, "VAR.P requires at least 1 argument") - } - return fn.VARP(argsList) + return fn.vars("VAR.P", argsList) +} + +// VARPA function returns the Variance of a given set of values. The syntax of +// the function is: +// +// VARPA(number1,[number2],...) +// +func (fn *formulaFuncs) VARPA(argsList *list.List) formulaArg { + return fn.vars("VARPA", argsList) } // WEIBULL function calculates the Weibull Probability Density Function or the diff --git a/calc_test.go b/calc_test.go index a2ac719957..46f4f08d2e 100644 --- a/calc_test.go +++ b/calc_test.go @@ -912,9 +912,9 @@ func TestCalcCellValue(t *testing.T) { "=SMALL(A1,1)": "1", "=SMALL(A1:F2,1)": "1", // STANDARDIZE - "=STANDARDIZE( 5.5, 5, 2 )": "0.25", - "=STANDARDIZE( 12, 15, 1.5 )": "-2", - "=STANDARDIZE( -2, 0, 5 )": "-0.4", + "=STANDARDIZE(5.5,5,2)": "0.25", + "=STANDARDIZE(12,15,1.5)": "-2", + "=STANDARDIZE(-2,0,5)": "-0.4", // STDEVP "=STDEVP(A1:B2,6,-1)": "2.40947204913349", // STDEV.P @@ -922,10 +922,20 @@ func TestCalcCellValue(t *testing.T) { // TRIMMEAN "=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,TRUE)": "4", + // VARA + "=VARA(1,3,5,0,C1)": "4.7", + "=VARA(1,3,5,0,C1,TRUE)": "3.86666666666667", // VARP - "=VARP(A1:A5)": "1.25", + "=VARP(A1:A5)": "1.25", + "=VARP(1,3,5,0,C1,TRUE)": "3.2", // VAR.P "=VAR.P(A1:A5)": "1.25", + // 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(2,5,1.5,TRUE)": "0.985212776817482", @@ -2191,12 +2201,18 @@ func TestCalcCellValue(t *testing.T) { "=TRIMMEAN(A1,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", "=TRIMMEAN(A1,1)": "#NUM!", "=TRIMMEAN(A1,-1)": "#NUM!", + // VAR + "=VAR()": "VAR requires at least 1 argument", + // VARA + "=VARA()": "VARA requires at least 1 argument", // VARP "=VARP()": "VARP requires at least 1 argument", "=VARP(\"\")": "#DIV/0!", // VAR.P "=VAR.P()": "VAR.P requires at least 1 argument", "=VAR.P(\"\")": "#DIV/0!", + // VARPA + "=VARPA()": "VARPA requires at least 1 argument", // WEIBULL "=WEIBULL()": "WEIBULL requires 4 arguments", "=WEIBULL(\"\",1,1,FALSE)": "#VALUE!", From 02b0fdf2c97bbdff35bc77bacf9126cb42e4f880 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 6 Nov 2021 00:11:16 +0800 Subject: [PATCH 481/957] ref #65: new formula functions PERCENTILE.EXC, QUARTILE.EXC and VAR.S --- calc.go | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++-- calc_test.go | 24 +++++++++++++++++ 2 files changed, 98 insertions(+), 2 deletions(-) diff --git a/calc.go b/calc.go index 657670c356..592f315707 100644 --- a/calc.go +++ b/calc.go @@ -495,6 +495,7 @@ type formulaFuncs struct { // ODD // OR // PDURATION +// PERCENTILE.EXC // PERCENTILE.INC // PERCENTILE // PERMUT @@ -510,6 +511,7 @@ type formulaFuncs struct { // PROPER // PV // QUARTILE +// QUARTILE.EXC // QUARTILE.INC // QUOTIENT // RADIANS @@ -574,6 +576,7 @@ type formulaFuncs struct { // VALUE // VAR // VAR.P +// VAR.S // VARA // VARP // VARPA @@ -5762,6 +5765,43 @@ func (fn *formulaFuncs) min(mina bool, argsList *list.List) formulaArg { return newNumberFormulaArg(min) } +// 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: +// +// PERCENTILE.EXC(array,k) +// +func (fn *formulaFuncs) PERCENTILEdotEXC(argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, "PERCENTILE.EXC requires 2 arguments") + } + array := argsList.Front().Value.(formulaArg).ToList() + k := argsList.Back().Value.(formulaArg).ToNumber() + if k.Type != ArgNumber { + return k + } + if k.Number <= 0 || k.Number >= 1 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + numbers := []float64{} + for _, arg := range array { + if arg.Type == ArgError { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + num := arg.ToNumber() + if num.Type == ArgNumber { + numbers = append(numbers, num.Number) + } + } + cnt := len(numbers) + sort.Float64s(numbers) + idx := k.Number * (float64(cnt) + 1) + base := math.Floor(idx) + next := base - 1 + proportion := idx - base + return newNumberFormulaArg(numbers[int(next)] + ((numbers[int(base)] - numbers[int(next)]) * proportion)) +} + // PERCENTILEdotINC 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. The syntax of the function is: @@ -5885,6 +5925,29 @@ func (fn *formulaFuncs) QUARTILE(argsList *list.List) formulaArg { return fn.PERCENTILE(args) } +// QUARTILEdotEXC function returns a requested quartile of a supplied range of +// values, based on a percentile range of 0 to 1 exclusive. The syntax of the +// function is: +// +// QUARTILE.EXC(array,quart) +// +func (fn *formulaFuncs) QUARTILEdotEXC(argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, "QUARTILE.EXC requires 2 arguments") + } + quart := argsList.Back().Value.(formulaArg).ToNumber() + if quart.Type != ArgNumber { + return quart + } + if quart.Number <= 0 || quart.Number >= 4 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + args := list.New().Init() + args.PushBack(argsList.Front().Value.(formulaArg)) + args.PushBack(newNumberFormulaArg(quart.Number / 4)) + return fn.PERCENTILEdotEXC(args) +} + // QUARTILEdotINC function returns a requested quartile of a supplied range of // values. The syntax of the function is: // @@ -6102,14 +6165,14 @@ func (fn *formulaFuncs) TRIMMEAN(argsList *list.List) formulaArg { } // vars is an implementation of the formula functions VAR, VARA, VARP, VAR.P -// and VARPA. +// VAR.S and VARPA. func (fn *formulaFuncs) vars(name string, argsList *list.List) formulaArg { if argsList.Len() < 1 { return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires at least 1 argument", name)) } summerA, summerB, count := 0.0, 0.0, 0.0 minimum := 0.0 - if name == "VAR" || name == "VARA" { + if name == "VAR" || name == "VAR.S" || name == "VARA" { minimum = 1.0 } for arg := argsList.Front(); arg != nil; arg = arg.Next() { @@ -6177,6 +6240,15 @@ func (fn *formulaFuncs) VARdotP(argsList *list.List) formulaArg { return fn.vars("VAR.P", argsList) } +// VARdotS function calculates the sample variance of a supplied set of +// values. The syntax of the function is: +// +// VAR.S(number1,[number2],...) +// +func (fn *formulaFuncs) VARdotS(argsList *list.List) formulaArg { + return fn.vars("VAR.S", argsList) +} + // VARPA function returns the Variance of a given set of values. The syntax of // the function is: // diff --git a/calc_test.go b/calc_test.go index 46f4f08d2e..eb63130fb3 100644 --- a/calc_test.go +++ b/calc_test.go @@ -878,6 +878,9 @@ func TestCalcCellValue(t *testing.T) { "=MINA(MUNIT(2))": "0", "=MINA(INT(1))": "1", "=MINA(A1:B4,MUNIT(1),INT(0),1,E1:F2,\"\")": "0", + // PERCENTILE.EXC + "=PERCENTILE.EXC(A1:A4,0.2)": "0", + "=PERCENTILE.EXC(A1:A4,0.6)": "2", // PERCENTILE.INC "=PERCENTILE.INC(A1:A4,0.2)": "0.6", // PERCENTILE @@ -892,6 +895,10 @@ func TestCalcCellValue(t *testing.T) { "=PERMUTATIONA(7,6)": "117649", // QUARTILE "=QUARTILE(A1:A4,2)": "1.5", + // QUARTILE.EXC + "=QUARTILE.EXC(A1:A4,1)": "0.25", + "=QUARTILE.EXC(A1:A4,2)": "1.5", + "=QUARTILE.EXC(A1:A4,3)": "2.75", // QUARTILE.INC "=QUARTILE.INC(A1:A4,0)": "0", // RANK @@ -933,6 +940,9 @@ func TestCalcCellValue(t *testing.T) { "=VARP(1,3,5,0,C1,TRUE)": "3.2", // 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,TRUE)": "4", // VARPA "=VARPA(1,3,5,0,C1)": "3.76", "=VARPA(1,3,5,0,C1,TRUE)": "3.22222222222222", @@ -2138,6 +2148,13 @@ func TestCalcCellValue(t *testing.T) { // MINA "=MINA()": "MINA requires at least 1 argument", "=MINA(NA())": "#N/A", + // 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.INC "=PERCENTILE.INC()": "PERCENTILE.INC requires 2 arguments", // PERCENTILE @@ -2161,6 +2178,11 @@ func TestCalcCellValue(t *testing.T) { "=QUARTILE(A1:A4,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", "=QUARTILE(A1:A4,-1)": "#NUM!", "=QUARTILE(A1:A4,5)": "#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.INC "=QUARTILE.INC()": "QUARTILE.INC requires 2 arguments", // RANK @@ -2211,6 +2233,8 @@ func TestCalcCellValue(t *testing.T) { // VAR.P "=VAR.P()": "VAR.P requires at least 1 argument", "=VAR.P(\"\")": "#DIV/0!", + // VAR.S + "=VAR.S()": "VAR.S requires at least 1 argument", // VARPA "=VARPA()": "VARPA requires at least 1 argument", // WEIBULL From 1df76b583c1b77fb37f14d30a54ff5f356280f60 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 7 Nov 2021 00:14:39 +0800 Subject: [PATCH 482/957] ref #65: new formula functions COUPPCD and PRICEMAT --- calc.go | 125 +++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 29 ++++++++++++ 2 files changed, 154 insertions(+) diff --git a/calc.go b/calc.go index 592f315707..dc57cd53f8 100644 --- a/calc.go +++ b/calc.go @@ -350,6 +350,7 @@ type formulaFuncs struct { // COUNT // COUNTA // COUNTBLANK +// COUPPCD // CSC // CSCH // CUMIPMT @@ -507,6 +508,7 @@ type formulaFuncs struct { // POWER // PPMT // PRICEDISC +// PRICEMAT // PRODUCT // PROPER // PV @@ -9504,6 +9506,67 @@ func (fn *formulaFuncs) AMORLINC(argsList *list.List) formulaArg { return newNumberFormulaArg(0) } +// prepareCouponArgs checking and prepare arguments for the formula functions +// COUPDAYBS, COUPDAYS, COUPDAYSNC, COUPPCD, COUPNUM and COUPNCD. +func (fn *formulaFuncs) prepareCouponArgs(name string, argsList *list.List) formulaArg { + if argsList.Len() != 3 && argsList.Len() != 4 { + return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires 3 or 4 arguments", name)) + } + args := list.New().Init() + args.PushBack(argsList.Front().Value.(formulaArg)) + settlement := fn.DATEVALUE(args) + if settlement.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + args.Init() + args.PushBack(argsList.Front().Next().Value.(formulaArg)) + maturity := fn.DATEVALUE(args) + if maturity.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + if settlement.Number >= maturity.Number { + return newErrorFormulaArg(formulaErrorNUM, fmt.Sprintf("%s requires maturity > settlement", name)) + } + frequency := argsList.Front().Next().Next().Value.(formulaArg).ToNumber() + if frequency.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + if !validateFrequency(frequency.Number) { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + basis := newNumberFormulaArg(0) + if argsList.Len() == 4 { + if basis = argsList.Back().Value.(formulaArg).ToNumber(); basis.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + } + return newListFormulaArg([]formulaArg{settlement, maturity, frequency, basis}) +} + +// 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]) +// +func (fn *formulaFuncs) COUPPCD(argsList *list.List) formulaArg { + args := fn.prepareCouponArgs("COUPPCD", argsList) + if args.Type != ArgList { + return args + } + settlement := timeFromExcelTime(args.List[0].Number, false) + maturity := timeFromExcelTime(args.List[1].Number, false) + date, years := maturity, settlement.Year()-maturity.Year() + date = date.AddDate(years, 0, 0) + if settlement.After(date) { + date = date.AddDate(1, 0, 0) + } + month := -12 / args.List[2].Number + for date.After(settlement) { + date = date.AddDate(0, int(month), 0) + } + return newNumberFormulaArg(daysBetween(excelMinTime1900.Unix(), makeDate(date.Year(), date.Month(), date.Day())) + 1) +} + // CUMIPMT function calculates the cumulative interest paid on a loan or // investment, between two specified periods. The syntax of the function is: // @@ -10388,6 +10451,68 @@ func (fn *formulaFuncs) PRICEDISC(argsList *list.List) formulaArg { return newNumberFormulaArg(redemption.Number * (1 - discount.Number*frac.Number)) } +// 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]) +// +func (fn *formulaFuncs) PRICEMAT(argsList *list.List) formulaArg { + if argsList.Len() != 5 && argsList.Len() != 6 { + return newErrorFormulaArg(formulaErrorVALUE, "PRICEMAT requires 5 or 6 arguments") + } + args := list.New().Init() + args.PushBack(argsList.Front().Value.(formulaArg)) + settlement := fn.DATEVALUE(args) + if settlement.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + args.Init() + args.PushBack(argsList.Front().Next().Value.(formulaArg)) + maturity := fn.DATEVALUE(args) + if maturity.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + if settlement.Number >= maturity.Number { + return newErrorFormulaArg(formulaErrorNUM, "PRICEMAT requires maturity > settlement") + } + args.Init() + args.PushBack(argsList.Front().Next().Next().Value.(formulaArg)) + issue := fn.DATEVALUE(args) + if issue.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + if issue.Number >= settlement.Number { + return newErrorFormulaArg(formulaErrorNUM, "PRICEMAT requires settlement > issue") + } + rate := argsList.Front().Next().Next().Next().Value.(formulaArg).ToNumber() + if rate.Type != ArgNumber { + return rate + } + if rate.Number < 0 { + return newErrorFormulaArg(formulaErrorNUM, "PRICEMAT requires rate >= 0") + } + yld := argsList.Front().Next().Next().Next().Next().Value.(formulaArg).ToNumber() + if yld.Type != ArgNumber { + return yld + } + if yld.Number < 0 { + return newErrorFormulaArg(formulaErrorNUM, "PRICEMAT requires yld >= 0") + } + basis := newNumberFormulaArg(0) + if argsList.Len() == 6 { + if basis = argsList.Back().Value.(formulaArg).ToNumber(); basis.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + } + dsm := yearFrac(settlement.Number, maturity.Number, int(basis.Number)) + if dsm.Type != ArgNumber { + return dsm + } + dis := yearFrac(issue.Number, settlement.Number, int(basis.Number)) + dim := yearFrac(issue.Number, maturity.Number, int(basis.Number)) + return newNumberFormulaArg(((1+dim.Number*rate.Number)/(1+dsm.Number*yld.Number) - dis.Number*rate.Number) * 100) +} + // PV function calculates the Present Value of an investment, based on a // series of future payments. The syntax of the function is: // diff --git a/calc_test.go b/calc_test.go index eb63130fb3..3e36ef82bc 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1393,6 +1393,10 @@ func TestCalcCellValue(t *testing.T) { "=AMORLINC(150,\"01/01/2015\",\"09/30/2015\",20,20,15%,4)": "0", "=AMORLINC(150,\"01/01/2015\",\"09/30/2015\",20,6,15%,4)": "0.6875", "=AMORLINC(150,\"01/01/2015\",\"09/30/2015\",20,0,15%,4)": "16.8125", + // COUPPCD + "=COUPPCD(\"01/01/2011\",\"10/25/2012\",4)": "40476", + "=COUPPCD(\"01/01/2011\",\"10/25/2012\",4,0)": "40476", + "=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", @@ -1456,6 +1460,9 @@ 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", + // 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", // PV "=PV(0,60,1000)": "-60000", "=PV(5%/12,60,1000)": "-52990.70632392748", @@ -2682,6 +2689,15 @@ func TestCalcCellValue(t *testing.T) { "=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", + // 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", // CUMIPMT "=CUMIPMT()": "CUMIPMT requires 6 arguments", "=CUMIPMT(0,0,0,0,0,2)": "#N/A", @@ -2850,6 +2866,19 @@ func TestCalcCellValue(t *testing.T) { "=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", + // 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", // PV "=PV()": "PV requires at least 3 arguments", "=PV(10%/4,16,2000,0,1,0)": "PV allows at most 5 arguments", From 8f82d8b02909ca96a9c7f7c3431d1ae990c90191 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 8 Nov 2021 00:19:28 +0800 Subject: [PATCH 483/957] ref #65: new formula functions COUPNCD and COUPNUM --- calc.go | 42 ++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 25 +++++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/calc.go b/calc.go index dc57cd53f8..580ecfb756 100644 --- a/calc.go +++ b/calc.go @@ -350,6 +350,8 @@ type formulaFuncs struct { // COUNT // COUNTA // COUNTBLANK +// COUPNCD +// COUPNUM // COUPPCD // CSC // CSCH @@ -9543,6 +9545,46 @@ func (fn *formulaFuncs) prepareCouponArgs(name string, argsList *list.List) form return newListFormulaArg([]formulaArg{settlement, maturity, frequency, basis}) } +// COUPNCD function calculates the number of coupons payable, between a +// 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]) +// +func (fn *formulaFuncs) COUPNCD(argsList *list.List) formulaArg { + args := fn.prepareCouponArgs("COUPNCD", argsList) + if args.Type != ArgList { + return args + } + settlement := timeFromExcelTime(args.List[0].Number, false) + maturity := timeFromExcelTime(args.List[1].Number, false) + ncd := time.Date(settlement.Year(), maturity.Month(), maturity.Day(), 0, 0, 0, 0, time.UTC) + if ncd.After(settlement) { + ncd = ncd.AddDate(-1, 0, 0) + } + for !ncd.After(settlement) { + ncd = ncd.AddDate(0, 12/int(args.List[2].Number), 0) + } + return newNumberFormulaArg(daysBetween(excelMinTime1900.Unix(), makeDate(ncd.Year(), ncd.Month(), ncd.Day())) + 1) +} + +// COUPNUM function calculates the number of coupons payable, between a +// 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]) +// +func (fn *formulaFuncs) COUPNUM(argsList *list.List) formulaArg { + args := fn.prepareCouponArgs("COUPNUM", argsList) + if args.Type != ArgList { + return args + } + maturity, dateValue := timeFromExcelTime(args.List[1].Number, false), fn.COUPPCD(argsList) + date := timeFromExcelTime(dateValue.Number, false) + months := (maturity.Year()-date.Year())*12 + int(maturity.Month()) - int(date.Month()) + return newNumberFormulaArg(float64(months) * args.List[2].Number / 12.0) +} + // COUPPCD function returns the previous coupon date, before the settlement // date for a security. The syntax of the function is: // diff --git a/calc_test.go b/calc_test.go index 3e36ef82bc..26c1ca18e7 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1393,6 +1393,13 @@ func TestCalcCellValue(t *testing.T) { "=AMORLINC(150,\"01/01/2015\",\"09/30/2015\",20,20,15%,4)": "0", "=AMORLINC(150,\"01/01/2015\",\"09/30/2015\",20,6,15%,4)": "0.6875", "=AMORLINC(150,\"01/01/2015\",\"09/30/2015\",20,0,15%,4)": "16.8125", + // COUPNCD + "=COUPNCD(\"01/01/2011\",\"10/25/2012\",4)": "40568", + "=COUPNCD(\"01/01/2011\",\"10/25/2012\",4,0)": "40568", + "=COUPNCD(\"10/25/2011\",\"01/01/2012\",4)": "40909", + // COUPNUM + "=COUPNUM(\"01/01/2011\",\"10/25/2012\",4)": "8", + "=COUPNUM(\"01/01/2011\",\"10/25/2012\",4,0)": "8", // COUPPCD "=COUPPCD(\"01/01/2011\",\"10/25/2012\",4)": "40476", "=COUPPCD(\"01/01/2011\",\"10/25/2012\",4,0)": "40476", @@ -2689,6 +2696,24 @@ func TestCalcCellValue(t *testing.T) { "=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", + // 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", + // 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", // COUPPCD "=COUPPCD()": "COUPPCD requires 3 or 4 arguments", "=COUPPCD(\"01/01/2011\",\"10/25/2012\",4,0,0)": "COUPPCD requires 3 or 4 arguments", From a68bc34b0c60143bc649415fd7ff7acca70d6bdf Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 9 Nov 2021 00:10:09 +0800 Subject: [PATCH 484/957] ref #65: new formula functions PERCENTRANK.EXC, PERCENTRANK.INC and PERCENTRANK --- calc.go | 91 +++++++++++++++++++++++++++++++++++++++++++++++++--- calc_test.go | 39 ++++++++++++++++++++++ lib.go | 11 +++++++ 3 files changed, 137 insertions(+), 4 deletions(-) diff --git a/calc.go b/calc.go index 580ecfb756..a58a96df37 100644 --- a/calc.go +++ b/calc.go @@ -501,6 +501,9 @@ type formulaFuncs struct { // PERCENTILE.EXC // PERCENTILE.INC // PERCENTILE +// PERCENTRANK.EXC +// PERCENTRANK.INC +// PERCENTRANK // PERMUT // PERMUTATIONA // PI @@ -5859,6 +5862,88 @@ func (fn *formulaFuncs) PERCENTILE(argsList *list.List) formulaArg { return newNumberFormulaArg(numbers[int(base)] + ((numbers[int(next)] - numbers[int(base)]) * proportion)) } +// percentrank is an implementation of the formula functions PERCENTRANK and +// PERCENTRANK.INC. +func (fn *formulaFuncs) percentrank(name string, argsList *list.List) formulaArg { + if argsList.Len() != 2 && argsList.Len() != 3 { + return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires 2 or 3 arguments", name)) + } + array := argsList.Front().Value.(formulaArg).ToList() + x := argsList.Front().Next().Value.(formulaArg).ToNumber() + if x.Type != ArgNumber { + return x + } + numbers := []float64{} + for _, arg := range array { + if arg.Type == ArgError { + return arg + } + num := arg.ToNumber() + if num.Type == ArgNumber { + numbers = append(numbers, num.Number) + } + } + cnt := len(numbers) + sort.Float64s(numbers) + if x.Number < numbers[0] || x.Number > numbers[cnt-1] { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + } + pos, significance := float64(inFloat64Slice(numbers, x.Number)), newNumberFormulaArg(3) + if argsList.Len() == 3 { + if significance = argsList.Back().Value.(formulaArg).ToNumber(); significance.Type != ArgNumber { + return significance + } + if significance.Number < 1 { + return newErrorFormulaArg(formulaErrorNUM, fmt.Sprintf("%s arguments significance should be > 1", name)) + } + } + if pos == -1 { + pos = 0 + cmp := numbers[0] + for cmp < x.Number { + pos++ + cmp = numbers[int(pos)] + } + 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) + if name == "PERCENTRANK.EXC" { + digit = pow * float64(pos+1) / (float64(cnt) + 1) + } + return newNumberFormulaArg(math.Floor(digit) / pow) +} + +// PERCENTRANKdotEXC function calculates the relative position, between 0 and +// 1 (exclusive), of a specified value within a supplied array. The syntax of +// the function is: +// +// PERCENTRANK.EXC(array,x,[significance]) +// +func (fn *formulaFuncs) PERCENTRANKdotEXC(argsList *list.List) formulaArg { + return fn.percentrank("PERCENTRANK.EXC", argsList) +} + +// PERCENTRANKdotINC function calculates the relative position, between 0 and +// 1 (inclusive), of a specified value within a supplied array.The syntax of +// the function is: +// +// PERCENTRANK.INC(array,x,[significance]) +// +func (fn *formulaFuncs) PERCENTRANKdotINC(argsList *list.List) formulaArg { + return fn.percentrank("PERCENTRANK.INC", argsList) +} + +// 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]) +// +func (fn *formulaFuncs) PERCENTRANK(argsList *list.List) formulaArg { + return fn.percentrank("PERCENTRANK", argsList) +} + // PERMUT function calculates the number of permutations of a specified number // of objects from a set of objects. The syntax of the function is: // @@ -5993,10 +6078,8 @@ func (fn *formulaFuncs) rank(name string, argsList *list.List) formulaArg { if order.Number == 0 { sort.Sort(sort.Reverse(sort.Float64Slice(arr))) } - for idx, n := range arr { - if num.Number == n { - return newNumberFormulaArg(float64(idx + 1)) - } + if idx := inFloat64Slice(arr, num.Number); idx != -1 { + return newNumberFormulaArg(float64(idx + 1)) } return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) } diff --git a/calc_test.go b/calc_test.go index 26c1ca18e7..c621f3b620 100644 --- a/calc_test.go +++ b/calc_test.go @@ -886,6 +886,24 @@ func TestCalcCellValue(t *testing.T) { // PERCENTILE "=PERCENTILE(A1:A4,0.2)": "0.6", "=PERCENTILE(0,0)": "0", + // PERCENTRANK.EXC + "=PERCENTRANK.EXC(A1:B4,0)": "0.142", + "=PERCENTRANK.EXC(A1:B4,2)": "0.428", + "=PERCENTRANK.EXC(A1:B4,2.5)": "0.5", + "=PERCENTRANK.EXC(A1:B4,2.6,1)": "0.5", + "=PERCENTRANK.EXC(A1:B4,5)": "0.857", + // PERCENTRANK.INC + "=PERCENTRANK.INC(A1:B4,0)": "0", + "=PERCENTRANK.INC(A1:B4,2)": "0.4", + "=PERCENTRANK.INC(A1:B4,2.5)": "0.5", + "=PERCENTRANK.INC(A1:B4,2.6,1)": "0.5", + "=PERCENTRANK.INC(A1:B4,5)": "1", + // PERCENTRANK + "=PERCENTRANK(A1:B4,0)": "0", + "=PERCENTRANK(A1:B4,2)": "0.4", + "=PERCENTRANK(A1:B4,2.5)": "0.5", + "=PERCENTRANK(A1:B4,2.6,1)": "0.5", + "=PERCENTRANK(A1:B4,5)": "1", // PERMUT "=PERMUT(6,6)": "720", "=PERMUT(7,6)": "5040", @@ -2176,6 +2194,27 @@ func TestCalcCellValue(t *testing.T) { "=PERCENTILE(0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", "=PERCENTILE(0,-1)": "#N/A", "=PERCENTILE(NA(),1)": "#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.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 + "=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", // PERMUT "=PERMUT()": "PERMUT requires 2 numeric arguments", "=PERMUT(\"\",0)": "strconv.ParseFloat: parsing \"\": invalid syntax", diff --git a/lib.go b/lib.go index c8e957c92a..f592fbe901 100644 --- a/lib.go +++ b/lib.go @@ -381,6 +381,17 @@ func inStrSlice(a []string, x string) int { return -1 } +// inFloat64Slice provides a method to check if an element is present in an +// float64 array, and return the index of its location, otherwise return -1. +func inFloat64Slice(a []float64, x float64) int { + for idx, n := range a { + if x == n { + return idx + } + } + return -1 +} + // boolPtr returns a pointer to a bool with the given value. func boolPtr(b bool) *bool { return &b } From 00eece4f53f034a8dff009330ca45f1012e64ee3 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 11 Nov 2021 00:43:42 +0800 Subject: [PATCH 485/957] ref #65: new formula function XNPV --- calc.go | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 40 +++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) diff --git a/calc.go b/calc.go index a58a96df37..225f77bf55 100644 --- a/calc.go +++ b/calc.go @@ -591,6 +591,7 @@ type formulaFuncs struct { // WEEKDAY // WEIBULL // WEIBULL.DIST +// XNPV // XOR // YEAR // YEARFRAC @@ -10984,6 +10985,71 @@ func (fn *formulaFuncs) TBILLYIELD(argsList *list.List) formulaArg { return newNumberFormulaArg(((100 - pr.Number) / pr.Number) * (360 / dsm)) } +// prepareXArgs prepare arguments for the formula function XIRR and XNPV. +func (fn *formulaFuncs) prepareXArgs(name string, 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) + continue + } + err = newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + return + } + if len(valuesArg) < 2 { + err = newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + return + } + args, date := list.New(), 0.0 + for _, arg := range dates.ToList() { + args.Init() + args.PushBack(arg) + dateValue := fn.DATEVALUE(args) + if dateValue.Type != ArgNumber { + err = newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + return + } + if dateValue.Number < date { + err = newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + return + } + datesArg = append(datesArg, dateValue.Number) + date = dateValue.Number + } + if len(valuesArg) != len(datesArg) { + err = newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + return + } + err = newEmptyFormulaArg() + return +} + +// 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) +// +func (fn *formulaFuncs) XNPV(argsList *list.List) formulaArg { + if argsList.Len() != 3 { + return newErrorFormulaArg(formulaErrorVALUE, "XNPV requires 3 arguments") + } + rate := argsList.Front().Value.(formulaArg).ToNumber() + if rate.Type != ArgNumber { + return rate + } + 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)) + if err.Type != ArgEmpty { + return err + } + date1, xnpv := dates[0], 0.0 + for idx, value := range values { + xnpv += value / math.Pow(1+rate.Number, (dates[idx]-date1)/365) + } + return newNumberFormulaArg(xnpv) +} + // YIELDDISC function calculates the annual yield of a discounted security. // The syntax of the function is: // diff --git a/calc_test.go b/calc_test.go index c621f3b620..6420715c5e 100644 --- a/calc_test.go +++ b/calc_test.go @@ -3391,6 +3391,46 @@ func TestCalcMIRR(t *testing.T) { } } +func TestCalcXNPV(t *testing.T) { + cellData := [][]interface{}{{nil, 0.05}, + {"01/01/2016", -10000, nil}, + {"02/01/2016", 2000}, + {"05/01/2016", 2400}, + {"07/01/2016", 2900}, + {"11/01/2016", 3500}, + {"01/01/2017", 4100}, + {}, + {"02/01/2016"}, + {"01/01/2016"}} + f := prepareCalcData(cellData) + formulaList := map[string]string{ + "=XNPV(B1,B2:B7,A2:A7)": "4447.938009440515", + } + 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{ + "=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!", + } + 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 TestCalcMATCH(t *testing.T) { f := NewFile() for cell, row := range map[string][]interface{}{ From 5de671f8bb8a4d8951d5511c3bd1745fc46ce842 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 12 Nov 2021 01:01:50 +0800 Subject: [PATCH 486/957] ref #65: new formula function MINUTE --- calc.go | 43 +++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 11 +++++++++++ 2 files changed, 54 insertions(+) diff --git a/calc.go b/calc.go index 225f77bf55..8d3369586f 100644 --- a/calc.go +++ b/calc.go @@ -471,6 +471,7 @@ type formulaFuncs struct { // MIDB // MIN // MINA +// MINUTE // MIRR // MOD // MONTH @@ -7112,6 +7113,17 @@ func isDateOnlyFmt(dateString string) bool { return false } +// 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 { + return true + } + } + return false +} + // strToTimePatternHandler1 parse and convert the given string in pattern // hh to the time. func strToTimePatternHandler1(submatch []string) (h, m int, s float64, err error) { @@ -7461,6 +7473,37 @@ func (fn *formulaFuncs) ISOWEEKNUM(argsList *list.List) formulaArg { return newNumberFormulaArg(float64(weeknum)) } +// MINUTE function returns an integer representing the minute component of a +// supplied Excel time. The syntax of the function is: +// +// MINUTE(serial_number) +// +func (fn *formulaFuncs) MINUTE(argsList *list.List) formulaArg { + if argsList.Len() != 1 { + return newErrorFormulaArg(formulaErrorVALUE, "MINUTE requires exactly 1 argument") + } + date := argsList.Front().Value.(formulaArg) + num := date.ToNumber() + if num.Type != ArgNumber { + timeString := strings.ToLower(date.Value()) + if !isTimeOnlyFmt(timeString) { + _, _, _, _, err := strToDate(timeString) + if err.Type == ArgError { + return err + } + } + _, m, _, _, _, err := strToTime(timeString) + if err.Type == ArgError { + return err + } + return newNumberFormulaArg(float64(m)) + } + if num.Number < 0 { + return newErrorFormulaArg(formulaErrorNUM, "MINUTE only accepts positive argument") + } + return newNumberFormulaArg(float64(timeFromExcelTime(num.Number, false).Minute())) +} + // MONTH function returns the month of a date represented by a serial number. // The month is given as an integer, ranging from 1 (January) to 12 // (December). The syntax of the function is: diff --git a/calc_test.go b/calc_test.go index 6420715c5e..90138a1d8a 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1105,6 +1105,12 @@ func TestCalcCellValue(t *testing.T) { "=ISOWEEKNUM(\"42370\")": "53", "=ISOWEEKNUM(\"01/01/2005\")": "53", "=ISOWEEKNUM(\"02/02/2005\")": "5", + // MINUTE + "=MINUTE(1)": "0", + "=MINUTE(0.04)": "57", + "=MINUTE(\"0.04\")": "57", + "=MINUTE(\"13:35:55\")": "35", + "=MINUTE(\"12/09/2015 08:55\")": "55", // MONTH "=MONTH(42171)": "6", "=MONTH(\"31-May-2015\")": "5", @@ -2438,6 +2444,11 @@ func TestCalcCellValue(t *testing.T) { "=ISOWEEKNUM(\"\")": "#VALUE!", "=ISOWEEKNUM(\"January 25, 100\")": "#VALUE!", "=ISOWEEKNUM(-1)": "#NUM!", + // MINUTE + "=MINUTE()": "MINUTE requires exactly 1 argument", + "=MINUTE(-1)": "MINUTE only accepts positive argument", + "=MINUTE(\"\")": "#VALUE!", + "=MINUTE(\"13:60:55\")": "#VALUE!", // MONTH "=MONTH()": "MONTH requires exactly 1 argument", "=MONTH(0,0)": "MONTH requires exactly 1 argument", From adecf447e15244207af5dfb7177447d278db7526 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 13 Nov 2021 14:11:16 +0800 Subject: [PATCH 487/957] This closes #1059, represent boolean in XML as 0/1 rather than true/false --- lib.go | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++++- lib_test.go | 31 ++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/lib.go b/lib.go index f592fbe901..0710a7a50a 100644 --- a/lib.go +++ b/lib.go @@ -16,6 +16,7 @@ import ( "bytes" "container/list" "encoding/xml" + "errors" "fmt" "io" "io/ioutil" @@ -398,7 +399,7 @@ func boolPtr(b bool) *bool { return &b } // intPtr returns a pointer to a int with the given value. func intPtr(i int) *int { return &i } -// float64Ptr returns a pofloat64er to a float64 with the given value. +// 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. @@ -412,6 +413,66 @@ func defaultTrue(b *bool) bool { return *b } +// MarshalXMLMarshalXML convert the boolean data type to literal values 0 or 1 +// on serialization. +func (avb attrValBool) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + attr := xml.Attr{ + Name: xml.Name{ + Space: start.Name.Space, + Local: "val", + }, + Value: "0", + } + if avb.Val != nil { + if *avb.Val { + attr.Value = "1" + } else { + attr.Value = "0" + } + } + start.Attr = []xml.Attr{attr} + e.EncodeToken(start) + e.EncodeToken(start.End()) + return nil +} + +// UnmarshalXML convert the literal values true, false, 1, 0 of the XML +// attribute to boolean data type on de-serialization. +func (avb *attrValBool) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + for { + t, err := d.Token() + if err != nil { + return err + } + found := false + switch t.(type) { + case xml.StartElement: + return errors.New("unexpected child of attrValBool") + case xml.EndElement: + found = true + } + if found { + break + } + } + for _, attr := range start.Attr { + if attr.Name.Local == "val" { + if attr.Value == "" { + val := true + avb.Val = &val + } else { + val, err := strconv.ParseBool(attr.Value) + if err != nil { + return err + } + avb.Val = &val + } + return nil + } + } + return nil +} + // parseFormatSet provides a method to convert format string to []byte and // handle empty string. func parseFormatSet(formatSet string) []byte { diff --git a/lib_test.go b/lib_test.go index 84a52bb1c4..35dd2a013f 100644 --- a/lib_test.go +++ b/lib_test.go @@ -5,6 +5,7 @@ import ( "bytes" "encoding/xml" "fmt" + "io" "os" "strconv" "strings" @@ -237,6 +238,36 @@ func TestInStrSlice(t *testing.T) { assert.EqualValues(t, -1, inStrSlice([]string{}, "")) } +func TestBoolValMarshal(t *testing.T) { + bold := true + node := &xlsxFont{B: &attrValBool{Val: &bold}} + data, err := xml.Marshal(node) + assert.NoError(t, err) + assert.Equal(t, ``, string(data)) + + node = &xlsxFont{} + err = xml.Unmarshal(data, node) + assert.NoError(t, err) + assert.NotEqual(t, nil, node) + assert.NotEqual(t, nil, node.B) + assert.NotEqual(t, nil, node.B.Val) + assert.Equal(t, true, *node.B.Val) +} + +func TestBoolValUnmarshalXML(t *testing.T) { + node := xlsxFont{} + assert.NoError(t, xml.Unmarshal([]byte(""), &node)) + assert.Equal(t, true, *node.B.Val) + for content, err := range map[string]string{ + "": "unexpected child of attrValBool", + "": "strconv.ParseBool: parsing \"x\": invalid syntax", + } { + assert.EqualError(t, xml.Unmarshal([]byte(content), &node), err) + } + attr := attrValBool{} + assert.EqualError(t, attr.UnmarshalXML(xml.NewDecoder(strings.NewReader("")), xml.StartElement{}), io.EOF.Error()) +} + func TestBytesReplace(t *testing.T) { s := []byte{0x01} assert.EqualValues(t, s, bytesReplace(s, []byte{}, []byte{}, 0)) From 57275db22e3a7f9dce51556f7579b704b8033dcb Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 14 Nov 2021 00:17:31 +0800 Subject: [PATCH 488/957] This closes #1057, merge column styles to reduce spreadsheet size --- col_test.go | 4 ++-- lib.go | 6 +++--- sheet.go | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/col_test.go b/col_test.go index 08f0eca69a..80fc676708 100644 --- a/col_test.go +++ b/col_test.go @@ -243,9 +243,9 @@ func TestOutlineLevel(t *testing.T) { assert.Equal(t, uint8(4), level) assert.NoError(t, err) - level, err = f.GetColOutlineLevel("Shee2", "A") + level, err = f.GetColOutlineLevel("SheetN", "A") assert.Equal(t, uint8(0), level) - assert.EqualError(t, err, "sheet Shee2 is not exist") + assert.EqualError(t, err, "sheet SheetN is not exist") assert.NoError(t, f.SetColWidth("Sheet2", "A", "D", 13)) assert.EqualError(t, f.SetColWidth("Sheet2", "A", "D", MaxColumnWidth+1), ErrColumnWidth.Error()) diff --git a/lib.go b/lib.go index 0710a7a50a..535161a29d 100644 --- a/lib.go +++ b/lib.go @@ -413,8 +413,8 @@ func defaultTrue(b *bool) bool { return *b } -// MarshalXMLMarshalXML convert the boolean data type to literal values 0 or 1 -// on serialization. +// 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 { attr := xml.Attr{ Name: xml.Name{ @@ -437,7 +437,7 @@ func (avb attrValBool) MarshalXML(e *xml.Encoder, start xml.StartElement) error } // UnmarshalXML convert the literal values true, false, 1, 0 of the XML -// attribute to boolean data type on de-serialization. +// attribute to boolean data type on deserialization. func (avb *attrValBool) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { for { t, err := d.Token() diff --git a/sheet.go b/sheet.go index dc935135b2..43e277aa8c 100644 --- a/sheet.go +++ b/sheet.go @@ -24,6 +24,7 @@ import ( "path/filepath" "reflect" "regexp" + "sort" "strconv" "strings" "unicode/utf8" @@ -149,6 +150,37 @@ func (f *File) workBookWriter() { } } +// mergeExpandedCols merge expanded columns. +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{} + for i, n := 0, len(ws.Cols.Col); i < n; { + left := i + for i++; i < n && reflect.DeepEqual( + xlsxCol{ + BestFit: ws.Cols.Col[i-1].BestFit, + Collapsed: ws.Cols.Col[i-1].Collapsed, + CustomWidth: ws.Cols.Col[i-1].CustomWidth, + Hidden: ws.Cols.Col[i-1].Hidden, + Max: ws.Cols.Col[i-1].Max + 1, + Min: ws.Cols.Col[i-1].Min + 1, + OutlineLevel: ws.Cols.Col[i-1].OutlineLevel, + Phonetic: ws.Cols.Col[i-1].Phonetic, + Style: ws.Cols.Col[i-1].Style, + Width: ws.Cols.Col[i-1].Width, + }, ws.Cols.Col[i]); i++ { + } + column := deepcopy.Copy(ws.Cols.Col[left]).(xlsxCol) + if left < i-1 { + column.Max = ws.Cols.Col[i-1].Min + } + columns = append(columns, column) + } + ws.Cols.Col = columns +} + // workSheetWriter provides a function to save xl/worksheets/sheet%d.xml after // serialize structure. func (f *File) workSheetWriter() { @@ -161,6 +193,9 @@ func (f *File) workSheetWriter() { if sheet.MergeCells != nil && len(sheet.MergeCells.Cells) > 0 { _ = f.mergeOverlapCells(sheet) } + 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) } From 72410361b07e7539037252467a38a73b32986dce Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 15 Nov 2021 00:28:10 +0800 Subject: [PATCH 489/957] ref #65, new formula functions: COUPDAYBS, COUPDAYS and COUPDAYSNC --- calc.go | 143 ++++++++++++++++++++++++++++++++++++++++++++++++--- calc_test.go | 33 ++++++++++++ 2 files changed, 169 insertions(+), 7 deletions(-) diff --git a/calc.go b/calc.go index 8d3369586f..badd30e689 100644 --- a/calc.go +++ b/calc.go @@ -350,6 +350,9 @@ type formulaFuncs struct { // COUNT // COUNTA // COUNTBLANK +// COUPDAYBS +// COUPDAYS +// COUPDAYSNC // COUPNCD // COUPNUM // COUPPCD @@ -9642,16 +9645,22 @@ func (fn *formulaFuncs) prepareCouponArgs(name string, argsList *list.List) form return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires 3 or 4 arguments", name)) } args := list.New().Init() - args.PushBack(argsList.Front().Value.(formulaArg)) - settlement := fn.DATEVALUE(args) + settlement := argsList.Front().Value.(formulaArg).ToNumber() if settlement.Type != ArgNumber { - return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + args.PushBack(argsList.Front().Value.(formulaArg)) + settlement = fn.DATEVALUE(args) + if settlement.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } } - args.Init() - args.PushBack(argsList.Front().Next().Value.(formulaArg)) - maturity := fn.DATEVALUE(args) + maturity := argsList.Front().Next().Value.(formulaArg).ToNumber() if maturity.Type != ArgNumber { - return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + args.Init() + args.PushBack(argsList.Front().Next().Value.(formulaArg)) + maturity = fn.DATEVALUE(args) + if maturity.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } } if settlement.Number >= maturity.Number { return newErrorFormulaArg(formulaErrorNUM, fmt.Sprintf("%s requires maturity > settlement", name)) @@ -9672,6 +9681,126 @@ func (fn *formulaFuncs) prepareCouponArgs(name string, argsList *list.List) form return newListFormulaArg([]formulaArg{settlement, maturity, frequency, basis}) } +// is30BasisMethod determine if the financial day count basis rules is 30/360 +// methods. +func is30BasisMethod(basis int) bool { + return basis == 0 || basis == 4 +} + +// getDaysInMonthRange return the day by given year, month range and day count +// basis. +func getDaysInMonthRange(year, fromMonth, toMonth, basis int) int { + if fromMonth > toMonth { + return 0 + } + return (toMonth - fromMonth + 1) * 30 +} + +// getDayOnBasis returns the day by given date and day count basis. +func getDayOnBasis(y, m, d, basis int) int { + if !is30BasisMethod(basis) { + return d + } + day := d + dim := getDaysInMonth(y, m) + if day > 30 || d >= dim || day >= dim { + day = 30 + } + return day +} + +// coupdays returns the number of days that base on date range and the day +// count basis to be used. +func coupdays(from, to time.Time, basis int) float64 { + days := 0 + fromY, fromM, fromD := from.Date() + toY, toM, toD := to.Date() + fromDay, toDay := getDayOnBasis(fromY, int(fromM), fromD, basis), getDayOnBasis(toY, int(toM), toD, basis) + if !is30BasisMethod(basis) { + return (daysBetween(excelMinTime1900.Unix(), makeDate(toY, toM, toDay)) + 1) - (daysBetween(excelMinTime1900.Unix(), makeDate(fromY, fromM, fromDay)) + 1) + } + if basis == 0 { + if (int(fromM) == 2 || fromDay < 30) && toD == 31 { + toDay = 31 + } + } else { + if int(fromM) == 2 && fromDay == 30 { + fromDay = getDaysInMonth(fromY, 2) + } + if int(toM) == 2 && toDay == 30 { + toDay = getDaysInMonth(toY, 2) + } + } + if fromY < toY || (fromY == toY && int(fromM) < int(toM)) { + days = 30 - fromDay + 1 + fromD = 1 + 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) + date = date.AddDate(0, 13-int(date.Month()), 0) + } + days += getDaysInMonthRange(toY, int(date.Month()), int(toM)-1, basis) + date = date.AddDate(0, int(toM)-int(date.Month()), 0) + } + days += toDay - fromDay + if days > 0 { + return float64(days) + } + return 0 +} + +// 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]) +// +func (fn *formulaFuncs) COUPDAYBS(argsList *list.List) formulaArg { + args := fn.prepareCouponArgs("COUPDAYBS", argsList) + if args.Type != ArgList { + return args + } + settlement := timeFromExcelTime(args.List[0].Number, false) + pcd := timeFromExcelTime(fn.COUPPCD(argsList).Number, false) + return newNumberFormulaArg(coupdays(pcd, settlement, int(args.List[3].Number))) +} + +// 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]) +// +func (fn *formulaFuncs) COUPDAYS(argsList *list.List) formulaArg { + args := fn.prepareCouponArgs("COUPDAYS", argsList) + if args.Type != ArgList { + return args + } + freq := args.List[2].Number + basis := int(args.List[3].Number) + if basis == 1 { + pcd := timeFromExcelTime(fn.COUPPCD(argsList).Number, false) + next := pcd.AddDate(0, 12/int(freq), 0) + return newNumberFormulaArg(coupdays(pcd, next, basis)) + } + return newNumberFormulaArg(float64(getYearDays(0, basis)) / freq) +} + +// 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]) +// +func (fn *formulaFuncs) COUPDAYSNC(argsList *list.List) formulaArg { + args := fn.prepareCouponArgs("COUPDAYSNC", argsList) + if args.Type != ArgList { + return args + } + settlement := timeFromExcelTime(args.List[0].Number, false) + basis := int(args.List[3].Number) + ncd := timeFromExcelTime(fn.COUPNCD(argsList).Number, false) + return newNumberFormulaArg(coupdays(settlement, ncd, basis)) +} + // COUPNCD function calculates the number of coupons payable, between a // security's settlement date and maturity date, rounded up to the nearest // whole coupon. The syntax of the function is: diff --git a/calc_test.go b/calc_test.go index 90138a1d8a..8402ab141f 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1417,6 +1417,18 @@ func TestCalcCellValue(t *testing.T) { "=AMORLINC(150,\"01/01/2015\",\"09/30/2015\",20,20,15%,4)": "0", "=AMORLINC(150,\"01/01/2015\",\"09/30/2015\",20,6,15%,4)": "0.6875", "=AMORLINC(150,\"01/01/2015\",\"09/30/2015\",20,0,15%,4)": "16.8125", + // COUPDAYBS + "=COUPDAYBS(\"02/24/2000\",\"11/24/2000\",4,4)": "0", + "=COUPDAYBS(\"03/27/2000\",\"11/29/2000\",4,4)": "28", + "=COUPDAYBS(\"02/29/2000\",\"04/01/2000\",4,4)": "58", + "=COUPDAYBS(\"01/01/2011\",\"10/25/2012\",4)": "66", + "=COUPDAYBS(\"01/01/2011\",\"10/25/2012\",4,1)": "68", + "=COUPDAYBS(\"10/31/2011\",\"02/26/2012\",4,0)": "65", + // COUPDAYS + "=COUPDAYS(\"01/01/2011\",\"10/25/2012\",4)": "90", + "=COUPDAYS(\"01/01/2011\",\"10/25/2012\",4,1)": "92", + // COUPDAYSNC + "=COUPDAYSNC(\"01/01/2011\",\"10/25/2012\",4)": "24", // COUPNCD "=COUPNCD(\"01/01/2011\",\"10/25/2012\",4)": "40568", "=COUPNCD(\"01/01/2011\",\"10/25/2012\",4,0)": "40568", @@ -2746,6 +2758,27 @@ func TestCalcCellValue(t *testing.T) { "=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", + // 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", + // 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", + // 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", // COUPNCD "=COUPNCD()": "COUPNCD requires 3 or 4 arguments", "=COUPNCD(\"01/01/2011\",\"10/25/2012\",4,0,0)": "COUPNCD requires 3 or 4 arguments", From bda8e7f8129dae0064c47f8e051f76492e1128f5 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 16 Nov 2021 00:40:44 +0800 Subject: [PATCH 490/957] This closes #1061, support multi-byte language on set header footer typo fixed and simplify code for read the data values arguments of formula functions --- calc.go | 336 ++++++++++++++--------------------------- comment.go | 6 +- datavalidation.go | 7 +- datavalidation_test.go | 4 +- errors.go | 5 + pivotTable.go | 4 +- sheet.go | 5 +- sheet_test.go | 12 +- styles_test.go | 4 +- xmlDrawing.go | 1 + 10 files changed, 140 insertions(+), 244 deletions(-) diff --git a/calc.go b/calc.go index badd30e689..59131d84d7 100644 --- a/calc.go +++ b/calc.go @@ -2849,7 +2849,7 @@ func (fn *formulaFuncs) ARABIC(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorVALUE, "ARABIC requires 1 numeric argument") } text := argsList.Front().Value.(formulaArg).Value() - if len(text) > 255 { + if len(text) > MaxFieldLength { return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) } text = strings.ToUpper(text) @@ -7400,47 +7400,12 @@ func (fn *formulaFuncs) DAYS(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "DAYS requires 2 arguments") } - var end, start float64 - endArg, startArg := argsList.Front().Value.(formulaArg), argsList.Back().Value.(formulaArg) - switch endArg.Type { - case ArgNumber: - end = endArg.Number - case ArgString: - endNum := endArg.ToNumber() - if endNum.Type == ArgNumber { - end = endNum.Number - } else { - args := list.New() - args.PushBack(endArg) - endValue := fn.DATEVALUE(args) - if endValue.Type == ArgError { - return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) - } - end = endValue.Number - } - default: - return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) - } - switch startArg.Type { - case ArgNumber: - start = startArg.Number - case ArgString: - startNum := startArg.ToNumber() - if startNum.Type == ArgNumber { - start = startNum.Number - } else { - args := list.New() - args.PushBack(startArg) - startValue := fn.DATEVALUE(args) - if startValue.Type == ArgError { - return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) - } - start = startValue.Number - } - default: - return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + args := fn.prepareDataValueArgs(2, argsList) + if args.Type != ArgList { + return args } - return newNumberFormulaArg(end - start) + end, start := args.List[0], args.List[1] + return newNumberFormulaArg(end.Number - start.Number) } // ISOWEEKNUM function returns the ISO week number of a supplied date. The @@ -7695,28 +7660,18 @@ func (fn *formulaFuncs) YEARFRAC(argsList *list.List) formulaArg { if argsList.Len() != 2 && argsList.Len() != 3 { return newErrorFormulaArg(formulaErrorVALUE, "YEARFRAC requires 3 or 4 arguments") } - var basisArg formulaArg - startArg, endArg := argsList.Front().Value.(formulaArg).ToNumber(), argsList.Front().Next().Value.(formulaArg).ToNumber() - args := list.New().Init() - if startArg.Type != ArgNumber { - args.PushBack(argsList.Front().Value.(formulaArg)) - if startArg = fn.DATEVALUE(args); startArg.Type != ArgNumber { - return startArg - } - } - if endArg.Type != ArgNumber { - args.Init() - args.PushBack(argsList.Front().Next().Value.(formulaArg)) - if endArg = fn.DATEVALUE(args); endArg.Type != ArgNumber { - return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) - } + args := fn.prepareDataValueArgs(2, argsList) + if args.Type != ArgList { + return args } + start, end := args.List[0], args.List[1] + basis := newNumberFormulaArg(0) if argsList.Len() == 3 { - if basisArg = argsList.Back().Value.(formulaArg).ToNumber(); basisArg.Type != ArgNumber { - return basisArg + if basis = argsList.Back().Value.(formulaArg).ToNumber(); basis.Type != ArgNumber { + return basis } } - return yearFrac(startArg.Number, endArg.Number, int(basisArg.Number)) + return yearFrac(start.Number, end.Number, int(basis.Number)) } // NOW function returns the current date and time. The function receives no @@ -7859,7 +7814,7 @@ func (fn *formulaFuncs) CHAR(argsList *list.List) formulaArg { return arg } num := int(arg.Number) - if num < 0 || num > 255 { + if num < 0 || num > MaxFieldLength { return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) } return newStringFormulaArg(fmt.Sprintf("%c", num)) @@ -9413,24 +9368,11 @@ func (fn *formulaFuncs) ACCRINT(argsList *list.List) formulaArg { if argsList.Len() > 8 { return newErrorFormulaArg(formulaErrorVALUE, "ACCRINT allows at most 8 arguments") } - args := list.New().Init() - args.PushBack(argsList.Front().Value.(formulaArg)) - issue := fn.DATEVALUE(args) - if issue.Type != ArgNumber { - return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) - } - args.Init() - args.PushBack(argsList.Front().Next().Value.(formulaArg)) - fi := fn.DATEVALUE(args) - if fi.Type != ArgNumber { - return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) - } - args.Init() - args.PushBack(argsList.Front().Next().Next().Value.(formulaArg)) - settlement := fn.DATEVALUE(args) - if settlement.Type != ArgNumber { - return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + args := fn.prepareDataValueArgs(3, argsList) + if args.Type != ArgList { + return args } + issue, settlement := args.List[0], args.List[2] rate := argsList.Front().Next().Next().Next().Value.(formulaArg).ToNumber() par := argsList.Front().Next().Next().Next().Next().Value.(formulaArg).ToNumber() frequency := argsList.Front().Next().Next().Next().Next().Next().Value.(formulaArg).ToNumber() @@ -9468,18 +9410,11 @@ func (fn *formulaFuncs) ACCRINTM(argsList *list.List) formulaArg { if argsList.Len() != 4 && argsList.Len() != 5 { return newErrorFormulaArg(formulaErrorVALUE, "ACCRINTM requires 4 or 5 arguments") } - args := list.New().Init() - args.PushBack(argsList.Front().Value.(formulaArg)) - issue := fn.DATEVALUE(args) - if issue.Type != ArgNumber { - return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) - } - args.Init() - args.PushBack(argsList.Front().Next().Value.(formulaArg)) - settlement := fn.DATEVALUE(args) - if settlement.Type != ArgNumber { - return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + args := fn.prepareDataValueArgs(2, argsList) + if args.Type != ArgList { + return args } + issue, settlement := args.List[0], args.List[1] if settlement.Number < issue.Number { return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) } @@ -9644,24 +9579,11 @@ func (fn *formulaFuncs) prepareCouponArgs(name string, argsList *list.List) form if argsList.Len() != 3 && argsList.Len() != 4 { return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires 3 or 4 arguments", name)) } - args := list.New().Init() - settlement := argsList.Front().Value.(formulaArg).ToNumber() - if settlement.Type != ArgNumber { - args.PushBack(argsList.Front().Value.(formulaArg)) - settlement = fn.DATEVALUE(args) - if settlement.Type != ArgNumber { - return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) - } - } - maturity := argsList.Front().Next().Value.(formulaArg).ToNumber() - if maturity.Type != ArgNumber { - args.Init() - args.PushBack(argsList.Front().Next().Value.(formulaArg)) - maturity = fn.DATEVALUE(args) - if maturity.Type != ArgNumber { - return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) - } + args := fn.prepareDataValueArgs(2, argsList) + if args.Type != ArgList { + return args } + settlement, maturity := args.List[0], args.List[1] if settlement.Number >= maturity.Number { return newErrorFormulaArg(formulaErrorNUM, fmt.Sprintf("%s requires maturity > settlement", name)) } @@ -10048,6 +9970,43 @@ func (fn *formulaFuncs) DDB(argsList *list.List) formulaArg { return newNumberFormulaArg(depreciation) } +// prepareDataValueArgs convert first N arguments to data value for the +// formula functions. +func (fn *formulaFuncs) prepareDataValueArgs(n int, argsList *list.List) formulaArg { + l := list.New() + dataValues := []formulaArg{} + getDateValue := func(arg formulaArg, l *list.List) formulaArg { + switch arg.Type { + case ArgNumber: + break + case ArgString: + num := arg.ToNumber() + if num.Type == ArgNumber { + arg = num + break + } + l.Init() + l.PushBack(arg) + arg = fn.DATEVALUE(l) + if arg.Type == ArgError { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + default: + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + return arg + } + for i, arg := 0, argsList.Front(); i < n; arg = arg.Next() { + dataValue := getDateValue(arg.Value.(formulaArg), l) + if dataValue.Type != ArgNumber { + return dataValue + } + dataValues = append(dataValues, dataValue) + i++ + } + return newListFormulaArg(dataValues) +} + // DISC function calculates the Discount Rate for a security. The syntax of // the function is: // @@ -10057,18 +10016,11 @@ func (fn *formulaFuncs) DISC(argsList *list.List) formulaArg { if argsList.Len() != 4 && argsList.Len() != 5 { return newErrorFormulaArg(formulaErrorVALUE, "DISC requires 4 or 5 arguments") } - args := list.New().Init() - args.PushBack(argsList.Front().Value.(formulaArg)) - settlement := fn.DATEVALUE(args) - if settlement.Type != ArgNumber { - return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) - } - args.Init() - args.PushBack(argsList.Front().Next().Value.(formulaArg)) - maturity := fn.DATEVALUE(args) - if maturity.Type != ArgNumber { - return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + 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, "DISC requires maturity > settlement") } @@ -10253,18 +10205,11 @@ 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 := list.New().Init() - args.PushBack(argsList.Front().Value.(formulaArg)) - settlement := fn.DATEVALUE(args) - if settlement.Type != ArgNumber { - return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) - } - args.Init() - args.PushBack(argsList.Front().Next().Value.(formulaArg)) - maturity := fn.DATEVALUE(args) - if maturity.Type != ArgNumber { - return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + 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") } @@ -10707,18 +10652,11 @@ func (fn *formulaFuncs) PRICEDISC(argsList *list.List) formulaArg { if argsList.Len() != 4 && argsList.Len() != 5 { return newErrorFormulaArg(formulaErrorVALUE, "PRICEDISC requires 4 or 5 arguments") } - args := list.New().Init() - args.PushBack(argsList.Front().Value.(formulaArg)) - settlement := fn.DATEVALUE(args) - if settlement.Type != ArgNumber { - return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) - } - args.Init() - args.PushBack(argsList.Front().Next().Value.(formulaArg)) - maturity := fn.DATEVALUE(args) - if maturity.Type != ArgNumber { - return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + 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, "PRICEDISC requires maturity > settlement") } @@ -10758,27 +10696,14 @@ func (fn *formulaFuncs) PRICEMAT(argsList *list.List) formulaArg { if argsList.Len() != 5 && argsList.Len() != 6 { return newErrorFormulaArg(formulaErrorVALUE, "PRICEMAT requires 5 or 6 arguments") } - args := list.New().Init() - args.PushBack(argsList.Front().Value.(formulaArg)) - settlement := fn.DATEVALUE(args) - if settlement.Type != ArgNumber { - return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) - } - args.Init() - args.PushBack(argsList.Front().Next().Value.(formulaArg)) - maturity := fn.DATEVALUE(args) - if maturity.Type != ArgNumber { - return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + args := fn.prepareDataValueArgs(3, argsList) + if args.Type != ArgList { + return args } + settlement, maturity, issue := args.List[0], args.List[1], args.List[2] if settlement.Number >= maturity.Number { return newErrorFormulaArg(formulaErrorNUM, "PRICEMAT requires maturity > settlement") } - args.Init() - args.PushBack(argsList.Front().Next().Next().Value.(formulaArg)) - issue := fn.DATEVALUE(args) - if issue.Type != ArgNumber { - return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) - } if issue.Number >= settlement.Number { return newErrorFormulaArg(formulaErrorNUM, "PRICEMAT requires settlement > issue") } @@ -10938,18 +10863,11 @@ func (fn *formulaFuncs) RECEIVED(argsList *list.List) formulaArg { if argsList.Len() > 5 { return newErrorFormulaArg(formulaErrorVALUE, "RECEIVED allows at most 5 arguments") } - args := list.New().Init() - args.PushBack(argsList.Front().Value.(formulaArg)) - settlement := fn.DATEVALUE(args) - if settlement.Type != ArgNumber { - return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) - } - args.Init() - args.PushBack(argsList.Front().Next().Value.(formulaArg)) - maturity := fn.DATEVALUE(args) - if maturity.Type != ArgNumber { - return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + args := fn.prepareDataValueArgs(2, argsList) + if args.Type != ArgList { + return args } + settlement, maturity := args.List[0], args.List[1] investment := argsList.Front().Next().Next().Value.(formulaArg).ToNumber() if investment.Type != ArgNumber { return investment @@ -11061,18 +10979,11 @@ func (fn *formulaFuncs) TBILLEQ(argsList *list.List) formulaArg { if argsList.Len() != 3 { return newErrorFormulaArg(formulaErrorVALUE, "TBILLEQ requires 3 arguments") } - args := list.New().Init() - args.PushBack(argsList.Front().Value.(formulaArg)) - settlement := fn.DATEVALUE(args) - if settlement.Type != ArgNumber { - return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) - } - args.Init() - args.PushBack(argsList.Front().Next().Value.(formulaArg)) - maturity := fn.DATEVALUE(args) - if maturity.Type != ArgNumber { - return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + args := fn.prepareDataValueArgs(2, argsList) + if args.Type != ArgList { + return args } + settlement, maturity := args.List[0], args.List[1] dsm := maturity.Number - settlement.Number if dsm > 365 || maturity.Number <= settlement.Number { return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) @@ -11096,18 +11007,11 @@ func (fn *formulaFuncs) TBILLPRICE(argsList *list.List) formulaArg { if argsList.Len() != 3 { return newErrorFormulaArg(formulaErrorVALUE, "TBILLPRICE requires 3 arguments") } - args := list.New().Init() - args.PushBack(argsList.Front().Value.(formulaArg)) - settlement := fn.DATEVALUE(args) - if settlement.Type != ArgNumber { - return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) - } - args.Init() - args.PushBack(argsList.Front().Next().Value.(formulaArg)) - maturity := fn.DATEVALUE(args) - if maturity.Type != ArgNumber { - return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + args := fn.prepareDataValueArgs(2, argsList) + if args.Type != ArgList { + return args } + settlement, maturity := args.List[0], args.List[1] dsm := maturity.Number - settlement.Number if dsm > 365 || maturity.Number <= settlement.Number { return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) @@ -11131,18 +11035,11 @@ func (fn *formulaFuncs) TBILLYIELD(argsList *list.List) formulaArg { if argsList.Len() != 3 { return newErrorFormulaArg(formulaErrorVALUE, "TBILLYIELD requires 3 arguments") } - args := list.New().Init() - args.PushBack(argsList.Front().Value.(formulaArg)) - settlement := fn.DATEVALUE(args) - if settlement.Type != ArgNumber { - return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) - } - args.Init() - args.PushBack(argsList.Front().Next().Value.(formulaArg)) - maturity := fn.DATEVALUE(args) - if maturity.Type != ArgNumber { - return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + args := fn.prepareDataValueArgs(2, argsList) + if args.Type != ArgList { + return args } + settlement, maturity := args.List[0], args.List[1] dsm := maturity.Number - settlement.Number if dsm > 365 || maturity.Number <= settlement.Number { return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) @@ -11231,18 +11128,11 @@ func (fn *formulaFuncs) YIELDDISC(argsList *list.List) formulaArg { if argsList.Len() != 4 && argsList.Len() != 5 { return newErrorFormulaArg(formulaErrorVALUE, "YIELDDISC requires 4 or 5 arguments") } - args := list.New().Init() - args.PushBack(argsList.Front().Value.(formulaArg)) - settlement := fn.DATEVALUE(args) - if settlement.Type != ArgNumber { - return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) - } - args.Init() - args.PushBack(argsList.Front().Next().Value.(formulaArg)) - maturity := fn.DATEVALUE(args) - if maturity.Type != ArgNumber { - return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + args := fn.prepareDataValueArgs(2, argsList) + if args.Type != ArgList { + return args } + settlement, maturity := args.List[0], args.List[1] pr := argsList.Front().Next().Next().Value.(formulaArg).ToNumber() if pr.Type != ArgNumber { return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) @@ -11279,23 +11169,19 @@ func (fn *formulaFuncs) YIELDMAT(argsList *list.List) formulaArg { if argsList.Len() != 5 && argsList.Len() != 6 { return newErrorFormulaArg(formulaErrorVALUE, "YIELDMAT requires 5 or 6 arguments") } - args := list.New().Init() - args.PushBack(argsList.Front().Value.(formulaArg)) - settlement := fn.DATEVALUE(args) - if settlement.Type != ArgNumber { - return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) - } - args.Init() - args.PushBack(argsList.Front().Next().Value.(formulaArg)) - maturity := fn.DATEVALUE(args) - if maturity.Type != ArgNumber { - return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + args := fn.prepareDataValueArgs(2, argsList) + if args.Type != ArgList { + return args } - args.Init() - args.PushBack(argsList.Front().Next().Next().Value.(formulaArg)) - issue := fn.DATEVALUE(args) + settlement, maturity := args.List[0], args.List[1] + arg := list.New().Init() + issue := argsList.Front().Next().Next().Value.(formulaArg).ToNumber() if issue.Type != ArgNumber { - return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + arg.PushBack(argsList.Front().Next().Next().Value.(formulaArg)) + issue = fn.DATEVALUE(arg) + if issue.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } } if issue.Number >= settlement.Number { return newErrorFormulaArg(formulaErrorNUM, "YIELDMAT requires settlement > issue") diff --git a/comment.go b/comment.go index a89d2bb610..07cd9f21dd 100644 --- a/comment.go +++ b/comment.go @@ -245,11 +245,11 @@ func (f *File) addDrawingVML(commentID int, drawingVML, cell string, lineCount, func (f *File) addComment(commentsXML, cell string, formatSet *formatComment) { a := formatSet.Author t := formatSet.Text - if len(a) > 255 { - a = a[0:255] + if len(a) > MaxFieldLength { + a = a[:MaxFieldLength] } if len(t) > 32512 { - t = t[0:32512] + t = t[:32512] } comments := f.commentsReader(commentsXML) authorID := 0 diff --git a/datavalidation.go b/datavalidation.go index 047a53c37e..80a0295ed2 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -35,11 +35,6 @@ const ( DataValidationTypeWhole ) -const ( - // dataValidationFormulaStrLen 255 characters - dataValidationFormulaStrLen = 255 -) - // DataValidationErrorStyle defined the style of data validation error alert. type DataValidationErrorStyle int @@ -120,7 +115,7 @@ func (dd *DataValidation) SetInput(title, msg string) { // SetDropList data validation list. func (dd *DataValidation) SetDropList(keys []string) error { formula := strings.Join(keys, ",") - if dataValidationFormulaStrLen < len(utf16.Encode([]rune(formula))) { + if MaxFieldLength < len(utf16.Encode([]rune(formula))) { return ErrDataValidationFormulaLenth } dd.Formula1 = fmt.Sprintf(`"%s"`, formulaEscaper.Replace(formula)) diff --git a/datavalidation_test.go b/datavalidation_test.go index 5986375f86..c35693ce26 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -54,8 +54,8 @@ func TestDataValidation(t *testing.T) { dvRange.Sqref = "A5:B6" for _, listValid := range [][]string{ {"1", "2", "3"}, - {strings.Repeat("&", 255)}, - {strings.Repeat("\u4E00", 255)}, + {strings.Repeat("&", MaxFieldLength)}, + {strings.Repeat("\u4E00", MaxFieldLength)}, {strings.Repeat("\U0001F600", 100), strings.Repeat("\u4E01", 50), "<&>"}, {`A<`, `B>`, `C"`, "D\t", `E'`, `F`}, } { diff --git a/errors.go b/errors.go index 56a2280d12..f9eedde7c6 100644 --- a/errors.go +++ b/errors.go @@ -51,6 +51,11 @@ func newInvalidStyleID(styleID int) error { return fmt.Errorf("invalid style ID %d, negative values are not supported", styleID) } +// 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) +} + var ( // ErrStreamSetColWidth defined the error message on set column width in // stream writing mode. diff --git a/pivotTable.go b/pivotTable.go index 6ce0173b9f..270ee9923d 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -662,8 +662,8 @@ func (f *File) getPivotTableFieldsSubtotal(fields []PivotTableField) []string { func (f *File) getPivotTableFieldsName(fields []PivotTableField) []string { field := make([]string, len(fields)) for idx, fld := range fields { - if len(fld.Name) > 255 { - field[idx] = fld.Name[0:255] + if len(fld.Name) > MaxFieldLength { + field[idx] = fld.Name[:MaxFieldLength] continue } field[idx] = fld.Name diff --git a/sheet.go b/sheet.go index 43e277aa8c..5738ced5fa 100644 --- a/sheet.go +++ b/sheet.go @@ -27,6 +27,7 @@ import ( "sort" "strconv" "strings" + "unicode/utf16" "unicode/utf8" "github.com/mohae/deepcopy" @@ -1092,8 +1093,8 @@ func (f *File) SetHeaderFooter(sheet string, settings *FormatHeaderFooter) error // Check 6 string type fields: OddHeader, OddFooter, EvenHeader, EvenFooter, // FirstFooter, FirstHeader for i := 4; i < v.NumField()-1; i++ { - if v.Field(i).Len() >= 255 { - return fmt.Errorf("field %s must be less than 255 characters", v.Type().Field(i).Name) + if len(utf16.Encode([]rune(v.Field(i).String()))) > MaxFieldLength { + return newFieldLengthError(v.Type().Field(i).Name) } } ws.HeaderFooter = &xlsxHeaderFooter{ diff --git a/sheet_test.go b/sheet_test.go index ef32d79bb0..93a4ab6482 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -217,10 +217,18 @@ func TestSetHeaderFooter(t *testing.T) { assert.EqualError(t, f.SetHeaderFooter("SheetN", nil), "sheet SheetN is not exist") // Test set header and footer with illegal setting. assert.EqualError(t, f.SetHeaderFooter("Sheet1", &FormatHeaderFooter{ - OddHeader: strings.Repeat("c", 256), - }), "field OddHeader must be less than 255 characters") + OddHeader: strings.Repeat("c", MaxFieldLength+1), + }), "field OddHeader must be less or equal than 255 characters") assert.NoError(t, f.SetHeaderFooter("Sheet1", nil)) + text := strings.Repeat("一", MaxFieldLength) + assert.NoError(t, f.SetHeaderFooter("Sheet1", &FormatHeaderFooter{ + OddHeader: text, + OddFooter: text, + EvenHeader: text, + EvenFooter: text, + FirstHeader: text, + })) assert.NoError(t, f.SetHeaderFooter("Sheet1", &FormatHeaderFooter{ DifferentFirst: true, DifferentOddEven: true, diff --git a/styles_test.go b/styles_test.go index a214aaa2f5..9129914d63 100644 --- a/styles_test.go +++ b/styles_test.go @@ -262,10 +262,10 @@ func TestGetDefaultFont(t *testing.T) { func TestSetDefaultFont(t *testing.T) { f := NewFile() - f.SetDefaultFont("Ariel") + f.SetDefaultFont("Arial") styles := f.stylesReader() s := f.GetDefaultFont() - assert.Equal(t, s, "Ariel", "Default font should change to Ariel") + assert.Equal(t, s, "Arial", "Default font should change to Arial") assert.Equal(t, *styles.CellStyles.CellStyle[0].CustomBuiltIn, true) } diff --git a/xmlDrawing.go b/xmlDrawing.go index 0bb11ace3b..dabb34a791 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -99,6 +99,7 @@ const ( MaxFontFamilyLength = 31 MaxFontSize = 409 MaxFileNameLength = 207 + MaxFieldLength = 255 MaxColumnWidth = 255 MaxRowHeight = 409 TotalRows = 1048576 From bc3c7d51a2efe5f0ad85667a8f9636f13941d577 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 17 Nov 2021 00:25:36 +0800 Subject: [PATCH 491/957] ref #65: new formula function PRICE - fix COUPPCD result accuracy issue - update close spreadsheet example in documentation and README --- README.md | 20 +++++--- README_zh.md | 20 +++++--- calc.go | 134 ++++++++++++++++++++++++++++++++++++++++++--------- calc_test.go | 21 ++++++++ cell.go | 4 +- crypt.go | 4 +- lib.go | 2 +- picture.go | 13 +++-- sheet.go | 10 ++-- styles.go | 2 +- table.go | 2 +- 11 files changed, 178 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index 6b874697a3..1245c62e97 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,12 @@ func main() { fmt.Println(err) return } + defer func() { + // Close the spreadsheet. + if err := f.Close(); err != nil { + fmt.Println(err) + } + }() // Get value from cell by given worksheet name and axis. cell, err := f.GetCellValue("Sheet1", "B2") if err != nil { @@ -96,10 +102,6 @@ func main() { } fmt.Println() } - // Close the spreadsheet. - if err = f.Close(); err != nil { - fmt.Println(err) - } } ``` @@ -184,6 +186,12 @@ func main() { fmt.Println(err) return } + defer func() { + // Close the spreadsheet. + if err := f.Close(); err != nil { + fmt.Println(err) + } + }() // Insert a picture. if err := f.AddPicture("Sheet1", "A2", "image.png", ""); err != nil { fmt.Println(err) @@ -207,10 +215,6 @@ func main() { if err = f.Save(); err != nil { fmt.Println(err) } - // Close the spreadsheet. - if err = f.Close(); err != nil { - fmt.Println(err) - } } ``` diff --git a/README_zh.md b/README_zh.md index 3b90eecda9..b946bb33d3 100644 --- a/README_zh.md +++ b/README_zh.md @@ -77,6 +77,12 @@ func main() { fmt.Println(err) return } + defer func() { + // 关闭工作簿 + if err := f.Close(); err != nil { + fmt.Println(err) + } + }() // 获取工作表中指定单元格的值 cell, err := f.GetCellValue("Sheet1", "B2") if err != nil { @@ -96,10 +102,6 @@ func main() { } fmt.Println() } - // 关闭工作簿 - if err = f.Close(); err != nil { - fmt.Println(err) - } } ``` @@ -184,6 +186,12 @@ func main() { fmt.Println(err) return } + defer func() { + // 关闭工作簿 + if err := f.Close(); err != nil { + fmt.Println(err) + } + }() // 插入图片 if err := f.AddPicture("Sheet1", "A2", "image.png", ""); err != nil { fmt.Println(err) @@ -207,10 +215,6 @@ func main() { if err = f.Save(); err != nil { fmt.Println(err) } - // 关闭工作簿 - if err = f.Close(); err != nil { - fmt.Println(err) - } } ``` diff --git a/calc.go b/calc.go index 59131d84d7..1357ad4f90 100644 --- a/calc.go +++ b/calc.go @@ -516,6 +516,7 @@ type formulaFuncs struct { // POISSON // POWER // PPMT +// PRICE // PRICEDISC // PRICEMAT // PRODUCT @@ -9723,6 +9724,40 @@ func (fn *formulaFuncs) COUPDAYSNC(argsList *list.List) formulaArg { return newNumberFormulaArg(coupdays(settlement, ncd, basis)) } +// coupons is an implementation of the formula function COUPNCD and COUPPCD. +func (fn *formulaFuncs) coupons(name string, arg formulaArg) formulaArg { + settlement := timeFromExcelTime(arg.List[0].Number, false) + maturity := timeFromExcelTime(arg.List[1].Number, false) + maturityDays := (maturity.Year()-settlement.Year())*12 + (int(maturity.Month()) - int(settlement.Month())) + coupon := 12 / int(arg.List[2].Number) + mod := maturityDays % coupon + year := settlement.Year() + month := int(settlement.Month()) + if mod == 0 && settlement.Day() >= maturity.Day() { + month += coupon + } else { + month += mod + } + if name != "COUPNCD" { + month -= coupon + } + if month > 11 { + year += 1 + month -= 12 + } else if month < 0 { + year -= 1 + month += 12 + } + day, lastDay := maturity.Day(), time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.UTC) + days := getDaysInMonth(lastDay.Year(), int(lastDay.Month())) + if getDaysInMonth(maturity.Year(), int(maturity.Month())) == maturity.Day() { + day = days + } else if day > 27 && day > days { + day = days + } + return newNumberFormulaArg(daysBetween(excelMinTime1900.Unix(), makeDate(year, time.Month(month), day)) + 1) +} + // COUPNCD function calculates the number of coupons payable, between a // security's settlement date and maturity date, rounded up to the nearest // whole coupon. The syntax of the function is: @@ -9734,16 +9769,7 @@ func (fn *formulaFuncs) COUPNCD(argsList *list.List) formulaArg { if args.Type != ArgList { return args } - settlement := timeFromExcelTime(args.List[0].Number, false) - maturity := timeFromExcelTime(args.List[1].Number, false) - ncd := time.Date(settlement.Year(), maturity.Month(), maturity.Day(), 0, 0, 0, 0, time.UTC) - if ncd.After(settlement) { - ncd = ncd.AddDate(-1, 0, 0) - } - for !ncd.After(settlement) { - ncd = ncd.AddDate(0, 12/int(args.List[2].Number), 0) - } - return newNumberFormulaArg(daysBetween(excelMinTime1900.Unix(), makeDate(ncd.Year(), ncd.Month(), ncd.Day())) + 1) + return fn.coupons("COUPNCD", args) } // COUPNUM function calculates the number of coupons payable, between a @@ -9773,18 +9799,7 @@ func (fn *formulaFuncs) COUPPCD(argsList *list.List) formulaArg { if args.Type != ArgList { return args } - settlement := timeFromExcelTime(args.List[0].Number, false) - maturity := timeFromExcelTime(args.List[1].Number, false) - date, years := maturity, settlement.Year()-maturity.Year() - date = date.AddDate(years, 0, 0) - if settlement.After(date) { - date = date.AddDate(1, 0, 0) - } - month := -12 / args.List[2].Number - for date.After(settlement) { - date = date.AddDate(0, int(month), 0) - } - return newNumberFormulaArg(daysBetween(excelMinTime1900.Unix(), makeDate(date.Year(), date.Month(), date.Day())) + 1) + return fn.coupons("COUPPCD", args) } // CUMIPMT function calculates the cumulative interest paid on a loan or @@ -10643,6 +10658,81 @@ func (fn *formulaFuncs) PPMT(argsList *list.List) formulaArg { return fn.ipmt("PPMT", argsList) } +// price is an implementation of the formula function PRICE. +func (fn *formulaFuncs) price(settlement, maturity, rate, yld, redemption, frequency, basis formulaArg) formulaArg { + if basis.Number < 0 || basis.Number > 4 { + return newErrorFormulaArg(formulaErrorNUM, "invalid basis") + } + argsList := list.New().Init() + argsList.PushBack(settlement) + argsList.PushBack(maturity) + argsList.PushBack(frequency) + argsList.PushBack(basis) + e := fn.COUPDAYS(argsList) + dsc := fn.COUPDAYSNC(argsList).Number / e.Number + n := fn.COUPNUM(argsList) + a := fn.COUPDAYBS(argsList) + ret := redemption.Number / math.Pow(1+yld.Number/frequency.Number, n.Number-1+dsc) + ret -= 100 * rate.Number / frequency.Number * a.Number / e.Number + t1 := 100 * rate.Number / frequency.Number + t2 := 1 + yld.Number/frequency.Number + for k := 0.0; k < n.Number; k++ { + ret += t1 / math.Pow(t2, k+dsc) + } + 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() + if rate.Type != ArgNumber { + return rate + } + if rate.Number < 0 { + return newErrorFormulaArg(formulaErrorNUM, "PRICE requires rate >= 0") + } + 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") + } + 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.price(settlement, maturity, rate, yld, redemption, frequency, basis) +} + // PRICEDISC function calculates the price, per $100 face value of a // discounted security. The syntax of the function is: // diff --git a/calc_test.go b/calc_test.go index 8402ab141f..894b154e0a 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1429,10 +1429,13 @@ func TestCalcCellValue(t *testing.T) { "=COUPDAYS(\"01/01/2011\",\"10/25/2012\",4,1)": "92", // COUPDAYSNC "=COUPDAYSNC(\"01/01/2011\",\"10/25/2012\",4)": "24", + "=COUPDAYSNC(\"04/01/2012\",\"03/31/2020\",2)": "179", // COUPNCD "=COUPNCD(\"01/01/2011\",\"10/25/2012\",4)": "40568", "=COUPNCD(\"01/01/2011\",\"10/25/2012\",4,0)": "40568", "=COUPNCD(\"10/25/2011\",\"01/01/2012\",4)": "40909", + "=COUPNCD(\"04/01/2012\",\"03/31/2020\",2)": "41182", + "=COUPNCD(\"01/01/2000\",\"08/30/2001\",2)": "36585", // COUPNUM "=COUPNUM(\"01/01/2011\",\"10/25/2012\",4)": "8", "=COUPNUM(\"01/01/2011\",\"10/25/2012\",4,0)": "8", @@ -1497,6 +1500,10 @@ func TestCalcCellValue(t *testing.T) { // PMT "=PMT(0,8,0,5000,1)": "-625", "=PMT(0.035/4,8,0,5000,1)": "-600.8520271804658", + // 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", // PPMT "=PPMT(0.05/12,2,60,50000)": "-738.2918003208238", "=PPMT(0.035/4,2,8,0,5000,1)": "-606.1094824182949", @@ -2951,6 +2958,20 @@ func TestCalcCellValue(t *testing.T) { "=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", + // 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", // PPMT "=PPMT()": "PPMT requires at least 4 arguments", "=PPMT(0,0,0,0,0,0,0)": "PPMT allows at most 6 arguments", diff --git a/cell.go b/cell.go index 41a9517886..e1c78036f6 100644 --- a/cell.go +++ b/cell.go @@ -354,7 +354,7 @@ func (f *File) SetCellStr(sheet, axis, value string) error { // table. func (f *File) setCellString(value string) (t string, v string) { if len(value) > TotalCellChars { - value = value[0:TotalCellChars] + value = value[:TotalCellChars] } t = "s" v = strconv.Itoa(f.setSharedString(value)) @@ -381,7 +381,7 @@ func (f *File) setSharedString(val string) int { // setCellStr provides a function to set string type to cell. func setCellStr(value string) (t string, v string, ns xml.Attr) { if len(value) > TotalCellChars { - value = value[0:TotalCellChars] + value = value[:TotalCellChars] } if len(value) > 0 { prefix, suffix := value[0], value[len(value)-1] diff --git a/crypt.go b/crypt.go index 24ac7eccf5..ae39bba16b 100644 --- a/crypt.go +++ b/crypt.go @@ -297,7 +297,7 @@ func encryptionMechanism(buffer []byte) (mechanism string, err error) { err = ErrUnknownEncryptMechanism return } - versionMajor, versionMinor := binary.LittleEndian.Uint16(buffer[0:2]), binary.LittleEndian.Uint16(buffer[2:4]) + versionMajor, versionMinor := binary.LittleEndian.Uint16(buffer[:2]), binary.LittleEndian.Uint16(buffer[2:4]) if versionMajor == 4 && versionMinor == 4 { mechanism = "agile" return @@ -600,7 +600,7 @@ func createIV(blockKey interface{}, encryption Encryption) ([]byte, error) { tmp := make([]byte, 0x36) iv = append(iv, tmp...) } else if len(iv) > encryptedKey.BlockSize { - iv = iv[0:encryptedKey.BlockSize] + iv = iv[:encryptedKey.BlockSize] } return iv, nil } diff --git a/lib.go b/lib.go index 535161a29d..ccb09ac6b2 100644 --- a/lib.go +++ b/lib.go @@ -526,7 +526,7 @@ func bytesReplace(s, old, new []byte, n int) []byte { } w += copy(s[w:], s[i:]) - return s[0:w] + return s[:w] } // genSheetPasswd provides a method to generate password for worksheet diff --git a/picture.go b/picture.go index 332c6392cb..2956ff1ac5 100644 --- a/picture.go +++ b/picture.go @@ -481,14 +481,20 @@ func (f *File) getSheetRelationshipsTargetByID(sheet, rID string) string { } // GetPicture provides a function to get picture base name and raw content -// embed in XLSX by given worksheet and cell name. This function returns the -// file name in XLSX and file contents as []byte data types. For example: +// 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: // // 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) @@ -497,9 +503,6 @@ func (f *File) getSheetRelationshipsTargetByID(sheet, rID string) string { // if err := ioutil.WriteFile(file, raw, 0644); err != nil { // fmt.Println(err) // } -// if err = f.Close(); err != nil { -// fmt.Println(err) -// } // func (f *File) GetPicture(sheet, cell string) (string, []byte, error) { col, row, err := CellNameToCoordinates(cell) diff --git a/sheet.go b/sheet.go index 5738ced5fa..1aa378b52f 100644 --- a/sheet.go +++ b/sheet.go @@ -234,7 +234,7 @@ func trimCell(column []xlsxC) []xlsxC { i++ } } - return col[0:i] + return col[:i] } // setContentTypes provides a function to read and update property of contents @@ -452,12 +452,14 @@ func (f *File) GetSheetIndex(name string) int { // 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) // } -// if err = f.Close(); err != nil { -// fmt.Println(err) -// } // func (f *File) GetSheetMap() map[int]string { wb := f.workbookReader() diff --git a/styles.go b/styles.go index 0ae9e516a4..183211b0a5 100644 --- a/styles.go +++ b/styles.go @@ -3137,7 +3137,7 @@ func ThemeColor(baseColor string, tint float64) string { if tint == 0 { return "FF" + baseColor } - r, _ := strconv.ParseUint(baseColor[0:2], 16, 64) + r, _ := strconv.ParseUint(baseColor[:2], 16, 64) g, _ := strconv.ParseUint(baseColor[2:4], 16, 64) b, _ := strconv.ParseUint(baseColor[4:6], 16, 64) var h, s, l float64 diff --git a/table.go b/table.go index 620cf20b35..a6959a42da 100644 --- a/table.go +++ b/table.go @@ -446,7 +446,7 @@ func (f *File) parseFilterExpression(expression string, tokens []string) ([]int, if re { conditional = 1 } - expression1, token1, err := f.parseFilterTokens(expression, tokens[0:3]) + expression1, token1, err := f.parseFilterTokens(expression, tokens[:3]) if err != nil { return expressions, t, err } From 6b277c61d22419b1006e9b5c557e1175fb2f2d99 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 18 Nov 2021 08:06:14 +0800 Subject: [PATCH 492/957] Fix sheet deletion fail in some case --- sheet.go | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/sheet.go b/sheet.go index 1aa378b52f..b9a0766f29 100644 --- a/sheet.go +++ b/sheet.go @@ -122,6 +122,19 @@ func (f *File) getWorkbookRelsPath() (path string) { return } +// getWorksheetPath construct a target XML as xl/worksheets/sheet%d by split +// path, compatible with different types of relative paths in +// workbook.xml.rels, for example: worksheets/sheet%d.xml +// 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), "/")) + if strings.HasPrefix(relTarget, "/") { + path = filepath.ToSlash(strings.TrimPrefix(strings.Replace(filepath.Clean(relTarget), "\\", "/", -1), "/")) + } + return path +} + // workbookReader provides a function to get the pointer to the workbook.xml // structure after deserialization. func (f *File) workbookReader() *xlsxWorkbook { @@ -491,15 +504,7 @@ 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 { - // Construct a target XML as xl/worksheets/sheet%d by split - // path, compatible with different types of relative paths in - // workbook.xml.rels, for example: worksheets/sheet%d.xml - // and /xl/worksheets/sheet%d.xml - path := filepath.ToSlash(strings.TrimPrefix( - strings.Replace(filepath.Clean(fmt.Sprintf("%s/%s", filepath.Dir(f.getWorkbookPath()), rel.Target)), "\\", "/", -1), "/")) - if strings.HasPrefix(rel.Target, "/") { - path = filepath.ToSlash(strings.TrimPrefix(strings.Replace(filepath.Clean(rel.Target), "\\", "/", -1), "/")) - } + path := f.getWorksheetPath(rel.Target) if _, ok := f.Pkg.Load(path); ok { maps[v.Name] = path } @@ -572,7 +577,7 @@ func (f *File) DeleteSheet(name string) { if wbRels != nil { for _, rel := range wbRels.Relationships { if rel.ID == sheet.ID { - sheetXML = rel.Target + sheetXML = f.getWorksheetPath(rel.Target) rels = "xl/worksheets/_rels/" + strings.TrimPrefix(f.sheetMap[sheetName], "xl/worksheets/") + ".rels" } } From 9b0aa7ac30c69dc0975c8945103dcf909d080912 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 20 Nov 2021 11:31:17 +0800 Subject: [PATCH 493/957] This closes #1060, fix build-in time number format parse error --- styles.go | 15 +++++++++++++-- styles_test.go | 1 + 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/styles.go b/styles.go index 183211b0a5..e32fb780a8 100644 --- a/styles.go +++ b/styles.go @@ -981,22 +981,33 @@ func parseTime(v string, format string) string { {"D", "2"}, {"yyyy", "2006"}, {"yy", "06"}, + {"MMMM", "%%%%"}, {"mmmm", "%%%%"}, + {"DDDD", "&&&&"}, {"dddd", "&&&&"}, + {"DD", "02"}, {"dd", "02"}, + {"D", "2"}, {"d", "2"}, + {"MMM", "Jan"}, {"mmm", "Jan"}, + {"MMSS", "0405"}, {"mmss", "0405"}, + {"SS", "05"}, {"ss", "05"}, {"s", "5"}, + {"MM:", "04:"}, {"mm:", "04:"}, + {":MM", ":04"}, {":mm", ":04"}, {"m:", "4:"}, {":m", ":4"}, + {"MM", "01"}, {"mm", "01"}, - {"am/pm", "pm"}, + {"AM/PM", "PM"}, + {"am/pm", "PM"}, + {"M/", "1/"}, {"m/", "1/"}, - {"m", "1"}, {"%%%%", "January"}, {"&&&&", "Monday"}, } diff --git a/styles_test.go b/styles_test.go index 9129914d63..19092afae5 100644 --- a/styles_test.go +++ b/styles_test.go @@ -307,6 +307,7 @@ func TestParseTime(t *testing.T) { assert.Equal(t, "3/4/2019 5:5:42", parseTime("43528.2123", "M/D/YYYY h:m:s")) assert.Equal(t, "3/4/2019 0:5:42", parseTime("43528.003958333335", "m/d/yyyy h:m:s")) assert.Equal(t, "3/4/2019 0:05:42", parseTime("43528.003958333335", "M/D/YYYY h:mm:s")) + assert.Equal(t, "3:30:00 PM", parseTime("0.64583333333333337", "h:mm:ss am/pm")) assert.Equal(t, "0:05", parseTime("43528.003958333335", "h:mm")) assert.Equal(t, "0:0", parseTime("6.9444444444444444E-5", "h:m")) assert.Equal(t, "0:00", parseTime("6.9444444444444444E-5", "h:mm")) From a6c8803e91018898f8e6f960340709e970a99560 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 21 Nov 2021 15:49:29 +0800 Subject: [PATCH 494/957] ref #65: new formula function XIRR --- calc.go | 88 ++++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 38 +++++++++++++++++++++++ 2 files changed, 126 insertions(+) diff --git a/calc.go b/calc.go index 1357ad4f90..21ece5d291 100644 --- a/calc.go +++ b/calc.go @@ -596,6 +596,7 @@ type formulaFuncs struct { // WEEKDAY // WEIBULL // WEIBULL.DIST +// XIRR // XNPV // XOR // YEAR @@ -11182,6 +11183,93 @@ func (fn *formulaFuncs) prepareXArgs(name string, values, dates formulaArg) (val return } +// xirr is an implementation of the formula function XIRR. +func (fn *formulaFuncs) xirr(values, dates []float64, guess float64) formulaArg { + positive, negative := false, false + for i := 0; i < len(values); i++ { + if values[i] > 0 { + positive = true + } + if values[i] < 0 { + negative = true + } + } + if !positive || !negative { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + result, epsMax, count, maxIterate, err := guess, 1e-10, 0, 50, false + for { + resultValue := xirrPart1(values, dates, result) + newRate := result - resultValue/xirrPart2(values, dates, result) + epsRate := math.Abs(newRate - result) + result = newRate + count++ + if epsRate <= epsMax || math.Abs(resultValue) <= epsMax { + break + } + if count > maxIterate { + err = true + break + } + } + if err || math.IsNaN(result) || math.IsInf(result, 0) { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + return newNumberFormulaArg(result) +} + +// xirrPart1 is a part of implementation of the formula function XIRR. +func xirrPart1(values, dates []float64, rate float64) float64 { + r := rate + 1 + result := values[0] + vlen := len(values) + firstDate := dates[0] + for i := 1; i < vlen; i++ { + result += values[i] / math.Pow(r, (dates[i]-firstDate)/365) + } + return result +} + +// xirrPart2 is a part of implementation of the formula function XIRR. +func xirrPart2(values, dates []float64, rate float64) float64 { + r := rate + 1 + result := 0.0 + vlen := len(values) + firstDate := dates[0] + for i := 1; i < vlen; i++ { + frac := (dates[i] - firstDate) / 365 + result -= frac * values[i] / math.Pow(r, frac+1) + } + return result +} + +// XIRR function returns the Internal Rate of Return for a supplied series of +// cash flows (i.e. a set of values, which includes an initial investment +// 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]) +// +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)) + if err.Type != ArgEmpty { + return err + } + guess := newNumberFormulaArg(0) + if argsList.Len() == 3 { + if guess = argsList.Back().Value.(formulaArg).ToNumber(); guess.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + if guess.Number <= -1 { + return newErrorFormulaArg(formulaErrorVALUE, "XIRR requires guess > -1") + } + } + return fn.xirr(values, dates, guess.Number) +} + // XNPV function calculates the Net Present Value for a schedule of cash flows // that is not necessarily periodic. The syntax of the function is: // diff --git a/calc_test.go b/calc_test.go index 894b154e0a..77c6e30efa 100644 --- a/calc_test.go +++ b/calc_test.go @@ -3456,6 +3456,44 @@ func TestCalcMIRR(t *testing.T) { } } +func TestCalcXIRR(t *testing.T) { + cellData := [][]interface{}{ + {-100.00, "01/01/2016"}, + {20.00, "04/01/2016"}, + {40.00, "10/01/2016"}, + {25.00, "02/01/2017"}, + {8.00, "03/01/2017"}, + {15.00, "06/01/2017"}, + {-1e-10, "09/01/2017"}} + 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", + } + 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{ + "=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!", + } + 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 TestCalcXNPV(t *testing.T) { cellData := [][]interface{}{{nil, 0.05}, {"01/01/2016", -10000, nil}, From 9561976074eb3f8260845735bf6028a5d5f3bd71 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 22 Nov 2021 00:39:39 +0800 Subject: [PATCH 495/957] ref #65: new formula function VDB --- calc.go | 138 +++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 27 ++++++++++ 2 files changed, 165 insertions(+) diff --git a/calc.go b/calc.go index 21ece5d291..381f11cc16 100644 --- a/calc.go +++ b/calc.go @@ -592,6 +592,7 @@ type formulaFuncs struct { // VARA // VARP // VARPA +// VDB // VLOOKUP // WEEKDAY // WEIBULL @@ -11145,6 +11146,143 @@ func (fn *formulaFuncs) TBILLYIELD(argsList *list.List) formulaArg { return newNumberFormulaArg(((100 - pr.Number) / pr.Number) * (360 / dsm)) } +// prepareVdbArgs checking and prepare arguments for the formula functions +// VDB. +func (fn *formulaFuncs) prepareVdbArgs(argsList *list.List) formulaArg { + cost := argsList.Front().Value.(formulaArg).ToNumber() + if cost.Type != ArgNumber { + return cost + } + if cost.Number < 0 { + return newErrorFormulaArg(formulaErrorNUM, "VDB requires cost >= 0") + } + salvage := argsList.Front().Next().Value.(formulaArg).ToNumber() + if salvage.Type != ArgNumber { + return salvage + } + if salvage.Number < 0 { + return newErrorFormulaArg(formulaErrorNUM, "VDB requires salvage >= 0") + } + life := argsList.Front().Next().Next().Value.(formulaArg).ToNumber() + if life.Type != ArgNumber { + return life + } + if life.Number <= 0 { + return newErrorFormulaArg(formulaErrorNUM, "VDB requires life > 0") + } + startPeriod := argsList.Front().Next().Next().Next().Value.(formulaArg).ToNumber() + if startPeriod.Type != ArgNumber { + return startPeriod + } + if startPeriod.Number < 0 { + return newErrorFormulaArg(formulaErrorNUM, "VDB requires start_period > 0") + } + endPeriod := argsList.Front().Next().Next().Next().Next().Value.(formulaArg).ToNumber() + if endPeriod.Type != ArgNumber { + return endPeriod + } + if startPeriod.Number > endPeriod.Number { + return newErrorFormulaArg(formulaErrorNUM, "VDB requires start_period <= end_period") + } + if endPeriod.Number > life.Number { + return newErrorFormulaArg(formulaErrorNUM, "VDB requires end_period <= life") + } + factor := newNumberFormulaArg(2) + if argsList.Len() > 5 { + if factor = argsList.Front().Next().Next().Next().Next().Next().Value.(formulaArg).ToNumber(); factor.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + if factor.Number < 0 { + return newErrorFormulaArg(formulaErrorVALUE, "VDB requires factor >= 0") + } + } + return newListFormulaArg([]formulaArg{cost, salvage, life, startPeriod, endPeriod, factor}) +} + +// vdb is a part of implementation of the formula function VDB. +func (fn *formulaFuncs) vdb(cost, salvage, life, life1, period, factor formulaArg) formulaArg { + var ddb, vdb, sln, term float64 + endInt, cs, nowSln := math.Ceil(period.Number), cost.Number-salvage.Number, false + ddbArgs := list.New() + for i := 1.0; i <= endInt; i++ { + if !nowSln { + ddbArgs.Init() + ddbArgs.PushBack(cost) + ddbArgs.PushBack(salvage) + ddbArgs.PushBack(life) + ddbArgs.PushBack(newNumberFormulaArg(i)) + ddbArgs.PushBack(factor) + ddb = fn.DDB(ddbArgs).Number + sln = cs / (life1.Number - i + 1) + if sln > ddb { + term = sln + nowSln = true + } else { + term = ddb + cs -= ddb + } + } else { + term = sln + } + if i == endInt { + term *= period.Number + 1 - endInt + } + vdb += term + } + return newNumberFormulaArg(vdb) +} + +// VDB function calculates the depreciation of an asset, using the Double +// Declining Balance Method, or another specified depreciation rate, for a +// specified period (including partial periods). The syntax of the function +// is: +// +// 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") + } + args := fn.prepareVdbArgs(argsList) + if args.Type != ArgList { + return args + } + cost, salvage, life, startPeriod, endPeriod, factor := args.List[0], args.List[1], args.List[2], args.List[3], args.List[4], args.List[5] + noSwitch := newBoolFormulaArg(false) + if argsList.Len() > 6 { + if noSwitch = argsList.Back().Value.(formulaArg).ToBool(); noSwitch.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + } + startInt, endInt, vdb, ddbArgs := math.Floor(startPeriod.Number), math.Ceil(endPeriod.Number), newNumberFormulaArg(0), list.New() + if noSwitch.Number == 1 { + for i := startInt + 1; i <= endInt; i++ { + ddbArgs.Init() + ddbArgs.PushBack(cost) + ddbArgs.PushBack(salvage) + ddbArgs.PushBack(life) + ddbArgs.PushBack(newNumberFormulaArg(i)) + ddbArgs.PushBack(factor) + term := fn.DDB(ddbArgs) + if i == startInt+1 { + term.Number *= math.Min(endPeriod.Number, startInt+1) - startPeriod.Number + } else if i == endInt { + term.Number *= endPeriod.Number + 1 - endInt + } + vdb.Number += term.Number + } + return vdb + } + life1, part := life, 0.0 + if startPeriod.Number != math.Floor(startPeriod.Number) && factor.Number > 1.0 && startPeriod.Number >= life.Number/2.0 { + part = startPeriod.Number - life.Number/2.0 + startPeriod.Number = life.Number / 2.0 + endPeriod.Number -= part + } + cost.Number -= fn.vdb(cost, salvage, life, life1, startPeriod, factor).Number + return fn.vdb(cost, salvage, life, newNumberFormulaArg(life.Number-startPeriod.Number), newNumberFormulaArg(endPeriod.Number-startPeriod.Number), factor) +} + // prepareXArgs prepare arguments for the formula function XIRR and XNPV. func (fn *formulaFuncs) prepareXArgs(name string, values, dates formulaArg) (valuesArg, datesArg []float64, err formulaArg) { for _, arg := range values.ToList() { diff --git a/calc_test.go b/calc_test.go index 77c6e30efa..c9896c76d4 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1537,6 +1537,17 @@ func TestCalcCellValue(t *testing.T) { "=TBILLPRICE(\"02/01/2017\",\"06/30/2017\",2.75%)": "98.86180555555556", // TBILLYIELD "=TBILLYIELD(\"02/01/2017\",\"06/30/2017\",99)": "0.024405125076266", + // VDB + "=VDB(10000,1000,5,0,1)": "4000", + "=VDB(10000,1000,5,1,3)": "3840", + "=VDB(10000,1000,5,3,5)": "1160", + "=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", // YIELDDISC "=YIELDDISC(\"01/01/2017\",\"06/30/2017\",97,100)": "0.0622012325059031", "=YIELDDISC(\"01/01/2017\",\"06/30/2017\",97,100,0)": "0.0622012325059031", @@ -3075,6 +3086,22 @@ func TestCalcCellValue(t *testing.T) { "=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!", + // 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!", // YIELDDISC "=YIELDDISC()": "YIELDDISC requires 4 or 5 arguments", "=YIELDDISC(\"\",\"06/30/2017\",97,100,0)": "#VALUE!", From 7907650a97115dbb771c7b977c5f260a1ff1cc65 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 24 Nov 2021 00:09:35 +0800 Subject: [PATCH 496/957] This closes #1069, support time zone location when set cell value --- cell.go | 2 ++ cell_test.go | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/cell.go b/cell.go index e1c78036f6..5c34bb9c3c 100644 --- a/cell.go +++ b/cell.go @@ -225,6 +225,8 @@ func (f *File) setCellTimeFunc(sheet, axis string, value time.Time) error { // timestamp. func setCellTime(value time.Time) (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) excelTime, err = timeToExcelTime(value) if err != nil { return diff --git a/cell_test.go b/cell_test.go index f699c05fb5..e49212f6a6 100644 --- a/cell_test.go +++ b/cell_test.go @@ -178,6 +178,24 @@ func TestSetCellBool(t *testing.T) { assert.EqualError(t, f.SetCellBool("Sheet1", "A", true), `cannot convert cell "A" to coordinates: invalid cell name "A"`) } +func TestSetCellTime(t *testing.T) { + date, err := time.Parse(time.RFC3339Nano, "2009-11-10T23:00:00Z") + assert.NoError(t, err) + for location, expected := range map[string]string{ + "America/New_York": "40127.75", + "Asia/Shanghai": "40128.291666666664", + "Europe/London": "40127.958333333336", + "UTC": "40127.958333333336", + } { + timezone, err := time.LoadLocation(location) + assert.NoError(t, err) + _, b, isNum, err := setCellTime(date.In(timezone)) + assert.NoError(t, err) + assert.Equal(t, true, isNum) + assert.Equal(t, expected, b) + } +} + func TestGetCellValue(t *testing.T) { // Test get cell value without r attribute of the row. f := NewFile() From f26df480e56561c30f9453a98ebf788acd48c3e2 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 25 Nov 2021 00:38:49 +0800 Subject: [PATCH 497/957] ref #65: new formula functions CONFIDENCE and CONFIDENCE.NORM --- calc.go | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++-- calc_test.go | 22 ++++++++++++++++++ 2 files changed, 85 insertions(+), 2 deletions(-) diff --git a/calc.go b/calc.go index 381f11cc16..a161dda0de 100644 --- a/calc.go +++ b/calc.go @@ -343,6 +343,8 @@ type formulaFuncs struct { // COMPLEX // CONCAT // CONCATENATE +// CONFIDENCE +// CONFIDENCE.NORM // COS // COSH // COT @@ -4945,6 +4947,65 @@ func (fn *formulaFuncs) CHIDIST(argsList *list.List) formulaArg { return newNumberFormulaArg(1 - (incompleteGamma(degress.Number/2, x.Number/2) / math.Gamma(degress.Number/2))) } +// confidence is an implementation of the formula function CONFIDENCE and +// CONFIDENCE.NORM. +func (fn *formulaFuncs) confidence(name string, argsList *list.List) formulaArg { + if argsList.Len() != 3 { + return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires 3 numeric arguments", name)) + } + alpha := argsList.Front().Value.(formulaArg).ToNumber() + if alpha.Type != ArgNumber { + return alpha + } + if alpha.Number <= 0 || alpha.Number >= 1 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + stdDev := argsList.Front().Next().Value.(formulaArg).ToNumber() + if stdDev.Type != ArgNumber { + return stdDev + } + if stdDev.Number <= 0 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + size := argsList.Back().Value.(formulaArg).ToNumber() + if size.Type != ArgNumber { + return size + } + if size.Number < 1 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + args := list.New() + args.Init() + args.PushBack(newNumberFormulaArg(alpha.Number / 2)) + args.PushBack(newNumberFormulaArg(0)) + args.PushBack(newNumberFormulaArg(1)) + return newNumberFormulaArg(-fn.NORMINV(args).Number * (stdDev.Number / math.Sqrt(size.Number))) +} + +// 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 +// that the standard deviation of the population is known. The syntax of the +// function is: +// +// CONFIDENCE(alpha,standard_dev,size) +// +func (fn *formulaFuncs) CONFIDENCE(argsList *list.List) formulaArg { + return fn.confidence("CONFIDENCE", argsList) +} + +// 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 +// assumed that the standard deviation of the population is known. The syntax +// of the Confidence.Norm function is: +// +// CONFIDENCE.NORM(alpha,standard_dev,size) +// +func (fn *formulaFuncs) CONFIDENCEdotNORM(argsList *list.List) formulaArg { + return fn.confidence("CONFIDENCE.NORM", argsList) +} + // calcStringCountSum is part of the implementation countSum. func calcStringCountSum(countText bool, count, sum float64, num, arg formulaArg) (float64, float64) { if countText && num.Type == ArgError && arg.String != "" { @@ -5517,7 +5578,7 @@ func norminv(p float64) (float64, error) { return 0, errors.New(formulaErrorNUM) } -// kth is an implementation of the formula function LARGE and SMALL. +// kth is an implementation of the formula functions LARGE and SMALL. func (fn *formulaFuncs) kth(name string, argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires 2 arguments", name)) @@ -11146,7 +11207,7 @@ func (fn *formulaFuncs) TBILLYIELD(argsList *list.List) formulaArg { return newNumberFormulaArg(((100 - pr.Number) / pr.Number) * (360 / dsm)) } -// prepareVdbArgs checking and prepare arguments for the formula functions +// prepareVdbArgs checking and prepare arguments for the formula function // VDB. func (fn *formulaFuncs) prepareVdbArgs(argsList *list.List) formulaArg { cost := argsList.Front().Value.(formulaArg).ToNumber() diff --git a/calc_test.go b/calc_test.go index c9896c76d4..880c40aafe 100644 --- a/calc_test.go +++ b/calc_test.go @@ -777,6 +777,10 @@ func TestCalcCellValue(t *testing.T) { // CHIDIST "=CHIDIST(0.5,3)": "0.918891411654676", "=CHIDIST(8,3)": "0.0460117056892315", + // CONFIDENCE + "=CONFIDENCE(0.05,0.07,100)": "0.0137197479028414", + // CONFIDENCE.NORM + "=CONFIDENCE.NORM(0.05,0.07,100)": "0.0137197479028414", // COUNT "=COUNT()": "0", "=COUNT(E1:F2,\"text\",1,INT(2))": "3", @@ -2133,6 +2137,24 @@ 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", + // 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.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!", // COUNTBLANK "=COUNTBLANK()": "COUNTBLANK requires 1 argument", "=COUNTBLANK(1,2)": "COUNTBLANK requires 1 argument", From 49c9ea40d7cf7c20e9b94723c84780f4d048f4a4 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 29 Nov 2021 01:21:03 +0800 Subject: [PATCH 498/957] ref #65: new formula function YIELD --- calc.go | 118 ++++++++++++++++++++++++++++++++++++++++++++++----- calc_test.go | 22 ++++++++++ merge.go | 8 ++-- styles.go | 4 +- 4 files changed, 135 insertions(+), 17 deletions(-) diff --git a/calc.go b/calc.go index a161dda0de..91199cc4ef 100644 --- a/calc.go +++ b/calc.go @@ -604,6 +604,7 @@ type formulaFuncs struct { // XOR // YEAR // YEARFRAC +// YIELD // YIELDDISC // YIELDMAT // Z.TEST @@ -1492,7 +1493,7 @@ func (fn *formulaFuncs) BESSELJ(argsList *list.List) formulaArg { return fn.bassel(argsList, false) } -// bassel is an implementation of the formula function BESSELI and BESSELJ. +// bassel is an implementation of the formula functions BESSELI and BESSELJ. func (fn *formulaFuncs) bassel(argsList *list.List, modfied bool) formulaArg { x, n := argsList.Front().Value.(formulaArg).ToNumber(), argsList.Back().Value.(formulaArg).ToNumber() if x.Type != ArgNumber { @@ -1826,7 +1827,7 @@ func (fn *formulaFuncs) BITXOR(argsList *list.List) formulaArg { return fn.bitwise("BITXOR", argsList) } -// bitwise is an implementation of the formula function BITAND, BITLSHIFT, +// bitwise is an implementation of the formula functions BITAND, BITLSHIFT, // BITOR, BITRSHIFT and BITXOR. func (fn *formulaFuncs) bitwise(name string, argsList *list.List) formulaArg { if argsList.Len() != 2 { @@ -1937,7 +1938,7 @@ func (fn *formulaFuncs) DEC2OCT(argsList *list.List) formulaArg { return fn.dec2x("DEC2OCT", argsList) } -// dec2x is an implementation of the formula function DEC2BIN, DEC2HEX and +// dec2x is an implementation of the formula functions DEC2BIN, DEC2HEX and // DEC2OCT. func (fn *formulaFuncs) dec2x(name string, argsList *list.List) formulaArg { if argsList.Len() < 1 { @@ -4586,7 +4587,7 @@ func calcStdev(stdeva bool, result, count float64, mean, token formulaArg) (floa return result, count } -// stdev is an implementation of the formula function STDEV and STDEVA. +// stdev is an implementation of the formula functions STDEV and STDEVA. func (fn *formulaFuncs) stdev(stdeva bool, argsList *list.List) formulaArg { count, result := -1.0, -1.0 var mean formulaArg @@ -4947,7 +4948,7 @@ func (fn *formulaFuncs) CHIDIST(argsList *list.List) formulaArg { return newNumberFormulaArg(1 - (incompleteGamma(degress.Number/2, x.Number/2) / math.Gamma(degress.Number/2))) } -// confidence is an implementation of the formula function CONFIDENCE and +// confidence is an implementation of the formula functions CONFIDENCE and // CONFIDENCE.NORM. func (fn *formulaFuncs) confidence(name string, argsList *list.List) formulaArg { if argsList.Len() != 3 { @@ -10735,12 +10736,21 @@ func (fn *formulaFuncs) price(settlement, maturity, rate, yld, redemption, frequ dsc := fn.COUPDAYSNC(argsList).Number / e.Number n := fn.COUPNUM(argsList) a := fn.COUPDAYBS(argsList) - ret := redemption.Number / math.Pow(1+yld.Number/frequency.Number, n.Number-1+dsc) - ret -= 100 * rate.Number / frequency.Number * a.Number / e.Number - t1 := 100 * rate.Number / frequency.Number - t2 := 1 + yld.Number/frequency.Number - for k := 0.0; k < n.Number; k++ { - ret += t1 / math.Pow(t2, k+dsc) + ret := 0.0 + if n.Number > 1 { + ret = redemption.Number / math.Pow(1+yld.Number/frequency.Number, n.Number-1+dsc) + ret -= 100 * rate.Number / frequency.Number * a.Number / e.Number + t1 := 100 * rate.Number / frequency.Number + t2 := 1 + yld.Number/frequency.Number + for k := 0.0; k < n.Number; k++ { + ret += t1 / math.Pow(t2, k+dsc) + } + } else { + dsc = e.Number - a.Number + t1 := 100*(rate.Number/frequency.Number) + redemption.Number + t2 := (yld.Number/frequency.Number)*(dsc/e.Number) + 1 + t3 := 100 * (rate.Number / frequency.Number) * (a.Number / e.Number) + ret = t1/t2 - t3 } return newNumberFormulaArg(ret) } @@ -11496,6 +11506,92 @@ func (fn *formulaFuncs) XNPV(argsList *list.List) formulaArg { return newNumberFormulaArg(xnpv) } +// yield is an implementation of the formula function YIELD. +func (fn *formulaFuncs) yield(settlement, maturity, rate, pr, redemption, frequency, basis formulaArg) formulaArg { + priceN, yield1, yield2 := newNumberFormulaArg(0), newNumberFormulaArg(0), newNumberFormulaArg(1) + price1 := fn.price(settlement, maturity, rate, yield1, redemption, frequency, basis) + if price1.Type != ArgNumber { + return price1 + } + price2 := fn.price(settlement, maturity, rate, yield2, redemption, frequency, basis) + yieldN := newNumberFormulaArg((yield2.Number - yield1.Number) * 0.5) + for iter := 0; iter < 100 && priceN.Number != pr.Number; iter++ { + priceN = fn.price(settlement, maturity, rate, yieldN, redemption, frequency, basis) + if pr.Number == price1.Number { + return yield1 + } else if pr.Number == price2.Number { + return yield2 + } else if pr.Number == priceN.Number { + return yieldN + } else if pr.Number < price2.Number { + yield2.Number *= 2.0 + price2 = fn.price(settlement, maturity, rate, yield2, redemption, frequency, basis) + yieldN.Number = (yield2.Number - yield1.Number) * 0.5 + } else { + if pr.Number < priceN.Number { + yield1 = yieldN + price1 = priceN + } else { + yield2 = yieldN + price2 = priceN + } + yieldN.Number = yield2.Number - (yield2.Number-yield1.Number)*((pr.Number-price2.Number)/(price1.Number-price2.Number)) + } + } + return yieldN +} + +// 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]) +// +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) +} + // YIELDDISC function calculates the annual yield of a discounted security. // The syntax of the function is: // diff --git a/calc_test.go b/calc_test.go index 880c40aafe..c18176f029 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1508,6 +1508,7 @@ func TestCalcCellValue(t *testing.T) { "=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", // PPMT "=PPMT(0.05/12,2,60,50000)": "-738.2918003208238", "=PPMT(0.035/4,2,8,0,5000,1)": "-606.1094824182949", @@ -1552,6 +1553,12 @@ func TestCalcCellValue(t *testing.T) { "=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", + // 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", + "=YIELD(\"01/01/2010\",\"06/30/2010\",0.5,1,1,1,4)": "1.91285866099894", + "=YIELD(\"01/01/2010\",\"06/30/2010\",0,1,1,1,4)": "0", + "=YIELD(\"01/01/2010\",\"01/02/2020\",100,68.15518653988686,1,1,1)": "64", // YIELDDISC "=YIELDDISC(\"01/01/2017\",\"06/30/2017\",97,100)": "0.0622012325059031", "=YIELDDISC(\"01/01/2017\",\"06/30/2017\",97,100,0)": "0.0622012325059031", @@ -3124,6 +3131,21 @@ func TestCalcCellValue(t *testing.T) { "=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!", + // 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(\"01/01/2010\",\"06/30/2015\",10%,101,100,4)": "PRICE requires rate >= 0", // YIELDDISC "=YIELDDISC()": "YIELDDISC requires 4 or 5 arguments", "=YIELDDISC(\"\",\"06/30/2017\",97,100,0)": "#VALUE!", diff --git a/merge.go b/merge.go index 1cd8acd92c..2d699a23f7 100644 --- a/merge.go +++ b/merge.go @@ -269,15 +269,15 @@ func (m *MergeCell) GetCellValue() string { return (*m)[1] } -// GetStartAxis returns the merge start axis. -// example: "C2" +// GetStartAxis returns the top left cell coordinates of merged range, for +// example: "C2". func (m *MergeCell) GetStartAxis() string { axis := strings.Split((*m)[0], ":") return axis[0] } -// GetEndAxis returns the merge end axis. -// example: "D4" +// GetEndAxis returns the bottom right cell coordinates of merged range, for +// example: "D4". func (m *MergeCell) GetEndAxis() string { axis := strings.Split((*m)[0], ":") return axis[1] diff --git a/styles.go b/styles.go index e32fb780a8..7f763770db 100644 --- a/styles.go +++ b/styles.go @@ -2151,8 +2151,8 @@ func (f *File) NewConditionalStyle(style string) (int, error) { return s.Dxfs.Count - 1, nil } -// GetDefaultFont provides the default font name currently set in the workbook -// Documents generated by excelize start with Calibri. +// 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 From bb0eb4a42be2c004a3c2ce59a8c748a9822b5f99 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 1 Dec 2021 00:10:31 +0800 Subject: [PATCH 499/957] This closes #1075, reload temporary files into memory on save --- file.go | 10 +++++++++- file_test.go | 9 +++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/file.go b/file.go index c0092a2eb4..a430c5f422 100644 --- a/file.go +++ b/file.go @@ -196,6 +196,14 @@ func (f *File) writeToZip(zw *zip.Writer) error { _, err = fi.Write(content.([]byte)) return true }) - + f.tempFiles.Range(func(path, content interface{}) bool { + var fi io.Writer + fi, err = zw.Create(path.(string)) + if err != nil { + return false + } + _, err = fi.Write(f.readBytes(path.(string))) + return true + }) return err } diff --git a/file_test.go b/file_test.go index ee5d322fae..8e65c5d46d 100644 --- a/file_test.go +++ b/file_test.go @@ -62,6 +62,15 @@ func TestWriteTo(t *testing.T) { _, err := f.WriteTo(bufio.NewWriter(&buf)) assert.Nil(t, err) } + // Test write with temporary file + { + f, buf := File{tempFiles: sync.Map{}}, bytes.Buffer{} + const maxUint16 = 1<<16 - 1 + f.tempFiles.Store("s", "") + f.tempFiles.Store(strings.Repeat("s", maxUint16+1), "") + _, err := f.WriteTo(bufio.NewWriter(&buf)) + assert.EqualError(t, err, "zip: FileHeader.Name too long") + } } func TestClose(t *testing.T) { From 45a1f08a2ad12ec613fd435ba5efcb830e617a71 Mon Sep 17 00:00:00 2001 From: Dokiy Date: Wed, 1 Dec 2021 19:11:51 +0800 Subject: [PATCH 500/957] Fix call getNumFmtID with builtInNumFmt return -1 --- styles.go | 6 +++--- styles_test.go | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/styles.go b/styles.go index 7f763770db..b5df352d47 100644 --- a/styles.go +++ b/styles.go @@ -2229,12 +2229,12 @@ func (f *File) newFont(style *Style) *xlsxFont { // If given number format code is not exist, will return -1. func getNumFmtID(styleSheet *xlsxStyleSheet, style *Style) (numFmtID int) { numFmtID = -1 - if styleSheet.NumFmts == nil { - return - } if _, ok := builtInNumFmt[style.NumFmt]; ok { return style.NumFmt } + if styleSheet.NumFmts == nil { + return + } if fmtCode, ok := currencyNumFmt[style.NumFmt]; ok { for _, numFmt := range styleSheet.NumFmts.NumFmt { if numFmt.FormatCode == fmtCode { diff --git a/styles_test.go b/styles_test.go index 19092afae5..69266eab62 100644 --- a/styles_test.go +++ b/styles_test.go @@ -330,3 +330,18 @@ func TestThemeColor(t *testing.T) { assert.Equal(t, clr[0], clr[1]) } } + +func TestGetNumFmtID(t *testing.T) { + f := NewFile() + + fs1, err := parseFormatStyleSet(`{"protection":{"hidden":false,"locked":false},"number_format":10}`) + assert.NoError(t, err) + id1 := getNumFmtID(&xlsxStyleSheet{}, fs1) + + fs2, err := parseFormatStyleSet(`{"protection":{"hidden":false,"locked":false},"number_format":0}`) + assert.NoError(t, err) + id2 := getNumFmtID(&xlsxStyleSheet{}, fs2) + + assert.NotEqual(t, id1, id2) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestStyleNumFmt.xlsx"))) +} \ No newline at end of file From aa359f1c748b5cbdc57ae032255e8b8940001e0b Mon Sep 17 00:00:00 2001 From: Michael Wiesenbauer Date: Thu, 2 Dec 2021 15:14:57 +0100 Subject: [PATCH 501/957] refactor DeleteSheet for better readability (#1078) Signed-off-by: Michael Wiesenbauer Co-authored-by: Michael Wiesenbauer --- sheet.go | 78 ++++++++++++++++++++++++++++++++------------------------ 1 file changed, 45 insertions(+), 33 deletions(-) diff --git a/sheet.go b/sheet.go index b9a0766f29..6244e02027 100644 --- a/sheet.go +++ b/sheet.go @@ -555,46 +555,58 @@ func (f *File) DeleteSheet(name string) { wbRels := f.relsReader(f.getWorkbookRelsPath()) activeSheetName := f.GetSheetName(f.GetActiveSheetIndex()) deleteLocalSheetID := f.GetSheetIndex(name) - // Delete and adjust defined names - if wb.DefinedNames != nil { - for idx := 0; idx < len(wb.DefinedNames.DefinedName); idx++ { - dn := wb.DefinedNames.DefinedName[idx] - if dn.LocalSheetID != nil { - localSheetID := *dn.LocalSheetID - if localSheetID == deleteLocalSheetID { - wb.DefinedNames.DefinedName = append(wb.DefinedNames.DefinedName[:idx], wb.DefinedNames.DefinedName[idx+1:]...) - idx-- - } else if localSheetID > deleteLocalSheetID { - wb.DefinedNames.DefinedName[idx].LocalSheetID = intPtr(*dn.LocalSheetID - 1) + deleteAndAdjustDefinedNames(wb, deleteLocalSheetID) + + for idx, sheet := range wb.Sheets.Sheet { + if !strings.EqualFold(sheet.Name, sheetName) { + continue + } + + wb.Sheets.Sheet = append(wb.Sheets.Sheet[:idx], wb.Sheets.Sheet[idx+1:]...) + var sheetXML, rels string + if wbRels != nil { + for _, rel := range wbRels.Relationships { + if rel.ID == sheet.ID { + sheetXML = f.getWorksheetPath(rel.Target) + rels = "xl/worksheets/_rels/" + strings.TrimPrefix(f.sheetMap[sheetName], "xl/worksheets/") + ".rels" } } } + target := f.deleteSheetFromWorkbookRels(sheet.ID) + f.deleteSheetFromContentTypes(target) + f.deleteCalcChain(sheet.SheetID, "") + delete(f.sheetMap, sheet.Name) + f.Pkg.Delete(sheetXML) + f.Pkg.Delete(rels) + f.Relationships.Delete(rels) + f.Sheet.Delete(sheetXML) + delete(f.xmlAttr, sheetXML) + f.SheetCount-- } - for idx, sheet := range wb.Sheets.Sheet { - if strings.EqualFold(sheet.Name, sheetName) { - wb.Sheets.Sheet = append(wb.Sheets.Sheet[:idx], wb.Sheets.Sheet[idx+1:]...) - var sheetXML, rels string - if wbRels != nil { - for _, rel := range wbRels.Relationships { - if rel.ID == sheet.ID { - sheetXML = f.getWorksheetPath(rel.Target) - rels = "xl/worksheets/_rels/" + strings.TrimPrefix(f.sheetMap[sheetName], "xl/worksheets/") + ".rels" - } - } + f.SetActiveSheet(f.GetSheetIndex(activeSheetName)) +} + +func deleteAndAdjustDefinedNames(wb *xlsxWorkbook, deleteLocalSheetID int) { + if wb == nil { + return + } + + if wb.DefinedNames == nil { + return + } + + for idx := 0; idx < len(wb.DefinedNames.DefinedName); idx++ { + dn := wb.DefinedNames.DefinedName[idx] + if dn.LocalSheetID != nil { + localSheetID := *dn.LocalSheetID + if localSheetID == deleteLocalSheetID { + wb.DefinedNames.DefinedName = append(wb.DefinedNames.DefinedName[:idx], wb.DefinedNames.DefinedName[idx+1:]...) + idx-- + } else if localSheetID > deleteLocalSheetID { + wb.DefinedNames.DefinedName[idx].LocalSheetID = intPtr(*dn.LocalSheetID - 1) } - target := f.deleteSheetFromWorkbookRels(sheet.ID) - f.deleteSheetFromContentTypes(target) - f.deleteCalcChain(sheet.SheetID, "") - delete(f.sheetMap, sheet.Name) - f.Pkg.Delete(sheetXML) - f.Pkg.Delete(rels) - f.Relationships.Delete(rels) - f.Sheet.Delete(sheetXML) - delete(f.xmlAttr, sheetXML) - f.SheetCount-- } } - f.SetActiveSheet(f.GetSheetIndex(activeSheetName)) } // deleteSheetFromWorkbookRels provides a function to remove worksheet From 577a07f08c6121d627323db00fdf9e74989a5515 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 3 Dec 2021 00:19:11 +0800 Subject: [PATCH 502/957] Simplify code and update unit test Improve unit test coverage for the functions: `NewStyle`, `SetActiveSheet`, `SearchSheet` and `deleteAndAdjustDefinedNames` Simplify code and add comments for the function: `deleteAndAdjustDefinedNames` --- col_test.go | 3 --- sheet.go | 9 +++------ sheet_test.go | 31 +++++++++++++++++++++++++++++++ styles.go | 22 ++++++++++++++-------- styles_test.go | 29 ++++++++++++++++++++++++++++- 5 files changed, 76 insertions(+), 18 deletions(-) diff --git a/col_test.go b/col_test.go index 80fc676708..da46f7856a 100644 --- a/col_test.go +++ b/col_test.go @@ -118,10 +118,7 @@ func TestGetColsError(t *testing.T) { _, err = f.GetCols("Sheet1") assert.EqualError(t, err, `strconv.Atoi: parsing "A": invalid syntax`) - f = NewFile() - f.Sheet.Delete("xl/worksheets/sheet1.xml") f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`B`)) - f.checked = nil _, err = f.GetCols("Sheet1") assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`) diff --git a/sheet.go b/sheet.go index 6244e02027..99ae1a2206 100644 --- a/sheet.go +++ b/sheet.go @@ -586,15 +586,12 @@ func (f *File) DeleteSheet(name string) { f.SetActiveSheet(f.GetSheetIndex(activeSheetName)) } +// deleteAndAdjustDefinedNames delete and adjust defined name in the workbook +// by given worksheet ID. func deleteAndAdjustDefinedNames(wb *xlsxWorkbook, deleteLocalSheetID int) { - if wb == nil { + if wb == nil || wb.DefinedNames == nil { return } - - if wb.DefinedNames == nil { - return - } - for idx := 0; idx < len(wb.DefinedNames.DefinedName); idx++ { dn := wb.DefinedNames.DefinedName[idx] if dn.LocalSheetID != nil { diff --git a/sheet_test.go b/sheet_test.go index 93a4ab6482..d33ba99dea 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -196,6 +196,24 @@ func TestSearchSheet(t *testing.T) { assert.NoError(t, f.SetCellValue("Sheet1", "A1", true)) _, err = f.SearchSheet("Sheet1", "") assert.NoError(t, err) + + f = NewFile() + f.Sheet.Delete("xl/worksheets/sheet1.xml") + 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`)) + result, err = f.SearchSheet("Sheet1", "A") + assert.EqualError(t, err, "cannot convert cell \"A\" to coordinates: invalid cell name \"A\"") + assert.Equal(t, []string(nil), result) + + 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.Equal(t, []string(nil), result) } func TestSetPageLayout(t *testing.T) { @@ -370,6 +388,14 @@ func TestSetActiveSheet(t *testing.T) { f = NewFile() f.SetActiveSheet(-1) assert.Equal(t, f.GetActiveSheetIndex(), 0) + + f = NewFile() + f.WorkBook.BookViews = nil + idx := f.NewSheet("Sheet2") + ws, ok = f.Sheet.Load("xl/worksheets/sheet2.xml") + assert.True(t, ok) + ws.(*xlsxWorksheet).SheetViews = &xlsxSheetViews{SheetView: []xlsxSheetView{}} + f.SetActiveSheet(idx) } func TestSetSheetName(t *testing.T) { @@ -414,6 +440,11 @@ func TestDeleteSheet(t *testing.T) { assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDeleteSheet2.xlsx"))) } +func TestDeleteAndAdjustDefinedNames(t *testing.T) { + deleteAndAdjustDefinedNames(nil, 0) + deleteAndAdjustDefinedNames(&xlsxWorkbook{}, 0) +} + func BenchmarkNewSheet(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { diff --git a/styles.go b/styles.go index b5df352d47..d9e3e8f2e3 100644 --- a/styles.go +++ b/styles.go @@ -2053,8 +2053,8 @@ func (f *File) NewStyle(style interface{}) (int, error) { var getXfIDFuncs = map[string]func(int, xlsxXf, *Style) bool{ "numFmt": func(numFmtID int, xf xlsxXf, style *Style) bool { - if style.NumFmt == 0 && style.CustomNumFmt == nil && numFmtID == -1 { - return xf.NumFmtID != nil || *xf.NumFmtID == 0 + if style.CustomNumFmt == nil && numFmtID == -1 { + return xf.NumFmtID != nil && *xf.NumFmtID == 0 } if style.NegRed || style.Lang != "" || style.DecimalPlaces != 2 { return false @@ -2232,14 +2232,20 @@ func getNumFmtID(styleSheet *xlsxStyleSheet, style *Style) (numFmtID int) { if _, ok := builtInNumFmt[style.NumFmt]; ok { return style.NumFmt } - if styleSheet.NumFmts == nil { - return + for lang, numFmt := range langNumFmt { + if _, ok := numFmt[style.NumFmt]; ok && lang == style.Lang { + numFmtID = style.NumFmt + return + } } if fmtCode, ok := currencyNumFmt[style.NumFmt]; ok { - for _, numFmt := range styleSheet.NumFmts.NumFmt { - if numFmt.FormatCode == fmtCode { - numFmtID = numFmt.NumFmtID - return + numFmtID = style.NumFmt + if styleSheet.NumFmts != nil { + for _, numFmt := range styleSheet.NumFmts.NumFmt { + if numFmt.FormatCode == fmtCode { + numFmtID = numFmt.NumFmtID + return + } } } } diff --git a/styles_test.go b/styles_test.go index 69266eab62..720340f52e 100644 --- a/styles_test.go +++ b/styles_test.go @@ -252,6 +252,33 @@ func TestNewStyle(t *testing.T) { rows, err := f.GetRows("Sheet1") assert.NoError(t, err) assert.Equal(t, [][]string{{"1.23E+00", "1.23E+00"}}, rows) + + f = NewFile() + // Test currency number format + customNumFmt := "[$$-409]#,##0.00" + style1, err := f.NewStyle(&Style{CustomNumFmt: &customNumFmt}) + assert.NoError(t, err) + style2, err := f.NewStyle(&Style{NumFmt: 165}) + assert.NoError(t, err) + assert.Equal(t, style1, style2) + + style3, err := f.NewStyle(&Style{NumFmt: 166}) + assert.NoError(t, err) + assert.Equal(t, 2, style3) + + f = NewFile() + f.Styles.NumFmts = nil + f.Styles.CellXfs.Xf = nil + style4, err := f.NewStyle(&Style{NumFmt: 160, Lang: "unknown"}) + assert.NoError(t, err) + assert.Equal(t, 1, 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) } func TestGetDefaultFont(t *testing.T) { @@ -344,4 +371,4 @@ func TestGetNumFmtID(t *testing.T) { assert.NotEqual(t, id1, id2) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestStyleNumFmt.xlsx"))) -} \ No newline at end of file +} From e0c6fa1beb0f1025489bbd21859bc9134c1d661a Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 4 Dec 2021 13:07:58 +0800 Subject: [PATCH 503/957] Update docs for SetSheetStyle, and added 2 formula functions ref #65: new formula functions DURATION and MDURATION fix incorrect example in SetSheetStyle docs --- calc.go | 110 +++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 30 ++++++++++++++ rows.go | 4 +- 3 files changed, 142 insertions(+), 2 deletions(-) diff --git a/calc.go b/calc.go index 91199cc4ef..0899508eeb 100644 --- a/calc.go +++ b/calc.go @@ -379,6 +379,7 @@ type formulaFuncs struct { // DISC // DOLLARDE // DOLLARFR +// DURATION // EFFECT // ENCODEURL // ERF @@ -471,6 +472,7 @@ type formulaFuncs struct { // MATCH // MAX // MDETERM +// MDURATION // MEDIAN // MID // MIDB @@ -10179,6 +10181,96 @@ func (fn *formulaFuncs) dollar(name string, argsList *list.List) formulaArg { return newNumberFormulaArg(math.Floor(dollar.Number) + cents) } +// prepareDurationArgs checking and prepare arguments for the formula +// functions DURATION and MDURATION. +func (fn *formulaFuncs) prepareDurationArgs(name string, argsList *list.List) formulaArg { + if argsList.Len() != 5 && argsList.Len() != 6 { + return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires 5 or 6 arguments", name)) + } + args := fn.prepareDataValueArgs(2, argsList) + if args.Type != ArgList { + return args + } + settlement, maturity := args.List[0], args.List[1] + if settlement.Number >= maturity.Number { + return newErrorFormulaArg(formulaErrorNUM, fmt.Sprintf("%s requires maturity > settlement", name)) + } + coupon := argsList.Front().Next().Next().Value.(formulaArg).ToNumber() + if coupon.Type != ArgNumber { + return coupon + } + if coupon.Number < 0 { + return newErrorFormulaArg(formulaErrorNUM, fmt.Sprintf("%s requires coupon >= 0", name)) + } + yld := argsList.Front().Next().Next().Next().Value.(formulaArg).ToNumber() + if yld.Type != ArgNumber { + return yld + } + if yld.Number < 0 { + return newErrorFormulaArg(formulaErrorNUM, fmt.Sprintf("%s requires yld >= 0", name)) + } + frequency := argsList.Front().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() == 6 { + if basis = argsList.Back().Value.(formulaArg).ToNumber(); basis.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + } + return newListFormulaArg([]formulaArg{settlement, maturity, coupon, yld, frequency, basis}) +} + +// duration is an implementation of the formula function DURATION. +func (fn *formulaFuncs) duration(settlement, maturity, coupon, yld, frequency, basis formulaArg) formulaArg { + frac := yearFrac(settlement.Number, maturity.Number, int(basis.Number)) + if frac.Type != ArgNumber { + return frac + } + argumments := list.New().Init() + argumments.PushBack(settlement) + argumments.PushBack(maturity) + argumments.PushBack(frequency) + argumments.PushBack(basis) + coups := fn.COUPNUM(argumments) + duration := 0.0 + p := 0.0 + coupon.Number *= 100 / frequency.Number + yld.Number /= frequency.Number + yld.Number++ + diff := frac.Number*frequency.Number - coups.Number + for t := 1.0; t < coups.Number; t++ { + tDiff := t + diff + add := coupon.Number / math.Pow(yld.Number, tDiff) + p += add + duration += tDiff * add + } + add := (coupon.Number + 100) / math.Pow(yld.Number, coups.Number+diff) + p += add + duration += (coups.Number + diff) * add + duration /= p + duration /= frequency.Number + return newNumberFormulaArg(duration) +} + +// DURATION function calculates the Duration (specifically, the Macaulay +// 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]) +// +func (fn *formulaFuncs) DURATION(argsList *list.List) formulaArg { + args := fn.prepareDurationArgs("DURATION", argsList) + if args.Type != ArgList { + return args + } + return fn.duration(args.List[0], args.List[1], args.List[2], args.List[3], args.List[4], args.List[5]) +} + // EFFECT function returns the effective annual interest rate for a given // nominal interest rate and number of compounding periods per year. The // syntax of the function is: @@ -10504,6 +10596,24 @@ func (fn *formulaFuncs) ISPMT(argsList *list.List) formulaArg { return newNumberFormulaArg(num) } +// MDURATION function calculates the Modified Macaulay Duration of a security +// that pays periodic interest, assuming a par value of $100. The syntax of +// the function is: +// +// MDURATION(settlement,maturity,coupon,yld,frequency,[basis]) +// +func (fn *formulaFuncs) MDURATION(argsList *list.List) formulaArg { + args := fn.prepareDurationArgs("MDURATION", argsList) + if args.Type != ArgList { + return args + } + duration := fn.duration(args.List[0], args.List[1], args.List[2], args.List[3], args.List[4], args.List[5]) + if duration.Type != ArgNumber { + return duration + } + return newNumberFormulaArg(duration.Number / (1 + args.List[3].Number/args.List[4].Number)) +} + // MIRR function returns the Modified Internal Rate of Return for a supplied // series of periodic cash flows (i.e. a set of values, which includes an // initial investment value and a series of net income values). The syntax of diff --git a/calc_test.go b/calc_test.go index c18176f029..2f1b310149 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1472,6 +1472,8 @@ func TestCalcCellValue(t *testing.T) { "=DOLLARDE(1.01,16)": "1.0625", // DOLLARFR "=DOLLARFR(1.0625,16)": "1.01", + // DURATION + "=DURATION(\"04/01/2015\",\"03/31/2025\",10%,8%,4)": "6.674422798483131", // EFFECT "=EFFECT(0.1,4)": "0.103812890625", "=EFFECT(0.025,2)": "0.02515625", @@ -1491,6 +1493,8 @@ func TestCalcCellValue(t *testing.T) { "=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", + // MDURATION + "=MDURATION(\"04/01/2015\",\"03/31/2025\",10%,8%,4)": "6.543551763218756", // NOMINAL "=NOMINAL(0.025,12)": "0.0247180352381129", // NPER @@ -2916,6 +2920,19 @@ func TestCalcCellValue(t *testing.T) { "=DOLLARFR(0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", "=DOLLARFR(0,-1)": "#NUM!", "=DOLLARFR(0,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", // EFFECT "=EFFECT()": "EFFECT requires 2 arguments", "=EFFECT(\"\",0)": "strconv.ParseFloat: parsing \"\": invalid syntax", @@ -2964,6 +2981,19 @@ func TestCalcCellValue(t *testing.T) { "=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", + // 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", // NOMINAL "=NOMINAL()": "NOMINAL requires 2 arguments", "=NOMINAL(\"\",0)": "strconv.ParseFloat: parsing \"\": invalid syntax", diff --git a/rows.go b/rows.go index 1e20f0a1e9..7b2f52ffaa 100644 --- a/rows.go +++ b/rows.go @@ -774,11 +774,11 @@ func checkRow(ws *xlsxWorksheet) error { // // For example set style of row 1 on Sheet1: // -// err = f.SetRowStyle("Sheet1", 1, style) +// err = f.SetRowStyle("Sheet1", 1, 1, styleID) // // Set style of rows 1 to 10 on Sheet1: // -// err = f.SetRowStyle("Sheet1", 1, 10, style) +// err = f.SetRowStyle("Sheet1", 1, 10, styleID) // func (f *File) SetRowStyle(sheet string, start, end, styleID int) error { if end < start { From 7af55a54552d36805ed9ad2e2173757fd6626a55 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 6 Dec 2021 08:16:32 +0800 Subject: [PATCH 504/957] ref #65: new formula function ODDFPRICE --- calc.go | 277 ++++++++++++++++++++++++++++++++++++++++++++++++++- calc_test.go | 29 ++++++ 2 files changed, 302 insertions(+), 4 deletions(-) diff --git a/calc.go b/calc.go index 0899508eeb..da32f9740b 100644 --- a/calc.go +++ b/calc.go @@ -504,6 +504,7 @@ type formulaFuncs struct { // OCT2DEC // OCT2HEX // ODD +// ODDFPRICE // OR // PDURATION // PERCENTILE.EXC @@ -9849,10 +9850,8 @@ func (fn *formulaFuncs) COUPNUM(argsList *list.List) formulaArg { if args.Type != ArgList { return args } - maturity, dateValue := timeFromExcelTime(args.List[1].Number, false), fn.COUPPCD(argsList) - date := timeFromExcelTime(dateValue.Number, false) - months := (maturity.Year()-date.Year())*12 + int(maturity.Month()) - int(date.Month()) - return newNumberFormulaArg(float64(months) * args.List[2].Number / 12.0) + frac := yearFrac(args.List[0].Number, args.List[1].Number, 0) + return newNumberFormulaArg(math.Ceil(frac.Number * args.List[2].Number)) } // COUPPCD function returns the previous coupon date, before the settlement @@ -10748,6 +10747,276 @@ func (fn *formulaFuncs) NPV(argsList *list.List) formulaArg { return newNumberFormulaArg(val) } +// 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{} + if startPeriod <= endPeriod { + for i := startPeriod; i <= endPeriod; i++ { + s = append(s, i) + } + } else { + for i := startPeriod; i >= endPeriod; i-- { + s = append(s, i) + } + } + return fold(f, initialValue, s) +} + +// fold is a part of implementation of the formula function ODDFPRICE. +func fold(f func(acc []float64, index float64) []float64, state []float64, source []float64) []float64 { + length, value := len(source), state + for index := 0; length > index; index++ { + value = f(value, source[index]) + } + return value +} + +// changeMonth is a part of implementation of the formula function ODDFPRICE. +func changeMonth(date time.Time, numMonths float64, returnLastMonth bool) time.Time { + offsetDay := 0 + if returnLastMonth && date.Day() == getDaysInMonth(date.Year(), int(date.Month())) { + offsetDay-- + } + newDate := date.AddDate(0, int(numMonths), offsetDay) + if returnLastMonth { + lastDay := getDaysInMonth(newDate.Year(), int(newDate.Month())) + return timeFromExcelTime(daysBetween(excelMinTime1900.Unix(), makeDate(newDate.Year(), newDate.Month(), lastDay))+1, false) + } + return newDate +} + +// 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) { + frontDate, trailingDate := startDate, endDate + s1 := frontDate.After(endDate) || frontDate.Equal(endDate) + s2 := endDate.After(frontDate) || endDate.Equal(frontDate) + stop := s2 + if numMonths > 0 { + stop = s1 + } + for !stop { + trailingDate = frontDate + frontDate = changeMonth(frontDate, numMonths, returnLastMonth) + fn := f(frontDate, trailingDate) + acc += fn + s1 = frontDate.After(endDate) || frontDate.Equal(endDate) + s2 = endDate.After(frontDate) || endDate.Equal(frontDate) + stop = s2 + if numMonths > 0 { + stop = s1 + } + } + return frontDate, trailingDate, acc +} + +// coupNumber is a part of implementation of the formula function ODDFPRICE. +func coupNumber(maturity, settlement, numMonths, basis 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() + couponsTemp, endOfMonthTemp := 0.0, getDaysInMonth(my, int(mm)) == md + endOfMonth := endOfMonthTemp + if !endOfMonthTemp && mm != 2 && md > 28 && md < getDaysInMonth(my, int(mm)) { + endOfMonth = getDaysInMonth(sy, int(sm)) == sd + } + startDate := changeMonth(settlementTime, 0, endOfMonth) + coupons := couponsTemp + if startDate.After(settlementTime) { + coupons++ + } + date := changeMonth(startDate, numMonths, endOfMonth) + f := func(pcd, ncd time.Time) float64 { + return 1 + } + _, _, result := datesAggregate(date, maturityTime, numMonths, basis, f, coupons, endOfMonth) + return result +} + +// prepareOddfpriceArgs checking and prepare arguments for the formula +// function ODDFPRICE. +func (fn *formulaFuncs) prepareOddfpriceArgs(argsList *list.List) formulaArg { + dateValues := fn.prepareDataValueArgs(4, argsList) + if dateValues.Type != ArgList { + return dateValues + } + settlement, maturity, issue, firstCoupon := dateValues.List[0], dateValues.List[1], dateValues.List[2], dateValues.List[3] + if issue.Number >= settlement.Number { + return newErrorFormulaArg(formulaErrorNUM, "ODDFPRICE requires settlement > issue") + } + if settlement.Number >= firstCoupon.Number { + return newErrorFormulaArg(formulaErrorNUM, "ODDFPRICE requires first_coupon > settlement") + } + if firstCoupon.Number >= maturity.Number { + return newErrorFormulaArg(formulaErrorNUM, "ODDFPRICE requires maturity > first_coupon") + } + rate := argsList.Front().Next().Next().Next().Next().Value.(formulaArg).ToNumber() + if rate.Type != ArgNumber { + return rate + } + if rate.Number < 0 { + return newErrorFormulaArg(formulaErrorNUM, "ODDFPRICE requires rate >= 0") + } + yld := argsList.Front().Next().Next().Next().Next().Next().Value.(formulaArg).ToNumber() + if yld.Type != ArgNumber { + return yld + } + if yld.Number < 0 { + return newErrorFormulaArg(formulaErrorNUM, "ODDFPRICE requires yld >= 0") + } + redemption := argsList.Front().Next().Next().Next().Next().Next().Next().Value.(formulaArg).ToNumber() + if redemption.Type != ArgNumber { + return redemption + } + if redemption.Number <= 0 { + return newErrorFormulaArg(formulaErrorNUM, "ODDFPRICE requires redemption > 0") + } + frequency := argsList.Front().Next().Next().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() == 9 { + if basis = argsList.Back().Value.(formulaArg).ToNumber(); basis.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + } + return newListFormulaArg([]formulaArg{settlement, maturity, issue, firstCoupon, rate, yld, redemption, frequency, basis}) +} + +// 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]) +// +func (fn *formulaFuncs) ODDFPRICE(argsList *list.List) formulaArg { + if argsList.Len() != 8 && argsList.Len() != 9 { + return newErrorFormulaArg(formulaErrorVALUE, "ODDFPRICE requires 8 or 9 arguments") + } + args := fn.prepareOddfpriceArgs(argsList) + if args.Type != ArgList { + return args + } + settlement, maturity, issue, firstCoupon, rate, yld, redemption, frequency, basisArg := + args.List[0], args.List[1], args.List[2], args.List[3], args.List[4], args.List[5], args.List[6], args.List[7], args.List[8] + if basisArg.Number < 0 || basisArg.Number > 4 { + return newErrorFormulaArg(formulaErrorNUM, "invalid basis") + } + issueTime := timeFromExcelTime(issue.Number, false) + settlementTime := timeFromExcelTime(settlement.Number, false) + maturityTime := timeFromExcelTime(maturity.Number, false) + firstCouponTime := timeFromExcelTime(firstCoupon.Number, false) + basis := int(basisArg.Number) + monthDays := getDaysInMonth(maturityTime.Year(), int(maturityTime.Month())) + returnLastMonth := monthDays == maturityTime.Day() + numMonths := 12 / frequency.Number + numMonthsNeg := -numMonths + mat := changeMonth(maturityTime, numMonthsNeg, returnLastMonth) + pcd, _, _ := datesAggregate(mat, firstCouponTime, numMonthsNeg, basisArg.Number, func(d1, d2 time.Time) float64 { + return 0 + }, 0, returnLastMonth) + if !pcd.Equal(firstCouponTime) { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + fnArgs := list.New().Init() + fnArgs.PushBack(settlement) + fnArgs.PushBack(maturity) + fnArgs.PushBack(frequency) + fnArgs.PushBack(basisArg) + e := fn.COUPDAYS(fnArgs) + n := fn.COUPNUM(fnArgs) + m := frequency.Number + dfc := coupdays(issueTime, firstCouponTime, basis) + if dfc < e.Number { + dsc := coupdays(settlementTime, firstCouponTime, basis) + a := coupdays(issueTime, settlementTime, basis) + x := yld.Number/m + 1 + y := dsc / e.Number + p1 := x + p3 := math.Pow(p1, n.Number-1+y) + term1 := redemption.Number / p3 + term2 := 100 * rate.Number / m * dfc / e.Number / math.Pow(p1, y) + f := func(acc []float64, index float64) []float64 { + return []float64{acc[0] + 100*rate.Number/m/math.Pow(p1, index-1+y)} + } + term3 := aggrBetween(2, math.Floor(n.Number), []float64{0}, f) + p2 := rate.Number / m + term4 := a / e.Number * p2 * 100 + return newNumberFormulaArg(term1 + term2 + term3[0] - term4) + } + fnArgs.Init() + fnArgs.PushBack(issue) + fnArgs.PushBack(firstCoupon) + fnArgs.PushBack(frequency) + nc := fn.COUPNUM(fnArgs) + lastCoupon := firstCoupon.Number + aggrFunc := func(acc []float64, index float64) []float64 { + lastCouponTime := timeFromExcelTime(lastCoupon, false) + earlyCoupon := daysBetween(excelMinTime1900.Unix(), makeDate(lastCouponTime.Year(), time.Month(float64(lastCouponTime.Month())+numMonthsNeg), lastCouponTime.Day())) + 1 + earlyCouponTime := timeFromExcelTime(earlyCoupon, false) + nl := e.Number + if basis == 1 { + nl = coupdays(earlyCouponTime, lastCouponTime, basis) + } + dci := coupdays(issueTime, lastCouponTime, basis) + if index > 1 { + dci = nl + } + startDate := earlyCoupon + if issue.Number > earlyCoupon { + startDate = issue.Number + } + endDate := lastCoupon + if settlement.Number < lastCoupon { + endDate = settlement.Number + } + startDateTime := timeFromExcelTime(startDate, false) + endDateTime := timeFromExcelTime(endDate, false) + a := coupdays(startDateTime, endDateTime, basis) + lastCoupon = earlyCoupon + dcnl := acc[0] + anl := acc[1] + return []float64{dcnl + dci/nl, anl + a/nl} + } + ag := aggrBetween(math.Floor(nc.Number), 1, []float64{0, 0}, aggrFunc) + dcnl, anl := ag[0], ag[1] + dsc := 0.0 + fnArgs.Init() + fnArgs.PushBack(settlement) + fnArgs.PushBack(firstCoupon) + fnArgs.PushBack(frequency) + if basis == 2 || basis == 3 { + d := timeFromExcelTime(fn.COUPNCD(fnArgs).Number, false) + dsc = coupdays(settlementTime, d, basis) + } else { + d := timeFromExcelTime(fn.COUPPCD(fnArgs).Number, false) + a := coupdays(d, settlementTime, basis) + dsc = e.Number - a + } + nq := coupNumber(firstCoupon.Number, settlement.Number, numMonths, basisArg.Number) + fnArgs.Init() + fnArgs.PushBack(firstCoupon) + fnArgs.PushBack(maturity) + fnArgs.PushBack(frequency) + fnArgs.PushBack(basisArg) + n = fn.COUPNUM(fnArgs) + x := yld.Number/m + 1 + y := dsc / e.Number + p1 := x + p3 := math.Pow(p1, y+nq+n.Number) + term1 := redemption.Number / p3 + term2 := 100 * rate.Number / m * dcnl / math.Pow(p1, nq+y) + f := func(acc []float64, index float64) []float64 { + return []float64{acc[0] + 100*rate.Number/m/math.Pow(p1, index+nq+y)} + } + term3 := aggrBetween(1, math.Floor(n.Number), []float64{0}, f) + term4 := 100 * rate.Number / m * anl + return newNumberFormulaArg(term1 + term2 + term3[0] - term4) +} + // PDURATION function calculates the number of periods required for an // investment to reach a specified future value. The syntax of the function // is: diff --git a/calc_test.go b/calc_test.go index 2f1b310149..4c4839a9e0 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1443,6 +1443,7 @@ func TestCalcCellValue(t *testing.T) { // COUPNUM "=COUPNUM(\"01/01/2011\",\"10/25/2012\",4)": "8", "=COUPNUM(\"01/01/2011\",\"10/25/2012\",4,0)": "8", + "=COUPNUM(\"09/30/2017\",\"03/31/2021\",4,0)": "14", // COUPPCD "=COUPPCD(\"01/01/2011\",\"10/25/2012\",4)": "40476", "=COUPPCD(\"01/01/2011\",\"10/25/2012\",4,0)": "40476", @@ -1503,6 +1504,14 @@ func TestCalcCellValue(t *testing.T) { "=NPER(0.06/4,-2000,60000,30000,1)": "52.794773709274764", // NPV "=NPV(0.02,-5000,\"\",800)": "-4133.025759323337", + // 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", // PDURATION "=PDURATION(0.04,10000,15000)": "10.33803507150765", // PMT @@ -3013,6 +3022,26 @@ func TestCalcCellValue(t *testing.T) { // NPV "=NPV()": "NPV requires at least 2 arguments", "=NPV(\"\",0)": "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", // PDURATION "=PDURATION()": "PDURATION requires 3 arguments", "=PDURATION(\"\",0,0)": "strconv.ParseFloat: parsing \"\": invalid syntax", From 3325c3946d0ab77083555bab334381a1167ee580 Mon Sep 17 00:00:00 2001 From: Dokiy <49900744+Dokiys@users.noreply.github.com> Date: Mon, 6 Dec 2021 22:37:25 +0800 Subject: [PATCH 505/957] Fix adjustMergeCellsHelper and add some test cases (#1082) Signed-off-by: Dokiys --- adjust.go | 42 +++++++--- adjust_test.go | 207 ++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 233 insertions(+), 16 deletions(-) diff --git a/adjust.go b/adjust.go index 9f2176feb6..243c774504 100644 --- a/adjust.go +++ b/adjust.go @@ -208,20 +208,23 @@ func (f *File) adjustMergeCells(ws *xlsxWorksheet, dir adjustDirection, num, off if y1 == num && y2 == num && offset < 0 { f.deleteMergeCell(ws, i) i-- + continue } - y1 = f.adjustMergeCellsHelper(y1, num, offset) - y2 = f.adjustMergeCellsHelper(y2, num, offset) + + y1, y2 = f.adjustMergeCellsHelper(y1, y2, num, offset) } else { if x1 == num && x2 == num && offset < 0 { f.deleteMergeCell(ws, i) i-- + continue } - x1 = f.adjustMergeCellsHelper(x1, num, offset) - x2 = f.adjustMergeCellsHelper(x2, num, offset) + + x1, x2 = f.adjustMergeCellsHelper(x1, x2, num, offset) } - if x1 == x2 && y1 == y2 && i >= 0 { + if x1 == x2 && y1 == y2 { f.deleteMergeCell(ws, i) i-- + continue } if areaData.Ref, err = f.coordinatesToAreaRef([]int{x1, y1, x2, y2}); err != nil { return err @@ -233,19 +236,34 @@ 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 // offset. -func (f *File) adjustMergeCellsHelper(pivot, num, offset int) int { - if pivot > num { - pivot += offset - if pivot < 1 { - return 1 +func (f *File) adjustMergeCellsHelper(p1, p2, num, offset int) (int, int) { + if p2 < p1 { + p1, p2 = p2, p1 + } + + if offset >= 0 { + if num <= p1 { + p1 += offset + p2 += offset + } else if num <= p2 { + p2 += offset } - return pivot + return p1, p2 + } + if num < p1 || (num == p1 && num == p2) { + p1 += offset + p2 += offset + } else if num <= p2 { + p2 += offset } - return pivot + return p1, p2 } // deleteMergeCell provides a function to delete merged cell by given index. func (f *File) deleteMergeCell(ws *xlsxWorksheet, idx int) { + if idx < 0 { + return + } if len(ws.MergeCells.Cells) > idx { ws.MergeCells.Cells = append(ws.MergeCells.Cells[:idx], ws.MergeCells.Cells[idx+1:]...) ws.MergeCells.Count = len(ws.MergeCells.Cells) diff --git a/adjust_test.go b/adjust_test.go index f56f7631ba..3509b5d536 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -45,6 +45,209 @@ func TestAdjustMergeCells(t *testing.T) { }, }, }, columns, 1, -1)) + + // testing adjustMergeCells + var cases []struct { + lable string + ws *xlsxWorksheet + dir adjustDirection + num int + offset int + expect string + } + + // testing insert + cases = []struct { + lable string + ws *xlsxWorksheet + dir adjustDirection + num int + offset int + expect string + }{ + { + lable: "insert row on ref", + ws: &xlsxWorksheet{ + MergeCells: &xlsxMergeCells{ + Cells: []*xlsxMergeCell{ + { + Ref: "A2:B3", + }, + }, + }, + }, + dir: rows, + num: 2, + offset: 1, + expect: "A3:B4", + }, + { + lable: "insert row on bottom of ref", + ws: &xlsxWorksheet{ + MergeCells: &xlsxMergeCells{ + Cells: []*xlsxMergeCell{ + { + Ref: "A2:B3", + }, + }, + }, + }, + dir: rows, + num: 3, + offset: 1, + expect: "A2:B4", + }, + { + lable: "insert column on the left", + ws: &xlsxWorksheet{ + MergeCells: &xlsxMergeCells{ + Cells: []*xlsxMergeCell{ + { + Ref: "A2:B3", + }, + }, + }, + }, + dir: columns, + num: 1, + offset: 1, + expect: "B2:C3", + }, + } + for _, c := range cases { + assert.NoError(t, f.adjustMergeCells(c.ws, c.dir, c.num, 1)) + assert.Equal(t, c.expect, c.ws.MergeCells.Cells[0].Ref, c.lable) + } + + // testing delete + cases = []struct { + lable string + ws *xlsxWorksheet + dir adjustDirection + num int + offset int + expect string + }{ + { + lable: "delete row on top of ref", + ws: &xlsxWorksheet{ + MergeCells: &xlsxMergeCells{ + Cells: []*xlsxMergeCell{ + { + Ref: "A2:B3", + }, + }, + }, + }, + dir: rows, + num: 2, + offset: -1, + expect: "A2:B2", + }, + { + lable: "delete row on bottom of ref", + ws: &xlsxWorksheet{ + MergeCells: &xlsxMergeCells{ + Cells: []*xlsxMergeCell{ + { + Ref: "A2:B3", + }, + }, + }, + }, + dir: rows, + num: 3, + offset: -1, + expect: "A2:B2", + }, + { + lable: "delete column on the ref left", + ws: &xlsxWorksheet{ + MergeCells: &xlsxMergeCells{ + Cells: []*xlsxMergeCell{ + { + Ref: "A2:B3", + }, + }, + }, + }, + dir: columns, + num: 1, + offset: -1, + expect: "A2:A3", + }, + { + lable: "delete column on the ref right", + ws: &xlsxWorksheet{ + MergeCells: &xlsxMergeCells{ + Cells: []*xlsxMergeCell{ + { + Ref: "A2:B3", + }, + }, + }, + }, + dir: columns, + num: 2, + offset: -1, + expect: "A2:A3", + }, + } + for _, c := range cases { + assert.NoError(t, f.adjustMergeCells(c.ws, c.dir, c.num, -1)) + assert.Equal(t, c.expect, c.ws.MergeCells.Cells[0].Ref, c.lable) + } + + // testing delete one row/column + cases = []struct { + lable string + ws *xlsxWorksheet + dir adjustDirection + num int + offset int + expect string + }{ + { + lable: "delete one row ref", + ws: &xlsxWorksheet{ + MergeCells: &xlsxMergeCells{ + Cells: []*xlsxMergeCell{ + { + Ref: "A1:B1", + }, + }, + }, + }, + dir: rows, + num: 1, + offset: -1, + }, + { + lable: "delete one column ref", + ws: &xlsxWorksheet{ + MergeCells: &xlsxMergeCells{ + Cells: []*xlsxMergeCell{ + { + Ref: "A1:A2", + }, + }, + }, + }, + dir: columns, + num: 1, + offset: -1, + }, + } + 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.lable) + } + + f = NewFile() + p1, p2 := f.adjustMergeCellsHelper(2, 1, 0, 0) + assert.Equal(t, 1, p1) + assert.Equal(t, 2, p2) + f.deleteMergeCell(nil, -1) } func TestAdjustAutoFilter(t *testing.T) { @@ -84,10 +287,6 @@ func TestAdjustHelper(t *testing.T) { assert.EqualError(t, f.adjustHelper("SheetN", rows, 0, 0), "sheet SheetN is not exist") } -func TestAdjustMergeCellsHelper(t *testing.T) { - assert.Equal(t, 1, NewFile().adjustMergeCellsHelper(1, 0, -2)) -} - func TestAdjustCalcChain(t *testing.T) { f := NewFile() f.CalcChain = &xlsxCalcChain{ From 44a13aa402b0189b119635d2f0a26961795c6bda Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 7 Dec 2021 00:26:53 +0800 Subject: [PATCH 506/957] Export 7 errors so users can act differently on different type of errors --- adjust_test.go | 14 +++++++------- calc_test.go | 22 +++++++++++----------- cell_test.go | 20 ++++++++++---------- chart_test.go | 6 +++--- col_test.go | 32 ++++++++++++++++---------------- comment_test.go | 2 +- crypt_test.go | 6 +++--- datavalidation_test.go | 6 +++--- date_test.go | 2 +- errors.go | 26 ++++++++++++++++++++++++++ excelize_test.go | 28 ++++++++++++++-------------- file.go | 3 +-- lib.go | 7 ++----- lib_test.go | 2 +- merge_test.go | 6 +++--- picture_test.go | 12 ++++++------ pivotTable_test.go | 4 ++-- rows_test.go | 26 +++++++++++++------------- shape_test.go | 2 +- sheet_test.go | 12 ++++++------ sparkline.go | 11 +++++------ sparkline_test.go | 14 +++++++------- stream_test.go | 8 ++++---- table_test.go | 10 +++++----- 24 files changed, 151 insertions(+), 130 deletions(-) diff --git a/adjust_test.go b/adjust_test.go index 3509b5d536..7a482f7193 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -17,7 +17,7 @@ func TestAdjustMergeCells(t *testing.T) { }, }, }, - }, rows, 0, 0), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + }, rows, 0, 0), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.EqualError(t, f.adjustMergeCells(&xlsxWorksheet{ MergeCells: &xlsxMergeCells{ Cells: []*xlsxMergeCell{ @@ -26,7 +26,7 @@ func TestAdjustMergeCells(t *testing.T) { }, }, }, - }, rows, 0, 0), `cannot convert cell "B" to coordinates: invalid cell name "B"`) + }, rows, 0, 0), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")).Error()) assert.NoError(t, f.adjustMergeCells(&xlsxWorksheet{ MergeCells: &xlsxMergeCells{ Cells: []*xlsxMergeCell{ @@ -265,12 +265,12 @@ func TestAdjustAutoFilter(t *testing.T) { AutoFilter: &xlsxAutoFilter{ Ref: "A:B1", }, - }, rows, 0, 0), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + }, rows, 0, 0), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.EqualError(t, f.adjustAutoFilter(&xlsxWorksheet{ AutoFilter: &xlsxAutoFilter{ Ref: "A1:B", }, - }, rows, 0, 0), `cannot convert cell "B" to coordinates: invalid cell name "B"`) + }, rows, 0, 0), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")).Error()) } func TestAdjustHelper(t *testing.T) { @@ -281,8 +281,8 @@ func TestAdjustHelper(t *testing.T) { f.Sheet.Store("xl/worksheets/sheet2.xml", &xlsxWorksheet{ AutoFilter: &xlsxAutoFilter{Ref: "A1:B"}}) // testing adjustHelper with illegal cell coordinates. - assert.EqualError(t, f.adjustHelper("Sheet1", rows, 0, 0), `cannot convert cell "A" to coordinates: invalid cell name "A"`) - assert.EqualError(t, f.adjustHelper("Sheet2", rows, 0, 0), `cannot convert cell "B" to coordinates: invalid cell name "B"`) + 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. assert.EqualError(t, f.adjustHelper("SheetN", rows, 0, 0), "sheet SheetN is not exist") } @@ -298,7 +298,7 @@ func TestAdjustCalcChain(t *testing.T) { assert.NoError(t, f.InsertRow("Sheet1", 1)) f.CalcChain.C[1].R = "invalid coordinates" - assert.EqualError(t, f.InsertCol("Sheet1", "A"), `cannot convert cell "invalid coordinates" to coordinates: invalid cell name "invalid coordinates"`) + assert.EqualError(t, f.InsertCol("Sheet1", "A"), newCellNameToCoordinatesError("invalid coordinates", newInvalidCellNameError("invalid coordinates")).Error()) f.CalcChain = nil assert.NoError(t, f.InsertCol("Sheet1", "A")) } diff --git a/calc_test.go b/calc_test.go index 4c4839a9e0..b1d4f26d21 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1857,7 +1857,7 @@ func TestCalcCellValue(t *testing.T) { // ABS "=ABS()": "ABS requires 1 numeric argument", `=ABS("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", - "=ABS(~)": `invalid column name "~"`, + "=ABS(~)": newInvalidColumnNameError("~").Error(), // ACOS "=ACOS()": "ACOS requires 1 numeric argument", `=ACOS("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", @@ -2699,15 +2699,15 @@ func TestCalcCellValue(t *testing.T) { // COLUMN "=COLUMN(1,2)": "COLUMN requires at most 1 argument", "=COLUMN(\"\")": "invalid reference", - "=COLUMN(Sheet1)": "invalid column name \"Sheet1\"", - "=COLUMN(Sheet1!A1!B1)": "invalid column name \"Sheet1\"", + "=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)": "invalid column name \"Sheet1\"", - "=COLUMNS(Sheet1!A1!B1)": "invalid column name \"Sheet1\"", - "=COLUMNS(Sheet1!Sheet1)": "invalid column name \"Sheet1\"", + "=COLUMNS(Sheet1)": newInvalidColumnNameError("Sheet1").Error(), + "=COLUMNS(Sheet1!A1!B1)": newInvalidColumnNameError("Sheet1").Error(), + "=COLUMNS(Sheet1!Sheet1)": newInvalidColumnNameError("Sheet1").Error(), // HLOOKUP "=HLOOKUP()": "HLOOKUP requires at least 3 arguments", "=HLOOKUP(D2,D1,1,FALSE)": "HLOOKUP requires second argument of table array", @@ -2751,15 +2751,15 @@ func TestCalcCellValue(t *testing.T) { // ROW "=ROW(1,2)": "ROW requires at most 1 argument", "=ROW(\"\")": "invalid reference", - "=ROW(Sheet1)": "invalid column name \"Sheet1\"", - "=ROW(Sheet1!A1!B1)": "invalid column name \"Sheet1\"", + "=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)": "invalid column name \"Sheet1\"", - "=ROWS(Sheet1!A1!B1)": "invalid column name \"Sheet1\"", - "=ROWS(Sheet1!Sheet1)": "invalid column name \"Sheet1\"", + "=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", diff --git a/cell_test.go b/cell_test.go index e49212f6a6..4a78a0676d 100644 --- a/cell_test.go +++ b/cell_test.go @@ -108,11 +108,11 @@ func TestCheckCellInArea(t *testing.T) { } ok, err := f.checkCellInArea("A1", "A:B") - assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.False(t, ok) ok, err = f.checkCellInArea("AA0", "Z0:AB1") - assert.EqualError(t, err, `cannot convert cell "AA0" to coordinates: invalid cell name "AA0"`) + assert.EqualError(t, err, newCellNameToCoordinatesError("AA0", newInvalidCellNameError("AA0")).Error()) assert.False(t, ok) } @@ -146,13 +146,13 @@ func TestSetCellFloat(t *testing.T) { assert.Equal(t, "123.42", val, "A1 should be 123.42") }) f := NewFile() - assert.EqualError(t, f.SetCellFloat(sheet, "A", 123.42, -1, 64), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.EqualError(t, f.SetCellFloat(sheet, "A", 123.42, -1, 64), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) } func TestSetCellValue(t *testing.T) { f := NewFile() - assert.EqualError(t, f.SetCellValue("Sheet1", "A", time.Now().UTC()), `cannot convert cell "A" to coordinates: invalid cell name "A"`) - assert.EqualError(t, f.SetCellValue("Sheet1", "A", time.Duration(1e13)), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + 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()) } func TestSetCellValues(t *testing.T) { @@ -175,7 +175,7 @@ func TestSetCellValues(t *testing.T) { func TestSetCellBool(t *testing.T) { f := NewFile() - assert.EqualError(t, f.SetCellBool("Sheet1", "A", true), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.EqualError(t, f.SetCellBool("Sheet1", "A", true), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) } func TestSetCellTime(t *testing.T) { @@ -336,7 +336,7 @@ func TestGetCellType(t *testing.T) { assert.NoError(t, err) assert.Equal(t, CellTypeString, cellType) _, err = f.GetCellType("Sheet1", "A") - assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) } func TestGetCellFormula(t *testing.T) { @@ -420,7 +420,7 @@ func TestSetCellFormula(t *testing.T) { assert.NoError(t, f.SetCellFormula("Sheet1", "C19", "SUM(Sheet2!D2,Sheet2!D9)")) // Test set cell formula with illegal rows number. - assert.EqualError(t, f.SetCellFormula("Sheet1", "C", "SUM(Sheet2!D2,Sheet2!D9)"), `cannot convert cell "C" to coordinates: invalid cell name "C"`) + 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"))) assert.NoError(t, f.Close()) @@ -523,7 +523,7 @@ func TestGetCellRichText(t *testing.T) { assert.EqualError(t, err, "sheet SheetN is not exist") // Test set cell rich text with illegal cell coordinates _, err = f.GetCellRichText("Sheet1", "A") - assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) } func TestSetCellRichText(t *testing.T) { f := NewFile() @@ -603,7 +603,7 @@ func TestSetCellRichText(t *testing.T) { // Test set cell rich text on not exists worksheet assert.EqualError(t, f.SetCellRichText("SheetN", "A1", richTextRun), "sheet SheetN is not exist") // Test set cell rich text with illegal cell coordinates - assert.EqualError(t, f.SetCellRichText("Sheet1", "A", richTextRun), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + 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 assert.EqualError(t, f.SetCellRichText("Sheet1", "A1", richTextRun), ErrCellCharsLength.Error()) diff --git a/chart_test.go b/chart_test.go index 2cd7131617..c99bfb2ce6 100644 --- a/chart_test.go +++ b/chart_test.go @@ -94,7 +94,7 @@ func TestChartSize(t *testing.T) { func TestAddDrawingChart(t *testing.T) { f := NewFile() - assert.EqualError(t, f.addDrawingChart("SheetN", "", "", 0, 0, 0, nil), `cannot convert cell "" to coordinates: invalid cell name ""`) + assert.EqualError(t, f.addDrawingChart("SheetN", "", "", 0, 0, 0, nil), newCellNameToCoordinatesError("", newInvalidCellNameError("")).Error()) } func TestAddChart(t *testing.T) { @@ -199,7 +199,7 @@ func TestAddChart(t *testing.T) { } assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddChart.xlsx"))) // Test with illegal cell coordinates - 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"}`), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + 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") // Test add combo chart with invalid format set @@ -252,7 +252,7 @@ func TestDeleteChart(t *testing.T) { // Test delete chart on not exists worksheet. assert.EqualError(t, f.DeleteChart("SheetN", "A1"), "sheet SheetN is not exist") // Test delete chart with invalid coordinates. - assert.EqualError(t, f.DeleteChart("Sheet1", ""), `cannot convert cell "" to coordinates: invalid cell name ""`) + assert.EqualError(t, f.DeleteChart("Sheet1", ""), newCellNameToCoordinatesError("", newInvalidCellNameError("")).Error()) // Test delete chart on no chart worksheet. assert.NoError(t, NewFile().DeleteChart("Sheet1", "A1")) assert.NoError(t, f.Close()) diff --git a/col_test.go b/col_test.go index da46f7856a..2dd27dbfb3 100644 --- a/col_test.go +++ b/col_test.go @@ -120,7 +120,7 @@ func TestGetColsError(t *testing.T) { f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`B`)) _, err = f.GetCols("Sheet1") - assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) f = NewFile() cols, err := f.Cols("Sheet1") @@ -130,7 +130,7 @@ func TestGetColsError(t *testing.T) { cols.curCol = 1 cols.sheetXML = []byte(`A`) _, err = cols.Rows() - assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) } func TestColsRows(t *testing.T) { @@ -208,12 +208,12 @@ func TestColumnVisibility(t *testing.T) { // Test get column visible with illegal cell coordinates. _, err = f.GetColVisible("Sheet1", "*") - assert.EqualError(t, err, `invalid column name "*"`) - assert.EqualError(t, f.SetColVisible("Sheet1", "*", false), `invalid column name "*"`) + assert.EqualError(t, err, newInvalidColumnNameError("*").Error()) + assert.EqualError(t, f.SetColVisible("Sheet1", "*", false), newInvalidColumnNameError("*").Error()) f.NewSheet("Sheet3") assert.NoError(t, f.SetColVisible("Sheet3", "E", false)) - assert.EqualError(t, f.SetColVisible("Sheet1", "A:-1", true), "invalid column name \"-1\"") + 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.NoError(t, f.SaveAs(filepath.Join("test", "TestColumnVisibility.xlsx"))) }) @@ -258,20 +258,20 @@ func TestOutlineLevel(t *testing.T) { assert.EqualError(t, err, "sheet SheetN is not exist") // Test set and get column outline level with illegal cell coordinates. - assert.EqualError(t, f.SetColOutlineLevel("Sheet1", "*", 1), `invalid column name "*"`) + assert.EqualError(t, f.SetColOutlineLevel("Sheet1", "*", 1), newInvalidColumnNameError("*").Error()) _, err = f.GetColOutlineLevel("Sheet1", "*") - assert.EqualError(t, err, `invalid column name "*"`) + 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.SetRowOutlineLevel("Sheet1", 0, 1), "invalid row number 0") + assert.EqualError(t, f.SetRowOutlineLevel("Sheet1", 0, 1), newInvalidRowNumberError(0).Error()) level, err = f.GetRowOutlineLevel("Sheet1", 2) assert.NoError(t, err) assert.Equal(t, uint8(7), level) _, err = f.GetRowOutlineLevel("Sheet1", 0) - assert.EqualError(t, err, `invalid row number 0`) + assert.EqualError(t, err, newInvalidRowNumberError(0).Error()) level, err = f.GetRowOutlineLevel("Sheet1", 10) assert.NoError(t, err) @@ -293,8 +293,8 @@ func TestSetColStyle(t *testing.T) { // Test set column style on not exists worksheet. assert.EqualError(t, f.SetColStyle("SheetN", "E", style), "sheet SheetN is not exist") // Test set column style with illegal cell coordinates. - assert.EqualError(t, f.SetColStyle("Sheet1", "*", style), `invalid column name "*"`) - assert.EqualError(t, f.SetColStyle("Sheet1", "A:*", style), `invalid column name "*"`) + assert.EqualError(t, f.SetColStyle("Sheet1", "*", style), newInvalidColumnNameError("*").Error()) + assert.EqualError(t, f.SetColStyle("Sheet1", "A:*", style), newInvalidColumnNameError("*").Error()) assert.NoError(t, f.SetColStyle("Sheet1", "B", style)) // Test set column style with already exists column with style. @@ -317,9 +317,9 @@ func TestColWidth(t *testing.T) { // Test set and get column width with illegal cell coordinates. width, err = f.GetColWidth("Sheet1", "*") assert.Equal(t, defaultColWidth, width) - assert.EqualError(t, err, `invalid column name "*"`) - assert.EqualError(t, f.SetColWidth("Sheet1", "*", "B", 1), `invalid column name "*"`) - assert.EqualError(t, f.SetColWidth("Sheet1", "A", "*", 1), `invalid column name "*"`) + 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. assert.EqualError(t, f.SetColWidth("SheetN", "B", "A", 12), "sheet SheetN is not exist") @@ -345,7 +345,7 @@ func TestInsertCol(t *testing.T) { assert.NoError(t, f.InsertCol(sheet1, "A")) // Test insert column with illegal cell coordinates. - assert.EqualError(t, f.InsertCol("Sheet1", "*"), `invalid column name "*"`) + assert.EqualError(t, f.InsertCol("Sheet1", "*"), newInvalidColumnNameError("*").Error()) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestInsertCol.xlsx"))) } @@ -366,7 +366,7 @@ func TestRemoveCol(t *testing.T) { assert.NoError(t, f.RemoveCol(sheet1, "A")) // Test remove column with illegal cell coordinates. - assert.EqualError(t, f.RemoveCol("Sheet1", "*"), `invalid column name "*"`) + 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") diff --git a/comment_test.go b/comment_test.go index fb36d29065..9eb07f434a 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 is not exist") // Test add comment on with illegal cell coordinates - assert.EqualError(t, f.AddComment("Sheet1", "A", `{"author":"Excelize: ","text":"This is a comment."}`), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + 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_test.go b/crypt_test.go index cb0b160b8d..0796482a0b 100644 --- a/crypt_test.go +++ b/crypt_test.go @@ -21,16 +21,16 @@ import ( func TestEncrypt(t *testing.T) { 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"}), "not support encryption currently") + assert.EqualError(t, f.SaveAs(filepath.Join("test", "BadEncrypt.xlsx"), Options{Password: "password"}), ErrEncrypt.Error()) assert.NoError(t, f.Close()) } func TestEncryptionMechanism(t *testing.T) { mechanism, err := encryptionMechanism([]byte{3, 0, 3, 0}) assert.Equal(t, mechanism, "extensible") - assert.EqualError(t, err, "unsupport encryption mechanism") + assert.EqualError(t, err, ErrUnsupportEncryptMechanism.Error()) _, err = encryptionMechanism([]byte{}) - assert.EqualError(t, err, "unknown encryption mechanism") + assert.EqualError(t, err, ErrUnknownEncryptMechanism.Error()) } func TestHashing(t *testing.T) { diff --git a/datavalidation_test.go b/datavalidation_test.go index c35693ce26..56e7d73578 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -164,13 +164,13 @@ func TestDeleteDataValidation(t *testing.T) { dvRange.Sqref = "A" assert.NoError(t, f.AddDataValidation("Sheet1", dvRange)) - assert.EqualError(t, f.DeleteDataValidation("Sheet1", "A1"), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.EqualError(t, f.DeleteDataValidation("Sheet1", "A1"), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) - assert.EqualError(t, f.DeleteDataValidation("Sheet1", "A1:A"), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.EqualError(t, f.DeleteDataValidation("Sheet1", "A1:A"), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") assert.True(t, ok) ws.(*xlsxWorksheet).DataValidations.DataValidation[0].Sqref = "A1:A" - assert.EqualError(t, f.DeleteDataValidation("Sheet1", "A1:B2"), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + 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") diff --git a/date_test.go b/date_test.go index cc516d572a..cc21e58a3f 100644 --- a/date_test.go +++ b/date_test.go @@ -99,5 +99,5 @@ func TestExcelDateToTime(t *testing.T) { } // Check error case _, err := ExcelDateToTime(-1, false) - assert.EqualError(t, err, "invalid date value -1.000000, negative values are not supported") + assert.EqualError(t, err, newInvalidExcelDateError(-1).Error()) } diff --git a/errors.go b/errors.go index f9eedde7c6..4230c148ec 100644 --- a/errors.go +++ b/errors.go @@ -56,6 +56,12 @@ func newFieldLengthError(name string) error { return fmt.Errorf("field %s must be less or equal than 255 characters", name) } +// newCellNameToCoordinatesError defined the error message on converts +// alphanumeric cell name to coordinates. +func newCellNameToCoordinatesError(cell string, err error) error { + return fmt.Errorf("cannot convert cell %q to coordinates: %v", cell, err) +} + var ( // ErrStreamSetColWidth defined the error message on set column width in // stream writing mode. @@ -139,4 +145,24 @@ var ( // ErrOptionsUnzipSizeLimit defined the error message for receiving // invalid UnzipSizeLimit and WorksheetUnzipMemLimit. ErrOptionsUnzipSizeLimit = errors.New("the value of UnzipSizeLimit should be greater than or equal to WorksheetUnzipMemLimit") + // ErrSave defined the error message for saving file. + ErrSave = errors.New("no path defined for file, consider File.WriteTo or File.Write") + // ErrAttrValBool defined the error message on marshal and unmarshal + // boolean type XML attribute. + ErrAttrValBool = errors.New("unexpected child of attrValBool") + // ErrSparklineType defined the error message on receive the invalid + // sparkline Type parameters. + ErrSparklineType = errors.New("parameter 'Type' must be 'line', 'column' or 'win_loss'") + // ErrSparklineLocation defined the error message on missing Location + // parameters + ErrSparklineLocation = errors.New("parameter 'Location' is required") + // ErrSparklineRange defined the error message on missing sparkline Range + // parameters + ErrSparklineRange = errors.New("parameter 'Range' is required") + // ErrSparkline defined the error message on receive the invalid sparkline + // parameters. + ErrSparkline = errors.New("must have the same number of 'Location' and 'Range' parameters") + // ErrSparklineStyle defined the error message on receive the invalid + // sparkline Style parameters. + ErrSparklineStyle = errors.New("parameter 'Style' must betweent 0-35") ) diff --git a/excelize_test.go b/excelize_test.go index f556d83c8a..a15e80125c 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -45,12 +45,12 @@ func TestOpenFile(t *testing.T) { // Test set cell value with illegal row number. assert.EqualError(t, f.SetCellDefault("Sheet2", "A", strconv.FormatFloat(float64(-100.1588), 'f', -1, 64)), - `cannot convert cell "A" to coordinates: invalid cell name "A"`) + newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.NoError(t, f.SetCellInt("Sheet2", "A1", 100)) // Test set cell integer value with illegal row number. - assert.EqualError(t, f.SetCellInt("Sheet2", "A", 100), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.EqualError(t, f.SetCellInt("Sheet2", "A", 100), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.NoError(t, f.SetCellStr("Sheet2", "C11", "Knowns")) // Test max characters in a cell. @@ -63,7 +63,7 @@ func TestOpenFile(t *testing.T) { assert.EqualError(t, f.SetCellStr("Sheet10", "b230", "10"), "sheet Sheet10 is not exist") // Test set cell string value with illegal row number. - assert.EqualError(t, f.SetCellStr("Sheet1", "A", "10"), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.EqualError(t, f.SetCellStr("Sheet1", "A", "10"), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) f.SetActiveSheet(2) // Test get cell formula with given rows number. @@ -77,7 +77,7 @@ func TestOpenFile(t *testing.T) { // Test get cell formula with illegal rows number. _, err = f.GetCellFormula("Sheet1", "B") - assert.EqualError(t, err, `cannot convert cell "B" to coordinates: invalid cell name "B"`) + assert.EqualError(t, err, newCellNameToCoordinatesError("B", newInvalidCellNameError("B")).Error()) // Test get shared cell formula _, err = f.GetCellFormula("Sheet2", "H11") assert.NoError(t, err) @@ -87,9 +87,9 @@ func TestOpenFile(t *testing.T) { // Test read cell value with given illegal rows number. _, err = f.GetCellValue("Sheet2", "a-1") - assert.EqualError(t, err, `cannot convert cell "A-1" to coordinates: invalid cell name "A-1"`) + assert.EqualError(t, err, newCellNameToCoordinatesError("A-1", newInvalidCellNameError("A-1")).Error()) _, err = f.GetCellValue("Sheet2", "A") - assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) // Test read cell value with given lowercase column number. _, err = f.GetCellValue("Sheet2", "a5") @@ -324,7 +324,7 @@ func TestNewFile(t *testing.T) { func TestAddDrawingVML(t *testing.T) { // Test addDrawingVML with illegal cell coordinates. f := NewFile() - assert.EqualError(t, f.addDrawingVML(0, "", "*", 0, 0), `cannot convert cell "*" to coordinates: invalid cell name "*"`) + assert.EqualError(t, f.addDrawingVML(0, "", "*", 0, 0), newCellNameToCoordinatesError("*", newInvalidCellNameError("*")).Error()) } func TestSetCellHyperLink(t *testing.T) { @@ -368,7 +368,7 @@ func TestSetCellHyperLink(t *testing.T) { assert.True(t, ok) ws.(*xlsxWorksheet).MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:A"}}} err = f.SetCellHyperLink("Sheet1", "A1", "https://github.com/xuri/excelize", "External") - assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) } func TestGetCellHyperLink(t *testing.T) { @@ -408,7 +408,7 @@ func TestGetCellHyperLink(t *testing.T) { assert.True(t, ok) ws.(*xlsxWorksheet).MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:A"}}} link, target, err = f.GetCellHyperLink("Sheet1", "A1") - assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.Equal(t, link, false) assert.Equal(t, target, "") } @@ -570,13 +570,13 @@ func TestSetCellStyleAlignment(t *testing.T) { assert.NoError(t, f.SetCellStyle("Sheet1", "A22", "A22", style)) // Test set cell style with given illegal rows number. - assert.EqualError(t, f.SetCellStyle("Sheet1", "A", "A22", style), `cannot convert cell "A" to coordinates: invalid cell name "A"`) - assert.EqualError(t, f.SetCellStyle("Sheet1", "A22", "A", style), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + 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. index, err := f.GetCellStyle("Sheet1", "A") assert.Equal(t, 0, index) - assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellStyleAlignment.xlsx"))) } @@ -1109,7 +1109,7 @@ func TestSetSheetRow(t *testing.T) { assert.NoError(t, f.SetSheetRow("Sheet1", "B27", &[]interface{}{"cell", nil, int32(42), float64(42), time.Now().UTC()})) assert.EqualError(t, f.SetSheetRow("Sheet1", "", &[]interface{}{"cell", nil, 2}), - `cannot convert cell "" to coordinates: invalid cell name ""`) + newCellNameToCoordinatesError("", newInvalidCellNameError("")).Error()) assert.EqualError(t, f.SetSheetRow("Sheet1", "B27", []interface{}{}), ErrParameterInvalid.Error()) assert.EqualError(t, f.SetSheetRow("Sheet1", "B27", &f), ErrParameterInvalid.Error()) @@ -1181,7 +1181,7 @@ func TestSetDefaultTimeStyle(t *testing.T) { assert.EqualError(t, f.setDefaultTimeStyle("SheetN", "", 0), "sheet SheetN is not exist") // Test set default time style on invalid cell - assert.EqualError(t, f.setDefaultTimeStyle("Sheet1", "", 42), "cannot convert cell \"\" to coordinates: invalid cell name \"\"") + assert.EqualError(t, f.setDefaultTimeStyle("Sheet1", "", 42), newCellNameToCoordinatesError("", newInvalidCellNameError("")).Error()) } func TestAddVBAProject(t *testing.T) { diff --git a/file.go b/file.go index a430c5f422..e2aeb4a043 100644 --- a/file.go +++ b/file.go @@ -14,7 +14,6 @@ package excelize import ( "archive/zip" "bytes" - "fmt" "io" "os" "path/filepath" @@ -58,7 +57,7 @@ func NewFile() *File { // Save provides a function to override the spreadsheet with origin path. func (f *File) Save() error { if f.Path == "" { - return fmt.Errorf("no path defined for file, consider File.WriteTo or File.Write") + return ErrSave } return f.SaveAs(f.Path) } diff --git a/lib.go b/lib.go index ccb09ac6b2..0efc180cc4 100644 --- a/lib.go +++ b/lib.go @@ -16,7 +16,6 @@ import ( "bytes" "container/list" "encoding/xml" - "errors" "fmt" "io" "io/ioutil" @@ -240,11 +239,9 @@ func ColumnNumberToName(num int) (string, error) { // excelize.CellNameToCoordinates("Z3") // returns 26, 3, nil // func CellNameToCoordinates(cell string) (int, int, error) { - const msg = "cannot convert cell %q to coordinates: %v" - colname, row, err := SplitCellName(cell) if err != nil { - return -1, -1, fmt.Errorf(msg, cell, err) + return -1, -1, newCellNameToCoordinatesError(cell, err) } if row > TotalRows { return -1, -1, ErrMaxRows @@ -447,7 +444,7 @@ func (avb *attrValBool) UnmarshalXML(d *xml.Decoder, start xml.StartElement) err found := false switch t.(type) { case xml.StartElement: - return errors.New("unexpected child of attrValBool") + return ErrAttrValBool case xml.EndElement: found = true } diff --git a/lib_test.go b/lib_test.go index 35dd2a013f..3346acc6d6 100644 --- a/lib_test.go +++ b/lib_test.go @@ -184,7 +184,7 @@ func TestCellNameToCoordinates_Error(t *testing.T) { } } _, _, err := CellNameToCoordinates("A1048577") - assert.EqualError(t, err, "row number exceeds maximum limit") + assert.EqualError(t, err, ErrMaxRows.Error()) } func TestCoordinatesToCellName_OK(t *testing.T) { diff --git a/merge_test.go b/merge_test.go index 8d9ad41e10..a28aeff321 100644 --- a/merge_test.go +++ b/merge_test.go @@ -12,7 +12,7 @@ func TestMergeCell(t *testing.T) { if !assert.NoError(t, err) { t.FailNow() } - assert.EqualError(t, f.MergeCell("Sheet1", "A", "B"), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + 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")) @@ -156,7 +156,7 @@ func TestUnmergeCell(t *testing.T) { mergeCellNum := len(sheet.MergeCells.Cells) - assert.EqualError(t, f.UnmergeCell("Sheet1", "A", "A"), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.EqualError(t, f.UnmergeCell("Sheet1", "A", "A"), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) // unmerge the mergecell that contains A1 assert.NoError(t, f.UnmergeCell(sheet1, "A1", "A1")) @@ -190,7 +190,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: "A:A"}}} - assert.EqualError(t, f.UnmergeCell("Sheet1", "A2", "B3"), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.EqualError(t, f.UnmergeCell("Sheet1", "A2", "B3"), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) } func TestFlatMergedCells(t *testing.T) { diff --git a/picture_test.go b/picture_test.go index 2927976857..5bf139d579 100644 --- a/picture_test.go +++ b/picture_test.go @@ -60,7 +60,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. - assert.EqualError(t, f.AddPictureFromBytes("Sheet1", "A", "", "Excel Logo", ".png", file), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + 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"), "")) @@ -110,7 +110,7 @@ func TestGetPicture(t *testing.T) { // Try to get picture from a worksheet with illegal cell coordinates. _, _, err = f.GetPicture("Sheet1", "A") - assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "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") @@ -165,7 +165,7 @@ func TestGetPicture(t *testing.T) { func TestAddDrawingPicture(t *testing.T) { // testing addDrawingPicture with illegal cell coordinates. f := NewFile() - assert.EqualError(t, f.addDrawingPicture("sheet1", "", "A", "", 0, 0, 0, 0, nil), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.EqualError(t, f.addDrawingPicture("sheet1", "", "A", "", 0, 0, 0, 0, nil), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) } func TestAddPictureFromBytes(t *testing.T) { @@ -195,7 +195,7 @@ func TestDeletePicture(t *testing.T) { // Test delete picture on not exists worksheet. assert.EqualError(t, f.DeletePicture("SheetN", "A1"), "sheet SheetN is not exist") // Test delete picture with invalid coordinates. - assert.EqualError(t, f.DeletePicture("Sheet1", ""), `cannot convert cell "" to coordinates: invalid cell name ""`) + assert.EqualError(t, f.DeletePicture("Sheet1", ""), newCellNameToCoordinatesError("", newInvalidCellNameError("")).Error()) assert.NoError(t, f.Close()) // Test delete picture on no chart worksheet. assert.NoError(t, NewFile().DeletePicture("Sheet1", "A1")) @@ -208,9 +208,9 @@ func TestDrawingResize(t *testing.T) { assert.EqualError(t, err, "sheet SheetN is not exist") // Test calculate drawing resize with invalid coordinates. _, _, _, _, err = f.drawingResize("Sheet1", "", 1, 1, nil) - assert.EqualError(t, err, `cannot convert cell "" to coordinates: invalid cell name ""`) + assert.EqualError(t, err, newCellNameToCoordinatesError("", newInvalidCellNameError("")).Error()) 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}`), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.EqualError(t, f.AddPicture("Sheet1", "A1", filepath.Join("test", "images", "excel.jpg"), `{"autofit": true}`), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) } diff --git a/pivotTable_test.go b/pivotTable_test.go index dbb82523c7..3487793308 100644 --- a/pivotTable_test.go +++ b/pivotTable_test.go @@ -158,7 +158,7 @@ func TestAddPivotTable(t *testing.T) { })) // Test empty pivot table options - assert.EqualError(t, f.AddPivotTable(nil), "parameter is required") + assert.EqualError(t, f.AddPivotTable(nil), ErrParameterRequired.Error()) // Test invalid data range assert.EqualError(t, f.AddPivotTable(&PivotTableOption{ DataRange: "Sheet1!$A$1:$A$1", @@ -227,7 +227,7 @@ func TestAddPivotTable(t *testing.T) { // Test adjust range with invalid range _, _, err := f.adjustRange("") - assert.EqualError(t, err, "parameter is required") + assert.EqualError(t, err, ErrParameterRequired.Error()) // Test adjust range with incorrect range _, _, err = f.adjustRange("sheet1!") assert.EqualError(t, err, "parameter is invalid") diff --git a/rows_test.go b/rows_test.go index 9fee3d9e45..0c154a46ab 100644 --- a/rows_test.go +++ b/rows_test.go @@ -111,10 +111,10 @@ func TestRowHeight(t *testing.T) { f := NewFile() sheet1 := f.GetSheetName(0) - assert.EqualError(t, f.SetRowHeight(sheet1, 0, defaultRowHeightPixels+1.0), "invalid row number 0") + assert.EqualError(t, f.SetRowHeight(sheet1, 0, defaultRowHeightPixels+1.0), newInvalidRowNumberError(0).Error()) _, err := f.GetRowHeight("Sheet1", 0) - assert.EqualError(t, err, "invalid row number 0") + assert.EqualError(t, err, newInvalidRowNumberError(0).Error()) assert.NoError(t, f.SetRowHeight(sheet1, 1, 111.0)) height, err := f.GetRowHeight(sheet1, 1) @@ -190,7 +190,7 @@ func TestColumns(t *testing.T) { rows.curRow = 3 rows.decoder = f.xmlNewDecoder(bytes.NewReader([]byte(`1`))) _, err = rows.Columns() - assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) // Test token is nil rows.decoder = f.xmlNewDecoder(bytes.NewReader(nil)) @@ -220,12 +220,12 @@ func TestRowVisibility(t *testing.T) { visiable, err = f.GetRowVisible("Sheet3", 25) assert.Equal(t, false, visiable) assert.NoError(t, err) - assert.EqualError(t, f.SetRowVisible("Sheet3", 0, true), "invalid row number 0") + assert.EqualError(t, f.SetRowVisible("Sheet3", 0, true), newInvalidRowNumberError(0).Error()) assert.EqualError(t, f.SetRowVisible("SheetN", 2, false), "sheet SheetN is not exist") visible, err := f.GetRowVisible("Sheet3", 0) assert.Equal(t, false, visible) - assert.EqualError(t, err, "invalid row number 0") + assert.EqualError(t, err, newInvalidRowNumberError(0).Error()) _, err = f.GetRowVisible("SheetN", 1) assert.EqualError(t, err, "sheet SheetN is not exist") @@ -245,9 +245,9 @@ func TestRemoveRow(t *testing.T) { assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/xuri/excelize", "External")) - assert.EqualError(t, f.RemoveRow(sheet1, -1), "invalid row number -1") + assert.EqualError(t, f.RemoveRow(sheet1, -1), newInvalidRowNumberError(-1).Error()) - assert.EqualError(t, f.RemoveRow(sheet1, 0), "invalid row number 0") + assert.EqualError(t, f.RemoveRow(sheet1, 0), newInvalidRowNumberError(0).Error()) assert.NoError(t, f.RemoveRow(sheet1, 4)) if !assert.Len(t, r.SheetData.Row, rowCount-1) { @@ -306,9 +306,9 @@ 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), "invalid row number -1") + assert.EqualError(t, f.InsertRow(sheet1, -1), newInvalidRowNumberError(-1).Error()) - assert.EqualError(t, f.InsertRow(sheet1, 0), "invalid row number 0") + assert.EqualError(t, f.InsertRow(sheet1, 0), newInvalidRowNumberError(0).Error()) assert.NoError(t, f.InsertRow(sheet1, 1)) if !assert.Len(t, r.SheetData.Row, rowCount+1) { @@ -484,7 +484,7 @@ func TestDuplicateRowZeroWithNoRows(t *testing.T) { t.Run("ZeroWithNoRows", func(t *testing.T) { f := NewFile() - assert.EqualError(t, f.DuplicateRow(sheet, 0), "invalid row number 0") + assert.EqualError(t, f.DuplicateRow(sheet, 0), newInvalidRowNumberError(0).Error()) if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "ZeroWithNoRows"))) { t.FailNow() @@ -800,7 +800,7 @@ func TestDuplicateRowInvalidRowNum(t *testing.T) { assert.NoError(t, f.SetCellStr(sheet, col, val)) } - assert.EqualError(t, f.DuplicateRow(sheet, row), fmt.Sprintf("invalid row number %d", row)) + assert.EqualError(t, f.DuplicateRow(sheet, row), newInvalidRowNumberError(row).Error()) for col, val := range cells { v, err := f.GetCellValue(sheet, col) @@ -822,7 +822,7 @@ func TestDuplicateRowInvalidRowNum(t *testing.T) { assert.NoError(t, f.SetCellStr(sheet, col, val)) } - assert.EqualError(t, f.DuplicateRowTo(sheet, row1, row2), fmt.Sprintf("invalid row number %d", row1)) + assert.EqualError(t, f.DuplicateRowTo(sheet, row1, row2), newInvalidRowNumberError(row1).Error()) for col, val := range cells { v, err := f.GetCellValue(sheet, col) @@ -897,7 +897,7 @@ func TestCheckRow(t *testing.T) { f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`12345`)) f.Sheet.Delete("xl/worksheets/sheet1.xml") delete(f.checked, "xl/worksheets/sheet1.xml") - assert.EqualError(t, f.SetCellValue("Sheet1", "A1", false), `cannot convert cell "-" to coordinates: invalid cell name "-"`) + assert.EqualError(t, f.SetCellValue("Sheet1", "A1", false), newCellNameToCoordinatesError("-", newInvalidCellNameError("-")).Error()) } func TestSetRowStyle(t *testing.T) { diff --git a/shape_test.go b/shape_test.go index a02e53da0e..2f005894d0 100644 --- a/shape_test.go +++ b/shape_test.go @@ -56,7 +56,7 @@ func TestAddShape(t *testing.T) { "color": "2980B9" } }] - }`), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + }`), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddShape1.xlsx"))) // Test add first shape for given sheet. diff --git a/sheet_test.go b/sheet_test.go index d33ba99dea..9bfee70aaa 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -207,7 +207,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, "cannot convert cell \"A\" to coordinates: invalid cell name \"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`)) @@ -236,7 +236,7 @@ func TestSetHeaderFooter(t *testing.T) { // Test set header and footer with illegal setting. assert.EqualError(t, f.SetHeaderFooter("Sheet1", &FormatHeaderFooter{ OddHeader: strings.Repeat("c", MaxFieldLength+1), - }), "field OddHeader must be less or equal than 255 characters") + }), newFieldLengthError("OddHeader").Error()) assert.NoError(t, f.SetHeaderFooter("Sheet1", nil)) text := strings.Repeat("一", MaxFieldLength) @@ -276,10 +276,10 @@ func TestDefinedName(t *testing.T) { Name: "Amount", RefersTo: "Sheet1!$A$2:$D$5", Comment: "defined name comment", - }), "the same name already exists on the scope") + }), ErrDefinedNameduplicate.Error()) assert.EqualError(t, f.DeleteDefinedName(&DefinedName{ Name: "No Exist Defined Name", - }), "no defined name on the scope") + }), ErrDefinedNameScope.Error()) assert.Exactly(t, "Sheet1!$A$2:$D$5", f.GetDefinedName()[1].RefersTo) assert.NoError(t, f.DeleteDefinedName(&DefinedName{ Name: "Amount", @@ -316,7 +316,7 @@ func TestInsertPageBreak(t *testing.T) { assert.NoError(t, f.InsertPageBreak("Sheet1", "B2")) assert.NoError(t, f.InsertPageBreak("Sheet1", "C3")) assert.NoError(t, f.InsertPageBreak("Sheet1", "C3")) - assert.EqualError(t, f.InsertPageBreak("Sheet1", "A"), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + 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.NoError(t, f.SaveAs(filepath.Join("test", "TestInsertPageBreak.xlsx"))) } @@ -342,7 +342,7 @@ func TestRemovePageBreak(t *testing.T) { assert.NoError(t, f.InsertPageBreak("Sheet2", "C2")) assert.NoError(t, f.RemovePageBreak("Sheet2", "B2")) - assert.EqualError(t, f.RemovePageBreak("Sheet1", "A"), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + 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.NoError(t, f.SaveAs(filepath.Join("test", "TestRemovePageBreak.xlsx"))) } diff --git a/sparkline.go b/sparkline.go index 917383d9e4..0d5ef6e1ca 100644 --- a/sparkline.go +++ b/sparkline.go @@ -13,7 +13,6 @@ package excelize import ( "encoding/xml" - "errors" "io" "strings" ) @@ -410,7 +409,7 @@ func (f *File) AddSparkline(sheet string, opt *SparklineOption) (err error) { sparkTypes = map[string]string{"line": "line", "column": "column", "win_loss": "stacked"} if opt.Type != "" { if specifiedSparkTypes, ok = sparkTypes[opt.Type]; !ok { - err = errors.New("parameter 'Type' must be 'line', 'column' or 'win_loss'") + err = ErrSparklineType return } sparkType = specifiedSparkTypes @@ -470,17 +469,17 @@ func (f *File) parseFormatAddSparklineSet(sheet string, opt *SparklineOption) (* return ws, ErrParameterRequired } if len(opt.Location) < 1 { - return ws, errors.New("parameter 'Location' is required") + return ws, ErrSparklineLocation } if len(opt.Range) < 1 { - return ws, errors.New("parameter 'Range' is required") + return ws, ErrSparklineRange } // The ranges and locations must match.\ if len(opt.Location) != len(opt.Range) { - return ws, errors.New(`must have the same number of 'Location' and 'Range' parameters`) + return ws, ErrSparkline } if opt.Style < 0 || opt.Style > 35 { - return ws, errors.New("parameter 'Style' must betweent 0-35") + return ws, ErrSparklineStyle } if ws.ExtLst == nil { ws.ExtLst = &xlsxExtLst{} diff --git a/sparkline_test.go b/sparkline_test.go index 0777ee16a3..c21687cbb9 100644 --- a/sparkline_test.go +++ b/sparkline_test.go @@ -220,38 +220,38 @@ func TestAddSparkline(t *testing.T) { Range: []string{"Sheet2!A3:E3"}, }), "sheet SheetN is not exist") - assert.EqualError(t, f.AddSparkline("Sheet1", nil), "parameter is required") + assert.EqualError(t, f.AddSparkline("Sheet1", nil), ErrParameterRequired.Error()) assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOption{ Range: []string{"Sheet2!A3:E3"}, - }), `parameter 'Location' is required`) + }), ErrSparklineLocation.Error()) assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOption{ Location: []string{"F3"}, - }), `parameter 'Range' is required`) + }), ErrSparklineRange.Error()) assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOption{ Location: []string{"F2", "F3"}, Range: []string{"Sheet2!A3:E3"}, - }), `must have the same number of 'Location' and 'Range' parameters`) + }), ErrSparkline.Error()) assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOption{ Location: []string{"F3"}, Range: []string{"Sheet2!A3:E3"}, Type: "unknown_type", - }), `parameter 'Type' must be 'line', 'column' or 'win_loss'`) + }), ErrSparklineType.Error()) assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOption{ Location: []string{"F3"}, Range: []string{"Sheet2!A3:E3"}, Style: -1, - }), `parameter 'Style' must betweent 0-35`) + }), ErrSparklineStyle.Error()) assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOption{ Location: []string{"F3"}, Range: []string{"Sheet2!A3:E3"}, Style: -1, - }), `parameter 'Style' must betweent 0-35`) + }), ErrSparklineStyle.Error()) ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") assert.True(t, ok) diff --git a/stream_test.go b/stream_test.go index 52cee4de6e..833a00ac9f 100644 --- a/stream_test.go +++ b/stream_test.go @@ -174,8 +174,8 @@ 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. - assert.EqualError(t, streamWriter.AddTable("A", "B1", `{}`), `cannot convert cell "A" to coordinates: invalid cell name "A"`) - assert.EqualError(t, streamWriter.AddTable("A1", "B", `{}`), `cannot convert cell "B" to coordinates: invalid cell name "B"`) + assert.EqualError(t, streamWriter.AddTable("A", "B1", `{}`), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) + assert.EqualError(t, streamWriter.AddTable("A1", "B", `{}`), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")).Error()) } func TestStreamMergeCells(t *testing.T) { @@ -184,7 +184,7 @@ func TestStreamMergeCells(t *testing.T) { assert.NoError(t, err) assert.NoError(t, streamWriter.MergeCell("A1", "D1")) // Test merge cells with illegal cell coordinates. - assert.EqualError(t, streamWriter.MergeCell("A", "D1"), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.EqualError(t, streamWriter.MergeCell("A", "D1"), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.NoError(t, streamWriter.Flush()) // Save spreadsheet by the given path. assert.NoError(t, file.SaveAs(filepath.Join("test", "TestStreamMergeCells.xlsx"))) @@ -204,7 +204,7 @@ func TestSetRow(t *testing.T) { file := NewFile() streamWriter, err := file.NewStreamWriter("Sheet1") assert.NoError(t, err) - assert.EqualError(t, streamWriter.SetRow("A", []interface{}{}), `cannot convert cell "A" to coordinates: invalid cell name "A"`) + assert.EqualError(t, streamWriter.SetRow("A", []interface{}{}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) } func TestSetCellValFunc(t *testing.T) { diff --git a/table_test.go b/table_test.go index 95738e1375..0a74b1b568 100644 --- a/table_test.go +++ b/table_test.go @@ -34,8 +34,8 @@ func TestAddTable(t *testing.T) { // 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. - assert.EqualError(t, f.AddTable("Sheet1", "A", "B1", `{}`), `cannot convert cell "A" to coordinates: invalid cell name "A"`) - assert.EqualError(t, f.AddTable("Sheet1", "A1", "B", `{}`), `cannot convert cell "B" to coordinates: invalid cell name "B"`) + 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"))) @@ -73,8 +73,8 @@ func TestAutoFilter(t *testing.T) { } // testing AutoFilter with illegal cell coordinates. - assert.EqualError(t, f.AutoFilter("Sheet1", "A", "B1", ""), `cannot convert cell "A" to coordinates: invalid cell name "A"`) - assert.EqualError(t, f.AutoFilter("Sheet1", "A1", "B", ""), `cannot convert cell "B" to coordinates: invalid cell name "B"`) + 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()) } func TestAutoFilterError(t *testing.T) { @@ -109,7 +109,7 @@ func TestAutoFilterError(t *testing.T) { assert.EqualError(t, f.autoFilter("Sheet1", "A1", 1, 1, &formatAutoFilter{ Column: "-", Expression: "-", - }), `invalid column name "-"`) + }), newInvalidColumnNameError("-").Error()) assert.EqualError(t, f.autoFilter("Sheet1", "A1", 1, 100, &formatAutoFilter{ Column: "A", Expression: "-", From 5bf35f8c1c18eafb2e423ae3b77868bcccc505ff Mon Sep 17 00:00:00 2001 From: jaby Date: Thu, 9 Dec 2021 02:40:11 +0100 Subject: [PATCH 507/957] This closes #1088 (#1089) Support check string equality with the string value of a defined name --- calc.go | 8 ++++++-- calc_test.go | 33 ++++++++++++++++++++++++++------- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/calc.go b/calc.go index da32f9740b..e9ac0c864f 100644 --- a/calc.go +++ b/calc.go @@ -736,6 +736,10 @@ func (f *File) evalInfixExp(sheet, cell string, tokens []efp.Token) (efp.Token, // current token is args or range, skip next token, order required: parse reference first if token.TSubType == efp.TokenSubTypeRange { if !opftStack.Empty() { + refTo := f.getDefinedNameRefTo(token.TValue, sheet) + if refTo != "" { + token.TValue = refTo + } // parse reference: must reference at here result, err := f.parseReference(sheet, token.TValue) if err != nil { @@ -871,13 +875,13 @@ func calcPow(rOpd, lOpd efp.Token, opdStack *Stack) error { // calcEq evaluate equal arithmetic operations. func calcEq(rOpd, lOpd efp.Token, opdStack *Stack) error { - opdStack.Push(efp.Token{TValue: strings.ToUpper(strconv.FormatBool(rOpd == lOpd)), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + opdStack.Push(efp.Token{TValue: strings.ToUpper(strconv.FormatBool(rOpd.TValue == lOpd.TValue)), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) 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 != lOpd)), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + opdStack.Push(efp.Token{TValue: strings.ToUpper(strconv.FormatBool(rOpd.TValue != lOpd.TValue)), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) return nil } diff --git a/calc_test.go b/calc_test.go index b1d4f26d21..1de8fb8f51 100644 --- a/calc_test.go +++ b/calc_test.go @@ -3323,22 +3323,41 @@ func TestCalculate(t *testing.T) { func TestCalcWithDefinedName(t *testing.T) { cellData := [][]interface{}{ - {"A1 value", "B1 value", nil}, + {"A1_as_string", "B1_as_string", 123, nil}, } f := prepareCalcData(cellData) assert.NoError(t, f.SetDefinedName(&DefinedName{Name: "defined_name1", RefersTo: "Sheet1!A1", Scope: "Workbook"})) assert.NoError(t, f.SetDefinedName(&DefinedName{Name: "defined_name1", RefersTo: "Sheet1!B1", Scope: "Sheet1"})) + assert.NoError(t, f.SetDefinedName(&DefinedName{Name: "defined_name2", RefersTo: "Sheet1!C1", Scope: "Workbook"})) - assert.NoError(t, f.SetCellFormula("Sheet1", "C1", "=defined_name1")) - result, err := f.CalcCellValue("Sheet1", "C1") + assert.NoError(t, f.SetCellFormula("Sheet1", "D1", "=defined_name1")) + result, err := f.CalcCellValue("Sheet1", "D1") assert.NoError(t, err) // DefinedName with scope WorkSheet takes precedence over DefinedName with scope Workbook, so we should get B1 value - assert.Equal(t, "B1 value", result, "=defined_name1") + assert.Equal(t, "B1_as_string", result, "=defined_name1") - assert.NoError(t, f.SetCellFormula("Sheet1", "C1", "=CONCATENATE(\"<\",defined_name1,\">\")")) - result, err = f.CalcCellValue("Sheet1", "C1") + 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") + assert.Equal(t, "", result, "=defined_name1") + + // comparing numeric values + 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`)) + result, err = f.CalcCellValue("Sheet1", "D1") + assert.NoError(t, err) + 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")`)) + result, err = f.CalcCellValue("Sheet1", "D1") + assert.NoError(t, err) + assert.Equal(t, "YES", result, `=IF("B1_as_string"=defined_name1,"YES","NO")`) } From 1b3040659d3155732961c45b0c2e13e39e0b2576 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 9 Dec 2021 20:16:12 +0800 Subject: [PATCH 508/957] ref #65: new formula functions: ISFORMULA, ISLOGICAL and ISREF --- calc.go | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 33 ++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+) diff --git a/calc.go b/calc.go index e9ac0c864f..ebfa8154b9 100644 --- a/calc.go +++ b/calc.go @@ -449,10 +449,13 @@ type formulaFuncs struct { // ISERR // ISERROR // ISEVEN +// ISFORMULA +// ISLOGICAL // ISNA // ISNONTEXT // ISNUMBER // ISODD +// ISREF // ISTEXT // ISO.CEILING // ISOWEEKNUM @@ -6622,6 +6625,44 @@ func (fn *formulaFuncs) ISEVEN(argsList *list.List) formulaArg { return newStringFormulaArg(result) } +// ISFORMULA function tests if a specified cell contains a formula, and if so, +// returns TRUE; Otherwise, the function returns FALSE. The syntax of the +// function is: +// +// ISFORMULA(reference) +// +func (fn *formulaFuncs) ISFORMULA(argsList *list.List) formulaArg { + if argsList.Len() != 1 { + return newErrorFormulaArg(formulaErrorVALUE, "ISFORMULA requires 1 argument") + } + arg := argsList.Front().Value.(formulaArg) + if arg.cellRefs != nil && arg.cellRefs.Len() == 1 { + ref := arg.cellRefs.Front().Value.(cellRef) + cell, _ := CoordinatesToCellName(ref.Col, ref.Row) + if formula, _ := fn.f.GetCellFormula(ref.Sheet, cell); len(formula) > 0 { + return newBoolFormulaArg(true) + } + } + return newBoolFormulaArg(false) +} + +// ISLOGICAL function tests if a supplied value (or expression) returns a +// 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) +// +func (fn *formulaFuncs) ISLOGICAL(argsList *list.List) formulaArg { + if argsList.Len() != 1 { + return newErrorFormulaArg(formulaErrorVALUE, "ISLOGICAL requires 1 argument") + } + val := argsList.Front().Value.(formulaArg).Value() + if strings.EqualFold("TRUE", val) || strings.EqualFold("FALSE", val) { + return newBoolFormulaArg(true) + } + return newBoolFormulaArg(false) +} + // ISNA function tests if an initial supplied expression (or value) returns // the Excel #N/A Error, and if so, returns TRUE; Otherwise the function // returns FALSE. The syntax of the function is: @@ -6704,6 +6745,23 @@ func (fn *formulaFuncs) ISODD(argsList *list.List) formulaArg { return newStringFormulaArg(result) } +// ISREF function tests if a supplied value is a reference. If so, the +// function returns TRUE; Otherwise it returns FALSE. The syntax of the +// function is: +// +// ISREF(value) +// +func (fn *formulaFuncs) ISREF(argsList *list.List) formulaArg { + if argsList.Len() != 1 { + return newErrorFormulaArg(formulaErrorVALUE, "ISREF requires 1 argument") + } + arg := argsList.Front().Value.(formulaArg) + if arg.cellRanges != nil && arg.cellRanges.Len() > 0 || arg.cellRefs != nil && arg.cellRefs.Len() > 0 { + return newBoolFormulaArg(true) + } + return newBoolFormulaArg(false) +} + // 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: // diff --git a/calc_test.go b/calc_test.go index 1de8fb8f51..50f8023eed 100644 --- a/calc_test.go +++ b/calc_test.go @@ -988,6 +988,17 @@ func TestCalcCellValue(t *testing.T) { // ISEVEN "=ISEVEN(A1)": "FALSE", "=ISEVEN(A2)": "TRUE", + // ISFORMULA + "=ISFORMULA(A1)": "FALSE", + "=ISFORMULA(\"A\")": "FALSE", + // ISLOGICAL + "=ISLOGICAL(TRUE)": "TRUE", + "=ISLOGICAL(FALSE)": "TRUE", + "=ISLOGICAL(A1=A2)": "TRUE", + "=ISLOGICAL(\"true\")": "TRUE", + "=ISLOGICAL(\"false\")": "TRUE", + "=ISLOGICAL(A1)": "FALSE", + "=ISLOGICAL(20/5)": "FALSE", // ISNA "=ISNA(A1)": "FALSE", "=ISNA(NA())": "TRUE", @@ -1002,6 +1013,11 @@ func TestCalcCellValue(t *testing.T) { // ISODD "=ISODD(A1)": "TRUE", "=ISODD(A2)": "FALSE", + // ISREF + "=ISREF(B1)": "TRUE", + "=ISREF(B1:B2)": "TRUE", + "=ISREF(\"text\")": "FALSE", + "=ISREF(B1*B2)": "FALSE", // ISTEXT "=ISTEXT(D1)": "TRUE", "=ISTEXT(A1)": "FALSE", @@ -2402,6 +2418,10 @@ func TestCalcCellValue(t *testing.T) { // ISEVEN "=ISEVEN()": "ISEVEN requires 1 argument", `=ISEVEN("text")`: "strconv.Atoi: parsing \"text\": invalid syntax", + // ISFORMULA + "=ISFORMULA()": "ISFORMULA requires 1 argument", + // ISLOGICAL + "=ISLOGICAL()": "ISLOGICAL requires 1 argument", // ISNA "=ISNA()": "ISNA requires 1 argument", // ISNONTEXT @@ -2411,6 +2431,8 @@ func TestCalcCellValue(t *testing.T) { // ISODD "=ISODD()": "ISODD requires 1 argument", `=ISODD("text")`: "strconv.Atoi: parsing \"text\": invalid syntax", + // ISREF + "=ISREF()": "ISREF requires 1 argument", // ISTEXT "=ISTEXT()": "ISTEXT requires 1 argument", // N @@ -3725,6 +3747,17 @@ func TestCalcMATCH(t *testing.T) { assert.Equal(t, newErrorFormulaArg(formulaErrorNA, formulaErrorNA), calcMatch(2, nil, []formulaArg{})) } +func TestCalcISFORMULA(t *testing.T) { + f := NewFile() + assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "=ISFORMULA(A1)")) + for _, formula := range []string{"=NA()", "=SUM(A1:A3)"} { + assert.NoError(t, f.SetCellFormula("Sheet1", "A1", formula)) + result, err := f.CalcCellValue("Sheet1", "B1") + assert.NoError(t, err, formula) + assert.Equal(t, "TRUE", 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 b33e39369aa3898d2f6e079240545f9d84abec73 Mon Sep 17 00:00:00 2001 From: jaby Date: Fri, 10 Dec 2021 09:48:02 +0100 Subject: [PATCH 509/957] This closes #1090 (#1092) Keep track of operators per function --- calc.go | 10 ++++++---- calc_test.go | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/calc.go b/calc.go index ebfa8154b9..d74a5fa71e 100644 --- a/calc.go +++ b/calc.go @@ -726,6 +726,7 @@ func (f *File) evalInfixExp(sheet, cell string, tokens []efp.Token) (efp.Token, if isFunctionStartToken(token) { 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 continue } @@ -738,7 +739,7 @@ func (f *File) evalInfixExp(sheet, cell string, tokens []efp.Token) (efp.Token, // current token is args or range, skip next token, order required: parse reference first if token.TSubType == efp.TokenSubTypeRange { - if !opftStack.Empty() { + if opftStack.Peek().(efp.Token) != opfStack.Peek().(efp.Token) { refTo := f.getDefinedNameRefTo(token.TValue, sheet) if refTo != "" { token.TValue = refTo @@ -783,7 +784,7 @@ func (f *File) evalInfixExp(sheet, cell string, tokens []efp.Token) (efp.Token, // current token is arg if token.TType == efp.TokenTypeArgument { - for !opftStack.Empty() { + for opftStack.Peek().(efp.Token) != opfStack.Peek().(efp.Token) { // calculate trigger topOpt := opftStack.Peek().(efp.Token) if err := calculate(opfdStack, topOpt); err != nil { @@ -826,7 +827,7 @@ func (f *File) evalInfixExpFunc(sheet, cell string, token, nextToken efp.Token, return nil } // current token is function stop - for !opftStack.Empty() { + for opftStack.Peek().(efp.Token) != opfStack.Peek().(efp.Token) { // calculate trigger topOpt := opftStack.Peek().(efp.Token) if err := calculate(opfdStack, topOpt); err != nil { @@ -847,9 +848,10 @@ func (f *File) evalInfixExpFunc(sheet, cell string, token, nextToken efp.Token, return errors.New(arg.Value()) } argsStack.Pop() + opftStack.Pop() // remove current function separator opfStack.Pop() if opfStack.Len() > 0 { // still in function stack - if nextToken.TType == efp.TokenTypeOperatorInfix { + if nextToken.TType == efp.TokenTypeOperatorInfix || opftStack.Len() > 1 { // mathematics calculate in formula function opfdStack.Push(efp.Token{TValue: arg.Value(), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) } else { diff --git a/calc_test.go b/calc_test.go index 50f8023eed..a2b1294148 100644 --- a/calc_test.go +++ b/calc_test.go @@ -3787,3 +3787,23 @@ func TestGetYearDays(t *testing.T) { assert.Equal(t, data[2], getYearDays(data[0], data[1])) } } + +func TestNestedFunctionsWithOperators(t *testing.T) { + f := NewFile() + formulaList := map[string]string{ + `=LEN("KEEP")`: "4", + `=LEN("REMOVEKEEP") - LEN("REMOVE")`: "4", + `=RIGHT("REMOVEKEEP", 4)`: "KEEP", + `=RIGHT("REMOVEKEEP", 10 - 6))`: "KEEP", + `=RIGHT("REMOVEKEEP", LEN("REMOVEKEEP") - 6)`: "KEEP", + `=RIGHT("REMOVEKEEP", LEN("REMOVEKEEP") - LEN("REMOV") - 1)`: "KEEP", + `=RIGHT("REMOVEKEEP", 10 - LEN("REMOVE"))`: "KEEP", + `=RIGHT("REMOVEKEEP", LEN("REMOVEKEEP") - LEN("REMOVE"))`: "KEEP", + } + 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) + } +} From 76aacfda0b7c0b3b21c2e0909b0f24eb9c5769a4 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 12 Dec 2021 00:07:01 +0800 Subject: [PATCH 510/957] ref #65: new formula function: SHEETS, and fix SHEET function count issue --- calc.go | 57 ++++++++++++++++++++++++++++++++++++++++++++++++---- calc_test.go | 47 ++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 97 insertions(+), 7 deletions(-) diff --git a/calc.go b/calc.go index d74a5fa71e..bd62d94710 100644 --- a/calc.go +++ b/calc.go @@ -556,6 +556,7 @@ type formulaFuncs struct { // SEC // SECH // SHEET +// SHEETS // SIGN // SIN // SINH @@ -6818,13 +6819,61 @@ 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() +// SHEET([value]) // func (fn *formulaFuncs) SHEET(argsList *list.List) formulaArg { - if argsList.Len() != 0 { - return newErrorFormulaArg(formulaErrorVALUE, "SHEET accepts no arguments") + if argsList.Len() > 1 { + return newErrorFormulaArg(formulaErrorVALUE, "SHEET accepts at most 1 argument") + } + if argsList.Len() == 0 { + return newNumberFormulaArg(float64(fn.f.GetSheetIndex(fn.sheet) + 1)) + } + arg := argsList.Front().Value.(formulaArg) + 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 { + 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 { + return newNumberFormulaArg(float64(sheetIdx + 1)) + } } - return newNumberFormulaArg(float64(fn.f.GetSheetIndex(fn.sheet) + 1)) + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) +} + +// SHEETS function returns the number of sheets in a supplied reference. The +// result includes sheets that are Visible, Hidden or Very Hidden. The syntax +// of the function is: +// +// SHEETS([reference]) +// +func (fn *formulaFuncs) SHEETS(argsList *list.List) formulaArg { + if argsList.Len() > 1 { + return newErrorFormulaArg(formulaErrorVALUE, "SHEETS accepts at most 1 argument") + } + if argsList.Len() == 0 { + return newNumberFormulaArg(float64(len(fn.f.GetSheetList()))) + } + arg := argsList.Front().Value.(formulaArg) + sheetMap := map[string]interface{}{} + if arg.cellRanges != nil && arg.cellRanges.Len() > 0 { + for rng := arg.cellRanges.Front(); rng != nil; rng = rng.Next() { + sheetMap[rng.Value.(cellRange).From.Sheet] = nil + } + } + if arg.cellRefs != nil && arg.cellRefs.Len() > 0 { + for ref := arg.cellRefs.Front(); ref != nil; ref = ref.Next() { + sheetMap[ref.Value.(cellRef).Sheet] = nil + } + } + if len(sheetMap) > 0 { + return newNumberFormulaArg(float64(len(sheetMap))) + } + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) } // T function tests if a supplied value is text and if so, returns the diff --git a/calc_test.go b/calc_test.go index a2b1294148..0aeff70194 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1028,7 +1028,11 @@ func TestCalcCellValue(t *testing.T) { "=N(TRUE)": "1", "=N(FALSE)": "0", // SHEET - "=SHEET()": "1", + "=SHEET()": "1", + "=SHEET(\"Sheet1\")": "1", + // SHEETS + "=SHEETS()": "1", + "=SHEETS(A1)": "1", // T "=T(\"text\")": "text", "=T(N(10))": "", @@ -2442,7 +2446,11 @@ func TestCalcCellValue(t *testing.T) { "=NA()": "#N/A", "=NA(1)": "NA accepts no arguments", // SHEET - "=SHEET(1)": "SHEET accepts no arguments", + "=SHEET(\"\",\"\")": "SHEET accepts at most 1 argument", + "=SHEET(\"Sheet2\")": "#N/A", + // SHEETS + "=SHEETS(\"\",\"\")": "SHEETS accepts at most 1 argument", + "=SHEETS(\"Sheet1\")": "#N/A", // T "=T()": "T requires 1 argument", "=T(NA())": "#N/A", @@ -2459,7 +2467,8 @@ func TestCalcCellValue(t *testing.T) { // IFNA "=IFNA()": "IFNA requires 2 arguments", // IFS - "=IFS()": "IFS requires at least 2 arguments", + "=IFS()": "IFS requires at least 2 arguments", + "=IFS(FALSE,FALSE)": "#N/A", // NOT "=NOT()": "NOT requires 1 argument", "=NOT(NOT())": "NOT requires 1 argument", @@ -3758,6 +3767,38 @@ func TestCalcISFORMULA(t *testing.T) { } } +func TestCalcSHEET(t *testing.T) { + f := NewFile() + f.NewSheet("Sheet2") + formulaList := map[string]string{ + "=SHEET(\"Sheet2\")": "2", + "=SHEET(Sheet2!A1)": "2", + "=SHEET(Sheet2!A1:A2)": "2", + } + for formula, expected := range formulaList { + assert.NoError(t, f.SetCellFormula("Sheet1", "A1", formula)) + result, err := f.CalcCellValue("Sheet1", "A1") + assert.NoError(t, err, formula) + assert.Equal(t, expected, result, formula) + } +} + +func TestCalcSHEETS(t *testing.T) { + f := NewFile() + f.NewSheet("Sheet2") + formulaList := map[string]string{ + "=SHEETS(Sheet1!A1:B1)": "1", + "=SHEETS(Sheet1!A1:Sheet1!A1)": "1", + "=SHEETS(Sheet1!A1:Sheet2!A1)": "2", + } + for formula, expected := range formulaList { + assert.NoError(t, f.SetCellFormula("Sheet1", "A1", formula)) + result, err := f.CalcCellValue("Sheet1", "A1") + assert.NoError(t, err, formula) + assert.Equal(t, expected, 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 33719334945f0ce0752cedfbd4267b83850ab85d Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 13 Dec 2021 00:02:25 +0800 Subject: [PATCH 511/957] ref #65: new formula functions: AVERAGEIF and COUNTIF --- calc.go | 110 ++++++++++++++++++++++++++++++++++++++++----------- calc_test.go | 48 +++++++++++++++++++++- 2 files changed, 133 insertions(+), 25 deletions(-) diff --git a/calc.go b/calc.go index bd62d94710..13d0ac2bfa 100644 --- a/calc.go +++ b/calc.go @@ -52,6 +52,7 @@ const ( criteriaEq criteriaLe criteriaGe + criteriaNe criteriaL criteriaG criteriaErr @@ -315,6 +316,7 @@ type formulaFuncs struct { // AVEDEV // AVERAGE // AVERAGEA +// AVERAGEIF // BASE // BESSELI // BESSELJ @@ -352,6 +354,7 @@ type formulaFuncs struct { // COUNT // COUNTA // COUNTBLANK +// COUNTIF // COUPDAYBS // COUPDAYS // COUPDAYSNC @@ -1419,6 +1422,10 @@ func formulaCriteriaParser(exp string) (fc *formulaCriteria) { 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 @@ -1467,6 +1474,8 @@ func formulaCriteriaEval(val string, criteria *formulaCriteria) (result bool, er case criteriaGe: value, expected, e = prepareValue(val, criteria.Condition) return value >= expected && e == nil, err + case criteriaNe: + return val != criteria.Condition, err case criteriaL: value, expected, e = prepareValue(val, criteria.Condition) return value < expected && e == nil, err @@ -4723,7 +4732,7 @@ func (fn *formulaFuncs) SUM(argsList *list.List) formulaArg { // func (fn *formulaFuncs) SUMIF(argsList *list.List) formulaArg { if argsList.Len() < 2 { - return newErrorFormulaArg(formulaErrorVALUE, "SUMIF requires at least 2 argument") + return newErrorFormulaArg(formulaErrorVALUE, "SUMIF requires at least 2 arguments") } var criteria = formulaCriteriaParser(argsList.Front().Next().Value.(formulaArg).String) var rangeMtx = argsList.Front().Value.(formulaArg).Matrix @@ -4740,9 +4749,7 @@ func (fn *formulaFuncs) SUMIF(argsList *list.List) formulaArg { if col.String == "" { continue } - if ok, err = formulaCriteriaEval(fromVal, criteria); err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) - } + ok, _ = formulaCriteriaEval(fromVal, criteria) if ok { if argsList.Len() == 3 { if len(sumRange) <= rowIdx || len(sumRange[rowIdx]) <= colIdx { @@ -4751,7 +4758,7 @@ func (fn *formulaFuncs) SUMIF(argsList *list.List) formulaArg { fromVal = sumRange[rowIdx][colIdx].String } if val, err = strconv.ParseFloat(fromVal, 64); err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + continue } sum += val } @@ -4927,6 +4934,57 @@ func (fn *formulaFuncs) AVERAGEA(argsList *list.List) formulaArg { return newNumberFormulaArg(sum / count) } +// AVERAGEIF function finds the values in a supplied array that satisfy a +// specified criteria, and returns the average (i.e. the statistical mean) of +// the corresponding values in a second supplied array. The syntax of the +// function is: +// +// 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") + } + var ( + criteria = formulaCriteriaParser(argsList.Front().Next().Value.(formulaArg).String) + rangeMtx = argsList.Front().Value.(formulaArg).Matrix + cellRange [][]formulaArg + args []formulaArg + val float64 + err error + ok bool + ) + if argsList.Len() == 3 { + cellRange = argsList.Back().Value.(formulaArg).Matrix + } + for rowIdx, row := range rangeMtx { + for colIdx, col := range row { + fromVal := col.String + if col.String == "" { + continue + } + ok, _ = formulaCriteriaEval(fromVal, criteria) + if ok { + if argsList.Len() == 3 { + if len(cellRange) <= rowIdx || len(cellRange[rowIdx]) <= colIdx { + continue + } + fromVal = cellRange[rowIdx][colIdx].String + } + if val, err = strconv.ParseFloat(fromVal, 64); err != nil { + continue + } + args = append(args, newNumberFormulaArg(val)) + } + } + } + count, sum := fn.countSum(false, args) + if count == 0 { + return newErrorFormulaArg(formulaErrorDIV, "AVERAGEIF divide by zero") + } + return newNumberFormulaArg(sum / count) +} + // incompleteGamma is an implementation of the incomplete gamma function. func incompleteGamma(a, x float64) float64 { max := 32 @@ -5134,28 +5192,34 @@ func (fn *formulaFuncs) COUNTBLANK(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "COUNTBLANK requires 1 argument") } - var count int - token := argsList.Front().Value.(formulaArg) - switch token.Type { - case ArgString: - if token.String == "" { + var count float64 + for _, cell := range argsList.Front().Value.(formulaArg).ToList() { + if cell.Value() == "" { count++ } - case ArgList, ArgMatrix: - for _, row := range token.ToList() { - switch row.Type { - case ArgString: - if row.String == "" { - count++ - } - case ArgEmpty: - count++ - } + } + return newNumberFormulaArg(count) +} + +// 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) +// +func (fn *formulaFuncs) COUNTIF(argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, "COUNTIF requires 2 arguments") + } + var ( + criteria = formulaCriteriaParser(argsList.Front().Next().Value.(formulaArg).String) + count float64 + ) + for _, cell := range argsList.Front().Value.(formulaArg).ToList() { + if ok, _ := formulaCriteriaEval(cell.Value(), criteria); ok { + count++ } - case ArgEmpty: - count++ } - return newNumberFormulaArg(float64(count)) + return newNumberFormulaArg(count) } // DEVSQ function calculates the sum of the squared deviations from the sample diff --git a/calc_test.go b/calc_test.go index 0aeff70194..91e71d7748 100644 --- a/calc_test.go +++ b/calc_test.go @@ -737,6 +737,7 @@ func TestCalcCellValue(t *testing.T) { `=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", // SUMSQ "=SUMSQ(A1:A4)": "14", "=SUMSQ(A1,B1,A2,B2,6)": "82", @@ -793,6 +794,11 @@ func TestCalcCellValue(t *testing.T) { "=COUNTBLANK(1)": "0", "=COUNTBLANK(B1:C1)": "1", "=COUNTBLANK(C1)": "1", + // COUNTIF + "=COUNTIF(D1:D9,\"Jan\")": "4", + "=COUNTIF(D1:D9,\"<>Jan\")": "5", + "=COUNTIF(A1:F9,\">=50000\")": "2", + "=COUNTIF(A1:F9,TRUE)": "0", // DEVSQ "=DEVSQ(1,3,5,2,9,7)": "47.5", "=DEVSQ(A1:D2)": "10", @@ -2150,7 +2156,7 @@ func TestCalcCellValue(t *testing.T) { "=SUM(1*)": ErrInvalidFormula.Error(), "=SUM(1/)": ErrInvalidFormula.Error(), // SUMIF - "=SUMIF()": "SUMIF requires at least 2 argument", + "=SUMIF()": "SUMIF requires at least 2 arguments", // SUMSQ `=SUMSQ("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", "=SUMSQ(C1:D2)": "strconv.ParseFloat: parsing \"Month\": invalid syntax", @@ -2171,8 +2177,13 @@ func TestCalcCellValue(t *testing.T) { "=AVEDEV(1,\"\")": "#VALUE!", // AVERAGE "=AVERAGE(H1)": "AVERAGE divide by zero", - // AVERAGE + // 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", // CHIDIST "=CHIDIST()": "CHIDIST requires 2 numeric arguments", "=CHIDIST(\"\",3)": "strconv.ParseFloat: parsing \"\": invalid syntax", @@ -2198,6 +2209,8 @@ func TestCalcCellValue(t *testing.T) { // COUNTBLANK "=COUNTBLANK()": "COUNTBLANK requires 1 argument", "=COUNTBLANK(1,2)": "COUNTBLANK requires 1 argument", + // COUNTIF + "=COUNTIF()": "COUNTIF requires 2 arguments", // DEVSQ "=DEVSQ()": "DEVSQ requires at least 1 numeric argument", "=DEVSQ(D1:D2)": "#N/A", @@ -3544,6 +3557,37 @@ func TestCalcBoolean(t *testing.T) { } } +func TestCalcAVERAGEIF(t *testing.T) { + f := prepareCalcData([][]interface{}{ + {"Monday", 500}, + {"Tuesday", 50}, + {"Thursday", 100}, + {"Friday", 100}, + {"Thursday", 200}, + {5, 300}, + {2, 200}, + {3, 100}, + {4, 50}, + {5, 100}, + {1, 50}, + {"TRUE", 200}, + {"TRUE", 250}, + {"FALSE", 50}, + }) + for formula, expected := range map[string]string{ + "=AVERAGEIF(A1:A14,\"Thursday\",B1:B14)": "150", + "=AVERAGEIF(A1:A14,5,B1:B14)": "200", + "=AVERAGEIF(A1:A14,\">2\",B1:B14)": "137.5", + "=AVERAGEIF(A1:A14,TRUE,B1:B14)": "225", + "=AVERAGEIF(A1:A14,\"<>TRUE\",B1:B14)": "150", + } { + 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 TestCalcHLOOKUP(t *testing.T) { cellData := [][]interface{}{ {"Example Result Table"}, From c0ac3165bd8923efdaf95b377771d14f5879b1ec Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 14 Dec 2021 00:38:16 +0800 Subject: [PATCH 512/957] ref #65: new formula function COUNTIFS --- calc.go | 49 ++++++++++++++++++++++++++++++++++++++++++++++--- calc_test.go | 6 ++++++ 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/calc.go b/calc.go index 13d0ac2bfa..79fefb1263 100644 --- a/calc.go +++ b/calc.go @@ -355,6 +355,7 @@ type formulaFuncs struct { // COUNTA // COUNTBLANK // COUNTIF +// COUNTIFS // COUPDAYBS // COUPDAYS // COUPDAYSNC @@ -5222,6 +5223,48 @@ func (fn *formulaFuncs) COUNTIF(argsList *list.List) formulaArg { return newNumberFormulaArg(count) } +// 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],...) +// +func (fn *formulaFuncs) COUNTIFS(argsList *list.List) formulaArg { + if argsList.Len() < 2 { + return newErrorFormulaArg(formulaErrorVALUE, "COUNTIFS requires at least 2 arguments") + } + if argsList.Len()%2 != 0 { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + } + group, rowsIdx := 0, map[int]struct{}{} + for criteriaRange := argsList.Front(); criteriaRange != nil; criteriaRange = criteriaRange.Next() { + criteria := criteriaRange.Next() + if group == 0 { + for rowIdx, row := range criteriaRange.Value.(formulaArg).Matrix { + for _, col := range row { + if ok, _ := formulaCriteriaEval(col.String, formulaCriteriaParser(criteria.Value.(formulaArg).Value())); ok { + rowsIdx[rowIdx] = struct{}{} + } + } + } + } else { + for rowIdx, row := range criteriaRange.Value.(formulaArg).Matrix { + if _, ok := rowsIdx[rowIdx]; !ok { + delete(rowsIdx, rowIdx) + continue + } + for _, col := range row { + if ok, _ := formulaCriteriaEval(col.String, formulaCriteriaParser(criteria.Value.(formulaArg).Value())); !ok { + delete(rowsIdx, rowIdx) + } + } + } + } + criteriaRange = criteriaRange.Next() + group++ + } + return newNumberFormulaArg(float64(len(rowsIdx))) +} + // DEVSQ function calculates the sum of the squared deviations from the sample // mean. The syntax of the function is: // @@ -6923,15 +6966,15 @@ func (fn *formulaFuncs) SHEETS(argsList *list.List) formulaArg { return newNumberFormulaArg(float64(len(fn.f.GetSheetList()))) } arg := argsList.Front().Value.(formulaArg) - sheetMap := map[string]interface{}{} + sheetMap := map[string]struct{}{} if arg.cellRanges != nil && arg.cellRanges.Len() > 0 { for rng := arg.cellRanges.Front(); rng != nil; rng = rng.Next() { - sheetMap[rng.Value.(cellRange).From.Sheet] = nil + sheetMap[rng.Value.(cellRange).From.Sheet] = struct{}{} } } if arg.cellRefs != nil && arg.cellRefs.Len() > 0 { for ref := arg.cellRefs.Front(); ref != nil; ref = ref.Next() { - sheetMap[ref.Value.(cellRef).Sheet] = nil + sheetMap[ref.Value.(cellRef).Sheet] = struct{}{} } } if len(sheetMap) > 0 { diff --git a/calc_test.go b/calc_test.go index 91e71d7748..0544806f2c 100644 --- a/calc_test.go +++ b/calc_test.go @@ -799,6 +799,9 @@ func TestCalcCellValue(t *testing.T) { "=COUNTIF(D1:D9,\"<>Jan\")": "5", "=COUNTIF(A1:F9,\">=50000\")": "2", "=COUNTIF(A1:F9,TRUE)": "0", + // COUNTIFS + "=COUNTIFS(A1:A9,2,D1:D9,\"Jan\")": "1", + "=COUNTIFS(F1:F9,\">20000\",D1:D9,\"Jan\")": "4", // DEVSQ "=DEVSQ(1,3,5,2,9,7)": "47.5", "=DEVSQ(A1:D2)": "10", @@ -2211,6 +2214,9 @@ func TestCalcCellValue(t *testing.T) { "=COUNTBLANK(1,2)": "COUNTBLANK requires 1 argument", // COUNTIF "=COUNTIF()": "COUNTIF requires 2 arguments", + // COUNTIFS + "=COUNTIFS()": "COUNTIFS requires at least 2 arguments", + "=COUNTIFS(A1:A9,2,D1:D9)": "#N/A", // DEVSQ "=DEVSQ()": "DEVSQ requires at least 1 numeric argument", "=DEVSQ(D1:D2)": "#N/A", From 63fe422299ed9d31d079711d1173b288faa6838d Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 17 Dec 2021 00:08:56 +0800 Subject: [PATCH 513/957] new formula function and update docs for formula functions ref #65, new formula function: INDEX (array formula not support yet) --- calc.go | 93 +++++++++++++++++++++++++++++++++++++++++++++------- calc_test.go | 19 +++++++++++ 2 files changed, 100 insertions(+), 12 deletions(-) diff --git a/calc.go b/calc.go index 79fefb1263..cf2f95a525 100644 --- a/calc.go +++ b/calc.go @@ -445,6 +445,7 @@ type formulaFuncs struct { // IMSUB // IMSUM // IMTAN +// INDEX // INT // INTRATE // IPMT @@ -5791,7 +5792,7 @@ func calcListMatrixMax(maxa bool, max float64, arg formulaArg) float64 { return max } -// max is an implementation of the formula function MAX and MAXA. +// max is an implementation of the formula functions MAX and MAXA. func (fn *formulaFuncs) max(maxa bool, argsList *list.List) formulaArg { max := -math.MaxFloat64 for token := argsList.Front(); token != nil; token = token.Next() { @@ -5926,7 +5927,7 @@ func calcListMatrixMin(mina bool, min float64, arg formulaArg) float64 { return min } -// min is an implementation of the formula function MIN and MINA. +// min is an implementation of the formula functions MIN and MINA. func (fn *formulaFuncs) min(mina bool, argsList *list.List) formulaArg { min := math.MaxFloat64 for token := argsList.Front(); token != nil; token = token.Next() { @@ -8136,7 +8137,7 @@ func (fn *formulaFuncs) CODE(argsList *list.List) formulaArg { return fn.code("CODE", argsList) } -// code is an implementation of the formula function CODE and UNICODE. +// code is an implementation of the formula functions CODE and UNICODE. func (fn *formulaFuncs) code(name string, argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires 1 argument", name)) @@ -8169,7 +8170,8 @@ func (fn *formulaFuncs) CONCATENATE(argsList *list.List) formulaArg { return fn.concat("CONCATENATE", argsList) } -// concat is an implementation of the formula function CONCAT and CONCATENATE. +// concat is an implementation of the formula functions CONCAT and +// CONCATENATE. func (fn *formulaFuncs) concat(name string, argsList *list.List) formulaArg { buf := bytes.Buffer{} for arg := argsList.Front(); arg != nil; arg = arg.Next() { @@ -8279,7 +8281,7 @@ func (fn *formulaFuncs) FINDB(argsList *list.List) formulaArg { return fn.find("FINDB", argsList) } -// find is an implementation of the formula function FIND and FINDB. +// find is an implementation of the formula functions FIND and FINDB. func (fn *formulaFuncs) find(name string, argsList *list.List) formulaArg { if argsList.Len() < 2 { return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires at least 2 arguments", name)) @@ -8333,7 +8335,7 @@ func (fn *formulaFuncs) LEFTB(argsList *list.List) formulaArg { return fn.leftRight("LEFTB", argsList) } -// leftRight is an implementation of the formula function LEFT, LEFTB, RIGHT, +// leftRight is an implementation of the formula functions LEFT, LEFTB, RIGHT, // RIGHTB. TODO: support DBCS include Japanese, Chinese (Simplified), Chinese // (Traditional), and Korean. func (fn *formulaFuncs) leftRight(name string, argsList *list.List) formulaArg { @@ -8422,7 +8424,7 @@ func (fn *formulaFuncs) MIDB(argsList *list.List) formulaArg { return fn.mid("MIDB", argsList) } -// mid is an implementation of the formula function MID and MIDB. TODO: +// mid is an implementation of the formula functions MID and MIDB. TODO: // support DBCS include Japanese, Chinese (Simplified), Chinese // (Traditional), and Korean. func (fn *formulaFuncs) mid(name string, argsList *list.List) formulaArg { @@ -8495,7 +8497,7 @@ func (fn *formulaFuncs) REPLACEB(argsList *list.List) formulaArg { return fn.replace("REPLACEB", argsList) } -// replace is an implementation of the formula function REPLACE and REPLACEB. +// replace is an implementation of the formula functions REPLACE and REPLACEB. // TODO: support DBCS include Japanese, Chinese (Simplified), Chinese // (Traditional), and Korean. func (fn *formulaFuncs) replace(name string, argsList *list.List) formulaArg { @@ -9491,6 +9493,73 @@ func iterateLookupArgs(lookupValue, lookupVector formulaArg) ([]formulaArg, int, return cols, matchIdx, ok } +// index is an implementation of the formula function INDEX. +func (fn *formulaFuncs) index(array formulaArg, rowIdx, colIdx int) formulaArg { + var cells []formulaArg + if array.Type == ArgMatrix { + cellMatrix := array.Matrix + if rowIdx < -1 || rowIdx >= len(cellMatrix) { + return newErrorFormulaArg(formulaErrorREF, "INDEX row_num out of range") + } + if rowIdx == -1 { + if colIdx >= len(cellMatrix[0]) { + return newErrorFormulaArg(formulaErrorREF, "INDEX col_num out of range") + } + column := [][]formulaArg{} + for _, cells = range cellMatrix { + column = append(column, []formulaArg{cells[colIdx]}) + } + return newMatrixFormulaArg(column) + } + cells = cellMatrix[rowIdx] + } + if colIdx < -1 || colIdx >= len(cells) { + return newErrorFormulaArg(formulaErrorREF, "INDEX col_num out of range") + } + return newListFormulaArg(cells) +} + +// 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]) +// +func (fn *formulaFuncs) INDEX(argsList *list.List) formulaArg { + if argsList.Len() < 2 || argsList.Len() > 3 { + return newErrorFormulaArg(formulaErrorVALUE, "INDEX requires 2 or 3 arguments") + } + array := argsList.Front().Value.(formulaArg) + if array.Type != ArgMatrix && array.Type != ArgList { + array = newMatrixFormulaArg([][]formulaArg{{array}}) + } + rowArg := argsList.Front().Next().Value.(formulaArg).ToNumber() + if rowArg.Type != ArgNumber { + return rowArg + } + rowIdx, colIdx := int(rowArg.Number)-1, -1 + if argsList.Len() == 3 { + colArg := argsList.Back().Value.(formulaArg).ToNumber() + if colArg.Type != ArgNumber { + return colArg + } + colIdx = int(colArg.Number) - 1 + } + if rowIdx == -1 && colIdx == -1 { + if len(array.ToList()) != 1 { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + return array.ToList()[0] + } + cells := fn.index(array, rowIdx, colIdx) + if cells.Type != ArgList { + return cells + } + if colIdx == -1 { + return newMatrixFormulaArg([][]formulaArg{cells.List}) + } + return cells.List[colIdx] +} + // LOOKUP function performs an approximate match lookup in a one-column or // one-row range, and returns the corresponding value from another one-column // or one-row range. The syntax of the function is: @@ -10011,7 +10080,7 @@ func (fn *formulaFuncs) COUPDAYSNC(argsList *list.List) formulaArg { return newNumberFormulaArg(coupdays(settlement, ncd, basis)) } -// coupons is an implementation of the formula function COUPNCD and COUPPCD. +// coupons is an implementation of the formula functions COUPNCD and COUPPCD. func (fn *formulaFuncs) coupons(name string, arg formulaArg) formulaArg { settlement := timeFromExcelTime(arg.List[0].Number, false) maturity := timeFromExcelTime(arg.List[1].Number, false) @@ -10106,7 +10175,7 @@ func (fn *formulaFuncs) CUMPRINC(argsList *list.List) formulaArg { return fn.cumip("CUMPRINC", argsList) } -// cumip is an implementation of the formula function CUMIPMT and CUMPRINC. +// cumip is an implementation of the formula functions CUMIPMT and CUMPRINC. func (fn *formulaFuncs) cumip(name string, argsList *list.List) formulaArg { if argsList.Len() != 6 { return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires 6 arguments", name)) @@ -10370,7 +10439,7 @@ func (fn *formulaFuncs) DOLLARFR(argsList *list.List) formulaArg { return fn.dollar("DOLLARFR", argsList) } -// dollar is an implementation of the formula function DOLLARDE and DOLLARFR. +// dollar is an implementation of the formula functions DOLLARDE and DOLLARFR. func (fn *formulaFuncs) dollar(name string, argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires 2 arguments", name)) @@ -10658,7 +10727,7 @@ func calcIpmt(name string, typ, per, pmt, pv, rate formulaArg) formulaArg { return newNumberFormulaArg(principal) } -// ipmt is an implementation of the formula function IPMT and PPMT. +// ipmt is an implementation of the formula functions IPMT and PPMT. func (fn *formulaFuncs) ipmt(name string, argsList *list.List) formulaArg { if argsList.Len() < 4 { return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires at least 4 arguments", name)) diff --git a/calc_test.go b/calc_test.go index 0544806f2c..89ec5e5832 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1403,6 +1403,15 @@ func TestCalcCellValue(t *testing.T) { "=VLOOKUP(MUNIT(3),MUNIT(3),1)": "0", "=VLOOKUP(A1,A3:B5,1)": "0", "=VLOOKUP(MUNIT(1),MUNIT(1),1,FALSE)": "1", + // INDEX + "=INDEX(0,0,0)": "0", + "=INDEX(A1,0,0)": "1", + "=INDEX(A1:A1,0,0)": "1", + "=SUM(INDEX(A1:B1,1))": "5", + "=SUM(INDEX(A1:B1,1,0))": "5", + "=SUM(INDEX(A1:B2,2,0))": "7", + "=SUM(INDEX(A1:B4,0,2))": "9", + "=SUM(INDEX(E1:F5,5,2))": "34440", // LOOKUP "=LOOKUP(F8,F8:F9,F8:F9)": "32080", "=LOOKUP(F8,F8:F9,D8:D9)": "Feb", @@ -2792,6 +2801,16 @@ func TestCalcCellValue(t *testing.T) { "=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", + // 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", // LOOKUP "=LOOKUP()": "LOOKUP requires at least 2 arguments", "=LOOKUP(D2,D1,D2)": "LOOKUP requires second argument of table array", From 6051434bf8988947e2a9688ff2359768db385087 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 19 Dec 2021 13:36:24 +0800 Subject: [PATCH 514/957] ref #65, new formula functions MAXIFS and MINIFS --- calc.go | 119 +++++++++++++++++++++++++++++++++++++++------------ calc_test.go | 36 ++++++++++++++++ 2 files changed, 128 insertions(+), 27 deletions(-) diff --git a/calc.go b/calc.go index cf2f95a525..0ce5aece59 100644 --- a/calc.go +++ b/calc.go @@ -479,6 +479,8 @@ type formulaFuncs struct { // LOWER // MATCH // MAX +// MAXA +// MAXIFS // MDETERM // MDURATION // MEDIAN @@ -486,6 +488,7 @@ type formulaFuncs struct { // MIDB // MIN // MINA +// MINIFS // MINUTE // MIRR // MOD @@ -5224,6 +5227,35 @@ func (fn *formulaFuncs) COUNTIF(argsList *list.List) formulaArg { return newNumberFormulaArg(count) } +// formulaIfsMatch function returns cells reference array which match criterias. +func formulaIfsMatch(args []formulaArg) (cellRefs []cellRef) { + for i := 0; i < len(args)-1; i += 2 { + match := []cellRef{} + matrix, criteria := args[i].Matrix, formulaCriteriaParser(args[i+1].Value()) + if i == 0 { + for rowIdx, row := range matrix { + for colIdx, col := range row { + if ok, _ := formulaCriteriaEval(col.Value(), criteria); ok { + match = append(match, cellRef{Col: colIdx, Row: rowIdx}) + } + } + } + } else { + for _, ref := range cellRefs { + value := matrix[ref.Row][ref.Col] + if ok, _ := formulaCriteriaEval(value.Value(), criteria); ok { + match = append(match, ref) + } + } + } + if len(match) == 0 { + return + } + cellRefs = match[:] + } + return +} + // COUNTIFS function returns the number of rows within a table, that satisfy a // set of given criteria. The syntax of the function is: // @@ -5236,34 +5268,11 @@ func (fn *formulaFuncs) COUNTIFS(argsList *list.List) formulaArg { if argsList.Len()%2 != 0 { return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) } - group, rowsIdx := 0, map[int]struct{}{} - for criteriaRange := argsList.Front(); criteriaRange != nil; criteriaRange = criteriaRange.Next() { - criteria := criteriaRange.Next() - if group == 0 { - for rowIdx, row := range criteriaRange.Value.(formulaArg).Matrix { - for _, col := range row { - if ok, _ := formulaCriteriaEval(col.String, formulaCriteriaParser(criteria.Value.(formulaArg).Value())); ok { - rowsIdx[rowIdx] = struct{}{} - } - } - } - } else { - for rowIdx, row := range criteriaRange.Value.(formulaArg).Matrix { - if _, ok := rowsIdx[rowIdx]; !ok { - delete(rowsIdx, rowIdx) - continue - } - for _, col := range row { - if ok, _ := formulaCriteriaEval(col.String, formulaCriteriaParser(criteria.Value.(formulaArg).Value())); !ok { - delete(rowsIdx, rowIdx) - } - } - } - } - criteriaRange = criteriaRange.Next() - group++ + args := []formulaArg{} + for arg := argsList.Front(); arg != nil; arg = arg.Next() { + args = append(args, arg.Value.(formulaArg)) } - return newNumberFormulaArg(float64(len(rowsIdx))) + return newNumberFormulaArg(float64(len(formulaIfsMatch(args)))) } // DEVSQ function calculates the sum of the squared deviations from the sample @@ -5765,6 +5774,34 @@ func (fn *formulaFuncs) MAXA(argsList *list.List) formulaArg { return fn.max(true, argsList) } +// MAXIFS function returns the maximum value from a subset of values that are +// specified according to one or more criteria. The syntax of the function +// is: +// +// 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") + } + if argsList.Len()%2 != 1 { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + } + max, maxRange, args := -math.MaxFloat64, 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 := maxRange[ref.Row][ref.Col].ToNumber(); num.Type == ArgNumber && max < num.Number { + max = num.Number + } + } + if max == -math.MaxFloat64 { + max = 0 + } + return newNumberFormulaArg(max) +} + // calcListMatrixMax is part of the implementation max. func calcListMatrixMax(maxa bool, max float64, arg formulaArg) float64 { for _, row := range arg.ToList() { @@ -5900,6 +5937,34 @@ func (fn *formulaFuncs) MINA(argsList *list.List) formulaArg { return fn.min(true, argsList) } +// MINIFS function returns the minimum value from a subset of values that are +// specified according to one or more criteria. The syntax of the function +// is: +// +// 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") + } + if argsList.Len()%2 != 1 { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + } + min, minRange, args := math.MaxFloat64, 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 := minRange[ref.Row][ref.Col].ToNumber(); num.Type == ArgNumber && min > num.Number { + min = num.Number + } + } + if min == math.MaxFloat64 { + min = 0 + } + return newNumberFormulaArg(min) +} + // calcListMatrixMin is part of the implementation min. func calcListMatrixMin(mina bool, min float64, arg formulaArg) float64 { for _, row := range arg.ToList() { diff --git a/calc_test.go b/calc_test.go index 89ec5e5832..97a7588ee9 100644 --- a/calc_test.go +++ b/calc_test.go @@ -2,6 +2,7 @@ package excelize import ( "container/list" + "math" "path/filepath" "strings" "testing" @@ -802,6 +803,7 @@ func TestCalcCellValue(t *testing.T) { // COUNTIFS "=COUNTIFS(A1:A9,2,D1:D9,\"Jan\")": "1", "=COUNTIFS(F1:F9,\">20000\",D1:D9,\"Jan\")": "4", + "=COUNTIFS(F1:F9,\">60000\",D1:D9,\"Jan\")": "0", // DEVSQ "=DEVSQ(1,3,5,2,9,7)": "47.5", "=DEVSQ(A1:D2)": "10", @@ -872,6 +874,8 @@ func TestCalcCellValue(t *testing.T) { "=MAXA(MUNIT(2))": "1", "=MAXA(INT(1))": "1", "=MAXA(A1:B4,MUNIT(1),INT(0),1,E1:F2,\"\")": "36693", + // MAXIFS + "=MAXIFS(F2:F4,A2:A4,\">0\")": "36693", // MEDIAN "=MEDIAN(A1:A5,12)": "2", "=MEDIAN(A1:A5)": "1.5", @@ -891,6 +895,8 @@ func TestCalcCellValue(t *testing.T) { "=MINA(MUNIT(2))": "0", "=MINA(INT(1))": "1", "=MINA(A1:B4,MUNIT(1),INT(0),1,E1:F2,\"\")": "0", + // MINIFS + "=MINIFS(F2:F4,A2:A4,\">0\")": "22100", // PERCENTILE.EXC "=PERCENTILE.EXC(A1:A4,0.2)": "0", "=PERCENTILE.EXC(A1:A4,0.6)": "2", @@ -2296,6 +2302,9 @@ func TestCalcCellValue(t *testing.T) { // MAXA "=MAXA()": "MAXA requires at least 1 argument", "=MAXA(NA())": "#N/A", + // MAXIFS + "=MAXIFS()": "MAXIFS requires at least 3 arguments", + "=MAXIFS(F2:F4,A2:A4,\">0\",D2:D9)": "#N/A", // MEDIAN "=MEDIAN()": "MEDIAN requires at least 1 argument", "=MEDIAN(\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", @@ -2306,6 +2315,9 @@ func TestCalcCellValue(t *testing.T) { // MINA "=MINA()": "MINA requires at least 1 argument", "=MINA(NA())": "#N/A", + // MINIFS + "=MINIFS()": "MINIFS requires at least 3 arguments", + "=MINIFS(F2:F4,A2:A4,\"<0\",D2:D9)": "#N/A", // PERCENTILE.EXC "=PERCENTILE.EXC()": "PERCENTILE.EXC requires 2 arguments", "=PERCENTILE.EXC(A1:A4,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", @@ -3679,6 +3691,30 @@ func TestCalcIRR(t *testing.T) { } } +func TestCalcMAXMINIFS(t *testing.T) { + f := NewFile() + for cell, row := range map[string][]interface{}{ + "A1": {1, -math.MaxFloat64 - 1}, + "A2": {2, -math.MaxFloat64 - 2}, + "A3": {3, math.MaxFloat64 + 1}, + "A4": {4, math.MaxFloat64 + 2}, + } { + assert.NoError(t, f.SetSheetRow("Sheet1", cell, &row)) + } + formulaList := map[string]string{ + "=MAX(B1:B2)": "0", + "=MAXIFS(B1:B2,A1:A2,\">0\")": "0", + "=MIN(B3:B4)": "0", + "=MINIFS(B3:B4,A3:A4,\"<0\")": "0", + } + 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 TestCalcMIRR(t *testing.T) { cellData := [][]interface{}{{-100}, {18}, {22.5}, {28}, {35.5}, {45}} f := prepareCalcData(cellData) From 089cd365a33c1cb0b50af2f4e766a05468cb8277 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 25 Dec 2021 21:51:09 +0800 Subject: [PATCH 515/957] This closes #1097, initialized formula function XLOOKUP and update test --- calc.go | 356 +++++++++++++++++++++++++++++++---------------- calc_test.go | 117 +++++++++++++++- excelize_test.go | 11 +- 3 files changed, 360 insertions(+), 124 deletions(-) diff --git a/calc.go b/calc.go index 0ce5aece59..682435ae9d 100644 --- a/calc.go +++ b/calc.go @@ -57,6 +57,17 @@ const ( criteriaG criteriaErr criteriaRegexp + + matchModeExact = 0 + matchModeMinGreater = 1 + matchModeMaxLess = -1 + matchModeWildcard = 2 + + searchModeLinear = 1 + searchModeReverseLinear = -1 + searchModeAscBinary = 2 + searchModeDescBinary = -2 + maxFinancialIterations = 128 financialPercision = 1.0e-08 // Date and time format regular expressions @@ -860,7 +871,7 @@ func (f *File) evalInfixExpFunc(sheet, cell string, token, nextToken efp.Token, opftStack.Pop() // remove current function separator opfStack.Pop() if opfStack.Len() > 0 { // still in function stack - if nextToken.TType == efp.TokenTypeOperatorInfix || opftStack.Len() > 1 { + if nextToken.TType == efp.TokenTypeOperatorInfix || (opftStack.Len() > 1 && opfdStack.Len() > 0) { // mathematics calculate in formula function opfdStack.Push(efp.Token{TValue: arg.Value(), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) } else { @@ -5227,7 +5238,7 @@ func (fn *formulaFuncs) COUNTIF(argsList *list.List) formulaArg { return newNumberFormulaArg(count) } -// formulaIfsMatch function returns cells reference array which match criterias. +// 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{} @@ -9013,7 +9024,7 @@ func matchPattern(pattern, name string) (matched bool) { // 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 formulaArg, caseSensitive, exactMatch bool) byte { +func compareFormulaArg(lhs, rhs, matchMode formulaArg, caseSensitive bool) byte { if lhs.Type != rhs.Type { return criteriaErr } @@ -9031,35 +9042,26 @@ func compareFormulaArg(lhs, rhs formulaArg, caseSensitive, exactMatch bool) byte if !caseSensitive { ls, rs = strings.ToLower(ls), strings.ToLower(rs) } - if exactMatch { - match := matchPattern(rs, ls) - if match { + if matchMode.Number == matchModeWildcard { + if matchPattern(rs, ls) { return criteriaEq } - return criteriaG - } - switch strings.Compare(ls, rs) { - case 1: - return criteriaG - case -1: - return criteriaL - case 0: - return criteriaEq + } - return criteriaErr + return map[int]byte{1: criteriaG, -1: criteriaL, 0: criteriaEq}[strings.Compare(ls, rs)] case ArgEmpty: return criteriaEq case ArgList: - return compareFormulaArgList(lhs, rhs, caseSensitive, exactMatch) + return compareFormulaArgList(lhs, rhs, matchMode, caseSensitive) case ArgMatrix: - return compareFormulaArgMatrix(lhs, rhs, caseSensitive, exactMatch) + return compareFormulaArgMatrix(lhs, rhs, matchMode, caseSensitive) } return criteriaErr } // compareFormulaArgList compares the left-hand sides and the right-hand sides // list type formula arguments. -func compareFormulaArgList(lhs, rhs formulaArg, caseSensitive, exactMatch bool) byte { +func compareFormulaArgList(lhs, rhs, matchMode formulaArg, caseSensitive bool) byte { if len(lhs.List) < len(rhs.List) { return criteriaL } @@ -9067,7 +9069,7 @@ func compareFormulaArgList(lhs, rhs formulaArg, caseSensitive, exactMatch bool) return criteriaG } for arg := range lhs.List { - criteria := compareFormulaArg(lhs.List[arg], rhs.List[arg], caseSensitive, exactMatch) + criteria := compareFormulaArg(lhs.List[arg], rhs.List[arg], matchMode, caseSensitive) if criteria != criteriaEq { return criteria } @@ -9077,7 +9079,7 @@ func compareFormulaArgList(lhs, rhs formulaArg, caseSensitive, exactMatch bool) // compareFormulaArgMatrix compares the left-hand sides and the right-hand sides // matrix type formula arguments. -func compareFormulaArgMatrix(lhs, rhs formulaArg, caseSensitive, exactMatch bool) byte { +func compareFormulaArgMatrix(lhs, rhs, matchMode formulaArg, caseSensitive bool) byte { if len(lhs.Matrix) < len(rhs.Matrix) { return criteriaL } @@ -9094,7 +9096,7 @@ func compareFormulaArgMatrix(lhs, rhs formulaArg, caseSensitive, exactMatch bool return criteriaG } for arg := range left { - criteria := compareFormulaArg(left[arg], right[arg], caseSensitive, exactMatch) + criteria := compareFormulaArg(left[arg], right[arg], matchMode, caseSensitive) if criteria != criteriaEq { return criteria } @@ -9190,7 +9192,7 @@ func (fn *formulaFuncs) COLUMNS(argsList *list.List) formulaArg { // checkHVLookupArgs checking arguments, prepare extract mode, lookup value, // and data for the formula functions HLOOKUP and VLOOKUP. -func checkHVLookupArgs(name string, argsList *list.List) (idx, matchIdx int, wasExact, exactMatch bool, lookupValue, tableArray, errArg formulaArg) { +func checkHVLookupArgs(name string, argsList *list.List) (idx int, lookupValue, tableArray, matchMode, errArg formulaArg) { unit := map[string]string{ "HLOOKUP": "row", "VLOOKUP": "col", @@ -9214,7 +9216,7 @@ func checkHVLookupArgs(name string, argsList *list.List) (idx, matchIdx int, was errArg = newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires numeric %s argument", name, unit)) return } - idx, matchIdx = int(arg.Number)-1, -1 + idx, matchMode = int(arg.Number)-1, newNumberFormulaArg(matchModeMaxLess) if argsList.Len() == 4 { rangeLookup := argsList.Back().Value.(formulaArg).ToBool() if rangeLookup.Type == ArgError { @@ -9222,7 +9224,7 @@ func checkHVLookupArgs(name string, argsList *list.List) (idx, matchIdx int, was return } if rangeLookup.Number == 0 { - exactMatch = true + matchMode = newNumberFormulaArg(matchModeWildcard) } } return @@ -9235,35 +9237,16 @@ func checkHVLookupArgs(name string, argsList *list.List) (idx, matchIdx int, was // HLOOKUP(lookup_value,table_array,row_index_num,[range_lookup]) // func (fn *formulaFuncs) HLOOKUP(argsList *list.List) formulaArg { - rowIdx, matchIdx, wasExact, exactMatch, - lookupValue, tableArray, errArg := checkHVLookupArgs("HLOOKUP", argsList) + rowIdx, lookupValue, tableArray, matchMode, errArg := checkHVLookupArgs("HLOOKUP", argsList) if errArg.Type == ArgError { return errArg } - row := tableArray.Matrix[0] - if exactMatch || len(tableArray.Matrix) == TotalRows { - start: - for idx, mtx := range row { - lhs := mtx - switch lookupValue.Type { - case ArgNumber: - if !lookupValue.Boolean { - lhs = mtx.ToNumber() - if lhs.Type == ArgError { - lhs = mtx - } - } - case ArgMatrix: - lhs = tableArray - } - if compareFormulaArg(lhs, lookupValue, false, exactMatch) == criteriaEq { - matchIdx = idx - wasExact = true - break start - } - } + var matchIdx int + var wasExact bool + if matchMode.Number == matchModeWildcard || len(tableArray.Matrix) == TotalRows { + matchIdx, wasExact = lookupLinearSearch(false, lookupValue, tableArray, matchMode, newNumberFormulaArg(searchModeLinear)) } else { - matchIdx, wasExact = hlookupBinarySearch(row, lookupValue) + matchIdx, wasExact = lookupBinarySearch(false, lookupValue, tableArray, matchMode, newNumberFormulaArg(searchModeAscBinary)) } if matchIdx == -1 { return newErrorFormulaArg(formulaErrorNA, "HLOOKUP no result found") @@ -9271,8 +9254,8 @@ func (fn *formulaFuncs) HLOOKUP(argsList *list.List) formulaArg { if rowIdx < 0 || rowIdx >= len(tableArray.Matrix) { return newErrorFormulaArg(formulaErrorNA, "HLOOKUP has invalid row index") } - row = tableArray.Matrix[rowIdx] - if wasExact || !exactMatch { + row := tableArray.Matrix[rowIdx] + if wasExact || matchMode.Number == matchModeWildcard { return row[matchIdx] } return newErrorFormulaArg(formulaErrorNA, "HLOOKUP no result found") @@ -9391,6 +9374,43 @@ func (fn *formulaFuncs) TRANSPOSE(argsList *list.List) formulaArg { return newMatrixFormulaArg(mtx) } +// 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{} + if vertical { + for _, row := range lookupArray.Matrix { + tableArray = append(tableArray, row[0]) + } + } else { + tableArray = lookupArray.Matrix[0] + } + matchIdx, wasExact := -1, false +start: + for i, cell := range tableArray { + lhs := cell + if lookupValue.Type == ArgNumber { + if lhs = cell.ToNumber(); lhs.Type == ArgError { + lhs = cell + } + } else if lookupValue.Type == ArgMatrix { + lhs = lookupArray + } + if compareFormulaArg(lhs, lookupValue, matchMode, false) == criteriaEq { + matchIdx = i + wasExact = true + if searchMode.Number == searchModeLinear { + break start + } + } + if matchMode.Number == matchModeMinGreater || matchMode.Number == matchModeMaxLess { + matchIdx = int(calcMatch(int(matchMode.Number), formulaCriteriaParser(lookupValue.Value()), tableArray).Number) + continue + } + } + return matchIdx, wasExact +} + // VLOOKUP function 'looks up' a given value in the left-hand column of a // data array (or table), and returns the corresponding value from another // column of the array. The syntax of the function is: @@ -9398,34 +9418,16 @@ func (fn *formulaFuncs) TRANSPOSE(argsList *list.List) formulaArg { // VLOOKUP(lookup_value,table_array,col_index_num,[range_lookup]) // func (fn *formulaFuncs) VLOOKUP(argsList *list.List) formulaArg { - colIdx, matchIdx, wasExact, exactMatch, - lookupValue, tableArray, errArg := checkHVLookupArgs("VLOOKUP", argsList) + colIdx, lookupValue, tableArray, matchMode, errArg := checkHVLookupArgs("VLOOKUP", argsList) if errArg.Type == ArgError { return errArg } - if exactMatch || len(tableArray.Matrix) == TotalRows { - start: - for idx, mtx := range tableArray.Matrix { - lhs := mtx[0] - switch lookupValue.Type { - case ArgNumber: - if !lookupValue.Boolean { - lhs = mtx[0].ToNumber() - if lhs.Type == ArgError { - lhs = mtx[0] - } - } - case ArgMatrix: - lhs = tableArray - } - if compareFormulaArg(lhs, lookupValue, false, exactMatch) == criteriaEq { - matchIdx = idx - wasExact = true - break start - } - } + var matchIdx int + var wasExact bool + if matchMode.Number == matchModeWildcard || len(tableArray.Matrix) == TotalRows { + matchIdx, wasExact = lookupLinearSearch(true, lookupValue, tableArray, matchMode, newNumberFormulaArg(searchModeLinear)) } else { - matchIdx, wasExact = vlookupBinarySearch(tableArray, lookupValue) + matchIdx, wasExact = lookupBinarySearch(true, lookupValue, tableArray, matchMode, newNumberFormulaArg(searchModeAscBinary)) } if matchIdx == -1 { return newErrorFormulaArg(formulaErrorNA, "VLOOKUP no result found") @@ -9434,67 +9436,52 @@ func (fn *formulaFuncs) VLOOKUP(argsList *list.List) formulaArg { if colIdx < 0 || colIdx >= len(mtx) { return newErrorFormulaArg(formulaErrorNA, "VLOOKUP has invalid column index") } - if wasExact || !exactMatch { + if wasExact || matchMode.Number == matchModeWildcard { return mtx[colIdx] } return newErrorFormulaArg(formulaErrorNA, "VLOOKUP no result found") } -// vlookupBinarySearch finds the position of a target value when range lookup +// lookupBinarySearch finds the position of a target value when range lookup // is TRUE, if the data of table array can't guarantee be sorted, it will // return wrong result. -func vlookupBinarySearch(tableArray, lookupValue formulaArg) (matchIdx int, wasExact bool) { - var low, high, lastMatchIdx int = 0, len(tableArray.Matrix) - 1, -1 +func lookupBinarySearch(vertical bool, lookupValue, lookupArray, matchMode, searchMode formulaArg) (matchIdx int, wasExact bool) { + tableArray := []formulaArg{} + if vertical { + for _, row := range lookupArray.Matrix { + tableArray = append(tableArray, row[0]) + } + } else { + tableArray = lookupArray.Matrix[0] + } + var low, high, lastMatchIdx int = 0, len(tableArray) - 1, -1 + count := high for low <= high { mid := low + (high-low)/2 - mtx := tableArray.Matrix[mid] - lhs := mtx[0] - switch lookupValue.Type { - case ArgNumber: - if !lookupValue.Boolean { - lhs = mtx[0].ToNumber() - if lhs.Type == ArgError { - lhs = mtx[0] - } + cell := tableArray[mid] + lhs := cell + if lookupValue.Type == ArgNumber { + if lhs = cell.ToNumber(); lhs.Type == ArgError { + lhs = cell } - case ArgMatrix: - lhs = tableArray + } else if lookupValue.Type == ArgMatrix && vertical { + lhs = lookupArray } - result := compareFormulaArg(lhs, lookupValue, false, false) + result := compareFormulaArg(lhs, lookupValue, matchMode, false) if result == criteriaEq { matchIdx, wasExact = mid, true + if searchMode.Number == searchModeDescBinary { + matchIdx = count - matchIdx + } return } else if result == criteriaG { high = mid - 1 } else if result == criteriaL { - matchIdx, low = mid, mid+1 + matchIdx = mid if lhs.Value() != "" { lastMatchIdx = matchIdx } - } else { - return -1, false - } - } - matchIdx, wasExact = lastMatchIdx, true - return -} - -// vlookupBinarySearch finds the position of a target value when range lookup -// is TRUE, if the data of table array can't guarantee be sorted, it will -// return wrong result. -func hlookupBinarySearch(row []formulaArg, lookupValue formulaArg) (matchIdx int, wasExact bool) { - var low, high, lastMatchIdx int = 0, len(row) - 1, -1 - for low <= high { - mid := low + (high-low)/2 - mtx := row[mid] - result := compareFormulaArg(mtx, lookupValue, false, false) - if result == criteriaEq { - matchIdx, wasExact = mid, true - return - } else if result == criteriaG { - high = mid - 1 - } else if result == criteriaL { - low, lastMatchIdx = mid+1, mid + low = mid + 1 } else { return -1, false } @@ -9542,7 +9529,7 @@ func iterateLookupArgs(lookupValue, lookupVector formulaArg) ([]formulaArg, int, } } } - compare := compareFormulaArg(lhs, col, false, false) + compare := compareFormulaArg(lhs, col, newNumberFormulaArg(matchModeMaxLess), false) // Find exact match if compare == criteriaEq { matchIdx = idx @@ -9584,6 +9571,139 @@ func (fn *formulaFuncs) index(array formulaArg, rowIdx, colIdx int) formulaArg { return newListFormulaArg(cells) } +// validateMatchMode check the number of match mode if be equal to 0, 1, -1 or +// 2. +func validateMatchMode(mode float64) bool { + return mode == matchModeExact || mode == matchModeMinGreater || mode == matchModeMaxLess || mode == matchModeWildcard +} + +// validateSearchMode check the number of search mode if be equal to 1, -1, 2 +// or -2. +func validateSearchMode(mode float64) bool { + return mode == searchModeLinear || mode == searchModeReverseLinear || mode == searchModeAscBinary || mode == searchModeDescBinary +} + +// prepareXlookupArgs checking and prepare arguments for the formula function +// XLOOKUP. +func (fn *formulaFuncs) prepareXlookupArgs(argsList *list.List) formulaArg { + if argsList.Len() < 3 { + return newErrorFormulaArg(formulaErrorVALUE, "XLOOKUP requires at least 3 arguments") + } + if argsList.Len() > 6 { + return newErrorFormulaArg(formulaErrorVALUE, "XLOOKUP allows at most 6 arguments") + } + lookupValue := argsList.Front().Value.(formulaArg) + lookupArray := argsList.Front().Next().Value.(formulaArg) + returnArray := argsList.Front().Next().Next().Value.(formulaArg) + ifNotFond := newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + matchMode, searchMode := newNumberFormulaArg(matchModeExact), newNumberFormulaArg(searchModeLinear) + if argsList.Len() > 3 { + ifNotFond = argsList.Front().Next().Next().Next().Value.(formulaArg) + } + if argsList.Len() > 4 { + if matchMode = argsList.Front().Next().Next().Next().Next().Value.(formulaArg).ToNumber(); matchMode.Type != ArgNumber { + return matchMode + } + } + if argsList.Len() > 5 { + if searchMode = argsList.Back().Value.(formulaArg).ToNumber(); searchMode.Type != ArgNumber { + return searchMode + } + } + if lookupArray.Type != ArgMatrix || returnArray.Type != ArgMatrix { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + } + if !validateMatchMode(matchMode.Number) || !validateSearchMode(searchMode.Number) { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + return newListFormulaArg([]formulaArg{lookupValue, lookupArray, returnArray, ifNotFond, matchMode, searchMode}) +} + +// 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 { + result := [][]formulaArg{} + for rowIdx, row := range returnArray.Matrix { + for colIdx, cell := range row { + if condition1 { + if condition2 { + result = append(result, []formulaArg{cell}) + continue + } + if returnArrayRows > 1 && returnArrayCols > 1 { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + } + if condition3 { + if returnArrayCols != lookupCols { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + if colIdx == matchIdx { + result = append(result, []formulaArg{cell}) + continue + } + } + if condition4 { + if returnArrayRows != lookupRows { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + if rowIdx == matchIdx { + if len(result) == 0 { + result = append(result, []formulaArg{cell}) + continue + } + result[0] = append(result[0], cell) + } + } + } + } + array := newMatrixFormulaArg(result) + cells := array.ToList() + if len(cells) == 1 { + return cells[0] + } + return array +} + +// XLOOKUP function searches a range or an array, and then returns the item +// corresponding to the first match it finds. If no match exists, then +// 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]) +// +func (fn *formulaFuncs) XLOOKUP(argsList *list.List) formulaArg { + args := fn.prepareXlookupArgs(argsList) + if args.Type != ArgList { + return args + } + lookupValue, lookupArray, returnArray, ifNotFond, matchMode, searchMode := args.List[0], args.List[1], args.List[2], args.List[3], args.List[4], args.List[5] + lookupRows, lookupCols := len(lookupArray.Matrix), 0 + if lookupRows > 0 { + lookupCols = len(lookupArray.Matrix[0]) + } + if lookupRows != 1 && lookupCols != 1 { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + verticalLookup := lookupRows >= lookupCols + var matchIdx int + switch searchMode.Number { + case searchModeLinear, searchModeReverseLinear: + matchIdx, _ = lookupLinearSearch(verticalLookup, lookupValue, lookupArray, matchMode, searchMode) + default: + matchIdx, _ = lookupBinarySearch(verticalLookup, lookupValue, lookupArray, matchMode, searchMode) + } + if matchIdx == -1 { + return ifNotFond + } + returnArrayRows, returnArrayCols := len(returnArray.Matrix), len(returnArray.Matrix[0]) + condition1 := lookupRows == 1 && lookupCols == 1 + condition2 := returnArrayRows == 1 || returnArrayCols == 1 + condition3 := lookupRows == 1 && lookupCols > 1 + condition4 := lookupRows > 1 && lookupCols == 1 + return fn.xlookup(lookupRows, lookupCols, returnArrayRows, returnArrayCols, matchIdx, condition1, condition2, condition3, condition4, returnArray) +} + // 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: // diff --git a/calc_test.go b/calc_test.go index 97a7588ee9..be2f18533a 100644 --- a/calc_test.go +++ b/calc_test.go @@ -3497,17 +3497,17 @@ func TestCalcToList(t *testing.T) { } func TestCalcCompareFormulaArg(t *testing.T) { - assert.Equal(t, compareFormulaArg(newEmptyFormulaArg(), newEmptyFormulaArg(), false, false), criteriaEq) + assert.Equal(t, compareFormulaArg(newEmptyFormulaArg(), newEmptyFormulaArg(), newNumberFormulaArg(matchModeMaxLess), false), criteriaEq) lhs := newListFormulaArg([]formulaArg{newEmptyFormulaArg()}) rhs := newListFormulaArg([]formulaArg{newEmptyFormulaArg(), newEmptyFormulaArg()}) - assert.Equal(t, compareFormulaArg(lhs, rhs, false, false), criteriaL) - assert.Equal(t, compareFormulaArg(rhs, lhs, false, false), criteriaG) + assert.Equal(t, compareFormulaArg(lhs, rhs, newNumberFormulaArg(matchModeMaxLess), false), criteriaL) + assert.Equal(t, compareFormulaArg(rhs, lhs, newNumberFormulaArg(matchModeMaxLess), false), criteriaG) lhs = newListFormulaArg([]formulaArg{newBoolFormulaArg(true)}) rhs = newListFormulaArg([]formulaArg{newBoolFormulaArg(true)}) - assert.Equal(t, compareFormulaArg(lhs, rhs, false, false), criteriaEq) + assert.Equal(t, compareFormulaArg(lhs, rhs, newNumberFormulaArg(matchModeMaxLess), false), criteriaEq) - assert.Equal(t, compareFormulaArg(formulaArg{Type: ArgUnknown}, formulaArg{Type: ArgUnknown}, false, false), criteriaErr) + assert.Equal(t, compareFormulaArg(formulaArg{Type: ArgUnknown}, formulaArg{Type: ArgUnknown}, newNumberFormulaArg(matchModeMaxLess), false), criteriaErr) } func TestCalcMatchPattern(t *testing.T) { @@ -3779,6 +3779,113 @@ func TestCalcXIRR(t *testing.T) { } } +func TestCalcXLOOKUP(t *testing.T) { + cellData := [][]interface{}{ + {}, + {nil, nil, "Quarter", "Gross Profit", "Net profit", "Profit %"}, + {nil, nil, "Qtr1", nil, 19342, 29.30}, + {}, + {nil, "Income Statement", "Qtr1", "Qtr2", "Qtr3", "Qtr4", "Total"}, + {nil, "Total sales", 50000, 78200, 89500, 91250, 308.95}, + {nil, "Cost of sales", -25000, -42050, -59450, -60450, -186950}, + {nil, "Gross Profit", 25000, 36150, 30050, 30800, 122000}, + {}, + {nil, "Depreciation", -899, -791, -202, -412, -2304}, + {nil, "Interest", -513, -853, -150, -956, -2472}, + {nil, "Earnings before Tax", 23588, 34506, 29698, 29432, 117224}, + {}, + {nil, "Tax", -4246, -6211, -5346, -5298, 21100}, + {}, + {nil, "Net profit", 19342, 28295, 24352, 24134, 96124}, + {nil, "Profit %", 0.293, 0.278, 0.234, 0.276, 0.269}, + } + f := prepareCalcData(cellData) + formulaList := map[string]string{ + "=SUM(XLOOKUP($C3,$C5:$C5,$C6:$C17,NA(),0,2))": "87272.293", + "=SUM(XLOOKUP($C3,$C5:$C5,$C6:$G6,NA(),0,-2))": "309258.95", + "=SUM(XLOOKUP($C3,$C5:$C5,$C6:$C17,NA(),0,-2))": "87272.293", + "=SUM(XLOOKUP($C3,$C5:$G5,$C6:$G17,NA(),0,2))": "87272.293", + "=SUM(XLOOKUP(D2,$B6:$B17,$C6:$G17,NA(),0,2))": "244000", + "=XLOOKUP(D2,$B6:$B17,C6:C17)": "25000", + "=XLOOKUP(D2,$B6:$B17,XLOOKUP($C3,$C5:$G5,$C6:$G17))": "25000", + "=XLOOKUP(\"*p*\",B2:B9,C2:C9,NA(),2)": "25000", + } + for formula, expected := range formulaList { + assert.NoError(t, f.SetCellFormula("Sheet1", "D3", formula)) + result, err := f.CalcCellValue("Sheet1", "D3") + 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($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", + } + 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) + } + + cellData = [][]interface{}{ + {"Salesperson", "Item", "Amont"}, + {"B", "Apples", 30, 25, 15, 50, 45, 18}, + {"L", "Oranges", 25, "D3", "E3"}, + {"C", "Grapes", 15}, + {"L", "Lemons", 50}, + {"L", "Oranges", 45}, + {"C", "Peaches", 18}, + {"B", "Pears", 40}, + {"B", "Apples", 55}, + } + f = prepareCalcData(cellData) + formulaList = map[string]string{ + // Test match mode with partial match (wildcards) + "=XLOOKUP(\"*p*\",B2:B9,C2:C9,NA(),2)": "30", + // Test match mode with approximate match in vertical (next larger item) + "=XLOOKUP(32,B2:B9,C2:C9,NA(),1)": "30", + // Test match mode with approximate match in horizontal (next larger item) + "=XLOOKUP(30,C2:F2,C3:F3,NA(),1)": "25", + // Test match mode with approximate match in vertical (next smaller item) + "=XLOOKUP(40,C2:C9,B2:B9,NA(),-1)": "Pears", + // Test match mode with approximate match in horizontal (next smaller item) + "=XLOOKUP(29,C2:F2,C3:F3,NA(),-1)": "D3", + // Test search mode + "=XLOOKUP(\"L\",A2:A9,C2:C9,NA(),0,1)": "25", + "=XLOOKUP(\"L\",A2:A9,C2:C9,NA(),0,-1)": "45", + "=XLOOKUP(\"L\",A2:A9,C2:C9,NA(),0,2)": "50", + "=XLOOKUP(\"L\",A2:A9,C2:C9,NA(),0,-2)": "45", + // Test match mode and search mode + "=XLOOKUP(29,C2:H2,C3:H3,NA(),-1,-1)": "D3", + "=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, err, formula) + assert.Equal(t, expected, result, formula) + } + calcError = map[string]string{ + // Test match mode with exact match + "=XLOOKUP(\"*p*\",B2:B9,C2:C9,NA(),0)": "#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) + } +} + func TestCalcXNPV(t *testing.T) { cellData := [][]interface{}{{nil, 0.05}, {"01/01/2016", -10000, nil}, diff --git a/excelize_test.go b/excelize_test.go index a15e80125c..4c136b6bea 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -590,7 +590,16 @@ func TestSetCellStyleBorder(t *testing.T) { var style int // Test set border on overlapping area with vertical variants shading styles gradient fill. - style, err = f.NewStyle(`{"border":[{"type":"left","color":"0000FF","style":2},{"type":"top","color":"00FF00","style":12},{"type":"bottom","color":"FFFF00","style":5},{"type":"right","color":"FF0000","style":6},{"type":"diagonalDown","color":"A020F0","style":9},{"type":"diagonalUp","color":"A020F0","style":8}]}`) + style, err = f.NewStyle(&Style{ + Border: []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 !assert.NoError(t, err) { t.FailNow() } From 6b1e592cbc7b1412da5f6d0badeaf1083117c762 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 26 Dec 2021 14:55:53 +0800 Subject: [PATCH 516/957] This closes #1095, support to set and get document application properties --- README.md | 2 +- README_zh.md | 2 +- calc.go | 1 + docProps.go | 100 +++++++++++++++++++++++++++++++++++++++++++++++ docProps_test.go | 45 +++++++++++++++++++++ test/Book1.xlsx | Bin 20753 -> 20738 bytes xmlApp.go | 58 ++++++++++++++++----------- xmlDrawing.go | 56 +++++++++++++------------- 8 files changed, 212 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index 1245c62e97..a0d2d3ddd3 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Excelize is a library written in pure Go providing a set of functions that allow go get github.com/xuri/excelize ``` -- If your packages are managed using [Go Modules](https://blog.golang.org/using-go-modules), please install with following command. +- If your packages are managed using [Go Modules](https://go.dev/blog/using-go-modules), please install with following command. ```bash go get github.com/xuri/excelize/v2 diff --git a/README_zh.md b/README_zh.md index b946bb33d3..919e954be4 100644 --- a/README_zh.md +++ b/README_zh.md @@ -23,7 +23,7 @@ Excelize 是 Go 语言编写的用于操作 Office Excel 文档基础库,基 go get github.com/xuri/excelize ``` -- 如果您使用 [Go Modules](https://blog.golang.org/using-go-modules) 管理软件包,请使用下面的命令来安装最新版本。 +- 如果您使用 [Go Modules](https://go.dev/blog/using-go-modules) 管理软件包,请使用下面的命令来安装最新版本。 ```bash go get github.com/xuri/excelize/v2 diff --git a/calc.go b/calc.go index 682435ae9d..b096af9487 100644 --- a/calc.go +++ b/calc.go @@ -626,6 +626,7 @@ type formulaFuncs struct { // WEIBULL // WEIBULL.DIST // XIRR +// XLOOKUP // XNPV // XOR // YEAR diff --git a/docProps.go b/docProps.go index bf294f292e..c8ab27caf7 100644 --- a/docProps.go +++ b/docProps.go @@ -19,6 +19,106 @@ import ( "reflect" ) +// 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. +// +// For example: +// +// 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 + fields []string + output []byte + immutable, mutable reflect.Value + field string + ) + app = new(xlsxProperties) + if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML("docProps/app.xml")))). + Decode(app); err != nil && err != io.EOF { + err = fmt.Errorf("xml decode error: %s", err) + return + } + fields = []string{"Application", "ScaleCrop", "DocSecurity", "Company", "LinksUpToDate", "HyperlinksChanged", "AppVersion"} + immutable, mutable = reflect.ValueOf(*appProperties), reflect.ValueOf(app).Elem() + for _, field = range fields { + immutableField := immutable.FieldByName(field) + switch immutableField.Kind() { + case reflect.Bool: + mutable.FieldByName(field).SetBool(immutableField.Bool()) + case reflect.Int: + mutable.FieldByName(field).SetInt(immutableField.Int()) + default: + mutable.FieldByName(field).SetString(immutableField.String()) + } + } + app.Vt = NameSpaceDocumentPropertiesVariantTypes.Value + output, err = xml.Marshal(app) + f.saveFileList("docProps/app.xml", output) + return +} + +// GetAppProps provides a function to get document application properties. +func (f *File) GetAppProps() (ret *AppProperties, err error) { + var app = new(xlsxProperties) + if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML("docProps/app.xml")))). + Decode(app); err != nil && err != io.EOF { + err = fmt.Errorf("xml decode error: %s", err) + return + } + ret, err = &AppProperties{ + Application: app.Application, + ScaleCrop: app.ScaleCrop, + DocSecurity: app.DocSecurity, + Company: app.Company, + LinksUpToDate: app.LinksUpToDate, + HyperlinksChanged: app.HyperlinksChanged, + AppVersion: app.AppVersion, + }, nil + return +} + // SetDocProps provides a function to set document core properties. The // properties that can be set are: // diff --git a/docProps_test.go b/docProps_test.go index df1b6c6ed7..a5c35f702e 100644 --- a/docProps_test.go +++ b/docProps_test.go @@ -20,6 +20,51 @@ import ( var MacintoshCyrillicCharset = []byte{0x8F, 0xF0, 0xE8, 0xE2, 0xE5, 0xF2, 0x20, 0xEC, 0xE8, 0xF0} +func TestSetAppProps(t *testing.T) { + f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + if !assert.NoError(t, err) { + t.FailNow() + } + assert.NoError(t, f.SetAppProps(&AppProperties{ + Application: "Microsoft Excel", + ScaleCrop: true, + DocSecurity: 3, + Company: "Company Name", + LinksUpToDate: true, + HyperlinksChanged: true, + AppVersion: "16.0000", + })) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetAppProps.xlsx"))) + f.Pkg.Store("docProps/app.xml", nil) + assert.NoError(t, f.SetAppProps(&AppProperties{})) + assert.NoError(t, f.Close()) + + // Test unsupported charset + f = NewFile() + f.Pkg.Store("docProps/app.xml", MacintoshCyrillicCharset) + assert.EqualError(t, f.SetAppProps(&AppProperties{}), "xml decode error: XML syntax error on line 1: invalid UTF-8") +} + +func TestGetAppProps(t *testing.T) { + f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + if !assert.NoError(t, err) { + t.FailNow() + } + props, err := f.GetAppProps() + assert.NoError(t, err) + assert.Equal(t, props.Application, "Microsoft Macintosh Excel") + f.Pkg.Store("docProps/app.xml", nil) + _, err = f.GetAppProps() + assert.NoError(t, err) + assert.NoError(t, f.Close()) + + // Test unsupported charset + f = NewFile() + f.Pkg.Store("docProps/app.xml", MacintoshCyrillicCharset) + _, err = f.GetAppProps() + assert.EqualError(t, err, "xml decode error: XML syntax error on line 1: invalid UTF-8") +} + func TestSetDocProps(t *testing.T) { f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if !assert.NoError(t, err) { diff --git a/test/Book1.xlsx b/test/Book1.xlsx index 64c9e70977cf2fbbd8ba08a29cd005f16d3d79b3..6a497e33afb45a5764a323326bd1f2f57480184d 100644 GIT binary patch delta 918 zcmbQZh_PuABVT|wGm8iV2L}g(ZO*LVjeM+3EFi{aaVB|22+NB3Ae8l=r4Gt!WV3>@ z_OlB^Sd;g&t4{vS@gA&cvVws8WCI`8%|cvUY!G7<1Vq>&EDLchs1^%xh0SdeTu`2Z zfYj!7lF_U%6Qnk)%d4JEmz+Fb#}DY2M(wXH!OqS&Lf*^&SV~CgF9TAubB4d&0F{0kZYpk z!6~Kz513i@=&Zl+h*hNfWkALfZjGfg0v@QjeRs%wz?&h~QJOF*IC6cYPjr0n@gIHl zR~^zL&6fYOVV-_CV2c>*x9q~^i_acBuz$F1l}h{6r)$}S|L7Hr+q}5xK{MAY zip>zv^1HQj{^TvkN?2Z%d1RQ^<7Dy% zXXVZ9Zu22=Fj>pX781ePUZxPn5-%$V=+LVXXIq>iP&{82g(;r1Jcs zQXBlC9{A{Q$qULW@IaVs9sqTBeSj3xWar5b14Jip2oRJ9sp8wY{cRmH1H&(F1_lWR QWZ*veL4YdjF>8~Iq7SU`-;;!N_45SA75K`84#OC6Ne$Yup) z?PnK;uqN+kSDpNu<2_i@WCa2F&AeRvY!D+91Vq>&EDLchsB#N&h0SdeTu`2ZfYj!7 zlF_U%-BO#?39))+S%Rn6h$BWGJ0D*H*dTzJH$kWVe$k%;&fl7f*R8qPk$_Lnm>|UlIGh%nNwL ze07f3Vnz4p4;e=L_Dk<=UZN=XY3T;LPR(1-SMVIP6MhuQ*;SRJ^Ww#t-e8j>YqdL; z)~{K_I-zbBU#InXhsR-QM$avaitQXu7G*!QznQjIjAvQfRj!g91*|tUEK9#=+vzO} z3U@dmc&z39)?)j^g}o`_`RylURUJ0h1~8Z93`r^W%i#sc%q?ej@7@JYAQY1nHOeEZ&_ zTVAg?On8?3_?P|p><16iY$gle^^d;qKJt9~u{rYTe*)ApYoC4O5AbH^2-@7BXT`|C zAj-nPfSRy2FLZ`R(d32B%9|_P=0l=nvb2{iB+g^JOd*V^URDssWiM+8L&)0&!ie(r zY99^ z_8su`g{TqqbAm9!{ahi81%6P)k6;WHe{+abygyWGp+D3okNhopL9T>{#$=5Es0RuH xq?i^tPhRgVr5)hS$Rxsm$a;J`x4*4pW?=Zm&A=eRfDFPWUkuP@lkx(I0RU#zb=m*` diff --git a/xmlApp.go b/xmlApp.go index fdb600845d..322640666e 100644 --- a/xmlApp.go +++ b/xmlApp.go @@ -13,38 +13,50 @@ package excelize 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 +} + // xlsxProperties specifies to an OOXML document properties such as the // template used, the number of pages and words, and the application name and // version. type xlsxProperties struct { XMLName xml.Name `xml:"http://schemas.openxmlformats.org/officeDocument/2006/extended-properties Properties"` - Template string - Manager string - Company string - Pages int - Words int - Characters int - PresentationFormat string - Lines int - Paragraphs int - Slides int - Notes int - TotalTime int - HiddenSlides int - MMClips int - ScaleCrop bool + Vt string `xml:"xmlns:vt,attr"` + Template string `xml:",omitempty"` + Manager string `xml:",omitempty"` + Company string `xml:",omitempty"` + Pages int `xml:",omitempty"` + Words int `xml:",omitempty"` + Characters int `xml:",omitempty"` + PresentationFormat string `xml:",omitempty"` + Lines int `xml:",omitempty"` + Paragraphs int `xml:",omitempty"` + Slides int `xml:",omitempty"` + Notes int `xml:",omitempty"` + TotalTime int `xml:",omitempty"` + HiddenSlides int `xml:",omitempty"` + MMClips int `xml:",omitempty"` + ScaleCrop bool `xml:",omitempty"` HeadingPairs *xlsxVectorVariant TitlesOfParts *xlsxVectorLpstr - LinksUpToDate bool - CharactersWithSpaces int - SharedDoc bool - HyperlinkBase string + LinksUpToDate bool `xml:",omitempty"` + CharactersWithSpaces int `xml:",omitempty"` + SharedDoc bool `xml:",omitempty"` + HyperlinkBase string `xml:",omitempty"` HLinks *xlsxVectorVariant - HyperlinksChanged bool + HyperlinksChanged bool `xml:",omitempty"` DigSig *xlsxDigSig - Application string - AppVersion string - DocSecurity int + Application string `xml:",omitempty"` + AppVersion string `xml:",omitempty"` + DocSecurity int `xml:",omitempty"` } // xlsxVectorVariant specifies the set of hyperlinks that were in this diff --git a/xmlDrawing.go b/xmlDrawing.go index dabb34a791..1690554770 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -19,37 +19,39 @@ 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"} - 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"} - 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"} + 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"} + 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"} + 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"} ) // 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" + 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" From 89b85934f60ba0012f3de6da03eb12959e4b4b72 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 27 Dec 2021 23:34:14 +0800 Subject: [PATCH 517/957] This closes #1096, memory usage optimization and another 4 changes - Unzip shared string table to system temporary file when large inner XML, reduce memory usage about 70% - Remove unnecessary exported variable `XMLHeader`, we can using `encoding/xml` package's `xml.Header` instead of it - Using constant instead of inline text for default XML path - Rename exported option field `WorksheetUnzipMemLimit` to `UnzipXMLSizeLimit` - Unit test and documentation updated --- calcchain.go | 6 ++-- calcchain_test.go | 2 +- cell.go | 90 +++++++++++++++++++++++++++++++---------------- cell_test.go | 17 +++++++++ docProps.go | 20 +++++------ docProps_test.go | 16 ++++----- errors.go | 4 +-- excelize.go | 82 +++++++++++++++++++++--------------------- excelize_test.go | 8 ++--- file.go | 23 +++++++----- lib.go | 14 +++++--- rows.go | 51 ++++++++++++++++++++++++--- rows_test.go | 11 ++++-- sheet.go | 6 ++-- stream.go | 2 +- styles.go | 6 ++-- styles_test.go | 2 +- templates.go | 15 ++++++-- xmlDrawing.go | 29 ++++++++------- 19 files changed, 260 insertions(+), 144 deletions(-) diff --git a/calcchain.go b/calcchain.go index 671d144707..8f5e277279 100644 --- a/calcchain.go +++ b/calcchain.go @@ -25,7 +25,7 @@ func (f *File) calcChainReader() *xlsxCalcChain { if f.CalcChain == nil { f.CalcChain = new(xlsxCalcChain) - if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML("xl/calcChain.xml")))). + if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(dafaultXMLPathCalcChain)))). Decode(f.CalcChain); err != nil && err != io.EOF { log.Printf("xml decode error: %s", err) } @@ -39,7 +39,7 @@ func (f *File) calcChainReader() *xlsxCalcChain { func (f *File) calcChainWriter() { if f.CalcChain != nil && f.CalcChain.C != nil { output, _ := xml.Marshal(f.CalcChain) - f.saveFileList("xl/calcChain.xml", output) + f.saveFileList(dafaultXMLPathCalcChain, output) } } @@ -54,7 +54,7 @@ func (f *File) deleteCalcChain(index int, axis string) { } if len(calc.C) == 0 { f.CalcChain = nil - f.Pkg.Delete("xl/calcChain.xml") + f.Pkg.Delete(dafaultXMLPathCalcChain) content := f.contentTypesReader() content.Lock() defer content.Unlock() diff --git a/calcchain_test.go b/calcchain_test.go index 4956f60d4c..6144ed5677 100644 --- a/calcchain_test.go +++ b/calcchain_test.go @@ -5,7 +5,7 @@ import "testing" func TestCalcChainReader(t *testing.T) { f := NewFile() f.CalcChain = nil - f.Pkg.Store("xl/calcChain.xml", MacintoshCyrillicCharset) + f.Pkg.Store(dafaultXMLPathCalcChain, MacintoshCyrillicCharset) f.calcChainReader() } diff --git a/cell.go b/cell.go index 5c34bb9c3c..daff3d9bcd 100644 --- a/cell.go +++ b/cell.go @@ -14,6 +14,7 @@ package excelize import ( "encoding/xml" "fmt" + "os" "reflect" "strconv" "strings" @@ -348,28 +349,49 @@ func (f *File) SetCellStr(sheet, axis, value string) error { ws.Lock() defer ws.Unlock() cellData.S = f.prepareCellStyle(ws, col, cellData.S) - cellData.T, cellData.V = f.setCellString(value) + cellData.T, cellData.V, err = f.setCellString(value) return err } // setCellString provides a function to set string type to shared string // table. -func (f *File) setCellString(value string) (t string, v string) { +func (f *File) setCellString(value string) (t, v string, err error) { if len(value) > TotalCellChars { value = value[:TotalCellChars] } t = "s" - v = strconv.Itoa(f.setSharedString(value)) + var si int + if si, err = f.setSharedString(value); err != nil { + return + } + v = strconv.Itoa(si) + return +} + +// 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() + if path, ok := f.tempFiles.Load(dafaultXMLPathSharedStrings); ok { + f.Pkg.Store(dafaultXMLPathSharedStrings, f.readBytes(dafaultXMLPathSharedStrings)) + f.tempFiles.Delete(dafaultXMLPathSharedStrings) + err = os.Remove(path.(string)) + f.SharedStrings, f.sharedStringItemMap = nil, nil + } return } // setSharedString provides a function to add string to the share string table. -func (f *File) setSharedString(val string) int { +func (f *File) setSharedString(val string) (int, error) { + if err := f.sharedStringsLoader(); err != nil { + return 0, err + } sst := f.sharedStringsReader() f.Lock() defer f.Unlock() if i, ok := f.sharedStringsMap[val]; ok { - return i + return i, nil } sst.Count++ sst.UniqueCount++ @@ -377,7 +399,7 @@ func (f *File) setSharedString(val string) int { _, val, t.Space = setCellStr(val) sst.SI = append(sst.SI, xlsxSI{T: &t}) f.sharedStringsMap[val] = sst.UniqueCount - 1 - return sst.UniqueCount - 1 + return sst.UniqueCount - 1, nil } // setCellStr provides a function to set string type to cell. @@ -762,6 +784,34 @@ func (f *File) GetCellRichText(sheet, cell string) (runs []RichTextRun, err erro return } +// newRpr create run properties for the rich text by given font format. +func newRpr(fnt *Font) *xlsxRPr { + rpr := xlsxRPr{} + trueVal := "" + if fnt.Bold { + rpr.B = &trueVal + } + if fnt.Italic { + rpr.I = &trueVal + } + if fnt.Strike { + rpr.Strike = &trueVal + } + if fnt.Underline != "" { + rpr.U = &attrValString{Val: &fnt.Underline} + } + if fnt.Family != "" { + rpr.RFont = &attrValString{Val: &fnt.Family} + } + if fnt.Size > 0.0 { + rpr.Sz = &attrValFloat{Val: &fnt.Size} + } + if fnt.Color != "" { + rpr.Color = &xlsxColor{RGB: getPaletteColor(fnt.Color)} + } + return &rpr +} + // 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: @@ -875,6 +925,9 @@ func (f *File) SetCellRichText(sheet, cell string, runs []RichTextRun) error { if err != nil { return err } + if err := f.sharedStringsLoader(); err != nil { + return err + } cellData.S = f.prepareCellStyle(ws, col, cellData.S) si := xlsxSI{} sst := f.sharedStringsReader() @@ -889,30 +942,7 @@ func (f *File) SetCellRichText(sheet, cell string, runs []RichTextRun) error { _, run.T.Val, run.T.Space = setCellStr(textRun.Text) fnt := textRun.Font if fnt != nil { - rpr := xlsxRPr{} - trueVal := "" - if fnt.Bold { - rpr.B = &trueVal - } - if fnt.Italic { - rpr.I = &trueVal - } - if fnt.Strike { - rpr.Strike = &trueVal - } - if fnt.Underline != "" { - rpr.U = &attrValString{Val: &fnt.Underline} - } - if fnt.Family != "" { - rpr.RFont = &attrValString{Val: &fnt.Family} - } - if fnt.Size > 0.0 { - rpr.Sz = &attrValFloat{Val: &fnt.Size} - } - if fnt.Color != "" { - rpr.Color = &xlsxColor{RGB: getPaletteColor(fnt.Color)} - } - run.RPr = &rpr + run.RPr = newRpr(fnt) } textRuns = append(textRuns, run) } diff --git a/cell_test.go b/cell_test.go index 4a78a0676d..03de73b9e2 100644 --- a/cell_test.go +++ b/cell_test.go @@ -649,3 +649,20 @@ func TestFormattedValue2(t *testing.T) { v = f.formattedValue(1, "43528", false) assert.Equal(t, "43528", v) } + +func TestSharedStringsError(t *testing.T) { + f, err := OpenFile(filepath.Join("test", "Book1.xlsx"), Options{UnzipXMLSizeLimit: 128}) + assert.NoError(t, err) + f.tempFiles.Store(dafaultXMLPathSharedStrings, "") + assert.Equal(t, "1", f.getFromStringItemMap(1)) + + // Test reload the file error on set cell cell and rich text. The error message was different between macOS and Windows. + err = f.SetCellValue("Sheet1", "A19", "A19") + assert.Error(t, err) + + f.tempFiles.Store(dafaultXMLPathSharedStrings, "") + err = f.SetCellRichText("Sheet1", "A19", []RichTextRun{}) + assert.Error(t, err) + + assert.NoError(t, f.Close()) +} diff --git a/docProps.go b/docProps.go index c8ab27caf7..271b370267 100644 --- a/docProps.go +++ b/docProps.go @@ -27,8 +27,8 @@ import ( // 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 +// | 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 @@ -41,8 +41,8 @@ import ( // 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. +// | 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 @@ -75,7 +75,7 @@ func (f *File) SetAppProps(appProperties *AppProperties) (err error) { field string ) app = new(xlsxProperties) - if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML("docProps/app.xml")))). + if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(dafaultXMLPathDocPropsApp)))). Decode(app); err != nil && err != io.EOF { err = fmt.Errorf("xml decode error: %s", err) return @@ -95,14 +95,14 @@ func (f *File) SetAppProps(appProperties *AppProperties) (err error) { } app.Vt = NameSpaceDocumentPropertiesVariantTypes.Value output, err = xml.Marshal(app) - f.saveFileList("docProps/app.xml", output) + f.saveFileList(dafaultXMLPathDocPropsApp, output) return } // GetAppProps provides a function to get document application properties. func (f *File) GetAppProps() (ret *AppProperties, err error) { var app = new(xlsxProperties) - if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML("docProps/app.xml")))). + if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(dafaultXMLPathDocPropsApp)))). Decode(app); err != nil && err != io.EOF { err = fmt.Errorf("xml decode error: %s", err) return @@ -181,7 +181,7 @@ func (f *File) SetDocProps(docProperties *DocProperties) (err error) { ) core = new(decodeCoreProperties) - if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML("docProps/core.xml")))). + if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(dafaultXMLPathDocPropsCore)))). Decode(core); err != nil && err != io.EOF { err = fmt.Errorf("xml decode error: %s", err) return @@ -223,7 +223,7 @@ func (f *File) SetDocProps(docProperties *DocProperties) (err error) { newProps.Modified.Text = docProperties.Modified } output, err = xml.Marshal(newProps) - f.saveFileList("docProps/core.xml", output) + f.saveFileList(dafaultXMLPathDocPropsCore, output) return } @@ -232,7 +232,7 @@ func (f *File) SetDocProps(docProperties *DocProperties) (err error) { func (f *File) GetDocProps() (ret *DocProperties, err error) { var core = new(decodeCoreProperties) - if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML("docProps/core.xml")))). + if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(dafaultXMLPathDocPropsCore)))). Decode(core); err != nil && err != io.EOF { err = fmt.Errorf("xml decode error: %s", err) return diff --git a/docProps_test.go b/docProps_test.go index a5c35f702e..97948c14ab 100644 --- a/docProps_test.go +++ b/docProps_test.go @@ -35,13 +35,13 @@ func TestSetAppProps(t *testing.T) { AppVersion: "16.0000", })) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetAppProps.xlsx"))) - f.Pkg.Store("docProps/app.xml", nil) + f.Pkg.Store(dafaultXMLPathDocPropsApp, nil) assert.NoError(t, f.SetAppProps(&AppProperties{})) assert.NoError(t, f.Close()) // Test unsupported charset f = NewFile() - f.Pkg.Store("docProps/app.xml", MacintoshCyrillicCharset) + f.Pkg.Store(dafaultXMLPathDocPropsApp, MacintoshCyrillicCharset) assert.EqualError(t, f.SetAppProps(&AppProperties{}), "xml decode error: XML syntax error on line 1: invalid UTF-8") } @@ -53,14 +53,14 @@ func TestGetAppProps(t *testing.T) { props, err := f.GetAppProps() assert.NoError(t, err) assert.Equal(t, props.Application, "Microsoft Macintosh Excel") - f.Pkg.Store("docProps/app.xml", nil) + f.Pkg.Store(dafaultXMLPathDocPropsApp, nil) _, err = f.GetAppProps() assert.NoError(t, err) assert.NoError(t, f.Close()) // Test unsupported charset f = NewFile() - f.Pkg.Store("docProps/app.xml", MacintoshCyrillicCharset) + f.Pkg.Store(dafaultXMLPathDocPropsApp, MacintoshCyrillicCharset) _, err = f.GetAppProps() assert.EqualError(t, err, "xml decode error: XML syntax error on line 1: invalid UTF-8") } @@ -87,13 +87,13 @@ func TestSetDocProps(t *testing.T) { Version: "1.0.0", })) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetDocProps.xlsx"))) - f.Pkg.Store("docProps/core.xml", nil) + f.Pkg.Store(dafaultXMLPathDocPropsCore, nil) assert.NoError(t, f.SetDocProps(&DocProperties{})) assert.NoError(t, f.Close()) // Test unsupported charset f = NewFile() - f.Pkg.Store("docProps/core.xml", MacintoshCyrillicCharset) + f.Pkg.Store(dafaultXMLPathDocPropsCore, MacintoshCyrillicCharset) assert.EqualError(t, f.SetDocProps(&DocProperties{}), "xml decode error: XML syntax error on line 1: invalid UTF-8") } @@ -105,14 +105,14 @@ func TestGetDocProps(t *testing.T) { props, err := f.GetDocProps() assert.NoError(t, err) assert.Equal(t, props.Creator, "Microsoft Office User") - f.Pkg.Store("docProps/core.xml", nil) + f.Pkg.Store(dafaultXMLPathDocPropsCore, nil) _, err = f.GetDocProps() assert.NoError(t, err) assert.NoError(t, f.Close()) // Test unsupported charset f = NewFile() - f.Pkg.Store("docProps/core.xml", MacintoshCyrillicCharset) + f.Pkg.Store(dafaultXMLPathDocPropsCore, MacintoshCyrillicCharset) _, err = f.GetDocProps() assert.EqualError(t, err, "xml decode error: XML syntax error on line 1: invalid UTF-8") } diff --git a/errors.go b/errors.go index 4230c148ec..9460803cbc 100644 --- a/errors.go +++ b/errors.go @@ -143,8 +143,8 @@ var ( // characters length that exceeds the limit. ErrCellCharsLength = fmt.Errorf("cell value must be 0-%d characters", TotalCellChars) // ErrOptionsUnzipSizeLimit defined the error message for receiving - // invalid UnzipSizeLimit and WorksheetUnzipMemLimit. - ErrOptionsUnzipSizeLimit = errors.New("the value of UnzipSizeLimit should be greater than or equal to WorksheetUnzipMemLimit") + // invalid UnzipSizeLimit and UnzipXMLSizeLimit. + ErrOptionsUnzipSizeLimit = errors.New("the value of UnzipSizeLimit should be greater than or equal to UnzipXMLSizeLimit") // ErrSave defined the error message for saving file. ErrSave = errors.New("no path defined for file, consider File.WriteTo or File.Write") // ErrAttrValBool defined the error message on marshal and unmarshal diff --git a/excelize.go b/excelize.go index c5778c8542..25acd54df6 100644 --- a/excelize.go +++ b/excelize.go @@ -32,29 +32,30 @@ import ( // File define a populated spreadsheet file struct. type File struct { sync.Mutex - options *Options - xmlAttr map[string][]xml.Attr - checked map[string]bool - sheetMap map[string]string - streams map[string]*StreamWriter - tempFiles sync.Map - CalcChain *xlsxCalcChain - Comments map[string]*xlsxComments - ContentTypes *xlsxTypes - Drawings sync.Map - Path string - SharedStrings *xlsxSST - sharedStringsMap map[string]int - Sheet sync.Map - SheetCount int - Styles *xlsxStyleSheet - Theme *xlsxTheme - DecodeVMLDrawing map[string]*decodeVmlDrawing - VMLDrawing map[string]*vmlDrawing - WorkBook *xlsxWorkbook - Relationships sync.Map - Pkg sync.Map - CharsetReader charsetTranscoderFn + options *Options + xmlAttr map[string][]xml.Attr + checked map[string]bool + sheetMap map[string]string + streams map[string]*StreamWriter + tempFiles sync.Map + CalcChain *xlsxCalcChain + Comments map[string]*xlsxComments + ContentTypes *xlsxTypes + Drawings sync.Map + Path string + SharedStrings *xlsxSST + sharedStringsMap map[string]int + sharedStringItemMap *sync.Map + Sheet sync.Map + SheetCount int + Styles *xlsxStyleSheet + Theme *xlsxTheme + DecodeVMLDrawing map[string]*decodeVmlDrawing + VMLDrawing map[string]*vmlDrawing + WorkBook *xlsxWorkbook + Relationships sync.Map + Pkg sync.Map + CharsetReader charsetTranscoderFn } type charsetTranscoderFn func(charset string, input io.Reader) (rdr io.Reader, err error) @@ -68,17 +69,18 @@ type charsetTranscoderFn func(charset string, input io.Reader) (rdr io.Reader, e // // UnzipSizeLimit specifies the unzip size limit in bytes on open the // spreadsheet, this value should be greater than or equal to -// WorksheetUnzipMemLimit, the default size limit is 16GB. +// UnzipXMLSizeLimit, the default size limit is 16GB. // -// WorksheetUnzipMemLimit specifies the memory limit on unzipping worksheet in -// bytes, worksheet XML will be extracted to system 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. +// UnzipXMLSizeLimit specifies the memory limit on unzipping worksheet and +// shared string table in bytes, worksheet XML will be extracted to system +// 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. type Options struct { - Password string - RawCellValue bool - UnzipSizeLimit int64 - WorksheetUnzipMemLimit int64 + Password string + RawCellValue bool + UnzipSizeLimit int64 + UnzipXMLSizeLimit int64 } // OpenFile take the name of an spreadsheet file and returns a populated @@ -111,7 +113,7 @@ func OpenFile(filename string, opt ...Options) (*File, error) { // newFile is object builder func newFile() *File { return &File{ - options: &Options{UnzipSizeLimit: UnzipSizeLimit, WorksheetUnzipMemLimit: StreamChunkSize}, + options: &Options{UnzipSizeLimit: UnzipSizeLimit, UnzipXMLSizeLimit: StreamChunkSize}, xmlAttr: make(map[string][]xml.Attr), checked: make(map[string]bool), sheetMap: make(map[string]string), @@ -138,17 +140,17 @@ func OpenReader(r io.Reader, opt ...Options) (*File, error) { f.options = parseOptions(opt...) if f.options.UnzipSizeLimit == 0 { f.options.UnzipSizeLimit = UnzipSizeLimit - if f.options.WorksheetUnzipMemLimit > f.options.UnzipSizeLimit { - f.options.UnzipSizeLimit = f.options.WorksheetUnzipMemLimit + if f.options.UnzipXMLSizeLimit > f.options.UnzipSizeLimit { + f.options.UnzipSizeLimit = f.options.UnzipXMLSizeLimit } } - if f.options.WorksheetUnzipMemLimit == 0 { - f.options.WorksheetUnzipMemLimit = StreamChunkSize - if f.options.UnzipSizeLimit < f.options.WorksheetUnzipMemLimit { - f.options.WorksheetUnzipMemLimit = f.options.UnzipSizeLimit + if f.options.UnzipXMLSizeLimit == 0 { + f.options.UnzipXMLSizeLimit = StreamChunkSize + if f.options.UnzipSizeLimit < f.options.UnzipXMLSizeLimit { + f.options.UnzipXMLSizeLimit = f.options.UnzipSizeLimit } } - if f.options.WorksheetUnzipMemLimit > f.options.UnzipSizeLimit { + if f.options.UnzipXMLSizeLimit > f.options.UnzipSizeLimit { return nil, ErrOptionsUnzipSizeLimit } if bytes.Contains(b, oleIdentifier) { diff --git a/excelize_test.go b/excelize_test.go index 4c136b6bea..9aaaae9d30 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -201,7 +201,7 @@ func TestCharsetTranscoder(t *testing.T) { func TestOpenReader(t *testing.T) { _, err := OpenReader(strings.NewReader("")) assert.EqualError(t, err, "zip: not a valid zip file") - _, err = OpenReader(bytes.NewReader(oleIdentifier), Options{Password: "password", WorksheetUnzipMemLimit: UnzipSizeLimit + 1}) + _, err = OpenReader(bytes.NewReader(oleIdentifier), Options{Password: "password", UnzipXMLSizeLimit: UnzipSizeLimit + 1}) assert.EqualError(t, err, "decrypted file failed") // Test open spreadsheet with unzip size limit. @@ -225,7 +225,7 @@ func TestOpenReader(t *testing.T) { assert.NoError(t, f.Close()) // Test open spreadsheet with invalid optioins. - _, err = OpenReader(bytes.NewReader(oleIdentifier), Options{UnzipSizeLimit: 1, WorksheetUnzipMemLimit: 2}) + _, err = OpenReader(bytes.NewReader(oleIdentifier), Options{UnzipSizeLimit: 1, UnzipXMLSizeLimit: 2}) assert.EqualError(t, err, ErrOptionsUnzipSizeLimit.Error()) // Test unexpected EOF. @@ -1208,7 +1208,7 @@ func TestContentTypesReader(t *testing.T) { // Test unsupported charset. f := NewFile() f.ContentTypes = nil - f.Pkg.Store("[Content_Types].xml", MacintoshCyrillicCharset) + f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) f.contentTypesReader() } @@ -1216,7 +1216,7 @@ func TestWorkbookReader(t *testing.T) { // Test unsupported charset. f := NewFile() f.WorkBook = nil - f.Pkg.Store("xl/workbook.xml", MacintoshCyrillicCharset) + f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset) f.workbookReader() } diff --git a/file.go b/file.go index e2aeb4a043..6c8bd93f76 100644 --- a/file.go +++ b/file.go @@ -14,6 +14,7 @@ package excelize import ( "archive/zip" "bytes" + "encoding/xml" "io" "os" "path/filepath" @@ -27,15 +28,15 @@ import ( // func NewFile() *File { f := newFile() - f.Pkg.Store("_rels/.rels", []byte(XMLHeader+templateRels)) - f.Pkg.Store("docProps/app.xml", []byte(XMLHeader+templateDocpropsApp)) - f.Pkg.Store("docProps/core.xml", []byte(XMLHeader+templateDocpropsCore)) - f.Pkg.Store("xl/_rels/workbook.xml.rels", []byte(XMLHeader+templateWorkbookRels)) - f.Pkg.Store("xl/theme/theme1.xml", []byte(XMLHeader+templateTheme)) - f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(XMLHeader+templateSheet)) - f.Pkg.Store("xl/styles.xml", []byte(XMLHeader+templateStyles)) - f.Pkg.Store("xl/workbook.xml", []byte(XMLHeader+templateWorkbook)) - f.Pkg.Store("[Content_Types].xml", []byte(XMLHeader+templateContentTypes)) + f.Pkg.Store("_rels/.rels", []byte(xml.Header+templateRels)) + f.Pkg.Store(dafaultXMLPathDocPropsApp, []byte(xml.Header+templateDocpropsApp)) + f.Pkg.Store(dafaultXMLPathDocPropsCore, []byte(xml.Header+templateDocpropsCore)) + f.Pkg.Store("xl/_rels/workbook.xml.rels", []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)) + f.Pkg.Store(defaultXMLPathWorkbook, []byte(xml.Header+templateWorkbook)) + f.Pkg.Store(defaultXMLPathContentTypes, []byte(xml.Header+templateContentTypes)) f.SheetCount = 1 f.CalcChain = f.calcChainReader() f.Comments = make(map[string]*xlsxComments) @@ -159,6 +160,7 @@ func (f *File) writeToZip(zw *zip.Writer) error { f.workBookWriter() f.workSheetWriter() f.relsWriter() + f.sharedStringsLoader() f.sharedStringsWriter() f.styleSheetWriter() @@ -196,6 +198,9 @@ func (f *File) writeToZip(zw *zip.Writer) error { return true }) f.tempFiles.Range(func(path, content interface{}) bool { + if _, ok := f.Pkg.Load(path); ok { + return true + } var fi io.Writer fi, err = zw.Create(path.(string)) if err != nil { diff --git a/lib.go b/lib.go index 0efc180cc4..8ec121b403 100644 --- a/lib.go +++ b/lib.go @@ -30,8 +30,8 @@ func (f *File) ReadZipReader(r *zip.Reader) (map[string][]byte, int, error) { var ( err error docPart = map[string]string{ - "[content_types].xml": "[Content_Types].xml", - "xl/sharedstrings.xml": "xl/sharedStrings.xml", + "[content_types].xml": defaultXMLPathContentTypes, + "xl/sharedstrings.xml": dafaultXMLPathSharedStrings, } fileList = make(map[string][]byte, len(r.File)) worksheets int @@ -47,9 +47,15 @@ func (f *File) ReadZipReader(r *zip.Reader) (map[string][]byte, int, error) { if partName, ok := docPart[strings.ToLower(fileName)]; ok { fileName = partName } + if strings.EqualFold(fileName, dafaultXMLPathSharedStrings) && fileSize > f.options.UnzipXMLSizeLimit { + if tempFile, err := f.unzipToTemp(v); err == nil { + f.tempFiles.Store(fileName, tempFile) + continue + } + } if strings.HasPrefix(fileName, "xl/worksheets/sheet") { worksheets++ - if fileSize > f.options.WorksheetUnzipMemLimit && !v.FileInfo().IsDir() { + if fileSize > f.options.UnzipXMLSizeLimit && !v.FileInfo().IsDir() { if tempFile, err := f.unzipToTemp(v); err == nil { f.tempFiles.Store(fileName, tempFile) continue @@ -120,7 +126,7 @@ func (f *File) readTemp(name string) (file *os.File, err error) { // saveFileList provides a function to update given file content in file list // of spreadsheet. func (f *File) saveFileList(name string, content []byte) { - f.Pkg.Store(name, append([]byte(XMLHeader), content...)) + f.Pkg.Store(name, append([]byte(xml.Header), content...)) } // Read file content as string in a archive file. diff --git a/rows.go b/rows.go index 7b2f52ffaa..5071bb6b1a 100644 --- a/rows.go +++ b/rows.go @@ -21,6 +21,7 @@ import ( "math/big" "os" "strconv" + "sync" "github.com/mohae/deepcopy" ) @@ -244,7 +245,7 @@ func (f *File) Rows(sheet string) (*Rows, error) { decoder *xml.Decoder tempFile *os.File ) - if needClose, decoder, tempFile, err = f.sheetDecoder(name); needClose && err == nil { + if needClose, decoder, tempFile, err = f.xmlDecoder(name); needClose && err == nil { defer tempFile.Close() } for { @@ -271,7 +272,7 @@ func (f *File) Rows(sheet string) (*Rows, error) { if xmlElement.Name.Local == "sheetData" { rows.f = f rows.sheet = name - _, rows.decoder, rows.tempFile, err = f.sheetDecoder(name) + _, rows.decoder, rows.tempFile, err = f.xmlDecoder(name) return &rows, err } } @@ -279,9 +280,46 @@ func (f *File) Rows(sheet string) (*Rows, error) { return &rows, nil } -// sheetDecoder creates XML decoder by given path in the zip from memory data +// getFromStringItemMap build shared string item map from system temporary +// file at one time, and return value by given to string index. +func (f *File) getFromStringItemMap(index int) string { + if f.sharedStringItemMap != nil { + if value, ok := f.sharedStringItemMap.Load(index); ok { + return value.(string) + } + return strconv.Itoa(index) + } + f.sharedStringItemMap = &sync.Map{} + needClose, decoder, tempFile, err := f.xmlDecoder(dafaultXMLPathSharedStrings) + if needClose && err == nil { + defer tempFile.Close() + } + var ( + inElement string + i int + ) + for { + token, _ := decoder.Token() + if token == nil { + break + } + switch xmlElement := token.(type) { + case xml.StartElement: + inElement = xmlElement.Name.Local + if inElement == "si" { + si := xlsxSI{} + _ = decoder.DecodeElement(&si, &xmlElement) + f.sharedStringItemMap.Store(i, si.String()) + i++ + } + } + } + return f.getFromStringItemMap(index) +} + +// xmlDecoder creates XML decoder by given path in the zip from memory data // or system temporary file. -func (f *File) sheetDecoder(name string) (bool, *xml.Decoder, *os.File, error) { +func (f *File) xmlDecoder(name string) (bool, *xml.Decoder, *os.File, error) { var ( content []byte err error @@ -373,7 +411,7 @@ func (f *File) sharedStringsReader() *xlsxSST { relPath := f.getWorkbookRelsPath() if f.SharedStrings == nil { var sharedStrings xlsxSST - ss := f.readXML("xl/sharedStrings.xml") + ss := f.readXML(dafaultXMLPathSharedStrings) if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(ss))). Decode(&sharedStrings); err != nil && err != io.EOF { log.Printf("xml decode error: %s", err) @@ -415,6 +453,9 @@ func (c *xlsxC) getValueFrom(f *File, d *xlsxSST, raw bool) (string, error) { if c.V != "" { xlsxSI := 0 xlsxSI, _ = strconv.Atoi(c.V) + if _, ok := f.tempFiles.Load(dafaultXMLPathSharedStrings); ok { + return f.formattedValue(c.S, f.getFromStringItemMap(xlsxSI), raw), nil + } if len(d.SI) > xlsxSI { return f.formattedValue(c.S, d.SI[xlsxSI].String(), raw), nil } diff --git a/rows_test.go b/rows_test.go index 0c154a46ab..63321ce4e9 100644 --- a/rows_test.go +++ b/rows_test.go @@ -56,11 +56,18 @@ func TestRows(t *testing.T) { assert.NoError(t, err) // Test reload the file to memory from system temporary directory. - f, err = OpenFile(filepath.Join("test", "Book1.xlsx"), Options{WorksheetUnzipMemLimit: 1024}) + 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 + err = f.SetCellValue("Sheet1", "A19", "A19") + assert.NoError(t, err) + value, err = f.GetCellValue("Sheet1", "A19") + assert.NoError(t, err) + assert.Equal(t, "A19", value) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetRow.xlsx"))) assert.NoError(t, f.Close()) } @@ -200,7 +207,7 @@ func TestColumns(t *testing.T) { func TestSharedStringsReader(t *testing.T) { f := NewFile() - f.Pkg.Store("xl/sharedStrings.xml", MacintoshCyrillicCharset) + f.Pkg.Store(dafaultXMLPathSharedStrings, MacintoshCyrillicCharset) f.sharedStringsReader() si := xlsxSI{} assert.EqualValues(t, "", si.String()) diff --git a/sheet.go b/sheet.go index 99ae1a2206..17f6693940 100644 --- a/sheet.go +++ b/sheet.go @@ -76,7 +76,7 @@ func (f *File) contentTypesReader() *xlsxTypes { f.ContentTypes = new(xlsxTypes) f.ContentTypes.Lock() defer f.ContentTypes.Unlock() - if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML("[Content_Types].xml")))). + 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) } @@ -89,7 +89,7 @@ func (f *File) contentTypesReader() *xlsxTypes { func (f *File) contentTypesWriter() { if f.ContentTypes != nil { output, _ := xml.Marshal(f.ContentTypes) - f.saveFileList("[Content_Types].xml", output) + f.saveFileList(defaultXMLPathContentTypes, output) } } @@ -304,7 +304,7 @@ func (f *File) relsWriter() { // setAppXML update docProps/app.xml file of XML. func (f *File) setAppXML() { - f.saveFileList("docProps/app.xml", []byte(templateDocpropsApp)) + f.saveFileList(dafaultXMLPathDocPropsApp, []byte(templateDocpropsApp)) } // replaceRelationshipsBytes; Some tools that read spreadsheet files have very diff --git a/stream.go b/stream.go index 65d6b72179..4bd721e996 100644 --- a/stream.go +++ b/stream.go @@ -112,7 +112,7 @@ func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) { } f.streams[sheetPath] = sw - _, _ = sw.rawData.WriteString(XMLHeader + `\n" +import "encoding/xml" var ( // XMLHeaderByte define an XML declaration can also contain a standalone // declaration. - XMLHeaderByte = []byte(XMLHeader) + XMLHeaderByte = []byte(xml.Header) +) + +const ( + defaultXMLPathContentTypes = "[Content_Types].xml" + dafaultXMLPathDocPropsApp = "docProps/app.xml" + dafaultXMLPathDocPropsCore = "docProps/core.xml" + dafaultXMLPathCalcChain = "xl/calcChain.xml" + dafaultXMLPathSharedStrings = "xl/sharedStrings.xml" + defaultXMLPathStyles = "xl/styles.xml" + defaultXMLPathWorkbook = "xl/workbook.xml" ) const templateDocpropsApp = `0Go Excelize` diff --git a/xmlDrawing.go b/xmlDrawing.go index 1690554770..4ae6a29558 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -37,21 +37,20 @@ var ( // 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" - + 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" From c5990ea3484932fd6066c04e36c63735889a8228 Mon Sep 17 00:00:00 2001 From: vst Date: Thu, 30 Dec 2021 00:36:04 +0800 Subject: [PATCH 518/957] Preserve horizontal tab character when set the cell value (#1108) --- cell.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cell.go b/cell.go index daff3d9bcd..983b26071c 100644 --- a/cell.go +++ b/cell.go @@ -409,7 +409,7 @@ func setCellStr(value string) (t string, v string, ns xml.Attr) { } if len(value) > 0 { prefix, suffix := value[0], value[len(value)-1] - for _, ascii := range []byte{10, 13, 32} { + for _, ascii := range []byte{9, 10, 13, 32} { if prefix == ascii || suffix == ascii { ns = xml.Attr{ Name: xml.Name{Space: NameSpaceXML, Local: "space"}, From e37e060d6f97274c1e967cea40609623493bce25 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 31 Dec 2021 00:00:01 +0800 Subject: [PATCH 519/957] This closes #1107, stream writer will create a time number format for time type cells Unit test coverage improved --- cell.go | 6 ++---- stream.go | 11 ++++++++--- stream_test.go | 43 +++++++++++++++++++++++-------------------- 3 files changed, 33 insertions(+), 27 deletions(-) diff --git a/cell.go b/cell.go index 983b26071c..146d0a2f79 100644 --- a/cell.go +++ b/cell.go @@ -228,8 +228,7 @@ func setCellTime(value time.Time) (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) - excelTime, err = timeToExcelTime(value) - if err != nil { + if excelTime, err = timeToExcelTime(value); err != nil { return } isNum = excelTime > 0 @@ -419,8 +418,7 @@ func setCellStr(value string) (t string, v string, ns xml.Attr) { } } } - t = "str" - v = bstrMarshal(value) + t, v = "str", bstrMarshal(value) return } diff --git a/stream.go b/stream.go index 4bd721e996..8df308d407 100644 --- a/stream.go +++ b/stream.go @@ -341,7 +341,7 @@ func (sw *StreamWriter) SetRow(axis string, values []interface{}, opts ...RowOpt val = v.Value setCellFormula(&c, v.Formula) } - if err = setCellValFunc(&c, val); err != nil { + if err = sw.setCellValFunc(&c, val); err != nil { _, _ = sw.rawData.WriteString(``) return err } @@ -424,7 +424,7 @@ func setCellFormula(c *xlsxC, formula string) { } // setCellValFunc provides a function to set value of a cell. -func setCellValFunc(c *xlsxC, val interface{}) (err error) { +func (sw *StreamWriter) setCellValFunc(c *xlsxC, val interface{}) (err error) { switch val := val.(type) { case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: err = setCellIntFunc(c, val) @@ -439,7 +439,12 @@ func setCellValFunc(c *xlsxC, val interface{}) (err error) { case time.Duration: c.T, c.V = setCellDuration(val) case time.Time: - c.T, c.V, _, err = setCellTime(val) + var isNum bool + c.T, c.V, isNum, err = setCellTime(val) + if isNum && c.S == 0 { + style, _ := sw.File.NewStyle(&Style{NumFmt: 22}) + c.S = style + } case bool: c.T, c.V = setCellBool(val) case nil: diff --git a/stream_test.go b/stream_test.go index 833a00ac9f..dd1be8a39b 100644 --- a/stream_test.go +++ b/stream_test.go @@ -58,7 +58,7 @@ func TestStreamWriter(t *testing.T) { 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{Hidden: true})) + 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()) for rowID := 10; rowID <= 51200; rowID++ { @@ -208,24 +208,27 @@ func TestSetRow(t *testing.T) { } func TestSetCellValFunc(t *testing.T) { + f := NewFile() + sw, err := f.NewStreamWriter("Sheet1") + assert.NoError(t, err) c := &xlsxC{} - assert.NoError(t, setCellValFunc(c, 128)) - assert.NoError(t, setCellValFunc(c, int8(-128))) - assert.NoError(t, setCellValFunc(c, int16(-32768))) - assert.NoError(t, setCellValFunc(c, int32(-2147483648))) - assert.NoError(t, setCellValFunc(c, int64(-9223372036854775808))) - assert.NoError(t, setCellValFunc(c, uint(128))) - assert.NoError(t, setCellValFunc(c, uint8(255))) - assert.NoError(t, setCellValFunc(c, uint16(65535))) - assert.NoError(t, setCellValFunc(c, uint32(4294967295))) - assert.NoError(t, setCellValFunc(c, uint64(18446744073709551615))) - assert.NoError(t, setCellValFunc(c, float32(100.1588))) - assert.NoError(t, setCellValFunc(c, float64(100.1588))) - assert.NoError(t, setCellValFunc(c, " Hello")) - assert.NoError(t, setCellValFunc(c, []byte(" Hello"))) - assert.NoError(t, setCellValFunc(c, time.Now().UTC())) - assert.NoError(t, setCellValFunc(c, time.Duration(1e13))) - assert.NoError(t, setCellValFunc(c, true)) - assert.NoError(t, setCellValFunc(c, nil)) - assert.NoError(t, setCellValFunc(c, complex64(5+10i))) + 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, float64(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))) } From 9e64df6a96685afcfbc7295beda38739868a6871 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 5 Jan 2022 00:13:29 +0800 Subject: [PATCH 520/957] Update create style example, using a pointer of the structure instead of JSON --- calc_test.go | 1 - cell.go | 12 ++++++++++-- stream.go | 2 +- stream_test.go | 2 +- styles.go | 52 +++++++++++++++++++++++++++++++++++++++++++------- 5 files changed, 57 insertions(+), 12 deletions(-) diff --git a/calc_test.go b/calc_test.go index be2f18533a..a9899d1650 100644 --- a/calc_test.go +++ b/calc_test.go @@ -3285,7 +3285,6 @@ func TestCalcCellValue(t *testing.T) { "=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(\"01/01/2010\",\"06/30/2015\",10%,101,100,4)": "PRICE requires rate >= 0", // YIELDDISC "=YIELDDISC()": "YIELDDISC requires 4 or 5 arguments", "=YIELDDISC(\"\",\"06/30/2017\",97,100,0)": "#VALUE!", diff --git a/cell.go b/cell.go index 146d0a2f79..dec743bf93 100644 --- a/cell.go +++ b/cell.go @@ -666,9 +666,17 @@ type HyperlinkOpts struct { // in this workbook. Maximum limit hyperlinks in a worksheet is 65530. The // below is example for external link. // -// err := f.SetCellHyperLink("Sheet1", "A3", "https://github.com/xuri/excelize", "External") +// if err := f.SetCellHyperLink("Sheet1", "A3", +// "https://github.com/xuri/excelize", "External"); err != nil { +// fmt.Println(err) +// } // // Set underline and font color style for the cell. -// style, err := f.NewStyle(`{"font":{"color":"#1265BE","underline":"single"}}`) +// 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) // // A this is another example for "Location": diff --git a/stream.go b/stream.go index 8df308d407..2f7bf44382 100644 --- a/stream.go +++ b/stream.go @@ -52,7 +52,7 @@ type StreamWriter struct { // if err != nil { // fmt.Println(err) // } -// styleID, err := file.NewStyle(`{"font":{"color":"#777777"}}`) +// styleID, err := file.NewStyle(&excelize.Style{Font: &excelize.Font{Color: "#777777"}}) // if err != nil { // fmt.Println(err) // } diff --git a/stream_test.go b/stream_test.go index dd1be8a39b..7a933809e1 100644 --- a/stream_test.go +++ b/stream_test.go @@ -53,7 +53,7 @@ func TestStreamWriter(t *testing.T) { assert.NoError(t, streamWriter.SetRow("A3", row)) // Test set cell with style. - styleID, err := file.NewStyle(`{"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}, 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)"}})) diff --git a/styles.go b/styles.go index da532fd1cc..e9aabbfdb7 100644 --- a/styles.go +++ b/styles.go @@ -2628,7 +2628,16 @@ func (f *File) GetCellStyle(sheet, axis string) (int, error) { // // For example create a borders of cell H9 on Sheet1: // -// style, err := f.NewStyle(`{"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}]}`) +// 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) // } @@ -2637,7 +2646,9 @@ func (f *File) GetCellStyle(sheet, axis string) (int, error) { // Set gradient fill with vertical variants shading styles for cell H9 on // Sheet1: // -// style, err := f.NewStyle(`{"fill":{"type":"gradient","color":["#FFFFFF","#E0EBF5"],"shading":1}}`) +// style, err := f.NewStyle(&excelize.Style{ +// Fill: excelize.Fill{Type: "gradient", Color: []string{"#FFFFFF", "#E0EBF5"}, Shading: 1}, +// }) // if err != nil { // fmt.Println(err) // } @@ -2645,7 +2656,9 @@ func (f *File) GetCellStyle(sheet, axis string) (int, error) { // // Set solid style pattern fill for cell H9 on Sheet1: // -// style, err := f.NewStyle(`{"fill":{"type":"pattern","color":["#E0EBF5"],"pattern":1}}`) +// style, err := f.NewStyle(&excelize.Style{ +// Fill: excelize.Fill{Type: "pattern", Color: []string{"#E0EBF5"}, Pattern: 1}, +// }) // if err != nil { // fmt.Println(err) // } @@ -2653,7 +2666,19 @@ func (f *File) GetCellStyle(sheet, axis string) (int, error) { // // Set alignment style for cell H9 on Sheet1: // -// 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":"","wrap_text":true}}`) +// 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) // } @@ -2664,7 +2689,7 @@ func (f *File) GetCellStyle(sheet, axis string) (int, error) { // for cell H9 on Sheet1: // // f.SetCellValue("Sheet1", "H9", 42920.5) -// style, err := f.NewStyle(`{"number_format": 22}`) +// style, err := f.NewStyle(&excelize.Style{NumFmt: 22}) // if err != nil { // fmt.Println(err) // } @@ -2672,7 +2697,15 @@ func (f *File) GetCellStyle(sheet, axis string) (int, error) { // // Set font style for cell H9 on Sheet1: // -// style, err := f.NewStyle(`{"font":{"bold":true,"italic":true,"family":"Times New Roman","size":36,"color":"#777777"}}`) +// 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) // } @@ -2680,7 +2713,12 @@ func (f *File) GetCellStyle(sheet, axis string) (int, error) { // // Hide and lock for cell H9 on Sheet1: // -// style, err := f.NewStyle(`{"protection":{"hidden":true, "locked":true}}`) +// style, err := f.NewStyle(&excelize.Style{ +// Protection: &excelize.Protection{ +// Hidden: true, +// Locked: true, +// }, +// }) // if err != nil { // fmt.Println(err) // } From af5c4d00e81b62a3f6ff6cb34a89502400552a2d Mon Sep 17 00:00:00 2001 From: "Jonham.Chen" Date: Sat, 8 Jan 2022 10:32:13 +0800 Subject: [PATCH 521/957] feat: implement SHA-512 algorithm to ProtectSheet (#1115) --- chart.go | 4 +-- crypt.go | 64 ++++++++++++++++++++++++++++++++++++------ crypt_test.go | 20 +++++++++++-- datavalidation.go | 26 ++++++++--------- datavalidation_test.go | 4 +-- errors.go | 50 +++++++++++++++++++++++---------- excelize_test.go | 57 ++++++++++++++++++++++++++++++++++--- pivotTable_test.go | 2 +- sheet.go | 49 ++++++++++++++++++++++++++++---- xmlWorksheet.go | 1 + 10 files changed, 223 insertions(+), 54 deletions(-) diff --git a/chart.go b/chart.go index 755c160ef8..b43f9f28b5 100644 --- a/chart.go +++ b/chart.go @@ -980,12 +980,12 @@ func (f *File) getFormatChart(format string, combo []string) (*formatChart, []*f return formatSet, comboCharts, err } if _, ok := chartValAxNumFmtFormatCode[comboChart.Type]; !ok { - return formatSet, comboCharts, newUnsupportChartType(comboChart.Type) + return formatSet, comboCharts, newUnsupportedChartType(comboChart.Type) } comboCharts = append(comboCharts, comboChart) } if _, ok := chartValAxNumFmtFormatCode[formatSet.Type]; !ok { - return formatSet, comboCharts, newUnsupportChartType(formatSet.Type) + return formatSet, comboCharts, newUnsupportedChartType(formatSet.Type) } return formatSet, comboCharts, err } diff --git a/crypt.go b/crypt.go index ae39bba16b..65b9956bdd 100644 --- a/crypt.go +++ b/crypt.go @@ -43,6 +43,7 @@ var ( packageOffset = 8 // First 8 bytes are the size of the stream packageEncryptionChunkSize = 4096 iterCount = 50000 + sheetProtectionSpinCount = 1e5 oleIdentifier = []byte{ 0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1, } @@ -146,7 +147,7 @@ func Decrypt(raw []byte, opt *Options) (packageBuf []byte, err error) { case "standard": return standardDecrypt(encryptionInfoBuf, encryptedPackageBuf, opt) default: - err = ErrUnsupportEncryptMechanism + err = ErrUnsupportedEncryptMechanism } return } @@ -307,7 +308,7 @@ func encryptionMechanism(buffer []byte) (mechanism string, err error) { } else if (versionMajor == 3 || versionMajor == 4) && versionMinor == 3 { mechanism = "extensible" } - err = ErrUnsupportEncryptMechanism + err = ErrUnsupportedEncryptMechanism return } @@ -387,14 +388,14 @@ func standardConvertPasswdToKey(header StandardEncryptionHeader, verifier Standa key = hashing("sha1", iterator, key) } var block int - hfinal := hashing("sha1", key, createUInt32LEBuffer(block, 4)) + hFinal := hashing("sha1", key, createUInt32LEBuffer(block, 4)) cbRequiredKeyLength := int(header.KeySize) / 8 cbHash := sha1.Size buf1 := bytes.Repeat([]byte{0x36}, 64) - buf1 = append(standardXORBytes(hfinal, buf1[:cbHash]), buf1[cbHash:]...) + buf1 = append(standardXORBytes(hFinal, buf1[:cbHash]), buf1[cbHash:]...) x1 := hashing("sha1", buf1) buf2 := bytes.Repeat([]byte{0x5c}, 64) - buf2 = append(standardXORBytes(hfinal, buf2[:cbHash]), buf2[cbHash:]...) + buf2 = append(standardXORBytes(hFinal, buf2[:cbHash]), buf2[cbHash:]...) x2 := hashing("sha1", buf2) x3 := append(x1, x2...) keyDerived := x3[:cbRequiredKeyLength] @@ -417,7 +418,8 @@ func standardXORBytes(a, b []byte) []byte { // ECMA-376 Agile Encryption // agileDecrypt decrypt the CFB file format with ECMA-376 agile encryption. -// Support cryptographic algorithm: MD4, MD5, RIPEMD-160, SHA1, SHA256, SHA384 and SHA512. +// Support cryptographic algorithm: MD4, MD5, RIPEMD-160, SHA1, SHA256, +// SHA384 and SHA512. func agileDecrypt(encryptionInfoBuf, encryptedPackageBuf []byte, opt *Options) (packageBuf []byte, err error) { var encryptionInfo Encryption if encryptionInfo, err = parseEncryptionInfo(encryptionInfoBuf[8:]); err != nil { @@ -605,11 +607,55 @@ func createIV(blockKey interface{}, encryption Encryption) ([]byte, error) { return iv, nil } -// randomBytes returns securely generated random bytes. It will return an error if the system's -// secure random number generator fails to function correctly, in which case the caller should not -// continue. +// randomBytes returns securely generated random bytes. It will return an +// error if the system's secure random number generator fails to function +// correctly, in which case the caller should not continue. func randomBytes(n int) ([]byte, error) { b := make([]byte, n) _, err := rand.Read(b) return b, err } + +// ISO Write Protection Method + +// genISOPasswdHash implements the ISO password hashing algorithm by given +// plaintext password, name of the cryptographic hash algorithm, salt value +// and spin count. +func genISOPasswdHash(passwd, hashAlgorithm, salt string, spinCount int) (hashValue, saltValue string, err error) { + if len(passwd) < 1 || len(passwd) > MaxFieldLength { + err = ErrPasswordLengthInvalid + return + } + hash, ok := map[string]string{ + "MD4": "md4", + "MD5": "md5", + "SHA-1": "sha1", + "SHA-256": "sha256", + "SHA-384": "sha384", + "SHA-512": "sha512", + }[hashAlgorithm] + if !ok { + err = ErrUnsupportedHashAlgorithm + return + } + var b bytes.Buffer + s, _ := randomBytes(16) + if salt != "" { + if s, err = base64.StdEncoding.DecodeString(salt); err != nil { + return + } + } + b.Write(s) + encoder := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder() + passwordBuffer, _ := encoder.Bytes([]byte(passwd)) + b.Write(passwordBuffer) + // Generate the initial hash. + key := hashing(hash, b.Bytes()) + // Now regenerate until spin count. + for i := 0; i < spinCount; i++ { + iterator := createUInt32LEBuffer(i, 4) + key = hashing(hash, key, iterator) + } + hashValue, saltValue = base64.StdEncoding.EncodeToString(key), base64.StdEncoding.EncodeToString(s) + return +} diff --git a/crypt_test.go b/crypt_test.go index 0796482a0b..0ad6f98fba 100644 --- a/crypt_test.go +++ b/crypt_test.go @@ -28,11 +28,27 @@ func TestEncrypt(t *testing.T) { func TestEncryptionMechanism(t *testing.T) { mechanism, err := encryptionMechanism([]byte{3, 0, 3, 0}) assert.Equal(t, mechanism, "extensible") - assert.EqualError(t, err, ErrUnsupportEncryptMechanism.Error()) + assert.EqualError(t, err, ErrUnsupportedEncryptMechanism.Error()) _, err = encryptionMechanism([]byte{}) assert.EqualError(t, err, ErrUnknownEncryptMechanism.Error()) } func TestHashing(t *testing.T) { - assert.Equal(t, hashing("unsupportHashAlgorithm", []byte{}), []uint8([]byte(nil))) + assert.Equal(t, hashing("unsupportedHashAlgorithm", []byte{}), []uint8([]byte(nil))) +} + +func TestGenISOPasswdHash(t *testing.T) { + for hashAlgorithm, expected := range map[string][]string{ + "MD4": {"2lZQZUubVHLm/t6KsuHX4w==", "TTHjJdU70B/6Zq83XGhHVA=="}, + "MD5": {"HWbqyd4dKKCjk1fEhk2kuQ==", "8ADyorkumWCayIukRhlVKQ=="}, + "SHA-1": {"XErQIV3Ol+nhXkyCxrLTEQm+mSc=", "I3nDtyf59ASaNX1l6KpFnA=="}, + "SHA-256": {"7oqMFyfED+mPrzRIBQ+KpKT4SClMHEPOZldliP15xAA=", "ru1R/w3P3Jna2Qo+EE8QiA=="}, + "SHA-384": {"nMODLlxsC8vr0btcq0kp/jksg5FaI3az5Sjo1yZk+/x4bFzsuIvpDKUhJGAk/fzo", "Zjq9/jHlgOY6MzFDSlVNZg=="}, + "SHA-512": {"YZ6jrGOFQgVKK3rDK/0SHGGgxEmFJglQIIRamZc2PkxVtUBp54fQn96+jVXEOqo6dtCSanqksXGcm/h3KaiR4Q==", "p5s/bybHBPtusI7EydTIrg=="}, + } { + hashValue, saltValue, err := genISOPasswdHash("password", hashAlgorithm, expected[1], int(sheetProtectionSpinCount)) + assert.NoError(t, err) + assert.Equal(t, expected[0], hashValue) + assert.Equal(t, expected[1], saltValue) + } } diff --git a/datavalidation.go b/datavalidation.go index 80a0295ed2..205d9488dc 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -29,7 +29,7 @@ const ( DataValidationTypeDate DataValidationTypeDecimal typeList // inline use - DataValidationTypeTextLeng + DataValidationTypeTextLength DataValidationTypeTime // DataValidationTypeWhole Integer DataValidationTypeWhole @@ -116,7 +116,7 @@ func (dd *DataValidation) SetInput(title, msg string) { func (dd *DataValidation) SetDropList(keys []string) error { formula := strings.Join(keys, ",") if MaxFieldLength < len(utf16.Encode([]rune(formula))) { - return ErrDataValidationFormulaLenth + return ErrDataValidationFormulaLength } dd.Formula1 = fmt.Sprintf(`"%s"`, formulaEscaper.Replace(formula)) dd.Type = convDataValidationType(typeList) @@ -155,7 +155,7 @@ func (dd *DataValidation) SetRange(f1, f2 interface{}, t DataValidationType, o D } dd.Formula1, dd.Formula2 = formula1, formula2 dd.Type = convDataValidationType(t) - dd.Operator = convDataValidationOperatior(o) + dd.Operator = convDataValidationOperator(o) return nil } @@ -192,22 +192,22 @@ func (dd *DataValidation) SetSqref(sqref string) { // convDataValidationType get excel data validation type. func convDataValidationType(t DataValidationType) string { typeMap := map[DataValidationType]string{ - typeNone: "none", - DataValidationTypeCustom: "custom", - DataValidationTypeDate: "date", - DataValidationTypeDecimal: "decimal", - typeList: "list", - DataValidationTypeTextLeng: "textLength", - DataValidationTypeTime: "time", - DataValidationTypeWhole: "whole", + typeNone: "none", + DataValidationTypeCustom: "custom", + DataValidationTypeDate: "date", + DataValidationTypeDecimal: "decimal", + typeList: "list", + DataValidationTypeTextLength: "textLength", + DataValidationTypeTime: "time", + DataValidationTypeWhole: "whole", } return typeMap[t] } -// convDataValidationOperatior get excel data validation operator. -func convDataValidationOperatior(o DataValidationOperator) string { +// convDataValidationOperator get excel data validation operator. +func convDataValidationOperator(o DataValidationOperator) string { typeMap := map[DataValidationOperator]string{ DataValidationOperatorBetween: "between", DataValidationOperatorEqual: "equal", diff --git a/datavalidation_test.go b/datavalidation_test.go index 56e7d73578..d07f1b11c6 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -94,7 +94,7 @@ func TestDataValidationError(t *testing.T) { t.Errorf("data validation error. Formula1 must be empty!") return } - assert.EqualError(t, err, ErrDataValidationFormulaLenth.Error()) + 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)) @@ -114,7 +114,7 @@ func TestDataValidationError(t *testing.T) { err = dvRange.SetDropList(keys) assert.Equal(t, prevFormula1, dvRange.Formula1, "Formula1 should be unchanged for invalid input %v", keys) - assert.EqualError(t, err, ErrDataValidationFormulaLenth.Error()) + assert.EqualError(t, err, ErrDataValidationFormulaLength.Error()) } assert.NoError(t, f.AddDataValidation("Sheet1", dvRange)) assert.NoError(t, dvRange.SetRange( diff --git a/errors.go b/errors.go index 9460803cbc..9f39a7a471 100644 --- a/errors.go +++ b/errors.go @@ -16,42 +16,50 @@ import ( "fmt" ) -// newInvalidColumnNameError defined the error message on receiving the invalid column name. +// newInvalidColumnNameError defined the error message on receiving the +// invalid column name. func newInvalidColumnNameError(col string) error { return fmt.Errorf("invalid column name %q", col) } -// newInvalidRowNumberError defined the error message on receiving the invalid row number. +// newInvalidRowNumberError defined the error message on receiving the invalid +// row number. func newInvalidRowNumberError(row int) error { return fmt.Errorf("invalid row number %d", row) } -// newInvalidCellNameError defined the error message on receiving the invalid cell name. +// newInvalidCellNameError defined the error message on receiving the invalid +// cell name. func newInvalidCellNameError(cell string) error { return fmt.Errorf("invalid cell name %q", cell) } -// newInvalidExcelDateError defined the error message on receiving the data with negative values. +// newInvalidExcelDateError defined the error message on receiving the data +// with negative values. func newInvalidExcelDateError(dateValue float64) error { return fmt.Errorf("invalid date value %f, negative values are not supported", dateValue) } -// newUnsupportChartType defined the error message on receiving the chart type are unsupported. -func newUnsupportChartType(chartType 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) } -// newUnzipSizeLimitError defined the error message on unzip size exceeds the limit. +// newUnzipSizeLimitError defined the error message on unzip size exceeds the +// limit. func newUnzipSizeLimitError(unzipSizeLimit int64) error { return fmt.Errorf("unzip size exceeds the %d bytes limit", unzipSizeLimit) } -// newInvalidStyleID defined the error message on receiving the invalid style ID. +// 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) } -// newFieldLengthError defined the error message on receiving the field length overflow. +// 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) } @@ -103,12 +111,18 @@ var ( ErrMaxFileNameLength = errors.New("file name 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 unsupport + // ErrUnknownEncryptMechanism defined the error message on unsupported // encryption mechanism. ErrUnknownEncryptMechanism = errors.New("unknown encryption mechanism") - // ErrUnsupportEncryptMechanism defined the error message on unsupport + // ErrUnsupportedEncryptMechanism defined the error message on unsupported // encryption mechanism. - ErrUnsupportEncryptMechanism = errors.New("unsupport encryption mechanism") + ErrUnsupportedEncryptMechanism = errors.New("unsupported encryption mechanism") + // ErrUnsupportedHashAlgorithm defined the error message on unsupported + // hash algorithm. + ErrUnsupportedHashAlgorithm = errors.New("unsupported hash algorithm") + // ErrPasswordLengthInvalid defined the error message on invalid password + // length. + ErrPasswordLengthInvalid = errors.New("password length invalid") // ErrParameterRequired defined the error message on receive the empty // parameter. ErrParameterRequired = errors.New("parameter is required") @@ -131,11 +145,17 @@ var ( // ErrSheetIdx defined the error message on receive the invalid worksheet // index. ErrSheetIdx = errors.New("invalid worksheet index") + // ErrUnprotectSheet defined the error message on worksheet has set no + // protection. + ErrUnprotectSheet = errors.New("worksheet has set no protect") + // ErrUnprotectSheetPassword defined the error message on remove sheet + // protection with password verification failed. + ErrUnprotectSheetPassword = errors.New("worksheet protect password not match") // ErrGroupSheets defined the error message on group sheets. ErrGroupSheets = errors.New("group worksheet must contain an active worksheet") - // ErrDataValidationFormulaLenth defined the error message for receiving a + // ErrDataValidationFormulaLength defined the error message for receiving a // data validation formula length that exceeds the limit. - ErrDataValidationFormulaLenth = errors.New("data validation must be 0-255 characters") + ErrDataValidationFormulaLength = errors.New("data validation must be 0-255 characters") // ErrDataValidationRange defined the error message on set decimal range // exceeds limit. ErrDataValidationRange = errors.New("data validation range exceeds limit") @@ -164,5 +184,5 @@ var ( ErrSparkline = errors.New("must have the same number of 'Location' and 'Range' parameters") // ErrSparklineStyle defined the error message on receive the invalid // sparkline Style parameters. - ErrSparklineStyle = errors.New("parameter 'Style' must betweent 0-35") + ErrSparklineStyle = errors.New("parameter 'Style' must between 0-35") ) diff --git a/excelize_test.go b/excelize_test.go index 9aaaae9d30..0edfe11e49 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -1160,13 +1160,44 @@ func TestHSL(t *testing.T) { func TestProtectSheet(t *testing.T) { f := NewFile() - assert.NoError(t, f.ProtectSheet("Sheet1", nil)) - assert.NoError(t, f.ProtectSheet("Sheet1", &FormatSheetProtection{ + 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{ Password: "password", EditScenarios: false, })) - + ws, err := f.workSheetReader(sheetName) + assert.NoError(t, err) + 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{ + AlgorithmName: "SHA-512", + Password: "password", + })) + 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.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 password verification + assert.NoError(t, f.UnprotectSheet(sheetName, "password")) + // Test protect worksheet with empty password + assert.NoError(t, f.ProtectSheet(sheetName, &FormatSheetProtection{})) + assert.Equal(t, "", ws.SheetProtection.Password) + // Test protect worksheet with password exceeds the limit length + assert.EqualError(t, f.ProtectSheet(sheetName, &FormatSheetProtection{ + AlgorithmName: "MD4", + Password: strings.Repeat("s", MaxFieldLength+1), + }), ErrPasswordLengthInvalid.Error()) + // Test protect worksheet with unsupported hash algorithm + assert.EqualError(t, f.ProtectSheet(sheetName, &FormatSheetProtection{ + AlgorithmName: "RIPEMD-160", + Password: "password", + }), ErrUnsupportedHashAlgorithm.Error()) // Test protect not exists worksheet. assert.EqualError(t, f.ProtectSheet("SheetN", nil), "sheet SheetN is not exist") } @@ -1176,12 +1207,30 @@ func TestUnprotectSheet(t *testing.T) { if !assert.NoError(t, err) { t.FailNow() } - // Test unprotect not exists worksheet. + // Test remove protection on not exists worksheet. assert.EqualError(t, f.UnprotectSheet("SheetN"), "sheet SheetN is not exist") assert.NoError(t, f.UnprotectSheet("Sheet1")) + assert.EqualError(t, f.UnprotectSheet("Sheet1", "password"), ErrUnprotectSheet.Error()) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestUnprotectSheet.xlsx"))) assert.NoError(t, f.Close()) + + f = NewFile() + sheetName := f.GetSheetName(0) + assert.NoError(t, f.ProtectSheet(sheetName, &FormatSheetProtection{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{ + AlgorithmName: "SHA-512", + Password: "password", + })) + ws, err := f.workSheetReader(sheetName) + assert.NoError(t, err) + ws.SheetProtection.SaltValue = "YWJjZA=====" + assert.EqualError(t, f.UnprotectSheet(sheetName, "wrongPassword"), "illegal base64 data at input byte 8") } func TestSetDefaultTimeStyle(t *testing.T) { diff --git a/pivotTable_test.go b/pivotTable_test.go index 3487793308..d7a8eb13b3 100644 --- a/pivotTable_test.go +++ b/pivotTable_test.go @@ -222,7 +222,7 @@ func TestAddPivotTable(t *testing.T) { PivotTableRange: "Sheet1!$G$2:$M$34", Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, - Data: []PivotTableField{{Data: "Sales", Subtotal: "-", Name: strings.Repeat("s", 256)}}, + Data: []PivotTableField{{Data: "Sales", Subtotal: "-", Name: strings.Repeat("s", MaxFieldLength+1)}}, })) // Test adjust range with invalid range diff --git a/sheet.go b/sheet.go index 17f6693940..26baca8d16 100644 --- a/sheet.go +++ b/sheet.go @@ -1129,10 +1129,14 @@ func (f *File) SetHeaderFooter(sheet string, settings *FormatHeaderFooter) error } // ProtectSheet provides a function to prevent other users from accidentally -// or deliberately changing, moving, or deleting data in a worksheet. For -// example, protect Sheet1 with protection settings: +// 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: // // err := f.ProtectSheet("Sheet1", &excelize.FormatSheetProtection{ +// AlgorithmName: "SHA-512", // Password: "password", // EditScenarios: false, // }) @@ -1168,22 +1172,55 @@ func (f *File) ProtectSheet(sheet string, settings *FormatSheetProtection) error Sort: settings.Sort, } if settings.Password != "" { - ws.SheetProtection.Password = genSheetPasswd(settings.Password) + if settings.AlgorithmName == "" { + ws.SheetProtection.Password = genSheetPasswd(settings.Password) + return err + } + hashValue, saltValue, err := genISOPasswdHash(settings.Password, settings.AlgorithmName, "", int(sheetProtectionSpinCount)) + if err != nil { + return err + } + ws.SheetProtection.Password = "" + ws.SheetProtection.AlgorithmName = settings.AlgorithmName + ws.SheetProtection.SaltValue = saltValue + ws.SheetProtection.HashValue = hashValue + ws.SheetProtection.SpinCount = int(sheetProtectionSpinCount) } return err } -// UnprotectSheet provides a function to unprotect an Excel worksheet. -func (f *File) UnprotectSheet(sheet string) error { +// UnprotectSheet provides a function to remove protection for a sheet, +// specified the second optional password parameter to remove sheet +// protection with password verification. +func (f *File) UnprotectSheet(sheet string, password ...string) error { ws, err := f.workSheetReader(sheet) if err != nil { return err } + // password verification + if len(password) > 0 { + if ws.SheetProtection == nil { + return ErrUnprotectSheet + } + if ws.SheetProtection.AlgorithmName == "" && ws.SheetProtection.Password != genSheetPasswd(password[0]) { + return ErrUnprotectSheetPassword + } + if ws.SheetProtection.AlgorithmName != "" { + // check with given salt value + hashValue, _, err := genISOPasswdHash(password[0], ws.SheetProtection.AlgorithmName, ws.SheetProtection.SaltValue, ws.SheetProtection.SpinCount) + if err != nil { + return err + } + if ws.SheetProtection.HashValue != hashValue { + return ErrUnprotectSheetPassword + } + } + } ws.SheetProtection = nil return err } -// trimSheetName provides a function to trim invaild characters by given worksheet +// 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 { diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 217f367d4e..b09d63009b 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -838,6 +838,7 @@ type formatConditional struct { // FormatSheetProtection directly maps the settings of worksheet protection. type FormatSheetProtection struct { + AlgorithmName string AutoFilter bool DeleteColumns bool DeleteRows bool From 2245fccca0beb25a1bf309a1c9cbd273512f125a Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 9 Jan 2022 00:20:42 +0800 Subject: [PATCH 522/957] Typo fix, rename exported constants, dependencies modules and copyright update Rename exported constants `NameSpaceDublinCoreMetadataIntiative` to `NameSpaceDublinCoreMetadataInitiative` --- LICENSE | 2 +- adjust.go | 8 ++++---- adjust_test.go | 32 +++++++++++++++--------------- calc.go | 2 +- calcchain.go | 8 ++++---- calcchain_test.go | 2 +- cell.go | 16 +++++++-------- cell_test.go | 4 ++-- chart.go | 2 +- col.go | 2 +- comment.go | 2 +- comment_test.go | 2 +- crypt.go | 2 +- crypt_test.go | 2 +- datavalidation.go | 2 +- datavalidation_test.go | 2 +- date.go | 2 +- docProps.go | 16 +++++++-------- docProps_test.go | 18 ++++++++--------- drawing.go | 2 +- drawing_test.go | 2 +- errors.go | 2 +- excelize.go | 2 +- excelize_test.go | 6 +++--- file.go | 6 +++--- go.mod | 10 +++++----- go.sum | 23 +++++++++++----------- lib.go | 6 +++--- merge.go | 22 ++++++++++----------- picture.go | 2 +- pivotTable.go | 16 +++++++-------- rows.go | 16 +++++++-------- rows_test.go | 17 ++++++++-------- shape.go | 2 +- sheet.go | 4 ++-- sheet_test.go | 2 +- sheetpr.go | 2 +- sheetpr_test.go | 4 ++-- sheetview.go | 2 +- sparkline.go | 2 +- stream.go | 28 +++++++++++++-------------- styles.go | 36 +++++++++++++++++----------------- table.go | 44 +++++++++++++++++++++--------------------- templates.go | 10 +++++----- vmlDrawing.go | 2 +- xmlApp.go | 2 +- xmlCalcChain.go | 2 +- xmlChart.go | 2 +- xmlChartSheet.go | 2 +- xmlComments.go | 2 +- xmlContentTypes.go | 2 +- xmlCore.go | 2 +- xmlDecodeDrawing.go | 8 +++----- xmlDrawing.go | 10 ++++------ xmlPivotCache.go | 2 +- xmlPivotTable.go | 2 +- xmlSharedStrings.go | 2 +- xmlStyles.go | 2 +- xmlTable.go | 2 +- xmlTheme.go | 2 +- xmlWorkbook.go | 2 +- xmlWorksheet.go | 2 +- 62 files changed, 220 insertions(+), 224 deletions(-) diff --git a/LICENSE b/LICENSE index 17591f25c4..10897e7d4c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 2016-2021 The excelize Authors. +Copyright (c) 2016-2022 The excelize Authors. All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/adjust.go b/adjust.go index 243c774504..d264afd8bf 100644 --- a/adjust.go +++ b/adjust.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2022 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. // @@ -81,13 +81,13 @@ func (f *File) adjustRowDimensions(ws *xlsxWorksheet, row, offset int) { for i := range ws.SheetData.Row { r := &ws.SheetData.Row[i] if newRow := r.R + offset; r.R >= row && newRow > 0 { - f.ajustSingleRowDimensions(r, newRow) + f.adjustSingleRowDimensions(r, newRow) } } } -// ajustSingleRowDimensions provides a function to ajust single row dimensions. -func (f *File) ajustSingleRowDimensions(r *xlsxRow, num int) { +// adjustSingleRowDimensions provides a function to adjust single row dimensions. +func (f *File) adjustSingleRowDimensions(r *xlsxRow, num int) { r.R = num for i, col := range r.C { colName, _, _ := SplitCellName(col.R) diff --git a/adjust_test.go b/adjust_test.go index 7a482f7193..98e7a8271c 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -48,7 +48,7 @@ func TestAdjustMergeCells(t *testing.T) { // testing adjustMergeCells var cases []struct { - lable string + label string ws *xlsxWorksheet dir adjustDirection num int @@ -58,7 +58,7 @@ func TestAdjustMergeCells(t *testing.T) { // testing insert cases = []struct { - lable string + label string ws *xlsxWorksheet dir adjustDirection num int @@ -66,7 +66,7 @@ func TestAdjustMergeCells(t *testing.T) { expect string }{ { - lable: "insert row on ref", + label: "insert row on ref", ws: &xlsxWorksheet{ MergeCells: &xlsxMergeCells{ Cells: []*xlsxMergeCell{ @@ -82,7 +82,7 @@ func TestAdjustMergeCells(t *testing.T) { expect: "A3:B4", }, { - lable: "insert row on bottom of ref", + label: "insert row on bottom of ref", ws: &xlsxWorksheet{ MergeCells: &xlsxMergeCells{ Cells: []*xlsxMergeCell{ @@ -98,7 +98,7 @@ func TestAdjustMergeCells(t *testing.T) { expect: "A2:B4", }, { - lable: "insert column on the left", + label: "insert column on the left", ws: &xlsxWorksheet{ MergeCells: &xlsxMergeCells{ Cells: []*xlsxMergeCell{ @@ -116,12 +116,12 @@ func TestAdjustMergeCells(t *testing.T) { } for _, c := range cases { assert.NoError(t, f.adjustMergeCells(c.ws, c.dir, c.num, 1)) - assert.Equal(t, c.expect, c.ws.MergeCells.Cells[0].Ref, c.lable) + assert.Equal(t, c.expect, c.ws.MergeCells.Cells[0].Ref, c.label) } // testing delete cases = []struct { - lable string + label string ws *xlsxWorksheet dir adjustDirection num int @@ -129,7 +129,7 @@ func TestAdjustMergeCells(t *testing.T) { expect string }{ { - lable: "delete row on top of ref", + label: "delete row on top of ref", ws: &xlsxWorksheet{ MergeCells: &xlsxMergeCells{ Cells: []*xlsxMergeCell{ @@ -145,7 +145,7 @@ func TestAdjustMergeCells(t *testing.T) { expect: "A2:B2", }, { - lable: "delete row on bottom of ref", + label: "delete row on bottom of ref", ws: &xlsxWorksheet{ MergeCells: &xlsxMergeCells{ Cells: []*xlsxMergeCell{ @@ -161,7 +161,7 @@ func TestAdjustMergeCells(t *testing.T) { expect: "A2:B2", }, { - lable: "delete column on the ref left", + label: "delete column on the ref left", ws: &xlsxWorksheet{ MergeCells: &xlsxMergeCells{ Cells: []*xlsxMergeCell{ @@ -177,7 +177,7 @@ func TestAdjustMergeCells(t *testing.T) { expect: "A2:A3", }, { - lable: "delete column on the ref right", + label: "delete column on the ref right", ws: &xlsxWorksheet{ MergeCells: &xlsxMergeCells{ Cells: []*xlsxMergeCell{ @@ -195,12 +195,12 @@ func TestAdjustMergeCells(t *testing.T) { } for _, c := range cases { assert.NoError(t, f.adjustMergeCells(c.ws, c.dir, c.num, -1)) - assert.Equal(t, c.expect, c.ws.MergeCells.Cells[0].Ref, c.lable) + assert.Equal(t, c.expect, c.ws.MergeCells.Cells[0].Ref, c.label) } // testing delete one row/column cases = []struct { - lable string + label string ws *xlsxWorksheet dir adjustDirection num int @@ -208,7 +208,7 @@ func TestAdjustMergeCells(t *testing.T) { expect string }{ { - lable: "delete one row ref", + label: "delete one row ref", ws: &xlsxWorksheet{ MergeCells: &xlsxMergeCells{ Cells: []*xlsxMergeCell{ @@ -223,7 +223,7 @@ func TestAdjustMergeCells(t *testing.T) { offset: -1, }, { - lable: "delete one column ref", + label: "delete one column ref", ws: &xlsxWorksheet{ MergeCells: &xlsxMergeCells{ Cells: []*xlsxMergeCell{ @@ -240,7 +240,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.lable) + assert.Equal(t, 0, len(c.ws.MergeCells.Cells), c.label) } f = NewFile() diff --git a/calc.go b/calc.go index b096af9487..89eb0879fc 100644 --- a/calc.go +++ b/calc.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2022 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. // diff --git a/calcchain.go b/calcchain.go index 8f5e277279..44a47c9f95 100644 --- a/calcchain.go +++ b/calcchain.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2022 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. // @@ -25,7 +25,7 @@ func (f *File) calcChainReader() *xlsxCalcChain { if f.CalcChain == nil { f.CalcChain = new(xlsxCalcChain) - if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(dafaultXMLPathCalcChain)))). + 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) } @@ -39,7 +39,7 @@ func (f *File) calcChainReader() *xlsxCalcChain { func (f *File) calcChainWriter() { if f.CalcChain != nil && f.CalcChain.C != nil { output, _ := xml.Marshal(f.CalcChain) - f.saveFileList(dafaultXMLPathCalcChain, output) + f.saveFileList(defaultXMLPathCalcChain, output) } } @@ -54,7 +54,7 @@ func (f *File) deleteCalcChain(index int, axis string) { } if len(calc.C) == 0 { f.CalcChain = nil - f.Pkg.Delete(dafaultXMLPathCalcChain) + f.Pkg.Delete(defaultXMLPathCalcChain) content := f.contentTypesReader() content.Lock() defer content.Unlock() diff --git a/calcchain_test.go b/calcchain_test.go index 6144ed5677..c36655bc5e 100644 --- a/calcchain_test.go +++ b/calcchain_test.go @@ -5,7 +5,7 @@ import "testing" func TestCalcChainReader(t *testing.T) { f := NewFile() f.CalcChain = nil - f.Pkg.Store(dafaultXMLPathCalcChain, MacintoshCyrillicCharset) + f.Pkg.Store(defaultXMLPathCalcChain, MacintoshCyrillicCharset) f.calcChainReader() } diff --git a/cell.go b/cell.go index dec743bf93..7a26e78a7a 100644 --- a/cell.go +++ b/cell.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2022 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. // @@ -372,9 +372,9 @@ func (f *File) setCellString(value string) (t, v string, err error) { func (f *File) sharedStringsLoader() (err error) { f.Lock() defer f.Unlock() - if path, ok := f.tempFiles.Load(dafaultXMLPathSharedStrings); ok { - f.Pkg.Store(dafaultXMLPathSharedStrings, f.readBytes(dafaultXMLPathSharedStrings)) - f.tempFiles.Delete(dafaultXMLPathSharedStrings) + if path, ok := f.tempFiles.Load(defaultXMLPathSharedStrings); ok { + f.Pkg.Store(defaultXMLPathSharedStrings, f.readBytes(defaultXMLPathSharedStrings)) + f.tempFiles.Delete(defaultXMLPathSharedStrings) err = os.Remove(path.(string)) f.SharedStrings, f.sharedStringItemMap = nil, nil } @@ -455,7 +455,7 @@ func (f *File) GetCellFormula(sheet, axis string) (string, error) { return "", false, nil } if c.F.T == STCellFormulaTypeShared && c.F.Si != nil { - return getSharedForumula(x, *c.F.Si, c.R), true, nil + return getSharedFormula(x, *c.F.Si, c.R), true, nil } return c.F.Content, true, nil }) @@ -621,7 +621,7 @@ func (ws *xlsxWorksheet) countSharedFormula() (count int) { } // GetCellHyperLink provides a function to get cell hyperlink by given -// worksheet name and axis. Boolean type value link will be ture if the cell +// 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: @@ -1232,7 +1232,7 @@ func parseSharedFormula(dCol, dRow int, orig []byte) (res string, start int) { return } -// getSharedForumula find a cell contains the same formula as another cell, +// getSharedFormula find a cell contains the same formula as another cell, // the "shared" value can be used for the t attribute and the si attribute can // be used to refer to the cell containing the formula. Two formulas are // considered to be the same when their respective representations in @@ -1240,7 +1240,7 @@ func parseSharedFormula(dCol, dRow int, orig []byte) (res string, start int) { // // Note that this function not validate ref tag to check the cell if or not in // allow area, and always return origin shared formula. -func getSharedForumula(ws *xlsxWorksheet, si int, axis string) string { +func getSharedFormula(ws *xlsxWorksheet, si int, axis 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 { diff --git a/cell_test.go b/cell_test.go index 03de73b9e2..cddd2f955a 100644 --- a/cell_test.go +++ b/cell_test.go @@ -653,14 +653,14 @@ func TestFormattedValue2(t *testing.T) { func TestSharedStringsError(t *testing.T) { f, err := OpenFile(filepath.Join("test", "Book1.xlsx"), Options{UnzipXMLSizeLimit: 128}) assert.NoError(t, err) - f.tempFiles.Store(dafaultXMLPathSharedStrings, "") + f.tempFiles.Store(defaultXMLPathSharedStrings, "") assert.Equal(t, "1", f.getFromStringItemMap(1)) // Test reload the file error on set cell cell and rich text. The error message was different between macOS and Windows. err = f.SetCellValue("Sheet1", "A19", "A19") assert.Error(t, err) - f.tempFiles.Store(dafaultXMLPathSharedStrings, "") + f.tempFiles.Store(defaultXMLPathSharedStrings, "") err = f.SetCellRichText("Sheet1", "A19", []RichTextRun{}) assert.Error(t, err) diff --git a/chart.go b/chart.go index b43f9f28b5..bbd276e78d 100644 --- a/chart.go +++ b/chart.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2022 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. // diff --git a/col.go b/col.go index c68820121d..8e0294f99b 100644 --- a/col.go +++ b/col.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2022 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. // diff --git a/comment.go b/comment.go index 07cd9f21dd..aa066ec0cc 100644 --- a/comment.go +++ b/comment.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2022 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. // diff --git a/comment_test.go b/comment_test.go index 9eb07f434a..1901f7f453 100644 --- a/comment_test.go +++ b/comment_test.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2022 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. // diff --git a/crypt.go b/crypt.go index 65b9956bdd..91beab290f 100644 --- a/crypt.go +++ b/crypt.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2022 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. // diff --git a/crypt_test.go b/crypt_test.go index 0ad6f98fba..a81d72d463 100644 --- a/crypt_test.go +++ b/crypt_test.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2022 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. // diff --git a/datavalidation.go b/datavalidation.go index 205d9488dc..4b0c4f34fd 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2022 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. // diff --git a/datavalidation_test.go b/datavalidation_test.go index d07f1b11c6..403cb156b1 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2022 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. // diff --git a/date.go b/date.go index c4acd6d575..9923f9f1a6 100644 --- a/date.go +++ b/date.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2022 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. // diff --git a/docProps.go b/docProps.go index 271b370267..770ed1a7f8 100644 --- a/docProps.go +++ b/docProps.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2022 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. // @@ -75,7 +75,7 @@ func (f *File) SetAppProps(appProperties *AppProperties) (err error) { field string ) app = new(xlsxProperties) - if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(dafaultXMLPathDocPropsApp)))). + 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) return @@ -95,14 +95,14 @@ func (f *File) SetAppProps(appProperties *AppProperties) (err error) { } app.Vt = NameSpaceDocumentPropertiesVariantTypes.Value output, err = xml.Marshal(app) - f.saveFileList(dafaultXMLPathDocPropsApp, output) + f.saveFileList(defaultXMLPathDocPropsApp, output) return } // GetAppProps provides a function to get document application properties. func (f *File) GetAppProps() (ret *AppProperties, err error) { var app = new(xlsxProperties) - if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(dafaultXMLPathDocPropsApp)))). + 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) return @@ -181,7 +181,7 @@ func (f *File) SetDocProps(docProperties *DocProperties) (err error) { ) core = new(decodeCoreProperties) - if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(dafaultXMLPathDocPropsCore)))). + 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) return @@ -189,7 +189,7 @@ func (f *File) SetDocProps(docProperties *DocProperties) (err error) { newProps, err = &xlsxCoreProperties{ Dc: NameSpaceDublinCore, Dcterms: NameSpaceDublinCoreTerms, - Dcmitype: NameSpaceDublinCoreMetadataIntiative, + Dcmitype: NameSpaceDublinCoreMetadataInitiative, XSI: NameSpaceXMLSchemaInstance, Title: core.Title, Subject: core.Subject, @@ -223,7 +223,7 @@ func (f *File) SetDocProps(docProperties *DocProperties) (err error) { newProps.Modified.Text = docProperties.Modified } output, err = xml.Marshal(newProps) - f.saveFileList(dafaultXMLPathDocPropsCore, output) + f.saveFileList(defaultXMLPathDocPropsCore, output) return } @@ -232,7 +232,7 @@ func (f *File) SetDocProps(docProperties *DocProperties) (err error) { func (f *File) GetDocProps() (ret *DocProperties, err error) { var core = new(decodeCoreProperties) - if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(dafaultXMLPathDocPropsCore)))). + 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) return diff --git a/docProps_test.go b/docProps_test.go index 97948c14ab..458280b291 100644 --- a/docProps_test.go +++ b/docProps_test.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2022 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. // @@ -35,13 +35,13 @@ func TestSetAppProps(t *testing.T) { AppVersion: "16.0000", })) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetAppProps.xlsx"))) - f.Pkg.Store(dafaultXMLPathDocPropsApp, nil) + f.Pkg.Store(defaultXMLPathDocPropsApp, nil) assert.NoError(t, f.SetAppProps(&AppProperties{})) assert.NoError(t, f.Close()) // Test unsupported charset f = NewFile() - f.Pkg.Store(dafaultXMLPathDocPropsApp, MacintoshCyrillicCharset) + f.Pkg.Store(defaultXMLPathDocPropsApp, MacintoshCyrillicCharset) assert.EqualError(t, f.SetAppProps(&AppProperties{}), "xml decode error: XML syntax error on line 1: invalid UTF-8") } @@ -53,14 +53,14 @@ func TestGetAppProps(t *testing.T) { props, err := f.GetAppProps() assert.NoError(t, err) assert.Equal(t, props.Application, "Microsoft Macintosh Excel") - f.Pkg.Store(dafaultXMLPathDocPropsApp, nil) + f.Pkg.Store(defaultXMLPathDocPropsApp, nil) _, err = f.GetAppProps() assert.NoError(t, err) assert.NoError(t, f.Close()) // Test unsupported charset f = NewFile() - f.Pkg.Store(dafaultXMLPathDocPropsApp, MacintoshCyrillicCharset) + f.Pkg.Store(defaultXMLPathDocPropsApp, MacintoshCyrillicCharset) _, err = f.GetAppProps() assert.EqualError(t, err, "xml decode error: XML syntax error on line 1: invalid UTF-8") } @@ -87,13 +87,13 @@ func TestSetDocProps(t *testing.T) { Version: "1.0.0", })) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetDocProps.xlsx"))) - f.Pkg.Store(dafaultXMLPathDocPropsCore, nil) + f.Pkg.Store(defaultXMLPathDocPropsCore, nil) assert.NoError(t, f.SetDocProps(&DocProperties{})) assert.NoError(t, f.Close()) // Test unsupported charset f = NewFile() - f.Pkg.Store(dafaultXMLPathDocPropsCore, MacintoshCyrillicCharset) + f.Pkg.Store(defaultXMLPathDocPropsCore, MacintoshCyrillicCharset) assert.EqualError(t, f.SetDocProps(&DocProperties{}), "xml decode error: XML syntax error on line 1: invalid UTF-8") } @@ -105,14 +105,14 @@ func TestGetDocProps(t *testing.T) { props, err := f.GetDocProps() assert.NoError(t, err) assert.Equal(t, props.Creator, "Microsoft Office User") - f.Pkg.Store(dafaultXMLPathDocPropsCore, nil) + f.Pkg.Store(defaultXMLPathDocPropsCore, nil) _, err = f.GetDocProps() assert.NoError(t, err) assert.NoError(t, f.Close()) // Test unsupported charset f = NewFile() - f.Pkg.Store(dafaultXMLPathDocPropsCore, MacintoshCyrillicCharset) + f.Pkg.Store(defaultXMLPathDocPropsCore, MacintoshCyrillicCharset) _, err = f.GetDocProps() assert.EqualError(t, err, "xml decode error: XML syntax error on line 1: invalid UTF-8") } diff --git a/drawing.go b/drawing.go index 86d5ca68c5..be4583ccaf 100644 --- a/drawing.go +++ b/drawing.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2022 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. // diff --git a/drawing_test.go b/drawing_test.go index f2413cfec6..fc89d472b1 100644 --- a/drawing_test.go +++ b/drawing_test.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2022 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. // diff --git a/errors.go b/errors.go index 9f39a7a471..8368fee9a3 100644 --- a/errors.go +++ b/errors.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2022 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. // diff --git a/excelize.go b/excelize.go index 25acd54df6..26bb3409a3 100644 --- a/excelize.go +++ b/excelize.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2022 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. diff --git a/excelize_test.go b/excelize_test.go index 0edfe11e49..c78797d974 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -83,7 +83,7 @@ func TestOpenFile(t *testing.T) { assert.NoError(t, err) _, err = f.GetCellFormula("Sheet2", "I11") assert.NoError(t, err) - getSharedForumula(&xlsxWorksheet{}, 0, "") + getSharedFormula(&xlsxWorksheet{}, 0, "") // Test read cell value with given illegal rows number. _, err = f.GetCellValue("Sheet2", "a-1") @@ -224,7 +224,7 @@ func TestOpenReader(t *testing.T) { assert.Equal(t, "SECRET", val) assert.NoError(t, f.Close()) - // Test open spreadsheet with invalid optioins. + // Test open spreadsheet with invalid options. _, err = OpenReader(bytes.NewReader(oleIdentifier), Options{UnzipSizeLimit: 1, UnzipXMLSizeLimit: 2}) assert.EqualError(t, err, ErrOptionsUnzipSizeLimit.Error()) @@ -1065,7 +1065,7 @@ func TestConditionalFormat(t *testing.T) { // 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 shold 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() diff --git a/file.go b/file.go index 6c8bd93f76..1849bea349 100644 --- a/file.go +++ b/file.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2022 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. // @@ -29,8 +29,8 @@ import ( func NewFile() *File { f := newFile() f.Pkg.Store("_rels/.rels", []byte(xml.Header+templateRels)) - f.Pkg.Store(dafaultXMLPathDocPropsApp, []byte(xml.Header+templateDocpropsApp)) - f.Pkg.Store(dafaultXMLPathDocPropsCore, []byte(xml.Header+templateDocpropsCore)) + 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("xl/theme/theme1.xml", []byte(xml.Header+templateTheme)) f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(xml.Header+templateSheet)) diff --git a/go.mod b/go.mod index 41f53a205d..b7aa1ba578 100644 --- a/go.mod +++ b/go.mod @@ -5,10 +5,10 @@ go 1.15 require ( github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 github.com/richardlehane/mscfb v1.0.3 - github.com/stretchr/testify v1.6.1 + github.com/stretchr/testify v1.7.0 github.com/xuri/efp v0.0.0-20210322160811-ab561f5b45e3 - golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 - golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb - golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985 - golang.org/x/text v0.3.6 + golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 + golang.org/x/image v0.0.0-20211028202545-6944b10bf410 + golang.org/x/net v0.0.0-20220107192237-5cfca573fb4d + golang.org/x/text v0.3.7 ) diff --git a/go.sum b/go.sum index 53c304719c..7fa8255bbf 100644 --- a/go.sum +++ b/go.sum @@ -9,25 +9,24 @@ github.com/richardlehane/mscfb v1.0.3/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7 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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +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-20210322160811-ab561f5b45e3 h1:EpI0bqf/eX9SdZDwlMmahKM+CDBgNbsXMhsN28XrM8o= github.com/xuri/efp v0.0.0-20210322160811-ab561f5b45e3/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= -golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI= -golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb h1:fqpd0EBDzlHRCjiphRR5Zo/RSWWQlWv34418dnEixWk= -golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985 h1:4CSI6oo7cOjJKajidEljs9h+uP0rRZBPPPhcCbj5mw8= -golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 h1:0es+/5331RGQPcXlMfP+WrnIIS6dNnNRe0WB02W0F4M= +golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/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-20220107192237-5cfca573fb4d h1:62NvYBuaanGXR2ZOfwDFkhhl6X1DUgf8qg3GuQvxZsE= +golang.org/x/net v0.0.0-20220107192237-5cfca573fb4d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 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/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -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 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= 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/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= diff --git a/lib.go b/lib.go index 8ec121b403..c125da6e60 100644 --- a/lib.go +++ b/lib.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2022 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. // @@ -31,7 +31,7 @@ func (f *File) ReadZipReader(r *zip.Reader) (map[string][]byte, int, error) { err error docPart = map[string]string{ "[content_types].xml": defaultXMLPathContentTypes, - "xl/sharedstrings.xml": dafaultXMLPathSharedStrings, + "xl/sharedstrings.xml": defaultXMLPathSharedStrings, } fileList = make(map[string][]byte, len(r.File)) worksheets int @@ -47,7 +47,7 @@ func (f *File) ReadZipReader(r *zip.Reader) (map[string][]byte, int, error) { if partName, ok := docPart[strings.ToLower(fileName)]; ok { fileName = partName } - if strings.EqualFold(fileName, dafaultXMLPathSharedStrings) && fileSize > f.options.UnzipXMLSizeLimit { + if strings.EqualFold(fileName, defaultXMLPathSharedStrings) && fileSize > f.options.UnzipXMLSizeLimit { if tempFile, err := f.unzipToTemp(v); err == nil { f.tempFiles.Store(fileName, tempFile) continue diff --git a/merge.go b/merge.go index 2d699a23f7..7119d28632 100644 --- a/merge.go +++ b/merge.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2022 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. // @@ -46,22 +46,22 @@ 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) +func (f *File) MergeCell(sheet, hCell, vCell string) error { + rect, err := areaRefToCoordinates(hCell + ":" + vCell) if err != nil { return err } // Correct the coordinate area, such correct C1:B3 to B1:C3. _ = sortCoordinates(rect) - hcell, _ = CoordinatesToCellName(rect[0], rect[1]) - vcell, _ = CoordinatesToCellName(rect[2], rect[3]) + hCell, _ = CoordinatesToCellName(rect[0], rect[1]) + vCell, _ = CoordinatesToCellName(rect[2], rect[3]) ws, err := f.workSheetReader(sheet) if err != nil { return err } - ref := hcell + ":" + vcell + ref := hCell + ":" + vCell if ws.MergeCells != nil { ws.MergeCells.Cells = append(ws.MergeCells.Cells, &xlsxMergeCell{Ref: ref, rect: rect}) } else { @@ -77,12 +77,12 @@ func (f *File) MergeCell(sheet, hcell, vcell string) error { // 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 string, hCell, vCell string) error { ws, err := f.workSheetReader(sheet) if err != nil { return err } - rect1, err := areaRefToCoordinates(hcell + ":" + vcell) + rect1, err := areaRefToCoordinates(hCell + ":" + vCell) if err != nil { return err } @@ -254,9 +254,9 @@ func mergeCell(cell1, cell2 *xlsxMergeCell) *xlsxMergeCell { if rect1[3] < rect2[3] { rect1[3], rect2[3] = rect2[3], rect1[3] } - hcell, _ := CoordinatesToCellName(rect1[0], rect1[1]) - vcell, _ := CoordinatesToCellName(rect1[2], rect1[3]) - return &xlsxMergeCell{rect: rect1, Ref: hcell + ":" + vcell} + hCell, _ := CoordinatesToCellName(rect1[0], rect1[1]) + vCell, _ := CoordinatesToCellName(rect1[2], rect1[3]) + return &xlsxMergeCell{rect: rect1, Ref: hCell + ":" + vCell} } // MergeCell define a merged cell data. diff --git a/picture.go b/picture.go index 2956ff1ac5..9429f4a97d 100644 --- a/picture.go +++ b/picture.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2022 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. // diff --git a/pivotTable.go b/pivotTable.go index 270ee9923d..d30eeb1dd7 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2022 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. // @@ -265,8 +265,8 @@ func (f *File) addPivotCache(pivotCacheID int, pivotCacheXML string, opt *PivotT } // data range has been checked order, _ := f.getPivotFieldsOrder(opt) - hcell, _ := CoordinatesToCellName(coordinates[0], coordinates[1]) - vcell, _ := CoordinatesToCellName(coordinates[2], coordinates[3]) + hCell, _ := CoordinatesToCellName(coordinates[0], coordinates[1]) + vCell, _ := CoordinatesToCellName(coordinates[2], coordinates[3]) pc := xlsxPivotCacheDefinition{ SaveData: false, RefreshOnLoad: true, @@ -276,7 +276,7 @@ func (f *File) addPivotCache(pivotCacheID int, pivotCacheXML string, opt *PivotT CacheSource: &xlsxCacheSource{ Type: "worksheet", WorksheetSource: &xlsxWorksheetSource{ - Ref: hcell + ":" + vcell, + Ref: hCell + ":" + vCell, Sheet: dataSheet, }, }, @@ -320,8 +320,8 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op return fmt.Errorf("parameter 'PivotTableRange' parsing error: %s", err.Error()) } - hcell, _ := CoordinatesToCellName(coordinates[0], coordinates[1]) - vcell, _ := CoordinatesToCellName(coordinates[2], coordinates[3]) + hCell, _ := CoordinatesToCellName(coordinates[0], coordinates[1]) + vCell, _ := CoordinatesToCellName(coordinates[2], coordinates[3]) pivotTableStyle := func() string { if opt.PivotTableStyleName == "" { @@ -345,7 +345,7 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op ShowError: &opt.ShowError, DataCaption: "Values", Location: &xlsxLocation{ - Ref: hcell + ":" + vcell, + Ref: hCell + ":" + vCell, FirstDataCol: 1, FirstDataRow: 1, FirstHeaderRow: 1, @@ -509,7 +509,7 @@ func (f *File) addPivotColFields(pt *xlsxPivotTableDefinition, opt *PivotTableOp }) } - //in order to create pivot in case there is many Columns and Many Datas + // in order to create pivot in case there is many Columns and Data if len(opt.Data) > 1 { pt.ColFields.Field = append(pt.ColFields.Field, &xlsxField{ X: -2, diff --git a/rows.go b/rows.go index 5071bb6b1a..ea9905b382 100644 --- a/rows.go +++ b/rows.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2022 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. // @@ -290,7 +290,7 @@ func (f *File) getFromStringItemMap(index int) string { return strconv.Itoa(index) } f.sharedStringItemMap = &sync.Map{} - needClose, decoder, tempFile, err := f.xmlDecoder(dafaultXMLPathSharedStrings) + needClose, decoder, tempFile, err := f.xmlDecoder(defaultXMLPathSharedStrings) if needClose && err == nil { defer tempFile.Close() } @@ -369,7 +369,7 @@ func (f *File) getRowHeight(sheet string, row int) int { return int(convertRowHeightToPixels(v.Ht)) } } - // Optimisation for when the row heights haven't changed. + // Optimization for when the row heights haven't changed. return int(defaultRowHeightPixels) } @@ -398,7 +398,7 @@ func (f *File) GetRowHeight(sheet string, row int) (float64, error) { return v.Ht, nil } } - // Optimisation for when the row heights haven't changed. + // Optimization for when the row heights haven't changed. return ht, nil } @@ -411,7 +411,7 @@ func (f *File) sharedStringsReader() *xlsxSST { relPath := f.getWorkbookRelsPath() if f.SharedStrings == nil { var sharedStrings xlsxSST - ss := f.readXML(dafaultXMLPathSharedStrings) + 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) @@ -443,7 +443,7 @@ func (f *File) sharedStringsReader() *xlsxSST { } // getValueFrom return a value from a column/row cell, this function is -// inteded to be used with for range on rows an argument with the spreadsheet +// 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() @@ -453,7 +453,7 @@ func (c *xlsxC) getValueFrom(f *File, d *xlsxSST, raw bool) (string, error) { if c.V != "" { xlsxSI := 0 xlsxSI, _ = strconv.Atoi(c.V) - if _, ok := f.tempFiles.Load(dafaultXMLPathSharedStrings); ok { + if _, ok := f.tempFiles.Load(defaultXMLPathSharedStrings); ok { return f.formattedValue(c.S, f.getFromStringItemMap(xlsxSI), raw), nil } if len(d.SI) > xlsxSI { @@ -684,7 +684,7 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) error { } rowCopy.C = append(make([]xlsxC, 0, len(rowCopy.C)), rowCopy.C...) - f.ajustSingleRowDimensions(&rowCopy, row2) + f.adjustSingleRowDimensions(&rowCopy, row2) if idx2 != -1 { ws.SheetData.Row[idx2] = rowCopy diff --git a/rows_test.go b/rows_test.go index 63321ce4e9..1c682e508b 100644 --- a/rows_test.go +++ b/rows_test.go @@ -207,7 +207,7 @@ func TestColumns(t *testing.T) { func TestSharedStringsReader(t *testing.T) { f := NewFile() - f.Pkg.Store(dafaultXMLPathSharedStrings, MacintoshCyrillicCharset) + f.Pkg.Store(defaultXMLPathSharedStrings, MacintoshCyrillicCharset) f.sharedStringsReader() si := xlsxSI{} assert.EqualValues(t, "", si.String()) @@ -221,16 +221,16 @@ func TestRowVisibility(t *testing.T) { f.NewSheet("Sheet3") assert.NoError(t, f.SetRowVisible("Sheet3", 2, false)) assert.NoError(t, f.SetRowVisible("Sheet3", 2, true)) - visiable, err := f.GetRowVisible("Sheet3", 2) - assert.Equal(t, true, visiable) + visible, err := f.GetRowVisible("Sheet3", 2) + assert.Equal(t, true, visible) assert.NoError(t, err) - visiable, err = f.GetRowVisible("Sheet3", 25) - assert.Equal(t, false, visiable) + visible, err = f.GetRowVisible("Sheet3", 25) + 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") - visible, err := f.GetRowVisible("Sheet3", 0) + visible, err = f.GetRowVisible("Sheet3", 0) assert.Equal(t, false, visible) assert.EqualError(t, err, newInvalidRowNumberError(0).Error()) _, err = f.GetRowVisible("SheetN", 1) @@ -330,8 +330,9 @@ func TestInsertRow(t *testing.T) { assert.NoError(t, f.SaveAs(filepath.Join("test", "TestInsertRow.xlsx"))) } -// Testing internal sructure state after insert operations. -// It is important for insert workflow to be constant to avoid side effect with functions related to internal structure. +// Testing 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) { f := NewFile() sheet1 := f.GetSheetName(0) diff --git a/shape.go b/shape.go index 61322dd981..974aa5fce8 100644 --- a/shape.go +++ b/shape.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2022 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. // diff --git a/sheet.go b/sheet.go index 26baca8d16..3eea6dc088 100644 --- a/sheet.go +++ b/sheet.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2022 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. // @@ -304,7 +304,7 @@ func (f *File) relsWriter() { // setAppXML update docProps/app.xml file of XML. func (f *File) setAppXML() { - f.saveFileList(dafaultXMLPathDocPropsApp, []byte(templateDocpropsApp)) + f.saveFileList(defaultXMLPathDocPropsApp, []byte(templateDocpropsApp)) } // replaceRelationshipsBytes; Some tools that read spreadsheet files have very diff --git a/sheet_test.go b/sheet_test.go index 9bfee70aaa..a5c99a6f16 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -400,7 +400,7 @@ func TestSetActiveSheet(t *testing.T) { func TestSetSheetName(t *testing.T) { f := NewFile() - // Test set workksheet with the same name. + // Test set worksheet with the same name. f.SetSheetName("Sheet1", "Sheet1") assert.Equal(t, "Sheet1", f.GetSheetName(0)) } diff --git a/sheetpr.go b/sheetpr.go index 6f46040cc5..d7e6d2a332 100644 --- a/sheetpr.go +++ b/sheetpr.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2022 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. // diff --git a/sheetpr_test.go b/sheetpr_test.go index 53532e9561..7c669e863a 100644 --- a/sheetpr_test.go +++ b/sheetpr_test.go @@ -153,10 +153,10 @@ func TestSheetPrOptions(t *testing.T) { } } -func TestSetSheetrOptions(t *testing.T) { +func TestSetSheetPrOptions(t *testing.T) { f := NewFile() assert.NoError(t, f.SetSheetPrOptions("Sheet1", TabColor(""))) - // Test SetSheetrOptions on not exists worksheet. + // Test SetSheetPrOptions on not exists worksheet. assert.EqualError(t, f.SetSheetPrOptions("SheetN"), "sheet SheetN is not exist") } diff --git a/sheetview.go b/sheetview.go index 91df04c1ab..7184a7a635 100644 --- a/sheetview.go +++ b/sheetview.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2022 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. // diff --git a/sparkline.go b/sparkline.go index 0d5ef6e1ca..b75a2f1864 100644 --- a/sparkline.go +++ b/sparkline.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2022 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. // diff --git a/stream.go b/stream.go index 2f7bf44382..3551e8f7a7 100644 --- a/stream.go +++ b/stream.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2022 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. // @@ -140,13 +140,13 @@ 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 { +func (sw *StreamWriter) AddTable(hCell, vCell, format string) error { formatSet, err := parseFormatTableSet(format) if err != nil { return err } - coordinates, err := areaRangeToCoordinates(hcell, vcell) + coordinates, err := areaRangeToCoordinates(hCell, vCell) if err != nil { return err } @@ -223,8 +223,8 @@ func (sw *StreamWriter) AddTable(hcell, vcell, format string) error { } // Extract values from a row in the StreamWriter. -func (sw *StreamWriter) getRowValues(hrow, hcol, vcol int) (res []string, err error) { - res = make([]string, vcol-hcol+1) +func (sw *StreamWriter) getRowValues(hRow, hCol, vCol int) (res []string, err error) { + res = make([]string, vCol-hCol+1) r, err := sw.rawData.Reader() if err != nil { @@ -240,7 +240,7 @@ func (sw *StreamWriter) getRowValues(hrow, hcol, vcol int) (res []string, err er if err != nil { return nil, err } - startElement, ok := getRowElement(token, hrow) + startElement, ok := getRowElement(token, hRow) if !ok { continue } @@ -254,17 +254,17 @@ func (sw *StreamWriter) getRowValues(hrow, hcol, vcol int) (res []string, err er if err != nil { return nil, err } - if col < hcol || col > vcol { + if col < hCol || col > vCol { continue } - res[col-hcol] = c.V + res[col-hCol] = c.V } return res, nil } } // Check if the token is an XLSX row with the matching row number. -func getRowElement(token xml.Token, hrow int) (startElement xml.StartElement, ok bool) { +func getRowElement(token xml.Token, hRow int) (startElement xml.StartElement, ok bool) { startElement, ok = token.(xml.StartElement) if !ok { return @@ -279,7 +279,7 @@ func getRowElement(token xml.Token, hrow int) (startElement xml.StartElement, ok continue } row, _ := strconv.Atoi(attr.Value) - if row == hrow { + if row == hRow { ok = true return } @@ -406,13 +406,13 @@ func (sw *StreamWriter) SetColWidth(min, max int, width float64) error { // MergeCell provides a function to merge cells by a given coordinate area for // 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) +func (sw *StreamWriter) MergeCell(hCell, vCell string) error { + _, err := areaRangeToCoordinates(hCell, vCell) if err != nil { return err } sw.mergeCellsCount++ - sw.mergeCells += fmt.Sprintf(``, hcell, vcell) + sw.mergeCells += fmt.Sprintf(``, hCell, vCell) return nil } @@ -563,7 +563,7 @@ func (bw *bufferedWriter) Write(p []byte) (n int, err error) { return bw.buf.Write(p) } -// WriteString wites to the in-memory buffer. The err is always nil. +// WriteString wite to the in-memory buffer. The err is always nil. func (bw *bufferedWriter) WriteString(p string) (n int, err error) { return bw.buf.WriteString(p) } diff --git a/styles.go b/styles.go index e9aabbfdb7..261e3df4dd 100644 --- a/styles.go +++ b/styles.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2022 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. // @@ -1101,7 +1101,7 @@ func (f *File) styleSheetWriter() { func (f *File) sharedStringsWriter() { if f.SharedStrings != nil { output, _ := xml.Marshal(f.SharedStrings) - f.saveFileList(dafaultXMLPathSharedStrings, f.replaceNameSpaceBytes(dafaultXMLPathSharedStrings, output)) + f.saveFileList(defaultXMLPathSharedStrings, f.replaceNameSpaceBytes(defaultXMLPathSharedStrings, output)) } } @@ -2168,7 +2168,7 @@ func (f *File) SetDefaultFont(fontName string) { s.CellStyles.CellStyle[0].CustomBuiltIn = &custom } -// readDefaultFont provides an unmarshalled font value. +// readDefaultFont provides an un-marshalled font value. func (f *File) readDefaultFont() *xlsxFont { s := f.stylesReader() return s.Fonts.Font[0] @@ -2724,42 +2724,42 @@ func (f *File) GetCellStyle(sheet, axis string) (int, error) { // } // err = f.SetCellStyle("Sheet1", "H9", "H9", style) // -func (f *File) SetCellStyle(sheet, hcell, vcell string, styleID int) error { - hcol, hrow, err := CellNameToCoordinates(hcell) +func (f *File) SetCellStyle(sheet, hCell, vCell string, styleID int) error { + hCol, hRow, err := CellNameToCoordinates(hCell) if err != nil { return err } - vcol, vrow, err := CellNameToCoordinates(vcell) + vCol, vRow, err := CellNameToCoordinates(vCell) if err != nil { return err } // Normalize the coordinate area, such correct C1:B3 to B1:C3. - if vcol < hcol { - vcol, hcol = hcol, vcol + if vCol < hCol { + vCol, hCol = hCol, vCol } - if vrow < hrow { - vrow, hrow = hrow, vrow + if vRow < hRow { + vRow, hRow = hRow, vRow } - hcolIdx := hcol - 1 - hrowIdx := hrow - 1 + hColIdx := hCol - 1 + hRowIdx := hRow - 1 - vcolIdx := vcol - 1 - vrowIdx := vrow - 1 + vColIdx := vCol - 1 + vRowIdx := vRow - 1 ws, err := f.workSheetReader(sheet) if err != nil { return err } - prepareSheetXML(ws, vcol, vrow) - makeContiguousColumns(ws, hrow, vrow, vcol) + prepareSheetXML(ws, vCol, vRow) + makeContiguousColumns(ws, hRow, vRow, vCol) ws.Lock() defer ws.Unlock() - for r := hrowIdx; r <= vrowIdx; r++ { - for k := hcolIdx; k <= vcolIdx; k++ { + for r := hRowIdx; r <= vRowIdx; r++ { + for k := hColIdx; k <= vColIdx; k++ { ws.SheetData.Row[r].C[k].S = styleID } } diff --git a/table.go b/table.go index a6959a42da..d2ef369cd9 100644 --- a/table.go +++ b/table.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2022 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. // @@ -61,27 +61,27 @@ func parseFormatTableSet(formatSet string) (*formatTable, error) { // TableStyleMedium1 - TableStyleMedium28 // TableStyleDark1 - TableStyleDark11 // -func (f *File) AddTable(sheet, hcell, vcell, format string) error { +func (f *File) AddTable(sheet, hCell, vCell, format string) error { formatSet, err := parseFormatTableSet(format) if err != nil { return err } // Coordinate conversion, convert C1:B3 to 2,0,1,2. - hcol, hrow, err := CellNameToCoordinates(hcell) + hCol, hRow, err := CellNameToCoordinates(hCell) if err != nil { return err } - vcol, vrow, err := CellNameToCoordinates(vcell) + vCol, vRow, err := CellNameToCoordinates(vCell) if err != nil { return err } - if vcol < hcol { - vcol, hcol = hcol, vcol + if vCol < hCol { + vCol, hCol = hCol, vCol } - if vrow < hrow { - vrow, hrow = hrow, vrow + if vRow < hRow { + vRow, hRow = hRow, vRow } tableID := f.countTables() + 1 @@ -94,7 +94,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, formatSet); err != nil { return err } f.addContentTypePart(tableID, "table") @@ -257,9 +257,9 @@ func parseAutoFilterSet(formatSet string) (*formatAutoFilter, error) { // Excel also allows some simple string matching operations: // // x == b* // begins with b -// x != b* // doesnt begin with b +// x != b* // doesn't begin with b // x == *b // ends with b -// x != *b // doesnt end with b +// x != *b // doesn't end with b // x == *b* // contains b // x != *b* // doesn't contains b // @@ -276,27 +276,27 @@ func parseAutoFilterSet(formatSet string) (*formatAutoFilter, error) { // col < 2000 // Price < 2000 // -func (f *File) AutoFilter(sheet, hcell, vcell, format string) error { - hcol, hrow, err := CellNameToCoordinates(hcell) +func (f *File) AutoFilter(sheet, hCell, vCell, format string) error { + hCol, hRow, err := CellNameToCoordinates(hCell) if err != nil { return err } - vcol, vrow, err := CellNameToCoordinates(vcell) + vCol, vRow, err := CellNameToCoordinates(vCell) if err != nil { return err } - if vcol < hcol { - vcol, hcol = hcol, vcol + if vCol < hCol { + vCol, hCol = hCol, vCol } - if vrow < hrow { - vrow, hrow = hrow, vrow + if vRow < hRow { + vRow, hRow = hRow, vRow } formatSet, _ := parseAutoFilterSet(format) - cellStart, _ := CoordinatesToCellName(hcol, hrow, true) - cellEnd, _ := CoordinatesToCellName(vcol, vrow, true) + cellStart, _ := CoordinatesToCellName(hCol, hRow, true) + cellEnd, _ := CoordinatesToCellName(vCol, vRow, true) ref, filterDB := cellStart+":"+cellEnd, "_xlnm._FilterDatabase" wb := f.workbookReader() sheetID := f.GetSheetIndex(sheet) @@ -324,8 +324,8 @@ func (f *File) AutoFilter(sheet, hcell, vcell, format string) error { wb.DefinedNames.DefinedName = append(wb.DefinedNames.DefinedName, d) } } - refRange := vcol - hcol - return f.autoFilter(sheet, ref, refRange, hcol, formatSet) + refRange := vCol - hCol + return f.autoFilter(sheet, ref, refRange, hCol, formatSet) } // autoFilter provides a function to extract the tokens from the filter diff --git a/templates.go b/templates.go index 1783d7c3ea..20ef31d70c 100644 --- a/templates.go +++ b/templates.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2022 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. // @@ -24,10 +24,10 @@ var ( const ( defaultXMLPathContentTypes = "[Content_Types].xml" - dafaultXMLPathDocPropsApp = "docProps/app.xml" - dafaultXMLPathDocPropsCore = "docProps/core.xml" - dafaultXMLPathCalcChain = "xl/calcChain.xml" - dafaultXMLPathSharedStrings = "xl/sharedStrings.xml" + defaultXMLPathDocPropsApp = "docProps/app.xml" + defaultXMLPathDocPropsCore = "docProps/core.xml" + defaultXMLPathCalcChain = "xl/calcChain.xml" + defaultXMLPathSharedStrings = "xl/sharedStrings.xml" defaultXMLPathStyles = "xl/styles.xml" defaultXMLPathWorkbook = "xl/workbook.xml" ) diff --git a/vmlDrawing.go b/vmlDrawing.go index 58166fa6d3..418b4c539e 100644 --- a/vmlDrawing.go +++ b/vmlDrawing.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2022 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. // diff --git a/xmlApp.go b/xmlApp.go index 322640666e..215ed23cad 100644 --- a/xmlApp.go +++ b/xmlApp.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2022 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. // diff --git a/xmlCalcChain.go b/xmlCalcChain.go index dfbb074e41..b8645f549e 100644 --- a/xmlCalcChain.go +++ b/xmlCalcChain.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2022 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. // diff --git a/xmlChart.go b/xmlChart.go index a838f51d62..3725845d2e 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2022 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. // diff --git a/xmlChartSheet.go b/xmlChartSheet.go index fcc34432c4..0e868f2dcf 100644 --- a/xmlChartSheet.go +++ b/xmlChartSheet.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2022 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. // diff --git a/xmlComments.go b/xmlComments.go index 7965c863e8..8f7a03dc9b 100644 --- a/xmlComments.go +++ b/xmlComments.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2022 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. // diff --git a/xmlContentTypes.go b/xmlContentTypes.go index 6b6db6391e..2f47f94fa7 100644 --- a/xmlContentTypes.go +++ b/xmlContentTypes.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2022 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. // diff --git a/xmlCore.go b/xmlCore.go index 8ed8f30471..9aa09bf237 100644 --- a/xmlCore.go +++ b/xmlCore.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2022 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. // diff --git a/xmlDecodeDrawing.go b/xmlDecodeDrawing.go index da333ef984..ad0b751aa5 100644 --- a/xmlDecodeDrawing.go +++ b/xmlDecodeDrawing.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2022 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. // @@ -111,10 +111,8 @@ type decodePicLocks struct { NoSelect bool `xml:"noSelect,attr,omitempty"` } -// decodeBlip directly maps the blip element in the namespace -// http://purl.oclc.org/ooxml/officeDoc ument/relationships - This element -// specifies the existence of an image (binary large image or picture) and -// contains a reference to the image data. +// decodeBlip element specifies the existence of an image (binary large image +// or picture) and contains a reference to the image data. type decodeBlip struct { Embed string `xml:"embed,attr"` Cstate string `xml:"cstate,attr,omitempty"` diff --git a/xmlDrawing.go b/xmlDrawing.go index 4ae6a29558..f51451bfe2 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2022 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. // @@ -62,7 +62,7 @@ const ( StrictNameSpaceSpreadSheet = "http://purl.oclc.org/ooxml/spreadsheetml/main" NameSpaceDublinCore = "http://purl.org/dc/elements/1.1/" NameSpaceDublinCoreTerms = "http://purl.org/dc/terms/" - NameSpaceDublinCoreMetadataIntiative = "http://purl.org/dc/dcmitype/" + NameSpaceDublinCoreMetadataInitiative = "http://purl.org/dc/dcmitype/" ContentTypeDrawing = "application/vnd.openxmlformats-officedocument.drawing+xml" ContentTypeDrawingML = "application/vnd.openxmlformats-officedocument.drawingml.chart+xml" ContentTypeMacro = "application/vnd.ms-excel.sheet.macroEnabled.main+xml" @@ -160,10 +160,8 @@ type xlsxPicLocks struct { NoSelect bool `xml:"noSelect,attr,omitempty"` } -// xlsxBlip directly maps the blip element in the namespace -// http://purl.oclc.org/ooxml/officeDoc ument/relationships - This element -// specifies the existence of an image (binary large image or picture) and -// contains a reference to the image data. +// 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"` diff --git a/xmlPivotCache.go b/xmlPivotCache.go index 2812cf415b..7f3dac0a3a 100644 --- a/xmlPivotCache.go +++ b/xmlPivotCache.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2022 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. // diff --git a/xmlPivotTable.go b/xmlPivotTable.go index 529b867a2c..38dfb1e487 100644 --- a/xmlPivotTable.go +++ b/xmlPivotTable.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2022 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. // diff --git a/xmlSharedStrings.go b/xmlSharedStrings.go index e505d262dc..3b4bb7ad0e 100644 --- a/xmlSharedStrings.go +++ b/xmlSharedStrings.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2022 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. // diff --git a/xmlStyles.go b/xmlStyles.go index afdc1700ca..d6fc43d740 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2022 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. // diff --git a/xmlTable.go b/xmlTable.go index cb343bd304..2fc8f4d69b 100644 --- a/xmlTable.go +++ b/xmlTable.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2022 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. // diff --git a/xmlTheme.go b/xmlTheme.go index ad557384f1..9e10bdd67d 100644 --- a/xmlTheme.go +++ b/xmlTheme.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2022 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. // diff --git a/xmlWorkbook.go b/xmlWorkbook.go index 0e8839b329..59e76017ee 100644 --- a/xmlWorkbook.go +++ b/xmlWorkbook.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2022 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. // diff --git a/xmlWorksheet.go b/xmlWorksheet.go index b09d63009b..5c121d9f08 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2022 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. // From 891e5baac1a6ac67123fbc6a68f801720882b8ec Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 11 Jan 2022 00:24:24 +0800 Subject: [PATCH 523/957] ref #1096, reduce memory usage by about 50% for large data spreadsheet --- cell.go | 14 ++++++++++++-- cell_test.go | 54 +++++++++++++++++++++++++++++++++++++++++++++++++--- excelize.go | 49 ++++++++++++++++++++++++----------------------- file.go | 5 +++++ rows.go | 35 ++++++++++++++++++++++------------ templates.go | 1 + 6 files changed, 117 insertions(+), 41 deletions(-) diff --git a/cell.go b/cell.go index 7a26e78a7a..ff9a131f80 100644 --- a/cell.go +++ b/cell.go @@ -375,8 +375,18 @@ func (f *File) sharedStringsLoader() (err error) { if path, ok := f.tempFiles.Load(defaultXMLPathSharedStrings); ok { f.Pkg.Store(defaultXMLPathSharedStrings, f.readBytes(defaultXMLPathSharedStrings)) f.tempFiles.Delete(defaultXMLPathSharedStrings) - err = os.Remove(path.(string)) - f.SharedStrings, f.sharedStringItemMap = nil, nil + if err = os.Remove(path.(string)); err != nil { + return + } + f.SharedStrings = nil + } + if f.sharedStringTemp != nil { + if err := f.sharedStringTemp.Close(); err != nil { + return err + } + f.tempFiles.Delete(defaultTempFileSST) + f.sharedStringItem, err = nil, os.Remove(f.sharedStringTemp.Name()) + f.sharedStringTemp = nil } return } diff --git a/cell_test.go b/cell_test.go index cddd2f955a..21e5a44cfa 100644 --- a/cell_test.go +++ b/cell_test.go @@ -2,6 +2,7 @@ package excelize import ( "fmt" + "os" "path/filepath" "reflect" "strconv" @@ -653,9 +654,12 @@ func TestFormattedValue2(t *testing.T) { func TestSharedStringsError(t *testing.T) { f, err := OpenFile(filepath.Join("test", "Book1.xlsx"), Options{UnzipXMLSizeLimit: 128}) assert.NoError(t, err) + tempFile, ok := f.tempFiles.Load(defaultXMLPathSharedStrings) + assert.True(t, ok) f.tempFiles.Store(defaultXMLPathSharedStrings, "") - assert.Equal(t, "1", f.getFromStringItemMap(1)) - + 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. err = f.SetCellValue("Sheet1", "A19", "A19") assert.Error(t, err) @@ -663,6 +667,50 @@ func TestSharedStringsError(t *testing.T) { f.tempFiles.Store(defaultXMLPathSharedStrings, "") err = f.SetCellRichText("Sheet1", "A19", []RichTextRun{}) assert.Error(t, err) - assert.NoError(t, f.Close()) + + f, err = OpenFile(filepath.Join("test", "Book1.xlsx"), Options{UnzipXMLSizeLimit: 128}) + assert.NoError(t, err) + rows, err := f.Rows("Sheet1") + assert.NoError(t, err) + const maxUint16 = 1<<16 - 1 + for rows.Next() { + if rows.CurrentRow() == 19 { + _, err := rows.Columns() + assert.NoError(t, err) + // Test get cell value from string item with invalid offset + f.sharedStringItem[1] = []uint{maxUint16 - 1, maxUint16} + assert.Equal(t, "1", f.getFromStringItem(1)) + break + } + } + assert.NoError(t, rows.Close()) + // Test shared string item temporary files has been closed before close the workbook + assert.NoError(t, f.sharedStringTemp.Close()) + assert.Error(t, f.Close()) + // Cleanup undelete temporary files + f.tempFiles.Range(func(k, v interface{}) bool { + return assert.NoError(t, os.Remove(v.(string))) + }) + + f, err = OpenFile(filepath.Join("test", "Book1.xlsx"), Options{UnzipXMLSizeLimit: 128}) + assert.NoError(t, err) + rows, err = f.Rows("Sheet1") + assert.NoError(t, err) + for rows.Next() { + if rows.CurrentRow() == 19 { + _, err := rows.Columns() + assert.NoError(t, err) + break + } + } + assert.NoError(t, rows.Close()) + assert.NoError(t, f.sharedStringTemp.Close()) + // Test shared string item temporary files has been closed before set the cell value + assert.Error(t, f.SetCellValue("Sheet1", "A1", "A1")) + assert.Error(t, f.Close()) + // Cleanup undelete temporary files + f.tempFiles.Range(func(k, v interface{}) bool { + return assert.NoError(t, os.Remove(v.(string))) + }) } diff --git a/excelize.go b/excelize.go index 26bb3409a3..6100ac457a 100644 --- a/excelize.go +++ b/excelize.go @@ -32,30 +32,31 @@ import ( // File define a populated spreadsheet file struct. type File struct { sync.Mutex - options *Options - xmlAttr map[string][]xml.Attr - checked map[string]bool - sheetMap map[string]string - streams map[string]*StreamWriter - tempFiles sync.Map - CalcChain *xlsxCalcChain - Comments map[string]*xlsxComments - ContentTypes *xlsxTypes - Drawings sync.Map - Path string - SharedStrings *xlsxSST - sharedStringsMap map[string]int - sharedStringItemMap *sync.Map - Sheet sync.Map - SheetCount int - Styles *xlsxStyleSheet - Theme *xlsxTheme - DecodeVMLDrawing map[string]*decodeVmlDrawing - VMLDrawing map[string]*vmlDrawing - WorkBook *xlsxWorkbook - Relationships sync.Map - Pkg sync.Map - CharsetReader charsetTranscoderFn + options *Options + xmlAttr map[string][]xml.Attr + checked map[string]bool + sheetMap map[string]string + streams map[string]*StreamWriter + tempFiles sync.Map + 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 + Theme *xlsxTheme + DecodeVMLDrawing map[string]*decodeVmlDrawing + VMLDrawing map[string]*vmlDrawing + WorkBook *xlsxWorkbook + Relationships sync.Map + Pkg sync.Map + CharsetReader charsetTranscoderFn } type charsetTranscoderFn func(charset string, input io.Reader) (rdr io.Reader, err error) diff --git a/file.go b/file.go index 1849bea349..1f2b772ff0 100644 --- a/file.go +++ b/file.go @@ -85,6 +85,11 @@ func (f *File) SaveAs(name string, opt ...Options) error { // Close closes and cleanup the open temporary file for the spreadsheet. func (f *File) Close() error { var err error + if f.sharedStringTemp != nil { + if err := f.sharedStringTemp.Close(); err != nil { + return err + } + } f.tempFiles.Range(func(k, v interface{}) bool { if err = os.Remove(v.(string)); err != nil { return false diff --git a/rows.go b/rows.go index ea9905b382..56301ddbb8 100644 --- a/rows.go +++ b/rows.go @@ -16,12 +16,12 @@ import ( "encoding/xml" "fmt" "io" + "io/ioutil" "log" "math" "math/big" "os" "strconv" - "sync" "github.com/mohae/deepcopy" ) @@ -280,23 +280,30 @@ func (f *File) Rows(sheet string) (*Rows, error) { return &rows, nil } -// getFromStringItemMap build shared string item map from system temporary +// getFromStringItem build shared string item offset list from system temporary // file at one time, and return value by given to string index. -func (f *File) getFromStringItemMap(index int) string { - if f.sharedStringItemMap != nil { - if value, ok := f.sharedStringItemMap.Load(index); ok { - return value.(string) +func (f *File) getFromStringItem(index int) string { + if f.sharedStringTemp != nil { + if len(f.sharedStringItem) <= index { + return strconv.Itoa(index) } - return strconv.Itoa(index) + offsetRange := f.sharedStringItem[index] + buf := make([]byte, offsetRange[1]-offsetRange[0]) + if _, err := f.sharedStringTemp.ReadAt(buf, int64(offsetRange[0])); err != nil { + return strconv.Itoa(index) + } + return string(buf) } - f.sharedStringItemMap = &sync.Map{} needClose, decoder, tempFile, err := f.xmlDecoder(defaultXMLPathSharedStrings) if needClose && err == nil { defer tempFile.Close() } + f.sharedStringItem = [][]uint{} + f.sharedStringTemp, _ = ioutil.TempFile(os.TempDir(), "excelize-") + f.tempFiles.Store(defaultTempFileSST, f.sharedStringTemp.Name()) var ( inElement string - i int + i, offset uint ) for { token, _ := decoder.Token() @@ -309,12 +316,16 @@ func (f *File) getFromStringItemMap(index int) string { if inElement == "si" { si := xlsxSI{} _ = decoder.DecodeElement(&si, &xmlElement) - f.sharedStringItemMap.Store(i, si.String()) + + startIdx := offset + n, _ := f.sharedStringTemp.WriteString(si.String()) + offset += uint(n) + f.sharedStringItem = append(f.sharedStringItem, []uint{startIdx, offset}) i++ } } } - return f.getFromStringItemMap(index) + return f.getFromStringItem(index) } // xmlDecoder creates XML decoder by given path in the zip from memory data @@ -454,7 +465,7 @@ 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.getFromStringItemMap(xlsxSI), raw), nil + 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 diff --git a/templates.go b/templates.go index 20ef31d70c..81b2c208ca 100644 --- a/templates.go +++ b/templates.go @@ -30,6 +30,7 @@ const ( defaultXMLPathSharedStrings = "xl/sharedStrings.xml" defaultXMLPathStyles = "xl/styles.xml" defaultXMLPathWorkbook = "xl/workbook.xml" + defaultTempFileSST = "sharedStrings" ) const templateDocpropsApp = `0Go Excelize` From b96329cc88b87da25a4389f1d4d5ad08cd40605a Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 12 Jan 2022 00:18:15 +0800 Subject: [PATCH 524/957] Breaking change for data validation and fixed #1117 - Remove second useless parameter `isCurrentSheet` of the function `SetSqrefDropList` - Fix missing page setup of worksheet after re-saving the spreadsheet --- datavalidation.go | 12 ++++-------- datavalidation_test.go | 7 ++----- sheet.go | 18 +++++++++--------- xmlWorksheet.go | 8 ++++---- 4 files changed, 19 insertions(+), 26 deletions(-) diff --git a/datavalidation.go b/datavalidation.go index 4b0c4f34fd..c8c9141a12 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -168,16 +168,12 @@ func (dd *DataValidation) SetRange(f1, f2 interface{}, t DataValidationType, o D // // dvRange := excelize.NewDataValidation(true) // dvRange.Sqref = "A7:B8" -// dvRange.SetSqrefDropList("$E$1:$E$3", true) +// dvRange.SetSqrefDropList("$E$1:$E$3") // f.AddDataValidation("Sheet1", dvRange) // -func (dd *DataValidation) SetSqrefDropList(sqref string, isCurrentSheet bool) error { - if isCurrentSheet { - dd.Formula1 = fmt.Sprintf("%s", sqref) - dd.Type = convDataValidationType(typeList) - return nil - } - return fmt.Errorf("cross-sheet sqref cell are not supported") +func (dd *DataValidation) SetSqrefDropList(sqref string) { + dd.Formula1 = fmt.Sprintf("%s", sqref) + dd.Type = convDataValidationType(typeList) } // SetSqref provides function to set data validation range in drop list. diff --git a/datavalidation_test.go b/datavalidation_test.go index 403cb156b1..eed1b034cc 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -81,15 +81,12 @@ func TestDataValidationError(t *testing.T) { dvRange := NewDataValidation(true) dvRange.SetSqref("A7:B8") dvRange.SetSqref("A7:B8") - assert.NoError(t, dvRange.SetSqrefDropList("$E$1:$E$3", true)) - - err := dvRange.SetSqrefDropList("$E$1:$E$3", false) - assert.EqualError(t, err, "cross-sheet sqref cell are not supported") + dvRange.SetSqrefDropList("$E$1:$E$3") assert.NoError(t, f.AddDataValidation("Sheet1", dvRange)) dvRange = NewDataValidation(true) - err = dvRange.SetDropList(make([]string, 258)) + err := dvRange.SetDropList(make([]string, 258)) if dvRange.Formula1 != "" { t.Errorf("data validation error. Formula1 must be empty!") return diff --git a/sheet.go b/sheet.go index 3eea6dc088..2426bf8a51 100644 --- a/sheet.go +++ b/sheet.go @@ -1335,49 +1335,49 @@ func (o *PageLayoutOrientation) getPageLayout(ps *xlsxPageSetUp) { // setPageLayout provides a method to set the paper size for the worksheet. func (p PageLayoutPaperSize) setPageLayout(ps *xlsxPageSetUp) { - ps.PaperSize = int(p) + 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 == 0 { + if ps == nil || ps.PaperSize == nil { *p = 1 return } - *p = PageLayoutPaperSize(ps.PaperSize) + *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 = int(p) + 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 == 0 { + if ps == nil || ps.FitToHeight == nil { *p = 1 return } - *p = FitToHeight(ps.FitToHeight) + *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 = int(p) + 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 == 0 { + if ps == nil || ps.FitToWidth == nil { *p = 1 return } - *p = FitToWidth(ps.FitToWidth) + *p = FitToWidth(*ps.FitToWidth) } // setPageLayout provides a method to set the scale for the worksheet. diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 5c121d9f08..649377b22e 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -112,16 +112,16 @@ type xlsxPageSetUp struct { Draft bool `xml:"draft,attr,omitempty"` Errors string `xml:"errors,attr,omitempty"` FirstPageNumber string `xml:"firstPageNumber,attr,omitempty"` - FitToHeight int `xml:"fitToHeight,attr,omitempty"` - FitToWidth int `xml:"fitToWidth,attr,omitempty"` + FitToHeight *int `xml:"fitToHeight,attr"` + FitToWidth *int `xml:"fitToWidth,attr,omitempty"` HorizontalDPI int `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"` PaperHeight string `xml:"paperHeight,attr,omitempty"` - PaperSize int `xml:"paperSize,attr,omitempty"` + PaperSize *int `xml:"paperSize,attr,omitempty"` PaperWidth string `xml:"paperWidth,attr,omitempty"` - Scale int `xml:"scale,attr,omitempty"` + Scale int `xml:"scale,attr"` UseFirstPageNumber bool `xml:"useFirstPageNumber,attr,omitempty"` UsePrinterDefaults bool `xml:"usePrinterDefaults,attr,omitempty"` VerticalDPI int `xml:"verticalDpi,attr,omitempty"` From 67127883dddf6a923d12231da8b089861bcca28c Mon Sep 17 00:00:00 2001 From: Dokiy <49900744+Dokiys@users.noreply.github.com> Date: Fri, 14 Jan 2022 00:28:31 +0800 Subject: [PATCH 525/957] Fix adjustMergeCells not modifies cell rect (#1118) --- adjust.go | 1 + adjust_test.go | 143 ++++++++++++++++++++++++++++--------------------- rows.go | 10 ++-- rows_test.go | 16 +++++- 4 files changed, 103 insertions(+), 67 deletions(-) diff --git a/adjust.go b/adjust.go index d264afd8bf..bf3ad7762e 100644 --- a/adjust.go +++ b/adjust.go @@ -226,6 +226,7 @@ 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 { return err } diff --git a/adjust_test.go b/adjust_test.go index 98e7a8271c..b2ec3c46aa 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -48,22 +48,24 @@ func TestAdjustMergeCells(t *testing.T) { // testing adjustMergeCells var cases []struct { - label string - ws *xlsxWorksheet - dir adjustDirection - num int - offset int - expect string + label string + ws *xlsxWorksheet + dir adjustDirection + num int + offset int + expect string + expectRect []int } // testing insert cases = []struct { - label string - ws *xlsxWorksheet - dir adjustDirection - num int - offset int - expect string + label string + ws *xlsxWorksheet + dir adjustDirection + num int + offset int + expect string + expectRect []int }{ { label: "insert row on ref", @@ -71,15 +73,17 @@ func TestAdjustMergeCells(t *testing.T) { MergeCells: &xlsxMergeCells{ Cells: []*xlsxMergeCell{ { - Ref: "A2:B3", + Ref: "A2:B3", + rect: []int{1, 2, 2, 3}, }, }, }, }, - dir: rows, - num: 2, - offset: 1, - expect: "A3:B4", + dir: rows, + num: 2, + offset: 1, + expect: "A3:B4", + expectRect: []int{1, 3, 2, 4}, }, { label: "insert row on bottom of ref", @@ -87,15 +91,17 @@ func TestAdjustMergeCells(t *testing.T) { MergeCells: &xlsxMergeCells{ Cells: []*xlsxMergeCell{ { - Ref: "A2:B3", + Ref: "A2:B3", + rect: []int{1, 2, 2, 3}, }, }, }, }, - dir: rows, - num: 3, - offset: 1, - expect: "A2:B4", + dir: rows, + num: 3, + offset: 1, + expect: "A2:B4", + expectRect: []int{1, 2, 2, 4}, }, { label: "insert column on the left", @@ -103,30 +109,34 @@ func TestAdjustMergeCells(t *testing.T) { MergeCells: &xlsxMergeCells{ Cells: []*xlsxMergeCell{ { - Ref: "A2:B3", + Ref: "A2:B3", + rect: []int{1, 2, 2, 3}, }, }, }, }, - dir: columns, - num: 1, - offset: 1, - expect: "B2:C3", + dir: columns, + num: 1, + offset: 1, + expect: "B2:C3", + expectRect: []int{2, 2, 3, 3}, }, } for _, c := range cases { assert.NoError(t, f.adjustMergeCells(c.ws, c.dir, c.num, 1)) assert.Equal(t, c.expect, c.ws.MergeCells.Cells[0].Ref, c.label) + assert.Equal(t, c.expectRect, c.ws.MergeCells.Cells[0].rect, c.label) } // testing delete cases = []struct { - label string - ws *xlsxWorksheet - dir adjustDirection - num int - offset int - expect string + label string + ws *xlsxWorksheet + dir adjustDirection + num int + offset int + expect string + expectRect []int }{ { label: "delete row on top of ref", @@ -134,15 +144,17 @@ func TestAdjustMergeCells(t *testing.T) { MergeCells: &xlsxMergeCells{ Cells: []*xlsxMergeCell{ { - Ref: "A2:B3", + Ref: "A2:B3", + rect: []int{1, 2, 2, 3}, }, }, }, }, - dir: rows, - num: 2, - offset: -1, - expect: "A2:B2", + dir: rows, + num: 2, + offset: -1, + expect: "A2:B2", + expectRect: []int{1, 2, 2, 2}, }, { label: "delete row on bottom of ref", @@ -150,15 +162,17 @@ func TestAdjustMergeCells(t *testing.T) { MergeCells: &xlsxMergeCells{ Cells: []*xlsxMergeCell{ { - Ref: "A2:B3", + Ref: "A2:B3", + rect: []int{1, 2, 2, 3}, }, }, }, }, - dir: rows, - num: 3, - offset: -1, - expect: "A2:B2", + dir: rows, + num: 3, + offset: -1, + expect: "A2:B2", + expectRect: []int{1, 2, 2, 2}, }, { label: "delete column on the ref left", @@ -166,15 +180,17 @@ func TestAdjustMergeCells(t *testing.T) { MergeCells: &xlsxMergeCells{ Cells: []*xlsxMergeCell{ { - Ref: "A2:B3", + Ref: "A2:B3", + rect: []int{1, 2, 2, 3}, }, }, }, }, - dir: columns, - num: 1, - offset: -1, - expect: "A2:A3", + dir: columns, + num: 1, + offset: -1, + expect: "A2:A3", + expectRect: []int{1, 2, 1, 3}, }, { label: "delete column on the ref right", @@ -182,15 +198,17 @@ func TestAdjustMergeCells(t *testing.T) { MergeCells: &xlsxMergeCells{ Cells: []*xlsxMergeCell{ { - Ref: "A2:B3", + Ref: "A2:B3", + rect: []int{1, 2, 2, 3}, }, }, }, }, - dir: columns, - num: 2, - offset: -1, - expect: "A2:A3", + dir: columns, + num: 2, + offset: -1, + expect: "A2:A3", + expectRect: []int{1, 2, 1, 3}, }, } for _, c := range cases { @@ -200,12 +218,13 @@ func TestAdjustMergeCells(t *testing.T) { // testing delete one row/column cases = []struct { - label string - ws *xlsxWorksheet - dir adjustDirection - num int - offset int - expect string + label string + ws *xlsxWorksheet + dir adjustDirection + num int + offset int + expect string + expectRect []int }{ { label: "delete one row ref", @@ -213,7 +232,8 @@ func TestAdjustMergeCells(t *testing.T) { MergeCells: &xlsxMergeCells{ Cells: []*xlsxMergeCell{ { - Ref: "A1:B1", + Ref: "A1:B1", + rect: []int{1, 1, 2, 1}, }, }, }, @@ -228,7 +248,8 @@ func TestAdjustMergeCells(t *testing.T) { MergeCells: &xlsxMergeCells{ Cells: []*xlsxMergeCell{ { - Ref: "A1:A2", + Ref: "A1:A2", + rect: []int{1, 1, 1, 2}, }, }, }, diff --git a/rows.go b/rows.go index 56301ddbb8..0ced386559 100644 --- a/rows.go +++ b/rows.go @@ -661,7 +661,8 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) error { if err != nil { return err } - if row > len(ws.SheetData.Row) || row2 < 1 || row == row2 { + + if row2 < 1 || row == row2 { return nil } @@ -675,14 +676,15 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) error { break } } - if !ok { - return nil - } if err := f.adjustHelper(sheet, rows, row2, 1); err != nil { return err } + if !ok { + return nil + } + idx2 := -1 for i, r := range ws.SheetData.Row { if r.R == row2 { diff --git a/rows_test.go b/rows_test.go index 1c682e508b..f6a3da429e 100644 --- a/rows_test.go +++ b/rows_test.go @@ -669,6 +669,7 @@ func TestDuplicateRowInsertBefore(t *testing.T) { f := newFileWithDefaults() assert.NoError(t, f.DuplicateRowTo(sheet, 2, 1)) + assert.NoError(t, f.DuplicateRowTo(sheet, 10, 4)) if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "InsertBefore"))) { t.FailNow() @@ -678,7 +679,7 @@ func TestDuplicateRowInsertBefore(t *testing.T) { "A1": cells["A2"], "B1": cells["B2"], "A2": cells["A1"], "B2": cells["B1"], "A3": cells["A2"], "B3": cells["B2"], - "A4": cells["A3"], "B4": cells["B3"], + "A5": cells["A3"], "B5": cells["B3"], } for cell, val := range expect { v, err := f.GetCellValue(sheet, cell) @@ -846,7 +847,18 @@ func TestDuplicateRowInvalidRowNum(t *testing.T) { } func TestDuplicateRowTo(t *testing.T) { - f := File{} + f, sheetName := NewFile(), "Sheet1" + // Test duplicate row with invalid target row number + assert.Equal(t, nil, f.DuplicateRowTo(sheetName, 1, 0)) + // Test duplicate row with equal source and target row number + 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 + f.Sheet.Store("xl/worksheets/sheet1.xml", &xlsxWorksheet{ + MergeCells: &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:B1"}}}}) + 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") } From 236ee61d201e45b1fe33a58b290adb7ee32d3488 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 15 Jan 2022 00:06:34 +0800 Subject: [PATCH 526/957] This closes #1119, style parsing issue fixed --- lib.go | 2 ++ xmlDecodeDrawing.go | 2 +- xmlStyles.go | 62 ++++++++++++++++++++++----------------------- xmlWorksheet.go | 12 ++++----- 4 files changed, 40 insertions(+), 38 deletions(-) diff --git a/lib.go b/lib.go index c125da6e60..caaeab2f90 100644 --- a/lib.go +++ b/lib.go @@ -473,6 +473,8 @@ func (avb *attrValBool) UnmarshalXML(d *xml.Decoder, start xml.StartElement) err return nil } } + defaultVal := true + avb.Val = &defaultVal return nil } diff --git a/xmlDecodeDrawing.go b/xmlDecodeDrawing.go index ad0b751aa5..0ca63d1d38 100644 --- a/xmlDecodeDrawing.go +++ b/xmlDecodeDrawing.go @@ -78,7 +78,7 @@ type decodeWsDr struct { type decodeTwoCellAnchor struct { From *decodeFrom `xml:"from"` To *decodeTo `xml:"to"` - Pic *decodePic `xml:"pic,omitempty"` + Pic *decodePic `xml:"pic"` ClientData *decodeClientData `xml:"clientData"` } diff --git a/xmlStyles.go b/xmlStyles.go index d6fc43d740..a9c3f3be6f 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -20,16 +20,16 @@ import ( type xlsxStyleSheet struct { sync.Mutex XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main styleSheet"` - NumFmts *xlsxNumFmts `xml:"numFmts,omitempty"` - Fonts *xlsxFonts `xml:"fonts,omitempty"` - Fills *xlsxFills `xml:"fills,omitempty"` - Borders *xlsxBorders `xml:"borders,omitempty"` - CellStyleXfs *xlsxCellStyleXfs `xml:"cellStyleXfs,omitempty"` - CellXfs *xlsxCellXfs `xml:"cellXfs,omitempty"` - CellStyles *xlsxCellStyles `xml:"cellStyles,omitempty"` - Dxfs *xlsxDxfs `xml:"dxfs,omitempty"` - TableStyles *xlsxTableStyles `xml:"tableStyles,omitempty"` - Colors *xlsxStyleColors `xml:"colors,omitempty"` + NumFmts *xlsxNumFmts `xml:"numFmts"` + Fonts *xlsxFonts `xml:"fonts"` + Fills *xlsxFills `xml:"fills"` + Borders *xlsxBorders `xml:"borders"` + CellStyleXfs *xlsxCellStyleXfs `xml:"cellStyleXfs"` + CellXfs *xlsxCellXfs `xml:"cellXfs"` + CellStyles *xlsxCellStyles `xml:"cellStyles"` + Dxfs *xlsxDxfs `xml:"dxfs"` + TableStyles *xlsxTableStyles `xml:"tableStyles"` + Colors *xlsxStyleColors `xml:"colors"` ExtLst *xlsxExtLst `xml:"extLst"` } @@ -60,7 +60,7 @@ type xlsxProtection struct { // xlsxLine expresses a single set of cell border. type xlsxLine struct { Style string `xml:"style,attr,omitempty"` - Color *xlsxColor `xml:"color,omitempty"` + Color *xlsxColor `xml:"color"` } // xlsxColor is a common mapping used for both the fgColor and bgColor elements. @@ -87,13 +87,13 @@ type xlsxFonts struct { // xlsxFont directly maps the font element. This element defines the // properties for one of the fonts used in this workbook. type xlsxFont struct { - B *attrValBool `xml:"b,omitempty"` - I *attrValBool `xml:"i,omitempty"` - Strike *attrValBool `xml:"strike,omitempty"` - Outline *attrValBool `xml:"outline,omitempty"` - Shadow *attrValBool `xml:"shadow,omitempty"` - Condense *attrValBool `xml:"condense,omitempty"` - Extend *attrValBool `xml:"extend,omitempty"` + B *attrValBool `xml:"b"` + I *attrValBool `xml:"i"` + Strike *attrValBool `xml:"strike"` + Outline *attrValBool `xml:"outline"` + Shadow *attrValBool `xml:"shadow"` + Condense *attrValBool `xml:"condense"` + Extend *attrValBool `xml:"extend"` U *attrValString `xml:"u"` Sz *attrValFloat `xml:"sz"` Color *xlsxColor `xml:"color"` @@ -109,14 +109,14 @@ type xlsxFont struct { // applied across the cell. type xlsxFills struct { Count int `xml:"count,attr"` - Fill []*xlsxFill `xml:"fill,omitempty"` + Fill []*xlsxFill `xml:"fill"` } // xlsxFill directly maps the fill element. This element specifies fill // formatting. type xlsxFill struct { - PatternFill *xlsxPatternFill `xml:"patternFill,omitempty"` - GradientFill *xlsxGradientFill `xml:"gradientFill,omitempty"` + PatternFill *xlsxPatternFill `xml:"patternFill"` + GradientFill *xlsxGradientFill `xml:"gradientFill"` } // xlsxPatternFill is used to specify cell fill information for pattern and @@ -138,7 +138,7 @@ type xlsxGradientFill struct { Right float64 `xml:"right,attr,omitempty"` Top float64 `xml:"top,attr,omitempty"` Type string `xml:"type,attr,omitempty"` - Stop []*xlsxGradientFillStop `xml:"stop,omitempty"` + Stop []*xlsxGradientFillStop `xml:"stop"` } // xlsxGradientFillStop directly maps the stop element. @@ -152,7 +152,7 @@ type xlsxGradientFillStop struct { // the workbook. type xlsxBorders struct { Count int `xml:"count,attr"` - Border []*xlsxBorder `xml:"border,omitempty"` + Border []*xlsxBorder `xml:"border"` } // xlsxBorder directly maps the border element. Expresses a single set of cell @@ -177,7 +177,7 @@ type xlsxBorder struct { type xlsxCellStyles struct { XMLName xml.Name `xml:"cellStyles"` Count int `xml:"count,attr"` - CellStyle []*xlsxCellStyle `xml:"cellStyle,omitempty"` + CellStyle []*xlsxCellStyle `xml:"cellStyle"` } // xlsxCellStyle directly maps the cellStyle element. This element represents @@ -187,10 +187,10 @@ type xlsxCellStyle struct { XMLName xml.Name `xml:"cellStyle"` Name string `xml:"name,attr"` XfID int `xml:"xfId,attr"` - BuiltInID *int `xml:"builtinId,attr,omitempty"` - ILevel *int `xml:"iLevel,attr,omitempty"` - Hidden *bool `xml:"hidden,attr,omitempty"` - CustomBuiltIn *bool `xml:"customBuiltin,attr,omitempty"` + BuiltInID *int `xml:"builtinId,attr"` + ILevel *int `xml:"iLevel,attr"` + Hidden *bool `xml:"hidden,attr"` + CustomBuiltIn *bool `xml:"customBuiltin,attr"` } // xlsxCellStyleXfs directly maps the cellStyleXfs element. This element @@ -245,7 +245,7 @@ type xlsxCellXfs struct { // to any formatting already present on the object using the dxf record. type xlsxDxfs struct { Count int `xml:"count,attr"` - Dxfs []*xlsxDxf `xml:"dxf,omitempty"` + Dxfs []*xlsxDxf `xml:"dxf"` } // xlsxDxf directly maps the dxf element. A single dxf record, expressing @@ -273,7 +273,7 @@ type xlsxTableStyles struct { Count int `xml:"count,attr"` DefaultPivotStyle string `xml:"defaultPivotStyle,attr"` DefaultTableStyle string `xml:"defaultTableStyle,attr"` - TableStyles []*xlsxTableStyle `xml:"tableStyle,omitempty"` + TableStyles []*xlsxTableStyle `xml:"tableStyle"` } // xlsxTableStyle directly maps the tableStyle element. This element represents @@ -293,7 +293,7 @@ type xlsxTableStyle struct { // to format and render the numeric value of a cell. type xlsxNumFmts struct { Count int `xml:"count,attr"` - NumFmt []*xlsxNumFmt `xml:"numFmt,omitempty"` + NumFmt []*xlsxNumFmt `xml:"numFmt"` } // xlsxNumFmt directly maps the numFmt element. This element specifies number diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 649377b22e..76c61884b5 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -113,15 +113,15 @@ type xlsxPageSetUp struct { Errors string `xml:"errors,attr,omitempty"` FirstPageNumber string `xml:"firstPageNumber,attr,omitempty"` FitToHeight *int `xml:"fitToHeight,attr"` - FitToWidth *int `xml:"fitToWidth,attr,omitempty"` + FitToWidth *int `xml:"fitToWidth,attr"` HorizontalDPI int `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"` PaperHeight string `xml:"paperHeight,attr,omitempty"` - PaperSize *int `xml:"paperSize,attr,omitempty"` + PaperSize *int `xml:"paperSize,attr"` PaperWidth string `xml:"paperWidth,attr,omitempty"` - Scale int `xml:"scale,attr"` + Scale int `xml:"scale,attr,omitempty"` UseFirstPageNumber bool `xml:"useFirstPageNumber,attr,omitempty"` UsePrinterDefaults bool `xml:"usePrinterDefaults,attr,omitempty"` VerticalDPI int `xml:"verticalDpi,attr,omitempty"` @@ -239,9 +239,9 @@ type xlsxSheetPr struct { CodeName string `xml:"codeName,attr,omitempty"` FilterMode bool `xml:"filterMode,attr,omitempty"` EnableFormatConditionsCalculation *bool `xml:"enableFormatConditionsCalculation,attr"` - TabColor *xlsxTabColor `xml:"tabColor,omitempty"` - OutlinePr *xlsxOutlinePr `xml:"outlinePr,omitempty"` - PageSetUpPr *xlsxPageSetUpPr `xml:"pageSetUpPr,omitempty"` + TabColor *xlsxTabColor `xml:"tabColor"` + OutlinePr *xlsxOutlinePr `xml:"outlinePr"` + PageSetUpPr *xlsxPageSetUpPr `xml:"pageSetUpPr"` } // xlsxOutlinePr maps to the outlinePr element. SummaryBelow allows you to From 50c4dedf8ddb4a7a696a668946bb536c2e8e5623 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 16 Jan 2022 14:28:35 +0800 Subject: [PATCH 527/957] This closes #1122, improve compatibility with LibreOffice Fixed the issue auto filter doesn't work on LibreOffice if the sheet name has spaces --- table.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/table.go b/table.go index d2ef369cd9..961f9e6e35 100644 --- a/table.go +++ b/table.go @@ -300,7 +300,7 @@ func (f *File) AutoFilter(sheet, hCell, vCell, format string) error { ref, filterDB := cellStart+":"+cellEnd, "_xlnm._FilterDatabase" wb := f.workbookReader() sheetID := f.GetSheetIndex(sheet) - filterRange := fmt.Sprintf("%s!%s", sheet, ref) + filterRange := fmt.Sprintf("'%s'!%s", sheet, ref) d := xlsxDefinedName{ Name: filterDB, Hidden: true, From 4daa6ed0b46fdd994e46403feb049b162eca19b8 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 17 Jan 2022 08:05:52 +0800 Subject: [PATCH 528/957] Breaking change: remove `TotalRows` of row iterator and performance optimization Reduce allocation memory 20%, and 80% GC times for the row's iterator --- cell_test.go | 8 +++ rows.go | 156 +++++++++++++++++++++------------------------------ rows_test.go | 12 +--- 3 files changed, 75 insertions(+), 101 deletions(-) diff --git a/cell_test.go b/cell_test.go index 21e5a44cfa..b3bb997df3 100644 --- a/cell_test.go +++ b/cell_test.go @@ -340,6 +340,14 @@ func TestGetCellType(t *testing.T) { assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) } +func TestGetValueFrom(t *testing.T) { + f := NewFile() + c := xlsxC{T: "s"} + value, err := c.getValueFrom(f, f.sharedStringsReader(), false) + assert.NoError(t, err) + assert.Equal(t, "", value) +} + func TestGetCellFormula(t *testing.T) { // Test get cell formula on not exist worksheet. f := NewFile() diff --git a/rows.go b/rows.go index 0ced386559..8072079388 100644 --- a/rows.go +++ b/rows.go @@ -68,29 +68,49 @@ func (f *File) GetRows(sheet string, opts ...Options) ([][]string, error) { // Rows defines an iterator to a sheet. type Rows struct { - err error - curRow, totalRows, stashRow int - rawCellValue bool - sheet string - f *File - tempFile *os.File - decoder *xml.Decoder + err error + curRow, seekRow int + needClose, rawCellValue bool + sheet string + f *File + tempFile *os.File + sst *xlsxSST + decoder *xml.Decoder + token xml.Token } // CurrentRow returns the row number that represents the current row. func (rows *Rows) CurrentRow() int { - return rows.curRow -} - -// TotalRows returns the total rows count in the worksheet. -func (rows *Rows) TotalRows() int { - return rows.totalRows + return rows.seekRow } // Next will return true if find the next row element. func (rows *Rows) Next() bool { - rows.curRow++ - return rows.curRow <= rows.totalRows + rows.seekRow++ + if rows.curRow >= rows.seekRow { + return true + } + for { + token, _ := rows.decoder.Token() + if token == nil { + return false + } + switch xmlElement := token.(type) { + case xml.StartElement: + if xmlElement.Name.Local == "row" { + rows.curRow++ + if rowNum, _ := attrValToInt("r", xmlElement.Attr); rowNum != 0 { + rows.curRow = rowNum + } + rows.token = token + return true + } + case xml.EndElement: + if xmlElement.Name.Local == "sheetData" { + return false + } + } + } } // Error will return the error when the error occurs. @@ -109,44 +129,40 @@ func (rows *Rows) Close() error { // Columns return the current row's column values. func (rows *Rows) Columns(opts ...Options) ([]string, error) { - var rowIterator rowXMLIterator - if rows.stashRow >= rows.curRow { - return rowIterator.columns, rowIterator.err + if rows.curRow > rows.seekRow { + return nil, nil } - rows.rawCellValue = parseOptions(opts...).RawCellValue - rowIterator.rows = rows - rowIterator.d = rows.f.sharedStringsReader() + var rowIterator rowXMLIterator + var token xml.Token + rows.rawCellValue, rows.sst = parseOptions(opts...).RawCellValue, rows.f.sharedStringsReader() for { - token, _ := rows.decoder.Token() - if token == nil { + if rows.token != nil { + token = rows.token + } else if token, _ = rows.decoder.Token(); token == nil { break } switch xmlElement := token.(type) { case xml.StartElement: rowIterator.inElement = xmlElement.Name.Local if rowIterator.inElement == "row" { - rowIterator.row++ - if rowIterator.attrR, rowIterator.err = attrValToInt("r", xmlElement.Attr); rowIterator.attrR != 0 { - rowIterator.row = rowIterator.attrR + rowNum := 0 + if rowNum, rowIterator.err = attrValToInt("r", xmlElement.Attr); rowNum != 0 { + rows.curRow = rowNum + } else if rows.token == nil { + rows.curRow++ } - if rowIterator.row > rowIterator.rows.curRow { - rowIterator.rows.stashRow = rowIterator.row - 1 + if rows.curRow > rows.seekRow { + rows.token = nil return rowIterator.columns, rowIterator.err } } - rowXMLHandler(&rowIterator, &xmlElement, rows.rawCellValue) - if rowIterator.err != nil { + if rows.rowXMLHandler(&rowIterator, &xmlElement, rows.rawCellValue); rowIterator.err != nil { + rows.token = nil return rowIterator.columns, rowIterator.err } + rows.token = nil case xml.EndElement: - rowIterator.inElement = xmlElement.Name.Local - if rowIterator.row == 0 && rowIterator.rows.curRow > 1 { - rowIterator.row = rowIterator.rows.curRow - } - if rowIterator.inElement == "row" && rowIterator.row+1 < rowIterator.rows.curRow { - return rowIterator.columns, rowIterator.err - } - if rowIterator.inElement == "sheetData" { + if xmlElement.Name.Local == "sheetData" { return rowIterator.columns, rowIterator.err } } @@ -173,29 +189,25 @@ func (err ErrSheetNotExist) Error() string { // rowXMLIterator defined runtime use field for the worksheet row SAX parser. type rowXMLIterator struct { - err error - inElement string - attrR, cellCol, row int - columns []string - rows *Rows - d *xlsxSST + err error + inElement string + cellCol int + columns []string } // rowXMLHandler parse the row XML element of the worksheet. -func rowXMLHandler(rowIterator *rowXMLIterator, xmlElement *xml.StartElement, raw bool) { - rowIterator.err = nil +func (rows *Rows) rowXMLHandler(rowIterator *rowXMLIterator, xmlElement *xml.StartElement, raw bool) { if rowIterator.inElement == "c" { rowIterator.cellCol++ colCell := xlsxC{} - _ = rowIterator.rows.decoder.DecodeElement(&colCell, xmlElement) + _ = rows.decoder.DecodeElement(&colCell, xmlElement) if colCell.R != "" { if rowIterator.cellCol, _, rowIterator.err = CellNameToCoordinates(colCell.R); rowIterator.err != nil { return } } blank := rowIterator.cellCol - len(rowIterator.columns) - val, _ := colCell.getValueFrom(rowIterator.rows.f, rowIterator.d, raw) - if val != "" || colCell.F != nil { + if val, _ := colCell.getValueFrom(rows.f, rows.sst, raw); val != "" || colCell.F != nil { rowIterator.columns = append(appendSpace(blank, rowIterator.columns), val) } } @@ -236,48 +248,10 @@ func (f *File) Rows(sheet string) (*Rows, error) { output, _ := xml.Marshal(worksheet) f.saveFileList(name, f.replaceNameSpaceBytes(name, output)) } - var ( - err error - inElement string - row int - rows Rows - needClose bool - decoder *xml.Decoder - tempFile *os.File - ) - if needClose, decoder, tempFile, err = f.xmlDecoder(name); needClose && err == nil { - defer tempFile.Close() - } - for { - token, _ := decoder.Token() - if token == nil { - break - } - switch xmlElement := token.(type) { - case xml.StartElement: - inElement = xmlElement.Name.Local - if inElement == "row" { - row++ - for _, attr := range xmlElement.Attr { - if attr.Name.Local == "r" { - row, err = strconv.Atoi(attr.Value) - if err != nil { - return &rows, err - } - } - } - rows.totalRows = row - } - case xml.EndElement: - if xmlElement.Name.Local == "sheetData" { - rows.f = f - rows.sheet = name - _, rows.decoder, rows.tempFile, err = f.xmlDecoder(name) - return &rows, err - } - } - } - return &rows, nil + var err error + rows := Rows{f: f, sheet: name} + rows.needClose, rows.decoder, rows.tempFile, err = f.xmlDecoder(name) + return &rows, err } // getFromStringItem build shared string item offset list from system temporary diff --git a/rows_test.go b/rows_test.go index f6a3da429e..0ac9271755 100644 --- a/rows_test.go +++ b/rows_test.go @@ -44,13 +44,6 @@ func TestRows(t *testing.T) { } assert.NoError(t, f.Close()) - f = NewFile() - f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`1B`)) - f.Sheet.Delete("xl/worksheets/sheet1.xml") - delete(f.checked, "xl/worksheets/sheet1.xml") - _, err = f.Rows("Sheet1") - assert.EqualError(t, err, `strconv.Atoi: parsing "A": invalid syntax`) - f.Pkg.Store("xl/worksheets/sheet1.xml", nil) _, err = f.Rows("Sheet1") assert.NoError(t, err) @@ -82,7 +75,6 @@ func TestRowsIterator(t *testing.T) { for rows.Next() { rowCount++ assert.Equal(t, rowCount, rows.CurrentRow()) - assert.Equal(t, expectedNumRow, rows.TotalRows()) require.True(t, rowCount <= expectedNumRow, "rowCount is greater than expected") } assert.Equal(t, expectedNumRow, rowCount) @@ -186,7 +178,7 @@ func TestColumns(t *testing.T) { assert.NoError(t, err) rows.decoder = f.xmlNewDecoder(bytes.NewReader([]byte(`1B`))) - rows.stashRow, rows.curRow = 0, 1 + assert.True(t, rows.Next()) _, err = rows.Columns() assert.EqualError(t, err, `strconv.Atoi: parsing "A": invalid syntax`) @@ -194,8 +186,8 @@ func TestColumns(t *testing.T) { _, err = rows.Columns() assert.NoError(t, err) - rows.curRow = 3 rows.decoder = f.xmlNewDecoder(bytes.NewReader([]byte(`1`))) + assert.True(t, rows.Next()) _, err = rows.Columns() assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) From 74f6ea94eae45c8fb89a23cc94802e57ce279a84 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 19 Jan 2022 00:51:09 +0800 Subject: [PATCH 529/957] ref #1054, breaking change for the column and row's iterator This removed 3 exported functions: `TotalCols`, `CurrentCol` and `CurrentRow` --- cell_test.go | 8 ++++++-- col.go | 10 ---------- col_test.go | 9 +++++---- rows.go | 5 ----- rows_test.go | 1 - 5 files changed, 11 insertions(+), 22 deletions(-) diff --git a/cell_test.go b/cell_test.go index b3bb997df3..8d00e2def2 100644 --- a/cell_test.go +++ b/cell_test.go @@ -682,8 +682,10 @@ func TestSharedStringsError(t *testing.T) { rows, err := f.Rows("Sheet1") assert.NoError(t, err) const maxUint16 = 1<<16 - 1 + currentRow := 0 for rows.Next() { - if rows.CurrentRow() == 19 { + currentRow++ + if currentRow == 19 { _, err := rows.Columns() assert.NoError(t, err) // Test get cell value from string item with invalid offset @@ -705,8 +707,10 @@ func TestSharedStringsError(t *testing.T) { assert.NoError(t, err) rows, err = f.Rows("Sheet1") assert.NoError(t, err) + currentRow = 0 for rows.Next() { - if rows.CurrentRow() == 19 { + currentRow++ + if currentRow == 19 { _, err := rows.Columns() assert.NoError(t, err) break diff --git a/col.go b/col.go index 8e0294f99b..a496e88f9c 100644 --- a/col.go +++ b/col.go @@ -40,16 +40,6 @@ type Cols struct { sheetXML []byte } -// CurrentCol returns the column number that represents the current column. -func (cols *Cols) CurrentCol() int { - return cols.curCol -} - -// TotalCols returns the total columns count in the worksheet. -func (cols *Cols) TotalCols() int { - return cols.totalCols -} - // GetCols return all the columns in a sheet by given worksheet name (case // sensitive). For example: // diff --git a/col_test.go b/col_test.go index 2dd27dbfb3..e325ed17ce 100644 --- a/col_test.go +++ b/col_test.go @@ -68,8 +68,6 @@ func TestColumnsIterator(t *testing.T) { for cols.Next() { colCount++ - assert.Equal(t, colCount, cols.CurrentCol()) - assert.Equal(t, expectedNumCol, cols.TotalCols()) require.True(t, colCount <= expectedNumCol, "colCount is greater than expected") } assert.Equal(t, expectedNumCol, colCount) @@ -85,8 +83,6 @@ func TestColumnsIterator(t *testing.T) { for cols.Next() { colCount++ - assert.Equal(t, colCount, cols.CurrentCol()) - assert.Equal(t, expectedNumCol, cols.TotalCols()) require.True(t, colCount <= 4, "colCount is greater than expected") } assert.Equal(t, expectedNumCol, colCount) @@ -131,6 +127,11 @@ func TestGetColsError(t *testing.T) { cols.sheetXML = []byte(`A`) _, err = cols.Rows() assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) + + f.Pkg.Store("xl/worksheets/sheet1.xml", nil) + f.Sheet.Store("xl/worksheets/sheet1.xml", nil) + _, err = f.Cols("Sheet1") + assert.NoError(t, err) } func TestColsRows(t *testing.T) { diff --git a/rows.go b/rows.go index 8072079388..8702f93340 100644 --- a/rows.go +++ b/rows.go @@ -79,11 +79,6 @@ type Rows struct { token xml.Token } -// CurrentRow returns the row number that represents the current row. -func (rows *Rows) CurrentRow() int { - return rows.seekRow -} - // Next will return true if find the next row element. func (rows *Rows) Next() bool { rows.seekRow++ diff --git a/rows_test.go b/rows_test.go index 0ac9271755..0d2ca23026 100644 --- a/rows_test.go +++ b/rows_test.go @@ -74,7 +74,6 @@ func TestRowsIterator(t *testing.T) { for rows.Next() { rowCount++ - assert.Equal(t, rowCount, rows.CurrentRow()) require.True(t, rowCount <= expectedNumRow, "rowCount is greater than expected") } assert.Equal(t, expectedNumRow, rowCount) From 3ee3c38f9c63de3782fad21aae9c05ee0530fc32 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 23 Jan 2022 00:32:34 +0800 Subject: [PATCH 530/957] Fix file corrupted in some cases, check file extension and format code Fix file corrupted when save as in XLAM / XLSM / XLTM / XLTX extension in some case New exported error ErrWorkbookExt has been added, and check file extension on save the workbook Format source code with `gofumpt` --- .gitignore | 5 +- README.md | 2 +- README_zh.md | 2 +- adjust_test.go | 6 ++- calc.go | 134 +++++++++++++++++++++++++++++++++++----------- calc_test.go | 11 ++-- cell_test.go | 9 ++-- comment.go | 2 +- crypt.go | 29 +++++----- datavalidation.go | 2 - date.go | 5 +- docProps.go | 7 ++- drawing.go | 9 ++-- errors.go | 3 ++ excelize.go | 11 ++-- excelize_test.go | 10 ++-- file.go | 11 ++++ lib.go | 6 +-- lib_test.go | 1 - picture.go | 9 ++-- picture_test.go | 7 ++- rows.go | 2 +- rows_test.go | 3 +- shape.go | 5 +- sheet.go | 4 +- sheet_test.go | 2 +- sheetpr_test.go | 3 -- sheetview_test.go | 1 - styles.go | 8 +-- templates.go | 8 ++- xmlDrawing.go | 4 ++ xmlPivotCache.go | 45 ++++++---------- xmlPivotTable.go | 9 ++-- 33 files changed, 225 insertions(+), 150 deletions(-) diff --git a/.gitignore b/.gitignore index 68532a7f91..4dce768053 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ ~$*.xlsx -test/Test*.xlsx +test/Test*.xlam test/Test*.xlsm +test/Test*.xlsx +test/Test*.xltm +test/Test*.xltx # generated files test/BadEncrypt.xlsx test/BadWorkbook.SaveAsEmptyStruct.xlsx diff --git a/README.md b/README.md index a0d2d3ddd3..8e16a88b03 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 XLSX / XLSM / 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.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/). ## Basic Usage diff --git a/README_zh.md b/README_zh.md index 919e954be4..dafdd93f1e 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 及以上版本创建的电子表格文档。支持 XLSX / XLSM / 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 等多种文档格式,高度兼容带有样式、图片(表)、透视表、切片器等复杂组件的文档,并提供流式读写 API,用于处理包含大规模数据的工作簿。可应用于各类报表平台、云计算、边缘计算等系统。使用本类库要求使用的 Go 语言为 1.15 或更高版本,完整的 API 使用文档请访问 [go.dev](https://pkg.go.dev/github.com/xuri/excelize/v2) 或查看 [参考文档](https://xuri.me/excelize/)。 ## 快速上手 diff --git a/adjust_test.go b/adjust_test.go index b2ec3c46aa..ab6bedcc06 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -298,9 +298,11 @@ func TestAdjustHelper(t *testing.T) { f := NewFile() f.NewSheet("Sheet2") f.Sheet.Store("xl/worksheets/sheet1.xml", &xlsxWorksheet{ - MergeCells: &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:B1"}}}}) + MergeCells: &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:B1"}}}, + }) f.Sheet.Store("xl/worksheets/sheet2.xml", &xlsxWorksheet{ - AutoFilter: &xlsxAutoFilter{Ref: "A1:B"}}) + AutoFilter: &xlsxAutoFilter{Ref: "A1:B"}, + }) // testing 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()) diff --git a/calc.go b/calc.go index 89eb0879fc..b3c6df8228 100644 --- a/calc.go +++ b/calc.go @@ -1377,7 +1377,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++ { - var matrixRow = []formulaArg{} + matrixRow := []formulaArg{} for col := valueRange[2]; col <= valueRange[3]; col++ { var cell, value string if cell, err = CoordinatesToCellName(col, row); err != nil { @@ -1473,7 +1473,7 @@ func formulaCriteriaParser(exp string) (fc *formulaCriteria) { func formulaCriteriaEval(val string, criteria *formulaCriteria) (result bool, err error) { var value, expected float64 var e error - var prepareValue = func(val, cond string) (value float64, expected float64, err error) { + prepareValue := func(val, cond string) (value float64, expected float64, err error) { if value, err = strconv.ParseFloat(val, 64); err != nil { return } @@ -3385,7 +3385,7 @@ func (fn *formulaFuncs) DECIMAL(argsList *list.List) formulaArg { if argsList.Len() != 2 { return newErrorFormulaArg(formulaErrorVALUE, "DECIMAL requires 2 numeric arguments") } - var text = argsList.Front().Value.(formulaArg).String + text := argsList.Front().Value.(formulaArg).String var radix int var err error radix, err = strconv.Atoi(argsList.Back().Value.(formulaArg).String) @@ -3934,7 +3934,7 @@ func (fn *formulaFuncs) MDETERM(argsList *list.List) (result formulaArg) { return newErrorFormulaArg(formulaErrorVALUE, "MDETERM requires at least 1 argument") } strMtx = argsList.Front().Value.(formulaArg).Matrix - var rows = len(strMtx) + rows := len(strMtx) for _, row := range argsList.Front().Value.(formulaArg).Matrix { if len(row) != rows { return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) @@ -4257,30 +4257,107 @@ type romanNumerals struct { var romanTable = [][]romanNumerals{ { - {1000, "M"}, {900, "CM"}, {500, "D"}, {400, "CD"}, {100, "C"}, {90, "XC"}, - {50, "L"}, {40, "XL"}, {10, "X"}, {9, "IX"}, {5, "V"}, {4, "IV"}, {1, "I"}, + {1000, "M"}, + {900, "CM"}, + {500, "D"}, + {400, "CD"}, + {100, "C"}, + {90, "XC"}, + {50, "L"}, + {40, "XL"}, + {10, "X"}, + {9, "IX"}, + {5, "V"}, + {4, "IV"}, + {1, "I"}, }, { - {1000, "M"}, {950, "LM"}, {900, "CM"}, {500, "D"}, {450, "LD"}, {400, "CD"}, - {100, "C"}, {95, "VC"}, {90, "XC"}, {50, "L"}, {45, "VL"}, {40, "XL"}, - {10, "X"}, {9, "IX"}, {5, "V"}, {4, "IV"}, {1, "I"}, + {1000, "M"}, + {950, "LM"}, + {900, "CM"}, + {500, "D"}, + {450, "LD"}, + {400, "CD"}, + {100, "C"}, + {95, "VC"}, + {90, "XC"}, + {50, "L"}, + {45, "VL"}, + {40, "XL"}, + {10, "X"}, + {9, "IX"}, + {5, "V"}, + {4, "IV"}, + {1, "I"}, }, { - {1000, "M"}, {990, "XM"}, {950, "LM"}, {900, "CM"}, {500, "D"}, {490, "XD"}, - {450, "LD"}, {400, "CD"}, {100, "C"}, {99, "IC"}, {90, "XC"}, {50, "L"}, - {45, "VL"}, {40, "XL"}, {10, "X"}, {9, "IX"}, {5, "V"}, {4, "IV"}, {1, "I"}, + {1000, "M"}, + {990, "XM"}, + {950, "LM"}, + {900, "CM"}, + {500, "D"}, + {490, "XD"}, + {450, "LD"}, + {400, "CD"}, + {100, "C"}, + {99, "IC"}, + {90, "XC"}, + {50, "L"}, + {45, "VL"}, + {40, "XL"}, + {10, "X"}, + {9, "IX"}, + {5, "V"}, + {4, "IV"}, + {1, "I"}, }, { - {1000, "M"}, {995, "VM"}, {990, "XM"}, {950, "LM"}, {900, "CM"}, {500, "D"}, - {495, "VD"}, {490, "XD"}, {450, "LD"}, {400, "CD"}, {100, "C"}, {99, "IC"}, - {90, "XC"}, {50, "L"}, {45, "VL"}, {40, "XL"}, {10, "X"}, {9, "IX"}, - {5, "V"}, {4, "IV"}, {1, "I"}, + {1000, "M"}, + {995, "VM"}, + {990, "XM"}, + {950, "LM"}, + {900, "CM"}, + {500, "D"}, + {495, "VD"}, + {490, "XD"}, + {450, "LD"}, + {400, "CD"}, + {100, "C"}, + {99, "IC"}, + {90, "XC"}, + {50, "L"}, + {45, "VL"}, + {40, "XL"}, + {10, "X"}, + {9, "IX"}, + {5, "V"}, + {4, "IV"}, + {1, "I"}, }, { - {1000, "M"}, {999, "IM"}, {995, "VM"}, {990, "XM"}, {950, "LM"}, {900, "CM"}, - {500, "D"}, {499, "ID"}, {495, "VD"}, {490, "XD"}, {450, "LD"}, {400, "CD"}, - {100, "C"}, {99, "IC"}, {90, "XC"}, {50, "L"}, {45, "VL"}, {40, "XL"}, - {10, "X"}, {9, "IX"}, {5, "V"}, {4, "IV"}, {1, "I"}, + {1000, "M"}, + {999, "IM"}, + {995, "VM"}, + {990, "XM"}, + {950, "LM"}, + {900, "CM"}, + {500, "D"}, + {499, "ID"}, + {495, "VD"}, + {490, "XD"}, + {450, "LD"}, + {400, "CD"}, + {100, "C"}, + {99, "IC"}, + {90, "XC"}, + {50, "L"}, + {45, "VL"}, + {40, "XL"}, + {10, "X"}, + {9, "IX"}, + {5, "V"}, + {4, "IV"}, + {1, "I"}, }, } @@ -4751,8 +4828,8 @@ func (fn *formulaFuncs) SUMIF(argsList *list.List) formulaArg { if argsList.Len() < 2 { return newErrorFormulaArg(formulaErrorVALUE, "SUMIF requires at least 2 arguments") } - var criteria = formulaCriteriaParser(argsList.Front().Next().Value.(formulaArg).String) - var rangeMtx = argsList.Front().Value.(formulaArg).Matrix + criteria := formulaCriteriaParser(argsList.Front().Next().Value.(formulaArg).String) + rangeMtx := argsList.Front().Value.(formulaArg).Matrix var sumRange [][]formulaArg if argsList.Len() == 3 { sumRange = argsList.Back().Value.(formulaArg).Matrix @@ -5886,7 +5963,7 @@ func (fn *formulaFuncs) MEDIAN(argsList *list.List) formulaArg { if argsList.Len() == 0 { return newErrorFormulaArg(formulaErrorVALUE, "MEDIAN requires at least 1 argument") } - var values = []float64{} + values := []float64{} var median, digits float64 var err error for token := argsList.Front(); token != nil; token = token.Next() { @@ -9047,7 +9124,6 @@ func compareFormulaArg(lhs, rhs, matchMode formulaArg, caseSensitive bool) byte if matchPattern(rs, ls) { return criteriaEq } - } return map[int]byte{1: criteriaG, -1: criteriaL, 0: criteriaEq}[strings.Compare(ls, rs)] case ArgEmpty: @@ -9931,9 +10007,8 @@ func (fn *formulaFuncs) ACCRINT(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) } } - cm := newBoolFormulaArg(true) if argsList.Len() == 8 { - if cm = argsList.Back().Value.(formulaArg).ToBool(); cm.Type != ArgNumber { + if cm := argsList.Back().Value.(formulaArg).ToBool(); cm.Type != ArgNumber { return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) } } @@ -10206,10 +10281,8 @@ func coupdays(from, to time.Time, basis int) float64 { date = date.AddDate(0, 13-int(date.Month()), 0) } days += getDaysInMonthRange(toY, int(date.Month()), int(toM)-1, basis) - date = date.AddDate(0, int(toM)-int(date.Month()), 0) } - days += toDay - fromDay - if days > 0 { + if days += toDay - fromDay; days > 0 { return float64(days) } return 0 @@ -11375,8 +11448,7 @@ func (fn *formulaFuncs) ODDFPRICE(argsList *list.List) formulaArg { if args.Type != ArgList { return args } - settlement, maturity, issue, firstCoupon, rate, yld, redemption, frequency, basisArg := - args.List[0], args.List[1], args.List[2], args.List[3], args.List[4], args.List[5], args.List[6], args.List[7], args.List[8] + settlement, maturity, issue, firstCoupon, rate, yld, redemption, frequency, basisArg := args.List[0], args.List[1], args.List[2], args.List[3], args.List[4], args.List[5], args.List[6], args.List[7], args.List[8] if basisArg.Number < 0 || basisArg.Number > 4 { return newErrorFormulaArg(formulaErrorNUM, "invalid basis") } diff --git a/calc_test.go b/calc_test.go index a9899d1650..c3be3080bb 100644 --- a/calc_test.go +++ b/calc_test.go @@ -3386,7 +3386,6 @@ func TestCalcCellValue(t *testing.T) { _, err = f.CalcCellValue("Sheet1", "A1") assert.EqualError(t, err, "not support UNSUPPORT function") assert.NoError(t, f.SaveAs(filepath.Join("test", "TestCalcCellValue.xlsx"))) - } func TestCalculate(t *testing.T) { @@ -3438,7 +3437,6 @@ func TestCalcWithDefinedName(t *testing.T) { result, err = f.CalcCellValue("Sheet1", "D1") assert.NoError(t, err) assert.Equal(t, "YES", result, `=IF("B1_as_string"=defined_name1,"YES","NO")`) - } func TestCalcISBLANK(t *testing.T) { @@ -3748,7 +3746,8 @@ func TestCalcXIRR(t *testing.T) { {25.00, "02/01/2017"}, {8.00, "03/01/2017"}, {15.00, "06/01/2017"}, - {-1e-10, "09/01/2017"}} + {-1e-10, "09/01/2017"}, + } f := prepareCalcData(cellData) formulaList := map[string]string{ "=XIRR(A1:A4,B1:B4)": "-0.196743861298328", @@ -3886,7 +3885,8 @@ func TestCalcXLOOKUP(t *testing.T) { } func TestCalcXNPV(t *testing.T) { - cellData := [][]interface{}{{nil, 0.05}, + cellData := [][]interface{}{ + {nil, 0.05}, {"01/01/2016", -10000, nil}, {"02/01/2016", 2000}, {"05/01/2016", 2400}, @@ -3895,7 +3895,8 @@ func TestCalcXNPV(t *testing.T) { {"01/01/2017", 4100}, {}, {"02/01/2016"}, - {"01/01/2016"}} + {"01/01/2016"}, + } f := prepareCalcData(cellData) formulaList := map[string]string{ "=XNPV(B1,B2:B7,A2:A7)": "4447.938009440515", diff --git a/cell_test.go b/cell_test.go index 8d00e2def2..f6f1098ddc 100644 --- a/cell_test.go +++ b/cell_test.go @@ -30,11 +30,13 @@ func TestConcurrency(t *testing.T) { _, err := f.GetCellValue("Sheet1", fmt.Sprintf("A%d", val)) assert.NoError(t, err) // Concurrency set rows - assert.NoError(t, f.SetSheetRow("Sheet1", "B6", &[]interface{}{" Hello", + 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), uint(1<<32 - 1), uint8(1<<8 - 1), uint16(1<<16 - 1), uint32(1<<32 - 1), - uint64(1<<32 - 1), true, complex64(5 + 10i)})) + uint64(1<<32 - 1), true, complex64(5 + 10i), + })) // Concurrency create style style, err := f.NewStyle(`{"font":{"color":"#1265BE","underline":"single"}}`) assert.NoError(t, err) @@ -384,7 +386,7 @@ func TestGetCellFormula(t *testing.T) { func ExampleFile_SetCellFloat() { f := NewFile() - var x = 3.14159265 + x := 3.14159265 if err := f.SetCellFloat("Sheet1", "A1", x, 2, 64); err != nil { fmt.Println(err) } @@ -534,6 +536,7 @@ func TestGetCellRichText(t *testing.T) { _, err = f.GetCellRichText("Sheet1", "A") assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) } + func TestSetCellRichText(t *testing.T) { f := NewFile() assert.NoError(t, f.SetRowHeight("Sheet1", 1, 35)) diff --git a/comment.go b/comment.go index aa066ec0cc..c0dc33b97f 100644 --- a/comment.go +++ b/comment.go @@ -74,7 +74,7 @@ func (f *File) GetComments() (comments map[string][]Comment) { // getSheetComments provides the method to get the target comment reference by // given worksheet file path. func (f *File) getSheetComments(sheetFile string) string { - var rels = "xl/worksheets/_rels/" + sheetFile + ".rels" + rels := "xl/worksheets/_rels/" + sheetFile + ".rels" if sheetRels := f.relsReader(rels); sheetRels != nil { sheetRels.Lock() defer sheetRels.Unlock() diff --git a/crypt.go b/crypt.go index 91beab290f..9912a6753d 100644 --- a/crypt.go +++ b/crypt.go @@ -168,16 +168,20 @@ func Encrypt(raw []byte, opt *Options) (packageBuf []byte, err error) { 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)}, - }}}, + 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), + }, + }, + }}, }, } @@ -481,7 +485,7 @@ func convertPasswdToKey(passwd string, blockKey []byte, encryption Encryption) ( // hashing data by specified hash algorithm. func hashing(hashAlgorithm string, buffer ...[]byte) (key []byte) { - var hashMap = map[string]hash.Hash{ + hashMap := map[string]hash.Hash{ "md4": md4.New(), "md5": md5.New(), "ripemd-160": ripemd160.New(), @@ -535,8 +539,7 @@ func crypt(encrypt bool, cipherAlgorithm, cipherChaining string, key, iv, input // cryptPackage encrypt / decrypt package by given packageKey and encryption // info. func cryptPackage(encrypt bool, packageKey, input []byte, encryption Encryption) (outputChunks []byte, err error) { - encryptedKey := encryption.KeyData - var offset = packageOffset + encryptedKey, offset := encryption.KeyData, packageOffset if encrypt { offset = 0 } diff --git a/datavalidation.go b/datavalidation.go index c8c9141a12..b8f939b64e 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -199,7 +199,6 @@ func convDataValidationType(t DataValidationType) string { } return typeMap[t] - } // convDataValidationOperator get excel data validation operator. @@ -216,7 +215,6 @@ func convDataValidationOperator(o DataValidationOperator) string { } return typeMap[o] - } // AddDataValidation provides set data validation on a range of the worksheet diff --git a/date.go b/date.go index 9923f9f1a6..04c9110e46 100644 --- a/date.go +++ b/date.go @@ -82,7 +82,6 @@ func shiftJulianToNoon(julianDays, julianFraction float64) (float64, float64) { // minutes, seconds and nanoseconds that comprised a given fraction of a day. // values would round to 1 us. func fractionOfADay(fraction float64) (hours, minutes, seconds, nanoseconds int) { - const ( c1us = 1e3 c1s = 1e9 @@ -137,7 +136,7 @@ func doTheFliegelAndVanFlandernAlgorithm(jd int) (day, month, year int) { // representation (stored as a floating point number) to a time.Time. func timeFromExcelTime(excelTime float64, date1904 bool) time.Time { var date time.Time - var wholeDaysPart = int(excelTime) + wholeDaysPart := int(excelTime) // Excel uses Julian dates prior to March 1st 1900, and Gregorian // thereafter. if wholeDaysPart <= 61 { @@ -152,7 +151,7 @@ func timeFromExcelTime(excelTime float64, date1904 bool) time.Time { } return date } - var floatPart = excelTime - float64(wholeDaysPart) + roundEpsilon + floatPart := excelTime - float64(wholeDaysPart) + roundEpsilon if date1904 { date = excel1904Epoc } else { diff --git a/docProps.go b/docProps.go index 770ed1a7f8..44c30c889d 100644 --- a/docProps.go +++ b/docProps.go @@ -101,7 +101,7 @@ func (f *File) SetAppProps(appProperties *AppProperties) (err error) { // GetAppProps provides a function to get document application properties. func (f *File) GetAppProps() (ret *AppProperties, err error) { - var app = new(xlsxProperties) + 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) @@ -204,8 +204,7 @@ 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 + newProps.Created.Text, newProps.Created.Type, newProps.Modified.Text, newProps.Modified.Type = core.Created.Text, core.Created.Type, core.Modified.Text, core.Modified.Type fields = []string{ "Category", "ContentStatus", "Creator", "Description", "Identifier", "Keywords", "LastModifiedBy", "Revision", "Subject", "Title", "Language", "Version", @@ -230,7 +229,7 @@ func (f *File) SetDocProps(docProperties *DocProperties) (err error) { // GetDocProps provides a function to get document core properties. func (f *File) GetDocProps() (ret *DocProperties, err error) { - var core = new(decodeCoreProperties) + core := new(decodeCoreProperties) if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathDocPropsCore)))). Decode(core); err != nil && err != io.EOF { diff --git a/drawing.go b/drawing.go index be4583ccaf..ac88032e22 100644 --- a/drawing.go +++ b/drawing.go @@ -157,7 +157,8 @@ func (f *File) addChart(formatSet *formatChart, comboCharts []*formatChart) { Cmpd: "sng", Algn: "ctr", SolidFill: &aSolidFill{ - SchemeClr: &aSchemeClr{Val: "tx1", + SchemeClr: &aSchemeClr{ + Val: "tx1", LumMod: &attrValInt{ Val: intPtr(15000), }, @@ -941,7 +942,8 @@ func (f *File) drawChartDLbls(formatSet *formatChart) *cDLbls { func (f *File) drawChartSeriesDLbls(formatSet *formatChart) *cDLbls { dLbls := f.drawChartDLbls(formatSet) chartSeriesDLbls := map[string]*cDLbls{ - Scatter: nil, Surface3D: nil, WireframeSurface3D: nil, Contour: nil, WireframeContour: nil, Bubble: nil, Bubble3D: nil} + Scatter: nil, Surface3D: nil, WireframeSurface3D: nil, Contour: nil, WireframeContour: nil, Bubble: nil, Bubble3D: nil, + } if _, ok := chartSeriesDLbls[formatSet.Type]; ok { return nil } @@ -1194,8 +1196,7 @@ func (f *File) addDrawingChart(sheet, drawingXML, cell string, width, height, rI 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) + colStart, rowStart, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, colIdx, rowIdx, formatSet.OffsetX, formatSet.OffsetY, width, height) content, cNvPrID := f.drawingParser(drawingXML) twoCellAnchor := xdrCellAnchor{} twoCellAnchor.EditAs = formatSet.Positioning diff --git a/errors.go b/errors.go index 8368fee9a3..ebbcef6c27 100644 --- a/errors.go +++ b/errors.go @@ -106,6 +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") // ErrMaxFileNameLength defined the error message on receive the file name // length overflow. ErrMaxFileNameLength = errors.New("file name length exceeds maximum limit") diff --git a/excelize.go b/excelize.go index 6100ac457a..2155f0affb 100644 --- a/excelize.go +++ b/excelize.go @@ -327,7 +327,7 @@ func checkSheetR0(ws *xlsxWorksheet, sheetData *xlsxSheetData, r0 *xlsxRow) { // 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 { - var uniqPart = map[string]string{ + uniqPart := map[string]string{ SourceRelationshipSharedStrings: "/xl/sharedStrings.xml", } rels := f.relsReader(relPath) @@ -434,7 +434,6 @@ func (f *File) AddVBAProject(bin string) error { if path.Ext(bin) != ".bin" { return ErrAddVBAProject } - f.setContentTypePartVBAProjectExtensions() wb := f.relsReader(f.getWorkbookRelsPath()) wb.Lock() defer wb.Unlock() @@ -463,9 +462,9 @@ func (f *File) AddVBAProject(bin string) error { return err } -// setContentTypePartVBAProjectExtensions provides a function to set the -// content type for relationship parts and the main document part. -func (f *File) setContentTypePartVBAProjectExtensions() { +// setContentTypePartProjectExtensions provides a function to set the content +// type for relationship parts and the main document part. +func (f *File) setContentTypePartProjectExtensions(contentType string) { var ok bool content := f.contentTypesReader() content.Lock() @@ -477,7 +476,7 @@ func (f *File) setContentTypePartVBAProjectExtensions() { } for idx, o := range content.Overrides { if o.PartName == "/xl/workbook.xml" { - content.Overrides[idx].ContentType = ContentTypeMacro + content.Overrides[idx].ContentType = contentType } } if !ok { diff --git a/excelize_test.go b/excelize_test.go index c78797d974..1548cc6f5a 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -175,7 +175,10 @@ func TestSaveFile(t *testing.T) { if !assert.NoError(t, err) { t.FailNow() } - assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSaveFile.xlsx"))) + assert.EqualError(t, f.SaveAs(filepath.Join("test", "TestSaveFile.xlsb")), ErrWorkbookExt.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) { @@ -189,7 +192,7 @@ func TestSaveAsWrongPath(t *testing.T) { f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) assert.NoError(t, err) // Test write file to not exist directory. - assert.EqualError(t, f.SaveAs(""), "open .: is a directory") + assert.Error(t, f.SaveAs(filepath.Join("x", "Book1.xlsx"))) assert.NoError(t, f.Close()) } @@ -1305,7 +1308,8 @@ func TestDeleteSheetFromWorkbookRels(t *testing.T) { func TestAttrValToInt(t *testing.T) { _, err := attrValToInt("r", []xml.Attr{ - {Name: xml.Name{Local: "r"}, Value: "s"}}) + {Name: xml.Name{Local: "r"}, Value: "s"}, + }) assert.EqualError(t, err, `strconv.Atoi: parsing "s": invalid syntax`) } diff --git a/file.go b/file.go index 1f2b772ff0..43d94b2fb1 100644 --- a/file.go +++ b/file.go @@ -70,6 +70,17 @@ func (f *File) SaveAs(name string, opt ...Options) error { return ErrMaxFileNameLength } f.Path = name + contentType, ok := map[string]string{ + ".xlam": ContentTypeAddinMacro, + ".xlsm": ContentTypeMacro, + ".xlsx": ContentTypeSheetML, + ".xltm": ContentTypeTemplateMacro, + ".xltx": ContentTypeTemplate, + }[filepath.Ext(f.Path)] + if !ok { + return ErrWorkbookExt + } + f.setContentTypePartProjectExtensions(contentType) file, err := os.OpenFile(filepath.Clean(name), os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0600) if err != nil { return err diff --git a/lib.go b/lib.go index caaeab2f90..a435452aff 100644 --- a/lib.go +++ b/lib.go @@ -490,7 +490,7 @@ func parseFormatSet(formatSet string) []byte { // namespaceStrictToTransitional provides a method to convert Strict and // Transitional namespaces. func namespaceStrictToTransitional(content []byte) []byte { - var namespaceTranslationDic = map[string]string{ + namespaceTranslationDic := map[string]string{ StrictSourceRelationship: SourceRelationship.Value, StrictSourceRelationshipOfficeDocument: SourceRelationshipOfficeDocument, StrictSourceRelationshipChart: SourceRelationshipChart, @@ -611,8 +611,8 @@ 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 { - var oldXmlns = []byte(`xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">`) - var newXmlns = []byte(templateNamespaceIDMap) + oldXmlns := []byte(`xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">`) + newXmlns := []byte(templateNamespaceIDMap) if attr, ok := f.xmlAttr[path]; ok { newXmlns = []byte(genXMLNamespace(attr)) } diff --git a/lib_test.go b/lib_test.go index 3346acc6d6..da75dee5b5 100644 --- a/lib_test.go +++ b/lib_test.go @@ -159,7 +159,6 @@ func TestJoinCellName_Error(t *testing.T) { test(col.Name, row) } } - } func TestCellNameToCoordinates_OK(t *testing.T) { diff --git a/picture.go b/picture.go index 9429f4a97d..f018bd9ae3 100644 --- a/picture.go +++ b/picture.go @@ -209,7 +209,7 @@ func (f *File) deleteSheetRelationships(sheet, rID string) { if !ok { name = strings.ToLower(sheet) + ".xml" } - var rels = "xl/worksheets/_rels/" + strings.TrimPrefix(name, "xl/worksheets/") + ".rels" + rels := "xl/worksheets/_rels/" + strings.TrimPrefix(name, "xl/worksheets/") + ".rels" sheetRels := f.relsReader(rels) if sheetRels == nil { sheetRels = &xlsxRelationships{} @@ -288,8 +288,7 @@ func (f *File) addDrawingPicture(sheet, drawingXML, cell, file string, width, he } 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, formatSet.OffsetX, formatSet.OffsetY, width, height) content, cNvPrID := f.drawingParser(drawingXML) twoCellAnchor := xdrCellAnchor{} twoCellAnchor.EditAs = formatSet.Positioning @@ -372,7 +371,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() { - var imageTypes = map[string]bool{"jpeg": false, "png": false, "gif": false, "tiff": false} + imageTypes := map[string]bool{"jpeg": false, "png": false, "gif": false, "tiff": false} content := f.contentTypesReader() content.Lock() defer content.Unlock() @@ -465,7 +464,7 @@ func (f *File) getSheetRelationshipsTargetByID(sheet, rID string) string { if !ok { name = strings.ToLower(sheet) + ".xml" } - var rels = "xl/worksheets/_rels/" + strings.TrimPrefix(name, "xl/worksheets/") + ".rels" + rels := "xl/worksheets/_rels/" + strings.TrimPrefix(name, "xl/worksheets/") + ".rels" sheetRels := f.relsReader(rels) if sheetRels == nil { sheetRels = &xlsxRelationships{} diff --git a/picture_test.go b/picture_test.go index 5bf139d579..8da7c3d870 100644 --- a/picture_test.go +++ b/picture_test.go @@ -1,19 +1,18 @@ package excelize import ( + "fmt" _ "image/gif" _ "image/jpeg" _ "image/png" - - _ "golang.org/x/image/tiff" - - "fmt" "io/ioutil" "os" "path/filepath" "strings" "testing" + _ "golang.org/x/image/tiff" + "github.com/stretchr/testify/assert" ) diff --git a/rows.go b/rows.go index 8702f93340..88df52c7af 100644 --- a/rows.go +++ b/rows.go @@ -362,7 +362,7 @@ func (f *File) GetRowHeight(sheet string, row int) (float64, error) { if row < 1 { return defaultRowHeightPixels, newInvalidRowNumberError(row) } - var ht = defaultRowHeight + ht := defaultRowHeight ws, err := f.workSheetReader(sheet) if err != nil { return ht, err diff --git a/rows_test.go b/rows_test.go index 0d2ca23026..4d81e6698b 100644 --- a/rows_test.go +++ b/rows_test.go @@ -847,7 +847,8 @@ func TestDuplicateRowTo(t *testing.T) { assert.Equal(t, nil, f.DuplicateRowTo(sheetName, 1, 2)) // Test duplicate row on the worksheet with illegal cell coordinates f.Sheet.Store("xl/worksheets/sheet1.xml", &xlsxWorksheet{ - MergeCells: &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:B1"}}}}) + MergeCells: &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:B1"}}}, + }) 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") diff --git a/shape.go b/shape.go index 974aa5fce8..be3735baeb 100644 --- a/shape.go +++ b/shape.go @@ -351,9 +351,8 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, formatSet *format width := int(float64(formatSet.Width) * formatSet.Format.XScale) height := int(float64(formatSet.Height) * formatSet.Format.YScale) - colStart, rowStart, colEnd, rowEnd, x2, y2 := - f.positionObjectPixels(sheet, colIdx, rowIdx, formatSet.Format.OffsetX, formatSet.Format.OffsetY, - width, height) + colStart, rowStart, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, colIdx, rowIdx, formatSet.Format.OffsetX, formatSet.Format.OffsetY, + width, height) content, cNvPrID := f.drawingParser(drawingXML) twoCellAnchor := xdrCellAnchor{} twoCellAnchor.EditAs = formatSet.Format.Positioning diff --git a/sheet.go b/sheet.go index 2426bf8a51..f10ca07d04 100644 --- a/sheet.go +++ b/sheet.go @@ -368,7 +368,7 @@ func (f *File) SetActiveSheet(index int) { // GetActiveSheetIndex provides a function to get active sheet index of the // spreadsheet. If not found the active sheet will be return integer 0. func (f *File) GetActiveSheetIndex() (index int) { - var sheetID = f.getActiveSheetID() + sheetID := f.getActiveSheetID() wb := f.workbookReader() if wb != nil { for idx, sheet := range wb.Sheets.Sheet { @@ -1723,7 +1723,7 @@ func (f *File) UngroupSheets() error { func (f *File) InsertPageBreak(sheet, cell string) (err error) { var ws *xlsxWorksheet var row, col int - var rowBrk, colBrk = -1, -1 + rowBrk, colBrk := -1, -1 if ws, err = f.workSheetReader(sheet); err != nil { return } diff --git a/sheet_test.go b/sheet_test.go index a5c99a6f16..7df9018b1c 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -123,7 +123,6 @@ 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) @@ -452,6 +451,7 @@ func BenchmarkNewSheet(b *testing.B) { } }) } + func newSheetWithSet() { file := NewFile() file.NewSheet("sheet1") diff --git a/sheetpr_test.go b/sheetpr_test.go index 7c669e863a..91685d8837 100644 --- a/sheetpr_test.go +++ b/sheetpr_test.go @@ -108,7 +108,6 @@ 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) @@ -258,7 +257,6 @@ 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) @@ -395,7 +393,6 @@ 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) diff --git a/sheetview_test.go b/sheetview_test.go index e323e2380c..9207decca3 100644 --- a/sheetview_test.go +++ b/sheetview_test.go @@ -81,7 +81,6 @@ func ExampleFile_SetSheetViewOptions() { // - zoomScale: 80 // Used correct value: // - zoomScale: 123 - } func ExampleFile_GetSheetViewOptions() { diff --git a/styles.go b/styles.go index 261e3df4dd..5c887dbd75 100644 --- a/styles.go +++ b/styles.go @@ -963,7 +963,7 @@ func parseTime(v string, format string) string { goFmt = format if strings.Contains(goFmt, "[") { - var re = regexp.MustCompile(`\[.+\]`) + re := regexp.MustCompile(`\[.+\]`) goFmt = re.ReplaceAllLiteralString(goFmt, "") } @@ -2388,7 +2388,7 @@ func getFillID(styleSheet *xlsxStyleSheet, style *Style) (fillID int) { // newFills provides a function to add fill elements in the styles.xml by // given cell format settings. func newFills(style *Style, fg bool) *xlsxFill { - var patterns = []string{ + patterns := []string{ "none", "solid", "mediumGray", @@ -2410,7 +2410,7 @@ func newFills(style *Style, fg bool) *xlsxFill { "gray0625", } - var variants = []float64{ + variants := []float64{ 90, 0, 45, @@ -2522,7 +2522,7 @@ func getBorderID(styleSheet *xlsxStyleSheet, style *Style) (borderID int) { // newBorders provides a function to add border elements in the styles.xml by // given borders format settings. func newBorders(style *Style) *xlsxBorder { - var styles = []string{ + styles := []string{ "none", "thin", "medium", diff --git a/templates.go b/templates.go index 81b2c208ca..d08f45ffd7 100644 --- a/templates.go +++ b/templates.go @@ -16,11 +16,9 @@ package excelize import "encoding/xml" -var ( - // XMLHeaderByte define an XML declaration can also contain a standalone - // declaration. - XMLHeaderByte = []byte(xml.Header) -) +// XMLHeaderByte define an XML declaration can also contain a standalone +// declaration. +var XMLHeaderByte = []byte(xml.Header) const ( defaultXMLPathContentTypes = "[Content_Types].xml" diff --git a/xmlDrawing.go b/xmlDrawing.go index f51451bfe2..61c25ca810 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -65,7 +65,11 @@ const ( NameSpaceDublinCoreMetadataInitiative = "http://purl.org/dc/dcmitype/" 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" ContentTypeSpreadSheetMLChartsheet = "application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml" ContentTypeSpreadSheetMLComments = "application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml" ContentTypeSpreadSheetMLPivotCacheDefinition = "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml" diff --git a/xmlPivotCache.go b/xmlPivotCache.go index 7f3dac0a3a..9591858384 100644 --- a/xmlPivotCache.go +++ b/xmlPivotCache.go @@ -79,8 +79,7 @@ type xlsxWorksheetSource struct { // PivotTable is a collection of ranges in the workbook. The ranges are // specified in the rangeSets collection. The logic for how the application // consolidates the data in the ranges is application- defined. -type xlsxConsolidation struct { -} +type xlsxConsolidation struct{} // xlsxCacheFields represents the collection of field definitions in the // source data. @@ -144,8 +143,7 @@ type xlsxSharedItems struct { } // xlsxMissing represents a value that was not specified. -type xlsxMissing struct { -} +type xlsxMissing struct{} // xlsxNumber represents a numeric value in the PivotTable. type xlsxNumber struct { @@ -167,18 +165,15 @@ type xlsxNumber struct { // xlsxTuples represents members for the OLAP sheet data entry, also known as // a tuple. -type xlsxTuples struct { -} +type xlsxTuples struct{} // xlsxBoolean represents a boolean value for an item in the PivotTable. -type xlsxBoolean struct { -} +type xlsxBoolean struct{} // xlsxError represents an error value. The use of this item indicates that an // error value is present in the PivotTable source. The error is recorded in // the value attribute. -type xlsxError struct { -} +type xlsxError struct{} // xlsxString represents a character value in a PivotTable. type xlsxString struct { @@ -199,45 +194,35 @@ type xlsxString struct { } // xlsxDateTime represents a date-time value in the PivotTable. -type xlsxDateTime struct { -} +type xlsxDateTime struct{} // xlsxFieldGroup represents the collection of properties for a field group. -type xlsxFieldGroup struct { -} +type xlsxFieldGroup struct{} // xlsxCacheHierarchies represents the collection of OLAP hierarchies in the // PivotCache. -type xlsxCacheHierarchies struct { -} +type xlsxCacheHierarchies struct{} // xlsxKpis represents the collection of Key Performance Indicators (KPIs) // defined on the OLAP server and stored in the PivotCache. -type xlsxKpis struct { -} +type xlsxKpis struct{} // xlsxTupleCache represents the cache of OLAP sheet data members, or tuples. -type xlsxTupleCache struct { -} +type xlsxTupleCache struct{} // xlsxCalculatedItems represents the collection of calculated items. -type xlsxCalculatedItems struct { -} +type xlsxCalculatedItems struct{} // xlsxCalculatedMembers represents the collection of calculated members in an // OLAP PivotTable. -type xlsxCalculatedMembers struct { -} +type xlsxCalculatedMembers struct{} // xlsxDimensions represents the collection of PivotTable OLAP dimensions. -type xlsxDimensions struct { -} +type xlsxDimensions struct{} // xlsxMeasureGroups represents the collection of PivotTable OLAP measure // groups. -type xlsxMeasureGroups struct { -} +type xlsxMeasureGroups struct{} // xlsxMaps represents the PivotTable OLAP measure group - Dimension maps. -type xlsxMaps struct { -} +type xlsxMaps struct{} diff --git a/xmlPivotTable.go b/xmlPivotTable.go index 38dfb1e487..a7543a747e 100644 --- a/xmlPivotTable.go +++ b/xmlPivotTable.go @@ -195,8 +195,7 @@ type xlsxItem struct { } // xlsxAutoSortScope represents the sorting scope for the PivotTable. -type xlsxAutoSortScope struct { -} +type xlsxAutoSortScope struct{} // xlsxRowFields represents the collection of row fields for the PivotTable. type xlsxRowFields struct { @@ -225,8 +224,7 @@ type xlsxI struct { } // xlsxX represents an array of indexes to cached shared item values. -type xlsxX struct { -} +type xlsxX struct{} // xlsxColFields represents the collection of fields that are on the column // axis of the PivotTable. @@ -281,8 +279,7 @@ type xlsxDataField struct { // xlsxConditionalFormats represents the collection of conditional formats // applied to a PivotTable. -type xlsxConditionalFormats struct { -} +type xlsxConditionalFormats struct{} // xlsxPivotTableStyleInfo represent information on style applied to the // PivotTable. From 156bf6d16ecbd5257d81e781138eaaaf357ffbec Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 27 Jan 2022 22:37:32 +0800 Subject: [PATCH 531/957] This closes #1129, make cell support inheritance columns/rows style Correct cells style in merge range Fix incorrect style ID returned on getting cell style in some cases Unit test updated and simplified code --- cell.go | 52 +++++++++++++++++++++++++++--------------------- col.go | 11 +++++----- col_test.go | 20 ++++++++++++------- excelize_test.go | 1 + merge.go | 7 +++++-- rows_test.go | 3 +++ styles.go | 4 ++-- 7 files changed, 58 insertions(+), 40 deletions(-) diff --git a/cell.go b/cell.go index ff9a131f80..35060ab4e4 100644 --- a/cell.go +++ b/cell.go @@ -200,12 +200,12 @@ func (f *File) setCellTimeFunc(sheet, axis string, value time.Time) error { if err != nil { return err } - cellData, col, _, err := f.prepareCell(ws, sheet, axis) + cellData, col, row, err := f.prepareCell(ws, sheet, axis) if err != nil { return err } ws.Lock() - cellData.S = f.prepareCellStyle(ws, col, cellData.S) + cellData.S = f.prepareCellStyle(ws, col, row, cellData.S) ws.Unlock() var isNum bool @@ -214,10 +214,7 @@ func (f *File) setCellTimeFunc(sheet, axis string, value time.Time) error { return err } if isNum { - err = f.setDefaultTimeStyle(sheet, axis, 22) - if err != nil { - return err - } + _ = f.setDefaultTimeStyle(sheet, axis, 22) } return err } @@ -254,13 +251,13 @@ func (f *File) SetCellInt(sheet, axis string, value int) error { if err != nil { return err } - cellData, col, _, err := f.prepareCell(ws, sheet, axis) + cellData, col, row, err := f.prepareCell(ws, sheet, axis) if err != nil { return err } ws.Lock() defer ws.Unlock() - cellData.S = f.prepareCellStyle(ws, col, cellData.S) + cellData.S = f.prepareCellStyle(ws, col, row, cellData.S) cellData.T, cellData.V = setCellInt(value) return err } @@ -279,13 +276,13 @@ func (f *File) SetCellBool(sheet, axis string, value bool) error { if err != nil { return err } - cellData, col, _, err := f.prepareCell(ws, sheet, axis) + cellData, col, row, err := f.prepareCell(ws, sheet, axis) if err != nil { return err } ws.Lock() defer ws.Unlock() - cellData.S = f.prepareCellStyle(ws, col, cellData.S) + cellData.S = f.prepareCellStyle(ws, col, row, cellData.S) cellData.T, cellData.V = setCellBool(value) return err } @@ -316,13 +313,13 @@ func (f *File) SetCellFloat(sheet, axis string, value float64, prec, bitSize int if err != nil { return err } - cellData, col, _, err := f.prepareCell(ws, sheet, axis) + cellData, col, row, err := f.prepareCell(ws, sheet, axis) if err != nil { return err } ws.Lock() defer ws.Unlock() - cellData.S = f.prepareCellStyle(ws, col, cellData.S) + cellData.S = f.prepareCellStyle(ws, col, row, cellData.S) cellData.T, cellData.V = setCellFloat(value, prec, bitSize) return err } @@ -341,13 +338,13 @@ func (f *File) SetCellStr(sheet, axis, value string) error { if err != nil { return err } - cellData, col, _, err := f.prepareCell(ws, sheet, axis) + cellData, col, row, err := f.prepareCell(ws, sheet, axis) if err != nil { return err } ws.Lock() defer ws.Unlock() - cellData.S = f.prepareCellStyle(ws, col, cellData.S) + cellData.S = f.prepareCellStyle(ws, col, row, cellData.S) cellData.T, cellData.V, err = f.setCellString(value) return err } @@ -439,13 +436,13 @@ func (f *File) SetCellDefault(sheet, axis, value string) error { if err != nil { return err } - cellData, col, _, err := f.prepareCell(ws, sheet, axis) + cellData, col, row, err := f.prepareCell(ws, sheet, axis) if err != nil { return err } ws.Lock() defer ws.Unlock() - cellData.S = f.prepareCellStyle(ws, col, cellData.S) + cellData.S = f.prepareCellStyle(ws, col, row, cellData.S) cellData.T, cellData.V = setCellDefault(value) return err } @@ -937,14 +934,14 @@ func (f *File) SetCellRichText(sheet, cell string, runs []RichTextRun) error { if err != nil { return err } - cellData, col, _, err := f.prepareCell(ws, sheet, cell) + cellData, col, row, err := f.prepareCell(ws, sheet, cell) if err != nil { return err } if err := f.sharedStringsLoader(); err != nil { return err } - cellData.S = f.prepareCellStyle(ws, col, cellData.S) + cellData.S = f.prepareCellStyle(ws, col, row, cellData.S) si := xlsxSI{} sst := f.sharedStringsReader() textRuns := []xlsxR{} @@ -1133,14 +1130,19 @@ func isTimeNumFmt(format string) bool { // 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, style int) int { +func (f *File) prepareCellStyle(ws *xlsxWorksheet, col, row, style int) int { if ws.Cols != nil && style == 0 { for _, c := range ws.Cols.Col { - if c.Min <= col && col <= c.Max { - style = c.Style + 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 { + return styleID + } + } return style } @@ -1150,6 +1152,11 @@ func (f *File) mergeCellsParser(ws *xlsxWorksheet, axis string) (string, error) axis = strings.ToUpper(axis) if ws.MergeCells != nil { for i := 0; i < len(ws.MergeCells.Cells); i++ { + if ws.MergeCells.Cells[i] == nil { + ws.MergeCells.Cells = append(ws.MergeCells.Cells[:i], ws.MergeCells.Cells[i+1:]...) + i-- + continue + } ok, err := f.checkCellInArea(axis, ws.MergeCells.Cells[i].Ref) if err != nil { return axis, err @@ -1170,8 +1177,7 @@ func (f *File) checkCellInArea(cell, area string) (bool, error) { return false, err } - rng := strings.Split(area, ":") - if len(rng) != 2 { + if rng := strings.Split(area, ":"); len(rng) != 2 { return false, err } coordinates, err := areaRefToCoordinates(area) diff --git a/col.go b/col.go index a496e88f9c..16d1f93c12 100644 --- a/col.go +++ b/col.go @@ -592,9 +592,8 @@ func (f *File) positionObjectPixels(sheet string, col, row, x1, y1, width, heigh row++ } - // Initialise end cell to the same as the start cell. - colEnd := col - rowEnd := row + // Initialized end cell to the same as the start cell. + colEnd, rowEnd := col, row width += x1 height += y1 @@ -632,7 +631,7 @@ func (f *File) getColWidth(sheet string, col int) int { return int(convertColWidthToPixels(width)) } } - // Optimisation for when the column widths haven't changed. + // Optimization for when the column widths haven't changed. return int(defaultColWidthPixels) } @@ -658,7 +657,7 @@ func (f *File) GetColWidth(sheet, col string) (float64, error) { return width, err } } - // Optimisation for when the column widths haven't changed. + // Optimization for when the column widths haven't changed. return defaultColWidth, err } @@ -707,7 +706,7 @@ func (f *File) RemoveCol(sheet, col string) error { return f.adjustHelper(sheet, columns, num, -1) } -// convertColWidthToPixels provieds function to convert the width of a cell +// convertColWidthToPixels provides function to convert the width of a cell // from user's units to pixels. Excel rounds the column width to the nearest // pixel. If the width hasn't been set by the user we use the default value. // If the column is hidden it has a value of zero. diff --git a/col_test.go b/col_test.go index e325ed17ce..df74523a27 100644 --- a/col_test.go +++ b/col_test.go @@ -289,18 +289,24 @@ func TestOutlineLevel(t *testing.T) { func TestSetColStyle(t *testing.T) { f := NewFile() assert.NoError(t, f.SetCellValue("Sheet1", "B2", "Hello")) - style, err := f.NewStyle(`{"fill":{"type":"pattern","color":["#94d3a2"],"pattern":1}}`) + 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", style), "sheet SheetN is not exist") + assert.EqualError(t, f.SetColStyle("SheetN", "E", styleID), "sheet SheetN is not exist") // Test set column style with illegal cell coordinates. - assert.EqualError(t, f.SetColStyle("Sheet1", "*", style), newInvalidColumnNameError("*").Error()) - assert.EqualError(t, f.SetColStyle("Sheet1", "A:*", style), newInvalidColumnNameError("*").Error()) + assert.EqualError(t, f.SetColStyle("Sheet1", "*", styleID), newInvalidColumnNameError("*").Error()) + assert.EqualError(t, f.SetColStyle("Sheet1", "A:*", styleID), newInvalidColumnNameError("*").Error()) - assert.NoError(t, f.SetColStyle("Sheet1", "B", style)) + assert.NoError(t, f.SetColStyle("Sheet1", "B", styleID)) // Test set column style with already exists column with style. - assert.NoError(t, f.SetColStyle("Sheet1", "B", style)) - assert.NoError(t, f.SetColStyle("Sheet1", "D:C", 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") + assert.True(t, ok) + ws.(*xlsxWorksheet).SheetData.Row[1].C[2].S = 0 + cellStyleID, err := f.GetCellStyle("Sheet1", "C2") + assert.NoError(t, err) + assert.Equal(t, styleID, cellStyleID) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetColStyle.xlsx"))) } diff --git a/excelize_test.go b/excelize_test.go index 1548cc6f5a..bafd446247 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -147,6 +147,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") // 02:46:40 assert.NoError(t, f.SetCellValue("Sheet2", "G5", time.Duration(1e13))) // Test completion column. diff --git a/merge.go b/merge.go index 7119d28632..3ba7d6ad0d 100644 --- a/merge.go +++ b/merge.go @@ -11,7 +11,9 @@ package excelize -import "strings" +import ( + "strings" +) // Rect gets merged cell rectangle coordinates sequence. func (mc *xlsxMergeCell) Rect() ([]int, error) { @@ -68,7 +70,8 @@ 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) - return err + styleID, _ := f.GetCellStyle(sheet, hCell) + return f.SetCellStyle(sheet, hCell, vCell, styleID) } // UnmergeCell provides a function to unmerge a given coordinate area. diff --git a/rows_test.go b/rows_test.go index 4d81e6698b..1286a3773d 100644 --- a/rows_test.go +++ b/rows_test.go @@ -921,6 +921,9 @@ func TestSetRowStyle(t *testing.T) { 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)) + cellStyleID, err := f.GetCellStyle("Sheet1", "B2") + assert.NoError(t, err) + assert.Equal(t, styleID, cellStyleID) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetRowStyle.xlsx"))) } diff --git a/styles.go b/styles.go index 5c887dbd75..5d373d38df 100644 --- a/styles.go +++ b/styles.go @@ -2613,11 +2613,11 @@ func (f *File) GetCellStyle(sheet, axis string) (int, error) { if err != nil { return 0, err } - cellData, col, _, err := f.prepareCell(ws, sheet, axis) + cellData, col, row, err := f.prepareCell(ws, sheet, axis) if err != nil { return 0, err } - return f.prepareCellStyle(ws, col, cellData.S), err + return f.prepareCellStyle(ws, col, row, cellData.S), err } // SetCellStyle provides a function to add style attribute for cells by given From 862dc9dc1324e11a517b03267d07dd95051d5e6e Mon Sep 17 00:00:00 2001 From: David Date: Fri, 4 Feb 2022 01:45:42 -0400 Subject: [PATCH 532/957] Support workbook views settings (#1136) --- sheetview.go | 14 +++++++++++++- sheetview_test.go | 3 +++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/sheetview.go b/sheetview.go index 7184a7a635..0fb955dd17 100644 --- a/sheetview.go +++ b/sheetview.go @@ -58,7 +58,11 @@ type ( // 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 - + // View is a SheetViewOption. It specifies a flag indicating + // how sheet is displayed, by default it uses empty string + // available options: pageLayout, pageBreakPreview + View string + /* TODO // ShowWhiteSpace is a SheetViewOption. It specifies a flag indicating // whether page layout view shall display margins. False means do not display @@ -80,6 +84,14 @@ func (o *TopLeftCell) getSheetViewOption(view *xlsxSheetView) { *o = TopLeftCell(string(view.TopLeftCell)) } +func (o View) setSheetViewOption(view *xlsxSheetView) { + view.View = string(o) +} + +func (o *View) getSheetViewOption(view *xlsxSheetView) { + *o = View(string(view.View)) +} + func (o DefaultGridColor) setSheetViewOption(view *xlsxSheetView) { view.DefaultGridColor = boolPtr(bool(o)) } diff --git a/sheetview_test.go b/sheetview_test.go index 9207decca3..6eb206eebc 100644 --- a/sheetview_test.go +++ b/sheetview_test.go @@ -14,6 +14,7 @@ var _ = []SheetViewOption{ ShowGridLines(true), ShowRowColHeaders(true), TopLeftCell("B2"), + View("pageLayout"), // SheetViewOptionPtr are also SheetViewOption new(DefaultGridColor), new(RightToLeft), @@ -30,6 +31,7 @@ var _ = []SheetViewOptionPtr{ (*ShowGridLines)(nil), (*ShowRowColHeaders)(nil), (*TopLeftCell)(nil), + (*View)(nil), } func ExampleFile_SetSheetViewOptions() { @@ -44,6 +46,7 @@ func ExampleFile_SetSheetViewOptions() { ShowRowColHeaders(true), ZoomScale(80), TopLeftCell("C3"), + View("pageLayout"), ); err != nil { fmt.Println(err) } From 0f1fcb78d5518695cb80cc2266290e37df38a1ee Mon Sep 17 00:00:00 2001 From: David Date: Sun, 6 Feb 2022 09:52:28 -0400 Subject: [PATCH 533/957] Support workbook views Showruler settings (#1138) --- sheetview.go | 15 +++++++++++++-- sheetview_test.go | 3 +++ xmlWorksheet.go | 1 + 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/sheetview.go b/sheetview.go index 0fb955dd17..5bb5aaff75 100644 --- a/sheetview.go +++ b/sheetview.go @@ -60,9 +60,12 @@ type ( ShowZeros bool // View is a SheetViewOption. It specifies a flag indicating // how sheet is displayed, by default it uses empty string - // available options: pageLayout, pageBreakPreview + // available options: normal, pageLayout, pageBreakPreview View string - + // ShowRuler is a SheetViewOption. It specifies a flag indicating + // this sheet should display ruler. + ShowRuler bool + /* TODO // ShowWhiteSpace is a SheetViewOption. It specifies a flag indicating // whether page layout view shall display margins. False means do not display @@ -124,6 +127,14 @@ func (o *ShowGridLines) getSheetViewOption(view *xlsxSheetView) { *o = ShowGridLines(defaultTrue(view.ShowGridLines)) // Excel default: true } +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 ShowZeros) setSheetViewOption(view *xlsxSheetView) { view.ShowZeros = boolPtr(bool(o)) } diff --git a/sheetview_test.go b/sheetview_test.go index 6eb206eebc..cfe628d716 100644 --- a/sheetview_test.go +++ b/sheetview_test.go @@ -15,6 +15,7 @@ var _ = []SheetViewOption{ ShowRowColHeaders(true), TopLeftCell("B2"), View("pageLayout"), + ShowRuler(false), // SheetViewOptionPtr are also SheetViewOption new(DefaultGridColor), new(RightToLeft), @@ -32,6 +33,7 @@ var _ = []SheetViewOptionPtr{ (*ShowRowColHeaders)(nil), (*TopLeftCell)(nil), (*View)(nil), + (*ShowRuler)(nil), } func ExampleFile_SetSheetViewOptions() { @@ -47,6 +49,7 @@ func ExampleFile_SetSheetViewOptions() { ZoomScale(80), TopLeftCell("C3"), View("pageLayout"), + ShowRuler(false), ); err != nil { fmt.Println(err) } diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 76c61884b5..83dd1ba8de 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -191,6 +191,7 @@ type xlsxSheetView struct { ShowZeros *bool `xml:"showZeros,attr,omitempty"` RightToLeft bool `xml:"rightToLeft,attr,omitempty"` TabSelected bool `xml:"tabSelected,attr,omitempty"` + ShowRuler *bool `xml:"showRuler,attr,omitempty"` ShowWhiteSpace *bool `xml:"showWhiteSpace,attr"` ShowOutlineSymbols bool `xml:"showOutlineSymbols,attr,omitempty"` DefaultGridColor *bool `xml:"defaultGridColor,attr"` From 3f8f4f52e68d408da5a2e5108af3cc99bf8586bc Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 8 Feb 2022 00:08:06 +0800 Subject: [PATCH 534/957] This closes #1139, `SetCellDefault` support non-numeric value - Add default value on getting `View` property of sheet views - Add examples and unit test for set sheet views - Re-order field on sheet view options - Fix incorrect build-in number format: 42 - Simplify code for the `stylesReader` function --- cell.go | 3 + sheetview.go | 153 +++++++++++++++++++++++----------------------- sheetview_test.go | 84 ++++++++++++++++--------- styles.go | 7 +-- 4 files changed, 136 insertions(+), 111 deletions(-) diff --git a/cell.go b/cell.go index 35060ab4e4..9af93f6356 100644 --- a/cell.go +++ b/cell.go @@ -450,6 +450,9 @@ func (f *File) SetCellDefault(sheet, axis, 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 { + t = "str" + } v = value return } diff --git a/sheetview.go b/sheetview.go index 5bb5aaff75..b0912ec7a7 100644 --- a/sheetview.go +++ b/sheetview.go @@ -27,74 +27,49 @@ type SheetViewOptionPtr interface { } 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 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 - // 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 - // ShowFormulas is a SheetViewOption. It specifies a flag indicating whether - // this sheet should display formulas. + // 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 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 - // 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 - // 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 - // 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 + // 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 - // 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 - // ShowRuler is a SheetViewOption. It specifies a flag indicating - // this sheet should display ruler. + // 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 - - /* TODO - // ShowWhiteSpace is a SheetViewOption. It specifies a flag indicating - // whether page layout view shall display margins. False means do not display - // left, right, top (header), and bottom (footer) margins (even when there is - // data in the header or footer). - ShowWhiteSpace bool - // WindowProtection is a SheetViewOption. - WindowProtection 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 TopLeftCell) setSheetViewOption(view *xlsxSheetView) { - view.TopLeftCell = string(o) -} - -func (o *TopLeftCell) getSheetViewOption(view *xlsxSheetView) { - *o = TopLeftCell(string(view.TopLeftCell)) -} - -func (o View) setSheetViewOption(view *xlsxSheetView) { - view.View = string(o) -} - -func (o *View) getSheetViewOption(view *xlsxSheetView) { - *o = View(string(view.View)) -} - func (o DefaultGridColor) setSheetViewOption(view *xlsxSheetView) { view.DefaultGridColor = boolPtr(bool(o)) } @@ -103,14 +78,6 @@ func (o *DefaultGridColor) getSheetViewOption(view *xlsxSheetView) { *o = DefaultGridColor(defaultTrue(view.DefaultGridColor)) // 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 ShowFormulas) setSheetViewOption(view *xlsxSheetView) { view.ShowFormulas = bool(o) // Excel default: false } @@ -127,12 +94,12 @@ func (o *ShowGridLines) getSheetViewOption(view *xlsxSheetView) { *o = ShowGridLines(defaultTrue(view.ShowGridLines)) // Excel default: true } -func (o ShowRuler) setSheetViewOption(view *xlsxSheetView) { - view.ShowRuler = boolPtr(bool(o)) +func (o ShowRowColHeaders) setSheetViewOption(view *xlsxSheetView) { + view.ShowRowColHeaders = boolPtr(bool(o)) } -func (o *ShowRuler) getSheetViewOption(view *xlsxSheetView) { - *o = ShowRuler(defaultTrue(view.ShowRuler)) // Excel default: true +func (o *ShowRowColHeaders) getSheetViewOption(view *xlsxSheetView) { + *o = ShowRowColHeaders(defaultTrue(view.ShowRowColHeaders)) // Excel default: true } func (o ShowZeros) setSheetViewOption(view *xlsxSheetView) { @@ -143,12 +110,40 @@ func (o *ShowZeros) getSheetViewOption(view *xlsxSheetView) { *o = ShowZeros(defaultTrue(view.ShowZeros)) // Excel default: true } -func (o ShowRowColHeaders) setSheetViewOption(view *xlsxSheetView) { - view.ShowRowColHeaders = boolPtr(bool(o)) +func (o RightToLeft) setSheetViewOption(view *xlsxSheetView) { + view.RightToLeft = bool(o) // Excel default: false } -func (o *ShowRowColHeaders) getSheetViewOption(view *xlsxSheetView) { - *o = ShowRowColHeaders(defaultTrue(view.ShowRowColHeaders)) // Excel default: true +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 = View("normal") +} + +func (o TopLeftCell) setSheetViewOption(view *xlsxSheetView) { + view.TopLeftCell = string(o) +} + +func (o *TopLeftCell) getSheetViewOption(view *xlsxSheetView) { + *o = TopLeftCell(string(view.TopLeftCell)) } func (o ZoomScale) setSheetViewOption(view *xlsxSheetView) { @@ -186,13 +181,15 @@ func (f *File) getSheetView(sheet string, viewIndex int) (*xlsxSheetView, error) // Available options: // // DefaultGridColor(bool) -// RightToLeft(bool) // ShowFormulas(bool) // ShowGridLines(bool) // ShowRowColHeaders(bool) -// ZoomScale(float64) -// TopLeftCell(string) // ShowZeros(bool) +// RightToLeft(bool) +// ShowRuler(bool) +// View(string) +// TopLeftCell(string) +// ZoomScale(float64) // // Example: // @@ -216,13 +213,15 @@ func (f *File) SetSheetViewOptions(name string, viewIndex int, opts ...SheetView // Available options: // // DefaultGridColor(bool) -// RightToLeft(bool) // ShowFormulas(bool) // ShowGridLines(bool) // ShowRowColHeaders(bool) -// ZoomScale(float64) -// TopLeftCell(string) // ShowZeros(bool) +// RightToLeft(bool) +// ShowRuler(bool) +// View(string) +// TopLeftCell(string) +// ZoomScale(float64) // // Example: // diff --git a/sheetview_test.go b/sheetview_test.go index cfe628d716..2bba8f9802 100644 --- a/sheetview_test.go +++ b/sheetview_test.go @@ -9,31 +9,39 @@ import ( var _ = []SheetViewOption{ DefaultGridColor(true), - RightToLeft(false), ShowFormulas(false), ShowGridLines(true), ShowRowColHeaders(true), - TopLeftCell("B2"), - View("pageLayout"), + ShowZeros(true), + RightToLeft(false), ShowRuler(false), + View("pageLayout"), + TopLeftCell("B2"), + ZoomScale(100), // SheetViewOptionPtr are also SheetViewOption new(DefaultGridColor), - new(RightToLeft), new(ShowFormulas), new(ShowGridLines), new(ShowRowColHeaders), + new(ShowZeros), + new(RightToLeft), + new(ShowRuler), + new(View), new(TopLeftCell), + new(ZoomScale), } var _ = []SheetViewOptionPtr{ (*DefaultGridColor)(nil), - (*RightToLeft)(nil), (*ShowFormulas)(nil), (*ShowGridLines)(nil), (*ShowRowColHeaders)(nil), - (*TopLeftCell)(nil), - (*View)(nil), + (*ShowZeros)(nil), + (*RightToLeft)(nil), (*ShowRuler)(nil), + (*View)(nil), + (*TopLeftCell)(nil), + (*ZoomScale)(nil), } func ExampleFile_SetSheetViewOptions() { @@ -42,14 +50,14 @@ func ExampleFile_SetSheetViewOptions() { if err := f.SetSheetViewOptions(sheet, 0, DefaultGridColor(false), - RightToLeft(false), ShowFormulas(true), ShowGridLines(true), ShowRowColHeaders(true), - ZoomScale(80), - TopLeftCell("C3"), - View("pageLayout"), + RightToLeft(false), ShowRuler(false), + View("pageLayout"), + TopLeftCell("C3"), + ZoomScale(80), ); err != nil { fmt.Println(err) } @@ -95,80 +103,98 @@ func ExampleFile_GetSheetViewOptions() { var ( defaultGridColor DefaultGridColor - rightToLeft RightToLeft showFormulas ShowFormulas showGridLines ShowGridLines - showZeros ShowZeros showRowColHeaders ShowRowColHeaders - zoomScale ZoomScale + showZeros ShowZeros + rightToLeft RightToLeft + showRuler ShowRuler + view View topLeftCell TopLeftCell + zoomScale ZoomScale ) if err := f.GetSheetViewOptions(sheet, 0, &defaultGridColor, - &rightToLeft, &showFormulas, &showGridLines, - &showZeros, &showRowColHeaders, - &zoomScale, + &showZeros, + &rightToLeft, + &showRuler, + &view, &topLeftCell, + &zoomScale, ); err != nil { fmt.Println(err) } fmt.Println("Default:") fmt.Println("- defaultGridColor:", defaultGridColor) - fmt.Println("- rightToLeft:", rightToLeft) fmt.Println("- showFormulas:", showFormulas) fmt.Println("- showGridLines:", showGridLines) - fmt.Println("- showZeros:", showZeros) fmt.Println("- showRowColHeaders:", showRowColHeaders) - fmt.Println("- zoomScale:", zoomScale) + 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, TopLeftCell("B2")); err != nil { + if err := f.SetSheetViewOptions(sheet, 0, ShowGridLines(false)); err != nil { fmt.Println(err) } - if err := f.GetSheetViewOptions(sheet, 0, &topLeftCell); err != nil { + if err := f.GetSheetViewOptions(sheet, 0, &showGridLines); err != nil { fmt.Println(err) } - if err := f.SetSheetViewOptions(sheet, 0, ShowGridLines(false)); err != nil { + if err := f.SetSheetViewOptions(sheet, 0, ShowZeros(false)); err != nil { fmt.Println(err) } - if err := f.GetSheetViewOptions(sheet, 0, &showGridLines); err != nil { + if err := f.GetSheetViewOptions(sheet, 0, &showZeros); err != nil { fmt.Println(err) } - if err := f.SetSheetViewOptions(sheet, 0, ShowZeros(false)); err != nil { + if err := f.SetSheetViewOptions(sheet, 0, View("pageLayout")); err != nil { fmt.Println(err) } - if err := f.GetSheetViewOptions(sheet, 0, &showZeros); err != nil { + 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 - // - rightToLeft: false // - showFormulas: false // - showGridLines: true - // - showZeros: true // - showRowColHeaders: true - // - zoomScale: 0 + // - showZeros: true + // - rightToLeft: false + // - showRuler: true + // - view: normal // - topLeftCell: "" + // - zoomScale: 0 // After change: // - showGridLines: false // - showZeros: false + // - view: pageLayout // - topLeftCell: B2 } diff --git a/styles.go b/styles.go index 5d373d38df..7678b847b3 100644 --- a/styles.go +++ b/styles.go @@ -53,7 +53,7 @@ var builtInNumFmt = map[int]string{ 39: "#,##0.00;(#,##0.00)", 40: "#,##0.00;[red](#,##0.00)", 41: `_(* #,##0_);_(* \(#,##0\);_(* "-"_);_(@_)`, - 42: `_("$"* #,##0_);_("$* \(#,##0\);_("$"* "-"_);_(@_)`, + 42: `_("$"* #,##0_);_("$"* \(#,##0\);_("$"* "-"_);_(@_)`, 43: `_(* #,##0.00_);_(* \(#,##0.00\);_(* "-"??_);_(@_)`, 44: `_("$"* #,##0.00_);_("$"* \(#,##0.00\);_("$"* "-"??_);_(@_)`, 45: "mm:ss", @@ -1074,16 +1074,13 @@ func is12HourTime(format string) bool { // stylesReader provides a function to get the pointer to the structure after // deserialization of xl/styles.xml. func (f *File) stylesReader() *xlsxStyleSheet { - var err error - if f.Styles == nil { f.Styles = new(xlsxStyleSheet) - if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathStyles)))). + 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 } From 4b64b26c52932a51ca97a2bb6bf372a07020e52b Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 13 Feb 2022 00:06:30 +0800 Subject: [PATCH 535/957] Ref: #660, #764, #1093, #1112, #1133 This improve number format support - Introduced NFP (number format parser) dependencies module - Initialize custom dates and times number format support - Dependencies module upgraded --- cell.go | 11 +- comment.go | 2 +- errors.go | 3 + go.mod | 9 +- go.sum | 20 +-- lib.go | 13 +- lib_test.go | 2 +- numfmt.go | 356 +++++++++++++++++++++++++++++++++++++++++++++++++ numfmt_test.go | 76 +++++++++++ pivotTable.go | 2 +- styles.go | 185 +++---------------------- styles_test.go | 20 --- 12 files changed, 484 insertions(+), 215 deletions(-) create mode 100644 numfmt.go create mode 100644 numfmt_test.go diff --git a/cell.go b/cell.go index 9af93f6356..b5b6ed45d3 100644 --- a/cell.go +++ b/cell.go @@ -1116,21 +1116,12 @@ func (f *File) formattedValue(s int, v string, raw bool) string { } for _, xlsxFmt := range styleSheet.NumFmts.NumFmt { if xlsxFmt.NumFmtID == numFmtID { - format := strings.ToLower(xlsxFmt.FormatCode) - if isTimeNumFmt(format) { - return parseTime(v, format) - } - return precise + return format(v, xlsxFmt.FormatCode) } } return precise } -// isTimeNumFmt determine if the given number format expression is a time number format. -func isTimeNumFmt(format string) bool { - return strings.Contains(format, "y") || strings.Contains(format, "m") || strings.Contains(strings.Replace(format, "red", "", -1), "d") || strings.Contains(format, "h") -} - // 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 { diff --git a/comment.go b/comment.go index c0dc33b97f..f3b3642e46 100644 --- a/comment.go +++ b/comment.go @@ -256,7 +256,7 @@ func (f *File) addComment(commentsXML, cell string, formatSet *formatComment) { if comments == nil { comments = &xlsxComments{Authors: xlsxAuthor{Author: []string{formatSet.Author}}} } - if inStrSlice(comments.Authors.Author, formatSet.Author) == -1 { + if inStrSlice(comments.Authors.Author, formatSet.Author, true) == -1 { comments.Authors.Author = append(comments.Authors.Author, formatSet.Author) authorID = len(comments.Authors.Author) - 1 } diff --git a/errors.go b/errors.go index ebbcef6c27..f0a3405582 100644 --- a/errors.go +++ b/errors.go @@ -123,6 +123,9 @@ var ( // ErrUnsupportedHashAlgorithm defined the error message on unsupported // hash algorithm. ErrUnsupportedHashAlgorithm = errors.New("unsupported hash algorithm") + // ErrUnsupportedNumberFormat defined the error message on unsupported number format + // expression. + ErrUnsupportedNumberFormat = errors.New("unsupported number format token") // ErrPasswordLengthInvalid defined the error message on invalid password // length. ErrPasswordLengthInvalid = errors.New("password length invalid") diff --git a/go.mod b/go.mod index b7aa1ba578..9d6e88d402 100644 --- a/go.mod +++ b/go.mod @@ -4,11 +4,12 @@ go 1.15 require ( github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 - github.com/richardlehane/mscfb v1.0.3 + github.com/richardlehane/mscfb v1.0.4 github.com/stretchr/testify v1.7.0 - github.com/xuri/efp v0.0.0-20210322160811-ab561f5b45e3 - golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 + github.com/xuri/efp v0.0.0-20220201101309-d64cf20d930d + github.com/xuri/nfp v0.0.0-20220210053112-1df76b07693e + golang.org/x/crypto v0.0.0-20220210151621-f4118a5b28e2 golang.org/x/image v0.0.0-20211028202545-6944b10bf410 - golang.org/x/net v0.0.0-20220107192237-5cfca573fb4d + golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd golang.org/x/text v0.3.7 ) diff --git a/go.sum b/go.sum index 7fa8255bbf..efd0f634e5 100644 --- a/go.sum +++ b/go.sum @@ -4,26 +4,30 @@ github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9 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.3 h1:rD8TBkYWkObWO0oLDFCbwMeZ4KoalxQy+QgniCj3nKI= -github.com/richardlehane/mscfb v1.0.3/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= +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/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-20210322160811-ab561f5b45e3 h1:EpI0bqf/eX9SdZDwlMmahKM+CDBgNbsXMhsN28XrM8o= -github.com/xuri/efp v0.0.0-20210322160811-ab561f5b45e3/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= -golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 h1:0es+/5331RGQPcXlMfP+WrnIIS6dNnNRe0WB02W0F4M= -golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +github.com/xuri/efp v0.0.0-20220201101309-d64cf20d930d h1:zFggKNM0CSDVuK4Gzd7RNw5hFCHOETKZ7Nb5MHw+bCE= +github.com/xuri/efp v0.0.0-20220201101309-d64cf20d930d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= +github.com/xuri/nfp v0.0.0-20220210053112-1df76b07693e h1:8Bg6HoC/EdUGR3Y9Vx12XoD/RfMta06hFamKO+NK7Bc= +github.com/xuri/nfp v0.0.0-20220210053112-1df76b07693e/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= +golang.org/x/crypto v0.0.0-20220210151621-f4118a5b28e2 h1:XdAboW3BNMv9ocSCOk/u1MFioZGzCNkiJZ19v9Oe3Ig= +golang.org/x/crypto v0.0.0-20220210151621-f4118a5b28e2/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-20220107192237-5cfca573fb4d h1:62NvYBuaanGXR2ZOfwDFkhhl6X1DUgf8qg3GuQvxZsE= -golang.org/x/net v0.0.0-20220107192237-5cfca573fb4d/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/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/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= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= diff --git a/lib.go b/lib.go index a435452aff..1bfdda2936 100644 --- a/lib.go +++ b/lib.go @@ -376,8 +376,11 @@ func inCoordinates(a [][]int, x []int) int { // inStrSlice provides a method to check if an element is present in an array, // and return the index of its location, otherwise return -1. -func inStrSlice(a []string, x string) int { +func inStrSlice(a []string, x string, caseSensitive bool) int { for idx, n := range a { + if !caseSensitive && strings.EqualFold(x, n) { + return idx + } if x == n { return idx } @@ -658,7 +661,7 @@ func (f *File) addNameSpaces(path string, ns xml.Attr) { // by the given attribute. func (f *File) setIgnorableNameSpace(path string, index int, ns xml.Attr) { ignorableNS := []string{"c14", "cdr14", "a14", "pic14", "x14", "xdr14", "x14ac", "dsp", "mso14", "dgm14", "x15", "x12ac", "x15ac", "xr", "xr2", "xr3", "xr4", "xr5", "xr6", "xr7", "xr8", "xr9", "xr10", "xr11", "xr12", "xr13", "xr14", "xr15", "x15", "x16", "x16r2", "mo", "mx", "mv", "o", "v"} - if inStrSlice(strings.Fields(f.xmlAttr[path][index].Value), ns.Name.Local) == -1 && inStrSlice(ignorableNS, ns.Name.Local) != -1 { + if inStrSlice(strings.Fields(f.xmlAttr[path][index].Value), ns.Name.Local, true) == -1 && inStrSlice(ignorableNS, ns.Name.Local, true) != -1 { f.xmlAttr[path][index].Value = strings.TrimSpace(fmt.Sprintf("%s %s", f.xmlAttr[path][index].Value, ns.Name.Local)) } } @@ -672,8 +675,7 @@ 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 := false - p := 0 + dot, n, p := false, false, 0 for i, v := range s { if v == '.' { if dot { @@ -686,10 +688,11 @@ func isNumeric(s string) (bool, int) { } return false, 0 } else if dot { + n = true p++ } } - return true, p + return n, p } var ( diff --git a/lib_test.go b/lib_test.go index da75dee5b5..1e2f3249c8 100644 --- a/lib_test.go +++ b/lib_test.go @@ -234,7 +234,7 @@ func TestSortCoordinates(t *testing.T) { } func TestInStrSlice(t *testing.T) { - assert.EqualValues(t, -1, inStrSlice([]string{}, "")) + assert.EqualValues(t, -1, inStrSlice([]string{}, "", true)) } func TestBoolValMarshal(t *testing.T) { diff --git a/numfmt.go b/numfmt.go new file mode 100644 index 0000000000..a724405e45 --- /dev/null +++ b/numfmt.go @@ -0,0 +1,356 @@ +// Copyright 2016 - 2022 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. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX / XLSM / XLTM 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. + +package excelize + +import ( + "fmt" + "strconv" + "strings" + "time" + + "github.com/xuri/nfp" +) + +// supportedTokenTypes list the supported number format token types currently. +var supportedTokenTypes = []string{ + nfp.TokenTypeCurrencyLanguage, + nfp.TokenTypeDateTimes, + nfp.TokenTypeElapsedDateTimes, + nfp.TokenTypeGeneral, + nfp.TokenTypeLiteral, + nfp.TokenSubTypeLanguageInfo, +} + +// numberFormat directly maps the number format parser runtime required +// fields. +type numberFormat struct { + section []nfp.Section + t time.Time + sectionIdx int + isNumberic, hours, seconds bool + number float64 + ap, afterPoint, beforePoint, localCode, result, value, valueSectionType string +} + +// 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 { + 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 +// expression. If the given number format is not supported, this will return +// the original cell value. +func format(value, numFmt string) string { + p := nfp.NumberFormatParser() + nf := numberFormat{section: p.Parse(numFmt), value: value} + nf.number, nf.valueSectionType = nf.getValueSectionType(value) + nf.prepareNumberic(value) + for i, section := range nf.section { + nf.sectionIdx = i + if section.Type != nf.valueSectionType { + continue + } + switch section.Type { + case nfp.TokenSectionPositive: + return nf.positiveHandler() + case nfp.TokenSectionNegative: + return nf.negativeHandler() + case nfp.TokenSectionZero: + return nf.zeroHandler() + default: + return nf.textHandler() + } + } + return value +} + +// 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 + for i, token := range nf.section[nf.sectionIdx].Items { + if inStrSlice(supportedTokenTypes, token.TType, true) == -1 || token.TType == nfp.TokenTypeGeneral { + result = fmt.Sprint(nf.number) + return + } + if token.TType == nfp.TokenTypeCurrencyLanguage { + if err := nf.currencyLanguageHandler(i, token); err != nil { + result = fmt.Sprint(nf.number) + return + } + } + if token.TType == nfp.TokenTypeDateTimes { + nf.dateTimesHandler(i, token) + } + if token.TType == nfp.TokenTypeElapsedDateTimes { + nf.elapsedDateTimesHandler(token) + } + if token.TType == nfp.TokenTypeLiteral { + nf.result += token.TValue + continue + } + } + result = nf.result + return +} + +// 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 { + err = ErrUnsupportedNumberFormat + return + } + if nf.localCode = part.Token.TValue; nf.localCode != "409" { + err = ErrUnsupportedNumberFormat + return + } + } + return +} + +// dateTimesHandler will be handling date and times types tokens for a number +// format expression. +func (nf *numberFormat) dateTimesHandler(i int, token nfp.Token) { + if idx := inStrSlice(nfp.AmPm, strings.ToUpper(token.TValue), false); idx != -1 { + if nf.ap == "" { + nextHours := nf.hoursNext(i) + aps := strings.Split(token.TValue, "/") + nf.ap = aps[0] + if nextHours > 12 { + nf.ap = aps[1] + } + } + nf.result += nf.ap + return + } + if strings.Contains(strings.ToUpper(token.TValue), "M") { + l := len(token.TValue) + if l == 1 && !nf.hours && !nf.secondsNext(i) { + nf.result += strconv.Itoa(int(nf.t.Month())) + return + } + if l == 2 && !nf.hours && !nf.secondsNext(i) { + nf.result += fmt.Sprintf("%02d", int(nf.t.Month())) + return + } + if l == 3 { + nf.result += nf.t.Month().String()[:3] + return + } + if l == 4 || l > 5 { + nf.result += nf.t.Month().String() + return + } + if l == 5 { + nf.result += nf.t.Month().String()[:1] + return + } + } + nf.yearsHandler(i, token) + nf.daysHandler(i, token) + nf.hoursHandler(i, token) + nf.minutesHandler(token) + nf.secondsHandler(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) { + years := strings.Contains(strings.ToUpper(token.TValue), "Y") + if years && len(token.TValue) <= 2 { + nf.result += strconv.Itoa(nf.t.Year())[2:] + return + } + if years && len(token.TValue) > 2 { + nf.result += strconv.Itoa(nf.t.Year()) + return + } +} + +// 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) { + if strings.Contains(strings.ToUpper(token.TValue), "D") { + switch len(token.TValue) { + case 1: + nf.result += strconv.Itoa(nf.t.Day()) + return + case 2: + nf.result += fmt.Sprintf("%02d", nf.t.Day()) + return + case 3: + nf.result += nf.t.Weekday().String()[:3] + return + default: + nf.result += nf.t.Weekday().String() + return + } + } +} + +// hoursHandler will be handling hours in the date and times types tokens for a +// number format expression. +func (nf *numberFormat) hoursHandler(i int, token nfp.Token) { + nf.hours = strings.Contains(strings.ToUpper(token.TValue), "H") + if nf.hours { + h := nf.t.Hour() + ap, ok := nf.apNext(i) + if ok { + nf.ap = ap[0] + if h > 12 { + h -= 12 + nf.ap = ap[1] + } + } + if nf.ap != "" && nf.hoursNext(i) == -1 && h > 12 { + h -= 12 + } + switch len(token.TValue) { + case 1: + nf.result += strconv.Itoa(h) + return + default: + nf.result += fmt.Sprintf("%02d", h) + return + } + } +} + +// minutesHandler will be handling minutes in the date and times types tokens +// for a number format expression. +func (nf *numberFormat) minutesHandler(token nfp.Token) { + if strings.Contains(strings.ToUpper(token.TValue), "M") { + nf.hours = false + switch len(token.TValue) { + case 1: + nf.result += strconv.Itoa(nf.t.Minute()) + return + default: + nf.result += fmt.Sprintf("%02d", nf.t.Minute()) + return + } + } +} + +// 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 + } + } +} + +// elapsedDateTimesHandler will be handling elapsed date and times types tokens +// for a number format expression. +func (nf *numberFormat) elapsedDateTimesHandler(token nfp.Token) { + if strings.Contains(strings.ToUpper(token.TValue), "H") { + nf.result += fmt.Sprintf("%.f", nf.t.Sub(excel1900Epoc).Hours()) + return + } + if strings.Contains(strings.ToUpper(token.TValue), "M") { + nf.result += fmt.Sprintf("%.f", nf.t.Sub(excel1900Epoc).Minutes()) + return + } + if strings.Contains(strings.ToUpper(token.TValue), "S") { + nf.result += fmt.Sprintf("%.f", nf.t.Sub(excel1900Epoc).Seconds()) + return + } +} + +// hoursNext detects if a token of type hours exists after a given tokens list. +func (nf *numberFormat) hoursNext(i int) int { + tokens := nf.section[nf.sectionIdx].Items + for idx := i + 1; idx < len(tokens); idx++ { + if tokens[idx].TType == nfp.TokenTypeDateTimes { + if strings.Contains(strings.ToUpper(tokens[idx].TValue), "H") { + t := timeFromExcelTime(nf.number, false) + return t.Hour() + } + } + } + return -1 +} + +// apNext detects if a token of type AM/PM exists after a given tokens list. +func (nf *numberFormat) apNext(i int) ([]string, bool) { + tokens := nf.section[nf.sectionIdx].Items + for idx := i + 1; idx < len(tokens); idx++ { + if tokens[idx].TType == nfp.TokenTypeDateTimes { + if strings.Contains(strings.ToUpper(tokens[idx].TValue), "H") { + return nil, false + } + if i := inStrSlice(nfp.AmPm, tokens[idx].TValue, false); i != -1 { + return strings.Split(tokens[idx].TValue, "/"), true + } + } + } + return nil, false +} + +// secondsNext detects if a token of type seconds exists after a given tokens +// list. +func (nf *numberFormat) secondsNext(i int) bool { + tokens := nf.section[nf.sectionIdx].Items + for idx := i + 1; idx < len(tokens); idx++ { + if tokens[idx].TType == nfp.TokenTypeDateTimes { + return strings.Contains(strings.ToUpper(tokens[idx].TValue), "S") + } + } + return false +} + +// negativeHandler will be handling negative selection for a number format +// expression. +func (nf *numberFormat) negativeHandler() string { + return fmt.Sprint(nf.number) +} + +// zeroHandler will be handling zero selection for a number format expression. +func (nf *numberFormat) zeroHandler() string { + return fmt.Sprint(nf.number) +} + +// textHandler will be handling text selection for a number format expression. +func (nf *numberFormat) textHandler() string { + return fmt.Sprint(nf.value) +} + +// 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 + } + if number > 0 { + return number, nfp.TokenSectionPositive + } + if number < 0 { + return number, nfp.TokenSectionNegative + } + return number, nfp.TokenSectionZero +} diff --git a/numfmt_test.go b/numfmt_test.go new file mode 100644 index 0000000000..b64287b8a3 --- /dev/null +++ b/numfmt_test.go @@ -0,0 +1,76 @@ +package excelize + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNumFmt(t *testing.T) { + for _, item := range [][]string{ + {"123", "general", "123"}, + {"43528", "y", "19"}, + {"43528", "Y", "19"}, + {"43528", "yy", "19"}, + {"43528", "YY", "19"}, + {"43528", "yyy", "2019"}, + {"43528", "YYY", "2019"}, + {"43528", "yyyy", "2019"}, + {"43528", "YYYY", "2019"}, + {"43528", "yyyyy", "2019"}, + {"43528", "YYYYY", "2019"}, + {"43528", "m", "3"}, + {"43528", "mm", "03"}, + {"43528", "mmm", "Mar"}, + {"43528", "mmmm", "March"}, + {"43528", "mmmmm", "M"}, + {"43528", "mmmmmm", "March"}, + {"43528", "d", "4"}, + {"43528", "dd", "04"}, + {"43528", "ddd", "Mon"}, + {"43528", "dddd", "Monday"}, + {"43528", "h", "0"}, + {"43528", "hh", "00"}, + {"43528", "hhh", "00"}, + {"43543.544872685183", "hhmm", "1304"}, + {"43543.544872685183", "mmhhmmmm", "0313March"}, + {"43543.544872685183", "mm hh mm mm", "03 13 04 03"}, + {"43543.544872685183", "mm hh m m", "03 13 4 3"}, + {"43543.544872685183", "m s", "4 37"}, + {"43528", "[h]", "1044672"}, + {"43528", "[m]", "62680320"}, + {"43528", "s", "0"}, + {"43528", "ss", "00"}, + {"43528", "[s]", "3760819200"}, + {"43543.544872685183", "h:mm:ss AM/PM", "1:04:37 PM"}, + {"43543.544872685183", "AM/PM h:mm:ss", "PM 1:04:37"}, + {"43543.086539351854", "hh:mm:ss AM/PM", "02:04:37 AM"}, + {"43543.086539351854", "AM/PM hh:mm:ss", "AM 02:04:37"}, + {"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.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"}, + {"43528.003958333335", "h:mm", "0:05"}, + {"6.9444444444444444E-5", "h:m", "0:0"}, + {"6.9444444444444444E-5", "h:mm", "0:00"}, + {"6.9444444444444444E-5", "h:m", "0:0"}, + {"0.50070601851851848", "h:m", "12:1"}, + {"0.97952546296296295", "h:m", "23:30"}, + {"43528", "mmmm", "March"}, + {"43528", "dddd", "Monday"}, + {"0", ";;;", "0"}, + {"43528", "[$-409]MM/DD/YYYY", "03/04/2019"}, + {"43528", "[$-111]MM/DD/YYYY", "43528"}, + {"43528", "[$US-409]MM/DD/YYYY", "43528"}, + {"43543.586539351854", "AM/PM h h:mm", "PM 14 2:04"}, + {"text", "AM/PM h h:mm", "text"}, + } { + result := format(item[0], item[1]) + assert.Equal(t, item[2], result, item) + } +} diff --git a/pivotTable.go b/pivotTable.go index d30eeb1dd7..d7e9c94a2f 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -632,7 +632,7 @@ func (f *File) getPivotFieldsIndex(fields []PivotTableField, opt *PivotTableOpti return pivotFieldsIndex, err } for _, field := range fields { - if pos := inStrSlice(orders, field.Data); pos != -1 { + if pos := inStrSlice(orders, field.Data, true); pos != -1 { pivotFieldsIndex = append(pivotFieldsIndex, pos) } } diff --git a/styles.go b/styles.go index 7678b847b3..6dea20e694 100644 --- a/styles.go +++ b/styles.go @@ -20,7 +20,6 @@ import ( "log" "math" "reflect" - "regexp" "strconv" "strings" ) @@ -756,7 +755,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{ - 0: formatToString, + 0: format, 1: formatToInt, 2: formatToFloat, 3: formatToInt, @@ -764,30 +763,30 @@ var builtInNumFmtFunc = map[int]func(v string, format string) string{ 9: formatToC, 10: formatToD, 11: formatToE, - 12: formatToString, // Doesn't support currently - 13: formatToString, // Doesn't support currently - 14: parseTime, - 15: parseTime, - 16: parseTime, - 17: parseTime, - 18: parseTime, - 19: parseTime, - 20: parseTime, - 21: parseTime, - 22: parseTime, + 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: formatToString, // Doesn't support currently - 42: formatToString, // Doesn't support currently - 43: formatToString, // Doesn't support currently - 44: formatToString, // Doesn't support currently - 45: parseTime, - 46: parseTime, - 47: parseTime, + 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: formatToString, + 49: format, } // validType defined the list of valid validation types. @@ -845,12 +844,6 @@ var criteriaType = map[string]string{ "continue month": "continueMonth", } -// formatToString provides a function to return original string by given -// built-in number formats code and cell string. -func formatToString(v string, format string) string { - return v -} - // formatToInt provides a function to convert original string to integer // format as string type by given built-in number formats code and cell // string. @@ -933,144 +926,6 @@ func formatToE(v string, format string) string { return fmt.Sprintf("%.2E", f) } -// parseTime provides a function to returns a string parsed using time.Time. -// Replace Excel placeholders with Go time placeholders. For example, replace -// yyyy with 2006. These are in a specific order, due to the fact that m is -// used in month, minute, and am/pm. It would be easier to fix that with -// regular expressions, but if it's possible to keep this simple it would be -// easier to maintain. Full-length month and days (e.g. March, Tuesday) have -// letters in them that would be replaced by other characters below (such as -// the 'h' in March, or the 'd' in Tuesday) below. First we convert them to -// arbitrary characters unused in Excel Date formats, and then at the end, -// turn them to what they should actually be. Based off: -// http://www.ozgrid.com/Excel/CustomFormats.htm -func parseTime(v string, format string) string { - var ( - f float64 - err error - goFmt string - ) - f, err = strconv.ParseFloat(v, 64) - if err != nil { - return v - } - val := timeFromExcelTime(f, false) - - if format == "" { - return v - } - - goFmt = format - - if strings.Contains(goFmt, "[") { - re := regexp.MustCompile(`\[.+\]`) - goFmt = re.ReplaceAllLiteralString(goFmt, "") - } - - // use only first variant - if strings.Contains(goFmt, ";") { - goFmt = goFmt[:strings.IndexByte(goFmt, ';')] - } - - replacements := []struct{ xltime, gotime string }{ - {"YYYY", "2006"}, - {"YY", "06"}, - {"MM", "01"}, - {"M", "1"}, - {"DD", "02"}, - {"D", "2"}, - {"yyyy", "2006"}, - {"yy", "06"}, - {"MMMM", "%%%%"}, - {"mmmm", "%%%%"}, - {"DDDD", "&&&&"}, - {"dddd", "&&&&"}, - {"DD", "02"}, - {"dd", "02"}, - {"D", "2"}, - {"d", "2"}, - {"MMM", "Jan"}, - {"mmm", "Jan"}, - {"MMSS", "0405"}, - {"mmss", "0405"}, - {"SS", "05"}, - {"ss", "05"}, - {"s", "5"}, - {"MM:", "04:"}, - {"mm:", "04:"}, - {":MM", ":04"}, - {":mm", ":04"}, - {"m:", "4:"}, - {":m", ":4"}, - {"MM", "01"}, - {"mm", "01"}, - {"AM/PM", "PM"}, - {"am/pm", "PM"}, - {"M/", "1/"}, - {"m/", "1/"}, - {"%%%%", "January"}, - {"&&&&", "Monday"}, - } - - replacementsGlobal := []struct{ xltime, gotime string }{ - {"\\-", "-"}, - {"\\ ", " "}, - {"\\.", "."}, - {"\\", ""}, - {"\"", ""}, - } - // It is the presence of the "am/pm" indicator that determines if this is - // a 12 hour or 24 hours time format, not the number of 'h' characters. - var padding bool - if val.Hour() == 0 && !strings.Contains(format, "hh") && !strings.Contains(format, "HH") { - padding = true - } - if is12HourTime(format) { - goFmt = strings.Replace(goFmt, "hh", "3", 1) - goFmt = strings.Replace(goFmt, "h", "3", 1) - goFmt = strings.Replace(goFmt, "HH", "3", 1) - goFmt = strings.Replace(goFmt, "H", "3", 1) - } else { - goFmt = strings.Replace(goFmt, "hh", "15", 1) - goFmt = strings.Replace(goFmt, "HH", "15", 1) - if 0 < val.Hour() && val.Hour() < 12 { - goFmt = strings.Replace(goFmt, "h", "3", 1) - goFmt = strings.Replace(goFmt, "H", "3", 1) - } else { - goFmt = strings.Replace(goFmt, "h", "15", 1) - goFmt = strings.Replace(goFmt, "H", "15", 1) - } - } - - for _, repl := range replacements { - goFmt = strings.Replace(goFmt, repl.xltime, repl.gotime, 1) - } - for _, repl := range replacementsGlobal { - goFmt = strings.Replace(goFmt, repl.xltime, repl.gotime, -1) - } - // If the hour is optional, strip it out, along with the possible dangling - // colon that would remain. - if val.Hour() < 1 { - goFmt = strings.Replace(goFmt, "]:", "]", 1) - goFmt = strings.Replace(goFmt, "[03]", "", 1) - goFmt = strings.Replace(goFmt, "[3]", "", 1) - goFmt = strings.Replace(goFmt, "[15]", "", 1) - } else { - goFmt = strings.Replace(goFmt, "[3]", "3", 1) - goFmt = strings.Replace(goFmt, "[15]", "15", 1) - } - s := val.Format(goFmt) - if padding { - s = strings.Replace(s, "00:", "0:", 1) - } - return s -} - -// is12HourTime checks whether an Excel time format string is a 12 hours form. -func is12HourTime(format string) bool { - return strings.Contains(format, "am/pm") || strings.Contains(format, "AM/PM") || strings.Contains(format, "a/p") || strings.Contains(format, "A/P") -} - // stylesReader provides a function to get the pointer to the structure after // deserialization of xl/styles.xml. func (f *File) stylesReader() *xlsxStyleSheet { diff --git a/styles_test.go b/styles_test.go index 3597c3676a..de3444fb07 100644 --- a/styles_test.go +++ b/styles_test.go @@ -325,26 +325,6 @@ func TestGetFillID(t *testing.T) { assert.Equal(t, -1, getFillID(NewFile().stylesReader(), &Style{Fill: Fill{Type: "unknown"}})) } -func TestParseTime(t *testing.T) { - assert.Equal(t, "2019", parseTime("43528", "YYYY")) - assert.Equal(t, "43528", parseTime("43528", "")) - - assert.Equal(t, "2019-03-04 05:05:42", parseTime("43528.2123", "YYYY-MM-DD hh:mm:ss")) - assert.Equal(t, "2019-03-04 05:05:42", parseTime("43528.2123", "YYYY-MM-DD hh:mm:ss;YYYY-MM-DD hh:mm:ss")) - assert.Equal(t, "3/4/2019 5:5:42", parseTime("43528.2123", "M/D/YYYY h:m:s")) - assert.Equal(t, "3/4/2019 0:5:42", parseTime("43528.003958333335", "m/d/yyyy h:m:s")) - assert.Equal(t, "3/4/2019 0:05:42", parseTime("43528.003958333335", "M/D/YYYY h:mm:s")) - assert.Equal(t, "3:30:00 PM", parseTime("0.64583333333333337", "h:mm:ss am/pm")) - assert.Equal(t, "0:05", parseTime("43528.003958333335", "h:mm")) - assert.Equal(t, "0:0", parseTime("6.9444444444444444E-5", "h:m")) - assert.Equal(t, "0:00", parseTime("6.9444444444444444E-5", "h:mm")) - assert.Equal(t, "0:0", parseTime("6.9444444444444444E-5", "h:m")) - assert.Equal(t, "12:1", parseTime("0.50070601851851848", "h:m")) - assert.Equal(t, "23:30", parseTime("0.97952546296296295", "h:m")) - assert.Equal(t, "March", parseTime("43528", "mmmm")) - assert.Equal(t, "Monday", parseTime("43528", "dddd")) -} - func TestThemeColor(t *testing.T) { for _, clr := range [][]string{ {"FF000000", ThemeColor("000000", -0.1)}, From ad09698515b4f70496c950cca02ce612af8170e0 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 14 Feb 2022 00:05:47 +0800 Subject: [PATCH 536/957] Initialize local month name and AM/PM format support for number format --- numfmt.go | 289 ++++++++++++++++++++++++++++++++++++++++++++++--- numfmt_test.go | 124 +++++++++++++++++++++ 2 files changed, 399 insertions(+), 14 deletions(-) diff --git a/numfmt.go b/numfmt.go index a724405e45..1f41f19781 100644 --- a/numfmt.go +++ b/numfmt.go @@ -20,14 +20,10 @@ import ( "github.com/xuri/nfp" ) -// supportedTokenTypes list the supported number format token types currently. -var supportedTokenTypes = []string{ - nfp.TokenTypeCurrencyLanguage, - nfp.TokenTypeDateTimes, - nfp.TokenTypeElapsedDateTimes, - nfp.TokenTypeGeneral, - nfp.TokenTypeLiteral, - nfp.TokenSubTypeLanguageInfo, +type languageInfo struct { + apFmt string + tags []string + localMonth func(t time.Time, abbr int) string } // numberFormat directly maps the number format parser runtime required @@ -41,6 +37,137 @@ type numberFormat struct { ap, afterPoint, beforePoint, localCode, result, value, valueSectionType string } +// supportedTokenTypes list the supported number format token types currently. +var ( + supportedTokenTypes = []string{ + nfp.TokenTypeCurrencyLanguage, + nfp.TokenTypeDateTimes, + nfp.TokenTypeElapsedDateTimes, + nfp.TokenTypeGeneral, + nfp.TokenTypeLiteral, + nfp.TokenSubTypeLanguageInfo, + } + // supportedLanguageInfo directly maps the supported language ID and tags. + supportedLanguageInfo = map[string]languageInfo{ + "36": {tags: []string{"af"}, localMonth: localMonthsNameAfrikaans, apFmt: apFmtAfrikaans}, + "445": {tags: []string{"bn-IN"}, localMonth: localMonthsNameBangla, apFmt: nfp.AmPm[0]}, + "4": {tags: []string{"zh-Hans"}, localMonth: localMonthsNameChinese1, apFmt: nfp.AmPm[2]}, + "7804": {tags: []string{"zh"}, localMonth: localMonthsNameChinese1, apFmt: nfp.AmPm[2]}, + "804": {tags: []string{"zh-CN"}, localMonth: localMonthsNameChinese1, apFmt: nfp.AmPm[2]}, + "1004": {tags: []string{"zh-SG"}, localMonth: localMonthsNameChinese2, apFmt: nfp.AmPm[2]}, + "7C04": {tags: []string{"zh-Hant"}, localMonth: localMonthsNameChinese3, apFmt: nfp.AmPm[2]}, + "C04": {tags: []string{"zh-HK"}, localMonth: localMonthsNameChinese2, apFmt: nfp.AmPm[2]}, + "1404": {tags: []string{"zh-MO"}, localMonth: localMonthsNameChinese3, apFmt: nfp.AmPm[2]}, + "404": {tags: []string{"zh-TW"}, localMonth: localMonthsNameChinese3, apFmt: nfp.AmPm[2]}, + "9": {tags: []string{"en"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, + "1000": {tags: []string{ + "aa", "aa-DJ", "aa-ER", "aa-ER", "aa-NA", "agq", "agq-CM", "ak", "ak-GH", "sq-ML", + "gsw-LI", "gsw-CH", "ar-TD", "ar-KM", "ar-DJ", "ar-ER", "ar-IL", "ar-MR", "ar-PS", + "ar-SO", "ar-SS", "ar-SD", "ar-001", "ast", "ast-ES", "asa", "asa-TZ", "ksf", "ksf-CM", + "bm", "bm-Latn-ML", "bas", "bas-CM", "bem", "bem-ZM", "bez", "bez-TZ", "byn", "byn-ER", + "brx", "brx-IN", "ca-AD", "ca-FR", "ca-IT", "ceb", "ceb-Latn", "ceb-Latn-PH", "tzm-Latn-MA", + "ccp", "ccp-Cakm", "ccp-Cakm-BD", "ccp-Cakm-IN", "ce-RU", "cgg", "cgg-UG", "cu-RU", "swc", + "swc-CD", "kw", "ke-GB", "da-GL", "dua", "dua-CM", "nl-AW", "nl-BQ", "nl-CW", "nl-SX", + "nl-SR", "dz", "ebu", "ebu-KE", "en-AS", "en-AI", "en-AG", "en-AT", "en-BS", "en-BB", + "en-BE", "en-BM", "en-BW", "en-IO", "en-VG", "en-BI", "en-CM", "en-KY", "en-CX", "en-CC", + "en-CK", "en-CY", "en-DK", "en-DM", "en-ER", "en-150", "en-FK", "en-FI", "en-FJ", "en-GM", + "en-DE", "en-GH", "en-GI", "en-GD", "en-GU", "en-GG", "en-GY", "en-IM", "en-IL", "en-JE", + "en-KE", "en-KI", "en-LS", "en-LR", "en-MO", "en-MG", "en-MW", "en-MT", "en-MH", "en-MU", + "en-FM", "en-MS", "en-NA", "en-NR", "en-NL", "en-NG", "en-NU", "en-NF", "en-MP", "en-PK", + "en-PW", "en-PG", "en-PN", "en-PR", "en-RW", "en-KN", "en-LC", "en-VC", "en-WS", "en-SC", + "en-SL", "en-SX", "en-SI", "en-SB", "en-SS", "en-SH", "en-SD", "en-SZ", "en-SE", "en-CH", + "en-TZ", "en-TK", "en-TO", "en-TC", "en-TV", "en-UG", "en-UM", "en-VI", "en-VU", "en-001", + "en-ZM", "eo", "eo-001", "ee", "ee-GH", "ee-TG", "ewo", "ewo-CM", "fo-DK", "fr-DZ", + "fr-BJ", "fr-BF", "fr-BI", "fr-CF", "fr-TD", "fr-KM", "fr-CG", "fr-DJ", "fr-GQ", "fr-GF", + "fr-PF", "fr-GA", "fr-GP", "fr-GN", "fr-MG", "fr-MQ", "fr-MR", "fr-MU", "fr-YT", "fr-NC", + "fr-NE", "fr-RW", "fr-BL", "fr-MF", "fr-PM", "fr-SC", "fr-SY", "fr-TG", "fr-TN", "fr-VU", + "fr-WF", "fur", "fur-IT", "ff-Latn-BF", "ff-CM", "ff-Latn-CM", "ff-Latn-GM", "ff-Latn-GH", + "ff-GN", "ff-Latn-GN", "ff-Latn-GW", "ff-Latn-LR", "ff-MR", "ff-Latn-MR", "ff-Latn-NE", + "ff-Latn-SL", "lg", "lg-UG", "de-BE", "de-IT", "el-CY", "guz", "guz-KE", "ha-Latn-GH", + "ha-Latn-NG", "ia-FR", "ia-001", "it-SM", "it-VA", "jv", "jv-Latn", "jv-Latn-ID", "dyo", + "dyo-SN", "kea", "kea-CV", "kab", "kab-DZ", "kkj", "kkj-CM", "kln", "kln-KE", "kam", + "kam-KE", "ks-Arab-IN", "ki", "ki-KE", "sw-TZ", "sw-UG", "ko-KP", "khq", "khq-ML", "ses", + "ses-ML", "nmg", "nmq-CM", "ku-Arab-IR", "lkt", "lkt-US", "lag", "lag-TZ", "ln", "ln-AO", + "ln-CF", "ln-CD", "nds", "nds-DE", "nds-NL", "lu", "lu-CD", "luo", "luo", "luo-KE", "luy", + "luy-KE", "jmc", "jmc-TZ", "mgh", "mgh-MZ", "kde", "kde-TZ", "mg", "mg-MG", "gv", "gv-IM", + "mas", "mas-KE", "mas-TZ", "mas-IR", "mer", "mer-KE", "mgo", "mgo-CM", "mfe", "mfe-MU", + "mua", "mua-CM", "nqo", "nqo-GN", "nqa", "naq-NA", "nnh", "nnh-CM", "jgo", "jgo-CM", + "lrc-IQ", "lrc-IR", "nd", "nd-ZW", "nb-SJ", "nus", "nus-SD", "nus-SS", "nyn", "nyn-UG", + "om-KE", "os", "os-GE", "os-RU", "ps-PK", "fa-AF", "pt-AO", "pt-CV", "pt-GQ", "pt-GW", + "pt-LU", "pt-MO", "pt-MZ", "pt-ST", "pt-CH", "pt-TL", "prg-001", "ksh", "ksh-DE", "rof", + "rof-TZ", "rn", "rn-BI", "ru-BY", "ru-KZ", "ru-KG", "ru-UA", "rwk", "rwk-TZ", "ssy", + "ssy-ER", "saq", "saq-KE", "sg", "sq-CF", "sbp", "sbp-TZ", "seh", "seh-MZ", "ksb", "ksb-TZ", + "sn", "sn-Latn", "sn-Latn-ZW", "xog", "xog-UG", "so-DJ", "so-ET", "so-KE", "nr", "nr-ZA", + "st-LS", "es-BZ", "es-BR", "es-PH", "zgh", "zgh-Tfng-MA", "zgh-Tfng", "ss", "ss-ZA", + "ss-SZ", "sv-AX", "shi", "shi-Tfng", "shi-Tfng-MA", "shi-Latn", "shi-Latn-MA", "dav", + "dav-KE", "ta-MY", "ta-SG", "twq", "twq-NE", "teo", "teo-KE", "teo-UG", "bo-IN", "tig", + "tig-ER", "to", "to-TO", "tr-CY", "uz-Arab", "us-Arab-AF", "vai", "vai-Vaii", + "vai-Vaii-LR", "vai-Latn-LR", "vai-Latn", "vo", "vo-001", "vun", "vun-TZ", "wae", + "wae-CH", "wal", "wae-ET", "yav", "yav-CM", "yo-BJ", "dje", "dje-NE", + }, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, + "C09": {tags: []string{"en-AU"}, localMonth: localMonthsNameEnglish, apFmt: strings.ToLower(nfp.AmPm[0])}, + "2829": {tags: []string{"en-BZ"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, + "1009": {tags: []string{"en-CA"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, + "2409": {tags: []string{"en-029"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, + "3C09": {tags: []string{"en-HK"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, + "4009": {tags: []string{"en-IN"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, + "1809": {tags: []string{"en-IE"}, localMonth: localMonthsNameEnglish, apFmt: strings.ToLower(nfp.AmPm[0])}, + "2009": {tags: []string{"en-JM"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, + "4409": {tags: []string{"en-MY"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, + "1409": {tags: []string{"en-NZ"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, + "3409": {tags: []string{"en-PH"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, + "4809": {tags: []string{"en-SG"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, + "1C09": {tags: []string{"en-ZA"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, + "2C09": {tags: []string{"en-TT"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, + "4C09": {tags: []string{"en-AE"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, + "809": {tags: []string{"en-GB"}, localMonth: localMonthsNameEnglish, apFmt: strings.ToLower(nfp.AmPm[0])}, + "409": {tags: []string{"en-US"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, + "3009": {tags: []string{"en-ZW"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, + "C": {tags: []string{"fr"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0]}, + "7": {tags: []string{"de"}, localMonth: localMonthsNameGerman, apFmt: nfp.AmPm[0]}, + "C07": {tags: []string{"de-AT"}, localMonth: localMonthsNameAustria, apFmt: nfp.AmPm[0]}, + "407": {tags: []string{"de-DE"}, localMonth: localMonthsNameGerman, apFmt: nfp.AmPm[0]}, + "10": {tags: []string{"it"}, localMonth: localMonthsNameItalian, apFmt: nfp.AmPm[0]}, + "11": {tags: []string{"ja"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese}, + "411": {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese}, + "12": {tags: []string{"ko"}, localMonth: localMonthsNameKorean, apFmt: apFmtKorean}, + "412": {tags: []string{"ko-KR"}, localMonth: localMonthsNameKorean, apFmt: apFmtKorean}, + } + // monthNamesBangla list the month names in the Bangla. + monthNamesBangla = []string{ + "\u099C\u09BE\u09A8\u09C1\u09AF\u09BC\u09BE\u09B0\u09C0", + "\u09AB\u09C7\u09AC\u09CD\u09B0\u09C1\u09AF\u09BC\u09BE\u09B0\u09C0", + "\u09AE\u09BE\u09B0\u09CD\u099A", + "\u098F\u09AA\u09CD\u09B0\u09BF\u09B2", + "\u09AE\u09C7", + "\u099C\u09C1\u09A8", + "\u099C\u09C1\u09B2\u09BE\u0987", + "\u0986\u0997\u09B8\u09CD\u099F", + "\u09B8\u09C7\u09AA\u09CD\u099F\u09C7\u09AE\u09CD\u09AC\u09B0", + "\u0985\u0995\u09CD\u099F\u09CB\u09AC\u09B0", + "\u09A8\u09AD\u09C7\u09AE\u09CD\u09AC\u09B0", + "\u09A1\u09BF\u09B8\u09C7\u09AE\u09CD\u09AC\u09B0", + } + // monthNamesAfrikaans list the month names in the Afrikaans. + monthNamesAfrikaans = []string{"Januarie", "Februarie", "Maart", "April", "Mei", "Junie", "Julie", "Augustus", "September", "Oktober", "November", "Desember"} + // monthNamesChinese list the month names in the Chinese. + monthNamesChinese = []string{"一", "二", "三", "四", "五", "六", "七", "八", "九", "十", "十一", "十二"} + // 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"} + // monthNamesGerman list the month names in the German. + monthNamesGerman = []string{"Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"} + // monthNamesAustria list the month names in the Austria. + monthNamesAustria = []string{"Jänner", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"} + // monthNamesItalian list the month names in the Italian. + monthNamesItalian = []string{"gennaio", "febbraio", "marzo", "aprile", "maggio", "giugno", "luglio", "agosto", "settembre", "ottobre", "novembre", "dicembre"} + // apFmtAfrikaans defined the AM/PM name in the Afrikaans. + apFmtAfrikaans = "vm./nm." + // apFmtJapanese defined the AM/PM name in the Japanese. + apFmtJapanese = "午前/午後" + // apFmtJapanese defined the AM/PM name in the Korean. + apFmtKorean = "오전/오후" +) + // prepareNumberic split the number into two before and after parts by a // decimal point. func (nf *numberFormat) prepareNumberic(value string) { @@ -116,21 +243,155 @@ func (nf *numberFormat) currencyLanguageHandler(i int, token nfp.Token) (err err err = ErrUnsupportedNumberFormat return } - if nf.localCode = part.Token.TValue; nf.localCode != "409" { + if _, ok := supportedLanguageInfo[strings.ToUpper(part.Token.TValue)]; !ok { err = ErrUnsupportedNumberFormat return } + nf.localCode = strings.ToUpper(part.Token.TValue) } return } +// localAmPm return AM/PM name by supported language ID. +func (nf *numberFormat) localAmPm(ap string) string { + if languageInfo, ok := supportedLanguageInfo[nf.localCode]; ok { + return languageInfo.apFmt + } + return ap +} + +// localMonthsNameEnglish returns the English name of the month. +func localMonthsNameEnglish(t time.Time, abbr int) string { + if abbr == 3 { + return t.Month().String()[:3] + } + if abbr == 4 { + return t.Month().String() + } + return t.Month().String()[:1] +} + +// 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]) + "." + } + if abbr == 4 { + return monthNamesAfrikaans[int(t.Month())-1] + } + return monthNamesAfrikaans[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]) + } + if abbr == 4 { + return monthNamesAustria[int(t.Month())-1] + } + return monthNamesAustria[int(t.Month())-1][:1] +} + +// localMonthsNameBangla returns the German name of the month. +func localMonthsNameBangla(t time.Time, abbr int) string { + if abbr == 3 || abbr == 4 { + return monthNamesBangla[int(t.Month())-1] + } + return string([]rune(monthNamesBangla[int(t.Month())-1])[:1]) +} + +// localMonthsNameFrench returns the French name of the month. +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 string([]rune(month)[:4]) + "." + } + if abbr == 4 { + return monthNamesFrench[int(t.Month())-1] + } + return monthNamesFrench[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] + } + if abbr == 4 { + return monthNamesItalian[int(t.Month())-1] + } + return monthNamesItalian[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]) + } + if abbr == 4 { + return monthNamesGerman[int(t.Month())-1] + } + return string([]rune(monthNamesGerman[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())) + "月" + } + if abbr == 4 { + return monthNamesChinese[int(t.Month())-1] + "月" + } + return monthNamesChinese[int(t.Month())-1] +} + +// 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 monthNamesChinese[int(t.Month())-1] +} + +// 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 strconv.Itoa(int(t.Month())) +} + +// 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 strconv.Itoa(int(t.Month())) +} + +// localMonthName return months name by supported language ID. +func (nf *numberFormat) localMonthsName(abbr int) string { + if languageInfo, ok := supportedLanguageInfo[nf.localCode]; ok { + return languageInfo.localMonth(nf.t, abbr) + } + return localMonthsNameEnglish(nf.t, abbr) +} + // dateTimesHandler will be handling date and times types tokens for a number // format expression. func (nf *numberFormat) dateTimesHandler(i int, token nfp.Token) { if idx := inStrSlice(nfp.AmPm, strings.ToUpper(token.TValue), false); idx != -1 { if nf.ap == "" { nextHours := nf.hoursNext(i) - aps := strings.Split(token.TValue, "/") + aps := strings.Split(nf.localAmPm(token.TValue), "/") nf.ap = aps[0] if nextHours > 12 { nf.ap = aps[1] @@ -150,15 +411,15 @@ func (nf *numberFormat) dateTimesHandler(i int, token nfp.Token) { return } if l == 3 { - nf.result += nf.t.Month().String()[:3] + nf.result += nf.localMonthsName(3) return } if l == 4 || l > 5 { - nf.result += nf.t.Month().String() + nf.result += nf.localMonthsName(4) return } if l == 5 { - nf.result += nf.t.Month().String()[:1] + nf.result += nf.localMonthsName(5) return } } @@ -304,7 +565,7 @@ func (nf *numberFormat) apNext(i int) ([]string, bool) { return nil, false } if i := inStrSlice(nfp.AmPm, tokens[idx].TValue, false); i != -1 { - return strings.Split(tokens[idx].TValue, "/"), true + return strings.Split(nf.localAmPm(tokens[idx].TValue), "/"), true } } } diff --git a/numfmt_test.go b/numfmt_test.go index b64287b8a3..a8a2813376 100644 --- a/numfmt_test.go +++ b/numfmt_test.go @@ -65,10 +65,134 @@ func TestNumFmt(t *testing.T) { {"43528", "dddd", "Monday"}, {"0", ";;;", "0"}, {"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"}, {"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."}, + {"44562.189571759256", "[$-36]mmmm dd yyyy h:mm AM/PM", "Januarie 01 2022 4:32 vm."}, + {"44562.189571759256", "[$-36]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 vm."}, + {"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"}, + {"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"}, + {"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", "[$-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 오전"}, } { result := format(item[0], item[1]) assert.Equal(t, item[2], result, item) From f87c39c41ddcb2fbb75a6035ba1dd28e4de8c71b Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 17 Feb 2022 00:09:11 +0800 Subject: [PATCH 537/957] This closes #1148, resolve limitations when adding VBA project to the workbook Added two exported functions `SetWorkbookPrOptions` and `GetWorkbookPrOptions` to support setting and getting the code name property of the workbook Re-order fields of the workbook properties group to improve the compatibility Go Modules dependencies upgrade Put workbook related operating in new `workbook.go` source code Library introduction docs block updated --- adjust.go | 12 ++-- calc.go | 12 ++-- calcchain.go | 12 ++-- cell.go | 12 ++-- chart.go | 12 ++-- col.go | 12 ++-- comment.go | 12 ++-- comment_test.go | 12 ++-- crypt.go | 12 ++-- crypt_test.go | 12 ++-- datavalidation.go | 12 ++-- datavalidation_test.go | 12 ++-- date.go | 12 ++-- docProps.go | 12 ++-- docProps_test.go | 12 ++-- drawing.go | 12 ++-- drawing_test.go | 12 ++-- errors.go | 12 ++-- excelize.go | 12 ++-- file.go | 12 ++-- go.mod | 6 +- go.sum | 12 ++-- lib.go | 12 ++-- merge.go | 12 ++-- numfmt.go | 14 ++-- picture.go | 12 ++-- pivotTable.go | 12 ++-- rows.go | 12 ++-- shape.go | 12 ++-- sheet.go | 81 ++--------------------- sheetpr.go | 14 ++-- sheetview.go | 12 ++-- sparkline.go | 12 ++-- stream.go | 12 ++-- styles.go | 12 ++-- table.go | 12 ++-- templates.go | 12 ++-- vmlDrawing.go | 12 ++-- workbook.go | 147 +++++++++++++++++++++++++++++++++++++++++ workbook_test.go | 43 ++++++++++++ xmlApp.go | 12 ++-- xmlCalcChain.go | 12 ++-- xmlChart.go | 12 ++-- xmlChartSheet.go | 12 ++-- xmlComments.go | 12 ++-- xmlContentTypes.go | 12 ++-- xmlCore.go | 12 ++-- xmlDecodeDrawing.go | 12 ++-- xmlDrawing.go | 12 ++-- xmlPivotCache.go | 12 ++-- xmlPivotTable.go | 12 ++-- xmlSharedStrings.go | 12 ++-- xmlStyles.go | 12 ++-- xmlTable.go | 12 ++-- xmlTheme.go | 12 ++-- xmlWorkbook.go | 38 +++++------ xmlWorksheet.go | 12 ++-- 57 files changed, 532 insertions(+), 411 deletions(-) create mode 100644 workbook.go create mode 100644 workbook_test.go diff --git a/adjust.go b/adjust.go index bf3ad7762e..e1c0e15e69 100644 --- a/adjust.go +++ b/adjust.go @@ -2,12 +2,12 @@ // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX / XLSM / XLTM 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. +// Package excelize 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. package excelize diff --git a/calc.go b/calc.go index b3c6df8228..a4c45fbf6d 100644 --- a/calc.go +++ b/calc.go @@ -2,12 +2,12 @@ // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX / XLSM / XLTM 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. +// Package excelize 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. package excelize diff --git a/calcchain.go b/calcchain.go index 44a47c9f95..a1f9c0c56a 100644 --- a/calcchain.go +++ b/calcchain.go @@ -2,12 +2,12 @@ // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX / XLSM / XLTM 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. +// Package excelize 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. package excelize diff --git a/cell.go b/cell.go index b5b6ed45d3..b6ddd35574 100644 --- a/cell.go +++ b/cell.go @@ -2,12 +2,12 @@ // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX / XLSM / XLTM 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. +// Package excelize 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. package excelize diff --git a/chart.go b/chart.go index bbd276e78d..4543770d01 100644 --- a/chart.go +++ b/chart.go @@ -2,12 +2,12 @@ // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX / XLSM / XLTM 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. +// Package excelize 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. package excelize diff --git a/col.go b/col.go index 16d1f93c12..827d7273b5 100644 --- a/col.go +++ b/col.go @@ -2,12 +2,12 @@ // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX / XLSM / XLTM 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. +// Package excelize 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. package excelize diff --git a/comment.go b/comment.go index f3b3642e46..6cebfeea3c 100644 --- a/comment.go +++ b/comment.go @@ -2,12 +2,12 @@ // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX / XLSM / XLTM 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. +// Package excelize 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. package excelize diff --git a/comment_test.go b/comment_test.go index 1901f7f453..952763dfd7 100644 --- a/comment_test.go +++ b/comment_test.go @@ -2,12 +2,12 @@ // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX / XLSM / XLTM 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. +// Package excelize 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. package excelize diff --git a/crypt.go b/crypt.go index 9912a6753d..c579a10b7a 100644 --- a/crypt.go +++ b/crypt.go @@ -2,12 +2,12 @@ // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX / XLSM / XLTM 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. +// Package excelize 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. package excelize diff --git a/crypt_test.go b/crypt_test.go index a81d72d463..45e881560c 100644 --- a/crypt_test.go +++ b/crypt_test.go @@ -2,12 +2,12 @@ // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX / XLSM / XLTM 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. +// Package excelize 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. package excelize diff --git a/datavalidation.go b/datavalidation.go index b8f939b64e..d0e927b636 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -2,12 +2,12 @@ // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX / XLSM / XLTM 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. +// Package excelize 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. package excelize diff --git a/datavalidation_test.go b/datavalidation_test.go index eed1b034cc..9ef11dcd38 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -2,12 +2,12 @@ // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX / XLSM / XLTM 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. +// Package excelize 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. package excelize diff --git a/date.go b/date.go index 04c9110e46..83d23ccf78 100644 --- a/date.go +++ b/date.go @@ -2,12 +2,12 @@ // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX / XLSM / XLTM 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. +// Package excelize 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. package excelize diff --git a/docProps.go b/docProps.go index 44c30c889d..41ea18e258 100644 --- a/docProps.go +++ b/docProps.go @@ -2,12 +2,12 @@ // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX / XLSM / XLTM 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. +// Package excelize 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. package excelize diff --git a/docProps_test.go b/docProps_test.go index 458280b291..545059d8df 100644 --- a/docProps_test.go +++ b/docProps_test.go @@ -2,12 +2,12 @@ // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX / XLSM / XLTM 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. +// Package excelize 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. package excelize diff --git a/drawing.go b/drawing.go index ac88032e22..0bc79005f7 100644 --- a/drawing.go +++ b/drawing.go @@ -2,12 +2,12 @@ // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX / XLSM / XLTM 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. +// Package excelize 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. package excelize diff --git a/drawing_test.go b/drawing_test.go index fc89d472b1..d33977fb89 100644 --- a/drawing_test.go +++ b/drawing_test.go @@ -2,12 +2,12 @@ // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX / XLSM / XLTM 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. +// Package excelize 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. package excelize diff --git a/errors.go b/errors.go index f0a3405582..1047704ffb 100644 --- a/errors.go +++ b/errors.go @@ -2,12 +2,12 @@ // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX / XLSM / XLTM 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. +// Package excelize 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. package excelize diff --git a/excelize.go b/excelize.go index 2155f0affb..0aebfc45db 100644 --- a/excelize.go +++ b/excelize.go @@ -2,12 +2,12 @@ // this source code is governed by a BSD-style license that can be found in // the LICENSE file. -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX / XLSM / XLTM 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. +// Package excelize 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. // // See https://xuri.me/excelize for more information about this package. package excelize diff --git a/file.go b/file.go index 43d94b2fb1..0cfed05ef8 100644 --- a/file.go +++ b/file.go @@ -2,12 +2,12 @@ // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX / XLSM / XLTM 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. +// Package excelize 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. package excelize diff --git a/go.mod b/go.mod index 9d6e88d402..16874b2189 100644 --- a/go.mod +++ b/go.mod @@ -6,9 +6,9 @@ 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-20220201101309-d64cf20d930d - github.com/xuri/nfp v0.0.0-20220210053112-1df76b07693e - golang.org/x/crypto v0.0.0-20220210151621-f4118a5b28e2 + 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 golang.org/x/image v0.0.0-20211028202545-6944b10bf410 golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd golang.org/x/text v0.3.7 diff --git a/go.sum b/go.sum index efd0f634e5..a4051d4dfd 100644 --- a/go.sum +++ b/go.sum @@ -11,12 +11,12 @@ 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-20220201101309-d64cf20d930d h1:zFggKNM0CSDVuK4Gzd7RNw5hFCHOETKZ7Nb5MHw+bCE= -github.com/xuri/efp v0.0.0-20220201101309-d64cf20d930d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= -github.com/xuri/nfp v0.0.0-20220210053112-1df76b07693e h1:8Bg6HoC/EdUGR3Y9Vx12XoD/RfMta06hFamKO+NK7Bc= -github.com/xuri/nfp v0.0.0-20220210053112-1df76b07693e/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= -golang.org/x/crypto v0.0.0-20220210151621-f4118a5b28e2 h1:XdAboW3BNMv9ocSCOk/u1MFioZGzCNkiJZ19v9Oe3Ig= -golang.org/x/crypto v0.0.0-20220210151621-f4118a5b28e2/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +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= 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= diff --git a/lib.go b/lib.go index 1bfdda2936..47ce2fe791 100644 --- a/lib.go +++ b/lib.go @@ -2,12 +2,12 @@ // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX / XLSM / XLTM 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. +// Package excelize 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. package excelize diff --git a/merge.go b/merge.go index 3ba7d6ad0d..376b68b29b 100644 --- a/merge.go +++ b/merge.go @@ -2,12 +2,12 @@ // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX / XLSM / XLTM 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. +// Package excelize 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. package excelize diff --git a/numfmt.go b/numfmt.go index 1f41f19781..4549110993 100644 --- a/numfmt.go +++ b/numfmt.go @@ -2,12 +2,12 @@ // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX / XLSM / XLTM 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. +// Package excelize 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. package excelize @@ -37,8 +37,8 @@ type numberFormat struct { ap, afterPoint, beforePoint, localCode, result, value, valueSectionType string } -// supportedTokenTypes list the supported number format token types currently. var ( + // supportedTokenTypes list the supported number format token types currently. supportedTokenTypes = []string{ nfp.TokenTypeCurrencyLanguage, nfp.TokenTypeDateTimes, diff --git a/picture.go b/picture.go index f018bd9ae3..180983d6bc 100644 --- a/picture.go +++ b/picture.go @@ -2,12 +2,12 @@ // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX / XLSM / XLTM 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. +// Package excelize 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. package excelize diff --git a/pivotTable.go b/pivotTable.go index d7e9c94a2f..5810968c98 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -2,12 +2,12 @@ // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX / XLSM / XLTM 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. +// Package excelize 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. package excelize diff --git a/rows.go b/rows.go index 88df52c7af..0d2490ca65 100644 --- a/rows.go +++ b/rows.go @@ -2,12 +2,12 @@ // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX / XLSM / XLTM 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. +// Package excelize 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. package excelize diff --git a/shape.go b/shape.go index be3735baeb..514171a8b7 100644 --- a/shape.go +++ b/shape.go @@ -2,12 +2,12 @@ // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX / XLSM / XLTM 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. +// Package excelize 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. package excelize diff --git a/sheet.go b/sheet.go index f10ca07d04..b440fb945f 100644 --- a/sheet.go +++ b/sheet.go @@ -2,12 +2,12 @@ // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX / XLSM / XLTM 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. +// Package excelize 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. package excelize @@ -93,35 +93,6 @@ func (f *File) contentTypesWriter() { } } -// 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 { - rels.Lock() - defer rels.Unlock() - for _, rel := range rels.Relationships { - if rel.Type == SourceRelationshipOfficeDocument { - path = strings.TrimPrefix(rel.Target, "/") - return - } - } - } - return -} - -// getWorkbookRelsPath provides a function to get the path of the workbook.xml.rels -// in the spreadsheet. -func (f *File) getWorkbookRelsPath() (path string) { - wbPath := f.getWorkbookPath() - wbDir := filepath.Dir(wbPath) - if wbDir == "." { - path = "_rels/" + filepath.Base(wbPath) + ".rels" - return - } - path = strings.TrimPrefix(filepath.Dir(wbPath)+"/_rels/"+filepath.Base(wbPath)+".rels", "/") - return -} - // getWorksheetPath construct a target XML as xl/worksheets/sheet%d by split // path, compatible with different types of relative paths in // workbook.xml.rels, for example: worksheets/sheet%d.xml @@ -135,35 +106,6 @@ func (f *File) getWorksheetPath(relTarget string) (path string) { return path } -// workbookReader provides a function to get the pointer to the workbook.xml -// structure after deserialization. -func (f *File) workbookReader() *xlsxWorkbook { - var err error - if f.WorkBook == nil { - wbPath := f.getWorkbookPath() - f.WorkBook = new(xlsxWorkbook) - if _, ok := f.xmlAttr[wbPath]; !ok { - d := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(wbPath)))) - f.xmlAttr[wbPath] = append(f.xmlAttr[wbPath], getRootElement(d)...) - f.addNameSpaces(wbPath, SourceRelationship) - } - 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 -} - -// workBookWriter provides a function to save workbook.xml after serialize -// structure. -func (f *File) workBookWriter() { - if f.WorkBook != nil { - output, _ := xml.Marshal(f.WorkBook) - f.saveFileList(f.getWorkbookPath(), replaceRelationshipsBytes(f.replaceNameSpaceBytes(f.getWorkbookPath(), output))) - } -} - // mergeExpandedCols merge expanded columns. func (f *File) mergeExpandedCols(ws *xlsxWorksheet) { sort.Slice(ws.Cols.Col, func(i, j int) bool { @@ -276,17 +218,6 @@ func (f *File) setSheet(index int, name string) { f.xmlAttr[path] = []xml.Attr{NameSpaceSpreadSheet} } -// 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{ - Name: trimSheetName(name), - SheetID: sheetID, - ID: "rId" + strconv.Itoa(rid), - }) -} - // relsWriter provides a function to save relationships after // serialize structure. func (f *File) relsWriter() { diff --git a/sheetpr.go b/sheetpr.go index d7e6d2a332..e8e6e9d431 100644 --- a/sheetpr.go +++ b/sheetpr.go @@ -2,12 +2,12 @@ // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX / XLSM / XLTM 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. +// Package excelize 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. package excelize @@ -25,7 +25,7 @@ type SheetPrOptionPtr interface { } type ( - // CodeName is a SheetPrOption + // CodeName is an option used for SheetPrOption and WorkbookPrOption CodeName string // EnableFormatConditionsCalculation is a SheetPrOption EnableFormatConditionsCalculation bool diff --git a/sheetview.go b/sheetview.go index b0912ec7a7..8650b322e2 100644 --- a/sheetview.go +++ b/sheetview.go @@ -2,12 +2,12 @@ // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX / XLSM / XLTM 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. +// Package excelize 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. package excelize diff --git a/sparkline.go b/sparkline.go index b75a2f1864..5a480b909f 100644 --- a/sparkline.go +++ b/sparkline.go @@ -2,12 +2,12 @@ // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX / XLSM / XLTM 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. +// Package excelize 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. package excelize diff --git a/stream.go b/stream.go index 3551e8f7a7..a9ec2cfc8c 100644 --- a/stream.go +++ b/stream.go @@ -2,12 +2,12 @@ // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX / XLSM / XLTM 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. +// Package excelize 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. package excelize diff --git a/styles.go b/styles.go index 6dea20e694..6d6d7bb495 100644 --- a/styles.go +++ b/styles.go @@ -2,12 +2,12 @@ // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX / XLSM / XLTM 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. +// Package excelize 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. package excelize diff --git a/table.go b/table.go index 961f9e6e35..1fcb448604 100644 --- a/table.go +++ b/table.go @@ -2,12 +2,12 @@ // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX / XLSM / XLTM 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. +// Package excelize 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. package excelize diff --git a/templates.go b/templates.go index d08f45ffd7..94683417a5 100644 --- a/templates.go +++ b/templates.go @@ -2,12 +2,12 @@ // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX / XLSM / XLTM 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. +// Package excelize 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. // // 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 418b4c539e..f9de49918e 100644 --- a/vmlDrawing.go +++ b/vmlDrawing.go @@ -2,12 +2,12 @@ // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX / XLSM / XLTM 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. +// Package excelize 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. package excelize diff --git a/workbook.go b/workbook.go new file mode 100644 index 0000000000..a2263b2d38 --- /dev/null +++ b/workbook.go @@ -0,0 +1,147 @@ +// Copyright 2016 - 2022 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. +// +// Package excelize 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. + +package excelize + +import ( + "bytes" + "encoding/xml" + "io" + "log" + "path/filepath" + "strconv" + "strings" +) + +// WorkbookPrOption is an option of a view of a workbook. See SetWorkbookPrOptions(). +type WorkbookPrOption interface { + setWorkbookPrOption(pr *xlsxWorkbookPr) +} + +// WorkbookPrOptionPtr is a writable WorkbookPrOption. See GetWorkbookPrOptions(). +type WorkbookPrOptionPtr interface { + WorkbookPrOption + getWorkbookPrOption(pr *xlsxWorkbookPr) +} + +// 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{ + Name: trimSheetName(name), + SheetID: sheetID, + ID: "rId" + strconv.Itoa(rid), + }) +} + +// 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 { + rels.Lock() + defer rels.Unlock() + for _, rel := range rels.Relationships { + if rel.Type == SourceRelationshipOfficeDocument { + path = strings.TrimPrefix(rel.Target, "/") + return + } + } + } + return +} + +// getWorkbookRelsPath provides a function to get the path of the workbook.xml.rels +// in the spreadsheet. +func (f *File) getWorkbookRelsPath() (path string) { + wbPath := f.getWorkbookPath() + wbDir := filepath.Dir(wbPath) + if wbDir == "." { + path = "_rels/" + filepath.Base(wbPath) + ".rels" + return + } + path = strings.TrimPrefix(filepath.Dir(wbPath)+"/_rels/"+filepath.Base(wbPath)+".rels", "/") + return +} + +// workbookReader provides a function to get the pointer to the workbook.xml +// structure after deserialization. +func (f *File) workbookReader() *xlsxWorkbook { + var err error + if f.WorkBook == nil { + wbPath := f.getWorkbookPath() + f.WorkBook = new(xlsxWorkbook) + if _, ok := f.xmlAttr[wbPath]; !ok { + d := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(wbPath)))) + f.xmlAttr[wbPath] = append(f.xmlAttr[wbPath], getRootElement(d)...) + f.addNameSpaces(wbPath, SourceRelationship) + } + 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 +} + +// workBookWriter provides a function to save workbook.xml after serialize +// structure. +func (f *File) workBookWriter() { + if f.WorkBook != nil { + output, _ := xml.Marshal(f.WorkBook) + f.saveFileList(f.getWorkbookPath(), replaceRelationshipsBytes(f.replaceNameSpaceBytes(f.getWorkbookPath(), output))) + } +} + +// SetWorkbookPrOptions provides a function to sets workbook properties. +// +// Available options: +// 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 CodeName) setWorkbookPrOption(pr *xlsxWorkbookPr) { + pr.CodeName = string(o) +} + +// GetWorkbookPrOptions provides a function to gets workbook properties. +// +// Available options: +// 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 +// code name of thw 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 new file mode 100644 index 0000000000..90fc3d706e --- /dev/null +++ b/workbook_test.go @@ -0,0 +1,43 @@ +package excelize + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func ExampleFile_SetWorkbookPrOptions() { + f := NewFile() + if err := f.SetWorkbookPrOptions( + CodeName("code"), + ); err != nil { + fmt.Println(err) + } + // Output: +} + +func ExampleFile_GetWorkbookPrOptions() { + f := NewFile() + var codeName CodeName + if err := f.GetWorkbookPrOptions(&codeName); err != nil { + fmt.Println(err) + } + fmt.Println("Defaults:") + fmt.Printf("- codeName: %q\n", codeName) + // Output: + // Defaults: + // - codeName: "" +} + +func TestWorkbookPr(t *testing.T) { + f := NewFile() + wb := f.workbookReader() + 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)) +} diff --git a/xmlApp.go b/xmlApp.go index 215ed23cad..abfd82b3d7 100644 --- a/xmlApp.go +++ b/xmlApp.go @@ -2,12 +2,12 @@ // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX / XLSM / XLTM 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. +// Package excelize 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. package excelize diff --git a/xmlCalcChain.go b/xmlCalcChain.go index b8645f549e..401bf2c894 100644 --- a/xmlCalcChain.go +++ b/xmlCalcChain.go @@ -2,12 +2,12 @@ // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX / XLSM / XLTM 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. +// Package excelize 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. package excelize diff --git a/xmlChart.go b/xmlChart.go index 3725845d2e..2e1e9585b2 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -2,12 +2,12 @@ // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX / XLSM / XLTM 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. +// Package excelize 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. package excelize diff --git a/xmlChartSheet.go b/xmlChartSheet.go index 0e868f2dcf..f0f2f6286f 100644 --- a/xmlChartSheet.go +++ b/xmlChartSheet.go @@ -4,12 +4,12 @@ // // struct code generated by github.com/xuri/xgen // -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX / XLSM / XLTM 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. +// Package excelize 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. package excelize diff --git a/xmlComments.go b/xmlComments.go index 8f7a03dc9b..b4602fc928 100644 --- a/xmlComments.go +++ b/xmlComments.go @@ -2,12 +2,12 @@ // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX / XLSM / XLTM 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. +// Package excelize 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. package excelize diff --git a/xmlContentTypes.go b/xmlContentTypes.go index 2f47f94fa7..5920f1f36f 100644 --- a/xmlContentTypes.go +++ b/xmlContentTypes.go @@ -2,12 +2,12 @@ // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX / XLSM / XLTM 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. +// Package excelize 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. package excelize diff --git a/xmlCore.go b/xmlCore.go index 9aa09bf237..22ae6bc088 100644 --- a/xmlCore.go +++ b/xmlCore.go @@ -2,12 +2,12 @@ // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX / XLSM / XLTM 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. +// Package excelize 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. package excelize diff --git a/xmlDecodeDrawing.go b/xmlDecodeDrawing.go index 0ca63d1d38..9091440023 100644 --- a/xmlDecodeDrawing.go +++ b/xmlDecodeDrawing.go @@ -2,12 +2,12 @@ // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX / XLSM / XLTM 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. +// Package excelize 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. package excelize diff --git a/xmlDrawing.go b/xmlDrawing.go index 61c25ca810..c96034ce5c 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -2,12 +2,12 @@ // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX / XLSM / XLTM 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. +// Package excelize 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. package excelize diff --git a/xmlPivotCache.go b/xmlPivotCache.go index 9591858384..0af7c44d69 100644 --- a/xmlPivotCache.go +++ b/xmlPivotCache.go @@ -2,12 +2,12 @@ // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX / XLSM / XLTM 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. +// Package excelize 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. package excelize diff --git a/xmlPivotTable.go b/xmlPivotTable.go index a7543a747e..897669babc 100644 --- a/xmlPivotTable.go +++ b/xmlPivotTable.go @@ -2,12 +2,12 @@ // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX / XLSM / XLTM 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. +// Package excelize 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. package excelize diff --git a/xmlSharedStrings.go b/xmlSharedStrings.go index 3b4bb7ad0e..683105e3ca 100644 --- a/xmlSharedStrings.go +++ b/xmlSharedStrings.go @@ -2,12 +2,12 @@ // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX / XLSM / XLTM 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. +// Package excelize 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. package excelize diff --git a/xmlStyles.go b/xmlStyles.go index a9c3f3be6f..c70ab605fd 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -2,12 +2,12 @@ // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX / XLSM / XLTM 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. +// Package excelize 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. package excelize diff --git a/xmlTable.go b/xmlTable.go index 2fc8f4d69b..4afc26d818 100644 --- a/xmlTable.go +++ b/xmlTable.go @@ -2,12 +2,12 @@ // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX / XLSM / XLTM 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. +// Package excelize 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. package excelize diff --git a/xmlTheme.go b/xmlTheme.go index 9e10bdd67d..6b9e207cb6 100644 --- a/xmlTheme.go +++ b/xmlTheme.go @@ -2,12 +2,12 @@ // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX / XLSM / XLTM 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. +// Package excelize 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. package excelize diff --git a/xmlWorkbook.go b/xmlWorkbook.go index 59e76017ee..f014bee562 100644 --- a/xmlWorkbook.go +++ b/xmlWorkbook.go @@ -2,12 +2,12 @@ // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX / XLSM / XLTM 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. +// Package excelize 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. package excelize @@ -107,24 +107,24 @@ type xlsxFileVersion struct { // http://schemas.openxmlformats.org/spreadsheetml/2006/main This element // defines a collection of workbook properties. type xlsxWorkbookPr struct { - AllowRefreshQuery bool `xml:"allowRefreshQuery,attr,omitempty"` - AutoCompressPictures bool `xml:"autoCompressPictures,attr,omitempty"` - BackupFile bool `xml:"backupFile,attr,omitempty"` - CheckCompatibility bool `xml:"checkCompatibility,attr,omitempty"` - CodeName string `xml:"codeName,attr,omitempty"` Date1904 bool `xml:"date1904,attr,omitempty"` - DefaultThemeVersion string `xml:"defaultThemeVersion,attr,omitempty"` + ShowObjects string `xml:"showObjects,attr,omitempty"` + ShowBorderUnselectedTables *bool `xml:"showBorderUnselectedTables,attr"` FilterPrivacy bool `xml:"filterPrivacy,attr,omitempty"` - HidePivotFieldList bool `xml:"hidePivotFieldList,attr,omitempty"` PromptedSolutions bool `xml:"promptedSolutions,attr,omitempty"` + ShowInkAnnotation *bool `xml:"showInkAnnotation,attr"` + BackupFile bool `xml:"backupFile,attr,omitempty"` + SaveExternalLinkValues *bool `xml:"saveExternalLinkValues,attr"` + UpdateLinks string `xml:"updateLinks,attr,omitempty"` + CodeName string `xml:"codeName,attr,omitempty"` + HidePivotFieldList bool `xml:"hidePivotFieldList,attr,omitempty"` + ShowPivotChartFilter bool `xml:"showPivotChartFilter,attr,omitempty"` + AllowRefreshQuery bool `xml:"allowRefreshQuery,attr,omitempty"` PublishItems bool `xml:"publishItems,attr,omitempty"` + CheckCompatibility bool `xml:"checkCompatibility,attr,omitempty"` + AutoCompressPictures *bool `xml:"autoCompressPictures,attr"` RefreshAllConnections bool `xml:"refreshAllConnections,attr,omitempty"` - SaveExternalLinkValues bool `xml:"saveExternalLinkValues,attr,omitempty"` - ShowBorderUnselectedTables bool `xml:"showBorderUnselectedTables,attr,omitempty"` - ShowInkAnnotation bool `xml:"showInkAnnotation,attr,omitempty"` - ShowObjects string `xml:"showObjects,attr,omitempty"` - ShowPivotChartFilter bool `xml:"showPivotChartFilter,attr,omitempty"` - UpdateLinks string `xml:"updateLinks,attr,omitempty"` + DefaultThemeVersion string `xml:"defaultThemeVersion,attr,omitempty"` } // xlsxBookViews directly maps the bookViews element. This element specifies the diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 83dd1ba8de..4a9c88a9dc 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -2,12 +2,12 @@ // this source code is governed by a BSD-style license that can be found in // the LICENSE file. // -// Package excelize providing a set of functions that allow you to write to -// and read from XLSX / XLSM / XLTM 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. +// Package excelize 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. package excelize From 07be99363156b2d1011954be7b5a4cc8f33b256b Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 18 Feb 2022 00:02:39 +0800 Subject: [PATCH 538/957] Fixed parsing decimal precision issue --- cell.go | 20 ++++++++++---------- cell_test.go | 8 ++++++-- lib.go | 9 +++++++-- numfmt.go | 28 +++++++++++++++------------- 4 files changed, 38 insertions(+), 27 deletions(-) diff --git a/cell.go b/cell.go index b6ddd35574..b44ed820e5 100644 --- a/cell.go +++ b/cell.go @@ -1082,23 +1082,23 @@ func (f *File) getCellStringFunc(sheet, axis string, fn func(x *xlsxWorksheet, c // 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 { - precise := v - isNum, precision := isNumeric(v) - if isNum && precision > 10 { - precise = roundPrecision(v, -1) - } if raw { return v } - if !isNum { - v = roundPrecision(v, 15) - precise = 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 } styleSheet := f.stylesReader() - if s >= len(styleSheet.CellXfs.Xf) { return precise } @@ -1116,7 +1116,7 @@ func (f *File) formattedValue(s int, v string, raw bool) string { } for _, xlsxFmt := range styleSheet.NumFmts.NumFmt { if xlsxFmt.NumFmtID == numFmtID { - return format(v, xlsxFmt.FormatCode) + return format(precise, xlsxFmt.FormatCode) } } return precise diff --git a/cell_test.go b/cell_test.go index f6f1098ddc..92d3d2fdcc 100644 --- a/cell_test.go +++ b/cell_test.go @@ -130,7 +130,7 @@ func TestSetCellFloat(t *testing.T) { assert.Equal(t, "123", val, "A1 should be 123") val, err = f.GetCellValue(sheet, "A2") assert.NoError(t, err) - assert.Equal(t, "123.0", val, "A2 should be 123.0") + assert.Equal(t, "123", val, "A2 should be 123") }) t.Run("with a decimal and precision limit", func(t *testing.T) { @@ -288,12 +288,14 @@ func TestGetCellValue(t *testing.T) { 275.39999999999998 68.900000000000006 8.8880000000000001E-2 - 4.0000000000000003E-5 + 4.0000000000000003e-5 2422.3000000000002 1101.5999999999999 275.39999999999998 68.900000000000006 1.1000000000000001 + 1234567890123_4 + 123456789_0123_4 `))) f.checked = nil rows, err = f.GetRows("Sheet1") @@ -325,6 +327,8 @@ func TestGetCellValue(t *testing.T) { "275.4", "68.9", "1.1", + "1234567890123_4", + "123456789_0123_4", }}, rows) assert.NoError(t, err) } diff --git a/lib.go b/lib.go index 47ce2fe791..d0ae62c3b0 100644 --- a/lib.go +++ b/lib.go @@ -675,22 +675,27 @@ 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, n, p := false, false, 0 + 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 == '-' { + continue + } return false, 0 } else if dot { - n = true p++ } + n = true } return n, p } diff --git a/numfmt.go b/numfmt.go index 4549110993..1a02d461ef 100644 --- a/numfmt.go +++ b/numfmt.go @@ -20,6 +20,7 @@ import ( "github.com/xuri/nfp" ) +// languageInfo defined the required fields of localization support for number format. type languageInfo struct { apFmt string tags []string @@ -191,16 +192,17 @@ func format(value, numFmt string) string { if section.Type != nf.valueSectionType { continue } - switch section.Type { - case nfp.TokenSectionPositive: - return nf.positiveHandler() - case nfp.TokenSectionNegative: - return nf.negativeHandler() - case nfp.TokenSectionZero: - return nf.zeroHandler() - default: - return nf.textHandler() + if nf.isNumberic { + switch section.Type { + case nfp.TokenSectionPositive: + return nf.positiveHandler() + case nfp.TokenSectionNegative: + return nf.negativeHandler() + default: + return nf.zeroHandler() + } } + return nf.textHandler() } return value } @@ -211,12 +213,12 @@ func (nf *numberFormat) positiveHandler() (result string) { nf.t, nf.hours, nf.seconds = timeFromExcelTime(nf.number, false), false, false for i, token := range nf.section[nf.sectionIdx].Items { if inStrSlice(supportedTokenTypes, token.TType, true) == -1 || token.TType == nfp.TokenTypeGeneral { - result = fmt.Sprint(nf.number) + result = nf.value return } if token.TType == nfp.TokenTypeCurrencyLanguage { if err := nf.currencyLanguageHandler(i, token); err != nil { - result = fmt.Sprint(nf.number) + result = nf.value return } } @@ -587,12 +589,12 @@ func (nf *numberFormat) secondsNext(i int) bool { // negativeHandler will be handling negative selection for a number format // expression. func (nf *numberFormat) negativeHandler() string { - return fmt.Sprint(nf.number) + return nf.value } // zeroHandler will be handling zero selection for a number format expression. func (nf *numberFormat) zeroHandler() string { - return fmt.Sprint(nf.number) + return nf.value } // textHandler will be handling text selection for a number format expression. From 8a6815fcccc1c9f005b615fb2eb008e6af6aacd9 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 22 Feb 2022 08:22:18 +0800 Subject: [PATCH 539/957] Improvement local month name and AM/PM format support in number format * Support for the Irish, Russian, Spanish, Thai, Turkish, Welsh, Yi, and Zulu --- numfmt.go | 178 +++++++++++++++++- numfmt_test.go | 495 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 672 insertions(+), 1 deletion(-) diff --git a/numfmt.go b/numfmt.go index 1a02d461ef..0e1c522c18 100644 --- a/numfmt.go +++ b/numfmt.go @@ -128,11 +128,37 @@ var ( "7": {tags: []string{"de"}, localMonth: localMonthsNameGerman, apFmt: nfp.AmPm[0]}, "C07": {tags: []string{"de-AT"}, localMonth: localMonthsNameAustria, apFmt: nfp.AmPm[0]}, "407": {tags: []string{"de-DE"}, localMonth: localMonthsNameGerman, apFmt: nfp.AmPm[0]}, + "3C": {tags: []string{"ga"}, localMonth: localMonthsNameIrish, apFmt: apFmtIrish}, + "83C": {tags: []string{"ga-IE"}, localMonth: localMonthsNameIrish, apFmt: apFmtIrish}, "10": {tags: []string{"it"}, localMonth: localMonthsNameItalian, apFmt: nfp.AmPm[0]}, "11": {tags: []string{"ja"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese}, "411": {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese}, "12": {tags: []string{"ko"}, localMonth: localMonthsNameKorean, apFmt: apFmtKorean}, "412": {tags: []string{"ko-KR"}, localMonth: localMonthsNameKorean, apFmt: apFmtKorean}, + "19": {tags: []string{"ru"}, localMonth: localMonthsNameRussian, apFmt: nfp.AmPm[0]}, + "819": {tags: []string{"ru-MD"}, localMonth: localMonthsNameRussian, apFmt: nfp.AmPm[0]}, + "419": {tags: []string{"ru-RU"}, localMonth: localMonthsNameRussian, apFmt: nfp.AmPm[0]}, + "A": {tags: []string{"es"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, + "2C0A": {tags: []string{"es-AR"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, + "200A": {tags: []string{"es-VE"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, + "400A": {tags: []string{"es-BO"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, + "340A": {tags: []string{"es-CL"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, + "240A": {tags: []string{"es-CO"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, + "140A": {tags: []string{"es-CR"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, + "5C0A": {tags: []string{"es-CU"}, localMonth: localMonthsNameSpanish, apFmt: apFmtCuba}, + "1C0A": {tags: []string{"es-DO"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, + "300A": {tags: []string{"es-EC"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, + "440A": {tags: []string{"es-SV"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, + "1E": {tags: []string{"th"}, localMonth: localMonthsNameThai, apFmt: nfp.AmPm[0]}, + "41E": {tags: []string{"th-TH"}, localMonth: localMonthsNameThai, apFmt: nfp.AmPm[0]}, + "1F": {tags: []string{"tr"}, localMonth: localMonthsNameTurkish, apFmt: apFmtTurkish}, + "41F": {tags: []string{"tr-TR"}, localMonth: localMonthsNameTurkish, apFmt: apFmtTurkish}, + "52": {tags: []string{"cy"}, localMonth: localMonthsNameWelsh, apFmt: apFmtWelsh}, + "452": {tags: []string{"cy-GB"}, localMonth: localMonthsNameWelsh, apFmt: apFmtWelsh}, + "78": {tags: []string{"ii"}, localMonth: localMonthsNameYi, apFmt: apFmtYi}, + "478": {tags: []string{"ii-CN"}, localMonth: localMonthsNameYi, apFmt: apFmtYi}, + "35": {tags: []string{"zu"}, localMonth: localMonthsNameZulu, apFmt: nfp.AmPm[0]}, + "435": {tags: []string{"zu-ZA"}, localMonth: localMonthsNameZulu, apFmt: nfp.AmPm[0]}, } // monthNamesBangla list the month names in the Bangla. monthNamesBangla = []string{ @@ -159,14 +185,55 @@ var ( monthNamesGerman = []string{"Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"} // monthNamesAustria list the month names in the Austria. monthNamesAustria = []string{"Jänner", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"} + // 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"} // monthNamesItalian list the month names in the Italian. monthNamesItalian = []string{"gennaio", "febbraio", "marzo", "aprile", "maggio", "giugno", "luglio", "agosto", "settembre", "ottobre", "novembre", "dicembre"} + // monthNamesRussian list the month names in the Russian. + monthNamesRussian = []string{"январь", "февраль", "март", "апрель", "май", "июнь", "июль", "август", "сентябрь", "октябрь", "ноябрь", "декабрь"} + // monthNamesSpanish list the month names in the Spanish. + monthNamesSpanish = []string{"enero", "febrero", "marzo", "abril", "mayo", "junio", "julio", "agosto", "septiembre", "octubre", "noviembre", "diciembre"} + // monthNamesThai list the month names in the Thai. + monthNamesThai = []string{ + "\u0e21\u0e01\u0e23\u0e32\u0e04\u0e21", + "\u0e01\u0e38\u0e21\u0e20\u0e32\u0e1e\u0e31\u0e19\u0e18\u0e4c", + "\u0e21\u0e35\u0e19\u0e32\u0e04\u0e21", + "\u0e40\u0e21\u0e29\u0e32\u0e22\u0e19", + "\u0e1e\u0e24\u0e29\u0e20\u0e32\u0e04\u0e21", + "\u0e21\u0e34\u0e16\u0e38\u0e19\u0e32\u0e22\u0e19", + "\u0e01\u0e23\u0e01\u0e0e\u0e32\u0e04\u0e21", + "\u0e2a\u0e34\u0e07\u0e2b\u0e32\u0e04\u0e21", + "\u0e01\u0e31\u0e19\u0e22\u0e32\u0e22\u0e19", + "\u0e15\u0e38\u0e25\u0e32\u0e04\u0e21", + "\u0e1e\u0e24\u0e28\u0e08\u0e34\u0e01\u0e32\u0e22\u0e19", + "\u0e18\u0e31\u0e19\u0e27\u0e32\u0e04\u0e21", + } + // 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"} + // monthNamesWelsh list the month names in the Welsh. + monthNamesWelsh = []string{"Ionawr", "Chwefror", "Mawrth", "Ebrill", "Mai", "Mehefin", "Gorffennaf", "Awst", "Medi", "Hydref", "Tachwedd", "Rhagfyr"} + // 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. + monthNamesZulu = []string{"Januwari", "Febhuwari", "Mashi", "Ephreli", "Meyi", "Juni", "Julayi", "Agasti", "Septemba", "Okthoba", "Novemba", "Disemba"} // apFmtAfrikaans defined the AM/PM name in the Afrikaans. apFmtAfrikaans = "vm./nm." + // apFmtCuba defined the AM/PM name in the Cuba. + apFmtCuba = "a.m./p.m." + // apFmtIrish defined the AM/PM name in the Irish. + apFmtIrish = "r.n./i.n." // apFmtJapanese defined the AM/PM name in the Japanese. apFmtJapanese = "午前/午後" - // apFmtJapanese defined the AM/PM name in the Korean. + // apFmtKorean defined the AM/PM name in the Korean. apFmtKorean = "오전/오후" + // apFmtSpanish defined the AM/PM name in the Spanish. + apFmtSpanish = "a. m./p. m." + // apFmtTurkish defined the AM/PM name in the Turkish. + apFmtTurkish = "\u00F6\u00F6/\u00F6\u0053" + // apFmtTurkish defined the AM/PM name in the Yi. + apFmtYi = "\ua3b8\ua111/\ua06f\ua2d2" + // apFmtWelsh defined the AM/PM name in the Welsh. + apFmtWelsh = "yb/yh" ) // prepareNumberic split the number into two before and after parts by a @@ -322,6 +389,26 @@ func localMonthsNameFrench(t time.Time, abbr int) string { return monthNamesFrench[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]) + } + } + if abbr == 4 { + return string([]rune(monthNamesIrish[int(t.Month())-1])) + } + return string([]rune(monthNamesIrish[int(t.Month())-1])[:1]) +} + // localMonthsNameItalian returns the Italian name of the month. func localMonthsNameItalian(t time.Time, abbr int) string { if abbr == 3 { @@ -379,6 +466,95 @@ func localMonthsNameKorean(t time.Time, abbr int) string { return strconv.Itoa(int(t.Month())) } +// localMonthsNameRussian returns the Russian name of the month. +func localMonthsNameRussian(t time.Time, abbr int) string { + if abbr == 3 { + month := monthNamesRussian[int(t.Month())-1] + if len([]rune(month)) <= 4 { + return month + } + return string([]rune(month)[:3]) + "." + } + if abbr == 4 { + return string([]rune(monthNamesRussian[int(t.Month())-1])) + } + return string([]rune(monthNamesRussian[int(t.Month())-1])[:1]) +} + +// 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] + } + if abbr == 4 { + return monthNamesSpanish[int(t.Month())-1] + } + return monthNamesSpanish[int(t.Month())-1][:1] +} + +// localMonthsNameThai returns the Thai name of the month. +func localMonthsNameThai(t time.Time, abbr int) string { + if abbr == 3 { + r := []rune(monthNamesThai[int(t.Month())-1]) + return string(r[:1]) + "." + string(r[len(r)-2:len(r)-1]) + "." + } + if abbr == 4 { + return string([]rune(monthNamesThai[int(t.Month())-1])) + } + return string([]rune(monthNamesThai[int(t.Month())-1])[:1]) +} + +// 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]) + } + if abbr == 4 { + return monthNamesTurkish[int(t.Month())-1] + } + return string([]rune(monthNamesTurkish[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 string([]rune(monthNamesWelsh[int(t.Month())-1])[:5]) + case 8, 9, 11, 12: + return string([]rune(monthNamesWelsh[int(t.Month())-1])[:4]) + default: + return string([]rune(monthNamesWelsh[int(t.Month())-1])[:3]) + } + } + if abbr == 4 { + return monthNamesWelsh[int(t.Month())-1] + } + return string([]rune(monthNamesWelsh[int(t.Month())-1])[:1]) +} + +// 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([]rune(monthNamesYi[int(t.Month())-1])[:1]) +} + +// 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]) + } + if abbr == 4 { + return monthNamesZulu[int(t.Month())-1] + } + return string([]rune(monthNamesZulu[int(t.Month())-1])[:1]) +} + // localMonthName return months name by supported language ID. func (nf *numberFormat) localMonthsName(abbr int) string { if languageInfo, ok := supportedLanguageInfo[nf.localCode]; ok { diff --git a/numfmt_test.go b/numfmt_test.go index a8a2813376..b60e2b62dc 100644 --- a/numfmt_test.go +++ b/numfmt_test.go @@ -178,6 +178,66 @@ func TestNumFmt(t *testing.T) { {"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"}, + {"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."}, + {"44652.18957170139", "[$-83C]mmm dd yyyy h:mm AM/PM", "Aib 01 2022 4:32 r.n."}, + {"44682.18957170139", "[$-83C]mmm dd yyyy h:mm AM/PM", "Beal 01 2022 4:32 r.n."}, + {"44713.18957170139", "[$-83C]mmm dd yyyy h:mm AM/PM", "Meith 01 2022 4:32 r.n."}, + {"44743.18957170139", "[$-83C]mmm dd yyyy h:mm AM/PM", "Iúil 01 2022 4:32 r.n."}, + {"44774.18957170139", "[$-83C]mmm dd yyyy h:mm AM/PM", "Lún 01 2022 4:32 r.n."}, + {"44805.18957170139", "[$-83C]mmm dd yyyy h:mm AM/PM", "MFómh 01 2022 4:32 r.n."}, + {"44835.18957170139", "[$-83C]mmm dd yyyy h:mm AM/PM", "DFómh 01 2022 4:32 r.n."}, + {"44866.18957170139", "[$-83C]mmm dd yyyy h:mm AM/PM", "Samh 01 2022 4:32 r.n."}, + {"44896.18957170139", "[$-83C]mmm dd yyyy h:mm AM/PM", "Noll 01 2022 4:32 r.n."}, + {"44562.189571759256", "[$-83C]mmmm dd yyyy h:mm AM/PM", "Eanáir 01 2022 4:32 r.n."}, + {"44593.189571759256", "[$-83C]mmmm dd yyyy h:mm AM/PM", "Feabhra 01 2022 4:32 r.n."}, + {"44621.18957170139", "[$-83C]mmmm dd yyyy h:mm AM/PM", "Márta 01 2022 4:32 r.n."}, + {"44652.18957170139", "[$-83C]mmmm dd yyyy h:mm AM/PM", "Aibreán 01 2022 4:32 r.n."}, + {"44682.18957170139", "[$-83C]mmmm dd yyyy h:mm AM/PM", "Bealtaine 01 2022 4:32 r.n."}, + {"44713.18957170139", "[$-83C]mmmm dd yyyy h:mm AM/PM", "Meitheamh 01 2022 4:32 r.n."}, + {"44743.18957170139", "[$-83C]mmmm dd yyyy h:mm AM/PM", "Iúil 01 2022 4:32 r.n."}, + {"44774.18957170139", "[$-83C]mmmm dd yyyy h:mm AM/PM", "Lúnasa 01 2022 4:32 r.n."}, + {"44805.18957170139", "[$-83C]mmmm dd yyyy h:mm AM/PM", "Meán Fómhair 01 2022 4:32 r.n."}, + {"44835.18957170139", "[$-83C]mmmm dd yyyy h:mm AM/PM", "Deireadh Fómhair 01 2022 4:32 r.n."}, + {"44866.18957170139", "[$-83C]mmmm dd yyyy h:mm AM/PM", "Samhain 01 2022 4:32 r.n."}, + {"44896.18957170139", "[$-83C]mmmm dd yyyy h:mm AM/PM", "Nollaig 01 2022 4:32 r.n."}, + {"44562.189571759256", "[$-3C]mmm dd yyyy h:mm AM/PM", "Ean 01 2022 4:32 r.n."}, + {"44593.189571759256", "[$-3C]mmm dd yyyy h:mm AM/PM", "Feabh 01 2022 4:32 r.n."}, + {"44621.18957170139", "[$-3C]mmm dd yyyy h:mm AM/PM", "Márta 01 2022 4:32 r.n."}, + {"44652.18957170139", "[$-3C]mmm dd yyyy h:mm AM/PM", "Aib 01 2022 4:32 r.n."}, + {"44682.18957170139", "[$-3C]mmm dd yyyy h:mm AM/PM", "Beal 01 2022 4:32 r.n."}, + {"44713.18957170139", "[$-3C]mmm dd yyyy h:mm AM/PM", "Meith 01 2022 4:32 r.n."}, + {"44743.18957170139", "[$-3C]mmm dd yyyy h:mm AM/PM", "Iúil 01 2022 4:32 r.n."}, + {"44774.18957170139", "[$-3C]mmm dd yyyy h:mm AM/PM", "Lún 01 2022 4:32 r.n."}, + {"44805.18957170139", "[$-3C]mmm dd yyyy h:mm AM/PM", "MFómh 01 2022 4:32 r.n."}, + {"44835.18957170139", "[$-3C]mmm dd yyyy h:mm AM/PM", "DFómh 01 2022 4:32 r.n."}, + {"44866.18957170139", "[$-3C]mmm dd yyyy h:mm AM/PM", "Samh 01 2022 4:32 r.n."}, + {"44896.18957170139", "[$-3C]mmm dd yyyy h:mm AM/PM", "Noll 01 2022 4:32 r.n."}, + {"44562.189571759256", "[$-3C]mmmm dd yyyy h:mm AM/PM", "Eanáir 01 2022 4:32 r.n."}, + {"44593.189571759256", "[$-3C]mmmm dd yyyy h:mm AM/PM", "Feabhra 01 2022 4:32 r.n."}, + {"44621.18957170139", "[$-3C]mmmm dd yyyy h:mm AM/PM", "Márta 01 2022 4:32 r.n."}, + {"44652.18957170139", "[$-3C]mmmm dd yyyy h:mm AM/PM", "Aibreán 01 2022 4:32 r.n."}, + {"44682.18957170139", "[$-3C]mmmm dd yyyy h:mm AM/PM", "Bealtaine 01 2022 4:32 r.n."}, + {"44713.18957170139", "[$-3C]mmmm dd yyyy h:mm AM/PM", "Meitheamh 01 2022 4:32 r.n."}, + {"44743.18957170139", "[$-3C]mmmm dd yyyy h:mm AM/PM", "Iúil 01 2022 4:32 r.n."}, + {"44774.18957170139", "[$-3C]mmmm dd yyyy h:mm AM/PM", "Lúnasa 01 2022 4:32 r.n."}, + {"44805.18957170139", "[$-3C]mmmm dd yyyy h:mm AM/PM", "Meán Fómhair 01 2022 4:32 r.n."}, + {"44835.18957170139", "[$-3C]mmmm dd yyyy h:mm AM/PM", "Deireadh Fómhair 01 2022 4:32 r.n."}, + {"44866.18957170139", "[$-3C]mmmm dd yyyy h:mm AM/PM", "Samhain 01 2022 4:32 r.n."}, + {"44896.18957170139", "[$-3C]mmmm dd yyyy h:mm AM/PM", "Nollaig 01 2022 4:32 r.n."}, + {"44562.189571759256", "[$-3C]mmmmm dd yyyy h:mm AM/PM", "E 01 2022 4:32 r.n."}, + {"44593.189571759256", "[$-3C]mmmmm dd yyyy h:mm AM/PM", "F 01 2022 4:32 r.n."}, + {"44621.18957170139", "[$-3C]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 r.n."}, + {"44652.18957170139", "[$-3C]mmmmm dd yyyy h:mm AM/PM", "A 01 2022 4:32 r.n."}, + {"44682.18957170139", "[$-3C]mmmmm dd yyyy h:mm AM/PM", "B 01 2022 4:32 r.n."}, + {"44713.18957170139", "[$-3C]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 r.n."}, + {"44743.18957170139", "[$-3C]mmmmm dd yyyy h:mm AM/PM", "I 01 2022 4:32 r.n."}, + {"44774.18957170139", "[$-3C]mmmmm dd yyyy h:mm AM/PM", "L 01 2022 4:32 r.n."}, + {"44805.18957170139", "[$-3C]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 r.n."}, + {"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"}, @@ -193,6 +253,441 @@ func TestNumFmt(t *testing.T) { {"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", "[$-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"}, + {"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."}, + {"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."}, + {"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."}, + {"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."}, + {"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."}, + {"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."}, + {"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."}, + {"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."}, + {"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."}, + {"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."}, + {"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"}, + {"44652.18957170139", "[$-1E]mmm dd yyyy h:mm AM/PM", "\u0e40.\u0e22. 01 2022 4:32 AM"}, + {"44682.18957170139", "[$-1E]mmm dd yyyy h:mm AM/PM", "\u0e1e.\u0e04. 01 2022 4:32 AM"}, + {"44713.18957170139", "[$-1E]mmm dd yyyy h:mm AM/PM", "\u0e21.\u0e22. 01 2022 4:32 AM"}, + {"44743.18957170139", "[$-1E]mmm dd yyyy h:mm AM/PM", "\u0e01.\u0e04. 01 2022 4:32 AM"}, + {"44774.18957170139", "[$-1E]mmm dd yyyy h:mm AM/PM", "\u0e2a.\u0e04. 01 2022 4:32 AM"}, + {"44805.18957170139", "[$-1E]mmm dd yyyy h:mm AM/PM", "\u0e01.\u0e22. 01 2022 4:32 AM"}, + {"44835.18957170139", "[$-1E]mmm dd yyyy h:mm AM/PM", "\u0e15.\u0e04. 01 2022 4:32 AM"}, + {"44866.18957170139", "[$-1E]mmm dd yyyy h:mm AM/PM", "\u0e1e.\u0e22. 01 2022 4:32 AM"}, + {"44896.18957170139", "[$-1E]mmm dd yyyy h:mm AM/PM", "\u0e18.\u0e04. 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-1E]mmmm dd yyyy h:mm AM/PM", "\u0e21\u0e01\u0e23\u0e32\u0e04\u0e21 01 2022 4:32 AM"}, + {"44593.189571759256", "[$-1E]mmmm dd yyyy h:mm AM/PM", "\u0e01\u0e38\u0e21\u0e20\u0e32\u0e1e\u0e31\u0e19\u0e18\u0e4c 01 2022 4:32 AM"}, + {"44621.18957170139", "[$-1E]mmmm dd yyyy h:mm AM/PM", "\u0e21\u0e35\u0e19\u0e32\u0e04\u0e21 01 2022 4:32 AM"}, + {"44652.18957170139", "[$-1E]mmmm dd yyyy h:mm AM/PM", "\u0e40\u0e21\u0e29\u0e32\u0e22\u0e19 01 2022 4:32 AM"}, + {"44682.18957170139", "[$-1E]mmmm dd yyyy h:mm AM/PM", "\u0e1e\u0e24\u0e29\u0e20\u0e32\u0e04\u0e21 01 2022 4:32 AM"}, + {"44713.18957170139", "[$-1E]mmmm dd yyyy h:mm AM/PM", "\u0e21\u0e34\u0e16\u0e38\u0e19\u0e32\u0e22\u0e19 01 2022 4:32 AM"}, + {"44743.18957170139", "[$-1E]mmmm dd yyyy h:mm AM/PM", "\u0e01\u0e23\u0e01\u0e0e\u0e32\u0e04\u0e21 01 2022 4:32 AM"}, + {"44774.18957170139", "[$-1E]mmmm dd yyyy h:mm AM/PM", "\u0e2a\u0e34\u0e07\u0e2b\u0e32\u0e04\u0e21 01 2022 4:32 AM"}, + {"44805.18957170139", "[$-1E]mmmm dd yyyy h:mm AM/PM", "\u0e01\u0e31\u0e19\u0e22\u0e32\u0e22\u0e19 01 2022 4:32 AM"}, + {"44835.18957170139", "[$-1E]mmmm dd yyyy h:mm AM/PM", "\u0e15\u0e38\u0e25\u0e32\u0e04\u0e21 01 2022 4:32 AM"}, + {"44866.18957170139", "[$-1E]mmmm dd yyyy h:mm AM/PM", "\u0e1e\u0e24\u0e28\u0e08\u0e34\u0e01\u0e32\u0e22\u0e19 01 2022 4:32 AM"}, + {"44896.18957170139", "[$-1E]mmmm dd yyyy h:mm AM/PM", "\u0e18\u0e31\u0e19\u0e27\u0e32\u0e04\u0e21 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-1E]mmmmm dd yyyy h:mm AM/PM", "\u0e21 01 2022 4:32 AM"}, + {"44593.189571759256", "[$-1E]mmmmm dd yyyy h:mm AM/PM", "\u0e01 01 2022 4:32 AM"}, + {"44621.18957170139", "[$-1E]mmmmm dd yyyy h:mm AM/PM", "\u0e21 01 2022 4:32 AM"}, + {"44652.18957170139", "[$-1E]mmmmm dd yyyy h:mm AM/PM", "\u0e40 01 2022 4:32 AM"}, + {"44682.18957170139", "[$-1E]mmmmm dd yyyy h:mm AM/PM", "\u0e1e 01 2022 4:32 AM"}, + {"44713.18957170139", "[$-1E]mmmmm dd yyyy h:mm AM/PM", "\u0e21 01 2022 4:32 AM"}, + {"44743.18957170139", "[$-1E]mmmmm dd yyyy h:mm AM/PM", "\u0e01 01 2022 4:32 AM"}, + {"44774.18957170139", "[$-1E]mmmmm dd yyyy h:mm AM/PM", "\u0e2a 01 2022 4:32 AM"}, + {"44805.18957170139", "[$-1E]mmmmm dd yyyy h:mm AM/PM", "\u0e01 01 2022 4:32 AM"}, + {"44835.18957170139", "[$-1E]mmmmm dd yyyy h:mm AM/PM", "\u0e15 01 2022 4:32 AM"}, + {"44866.18957170139", "[$-1E]mmmmm dd yyyy h:mm AM/PM", "\u0e1e 01 2022 4:32 AM"}, + {"44896.18957170139", "[$-1E]mmmmm dd yyyy h:mm AM/PM", "\u0e18 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-41E]mmm dd yyyy h:mm AM/PM", "\u0e21.\u0e04. 01 2022 4:32 AM"}, + {"44593.189571759256", "[$-41E]mmm dd yyyy h:mm AM/PM", "\u0e01.\u0e18. 01 2022 4:32 AM"}, + {"44621.18957170139", "[$-41E]mmm dd yyyy h:mm AM/PM", "\u0e21.\u0e04. 01 2022 4:32 AM"}, + {"44652.18957170139", "[$-41E]mmm dd yyyy h:mm AM/PM", "\u0e40.\u0e22. 01 2022 4:32 AM"}, + {"44682.18957170139", "[$-41E]mmm dd yyyy h:mm AM/PM", "\u0e1e.\u0e04. 01 2022 4:32 AM"}, + {"44713.18957170139", "[$-41E]mmm dd yyyy h:mm AM/PM", "\u0e21.\u0e22. 01 2022 4:32 AM"}, + {"44743.18957170139", "[$-41E]mmm dd yyyy h:mm AM/PM", "\u0e01.\u0e04. 01 2022 4:32 AM"}, + {"44774.18957170139", "[$-41E]mmm dd yyyy h:mm AM/PM", "\u0e2a.\u0e04. 01 2022 4:32 AM"}, + {"44805.18957170139", "[$-41E]mmm dd yyyy h:mm AM/PM", "\u0e01.\u0e22. 01 2022 4:32 AM"}, + {"44835.18957170139", "[$-41E]mmm dd yyyy h:mm AM/PM", "\u0e15.\u0e04. 01 2022 4:32 AM"}, + {"44866.18957170139", "[$-41E]mmm dd yyyy h:mm AM/PM", "\u0e1e.\u0e22. 01 2022 4:32 AM"}, + {"44896.18957170139", "[$-41E]mmm dd yyyy h:mm AM/PM", "\u0e18.\u0e04. 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-41E]mmmm dd yyyy h:mm AM/PM", "\u0e21\u0e01\u0e23\u0e32\u0e04\u0e21 01 2022 4:32 AM"}, + {"44593.189571759256", "[$-41E]mmmm dd yyyy h:mm AM/PM", "\u0e01\u0e38\u0e21\u0e20\u0e32\u0e1e\u0e31\u0e19\u0e18\u0e4c 01 2022 4:32 AM"}, + {"44621.18957170139", "[$-41E]mmmm dd yyyy h:mm AM/PM", "\u0e21\u0e35\u0e19\u0e32\u0e04\u0e21 01 2022 4:32 AM"}, + {"44652.18957170139", "[$-41E]mmmm dd yyyy h:mm AM/PM", "\u0e40\u0e21\u0e29\u0e32\u0e22\u0e19 01 2022 4:32 AM"}, + {"44682.18957170139", "[$-41E]mmmm dd yyyy h:mm AM/PM", "\u0e1e\u0e24\u0e29\u0e20\u0e32\u0e04\u0e21 01 2022 4:32 AM"}, + {"44713.18957170139", "[$-41E]mmmm dd yyyy h:mm AM/PM", "\u0e21\u0e34\u0e16\u0e38\u0e19\u0e32\u0e22\u0e19 01 2022 4:32 AM"}, + {"44743.18957170139", "[$-41E]mmmm dd yyyy h:mm AM/PM", "\u0e01\u0e23\u0e01\u0e0e\u0e32\u0e04\u0e21 01 2022 4:32 AM"}, + {"44774.18957170139", "[$-41E]mmmm dd yyyy h:mm AM/PM", "\u0e2a\u0e34\u0e07\u0e2b\u0e32\u0e04\u0e21 01 2022 4:32 AM"}, + {"44805.18957170139", "[$-41E]mmmm dd yyyy h:mm AM/PM", "\u0e01\u0e31\u0e19\u0e22\u0e32\u0e22\u0e19 01 2022 4:32 AM"}, + {"44835.18957170139", "[$-41E]mmmm dd yyyy h:mm AM/PM", "\u0e15\u0e38\u0e25\u0e32\u0e04\u0e21 01 2022 4:32 AM"}, + {"44866.18957170139", "[$-41E]mmmm dd yyyy h:mm AM/PM", "\u0e1e\u0e24\u0e28\u0e08\u0e34\u0e01\u0e32\u0e22\u0e19 01 2022 4:32 AM"}, + {"44896.18957170139", "[$-41E]mmmm dd yyyy h:mm AM/PM", "\u0e18\u0e31\u0e19\u0e27\u0e32\u0e04\u0e21 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-41E]mmmmm dd yyyy h:mm AM/PM", "\u0e21 01 2022 4:32 AM"}, + {"44593.189571759256", "[$-41E]mmmmm dd yyyy h:mm AM/PM", "\u0e01 01 2022 4:32 AM"}, + {"44621.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM", "\u0e21 01 2022 4:32 AM"}, + {"44652.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM", "\u0e40 01 2022 4:32 AM"}, + {"44682.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM", "\u0e1e 01 2022 4:32 AM"}, + {"44713.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM", "\u0e21 01 2022 4:32 AM"}, + {"44743.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM", "\u0e01 01 2022 4:32 AM"}, + {"44774.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM", "\u0e2a 01 2022 4:32 AM"}, + {"44805.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM", "\u0e01 01 2022 4:32 AM"}, + {"44835.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM", "\u0e15 01 2022 4:32 AM"}, + {"44866.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM", "\u0e1e 01 2022 4:32 AM"}, + {"44896.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM", "\u0e18 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-1F]mmm dd yyyy h:mm AM/PM", "Oca 01 2022 4:32 \u00F6\u00F6"}, + {"44593.189571759256", "[$-1F]mmm dd yyyy h:mm AM/PM", "Şub 01 2022 4:32 \u00F6\u00F6"}, + {"44621.18957170139", "[$-1F]mmm dd yyyy h:mm AM/PM", "Mar 01 2022 4:32 \u00F6\u00F6"}, + {"44652.18957170139", "[$-1F]mmm dd yyyy h:mm AM/PM", "Nis 01 2022 4:32 \u00F6\u00F6"}, + {"44682.18957170139", "[$-1F]mmm dd yyyy h:mm AM/PM", "May 01 2022 4:32 \u00F6\u00F6"}, + {"44713.18957170139", "[$-1F]mmm dd yyyy h:mm AM/PM", "Haz 01 2022 4:32 \u00F6\u00F6"}, + {"44743.18957170139", "[$-1F]mmm dd yyyy h:mm AM/PM", "Tem 01 2022 4:32 \u00F6\u00F6"}, + {"44774.18957170139", "[$-1F]mmm dd yyyy h:mm AM/PM", "Ağu 01 2022 4:32 \u00F6\u00F6"}, + {"44805.18957170139", "[$-1F]mmm dd yyyy h:mm AM/PM", "Eyl 01 2022 4:32 \u00F6\u00F6"}, + {"44835.18957170139", "[$-1F]mmm dd yyyy h:mm AM/PM", "Eki 01 2022 4:32 \u00F6\u00F6"}, + {"44866.18957170139", "[$-1F]mmm dd yyyy h:mm AM/PM", "Kas 01 2022 4:32 \u00F6\u00F6"}, + {"44896.18957170139", "[$-1F]mmm dd yyyy h:mm AM/PM", "Ara 01 2022 4:32 \u00F6\u00F6"}, + {"44562.189571759256", "[$-1F]mmmm dd yyyy h:mm AM/PM", "Ocak 01 2022 4:32 \u00F6\u00F6"}, + {"44593.189571759256", "[$-1F]mmmm dd yyyy h:mm AM/PM", "Şubat 01 2022 4:32 \u00F6\u00F6"}, + {"44621.18957170139", "[$-1F]mmmm dd yyyy h:mm AM/PM", "Mart 01 2022 4:32 \u00F6\u00F6"}, + {"44652.18957170139", "[$-1F]mmmm dd yyyy h:mm AM/PM", "Nisan 01 2022 4:32 \u00F6\u00F6"}, + {"44682.18957170139", "[$-1F]mmmm dd yyyy h:mm AM/PM", "Mayıs 01 2022 4:32 \u00F6\u00F6"}, + {"44713.18957170139", "[$-1F]mmmm dd yyyy h:mm AM/PM", "Haziran 01 2022 4:32 \u00F6\u00F6"}, + {"44743.18957170139", "[$-1F]mmmm dd yyyy h:mm AM/PM", "Temmuz 01 2022 4:32 \u00F6\u00F6"}, + {"44774.18957170139", "[$-1F]mmmm dd yyyy h:mm AM/PM", "Ağustos 01 2022 4:32 \u00F6\u00F6"}, + {"44805.18957170139", "[$-1F]mmmm dd yyyy h:mm AM/PM", "Eylül 01 2022 4:32 \u00F6\u00F6"}, + {"44835.18957170139", "[$-1F]mmmm dd yyyy h:mm AM/PM", "Ekim 01 2022 4:32 \u00F6\u00F6"}, + {"44866.18957170139", "[$-1F]mmmm dd yyyy h:mm AM/PM", "Kasım 01 2022 4:32 \u00F6\u00F6"}, + {"44896.18957170139", "[$-1F]mmmm dd yyyy h:mm AM/PM", "Aralık 01 2022 4:32 \u00F6\u00F6"}, + {"44562.189571759256", "[$-1F]mmmmm dd yyyy h:mm AM/PM", "O 01 2022 4:32 \u00F6\u00F6"}, + {"44593.189571759256", "[$-1F]mmmmm dd yyyy h:mm AM/PM", "Ş 01 2022 4:32 \u00F6\u00F6"}, + {"44621.18957170139", "[$-1F]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 \u00F6\u00F6"}, + {"44652.18957170139", "[$-1F]mmmmm dd yyyy h:mm AM/PM", "N 01 2022 4:32 \u00F6\u00F6"}, + {"44682.18957170139", "[$-1F]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 \u00F6\u00F6"}, + {"44713.18957170139", "[$-1F]mmmmm dd yyyy h:mm AM/PM", "H 01 2022 4:32 \u00F6\u00F6"}, + {"44743.18957170139", "[$-1F]mmmmm dd yyyy h:mm AM/PM", "T 01 2022 4:32 \u00F6\u00F6"}, + {"44774.18957170139", "[$-1F]mmmmm dd yyyy h:mm AM/PM", "A 01 2022 4:32 \u00F6\u00F6"}, + {"44805.18957170139", "[$-1F]mmmmm dd yyyy h:mm AM/PM", "E 01 2022 4:32 \u00F6\u00F6"}, + {"44835.18957170139", "[$-1F]mmmmm dd yyyy h:mm AM/PM", "E 01 2022 4:32 \u00F6\u00F6"}, + {"44866.18957170139", "[$-1F]mmmmm dd yyyy h:mm AM/PM", "K 01 2022 4:32 \u00F6\u00F6"}, + {"44896.18957170139", "[$-1F]mmmmm dd yyyy h:mm AM/PM", "A 01 2022 4:32 \u00F6\u00F6"}, + {"44562.189571759256", "[$-41F]mmm dd yyyy h:mm AM/PM", "Oca 01 2022 4:32 \u00F6\u00F6"}, + {"44593.189571759256", "[$-41F]mmm dd yyyy h:mm AM/PM", "Şub 01 2022 4:32 \u00F6\u00F6"}, + {"44621.18957170139", "[$-41F]mmm dd yyyy h:mm AM/PM", "Mar 01 2022 4:32 \u00F6\u00F6"}, + {"44652.18957170139", "[$-41F]mmm dd yyyy h:mm AM/PM", "Nis 01 2022 4:32 \u00F6\u00F6"}, + {"44682.18957170139", "[$-41F]mmm dd yyyy h:mm AM/PM", "May 01 2022 4:32 \u00F6\u00F6"}, + {"44713.18957170139", "[$-41F]mmm dd yyyy h:mm AM/PM", "Haz 01 2022 4:32 \u00F6\u00F6"}, + {"44743.18957170139", "[$-41F]mmm dd yyyy h:mm AM/PM", "Tem 01 2022 4:32 \u00F6\u00F6"}, + {"44774.18957170139", "[$-41F]mmm dd yyyy h:mm AM/PM", "Ağu 01 2022 4:32 \u00F6\u00F6"}, + {"44805.18957170139", "[$-41F]mmm dd yyyy h:mm AM/PM", "Eyl 01 2022 4:32 \u00F6\u00F6"}, + {"44835.18957170139", "[$-41F]mmm dd yyyy h:mm AM/PM", "Eki 01 2022 4:32 \u00F6\u00F6"}, + {"44866.18957170139", "[$-41F]mmm dd yyyy h:mm AM/PM", "Kas 01 2022 4:32 \u00F6\u00F6"}, + {"44896.18957170139", "[$-41F]mmm dd yyyy h:mm AM/PM", "Ara 01 2022 4:32 \u00F6\u00F6"}, + {"44562.189571759256", "[$-41F]mmmm dd yyyy h:mm AM/PM", "Ocak 01 2022 4:32 \u00F6\u00F6"}, + {"44593.189571759256", "[$-41F]mmmm dd yyyy h:mm AM/PM", "Şubat 01 2022 4:32 \u00F6\u00F6"}, + {"44621.18957170139", "[$-41F]mmmm dd yyyy h:mm AM/PM", "Mart 01 2022 4:32 \u00F6\u00F6"}, + {"44652.18957170139", "[$-41F]mmmm dd yyyy h:mm AM/PM", "Nisan 01 2022 4:32 \u00F6\u00F6"}, + {"44682.18957170139", "[$-41F]mmmm dd yyyy h:mm AM/PM", "Mayıs 01 2022 4:32 \u00F6\u00F6"}, + {"44713.18957170139", "[$-41F]mmmm dd yyyy h:mm AM/PM", "Haziran 01 2022 4:32 \u00F6\u00F6"}, + {"44743.18957170139", "[$-41F]mmmm dd yyyy h:mm AM/PM", "Temmuz 01 2022 4:32 \u00F6\u00F6"}, + {"44774.18957170139", "[$-41F]mmmm dd yyyy h:mm AM/PM", "Ağustos 01 2022 4:32 \u00F6\u00F6"}, + {"44805.18957170139", "[$-41F]mmmm dd yyyy h:mm AM/PM", "Eylül 01 2022 4:32 \u00F6\u00F6"}, + {"44835.18957170139", "[$-41F]mmmm dd yyyy h:mm AM/PM", "Ekim 01 2022 4:32 \u00F6\u00F6"}, + {"44866.18957170139", "[$-41F]mmmm dd yyyy h:mm AM/PM", "Kasım 01 2022 4:32 \u00F6\u00F6"}, + {"44896.18957170139", "[$-41F]mmmm dd yyyy h:mm AM/PM", "Aralık 01 2022 4:32 \u00F6\u00F6"}, + {"44562.189571759256", "[$-41F]mmmmm dd yyyy h:mm AM/PM", "O 01 2022 4:32 \u00F6\u00F6"}, + {"44593.189571759256", "[$-41F]mmmmm dd yyyy h:mm AM/PM", "Ş 01 2022 4:32 \u00F6\u00F6"}, + {"44621.18957170139", "[$-41F]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 \u00F6\u00F6"}, + {"44652.18957170139", "[$-41F]mmmmm dd yyyy h:mm AM/PM", "N 01 2022 4:32 \u00F6\u00F6"}, + {"44682.18957170139", "[$-41F]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 \u00F6\u00F6"}, + {"44713.18957170139", "[$-41F]mmmmm dd yyyy h:mm AM/PM", "H 01 2022 4:32 \u00F6\u00F6"}, + {"44743.18957170139", "[$-41F]mmmmm dd yyyy h:mm AM/PM", "T 01 2022 4:32 \u00F6\u00F6"}, + {"44774.18957170139", "[$-41F]mmmmm dd yyyy h:mm AM/PM", "A 01 2022 4:32 \u00F6\u00F6"}, + {"44805.18957170139", "[$-41F]mmmmm dd yyyy h:mm AM/PM", "E 01 2022 4:32 \u00F6\u00F6"}, + {"44835.18957170139", "[$-41F]mmmmm dd yyyy h:mm AM/PM", "E 01 2022 4:32 \u00F6\u00F6"}, + {"44866.18957170139", "[$-41F]mmmmm dd yyyy h:mm AM/PM", "K 01 2022 4:32 \u00F6\u00F6"}, + {"44896.18957170139", "[$-41F]mmmmm dd yyyy h:mm AM/PM", "A 01 2022 4:32 \u00F6\u00F6"}, + {"44562.189571759256", "[$-52]mmm dd yyyy h:mm AM/PM", "Ion 01 2022 4:32 yb"}, + {"44593.189571759256", "[$-52]mmm dd yyyy h:mm AM/PM", "Chwef 01 2022 4:32 yb"}, + {"44621.18957170139", "[$-52]mmm dd yyyy h:mm AM/PM", "Maw 01 2022 4:32 yb"}, + {"44652.18957170139", "[$-52]mmm dd yyyy h:mm AM/PM", "Ebr 01 2022 4:32 yb"}, + {"44682.18957170139", "[$-52]mmm dd yyyy h:mm AM/PM", "Mai 01 2022 4:32 yb"}, + {"44713.18957170139", "[$-52]mmm dd yyyy h:mm AM/PM", "Meh 01 2022 4:32 yb"}, + {"44743.18957170139", "[$-52]mmm dd yyyy h:mm AM/PM", "Gorff 01 2022 4:32 yb"}, + {"44774.18957170139", "[$-52]mmm dd yyyy h:mm AM/PM", "Awst 01 2022 4:32 yb"}, + {"44805.18957170139", "[$-52]mmm dd yyyy h:mm AM/PM", "Medi 01 2022 4:32 yb"}, + {"44835.18957170139", "[$-52]mmm dd yyyy h:mm AM/PM", "Hyd 01 2022 4:32 yb"}, + {"44866.18957170139", "[$-52]mmm dd yyyy h:mm AM/PM", "Tach 01 2022 4:32 yb"}, + {"44896.18957170139", "[$-52]mmm dd yyyy h:mm AM/PM", "Rhag 01 2022 4:32 yb"}, + {"44562.189571759256", "[$-52]mmmm dd yyyy h:mm AM/PM", "Ionawr 01 2022 4:32 yb"}, + {"44593.189571759256", "[$-52]mmmm dd yyyy h:mm AM/PM", "Chwefror 01 2022 4:32 yb"}, + {"44621.18957170139", "[$-52]mmmm dd yyyy h:mm AM/PM", "Mawrth 01 2022 4:32 yb"}, + {"44652.18957170139", "[$-52]mmmm dd yyyy h:mm AM/PM", "Ebrill 01 2022 4:32 yb"}, + {"44682.18957170139", "[$-52]mmmm dd yyyy h:mm AM/PM", "Mai 01 2022 4:32 yb"}, + {"44713.18957170139", "[$-52]mmmm dd yyyy h:mm AM/PM", "Mehefin 01 2022 4:32 yb"}, + {"44743.18957170139", "[$-52]mmmm dd yyyy h:mm AM/PM", "Gorffennaf 01 2022 4:32 yb"}, + {"44774.18957170139", "[$-52]mmmm dd yyyy h:mm AM/PM", "Awst 01 2022 4:32 yb"}, + {"44805.18957170139", "[$-52]mmmm dd yyyy h:mm AM/PM", "Medi 01 2022 4:32 yb"}, + {"44835.18957170139", "[$-52]mmmm dd yyyy h:mm AM/PM", "Hydref 01 2022 4:32 yb"}, + {"44866.18957170139", "[$-52]mmmm dd yyyy h:mm AM/PM", "Tachwedd 01 2022 4:32 yb"}, + {"44896.18957170139", "[$-52]mmmm dd yyyy h:mm AM/PM", "Rhagfyr 01 2022 4:32 yb"}, + {"44562.189571759256", "[$-52]mmmmm dd yyyy h:mm AM/PM", "I 01 2022 4:32 yb"}, + {"44593.189571759256", "[$-52]mmmmm dd yyyy h:mm AM/PM", "C 01 2022 4:32 yb"}, + {"44621.18957170139", "[$-52]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 yb"}, + {"44652.18957170139", "[$-52]mmmmm dd yyyy h:mm AM/PM", "E 01 2022 4:32 yb"}, + {"44682.18957170139", "[$-52]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 yb"}, + {"44713.18957170139", "[$-52]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 yb"}, + {"44743.18957170139", "[$-52]mmmmm dd yyyy h:mm AM/PM", "G 01 2022 4:32 yb"}, + {"44774.18957170139", "[$-52]mmmmm dd yyyy h:mm AM/PM", "A 01 2022 4:32 yb"}, + {"44805.18957170139", "[$-52]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 yb"}, + {"44835.18957170139", "[$-52]mmmmm dd yyyy h:mm AM/PM", "H 01 2022 4:32 yb"}, + {"44866.18957170139", "[$-52]mmmmm dd yyyy h:mm AM/PM", "T 01 2022 4:32 yb"}, + {"44896.18957170139", "[$-52]mmmmm dd yyyy h:mm AM/PM", "R 01 2022 4:32 yb"}, + {"44562.189571759256", "[$-452]mmm dd yyyy h:mm AM/PM", "Ion 01 2022 4:32 yb"}, + {"44593.189571759256", "[$-452]mmm dd yyyy h:mm AM/PM", "Chwef 01 2022 4:32 yb"}, + {"44621.18957170139", "[$-452]mmm dd yyyy h:mm AM/PM", "Maw 01 2022 4:32 yb"}, + {"44652.18957170139", "[$-452]mmm dd yyyy h:mm AM/PM", "Ebr 01 2022 4:32 yb"}, + {"44682.18957170139", "[$-452]mmm dd yyyy h:mm AM/PM", "Mai 01 2022 4:32 yb"}, + {"44713.18957170139", "[$-452]mmm dd yyyy h:mm AM/PM", "Meh 01 2022 4:32 yb"}, + {"44743.18957170139", "[$-452]mmm dd yyyy h:mm AM/PM", "Gorff 01 2022 4:32 yb"}, + {"44774.18957170139", "[$-452]mmm dd yyyy h:mm AM/PM", "Awst 01 2022 4:32 yb"}, + {"44805.18957170139", "[$-452]mmm dd yyyy h:mm AM/PM", "Medi 01 2022 4:32 yb"}, + {"44835.18957170139", "[$-452]mmm dd yyyy h:mm AM/PM", "Hyd 01 2022 4:32 yb"}, + {"44866.18957170139", "[$-452]mmm dd yyyy h:mm AM/PM", "Tach 01 2022 4:32 yb"}, + {"44896.18957170139", "[$-452]mmm dd yyyy h:mm AM/PM", "Rhag 01 2022 4:32 yb"}, + {"44562.189571759256", "[$-452]mmmm dd yyyy h:mm AM/PM", "Ionawr 01 2022 4:32 yb"}, + {"44593.189571759256", "[$-452]mmmm dd yyyy h:mm AM/PM", "Chwefror 01 2022 4:32 yb"}, + {"44621.18957170139", "[$-452]mmmm dd yyyy h:mm AM/PM", "Mawrth 01 2022 4:32 yb"}, + {"44652.18957170139", "[$-452]mmmm dd yyyy h:mm AM/PM", "Ebrill 01 2022 4:32 yb"}, + {"44682.18957170139", "[$-452]mmmm dd yyyy h:mm AM/PM", "Mai 01 2022 4:32 yb"}, + {"44713.18957170139", "[$-452]mmmm dd yyyy h:mm AM/PM", "Mehefin 01 2022 4:32 yb"}, + {"44743.18957170139", "[$-452]mmmm dd yyyy h:mm AM/PM", "Gorffennaf 01 2022 4:32 yb"}, + {"44774.18957170139", "[$-452]mmmm dd yyyy h:mm AM/PM", "Awst 01 2022 4:32 yb"}, + {"44805.18957170139", "[$-452]mmmm dd yyyy h:mm AM/PM", "Medi 01 2022 4:32 yb"}, + {"44835.18957170139", "[$-452]mmmm dd yyyy h:mm AM/PM", "Hydref 01 2022 4:32 yb"}, + {"44866.18957170139", "[$-452]mmmm dd yyyy h:mm AM/PM", "Tachwedd 01 2022 4:32 yb"}, + {"44896.18957170139", "[$-452]mmmm dd yyyy h:mm AM/PM", "Rhagfyr 01 2022 4:32 yb"}, + {"44562.189571759256", "[$-452]mmmmm dd yyyy h:mm AM/PM", "I 01 2022 4:32 yb"}, + {"44593.189571759256", "[$-452]mmmmm dd yyyy h:mm AM/PM", "C 01 2022 4:32 yb"}, + {"44621.18957170139", "[$-452]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 yb"}, + {"44652.18957170139", "[$-452]mmmmm dd yyyy h:mm AM/PM", "E 01 2022 4:32 yb"}, + {"44682.18957170139", "[$-452]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 yb"}, + {"44713.18957170139", "[$-452]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 yb"}, + {"44743.18957170139", "[$-452]mmmmm dd yyyy h:mm AM/PM", "G 01 2022 4:32 yb"}, + {"44774.18957170139", "[$-452]mmmmm dd yyyy h:mm AM/PM", "A 01 2022 4:32 yb"}, + {"44805.18957170139", "[$-452]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 yb"}, + {"44835.18957170139", "[$-452]mmmmm dd yyyy h:mm AM/PM", "H 01 2022 4:32 yb"}, + {"44866.18957170139", "[$-452]mmmmm dd yyyy h:mm AM/PM", "T 01 2022 4:32 yb"}, + {"44896.18957170139", "[$-452]mmmmm dd yyyy h:mm AM/PM", "R 01 2022 4:32 yb"}, + {"44562.189571759256", "[$-78]mmm dd yyyy h:mm AM/PM", "\ua2cd\ua1aa 01 2022 4:32 \ua3b8\ua111"}, + {"44593.189571759256", "[$-78]mmm dd yyyy h:mm AM/PM", "\ua44d\ua1aa 01 2022 4:32 \ua3b8\ua111"}, + {"44621.18957170139", "[$-78]mmm dd yyyy h:mm AM/PM", "\ua315\ua1aa 01 2022 4:32 \ua3b8\ua111"}, + {"44652.18957170139", "[$-78]mmm dd yyyy h:mm AM/PM", "\ua1d6\ua1aa 01 2022 4:32 \ua3b8\ua111"}, + {"44682.18957170139", "[$-78]mmm dd yyyy h:mm AM/PM", "\ua26c\ua1aa 01 2022 4:32 \ua3b8\ua111"}, + {"44713.18957170139", "[$-78]mmm dd yyyy h:mm AM/PM", "\ua0d8\ua1aa 01 2022 4:32 \ua3b8\ua111"}, + {"44743.18957170139", "[$-78]mmm dd yyyy h:mm AM/PM", "\ua3c3\ua1aa 01 2022 4:32 \ua3b8\ua111"}, + {"44774.18957170139", "[$-78]mmm dd yyyy h:mm AM/PM", "\ua246\ua1aa 01 2022 4:32 \ua3b8\ua111"}, + {"44805.18957170139", "[$-78]mmm dd yyyy h:mm AM/PM", "\ua22c\ua1aa 01 2022 4:32 \ua3b8\ua111"}, + {"44835.18957170139", "[$-78]mmm dd yyyy h:mm AM/PM", "\ua2b0\ua1aa 01 2022 4:32 \ua3b8\ua111"}, + {"44866.18957170139", "[$-78]mmm dd yyyy h:mm AM/PM", "\ua2b0\ua2aa\ua1aa 01 2022 4:32 \ua3b8\ua111"}, + {"44896.18957170139", "[$-78]mmm dd yyyy h:mm AM/PM", "\ua2b0\ua44b\ua1aa 01 2022 4:32 \ua3b8\ua111"}, + {"44562.189571759256", "[$-78]mmmm dd yyyy h:mm AM/PM", "\ua2cd\ua1aa 01 2022 4:32 \ua3b8\ua111"}, + {"44593.189571759256", "[$-78]mmmm dd yyyy h:mm AM/PM", "\ua44d\ua1aa 01 2022 4:32 \ua3b8\ua111"}, + {"44621.18957170139", "[$-78]mmmm dd yyyy h:mm AM/PM", "\ua315\ua1aa 01 2022 4:32 \ua3b8\ua111"}, + {"44652.18957170139", "[$-78]mmmm dd yyyy h:mm AM/PM", "\ua1d6\ua1aa 01 2022 4:32 \ua3b8\ua111"}, + {"44682.18957170139", "[$-78]mmmm dd yyyy h:mm AM/PM", "\ua26c\ua1aa 01 2022 4:32 \ua3b8\ua111"}, + {"44713.18957170139", "[$-78]mmmm dd yyyy h:mm AM/PM", "\ua0d8\ua1aa 01 2022 4:32 \ua3b8\ua111"}, + {"44743.18957170139", "[$-78]mmmm dd yyyy h:mm AM/PM", "\ua3c3\ua1aa 01 2022 4:32 \ua3b8\ua111"}, + {"44774.18957170139", "[$-78]mmmm dd yyyy h:mm AM/PM", "\ua246\ua1aa 01 2022 4:32 \ua3b8\ua111"}, + {"44805.18957170139", "[$-78]mmmm dd yyyy h:mm AM/PM", "\ua22c\ua1aa 01 2022 4:32 \ua3b8\ua111"}, + {"44835.18957170139", "[$-78]mmmm dd yyyy h:mm AM/PM", "\ua2b0\ua1aa 01 2022 4:32 \ua3b8\ua111"}, + {"44866.18957170139", "[$-78]mmmm dd yyyy h:mm AM/PM", "\ua2b0\ua2aa\ua1aa 01 2022 4:32 \ua3b8\ua111"}, + {"44896.18957170139", "[$-78]mmmm dd yyyy h:mm AM/PM", "\ua2b0\ua44b\ua1aa 01 2022 4:32 \ua3b8\ua111"}, + {"44562.189571759256", "[$-78]mmmmm dd yyyy h:mm AM/PM", "\ua2cd 01 2022 4:32 \ua3b8\ua111"}, + {"44593.189571759256", "[$-78]mmmmm dd yyyy h:mm AM/PM", "\ua44d 01 2022 4:32 \ua3b8\ua111"}, + {"44621.18957170139", "[$-78]mmmmm dd yyyy h:mm AM/PM", "\ua315 01 2022 4:32 \ua3b8\ua111"}, + {"44652.18957170139", "[$-78]mmmmm dd yyyy h:mm AM/PM", "\ua1d6 01 2022 4:32 \ua3b8\ua111"}, + {"44682.18957170139", "[$-78]mmmmm dd yyyy h:mm AM/PM", "\ua26c 01 2022 4:32 \ua3b8\ua111"}, + {"44713.18957170139", "[$-78]mmmmm dd yyyy h:mm AM/PM", "\ua0d8 01 2022 4:32 \ua3b8\ua111"}, + {"44743.18957170139", "[$-78]mmmmm dd yyyy h:mm AM/PM", "\ua3c3 01 2022 4:32 \ua3b8\ua111"}, + {"44774.18957170139", "[$-78]mmmmm dd yyyy h:mm AM/PM", "\ua246 01 2022 4:32 \ua3b8\ua111"}, + {"44805.18957170139", "[$-78]mmmmm dd yyyy h:mm AM/PM", "\ua22c 01 2022 4:32 \ua3b8\ua111"}, + {"44835.18957170139", "[$-78]mmmmm dd yyyy h:mm AM/PM", "\ua2b0 01 2022 4:32 \ua3b8\ua111"}, + {"44866.18957170139", "[$-78]mmmmm dd yyyy h:mm AM/PM", "\ua2b0 01 2022 4:32 \ua3b8\ua111"}, + {"44896.18957170139", "[$-78]mmmmm dd yyyy h:mm AM/PM", "\ua2b0 01 2022 4:32 \ua3b8\ua111"}, + {"44562.189571759256", "[$-478]mmm dd yyyy h:mm AM/PM", "\ua2cd\ua1aa 01 2022 4:32 \ua3b8\ua111"}, + {"44593.189571759256", "[$-478]mmm dd yyyy h:mm AM/PM", "\ua44d\ua1aa 01 2022 4:32 \ua3b8\ua111"}, + {"44621.18957170139", "[$-478]mmm dd yyyy h:mm AM/PM", "\ua315\ua1aa 01 2022 4:32 \ua3b8\ua111"}, + {"44652.18957170139", "[$-478]mmm dd yyyy h:mm AM/PM", "\ua1d6\ua1aa 01 2022 4:32 \ua3b8\ua111"}, + {"44682.18957170139", "[$-478]mmm dd yyyy h:mm AM/PM", "\ua26c\ua1aa 01 2022 4:32 \ua3b8\ua111"}, + {"44713.18957170139", "[$-478]mmm dd yyyy h:mm AM/PM", "\ua0d8\ua1aa 01 2022 4:32 \ua3b8\ua111"}, + {"44743.18957170139", "[$-478]mmm dd yyyy h:mm AM/PM", "\ua3c3\ua1aa 01 2022 4:32 \ua3b8\ua111"}, + {"44774.18957170139", "[$-478]mmm dd yyyy h:mm AM/PM", "\ua246\ua1aa 01 2022 4:32 \ua3b8\ua111"}, + {"44805.18957170139", "[$-478]mmm dd yyyy h:mm AM/PM", "\ua22c\ua1aa 01 2022 4:32 \ua3b8\ua111"}, + {"44835.18957170139", "[$-478]mmm dd yyyy h:mm AM/PM", "\ua2b0\ua1aa 01 2022 4:32 \ua3b8\ua111"}, + {"44866.18957170139", "[$-478]mmm dd yyyy h:mm AM/PM", "\ua2b0\ua2aa\ua1aa 01 2022 4:32 \ua3b8\ua111"}, + {"44896.18957170139", "[$-478]mmm dd yyyy h:mm AM/PM", "\ua2b0\ua44b\ua1aa 01 2022 4:32 \ua3b8\ua111"}, + {"44562.189571759256", "[$-478]mmmm dd yyyy h:mm AM/PM", "\ua2cd\ua1aa 01 2022 4:32 \ua3b8\ua111"}, + {"44593.189571759256", "[$-478]mmmm dd yyyy h:mm AM/PM", "\ua44d\ua1aa 01 2022 4:32 \ua3b8\ua111"}, + {"44621.18957170139", "[$-478]mmmm dd yyyy h:mm AM/PM", "\ua315\ua1aa 01 2022 4:32 \ua3b8\ua111"}, + {"44652.18957170139", "[$-478]mmmm dd yyyy h:mm AM/PM", "\ua1d6\ua1aa 01 2022 4:32 \ua3b8\ua111"}, + {"44682.18957170139", "[$-478]mmmm dd yyyy h:mm AM/PM", "\ua26c\ua1aa 01 2022 4:32 \ua3b8\ua111"}, + {"44713.18957170139", "[$-478]mmmm dd yyyy h:mm AM/PM", "\ua0d8\ua1aa 01 2022 4:32 \ua3b8\ua111"}, + {"44743.18957170139", "[$-478]mmmm dd yyyy h:mm AM/PM", "\ua3c3\ua1aa 01 2022 4:32 \ua3b8\ua111"}, + {"44774.18957170139", "[$-478]mmmm dd yyyy h:mm AM/PM", "\ua246\ua1aa 01 2022 4:32 \ua3b8\ua111"}, + {"44805.18957170139", "[$-478]mmmm dd yyyy h:mm AM/PM", "\ua22c\ua1aa 01 2022 4:32 \ua3b8\ua111"}, + {"44835.18957170139", "[$-478]mmmm dd yyyy h:mm AM/PM", "\ua2b0\ua1aa 01 2022 4:32 \ua3b8\ua111"}, + {"44866.18957170139", "[$-478]mmmm dd yyyy h:mm AM/PM", "\ua2b0\ua2aa\ua1aa 01 2022 4:32 \ua3b8\ua111"}, + {"44896.18957170139", "[$-478]mmmm dd yyyy h:mm AM/PM", "\ua2b0\ua44b\ua1aa 01 2022 4:32 \ua3b8\ua111"}, + {"44562.189571759256", "[$-478]mmmmm dd yyyy h:mm AM/PM", "\ua2cd 01 2022 4:32 \ua3b8\ua111"}, + {"44593.189571759256", "[$-478]mmmmm dd yyyy h:mm AM/PM", "\ua44d 01 2022 4:32 \ua3b8\ua111"}, + {"44621.18957170139", "[$-478]mmmmm dd yyyy h:mm AM/PM", "\ua315 01 2022 4:32 \ua3b8\ua111"}, + {"44652.18957170139", "[$-478]mmmmm dd yyyy h:mm AM/PM", "\ua1d6 01 2022 4:32 \ua3b8\ua111"}, + {"44682.18957170139", "[$-478]mmmmm dd yyyy h:mm AM/PM", "\ua26c 01 2022 4:32 \ua3b8\ua111"}, + {"44713.18957170139", "[$-478]mmmmm dd yyyy h:mm AM/PM", "\ua0d8 01 2022 4:32 \ua3b8\ua111"}, + {"44743.18957170139", "[$-478]mmmmm dd yyyy h:mm AM/PM", "\ua3c3 01 2022 4:32 \ua3b8\ua111"}, + {"44774.18957170139", "[$-478]mmmmm dd yyyy h:mm AM/PM", "\ua246 01 2022 4:32 \ua3b8\ua111"}, + {"44805.18957170139", "[$-478]mmmmm dd yyyy h:mm AM/PM", "\ua22c 01 2022 4:32 \ua3b8\ua111"}, + {"44835.18957170139", "[$-478]mmmmm dd yyyy h:mm AM/PM", "\ua2b0 01 2022 4:32 \ua3b8\ua111"}, + {"44866.18957170139", "[$-478]mmmmm dd yyyy h:mm AM/PM", "\ua2b0 01 2022 4:32 \ua3b8\ua111"}, + {"44896.18957170139", "[$-478]mmmmm dd yyyy h:mm AM/PM", "\ua2b0 01 2022 4:32 \ua3b8\ua111"}, + {"44562.189571759256", "[$-35]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"}, + {"44593.189571759256", "[$-35]mmm dd yyyy h:mm AM/PM", "Feb 01 2022 4:32 AM"}, + {"44621.18957170139", "[$-35]mmm dd yyyy h:mm AM/PM", "Mas 01 2022 4:32 AM"}, + {"44652.18957170139", "[$-35]mmm dd yyyy h:mm AM/PM", "Eph 01 2022 4:32 AM"}, + {"44682.18957170139", "[$-35]mmm dd yyyy h:mm AM/PM", "Mey 01 2022 4:32 AM"}, + {"44713.18957170139", "[$-35]mmm dd yyyy h:mm AM/PM", "Jun 01 2022 4:32 AM"}, + {"44743.18957170139", "[$-35]mmm dd yyyy h:mm AM/PM", "Jul 01 2022 4:32 AM"}, + {"44774.18957170139", "[$-35]mmm dd yyyy h:mm AM/PM", "Agas 01 2022 4:32 AM"}, + {"44805.18957170139", "[$-35]mmm dd yyyy h:mm AM/PM", "Sep 01 2022 4:32 AM"}, + {"44835.18957170139", "[$-35]mmm dd yyyy h:mm AM/PM", "Okt 01 2022 4:32 AM"}, + {"44866.18957170139", "[$-35]mmm dd yyyy h:mm AM/PM", "Nov 01 2022 4:32 AM"}, + {"44896.18957170139", "[$-35]mmm dd yyyy h:mm AM/PM", "Dis 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-35]mmmm dd yyyy h:mm AM/PM", "Januwari 01 2022 4:32 AM"}, + {"44593.189571759256", "[$-35]mmmm dd yyyy h:mm AM/PM", "Febhuwari 01 2022 4:32 AM"}, + {"44621.18957170139", "[$-35]mmmm dd yyyy h:mm AM/PM", "Mashi 01 2022 4:32 AM"}, + {"44652.18957170139", "[$-35]mmmm dd yyyy h:mm AM/PM", "Ephreli 01 2022 4:32 AM"}, + {"44682.18957170139", "[$-35]mmmm dd yyyy h:mm AM/PM", "Meyi 01 2022 4:32 AM"}, + {"44713.18957170139", "[$-35]mmmm dd yyyy h:mm AM/PM", "Juni 01 2022 4:32 AM"}, + {"44743.18957170139", "[$-35]mmmm dd yyyy h:mm AM/PM", "Julayi 01 2022 4:32 AM"}, + {"44774.18957170139", "[$-35]mmmm dd yyyy h:mm AM/PM", "Agasti 01 2022 4:32 AM"}, + {"44805.18957170139", "[$-35]mmmm dd yyyy h:mm AM/PM", "Septemba 01 2022 4:32 AM"}, + {"44835.18957170139", "[$-35]mmmm dd yyyy h:mm AM/PM", "Okthoba 01 2022 4:32 AM"}, + {"44866.18957170139", "[$-35]mmmm dd yyyy h:mm AM/PM", "Novemba 01 2022 4:32 AM"}, + {"44896.18957170139", "[$-35]mmmm dd yyyy h:mm AM/PM", "Disemba 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-35]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"}, + {"44593.189571759256", "[$-35]mmmmm dd yyyy h:mm AM/PM", "F 01 2022 4:32 AM"}, + {"44621.18957170139", "[$-35]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 AM"}, + {"44652.18957170139", "[$-35]mmmmm dd yyyy h:mm AM/PM", "E 01 2022 4:32 AM"}, + {"44682.18957170139", "[$-35]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 AM"}, + {"44713.18957170139", "[$-35]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"}, + {"44743.18957170139", "[$-35]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"}, + {"44774.18957170139", "[$-35]mmmmm dd yyyy h:mm AM/PM", "A 01 2022 4:32 AM"}, + {"44805.18957170139", "[$-35]mmmmm dd yyyy h:mm AM/PM", "S 01 2022 4:32 AM"}, + {"44835.18957170139", "[$-35]mmmmm dd yyyy h:mm AM/PM", "O 01 2022 4:32 AM"}, + {"44866.18957170139", "[$-35]mmmmm dd yyyy h:mm AM/PM", "N 01 2022 4:32 AM"}, + {"44896.18957170139", "[$-35]mmmmm dd yyyy h:mm AM/PM", "D 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-435]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"}, + {"44593.189571759256", "[$-435]mmm dd yyyy h:mm AM/PM", "Feb 01 2022 4:32 AM"}, + {"44621.18957170139", "[$-435]mmm dd yyyy h:mm AM/PM", "Mas 01 2022 4:32 AM"}, + {"44652.18957170139", "[$-435]mmm dd yyyy h:mm AM/PM", "Eph 01 2022 4:32 AM"}, + {"44682.18957170139", "[$-435]mmm dd yyyy h:mm AM/PM", "Mey 01 2022 4:32 AM"}, + {"44713.18957170139", "[$-435]mmm dd yyyy h:mm AM/PM", "Jun 01 2022 4:32 AM"}, + {"44743.18957170139", "[$-435]mmm dd yyyy h:mm AM/PM", "Jul 01 2022 4:32 AM"}, + {"44774.18957170139", "[$-435]mmm dd yyyy h:mm AM/PM", "Agas 01 2022 4:32 AM"}, + {"44805.18957170139", "[$-435]mmm dd yyyy h:mm AM/PM", "Sep 01 2022 4:32 AM"}, + {"44835.18957170139", "[$-435]mmm dd yyyy h:mm AM/PM", "Okt 01 2022 4:32 AM"}, + {"44866.18957170139", "[$-435]mmm dd yyyy h:mm AM/PM", "Nov 01 2022 4:32 AM"}, + {"44896.18957170139", "[$-435]mmm dd yyyy h:mm AM/PM", "Dis 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-435]mmmm dd yyyy h:mm AM/PM", "Januwari 01 2022 4:32 AM"}, + {"44593.189571759256", "[$-435]mmmm dd yyyy h:mm AM/PM", "Febhuwari 01 2022 4:32 AM"}, + {"44621.18957170139", "[$-435]mmmm dd yyyy h:mm AM/PM", "Mashi 01 2022 4:32 AM"}, + {"44652.18957170139", "[$-435]mmmm dd yyyy h:mm AM/PM", "Ephreli 01 2022 4:32 AM"}, + {"44682.18957170139", "[$-435]mmmm dd yyyy h:mm AM/PM", "Meyi 01 2022 4:32 AM"}, + {"44713.18957170139", "[$-435]mmmm dd yyyy h:mm AM/PM", "Juni 01 2022 4:32 AM"}, + {"44743.18957170139", "[$-435]mmmm dd yyyy h:mm AM/PM", "Julayi 01 2022 4:32 AM"}, + {"44774.18957170139", "[$-435]mmmm dd yyyy h:mm AM/PM", "Agasti 01 2022 4:32 AM"}, + {"44805.18957170139", "[$-435]mmmm dd yyyy h:mm AM/PM", "Septemba 01 2022 4:32 AM"}, + {"44835.18957170139", "[$-435]mmmm dd yyyy h:mm AM/PM", "Okthoba 01 2022 4:32 AM"}, + {"44866.18957170139", "[$-435]mmmm dd yyyy h:mm AM/PM", "Novemba 01 2022 4:32 AM"}, + {"44896.18957170139", "[$-435]mmmm dd yyyy h:mm AM/PM", "Disemba 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-435]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"}, + {"44593.189571759256", "[$-435]mmmmm dd yyyy h:mm AM/PM", "F 01 2022 4:32 AM"}, + {"44621.18957170139", "[$-435]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 AM"}, + {"44652.18957170139", "[$-435]mmmmm dd yyyy h:mm AM/PM", "E 01 2022 4:32 AM"}, + {"44682.18957170139", "[$-435]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 AM"}, + {"44713.18957170139", "[$-435]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"}, + {"44743.18957170139", "[$-435]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"}, + {"44774.18957170139", "[$-435]mmmmm dd yyyy h:mm AM/PM", "A 01 2022 4:32 AM"}, + {"44805.18957170139", "[$-435]mmmmm dd yyyy h:mm AM/PM", "S 01 2022 4:32 AM"}, + {"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"}, } { result := format(item[0], item[1]) assert.Equal(t, item[2], result, item) From e84130e55cdd172f9d07beaa563053aee40f645e Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 23 Feb 2022 22:42:07 +0800 Subject: [PATCH 540/957] Improvement local month name and AM/PM format support in number format * Support for the Vietnamese, Wolof and Xhosa --- numfmt.go | 67 ++++++++++++++- numfmt_test.go | 216 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 282 insertions(+), 1 deletion(-) diff --git a/numfmt.go b/numfmt.go index 0e1c522c18..c036b31ffe 100644 --- a/numfmt.go +++ b/numfmt.go @@ -155,6 +155,12 @@ var ( "41F": {tags: []string{"tr-TR"}, localMonth: localMonthsNameTurkish, apFmt: apFmtTurkish}, "52": {tags: []string{"cy"}, localMonth: localMonthsNameWelsh, apFmt: apFmtWelsh}, "452": {tags: []string{"cy-GB"}, localMonth: localMonthsNameWelsh, apFmt: apFmtWelsh}, + "2A": {tags: []string{"vi"}, localMonth: localMonthsNameVietnamese, apFmt: apFmtVietnamese}, + "42A": {tags: []string{"vi-VN"}, localMonth: localMonthsNameVietnamese, apFmt: apFmtVietnamese}, + "88": {tags: []string{"wo"}, localMonth: localMonthsNameWolof, apFmt: apFmtWolof}, + "488": {tags: []string{"wo-SN"}, localMonth: localMonthsNameWolof, apFmt: apFmtWolof}, + "34": {tags: []string{"xh"}, localMonth: localMonthsNameXhosa, apFmt: nfp.AmPm[0]}, + "434": {tags: []string{"xh-ZA"}, localMonth: localMonthsNameXhosa, apFmt: nfp.AmPm[0]}, "78": {tags: []string{"ii"}, localMonth: localMonthsNameYi, apFmt: apFmtYi}, "478": {tags: []string{"ii-CN"}, localMonth: localMonthsNameYi, apFmt: apFmtYi}, "35": {tags: []string{"zu"}, localMonth: localMonthsNameZulu, apFmt: nfp.AmPm[0]}, @@ -212,6 +218,10 @@ var ( monthNamesTurkish = []string{"Ocak", "Şubat", "Mart", "Nisan", "Mayıs", "Haziran", "Temmuz", "Ağustos", "Eylül", "Ekim", "Kasım", "Aralık"} // monthNamesWelsh list the month names in the Welsh. 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"} + // monthNamesXhosa list the month names in the Xhosa. + monthNamesXhosa = []string{"Januwari", "Febuwari", "Matshi", "Aprili", "Meyi", "Juni", "Julayi", "Agasti", "Septemba", "Oktobha", "Novemba", "Disemba"} // 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. @@ -230,7 +240,11 @@ var ( apFmtSpanish = "a. m./p. m." // apFmtTurkish defined the AM/PM name in the Turkish. apFmtTurkish = "\u00F6\u00F6/\u00F6\u0053" - // apFmtTurkish defined the AM/PM name in the Yi. + // apFmtVietnamese defined the AM/PM name in the Vietnamese. + apFmtVietnamese = "SA/CH" + // apFmtWolof defined the AM/PM name in the Wolof. + apFmtWolof = "Sub/Ngo" + // apFmtYi defined the AM/PM name in the Yi. apFmtYi = "\ua3b8\ua111/\ua06f\ua2d2" // apFmtWelsh defined the AM/PM name in the Welsh. apFmtWelsh = "yb/yh" @@ -533,6 +547,57 @@ func localMonthsNameWelsh(t time.Time, abbr int) string { return string([]rune(monthNamesWelsh[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())) + } + if abbr == 5 { + return "T " + strconv.Itoa(int(t.Month())) + } + return "Tháng " + strconv.Itoa(int(t.Month())) +} + +// 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]) + "." + } + } + if abbr == 4 { + return monthNamesWolof[int(t.Month())-1] + } + return string([]rune(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]) + "." + } + } + if abbr == 4 { + return "u" + monthNamesXhosa[int(t.Month())-1] + } + return "u" +} + // localMonthsNameYi returns the Yi name of the month. func localMonthsNameYi(t time.Time, abbr int) string { if abbr == 3 || abbr == 4 { diff --git a/numfmt_test.go b/numfmt_test.go index b60e2b62dc..2969e75976 100644 --- a/numfmt_test.go +++ b/numfmt_test.go @@ -472,6 +472,78 @@ func TestNumFmt(t *testing.T) { {"44835.18957170139", "[$-41F]mmmmm dd yyyy h:mm AM/PM", "E 01 2022 4:32 \u00F6\u00F6"}, {"44866.18957170139", "[$-41F]mmmmm dd yyyy h:mm AM/PM", "K 01 2022 4:32 \u00F6\u00F6"}, {"44896.18957170139", "[$-41F]mmmmm dd yyyy h:mm AM/PM", "A 01 2022 4:32 \u00F6\u00F6"}, + {"44562.189571759256", "[$-2A]mmm dd yyyy h:mm AM/PM", "Thg 1 01 2022 4:32 SA"}, + {"44593.189571759256", "[$-2A]mmm dd yyyy h:mm AM/PM", "Thg 2 01 2022 4:32 SA"}, + {"44621.18957170139", "[$-2A]mmm dd yyyy h:mm AM/PM", "Thg 3 01 2022 4:32 SA"}, + {"44652.18957170139", "[$-2A]mmm dd yyyy h:mm AM/PM", "Thg 4 01 2022 4:32 SA"}, + {"44682.18957170139", "[$-2A]mmm dd yyyy h:mm AM/PM", "Thg 5 01 2022 4:32 SA"}, + {"44713.18957170139", "[$-2A]mmm dd yyyy h:mm AM/PM", "Thg 6 01 2022 4:32 SA"}, + {"44743.18957170139", "[$-2A]mmm dd yyyy h:mm AM/PM", "Thg 7 01 2022 4:32 SA"}, + {"44774.18957170139", "[$-2A]mmm dd yyyy h:mm AM/PM", "Thg 8 01 2022 4:32 SA"}, + {"44805.18957170139", "[$-2A]mmm dd yyyy h:mm AM/PM", "Thg 9 01 2022 4:32 SA"}, + {"44835.18957170139", "[$-2A]mmm dd yyyy h:mm AM/PM", "Thg 10 01 2022 4:32 SA"}, + {"44866.18957170139", "[$-2A]mmm dd yyyy h:mm AM/PM", "Thg 11 01 2022 4:32 SA"}, + {"44896.18957170139", "[$-2A]mmm dd yyyy h:mm AM/PM", "Thg 12 01 2022 4:32 SA"}, + {"44562.189571759256", "[$-2A]mmmm dd yyyy h:mm AM/PM", "Tháng 1 01 2022 4:32 SA"}, + {"44593.189571759256", "[$-2A]mmmm dd yyyy h:mm AM/PM", "Tháng 2 01 2022 4:32 SA"}, + {"44621.18957170139", "[$-2A]mmmm dd yyyy h:mm AM/PM", "Tháng 3 01 2022 4:32 SA"}, + {"44652.18957170139", "[$-2A]mmmm dd yyyy h:mm AM/PM", "Tháng 4 01 2022 4:32 SA"}, + {"44682.18957170139", "[$-2A]mmmm dd yyyy h:mm AM/PM", "Tháng 5 01 2022 4:32 SA"}, + {"44713.18957170139", "[$-2A]mmmm dd yyyy h:mm AM/PM", "Tháng 6 01 2022 4:32 SA"}, + {"44743.18957170139", "[$-2A]mmmm dd yyyy h:mm AM/PM", "Tháng 7 01 2022 4:32 SA"}, + {"44774.18957170139", "[$-2A]mmmm dd yyyy h:mm AM/PM", "Tháng 8 01 2022 4:32 SA"}, + {"44805.18957170139", "[$-2A]mmmm dd yyyy h:mm AM/PM", "Tháng 9 01 2022 4:32 SA"}, + {"44835.18957170139", "[$-2A]mmmm dd yyyy h:mm AM/PM", "Tháng 10 01 2022 4:32 SA"}, + {"44866.18957170139", "[$-2A]mmmm dd yyyy h:mm AM/PM", "Tháng 11 01 2022 4:32 SA"}, + {"44896.18957170139", "[$-2A]mmmm dd yyyy h:mm AM/PM", "Tháng 12 01 2022 4:32 SA"}, + {"44562.189571759256", "[$-2A]mmmmm dd yyyy h:mm AM/PM", "T 1 01 2022 4:32 SA"}, + {"44593.189571759256", "[$-2A]mmmmm dd yyyy h:mm AM/PM", "T 2 01 2022 4:32 SA"}, + {"44621.18957170139", "[$-2A]mmmmm dd yyyy h:mm AM/PM", "T 3 01 2022 4:32 SA"}, + {"44652.18957170139", "[$-2A]mmmmm dd yyyy h:mm AM/PM", "T 4 01 2022 4:32 SA"}, + {"44682.18957170139", "[$-2A]mmmmm dd yyyy h:mm AM/PM", "T 5 01 2022 4:32 SA"}, + {"44713.18957170139", "[$-2A]mmmmm dd yyyy h:mm AM/PM", "T 6 01 2022 4:32 SA"}, + {"44743.18957170139", "[$-2A]mmmmm dd yyyy h:mm AM/PM", "T 7 01 2022 4:32 SA"}, + {"44774.18957170139", "[$-2A]mmmmm dd yyyy h:mm AM/PM", "T 8 01 2022 4:32 SA"}, + {"44805.18957170139", "[$-2A]mmmmm dd yyyy h:mm AM/PM", "T 9 01 2022 4:32 SA"}, + {"44835.18957170139", "[$-2A]mmmmm dd yyyy h:mm AM/PM", "T 10 01 2022 4:32 SA"}, + {"44866.18957170139", "[$-2A]mmmmm dd yyyy h:mm AM/PM", "T 11 01 2022 4:32 SA"}, + {"44896.18957170139", "[$-2A]mmmmm dd yyyy h:mm AM/PM", "T 12 01 2022 4:32 SA"}, + {"44562.189571759256", "[$-42A]mmm dd yyyy h:mm AM/PM", "Thg 1 01 2022 4:32 SA"}, + {"44593.189571759256", "[$-42A]mmm dd yyyy h:mm AM/PM", "Thg 2 01 2022 4:32 SA"}, + {"44621.18957170139", "[$-42A]mmm dd yyyy h:mm AM/PM", "Thg 3 01 2022 4:32 SA"}, + {"44652.18957170139", "[$-42A]mmm dd yyyy h:mm AM/PM", "Thg 4 01 2022 4:32 SA"}, + {"44682.18957170139", "[$-42A]mmm dd yyyy h:mm AM/PM", "Thg 5 01 2022 4:32 SA"}, + {"44713.18957170139", "[$-42A]mmm dd yyyy h:mm AM/PM", "Thg 6 01 2022 4:32 SA"}, + {"44743.18957170139", "[$-42A]mmm dd yyyy h:mm AM/PM", "Thg 7 01 2022 4:32 SA"}, + {"44774.18957170139", "[$-42A]mmm dd yyyy h:mm AM/PM", "Thg 8 01 2022 4:32 SA"}, + {"44805.18957170139", "[$-42A]mmm dd yyyy h:mm AM/PM", "Thg 9 01 2022 4:32 SA"}, + {"44835.18957170139", "[$-42A]mmm dd yyyy h:mm AM/PM", "Thg 10 01 2022 4:32 SA"}, + {"44866.18957170139", "[$-42A]mmm dd yyyy h:mm AM/PM", "Thg 11 01 2022 4:32 SA"}, + {"44896.18957170139", "[$-42A]mmm dd yyyy h:mm AM/PM", "Thg 12 01 2022 4:32 SA"}, + {"44562.189571759256", "[$-42A]mmmm dd yyyy h:mm AM/PM", "Tháng 1 01 2022 4:32 SA"}, + {"44593.189571759256", "[$-42A]mmmm dd yyyy h:mm AM/PM", "Tháng 2 01 2022 4:32 SA"}, + {"44621.18957170139", "[$-42A]mmmm dd yyyy h:mm AM/PM", "Tháng 3 01 2022 4:32 SA"}, + {"44652.18957170139", "[$-42A]mmmm dd yyyy h:mm AM/PM", "Tháng 4 01 2022 4:32 SA"}, + {"44682.18957170139", "[$-42A]mmmm dd yyyy h:mm AM/PM", "Tháng 5 01 2022 4:32 SA"}, + {"44713.18957170139", "[$-42A]mmmm dd yyyy h:mm AM/PM", "Tháng 6 01 2022 4:32 SA"}, + {"44743.18957170139", "[$-42A]mmmm dd yyyy h:mm AM/PM", "Tháng 7 01 2022 4:32 SA"}, + {"44774.18957170139", "[$-42A]mmmm dd yyyy h:mm AM/PM", "Tháng 8 01 2022 4:32 SA"}, + {"44805.18957170139", "[$-42A]mmmm dd yyyy h:mm AM/PM", "Tháng 9 01 2022 4:32 SA"}, + {"44835.18957170139", "[$-42A]mmmm dd yyyy h:mm AM/PM", "Tháng 10 01 2022 4:32 SA"}, + {"44866.18957170139", "[$-42A]mmmm dd yyyy h:mm AM/PM", "Tháng 11 01 2022 4:32 SA"}, + {"44896.18957170139", "[$-42A]mmmm dd yyyy h:mm AM/PM", "Tháng 12 01 2022 4:32 SA"}, + {"44562.189571759256", "[$-42A]mmmmm dd yyyy h:mm AM/PM", "T 1 01 2022 4:32 SA"}, + {"44593.189571759256", "[$-42A]mmmmm dd yyyy h:mm AM/PM", "T 2 01 2022 4:32 SA"}, + {"44621.18957170139", "[$-42A]mmmmm dd yyyy h:mm AM/PM", "T 3 01 2022 4:32 SA"}, + {"44652.18957170139", "[$-42A]mmmmm dd yyyy h:mm AM/PM", "T 4 01 2022 4:32 SA"}, + {"44682.18957170139", "[$-42A]mmmmm dd yyyy h:mm AM/PM", "T 5 01 2022 4:32 SA"}, + {"44713.18957170139", "[$-42A]mmmmm dd yyyy h:mm AM/PM", "T 6 01 2022 4:32 SA"}, + {"44743.18957170139", "[$-42A]mmmmm dd yyyy h:mm AM/PM", "T 7 01 2022 4:32 SA"}, + {"44774.18957170139", "[$-42A]mmmmm dd yyyy h:mm AM/PM", "T 8 01 2022 4:32 SA"}, + {"44805.18957170139", "[$-42A]mmmmm dd yyyy h:mm AM/PM", "T 9 01 2022 4:32 SA"}, + {"44835.18957170139", "[$-42A]mmmmm dd yyyy h:mm AM/PM", "T 10 01 2022 4:32 SA"}, + {"44866.18957170139", "[$-42A]mmmmm dd yyyy h:mm AM/PM", "T 11 01 2022 4:32 SA"}, + {"44896.18957170139", "[$-42A]mmmmm dd yyyy h:mm AM/PM", "T 12 01 2022 4:32 SA"}, {"44562.189571759256", "[$-52]mmm dd yyyy h:mm AM/PM", "Ion 01 2022 4:32 yb"}, {"44593.189571759256", "[$-52]mmm dd yyyy h:mm AM/PM", "Chwef 01 2022 4:32 yb"}, {"44621.18957170139", "[$-52]mmm dd yyyy h:mm AM/PM", "Maw 01 2022 4:32 yb"}, @@ -544,6 +616,150 @@ func TestNumFmt(t *testing.T) { {"44835.18957170139", "[$-452]mmmmm dd yyyy h:mm AM/PM", "H 01 2022 4:32 yb"}, {"44866.18957170139", "[$-452]mmmmm dd yyyy h:mm AM/PM", "T 01 2022 4:32 yb"}, {"44896.18957170139", "[$-452]mmmmm dd yyyy h:mm AM/PM", "R 01 2022 4:32 yb"}, + {"44562.189571759256", "[$-88]mmm dd yyyy h:mm AM/PM", "Sam. 01 2022 4:32 Sub"}, + {"44593.189571759256", "[$-88]mmm dd yyyy h:mm AM/PM", "Few. 01 2022 4:32 Sub"}, + {"44621.18957170139", "[$-88]mmm dd yyyy h:mm AM/PM", "Maa 01 2022 4:32 Sub"}, + {"44652.18957170139", "[$-88]mmm dd yyyy h:mm AM/PM", "Awr. 01 2022 4:32 Sub"}, + {"44682.18957170139", "[$-88]mmm dd yyyy h:mm AM/PM", "Me 01 2022 4:32 Sub"}, + {"44713.18957170139", "[$-88]mmm dd yyyy h:mm AM/PM", "Suw 01 2022 4:32 Sub"}, + {"44743.18957170139", "[$-88]mmm dd yyyy h:mm AM/PM", "Sul. 01 2022 4:32 Sub"}, + {"44774.18957170139", "[$-88]mmm dd yyyy h:mm AM/PM", "Ut 01 2022 4:32 Sub"}, + {"44805.18957170139", "[$-88]mmm dd yyyy h:mm AM/PM", "Sept. 01 2022 4:32 Sub"}, + {"44835.18957170139", "[$-88]mmm dd yyyy h:mm AM/PM", "Okt. 01 2022 4:32 Sub"}, + {"44866.18957170139", "[$-88]mmm dd yyyy h:mm AM/PM", "Now. 01 2022 4:32 Sub"}, + {"44896.18957170139", "[$-88]mmm dd yyyy h:mm AM/PM", "Des. 01 2022 4:32 Sub"}, + {"44562.189571759256", "[$-88]mmmm dd yyyy h:mm AM/PM", "Samwiye 01 2022 4:32 Sub"}, + {"44593.189571759256", "[$-88]mmmm dd yyyy h:mm AM/PM", "Fewriye 01 2022 4:32 Sub"}, + {"44621.18957170139", "[$-88]mmmm dd yyyy h:mm AM/PM", "Maars 01 2022 4:32 Sub"}, + {"44652.18957170139", "[$-88]mmmm dd yyyy h:mm AM/PM", "Awril 01 2022 4:32 Sub"}, + {"44682.18957170139", "[$-88]mmmm dd yyyy h:mm AM/PM", "Me 01 2022 4:32 Sub"}, + {"44713.18957170139", "[$-88]mmmm dd yyyy h:mm AM/PM", "Suwe 01 2022 4:32 Sub"}, + {"44743.18957170139", "[$-88]mmmm dd yyyy h:mm AM/PM", "Sullet 01 2022 4:32 Sub"}, + {"44774.18957170139", "[$-88]mmmm dd yyyy h:mm AM/PM", "Ut 01 2022 4:32 Sub"}, + {"44805.18957170139", "[$-88]mmmm dd yyyy h:mm AM/PM", "Septàmbar 01 2022 4:32 Sub"}, + {"44835.18957170139", "[$-88]mmmm dd yyyy h:mm AM/PM", "Oktoobar 01 2022 4:32 Sub"}, + {"44866.18957170139", "[$-88]mmmm dd yyyy h:mm AM/PM", "Noowàmbar 01 2022 4:32 Sub"}, + {"44896.18957170139", "[$-88]mmmm dd yyyy h:mm AM/PM", "Desàmbar 01 2022 4:32 Sub"}, + {"44562.189571759256", "[$-88]mmmmm dd yyyy h:mm AM/PM", "S 01 2022 4:32 Sub"}, + {"44593.189571759256", "[$-88]mmmmm dd yyyy h:mm AM/PM", "F 01 2022 4:32 Sub"}, + {"44621.18957170139", "[$-88]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 Sub"}, + {"44652.18957170139", "[$-88]mmmmm dd yyyy h:mm AM/PM", "A 01 2022 4:32 Sub"}, + {"44682.18957170139", "[$-88]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 Sub"}, + {"44713.18957170139", "[$-88]mmmmm dd yyyy h:mm AM/PM", "S 01 2022 4:32 Sub"}, + {"44743.18957170139", "[$-88]mmmmm dd yyyy h:mm AM/PM", "S 01 2022 4:32 Sub"}, + {"44774.18957170139", "[$-88]mmmmm dd yyyy h:mm AM/PM", "U 01 2022 4:32 Sub"}, + {"44805.18957170139", "[$-88]mmmmm dd yyyy h:mm AM/PM", "S 01 2022 4:32 Sub"}, + {"44835.18957170139", "[$-88]mmmmm dd yyyy h:mm AM/PM", "O 01 2022 4:32 Sub"}, + {"44866.18957170139", "[$-88]mmmmm dd yyyy h:mm AM/PM", "N 01 2022 4:32 Sub"}, + {"44896.18957170139", "[$-88]mmmmm dd yyyy h:mm AM/PM", "D 01 2022 4:32 Sub"}, + {"44562.189571759256", "[$-488]mmm dd yyyy h:mm AM/PM", "Sam. 01 2022 4:32 Sub"}, + {"44593.189571759256", "[$-488]mmm dd yyyy h:mm AM/PM", "Few. 01 2022 4:32 Sub"}, + {"44621.18957170139", "[$-488]mmm dd yyyy h:mm AM/PM", "Maa 01 2022 4:32 Sub"}, + {"44652.18957170139", "[$-488]mmm dd yyyy h:mm AM/PM", "Awr. 01 2022 4:32 Sub"}, + {"44682.18957170139", "[$-488]mmm dd yyyy h:mm AM/PM", "Me 01 2022 4:32 Sub"}, + {"44713.18957170139", "[$-488]mmm dd yyyy h:mm AM/PM", "Suw 01 2022 4:32 Sub"}, + {"44743.18957170139", "[$-488]mmm dd yyyy h:mm AM/PM", "Sul. 01 2022 4:32 Sub"}, + {"44774.18957170139", "[$-488]mmm dd yyyy h:mm AM/PM", "Ut 01 2022 4:32 Sub"}, + {"44805.18957170139", "[$-488]mmm dd yyyy h:mm AM/PM", "Sept. 01 2022 4:32 Sub"}, + {"44835.18957170139", "[$-488]mmm dd yyyy h:mm AM/PM", "Okt. 01 2022 4:32 Sub"}, + {"44866.18957170139", "[$-488]mmm dd yyyy h:mm AM/PM", "Now. 01 2022 4:32 Sub"}, + {"44896.18957170139", "[$-488]mmm dd yyyy h:mm AM/PM", "Des. 01 2022 4:32 Sub"}, + {"44562.189571759256", "[$-488]mmmm dd yyyy h:mm AM/PM", "Samwiye 01 2022 4:32 Sub"}, + {"44593.189571759256", "[$-488]mmmm dd yyyy h:mm AM/PM", "Fewriye 01 2022 4:32 Sub"}, + {"44621.18957170139", "[$-488]mmmm dd yyyy h:mm AM/PM", "Maars 01 2022 4:32 Sub"}, + {"44652.18957170139", "[$-488]mmmm dd yyyy h:mm AM/PM", "Awril 01 2022 4:32 Sub"}, + {"44682.18957170139", "[$-488]mmmm dd yyyy h:mm AM/PM", "Me 01 2022 4:32 Sub"}, + {"44713.18957170139", "[$-488]mmmm dd yyyy h:mm AM/PM", "Suwe 01 2022 4:32 Sub"}, + {"44743.18957170139", "[$-488]mmmm dd yyyy h:mm AM/PM", "Sullet 01 2022 4:32 Sub"}, + {"44774.18957170139", "[$-488]mmmm dd yyyy h:mm AM/PM", "Ut 01 2022 4:32 Sub"}, + {"44805.18957170139", "[$-488]mmmm dd yyyy h:mm AM/PM", "Septàmbar 01 2022 4:32 Sub"}, + {"44835.18957170139", "[$-488]mmmm dd yyyy h:mm AM/PM", "Oktoobar 01 2022 4:32 Sub"}, + {"44866.18957170139", "[$-488]mmmm dd yyyy h:mm AM/PM", "Noowàmbar 01 2022 4:32 Sub"}, + {"44896.18957170139", "[$-488]mmmm dd yyyy h:mm AM/PM", "Desàmbar 01 2022 4:32 Sub"}, + {"44562.189571759256", "[$-488]mmmmm dd yyyy h:mm AM/PM", "S 01 2022 4:32 Sub"}, + {"44593.189571759256", "[$-488]mmmmm dd yyyy h:mm AM/PM", "F 01 2022 4:32 Sub"}, + {"44621.18957170139", "[$-488]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 Sub"}, + {"44652.18957170139", "[$-488]mmmmm dd yyyy h:mm AM/PM", "A 01 2022 4:32 Sub"}, + {"44682.18957170139", "[$-488]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 Sub"}, + {"44713.18957170139", "[$-488]mmmmm dd yyyy h:mm AM/PM", "S 01 2022 4:32 Sub"}, + {"44743.18957170139", "[$-488]mmmmm dd yyyy h:mm AM/PM", "S 01 2022 4:32 Sub"}, + {"44774.18957170139", "[$-488]mmmmm dd yyyy h:mm AM/PM", "U 01 2022 4:32 Sub"}, + {"44805.18957170139", "[$-488]mmmmm dd yyyy h:mm AM/PM", "S 01 2022 4:32 Sub"}, + {"44835.18957170139", "[$-488]mmmmm dd yyyy h:mm AM/PM", "O 01 2022 4:32 Sub"}, + {"44866.18957170139", "[$-488]mmmmm dd yyyy h:mm AM/PM", "N 01 2022 4:32 Sub"}, + {"44896.18957170139", "[$-488]mmmmm dd yyyy h:mm AM/PM", "D 01 2022 4:32 Sub"}, + {"44562.189571759256", "[$-34]mmm dd yyyy h:mm AM/PM", "uJan. 01 2022 4:32 AM"}, + {"44593.189571759256", "[$-34]mmm dd yyyy h:mm AM/PM", "uFeb. 01 2022 4:32 AM"}, + {"44621.18957170139", "[$-34]mmm dd yyyy h:mm AM/PM", "uMat. 01 2022 4:32 AM"}, + {"44652.18957170139", "[$-34]mmm dd yyyy h:mm AM/PM", "uEpr. 01 2022 4:32 AM"}, + {"44682.18957170139", "[$-34]mmm dd yyyy h:mm AM/PM", "uMey. 01 2022 4:32 AM"}, + {"44713.18957170139", "[$-34]mmm dd yyyy h:mm AM/PM", "uJun. 01 2022 4:32 AM"}, + {"44743.18957170139", "[$-34]mmm dd yyyy h:mm AM/PM", "uJul. 01 2022 4:32 AM"}, + {"44774.18957170139", "[$-34]mmm dd yyyy h:mm AM/PM", "uAg. 01 2022 4:32 AM"}, + {"44805.18957170139", "[$-34]mmm dd yyyy h:mm AM/PM", "uSep. 01 2022 4:32 AM"}, + {"44835.18957170139", "[$-34]mmm dd yyyy h:mm AM/PM", "uOkt. 01 2022 4:32 AM"}, + {"44866.18957170139", "[$-34]mmm dd yyyy h:mm AM/PM", "uNov. 01 2022 4:32 AM"}, + {"44896.18957170139", "[$-34]mmm dd yyyy h:mm AM/PM", "uDis. 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-34]mmmm dd yyyy h:mm AM/PM", "uJanuwari 01 2022 4:32 AM"}, + {"44593.189571759256", "[$-34]mmmm dd yyyy h:mm AM/PM", "uFebuwari 01 2022 4:32 AM"}, + {"44621.18957170139", "[$-34]mmmm dd yyyy h:mm AM/PM", "uMatshi 01 2022 4:32 AM"}, + {"44652.18957170139", "[$-34]mmmm dd yyyy h:mm AM/PM", "uAprili 01 2022 4:32 AM"}, + {"44682.18957170139", "[$-34]mmmm dd yyyy h:mm AM/PM", "uMeyi 01 2022 4:32 AM"}, + {"44713.18957170139", "[$-34]mmmm dd yyyy h:mm AM/PM", "uJuni 01 2022 4:32 AM"}, + {"44743.18957170139", "[$-34]mmmm dd yyyy h:mm AM/PM", "uJulayi 01 2022 4:32 AM"}, + {"44774.18957170139", "[$-34]mmmm dd yyyy h:mm AM/PM", "uAgasti 01 2022 4:32 AM"}, + {"44805.18957170139", "[$-34]mmmm dd yyyy h:mm AM/PM", "uSeptemba 01 2022 4:32 AM"}, + {"44835.18957170139", "[$-34]mmmm dd yyyy h:mm AM/PM", "uOktobha 01 2022 4:32 AM"}, + {"44866.18957170139", "[$-34]mmmm dd yyyy h:mm AM/PM", "uNovemba 01 2022 4:32 AM"}, + {"44896.18957170139", "[$-34]mmmm dd yyyy h:mm AM/PM", "uDisemba 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-34]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"}, + {"44593.189571759256", "[$-34]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"}, + {"44621.18957170139", "[$-34]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"}, + {"44652.18957170139", "[$-34]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"}, + {"44682.18957170139", "[$-34]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"}, + {"44713.18957170139", "[$-34]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"}, + {"44743.18957170139", "[$-34]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"}, + {"44774.18957170139", "[$-34]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"}, + {"44805.18957170139", "[$-34]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"}, + {"44835.18957170139", "[$-34]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"}, + {"44866.18957170139", "[$-34]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"}, + {"44896.18957170139", "[$-34]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-434]mmm dd yyyy h:mm AM/PM", "uJan. 01 2022 4:32 AM"}, + {"44593.189571759256", "[$-434]mmm dd yyyy h:mm AM/PM", "uFeb. 01 2022 4:32 AM"}, + {"44621.18957170139", "[$-434]mmm dd yyyy h:mm AM/PM", "uMat. 01 2022 4:32 AM"}, + {"44652.18957170139", "[$-434]mmm dd yyyy h:mm AM/PM", "uEpr. 01 2022 4:32 AM"}, + {"44682.18957170139", "[$-434]mmm dd yyyy h:mm AM/PM", "uMey. 01 2022 4:32 AM"}, + {"44713.18957170139", "[$-434]mmm dd yyyy h:mm AM/PM", "uJun. 01 2022 4:32 AM"}, + {"44743.18957170139", "[$-434]mmm dd yyyy h:mm AM/PM", "uJul. 01 2022 4:32 AM"}, + {"44774.18957170139", "[$-434]mmm dd yyyy h:mm AM/PM", "uAg. 01 2022 4:32 AM"}, + {"44805.18957170139", "[$-434]mmm dd yyyy h:mm AM/PM", "uSep. 01 2022 4:32 AM"}, + {"44835.18957170139", "[$-434]mmm dd yyyy h:mm AM/PM", "uOkt. 01 2022 4:32 AM"}, + {"44866.18957170139", "[$-434]mmm dd yyyy h:mm AM/PM", "uNov. 01 2022 4:32 AM"}, + {"44896.18957170139", "[$-434]mmm dd yyyy h:mm AM/PM", "uDis. 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-434]mmmm dd yyyy h:mm AM/PM", "uJanuwari 01 2022 4:32 AM"}, + {"44593.189571759256", "[$-434]mmmm dd yyyy h:mm AM/PM", "uFebuwari 01 2022 4:32 AM"}, + {"44621.18957170139", "[$-434]mmmm dd yyyy h:mm AM/PM", "uMatshi 01 2022 4:32 AM"}, + {"44652.18957170139", "[$-434]mmmm dd yyyy h:mm AM/PM", "uAprili 01 2022 4:32 AM"}, + {"44682.18957170139", "[$-434]mmmm dd yyyy h:mm AM/PM", "uMeyi 01 2022 4:32 AM"}, + {"44713.18957170139", "[$-434]mmmm dd yyyy h:mm AM/PM", "uJuni 01 2022 4:32 AM"}, + {"44743.18957170139", "[$-434]mmmm dd yyyy h:mm AM/PM", "uJulayi 01 2022 4:32 AM"}, + {"44774.18957170139", "[$-434]mmmm dd yyyy h:mm AM/PM", "uAgasti 01 2022 4:32 AM"}, + {"44805.18957170139", "[$-434]mmmm dd yyyy h:mm AM/PM", "uSeptemba 01 2022 4:32 AM"}, + {"44835.18957170139", "[$-434]mmmm dd yyyy h:mm AM/PM", "uOktobha 01 2022 4:32 AM"}, + {"44866.18957170139", "[$-434]mmmm dd yyyy h:mm AM/PM", "uNovemba 01 2022 4:32 AM"}, + {"44896.18957170139", "[$-434]mmmm dd yyyy h:mm AM/PM", "uDisemba 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-434]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"}, + {"44593.189571759256", "[$-434]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"}, + {"44621.18957170139", "[$-434]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"}, + {"44652.18957170139", "[$-434]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"}, + {"44682.18957170139", "[$-434]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"}, + {"44713.18957170139", "[$-434]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"}, + {"44743.18957170139", "[$-434]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"}, + {"44774.18957170139", "[$-434]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"}, + {"44805.18957170139", "[$-434]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"}, + {"44835.18957170139", "[$-434]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"}, + {"44866.18957170139", "[$-434]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"}, + {"44896.18957170139", "[$-434]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"}, {"44562.189571759256", "[$-78]mmm dd yyyy h:mm AM/PM", "\ua2cd\ua1aa 01 2022 4:32 \ua3b8\ua111"}, {"44593.189571759256", "[$-78]mmm dd yyyy h:mm AM/PM", "\ua44d\ua1aa 01 2022 4:32 \ua3b8\ua111"}, {"44621.18957170139", "[$-78]mmm dd yyyy h:mm AM/PM", "\ua315\ua1aa 01 2022 4:32 \ua3b8\ua111"}, From 3231817169051fd2cd3452d276447e488a3a70b9 Mon Sep 17 00:00:00 2001 From: wangxuliBY <100419125+wangxuliBY@users.noreply.github.com> Date: Fri, 25 Feb 2022 21:37:49 +0800 Subject: [PATCH 541/957] This fixed code review issue in PR #1154 (#1159) --- workbook.go | 22 ++++++++++++++++++++++ workbook_test.go | 16 +++++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/workbook.go b/workbook.go index a2263b2d38..3d9fa480b2 100644 --- a/workbook.go +++ b/workbook.go @@ -32,6 +32,11 @@ type WorkbookPrOptionPtr interface { getWorkbookPrOption(pr *xlsxWorkbookPr) } +type ( + // 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) { @@ -104,6 +109,7 @@ func (f *File) workBookWriter() { // SetWorkbookPrOptions provides a function to sets workbook properties. // // Available options: +// FilterPrivacy(bool) // CodeName(string) func (f *File) SetWorkbookPrOptions(opts ...WorkbookPrOption) error { wb := f.workbookReader() @@ -118,6 +124,11 @@ func (f *File) SetWorkbookPrOptions(opts ...WorkbookPrOption) error { return nil } +// 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) @@ -126,6 +137,7 @@ func (o CodeName) setWorkbookPrOption(pr *xlsxWorkbookPr) { // GetWorkbookPrOptions provides a function to gets workbook properties. // // Available options: +// FilterPrivacy(bool) // CodeName(string) func (f *File) GetWorkbookPrOptions(opts ...WorkbookPrOptionPtr) error { wb := f.workbookReader() @@ -136,6 +148,16 @@ func (f *File) GetWorkbookPrOptions(opts ...WorkbookPrOptionPtr) error { return nil } +// getWorkbookPrOption implements the WorkbookPrOption interface and get the +// filter privacy of thw 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 thw workbook. func (o *CodeName) getWorkbookPrOption(pr *xlsxWorkbookPr) { diff --git a/workbook_test.go b/workbook_test.go index 90fc3d706e..e31caf26ac 100644 --- a/workbook_test.go +++ b/workbook_test.go @@ -10,6 +10,7 @@ import ( func ExampleFile_SetWorkbookPrOptions() { f := NewFile() if err := f.SetWorkbookPrOptions( + FilterPrivacy(false), CodeName("code"), ); err != nil { fmt.Println(err) @@ -19,14 +20,22 @@ func ExampleFile_SetWorkbookPrOptions() { func ExampleFile_GetWorkbookPrOptions() { f := NewFile() - var codeName CodeName + var ( + filterPrivacy FilterPrivacy + codeName CodeName + ) + 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("- filterPrivacy: %t\n", filterPrivacy) fmt.Printf("- codeName: %q\n", codeName) // Output: // Defaults: + // - filterPrivacy: true // - codeName: "" } @@ -40,4 +49,9 @@ func TestWorkbookPr(t *testing.T) { 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)) } From 92764195dc548ab80f336f83a283000cd45eeb34 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 25 Feb 2022 22:24:08 +0800 Subject: [PATCH 542/957] This improvement number format support * Local month name and AM/PM format support Tibetan and Traditional Mongolian * Support text place holder --- numfmt.go | 57 +++++++++++++++++++++++++++++-- numfmt_test.go | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+), 2 deletions(-) diff --git a/numfmt.go b/numfmt.go index c036b31ffe..50ce1f313b 100644 --- a/numfmt.go +++ b/numfmt.go @@ -46,6 +46,7 @@ var ( nfp.TokenTypeElapsedDateTimes, nfp.TokenTypeGeneral, nfp.TokenTypeLiteral, + nfp.TokenTypeTextPlaceHolder, nfp.TokenSubTypeLanguageInfo, } // supportedLanguageInfo directly maps the supported language ID and tags. @@ -135,6 +136,9 @@ var ( "411": {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese}, "12": {tags: []string{"ko"}, localMonth: localMonthsNameKorean, apFmt: apFmtKorean}, "412": {tags: []string{"ko-KR"}, localMonth: localMonthsNameKorean, apFmt: apFmtKorean}, + "7C50": {tags: []string{"mn-Mong"}, localMonth: localMonthsNameTraditionalMongolian, apFmt: nfp.AmPm[0]}, + "850": {tags: []string{"mn-Mong-CN"}, localMonth: localMonthsNameTraditionalMongolian, apFmt: nfp.AmPm[0]}, + "C50": {tags: []string{"mn-Mong-MN"}, localMonth: localMonthsNameTraditionalMongolian, apFmt: nfp.AmPm[0]}, "19": {tags: []string{"ru"}, localMonth: localMonthsNameRussian, apFmt: nfp.AmPm[0]}, "819": {tags: []string{"ru-MD"}, localMonth: localMonthsNameRussian, apFmt: nfp.AmPm[0]}, "419": {tags: []string{"ru-RU"}, localMonth: localMonthsNameRussian, apFmt: nfp.AmPm[0]}, @@ -151,6 +155,8 @@ var ( "440A": {tags: []string{"es-SV"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, "1E": {tags: []string{"th"}, localMonth: localMonthsNameThai, apFmt: nfp.AmPm[0]}, "41E": {tags: []string{"th-TH"}, localMonth: localMonthsNameThai, apFmt: nfp.AmPm[0]}, + "51": {tags: []string{"bo"}, localMonth: localMonthsNameTibetan, apFmt: apFmtTibetan}, + "451": {tags: []string{"bo-CN"}, localMonth: localMonthsNameTibetan, apFmt: apFmtTibetan}, "1F": {tags: []string{"tr"}, localMonth: localMonthsNameTurkish, apFmt: apFmtTurkish}, "41F": {tags: []string{"tr-TR"}, localMonth: localMonthsNameTurkish, apFmt: apFmtTurkish}, "52": {tags: []string{"cy"}, localMonth: localMonthsNameWelsh, apFmt: apFmtWelsh}, @@ -214,6 +220,21 @@ var ( "\u0e1e\u0e24\u0e28\u0e08\u0e34\u0e01\u0e32\u0e22\u0e19", "\u0e18\u0e31\u0e19\u0e27\u0e32\u0e04\u0e21", } + // monthNamesTibetan list the month names in the Tibetan. + monthNamesTibetan = []string{ + "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f51\u0f44\u0f0b\u0f54\u0f7c\u0f0b", + "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f42\u0f49\u0f72\u0f66\u0f0b\u0f54\u0f0b", + "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f42\u0f66\u0f74\u0f58\u0f0b\u0f54\u0f0b", + "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f5e\u0f72\u0f0b\u0f54\u0f0b", + "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f63\u0f94\u0f0b\u0f54\u0f0b", + "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f51\u0fb2\u0f74\u0f42\u0f0b\u0f54\u0f0b", + "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f51\u0f74\u0f53\u0f0b\u0f54\u0f0b", + "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f62\u0f92\u0fb1\u0f51\u0f0b\u0f54\u0f0b", + "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f51\u0f42\u0f74\u0f0b\u0f54\u0f0b", + "\u0f66\u0fa4\u0fb1\u0f72\u0f0b\u0f5f\u0fb3\u0f0b\u0f56\u0f45\u0f74\u0f0b\u0f54\u0f0d", + "\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", + } // 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"} // monthNamesWelsh list the month names in the Welsh. @@ -238,6 +259,8 @@ var ( apFmtKorean = "오전/오후" // apFmtSpanish defined the AM/PM name in the Spanish. apFmtSpanish = "a. m./p. m." + // apFmtTibetan defined the AM/PM name in the Tibetan. + apFmtTibetan = "\u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b/\u0f55\u0fb1\u0f72\u0f0b\u0f51\u0fb2\u0f7c\u0f0b" // apFmtTurkish defined the AM/PM name in the Turkish. apFmtTurkish = "\u00F6\u00F6/\u00F6\u0053" // apFmtVietnamese defined the AM/PM name in the Vietnamese. @@ -480,6 +503,14 @@ func localMonthsNameKorean(t time.Time, abbr int) string { return strconv.Itoa(int(t.Month())) } +// localMonthsNameTraditionalMongolian returns the Traditional Mongolian name of the month. +func localMonthsNameTraditionalMongolian(t time.Time, abbr int) string { + if abbr == 5 { + return "M" + } + return fmt.Sprintf("M%02d", int(t.Month())) +} + // localMonthsNameRussian returns the Russian name of the month. func localMonthsNameRussian(t time.Time, abbr int) string { if abbr == 3 { @@ -518,6 +549,20 @@ func localMonthsNameThai(t time.Time, abbr int) string { return string([]rune(monthNamesThai[int(t.Month())-1])[:1]) } +// 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] + } + if abbr == 5 { + if t.Month() == 10 { + return "\u0f66" + } + return "\u0f5f" + } + return string(monthNamesTibetan[int(t.Month())-1]) +} + // localMonthsNameTurkish returns the Turkish name of the month. func localMonthsNameTurkish(t time.Time, abbr int) string { if abbr == 3 { @@ -839,8 +884,16 @@ func (nf *numberFormat) zeroHandler() string { } // textHandler will be handling text selection for a number format expression. -func (nf *numberFormat) textHandler() string { - return fmt.Sprint(nf.value) +func (nf *numberFormat) textHandler() (result string) { + for _, token := range nf.section[nf.sectionIdx].Items { + if token.TType == nfp.TokenTypeLiteral { + result += token.TValue + } + if token.TType == nfp.TokenTypeTextPlaceHolder { + result += nf.value + } + } + return result } // getValueSectionType returns its applicable number format expression section diff --git a/numfmt_test.go b/numfmt_test.go index 2969e75976..80534e97e7 100644 --- a/numfmt_test.go +++ b/numfmt_test.go @@ -253,6 +253,24 @@ func TestNumFmt(t *testing.T) { {"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"}, + {"44896.18957170139", "[$-7C50]mmmm dd yyyy h:mm AM/PM", "M12 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-7C50]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 AM"}, + {"44896.18957170139", "[$-7C50]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-850]mmm dd yyyy h:mm AM/PM", "M01 01 2022 4:32 AM"}, + {"44896.18957170139", "[$-850]mmm dd yyyy h:mm AM/PM", "M12 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-850]mmmm dd yyyy h:mm AM/PM", "M01 01 2022 4:32 AM"}, + {"44896.18957170139", "[$-850]mmmm dd yyyy h:mm AM/PM", "M12 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-850]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 AM"}, + {"44896.18957170139", "[$-850]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-C50]mmm dd yyyy h:mm AM/PM", "M01 01 2022 4:32 AM"}, + {"44896.18957170139", "[$-C50]mmm dd yyyy h:mm AM/PM", "M12 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-C50]mmmm dd yyyy h:mm AM/PM", "M01 01 2022 4:32 AM"}, + {"44896.18957170139", "[$-C50]mmmm dd yyyy h:mm AM/PM", "M12 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-C50]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 AM"}, + {"44896.18957170139", "[$-C50]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 AM"}, {"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"}, @@ -400,6 +418,78 @@ func TestNumFmt(t *testing.T) { {"44835.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM", "\u0e15 01 2022 4:32 AM"}, {"44866.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM", "\u0e1e 01 2022 4:32 AM"}, {"44896.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM", "\u0e18 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-51]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f21 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44593.189571759256", "[$-51]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f22 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44621.18957170139", "[$-51]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f23 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44652.18957170139", "[$-51]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f24 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44682.18957170139", "[$-51]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f25 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44713.18957170139", "[$-51]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f26 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44743.18957170139", "[$-51]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f27 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44774.18957170139", "[$-51]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f28 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44805.18957170139", "[$-51]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f29 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44835.18957170139", "[$-51]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f21\u0f20 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44866.18957170139", "[$-51]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f21\u0f21 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44896.18957170139", "[$-51]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f21\u0f22 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44562.189571759256", "[$-51]mmmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f51\u0f44\u0f0b\u0f54\u0f7c\u0f0b 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44593.189571759256", "[$-51]mmmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f42\u0f49\u0f72\u0f66\u0f0b\u0f54\u0f0b 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44621.18957170139", "[$-51]mmmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f42\u0f66\u0f74\u0f58\u0f0b\u0f54\u0f0b 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44652.18957170139", "[$-51]mmmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f5e\u0f72\u0f0b\u0f54\u0f0b 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44682.18957170139", "[$-51]mmmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f63\u0f94\u0f0b\u0f54\u0f0b 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44713.18957170139", "[$-51]mmmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f51\u0fb2\u0f74\u0f42\u0f0b\u0f54\u0f0b 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44743.18957170139", "[$-51]mmmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f51\u0f74\u0f53\u0f0b\u0f54\u0f0b 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44774.18957170139", "[$-51]mmmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f62\u0f92\u0fb1\u0f51\u0f0b\u0f54\u0f0b 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44805.18957170139", "[$-51]mmmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f51\u0f42\u0f74\u0f0b\u0f54\u0f0b 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44835.18957170139", "[$-51]mmmm dd yyyy h:mm AM/PM", "\u0f66\u0fa4\u0fb1\u0f72\u0f0b\u0f5f\u0fb3\u0f0b\u0f56\u0f45\u0f74\u0f0b\u0f54\u0f0d 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44866.18957170139", "[$-51]mmmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f45\u0f74\u0f0b\u0f42\u0f45\u0f72\u0f42\u0f0b\u0f54\u0f0b 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44896.18957170139", "[$-51]mmmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f45\u0f74\u0f0b\u0f42\u0f49\u0f72\u0f66\u0f0b\u0f54\u0f0b 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44562.189571759256", "[$-51]mmmmm dd yyyy h:mm AM/PM", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44593.189571759256", "[$-51]mmmmm dd yyyy h:mm AM/PM", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44621.18957170139", "[$-51]mmmmm dd yyyy h:mm AM/PM", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44652.18957170139", "[$-51]mmmmm dd yyyy h:mm AM/PM", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44682.18957170139", "[$-51]mmmmm dd yyyy h:mm AM/PM", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44713.18957170139", "[$-51]mmmmm dd yyyy h:mm AM/PM", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44743.18957170139", "[$-51]mmmmm dd yyyy h:mm AM/PM", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44774.18957170139", "[$-51]mmmmm dd yyyy h:mm AM/PM", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44805.18957170139", "[$-51]mmmmm dd yyyy h:mm AM/PM", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44835.18957170139", "[$-51]mmmmm dd yyyy h:mm AM/PM", "\u0f66 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44866.18957170139", "[$-51]mmmmm dd yyyy h:mm AM/PM", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44896.18957170139", "[$-51]mmmmm dd yyyy h:mm AM/PM", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44562.189571759256", "[$-451]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f21 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44593.189571759256", "[$-451]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f22 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44621.18957170139", "[$-451]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f23 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44652.18957170139", "[$-451]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f24 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44682.18957170139", "[$-451]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f25 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44713.18957170139", "[$-451]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f26 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44743.18957170139", "[$-451]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f27 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44774.18957170139", "[$-451]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f28 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44805.18957170139", "[$-451]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f29 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44835.18957170139", "[$-451]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f21\u0f20 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44866.18957170139", "[$-451]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f21\u0f21 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44896.18957170139", "[$-451]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f21\u0f22 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44562.189571759256", "[$-451]mmmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f51\u0f44\u0f0b\u0f54\u0f7c\u0f0b 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44593.189571759256", "[$-451]mmmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f42\u0f49\u0f72\u0f66\u0f0b\u0f54\u0f0b 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44621.18957170139", "[$-451]mmmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f42\u0f66\u0f74\u0f58\u0f0b\u0f54\u0f0b 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44652.18957170139", "[$-451]mmmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f5e\u0f72\u0f0b\u0f54\u0f0b 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44682.18957170139", "[$-451]mmmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f63\u0f94\u0f0b\u0f54\u0f0b 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44713.18957170139", "[$-451]mmmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f51\u0fb2\u0f74\u0f42\u0f0b\u0f54\u0f0b 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44743.18957170139", "[$-451]mmmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f51\u0f74\u0f53\u0f0b\u0f54\u0f0b 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44774.18957170139", "[$-451]mmmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f62\u0f92\u0fb1\u0f51\u0f0b\u0f54\u0f0b 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44805.18957170139", "[$-451]mmmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f51\u0f42\u0f74\u0f0b\u0f54\u0f0b 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44835.18957170139", "[$-451]mmmm dd yyyy h:mm AM/PM", "\u0f66\u0fa4\u0fb1\u0f72\u0f0b\u0f5f\u0fb3\u0f0b\u0f56\u0f45\u0f74\u0f0b\u0f54\u0f0d 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44866.18957170139", "[$-451]mmmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f45\u0f74\u0f0b\u0f42\u0f45\u0f72\u0f42\u0f0b\u0f54\u0f0b 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44896.18957170139", "[$-451]mmmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f45\u0f74\u0f0b\u0f42\u0f49\u0f72\u0f66\u0f0b\u0f54\u0f0b 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44562.189571759256", "[$-451]mmmmm dd yyyy h:mm AM/PM", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44593.189571759256", "[$-451]mmmmm dd yyyy h:mm AM/PM", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44621.18957170139", "[$-451]mmmmm dd yyyy h:mm AM/PM", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44652.18957170139", "[$-451]mmmmm dd yyyy h:mm AM/PM", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44682.18957170139", "[$-451]mmmmm dd yyyy h:mm AM/PM", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44713.18957170139", "[$-451]mmmmm dd yyyy h:mm AM/PM", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44743.18957170139", "[$-451]mmmmm dd yyyy h:mm AM/PM", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44774.18957170139", "[$-451]mmmmm dd yyyy h:mm AM/PM", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44805.18957170139", "[$-451]mmmmm dd yyyy h:mm AM/PM", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44835.18957170139", "[$-451]mmmmm dd yyyy h:mm AM/PM", "\u0f66 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44866.18957170139", "[$-451]mmmmm dd yyyy h:mm AM/PM", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44896.18957170139", "[$-451]mmmmm dd yyyy h:mm AM/PM", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, {"44562.189571759256", "[$-1F]mmm dd yyyy h:mm AM/PM", "Oca 01 2022 4:32 \u00F6\u00F6"}, {"44593.189571759256", "[$-1F]mmm dd yyyy h:mm AM/PM", "Şub 01 2022 4:32 \u00F6\u00F6"}, {"44621.18957170139", "[$-1F]mmm dd yyyy h:mm AM/PM", "Mar 01 2022 4:32 \u00F6\u00F6"}, @@ -904,6 +994,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"}, + {"text_", "General", "text_"}, + {"text_", "\"=====\"@@@\"--\"@\"----\"", "=====text_text_text_--text_----"}, } { result := format(item[0], item[1]) assert.Equal(t, item[2], result, item) From 471c8f22d0ac6cc17915eea25d171e578c06ac7d Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 26 Feb 2022 21:32:57 +0800 Subject: [PATCH 543/957] This closes #1160, and added 4 new formula functions * Fix show sheet tabs issue * Ref #65, new formula functions: ERROR.TYPE, HOUR, SECOND TIMEVALUE --- calc.go | 147 +++++++++++++++++++++++++++++++++++++++++++++++-- calc_test.go | 63 ++++++++++++++++++--- xmlWorkbook.go | 20 +++---- 3 files changed, 208 insertions(+), 22 deletions(-) diff --git a/calc.go b/calc.go index a4c45fbf6d..d4828be8a7 100644 --- a/calc.go +++ b/calc.go @@ -401,6 +401,7 @@ type formulaFuncs struct { // ERF.PRECISE // ERFC // ERFC.PRECISE +// ERROR.TYPE // EVEN // EXACT // EXP @@ -427,6 +428,7 @@ type formulaFuncs struct { // HEX2DEC // HEX2OCT // HLOOKUP +// HOUR // IF // IFERROR // IFNA @@ -574,6 +576,7 @@ type formulaFuncs struct { // RRI // SEC // SECH +// SECOND // SHEET // SHEETS // SIGN @@ -604,6 +607,7 @@ type formulaFuncs struct { // TBILLYIELD // TEXTJOIN // TIME +// TIMEVALUE // TODAY // TRANSPOSE // TRIM @@ -852,7 +856,9 @@ func (f *File) evalInfixExpFunc(sheet, cell string, token, nextToken efp.Token, // calculate trigger topOpt := opftStack.Peek().(efp.Token) if err := calculate(opfdStack, topOpt); err != nil { - return err + argsStack.Peek().(*list.List).PushBack(newErrorFormulaArg(err.Error(), err.Error())) + opftStack.Pop() + continue } opftStack.Pop() } @@ -874,7 +880,11 @@ 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 - opfdStack.Push(efp.Token{TValue: arg.Value(), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + 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}) + } } else { argsStack.Peek().(*list.List).PushBack(arg) } @@ -1096,11 +1106,19 @@ func calculate(opdStack *Stack, opt efp.Token) error { fn, ok := tokenCalcFunc[opt.TValue] if ok { if opdStack.Len() < 2 { + if opdStack.Len() == 1 { + rOpd := opdStack.Pop().(efp.Token) + if rOpd.TSubType == efp.TokenSubTypeError { + return errors.New(rOpd.TValue) + } + } return ErrInvalidFormula } rOpd := opdStack.Pop().(efp.Token) lOpd := opdStack.Pop().(efp.Token) - + if lOpd.TSubType == efp.TokenSubTypeError { + return errors.New(lOpd.TValue) + } if err := fn(rOpd, lOpd, opdStack); err != nil { return err } @@ -4797,8 +4815,8 @@ func (fn *formulaFuncs) SUM(argsList *list.List) formulaArg { for arg := argsList.Front(); arg != nil; arg = arg.Next() { token := arg.Value.(formulaArg) switch token.Type { - case ArgUnknown: - continue + case ArgError: + return token case ArgString: if num := token.ToNumber(); num.Type == ArgNumber { sum += num.Number @@ -6787,6 +6805,29 @@ func (fn *formulaFuncs) ZTEST(argsList *list.List) formulaArg { // Information Functions +// 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) +// +func (fn *formulaFuncs) ERRORdotTYPE(argsList *list.List) formulaArg { + if argsList.Len() != 1 { + return newErrorFormulaArg(formulaErrorVALUE, "ERROR.TYPE requires 1 argument") + } + token := argsList.Front().Value.(formulaArg) + if token.Type == ArgError { + for i, errType := range []string{ + formulaErrorNULL, formulaErrorDIV, formulaErrorVALUE, formulaErrorREF, + formulaErrorNAME, formulaErrorNUM, formulaErrorNA, + } { + if errType == token.String { + return newNumberFormulaArg(float64(i) + 1) + } + } + } + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) +} + // ISBLANK function tests if a specified cell is blank (empty) and if so, // returns TRUE; Otherwise the function returns FALSE. The syntax of the // function is: @@ -7884,6 +7925,40 @@ func (fn *formulaFuncs) ISOWEEKNUM(argsList *list.List) formulaArg { return newNumberFormulaArg(float64(weeknum)) } +// HOUR function returns an integer representing the hour component of a +// supplied Excel time. The syntax of the function is: +// +// HOUR(serial_number) +// +func (fn *formulaFuncs) HOUR(argsList *list.List) formulaArg { + if argsList.Len() != 1 { + return newErrorFormulaArg(formulaErrorVALUE, "HOUR requires exactly 1 argument") + } + date := argsList.Front().Value.(formulaArg) + num := date.ToNumber() + if num.Type != ArgNumber { + timeString := strings.ToLower(date.Value()) + if !isTimeOnlyFmt(timeString) { + _, _, _, _, err := strToDate(timeString) + if err.Type == ArgError { + return err + } + } + h, _, _, pm, _, err := strToTime(timeString) + if err.Type == ArgError { + return err + } + if pm { + h += 12 + } + return newNumberFormulaArg(float64(h)) + } + if num.Number < 0 { + return newErrorFormulaArg(formulaErrorNUM, "HOUR only accepts positive argument") + } + return newNumberFormulaArg(float64(timeFromExcelTime(num.Number, false).Hour())) +} + // MINUTE function returns an integer representing the minute component of a // supplied Excel time. The syntax of the function is: // @@ -8131,6 +8206,37 @@ func (fn *formulaFuncs) NOW(argsList *list.List) formulaArg { return newNumberFormulaArg(25569.0 + float64(now.Unix()+int64(offset))/86400) } +// SECOND function returns an integer representing the second component of a +// supplied Excel time. The syntax of the function is: +// +// SECOND(serial_number) +// +func (fn *formulaFuncs) SECOND(argsList *list.List) formulaArg { + if argsList.Len() != 1 { + return newErrorFormulaArg(formulaErrorVALUE, "SECOND requires exactly 1 argument") + } + date := argsList.Front().Value.(formulaArg) + num := date.ToNumber() + if num.Type != ArgNumber { + timeString := strings.ToLower(date.Value()) + if !isTimeOnlyFmt(timeString) { + _, _, _, _, err := strToDate(timeString) + if err.Type == ArgError { + return err + } + } + _, _, s, _, _, err := strToTime(timeString) + if err.Type == ArgError { + return err + } + return newNumberFormulaArg(float64(int(s) % 60)) + } + if num.Number < 0 { + return newErrorFormulaArg(formulaErrorNUM, "SECOND only accepts positive argument") + } + return newNumberFormulaArg(float64(timeFromExcelTime(num.Number, false).Second())) +} + // 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 @@ -8155,6 +8261,37 @@ func (fn *formulaFuncs) TIME(argsList *list.List) formulaArg { return newNumberFormulaArg(t) } +// TIMEVALUE function converts a text representation of a time, into an Excel +// time. The syntax of the Timevalue function is: +// +// TIMEVALUE(time_text) +// +func (fn *formulaFuncs) TIMEVALUE(argsList *list.List) formulaArg { + if argsList.Len() != 1 { + return newErrorFormulaArg(formulaErrorVALUE, "TIMEVALUE requires exactly 1 argument") + } + date := argsList.Front().Value.(formulaArg) + timeString := strings.ToLower(date.Value()) + if !isTimeOnlyFmt(timeString) { + _, _, _, _, err := strToDate(timeString) + if err.Type == ArgError { + return err + } + } + h, m, s, pm, _, err := strToTime(timeString) + if err.Type == ArgError { + return err + } + if pm { + h += 12 + } + args := list.New() + args.PushBack(newNumberFormulaArg(float64(h))) + args.PushBack(newNumberFormulaArg(float64(m))) + args.PushBack(newNumberFormulaArg(s)) + return fn.TIME(args) +} + // TODAY function returns the current date. The function has no arguments and // therefore. The syntax of the function is: // diff --git a/calc_test.go b/calc_test.go index c3be3080bb..e49b29ec9d 100644 --- a/calc_test.go +++ b/calc_test.go @@ -990,6 +990,12 @@ func TestCalcCellValue(t *testing.T) { "=WEIBULL.DIST(1,3,1,FALSE)": "1.103638323514327", "=WEIBULL.DIST(2,5,1.5,TRUE)": "0.985212776817482", // Information Functions + // ERROR.TYPE + "=ERROR.TYPE(1/0)": "2", + "=ERROR.TYPE(COT(0))": "2", + "=ERROR.TYPE(XOR(\"text\"))": "3", + "=ERROR.TYPE(HEX2BIN(2,1))": "6", + "=ERROR.TYPE(NA())": "7", // ISBLANK "=ISBLANK(A1)": "FALSE", "=ISBLANK(A5)": "TRUE", @@ -1139,6 +1145,13 @@ func TestCalcCellValue(t *testing.T) { "=DAYS(2,1)": "1", "=DAYS(INT(2),INT(1))": "1", "=DAYS(\"02/02/2015\",\"01/01/2015\")": "32", + // HOUR + "=HOUR(1)": "0", + "=HOUR(43543.5032060185)": "12", + "=HOUR(\"43543.5032060185\")": "12", + "=HOUR(\"13:00:55\")": "13", + "=HOUR(\"1:00 PM\")": "13", + "=HOUR(\"12/09/2015 08:55\")": "8", // ISOWEEKNUM "=ISOWEEKNUM(42370)": "53", "=ISOWEEKNUM(\"42370\")": "53", @@ -1183,10 +1196,26 @@ func TestCalcCellValue(t *testing.T) { "=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", + // SECOND + "=SECOND(\"13:35:55\")": "55", + "=SECOND(\"13:10:60\")": "0", + "=SECOND(\"13:10:61\")": "1", + "=SECOND(\"08:17:00\")": "0", + "=SECOND(\"12/09/2015 08:55\")": "0", + "=SECOND(\"12/09/2011 08:17:23\")": "23", + "=SECOND(\"43543.5032060185\")": "37", + "=SECOND(43543.5032060185)": "37", // TIME "=TIME(5,44,32)": "0.239259259259259", "=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(\"00:02:23\")": "0.00165509259259259", + "=TIMEVALUE(\"01/01/2011 02:23\")": "0.0993055555555556", // WEEKDAY "=WEEKDAY(0)": "7", "=WEEKDAY(47119)": "2", @@ -2167,12 +2196,14 @@ func TestCalcCellValue(t *testing.T) { "=POISSON(0,0,\"\")": "strconv.ParseBool: parsing \"\": invalid syntax", "=POISSON(0,-1,TRUE)": "#N/A", // SUM - "=SUM((": ErrInvalidFormula.Error(), - "=SUM(-)": ErrInvalidFormula.Error(), - "=SUM(1+)": ErrInvalidFormula.Error(), - "=SUM(1-)": ErrInvalidFormula.Error(), - "=SUM(1*)": ErrInvalidFormula.Error(), - "=SUM(1/)": ErrInvalidFormula.Error(), + "=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!", // SUMIF "=SUMIF()": "SUMIF requires at least 2 arguments", // SUMSQ @@ -2453,6 +2484,9 @@ func TestCalcCellValue(t *testing.T) { "=ZTEST(A1,1)": "#DIV/0!", "=ZTEST(A1,1,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", // Information Functions + // ERROR.TYPE + "=ERROR.TYPE()": "ERROR.TYPE requires 1 argument", + "=ERROR.TYPE(1)": "#N/A", // ISBLANK "=ISBLANK(A1,A2)": "ISBLANK requires 1 argument", // ISERR @@ -2582,6 +2616,11 @@ func TestCalcCellValue(t *testing.T) { "=DAYS(0,\"\")": "#VALUE!", "=DAYS(NA(),0)": "#VALUE!", "=DAYS(0,NA())": "#VALUE!", + // HOUR + "=HOUR()": "HOUR requires exactly 1 argument", + "=HOUR(-1)": "HOUR only accepts positive argument", + "=HOUR(\"\")": "#VALUE!", + "=HOUR(\"25:10:55\")": "#VALUE!", // ISOWEEKNUM "=ISOWEEKNUM()": "ISOWEEKNUM requires 1 argument", "=ISOWEEKNUM(\"\")": "#VALUE!", @@ -2612,10 +2651,20 @@ func TestCalcCellValue(t *testing.T) { "=YEARFRAC(42005,42094,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", // NOW "=NOW(A1)": "NOW accepts no arguments", + // SECOND + "=SECOND()": "SECOND requires exactly 1 argument", + "=SECOND(-1)": "SECOND only accepts positive argument", + "=SECOND(\"\")": "#VALUE!", + "=SECOND(\"25:55\")": "#VALUE!", // TIME "=TIME()": "TIME requires 3 number arguments", "=TIME(\"\",0,0)": "TIME requires 3 number arguments", "=TIME(0,0,-1)": "#NUM!", + // TIMEVALUE + "=TIMEVALUE()": "TIMEVALUE requires exactly 1 argument", + "=TIMEVALUE(1)": "#VALUE!", + "=TIMEVALUE(-1)": "#VALUE!", + "=TIMEVALUE(\"25:55\")": "#VALUE!", // TODAY "=TODAY(A1)": "TODAY accepts no arguments", // WEEKDAY @@ -3354,7 +3403,7 @@ func TestCalcCellValue(t *testing.T) { f := prepareCalcData(cellData) assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) result, err := f.CalcCellValue("Sheet1", "C1") - assert.EqualError(t, err, expected) + assert.EqualError(t, err, expected, formula) assert.Equal(t, "", result, formula) } diff --git a/xmlWorkbook.go b/xmlWorkbook.go index f014bee562..2bb417c36d 100644 --- a/xmlWorkbook.go +++ b/xmlWorkbook.go @@ -139,19 +139,19 @@ type xlsxBookViews struct { // http://schemas.openxmlformats.org/spreadsheetml/2006/main This element // specifies a single Workbook view. type xlsxWorkBookView struct { - ActiveTab int `xml:"activeTab,attr,omitempty"` - AutoFilterDateGrouping bool `xml:"autoFilterDateGrouping,attr,omitempty"` - FirstSheet int `xml:"firstSheet,attr,omitempty"` - Minimized bool `xml:"minimized,attr,omitempty"` - ShowHorizontalScroll bool `xml:"showHorizontalScroll,attr,omitempty"` - ShowSheetTabs bool `xml:"showSheetTabs,attr,omitempty"` - ShowVerticalScroll bool `xml:"showVerticalScroll,attr,omitempty"` - TabRatio int `xml:"tabRatio,attr,omitempty"` Visibility string `xml:"visibility,attr,omitempty"` - WindowHeight int `xml:"windowHeight,attr,omitempty"` - WindowWidth int `xml:"windowWidth,attr,omitempty"` + Minimized bool `xml:"minimized,attr,omitempty"` + ShowHorizontalScroll *bool `xml:"showHorizontalScroll,attr"` + ShowVerticalScroll *bool `xml:"showVerticalScroll,attr"` + ShowSheetTabs *bool `xml:"showSheetTabs,attr"` XWindow string `xml:"xWindow,attr,omitempty"` YWindow string `xml:"yWindow,attr,omitempty"` + WindowWidth int `xml:"windowWidth,attr,omitempty"` + WindowHeight int `xml:"windowHeight,attr,omitempty"` + TabRatio int `xml:"tabRatio,attr,omitempty"` + FirstSheet int `xml:"firstSheet,attr,omitempty"` + ActiveTab int `xml:"activeTab,attr,omitempty"` + AutoFilterDateGrouping *bool `xml:"autoFilterDateGrouping,attr"` } // xlsxSheets directly maps the sheets element from the namespace From 42a9665aa91912f570bb83052b20cc50529d7b6a Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 28 Feb 2022 01:01:24 +0800 Subject: [PATCH 544/957] ref #65: formula function INDIRECT support and formula engine improvement Support calculation with the none parameter formula function after infix operator notation --- calc.go | 137 +++++++++++++++++++++++++++++++++++++++------------ calc_test.go | 22 +++++++++ 2 files changed, 127 insertions(+), 32 deletions(-) diff --git a/calc.go b/calc.go index d4828be8a7..8358d3338b 100644 --- a/calc.go +++ b/calc.go @@ -459,6 +459,7 @@ type formulaFuncs struct { // IMSUM // IMTAN // INDEX +// INDIRECT // INT // INTRATE // IPMT @@ -851,22 +852,7 @@ func (f *File) evalInfixExpFunc(sheet, cell string, token, nextToken efp.Token, if !isFunctionStopToken(token) { return nil } - // current token is function stop - for opftStack.Peek().(efp.Token) != opfStack.Peek().(efp.Token) { - // calculate trigger - topOpt := opftStack.Peek().(efp.Token) - if err := calculate(opfdStack, topOpt); err != nil { - argsStack.Peek().(*list.List).PushBack(newErrorFormulaArg(err.Error(), err.Error())) - opftStack.Pop() - continue - } - opftStack.Pop() - } - - // push opfd to args - if opfdStack.Len() > 0 { - argsStack.Peek().(*list.List).PushBack(newStringFormulaArg(opfdStack.Pop().(efp.Token).TValue)) - } + prepareEvalInfixExp(opfStack, opftStack, opfdStack, argsStack) // call formula function to evaluate arg := callFuncByName(&formulaFuncs{f: f, sheet: sheet, cell: cell}, strings.NewReplacer( "_xlfn.", "", ".", "dot").Replace(opfStack.Peek().(efp.Token).TValue), @@ -894,6 +880,34 @@ func (f *File) evalInfixExpFunc(sheet, cell string, token, nextToken efp.Token, return nil } +// prepareEvalInfixExp check the token and stack state for formula function +// evaluate. +func prepareEvalInfixExp(opfStack, opftStack, opfdStack, argsStack *Stack) { + // current token is function stop + for opftStack.Peek().(efp.Token) != opfStack.Peek().(efp.Token) { + // calculate trigger + topOpt := opftStack.Peek().(efp.Token) + if err := calculate(opfdStack, topOpt); err != nil { + argsStack.Peek().(*list.List).PushBack(newErrorFormulaArg(err.Error(), err.Error())) + opftStack.Pop() + continue + } + opftStack.Pop() + } + argument := true + if opftStack.Len() > 2 && opfdStack.Len() == 1 { + topOpt := opftStack.Pop() + if opftStack.Peek().(efp.Token).TType == efp.TokenTypeOperatorInfix { + argument = false + } + opftStack.Push(topOpt) + } + // push opfd to args + if argument && opfdStack.Len() > 0 { + argsStack.Peek().(*list.List).PushBack(newStringFormulaArg(opfdStack.Pop().(efp.Token).TValue)) + } +} + // calcPow evaluate exponentiation arithmetic operations. func calcPow(rOpd, lOpd efp.Token, opdStack *Stack) error { lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64) @@ -1080,6 +1094,16 @@ func calculate(opdStack *Stack, opt efp.Token) error { result := 0 - opdVal opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) } + if opt.TValue == "-" && opt.TType == efp.TokenTypeOperatorInfix { + if opdStack.Len() < 2 { + return ErrInvalidFormula + } + rOpd := opdStack.Pop().(efp.Token) + lOpd := opdStack.Pop().(efp.Token) + if err := calcSubtract(rOpd, lOpd, opdStack); err != nil { + return err + } + } tokenCalcFunc := map[string]func(rOpd, lOpd efp.Token, opdStack *Stack) error{ "^": calcPow, "*": calcMultiply, @@ -1093,29 +1117,16 @@ func calculate(opdStack *Stack, opt efp.Token) error { ">=": calcGe, "&": calcSplice, } - if opt.TValue == "-" && opt.TType == efp.TokenTypeOperatorInfix { - if opdStack.Len() < 2 { - return ErrInvalidFormula - } - rOpd := opdStack.Pop().(efp.Token) - lOpd := opdStack.Pop().(efp.Token) - if err := calcSubtract(rOpd, lOpd, opdStack); err != nil { - return err - } - } fn, ok := tokenCalcFunc[opt.TValue] if ok { if opdStack.Len() < 2 { - if opdStack.Len() == 1 { - rOpd := opdStack.Pop().(efp.Token) - if rOpd.TSubType == efp.TokenSubTypeError { - return errors.New(rOpd.TValue) - } - } return ErrInvalidFormula } rOpd := opdStack.Pop().(efp.Token) lOpd := opdStack.Pop().(efp.Token) + if rOpd.TSubType == efp.TokenSubTypeError { + return errors.New(rOpd.TValue) + } if lOpd.TSubType == efp.TokenSubTypeError { return errors.New(lOpd.TValue) } @@ -9959,6 +9970,68 @@ func (fn *formulaFuncs) INDEX(argsList *list.List) formulaArg { return cells.List[colIdx] } +// INDIRECT function converts a text string into a cell reference. The syntax +// of the Indirect function is: +// +// 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") + } + refText := argsList.Front().Value.(formulaArg).Value() + a1 := newBoolFormulaArg(true) + if argsList.Len() == 2 { + if a1 = argsList.Back().Value.(formulaArg).ToBool(); a1.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + } + R1C1ToA1 := func(ref string) (cell string, err error) { + parts := strings.Split(strings.TrimLeft(ref, "R"), "C") + if len(parts) != 2 { + return + } + row, err := strconv.Atoi(parts[0]) + if err != nil { + return + } + col, err := strconv.Atoi(parts[1]) + if err != nil { + return + } + cell, err = CoordinatesToCellName(col, row) + return + } + refs := strings.Split(refText, ":") + fromRef, toRef := refs[0], "" + if len(refs) == 2 { + toRef = refs[1] + } + if a1.Number == 0 { + from, err := R1C1ToA1(refs[0]) + if err != nil { + return newErrorFormulaArg(formulaErrorREF, formulaErrorREF) + } + fromRef = from + if len(refs) == 2 { + to, err := R1C1ToA1(refs[1]) + if err != nil { + return newErrorFormulaArg(formulaErrorREF, formulaErrorREF) + } + toRef = to + } + } + if len(refs) == 1 { + value, err := fn.f.GetCellValue(fn.sheet, fromRef) + if err != nil { + return newErrorFormulaArg(formulaErrorREF, formulaErrorREF) + } + return newStringFormulaArg(value) + } + arg, _ := fn.f.parseReference(fn.sheet, fromRef+":"+toRef) + return arg +} + // LOOKUP function performs an approximate match lookup in a one-column or // one-row range, and returns the corresponding value from another one-column // or one-row range. The syntax of the function is: diff --git a/calc_test.go b/calc_test.go index e49b29ec9d..e620eb362d 100644 --- a/calc_test.go +++ b/calc_test.go @@ -724,6 +724,7 @@ func TestCalcCellValue(t *testing.T) { "=((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", + "=SUM(1+ROW())": "2", // SUMIF `=SUMIF(F1:F5, "")`: "0", `=SUMIF(A1:A5, "3")`: "3", @@ -1447,6 +1448,16 @@ func TestCalcCellValue(t *testing.T) { "=SUM(INDEX(A1:B2,2,0))": "7", "=SUM(INDEX(A1:B4,0,2))": "9", "=SUM(INDEX(E1:F5,5,2))": "34440", + // INDIRECT + "=INDIRECT(\"E1\")": "Team", + "=INDIRECT(\"E\"&1)": "Team", + "=INDIRECT(\"E\"&ROW())": "Team", + "=INDIRECT(\"E\"&ROW(),TRUE)": "Team", + "=INDIRECT(\"R1C5\",FALSE)": "Team", + "=INDIRECT(\"R\"&1&\"C\"&5,FALSE)": "Team", + "=SUM(INDIRECT(\"A1:B2\"))": "12", + "=SUM(INDIRECT(\"A1:B2\",TRUE))": "12", + "=SUM(INDIRECT(\"R1C1:R2C2\",FALSE))": "12", // LOOKUP "=LOOKUP(F8,F8:F9,F8:F9)": "32080", "=LOOKUP(F8,F8:F9,D8:D9)": "Feb", @@ -2872,6 +2883,17 @@ func TestCalcCellValue(t *testing.T) { "=INDEX(A1:A2,0,0)": "#VALUE!", "=INDEX(0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", "=INDEX(0,0,\"\")": "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!", // LOOKUP "=LOOKUP()": "LOOKUP requires at least 2 arguments", "=LOOKUP(D2,D1,D2)": "LOOKUP requires second argument of table array", From 1efa2838875efe06a9a4b7b1d582f70205de3aa5 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 1 Mar 2022 00:10:59 +0800 Subject: [PATCH 545/957] ref #65, new formula functions FORMULATEXT and TYPE --- calc.go | 67 ++++++++++++++++++++++++++++++++++++++++++++++------ calc_test.go | 29 +++++++++++++++++++++-- 2 files changed, 87 insertions(+), 9 deletions(-) diff --git a/calc.go b/calc.go index 8358d3338b..0f44912677 100644 --- a/calc.go +++ b/calc.go @@ -416,6 +416,7 @@ type formulaFuncs struct { // FLOOR // FLOOR.MATH // FLOOR.PRECISE +// FORMULATEXT // FV // FVSCHEDULE // GAMMA @@ -615,6 +616,7 @@ type formulaFuncs struct { // TRIMMEAN // TRUE // TRUNC +// TYPE // UNICHAR // UNICODE // UPPER @@ -6874,7 +6876,7 @@ func (fn *formulaFuncs) ISERR(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorVALUE, "ISERR requires 1 argument") } token := argsList.Front().Value.(formulaArg) - result := "FALSE" + result := false if token.Type == ArgError { for _, errType := range []string{ formulaErrorDIV, formulaErrorNAME, formulaErrorNUM, @@ -6882,11 +6884,11 @@ func (fn *formulaFuncs) ISERR(argsList *list.List) formulaArg { formulaErrorSPILL, formulaErrorCALC, formulaErrorGETTINGDATA, } { if errType == token.String { - result = "TRUE" + result = true } } } - return newStringFormulaArg(result) + return newBoolFormulaArg(result) } // ISERROR function tests if an initial supplied expression (or value) returns @@ -6900,7 +6902,7 @@ func (fn *formulaFuncs) ISERROR(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorVALUE, "ISERROR requires 1 argument") } token := argsList.Front().Value.(formulaArg) - result := "FALSE" + result := false if token.Type == ArgError { for _, errType := range []string{ formulaErrorDIV, formulaErrorNAME, formulaErrorNA, formulaErrorNUM, @@ -6908,11 +6910,11 @@ func (fn *formulaFuncs) ISERROR(argsList *list.List) formulaArg { formulaErrorCALC, formulaErrorGETTINGDATA, } { if errType == token.String { - result = "TRUE" + result = true } } } - return newStringFormulaArg(result) + return newBoolFormulaArg(result) } // ISEVEN function tests if a supplied number (or numeric expression) @@ -7190,6 +7192,32 @@ func (fn *formulaFuncs) SHEETS(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) } +// TYPE function returns an integer that represents the value's data type. The +// syntax of the function is: +// +// TYPE(value) +// +func (fn *formulaFuncs) TYPE(argsList *list.List) formulaArg { + if argsList.Len() != 1 { + return newErrorFormulaArg(formulaErrorVALUE, "TYPE requires 1 argument") + } + token := argsList.Front().Value.(formulaArg) + switch token.Type { + case ArgError: + 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 { + return newNumberFormulaArg(4) + } + return newNumberFormulaArg(2) + } +} + // T function tests if a supplied value is text and if so, returns the // supplied text; Otherwise, the function returns an empty text string. The // syntax of the function is: @@ -7343,7 +7371,6 @@ func (fn *formulaFuncs) NOT(argsList *list.List) formulaArg { case ArgNumber: return newBoolFormulaArg(!(token.Number != 0)) case ArgError: - return token } return newErrorFormulaArg(formulaErrorVALUE, "NOT expects 1 boolean or numeric argument") @@ -9415,6 +9442,32 @@ func (fn *formulaFuncs) COLUMNS(argsList *list.List) formulaArg { return newNumberFormulaArg(float64(result)) } +// FORMULATEXT function returns a formula as a text string. The syntax of the +// function is: +// +// FORMULATEXT(reference) +// +func (fn *formulaFuncs) FORMULATEXT(argsList *list.List) formulaArg { + if argsList.Len() != 1 { + return newErrorFormulaArg(formulaErrorVALUE, "FORMULATEXT requires 1 argument") + } + refs := argsList.Front().Value.(formulaArg).cellRefs + col, row := 0, 0 + if refs != nil && refs.Len() > 0 { + col, row = refs.Front().Value.(cellRef).Col, refs.Front().Value.(cellRef).Row + } + ranges := argsList.Front().Value.(formulaArg).cellRanges + if ranges != nil && ranges.Len() > 0 { + col, row = ranges.Front().Value.(cellRange).From.Col, ranges.Front().Value.(cellRange).From.Row + } + cell, err := CoordinatesToCellName(col, row) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + formula, _ := fn.f.GetCellFormula(fn.sheet, cell) + return newStringFormulaArg(formula) +} + // checkHVLookupArgs checking arguments, prepare extract mode, lookup value, // and data for the formula functions HLOOKUP and VLOOKUP. func checkHVLookupArgs(name string, argsList *list.List) (idx int, lookupValue, tableArray, matchMode, errArg formulaArg) { diff --git a/calc_test.go b/calc_test.go index e620eb362d..d01f31bd5a 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1005,8 +1005,9 @@ func TestCalcCellValue(t *testing.T) { "=ISERR(NA())": "FALSE", "=ISERR(POWER(0,-1)))": "TRUE", // ISERROR - "=ISERROR(A1)": "FALSE", - "=ISERROR(NA())": "TRUE", + "=ISERROR(A1)": "FALSE", + "=ISERROR(NA())": "TRUE", + "=ISERROR(\"#VALUE!\")": "FALSE", // ISEVEN "=ISEVEN(A1)": "FALSE", "=ISEVEN(A2)": "TRUE", @@ -1055,6 +1056,14 @@ func TestCalcCellValue(t *testing.T) { // SHEETS "=SHEETS()": "1", "=SHEETS(A1)": "1", + // TYPE + "=TYPE(2)": "1", + "=TYPE(10/2)": "1", + "=TYPE(C1)": "1", + "=TYPE(\"text\")": "2", + "=TYPE(TRUE)": "4", + "=TYPE(NA())": "16", + "=TYPE(MUNIT(2))": "64", // T "=T(\"text\")": "text", "=T(N(10))": "", @@ -2536,6 +2545,8 @@ func TestCalcCellValue(t *testing.T) { // SHEETS "=SHEETS(\"\",\"\")": "SHEETS accepts at most 1 argument", "=SHEETS(\"Sheet1\")": "#N/A", + // TYPE + "=TYPE()": "TYPE requires 1 argument", // T "=T()": "T requires 1 argument", "=T(NA())": "#N/A", @@ -2839,6 +2850,9 @@ func TestCalcCellValue(t *testing.T) { "=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!", // HLOOKUP "=HLOOKUP()": "HLOOKUP requires at least 3 arguments", "=HLOOKUP(D2,D1,1,FALSE)": "HLOOKUP requires second argument of table array", @@ -3693,6 +3707,17 @@ func TestCalcAVERAGEIF(t *testing.T) { } } +func TestCalcFORMULATEXT(t *testing.T) { + f, formulaText := NewFile(), "=SUM(B1:C1)" + assert.NoError(t, f.SetCellFormula("Sheet1", "A1", formulaText)) + for _, formula := range []string{"=FORMULATEXT(A1)", "=FORMULATEXT(A:A)", "=FORMULATEXT(A1:B1)"} { + assert.NoError(t, f.SetCellFormula("Sheet1", "D1", formula), formula) + result, err := f.CalcCellValue("Sheet1", "D1") + assert.NoError(t, err, formula) + assert.Equal(t, formulaText, result, formula) + } +} + func TestCalcHLOOKUP(t *testing.T) { cellData := [][]interface{}{ {"Example Result Table"}, From 3971e8a48b25614dfe685c4766c1bb66cd22fe18 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 2 Mar 2022 00:05:37 +0800 Subject: [PATCH 546/957] ref #65, new formula functions: COVAR, COVARIANCE.P, EXPON.DIST and EXPONDIST --- calc.go | 96 ++++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 61 +++++++++++++++++++++++++++++++++ 2 files changed, 157 insertions(+) diff --git a/calc.go b/calc.go index 0f44912677..ab205ce21e 100644 --- a/calc.go +++ b/calc.go @@ -373,6 +373,8 @@ type formulaFuncs struct { // COUPNCD // COUPNUM // COUPPCD +// COVAR +// COVARIANCE.P // CSC // CSCH // CUMIPMT @@ -405,6 +407,8 @@ type formulaFuncs struct { // EVEN // EXACT // EXP +// EXPON.DIST +// EXPONDIST // FACT // FACTDOUBLE // FALSE @@ -5203,6 +5207,51 @@ func (fn *formulaFuncs) CONFIDENCEdotNORM(argsList *list.List) formulaArg { return fn.confidence("CONFIDENCE.NORM", argsList) } +// 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 { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, "COVAR requires 2 arguments") + } + array1 := argsList.Front().Value.(formulaArg) + array2 := argsList.Back().Value.(formulaArg) + left, right := array1.ToList(), array2.ToList() + n := len(left) + if n != len(right) { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + } + l1, l2 := list.New(), list.New() + l1.PushBack(array1) + l2.PushBack(array2) + result, skip := 0.0, 0 + mean1, mean2 := fn.AVERAGE(l1), fn.AVERAGE(l2) + for i := 0; i < n; i++ { + arg1 := left[i].ToNumber() + arg2 := right[i].ToNumber() + if arg1.Type == ArgError || arg2.Type == ArgError { + skip++ + continue + } + result += (arg1.Number - mean1.Number) * (arg2.Number - mean2.Number) + } + return newNumberFormulaArg(result / float64(n-skip)) +} + +// 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) +} + // calcStringCountSum is part of the implementation countSum. func calcStringCountSum(countText bool, count, sum float64, num, arg formulaArg) (float64, float64) { if countText && num.Type == ArgError && arg.String != "" { @@ -5628,6 +5677,53 @@ func (fn *formulaFuncs) KURT(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV) } +// EXPONdotDIST function returns the value of the exponential distribution for +// a give value of x. The user can specify whether the probability density +// function or the cumulative distribution function is used. The syntax of the +// Expondist function is: +// +// 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") + } + return fn.EXPONDIST(argsList) +} + +// EXPONDIST function returns the value of the exponential distribution for a +// give value of x. The user can specify whether the probability density +// function or the cumulative distribution function is used. The syntax of the +// Expondist function is: +// +// EXPONDIST(x,lambda,cumulative) +// +func (fn *formulaFuncs) EXPONDIST(argsList *list.List) formulaArg { + if argsList.Len() != 3 { + return newErrorFormulaArg(formulaErrorVALUE, "EXPONDIST requires 3 arguments") + } + var x, lambda, cumulative formulaArg + if x = argsList.Front().Value.(formulaArg).ToNumber(); x.Type != ArgNumber { + return x + } + if lambda = argsList.Front().Next().Value.(formulaArg).ToNumber(); lambda.Type != ArgNumber { + return lambda + } + if cumulative = argsList.Back().Value.(formulaArg).ToBool(); cumulative.Type == ArgError { + return cumulative + } + if x.Number < 0 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + if lambda.Number <= 0 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + if cumulative.Number == 1 { + return newNumberFormulaArg(1 - math.Exp(-lambda.Number*x.Number)) + } + return newNumberFormulaArg(lambda.Number * math.Exp(-lambda.Number*x.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 d01f31bd5a..a804b58fcf 100644 --- a/calc_test.go +++ b/calc_test.go @@ -834,6 +834,14 @@ func TestCalcCellValue(t *testing.T) { "=KURT(F1:F9)": "-1.03350350255137", "=KURT(F1,F2:F9)": "-1.03350350255137", "=KURT(INT(1),MUNIT(2))": "-3.33333333333334", + // EXPON.DIST + "=EXPON.DIST(0.5,1,TRUE)": "0.393469340287367", + "=EXPON.DIST(0.5,1,FALSE)": "0.606530659712633", + "=EXPON.DIST(2,1,TRUE)": "0.864664716763387", + // EXPONDIST + "=EXPONDIST(0.5,1,TRUE)": "0.393469340287367", + "=EXPONDIST(0.5,1,FALSE)": "0.606530659712633", + "=EXPONDIST(2,1,TRUE)": "0.864664716763387", // NORM.DIST "=NORM.DIST(0.8,1,0.3,TRUE)": "0.252492537546923", "=NORM.DIST(50,40,20,FALSE)": "0.017603266338215", @@ -2315,6 +2323,20 @@ func TestCalcCellValue(t *testing.T) { // KURT "=KURT()": "KURT requires at least 1 argument", "=KURT(F1,INT(1))": "#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!", + // 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!", // NORM.DIST "=NORM.DIST()": "NORM.DIST requires 4 arguments", // NORMDIST @@ -3707,6 +3729,45 @@ func TestCalcAVERAGEIF(t *testing.T) { } } +func TestCalcCOVAR(t *testing.T) { + cellData := [][]interface{}{ + {"array1", "array2"}, + {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{ + "=COVAR(A1:A9,B1:B9)": "16.633125", + "=COVAR(A2:A9,B2:B9)": "16.633125", + "=COVARIANCE.P(A1:A9,B1:B9)": "16.633125", + "=COVARIANCE.P(A2:A9,B2:B9)": "16.633125", + } + 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{ + "=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", + } + 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 TestCalcFORMULATEXT(t *testing.T) { f, formulaText := NewFile(), "=SUM(B1:C1)" assert.NoError(t, f.SetCellFormula("Sheet1", "A1", formulaText)) From 129052ae7db0fd2c59b1ea9158df0e75450cad42 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 4 Mar 2022 00:44:50 +0800 Subject: [PATCH 547/957] This closes #1164, fix nested formula calculation result error --- calc.go | 11 +++++++++++ calc_test.go | 1 + 2 files changed, 12 insertions(+) diff --git a/calc.go b/calc.go index ab205ce21e..c3b8bf9412 100644 --- a/calc.go +++ b/calc.go @@ -809,6 +809,17 @@ 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(), + }) + 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 diff --git a/calc_test.go b/calc_test.go index a804b58fcf..3749702f1a 100644 --- a/calc_test.go +++ b/calc_test.go @@ -725,6 +725,7 @@ func TestCalcCellValue(t *testing.T) { "=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", "=SUM(1+ROW())": "2", + "=SUM((SUM(2))+1)": "3", // SUMIF `=SUMIF(F1:F5, "")`: "0", `=SUMIF(A1:A5, "3")`: "3", From f0cb29cf6668ab96992b1e48278d9f5b1f9e4976 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 5 Mar 2022 14:48:34 +0800 Subject: [PATCH 548/957] This closes #1162, improve the compatibility with alternate content Preserve alternate content in the workbook, worksheet, and drawingML --- comment_test.go | 3 +- drawing.go | 6 +++ drawing_test.go | 7 +++- rows_test.go | 5 ++- sheet.go | 7 ++++ sheet_test.go | 17 ++++++++- workbook.go | 7 ++++ xmlDecodeDrawing.go | 13 ++++--- xmlDrawing.go | 15 ++++---- xmlWorkbook.go | 44 +++++++++++----------- xmlWorksheet.go | 92 +++++++++++++++++++++++++-------------------- 11 files changed, 137 insertions(+), 79 deletions(-) diff --git a/comment_test.go b/comment_test.go index 952763dfd7..01f1e42e3e 100644 --- a/comment_test.go +++ b/comment_test.go @@ -12,6 +12,7 @@ package excelize import ( + "encoding/xml" "path/filepath" "strings" "testing" @@ -38,7 +39,7 @@ func TestAddComments(t *testing.T) { } f.Comments["xl/comments2.xml"] = nil - f.Pkg.Store("xl/comments2.xml", []byte(`Excelize: Excelize: `)) + f.Pkg.Store("xl/comments2.xml", []byte(xml.Header+`Excelize: Excelize: `)) comments := f.GetComments() assert.EqualValues(t, 2, len(comments["Sheet1"])) assert.EqualValues(t, 1, len(comments["Sheet2"])) diff --git a/drawing.go b/drawing.go index 0bc79005f7..5582bb4ff1 100644 --- a/drawing.go +++ b/drawing.go @@ -1160,6 +1160,12 @@ func (f *File) drawingParser(path string) (*xlsxWsDr, int) { log.Printf("xml decode error: %s", err) } content.R = decodeWsDr.R + for _, v := range decodeWsDr.AlternateContent { + content.AlternateContent = append(content.AlternateContent, &xlsxAlternateContent{ + Content: v.Content, + XMLNSMC: SourceRelationshipCompatibility.Value, + }) + } for _, v := range decodeWsDr.OneCellAnchor { content.OneCellAnchor = append(content.OneCellAnchor, &xdrCellAnchor{ EditAs: v.EditAs, diff --git a/drawing_test.go b/drawing_test.go index d33977fb89..e37b771fdd 100644 --- a/drawing_test.go +++ b/drawing_test.go @@ -12,6 +12,7 @@ package excelize import ( + "encoding/xml" "sync" "testing" ) @@ -22,9 +23,13 @@ func TestDrawingParser(t *testing.T) { Pkg: sync.Map{}, } f.Pkg.Store("charset", MacintoshCyrillicCharset) - f.Pkg.Store("wsDr", []byte(``)) + 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 + f.Drawings = sync.Map{} + f.Pkg.Store("wsDr", []byte(xml.Header+``)) + f.drawingParser("wsDr") } diff --git a/rows_test.go b/rows_test.go index 1286a3773d..208b2de841 100644 --- a/rows_test.go +++ b/rows_test.go @@ -2,6 +2,7 @@ package excelize import ( "bytes" + "encoding/xml" "fmt" "path/filepath" "testing" @@ -901,12 +902,12 @@ func TestErrSheetNotExistError(t *testing.T) { func TestCheckRow(t *testing.T) { f := NewFile() - f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`12345`)) + f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(xml.Header+`12345`)) _, err := f.GetRows("Sheet1") assert.NoError(t, err) assert.NoError(t, f.SetCellValue("Sheet1", "A1", false)) f = NewFile() - f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`12345`)) + f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(xml.Header+`12345`)) f.Sheet.Delete("xl/worksheets/sheet1.xml") delete(f.checked, "xl/worksheets/sheet1.xml") assert.EqualError(t, f.SetCellValue("Sheet1", "A1", false), newCellNameToCoordinatesError("-", newInvalidCellNameError("-")).Error()) diff --git a/sheet.go b/sheet.go index b440fb945f..78fcaf2130 100644 --- a/sheet.go +++ b/sheet.go @@ -158,6 +158,13 @@ func (f *File) workSheetWriter() { if sheet.SheetPr != nil || sheet.Drawing != nil || sheet.Hyperlinks != nil || sheet.Picture != nil || sheet.TableParts != nil { f.addNameSpaces(p.(string), SourceRelationship) } + if sheet.DecodeAlternateContent != nil { + sheet.AlternateContent = &xlsxAlternateContent{ + Content: sheet.DecodeAlternateContent.Content, + XMLNSMC: SourceRelationshipCompatibility.Value, + } + } + sheet.DecodeAlternateContent = nil // reusing buffer _ = encoder.Encode(sheet) f.saveFileList(p.(string), replaceRelationshipsBytes(f.replaceNameSpaceBytes(p.(string), buffer.Bytes()))) diff --git a/sheet_test.go b/sheet_test.go index 7df9018b1c..429f617247 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -1,6 +1,7 @@ package excelize import ( + "encoding/xml" "fmt" "path/filepath" "strconv" @@ -404,6 +405,20 @@ func TestSetSheetName(t *testing.T) { assert.Equal(t, "Sheet1", f.GetSheetName(0)) } +func TestWorksheetWriter(t *testing.T) { + f := NewFile() + // Test set cell value with alternate content + f.Sheet.Delete("xl/worksheets/sheet1.xml") + worksheet := xml.Header + `%d` + f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(worksheet, 1))) + f.checked = nil + assert.NoError(t, f.SetCellValue("Sheet1", "A1", 2)) + f.workSheetWriter() + value, ok := f.Pkg.Load("xl/worksheets/sheet1.xml") + assert.True(t, ok) + assert.Equal(t, fmt.Sprintf(worksheet, 2), string(value.([]byte))) +} + func TestGetWorkbookPath(t *testing.T) { f := NewFile() f.Pkg.Delete("_rels/.rels") @@ -413,7 +428,7 @@ func TestGetWorkbookPath(t *testing.T) { func TestGetWorkbookRelsPath(t *testing.T) { f := NewFile() f.Pkg.Delete("xl/_rels/.rels") - f.Pkg.Store("_rels/.rels", []byte(``)) + f.Pkg.Store("_rels/.rels", []byte(xml.Header+``)) assert.Equal(t, "_rels/workbook.xml.rels", f.getWorkbookRelsPath()) } diff --git a/workbook.go b/workbook.go index 3d9fa480b2..c65397b6fc 100644 --- a/workbook.go +++ b/workbook.go @@ -101,6 +101,13 @@ func (f *File) workbookReader() *xlsxWorkbook { // structure. func (f *File) workBookWriter() { if f.WorkBook != nil { + if f.WorkBook.DecodeAlternateContent != nil { + f.WorkBook.AlternateContent = &xlsxAlternateContent{ + Content: f.WorkBook.DecodeAlternateContent.Content, + XMLNSMC: SourceRelationshipCompatibility.Value, + } + } + f.WorkBook.DecodeAlternateContent = nil output, _ := xml.Marshal(f.WorkBook) f.saveFileList(f.getWorkbookPath(), replaceRelationshipsBytes(f.replaceNameSpaceBytes(f.getWorkbookPath(), output))) } diff --git a/xmlDecodeDrawing.go b/xmlDecodeDrawing.go index 9091440023..fb920be1d8 100644 --- a/xmlDecodeDrawing.go +++ b/xmlDecodeDrawing.go @@ -63,12 +63,13 @@ type decodeCNvSpPr struct { // changed after serialization and deserialization, two different structures // are defined. decodeWsDr just for deserialization. type decodeWsDr struct { - A string `xml:"xmlns a,attr"` - Xdr string `xml:"xmlns xdr,attr"` - R string `xml:"xmlns r,attr"` - OneCellAnchor []*decodeCellAnchor `xml:"oneCellAnchor,omitempty"` - TwoCellAnchor []*decodeCellAnchor `xml:"twoCellAnchor,omitempty"` - XMLName xml.Name `xml:"http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing wsDr,omitempty"` + XMLName xml.Name `xml:"http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing wsDr,omitempty"` + A string `xml:"xmlns a,attr"` + Xdr string `xml:"xmlns xdr,attr"` + R string `xml:"xmlns r,attr"` + AlternateContent []*xlsxInnerXML `xml:"http://schemas.openxmlformats.org/markup-compatibility/2006 AlternateContent"` + OneCellAnchor []*decodeCellAnchor `xml:"oneCellAnchor,omitempty"` + TwoCellAnchor []*decodeCellAnchor `xml:"twoCellAnchor,omitempty"` } // decodeTwoCellAnchor directly maps the oneCellAnchor (One Cell Anchor Shape diff --git a/xmlDrawing.go b/xmlDrawing.go index c96034ce5c..4bf43ec89b 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -320,13 +320,14 @@ type xlsxPoint2D struct { // wsDr. type xlsxWsDr struct { sync.Mutex - XMLName xml.Name `xml:"xdr:wsDr"` - AbsoluteAnchor []*xdrCellAnchor `xml:"xdr:absoluteAnchor"` - OneCellAnchor []*xdrCellAnchor `xml:"xdr:oneCellAnchor"` - TwoCellAnchor []*xdrCellAnchor `xml:"xdr:twoCellAnchor"` - A string `xml:"xmlns:a,attr,omitempty"` - Xdr string `xml:"xmlns:xdr,attr,omitempty"` - R string `xml:"xmlns:r,attr,omitempty"` + XMLName xml.Name `xml:"xdr:wsDr"` + A string `xml:"xmlns:a,attr,omitempty"` + Xdr string `xml:"xmlns:xdr,attr,omitempty"` + R string `xml:"xmlns:r,attr,omitempty"` + AlternateContent []*xlsxAlternateContent `xml:"mc:AlternateContent"` + AbsoluteAnchor []*xdrCellAnchor `xml:"xdr:absoluteAnchor"` + OneCellAnchor []*xdrCellAnchor `xml:"xdr:oneCellAnchor"` + TwoCellAnchor []*xdrCellAnchor `xml:"xdr:twoCellAnchor"` } // xlsxGraphicFrame (Graphic Frame) directly maps the xdr:graphicFrame element. diff --git a/xmlWorkbook.go b/xmlWorkbook.go index 2bb417c36d..e344dbff99 100644 --- a/xmlWorkbook.go +++ b/xmlWorkbook.go @@ -35,27 +35,29 @@ type xlsxRelationship struct { // content of the workbook. The workbook's child elements each have their own // subclause references. type xlsxWorkbook struct { - XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main workbook"` - Conformance string `xml:"conformance,attr,omitempty"` - FileVersion *xlsxFileVersion `xml:"fileVersion"` - FileSharing *xlsxExtLst `xml:"fileSharing"` - WorkbookPr *xlsxWorkbookPr `xml:"workbookPr"` - WorkbookProtection *xlsxWorkbookProtection `xml:"workbookProtection"` - BookViews *xlsxBookViews `xml:"bookViews"` - Sheets xlsxSheets `xml:"sheets"` - FunctionGroups *xlsxExtLst `xml:"functionGroups"` - ExternalReferences *xlsxExternalReferences `xml:"externalReferences"` - DefinedNames *xlsxDefinedNames `xml:"definedNames"` - CalcPr *xlsxCalcPr `xml:"calcPr"` - OleSize *xlsxExtLst `xml:"oleSize"` - CustomWorkbookViews *xlsxCustomWorkbookViews `xml:"customWorkbookViews"` - PivotCaches *xlsxPivotCaches `xml:"pivotCaches"` - SmartTagPr *xlsxExtLst `xml:"smartTagPr"` - SmartTagTypes *xlsxExtLst `xml:"smartTagTypes"` - WebPublishing *xlsxExtLst `xml:"webPublishing"` - FileRecoveryPr *xlsxFileRecoveryPr `xml:"fileRecoveryPr"` - WebPublishObjects *xlsxExtLst `xml:"webPublishObjects"` - ExtLst *xlsxExtLst `xml:"extLst"` + XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main workbook"` + Conformance string `xml:"conformance,attr,omitempty"` + FileVersion *xlsxFileVersion `xml:"fileVersion"` + FileSharing *xlsxExtLst `xml:"fileSharing"` + AlternateContent *xlsxAlternateContent `xml:"mc:AlternateContent"` + DecodeAlternateContent *xlsxInnerXML `xml:"http://schemas.openxmlformats.org/markup-compatibility/2006 AlternateContent"` + WorkbookPr *xlsxWorkbookPr `xml:"workbookPr"` + WorkbookProtection *xlsxWorkbookProtection `xml:"workbookProtection"` + BookViews *xlsxBookViews `xml:"bookViews"` + Sheets xlsxSheets `xml:"sheets"` + FunctionGroups *xlsxExtLst `xml:"functionGroups"` + ExternalReferences *xlsxExternalReferences `xml:"externalReferences"` + DefinedNames *xlsxDefinedNames `xml:"definedNames"` + CalcPr *xlsxCalcPr `xml:"calcPr"` + OleSize *xlsxExtLst `xml:"oleSize"` + CustomWorkbookViews *xlsxCustomWorkbookViews `xml:"customWorkbookViews"` + PivotCaches *xlsxPivotCaches `xml:"pivotCaches"` + SmartTagPr *xlsxExtLst `xml:"smartTagPr"` + SmartTagTypes *xlsxExtLst `xml:"smartTagTypes"` + WebPublishing *xlsxExtLst `xml:"webPublishing"` + FileRecoveryPr *xlsxFileRecoveryPr `xml:"fileRecoveryPr"` + WebPublishObjects *xlsxExtLst `xml:"webPublishObjects"` + ExtLst *xlsxExtLst `xml:"extLst"` } // xlsxFileRecoveryPr maps sheet recovery information. This element defines diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 4a9c88a9dc..c327d3c4ac 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -20,46 +20,48 @@ import ( // http://schemas.openxmlformats.org/spreadsheetml/2006/main. type xlsxWorksheet struct { sync.Mutex - XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main worksheet"` - SheetPr *xlsxSheetPr `xml:"sheetPr"` - Dimension *xlsxDimension `xml:"dimension"` - SheetViews *xlsxSheetViews `xml:"sheetViews"` - SheetFormatPr *xlsxSheetFormatPr `xml:"sheetFormatPr"` - Cols *xlsxCols `xml:"cols"` - SheetData xlsxSheetData `xml:"sheetData"` - SheetCalcPr *xlsxInnerXML `xml:"sheetCalcPr"` - SheetProtection *xlsxSheetProtection `xml:"sheetProtection"` - ProtectedRanges *xlsxInnerXML `xml:"protectedRanges"` - Scenarios *xlsxInnerXML `xml:"scenarios"` - AutoFilter *xlsxAutoFilter `xml:"autoFilter"` - SortState *xlsxSortState `xml:"sortState"` - DataConsolidate *xlsxInnerXML `xml:"dataConsolidate"` - CustomSheetViews *xlsxCustomSheetViews `xml:"customSheetViews"` - MergeCells *xlsxMergeCells `xml:"mergeCells"` - PhoneticPr *xlsxPhoneticPr `xml:"phoneticPr"` - ConditionalFormatting []*xlsxConditionalFormatting `xml:"conditionalFormatting"` - DataValidations *xlsxDataValidations `xml:"dataValidations"` - Hyperlinks *xlsxHyperlinks `xml:"hyperlinks"` - PrintOptions *xlsxPrintOptions `xml:"printOptions"` - PageMargins *xlsxPageMargins `xml:"pageMargins"` - PageSetUp *xlsxPageSetUp `xml:"pageSetup"` - HeaderFooter *xlsxHeaderFooter `xml:"headerFooter"` - RowBreaks *xlsxBreaks `xml:"rowBreaks"` - ColBreaks *xlsxBreaks `xml:"colBreaks"` - CustomProperties *xlsxInnerXML `xml:"customProperties"` - CellWatches *xlsxInnerXML `xml:"cellWatches"` - IgnoredErrors *xlsxInnerXML `xml:"ignoredErrors"` - SmartTags *xlsxInnerXML `xml:"smartTags"` - Drawing *xlsxDrawing `xml:"drawing"` - LegacyDrawing *xlsxLegacyDrawing `xml:"legacyDrawing"` - LegacyDrawingHF *xlsxLegacyDrawingHF `xml:"legacyDrawingHF"` - DrawingHF *xlsxDrawingHF `xml:"drawingHF"` - Picture *xlsxPicture `xml:"picture"` - OleObjects *xlsxInnerXML `xml:"oleObjects"` - Controls *xlsxInnerXML `xml:"controls"` - WebPublishItems *xlsxInnerXML `xml:"webPublishItems"` - TableParts *xlsxTableParts `xml:"tableParts"` - ExtLst *xlsxExtLst `xml:"extLst"` + XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main worksheet"` + SheetPr *xlsxSheetPr `xml:"sheetPr"` + Dimension *xlsxDimension `xml:"dimension"` + SheetViews *xlsxSheetViews `xml:"sheetViews"` + SheetFormatPr *xlsxSheetFormatPr `xml:"sheetFormatPr"` + Cols *xlsxCols `xml:"cols"` + SheetData xlsxSheetData `xml:"sheetData"` + SheetCalcPr *xlsxInnerXML `xml:"sheetCalcPr"` + SheetProtection *xlsxSheetProtection `xml:"sheetProtection"` + ProtectedRanges *xlsxInnerXML `xml:"protectedRanges"` + Scenarios *xlsxInnerXML `xml:"scenarios"` + AutoFilter *xlsxAutoFilter `xml:"autoFilter"` + SortState *xlsxSortState `xml:"sortState"` + DataConsolidate *xlsxInnerXML `xml:"dataConsolidate"` + CustomSheetViews *xlsxCustomSheetViews `xml:"customSheetViews"` + MergeCells *xlsxMergeCells `xml:"mergeCells"` + PhoneticPr *xlsxPhoneticPr `xml:"phoneticPr"` + ConditionalFormatting []*xlsxConditionalFormatting `xml:"conditionalFormatting"` + DataValidations *xlsxDataValidations `xml:"dataValidations"` + Hyperlinks *xlsxHyperlinks `xml:"hyperlinks"` + PrintOptions *xlsxPrintOptions `xml:"printOptions"` + PageMargins *xlsxPageMargins `xml:"pageMargins"` + PageSetUp *xlsxPageSetUp `xml:"pageSetup"` + HeaderFooter *xlsxHeaderFooter `xml:"headerFooter"` + RowBreaks *xlsxBreaks `xml:"rowBreaks"` + ColBreaks *xlsxBreaks `xml:"colBreaks"` + CustomProperties *xlsxInnerXML `xml:"customProperties"` + CellWatches *xlsxInnerXML `xml:"cellWatches"` + IgnoredErrors *xlsxInnerXML `xml:"ignoredErrors"` + SmartTags *xlsxInnerXML `xml:"smartTags"` + Drawing *xlsxDrawing `xml:"drawing"` + LegacyDrawing *xlsxLegacyDrawing `xml:"legacyDrawing"` + LegacyDrawingHF *xlsxLegacyDrawingHF `xml:"legacyDrawingHF"` + DrawingHF *xlsxDrawingHF `xml:"drawingHF"` + Picture *xlsxPicture `xml:"picture"` + OleObjects *xlsxInnerXML `xml:"oleObjects"` + Controls *xlsxInnerXML `xml:"controls"` + WebPublishItems *xlsxInnerXML `xml:"webPublishItems"` + TableParts *xlsxTableParts `xml:"tableParts"` + ExtLst *xlsxExtLst `xml:"extLst"` + AlternateContent *xlsxAlternateContent `xml:"mc:AlternateContent"` + DecodeAlternateContent *xlsxInnerXML `xml:"http://schemas.openxmlformats.org/markup-compatibility/2006 AlternateContent"` } // xlsxDrawing change r:id to rid in the namespace. @@ -692,6 +694,16 @@ type xlsxLegacyDrawingHF struct { RID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr,omitempty"` } +// xlsxAlternateContent is a container for a sequence of multiple +// representations of a given piece of content. The program reading the file +// should only process one of these, and the one chosen should be based on +// which conditions match. +type xlsxAlternateContent struct { + XMLNSMC string `xml:"xmlns:mc,attr,omitempty"` + Content string `xml:",innerxml"` +} + +// xlsxInnerXML holds parts of XML content currently not unmarshal. type xlsxInnerXML struct { Content string `xml:",innerxml"` } From 354d1696d8999ea00cb420f633a9abaae67228b6 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 6 Mar 2022 00:29:33 +0800 Subject: [PATCH 549/957] ref #65, new formula functions: CORREL, SUMX2MY2, SUMX2PY2, and SUMXMY2 --- calc.go | 100 +++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 21 +++++++++++ 2 files changed, 121 insertions(+) diff --git a/calc.go b/calc.go index c3b8bf9412..2febdebd8a 100644 --- a/calc.go +++ b/calc.go @@ -358,6 +358,7 @@ type formulaFuncs struct { // CONCATENATE // CONFIDENCE // CONFIDENCE.NORM +// CORREL // COS // COSH // COT @@ -603,6 +604,9 @@ type formulaFuncs struct { // SUM // SUMIF // SUMSQ +// SUMX2MY2 +// SUMX2PY2 +// SUMXMY2 // SWITCH // SYD // T @@ -4945,6 +4949,63 @@ func (fn *formulaFuncs) SUMSQ(argsList *list.List) formulaArg { return newNumberFormulaArg(sq) } +// sumx is an implementation of the formula functions SUMX2MY2, SUMX2PY2 and +// SUMXMY2. +func (fn *formulaFuncs) sumx(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) + array2 := argsList.Back().Value.(formulaArg) + left, right := array1.ToList(), array2.ToList() + n := len(left) + if n != len(right) { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + } + result := 0.0 + for i := 0; i < n; i++ { + if lhs, rhs := left[i].ToNumber(), right[i].ToNumber(); lhs.Number != 0 && rhs.Number != 0 { + switch name { + case "SUMX2MY2": + result += lhs.Number*lhs.Number - rhs.Number*rhs.Number + case "SUMX2PY2": + result += lhs.Number*lhs.Number + rhs.Number*rhs.Number + default: + result += (lhs.Number - rhs.Number) * (lhs.Number - rhs.Number) + } + } + } + return newNumberFormulaArg(result) +} + +// 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) +// +func (fn *formulaFuncs) SUMX2MY2(argsList *list.List) formulaArg { + return fn.sumx("SUMX2MY2", argsList) +} + +// 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) +// +func (fn *formulaFuncs) SUMX2PY2(argsList *list.List) formulaArg { + return fn.sumx("SUMX2PY2", argsList) +} + +// SUMXMY2 function returns the sum of the squares of differences between +// corresponding values in two supplied arrays. The syntax of the function +// is: +// +// SUMXMY2(array_x,array_y) +// +func (fn *formulaFuncs) SUMXMY2(argsList *list.List) formulaArg { + return fn.sumx("SUMXMY2", argsList) +} + // TAN function calculates the tangent of a given angle. The syntax of the // function is: // @@ -5306,6 +5367,45 @@ func (fn *formulaFuncs) countSum(countText bool, args []formulaArg) (count, sum return } +// CORREL function calculates the Pearson Product-Moment Correlation +// Coefficient for two sets of values. The syntax of the function is: +// +// CORREL(array1,array2) +// +func (fn *formulaFuncs) CORREL(argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, "CORREL requires 2 arguments") + } + array1 := argsList.Front().Value.(formulaArg) + array2 := argsList.Back().Value.(formulaArg) + left, right := array1.ToList(), array2.ToList() + n := len(left) + if n != len(right) { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + } + l1, l2, l3 := list.New(), list.New(), list.New() + for i := 0; i < n; i++ { + if lhs, rhs := left[i].ToNumber(), right[i].ToNumber(); lhs.Number != 0 && rhs.Number != 0 { + l1.PushBack(lhs) + l2.PushBack(rhs) + } + } + stdev1, stdev2 := fn.STDEV(l1), fn.STDEV(l2) + if stdev1.Number == 0 || stdev2.Number == 0 { + return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV) + } + mean1, mean2, skip := fn.AVERAGE(l1), fn.AVERAGE(l2), 0 + for i := 0; i < n; i++ { + lhs, rhs := left[i].ToNumber(), right[i].ToNumber() + if lhs.Number == 0 || rhs.Number == 0 { + skip++ + continue + } + l3.PushBack(newNumberFormulaArg((lhs.Number - mean1.Number) * (rhs.Number - mean2.Number))) + } + return newNumberFormulaArg(fn.SUM(l3).Number / float64(n-skip-1) / stdev1.Number / stdev2.Number) +} + // COUNT function returns the count of numeric values in a supplied set of // cells or values. This count includes both numbers and dates. The syntax of // the function is: diff --git a/calc_test.go b/calc_test.go index 3749702f1a..fa216c9f21 100644 --- a/calc_test.go +++ b/calc_test.go @@ -747,6 +747,12 @@ func TestCalcCellValue(t *testing.T) { `=SUMSQ("",A1,B1,A2,B2,6)`: "82", `=SUMSQ(1,SUMSQ(1))`: "2", "=SUMSQ(MUNIT(3))": "0", + // SUMX2MY2 + "=SUMX2MY2(A1:A4,B1:B4)": "-36", + // SUMX2PY2 + "=SUMX2PY2(A1:A4,B1:B4)": "46", + // SUMXMY2 + "=SUMXMY2(A1:A4,B1:B4)": "18", // TAN "=TAN(1.047197551)": "1.732050806782486", "=TAN(0)": "0", @@ -785,6 +791,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", + // CORREL + "=CORREL(A1:A5,B1:B5)": "1", // COUNT "=COUNT()": "0", "=COUNT(E1:F2,\"text\",1,INT(2))": "3", @@ -2238,6 +2246,15 @@ func TestCalcCellValue(t *testing.T) { // SUMSQ `=SUMSQ("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", "=SUMSQ(C1:D2)": "strconv.ParseFloat: parsing \"Month\": invalid syntax", + // SUMX2MY2 + "=SUMX2MY2()": "SUMX2MY2 requires 2 arguments", + "=SUMX2MY2(A1,B1:B2)": "#N/A", + // SUMX2PY2 + "=SUMX2PY2()": "SUMX2PY2 requires 2 arguments", + "=SUMX2PY2(A1,B1:B2)": "#N/A", + // SUMXMY2 + "=SUMXMY2()": "SUMXMY2 requires 2 arguments", + "=SUMXMY2(A1,B1:B2)": "#N/A", // TAN "=TAN()": "TAN requires 1 numeric argument", `=TAN("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", @@ -2284,6 +2301,10 @@ func TestCalcCellValue(t *testing.T) { "=CONFIDENCE.NORM(1,0.07,100)": "#NUM!", "=CONFIDENCE.NORM(0.05,0,100)": "#NUM!", "=CONFIDENCE.NORM(0.05,0.07,0.5)": "#NUM!", + // CORREL + "=CORREL()": "CORREL requires 2 arguments", + "=CORREL(A1:A3,B1:B5)": "#N/A", + "=CORREL(A1:A1,B1:B1)": "#DIV/0!", // COUNTBLANK "=COUNTBLANK()": "COUNTBLANK requires 1 argument", "=COUNTBLANK(1,2)": "COUNTBLANK requires 1 argument", From 61eb265c29685957bcf16b25dba3d389c548dfee Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 7 Mar 2022 00:07:03 +0800 Subject: [PATCH 550/957] This closes #1171, improve the compatibility and added new formula function ref #65, added new formula function: FINV --- calc.go | 335 +++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 48 +++++++ xmlWorkbook.go | 4 +- 3 files changed, 385 insertions(+), 2 deletions(-) diff --git a/calc.go b/calc.go index 2febdebd8a..e3db8826de 100644 --- a/calc.go +++ b/calc.go @@ -415,6 +415,7 @@ type formulaFuncs struct { // FALSE // FIND // FINDB +// FINV // FISHER // FISHERINV // FIXED @@ -5835,6 +5836,340 @@ func (fn *formulaFuncs) EXPONDIST(argsList *list.List) formulaArg { return newNumberFormulaArg(lambda.Number * math.Exp(-lambda.Number*x.Number)) } +// d1mach returns double precision real machine constants. +func d1mach(i int) float64 { + arr := []float64{ + 2.2250738585072014e-308, + 1.7976931348623158e+308, + 1.1102230246251565e-16, + 2.2204460492503131e-16, + 0.301029995663981195, + } + if i > len(arr) { + return 0 + } + return arr[i-1] +} + +// chebyshevInit determines the number of terms for the double precision +// orthogonal series "dos" needed to insure the error is no larger +// than "eta". Ordinarily eta will be chosen to be one-tenth machine +// precision. +func chebyshevInit(nos int, eta float64, dos []float64) int { + i, e := 0, 0.0 + if nos < 1 { + return 0 + } + for ii := 1; ii <= nos; ii++ { + i = nos - ii + e += math.Abs(dos[i]) + if e > eta { + return i + } + } + return i +} + +// chebyshevEval evaluates the n-term Chebyshev series "a" at "x". +func chebyshevEval(n int, x float64, a []float64) float64 { + if n < 1 || n > 1000 || x < -1.1 || x > 1.1 { + return math.NaN() + } + twox, b0, b1, b2 := x*2, 0.0, 0.0, 0.0 + for i := 1; i <= n; i++ { + b2 = b1 + b1 = b0 + b0 = twox*b1 - b2 + a[n-i] + } + return (b0 - b2) * 0.5 +} + +// lgammacor is an implementation for the log(gamma) correction. +func lgammacor(x float64) float64 { + algmcs := []float64{ + 0.1666389480451863247205729650822, -0.1384948176067563840732986059135e-4, + 0.9810825646924729426157171547487e-8, -0.1809129475572494194263306266719e-10, + 0.6221098041892605227126015543416e-13, -0.3399615005417721944303330599666e-15, + 0.2683181998482698748957538846666e-17, -0.2868042435334643284144622399999e-19, + 0.3962837061046434803679306666666e-21, -0.6831888753985766870111999999999e-23, + 0.1429227355942498147573333333333e-24, -0.3547598158101070547199999999999e-26, + 0.1025680058010470912000000000000e-27, -0.3401102254316748799999999999999e-29, + 0.1276642195630062933333333333333e-30, + } + nalgm := chebyshevInit(15, d1mach(3), algmcs) + xbig := 1.0 / math.Sqrt(d1mach(3)) + xmax := math.Exp(math.Min(math.Log(d1mach(2)/12.0), -math.Log(12.0*d1mach(1)))) + if x < 10.0 { + return math.NaN() + } else if x >= xmax { + return 4.930380657631324e-32 + } else if x < xbig { + tmp := 10.0 / x + return chebyshevEval(nalgm, tmp*tmp*2.0-1.0, algmcs) / x + } + return 1.0 / (x * 12.0) +} + +// logrelerr compute the relative error logarithm. +func logrelerr(x float64) float64 { + alnrcs := []float64{ + 0.10378693562743769800686267719098e+1, -0.13364301504908918098766041553133, + 0.19408249135520563357926199374750e-1, -0.30107551127535777690376537776592e-2, + 0.48694614797154850090456366509137e-3, -0.81054881893175356066809943008622e-4, + 0.13778847799559524782938251496059e-4, -0.23802210894358970251369992914935e-5, + 0.41640416213865183476391859901989e-6, -0.73595828378075994984266837031998e-7, + 0.13117611876241674949152294345011e-7, -0.23546709317742425136696092330175e-8, + 0.42522773276034997775638052962567e-9, -0.77190894134840796826108107493300e-10, + 0.14075746481359069909215356472191e-10, -0.25769072058024680627537078627584e-11, + 0.47342406666294421849154395005938e-12, -0.87249012674742641745301263292675e-13, + 0.16124614902740551465739833119115e-13, -0.29875652015665773006710792416815e-14, + 0.55480701209082887983041321697279e-15, -0.10324619158271569595141333961932e-15, + 0.19250239203049851177878503244868e-16, -0.35955073465265150011189707844266e-17, + 0.67264542537876857892194574226773e-18, -0.12602624168735219252082425637546e-18, + 0.23644884408606210044916158955519e-19, -0.44419377050807936898878389179733e-20, + 0.83546594464034259016241293994666e-21, -0.15731559416479562574899253521066e-21, + 0.29653128740247422686154369706666e-22, -0.55949583481815947292156013226666e-23, + 0.10566354268835681048187284138666e-23, -0.19972483680670204548314999466666e-24, + 0.37782977818839361421049855999999e-25, -0.71531586889081740345038165333333e-26, + 0.13552488463674213646502024533333e-26, -0.25694673048487567430079829333333e-27, + 0.48747756066216949076459519999999e-28, -0.92542112530849715321132373333333e-29, + 0.17578597841760239233269760000000e-29, -0.33410026677731010351377066666666e-30, + 0.63533936180236187354180266666666e-31, + } + nlnrel := chebyshevInit(43, 0.1*d1mach(3), alnrcs) + if x <= -1 { + return math.NaN() + } + if math.Abs(x) <= 0.375 { + return x * (1.0 - x*chebyshevEval(nlnrel, x/0.375, alnrcs)) + } + return math.Log(x + 1.0) +} + +// logBeta is an implementation for the log of the beta distribution +// function. +func logBeta(a, b float64) float64 { + corr, p, q := 0.0, a, a + if b < p { + p = b + } + if b > q { + q = b + } + if p < 0 { + return math.NaN() + } + if p == 0 { + return math.MaxFloat64 + } + 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)) + } + if q >= 10 { + corr = lgammacor(q) - lgammacor(p+q) + val, _ := math.Lgamma(p) + return val + corr + p - p*math.Log(p+q) + (q-0.5)*logrelerr(-p/(p+q)) + } + return math.Log(math.Gamma(p) * (math.Gamma(q) / math.Gamma(p+q))) +} + +// pbetaRaw is a part of pbeta for the beta distribution. +func pbetaRaw(alnsml, ans, eps, p, pin, q, sml, x, y float64) float64 { + if q > 1.0 { + xb := p*math.Log(y) + q*math.Log(1.0-y) - logBeta(p, q) - math.Log(q) + ib := int(math.Max(xb/alnsml, 0.0)) + term := math.Exp(xb - float64(ib)*alnsml) + c := 1.0 / (1.0 - y) + p1 := q * c / (p + q - 1.0) + finsum := 0.0 + n := int(q) + if q == float64(n) { + n = n - 1 + } + for i := 1; i <= n; i++ { + if p1 <= 1 && term/eps <= finsum { + break + } + xi := float64(i) + term = (q - xi + 1.0) * c * term / (p + q - xi) + if term > 1.0 { + ib = ib - 1 + term = term * sml + } + if ib == 0 { + finsum = finsum + term + } + } + ans = ans + finsum + } + if y != x || p != pin { + ans = 1.0 - ans + } + ans = math.Max(math.Min(ans, 1.0), 0.0) + return ans +} + +// pbeta returns distribution function of the beta distribution. +func pbeta(x, pin, qin float64) (ans float64) { + eps := d1mach(3) + alneps := math.Log(eps) + sml := d1mach(1) + alnsml := math.Log(sml) + y := x + p := pin + q := qin + if p/(p+q) < x { + y = 1.0 - y + p = qin + q = pin + } + if (p+q)*y/(p+1.0) < eps { + xb := p*math.Log(math.Max(y, sml)) - math.Log(p) - logBeta(p, q) + if xb > alnsml && y != 0.0 { + ans = math.Exp(xb) + } + if y != x || p != pin { + ans = 1.0 - ans + } + } else { + ps := q - math.Floor(q) + if ps == 0.0 { + ps = 1.0 + } + xb := p*math.Log(y) - logBeta(ps, p) - math.Log(p) + if xb >= alnsml { + ans = math.Exp(xb) + term := ans * p + if ps != 1.0 { + n := int(math.Max(alneps/math.Log(y), 4.0)) + for i := 1; i <= n; i++ { + xi := float64(i) + term = term * (xi - ps) * y / xi + ans = ans + term/(p+xi) + } + } + } + ans = pbetaRaw(alnsml, ans, eps, p, pin, q, sml, x, y) + } + return ans +} + +// 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 { + 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)) + if prop3*prop4 <= 0 { + prev = math.Max(math.Abs(j), maxCumulative) + } + h := 1.0 + for iteratorCount := 0; iteratorCount < 1000; iteratorCount++ { + j = h * prop3 + if math.Abs(j) < prev { + i = beta3 - j + if i >= 0 && i <= 1.0 { + if prev <= alpha3 { + return beta3 + } + if math.Abs(prop3) <= alpha3 { + return beta3 + } + if i != 0 && i != 1.0 { + break + } + } + } + h /= 3.0 + } + if i == beta3 { + return beta3 + } + beta3, prop4 = i, prop3 + } + return beta3 +} + +// betainv is an implementation for the quantile of the beta distribution. +func betainv(probability, alpha, beta, lower, upper float64) float64 { + minCumulative, maxCumulative := 1.0e-300, 3.0e-308 + lowerBound, upperBound := maxCumulative, 1.0-2.22e-16 + needSwap := false + var alpha1, alpha2, beta1, beta2, beta3, prob1, x, y float64 + if probability <= 0.5 { + prob1, alpha1, beta1 = probability, alpha, beta + } else { + prob1, alpha1, beta1, needSwap = 1.0-probability, beta, alpha, true + } + logbeta := 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 { + alpha2, beta2, prob2 = 1/(alpha1+alpha1-1), 1/(beta1+beta1-1), (prob3*prob3-3)/6 + x = 2 / (alpha2 + beta2) + y = prob3*math.Sqrt(x+prob2)/x - (beta2-alpha2)*(prob2+5/6.0-2/(x*3)) + beta3 = alpha1 / (alpha1 + beta1*math.Exp(y+y)) + } else { + 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) + } else { + beta2 = (prob2 + alpha1*4 - 2) / beta2 + if beta2 <= 1 { + beta3 = math.Exp((logbeta + math.Log(alpha1*prob1)) / alpha1) + } else { + beta3 = 1 - 2/(beta2+1) + } + } + } + beta2, prob2 = 1-beta1, 1-alpha1 + if beta3 < lowerBound { + beta3 = lowerBound + } else if beta3 > upperBound { + 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) + if needSwap { + beta3 = 1.0 - beta3 + } + return (upper-lower)*beta3 + lower +} + +// 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) +// +func (fn *formulaFuncs) FINV(argsList *list.List) formulaArg { + if argsList.Len() != 3 { + return newErrorFormulaArg(formulaErrorVALUE, "FINV requires 3 arguments") + } + var probability, d1, d2 formulaArg + if probability = argsList.Front().Value.(formulaArg).ToNumber(); probability.Type != ArgNumber { + return probability + } + if d1 = argsList.Front().Next().Value.(formulaArg).ToNumber(); d1.Type != ArgNumber { + return d1 + } + if d2 = argsList.Back().Value.(formulaArg).ToNumber(); d2.Type != ArgNumber { + return d2 + } + if probability.Number <= 0 || probability.Number > 1 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + if d1.Number < 1 || d1.Number >= math.Pow10(10) { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + if d2.Number < 1 || d2.Number >= math.Pow10(10) { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + return newNumberFormulaArg((1/betainv(1.0-(1.0-probability.Number), d2.Number/2, d1.Number/2, 0, 1) - 1.0) * (d2.Number / d1.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 fa216c9f21..80ba8ef774 100644 --- a/calc_test.go +++ b/calc_test.go @@ -851,6 +851,14 @@ 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", + // FINV + "=FINV(0.2,1,2)": "3.55555555555555", + "=FINV(0.6,1,2)": "0.380952380952381", + "=FINV(0.6,2,2)": "0.666666666666667", + "=FINV(0.6,4,4)": "0.763454070045235", + "=FINV(0.5,4,8)": "0.914645355977072", + "=FINV(0.1,79,86)": "1.32646097270444", + "=FINV(1,40,5)": "0", // NORM.DIST "=NORM.DIST(0.8,1,0.3,TRUE)": "0.252492537546923", "=NORM.DIST(50,40,20,FALSE)": "0.017603266338215", @@ -2359,6 +2367,14 @@ func TestCalcCellValue(t *testing.T) { "=EXPONDIST(0,1,\"\")": "strconv.ParseBool: parsing \"\": invalid syntax", "=EXPONDIST(-1,1,TRUE)": "#NUM!", "=EXPONDIST(1,0,TRUE)": "#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!", // NORM.DIST "=NORM.DIST()": "NORM.DIST requires 4 arguments", // NORMDIST @@ -4220,6 +4236,38 @@ func TestGetYearDays(t *testing.T) { } } +func TestCalcD1mach(t *testing.T) { + assert.Equal(t, 0.0, d1mach(6)) +} + +func TestCalcChebyshevInit(t *testing.T) { + assert.Equal(t, 0, chebyshevInit(0, 0, nil)) + assert.Equal(t, 0, chebyshevInit(1, 0, []float64{0})) +} + +func TestCalcChebyshevEval(t *testing.T) { + assert.True(t, math.IsNaN(chebyshevEval(0, 0, nil))) +} + +func TestCalcLgammacor(t *testing.T) { + assert.True(t, math.IsNaN(lgammacor(9))) + assert.Equal(t, 4.930380657631324e-32, lgammacor(3.7451940309632633e+306)) + assert.Equal(t, 8.333333333333334e-10, lgammacor(10e+07)) +} + +func TestCalcLgammaerr(t *testing.T) { + assert.True(t, math.IsNaN(logrelerr(-2))) +} + +func TestCalcLogBeta(t *testing.T) { + assert.True(t, math.IsNaN(logBeta(-1, -1))) + assert.Equal(t, math.MaxFloat64, logBeta(0, 0)) +} + +func TestCalcBetainvProbIterator(t *testing.T) { + assert.Equal(t, 1.0, betainvProbIterator(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, true)) +} + func TestNestedFunctionsWithOperators(t *testing.T) { f := NewFile() formulaList := map[string]string{ diff --git a/xmlWorkbook.go b/xmlWorkbook.go index e344dbff99..a500a34d4c 100644 --- a/xmlWorkbook.go +++ b/xmlWorkbook.go @@ -39,10 +39,10 @@ type xlsxWorkbook struct { Conformance string `xml:"conformance,attr,omitempty"` FileVersion *xlsxFileVersion `xml:"fileVersion"` FileSharing *xlsxExtLst `xml:"fileSharing"` - AlternateContent *xlsxAlternateContent `xml:"mc:AlternateContent"` - DecodeAlternateContent *xlsxInnerXML `xml:"http://schemas.openxmlformats.org/markup-compatibility/2006 AlternateContent"` WorkbookPr *xlsxWorkbookPr `xml:"workbookPr"` WorkbookProtection *xlsxWorkbookProtection `xml:"workbookProtection"` + AlternateContent *xlsxAlternateContent `xml:"mc:AlternateContent"` + DecodeAlternateContent *xlsxInnerXML `xml:"http://schemas.openxmlformats.org/markup-compatibility/2006 AlternateContent"` BookViews *xlsxBookViews `xml:"bookViews"` Sheets xlsxSheets `xml:"sheets"` FunctionGroups *xlsxExtLst `xml:"functionGroups"` From 56aa6b82637b3210be470a8ebac1fdec2b2a6a30 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 8 Mar 2022 00:03:02 +0800 Subject: [PATCH 551/957] ref #65, new formula functions and read boolean data type cell value support * added 3 new formula functions: BETAINV, BETA.INV, F.INV.RT --- calc.go | 1658 ++++++++++++++++++++++++---------------------- calc_test.go | 46 ++ excelize_test.go | 9 +- rows.go | 10 + rows_test.go | 4 + 5 files changed, 937 insertions(+), 790 deletions(-) diff --git a/calc.go b/calc.go index e3db8826de..ada84963fe 100644 --- a/calc.go +++ b/calc.go @@ -333,6 +333,8 @@ type formulaFuncs struct { // BESSELJ // BESSELK // BESSELY +// BETAINV +// BETA.INV // BIN2DEC // BIN2HEX // BIN2OCT @@ -415,6 +417,7 @@ type formulaFuncs struct { // FALSE // FIND // FINDB +// F.INV.RT // FINV // FISHER // FISHERINV @@ -5187,966 +5190,1028 @@ func (fn *formulaFuncs) AVERAGEIF(argsList *list.List) formulaArg { return newNumberFormulaArg(sum / count) } -// incompleteGamma is an implementation of the incomplete gamma function. -func incompleteGamma(a, x float64) float64 { - max := 32 - summer := 0.0 - for n := 0; n <= max; n++ { - divisor := a - for i := 1; i <= n; i++ { - divisor *= (a + float64(i)) - } - summer += math.Pow(x, float64(n)) / divisor +// d1mach returns double precision real machine constants. +func d1mach(i int) float64 { + arr := []float64{ + 2.2250738585072014e-308, + 1.7976931348623158e+308, + 1.1102230246251565e-16, + 2.2204460492503131e-16, + 0.301029995663981195, } - return math.Pow(x, a) * math.Exp(0-x) * summer + if i > len(arr) { + return 0 + } + return arr[i-1] } -// CHIDIST function calculates the right-tailed probability of the chi-square -// distribution. The syntax of the function is: -// -// CHIDIST(x,degrees_freedom) -// -func (fn *formulaFuncs) CHIDIST(argsList *list.List) formulaArg { - if argsList.Len() != 2 { - return newErrorFormulaArg(formulaErrorVALUE, "CHIDIST requires 2 numeric arguments") - } - x := argsList.Front().Value.(formulaArg).ToNumber() - if x.Type != ArgNumber { - return x +// chebyshevInit determines the number of terms for the double precision +// orthogonal series "dos" needed to insure the error is no larger +// than "eta". Ordinarily eta will be chosen to be one-tenth machine +// precision. +func chebyshevInit(nos int, eta float64, dos []float64) int { + i, e := 0, 0.0 + if nos < 1 { + return 0 } - degress := argsList.Back().Value.(formulaArg).ToNumber() - if degress.Type != ArgNumber { - return degress + for ii := 1; ii <= nos; ii++ { + i = nos - ii + e += math.Abs(dos[i]) + if e > eta { + return i + } } - return newNumberFormulaArg(1 - (incompleteGamma(degress.Number/2, x.Number/2) / math.Gamma(degress.Number/2))) + return i } -// confidence is an implementation of the formula functions CONFIDENCE and -// CONFIDENCE.NORM. -func (fn *formulaFuncs) confidence(name string, argsList *list.List) formulaArg { - if argsList.Len() != 3 { - return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires 3 numeric arguments", name)) +// chebyshevEval evaluates the n-term Chebyshev series "a" at "x". +func chebyshevEval(n int, x float64, a []float64) float64 { + if n < 1 || n > 1000 || x < -1.1 || x > 1.1 { + return math.NaN() } - alpha := argsList.Front().Value.(formulaArg).ToNumber() - if alpha.Type != ArgNumber { - return alpha + twox, b0, b1, b2 := x*2, 0.0, 0.0, 0.0 + for i := 1; i <= n; i++ { + b2 = b1 + b1 = b0 + b0 = twox*b1 - b2 + a[n-i] } - if alpha.Number <= 0 || alpha.Number >= 1 { - return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + return (b0 - b2) * 0.5 +} + +// lgammacor is an implementation for the log(gamma) correction. +func lgammacor(x float64) float64 { + algmcs := []float64{ + 0.1666389480451863247205729650822, -0.1384948176067563840732986059135e-4, + 0.9810825646924729426157171547487e-8, -0.1809129475572494194263306266719e-10, + 0.6221098041892605227126015543416e-13, -0.3399615005417721944303330599666e-15, + 0.2683181998482698748957538846666e-17, -0.2868042435334643284144622399999e-19, + 0.3962837061046434803679306666666e-21, -0.6831888753985766870111999999999e-23, + 0.1429227355942498147573333333333e-24, -0.3547598158101070547199999999999e-26, + 0.1025680058010470912000000000000e-27, -0.3401102254316748799999999999999e-29, + 0.1276642195630062933333333333333e-30, } - stdDev := argsList.Front().Next().Value.(formulaArg).ToNumber() - if stdDev.Type != ArgNumber { - return stdDev + nalgm := chebyshevInit(15, d1mach(3), algmcs) + xbig := 1.0 / math.Sqrt(d1mach(3)) + xmax := math.Exp(math.Min(math.Log(d1mach(2)/12.0), -math.Log(12.0*d1mach(1)))) + if x < 10.0 { + return math.NaN() + } else if x >= xmax { + return 4.930380657631324e-32 + } else if x < xbig { + tmp := 10.0 / x + return chebyshevEval(nalgm, tmp*tmp*2.0-1.0, algmcs) / x } - if stdDev.Number <= 0 { - return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + return 1.0 / (x * 12.0) +} + +// logrelerr compute the relative error logarithm. +func logrelerr(x float64) float64 { + alnrcs := []float64{ + 0.10378693562743769800686267719098e+1, -0.13364301504908918098766041553133, + 0.19408249135520563357926199374750e-1, -0.30107551127535777690376537776592e-2, + 0.48694614797154850090456366509137e-3, -0.81054881893175356066809943008622e-4, + 0.13778847799559524782938251496059e-4, -0.23802210894358970251369992914935e-5, + 0.41640416213865183476391859901989e-6, -0.73595828378075994984266837031998e-7, + 0.13117611876241674949152294345011e-7, -0.23546709317742425136696092330175e-8, + 0.42522773276034997775638052962567e-9, -0.77190894134840796826108107493300e-10, + 0.14075746481359069909215356472191e-10, -0.25769072058024680627537078627584e-11, + 0.47342406666294421849154395005938e-12, -0.87249012674742641745301263292675e-13, + 0.16124614902740551465739833119115e-13, -0.29875652015665773006710792416815e-14, + 0.55480701209082887983041321697279e-15, -0.10324619158271569595141333961932e-15, + 0.19250239203049851177878503244868e-16, -0.35955073465265150011189707844266e-17, + 0.67264542537876857892194574226773e-18, -0.12602624168735219252082425637546e-18, + 0.23644884408606210044916158955519e-19, -0.44419377050807936898878389179733e-20, + 0.83546594464034259016241293994666e-21, -0.15731559416479562574899253521066e-21, + 0.29653128740247422686154369706666e-22, -0.55949583481815947292156013226666e-23, + 0.10566354268835681048187284138666e-23, -0.19972483680670204548314999466666e-24, + 0.37782977818839361421049855999999e-25, -0.71531586889081740345038165333333e-26, + 0.13552488463674213646502024533333e-26, -0.25694673048487567430079829333333e-27, + 0.48747756066216949076459519999999e-28, -0.92542112530849715321132373333333e-29, + 0.17578597841760239233269760000000e-29, -0.33410026677731010351377066666666e-30, + 0.63533936180236187354180266666666e-31, } - size := argsList.Back().Value.(formulaArg).ToNumber() - if size.Type != ArgNumber { - return size + nlnrel := chebyshevInit(43, 0.1*d1mach(3), alnrcs) + if x <= -1 { + return math.NaN() } - if size.Number < 1 { - return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + if math.Abs(x) <= 0.375 { + return x * (1.0 - x*chebyshevEval(nlnrel, x/0.375, alnrcs)) } - args := list.New() - args.Init() - args.PushBack(newNumberFormulaArg(alpha.Number / 2)) - args.PushBack(newNumberFormulaArg(0)) - args.PushBack(newNumberFormulaArg(1)) - return newNumberFormulaArg(-fn.NORMINV(args).Number * (stdDev.Number / math.Sqrt(size.Number))) -} - -// 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 -// that the standard deviation of the population is known. The syntax of the -// function is: -// -// CONFIDENCE(alpha,standard_dev,size) -// -func (fn *formulaFuncs) CONFIDENCE(argsList *list.List) formulaArg { - return fn.confidence("CONFIDENCE", argsList) -} - -// 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 -// assumed that the standard deviation of the population is known. The syntax -// of the Confidence.Norm function is: -// -// CONFIDENCE.NORM(alpha,standard_dev,size) -// -func (fn *formulaFuncs) CONFIDENCEdotNORM(argsList *list.List) formulaArg { - return fn.confidence("CONFIDENCE.NORM", argsList) + return math.Log(x + 1.0) } -// 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 { - if argsList.Len() != 2 { - return newErrorFormulaArg(formulaErrorVALUE, "COVAR requires 2 arguments") +// logBeta is an implementation for the log of the beta distribution +// function. +func logBeta(a, b float64) float64 { + corr, p, q := 0.0, a, a + if b < p { + p = b } - array1 := argsList.Front().Value.(formulaArg) - array2 := argsList.Back().Value.(formulaArg) - left, right := array1.ToList(), array2.ToList() - n := len(left) - if n != len(right) { - return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + if b > q { + q = b } - l1, l2 := list.New(), list.New() - l1.PushBack(array1) - l2.PushBack(array2) - result, skip := 0.0, 0 - mean1, mean2 := fn.AVERAGE(l1), fn.AVERAGE(l2) - for i := 0; i < n; i++ { - arg1 := left[i].ToNumber() - arg2 := right[i].ToNumber() - if arg1.Type == ArgError || arg2.Type == ArgError { - skip++ - continue - } - result += (arg1.Number - mean1.Number) * (arg2.Number - mean2.Number) + if p < 0 { + return math.NaN() } - return newNumberFormulaArg(result / float64(n-skip)) -} - -// 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") + if p == 0 { + return math.MaxFloat64 } - return fn.COVAR(argsList) -} - -// calcStringCountSum is part of the implementation countSum. -func calcStringCountSum(countText bool, count, sum float64, num, arg formulaArg) (float64, float64) { - if countText && num.Type == ArgError && arg.String != "" { - count++ + 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)) } - if num.Type == ArgNumber { - sum += num.Number - count++ + if q >= 10 { + corr = lgammacor(q) - lgammacor(p+q) + val, _ := math.Lgamma(p) + return val + corr + p - p*math.Log(p+q) + (q-0.5)*logrelerr(-p/(p+q)) } - return count, sum + return math.Log(math.Gamma(p) * (math.Gamma(q) / math.Gamma(p+q))) } -// countSum get count and sum for a formula arguments array. -func (fn *formulaFuncs) countSum(countText bool, args []formulaArg) (count, sum float64) { - for _, arg := range args { - switch arg.Type { - case ArgNumber: - if countText || !arg.Boolean { - sum += arg.Number - count++ - } - case ArgString: - if !countText && (arg.Value() == "TRUE" || arg.Value() == "FALSE") { - continue - } else if countText && (arg.Value() == "TRUE" || arg.Value() == "FALSE") { - num := arg.ToBool() - if num.Type == ArgNumber { - count++ - sum += num.Number - continue - } +// pbetaRaw is a part of pbeta for the beta distribution. +func pbetaRaw(alnsml, ans, eps, p, pin, q, sml, x, y float64) float64 { + if q > 1.0 { + xb := p*math.Log(y) + q*math.Log(1.0-y) - logBeta(p, q) - math.Log(q) + ib := int(math.Max(xb/alnsml, 0.0)) + term := math.Exp(xb - float64(ib)*alnsml) + c := 1.0 / (1.0 - y) + p1 := q * c / (p + q - 1.0) + finsum := 0.0 + n := int(q) + if q == float64(n) { + n = n - 1 + } + for i := 1; i <= n; i++ { + if p1 <= 1 && term/eps <= finsum { + break + } + xi := float64(i) + term = (q - xi + 1.0) * c * term / (p + q - xi) + if term > 1.0 { + ib = ib - 1 + term = term * sml + } + if ib == 0 { + finsum = finsum + term } - num := arg.ToNumber() - count, sum = calcStringCountSum(countText, count, sum, num, arg) - case ArgList, ArgMatrix: - cnt, summary := fn.countSum(countText, arg.ToList()) - sum += summary - count += cnt } + ans = ans + finsum } - return + if y != x || p != pin { + ans = 1.0 - ans + } + ans = math.Max(math.Min(ans, 1.0), 0.0) + return ans } -// CORREL function calculates the Pearson Product-Moment Correlation -// Coefficient for two sets of values. The syntax of the function is: -// -// CORREL(array1,array2) -// -func (fn *formulaFuncs) CORREL(argsList *list.List) formulaArg { - if argsList.Len() != 2 { - return newErrorFormulaArg(formulaErrorVALUE, "CORREL requires 2 arguments") - } - array1 := argsList.Front().Value.(formulaArg) - array2 := argsList.Back().Value.(formulaArg) - left, right := array1.ToList(), array2.ToList() - n := len(left) - if n != len(right) { - return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) +// pbeta returns distribution function of the beta distribution. +func pbeta(x, pin, qin float64) (ans float64) { + eps := d1mach(3) + alneps := math.Log(eps) + sml := d1mach(1) + alnsml := math.Log(sml) + y := x + p := pin + q := qin + if p/(p+q) < x { + y = 1.0 - y + p = qin + q = pin } - l1, l2, l3 := list.New(), list.New(), list.New() - for i := 0; i < n; i++ { - if lhs, rhs := left[i].ToNumber(), right[i].ToNumber(); lhs.Number != 0 && rhs.Number != 0 { - l1.PushBack(lhs) - l2.PushBack(rhs) + if (p+q)*y/(p+1.0) < eps { + xb := p*math.Log(math.Max(y, sml)) - math.Log(p) - logBeta(p, q) + if xb > alnsml && y != 0.0 { + ans = math.Exp(xb) } - } - stdev1, stdev2 := fn.STDEV(l1), fn.STDEV(l2) - if stdev1.Number == 0 || stdev2.Number == 0 { - return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV) - } - mean1, mean2, skip := fn.AVERAGE(l1), fn.AVERAGE(l2), 0 - for i := 0; i < n; i++ { - lhs, rhs := left[i].ToNumber(), right[i].ToNumber() - if lhs.Number == 0 || rhs.Number == 0 { - skip++ - continue + if y != x || p != pin { + ans = 1.0 - ans } - l3.PushBack(newNumberFormulaArg((lhs.Number - mean1.Number) * (rhs.Number - mean2.Number))) - } - return newNumberFormulaArg(fn.SUM(l3).Number / float64(n-skip-1) / stdev1.Number / stdev2.Number) -} - -// COUNT function returns the count of numeric values in a supplied set of -// cells or values. This count includes both numbers and dates. The syntax of -// the function is: -// -// COUNT(value1,[value2],...) -// -func (fn *formulaFuncs) COUNT(argsList *list.List) formulaArg { - var count int - for token := argsList.Front(); token != nil; token = token.Next() { - arg := token.Value.(formulaArg) - switch arg.Type { - case ArgString: - if arg.ToNumber().Type != ArgError { - count++ - } - case ArgNumber: - count++ - case ArgMatrix: - for _, row := range arg.Matrix { - for _, value := range row { - if value.ToNumber().Type != ArgError { - count++ - } + } else { + ps := q - math.Floor(q) + if ps == 0.0 { + ps = 1.0 + } + xb := p*math.Log(y) - logBeta(ps, p) - math.Log(p) + if xb >= alnsml { + ans = math.Exp(xb) + term := ans * p + if ps != 1.0 { + n := int(math.Max(alneps/math.Log(y), 4.0)) + for i := 1; i <= n; i++ { + xi := float64(i) + term = term * (xi - ps) * y / xi + ans = ans + term/(p+xi) } } } + ans = pbetaRaw(alnsml, ans, eps, p, pin, q, sml, x, y) } - return newNumberFormulaArg(float64(count)) + return ans } -// 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],...) -// -func (fn *formulaFuncs) COUNTA(argsList *list.List) formulaArg { - var count int - for token := argsList.Front(); token != nil; token = token.Next() { - arg := token.Value.(formulaArg) - switch arg.Type { - case ArgString: - if arg.String != "" { - count++ - } - case ArgNumber: - count++ - case ArgMatrix: - for _, row := range arg.ToList() { - switch row.Type { - case ArgString: - if row.String != "" { - count++ +// 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 { + 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)) + if prop3*prop4 <= 0 { + prev = math.Max(math.Abs(j), maxCumulative) + } + h := 1.0 + for iteratorCount := 0; iteratorCount < 1000; iteratorCount++ { + j = h * prop3 + if math.Abs(j) < prev { + i = beta3 - j + if i >= 0 && i <= 1.0 { + if prev <= alpha3 { + return beta3 + } + if math.Abs(prop3) <= alpha3 { + return beta3 + } + if i != 0 && i != 1.0 { + break } - case ArgNumber: - count++ } } + h /= 3.0 + } + if i == beta3 { + return beta3 } + beta3, prop4 = i, prop3 } - return newNumberFormulaArg(float64(count)) + return beta3 } -// COUNTBLANK function returns the number of blank cells in a supplied range. -// The syntax of the function is: -// -// COUNTBLANK(range) -// -func (fn *formulaFuncs) COUNTBLANK(argsList *list.List) formulaArg { - if argsList.Len() != 1 { - return newErrorFormulaArg(formulaErrorVALUE, "COUNTBLANK requires 1 argument") +// calcBetainv is an implementation for the quantile of the beta +// distribution. +func calcBetainv(probability, alpha, beta, lower, upper float64) float64 { + minCumulative, maxCumulative := 1.0e-300, 3.0e-308 + lowerBound, upperBound := maxCumulative, 1.0-2.22e-16 + needSwap := false + var alpha1, alpha2, beta1, beta2, beta3, prob1, x, y float64 + if probability <= 0.5 { + prob1, alpha1, beta1 = probability, alpha, beta + } else { + prob1, alpha1, beta1, needSwap = 1.0-probability, beta, alpha, true } - var count float64 - for _, cell := range argsList.Front().Value.(formulaArg).ToList() { - if cell.Value() == "" { - count++ + logbeta := 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 { + alpha2, beta2, prob2 = 1/(alpha1+alpha1-1), 1/(beta1+beta1-1), (prob3*prob3-3)/6 + x = 2 / (alpha2 + beta2) + y = prob3*math.Sqrt(x+prob2)/x - (beta2-alpha2)*(prob2+5/6.0-2/(x*3)) + beta3 = alpha1 / (alpha1 + beta1*math.Exp(y+y)) + } else { + 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) + } else { + beta2 = (prob2 + alpha1*4 - 2) / beta2 + if beta2 <= 1 { + beta3 = math.Exp((logbeta + math.Log(alpha1*prob1)) / alpha1) + } else { + beta3 = 1 - 2/(beta2+1) + } } } - return newNumberFormulaArg(count) + beta2, prob2 = 1-beta1, 1-alpha1 + if beta3 < lowerBound { + beta3 = lowerBound + } else if beta3 > upperBound { + 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) + if needSwap { + beta3 = 1.0 - beta3 + } + return (upper-lower)*beta3 + lower } -// 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) -// -func (fn *formulaFuncs) COUNTIF(argsList *list.List) formulaArg { - if argsList.Len() != 2 { - return newErrorFormulaArg(formulaErrorVALUE, "COUNTIF requires 2 arguments") +// betainv is an implementation of the formula functions BETAINV and +// BETA.INV. +func (fn *formulaFuncs) betainv(name string, argsList *list.List) formulaArg { + if argsList.Len() < 3 { + return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires at least 3 arguments", name)) } - var ( - criteria = formulaCriteriaParser(argsList.Front().Next().Value.(formulaArg).String) - count float64 - ) - for _, cell := range argsList.Front().Value.(formulaArg).ToList() { - if ok, _ := formulaCriteriaEval(cell.Value(), criteria); ok { - count++ - } + if argsList.Len() > 5 { + return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires at most 5 arguments", name)) } - return newNumberFormulaArg(count) -} - -// 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{} - matrix, criteria := args[i].Matrix, formulaCriteriaParser(args[i+1].Value()) - if i == 0 { - for rowIdx, row := range matrix { - for colIdx, col := range row { - if ok, _ := formulaCriteriaEval(col.Value(), criteria); ok { - match = append(match, cellRef{Col: colIdx, Row: rowIdx}) - } - } - } - } else { - for _, ref := range cellRefs { - value := matrix[ref.Row][ref.Col] - if ok, _ := formulaCriteriaEval(value.Value(), criteria); ok { - match = append(match, ref) - } - } - } - if len(match) == 0 { - return - } - cellRefs = match[:] + probability := argsList.Front().Value.(formulaArg).ToNumber() + if probability.Type != ArgNumber { + return probability } - return -} - -// 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],...) -// -func (fn *formulaFuncs) COUNTIFS(argsList *list.List) formulaArg { - if argsList.Len() < 2 { - return newErrorFormulaArg(formulaErrorVALUE, "COUNTIFS requires at least 2 arguments") + if probability.Number <= 0 || probability.Number >= 1 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) } - if argsList.Len()%2 != 0 { - return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + alpha := argsList.Front().Next().Value.(formulaArg).ToNumber() + if alpha.Type != ArgNumber { + return alpha } - args := []formulaArg{} - for arg := argsList.Front(); arg != nil; arg = arg.Next() { - args = append(args, arg.Value.(formulaArg)) + beta := argsList.Front().Next().Next().Value.(formulaArg).ToNumber() + if beta.Type != ArgNumber { + return beta } - return newNumberFormulaArg(float64(len(formulaIfsMatch(args)))) -} - -// DEVSQ function calculates the sum of the squared deviations from the sample -// mean. The syntax of the function is: -// -// 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") + if alpha.Number <= 0 || beta.Number <= 0 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) } - 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 { - continue - } - count++ - if count == 0 { - result = math.Pow(num.Number-avg.Number, 2) - continue - } - result += math.Pow(num.Number-avg.Number, 2) + 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 count == -1 { - return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + if argsList.Len() == 5 { + if b = argsList.Back().Value.(formulaArg).ToNumber(); b.Type != ArgNumber { + return b + } } - return newNumberFormulaArg(result) + if a.Number == b.Number { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + return newNumberFormulaArg(calcBetainv(probability.Number, alpha.Number, beta.Number, a.Number, b.Number)) } -// FISHER function calculates the Fisher Transformation for a supplied value. -// The syntax of the function is: +// BETAINV function uses an iterative procedure to calculate the inverse of +// the cumulative beta probability density function for a supplied +// probability. The syntax of the function is: // -// FISHER(x) +// BETAINV(probability,alpha,beta,[A],[B]) // -func (fn *formulaFuncs) FISHER(argsList *list.List) formulaArg { - if argsList.Len() != 1 { - return newErrorFormulaArg(formulaErrorVALUE, "FISHER requires 1 numeric argument") - } - token := argsList.Front().Value.(formulaArg) - switch token.Type { - case ArgString: - arg := token.ToNumber() - if arg.Type == ArgNumber { - if arg.Number <= -1 || arg.Number >= 1 { - return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) - } - return newNumberFormulaArg(0.5 * math.Log((1+arg.Number)/(1-arg.Number))) - } - case ArgNumber: - if token.Number <= -1 || token.Number >= 1 { - return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) - } - return newNumberFormulaArg(0.5 * math.Log((1+token.Number)/(1-token.Number))) - } - return newErrorFormulaArg(formulaErrorVALUE, "FISHER requires 1 numeric argument") +func (fn *formulaFuncs) BETAINV(argsList *list.List) formulaArg { + return fn.betainv("BETAINV", argsList) } -// FISHERINV function calculates the inverse of the Fisher Transformation and -// returns a value between -1 and +1. The syntax of the function is: +// BETAdotINV function uses an iterative procedure to calculate the inverse of +// the cumulative beta probability density function for a supplied +// probability. The syntax of the function is: // -// FISHERINV(y) +// BETA.INV(probability,alpha,beta,[A],[B]) // -func (fn *formulaFuncs) FISHERINV(argsList *list.List) formulaArg { - if argsList.Len() != 1 { - return newErrorFormulaArg(formulaErrorVALUE, "FISHERINV requires 1 numeric argument") - } - token := argsList.Front().Value.(formulaArg) - switch token.Type { - case ArgString: - arg := token.ToNumber() - if arg.Type == ArgNumber { - return newNumberFormulaArg((math.Exp(2*arg.Number) - 1) / (math.Exp(2*arg.Number) + 1)) - } - case ArgNumber: - return newNumberFormulaArg((math.Exp(2*token.Number) - 1) / (math.Exp(2*token.Number) + 1)) - } - return newErrorFormulaArg(formulaErrorVALUE, "FISHERINV requires 1 numeric argument") +func (fn *formulaFuncs) BETAdotINV(argsList *list.List) formulaArg { + return fn.betainv("BETA.INV", argsList) } -// GAMMA function returns the value of the Gamma Function, Γ(n), for a -// specified number, n. The syntax of the function is: -// -// GAMMA(number) -// -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) +// incompleteGamma is an implementation of the incomplete gamma function. +func incompleteGamma(a, x float64) float64 { + max := 32 + summer := 0.0 + for n := 0; n <= max; n++ { + divisor := a + for i := 1; i <= n; i++ { + divisor *= (a + float64(i)) } - return newNumberFormulaArg(math.Gamma(token.Number)) + summer += math.Pow(x, float64(n)) / divisor } - return newErrorFormulaArg(formulaErrorVALUE, "GAMMA requires 1 numeric argument") + return math.Pow(x, a) * math.Exp(0-x) * summer } -// GAMMALN function returns the natural logarithm of the Gamma Function, Γ -// (n). The syntax of the function is: +// CHIDIST function calculates the right-tailed probability of the chi-square +// distribution. The syntax of the function is: // -// GAMMALN(x) +// CHIDIST(x,degrees_freedom) // -func (fn *formulaFuncs) GAMMALN(argsList *list.List) formulaArg { - if argsList.Len() != 1 { - return newErrorFormulaArg(formulaErrorVALUE, "GAMMALN requires 1 numeric argument") +func (fn *formulaFuncs) CHIDIST(argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, "CHIDIST requires 2 numeric arguments") } - 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 x } - return newErrorFormulaArg(formulaErrorVALUE, "GAMMALN requires 1 numeric argument") + degress := argsList.Back().Value.(formulaArg).ToNumber() + if degress.Type != ArgNumber { + return degress + } + return newNumberFormulaArg(1 - (incompleteGamma(degress.Number/2, x.Number/2) / math.Gamma(degress.Number/2))) } -// GEOMEAN function calculates the geometric mean of a supplied set of values. -// The syntax of the function is: -// -// 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") +// confidence is an implementation of the formula functions CONFIDENCE and +// CONFIDENCE.NORM. +func (fn *formulaFuncs) confidence(name string, argsList *list.List) formulaArg { + if argsList.Len() != 3 { + return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires 3 numeric arguments", name)) } - product := fn.PRODUCT(argsList) - if product.Type != ArgNumber { - return product + alpha := argsList.Front().Value.(formulaArg).ToNumber() + if alpha.Type != ArgNumber { + return alpha } - count := fn.COUNT(argsList) - min := fn.MIN(argsList) - if product.Number > 0 && min.Number > 0 { - return newNumberFormulaArg(math.Pow(product.Number, (1 / count.Number))) + if alpha.Number <= 0 || alpha.Number >= 1 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) } - return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + stdDev := argsList.Front().Next().Value.(formulaArg).ToNumber() + if stdDev.Type != ArgNumber { + return stdDev + } + if stdDev.Number <= 0 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + size := argsList.Back().Value.(formulaArg).ToNumber() + if size.Type != ArgNumber { + return size + } + if size.Number < 1 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + args := list.New() + args.Init() + args.PushBack(newNumberFormulaArg(alpha.Number / 2)) + args.PushBack(newNumberFormulaArg(0)) + args.PushBack(newNumberFormulaArg(1)) + return newNumberFormulaArg(-fn.NORMINV(args).Number * (stdDev.Number / math.Sqrt(size.Number))) } -// HARMEAN function calculates the harmonic mean of a supplied set of values. -// The syntax of the function is: +// 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 +// that the standard deviation of the population is known. The syntax of the +// function is: // -// HARMEAN(number1,[number2],...) +// CONFIDENCE(alpha,standard_dev,size) // -func (fn *formulaFuncs) HARMEAN(argsList *list.List) formulaArg { - if argsList.Len() < 1 { - return newErrorFormulaArg(formulaErrorVALUE, "HARMEAN requires at least 1 argument") +func (fn *formulaFuncs) CONFIDENCE(argsList *list.List) formulaArg { + return fn.confidence("CONFIDENCE", argsList) +} + +// 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 +// assumed that the standard deviation of the population is known. The syntax +// of the Confidence.Norm function is: +// +// CONFIDENCE.NORM(alpha,standard_dev,size) +// +func (fn *formulaFuncs) CONFIDENCEdotNORM(argsList *list.List) formulaArg { + return fn.confidence("CONFIDENCE.NORM", argsList) +} + +// 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 { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, "COVAR requires 2 arguments") } - if min := fn.MIN(argsList); min.Number < 0 { + array1 := argsList.Front().Value.(formulaArg) + array2 := argsList.Back().Value.(formulaArg) + left, right := array1.ToList(), array2.ToList() + n := len(left) + if n != len(right) { return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) } - number, val, cnt := 0.0, 0.0, 0.0 - for token := argsList.Front(); token != nil; token = token.Next() { - arg := token.Value.(formulaArg) + l1, l2 := list.New(), list.New() + l1.PushBack(array1) + l2.PushBack(array2) + result, skip := 0.0, 0 + mean1, mean2 := fn.AVERAGE(l1), fn.AVERAGE(l2) + for i := 0; i < n; i++ { + arg1 := left[i].ToNumber() + arg2 := right[i].ToNumber() + if arg1.Type == ArgError || arg2.Type == ArgError { + skip++ + continue + } + result += (arg1.Number - mean1.Number) * (arg2.Number - mean2.Number) + } + return newNumberFormulaArg(result / float64(n-skip)) +} + +// 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) +} + +// calcStringCountSum is part of the implementation countSum. +func calcStringCountSum(countText bool, count, sum float64, num, arg formulaArg) (float64, float64) { + if countText && num.Type == ArgError && arg.String != "" { + count++ + } + if num.Type == ArgNumber { + sum += num.Number + count++ + } + return count, sum +} + +// countSum get count and sum for a formula arguments array. +func (fn *formulaFuncs) countSum(countText bool, args []formulaArg) (count, sum float64) { + for _, arg := range args { switch arg.Type { + case ArgNumber: + if countText || !arg.Boolean { + sum += arg.Number + count++ + } case ArgString: - num := arg.ToNumber() - if num.Type != ArgNumber { + if !countText && (arg.Value() == "TRUE" || arg.Value() == "FALSE") { continue + } else if countText && (arg.Value() == "TRUE" || arg.Value() == "FALSE") { + num := arg.ToBool() + if num.Type == ArgNumber { + count++ + sum += num.Number + continue + } } - number = num.Number - case ArgNumber: - number = arg.Number - } - if number <= 0 { - return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + num := arg.ToNumber() + count, sum = calcStringCountSum(countText, count, sum, num, arg) + case ArgList, ArgMatrix: + cnt, summary := fn.countSum(countText, arg.ToList()) + sum += summary + count += cnt } - val += (1 / number) - cnt++ } - return newNumberFormulaArg(1 / (val / cnt)) + return } -// KURT function calculates the kurtosis of a supplied set of values. The -// syntax of the function is: +// CORREL function calculates the Pearson Product-Moment Correlation +// Coefficient for two sets of values. The syntax of the function is: // -// KURT(number1,[number2],...) +// CORREL(array1,array2) // -func (fn *formulaFuncs) KURT(argsList *list.List) formulaArg { - if argsList.Len() < 1 { - return newErrorFormulaArg(formulaErrorVALUE, "KURT requires at least 1 argument") +func (fn *formulaFuncs) CORREL(argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, "CORREL requires 2 arguments") } - mean, stdev := fn.AVERAGE(argsList), fn.STDEV(argsList) - if stdev.Number > 0 { - count, summer := 0.0, 0.0 - for arg := argsList.Front(); arg != nil; arg = arg.Next() { - token := arg.Value.(formulaArg) - switch token.Type { - case ArgString, ArgNumber: - num := token.ToNumber() - if num.Type == ArgError { - continue - } - summer += math.Pow((num.Number-mean.Number)/stdev.Number, 4) + array1 := argsList.Front().Value.(formulaArg) + array2 := argsList.Back().Value.(formulaArg) + left, right := array1.ToList(), array2.ToList() + n := len(left) + if n != len(right) { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + } + l1, l2, l3 := list.New(), list.New(), list.New() + for i := 0; i < n; i++ { + if lhs, rhs := left[i].ToNumber(), right[i].ToNumber(); lhs.Number != 0 && rhs.Number != 0 { + l1.PushBack(lhs) + l2.PushBack(rhs) + } + } + stdev1, stdev2 := fn.STDEV(l1), fn.STDEV(l2) + if stdev1.Number == 0 || stdev2.Number == 0 { + return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV) + } + mean1, mean2, skip := fn.AVERAGE(l1), fn.AVERAGE(l2), 0 + for i := 0; i < n; i++ { + lhs, rhs := left[i].ToNumber(), right[i].ToNumber() + if lhs.Number == 0 || rhs.Number == 0 { + skip++ + continue + } + l3.PushBack(newNumberFormulaArg((lhs.Number - mean1.Number) * (rhs.Number - mean2.Number))) + } + return newNumberFormulaArg(fn.SUM(l3).Number / float64(n-skip-1) / stdev1.Number / stdev2.Number) +} + +// COUNT function returns the count of numeric values in a supplied set of +// cells or values. This count includes both numbers and dates. The syntax of +// the function is: +// +// COUNT(value1,[value2],...) +// +func (fn *formulaFuncs) COUNT(argsList *list.List) formulaArg { + var count int + for token := argsList.Front(); token != nil; token = token.Next() { + arg := token.Value.(formulaArg) + switch arg.Type { + case ArgString: + if arg.ToNumber().Type != ArgError { count++ - case ArgList, ArgMatrix: - for _, row := range token.ToList() { - if row.Type == ArgNumber || row.Type == ArgString { - num := row.ToNumber() - if num.Type == ArgError { - continue - } - summer += math.Pow((num.Number-mean.Number)/stdev.Number, 4) + } + case ArgNumber: + count++ + case ArgMatrix: + for _, row := range arg.Matrix { + for _, value := range row { + if value.ToNumber().Type != ArgError { count++ } } } } - if count > 3 { - return newNumberFormulaArg(summer*(count*(count+1)/((count-1)*(count-2)*(count-3))) - (3 * math.Pow(count-1, 2) / ((count - 2) * (count - 3)))) - } } - return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV) + return newNumberFormulaArg(float64(count)) } -// EXPONdotDIST function returns the value of the exponential distribution for -// a give value of x. The user can specify whether the probability density -// function or the cumulative distribution function is used. The syntax of the -// Expondist function is: +// COUNTA function returns the number of non-blanks within a supplied set of +// cells or values. The syntax of the function is: // -// EXPON.DIST(x,lambda,cumulative) +// COUNTA(value1,[value2],...) // -func (fn *formulaFuncs) EXPONdotDIST(argsList *list.List) formulaArg { - if argsList.Len() != 3 { - return newErrorFormulaArg(formulaErrorVALUE, "EXPON.DIST requires 3 arguments") +func (fn *formulaFuncs) COUNTA(argsList *list.List) formulaArg { + var count int + for token := argsList.Front(); token != nil; token = token.Next() { + arg := token.Value.(formulaArg) + switch arg.Type { + case ArgString: + if arg.String != "" { + count++ + } + case ArgNumber: + count++ + case ArgMatrix: + for _, row := range arg.ToList() { + switch row.Type { + case ArgString: + if row.String != "" { + count++ + } + case ArgNumber: + count++ + } + } + } } - return fn.EXPONDIST(argsList) + return newNumberFormulaArg(float64(count)) } -// EXPONDIST function returns the value of the exponential distribution for a -// give value of x. The user can specify whether the probability density -// function or the cumulative distribution function is used. The syntax of the -// Expondist function is: +// COUNTBLANK function returns the number of blank cells in a supplied range. +// The syntax of the function is: // -// EXPONDIST(x,lambda,cumulative) +// COUNTBLANK(range) // -func (fn *formulaFuncs) EXPONDIST(argsList *list.List) formulaArg { - if argsList.Len() != 3 { - return newErrorFormulaArg(formulaErrorVALUE, "EXPONDIST requires 3 arguments") - } - var x, lambda, cumulative formulaArg - if x = argsList.Front().Value.(formulaArg).ToNumber(); x.Type != ArgNumber { - return x - } - if lambda = argsList.Front().Next().Value.(formulaArg).ToNumber(); lambda.Type != ArgNumber { - return lambda +func (fn *formulaFuncs) COUNTBLANK(argsList *list.List) formulaArg { + if argsList.Len() != 1 { + return newErrorFormulaArg(formulaErrorVALUE, "COUNTBLANK requires 1 argument") } - if cumulative = argsList.Back().Value.(formulaArg).ToBool(); cumulative.Type == ArgError { - return cumulative - } - if x.Number < 0 { - return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) - } - if lambda.Number <= 0 { - return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) - } - if cumulative.Number == 1 { - return newNumberFormulaArg(1 - math.Exp(-lambda.Number*x.Number)) + var count float64 + for _, cell := range argsList.Front().Value.(formulaArg).ToList() { + if cell.Value() == "" { + count++ + } } - return newNumberFormulaArg(lambda.Number * math.Exp(-lambda.Number*x.Number)) + return newNumberFormulaArg(count) } -// d1mach returns double precision real machine constants. -func d1mach(i int) float64 { - arr := []float64{ - 2.2250738585072014e-308, - 1.7976931348623158e+308, - 1.1102230246251565e-16, - 2.2204460492503131e-16, - 0.301029995663981195, +// 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) +// +func (fn *formulaFuncs) COUNTIF(argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, "COUNTIF requires 2 arguments") } - if i > len(arr) { - return 0 + var ( + criteria = formulaCriteriaParser(argsList.Front().Next().Value.(formulaArg).String) + count float64 + ) + for _, cell := range argsList.Front().Value.(formulaArg).ToList() { + if ok, _ := formulaCriteriaEval(cell.Value(), criteria); ok { + count++ + } } - return arr[i-1] + return newNumberFormulaArg(count) } -// chebyshevInit determines the number of terms for the double precision -// orthogonal series "dos" needed to insure the error is no larger -// than "eta". Ordinarily eta will be chosen to be one-tenth machine -// precision. -func chebyshevInit(nos int, eta float64, dos []float64) int { - i, e := 0, 0.0 - if nos < 1 { - return 0 - } - for ii := 1; ii <= nos; ii++ { - i = nos - ii - e += math.Abs(dos[i]) - if e > eta { - return i +// 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{} + matrix, criteria := args[i].Matrix, formulaCriteriaParser(args[i+1].Value()) + if i == 0 { + for rowIdx, row := range matrix { + for colIdx, col := range row { + if ok, _ := formulaCriteriaEval(col.Value(), criteria); ok { + match = append(match, cellRef{Col: colIdx, Row: rowIdx}) + } + } + } + } else { + for _, ref := range cellRefs { + value := matrix[ref.Row][ref.Col] + if ok, _ := formulaCriteriaEval(value.Value(), criteria); ok { + match = append(match, ref) + } + } + } + if len(match) == 0 { + return } + cellRefs = match[:] } - return i + return } -// chebyshevEval evaluates the n-term Chebyshev series "a" at "x". -func chebyshevEval(n int, x float64, a []float64) float64 { - if n < 1 || n > 1000 || x < -1.1 || x > 1.1 { - return math.NaN() - } - twox, b0, b1, b2 := x*2, 0.0, 0.0, 0.0 - for i := 1; i <= n; i++ { - b2 = b1 - b1 = b0 - b0 = twox*b1 - b2 + a[n-i] +// 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],...) +// +func (fn *formulaFuncs) COUNTIFS(argsList *list.List) formulaArg { + if argsList.Len() < 2 { + return newErrorFormulaArg(formulaErrorVALUE, "COUNTIFS requires at least 2 arguments") } - return (b0 - b2) * 0.5 -} - -// lgammacor is an implementation for the log(gamma) correction. -func lgammacor(x float64) float64 { - algmcs := []float64{ - 0.1666389480451863247205729650822, -0.1384948176067563840732986059135e-4, - 0.9810825646924729426157171547487e-8, -0.1809129475572494194263306266719e-10, - 0.6221098041892605227126015543416e-13, -0.3399615005417721944303330599666e-15, - 0.2683181998482698748957538846666e-17, -0.2868042435334643284144622399999e-19, - 0.3962837061046434803679306666666e-21, -0.6831888753985766870111999999999e-23, - 0.1429227355942498147573333333333e-24, -0.3547598158101070547199999999999e-26, - 0.1025680058010470912000000000000e-27, -0.3401102254316748799999999999999e-29, - 0.1276642195630062933333333333333e-30, + if argsList.Len()%2 != 0 { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) } - nalgm := chebyshevInit(15, d1mach(3), algmcs) - xbig := 1.0 / math.Sqrt(d1mach(3)) - xmax := math.Exp(math.Min(math.Log(d1mach(2)/12.0), -math.Log(12.0*d1mach(1)))) - if x < 10.0 { - return math.NaN() - } else if x >= xmax { - return 4.930380657631324e-32 - } else if x < xbig { - tmp := 10.0 / x - return chebyshevEval(nalgm, tmp*tmp*2.0-1.0, algmcs) / x + args := []formulaArg{} + for arg := argsList.Front(); arg != nil; arg = arg.Next() { + args = append(args, arg.Value.(formulaArg)) } - return 1.0 / (x * 12.0) + return newNumberFormulaArg(float64(len(formulaIfsMatch(args)))) } -// logrelerr compute the relative error logarithm. -func logrelerr(x float64) float64 { - alnrcs := []float64{ - 0.10378693562743769800686267719098e+1, -0.13364301504908918098766041553133, - 0.19408249135520563357926199374750e-1, -0.30107551127535777690376537776592e-2, - 0.48694614797154850090456366509137e-3, -0.81054881893175356066809943008622e-4, - 0.13778847799559524782938251496059e-4, -0.23802210894358970251369992914935e-5, - 0.41640416213865183476391859901989e-6, -0.73595828378075994984266837031998e-7, - 0.13117611876241674949152294345011e-7, -0.23546709317742425136696092330175e-8, - 0.42522773276034997775638052962567e-9, -0.77190894134840796826108107493300e-10, - 0.14075746481359069909215356472191e-10, -0.25769072058024680627537078627584e-11, - 0.47342406666294421849154395005938e-12, -0.87249012674742641745301263292675e-13, - 0.16124614902740551465739833119115e-13, -0.29875652015665773006710792416815e-14, - 0.55480701209082887983041321697279e-15, -0.10324619158271569595141333961932e-15, - 0.19250239203049851177878503244868e-16, -0.35955073465265150011189707844266e-17, - 0.67264542537876857892194574226773e-18, -0.12602624168735219252082425637546e-18, - 0.23644884408606210044916158955519e-19, -0.44419377050807936898878389179733e-20, - 0.83546594464034259016241293994666e-21, -0.15731559416479562574899253521066e-21, - 0.29653128740247422686154369706666e-22, -0.55949583481815947292156013226666e-23, - 0.10566354268835681048187284138666e-23, -0.19972483680670204548314999466666e-24, - 0.37782977818839361421049855999999e-25, -0.71531586889081740345038165333333e-26, - 0.13552488463674213646502024533333e-26, -0.25694673048487567430079829333333e-27, - 0.48747756066216949076459519999999e-28, -0.92542112530849715321132373333333e-29, - 0.17578597841760239233269760000000e-29, -0.33410026677731010351377066666666e-30, - 0.63533936180236187354180266666666e-31, +// DEVSQ function calculates the sum of the squared deviations from the sample +// mean. The syntax of the function is: +// +// 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") } - nlnrel := chebyshevInit(43, 0.1*d1mach(3), alnrcs) - if x <= -1 { - return math.NaN() + 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 { + continue + } + count++ + if count == 0 { + result = math.Pow(num.Number-avg.Number, 2) + continue + } + result += math.Pow(num.Number-avg.Number, 2) + } } - if math.Abs(x) <= 0.375 { - return x * (1.0 - x*chebyshevEval(nlnrel, x/0.375, alnrcs)) + if count == -1 { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) } - return math.Log(x + 1.0) + return newNumberFormulaArg(result) } -// logBeta is an implementation for the log of the beta distribution -// function. -func logBeta(a, b float64) float64 { - corr, p, q := 0.0, a, a - if b < p { - p = b +// FISHER function calculates the Fisher Transformation for a supplied value. +// The syntax of the function is: +// +// FISHER(x) +// +func (fn *formulaFuncs) FISHER(argsList *list.List) formulaArg { + if argsList.Len() != 1 { + return newErrorFormulaArg(formulaErrorVALUE, "FISHER requires 1 numeric argument") } - if b > q { - q = b + token := argsList.Front().Value.(formulaArg) + switch token.Type { + case ArgString: + arg := token.ToNumber() + if arg.Type == ArgNumber { + if arg.Number <= -1 || arg.Number >= 1 { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + } + return newNumberFormulaArg(0.5 * math.Log((1+arg.Number)/(1-arg.Number))) + } + case ArgNumber: + if token.Number <= -1 || token.Number >= 1 { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + } + return newNumberFormulaArg(0.5 * math.Log((1+token.Number)/(1-token.Number))) } - if p < 0 { - return math.NaN() + return newErrorFormulaArg(formulaErrorVALUE, "FISHER requires 1 numeric argument") +} + +// 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) +// +func (fn *formulaFuncs) FISHERINV(argsList *list.List) formulaArg { + if argsList.Len() != 1 { + return newErrorFormulaArg(formulaErrorVALUE, "FISHERINV requires 1 numeric argument") } - if p == 0 { - return math.MaxFloat64 + token := argsList.Front().Value.(formulaArg) + switch token.Type { + case ArgString: + arg := token.ToNumber() + if arg.Type == ArgNumber { + return newNumberFormulaArg((math.Exp(2*arg.Number) - 1) / (math.Exp(2*arg.Number) + 1)) + } + case ArgNumber: + return newNumberFormulaArg((math.Exp(2*token.Number) - 1) / (math.Exp(2*token.Number) + 1)) } - 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)) + return newErrorFormulaArg(formulaErrorVALUE, "FISHERINV requires 1 numeric argument") +} + +// GAMMA function returns the value of the Gamma Function, Γ(n), for a +// specified number, n. The syntax of the function is: +// +// GAMMA(number) +// +func (fn *formulaFuncs) GAMMA(argsList *list.List) formulaArg { + if argsList.Len() != 1 { + return newErrorFormulaArg(formulaErrorVALUE, "GAMMA requires 1 numeric argument") } - if q >= 10 { - corr = lgammacor(q) - lgammacor(p+q) - val, _ := math.Lgamma(p) - return val + corr + p - p*math.Log(p+q) + (q-0.5)*logrelerr(-p/(p+q)) + 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)) } - return math.Log(math.Gamma(p) * (math.Gamma(q) / math.Gamma(p+q))) + return newErrorFormulaArg(formulaErrorVALUE, "GAMMA requires 1 numeric argument") } -// pbetaRaw is a part of pbeta for the beta distribution. -func pbetaRaw(alnsml, ans, eps, p, pin, q, sml, x, y float64) float64 { - if q > 1.0 { - xb := p*math.Log(y) + q*math.Log(1.0-y) - logBeta(p, q) - math.Log(q) - ib := int(math.Max(xb/alnsml, 0.0)) - term := math.Exp(xb - float64(ib)*alnsml) - c := 1.0 / (1.0 - y) - p1 := q * c / (p + q - 1.0) - finsum := 0.0 - n := int(q) - if q == float64(n) { - n = n - 1 - } - for i := 1; i <= n; i++ { - if p1 <= 1 && term/eps <= finsum { - break - } - xi := float64(i) - term = (q - xi + 1.0) * c * term / (p + q - xi) - if term > 1.0 { - ib = ib - 1 - term = term * sml - } - if ib == 0 { - finsum = finsum + term +// GAMMALN function returns the natural logarithm of the Gamma Function, Γ +// (n). The syntax of the function is: +// +// GAMMALN(x) +// +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) } - ans = ans + finsum + return newNumberFormulaArg(math.Log(math.Gamma(token.Number))) } - if y != x || p != pin { - ans = 1.0 - ans + return newErrorFormulaArg(formulaErrorVALUE, "GAMMALN requires 1 numeric argument") +} + +// GEOMEAN function calculates the geometric mean of a supplied set of values. +// The syntax of the function is: +// +// 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") } - ans = math.Max(math.Min(ans, 1.0), 0.0) - return ans + product := fn.PRODUCT(argsList) + if product.Type != ArgNumber { + return product + } + 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 newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) } -// pbeta returns distribution function of the beta distribution. -func pbeta(x, pin, qin float64) (ans float64) { - eps := d1mach(3) - alneps := math.Log(eps) - sml := d1mach(1) - alnsml := math.Log(sml) - y := x - p := pin - q := qin - if p/(p+q) < x { - y = 1.0 - y - p = qin - q = pin +// HARMEAN function calculates the harmonic mean of a supplied set of values. +// The syntax of the function is: +// +// HARMEAN(number1,[number2],...) +// +func (fn *formulaFuncs) HARMEAN(argsList *list.List) formulaArg { + if argsList.Len() < 1 { + return newErrorFormulaArg(formulaErrorVALUE, "HARMEAN requires at least 1 argument") } - if (p+q)*y/(p+1.0) < eps { - xb := p*math.Log(math.Max(y, sml)) - math.Log(p) - logBeta(p, q) - if xb > alnsml && y != 0.0 { - ans = math.Exp(xb) - } - if y != x || p != pin { - ans = 1.0 - ans - } - } else { - ps := q - math.Floor(q) - if ps == 0.0 { - ps = 1.0 - } - xb := p*math.Log(y) - logBeta(ps, p) - math.Log(p) - if xb >= alnsml { - ans = math.Exp(xb) - term := ans * p - if ps != 1.0 { - n := int(math.Max(alneps/math.Log(y), 4.0)) - for i := 1; i <= n; i++ { - xi := float64(i) - term = term * (xi - ps) * y / xi - ans = ans + term/(p+xi) - } + if min := fn.MIN(argsList); min.Number < 0 { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + } + number, val, cnt := 0.0, 0.0, 0.0 + for token := argsList.Front(); token != nil; token = token.Next() { + arg := token.Value.(formulaArg) + switch arg.Type { + case ArgString: + num := arg.ToNumber() + if num.Type != ArgNumber { + continue } + number = num.Number + case ArgNumber: + number = arg.Number } - ans = pbetaRaw(alnsml, ans, eps, p, pin, q, sml, x, y) + if number <= 0 { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + } + val += (1 / number) + cnt++ } - return ans + return newNumberFormulaArg(1 / (val / cnt)) } -// 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 { - 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)) - if prop3*prop4 <= 0 { - prev = math.Max(math.Abs(j), maxCumulative) - } - h := 1.0 - for iteratorCount := 0; iteratorCount < 1000; iteratorCount++ { - j = h * prop3 - if math.Abs(j) < prev { - i = beta3 - j - if i >= 0 && i <= 1.0 { - if prev <= alpha3 { - return beta3 - } - if math.Abs(prop3) <= alpha3 { - return beta3 - } - if i != 0 && i != 1.0 { - break +// KURT function calculates the kurtosis of a supplied set of values. The +// syntax of the function is: +// +// KURT(number1,[number2],...) +// +func (fn *formulaFuncs) KURT(argsList *list.List) formulaArg { + if argsList.Len() < 1 { + return newErrorFormulaArg(formulaErrorVALUE, "KURT requires at least 1 argument") + } + mean, stdev := fn.AVERAGE(argsList), fn.STDEV(argsList) + if stdev.Number > 0 { + count, summer := 0.0, 0.0 + for arg := argsList.Front(); arg != nil; arg = arg.Next() { + token := arg.Value.(formulaArg) + switch token.Type { + case ArgString, ArgNumber: + num := token.ToNumber() + if num.Type == ArgError { + continue + } + summer += math.Pow((num.Number-mean.Number)/stdev.Number, 4) + count++ + case ArgList, ArgMatrix: + for _, row := range token.ToList() { + if row.Type == ArgNumber || row.Type == ArgString { + num := row.ToNumber() + if num.Type == ArgError { + continue + } + summer += math.Pow((num.Number-mean.Number)/stdev.Number, 4) + count++ } } } - h /= 3.0 } - if i == beta3 { - return beta3 + if count > 3 { + return newNumberFormulaArg(summer*(count*(count+1)/((count-1)*(count-2)*(count-3))) - (3 * math.Pow(count-1, 2) / ((count - 2) * (count - 3)))) } - beta3, prop4 = i, prop3 } - return beta3 + return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV) } -// betainv is an implementation for the quantile of the beta distribution. -func betainv(probability, alpha, beta, lower, upper float64) float64 { - minCumulative, maxCumulative := 1.0e-300, 3.0e-308 - lowerBound, upperBound := maxCumulative, 1.0-2.22e-16 - needSwap := false - var alpha1, alpha2, beta1, beta2, beta3, prob1, x, y float64 - if probability <= 0.5 { - prob1, alpha1, beta1 = probability, alpha, beta - } else { - prob1, alpha1, beta1, needSwap = 1.0-probability, beta, alpha, true +// EXPONdotDIST function returns the value of the exponential distribution for +// a give value of x. The user can specify whether the probability density +// function or the cumulative distribution function is used. The syntax of the +// Expondist function is: +// +// 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") } - logbeta := 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 { - alpha2, beta2, prob2 = 1/(alpha1+alpha1-1), 1/(beta1+beta1-1), (prob3*prob3-3)/6 - x = 2 / (alpha2 + beta2) - y = prob3*math.Sqrt(x+prob2)/x - (beta2-alpha2)*(prob2+5/6.0-2/(x*3)) - beta3 = alpha1 / (alpha1 + beta1*math.Exp(y+y)) - } else { - 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) - } else { - beta2 = (prob2 + alpha1*4 - 2) / beta2 - if beta2 <= 1 { - beta3 = math.Exp((logbeta + math.Log(alpha1*prob1)) / alpha1) - } else { - beta3 = 1 - 2/(beta2+1) - } - } + return fn.EXPONDIST(argsList) +} + +// EXPONDIST function returns the value of the exponential distribution for a +// give value of x. The user can specify whether the probability density +// function or the cumulative distribution function is used. The syntax of the +// Expondist function is: +// +// EXPONDIST(x,lambda,cumulative) +// +func (fn *formulaFuncs) EXPONDIST(argsList *list.List) formulaArg { + if argsList.Len() != 3 { + return newErrorFormulaArg(formulaErrorVALUE, "EXPONDIST requires 3 arguments") } - beta2, prob2 = 1-beta1, 1-alpha1 - if beta3 < lowerBound { - beta3 = lowerBound - } else if beta3 > upperBound { - beta3 = upperBound + var x, lambda, cumulative formulaArg + if x = argsList.Front().Value.(formulaArg).ToNumber(); x.Type != ArgNumber { + return x } - 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) - if needSwap { - beta3 = 1.0 - beta3 + if lambda = argsList.Front().Next().Value.(formulaArg).ToNumber(); lambda.Type != ArgNumber { + return lambda } - return (upper-lower)*beta3 + lower + if cumulative = argsList.Back().Value.(formulaArg).ToBool(); cumulative.Type == ArgError { + return cumulative + } + if x.Number < 0 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + if lambda.Number <= 0 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + if cumulative.Number == 1 { + return newNumberFormulaArg(1 - math.Exp(-lambda.Number*x.Number)) + } + return newNumberFormulaArg(lambda.Number * math.Exp(-lambda.Number*x.Number)) } -// 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) -// -func (fn *formulaFuncs) FINV(argsList *list.List) formulaArg { +// finv is an implementation of the formula functions F.INV.RT and FINV. +func (fn *formulaFuncs) finv(name string, argsList *list.List) formulaArg { if argsList.Len() != 3 { - return newErrorFormulaArg(formulaErrorVALUE, "FINV requires 3 arguments") + return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires 3 arguments", name)) } var probability, d1, d2 formulaArg if probability = argsList.Front().Value.(formulaArg).ToNumber(); probability.Type != ArgNumber { @@ -6167,7 +6232,26 @@ func (fn *formulaFuncs) FINV(argsList *list.List) formulaArg { if d2.Number < 1 || d2.Number >= math.Pow10(10) { return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) } - return newNumberFormulaArg((1/betainv(1.0-(1.0-probability.Number), d2.Number/2, d1.Number/2, 0, 1) - 1.0) * (d2.Number / d1.Number)) + return newNumberFormulaArg((1/calcBetainv(1.0-(1.0-probability.Number), d2.Number/2, d1.Number/2, 0, 1) - 1.0) * (d2.Number / d1.Number)) +} + +// FdotINVdotRT function calculates the inverse of the (right-tailed) F +// Probability Distribution for a supplied probability. The syntax of the +// function is: +// +// F.INV.RT(probability,deg_freedom1,deg_freedom2) +// +func (fn *formulaFuncs) FdotINVdotRT(argsList *list.List) formulaArg { + return fn.finv("F.INV.RT", argsList) +} + +// 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) +// +func (fn *formulaFuncs) FINV(argsList *list.List) formulaArg { + return fn.finv("FINV", argsList) } // NORMdotDIST function calculates the Normal Probability Density Function or diff --git a/calc_test.go b/calc_test.go index 80ba8ef774..63e5984efb 100644 --- a/calc_test.go +++ b/calc_test.go @@ -784,6 +784,10 @@ func TestCalcCellValue(t *testing.T) { "=AVERAGEA(A1)": "1", "=AVERAGEA(A1:A2)": "1.5", "=AVERAGEA(D2:F9)": "12671.375", + // BETAINV + "=BETAINV(0.2,4,5,0,1)": "0.303225844664082", + // BETA.INV + "=BETA.INV(0.2,4,5,0,1)": "0.303225844664082", // CHIDIST "=CHIDIST(0.5,3)": "0.918891411654676", "=CHIDIST(8,3)": "0.0460117056892315", @@ -859,6 +863,14 @@ func TestCalcCellValue(t *testing.T) { "=FINV(0.5,4,8)": "0.914645355977072", "=FINV(0.1,79,86)": "1.32646097270444", "=FINV(1,40,5)": "0", + // F.INV.RT + "=F.INV.RT(0.2,1,2)": "3.55555555555555", + "=F.INV.RT(0.6,1,2)": "0.380952380952381", + "=F.INV.RT(0.6,2,2)": "0.666666666666667", + "=F.INV.RT(0.6,4,4)": "0.763454070045235", + "=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", // NORM.DIST "=NORM.DIST(0.8,1,0.3,TRUE)": "0.252492537546923", "=NORM.DIST(50,40,20,FALSE)": "0.017603266338215", @@ -2282,6 +2294,32 @@ func TestCalcCellValue(t *testing.T) { "=AVERAGE(H1)": "AVERAGE divide by zero", // AVERAGEA "=AVERAGEA(H1)": "AVERAGEA divide by zero", + // 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!", + // 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!", // AVERAGEIF "=AVERAGEIF()": "AVERAGEIF requires at least 2 arguments", "=AVERAGEIF(H1,\"\")": "AVERAGEIF divide by zero", @@ -2375,6 +2413,14 @@ func TestCalcCellValue(t *testing.T) { "=FINV(0,1,2)": "#NUM!", "=FINV(0.2,0.5,2)": "#NUM!", "=FINV(0.2,1,0.5)": "#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!", // NORM.DIST "=NORM.DIST()": "NORM.DIST requires 4 arguments", // NORMDIST diff --git a/excelize_test.go b/excelize_test.go index bafd446247..7d3830495b 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -130,14 +130,17 @@ func TestOpenFile(t *testing.T) { // Test boolean write booltest := []struct { value bool + raw bool expected string }{ - {false, "0"}, - {true, "1"}, + {false, true, "0"}, + {true, true, "1"}, + {false, false, "FALSE"}, + {true, false, "TRUE"}, } for _, test := range booltest { assert.NoError(t, f.SetCellValue("Sheet2", "F16", test.value)) - val, err := f.GetCellValue("Sheet2", "F16") + val, err := f.GetCellValue("Sheet2", "F16", Options{RawCellValue: test.raw}) assert.NoError(t, err) assert.Equal(t, test.expected, val) } diff --git a/rows.go b/rows.go index 0d2490ca65..81eaeeb823 100644 --- a/rows.go +++ b/rows.go @@ -429,6 +429,16 @@ func (c *xlsxC) getValueFrom(f *File, d *xlsxSST, raw bool) (string, error) { f.Lock() 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 case "s": if c.V != "" { xlsxSI := 0 diff --git a/rows_test.go b/rows_test.go index 208b2de841..22b038aa8a 100644 --- a/rows_test.go +++ b/rows_test.go @@ -950,6 +950,10 @@ 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++ { From 74b1a998d6018785878ac43b4a4bdcb906766a40 Mon Sep 17 00:00:00 2001 From: longphee <88870324+longphee@users.noreply.github.com> Date: Wed, 9 Mar 2022 12:34:48 +0800 Subject: [PATCH 552/957] This closes #1172, support set hole size for doughnut (#1173) --- chart_test.go | 2 +- drawing.go | 7 ++++++- xmlChart.go | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/chart_test.go b/chart_test.go index c99bfb2ce6..b1b4791f29 100644 --- a/chart_test.go +++ b/chart_test.go @@ -137,7 +137,7 @@ func TestAddChart(t *testing.T) { 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", "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"}`)) + 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", "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"}`)) diff --git a/drawing.go b/drawing.go index 5582bb4ff1..3af789f58d 100644 --- a/drawing.go +++ b/drawing.go @@ -513,13 +513,18 @@ func (f *File) drawBaseChart(formatSet *formatChart) *cPlotArea { // drawDoughnutChart provides a function to draw the c:plotArea element for // doughnut chart by given format sets. func (f *File) drawDoughnutChart(formatSet *formatChart) *cPlotArea { + holeSize := 75 + if formatSet.HoleSize > 0 && formatSet.HoleSize <= 90{ + holeSize = formatSet.HoleSize + } + return &cPlotArea{ DoughnutChart: &cCharts{ VaryColors: &attrValBool{ Val: boolPtr(formatSet.VaryColors), }, Ser: f.drawChartSeries(formatSet), - HoleSize: &attrValInt{Val: intPtr(75)}, + HoleSize: &attrValInt{Val: intPtr(holeSize)}, }, } } diff --git a/xmlChart.go b/xmlChart.go index 2e1e9585b2..45f1ce6dff 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -599,7 +599,7 @@ type formatChart struct { ShowBlanksAs string `json:"show_blanks_as"` ShowHiddenData bool `json:"show_hidden_data"` SetRotation int `json:"set_rotation"` - SetHoleSize int `json:"set_hole_size"` + HoleSize int `json:"hole_size"` order int } From aee7bdd3a07cbf7f436bf79461b170a3823c3fa1 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 10 Mar 2022 00:05:20 +0800 Subject: [PATCH 553/957] ref #65, new formula functions: F.INV and GAUSS --- calc.go | 59 +++++++++++++++++++++++++++++++++++++++++++++++----- calc_test.go | 18 ++++++++++++++++ 2 files changed, 72 insertions(+), 5 deletions(-) diff --git a/calc.go b/calc.go index ada84963fe..2d1aa0e62b 100644 --- a/calc.go +++ b/calc.go @@ -417,6 +417,7 @@ type formulaFuncs struct { // FALSE // FIND // FINDB +// F.INV // F.INV.RT // FINV // FISHER @@ -430,6 +431,7 @@ type formulaFuncs struct { // FVSCHEDULE // GAMMA // GAMMALN +// GAUSS // GCD // GEOMEAN // GESTEP @@ -6064,6 +6066,28 @@ func (fn *formulaFuncs) GAMMALN(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorVALUE, "GAMMALN requires 1 numeric argument") } +// 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: +// +// GAUSS(z) +// +func (fn *formulaFuncs) GAUSS(argsList *list.List) formulaArg { + if argsList.Len() != 1 { + return newErrorFormulaArg(formulaErrorVALUE, "GAUSS requires 1 numeric argument") + } + args := list.New().Init() + args.PushBack(argsList.Front().Value.(formulaArg)) + args.PushBack(formulaArg{Type: ArgNumber, Number: 0}) + args.PushBack(formulaArg{Type: ArgNumber, Number: 1}) + args.PushBack(newBoolFormulaArg(true)) + normdist := fn.NORMDIST(args) + if normdist.Type != ArgNumber { + return normdist + } + return newNumberFormulaArg(normdist.Number - 0.5) +} + // GEOMEAN function calculates the geometric mean of a supplied set of values. // The syntax of the function is: // @@ -6208,8 +6232,9 @@ func (fn *formulaFuncs) EXPONDIST(argsList *list.List) formulaArg { return newNumberFormulaArg(lambda.Number * math.Exp(-lambda.Number*x.Number)) } -// finv is an implementation of the formula functions F.INV.RT and FINV. -func (fn *formulaFuncs) finv(name string, argsList *list.List) formulaArg { +// 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 { if argsList.Len() != 3 { return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires 3 arguments", name)) } @@ -6232,7 +6257,21 @@ func (fn *formulaFuncs) finv(name string, argsList *list.List) formulaArg { if d2.Number < 1 || d2.Number >= math.Pow10(10) { return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) } - return newNumberFormulaArg((1/calcBetainv(1.0-(1.0-probability.Number), d2.Number/2, d1.Number/2, 0, 1) - 1.0) * (d2.Number / d1.Number)) + return newListFormulaArg([]formulaArg{probability, d1, d2}) +} + +// 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) +// +func (fn *formulaFuncs) FdotINV(argsList *list.List) formulaArg { + args := fn.prepareFinvArgs("F.INV", argsList) + if args.Type != ArgList { + return args + } + probability, d1, d2 := args.List[0], args.List[1], args.List[2] + return newNumberFormulaArg((1/calcBetainv(1-probability.Number, d2.Number/2, d1.Number/2, 0, 1) - 1) * (d2.Number / d1.Number)) } // FdotINVdotRT function calculates the inverse of the (right-tailed) F @@ -6242,7 +6281,12 @@ func (fn *formulaFuncs) finv(name string, argsList *list.List) formulaArg { // F.INV.RT(probability,deg_freedom1,deg_freedom2) // func (fn *formulaFuncs) FdotINVdotRT(argsList *list.List) formulaArg { - return fn.finv("F.INV.RT", argsList) + args := fn.prepareFinvArgs("F.INV.RT", argsList) + if args.Type != ArgList { + return args + } + probability, d1, d2 := args.List[0], args.List[1], args.List[2] + return newNumberFormulaArg((1/calcBetainv(1-(1-probability.Number), d2.Number/2, d1.Number/2, 0, 1) - 1) * (d2.Number / d1.Number)) } // FINV function calculates the inverse of the (right-tailed) F Probability @@ -6251,7 +6295,12 @@ func (fn *formulaFuncs) FdotINVdotRT(argsList *list.List) formulaArg { // FINV(probability,deg_freedom1,deg_freedom2) // func (fn *formulaFuncs) FINV(argsList *list.List) formulaArg { - return fn.finv("FINV", argsList) + args := fn.prepareFinvArgs("FINV", argsList) + if args.Type != ArgList { + return args + } + probability, d1, d2 := args.List[0], args.List[1], args.List[2] + return newNumberFormulaArg((1/calcBetainv(1-(1-probability.Number), d2.Number/2, d1.Number/2, 0, 1) - 1) * (d2.Number / d1.Number)) } // NORMdotDIST function calculates the Normal Probability Density Function or diff --git a/calc_test.go b/calc_test.go index 63e5984efb..9804cc9f30 100644 --- a/calc_test.go +++ b/calc_test.go @@ -838,6 +838,11 @@ func TestCalcCellValue(t *testing.T) { // GAMMALN "=GAMMALN(4.5)": "2.45373657084244", "=GAMMALN(INT(1))": "0", + // GAUSS + "=GAUSS(-5)": "-0.499999713348428", + "=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 @@ -855,6 +860,8 @@ 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", + // F.INV + "=F.INV(0.9,2,5)": "3.77971607877395", // FINV "=FINV(0.2,1,2)": "3.55555555555555", "=FINV(0.6,1,2)": "0.380952380952381", @@ -2380,6 +2387,9 @@ func TestCalcCellValue(t *testing.T) { "=GAMMALN(F1)": "GAMMALN requires 1 numeric argument", "=GAMMALN(0)": "#N/A", "=GAMMALN(INT(0))": "#N/A", + // GAUSS + "=GAUSS()": "GAUSS requires 1 numeric argument", + "=GAUSS(\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", // GEOMEAN "=GEOMEAN()": "GEOMEAN requires at least 1 numeric argument", "=GEOMEAN(0)": "#NUM!", @@ -2405,6 +2415,14 @@ func TestCalcCellValue(t *testing.T) { "=EXPONDIST(0,1,\"\")": "strconv.ParseBool: parsing \"\": invalid syntax", "=EXPONDIST(-1,1,TRUE)": "#NUM!", "=EXPONDIST(1,0,TRUE)": "#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!", // FINV "=FINV()": "FINV requires 3 arguments", "=FINV(\"\",1,2)": "strconv.ParseFloat: parsing \"\": invalid syntax", From 361611c23ae56cac091d5c7d73f5ea305b5bd3fc Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 11 Mar 2022 00:07:30 +0800 Subject: [PATCH 554/957] ref #65, new formula functions: GAMMA.DIST and GAMMADIST --- calc.go | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/calc.go b/calc.go index 2d1aa0e62b..1f1408e751 100644 --- a/calc.go +++ b/calc.go @@ -430,6 +430,8 @@ type formulaFuncs struct { // FV // FVSCHEDULE // GAMMA +// GAMMA.DIST +// GAMMADIST // GAMMALN // GAUSS // GCD @@ -6038,6 +6040,54 @@ func (fn *formulaFuncs) GAMMA(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorVALUE, "GAMMA requires 1 numeric argument") } +// GAMMAdotDIST function returns the Gamma Distribution, which is frequently +// used to provide probabilities for values that may have a skewed +// distribution, such as queuing analysis. +// +// 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") + } + return fn.GAMMADIST(argsList) +} + +// GAMMADIST function returns the Gamma Distribution, which is frequently used +// to provide probabilities for values that may have a skewed distribution, +// such as queuing analysis. +// +// GAMMADIST(x,alpha,beta,cumulative) +// +func (fn *formulaFuncs) GAMMADIST(argsList *list.List) formulaArg { + if argsList.Len() != 4 { + return newErrorFormulaArg(formulaErrorVALUE, "GAMMADIST requires 4 arguments") + } + var x, alpha, beta, cumulative formulaArg + if x = argsList.Front().Value.(formulaArg).ToNumber(); x.Type != ArgNumber { + return x + } + if x.Number < 0 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + if alpha = argsList.Front().Next().Value.(formulaArg).ToNumber(); alpha.Type != ArgNumber { + return alpha + } + if beta = argsList.Back().Prev().Value.(formulaArg).ToNumber(); beta.Type != ArgNumber { + return beta + } + if alpha.Number <= 0 || beta.Number <= 0 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + if cumulative = argsList.Back().Value.(formulaArg).ToBool(); cumulative.Type == ArgError { + return cumulative + } + 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))) +} + // 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 9804cc9f30..0c9fb2ea0a 100644 --- a/calc_test.go +++ b/calc_test.go @@ -835,6 +835,12 @@ func TestCalcCellValue(t *testing.T) { "=GAMMA(INT(1))": "1", "=GAMMA(1.5)": "0.886226925452758", "=GAMMA(5.5)": "52.34277778455352", + // GAMMA.DIST + "=GAMMA.DIST(6,3,2,FALSE)": "0.112020903827694", + "=GAMMA.DIST(6,3,2,TRUE)": "0.576809918873156", + // GAMMADIST + "=GAMMADIST(6,3,2,FALSE)": "0.112020903827694", + "=GAMMADIST(6,3,2,TRUE)": "0.576809918873156", // GAMMALN "=GAMMALN(4.5)": "2.45373657084244", "=GAMMALN(INT(1))": "0", @@ -2382,6 +2388,24 @@ func TestCalcCellValue(t *testing.T) { "=GAMMA(F1)": "GAMMA requires 1 numeric argument", "=GAMMA(0)": "#N/A", "=GAMMA(INT(0))": "#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!", + // 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!", // GAMMALN "=GAMMALN()": "GAMMALN requires 1 numeric argument", "=GAMMALN(F1)": "GAMMALN requires 1 numeric argument", From e1d660dda7c02475a8529a252e6c1fd171065429 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 12 Mar 2022 00:45:27 +0800 Subject: [PATCH 555/957] 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 556/957] 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 557/957] 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 558/957] 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 559/957] 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 560/957] 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 561/957] 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 562/957] 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 563/957] 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 564/957] 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 565/957] 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 566/957] 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 567/957] 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 568/957] 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 569/957] 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 570/957] 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 571/957] 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 572/957] 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 573/957] 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 574/957] 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 575/957] 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 576/957] 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 577/957] 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 578/957] 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 579/957] 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 580/957] 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 581/957] 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 582/957] 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 583/957] 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 584/957] 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 585/957] 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 586/957] 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 587/957] 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 588/957] 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 589/957] 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 590/957] 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 591/957] 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 592/957] 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 593/957] 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 594/957] 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 595/957] 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 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 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 596/957] 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 597/957] 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 598/957] 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 599/957] 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 600/957] 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 601/957] 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 602/957] 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 603/957] 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 604/957] 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 605/957] 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 606/957] 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 607/957] 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 608/957] 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 609/957] 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 610/957] 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 611/957] 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 612/957] 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 613/957] 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 614/957] 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 615/957] 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 616/957] 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 617/957] 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 618/957] 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 619/957] 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 620/957] 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 621/957] 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 622/957] 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 623/957] 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 624/957] 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 625/957] 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 626/957] 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 627/957] 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 628/957] 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 629/957] 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 630/957] 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 631/957] 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 632/957] 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 633/957] 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 634/957] 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 635/957] 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 636/957] 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 637/957] 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 638/957] 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 639/957] 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 640/957] 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 641/957] 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 642/957] 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 643/957] 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 644/957] 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 645/957] 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 646/957] 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 647/957] 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 648/957] 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 649/957] 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 650/957] 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 651/957] 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 652/957] 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 653/957] 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 654/957] 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 655/957] 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 656/957] 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 657/957] 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 658/957] 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 659/957] 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 660/957] 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 661/957] 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 662/957] 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 663/957] 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 664/957] 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 665/957] 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 666/957] 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 667/957] 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 668/957] 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 669/957] 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 670/957] 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 671/957] 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 672/957] 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 673/957] 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 674/957] 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 675/957] 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 676/957] 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 677/957] 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 678/957] 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 679/957] 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 680/957] 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 681/957] 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 682/957] 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 683/957] 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 684/957] 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 685/957] 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 686/957] 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 687/957] 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 688/957] 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 689/957] 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 690/957] 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 691/957] 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 692/957] 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 693/957] 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 694/957] 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 695/957] 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 696/957] 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 697/957] 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 698/957] 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 699/957] 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 700/957] 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 701/957] 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 702/957] 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 703/957] 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 704/957] 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 705/957] 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 706/957] 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 707/957] 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 708/957] 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 709/957] 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 710/957] 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 711/957] 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 712/957] 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 713/957] 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 714/957] 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 715/957] 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 716/957] 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 717/957] 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 718/957] 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 719/957] 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 720/957] 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 721/957] 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 722/957] 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 723/957] 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 724/957] 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 725/957] 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 726/957] 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 727/957] 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 728/957] 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 729/957] 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 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 delta 5266 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 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 730/957] 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 731/957] 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 732/957] 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 733/957] 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 734/957] 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 735/957] 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 736/957] 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 737/957] 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 738/957] 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 739/957] 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 740/957] 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 741/957] 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 742/957] 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 743/957] 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 744/957] 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 745/957] 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 746/957] 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 747/957] 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 748/957] 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 749/957] 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 750/957] 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 751/957] 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 752/957] 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 753/957] 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 754/957] 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 755/957] 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 756/957] 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 757/957] 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 758/957] 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 759/957] 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 760/957] 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 761/957] 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 762/957] 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 763/957] 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 764/957] 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 765/957] 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 766/957] 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 767/957] 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 } From 2c8dc5c1504ad2bd209d07c21ea878734464fcba Mon Sep 17 00:00:00 2001 From: David Date: Tue, 11 Jul 2023 11:43:45 -0400 Subject: [PATCH 768/957] This closes #1169, initialize add form controls supported (#1181) - Breaking changes: * Change `func (f *File) AddShape(sheet, cell string, opts *Shape) error` to `func (f *File) AddShape(sheet string, opts *Shape) error` * Rename the `Runs` field to `Paragraph` in the exported `Comment` data type - Add new exported function `AddFormControl` support to add button and radio form controls - Add check for shape type for the `AddShape` function, an error will be returned if no shape type is specified - Updated functions documentation and the unit tests --- comment.go | 429 --------------------- excelize_test.go | 11 +- shape.go | 7 +- shape_test.go | 41 ++- test/vbaProject.bin | Bin 15360 -> 16384 bytes vml.go | 655 +++++++++++++++++++++++++++++++++ vmlDrawing.go | 120 ++++-- comment_test.go => vml_test.go | 96 ++++- xmlComments.go | 10 +- xmlDrawing.go | 1 + 10 files changed, 877 insertions(+), 493 deletions(-) delete mode 100644 comment.go create mode 100644 vml.go rename comment_test.go => vml_test.go (58%) diff --git a/comment.go b/comment.go deleted file mode 100644 index 28ba40bce5..0000000000 --- a/comment.go +++ /dev/null @@ -1,429 +0,0 @@ -// 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. -// -// Package excelize 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. - -package excelize - -import ( - "bytes" - "encoding/xml" - "fmt" - "io" - "path/filepath" - "strconv" - "strings" -) - -// 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) - } - } - comments = append(comments, comment) - } - } - return comments, nil -} - -// getSheetComments provides the method to get the target comment reference by -// given worksheet file path. -func (f *File) getSheetComments(sheetFile string) string { - rels, _ := f.relsReader("xl/worksheets/_rels/" + sheetFile + ".rels") - if sheetRels := rels; sheetRels != nil { - sheetRels.mu.Lock() - defer sheetRels.mu.Unlock() - for _, v := range sheetRels.Relationships { - if v.Type == SourceRelationshipComments { - return v.Target - } - } - } - return "" -} - -// AddComment provides the method to add comment in a sheet by given worksheet -// index, cell and format set (such as author and text). Note that the max -// author length is 255 and the max text length is 32512. For example, add a -// comment in Sheet1!$A$30: -// -// err := f.AddComment("Sheet1", 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 { - return err - } - commentID := f.countComments() + 1 - drawingVML := "xl/drawings/vmlDrawing" + strconv.Itoa(commentID) + ".vml" - sheetRelationshipsComments := "../comments" + strconv.Itoa(commentID) + ".xml" - sheetRelationshipsDrawingVML := "../drawings/vmlDrawing" + strconv.Itoa(commentID) + ".vml" - if ws.LegacyDrawing != nil { - // 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.ReplaceAll(sheetRelationshipsDrawingVML, "..", "xl") - } else { - // Add first comment for given sheet. - 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) - f.addSheetLegacyDrawing(sheet, rID) - } - commentsXML := "xl/comments" + strconv.Itoa(commentID) + ".xml" - 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 - } - } - } - 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 - } - if err = f.addComment(commentsXML, comment); err != nil { - return err - } - return f.addContentTypePart(commentID, "comments") -} - -// DeleteComment provides the method to delete comment in a sheet by given -// worksheet name. For example, delete the comment in Sheet1!$A$30: -// -// 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) - } - 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 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(cmts.CommentList.Comment) > 1 { - cmts.CommentList.Comment = append( - cmts.CommentList.Comment[:i], - cmts.CommentList.Comment[i+1:]..., - ) - i-- - continue - } - cmts.CommentList.Comment = nil - } - f.Comments[commentsXML] = cmts - } - return err -} - -// 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 { - col, row, err := CellNameToCoordinates(cell) - if err != nil { - return err - } - yAxis := col - 1 - xAxis := row - 1 - vml := f.VMLDrawing[drawingVML] - if vml == nil { - vml = &vmlDrawing{ - XMLNSv: "urn:schemas-microsoft-com:vml", - XMLNSo: "urn:schemas-microsoft-com:office:office", - XMLNSx: "urn:schemas-microsoft-com:office:excel", - XMLNSmv: "http://macVmlSchemaUri", - Shapelayout: &xlsxShapelayout{ - Ext: "edit", - IDmap: &xlsxIDmap{ - Ext: "edit", - Data: commentID, - }, - }, - Shapetype: &xlsxShapetype{ - ID: "_x0000_t202", - Coordsize: "21600,21600", - Spt: 202, - Path: "m0,0l0,21600,21600,21600,21600,0xe", - Stroke: &xlsxStroke{ - Joinstyle: "miter", - }, - VPath: &vPath{ - Gradientshapeok: "t", - Connecttype: "rect", - }, - }, - } - // load exist comment shapes from xl/drawings/vmlDrawing%d.vml - d, err := f.decodeVMLDrawingReader(drawingVML) - if err != nil { - return err - } - 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{ - Color2: "#FBFE82", - Angle: -180, - Type: "gradient", - Fill: &oFill{ - Ext: "view", - Type: "gradientUnscaled", - }, - }, - Shadow: &vShadow{ - On: "t", - Color: "black", - Obscured: "t", - }, - Path: &vPath{ - Connecttype: "none", - }, - Textbox: &vTextbox{ - Style: "mso-direction-alt:auto", - Div: &xlsxDiv{ - Style: "text-align:left", - }, - }, - ClientData: &xClientData{ - ObjectType: "Note", - Anchor: fmt.Sprintf( - "%d, 23, %d, 0, %d, %d, %d, 5", - 1+yAxis, 1+xAxis, 2+yAxis+lineCount, colCount+yAxis, 2+xAxis+lineCount), - AutoFill: "True", - Row: xAxis, - Column: yAxis, - }, - } - s, _ := xml.Marshal(sp) - shape := 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: string(s[13 : len(s)-14]), - } - vml.Shape = append(vml.Shape, shape) - f.VMLDrawing[drawingVML] = vml - return err -} - -// 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) error { - if comment.Author == "" { - comment.Author = "Author" - } - if len(comment.Author) > MaxFieldLength { - comment.Author = comment.Author[:MaxFieldLength] - } - 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 - } - defaultFont, err := f.GetDefaultFont() - if err != nil { - return err - } - chars, cmt := 0, xlsxComment{ - Ref: comment.Cell, - AuthorID: authorID, - 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) - } - 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 -// the folder xl. -func (f *File) countComments() int { - c1, c2 := 0, 0 - f.Pkg.Range(func(k, v interface{}) bool { - if strings.Contains(k.(string), "xl/comments") { - c1++ - } - return true - }) - for rel := range f.Comments { - if strings.Contains(rel, "xl/comments") { - c2++ - } - } - if c1 < c2 { - return c2 - } - return c1 -} - -// 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, 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)))). - Decode(f.DecodeVMLDrawing[path]); err != nil && err != io.EOF { - return nil, err - } - } - } - return f.DecodeVMLDrawing[path], nil -} - -// vmlDrawingWriter provides a function to save xl/drawings/vmlDrawing%d.xml -// after serialize structure. -func (f *File) vmlDrawingWriter() { - for path, vml := range f.VMLDrawing { - if vml != nil { - v, _ := xml.Marshal(vml) - f.Pkg.Store(path, v) - } - } -} - -// 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, 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)))). - Decode(f.Comments[path]); err != nil && err != io.EOF { - return nil, err - } - } - } - return f.Comments[path], nil -} - -// commentsWriter provides a function to save xl/comments%d.xml after -// serialize structure. -func (f *File) commentsWriter() { - for path, c := range f.Comments { - if c != nil { - v, _ := xml.Marshal(c) - f.saveFileList(path, v) - } - } -} diff --git a/excelize_test.go b/excelize_test.go index 9372be5865..9bc0107db1 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -383,15 +383,6 @@ func TestNewFile(t *testing.T) { assert.NoError(t, f.Save()) } -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) { f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) assert.NoError(t, err) @@ -978,7 +969,7 @@ func TestSetDeleteSheet(t *testing.T) { f, err := prepareTestBook4() 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.AddComment("Sheet1", Comment{Cell: "A1", Author: "Excelize", Paragraph: []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/shape.go b/shape.go index 8d9f81455d..3f3dbe2e4f 100644 --- a/shape.go +++ b/shape.go @@ -22,6 +22,9 @@ func parseShapeOptions(opts *Shape) (*Shape, error) { if opts == nil { return nil, ErrParameterInvalid } + if opts.Type == "" { + return nil, ErrParameterInvalid + } if opts.Width == 0 { opts.Width = defaultShapeSize } @@ -285,7 +288,7 @@ func parseShapeOptions(opts *Shape) (*Shape, error) { // wavy // wavyHeavy // wavyDbl -func (f *File) AddShape(sheet, cell string, opts *Shape) error { +func (f *File) AddShape(sheet string, opts *Shape) error { options, err := parseShapeOptions(opts) if err != nil { return err @@ -313,7 +316,7 @@ func (f *File) AddShape(sheet, cell string, opts *Shape) error { f.addSheetDrawing(sheet, rID) f.addSheetNameSpace(sheet, SourceRelationship) } - if err = f.addDrawingShape(sheet, drawingXML, cell, options); err != nil { + if err = f.addDrawingShape(sheet, drawingXML, opts.Cell, options); err != nil { return err } return f.addContentTypePart(drawingID, "drawings") diff --git a/shape_test.go b/shape_test.go index c9ba9d90a2..2a9fa08cb3 100644 --- a/shape_test.go +++ b/shape_test.go @@ -12,18 +12,19 @@ func TestAddShape(t *testing.T) { if !assert.NoError(t, err) { t.FailNow() } - shape := &Shape{ + assert.NoError(t, f.AddShape("Sheet1", &Shape{ + Cell: "A30", Type: "rect", 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: []RichTextRun{{Text: "Rectangle"}, {}}})) - assert.NoError(t, f.AddShape("Sheet1", "C30", &Shape{Type: "rect"})) - assert.EqualError(t, f.AddShape("Sheet3", "H1", + })) + assert.NoError(t, f.AddShape("Sheet1", &Shape{Cell: "B30", Type: "rect", Paragraph: []RichTextRun{{Text: "Rectangle"}, {}}})) + assert.NoError(t, f.AddShape("Sheet1", &Shape{Cell: "C30", Type: "rect"})) + assert.EqualError(t, f.AddShape("Sheet3", &Shape{ + Cell: "H1", Type: "ellipseRibbon", Line: ShapeLine{Color: "4286F4"}, Fill: Fill{Color: []string{"8EB9FF"}}, @@ -41,15 +42,24 @@ func TestAddShape(t *testing.T) { }, }, ), "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.EqualError(t, f.AddShape("Sheet3", nil), ErrParameterInvalid.Error()) + assert.EqualError(t, f.AddShape("Sheet1", &Shape{Cell: "A1"}), ErrParameterInvalid.Error()) + assert.EqualError(t, f.AddShape("Sheet1", &Shape{ + Cell: "A", + Type: "rect", + Paragraph: []RichTextRun{ + {Text: "Rectangle", Font: &Font{Color: "CD5C5C"}}, + {Text: "Shape", Font: &Font{Bold: true, Color: "2980B9"}}, + }, + }), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddShape1.xlsx"))) // Test add first shape for given sheet f = NewFile() lineWidth := 1.2 - assert.NoError(t, f.AddShape("Sheet1", "A1", + assert.NoError(t, f.AddShape("Sheet1", &Shape{ + Cell: "A1", Type: "ellipseRibbon", Line: ShapeLine{Color: "4286F4", Width: &lineWidth}, Fill: Fill{Color: []string{"8EB9FF"}}, @@ -69,16 +79,23 @@ func TestAddShape(t *testing.T) { })) 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", shape), ErrSheetNameInvalid.Error()) + assert.EqualError(t, f.AddShape("Sheet:1", &Shape{ + Cell: "A30", + Type: "rect", + Paragraph: []RichTextRun{ + {Text: "Rectangle", Font: &Font{Color: "CD5C5C"}}, + {Text: "Shape", Font: &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", &Shape{Type: "rect", Paragraph: []RichTextRun{{Text: "Rectangle"}, {}}}), "XML syntax error on line 1: invalid UTF-8") + assert.EqualError(t, f.AddShape("Sheet1", &Shape{Cell: "B30", 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: []RichTextRun{{Text: "Rectangle"}, {}}}), "XML syntax error on line 1: invalid UTF-8") + assert.EqualError(t, f.AddShape("Sheet1", &Shape{Cell: "B30", Type: "rect", Paragraph: []RichTextRun{{Text: "Rectangle"}, {}}}), "XML syntax error on line 1: invalid UTF-8") } func TestAddDrawingShape(t *testing.T) { diff --git a/test/vbaProject.bin b/test/vbaProject.bin index 7e88db07c1f26ef2d4c082543627c13a90d546f6..77b6bf829215544023e152349675c5e7fbffad6c 100644 GIT binary patch literal 16384 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 literal 15360 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 1 { + cmts.CommentList.Comment = append( + cmts.CommentList.Comment[:i], + cmts.CommentList.Comment[i+1:]..., + ) + i-- + continue + } + cmts.CommentList.Comment = nil + } + f.Comments[commentsXML] = cmts + } + return err +} + +// addComment provides a function to create chart as xl/comments%d.xml by +// given cell and format sets. +func (f *File) addComment(commentsXML string, opts vmlOptions) error { + if opts.Author == "" { + opts.Author = "Author" + } + if len(opts.Author) > MaxFieldLength { + opts.Author = opts.Author[:MaxFieldLength] + } + cmts, err := f.commentsReader(commentsXML) + if err != nil { + return err + } + var authorID int + if cmts == nil { + cmts = &xlsxComments{Authors: xlsxAuthor{Author: []string{opts.Author}}} + } + if inStrSlice(cmts.Authors.Author, opts.Author, true) == -1 { + cmts.Authors.Author = append(cmts.Authors.Author, opts.Author) + authorID = len(cmts.Authors.Author) - 1 + } + defaultFont, err := f.GetDefaultFont() + if err != nil { + return err + } + chars, cmt := 0, xlsxComment{ + Ref: opts.Cell, + AuthorID: authorID, + Text: xlsxText{R: []xlsxR{}}, + } + if opts.Text != "" { + if len(opts.Text) > TotalCellChars { + opts.Text = opts.Text[:TotalCellChars] + } + cmt.Text.T = stringPtr(opts.Text) + chars += len(opts.Text) + } + for _, run := range opts.Paragraph { + 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) + } + 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 +// the folder xl. +func (f *File) countComments() int { + c1, c2 := 0, 0 + f.Pkg.Range(func(k, v interface{}) bool { + if strings.Contains(k.(string), "xl/comments") { + c1++ + } + return true + }) + for rel := range f.Comments { + if strings.Contains(rel, "xl/comments") { + c2++ + } + } + if c1 < c2 { + return c2 + } + return c1 +} + +// 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, 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)))). + Decode(f.Comments[path]); err != nil && err != io.EOF { + return nil, err + } + } + } + return f.Comments[path], nil +} + +// commentsWriter provides a function to save xl/comments%d.xml after +// serialize structure. +func (f *File) commentsWriter() { + for path, c := range f.Comments { + if c != nil { + v, _ := xml.Marshal(c) + f.saveFileList(path, v) + } + } +} + +// AddFormControl provides the method to add form control button in a worksheet +// by given worksheet name and form control options. Supported form control +// type: button and radio. The file extension should be XLSM or XLTM. For +// example: +// +// err := f.AddFormControl("Sheet1", excelize.FormControl{ +// Cell: "A1", +// Type: excelize.FormControlButton, +// Macro: "Button1_Click", +// Width: 140, +// Height: 60, +// Text: "Button 1\r\n", +// Paragraph: []excelize.RichTextRun{ +// { +// Font: &excelize.Font{ +// Bold: true, +// Italic: true, +// Underline: "single", +// Family: "Times New Roman", +// Size: 14, +// Color: "777777", +// }, +// Text: "C1=A1+B1", +// }, +// }, +// }) +func (f *File) AddFormControl(sheet string, opts FormControl) error { + return f.addVMLObject(vmlOptions{ + FormCtrl: true, + Sheet: sheet, + Type: opts.Type, + Checked: opts.Checked, + Cell: opts.Cell, + Macro: opts.Macro, + Width: opts.Width, + Height: opts.Height, + Format: opts.Format, + Text: opts.Text, + Paragraph: opts.Paragraph, + }) +} + +// countVMLDrawing provides a function to get VML drawing files count storage +// in the folder xl/drawings. +func (f *File) countVMLDrawing() int { + c1, c2 := 0, 0 + f.Pkg.Range(func(k, v interface{}) bool { + if strings.Contains(k.(string), "xl/drawings/vmlDrawing") { + c1++ + } + return true + }) + for rel := range f.VMLDrawing { + if strings.Contains(rel, "xl/drawings/vmlDrawing") { + c2++ + } + } + if c1 < c2 { + return c2 + } + return c1 +} + +// 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, 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)))). + Decode(f.DecodeVMLDrawing[path]); err != nil && err != io.EOF { + return nil, err + } + } + } + return f.DecodeVMLDrawing[path], nil +} + +// vmlDrawingWriter provides a function to save xl/drawings/vmlDrawing%d.xml +// after serialize structure. +func (f *File) vmlDrawingWriter() { + for path, vml := range f.VMLDrawing { + if vml != nil { + v, _ := xml.Marshal(vml) + f.Pkg.Store(path, v) + } + } +} + +// addVMLObject provides a function to create VML drawing parts and +// relationships for comments and form controls. +func (f *File) addVMLObject(opts vmlOptions) error { + // Read sheet data. + ws, err := f.workSheetReader(opts.Sheet) + if err != nil { + return err + } + vmlID := f.countComments() + 1 + if opts.FormCtrl { + if opts.Type > FormControlRadio { + return ErrParameterInvalid + } + vmlID = f.countVMLDrawing() + 1 + } + drawingVML := "xl/drawings/vmlDrawing" + strconv.Itoa(vmlID) + ".vml" + sheetRelationshipsDrawingVML := "../drawings/vmlDrawing" + strconv.Itoa(vmlID) + ".vml" + if ws.LegacyDrawing != nil { + // The worksheet already has a VML relationships, use the relationships drawing ../drawings/vmlDrawing%d.vml. + sheetRelationshipsDrawingVML = f.getSheetRelationshipsTargetByID(opts.Sheet, ws.LegacyDrawing.RID) + vmlID, _ = strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(sheetRelationshipsDrawingVML, "../drawings/vmlDrawing"), ".vml")) + drawingVML = strings.ReplaceAll(sheetRelationshipsDrawingVML, "..", "xl") + } else { + // Add first VML drawing for given sheet. + sheetXMLPath, _ := f.getSheetXMLPath(opts.Sheet) + sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetXMLPath, "xl/worksheets/") + ".rels" + rID := f.addRels(sheetRels, SourceRelationshipDrawingVML, sheetRelationshipsDrawingVML, "") + if !opts.FormCtrl { + sheetRelationshipsComments := "../comments" + strconv.Itoa(vmlID) + ".xml" + f.addRels(sheetRels, SourceRelationshipComments, sheetRelationshipsComments, "") + } + f.addSheetNameSpace(opts.Sheet, SourceRelationship) + f.addSheetLegacyDrawing(opts.Sheet, rID) + } + if err = f.addDrawingVML(vmlID, drawingVML, prepareFormCtrlOptions(&opts)); err != nil { + return err + } + if !opts.FormCtrl { + commentsXML := "xl/comments" + strconv.Itoa(vmlID) + ".xml" + if err = f.addComment(commentsXML, opts); err != nil { + return err + } + } + return f.addContentTypePart(vmlID, "comments") +} + +// prepareFormCtrlOptions provides a function to parse the format settings of +// the form control with default value. +func prepareFormCtrlOptions(opts *vmlOptions) *vmlOptions { + for _, runs := range opts.Paragraph { + for _, subStr := range strings.Split(runs.Text, "\n") { + opts.rows++ + if chars := len(subStr); chars > opts.cols { + opts.cols = chars + } + } + } + if len(opts.Paragraph) == 0 { + opts.rows, opts.cols = 1, len(opts.Text) + } + if opts.Format.ScaleX == 0 { + opts.Format.ScaleX = 1 + } + if opts.Format.ScaleY == 0 { + opts.Format.ScaleY = 1 + } + if opts.cols == 0 { + opts.cols = 8 + } + if opts.Width == 0 { + opts.Width = uint(opts.cols * 9) + } + if opts.Height == 0 { + opts.Height = uint(opts.rows * 25) + } + return opts +} + +// formCtrlText returns font element in the VML for control form text. +func formCtrlText(opts *vmlOptions) []vmlFont { + var font []vmlFont + if opts.Text != "" { + font = append(font, vmlFont{Content: opts.Text}) + } + for _, run := range opts.Paragraph { + fnt := vmlFont{ + Content: run.Text + "

\r\n", + } + if run.Font != nil { + fnt.Face = run.Font.Family + fnt.Color = run.Font.Color + if !strings.HasPrefix(run.Font.Color, "#") { + fnt.Color = "#" + fnt.Color + } + if run.Font.Size != 0 { + fnt.Size = uint(run.Font.Size * 20) + } + if run.Font.Underline == "single" { + fnt.Content = "" + fnt.Content + "" + } + if run.Font.Italic { + fnt.Content = "" + fnt.Content + "" + } + if run.Font.Bold { + fnt.Content = "" + fnt.Content + "" + } + } + font = append(font, fnt) + } + return font +} + +var ( + formCtrlPresets = map[FormControlType]struct { + objectType string + filled string + fillColor string + stroked string + strokeColor string + strokeButton string + fill *vFill + textHAlign string + textVAlign string + noThreeD *string + firstButton *string + shadow *vShadow + }{ + FormControlNote: { + objectType: "Note", + filled: "", + fillColor: "#FBF6D6", + stroked: "", + strokeColor: "#EDEAA1", + strokeButton: "", + fill: &vFill{ + Color2: "#FBFE82", + Angle: -180, + Type: "gradient", + Fill: &oFill{Ext: "view", Type: "gradientUnscaled"}, + }, + textHAlign: "", + textVAlign: "", + noThreeD: nil, + firstButton: nil, + shadow: &vShadow{On: "t", Color: "black", Obscured: "t"}, + }, + FormControlButton: { + objectType: "Button", + filled: "", + fillColor: "buttonFace [67]", + stroked: "", + strokeColor: "windowText [64]", + strokeButton: "t", + fill: &vFill{ + Color2: "buttonFace [67]", + Angle: -180, + Type: "gradient", + Fill: &oFill{Ext: "view", Type: "gradientUnscaled"}, + }, + textHAlign: "Center", + textVAlign: "Center", + noThreeD: nil, + firstButton: nil, + shadow: nil, + }, + FormControlRadio: { + objectType: "Radio", + filled: "f", + fillColor: "window [65]", + stroked: "f", + strokeColor: "windowText [64]", + strokeButton: "", + fill: nil, + textHAlign: "", + textVAlign: "Center", + noThreeD: stringPtr(""), + firstButton: stringPtr(""), + shadow: nil, + }, + } +) + +// addDrawingVML provides a function to create VML drawing XML as +// xl/drawings/vmlDrawing%d.vml by given data ID, XML path and VML options. The +// anchor value is a comma-separated list of data written out as: LeftColumn, +// LeftOffset, TopRow, TopOffset, RightColumn, RightOffset, BottomRow, +// BottomOffset. +func (f *File) addDrawingVML(dataID int, drawingVML string, opts *vmlOptions) error { + col, row, err := CellNameToCoordinates(opts.Cell) + if err != nil { + return err + } + anchor := fmt.Sprintf("%d, 23, %d, 0, %d, %d, %d, 5", col, row, col+opts.rows+2, col+opts.cols-1, row+opts.rows+2) + vmlID, vml, preset := 202, f.VMLDrawing[drawingVML], formCtrlPresets[opts.Type] + style := "position:absolute;73.5pt;width:108pt;height:59.25pt;z-index:1;visibility:hidden" + var font []vmlFont + if opts.FormCtrl { + vmlID = 201 + style = "position:absolute;73.5pt;width:108pt;height:59.25pt;z-index:1;mso-wrap-style:tight" + colStart, rowStart, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(opts.Sheet, col, row, opts.Format.OffsetX, opts.Format.OffsetY, int(opts.Width), int(opts.Height)) + anchor = fmt.Sprintf("%d, 0, %d, 0, %d, %d, %d, %d", colStart, rowStart, colEnd, x2, rowEnd, y2) + font = formCtrlText(opts) + } + if vml == nil { + vml = &vmlDrawing{ + XMLNSv: "urn:schemas-microsoft-com:vml", + XMLNSo: "urn:schemas-microsoft-com:office:office", + XMLNSx: "urn:schemas-microsoft-com:office:excel", + XMLNSmv: "http://macVmlSchemaUri", + ShapeLayout: &xlsxShapeLayout{ + Ext: "edit", IDmap: &xlsxIDmap{Ext: "edit", Data: dataID}, + }, + ShapeType: &xlsxShapeType{ + ID: fmt.Sprintf("_x0000_t%d", vmlID), + CoordSize: "21600,21600", + Spt: 202, + Path: "m0,0l0,21600,21600,21600,21600,0xe", + Stroke: &xlsxStroke{JoinStyle: "miter"}, + VPath: &vPath{GradientShapeOK: "t", ConnectType: "rect"}, + }, + } + // load exist VML shapes from xl/drawings/vmlDrawing%d.vml + d, err := f.decodeVMLDrawingReader(drawingVML) + if err != nil { + return err + } + if d != nil { + vml.ShapeType.ID = d.ShapeType.ID + vml.ShapeType.CoordSize = d.ShapeType.CoordSize + vml.ShapeType.Spt = d.ShapeType.Spt + vml.ShapeType.Path = d.ShapeType.Path + for _, v := range d.Shape { + s := xlsxShape{ + ID: v.ID, + Type: v.Type, + Style: v.Style, + Button: v.Button, + Filled: v.Filled, + FillColor: v.FillColor, + InsetMode: v.InsetMode, + Stroked: v.Stroked, + StrokeColor: v.StrokeColor, + Val: v.Val, + } + vml.Shape = append(vml.Shape, s) + } + } + } + sp := encodeShape{ + Fill: preset.fill, + Shadow: preset.shadow, + Path: &vPath{ConnectType: "none"}, + TextBox: &vTextBox{ + Style: "mso-direction-alt:auto", + Div: &xlsxDiv{Style: "text-align:left", Font: font}, + }, + ClientData: &xClientData{ + ObjectType: preset.objectType, + Anchor: anchor, + AutoFill: "True", + Row: row - 1, + Column: col - 1, + TextHAlign: preset.textHAlign, + TextVAlign: preset.textVAlign, + NoThreeD: preset.noThreeD, + FirstButton: preset.firstButton, + }, + } + if opts.FormCtrl { + sp.ClientData.FmlaMacro = opts.Macro + } + if opts.Type == FormControlRadio && opts.Checked { + sp.ClientData.Checked = stringPtr("1") + } + s, _ := xml.Marshal(sp) + shape := xlsxShape{ + ID: "_x0000_s1025", + Type: fmt.Sprintf("#_x0000_t%d", vmlID), + Style: style, + Button: preset.strokeButton, + Filled: preset.filled, + FillColor: preset.fillColor, + Stroked: preset.stroked, + StrokeColor: preset.strokeColor, + Val: string(s[13 : len(s)-14]), + } + vml.Shape = append(vml.Shape, shape) + f.VMLDrawing[drawingVML] = vml + return err +} diff --git a/vmlDrawing.go b/vmlDrawing.go index 1a49d72212..89f91d9278 100644 --- a/vmlDrawing.go +++ b/vmlDrawing.go @@ -21,15 +21,15 @@ type vmlDrawing struct { XMLNSo string `xml:"xmlns:o,attr"` XMLNSx string `xml:"xmlns:x,attr"` XMLNSmv string `xml:"xmlns:mv,attr"` - Shapelayout *xlsxShapelayout `xml:"o:shapelayout"` - Shapetype *xlsxShapetype `xml:"v:shapetype"` + ShapeLayout *xlsxShapeLayout `xml:"o:shapelayout"` + ShapeType *xlsxShapeType `xml:"v:shapetype"` Shape []xlsxShape `xml:"v:shape"` } -// xlsxShapelayout directly maps the shapelayout element. This element contains +// xlsxShapeLayout directly maps the shapelayout element. This element contains // child elements that store information used in the editing and layout of // shapes. -type xlsxShapelayout struct { +type xlsxShapeLayout struct { Ext string `xml:"v:ext,attr"` IDmap *xlsxIDmap `xml:"o:idmap"` } @@ -46,16 +46,19 @@ type xlsxShape struct { ID string `xml:"id,attr"` Type string `xml:"type,attr"` Style string `xml:"style,attr"` - Fillcolor string `xml:"fillcolor,attr"` - Insetmode string `xml:"urn:schemas-microsoft-com:office:office insetmode,attr,omitempty"` - Strokecolor string `xml:"strokecolor,attr,omitempty"` + Button string `xml:"o:button,attr,omitempty"` + Filled string `xml:"filled,attr,omitempty"` + FillColor string `xml:"fillcolor,attr"` + InsetMode string `xml:"urn:schemas-microsoft-com:office:office insetmode,attr,omitempty"` + Stroked string `xml:"stroked,attr,omitempty"` + StrokeColor string `xml:"strokecolor,attr,omitempty"` Val string `xml:",innerxml"` } -// xlsxShapetype directly maps the shapetype element. -type xlsxShapetype struct { +// xlsxShapeType directly maps the shapetype element. +type xlsxShapeType struct { ID string `xml:"id,attr"` - Coordsize string `xml:"coordsize,attr"` + CoordSize string `xml:"coordsize,attr"` Spt int `xml:"o:spt,attr"` Path string `xml:"path,attr"` Stroke *xlsxStroke `xml:"v:stroke"` @@ -64,13 +67,13 @@ type xlsxShapetype struct { // xlsxStroke directly maps the stroke element. type xlsxStroke struct { - Joinstyle string `xml:"joinstyle,attr"` + JoinStyle string `xml:"joinstyle,attr"` } // vPath directly maps the v:path element. type vPath struct { - Gradientshapeok string `xml:"gradientshapeok,attr,omitempty"` - Connecttype string `xml:"o:connecttype,attr"` + GradientShapeOK string `xml:"gradientshapeok,attr,omitempty"` + ConnectType string `xml:"o:connecttype,attr"` } // vFill directly maps the v:fill element. This element must be defined within a @@ -96,16 +99,24 @@ type vShadow struct { Obscured string `xml:"obscured,attr"` } -// vTextbox directly maps the v:textbox element. This element must be defined +// vTextBox directly maps the v:textbox element. This element must be defined // within a Shape element. -type vTextbox struct { +type vTextBox struct { Style string `xml:"style,attr"` Div *xlsxDiv `xml:"div"` } // xlsxDiv directly maps the div element. type xlsxDiv struct { - Style string `xml:"style,attr"` + Style string `xml:"style,attr"` + Font []vmlFont `xml:"font"` +} + +type vmlFont struct { + Face string `xml:"face,attr,omitempty"` + Size uint `xml:"size,attr,omitempty"` + Color string `xml:"color,attr,omitempty"` + Content string `xml:",innerxml"` } // xClientData (Attached Object Data) directly maps the x:ClientData element. @@ -116,24 +127,49 @@ type xlsxDiv struct { // child elements is appropriate. Relevant groups are identified for each child // element. type xClientData struct { - ObjectType string `xml:"ObjectType,attr"` - 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"` - Column int `xml:"x:Column"` + ObjectType string `xml:"ObjectType,attr"` + 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"` + Column int `xml:"x:Column"` + FmlaMacro string `xml:"x:FmlaMacro,omitempty"` + TextHAlign string `xml:"x:TextHAlign,omitempty"` + TextVAlign string `xml:"x:TextVAlign,omitempty"` + Checked *string `xml:"x:Checked,omitempty"` + NoThreeD *string `xml:"x:NoThreeD,omitempty"` + FirstButton *string `xml:"x:FirstButton,omitempty"` } // decodeVmlDrawing defines the structure used to parse the file // xl/drawings/vmlDrawing%d.vml. type decodeVmlDrawing struct { - Shape []decodeShape `xml:"urn:schemas-microsoft-com:vml shape"` + ShapeType decodeShapeType `xml:"urn:schemas-microsoft-com:vml shapetype"` + Shape []decodeShape `xml:"urn:schemas-microsoft-com:vml shape"` +} + +// decodeShapeType defines the structure used to parse the shapetype element in +// the file xl/drawings/vmlDrawing%d.vml. +type decodeShapeType struct { + ID string `xml:"id,attr"` + CoordSize string `xml:"coordsize,attr"` + Spt int `xml:"spt,attr"` + Path string `xml:"path,attr"` } // decodeShape defines the structure used to parse the particular shape element. type decodeShape struct { - Val string `xml:",innerxml"` + ID string `xml:"id,attr"` + Type string `xml:"type,attr"` + Style string `xml:"style,attr"` + Button string `xml:"button,attr,omitempty"` + Filled string `xml:"filled,attr,omitempty"` + FillColor string `xml:"fillcolor,attr"` + InsetMode string `xml:"urn:schemas-microsoft-com:office:office insetmode,attr,omitempty"` + Stroked string `xml:"stroked,attr,omitempty"` + StrokeColor string `xml:"strokecolor,attr,omitempty"` + Val string `xml:",innerxml"` } // encodeShape defines the structure used to re-serialization shape element. @@ -141,6 +177,38 @@ type encodeShape struct { Fill *vFill `xml:"v:fill"` Shadow *vShadow `xml:"v:shadow"` Path *vPath `xml:"v:path"` - Textbox *vTextbox `xml:"v:textbox"` + TextBox *vTextBox `xml:"v:textbox"` ClientData *xClientData `xml:"x:ClientData"` } + +// vmlOptions defines the structure used to internal comments and form controls. +type vmlOptions struct { + rows int + cols int + FormCtrl bool + Sheet string + Author string + AuthorID int + Cell string + Checked bool + Text string + Macro string + Width uint + Height uint + Paragraph []RichTextRun + Type FormControlType + Format GraphicOptions +} + +// FormControl directly maps the form controls information. +type FormControl struct { + Cell string + Macro string + Width uint + Height uint + Checked bool + Text string + Paragraph []RichTextRun + Type FormControlType + Format GraphicOptions +} diff --git a/comment_test.go b/vml_test.go similarity index 58% rename from comment_test.go rename to vml_test.go index b6ea2aa042..47ec33d17c 100644 --- a/comment_test.go +++ b/vml_test.go @@ -13,6 +13,7 @@ package excelize import ( "encoding/xml" + "os" "path/filepath" "strings" "testing" @@ -27,13 +28,13 @@ func TestAddComment(t *testing.T) { } 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."}}})) + assert.NoError(t, f.AddComment("Sheet1", Comment{Cell: "A30", Author: s, Text: s, Paragraph: []RichTextRun{{Text: s}, {Text: s}}})) + assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "B7", Author: "Excelize", Text: s[:TotalCellChars-1], Paragraph: []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", Comment{Cell: "B7", Author: "Excelize", Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}}), "sheet SheetN does not exist") + assert.EqualError(t, f.AddComment("SheetN", Comment{Cell: "B7", Author: "Excelize", Paragraph: []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()) + assert.EqualError(t, f.AddComment("Sheet1", Comment{Cell: "A", Author: "Excelize", Paragraph: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) comments, err := f.GetComments("Sheet1") assert.NoError(t, err) assert.Len(t, comments, 2) @@ -86,11 +87,11 @@ func TestDeleteComment(t *testing.T) { } 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.AddComment("Sheet2", Comment{Cell: "A41", Paragraph: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment2."}}})) + assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "C41", Paragraph: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment3."}}})) + assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "C41", Paragraph: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment3-1."}}})) + assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "C42", Paragraph: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment4."}}})) + assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "C41", Paragraph: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment2."}}})) assert.NoError(t, f.DeleteComment("Sheet2", "A40")) @@ -144,3 +145,80 @@ func TestCountComments(t *testing.T) { f.Comments["xl/comments1.xml"] = nil assert.Equal(t, f.countComments(), 1) } + +func TestAddDrawingVML(t *testing.T) { + // Test addDrawingVML with illegal cell reference + f := NewFile() + assert.EqualError(t, f.addDrawingVML(0, "", &vmlOptions{Cell: "*"}), newCellNameToCoordinatesError("*", newInvalidCellNameError("*")).Error()) + + f.Pkg.Store("xl/drawings/vmlDrawing1.vml", MacintoshCyrillicCharset) + assert.EqualError(t, f.addDrawingVML(0, "xl/drawings/vmlDrawing1.vml", &vmlOptions{Cell: "A1"}), "XML syntax error on line 1: invalid UTF-8") +} + +func TestAddFormControl(t *testing.T) { + f := NewFile() + assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ + Cell: "D1", + Type: FormControlButton, + Macro: "Button1_Click", + })) + assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ + Cell: "A1", + Type: FormControlButton, + Macro: "Button1_Click", + Width: 140, + Height: 60, + Text: "Button 1\r\n", + Paragraph: []RichTextRun{ + { + Font: &Font{ + Bold: true, + Italic: true, + Underline: "single", + Family: "Times New Roman", + Size: 14, + Color: "777777", + }, + Text: "C1=A1+B1", + }, + }, + })) + assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ + Cell: "A5", + Type: FormControlRadio, + Text: "Option Button 1", + Checked: true, + })) + assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ + Cell: "A6", + Type: FormControlRadio, + Text: "Option Button 2", + })) + assert.NoError(t, f.SetSheetProps("Sheet1", &SheetPropsOptions{CodeName: stringPtr("Sheet1")})) + file, err := os.ReadFile(filepath.Join("test", "vbaProject.bin")) + assert.NoError(t, err) + assert.NoError(t, f.AddVBAProject(file)) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddFormControl.xlsm"))) + assert.NoError(t, f.Close()) + f, err = OpenFile(filepath.Join("test", "TestAddFormControl.xlsm")) + assert.NoError(t, err) + assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ + Cell: "D4", + Type: FormControlButton, + Macro: "Button1_Click", + Text: "Button 2", + })) + // Test add unsupported form control + assert.Equal(t, f.AddFormControl("Sheet1", FormControl{ + Cell: "A1", + Type: 0x37, + Macro: "Button1_Click", + }), ErrParameterInvalid) + // Test add form control on not exists worksheet + assert.Equal(t, f.AddFormControl("SheetN", FormControl{ + Cell: "A1", + Type: FormControlButton, + Macro: "Button1_Click", + }), newNoExistSheetError("SheetN")) + assert.NoError(t, f.Close()) +} diff --git a/xmlComments.go b/xmlComments.go index 214c15e743..c27cd70e4c 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 - AuthorID int - Cell string - Text string - Runs []RichTextRun + Author string + AuthorID int + Cell string + Text string + Paragraph []RichTextRun } diff --git a/xmlDrawing.go b/xmlDrawing.go index dae3bcc3e5..affa039049 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -606,6 +606,7 @@ type GraphicOptions struct { // Shape directly maps the format settings of the shape. type Shape struct { + Cell string Type string Macro string Width uint From b667987084c8054b7dcf2f8d2284ac1ae32e9750 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 13 Jul 2023 00:03:24 +0800 Subject: [PATCH 769/957] This closes #301, support delete and add radio button form control - New exported function `DeleteFormControl` has been added - Update unit tests - Fix comments was missing after form control added - Update pull request templates --- PULL_REQUEST_TEMPLATE.md | 2 +- vml.go | 240 ++++++++++++++++++++++++++------------- vmlDrawing.go | 14 +++ vml_test.go | 35 +++++- 4 files changed, 212 insertions(+), 79 deletions(-) diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md index d2ac755e96..8479a73206 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -21,7 +21,7 @@ - + ## Types of changes diff --git a/vml.go b/vml.go index ac68d1011e..02d9e19bc7 100644 --- a/vml.go +++ b/vml.go @@ -28,6 +28,7 @@ type FormControlType byte const ( FormControlNote FormControlType = iota FormControlButton + FormControlCheckbox FormControlRadio ) @@ -114,8 +115,9 @@ func (f *File) AddComment(sheet string, opts Comment) error { }) } -// DeleteComment provides the method to delete comment in a sheet by given -// worksheet name. For example, delete the comment in Sheet1!$A$30: +// DeleteComment provides the method to delete comment in a worksheet by given +// worksheet name and cell reference. For example, delete the comment in +// Sheet1!$A$30: // // err := f.DeleteComment("Sheet1", "A30") func (f *File) DeleteComment(sheet, cell string) error { @@ -315,6 +317,80 @@ func (f *File) AddFormControl(sheet string, opts FormControl) error { }) } +// DeleteFormControl provides the method to delete form control in a worksheet +// by given worksheet name and cell reference. For example, delete the form +// control in Sheet1!$A$30: +// +// err := f.DeleteFormControl("Sheet1", "A30") +func (f *File) DeleteFormControl(sheet, cell string) error { + ws, err := f.workSheetReader(sheet) + if err != nil { + return err + } + col, row, err := CellNameToCoordinates(cell) + if err != nil { + return err + } + if ws.LegacyDrawing == nil { + return err + } + sheetRelationshipsDrawingVML := f.getSheetRelationshipsTargetByID(sheet, ws.LegacyDrawing.RID) + vmlID, _ := strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(sheetRelationshipsDrawingVML, "../drawings/vmlDrawing"), ".vml")) + drawingVML := strings.ReplaceAll(sheetRelationshipsDrawingVML, "..", "xl") + vml := f.VMLDrawing[drawingVML] + if vml == nil { + vml = &vmlDrawing{ + XMLNSv: "urn:schemas-microsoft-com:vml", + XMLNSo: "urn:schemas-microsoft-com:office:office", + XMLNSx: "urn:schemas-microsoft-com:office:excel", + XMLNSmv: "http://macVmlSchemaUri", + ShapeLayout: &xlsxShapeLayout{ + Ext: "edit", IDmap: &xlsxIDmap{Ext: "edit", Data: vmlID}, + }, + ShapeType: &xlsxShapeType{ + Stroke: &xlsxStroke{JoinStyle: "miter"}, + VPath: &vPath{GradientShapeOK: "t", ConnectType: "rect"}, + }, + } + // load exist VML shapes from xl/drawings/vmlDrawing%d.vml + d, err := f.decodeVMLDrawingReader(drawingVML) + if err != nil { + return err + } + if d != nil { + vml.ShapeType.ID = d.ShapeType.ID + vml.ShapeType.CoordSize = d.ShapeType.CoordSize + vml.ShapeType.Spt = d.ShapeType.Spt + vml.ShapeType.Path = d.ShapeType.Path + for _, v := range d.Shape { + s := xlsxShape{ + ID: v.ID, + Type: v.Type, + Style: v.Style, + Button: v.Button, + Filled: v.Filled, + FillColor: v.FillColor, + InsetMode: v.InsetMode, + Stroked: v.Stroked, + StrokeColor: v.StrokeColor, + Val: v.Val, + } + vml.Shape = append(vml.Shape, s) + } + } + } + for i, sp := range vml.Shape { + var shapeVal decodeShapeVal + if err = xml.Unmarshal([]byte(fmt.Sprintf("%s", sp.Val)), &shapeVal); err == nil && + shapeVal.ClientData.ObjectType != "Note" && shapeVal.ClientData.Column == col-1 && shapeVal.ClientData.Row == row-1 { + vml.Shape = append(vml.Shape[:i], vml.Shape[i+1:]...) + break + } + } + f.VMLDrawing[drawingVML] = vml + return err +} + // countVMLDrawing provides a function to get VML drawing files count storage // in the folder xl/drawings. func (f *File) countVMLDrawing() int { @@ -380,6 +456,8 @@ func (f *File) addVMLObject(opts vmlOptions) error { } drawingVML := "xl/drawings/vmlDrawing" + strconv.Itoa(vmlID) + ".vml" sheetRelationshipsDrawingVML := "../drawings/vmlDrawing" + strconv.Itoa(vmlID) + ".vml" + sheetXMLPath, _ := f.getSheetXMLPath(opts.Sheet) + sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetXMLPath, "xl/worksheets/") + ".rels" if ws.LegacyDrawing != nil { // The worksheet already has a VML relationships, use the relationships drawing ../drawings/vmlDrawing%d.vml. sheetRelationshipsDrawingVML = f.getSheetRelationshipsTargetByID(opts.Sheet, ws.LegacyDrawing.RID) @@ -387,13 +465,7 @@ func (f *File) addVMLObject(opts vmlOptions) error { drawingVML = strings.ReplaceAll(sheetRelationshipsDrawingVML, "..", "xl") } else { // Add first VML drawing for given sheet. - sheetXMLPath, _ := f.getSheetXMLPath(opts.Sheet) - sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetXMLPath, "xl/worksheets/") + ".rels" rID := f.addRels(sheetRels, SourceRelationshipDrawingVML, sheetRelationshipsDrawingVML, "") - if !opts.FormCtrl { - sheetRelationshipsComments := "../comments" + strconv.Itoa(vmlID) + ".xml" - f.addRels(sheetRels, SourceRelationshipComments, sheetRelationshipsComments, "") - } f.addSheetNameSpace(opts.Sheet, SourceRelationship) f.addSheetLegacyDrawing(opts.Sheet, rID) } @@ -405,6 +477,10 @@ func (f *File) addVMLObject(opts vmlOptions) error { if err = f.addComment(commentsXML, opts); err != nil { return err } + if sheetXMLPath, ok := f.getSheetXMLPath(opts.Sheet); ok && f.getSheetComments(filepath.Base(sheetXMLPath)) == "" { + sheetRelationshipsComments := "../comments" + strconv.Itoa(vmlID) + ".xml" + f.addRels(sheetRels, SourceRelationshipComments, sheetRelationshipsComments, "") + } } return f.addContentTypePart(vmlID, "comments") } @@ -475,75 +551,87 @@ func formCtrlText(opts *vmlOptions) []vmlFont { return font } -var ( - formCtrlPresets = map[FormControlType]struct { - objectType string - filled string - fillColor string - stroked string - strokeColor string - strokeButton string - fill *vFill - textHAlign string - textVAlign string - noThreeD *string - firstButton *string - shadow *vShadow - }{ - FormControlNote: { - objectType: "Note", - filled: "", - fillColor: "#FBF6D6", - stroked: "", - strokeColor: "#EDEAA1", - strokeButton: "", - fill: &vFill{ - Color2: "#FBFE82", - Angle: -180, - Type: "gradient", - Fill: &oFill{Ext: "view", Type: "gradientUnscaled"}, - }, - textHAlign: "", - textVAlign: "", - noThreeD: nil, - firstButton: nil, - shadow: &vShadow{On: "t", Color: "black", Obscured: "t"}, - }, - FormControlButton: { - objectType: "Button", - filled: "", - fillColor: "buttonFace [67]", - stroked: "", - strokeColor: "windowText [64]", - strokeButton: "t", - fill: &vFill{ - Color2: "buttonFace [67]", - Angle: -180, - Type: "gradient", - Fill: &oFill{Ext: "view", Type: "gradientUnscaled"}, - }, - textHAlign: "Center", - textVAlign: "Center", - noThreeD: nil, - firstButton: nil, - shadow: nil, +var formCtrlPresets = map[FormControlType]struct { + objectType string + filled string + fillColor string + stroked string + strokeColor string + strokeButton string + fill *vFill + textHAlign string + textVAlign string + noThreeD *string + firstButton *string + shadow *vShadow +}{ + FormControlNote: { + objectType: "Note", + filled: "", + fillColor: "#FBF6D6", + stroked: "", + strokeColor: "#EDEAA1", + strokeButton: "", + fill: &vFill{ + Color2: "#FBFE82", + Angle: -180, + Type: "gradient", + Fill: &oFill{Ext: "view", Type: "gradientUnscaled"}, }, - FormControlRadio: { - objectType: "Radio", - filled: "f", - fillColor: "window [65]", - stroked: "f", - strokeColor: "windowText [64]", - strokeButton: "", - fill: nil, - textHAlign: "", - textVAlign: "Center", - noThreeD: stringPtr(""), - firstButton: stringPtr(""), - shadow: nil, + textHAlign: "", + textVAlign: "", + noThreeD: nil, + firstButton: nil, + shadow: &vShadow{On: "t", Color: "black", Obscured: "t"}, + }, + FormControlButton: { + objectType: "Button", + filled: "", + fillColor: "buttonFace [67]", + stroked: "", + strokeColor: "windowText [64]", + strokeButton: "t", + fill: &vFill{ + Color2: "buttonFace [67]", + Angle: -180, + Type: "gradient", + Fill: &oFill{Ext: "view", Type: "gradientUnscaled"}, }, - } -) + textHAlign: "Center", + textVAlign: "Center", + noThreeD: nil, + firstButton: nil, + shadow: nil, + }, + FormControlCheckbox: { + objectType: "Checkbox", + filled: "f", + fillColor: "window [65]", + stroked: "f", + strokeColor: "windowText [64]", + strokeButton: "", + fill: nil, + textHAlign: "", + textVAlign: "Center", + noThreeD: stringPtr(""), + firstButton: nil, + shadow: nil, + }, + FormControlRadio: { + objectType: "Radio", + filled: "f", + fillColor: "window [65]", + stroked: "f", + strokeColor: "windowText [64]", + strokeButton: "", + fill: nil, + textHAlign: "", + textVAlign: "Center", + noThreeD: stringPtr(""), + firstButton: stringPtr(""), + shadow: nil, + }, +} // addDrawingVML provides a function to create VML drawing XML as // xl/drawings/vmlDrawing%d.vml by given data ID, XML path and VML options. The @@ -634,7 +722,7 @@ func (f *File) addDrawingVML(dataID int, drawingVML string, opts *vmlOptions) er if opts.FormCtrl { sp.ClientData.FmlaMacro = opts.Macro } - if opts.Type == FormControlRadio && opts.Checked { + if (opts.Type == FormControlCheckbox || opts.Type == FormControlRadio) && opts.Checked { sp.ClientData.Checked = stringPtr("1") } s, _ := xml.Marshal(sp) diff --git a/vmlDrawing.go b/vmlDrawing.go index 89f91d9278..eae224a37b 100644 --- a/vmlDrawing.go +++ b/vmlDrawing.go @@ -172,6 +172,20 @@ type decodeShape struct { Val string `xml:",innerxml"` } +// decodeShapeVal defines the structure used to parse the sub-element of the +// shape in the file xl/drawings/vmlDrawing%d.vml. +type decodeShapeVal struct { + ClientData decodeVMLClientData `xml:"ClientData"` +} + +// decodeVMLClientData defines the structure used to parse the x:ClientData +// element in the file xl/drawings/vmlDrawing%d.vml. +type decodeVMLClientData struct { + ObjectType string `xml:"ObjectType,attr"` + Column int + Row int +} + // encodeShape defines the structure used to re-serialization shape element. type encodeShape struct { Fill *vFill `xml:"v:fill"` diff --git a/vml_test.go b/vml_test.go index 47ec33d17c..09262ec8c9 100644 --- a/vml_test.go +++ b/vml_test.go @@ -155,7 +155,7 @@ func TestAddDrawingVML(t *testing.T) { assert.EqualError(t, f.addDrawingVML(0, "xl/drawings/vmlDrawing1.vml", &vmlOptions{Cell: "A1"}), "XML syntax error on line 1: invalid UTF-8") } -func TestAddFormControl(t *testing.T) { +func TestFormControl(t *testing.T) { f := NewFile() assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ Cell: "D1", @@ -185,12 +185,23 @@ func TestAddFormControl(t *testing.T) { })) assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ Cell: "A5", + Type: FormControlCheckbox, + Text: "Check Box 1", + Checked: true, + })) + assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ + Cell: "A6", + Type: FormControlCheckbox, + Text: "Check Box 2", + })) + assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ + Cell: "A7", Type: FormControlRadio, Text: "Option Button 1", Checked: true, })) assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ - Cell: "A6", + Cell: "A8", Type: FormControlRadio, Text: "Option Button 2", })) @@ -221,4 +232,24 @@ func TestAddFormControl(t *testing.T) { Macro: "Button1_Click", }), newNoExistSheetError("SheetN")) assert.NoError(t, f.Close()) + // Test delete form control + f, err = OpenFile(filepath.Join("test", "TestAddFormControl.xlsm")) + assert.NoError(t, err) + assert.NoError(t, f.DeleteFormControl("Sheet1", "D1")) + assert.NoError(t, f.DeleteFormControl("Sheet1", "A1")) + // Test delete form control on not exists worksheet + assert.Equal(t, f.DeleteFormControl("SheetN", "A1"), newNoExistSheetError("SheetN")) + // Test delete form control on not exists worksheet + assert.Equal(t, f.DeleteFormControl("Sheet1", "A"), newCellNameToCoordinatesError("A", newInvalidCellNameError("A"))) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDeleteFormControl.xlsm"))) + assert.NoError(t, f.Close()) + // Test delete form control with expected element + f, err = OpenFile(filepath.Join("test", "TestAddFormControl.xlsm")) + assert.NoError(t, err) + f.Pkg.Store("xl/drawings/vmlDrawing1.vml", MacintoshCyrillicCharset) + assert.Error(t, f.DeleteFormControl("Sheet1", "A1"), "XML syntax error on line 1: invalid UTF-8") + assert.NoError(t, f.Close()) + // Test delete form control on a worksheet without form control + f = NewFile() + assert.NoError(t, f.DeleteFormControl("Sheet1", "A1")) } From 8d996ca138d40069bce92ef8e155f7a268979045 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 21 Jul 2023 00:03:37 +0800 Subject: [PATCH 770/957] This closes #1582, fixes the formula calculation bug, and improves form controls - Fix incorrect formula calculate results on a nested argument function which returns a numeric result - Add a new exported error variable `ErrorFormControlValue` - Rename exported enumeration `FormControlCheckbox` to `FormControlCheckBox` - Rename exported enumeration `FormControlRadio` to `FormControlOptionButton` - The `AddFormControl` function supports new 5 form controls: spin button, check box, group box, label, and scroll bar - Update documentation for the `GraphicOptions` data type, `AddFormControl` and `NewStreamWriter` functions - Update the unit tests --- calc.go | 16 +-- calc_test.go | 17 +++ errors.go | 3 + picture.go | 56 +++++---- picture_test.go | 2 + stream.go | 18 +-- vml.go | 323 ++++++++++++++++++++++++++++++++++-------------- vmlDrawing.go | 93 +++++++++----- vml_test.go | 84 +++++++------ xmlDrawing.go | 4 + 10 files changed, 417 insertions(+), 199 deletions(-) diff --git a/calc.go b/calc.go index 3c449492c3..66493bfa97 100644 --- a/calc.go +++ b/calc.go @@ -1053,16 +1053,16 @@ func (f *File) evalInfixExpFunc(ctx *calcContext, sheet, cell string, token, nex if nextToken.TType == efp.TokenTypeOperatorInfix || (opftStack.Len() > 1 && opfdStack.Len() > 0) { // mathematics calculate in formula function opfdStack.Push(arg) - } else { - argsStack.Peek().(*list.List).PushBack(arg) - } - } else { - val := arg.Value() - if arg.Type == ArgMatrix && len(arg.Matrix) > 0 && len(arg.Matrix[0]) > 0 { - val = arg.Matrix[0][0].Value() + return newEmptyFormulaArg() } - opdStack.Push(newStringFormulaArg(val)) + argsStack.Peek().(*list.List).PushBack(arg) + return newEmptyFormulaArg() + } + if arg.Type == ArgMatrix && len(arg.Matrix) > 0 && len(arg.Matrix[0]) > 0 { + opdStack.Push(arg.Matrix[0][0]) + return newEmptyFormulaArg() } + opdStack.Push(arg) return newEmptyFormulaArg() } diff --git a/calc_test.go b/calc_test.go index 24c6efa3e8..702aeb4d88 100644 --- a/calc_test.go +++ b/calc_test.go @@ -5918,6 +5918,23 @@ func TestCalcCellResolver(t *testing.T) { assert.NoError(t, err, formula) assert.Equal(t, expected, result, formula) } + // Test calculates formula that contains a nested argument function which returns a numeric result + f = NewFile() + for _, cell := range []string{"A1", "B2", "B3", "B4"} { + assert.NoError(t, f.SetCellValue("Sheet1", cell, "ABC")) + } + for cell, formula := range map[string]string{ + "A2": "IF(B2<>\"\",MAX(A1:A1)+1,\"\")", + "A3": "IF(B3<>\"\",MAX(A1:A2)+1,\"\")", + "A4": "IF(B4<>\"\",MAX(A1:A3)+1,\"\")", + } { + assert.NoError(t, f.SetCellFormula("Sheet1", cell, formula)) + } + for cell, expected := range map[string]string{"A2": "1", "A3": "2", "A4": "3"} { + result, err := f.CalcCellValue("Sheet1", cell) + assert.NoError(t, err) + assert.Equal(t, expected, result) + } } func TestEvalInfixExp(t *testing.T) { diff --git a/errors.go b/errors.go index b8d2022f72..254890efa8 100644 --- a/errors.go +++ b/errors.go @@ -255,4 +255,7 @@ var ( // ErrUnprotectWorkbookPassword defined the error message on remove workbook // protection with password verification failed. ErrUnprotectWorkbookPassword = errors.New("workbook protect password not match") + // ErrorFormControlValue defined the error message for receiving a scroll + // value exceeds limit. + ErrorFormControlValue = fmt.Errorf("scroll value must be between 0 and %d", MaxFormControlValue) ) diff --git a/picture.go b/picture.go index fb14c951b2..9b026f567d 100644 --- a/picture.go +++ b/picture.go @@ -110,41 +110,46 @@ func parseGraphicOptions(opts *GraphicOptions) *GraphicOptions { // } // } // -// The optional parameter "AutoFit" specifies if you make image size auto-fits the -// cell, the default value of that is 'false'. +// The optional parameter "AltText" is used to add alternative text to a graph +// object. // -// The optional parameter "Hyperlink" specifies the hyperlink of the image. +// The optional parameter "PrintObject" indicates whether the graph object is +// printed when the worksheet is printed, the default value of that is 'true'. // -// 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 "HyperlinkType" is "Location", -// coordinates need to start with "#". +// The optional parameter "Locked" indicates whether lock the graph object. +// Locking an object has no effect unless the sheet is protected. // -// 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 "LockAspectRatio" indicates whether lock aspect ratio +// for the graph object, the default value of that is 'false'. // -// 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 "AutoFit" specifies if you make graph object size +// auto-fits the cell, the default value of that is 'false'. // -// The optional parameter "LockAspectRatio" indicates whether lock aspect -// ratio for the image, the default value of that is 'false'. +// The optional parameter "OffsetX" specifies the horizontal offset of the graph +// object with the cell, the default value of that is 0. // -// The optional parameter "Locked" indicates whether lock the image. Locking -// an object has no effect unless the sheet is protected. +// The optional parameter "OffsetY" specifies the vertical offset of the graph +// object with the cell, the default value of that is 0. // -// The optional parameter "OffsetX" specifies the horizontal offset of the -// image with the cell, the default value of that is 0. +// The optional parameter "ScaleX" specifies the horizontal scale of graph +// object, the default value of that is 1.0 which presents 100%. // -// The optional parameter "ScaleX" specifies the horizontal scale of images, +// The optional parameter "ScaleY" specifies the vertical scale of graph object, // 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 "Hyperlink" specifies the hyperlink of the graph +// object. // -// The optional parameter "ScaleY" specifies the vertical scale of images, -// the default value of that is 1.0 which presents 100%. +// 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 "HyperlinkType" is "Location", +// coordinates need to start with "#". +// +// The optional parameter "Positioning" defines 3 types of the position of a +// graph object in a spreadsheet: "oneCell" (Move but don't size with +// cells), "twoCell" (Move and size with cells), and "absolute" (Don't move or +// size with cells). If you don't set this parameter, the default positioning +// is to move and size with cells. func (f *File) AddPicture(sheet, cell, name string, opts *GraphicOptions) error { var err error // Check picture exists first. @@ -330,6 +335,9 @@ func (f *File) addDrawingPicture(sheet, drawingXML, cell, ext string, rID, hyper if err != nil { return err } + if opts.Positioning != "" && inStrSlice(supportedPositioning, opts.Positioning, true) == -1 { + return ErrParameterInvalid + } width, height := img.Width, img.Height if opts.AutoFit { if width, height, col, row, err = f.drawingResize(sheet, cell, float64(width), float64(height), opts); err != nil { diff --git a/picture_test.go b/picture_test.go index a2e0fb726c..c3c0d6e0d9 100644 --- a/picture_test.go +++ b/picture_test.go @@ -192,6 +192,8 @@ func TestAddDrawingPicture(t *testing.T) { 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()) + // Test addDrawingPicture with invalid positioning types + assert.Equal(t, f.addDrawingPicture("sheet1", "", "A1", "", 0, 0, image.Config{}, &GraphicOptions{Positioning: "x"}), ErrParameterInvalid) path := "xl/drawings/drawing1.xml" f.Pkg.Store(path, MacintoshCyrillicCharset) diff --git a/stream.go b/stream.go index 82d7129630..13a14d8561 100644 --- a/stream.go +++ b/stream.go @@ -38,14 +38,16 @@ type StreamWriter struct { tableParts string } -// 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 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 -// with numbers and style: +// NewStreamWriter returns stream writer struct by given worksheet name used for +// writing data on a new existing empty worksheet with large amounts of data. +// Note that after writing data with the stream writer for the worksheet, you +// must call the 'Flush' method to end the streaming writing process, ensure +// that the order of row numbers is ascending when set rows, and the normal +// mode functions and stream mode functions can not be work mixed to writing +// data on the worksheets. The stream writer will try to use temporary files on +// disk to reduce the memory usage when in-memory chunks data over 16MB, and +// you can't get cell value at this time. For example, set data for worksheet +// of size 102400 rows x 50 columns with numbers and style: // // f := excelize.NewFile() // defer func() { diff --git a/vml.go b/vml.go index 02d9e19bc7..cbe07e2119 100644 --- a/vml.go +++ b/vml.go @@ -28,8 +28,12 @@ type FormControlType byte const ( FormControlNote FormControlType = iota FormControlButton - FormControlCheckbox - FormControlRadio + FormControlOptionButton + FormControlSpinButton + FormControlCheckBox + FormControlGroupBox + FormControlLabel + FormControlScrollBar ) // GetComments retrieves all comments in a worksheet by given worksheet name. @@ -105,13 +109,8 @@ func (f *File) getSheetComments(sheetFile string) string { // }) func (f *File) AddComment(sheet string, opts Comment) error { return f.addVMLObject(vmlOptions{ - Sheet: sheet, - Author: opts.Author, - AuthorID: opts.AuthorID, - Cell: opts.Cell, - Text: opts.Text, - Type: FormControlNote, - Paragraph: opts.Paragraph, + sheet: sheet, Comment: opts, + FormControl: FormControl{Cell: opts.Cell, Type: FormControlNote}, }) } @@ -184,18 +183,18 @@ func (f *File) addComment(commentsXML string, opts vmlOptions) error { return err } chars, cmt := 0, xlsxComment{ - Ref: opts.Cell, + Ref: opts.Comment.Cell, AuthorID: authorID, Text: xlsxText{R: []xlsxR{}}, } - if opts.Text != "" { - if len(opts.Text) > TotalCellChars { - opts.Text = opts.Text[:TotalCellChars] + if opts.Comment.Text != "" { + if len(opts.Comment.Text) > TotalCellChars { + opts.Comment.Text = opts.Comment.Text[:TotalCellChars] } - cmt.Text.T = stringPtr(opts.Text) - chars += len(opts.Text) + cmt.Text.T = stringPtr(opts.Comment.Text) + chars += len(opts.Comment.Text) } - for _, run := range opts.Paragraph { + for _, run := range opts.Comment.Paragraph { if chars == TotalCellChars { break } @@ -277,9 +276,15 @@ func (f *File) commentsWriter() { // AddFormControl provides the method to add form control button in a worksheet // by given worksheet name and form control options. Supported form control -// type: button and radio. The file extension should be XLSM or XLTM. For -// example: +// type: button, check box, group box, label, option button, scroll bar and +// spinner. If set macro for the form control, the workbook extension should be +// XLSM or XLTM. Scroll value must be between 0 and 30000. // +// Example 1, add button form control with macro, rich-text, custom button size, +// print property on Sheet1!A1, and let the button do not move or size with +// cells: +// +// enable := true // err := f.AddFormControl("Sheet1", excelize.FormControl{ // Cell: "A1", // Type: excelize.FormControlButton, @@ -300,20 +305,51 @@ func (f *File) commentsWriter() { // Text: "C1=A1+B1", // }, // }, +// Format: excelize.GraphicOptions{ +// PrintObject: &enable, +// Positioning: "absolute", +// }, +// }) +// +// Example 2, add option button form control with checked status and text on +// Sheet1!A1: +// +// err := f.AddFormControl("Sheet1", excelize.FormControl{ +// Cell: "A1", +// Type: excelize.FormControlOptionButton, +// Text: "Option Button 1", +// Checked: true, +// }) +// +// Example 3, add spin button form control on Sheet1!A2 to increase or decrease +// the value of Sheet1!A1: +// +// err := f.AddFormControl("Sheet1", excelize.FormControl{ +// Cell: "A2", +// Type: excelize.FormControlSpinButton, +// CurrentVal: 7, +// MinVal: 5, +// MaxVal: 10, +// IncChange: 1, +// CellLink: "A1", +// }) +// +// Example 4, add horizontally scroll bar form control on Sheet1!A2 to change +// the value of Sheet1!A1 by click the scroll arrows or drag the scroll box: +// +// err := f.AddFormControl("Sheet1", excelize.FormControl{ +// Cell: "A2", +// Type: excelize.FormControlScrollBar, +// CurrentVal: 50, +// MinVal: 10, +// MaxVal: 100, +// IncChange: 1, +// PageChange: 1, +// CellLink: "A1", // }) func (f *File) AddFormControl(sheet string, opts FormControl) error { return f.addVMLObject(vmlOptions{ - FormCtrl: true, - Sheet: sheet, - Type: opts.Type, - Checked: opts.Checked, - Cell: opts.Cell, - Macro: opts.Macro, - Width: opts.Width, - Height: opts.Height, - Format: opts.Format, - Text: opts.Text, - Paragraph: opts.Paragraph, + formCtrl: true, sheet: sheet, FormControl: opts, }) } @@ -443,41 +479,41 @@ func (f *File) vmlDrawingWriter() { // relationships for comments and form controls. func (f *File) addVMLObject(opts vmlOptions) error { // Read sheet data. - ws, err := f.workSheetReader(opts.Sheet) + ws, err := f.workSheetReader(opts.sheet) if err != nil { return err } vmlID := f.countComments() + 1 - if opts.FormCtrl { - if opts.Type > FormControlRadio { + if opts.formCtrl { + if opts.Type > FormControlScrollBar { return ErrParameterInvalid } vmlID = f.countVMLDrawing() + 1 } drawingVML := "xl/drawings/vmlDrawing" + strconv.Itoa(vmlID) + ".vml" sheetRelationshipsDrawingVML := "../drawings/vmlDrawing" + strconv.Itoa(vmlID) + ".vml" - sheetXMLPath, _ := f.getSheetXMLPath(opts.Sheet) + sheetXMLPath, _ := f.getSheetXMLPath(opts.sheet) sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetXMLPath, "xl/worksheets/") + ".rels" if ws.LegacyDrawing != nil { // The worksheet already has a VML relationships, use the relationships drawing ../drawings/vmlDrawing%d.vml. - sheetRelationshipsDrawingVML = f.getSheetRelationshipsTargetByID(opts.Sheet, ws.LegacyDrawing.RID) + sheetRelationshipsDrawingVML = f.getSheetRelationshipsTargetByID(opts.sheet, ws.LegacyDrawing.RID) vmlID, _ = strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(sheetRelationshipsDrawingVML, "../drawings/vmlDrawing"), ".vml")) drawingVML = strings.ReplaceAll(sheetRelationshipsDrawingVML, "..", "xl") } else { // Add first VML drawing for given sheet. rID := f.addRels(sheetRels, SourceRelationshipDrawingVML, sheetRelationshipsDrawingVML, "") - f.addSheetNameSpace(opts.Sheet, SourceRelationship) - f.addSheetLegacyDrawing(opts.Sheet, rID) + f.addSheetNameSpace(opts.sheet, SourceRelationship) + f.addSheetLegacyDrawing(opts.sheet, rID) } if err = f.addDrawingVML(vmlID, drawingVML, prepareFormCtrlOptions(&opts)); err != nil { return err } - if !opts.FormCtrl { + if !opts.formCtrl { commentsXML := "xl/comments" + strconv.Itoa(vmlID) + ".xml" if err = f.addComment(commentsXML, opts); err != nil { return err } - if sheetXMLPath, ok := f.getSheetXMLPath(opts.Sheet); ok && f.getSheetComments(filepath.Base(sheetXMLPath)) == "" { + if sheetXMLPath, ok := f.getSheetXMLPath(opts.sheet); ok && f.getSheetComments(filepath.Base(sheetXMLPath)) == "" { sheetRelationshipsComments := "../comments" + strconv.Itoa(vmlID) + ".xml" f.addRels(sheetRels, SourceRelationshipComments, sheetRelationshipsComments, "") } @@ -488,7 +524,7 @@ func (f *File) addVMLObject(opts vmlOptions) error { // prepareFormCtrlOptions provides a function to parse the format settings of // the form control with default value. func prepareFormCtrlOptions(opts *vmlOptions) *vmlOptions { - for _, runs := range opts.Paragraph { + for _, runs := range opts.FormControl.Paragraph { for _, subStr := range strings.Split(runs.Text, "\n") { opts.rows++ if chars := len(subStr); chars > opts.cols { @@ -496,8 +532,8 @@ func prepareFormCtrlOptions(opts *vmlOptions) *vmlOptions { } } } - if len(opts.Paragraph) == 0 { - opts.rows, opts.cols = 1, len(opts.Text) + if len(opts.FormControl.Paragraph) == 0 { + opts.rows, opts.cols = 1, len(opts.FormControl.Text) } if opts.Format.ScaleX == 0 { opts.Format.ScaleX = 1 @@ -520,10 +556,10 @@ func prepareFormCtrlOptions(opts *vmlOptions) *vmlOptions { // formCtrlText returns font element in the VML for control form text. func formCtrlText(opts *vmlOptions) []vmlFont { var font []vmlFont - if opts.Text != "" { - font = append(font, vmlFont{Content: opts.Text}) + if opts.FormControl.Text != "" { + font = append(font, vmlFont{Content: opts.FormControl.Text}) } - for _, run := range opts.Paragraph { + for _, run := range opts.FormControl.Paragraph { fnt := vmlFont{ Content: run.Text + "

\r\n", } @@ -551,22 +587,10 @@ func formCtrlText(opts *vmlOptions) []vmlFont { return font } -var formCtrlPresets = map[FormControlType]struct { - objectType string - filled string - fillColor string - stroked string - strokeColor string - strokeButton string - fill *vFill - textHAlign string - textVAlign string - noThreeD *string - firstButton *string - shadow *vShadow -}{ +var formCtrlPresets = map[FormControlType]formCtrlPreset{ FormControlNote: { objectType: "Note", + autoFill: "True", filled: "", fillColor: "#FBF6D6", stroked: "", @@ -586,6 +610,7 @@ var formCtrlPresets = map[FormControlType]struct { }, FormControlButton: { objectType: "Button", + autoFill: "True", filled: "", fillColor: "buttonFace [67]", stroked: "", @@ -603,8 +628,9 @@ var formCtrlPresets = map[FormControlType]struct { firstButton: nil, shadow: nil, }, - FormControlCheckbox: { + FormControlCheckBox: { objectType: "Checkbox", + autoFill: "True", filled: "f", fillColor: "window [65]", stroked: "f", @@ -617,8 +643,39 @@ var formCtrlPresets = map[FormControlType]struct { firstButton: nil, shadow: nil, }, - FormControlRadio: { + FormControlGroupBox: { + objectType: "GBox", + autoFill: "False", + filled: "f", + fillColor: "", + stroked: "f", + strokeColor: "windowText [64]", + strokeButton: "", + fill: nil, + textHAlign: "", + textVAlign: "", + noThreeD: stringPtr(""), + firstButton: nil, + shadow: nil, + }, + FormControlLabel: { + objectType: "Label", + autoFill: "False", + filled: "f", + fillColor: "window [65]", + stroked: "f", + strokeColor: "windowText [64]", + strokeButton: "", + fill: nil, + textHAlign: "", + textVAlign: "", + noThreeD: nil, + firstButton: nil, + shadow: nil, + }, + FormControlOptionButton: { objectType: "Radio", + autoFill: "False", filled: "f", fillColor: "window [65]", stroked: "f", @@ -631,6 +688,116 @@ var formCtrlPresets = map[FormControlType]struct { firstButton: stringPtr(""), shadow: nil, }, + FormControlScrollBar: { + objectType: "Scroll", + autoFill: "", + filled: "", + fillColor: "", + stroked: "f", + strokeColor: "windowText [64]", + strokeButton: "", + fill: nil, + textHAlign: "", + textVAlign: "", + noThreeD: nil, + firstButton: nil, + shadow: nil, + }, + FormControlSpinButton: { + objectType: "Spin", + autoFill: "False", + filled: "", + fillColor: "", + stroked: "f", + strokeColor: "windowText [64]", + strokeButton: "", + fill: nil, + textHAlign: "", + textVAlign: "", + noThreeD: nil, + firstButton: nil, + shadow: nil, + }, +} + +// addFormCtrl check and add scroll bar or spinner form control by given options. +func (sp *encodeShape) addFormCtrl(opts *vmlOptions) error { + if opts.Type != FormControlScrollBar && opts.Type != FormControlSpinButton { + return nil + } + if opts.CurrentVal > MaxFormControlValue || + opts.MinVal > MaxFormControlValue || + opts.MaxVal > MaxFormControlValue || + opts.IncChange > MaxFormControlValue || + opts.PageChange > MaxFormControlValue { + return ErrorFormControlValue + } + if opts.CellLink != "" { + if _, _, err := CellNameToCoordinates(opts.CellLink); err != nil { + return err + } + } + sp.ClientData.FmlaLink = opts.CellLink + sp.ClientData.Val = opts.CurrentVal + sp.ClientData.Min = opts.MinVal + sp.ClientData.Max = opts.MaxVal + sp.ClientData.Inc = opts.IncChange + sp.ClientData.Page = opts.PageChange + if opts.Type == FormControlScrollBar { + if opts.Horizontally { + sp.ClientData.Horiz = stringPtr("") + } + sp.ClientData.Dx = 15 + } + return nil +} + +// addFormCtrlShape returns a VML shape by given preset and options. +func (f *File) addFormCtrlShape(preset formCtrlPreset, col, row int, anchor string, opts *vmlOptions) (*encodeShape, error) { + sp := encodeShape{ + Fill: preset.fill, + Shadow: preset.shadow, + Path: &vPath{ConnectType: "none"}, + TextBox: &vTextBox{ + Style: "mso-direction-alt:auto", + Div: &xlsxDiv{Style: "text-align:left"}, + }, + ClientData: &xClientData{ + ObjectType: preset.objectType, + Anchor: anchor, + AutoFill: preset.autoFill, + Row: row - 1, + Column: col - 1, + TextHAlign: preset.textHAlign, + TextVAlign: preset.textVAlign, + NoThreeD: preset.noThreeD, + FirstButton: preset.firstButton, + }, + } + if opts.Format.PrintObject != nil && !*opts.Format.PrintObject { + sp.ClientData.PrintObject = "False" + } + if opts.Format.Positioning != "" { + idx := inStrSlice(supportedPositioning, opts.Format.Positioning, true) + if idx == -1 { + return &sp, ErrParameterInvalid + } + sp.ClientData.MoveWithCells = []*string{stringPtr(""), nil, nil}[idx] + sp.ClientData.SizeWithCells = []*string{stringPtr(""), stringPtr(""), nil}[idx] + } + if opts.FormControl.Type == FormControlNote { + sp.ClientData.MoveWithCells = stringPtr("") + sp.ClientData.SizeWithCells = stringPtr("") + } + if !opts.formCtrl { + return &sp, nil + } + sp.TextBox.Div.Font = formCtrlText(opts) + sp.ClientData.FmlaMacro = opts.Macro + if (opts.Type == FormControlCheckBox || opts.Type == FormControlOptionButton) && opts.Checked { + sp.ClientData.Checked = 1 + } + return &sp, sp.addFormCtrl(opts) } // addDrawingVML provides a function to create VML drawing XML as @@ -639,20 +806,18 @@ var formCtrlPresets = map[FormControlType]struct { // LeftOffset, TopRow, TopOffset, RightColumn, RightOffset, BottomRow, // BottomOffset. func (f *File) addDrawingVML(dataID int, drawingVML string, opts *vmlOptions) error { - col, row, err := CellNameToCoordinates(opts.Cell) + col, row, err := CellNameToCoordinates(opts.FormControl.Cell) if err != nil { return err } anchor := fmt.Sprintf("%d, 23, %d, 0, %d, %d, %d, 5", col, row, col+opts.rows+2, col+opts.cols-1, row+opts.rows+2) vmlID, vml, preset := 202, f.VMLDrawing[drawingVML], formCtrlPresets[opts.Type] style := "position:absolute;73.5pt;width:108pt;height:59.25pt;z-index:1;visibility:hidden" - var font []vmlFont - if opts.FormCtrl { + if opts.formCtrl { vmlID = 201 style = "position:absolute;73.5pt;width:108pt;height:59.25pt;z-index:1;mso-wrap-style:tight" - colStart, rowStart, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(opts.Sheet, col, row, opts.Format.OffsetX, opts.Format.OffsetY, int(opts.Width), int(opts.Height)) + colStart, rowStart, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(opts.sheet, col, row, opts.Format.OffsetX, opts.Format.OffsetY, int(opts.Width), int(opts.Height)) anchor = fmt.Sprintf("%d, 0, %d, 0, %d, %d, %d, %d", colStart, rowStart, colEnd, x2, rowEnd, y2) - font = formCtrlText(opts) } if vml == nil { vml = &vmlDrawing{ @@ -699,31 +864,9 @@ func (f *File) addDrawingVML(dataID int, drawingVML string, opts *vmlOptions) er } } } - sp := encodeShape{ - Fill: preset.fill, - Shadow: preset.shadow, - Path: &vPath{ConnectType: "none"}, - TextBox: &vTextBox{ - Style: "mso-direction-alt:auto", - Div: &xlsxDiv{Style: "text-align:left", Font: font}, - }, - ClientData: &xClientData{ - ObjectType: preset.objectType, - Anchor: anchor, - AutoFill: "True", - Row: row - 1, - Column: col - 1, - TextHAlign: preset.textHAlign, - TextVAlign: preset.textVAlign, - NoThreeD: preset.noThreeD, - FirstButton: preset.firstButton, - }, - } - if opts.FormCtrl { - sp.ClientData.FmlaMacro = opts.Macro - } - if (opts.Type == FormControlCheckbox || opts.Type == FormControlRadio) && opts.Checked { - sp.ClientData.Checked = stringPtr("1") + sp, err := f.addFormCtrlShape(preset, col, row, anchor, opts) + if err != nil { + return err } s, _ := xml.Marshal(sp) shape := xlsxShape{ diff --git a/vmlDrawing.go b/vmlDrawing.go index eae224a37b..d09054fa4b 100644 --- a/vmlDrawing.go +++ b/vmlDrawing.go @@ -48,7 +48,7 @@ type xlsxShape struct { Style string `xml:"style,attr"` Button string `xml:"o:button,attr,omitempty"` Filled string `xml:"filled,attr,omitempty"` - FillColor string `xml:"fillcolor,attr"` + FillColor string `xml:"fillcolor,attr,omitempty"` InsetMode string `xml:"urn:schemas-microsoft-com:office:office insetmode,attr,omitempty"` Stroked string `xml:"stroked,attr,omitempty"` StrokeColor string `xml:"strokecolor,attr,omitempty"` @@ -128,18 +128,28 @@ type vmlFont struct { // element. type xClientData struct { ObjectType string `xml:"ObjectType,attr"` - MoveWithCells string `xml:"x:MoveWithCells"` - SizeWithCells string `xml:"x:SizeWithCells"` + 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"` - Column int `xml:"x:Column"` + Locked string `xml:"x:Locked,omitempty"` + PrintObject string `xml:"x:PrintObject,omitempty"` + AutoFill string `xml:"x:AutoFill,omitempty"` FmlaMacro string `xml:"x:FmlaMacro,omitempty"` TextHAlign string `xml:"x:TextHAlign,omitempty"` TextVAlign string `xml:"x:TextVAlign,omitempty"` - Checked *string `xml:"x:Checked,omitempty"` - NoThreeD *string `xml:"x:NoThreeD,omitempty"` - FirstButton *string `xml:"x:FirstButton,omitempty"` + Row int `xml:"x:Row"` + Column int `xml:"x:Column"` + Checked int `xml:"x:Checked,omitempty"` + FmlaLink string `xml:"x:FmlaLink,omitempty"` + NoThreeD *string `xml:"x:NoThreeD"` + FirstButton *string `xml:"x:FirstButton"` + Val uint `xml:"x:Val,omitempty"` + Min uint `xml:"x:Min,omitempty"` + Max uint `xml:"x:Max,omitempty"` + Inc uint `xml:"x:Inc,omitempty"` + Page uint `xml:"x:Page,omitempty"` + Horiz *string `xml:"x:Horiz"` + Dx uint `xml:"x:Dx,omitempty"` } // decodeVmlDrawing defines the structure used to parse the file @@ -165,7 +175,7 @@ type decodeShape struct { Style string `xml:"style,attr"` Button string `xml:"button,attr,omitempty"` Filled string `xml:"filled,attr,omitempty"` - FillColor string `xml:"fillcolor,attr"` + FillColor string `xml:"fillcolor,attr,omitempty"` InsetMode string `xml:"urn:schemas-microsoft-com:office:office insetmode,attr,omitempty"` Stroked string `xml:"stroked,attr,omitempty"` StrokeColor string `xml:"strokecolor,attr,omitempty"` @@ -195,34 +205,49 @@ type encodeShape struct { ClientData *xClientData `xml:"x:ClientData"` } +// formCtrlPreset defines the structure used to form control presets. +type formCtrlPreset struct { + autoFill string + fill *vFill + fillColor string + filled string + firstButton *string + noThreeD *string + objectType string + shadow *vShadow + strokeButton string + strokeColor string + stroked string + textHAlign string + textVAlign string +} + // vmlOptions defines the structure used to internal comments and form controls. type vmlOptions struct { - rows int - cols int - FormCtrl bool - Sheet string - Author string - AuthorID int - Cell string - Checked bool - Text string - Macro string - Width uint - Height uint - Paragraph []RichTextRun - Type FormControlType - Format GraphicOptions + rows int + cols int + formCtrl bool + sheet string + Comment + FormControl } // FormControl directly maps the form controls information. type FormControl struct { - Cell string - Macro string - Width uint - Height uint - Checked bool - Text string - Paragraph []RichTextRun - Type FormControlType - Format GraphicOptions + Cell string + Macro string + Width uint + Height uint + Checked bool + CurrentVal uint + MinVal uint + MaxVal uint + IncChange uint + PageChange uint + Horizontally bool + CellLink string + Text string + Paragraph []RichTextRun + Type FormControlType + Format GraphicOptions } diff --git a/vml_test.go b/vml_test.go index 09262ec8c9..aba262d267 100644 --- a/vml_test.go +++ b/vml_test.go @@ -149,26 +149,20 @@ func TestCountComments(t *testing.T) { func TestAddDrawingVML(t *testing.T) { // Test addDrawingVML with illegal cell reference f := NewFile() - assert.EqualError(t, f.addDrawingVML(0, "", &vmlOptions{Cell: "*"}), newCellNameToCoordinatesError("*", newInvalidCellNameError("*")).Error()) + assert.Equal(t, f.addDrawingVML(0, "", &vmlOptions{FormControl: FormControl{Cell: "*"}}), newCellNameToCoordinatesError("*", newInvalidCellNameError("*"))) f.Pkg.Store("xl/drawings/vmlDrawing1.vml", MacintoshCyrillicCharset) - assert.EqualError(t, f.addDrawingVML(0, "xl/drawings/vmlDrawing1.vml", &vmlOptions{Cell: "A1"}), "XML syntax error on line 1: invalid UTF-8") + assert.EqualError(t, f.addDrawingVML(0, "xl/drawings/vmlDrawing1.vml", &vmlOptions{FormControl: FormControl{Cell: "A1"}}), "XML syntax error on line 1: invalid UTF-8") } func TestFormControl(t *testing.T) { f := NewFile() assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ - Cell: "D1", - Type: FormControlButton, - Macro: "Button1_Click", + Cell: "D1", Type: FormControlButton, Macro: "Button1_Click", })) assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ - Cell: "A1", - Type: FormControlButton, - Macro: "Button1_Click", - Width: 140, - Height: 60, - Text: "Button 1\r\n", + Cell: "A1", Type: FormControlButton, Macro: "Button1_Click", + Width: 140, Height: 60, Text: "Button 1\r\n", Paragraph: []RichTextRun{ { Font: &Font{ @@ -182,28 +176,42 @@ func TestFormControl(t *testing.T) { Text: "C1=A1+B1", }, }, + Format: GraphicOptions{PrintObject: boolPtr(true), Positioning: "absolute"}, })) assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ - Cell: "A5", - Type: FormControlCheckbox, - Text: "Check Box 1", - Checked: true, + Cell: "A5", Type: FormControlCheckBox, Text: "Check Box 1", + Checked: true, Format: GraphicOptions{ + PrintObject: boolPtr(false), Positioning: "oneCell", + }, + })) + assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ + Cell: "A6", Type: FormControlCheckBox, Text: "Check Box 2", + Format: GraphicOptions{Positioning: "twoCell"}, + })) + assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ + Cell: "A7", Type: FormControlOptionButton, Text: "Option Button 1", Checked: true, + })) + assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ + Cell: "A8", Type: FormControlOptionButton, Text: "Option Button 2", })) assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ - Cell: "A6", - Type: FormControlCheckbox, - Text: "Check Box 2", + Cell: "D3", Type: FormControlGroupBox, Text: "Group Box 1", + Width: 140, Height: 60, })) assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ - Cell: "A7", - Type: FormControlRadio, - Text: "Option Button 1", - Checked: true, + Cell: "A9", Type: FormControlLabel, Text: "Label 1", Width: 140, })) assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ - Cell: "A8", - Type: FormControlRadio, - Text: "Option Button 2", + Cell: "C5", Type: FormControlSpinButton, Width: 40, Height: 60, + CurrentVal: 7, MinVal: 5, MaxVal: 10, IncChange: 1, CellLink: "C2", + })) + assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ + Cell: "D7", Type: FormControlScrollBar, Width: 140, Height: 20, + CurrentVal: 50, MinVal: 10, MaxVal: 100, IncChange: 1, PageChange: 1, Horizontally: true, CellLink: "C3", + })) + assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ + Cell: "G1", Type: FormControlScrollBar, Width: 20, Height: 140, + CurrentVal: 50, MinVal: 1000, MaxVal: 100, IncChange: 1, PageChange: 1, CellLink: "C4", })) assert.NoError(t, f.SetSheetProps("Sheet1", &SheetPropsOptions{CodeName: stringPtr("Sheet1")})) file, err := os.ReadFile(filepath.Join("test", "vbaProject.bin")) @@ -214,23 +222,29 @@ func TestFormControl(t *testing.T) { f, err = OpenFile(filepath.Join("test", "TestAddFormControl.xlsm")) assert.NoError(t, err) assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ - Cell: "D4", - Type: FormControlButton, - Macro: "Button1_Click", - Text: "Button 2", + Cell: "D4", Type: FormControlButton, Macro: "Button1_Click", Text: "Button 2", })) // Test add unsupported form control assert.Equal(t, f.AddFormControl("Sheet1", FormControl{ - Cell: "A1", - Type: 0x37, - Macro: "Button1_Click", + Cell: "A1", Type: 0x37, Macro: "Button1_Click", }), ErrParameterInvalid) // Test add form control on not exists worksheet assert.Equal(t, f.AddFormControl("SheetN", FormControl{ - Cell: "A1", - Type: FormControlButton, - Macro: "Button1_Click", + Cell: "A1", Type: FormControlButton, Macro: "Button1_Click", }), newNoExistSheetError("SheetN")) + // Test add form control with invalid positioning types + assert.Equal(t, f.AddFormControl("Sheet1", FormControl{ + Cell: "A1", Type: FormControlButton, + Format: GraphicOptions{Positioning: "x"}, + }), ErrParameterInvalid) + // Test add spin form control with illegal cell link reference + assert.Equal(t, f.AddFormControl("Sheet1", FormControl{ + Cell: "C5", Type: FormControlSpinButton, CellLink: "*", + }), newCellNameToCoordinatesError("*", newInvalidCellNameError("*"))) + // Test add spin form control with invalid scroll value + assert.Equal(t, f.AddFormControl("Sheet1", FormControl{ + Cell: "C5", Type: FormControlSpinButton, CurrentVal: MaxFormControlValue + 1, + }), ErrorFormControlValue) assert.NoError(t, f.Close()) // Test delete form control f, err = OpenFile(filepath.Join("test", "TestAddFormControl.xlsm")) diff --git a/xmlDrawing.go b/xmlDrawing.go index affa039049..ba38655684 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -126,6 +126,7 @@ const ( MaxColumnWidth = 255 MaxFieldLength = 255 MaxFilePathLength = 207 + MaxFormControlValue = 30000 MaxFontFamilyLength = 31 MaxFontSize = 409 MaxRowHeight = 409 @@ -218,6 +219,9 @@ var supportedDrawingUnderlineTypes = []string{ "wavyDbl", } +// supportedPositioning defined supported positioning types. +var supportedPositioning = []string{"absolute", "oneCell", "twoCell"} + // 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 7f3d6636280418e0c3d778548fee9218890769e7 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 25 Jul 2023 00:08:24 +0800 Subject: [PATCH 771/957] This closes #1584, fix graphic object counter issues - Optimize number format parser - Update documentation for the `AddFormControl` function - Update unit tests - Upgrade dependencies package --- go.mod | 8 +++---- go.sum | 16 +++++++++----- picture.go | 11 ++++------ pivotTable_test.go | 6 ++--- shape.go | 8 +++---- vml.go | 55 +++++++++++++++++++++++----------------------- 6 files changed, 52 insertions(+), 52 deletions(-) diff --git a/go.mod b/go.mod index 58a3d71555..8a6347ec41 100644 --- a/go.mod +++ b/go.mod @@ -7,11 +7,11 @@ 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-20230503010013-3f38cdbb0b83 - golang.org/x/crypto v0.9.0 + github.com/xuri/nfp v0.0.0-20230723160540-a7d120392641 + golang.org/x/crypto v0.11.0 golang.org/x/image v0.5.0 - golang.org/x/net v0.10.0 - golang.org/x/text v0.9.0 + golang.org/x/net v0.12.0 + golang.org/x/text v0.11.0 ) require github.com/richardlehane/msoleps v1.0.3 // indirect diff --git a/go.sum b/go.sum index 35ed9c2945..7bc9eb62d0 100644 --- a/go.sum +++ b/go.sum @@ -17,13 +17,13 @@ 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-20230503010013-3f38cdbb0b83 h1:xVwnvkzzi+OiwhIkWOXvh1skFI6bagk8OvGuazM80Rw= -github.com/xuri/nfp v0.0.0-20230503010013-3f38cdbb0b83/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= +github.com/xuri/nfp v0.0.0-20230723160540-a7d120392641 h1:1SQuQwUorWlROdGAbsAJrMInj02yCUsYFNi/MzTJ6cA= +github.com/xuri/nfp v0.0.0-20230723160540-a7d120392641/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= -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/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= +golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= 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,9 @@ 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.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= +golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= 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= @@ -44,16 +45,19 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc 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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.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.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= 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.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/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 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/picture.go b/picture.go index 9b026f567d..152ab9ac17 100644 --- a/picture.go +++ b/picture.go @@ -308,23 +308,20 @@ 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() int { - var c1, c2 int + drawings := map[string]struct{}{} f.Pkg.Range(func(k, v interface{}) bool { if strings.Contains(k.(string), "xl/drawings/drawing") { - c1++ + drawings[k.(string)] = struct{}{} } return true }) f.Drawings.Range(func(rel, value interface{}) bool { if strings.Contains(rel.(string), "xl/drawings/drawing") { - c2++ + drawings[rel.(string)] = struct{}{} } return true }) - if c1 < c2 { - return c2 - } - return c1 + return len(drawings) } // addDrawingPicture provides a function to add picture by given sheet, diff --git a/pivotTable_test.go b/pivotTable_test.go index 24ccb4a0bc..2de3e07f89 100644 --- a/pivotTable_test.go +++ b/pivotTable_test.go @@ -113,7 +113,7 @@ func TestAddPivotTable(t *testing.T) { assert.NoError(t, err) assert.NoError(t, f.AddPivotTable(&PivotTableOptions{ DataRange: "Sheet1!$A$1:$E$31", - PivotTableRange: "Sheet2!$A$1:$AR$15", + PivotTableRange: "Sheet2!$A$1:$AN$17", Rows: []PivotTableField{{Data: "Month"}}, Columns: []PivotTableField{{Data: "Region", DefaultSubtotal: true}, {Data: "Type", DefaultSubtotal: true}, {Data: "Year"}}, Data: []PivotTableField{{Data: "Sales", Subtotal: "Min", Name: "Summarize by Min"}}, @@ -126,7 +126,7 @@ func TestAddPivotTable(t *testing.T) { })) assert.NoError(t, f.AddPivotTable(&PivotTableOptions{ DataRange: "Sheet1!$A$1:$E$31", - PivotTableRange: "Sheet2!$A$18:$AR$54", + PivotTableRange: "Sheet2!$A$20:$AR$60", Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Type"}}, Columns: []PivotTableField{{Data: "Region", DefaultSubtotal: true}, {Data: "Year"}}, Data: []PivotTableField{{Data: "Sales", Subtotal: "Product", Name: "Summarize by Product"}}, @@ -146,7 +146,7 @@ func TestAddPivotTable(t *testing.T) { })) assert.NoError(t, f.AddPivotTable(&PivotTableOptions{ DataRange: "dataRange", - PivotTableRange: "Sheet2!$A$57:$AJ$91", + PivotTableRange: "Sheet2!$A$65:$AJ$100", Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, Columns: []PivotTableField{{Data: "Region", DefaultSubtotal: true}, {Data: "Type"}}, Data: []PivotTableField{{Data: "Sales", Subtotal: "Sum", Name: "Sum of Sales"}, {Data: "Sales", Subtotal: "Average", Name: "Average of Sales"}}, diff --git a/shape.go b/shape.go index 3f3dbe2e4f..73cc5a3c6b 100644 --- a/shape.go +++ b/shape.go @@ -50,13 +50,13 @@ func parseShapeOptions(opts *Shape) (*Shape, error) { } // AddShape provides the method to add shape in a sheet by given worksheet -// index, shape format set (such as offset, scale, aspect ratio setting and -// print settings) and properties set. For example, add text box (rect shape) -// in Sheet1: +// name and shape format set (such as offset, scale, aspect ratio setting and +// print settings). For example, add text box (rect shape) in Sheet1: // // lineWidth := 1.2 -// err := f.AddShape("Sheet1", "G6", +// err := f.AddShape("Sheet1", // &excelize.Shape{ +// Cell: "G6", // Type: "rect", // Line: excelize.ShapeLine{Color: "4286F4", Width: &lineWidth}, // Fill: excelize.Fill{Color: []string{"8EB9FF"}, Pattern: 1}, diff --git a/vml.go b/vml.go index cbe07e2119..b99a979d69 100644 --- a/vml.go +++ b/vml.go @@ -229,22 +229,19 @@ func (f *File) addComment(commentsXML string, opts vmlOptions) error { // countComments provides a function to get comments files count storage in // the folder xl. func (f *File) countComments() int { - c1, c2 := 0, 0 + comments := map[string]struct{}{} f.Pkg.Range(func(k, v interface{}) bool { if strings.Contains(k.(string), "xl/comments") { - c1++ + comments[k.(string)] = struct{}{} } return true }) for rel := range f.Comments { if strings.Contains(rel, "xl/comments") { - c2++ + comments[rel] = struct{}{} } } - if c1 < c2 { - return c2 - } - return c1 + return len(comments) } // commentsReader provides a function to get the pointer to the structure @@ -281,12 +278,12 @@ func (f *File) commentsWriter() { // XLSM or XLTM. Scroll value must be between 0 and 30000. // // Example 1, add button form control with macro, rich-text, custom button size, -// print property on Sheet1!A1, and let the button do not move or size with +// print property on Sheet1!A2, and let the button do not move or size with // cells: // // enable := true // err := f.AddFormControl("Sheet1", excelize.FormControl{ -// Cell: "A1", +// Cell: "A2", // Type: excelize.FormControlButton, // Macro: "Button1_Click", // Width: 140, @@ -321,12 +318,14 @@ func (f *File) commentsWriter() { // Checked: true, // }) // -// Example 3, add spin button form control on Sheet1!A2 to increase or decrease +// Example 3, add spin button form control on Sheet1!B1 to increase or decrease // the value of Sheet1!A1: // // err := f.AddFormControl("Sheet1", excelize.FormControl{ -// Cell: "A2", +// Cell: "B1", // Type: excelize.FormControlSpinButton, +// Width: 15, +// Height: 40, // CurrentVal: 7, // MinVal: 5, // MaxVal: 10, @@ -338,14 +337,17 @@ func (f *File) commentsWriter() { // the value of Sheet1!A1 by click the scroll arrows or drag the scroll box: // // err := f.AddFormControl("Sheet1", excelize.FormControl{ -// Cell: "A2", -// Type: excelize.FormControlScrollBar, -// CurrentVal: 50, -// MinVal: 10, -// MaxVal: 100, -// IncChange: 1, -// PageChange: 1, -// CellLink: "A1", +// Cell: "A2", +// Type: excelize.FormControlScrollBar, +// Width: 140, +// Height: 20, +// CurrentVal: 50, +// MinVal: 10, +// MaxVal: 100, +// IncChange: 1, +// PageChange: 1, +// CellLink: "A1", +// Horizontally: true, // }) func (f *File) AddFormControl(sheet string, opts FormControl) error { return f.addVMLObject(vmlOptions{ @@ -355,9 +357,9 @@ func (f *File) AddFormControl(sheet string, opts FormControl) error { // DeleteFormControl provides the method to delete form control in a worksheet // by given worksheet name and cell reference. For example, delete the form -// control in Sheet1!$A$30: +// control in Sheet1!$A$1: // -// err := f.DeleteFormControl("Sheet1", "A30") +// err := f.DeleteFormControl("Sheet1", "A1") func (f *File) DeleteFormControl(sheet, cell string) error { ws, err := f.workSheetReader(sheet) if err != nil { @@ -430,22 +432,19 @@ func (f *File) DeleteFormControl(sheet, cell string) error { // countVMLDrawing provides a function to get VML drawing files count storage // in the folder xl/drawings. func (f *File) countVMLDrawing() int { - c1, c2 := 0, 0 + drawings := map[string]struct{}{} f.Pkg.Range(func(k, v interface{}) bool { if strings.Contains(k.(string), "xl/drawings/vmlDrawing") { - c1++ + drawings[k.(string)] = struct{}{} } return true }) for rel := range f.VMLDrawing { if strings.Contains(rel, "xl/drawings/vmlDrawing") { - c2++ + drawings[rel] = struct{}{} } } - if c1 < c2 { - return c2 - } - return c1 + return len(drawings) } // decodeVMLDrawingReader provides a function to get the pointer to the From 2e9c2904f2965fc45ad0fa1d5e2bfc7c0632a7cc Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 27 Jul 2023 00:03:15 +0800 Subject: [PATCH 772/957] This closes #1587, fix incorrect date time format result --- numfmt.go | 6 +++--- numfmt_test.go | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/numfmt.go b/numfmt.go index da387fb384..0acbfcf7b6 100644 --- a/numfmt.go +++ b/numfmt.go @@ -1855,15 +1855,15 @@ func (nf *numberFormat) secondsHandler(token nfp.Token) { // for a number format expression. func (nf *numberFormat) elapsedDateTimesHandler(token nfp.Token) { if strings.Contains(strings.ToUpper(token.TValue), "H") { - nf.result += fmt.Sprintf("%.f", nf.t.Sub(excel1900Epoc).Hours()) + nf.result += fmt.Sprintf("%.f", math.Floor(nf.t.Sub(excel1900Epoc).Hours())) return } if strings.Contains(strings.ToUpper(token.TValue), "M") { - nf.result += fmt.Sprintf("%.f", nf.t.Sub(excel1900Epoc).Minutes()) + nf.result += fmt.Sprintf("%.f", math.Floor(nf.t.Sub(excel1900Epoc).Minutes())) return } if strings.Contains(strings.ToUpper(token.TValue), "S") { - nf.result += fmt.Sprintf("%.f", nf.t.Sub(excel1900Epoc).Seconds()) + nf.result += fmt.Sprintf("%.f", math.Floor(nf.t.Sub(excel1900Epoc).Seconds())) return } } diff --git a/numfmt_test.go b/numfmt_test.go index 8cf7afadd3..cdd3f2eeb3 100644 --- a/numfmt_test.go +++ b/numfmt_test.go @@ -48,6 +48,7 @@ func TestNumFmt(t *testing.T) { {"43543.086539351854", "hh:mm:ss AM/PM", "02:04:37 AM"}, {"43543.086539351854", "AM/PM hh:mm:ss", "AM 02:04:37"}, {"43543.086539351854", "AM/PM hh:mm:ss a/p", "AM 02:04:37 a"}, + {"0.609375", "[HH]:mm:ss", "14:37:30"}, {"43528", "YYYY", "2019"}, {"43528", "", "43528"}, {"43528.2123", "YYYY-MM-DD hh:mm:ss", "2019-03-04 05:05:43"}, From a07c8cd0b4a2009f147b5111472565e0a3f85020 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 28 Jul 2023 00:24:08 +0800 Subject: [PATCH 773/957] This closes #1588, closes #1591, breaking changes for the `AddChart` function - Removed exported `ChartTitle` data type - The `AddChart` function now supports formatting and setting rich text titles for the chart - New exported function `GetFormControl` for getting form control - Made case in-sensitive for internal worksheet XML path to improve compatibility - Update the unit tests - Update the documentation and internal comments on the codes --- README.md | 6 +- README_zh.md | 6 +- chart.go | 26 +++++-- chart_test.go | 150 ++++++++++++++++++------------------- drawing.go | 62 +--------------- lib.go | 2 +- picture.go | 2 +- shape.go | 2 +- vml.go | 90 +++++++++++++++++++++- vmlDrawing.go | 9 +++ vml_test.go | 202 +++++++++++++++++++++++++++++++++++++------------- xmlChart.go | 7 +- 12 files changed, 352 insertions(+), 212 deletions(-) diff --git a/README.md b/README.md index 0177eafdb6..460ace4092 100644 --- a/README.md +++ b/README.md @@ -165,8 +165,10 @@ func main() { Categories: "Sheet1!$B$1:$D$1", Values: "Sheet1!$B$4:$D$4", }}, - Title: excelize.ChartTitle{ - Name: "Fruit 3D Clustered Column Chart", + Title: []excelize.RichTextRun{ + { + Text: "Fruit 3D Clustered Column Chart", + }, }, }); err != nil { fmt.Println(err) diff --git a/README_zh.md b/README_zh.md index c6ad9075e4..8bb3068ca3 100644 --- a/README_zh.md +++ b/README_zh.md @@ -165,8 +165,10 @@ func main() { Categories: "Sheet1!$B$1:$D$1", Values: "Sheet1!$B$4:$D$4", }}, - Title: excelize.ChartTitle{ - Name: "Fruit 3D Clustered Column Chart", + Title: []excelize.RichTextRun{ + { + Text: "Fruit 3D Clustered Column Chart", + }, }, }); err != nil { fmt.Println(err) diff --git a/chart.go b/chart.go index 83b653763c..ac13729bd7 100644 --- a/chart.go +++ b/chart.go @@ -507,8 +507,16 @@ func parseChartOptions(opts *Chart) (*Chart, error) { if opts.Legend.Position == "" { opts.Legend.Position = defaultChartLegendPosition } - if opts.Title.Name == "" { - opts.Title.Name = " " + for i := range opts.Title { + if opts.Title[i].Font == nil { + opts.Title[i].Font = &Font{} + } + if opts.Title[i].Font.Color == "" { + opts.Title[i].Font.Color = "595959" + } + if opts.Title[i].Font.Size == 0 { + opts.Title[i].Font.Size = 14 + } } if opts.VaryColors == nil { opts.VaryColors = boolPtr(true) @@ -569,8 +577,10 @@ func parseChartOptions(opts *Chart) (*Chart, error) { // Values: "Sheet1!$B$4:$D$4", // }, // }, -// Title: excelize.ChartTitle{ -// Name: "Fruit 3D Clustered Column Chart", +// Title: []excelize.RichTextRun{ +// { +// Text: "Fruit 3D Clustered Column Chart", +// }, // }, // Legend: excelize.ChartLegend{ // ShowLegendKey: false, @@ -727,7 +737,7 @@ func parseChartOptions(opts *Chart) (*Chart, error) { // // Title // -// Name: Set the name (title) for the chart. The name is displayed above the +// Title: 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. @@ -912,8 +922,10 @@ func parseChartOptions(opts *Chart) (*Chart, error) { // LockAspectRatio: false, // Locked: &disable, // }, -// Title: excelize.ChartTitle{ -// Name: "Clustered Column - Line Chart", +// Title: []excelize.RichTextRun{ +// { +// Text: "Clustered Column - Line Chart", +// }, // }, // Legend: excelize.ChartLegend{ // Position: "left", diff --git a/chart_test.go b/chart_test.go index 49b835514d..4d61c9be39 100644 --- a/chart_test.go +++ b/chart_test.go @@ -52,7 +52,7 @@ func TestChartSize(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"}, }, - Title: ChartTitle{Name: "3D Clustered Column Chart"}, + Title: []RichTextRun{{Text: "3D Clustered Column Chart"}}, })) var buffer bytes.Buffer @@ -206,69 +206,69 @@ 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"}, 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"}}, - {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: []RichTextRun{{Text: "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: []RichTextRun{{Text: "2D Stacked Column Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "P16", opts: &Chart{Type: ColPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "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: []RichTextRun{{Text: "3D Clustered Column Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "P30", opts: &Chart{Type: Col3DStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Stacked Column Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "X30", opts: &Chart{Type: Col3DPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "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: []RichTextRun{{Text: "Radar Chart"}}, PlotArea: plotArea, ShowBlanksAs: "span"}}, + {sheetName: "Sheet1", cell: "AF1", opts: &Chart{Type: Col3DConeStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Column Cone Stacked Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "AF16", opts: &Chart{Type: Col3DConeClustered, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Column Cone Clustered Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "AF30", opts: &Chart{Type: Col3DConePercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "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: []RichTextRun{{Text: "3D Column Cone Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "AN1", opts: &Chart{Type: Col3DPyramidStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "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: []RichTextRun{{Text: "3D Column Pyramid Clustered Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "AN30", opts: &Chart{Type: Col3DPyramidPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "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: []RichTextRun{{Text: "3D Column Pyramid Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "AV1", opts: &Chart{Type: Col3DCylinderStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Column Cylinder Stacked Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "AV16", opts: &Chart{Type: Col3DCylinderClustered, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Column Cylinder Clustered Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "AV30", opts: &Chart{Type: Col3DCylinderPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "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: []RichTextRun{{Text: "3D Column Cylinder Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "P45", opts: &Chart{Type: Col3D, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "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: []RichTextRun{{Text: "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: []RichTextRun{{Text: "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: []RichTextRun{{Text: "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: []RichTextRun{{Text: "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: []RichTextRun{{Text: "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: []RichTextRun{{Text: "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, Secondary: true, Minimum: &zero}, YAxis: ChartAxis{ReverseOrder: true, Minimum: &zero}}}, + {sheetName: "Sheet2", cell: "P48", opts: &Chart{Type: Bar, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "2D Clustered Bar Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "X48", opts: &Chart{Type: BarStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "2D Stacked Bar Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "P64", opts: &Chart{Type: BarPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "2D Stacked 100% Bar Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "X64", opts: &Chart{Type: Bar3DClustered, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Clustered Bar Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "P80", opts: &Chart{Type: Bar3DStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "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: []RichTextRun{{Text: "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"}}, - {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: []RichTextRun{{Text: "2D Area Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "AN1", opts: &Chart{Type: AreaStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "2D Stacked Area Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "AF16", opts: &Chart{Type: AreaPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "2D 100% Stacked Area Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "AN16", opts: &Chart{Type: Area3D, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Area Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "AF32", opts: &Chart{Type: Area3DStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Stacked Area Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "AN32", opts: &Chart{Type: Area3DPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "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: []RichTextRun{{Text: "3D Bar Cylinder Stacked Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "AF64", opts: &Chart{Type: Bar3DCylinderClustered, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Bar Cylinder Clustered Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "AF80", opts: &Chart{Type: Bar3DCylinderPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "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: []RichTextRun{{Text: "3D Bar Cone Stacked Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "AN64", opts: &Chart{Type: Bar3DConeClustered, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Bar Cone Clustered Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "AN80", opts: &Chart{Type: Bar3DConePercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "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: []RichTextRun{{Text: "3D Bar Pyramid Stacked Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "AV64", opts: &Chart{Type: Bar3DPyramidClustered, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Bar Pyramid Clustered Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "AV80", opts: &Chart{Type: Bar3DPyramidPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "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: []RichTextRun{{Text: "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: []RichTextRun{{Text: "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: []RichTextRun{{Text: "Contour Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "BD1", opts: &Chart{Type: WireframeContour, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "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: []RichTextRun{{Text: "Bubble Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "BD32", opts: &Chart{Type: Bubble3D, Series: series4, Format: format, Legend: legend, Title: []RichTextRun{{Text: "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: []RichTextRun{{Text: "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: []RichTextRun{{Text: "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)) } @@ -280,32 +280,32 @@ 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}, YAxis: ChartAxis{Secondary: true}})) + assert.NoError(t, f.AddChart("Combo Charts", props[0].(string), &Chart{Type: Col, Series: series[:4], Format: format, Legend: legend, Title: []RichTextRun{{Text: 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"}, "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].(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.AddChart("Combo Charts", axis, &Chart{Type: AreaStacked, Series: series[:4], Format: format, Legend: legend, Title: []RichTextRun{{Text: 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()) // 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: []RichTextRun{{Text: "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: 0x37, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "Bubble 3D Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}), newUnsupportedChartType(0x37).Error()) + assert.EqualError(t, f.AddChart("Sheet2", "BD32", &Chart{Type: 0x37, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "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: []RichTextRun{{Text: "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: 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.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: []RichTextRun{{Text: "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: []RichTextRun{{Text: "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: []RichTextRun{{Text: "2D Column Chart"}}}), "XML syntax error on line 1: invalid UTF-8") } func TestAddChartSheet(t *testing.T) { @@ -323,7 +323,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: []RichTextRun{{Text: "Fruit 3D Clustered Column Chart"}}})) // Test set the chartsheet as active sheet var sheetIdx int for idx, sheetName := range f.GetSheetList() { @@ -338,11 +338,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: []RichTextRun{{Text: "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: []RichTextRun{{Text: "Fruit 3D Clustered Column Chart"}}}), ErrSheetNameInvalid.Error()) // Test with unsupported chart type - assert.EqualError(t, f.AddChartSheet("Chart2", &Chart{Type: 0x37, Series: series, Title: ChartTitle{Name: "Fruit 3D Clustered Column Chart"}}), newUnsupportedChartType(0x37).Error()) + assert.EqualError(t, f.AddChartSheet("Chart2", &Chart{Type: 0x37, Series: series, Title: []RichTextRun{{Text: "Fruit 3D Clustered Column Chart"}}}), newUnsupportedChartType(0x37).Error()) assert.NoError(t, f.UpdateLinkedValue()) @@ -351,7 +351,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: []RichTextRun{{Text: "2D Column Chart"}}}), "XML syntax error on line 1: invalid UTF-8") } func TestDeleteChart(t *testing.T) { @@ -386,7 +386,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: []RichTextRun{{Text: "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 @@ -435,12 +435,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: []RichTextRun{{Text: "Line chart without log scaling"}}}}, + {cell: "M1", opts: &Chart{Type: Line, Dimension: ChartDimension{Width: dimension[0], Height: dimension[1]}, Series: series, Title: []RichTextRun{{Text: "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: []RichTextRun{{Text: "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: []RichTextRun{{Text: "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: []RichTextRun{{Text: "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: []RichTextRun{{Text: "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 9e2c9f3065..400c990248 100644 --- a/drawing.go +++ b/drawing.go @@ -63,67 +63,7 @@ func (f *File) addChart(opts *Chart, comboCharts []*Chart) { Lang: &attrValString{Val: stringPtr("en-US")}, RoundedCorners: &attrValBool{Val: boolPtr(false)}, Chart: cChart{ - 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), - }, - }, - }, - 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", - }, - T: opts.Title.Name, - }, - }, - }, - }, - }, - TxPr: cTxPr{ - P: aP{ - PPr: &aPPr{ - DefRPr: aRPr{ - Kern: 1200, - U: "none", - Sz: 14000, - Strike: "noStrike", - }, - }, - EndParaRPr: &aEndParaRPr{ - Lang: "en-US", - }, - }, - }, - Overlay: &attrValBool{Val: boolPtr(false)}, - }, + Title: f.drawPlotAreaTitles(opts.Title, ""), View3D: &cView3D{ RotX: &attrValInt{Val: intPtr(chartView3DRotX[opts.Type])}, RotY: &attrValInt{Val: intPtr(chartView3DRotY[opts.Type])}, diff --git a/lib.go b/lib.go index 887946aef5..379880755b 100644 --- a/lib.go +++ b/lib.go @@ -53,7 +53,7 @@ func (f *File) ReadZipReader(r *zip.Reader) (map[string][]byte, int, error) { continue } } - if strings.HasPrefix(fileName, "xl/worksheets/sheet") { + if strings.HasPrefix(strings.ToLower(fileName), "xl/worksheets/sheet") { worksheets++ if fileSize > f.options.UnzipXMLSizeLimit && !v.FileInfo().IsDir() { if tempFile, err := f.unzipToTemp(v); err == nil { diff --git a/picture.go b/picture.go index 152ab9ac17..802f515a9a 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 } - // Read sheet data. + // Read sheet data f.mu.Lock() ws, err := f.workSheetReader(sheet) if err != nil { diff --git a/shape.go b/shape.go index 73cc5a3c6b..53f1649011 100644 --- a/shape.go +++ b/shape.go @@ -293,7 +293,7 @@ func (f *File) AddShape(sheet string, opts *Shape) error { if err != nil { return err } - // Read sheet data. + // Read sheet data ws, err := f.workSheetReader(sheet) if err != nil { return err diff --git a/vml.go b/vml.go index b99a979d69..540e36ba17 100644 --- a/vml.go +++ b/vml.go @@ -390,7 +390,7 @@ func (f *File) DeleteFormControl(sheet, cell string) error { VPath: &vPath{GradientShapeOK: "t", ConnectType: "rect"}, }, } - // load exist VML shapes from xl/drawings/vmlDrawing%d.vml + // Load exist VML shapes from xl/drawings/vmlDrawing%d.vml d, err := f.decodeVMLDrawingReader(drawingVML) if err != nil { return err @@ -477,7 +477,7 @@ func (f *File) vmlDrawingWriter() { // addVMLObject provides a function to create VML drawing parts and // relationships for comments and form controls. func (f *File) addVMLObject(opts vmlOptions) error { - // Read sheet data. + // Read sheet data ws, err := f.workSheetReader(opts.sheet) if err != nil { return err @@ -836,7 +836,7 @@ func (f *File) addDrawingVML(dataID int, drawingVML string, opts *vmlOptions) er VPath: &vPath{GradientShapeOK: "t", ConnectType: "rect"}, }, } - // load exist VML shapes from xl/drawings/vmlDrawing%d.vml + // Load exist VML shapes from xl/drawings/vmlDrawing%d.vml d, err := f.decodeVMLDrawingReader(drawingVML) if err != nil { return err @@ -883,3 +883,87 @@ func (f *File) addDrawingVML(dataID int, drawingVML string, opts *vmlOptions) er f.VMLDrawing[drawingVML] = vml return err } + +// GetFormControls retrieves all form controls in a worksheet by a given +// worksheet name. Note that, this function does not support getting the width, +// height, text, rich text, and format currently. +func (f *File) GetFormControls(sheet string) ([]FormControl, error) { + var formControls []FormControl + // Read sheet data + ws, err := f.workSheetReader(sheet) + if err != nil { + return formControls, err + } + if ws.LegacyDrawing == nil { + return formControls, err + } + target := f.getSheetRelationshipsTargetByID(sheet, ws.LegacyDrawing.RID) + drawingVML := strings.ReplaceAll(target, "..", "xl") + vml := f.VMLDrawing[drawingVML] + if vml == nil { + // Load exist VML shapes from xl/drawings/vmlDrawing%d.vml + d, err := f.decodeVMLDrawingReader(drawingVML) + if err != nil { + return formControls, err + } + for _, sp := range d.Shape { + if sp.Type != "#_x0000_t201" { + continue + } + formControl, err := extractFormControl(sp.Val) + if err != nil { + return formControls, err + } + if formControl.Type == FormControlNote || formControl.Cell == "" { + continue + } + formControls = append(formControls, formControl) + } + return formControls, err + } + for _, sp := range vml.Shape { + if sp.Type != "#_x0000_t201" { + continue + } + formControl, err := extractFormControl(sp.Val) + if err != nil { + return formControls, err + } + if formControl.Type == FormControlNote || formControl.Cell == "" { + continue + } + formControls = append(formControls, formControl) + } + return formControls, err +} + +// extractFormControl provides a function to extract form controls for a +// worksheets by given client data. +func extractFormControl(clientData string) (FormControl, error) { + var ( + err error + formControl FormControl + shapeVal decodeShapeVal + ) + if err = xml.Unmarshal([]byte(fmt.Sprintf("%s", clientData)), &shapeVal); err != nil { + return formControl, err + } + for formCtrlType, preset := range formCtrlPresets { + if shapeVal.ClientData.ObjectType == preset.objectType { + formControl.Type = formCtrlType + if formControl.Cell, err = CoordinatesToCellName(shapeVal.ClientData.Column+1, shapeVal.ClientData.Row+1); err != nil { + return formControl, err + } + formControl.Macro = shapeVal.ClientData.FmlaMacro + formControl.Checked = shapeVal.ClientData.Checked != 0 + formControl.CellLink = shapeVal.ClientData.FmlaLink + formControl.CurrentVal = shapeVal.ClientData.Val + formControl.MinVal = shapeVal.ClientData.Min + formControl.MaxVal = shapeVal.ClientData.Max + formControl.IncChange = shapeVal.ClientData.Inc + formControl.PageChange = shapeVal.ClientData.Page + formControl.Horizontally = shapeVal.ClientData.Horiz != nil + } + } + return formControl, err +} diff --git a/vmlDrawing.go b/vmlDrawing.go index d09054fa4b..76e90115c4 100644 --- a/vmlDrawing.go +++ b/vmlDrawing.go @@ -192,8 +192,17 @@ type decodeShapeVal struct { // element in the file xl/drawings/vmlDrawing%d.vml. type decodeVMLClientData struct { ObjectType string `xml:"ObjectType,attr"` + FmlaMacro string Column int Row int + Checked int + FmlaLink string + Val uint + Min uint + Max uint + Inc uint + Page uint + Horiz *string } // encodeShape defines the structure used to re-serialization shape element. diff --git a/vml_test.go b/vml_test.go index aba262d267..52a7dada51 100644 --- a/vml_test.go +++ b/vml_test.go @@ -13,6 +13,7 @@ package excelize import ( "encoding/xml" + "fmt" "os" "path/filepath" "strings" @@ -157,62 +158,83 @@ func TestAddDrawingVML(t *testing.T) { func TestFormControl(t *testing.T) { f := NewFile() - assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ - Cell: "D1", Type: FormControlButton, Macro: "Button1_Click", - })) - assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ - Cell: "A1", Type: FormControlButton, Macro: "Button1_Click", - Width: 140, Height: 60, Text: "Button 1\r\n", - Paragraph: []RichTextRun{ - { - Font: &Font{ - Bold: true, - Italic: true, - Underline: "single", - Family: "Times New Roman", - Size: 14, - Color: "777777", + formControls := []FormControl{ + { + Cell: "D1", Type: FormControlButton, Macro: "Button1_Click", + }, + { + Cell: "A1", Type: FormControlButton, Macro: "Button1_Click", + Width: 140, Height: 60, Text: "Button 1\r\n", + Paragraph: []RichTextRun{ + { + Font: &Font{ + Bold: true, + Italic: true, + Underline: "single", + Family: "Times New Roman", + Size: 14, + Color: "777777", + }, + Text: "C1=A1+B1", }, - Text: "C1=A1+B1", }, + Format: GraphicOptions{PrintObject: boolPtr(true), Positioning: "absolute"}, }, - Format: GraphicOptions{PrintObject: boolPtr(true), Positioning: "absolute"}, - })) - assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ - Cell: "A5", Type: FormControlCheckBox, Text: "Check Box 1", - Checked: true, Format: GraphicOptions{ - PrintObject: boolPtr(false), Positioning: "oneCell", + { + Cell: "A5", Type: FormControlCheckBox, Text: "Check Box 1", + Checked: true, Format: GraphicOptions{ + PrintObject: boolPtr(false), Positioning: "oneCell", + }, }, - })) - assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ - Cell: "A6", Type: FormControlCheckBox, Text: "Check Box 2", - Format: GraphicOptions{Positioning: "twoCell"}, - })) - assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ - Cell: "A7", Type: FormControlOptionButton, Text: "Option Button 1", Checked: true, - })) - assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ - Cell: "A8", Type: FormControlOptionButton, Text: "Option Button 2", - })) - assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ - Cell: "D3", Type: FormControlGroupBox, Text: "Group Box 1", - Width: 140, Height: 60, - })) - assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ - Cell: "A9", Type: FormControlLabel, Text: "Label 1", Width: 140, - })) - assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ - Cell: "C5", Type: FormControlSpinButton, Width: 40, Height: 60, - CurrentVal: 7, MinVal: 5, MaxVal: 10, IncChange: 1, CellLink: "C2", - })) - assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ - Cell: "D7", Type: FormControlScrollBar, Width: 140, Height: 20, - CurrentVal: 50, MinVal: 10, MaxVal: 100, IncChange: 1, PageChange: 1, Horizontally: true, CellLink: "C3", - })) - assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ - Cell: "G1", Type: FormControlScrollBar, Width: 20, Height: 140, - CurrentVal: 50, MinVal: 1000, MaxVal: 100, IncChange: 1, PageChange: 1, CellLink: "C4", - })) + { + Cell: "A6", Type: FormControlCheckBox, Text: "Check Box 2", + Format: GraphicOptions{Positioning: "twoCell"}, + }, + { + Cell: "A7", Type: FormControlOptionButton, Text: "Option Button 1", Checked: true, + }, + { + Cell: "A8", Type: FormControlOptionButton, Text: "Option Button 2", + }, + { + Cell: "D3", Type: FormControlGroupBox, Text: "Group Box 1", + Width: 140, Height: 60, + }, + { + Cell: "A9", Type: FormControlLabel, Text: "Label 1", Width: 140, + }, + { + Cell: "C5", Type: FormControlSpinButton, Width: 40, Height: 60, + CurrentVal: 7, MinVal: 5, MaxVal: 10, IncChange: 1, CellLink: "C2", + }, + { + Cell: "D7", Type: FormControlScrollBar, Width: 140, Height: 20, + CurrentVal: 50, MinVal: 10, MaxVal: 100, IncChange: 1, PageChange: 1, Horizontally: true, CellLink: "C3", + }, + { + Cell: "G1", Type: FormControlScrollBar, Width: 20, Height: 140, + CurrentVal: 50, MinVal: 1000, MaxVal: 100, IncChange: 1, PageChange: 1, CellLink: "C4", + }, + } + for _, formCtrl := range formControls { + assert.NoError(t, f.AddFormControl("Sheet1", formCtrl)) + } + // Test get from controls + result, err := f.GetFormControls("Sheet1") + assert.NoError(t, err) + assert.Len(t, result, 11) + for i, formCtrl := range formControls { + assert.Equal(t, formCtrl.Type, result[i].Type) + assert.Equal(t, formCtrl.Cell, result[i].Cell) + assert.Equal(t, formCtrl.Macro, result[i].Macro) + assert.Equal(t, formCtrl.Checked, result[i].Checked) + assert.Equal(t, formCtrl.CurrentVal, result[i].CurrentVal) + assert.Equal(t, formCtrl.MinVal, result[i].MinVal) + assert.Equal(t, formCtrl.MaxVal, result[i].MaxVal) + assert.Equal(t, formCtrl.IncChange, result[i].IncChange) + assert.Equal(t, formCtrl.Horizontally, result[i].Horizontally) + assert.Equal(t, formCtrl.CellLink, result[i].CellLink) + } assert.NoError(t, f.SetSheetProps("Sheet1", &SheetPropsOptions{CodeName: stringPtr("Sheet1")})) file, err := os.ReadFile(filepath.Join("test", "vbaProject.bin")) assert.NoError(t, err) @@ -221,9 +243,18 @@ func TestFormControl(t *testing.T) { assert.NoError(t, f.Close()) f, err = OpenFile(filepath.Join("test", "TestAddFormControl.xlsm")) assert.NoError(t, err) + // Test get from controls before add form controls + result, err = f.GetFormControls("Sheet1") + assert.NoError(t, err) + assert.Len(t, result, 11) + // Test add from control to a worksheet which already contains form controls assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ Cell: "D4", Type: FormControlButton, Macro: "Button1_Click", Text: "Button 2", })) + // Test get from controls after add form controls + result, err = f.GetFormControls("Sheet1") + assert.NoError(t, err) + assert.Len(t, result, 12) // Test add unsupported form control assert.Equal(t, f.AddFormControl("Sheet1", FormControl{ Cell: "A1", Type: 0x37, Macro: "Button1_Click", @@ -251,9 +282,13 @@ func TestFormControl(t *testing.T) { assert.NoError(t, err) assert.NoError(t, f.DeleteFormControl("Sheet1", "D1")) assert.NoError(t, f.DeleteFormControl("Sheet1", "A1")) + // Test get from controls after delete form controls + result, err = f.GetFormControls("Sheet1") + assert.NoError(t, err) + assert.Len(t, result, 9) // Test delete form control on not exists worksheet assert.Equal(t, f.DeleteFormControl("SheetN", "A1"), newNoExistSheetError("SheetN")) - // Test delete form control on not exists worksheet + // Test delete form control with illegal cell link reference assert.Equal(t, f.DeleteFormControl("Sheet1", "A"), newCellNameToCoordinatesError("A", newInvalidCellNameError("A"))) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDeleteFormControl.xlsm"))) assert.NoError(t, f.Close()) @@ -266,4 +301,65 @@ func TestFormControl(t *testing.T) { // Test delete form control on a worksheet without form control f = NewFile() assert.NoError(t, f.DeleteFormControl("Sheet1", "A1")) + // Test get form controls on a worksheet without form control + _, err = f.GetFormControls("Sheet1") + assert.NoError(t, err) + // Test get form controls on not exists worksheet + _, err = f.GetFormControls("SheetN") + assert.Equal(t, err, newNoExistSheetError("SheetN")) + // Test get form controls with unsupported charset VML drawing + f, err = OpenFile(filepath.Join("test", "TestAddFormControl.xlsm")) + assert.NoError(t, err) + f.Pkg.Store("xl/drawings/vmlDrawing1.vml", MacintoshCyrillicCharset) + _, err = f.GetFormControls("Sheet1") + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") + // Test get form controls with unsupported shape type + f.DecodeVMLDrawing["xl/drawings/vmlDrawing1.vml"] = &decodeVmlDrawing{ + Shape: []decodeShape{{Type: "_x0000_t202"}}, + } + formControls, err = f.GetFormControls("Sheet1") + assert.NoError(t, err) + assert.Len(t, formControls, 0) + // Test get form controls with invalid column number + f.DecodeVMLDrawing["xl/drawings/vmlDrawing1.vml"] = &decodeVmlDrawing{ + Shape: []decodeShape{{Type: "#_x0000_t201", Val: fmt.Sprintf("%d", MaxColumns)}}, + } + formControls, err = f.GetFormControls("Sheet1") + assert.Equal(t, err, ErrColumnNumber) + assert.Len(t, formControls, 0) + // Test get form controls with comment (Note) shape type + f.DecodeVMLDrawing["xl/drawings/vmlDrawing1.vml"] = &decodeVmlDrawing{ + Shape: []decodeShape{{Type: "#_x0000_t201", Val: ""}}, + } + formControls, err = f.GetFormControls("Sheet1") + assert.NoError(t, err) + assert.Len(t, formControls, 0) + // Test get form controls with unsupported shape type + f.VMLDrawing["xl/drawings/vmlDrawing1.vml"] = &vmlDrawing{ + Shape: []xlsxShape{{Type: "_x0000_t202"}}, + } + formControls, err = f.GetFormControls("Sheet1") + assert.NoError(t, err) + assert.Len(t, formControls, 0) + // Test get form controls with invalid column number + f.VMLDrawing["xl/drawings/vmlDrawing1.vml"] = &vmlDrawing{ + Shape: []xlsxShape{{Type: "#_x0000_t201", Val: fmt.Sprintf("%d", MaxColumns)}}, + } + formControls, err = f.GetFormControls("Sheet1") + assert.Equal(t, err, ErrColumnNumber) + assert.Len(t, formControls, 0) + // Test get form controls with comment (Note) shape type + f.VMLDrawing["xl/drawings/vmlDrawing1.vml"] = &vmlDrawing{ + Shape: []xlsxShape{{Type: "#_x0000_t201", Val: ""}}, + } + formControls, err = f.GetFormControls("Sheet1") + assert.NoError(t, err) + assert.Len(t, formControls, 0) + assert.NoError(t, f.Close()) +} + +func TestExtractFormControl(t *testing.T) { + // Test extract form control with unsupported charset + _, err := extractFormControl(string(MacintoshCyrillicCharset)) + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") } diff --git a/xmlChart.go b/xmlChart.go index 7be783bd68..c69a6afdd8 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -570,7 +570,7 @@ type Chart struct { Format GraphicOptions Dimension ChartDimension Legend ChartLegend - Title ChartTitle + Title []RichTextRun VaryColors *bool XAxis ChartAxis YAxis ChartAxis @@ -608,8 +608,3 @@ type ChartSeries struct { Line ChartLine Marker ChartMarker } - -// ChartTitle directly maps the format settings of the chart title. -type ChartTitle struct { - Name string -} From 5fe30eb4565980503acbabd1dad47ac3ec26d5fd Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 31 Jul 2023 00:08:10 +0800 Subject: [PATCH 774/957] This closes #1590, add the Japanese calendar number format support - The `GetFormControl` now support to get text, rich-text and font format of the form controls - Update the unit tests and the documentation --- cell.go | 15 +++- cell_test.go | 8 +++ go.mod | 2 +- go.sum | 4 +- numfmt.go | 181 +++++++++++++++++++++++++++++-------------------- numfmt_test.go | 12 ++++ vml.go | 114 ++++++++++++++++++++++++++++--- vmlDrawing.go | 51 ++++++++++++-- vml_test.go | 53 +++++++++++++-- xmlStyles.go | 5 +- 10 files changed, 347 insertions(+), 98 deletions(-) diff --git a/cell.go b/cell.go index 2de8e85385..5842aeec17 100644 --- a/cell.go +++ b/cell.go @@ -1368,15 +1368,24 @@ func (f *File) formattedValue(c *xlsxC, raw bool, cellType CellType) (string, er if fmtCode, ok := f.getBuiltInNumFmtCode(numFmtID); ok { return f.applyBuiltInNumFmt(c, fmtCode, numFmtID, date1904, cellType), err } + return f.applyNumFmt(c, styleSheet, numFmtID, date1904, cellType), err +} + +// applyNumFmt provides a function to returns formatted cell value with custom +// number format code. +func (f *File) applyNumFmt(c *xlsxC, styleSheet *xlsxStyleSheet, numFmtID int, date1904 bool, cellType CellType) string { if styleSheet.NumFmts == nil { - return c.V, err + return c.V } for _, xlsxFmt := range styleSheet.NumFmts.NumFmt { if xlsxFmt.NumFmtID == numFmtID { - return format(c.V, xlsxFmt.FormatCode, date1904, cellType, f.options), err + if xlsxFmt.FormatCode16 != "" { + return format(c.V, xlsxFmt.FormatCode16, date1904, cellType, f.options) + } + return format(c.V, xlsxFmt.FormatCode, date1904, cellType, f.options) } } - return c.V, err + return c.V } // prepareCellStyle provides a function to prepare style index of cell in diff --git a/cell_test.go b/cell_test.go index 7b53d86d19..1770e91a67 100644 --- a/cell_test.go +++ b/cell_test.go @@ -927,6 +927,14 @@ func TestFormattedValueNilWorkbookPr(t *testing.T) { assert.Equal(t, "43528", result) } +func TestApplyNumFmt(t *testing.T) { + f := NewFile() + assert.Equal(t, "\u4EE4\u548C\u5143年9月1日", f.applyNumFmt(&xlsxC{V: "43709"}, + &xlsxStyleSheet{NumFmts: &xlsxNumFmts{NumFmt: []*xlsxNumFmt{ + {NumFmtID: 164, FormatCode16: "[$-ja-JP-x-gannen,80]ggge\"年\"m\"月\"d\"日\";@"}, + }}}, 164, false, CellTypeNumber)) +} + func TestSharedStringsError(t *testing.T) { f, err := OpenFile(filepath.Join("test", "Book1.xlsx"), Options{UnzipXMLSizeLimit: 128}) assert.NoError(t, err) diff --git a/go.mod b/go.mod index 8a6347ec41..9395c13e73 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-20230723160540-a7d120392641 + github.com/xuri/nfp v0.0.0-20230730012209-aee513b45ff4 golang.org/x/crypto v0.11.0 golang.org/x/image v0.5.0 golang.org/x/net v0.12.0 diff --git a/go.sum b/go.sum index 7bc9eb62d0..74da2d5172 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-20230723160540-a7d120392641 h1:1SQuQwUorWlROdGAbsAJrMInj02yCUsYFNi/MzTJ6cA= -github.com/xuri/nfp v0.0.0-20230723160540-a7d120392641/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= +github.com/xuri/nfp v0.0.0-20230730012209-aee513b45ff4 h1:7TXNzvlvE0E/oLDazWm2Xip72G9Su+jRzvziSxwO6Ww= +github.com/xuri/nfp v0.0.0-20230730012209-aee513b45ff4/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 0acbfcf7b6..59a7154d5d 100644 --- a/numfmt.go +++ b/numfmt.go @@ -26,6 +26,7 @@ import ( type languageInfo struct { apFmt string tags []string + useGannen bool localMonth func(t time.Time, abbr int) string } @@ -768,71 +769,85 @@ var ( "vai-Vaii-LR", "vai-Latn-LR", "vai-Latn", "vo", "vo-001", "vun", "vun-TZ", "wae", "wae-CH", "wal", "wae-ET", "yav", "yav-CM", "yo-BJ", "dje", "dje-NE", }, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, - "C09": {tags: []string{"en-AU"}, localMonth: localMonthsNameEnglish, apFmt: strings.ToLower(nfp.AmPm[0])}, - "2829": {tags: []string{"en-BZ"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, - "1009": {tags: []string{"en-CA"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, - "2409": {tags: []string{"en-029"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, - "3C09": {tags: []string{"en-HK"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, - "4009": {tags: []string{"en-IN"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, - "1809": {tags: []string{"en-IE"}, localMonth: localMonthsNameEnglish, apFmt: strings.ToLower(nfp.AmPm[0])}, - "2009": {tags: []string{"en-JM"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, - "4409": {tags: []string{"en-MY"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, - "1409": {tags: []string{"en-NZ"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, - "3409": {tags: []string{"en-PH"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, - "4809": {tags: []string{"en-SG"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, - "1C09": {tags: []string{"en-ZA"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, - "2C09": {tags: []string{"en-TT"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, - "4C09": {tags: []string{"en-AE"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, - "809": {tags: []string{"en-GB"}, localMonth: localMonthsNameEnglish, apFmt: strings.ToLower(nfp.AmPm[0])}, - "409": {tags: []string{"en-US"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, - "3009": {tags: []string{"en-ZW"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, - "C": {tags: []string{"fr"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0]}, - "7": {tags: []string{"de"}, localMonth: localMonthsNameGerman, apFmt: nfp.AmPm[0]}, - "C07": {tags: []string{"de-AT"}, localMonth: localMonthsNameAustria, apFmt: nfp.AmPm[0]}, - "407": {tags: []string{"de-DE"}, localMonth: localMonthsNameGerman, apFmt: nfp.AmPm[0]}, - "3C": {tags: []string{"ga"}, localMonth: localMonthsNameIrish, apFmt: apFmtIrish}, - "83C": {tags: []string{"ga-IE"}, localMonth: localMonthsNameIrish, apFmt: apFmtIrish}, - "10": {tags: []string{"it"}, localMonth: localMonthsNameItalian, apFmt: nfp.AmPm[0]}, - "11": {tags: []string{"ja"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese}, - "411": {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese}, - "12": {tags: []string{"ko"}, localMonth: localMonthsNameKorean, apFmt: apFmtKorean}, - "412": {tags: []string{"ko-KR"}, localMonth: localMonthsNameKorean, apFmt: apFmtKorean}, - "7C50": {tags: []string{"mn-Mong"}, localMonth: localMonthsNameTraditionalMongolian, apFmt: nfp.AmPm[0]}, - "850": {tags: []string{"mn-Mong-CN"}, localMonth: localMonthsNameTraditionalMongolian, apFmt: nfp.AmPm[0]}, - "C50": {tags: []string{"mn-Mong-MN"}, localMonth: localMonthsNameTraditionalMongolian, apFmt: nfp.AmPm[0]}, - "19": {tags: []string{"ru"}, localMonth: localMonthsNameRussian, apFmt: nfp.AmPm[0]}, - "819": {tags: []string{"ru-MD"}, localMonth: localMonthsNameRussian, apFmt: nfp.AmPm[0]}, - "419": {tags: []string{"ru-RU"}, localMonth: localMonthsNameRussian, apFmt: nfp.AmPm[0]}, - "A": {tags: []string{"es"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, - "2C0A": {tags: []string{"es-AR"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, - "200A": {tags: []string{"es-VE"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, - "400A": {tags: []string{"es-BO"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, - "340A": {tags: []string{"es-CL"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, - "240A": {tags: []string{"es-CO"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, - "140A": {tags: []string{"es-CR"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, - "5C0A": {tags: []string{"es-CU"}, localMonth: localMonthsNameSpanish, apFmt: apFmtCuba}, - "1C0A": {tags: []string{"es-DO"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, - "300A": {tags: []string{"es-EC"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, - "440A": {tags: []string{"es-SV"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, - "1E": {tags: []string{"th"}, localMonth: localMonthsNameThai, apFmt: nfp.AmPm[0]}, - "41E": {tags: []string{"th-TH"}, localMonth: localMonthsNameThai, apFmt: nfp.AmPm[0]}, - "51": {tags: []string{"bo"}, localMonth: localMonthsNameTibetan, apFmt: apFmtTibetan}, - "451": {tags: []string{"bo-CN"}, localMonth: localMonthsNameTibetan, apFmt: apFmtTibetan}, - "1F": {tags: []string{"tr"}, localMonth: localMonthsNameTurkish, apFmt: apFmtTurkish}, - "41F": {tags: []string{"tr-TR"}, localMonth: localMonthsNameTurkish, apFmt: apFmtTurkish}, - "52": {tags: []string{"cy"}, localMonth: localMonthsNameWelsh, apFmt: apFmtWelsh}, - "452": {tags: []string{"cy-GB"}, localMonth: localMonthsNameWelsh, apFmt: apFmtWelsh}, - "2A": {tags: []string{"vi"}, localMonth: localMonthsNameVietnamese, apFmt: apFmtVietnamese}, - "42A": {tags: []string{"vi-VN"}, localMonth: localMonthsNameVietnamese, apFmt: apFmtVietnamese}, - "88": {tags: []string{"wo"}, localMonth: localMonthsNameWolof, apFmt: apFmtWolof}, - "488": {tags: []string{"wo-SN"}, localMonth: localMonthsNameWolof, apFmt: apFmtWolof}, - "34": {tags: []string{"xh"}, localMonth: localMonthsNameXhosa, apFmt: nfp.AmPm[0]}, - "434": {tags: []string{"xh-ZA"}, localMonth: localMonthsNameXhosa, apFmt: nfp.AmPm[0]}, - "78": {tags: []string{"ii"}, localMonth: localMonthsNameYi, apFmt: apFmtYi}, - "478": {tags: []string{"ii-CN"}, localMonth: localMonthsNameYi, apFmt: apFmtYi}, - "35": {tags: []string{"zu"}, localMonth: localMonthsNameZulu, apFmt: nfp.AmPm[0]}, - "435": {tags: []string{"zu-ZA"}, localMonth: localMonthsNameZulu, apFmt: nfp.AmPm[0]}, - } + "C09": {tags: []string{"en-AU"}, localMonth: localMonthsNameEnglish, apFmt: strings.ToLower(nfp.AmPm[0])}, + "2829": {tags: []string{"en-BZ"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, + "1009": {tags: []string{"en-CA"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, + "2409": {tags: []string{"en-029"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, + "3C09": {tags: []string{"en-HK"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, + "4009": {tags: []string{"en-IN"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, + "1809": {tags: []string{"en-IE"}, localMonth: localMonthsNameEnglish, apFmt: strings.ToLower(nfp.AmPm[0])}, + "2009": {tags: []string{"en-JM"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, + "4409": {tags: []string{"en-MY"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, + "1409": {tags: []string{"en-NZ"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, + "3409": {tags: []string{"en-PH"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, + "4809": {tags: []string{"en-SG"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, + "1C09": {tags: []string{"en-ZA"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, + "2C09": {tags: []string{"en-TT"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, + "4C09": {tags: []string{"en-AE"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, + "809": {tags: []string{"en-GB"}, localMonth: localMonthsNameEnglish, apFmt: strings.ToLower(nfp.AmPm[0])}, + "409": {tags: []string{"en-US"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, + "3009": {tags: []string{"en-ZW"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, + "C": {tags: []string{"fr"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0]}, + "7": {tags: []string{"de"}, localMonth: localMonthsNameGerman, apFmt: nfp.AmPm[0]}, + "C07": {tags: []string{"de-AT"}, localMonth: localMonthsNameAustria, apFmt: nfp.AmPm[0]}, + "407": {tags: []string{"de-DE"}, localMonth: localMonthsNameGerman, apFmt: nfp.AmPm[0]}, + "3C": {tags: []string{"ga"}, localMonth: localMonthsNameIrish, apFmt: apFmtIrish}, + "83C": {tags: []string{"ga-IE"}, localMonth: localMonthsNameIrish, apFmt: apFmtIrish}, + "10": {tags: []string{"it"}, localMonth: localMonthsNameItalian, apFmt: nfp.AmPm[0]}, + "11": {tags: []string{"ja"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese}, + "411": {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese}, + "800411": {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese}, + "JP-X-GANNEN,80": {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese, useGannen: true}, + "12": {tags: []string{"ko"}, localMonth: localMonthsNameKorean, apFmt: apFmtKorean}, + "412": {tags: []string{"ko-KR"}, localMonth: localMonthsNameKorean, apFmt: apFmtKorean}, + "7C50": {tags: []string{"mn-Mong"}, localMonth: localMonthsNameTraditionalMongolian, apFmt: nfp.AmPm[0]}, + "850": {tags: []string{"mn-Mong-CN"}, localMonth: localMonthsNameTraditionalMongolian, apFmt: nfp.AmPm[0]}, + "C50": {tags: []string{"mn-Mong-MN"}, localMonth: localMonthsNameTraditionalMongolian, apFmt: nfp.AmPm[0]}, + "19": {tags: []string{"ru"}, localMonth: localMonthsNameRussian, apFmt: nfp.AmPm[0]}, + "819": {tags: []string{"ru-MD"}, localMonth: localMonthsNameRussian, apFmt: nfp.AmPm[0]}, + "419": {tags: []string{"ru-RU"}, localMonth: localMonthsNameRussian, apFmt: nfp.AmPm[0]}, + "A": {tags: []string{"es"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, + "2C0A": {tags: []string{"es-AR"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, + "200A": {tags: []string{"es-VE"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, + "400A": {tags: []string{"es-BO"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, + "340A": {tags: []string{"es-CL"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, + "240A": {tags: []string{"es-CO"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, + "140A": {tags: []string{"es-CR"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, + "5C0A": {tags: []string{"es-CU"}, localMonth: localMonthsNameSpanish, apFmt: apFmtCuba}, + "1C0A": {tags: []string{"es-DO"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, + "300A": {tags: []string{"es-EC"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, + "440A": {tags: []string{"es-SV"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, + "1E": {tags: []string{"th"}, localMonth: localMonthsNameThai, apFmt: nfp.AmPm[0]}, + "41E": {tags: []string{"th-TH"}, localMonth: localMonthsNameThai, apFmt: nfp.AmPm[0]}, + "51": {tags: []string{"bo"}, localMonth: localMonthsNameTibetan, apFmt: apFmtTibetan}, + "451": {tags: []string{"bo-CN"}, localMonth: localMonthsNameTibetan, apFmt: apFmtTibetan}, + "1F": {tags: []string{"tr"}, localMonth: localMonthsNameTurkish, apFmt: apFmtTurkish}, + "41F": {tags: []string{"tr-TR"}, localMonth: localMonthsNameTurkish, apFmt: apFmtTurkish}, + "52": {tags: []string{"cy"}, localMonth: localMonthsNameWelsh, apFmt: apFmtWelsh}, + "452": {tags: []string{"cy-GB"}, localMonth: localMonthsNameWelsh, apFmt: apFmtWelsh}, + "2A": {tags: []string{"vi"}, localMonth: localMonthsNameVietnamese, apFmt: apFmtVietnamese}, + "42A": {tags: []string{"vi-VN"}, localMonth: localMonthsNameVietnamese, apFmt: apFmtVietnamese}, + "88": {tags: []string{"wo"}, localMonth: localMonthsNameWolof, apFmt: apFmtWolof}, + "488": {tags: []string{"wo-SN"}, localMonth: localMonthsNameWolof, apFmt: apFmtWolof}, + "34": {tags: []string{"xh"}, localMonth: localMonthsNameXhosa, apFmt: nfp.AmPm[0]}, + "434": {tags: []string{"xh-ZA"}, localMonth: localMonthsNameXhosa, apFmt: nfp.AmPm[0]}, + "78": {tags: []string{"ii"}, localMonth: localMonthsNameYi, apFmt: apFmtYi}, + "478": {tags: []string{"ii-CN"}, localMonth: localMonthsNameYi, apFmt: apFmtYi}, + "35": {tags: []string{"zu"}, localMonth: localMonthsNameZulu, apFmt: nfp.AmPm[0]}, + "435": {tags: []string{"zu-ZA"}, localMonth: localMonthsNameZulu, apFmt: nfp.AmPm[0]}, + } + // japaneseEraYears list the Japanese era name periods. + japaneseEraYears = []time.Time{ + time.Date(1868, time.August, 8, 0, 0, 0, 0, time.UTC), + time.Date(1912, time.June, 30, 0, 0, 0, 0, time.UTC), + time.Date(1926, time.November, 25, 0, 0, 0, 0, time.UTC), + time.Date(1989, time.January, 8, 0, 0, 0, 0, time.UTC), + time.Date(2019, time.April, 1, 0, 0, 0, 0, time.UTC), + } + // japaneseEraNames list the Japanese era name for the Japanese emperor reign calendar. + japaneseEraNames = []string{"\u660E\u6CBB", "\u5927\u6B63", "\u662D\u548C", "\u5E73\u6210", "\u4EE4\u548C"} + // japaneseEraYear list the Japanese era name symbols. + japaneseEraSymbols = []string{"M", "T", "S", "H", "R"} // monthNamesBangla list the month names in the Bangla. monthNamesBangla = []string{ "\u099C\u09BE\u09A8\u09C1\u09AF\u09BC\u09BE\u09B0\u09C0", @@ -1329,7 +1344,9 @@ func (nf *numberFormat) dateTimeHandler() string { if changeNumFmtCode, err := nf.currencyLanguageHandler(token); err != nil || changeNumFmtCode { return nf.value } - nf.result += nf.currencyString + if !supportedLanguageInfo[nf.localCode].useGannen { + nf.result += nf.currencyString + } } if token.TType == nfp.TokenTypeDateTimes { nf.dateTimesHandler(i, token) @@ -1752,15 +1769,35 @@ 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(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:] - return - } - if years && len(token.TValue) > 2 { + if strings.Contains(strings.ToUpper(token.TValue), "Y") { + if len(token.TValue) <= 2 { + nf.result += strconv.Itoa(nf.t.Year())[2:] + return + } nf.result += strconv.Itoa(nf.t.Year()) return } + if strings.Contains(strings.ToUpper(token.TValue), "G") { + for i := len(japaneseEraYears) - 1; i > 0; i-- { + if y := japaneseEraYears[i]; nf.t.After(y) { + switch len(token.TValue) { + case 1: + nf.result += japaneseEraSymbols[i] + case 2: + nf.result += japaneseEraNames[i][:3] + default: + nf.result += japaneseEraNames[i] + } + year := nf.t.Year() - y.Year() + 1 + if year == 1 && len(token.TValue) > 1 && supportedLanguageInfo[nf.localCode].useGannen { + nf.result += "\u5143" + break + } + nf.result += strconv.Itoa(year) + break + } + } + } } // daysHandler will be handling days in the date and times types tokens for a diff --git a/numfmt_test.go b/numfmt_test.go index cdd3f2eeb3..05646a027f 100644 --- a/numfmt_test.go +++ b/numfmt_test.go @@ -420,6 +420,18 @@ func TestNumFmt(t *testing.T) { {"44835.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM", "\u0e15 01 2022 4:32 AM"}, {"44866.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM", "\u0e1e 01 2022 4:32 AM"}, {"44896.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM", "\u0e18 01 2022 4:32 AM"}, + {"43709", "[$-411]ge\"年\"m\"月\"d\"日\";@", "R1年9月1日"}, + {"43709", "[$-411]gge\"年\"m\"月\"d\"日\";@", "\u4EE41年9月1日"}, + {"43709", "[$-411]ggge\"年\"m\"月\"d\"日\";@", "\u4EE4\u548C1年9月1日"}, + {"43709", "[$-ja-JP-x-gannen,80]ge\"年\"m\"月\"d\"日\";@", "R1年9月1日"}, + {"43709", "[$-ja-JP-x-gannen,80]gge\"年\"m\"月\"d\"日\";@", "\u4EE4\u5143年9月1日"}, + {"43709", "[$-ja-JP-x-gannen,80]ggge\"年\"m\"月\"d\"日\";@", "\u4EE4\u548C\u5143年9月1日"}, + {"43466.189571759256", "[$-411]ge\"年\"m\"月\"d\"日\";@", "H31年1月1日"}, + {"43466.189571759256", "[$-411]gge\"年\"m\"月\"d\"日\";@", "\u5E7331年1月1日"}, + {"43466.189571759256", "[$-411]ggge\"年\"m\"月\"d\"日\";@", "\u5E73\u621031年1月1日"}, + {"44896.18957170139", "[$-411]ge\"年\"m\"月\"d\"日\";@", "R4年12月1日"}, + {"44896.18957170139", "[$-411]gge\"年\"m\"月\"d\"日\";@", "\u4EE44年12月1日"}, + {"44896.18957170139", "[$-411]ggge\"年\"m\"月\"d\"日\";@", "\u4EE4\u548C4年12月1日"}, {"44562.189571759256", "[$-51]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f21 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, {"44593.189571759256", "[$-51]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f22 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, {"44621.18957170139", "[$-51]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f23 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, diff --git a/vml.go b/vml.go index 540e36ba17..844149e5c3 100644 --- a/vml.go +++ b/vml.go @@ -420,9 +420,15 @@ func (f *File) DeleteFormControl(sheet, cell string) error { for i, sp := range vml.Shape { var shapeVal decodeShapeVal if err = xml.Unmarshal([]byte(fmt.Sprintf("%s", sp.Val)), &shapeVal); err == nil && - shapeVal.ClientData.ObjectType != "Note" && shapeVal.ClientData.Column == col-1 && shapeVal.ClientData.Row == row-1 { - vml.Shape = append(vml.Shape[:i], vml.Shape[i+1:]...) - break + shapeVal.ClientData.ObjectType != "Note" && shapeVal.ClientData.Anchor != "" { + leftCol, topRow, err := extractAnchorCell(shapeVal.ClientData.Anchor) + if err != nil { + return err + } + if leftCol == col-1 && topRow == row-1 { + vml.Shape = append(vml.Shape[:i], vml.Shape[i+1:]...) + break + } } } f.VMLDrawing[drawingVML] = vml @@ -454,7 +460,7 @@ func (f *File) decodeVMLDrawingReader(path string) (*decodeVmlDrawing, error) { 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(bytesReplace(namespaceStrictToTransitional(c.([]byte)), []byte("
\r\n"), []byte("

\r\n"), -1))). Decode(f.DecodeVMLDrawing[path]); err != nil && err != io.EOF { return nil, err } @@ -574,6 +580,9 @@ func formCtrlText(opts *vmlOptions) []vmlFont { if run.Font.Underline == "single" { fnt.Content = "" + fnt.Content + "" } + if run.Font.Underline == "double" { + fnt.Content = "" + fnt.Content + "" + } if run.Font.Italic { fnt.Content = "" + fnt.Content + "" } @@ -765,8 +774,8 @@ func (f *File) addFormCtrlShape(preset formCtrlPreset, col, row int, anchor stri ObjectType: preset.objectType, Anchor: anchor, AutoFill: preset.autoFill, - Row: row - 1, - Column: col - 1, + Row: intPtr(row - 1), + Column: intPtr(col - 1), TextHAlign: preset.textHAlign, TextVAlign: preset.textVAlign, NoThreeD: preset.noThreeD, @@ -885,8 +894,8 @@ func (f *File) addDrawingVML(dataID int, drawingVML string, opts *vmlOptions) er } // GetFormControls retrieves all form controls in a worksheet by a given -// worksheet name. Note that, this function does not support getting the width, -// height, text, rich text, and format currently. +// worksheet name. Note that, this function does not support getting the width +// and height of the form controls currently. func (f *File) GetFormControls(sheet string) ([]FormControl, error) { var formControls []FormControl // Read sheet data @@ -949,9 +958,18 @@ func extractFormControl(clientData string) (FormControl, error) { return formControl, err } for formCtrlType, preset := range formCtrlPresets { - if shapeVal.ClientData.ObjectType == preset.objectType { + if shapeVal.ClientData.ObjectType == preset.objectType && shapeVal.ClientData.Anchor != "" { + formControl.Paragraph = extractVMLFont(shapeVal.TextBox.Div.Font) + if len(formControl.Paragraph) > 0 && formControl.Paragraph[0].Font == nil { + formControl.Text = formControl.Paragraph[0].Text + formControl.Paragraph = formControl.Paragraph[1:] + } formControl.Type = formCtrlType - if formControl.Cell, err = CoordinatesToCellName(shapeVal.ClientData.Column+1, shapeVal.ClientData.Row+1); err != nil { + col, row, err := extractAnchorCell(shapeVal.ClientData.Anchor) + if err != nil { + return formControl, err + } + if formControl.Cell, err = CoordinatesToCellName(col+1, row+1); err != nil { return formControl, err } formControl.Macro = shapeVal.ClientData.FmlaMacro @@ -967,3 +985,79 @@ func extractFormControl(clientData string) (FormControl, error) { } return formControl, err } + +// extractAnchorCell extract left-top cell coordinates from given VML anchor +// comma-separated list values. +func extractAnchorCell(anchor string) (int, int, error) { + var ( + leftCol, topRow int + err error + pos = strings.Split(anchor, ",") + ) + if len(pos) != 8 { + return leftCol, topRow, ErrParameterInvalid + } + leftCol, err = strconv.Atoi(strings.TrimSpace(pos[0])) + if err != nil { + return leftCol, topRow, ErrColumnNumber + } + topRow, err = strconv.Atoi(strings.TrimSpace(pos[2])) + return leftCol, topRow, err +} + +// extractVMLFont extract rich-text and font format from given VML font element. +func extractVMLFont(font []decodeVMLFont) []RichTextRun { + var runs []RichTextRun + extractU := func(u *decodeVMLFontU, run *RichTextRun) { + if u == nil { + return + } + run.Text += u.Val + if run.Font == nil { + run.Font = &Font{} + } + run.Font.Underline = "single" + if u.Class == "font1" { + run.Font.Underline = "double" + } + } + extractI := func(i *decodeVMLFontI, run *RichTextRun) { + if i == nil { + return + } + extractU(i.U, run) + run.Text += i.Val + if run.Font == nil { + run.Font = &Font{} + } + run.Font.Italic = true + } + extractB := func(b *decodeVMLFontB, run *RichTextRun) { + if b == nil { + return + } + extractI(b.I, run) + run.Text += b.Val + if run.Font == nil { + run.Font = &Font{} + } + run.Font.Bold = true + } + for _, fnt := range font { + var run RichTextRun + extractB(fnt.B, &run) + extractI(fnt.I, &run) + extractU(fnt.U, &run) + run.Text += fnt.Val + if fnt.Face != "" || fnt.Size > 0 || fnt.Color != "" { + if run.Font == nil { + run.Font = &Font{} + } + run.Font.Family = fnt.Face + run.Font.Size = float64(fnt.Size / 20) + run.Font.Color = fnt.Color + } + runs = append(runs, run) + } + return runs +} diff --git a/vmlDrawing.go b/vmlDrawing.go index 76e90115c4..7ebedb7aeb 100644 --- a/vmlDrawing.go +++ b/vmlDrawing.go @@ -137,8 +137,8 @@ type xClientData struct { FmlaMacro string `xml:"x:FmlaMacro,omitempty"` TextHAlign string `xml:"x:TextHAlign,omitempty"` TextVAlign string `xml:"x:TextVAlign,omitempty"` - Row int `xml:"x:Row"` - Column int `xml:"x:Column"` + Row *int `xml:"x:Row"` + Column *int `xml:"x:Column"` Checked int `xml:"x:Checked,omitempty"` FmlaLink string `xml:"x:FmlaLink,omitempty"` NoThreeD *string `xml:"x:NoThreeD"` @@ -185,16 +185,59 @@ type decodeShape struct { // decodeShapeVal defines the structure used to parse the sub-element of the // shape in the file xl/drawings/vmlDrawing%d.vml. type decodeShapeVal struct { + TextBox decodeVMLTextBox `xml:"textbox"` ClientData decodeVMLClientData `xml:"ClientData"` } +// decodeVMLFontU defines the structure used to parse the u element in the VML. +type decodeVMLFontU struct { + Class string `xml:"class,attr"` + Val string `xml:",chardata"` +} + +// decodeVMLFontI defines the structure used to parse the i element in the VML. +type decodeVMLFontI struct { + U *decodeVMLFontU `xml:"u"` + Val string `xml:",chardata"` +} + +// decodeVMLFontB defines the structure used to parse the b element in the VML. +type decodeVMLFontB struct { + I *decodeVMLFontI `xml:"i"` + U *decodeVMLFontU `xml:"u"` + Val string `xml:",chardata"` +} + +// decodeVMLFont defines the structure used to parse the font element in the VML. +type decodeVMLFont struct { + Face string `xml:"face,attr,omitempty"` + Size uint `xml:"size,attr,omitempty"` + Color string `xml:"color,attr,omitempty"` + B *decodeVMLFontB `xml:"b"` + I *decodeVMLFontI `xml:"i"` + U *decodeVMLFontU `xml:"u"` + Val string `xml:",chardata"` +} + +// decodeVMLDiv defines the structure used to parse the div element in the VML. +type decodeVMLDiv struct { + Font []decodeVMLFont `xml:"font"` +} + +// decodeVMLTextBox defines the structure used to parse the v:textbox element in +// the file xl/drawings/vmlDrawing%d.vml. +type decodeVMLTextBox struct { + Div decodeVMLDiv `xml:"div"` +} + // decodeVMLClientData defines the structure used to parse the x:ClientData // element in the file xl/drawings/vmlDrawing%d.vml. type decodeVMLClientData struct { ObjectType string `xml:"ObjectType,attr"` + Anchor string FmlaMacro string - Column int - Row int + Column *int + Row *int Checked int FmlaLink string Val uint diff --git a/vml_test.go b/vml_test.go index 52a7dada51..34d7f25d05 100644 --- a/vml_test.go +++ b/vml_test.go @@ -164,7 +164,7 @@ func TestFormControl(t *testing.T) { }, { Cell: "A1", Type: FormControlButton, Macro: "Button1_Click", - Width: 140, Height: 60, Text: "Button 1\r\n", + Width: 140, Height: 60, Text: "Button 1\n", Paragraph: []RichTextRun{ { Font: &Font{ @@ -234,6 +234,8 @@ func TestFormControl(t *testing.T) { assert.Equal(t, formCtrl.IncChange, result[i].IncChange) assert.Equal(t, formCtrl.Horizontally, result[i].Horizontally) assert.Equal(t, formCtrl.CellLink, result[i].CellLink) + assert.Equal(t, formCtrl.Text, result[i].Text) + assert.Equal(t, len(formCtrl.Paragraph), len(result[i].Paragraph)) } assert.NoError(t, f.SetSheetProps("Sheet1", &SheetPropsOptions{CodeName: stringPtr("Sheet1")})) file, err := os.ReadFile(filepath.Join("test", "vbaProject.bin")) @@ -249,7 +251,8 @@ func TestFormControl(t *testing.T) { assert.Len(t, result, 11) // Test add from control to a worksheet which already contains form controls assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ - Cell: "D4", Type: FormControlButton, Macro: "Button1_Click", Text: "Button 2", + Cell: "D4", Type: FormControlButton, Macro: "Button1_Click", + Paragraph: []RichTextRun{{Font: &Font{Underline: "double"}, Text: "Button 2"}}, })) // Test get from controls after add form controls result, err = f.GetFormControls("Sheet1") @@ -297,6 +300,11 @@ func TestFormControl(t *testing.T) { assert.NoError(t, err) f.Pkg.Store("xl/drawings/vmlDrawing1.vml", MacintoshCyrillicCharset) assert.Error(t, f.DeleteFormControl("Sheet1", "A1"), "XML syntax error on line 1: invalid UTF-8") + // Test delete form controls with invalid shape anchor + f.DecodeVMLDrawing["xl/drawings/vmlDrawing1.vml"] = &decodeVmlDrawing{ + Shape: []decodeShape{{Type: "#_x0000_t201", Val: "0"}}, + } + assert.Equal(t, ErrParameterInvalid, f.DeleteFormControl("Sheet1", "A1")) assert.NoError(t, f.Close()) // Test delete form control on a worksheet without form control f = NewFile() @@ -320,9 +328,39 @@ func TestFormControl(t *testing.T) { formControls, err = f.GetFormControls("Sheet1") assert.NoError(t, err) assert.Len(t, formControls, 0) + // Test get form controls with bold font format + f.DecodeVMLDrawing["xl/drawings/vmlDrawing1.vml"] = &decodeVmlDrawing{ + Shape: []decodeShape{{Type: "#_x0000_t201", Val: "
Text
0,0,0,0,0,0,0,0"}}, + } + formControls, err = f.GetFormControls("Sheet1") + assert.NoError(t, err) + assert.True(t, formControls[0].Paragraph[0].Font.Bold) + // Test get form controls with italic font format + f.DecodeVMLDrawing["xl/drawings/vmlDrawing1.vml"] = &decodeVmlDrawing{ + Shape: []decodeShape{{Type: "#_x0000_t201", Val: "
Text
0,0,0,0,0,0,0,0"}}, + } + formControls, err = f.GetFormControls("Sheet1") + assert.NoError(t, err) + assert.True(t, formControls[0].Paragraph[0].Font.Italic) + // Test get form controls with font format + f.DecodeVMLDrawing["xl/drawings/vmlDrawing1.vml"] = &decodeVmlDrawing{ + Shape: []decodeShape{{Type: "#_x0000_t201", Val: "
Text
0,0,0,0,0,0,0,0"}}, + } + formControls, err = f.GetFormControls("Sheet1") + assert.NoError(t, err) + assert.Equal(t, "Calibri", formControls[0].Paragraph[0].Font.Family) + assert.Equal(t, 14.0, formControls[0].Paragraph[0].Font.Size) + assert.Equal(t, "#777777", formControls[0].Paragraph[0].Font.Color) + // Test get form controls with italic font format + f.DecodeVMLDrawing["xl/drawings/vmlDrawing1.vml"] = &decodeVmlDrawing{ + Shape: []decodeShape{{Type: "#_x0000_t201", Val: "
Text
0,0,0,0,0,0,0,0"}}, + } + formControls, err = f.GetFormControls("Sheet1") + assert.NoError(t, err) + assert.True(t, formControls[0].Paragraph[0].Font.Italic) // Test get form controls with invalid column number f.DecodeVMLDrawing["xl/drawings/vmlDrawing1.vml"] = &decodeVmlDrawing{ - Shape: []decodeShape{{Type: "#_x0000_t201", Val: fmt.Sprintf("%d", MaxColumns)}}, + Shape: []decodeShape{{Type: "#_x0000_t201", Val: fmt.Sprintf("%d,0,0,0,0,0,0,0", MaxColumns)}}, } formControls, err = f.GetFormControls("Sheet1") assert.Equal(t, err, ErrColumnNumber) @@ -343,11 +381,18 @@ func TestFormControl(t *testing.T) { assert.Len(t, formControls, 0) // Test get form controls with invalid column number f.VMLDrawing["xl/drawings/vmlDrawing1.vml"] = &vmlDrawing{ - Shape: []xlsxShape{{Type: "#_x0000_t201", Val: fmt.Sprintf("%d", MaxColumns)}}, + Shape: []xlsxShape{{Type: "#_x0000_t201", Val: fmt.Sprintf("%d,0,0,0,0,0,0,0", MaxColumns)}}, } formControls, err = f.GetFormControls("Sheet1") assert.Equal(t, err, ErrColumnNumber) assert.Len(t, formControls, 0) + // Test get form controls with invalid shape anchor + f.VMLDrawing["xl/drawings/vmlDrawing1.vml"] = &vmlDrawing{ + Shape: []xlsxShape{{Type: "#_x0000_t201", Val: "x,0,0,0,0,0,0,0"}}, + } + formControls, err = f.GetFormControls("Sheet1") + assert.Equal(t, ErrColumnNumber, err) + assert.Len(t, formControls, 0) // Test get form controls with comment (Note) shape type f.VMLDrawing["xl/drawings/vmlDrawing1.vml"] = &vmlDrawing{ Shape: []xlsxShape{{Type: "#_x0000_t201", Val: ""}}, diff --git a/xmlStyles.go b/xmlStyles.go index 3a56f6f618..80d9959404 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -300,8 +300,9 @@ type xlsxNumFmts struct { // format properties which indicate how to format and render the numeric value // of a cell. type xlsxNumFmt struct { - NumFmtID int `xml:"numFmtId,attr"` - FormatCode string `xml:"formatCode,attr,omitempty"` + NumFmtID int `xml:"numFmtId,attr"` + FormatCode string `xml:"formatCode,attr,omitempty"` + FormatCode16 string `xml:"http://schemas.microsoft.com/office/spreadsheetml/2015/02/main formatCode16,attr,omitempty"` } // xlsxStyleColors directly maps the colors' element. Color information From aa3c79a8118a4b5d9926644fb7a1e022b0f4607b Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 1 Aug 2023 00:11:02 +0800 Subject: [PATCH 775/957] Support apply number format with the Japanese era years --- numfmt.go | 89 ++++++++++++++++++++++++++++++++------------------ numfmt_test.go | 13 ++++++++ 2 files changed, 71 insertions(+), 31 deletions(-) diff --git a/numfmt.go b/numfmt.go index 59a7154d5d..76afa26954 100644 --- a/numfmt.go +++ b/numfmt.go @@ -33,18 +33,18 @@ 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 - sectionIdx int - 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 + opts *Options + cellType CellType + section []nfp.Section + t time.Time + sectionIdx int + date1904, isNumeric, hours, seconds, useMillisecond, useGannen 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 } // CultureName is the type of supported language country codes types for apply @@ -797,6 +797,7 @@ var ( "11": {tags: []string{"ja"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese}, "411": {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese}, "800411": {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese}, + "JP-X-GANNEN": {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese}, "JP-X-GANNEN,80": {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese, useGannen: true}, "12": {tags: []string{"ko"}, localMonth: localMonthsNameKorean, apFmt: apFmtKorean}, "412": {tags: []string{"ko-KR"}, localMonth: localMonthsNameKorean, apFmt: apFmtKorean}, @@ -1344,7 +1345,7 @@ func (nf *numberFormat) dateTimeHandler() string { if changeNumFmtCode, err := nf.currencyLanguageHandler(token); err != nil || changeNumFmtCode { return nf.value } - if !supportedLanguageInfo[nf.localCode].useGannen { + if !strings.EqualFold(nf.localCode, "JP-X-GANNEN") && !strings.EqualFold(nf.localCode, "JP-X-GANNEN,80") { nf.result += nf.currencyString } } @@ -1766,6 +1767,18 @@ func (nf *numberFormat) dateTimesHandler(i int, token nfp.Token) { nf.secondsHandler(token) } +// eraYear convert time to the Japanese era years. +func eraYear(t time.Time) (int, int) { + i, year := 0, -1 + for i = len(japaneseEraYears) - 1; i > 0; i-- { + if y := japaneseEraYears[i]; t.After(y) { + year = t.Year() - y.Year() + 1 + break + } + } + return i, year +} + // yearsHandler will be handling years in the date and times types tokens for a // number format expression. func (nf *numberFormat) yearsHandler(token nfp.Token) { @@ -1778,24 +1791,38 @@ func (nf *numberFormat) yearsHandler(token nfp.Token) { return } if strings.Contains(strings.ToUpper(token.TValue), "G") { - for i := len(japaneseEraYears) - 1; i > 0; i-- { - if y := japaneseEraYears[i]; nf.t.After(y) { - switch len(token.TValue) { - case 1: - nf.result += japaneseEraSymbols[i] - case 2: - nf.result += japaneseEraNames[i][:3] - default: - nf.result += japaneseEraNames[i] - } - year := nf.t.Year() - y.Year() + 1 - if year == 1 && len(token.TValue) > 1 && supportedLanguageInfo[nf.localCode].useGannen { - nf.result += "\u5143" - break - } - nf.result += strconv.Itoa(year) - break - } + i, year := eraYear(nf.t) + if year == -1 { + return + } + nf.useGannen = supportedLanguageInfo[nf.localCode].useGannen + switch len(token.TValue) { + case 1: + nf.useGannen = false + nf.result += japaneseEraSymbols[i] + case 2: + nf.result += japaneseEraNames[i][:3] + default: + nf.result += japaneseEraNames[i] + } + return + } + if strings.Contains(strings.ToUpper(token.TValue), "E") { + _, year := eraYear(nf.t) + if year == -1 { + nf.result += strconv.Itoa(nf.t.Year()) + return + } + if year == 1 && nf.useGannen { + nf.result += "\u5143" + return + } + if len(token.TValue) == 1 && !nf.useGannen { + nf.result += strconv.Itoa(year) + return + } + if len(token.TValue) == 2 { + nf.result += fmt.Sprintf("%02d", year) } } } diff --git a/numfmt_test.go b/numfmt_test.go index 05646a027f..971d629965 100644 --- a/numfmt_test.go +++ b/numfmt_test.go @@ -420,12 +420,25 @@ func TestNumFmt(t *testing.T) { {"44835.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM", "\u0e15 01 2022 4:32 AM"}, {"44866.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM", "\u0e1e 01 2022 4:32 AM"}, {"44896.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM", "\u0e18 01 2022 4:32 AM"}, + {"100", "[$-411]ge\"年\"m\"月\"d\"日\";@", "1900年4月9日"}, {"43709", "[$-411]ge\"年\"m\"月\"d\"日\";@", "R1年9月1日"}, {"43709", "[$-411]gge\"年\"m\"月\"d\"日\";@", "\u4EE41年9月1日"}, {"43709", "[$-411]ggge\"年\"m\"月\"d\"日\";@", "\u4EE4\u548C1年9月1日"}, + {"43709", "[$-411]gee\"年\"m\"月\"d\"日\";@", "R01年9月1日"}, + {"43709", "[$-411]ggee\"年\"m\"月\"d\"日\";@", "\u4EE401年9月1日"}, + {"43709", "[$-411]gggee\"年\"m\"月\"d\"日\";@", "\u4EE4\u548C01年9月1日"}, + {"43709", "[$-ja-JP-x-gannen]ge\"年\"m\"月\"d\"日\";@", "R1年9月1日"}, + {"43709", "[$-ja-JP-x-gannen]gge\"年\"m\"月\"d\"日\";@", "\u4EE41年9月1日"}, + {"43709", "[$-ja-JP-x-gannen]ggge\"年\"m\"月\"d\"日\";@", "\u4EE4\u548C1年9月1日"}, + {"43709", "[$-ja-JP-x-gannen]gee\"年\"m\"月\"d\"日\";@", "R01年9月1日"}, + {"43709", "[$-ja-JP-x-gannen]ggee\"年\"m\"月\"d\"日\";@", "\u4EE401年9月1日"}, + {"43709", "[$-ja-JP-x-gannen]gggee\"年\"m\"月\"d\"日\";@", "\u4EE4\u548C01年9月1日"}, {"43709", "[$-ja-JP-x-gannen,80]ge\"年\"m\"月\"d\"日\";@", "R1年9月1日"}, {"43709", "[$-ja-JP-x-gannen,80]gge\"年\"m\"月\"d\"日\";@", "\u4EE4\u5143年9月1日"}, {"43709", "[$-ja-JP-x-gannen,80]ggge\"年\"m\"月\"d\"日\";@", "\u4EE4\u548C\u5143年9月1日"}, + {"43709", "[$-ja-JP-x-gannen,80]gee\"年\"m\"月\"d\"日\";@", "R01年9月1日"}, + {"43709", "[$-ja-JP-x-gannen,80]ggee\"年\"m\"月\"d\"日\";@", "\u4EE4\u5143年9月1日"}, + {"43709", "[$-ja-JP-x-gannen,80]gggee\"年\"m\"月\"d\"日\";@", "\u4EE4\u548C\u5143年9月1日"}, {"43466.189571759256", "[$-411]ge\"年\"m\"月\"d\"日\";@", "H31年1月1日"}, {"43466.189571759256", "[$-411]gge\"年\"m\"月\"d\"日\";@", "\u5E7331年1月1日"}, {"43466.189571759256", "[$-411]ggge\"年\"m\"月\"d\"日\";@", "\u5E73\u621031年1月1日"}, From eb175906e7cca0dbe8e276d9b4ec4aa5bb356ca2 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 6 Aug 2023 00:02:48 +0800 Subject: [PATCH 776/957] This fixes #1599, and improve date and time number format - Fix basic arithmetic operator priority issues - Support apply date and time number format with 52 languages: Estonian, Faroese, Filipino, Finnish, Frisian, Fulah, Galician, Georgian, Greek, Greenlandic, Guarani, Gujarati, Hausa, Hawaiian, Hebrew, Hindi, Hungarian, Icelandic, Igbo, Indonesian, Inuktitut, Kannada, Kashmiri, Kazakh, Khmer, Kiche, Kinyarwanda, Kiswahili, Konkani, Kyrgyz, Lao, Latin, Latvian, Lithuanian, Luxembourgish, Macedonian, Malay, Malayalam, Maltese, Maori, Mapudungun, Marathi, Mohawk, Morocco, Nepali, Nigeria, Norwegian, Occitan, Odia, Oromo, Pashto and Syllabics - Support apply the Chinese weekdays' number formats - Update the unit test and dependencies modules --- calc.go | 14 +- calc_test.go | 67 +- go.mod | 6 +- go.sum | 12 +- numfmt.go | 1659 +++++++++++++++++++++++++++++++++++++++++++++--- numfmt_test.go | 1060 ++++++++++++++++++++++++++++++- 6 files changed, 2656 insertions(+), 162 deletions(-) diff --git a/calc.go b/calc.go index 66493bfa97..048be02e2a 100644 --- a/calc.go +++ b/calc.go @@ -108,13 +108,13 @@ var ( "/": 4, "+": 3, "-": 3, - "=": 2, - "<>": 2, - "<": 2, - "<=": 2, - ">": 2, - ">=": 2, - "&": 1, + "&": 2, + "=": 1, + "<>": 1, + "<": 1, + "<=": 1, + ">": 1, + ">=": 1, } month2num = map[string]int{ "january": 1, diff --git a/calc_test.go b/calc_test.go index 702aeb4d88..96dfbdda2f 100644 --- a/calc_test.go +++ b/calc_test.go @@ -35,37 +35,42 @@ func TestCalcCellValue(t *testing.T) { {nil, nil, nil, "Feb", "South 2", 45500}, } mathCalc := map[string]string{ - "=2^3": "8", - "=1=1": "TRUE", - "=1=2": "FALSE", - "=1<2": "TRUE", - "=3<2": "FALSE", - "=1<\"-1\"": "TRUE", - "=\"-1\"<1": "FALSE", - "=\"-1\"<\"-2\"": "TRUE", - "=2<=3": "TRUE", - "=2<=1": "FALSE", - "=1<=\"-1\"": "TRUE", - "=\"-1\"<=1": "FALSE", - "=\"-1\"<=\"-2\"": "TRUE", - "=2>1": "TRUE", - "=2>3": "FALSE", - "=1>\"-1\"": "FALSE", - "=\"-1\">-1": "TRUE", - "=\"-1\">\"-2\"": "FALSE", - "=2>=1": "TRUE", - "=2>=3": "FALSE", - "=1>=\"-1\"": "FALSE", - "=\"-1\">=-1": "TRUE", - "=\"-1\">=\"-2\"": "FALSE", - "=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", + "=2^3": "8", + "=1=1": "TRUE", + "=1=2": "FALSE", + "=1<2": "TRUE", + "=3<2": "FALSE", + "=1<\"-1\"": "TRUE", + "=\"-1\"<1": "FALSE", + "=\"-1\"<\"-2\"": "TRUE", + "=2<=3": "TRUE", + "=2<=1": "FALSE", + "=1<=\"-1\"": "TRUE", + "=\"-1\"<=1": "FALSE", + "=\"-1\"<=\"-2\"": "TRUE", + "=2>1": "TRUE", + "=2>3": "FALSE", + "=1>\"-1\"": "FALSE", + "=\"-1\">-1": "TRUE", + "=\"-1\">\"-2\"": "FALSE", + "=2>=1": "TRUE", + "=2>=3": "FALSE", + "=1>=\"-1\"": "FALSE", + "=\"-1\">=-1": "TRUE", + "=\"-1\">=\"-2\"": "FALSE", + "=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", + "=TRUE()&FALSE()": "TRUEFALSE", + "=TRUE()&FALSE()<>FALSE": "TRUE", + "=TRUE()&\"1\"": "TRUE1", + "=TRUE<>FALSE()": "TRUE", + "=TRUE<>1&\"x\"": "TRUE", // Engineering Functions // BESSELI "=BESSELI(4.5,1)": "15.3892227537359", diff --git a/go.mod b/go.mod index 9395c13e73..11d3adafd6 100644 --- a/go.mod +++ b/go.mod @@ -6,11 +6,11 @@ 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-20230422071738-01f4e37c47e9 - github.com/xuri/nfp v0.0.0-20230730012209-aee513b45ff4 + github.com/xuri/efp v0.0.0-20230802181842-ad255f2331ca + github.com/xuri/nfp v0.0.0-20230802015359-2d5eeba905e9 golang.org/x/crypto v0.11.0 golang.org/x/image v0.5.0 - golang.org/x/net v0.12.0 + golang.org/x/net v0.13.0 golang.org/x/text v0.11.0 ) diff --git a/go.sum b/go.sum index 74da2d5172..ce3d29d310 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-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-20230730012209-aee513b45ff4 h1:7TXNzvlvE0E/oLDazWm2Xip72G9Su+jRzvziSxwO6Ww= -github.com/xuri/nfp v0.0.0-20230730012209-aee513b45ff4/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= +github.com/xuri/efp v0.0.0-20230802181842-ad255f2331ca h1:uvPMDVyP7PXMMioYdyPH+0O+Ta/UO1WFfNYMO3Wz0eg= +github.com/xuri/efp v0.0.0-20230802181842-ad255f2331ca/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= +github.com/xuri/nfp v0.0.0-20230802015359-2d5eeba905e9 h1:jmhvNv5by7bXDzzjzBXaIWmEI4lMYfv5iJtI5Pw5/aM= +github.com/xuri/nfp v0.0.0-20230802015359-2d5eeba905e9/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= @@ -33,8 +33,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v 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.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= -golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= +golang.org/x/net v0.13.0 h1:Nvo8UFsZ8X3BhAC9699Z1j7XQ3rsZnUUm7jfBEk1ueY= +golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= 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= diff --git a/numfmt.go b/numfmt.go index 76afa26954..c82fe7c81d 100644 --- a/numfmt.go +++ b/numfmt.go @@ -770,7 +770,7 @@ var ( "wae-CH", "wal", "wae-ET", "yav", "yav-CM", "yo-BJ", "dje", "dje-NE", }, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, "C09": {tags: []string{"en-AU"}, localMonth: localMonthsNameEnglish, apFmt: strings.ToLower(nfp.AmPm[0])}, - "2829": {tags: []string{"en-BZ"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, + "2809": {tags: []string{"en-BZ"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, "1009": {tags: []string{"en-CA"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, "2409": {tags: []string{"en-029"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, "3C09": {tags: []string{"en-HK"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, @@ -787,23 +787,156 @@ var ( "809": {tags: []string{"en-GB"}, localMonth: localMonthsNameEnglish, apFmt: strings.ToLower(nfp.AmPm[0])}, "409": {tags: []string{"en-US"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, "3009": {tags: []string{"en-ZW"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, + "25": {tags: []string{"et"}, localMonth: localMonthsNameEstonian, apFmt: nfp.AmPm[0]}, + "425": {tags: []string{"et-EE"}, localMonth: localMonthsNameEstonian, apFmt: nfp.AmPm[0]}, + "38": {tags: []string{"fo"}, localMonth: localMonthsNameFaroese, apFmt: apFmtFaroese}, + "438": {tags: []string{"fo-FO"}, localMonth: localMonthsNameFaroese, apFmt: apFmtFaroese}, + "64": {tags: []string{"fil"}, localMonth: localMonthsNameFilipino, apFmt: nfp.AmPm[0]}, + "464": {tags: []string{"fil-PH"}, localMonth: localMonthsNameFilipino, apFmt: nfp.AmPm[0]}, + "B": {tags: []string{"fi"}, localMonth: localMonthsNameFinnish, apFmt: apFmtFinnish}, + "40B": {tags: []string{"fi-FI"}, localMonth: localMonthsNameFinnish, apFmt: apFmtFinnish}, "C": {tags: []string{"fr"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0]}, + "80C": {tags: []string{"fr-BE"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0]}, + "2C0C": {tags: []string{"fr-CM"}, localMonth: localMonthsNameFrench, apFmt: apFmtCameroon}, + "C0C": {tags: []string{"fr-CA"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0]}, + "1C0C": {tags: []string{"fr-029"}, localMonth: localMonthsNameCaribbean, apFmt: nfp.AmPm[0]}, + "240C": {tags: []string{"fr-CD"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0]}, + "300C": {tags: []string{"fr-CI"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0]}, + "40C": {tags: []string{"fr-FR"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0]}, + "3C0C": {tags: []string{"fr-HT"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0]}, + "140C": {tags: []string{"fr-LU"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0]}, + "340C": {tags: []string{"fr-ML"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0]}, + "380C": {tags: []string{"fr-MA"}, localMonth: localMonthsNameMorocco, apFmt: nfp.AmPm[0]}, + "180C": {tags: []string{"fr-MC"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0]}, + "200C": {tags: []string{"fr-RE"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0]}, + "280C": {tags: []string{"fr-SN"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0]}, + "62": {tags: []string{"fy"}, localMonth: localMonthsNameFrisian, apFmt: nfp.AmPm[0]}, + "462": {tags: []string{"fy-NL"}, localMonth: localMonthsNameFrisian, apFmt: nfp.AmPm[0]}, + "67": {tags: []string{"ff"}, localMonth: localMonthsNameFulah, apFmt: nfp.AmPm[0]}, + "7C67": {tags: []string{"ff-Latn"}, localMonth: localMonthsNameFulah, apFmt: nfp.AmPm[0]}, + "467": {tags: []string{"ff-NG", "ff-Latn-NG"}, localMonth: localMonthsNameNigeria, apFmt: apFmtNigeria}, + "867": {tags: []string{"ff-SN"}, localMonth: localMonthsNameNigeria, apFmt: nfp.AmPm[0]}, + "56": {tags: []string{"gl"}, localMonth: localMonthsNameGalician, apFmt: apFmtCuba}, + "456": {tags: []string{"gl-ES"}, localMonth: localMonthsNameGalician, apFmt: apFmtCuba}, + "37": {tags: []string{"ka"}, localMonth: localMonthsNameGeorgian, apFmt: nfp.AmPm[0]}, + "437": {tags: []string{"ka-GE"}, localMonth: localMonthsNameGeorgian, apFmt: nfp.AmPm[0]}, "7": {tags: []string{"de"}, localMonth: localMonthsNameGerman, apFmt: nfp.AmPm[0]}, "C07": {tags: []string{"de-AT"}, localMonth: localMonthsNameAustria, apFmt: nfp.AmPm[0]}, "407": {tags: []string{"de-DE"}, localMonth: localMonthsNameGerman, apFmt: nfp.AmPm[0]}, + "1407": {tags: []string{"de-LI"}, localMonth: localMonthsNameGerman, apFmt: nfp.AmPm[0]}, + "807": {tags: []string{"de-CH"}, localMonth: localMonthsNameGerman, apFmt: nfp.AmPm[0]}, + "8": {tags: []string{"el"}, localMonth: localMonthsNameGreek, apFmt: apFmtGreek}, + "408": {tags: []string{"el-GR"}, localMonth: localMonthsNameGreek, apFmt: apFmtGreek}, + "6F": {tags: []string{"kl"}, localMonth: localMonthsNameGreenlandic, apFmt: nfp.AmPm[0]}, + "46F": {tags: []string{"kl-GL"}, localMonth: localMonthsNameGreenlandic, apFmt: nfp.AmPm[0]}, + "74": {tags: []string{"gn"}, localMonth: localMonthsNameGuarani, apFmt: apFmtCuba}, + "474": {tags: []string{"gn-PY"}, localMonth: localMonthsNameGuarani, apFmt: apFmtCuba}, + "47": {tags: []string{"gu"}, localMonth: localMonthsNameGujarati, apFmt: apFmtGujarati}, + "447": {tags: []string{"gu-IN"}, localMonth: localMonthsNameGujarati, apFmt: apFmtGujarati}, + "68": {tags: []string{"ha"}, localMonth: localMonthsNameHausa, apFmt: nfp.AmPm[0]}, + "7C68": {tags: []string{"ha-Latn"}, localMonth: localMonthsNameHausa, apFmt: nfp.AmPm[0]}, + "468": {tags: []string{"ha-Latn-NG"}, localMonth: localMonthsNameHausa, apFmt: nfp.AmPm[0]}, + "75": {tags: []string{"haw"}, localMonth: localMonthsNameHawaiian, apFmt: nfp.AmPm[0]}, + "475": {tags: []string{"haw-US"}, localMonth: localMonthsNameHawaiian, apFmt: nfp.AmPm[0]}, + "D": {tags: []string{"he"}, localMonth: localMonthsNameHebrew, apFmt: nfp.AmPm[0]}, + "40D": {tags: []string{"he-IL"}, localMonth: localMonthsNameHebrew, apFmt: nfp.AmPm[0]}, + "39": {tags: []string{"hi"}, localMonth: localMonthsNameHindi, apFmt: apFmtHindi}, + "439": {tags: []string{"hi-IN"}, localMonth: localMonthsNameHindi, apFmt: apFmtHindi}, + "E": {tags: []string{"hu"}, localMonth: localMonthsNameHungarian, apFmt: apFmtHungarian}, + "40E": {tags: []string{"hu-HU"}, localMonth: localMonthsNameHungarian, apFmt: apFmtHungarian}, + "F": {tags: []string{"is"}, localMonth: localMonthsNameIcelandic, apFmt: apFmtIcelandic}, + "40F": {tags: []string{"is-IS"}, localMonth: localMonthsNameIcelandic, apFmt: apFmtIcelandic}, + "70": {tags: []string{"ig"}, localMonth: localMonthsNameIgbo, apFmt: apFmtIgbo}, + "470": {tags: []string{"ig-NG"}, localMonth: localMonthsNameIgbo, apFmt: apFmtIgbo}, + "21": {tags: []string{"id"}, localMonth: localMonthsNameIndonesian, apFmt: nfp.AmPm[0]}, + "421": {tags: []string{"id-ID"}, localMonth: localMonthsNameIndonesian, apFmt: nfp.AmPm[0]}, + "5D": {tags: []string{"iu"}, localMonth: localMonthsNameInuktitut, apFmt: nfp.AmPm[0]}, + "7C5D": {tags: []string{"iu-Latn"}, localMonth: localMonthsNameInuktitut, apFmt: nfp.AmPm[0]}, + "85D": {tags: []string{"iu-Latn-CA"}, localMonth: localMonthsNameInuktitut, apFmt: nfp.AmPm[0]}, + "785D": {tags: []string{"iu-Cans"}, localMonth: localMonthsNameSyllabics, apFmt: nfp.AmPm[0]}, + "45D": {tags: []string{"iu-Cans-CA"}, localMonth: localMonthsNameSyllabics, apFmt: nfp.AmPm[0]}, "3C": {tags: []string{"ga"}, localMonth: localMonthsNameIrish, apFmt: apFmtIrish}, "83C": {tags: []string{"ga-IE"}, localMonth: localMonthsNameIrish, apFmt: apFmtIrish}, "10": {tags: []string{"it"}, localMonth: localMonthsNameItalian, apFmt: nfp.AmPm[0]}, + "410": {tags: []string{"it-IT"}, localMonth: localMonthsNameItalian, apFmt: nfp.AmPm[0]}, + "810": {tags: []string{"it-CH"}, localMonth: localMonthsNameItalian, apFmt: nfp.AmPm[0]}, "11": {tags: []string{"ja"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese}, "411": {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese}, "800411": {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese}, "JP-X-GANNEN": {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese}, "JP-X-GANNEN,80": {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese, useGannen: true}, + "4B": {tags: []string{"kn"}, localMonth: localMonthsNameKannada, apFmt: apFmtKannada}, + "44B": {tags: []string{"kn-IN"}, localMonth: localMonthsNameKannada, apFmt: apFmtKannada}, + "471": {tags: []string{"kr-Latn-NG"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, + "60": {tags: []string{"ks"}, localMonth: localMonthsNameKashmiri, apFmt: nfp.AmPm[0]}, + "460": {tags: []string{"ks-Arab"}, localMonth: localMonthsNameKashmiri, apFmt: nfp.AmPm[0]}, + "860": {tags: []string{"ks-Deva-IN"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, + "3F": {tags: []string{"kk"}, localMonth: localMonthsNameKazakh, apFmt: nfp.AmPm[0]}, + "43F": {tags: []string{"kk-KZ"}, localMonth: localMonthsNameKazakh, apFmt: nfp.AmPm[0]}, + "53": {tags: []string{"km"}, localMonth: localMonthsNameKhmer, apFmt: apFmtKhmer}, + "453": {tags: []string{"km-KH"}, localMonth: localMonthsNameKhmer, apFmt: apFmtKhmer}, + "86": {tags: []string{"quc"}, localMonth: localMonthsNameKiche, apFmt: apFmtCuba}, + "486": {tags: []string{"quc-Latn-GT"}, localMonth: localMonthsNameKiche, apFmt: apFmtCuba}, + "87": {tags: []string{"rw"}, localMonth: localMonthsNameKinyarwanda, apFmt: nfp.AmPm[0]}, + "487": {tags: []string{"rw-RW"}, localMonth: localMonthsNameKinyarwanda, apFmt: nfp.AmPm[0]}, + "41": {tags: []string{"sw"}, localMonth: localMonthsNameKiswahili, apFmt: nfp.AmPm[0]}, + "441": {tags: []string{"sw-KE"}, localMonth: localMonthsNameKiswahili, apFmt: nfp.AmPm[0]}, + "57": {tags: []string{"kok"}, localMonth: localMonthsNameKonkani, apFmt: apFmtKonkani}, + "457": {tags: []string{"kok-IN"}, localMonth: localMonthsNameKonkani, apFmt: apFmtKonkani}, "12": {tags: []string{"ko"}, localMonth: localMonthsNameKorean, apFmt: apFmtKorean}, "412": {tags: []string{"ko-KR"}, localMonth: localMonthsNameKorean, apFmt: apFmtKorean}, + "40": {tags: []string{"ky"}, localMonth: localMonthsNameKyrgyz, apFmt: apFmtKyrgyz}, + "440": {tags: []string{"ky-KG"}, localMonth: localMonthsNameKyrgyz, apFmt: apFmtKyrgyz}, + "54": {tags: []string{"lo"}, localMonth: localMonthsNameLao, apFmt: apFmtLao}, + "454": {tags: []string{"lo-LA"}, localMonth: localMonthsNameLao, apFmt: apFmtLao}, + "476": {tags: []string{"la-VA"}, localMonth: localMonthsNameLatin, apFmt: nfp.AmPm[0]}, + "26": {tags: []string{"lv"}, localMonth: localMonthsNameLatvian, apFmt: apFmtLatvian}, + "426": {tags: []string{"lv-LV"}, localMonth: localMonthsNameLatvian, apFmt: apFmtLatvian}, + "27": {tags: []string{"lt"}, localMonth: localMonthsNameLithuanian, apFmt: apFmtLithuanian}, + "427": {tags: []string{"lt-LT"}, localMonth: localMonthsNameLithuanian, apFmt: apFmtLithuanian}, + "7C2E": {tags: []string{"dsb"}, localMonth: localMonthsNameLowerSorbian, apFmt: nfp.AmPm[0]}, + "82E": {tags: []string{"dsb-DE"}, localMonth: localMonthsNameLowerSorbian, apFmt: nfp.AmPm[0]}, + "6E": {tags: []string{"lb"}, localMonth: localMonthsNameLuxembourgish, apFmt: nfp.AmPm[0]}, + "46E": {tags: []string{"lb-LU"}, localMonth: localMonthsNameLuxembourgish, apFmt: nfp.AmPm[0]}, + "2F": {tags: []string{"mk"}, localMonth: localMonthsNameMacedonian, apFmt: apFmtMacedonian}, + "42F": {tags: []string{"mk-MK"}, localMonth: localMonthsNameMacedonian, apFmt: apFmtMacedonian}, + "3E": {tags: []string{"ms"}, localMonth: localMonthsNameMalay, apFmt: apFmtMalay}, + "83E": {tags: []string{"ms-BN"}, localMonth: localMonthsNameMalay, apFmt: apFmtMalay}, + "43E": {tags: []string{"ms-MY"}, localMonth: localMonthsNameMalay, apFmt: apFmtMalay}, + "4C": {tags: []string{"ml"}, localMonth: localMonthsNameMalayalam, apFmt: nfp.AmPm[0]}, + "44C": {tags: []string{"ml-IN"}, localMonth: localMonthsNameMalayalam, apFmt: nfp.AmPm[0]}, + "3A": {tags: []string{"mt"}, localMonth: localMonthsNameMaltese, apFmt: nfp.AmPm[0]}, + "43A": {tags: []string{"mt-MT"}, localMonth: localMonthsNameMaltese, apFmt: nfp.AmPm[0]}, + "81": {tags: []string{"mi"}, localMonth: localMonthsNameMaori, apFmt: apFmtCuba}, + "481": {tags: []string{"mi-NZ"}, localMonth: localMonthsNameMaori, apFmt: apFmtCuba}, + "7A": {tags: []string{"arn"}, localMonth: localMonthsNameMapudungun, apFmt: nfp.AmPm[0]}, + "47A": {tags: []string{"arn-CL"}, localMonth: localMonthsNameMapudungun, apFmt: nfp.AmPm[0]}, + "4E": {tags: []string{"mr"}, localMonth: localMonthsNameMarathi, apFmt: apFmtKonkani}, + "44E": {tags: []string{"mr-IN"}, localMonth: localMonthsNameMarathi, apFmt: apFmtKonkani}, + "7C": {tags: []string{"moh"}, localMonth: localMonthsNameMohawk, apFmt: nfp.AmPm[0]}, + "47C": {tags: []string{"moh-CA"}, localMonth: localMonthsNameMohawk, apFmt: nfp.AmPm[0]}, + "50": {tags: []string{"mn"}, localMonth: localMonthsNameMongolian, apFmt: apFmtMongolian}, + "7850": {tags: []string{"mn-Cyrl"}, localMonth: localMonthsNameMongolian, apFmt: apFmtMongolian}, + "450": {tags: []string{"mn-MN"}, localMonth: localMonthsNameMongolian, apFmt: apFmtMongolian}, "7C50": {tags: []string{"mn-Mong"}, localMonth: localMonthsNameTraditionalMongolian, apFmt: nfp.AmPm[0]}, "850": {tags: []string{"mn-Mong-CN"}, localMonth: localMonthsNameTraditionalMongolian, apFmt: nfp.AmPm[0]}, "C50": {tags: []string{"mn-Mong-MN"}, localMonth: localMonthsNameTraditionalMongolian, apFmt: nfp.AmPm[0]}, + "61": {tags: []string{"ne"}, localMonth: localMonthsNameNepali, apFmt: apFmtHindi}, + "861": {tags: []string{"ne-IN"}, localMonth: localMonthsNameNepaliIN, apFmt: apFmtHindi}, + "461": {tags: []string{"ne-NP"}, localMonth: localMonthsNameNepali, apFmt: apFmtHindi}, + "14": {tags: []string{"no"}, localMonth: localMonthsNameNorwegian, apFmt: apFmtCuba}, + "7C14": {tags: []string{"nb-NO"}, localMonth: localMonthsNameNorwegian, apFmt: apFmtCuba}, + "414": {tags: []string{"nn"}, localMonth: localMonthsNameNorwegian, apFmt: apFmtCuba}, + "7814": {tags: []string{"nn"}, localMonth: localMonthsNameNorwegian, apFmt: apFmtNorwegian}, + "814": {tags: []string{"nn-NO"}, localMonth: localMonthsNameNorwegian, apFmt: apFmtNorwegian}, + "82": {tags: []string{"oc"}, localMonth: localMonthsNameOccitan, apFmt: nfp.AmPm[0]}, + "482": {tags: []string{"oc-FR"}, localMonth: localMonthsNameOccitan, apFmt: nfp.AmPm[0]}, + "48": {tags: []string{"or"}, localMonth: localMonthsNameOdia, apFmt: nfp.AmPm[0]}, + "448": {tags: []string{"or-IN"}, localMonth: localMonthsNameOdia, apFmt: nfp.AmPm[0]}, + "72": {tags: []string{"om"}, localMonth: localMonthsNameOromo, apFmt: apFmtOromo}, + "472": {tags: []string{"om-ET"}, localMonth: localMonthsNameOromo, apFmt: apFmtOromo}, + "63": {tags: []string{"ps"}, localMonth: localMonthsNamePashto, apFmt: apFmtPashto}, + "463": {tags: []string{"ps-AF"}, localMonth: localMonthsNamePashto, apFmt: apFmtPashto}, "19": {tags: []string{"ru"}, localMonth: localMonthsNameRussian, apFmt: nfp.AmPm[0]}, "819": {tags: []string{"ru-MD"}, localMonth: localMonthsNameRussian, apFmt: nfp.AmPm[0]}, "419": {tags: []string{"ru-RU"}, localMonth: localMonthsNameRussian, apFmt: nfp.AmPm[0]}, @@ -849,6 +982,14 @@ var ( japaneseEraNames = []string{"\u660E\u6CBB", "\u5927\u6B63", "\u662D\u548C", "\u5E73\u6210", "\u4EE4\u548C"} // japaneseEraYear list the Japanese era name symbols. japaneseEraSymbols = []string{"M", "T", "S", "H", "R"} + // 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 the Afrikaans. + monthNamesAfrikaansAbbr = []string{"Jan.", "Feb.", "Maa.", "Apr.", "Mei", "Jun.", "Jul.", "Aug.", "Sep.", "Okt.", "Nov.", "Des."} + // monthNamesAustria list the month names in the Austrian. + monthNamesAustria = []string{"Jänner", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"} + // monthNamesAustriaAbbr list the month name abbreviations in the Austrian. + monthNamesAustriaAbbr = []string{"Jän", "Feb", "Mär", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"} // monthNamesBangla list the month names in the Bangla. monthNamesBangla = []string{ "\u099C\u09BE\u09A8\u09C1\u09AF\u09BC\u09BE\u09B0\u09C0", @@ -864,48 +1005,728 @@ var ( "\u09A8\u09AD\u09C7\u09AE\u09CD\u09AC\u09B0", "\u09A1\u09BF\u09B8\u09C7\u09AE\u09CD\u09AC\u09B0", } - // 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."} + // monthNamesCaribbean list the month names in the Caribbean. + monthNamesCaribbean = []string{"Janvier", "Février", "Mars", "Avril", "Mai", "Juin", "Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre"} + // monthNamesCaribbeanAbbr lists the month name abbreviations in the Caribbean. + monthNamesCaribbeanAbbr = []string{"Janv.", "Févr.", "Mars", "Avr.", "Mai", "Juin", "Juil.", "Août", "Sept.", "Oct.", "Nov.", "Déc."} // 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"} + monthNamesChinese = []string{"一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"} + // monthNamesChineseAbbr lists the month name abbreviations in the Chinese. + monthNamesChineseAbbr = []string{"一", "二", "三", "四", "五", "六", "七", "八", "九", "十", "十一", "十二"} + // monthNamesChineseNum list the month number and character abbreviation in the Chinese. + monthNamesChineseNum = []string{"0月", "1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月"} + // monthNamesEstonian list the month names in the Estonian. + monthNamesEstonian = []string{"jaanuar", "veebruar", "märts", "aprill", "mai", "juuni", "juuli", "august", "september", "oktoober", "november", "detsember"} + // monthNamesEstonianAbbr lists the month name abbreviations in the Estonian. + monthNamesEstonianAbbr = []string{"jaan", "veebr", "märts", "apr", "mai", "juuni", "juuli", "aug", "sept", "okt", "nov", "dets"} + // monthNamesFaroese list the month names in the Faroese. + monthNamesFaroese = []string{"januar", "februar", "mars", "apríl", "mai", "juni", "juli", "august", "september", "oktober", "november", "desember"} + // monthsNameFaroeseAbbr lists the month name abbreviations in the Faroese. + monthsNameFaroeseAbbr = []string{"jan", "feb", "mar", "apr", "mai", "jun", "jul", "aug", "sep", "okt", "nov", "des"} + // monthNamesFilipino list the month names in the Filipino. + monthNamesFilipino = []string{"Enero", "Pebrero", "Marso", "Abril", "Mayo", "Hunyo", "Hulyo", "Agosto", "Setyembre", "Oktubre", "Nobyembre", "Disyembre"} + // monthNamesFilipinoAbbr lists the month name abbreviations in the Filipino. + monthNamesFilipinoAbbr = []string{"Ene", "Peb", "Mar", "Abr", "May", "Hun", "Hul", "Ago", "Set", "Okt", "Nob", "Dis"} + // monthsNamesFinnish list the month names in the Finnish. + monthNamesFinnish = []string{"Etammikuu", "helmikuu", "maaliskuu", "huhtikuu", "toukokuu", "kesäkuu", "heinäkuu", "elokuu", "syyskuu", "lokakuu", "marraskuu", "joulukuu"} + // monthsNamesFinnishAbbr lists the month name abbreviations in the Finnish. + monthNamesFinnishAbbr = []string{"tammi", "helmi", "maalis", "huhti", "touko", "kesä", "heinä", "elo", "syys", "loka", "marras", "joulu"} // 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."} + // monthNamesFrenchAbbr lists the month name abbreviations in the French. + monthNamesFrenchAbbr = []string{"janv.", "févr.", "mars", "avr.", "mai", "juin", "juil.", "août", "sept.", "oct.", "nov.", "déc."} + // monthNamesFrisian list the month names in the Frisian. + monthNamesFrisian = []string{"Jannewaris", "Febrewaris", "Maart", "April", "Maaie", "Juny", "July", "Augustus", "Septimber", "Oktober", "Novimber", "Desimber"} + // monthNamesFrisianAbbr lists the month name abbreviations in the Frisian. + monthNamesFrisianAbbr = []string{"jan", "feb", "Mrt", "Apr", "maa", "Jun", "Jul", "Aug", "sep", "Okt", "Nov", "Des"} + // monthNamesFulah list the month names in the Fulah. + monthNamesFulah = []string{"siilo", "colte", "mbooy", "seeɗto", "duujal", "korse", "morso", "juko", "siilto", "yarkomaa", "jolal", "bowte"} + // monthNamesFulahAbbr lists the month name abbreviations in the Fulah. + monthNamesFulahAbbr = []string{"sii", "col", "mbo", "see", "duu", "kor", "mor", "juk", "slt", "yar", "jol", "bow"} + // monthNamesGalician list the month names in the Galician. + monthNamesGalician = []string{"Xaneiro", "Febreiro", "Marzo", "Abril", "Maio", "Xuño", "Xullo", "Agosto", "Setembro", "Outubro", "Novembro", "Decembro"} + // monthNamesGalicianAbbr lists the month name abbreviations in the Galician. + monthNamesGalicianAbbr = []string{"Xan.", "Feb.", "Mar.", "Abr.", "Maio", "Xuño", "Xul.", "Ago.", "Set.", "Out.", "Nov.", "Dec."} + // monthNamesGeorgian list the month names in the Georgian. + monthNamesGeorgian = []string{ + "\u10D8\u10D0\u10DC\u10D5\u10D0\u10E0\u10D8", + "\u10D7\u10D4\u10D1\u10D4\u10E0\u10D5\u10D0\u10DA\u10D8", + "\u10DB\u10D0\u10E0\u10E2\u10D8", + "\u10D0\u10DE\u10E0\u10D8\u10DA\u10D8", + "\u10DB\u10D0\u10D8\u10E1\u10D8", + "\u10D8\u10D5\u10DC\u10D8\u10E1\u10D8", + "\u10D8\u10D5\u10DA\u10D8\u10E1\u10D8", + "\u10D0\u10D2\u10D5\u10D8\u10E1\u10E2\u10DD", + "\u10E1\u10D4\u10E5\u10E2\u10D4\u10DB\u10D1\u10D4\u10E0\u10D8", + "\u10DD\u10E5\u10E2\u10DD\u10DB\u10D1\u10D4\u10E0\u10D8", + "\u10DC\u10DD\u10D4\u10DB\u10D1\u10D4\u10E0\u10D8", + "\u10D3\u10D4\u10D9\u10D4\u10DB\u10D1\u10D4\u10E0\u10D8", + } + // monthNamesGeorgianAbbr lists the month name abbreviations in the Georgian. + monthNamesGeorgianAbbr = []string{ + "\u10D8\u10D0\u10DC", + "\u10D7\u10D4\u10D1", + "\u10DB\u10D0\u10E0", + "\u10D0\u10DE\u10E0", + "\u10DB\u10D0\u10D8", + "\u10D8\u10D5\u10DC", + "\u10D8\u10D5\u10DA", + "\u10D0\u10D2\u10D5", + "\u10E1\u10D4\u10E5", + "\u10DD\u10E5\u10E2", + "\u10DC\u10DD\u10D4", + "\u10D3\u10D4\u10D9", + } // 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 list the month abbreviations in the 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"} + // monthNamesGreek list the month names in the Greek. + monthNamesGreek = []string{ + "\u0399\u03B1\u03BD\u03BF\u03C5\u03AC\u03C1\u03B9\u03BF\u03C2", + "\u03A6\u03B5\u03B2\u03C1\u03BF\u03C5\u03AC\u03C1\u03B9\u03BF\u03C2", + "\u039C\u03AC\u03C1\u03C4\u03B9\u03BF\u03C2", + "\u0391\u03C0\u03C1\u03AF\u03BB\u03B9\u03BF\u03C2", + "\u039C\u03AC\u03B9\u03BF\u03C2", + "\u0399\u03BF\u03CD\u03BD\u03B9\u03BF\u03C2", + "\u0399\u03BF\u03CD\u03BB\u03B9\u03BF\u03C2", + "\u0391\u03CD\u03B3\u03BF\u03C5\u03C3\u03C4\u03BF\u03C2", + "\u03A3\u03B5\u03C0\u03C4\u03AD\u03BC\u03B2\u03C1\u03B9\u03BF\u03C2", + "\u039F\u03BA\u03C4\u03CE\u03B2\u03C1\u03B9\u03BF\u03C2", + "\u039D\u03BF\u03AD\u03BC\u03B2\u03C1\u03B9\u03BF\u03C2", + "\u0394\u03B5\u03BA\u03AD\u03BC\u03B2\u03C1\u03B9\u03BF\u03C2", + } + // monthNamesGreekAbbr list the month abbreviations in the Greek. + monthNamesGreekAbbr = []string{ + "\u0399\u03B1\u03BD", + "\u03A6\u03B5\u03B2", + "\u039C\u03B1\u03C1", + "\u0391\u03C0\u03C1", + "\u039C\u03B1\u03CA", + "\u0399\u03BF\u03C5\u03BD", + "\u0399\u03BF\u03C5\u03BB", + "\u0391\u03C5\u03B3", + "\u03A3\u03B5\u03C0", + "\u039F\u03BA\u03C4", + "\u039D\u03BF\u03B5", + "\u0394\u03B5\u03BA", + } + // monthNamesGreenlandic list the month names in the Greenlandic. + monthNamesGreenlandic = []string{"januaari", "februaari", "marsi", "apriili", "maaji", "juuni", "juuli", "aggusti", "septembari", "oktobari", "novembari", "decembari"} + // monthNamesGreenlandicAbbr list the month abbreviations in the Greenlandic. + monthNamesGreenlandicAbbr = []string{"jan", "feb", "mar", "apr", "mai", "jun", "jul", "aug", "sep", "okt", "nov", "dec"} + // monthNamesGuarani list the month names in the Guarani. + monthNamesGuarani = []string{"jasyte\u0129", "jasyk%F5i", "jasyapy", "jasyrundy", "jasypo", "jasypote\u0129", "jasypok%F5i", "jasypoapy", "jasyporundy", "jasypa", "jasypate\u0129", "jasypak%F5i"} + // monthNamesGuaraniAbbr list the month abbreviations in the Guarani. + monthNamesGuaraniAbbr = []string{"jteĩ", "jkõi", "japy", "jrun", "jpo", "jpot", "jpok", "jpoa", "jpor", "jpa", "jpat", "jpak"} + // monthNamesGujarati list the month names in the Gujarati. + monthNamesGujarati = []string{ + "\u0A9C\u0ABE\u0AA8\u0ACD\u0AAF\u0AC1\u0A86\u0AB0\u0AC0", + "\u0AAB\u0AC7\u0AAC\u0ACD\u0AB0\u0AC1\u0A86\u0AB0\u0AC0", + "\u0AAE\u0ABE\u0AB0\u0ACD\u0A9A", + "\u0A8F\u0AAA\u0ACD\u0AB0\u0ABF\u0AB2", + "\u0AAE\u0AC7", + "\u0A9C\u0AC2\u0AA8", + "\u0A9C\u0AC1\u0AB2\u0ABE\u0A88", + "\u0A91\u0A97\u0AB8\u0ACD\u0A9F", + "\u0AB8\u0AAA\u0ACD\u0A9F\u0AC7\u0AAE\u0ACD\u0AAC\u0AB0", + "\u0A91\u0A95\u0ACD\u0A9F\u0ACB\u0AAC\u0AB0", + "\u0AA8\u0AB5\u0AC7\u0AAE\u0ACD\u0AAC\u0AB0", + "\u0AA1\u0ABF\u0AB8\u0AC7\u0AAE\u0ACD\u0AAC\u0AB0", + } + // monthNamesGujaratiAbbr list the month abbreviations in the Gujarati. + monthNamesGujaratiAbbr = []string{ + "\u0A9C\u0ABE\u0AA8\u0ACD\u0AAF\u0AC1", + "\u0AAB\u0AC7\u0AAC\u0ACD\u0AB0\u0AC1", + "\u0AAE\u0ABE\u0AB0\u0ACD\u0A9A", + "\u0A8F\u0AAA\u0ACD\u0AB0\u0ABF\u0AB2", + "\u0AAE\u0AC7", + "\u0A9C\u0AC2\u0AA8", + "\u0A9C\u0AC1\u0AB2\u0ABE\u0A88", + "\u0A91\u0A97", + "\u0AB8\u0AAA\u0ACD\u0A9F\u0AC7", + "\u0A91\u0A95\u0ACD\u0A9F\u0ACB", + "\u0AA8\u0AB5\u0AC7", + "\u0AA1\u0ABF\u0AB8\u0AC7", + } + // monthNamesHausa list the month names in the Hausa. + monthNamesHausa = []string{"Janairu", "Fabrairu", "Maris", "Afirilu", "Mayu", "Yuni", "Yuli", "Agusta", "Satumba", "Oktoba", "Nuwamba", "Disamba"} + // monthNamesHawaiian list the month names in the Hawaiian. + monthNamesHawaiian = []string{"Ianuali", "Pepeluali", "Malaki", "ʻApelila", "Mei", "Iune", "Iulai", "ʻAukake", "Kepakemapa", "ʻOkakopa", "Nowemapa", "Kekemapa"} + // monthNamesHawaiianAbbr list the month name abbreviations in the Hawaiiann. + monthNamesHawaiianAbbr = []string{"Ian.", "Pep.", "Mal.", "ʻAp.", "Mei", "Iun.", "Iul.", "ʻAu.", "Kep.", "ʻOk.", "Now.", "Kek."} + // monthNamesHebrew list the month names in the Hebrew. + monthNamesHebrew = []string{ + "\u05D9\u05E0\u05D5\u05D0\u05E8", + "\u05E4\u05D1\u05E8\u05D5\u05D0\u05E8", + "\u05DE\u05E8\u05E5", + "\u05D0\u05E4\u05E8\u05D9\u05DC", + "\u05DE\u05D0\u05D9", + "\u05D9\u05D5\u05E0\u05D9", + "\u05D9\u05D5\u05DC\u05D9", + "\u05D0\u05D5\u05D2\u05D5\u05E1\u05D8", + "\u05E1\u05E4\u05D8\u05DE\u05D1\u05E8", + "\u05D0\u05D5\u05E7\u05D8\u05D5\u05D1\u05E8", + "\u05E0\u05D5\u05D1\u05DE\u05D1\u05E8", + "\u05D3\u05E6\u05DE\u05D1\u05E8", + } + // monthNamesHindi list the month names in the Hindi. + monthNamesHindi = []string{ + "\u091C\u0928\u0935\u0930\u0940", + "\u092B\u0930\u0935\u0930\u0940", + "\u092E\u093E\u0930\u094D\u091A", + "\u0905\u092A\u094D\u0930\u0948\u0932", + "\u092E\u0908", + "\u091C\u0942\u0928", + "\u091C\u0941\u0932\u093E\u0908", + "\u0905\u0917\u0938\u094D\u0924", + "\u0938\u093F\u0924\u092E\u094D\u092C\u0930", + "\u0905\u0915\u094D\u0924\u0942\u092C\u0930", + "\u0928\u0935\u092E\u094D\u092C\u0930", + "\u0926\u093F\u0938\u092E\u094D\u092C\u0930", + } + // monthNamesHungarian list the month names in the Hungarian. + monthNamesHungarian = []string{"január", "február", "március", "április", "május", "június", "július", "augusztus", "szeptember", "október", "november", "december"} + // monthNamesHungarianAbbr list the month name abbreviations in the Hungarian. + monthNamesHungarianAbbr = []string{"jan.", "febr.", "márc.", "ápr.", "máj.", "jún.", "júl.", "aug.", "szept.", "okt.", "nov.", "dec."} + // monthNamesIcelandic list the month names in the Icelandic. + monthNamesIcelandic = []string{"janúar", "febrúar", "mars", "apríl", "maí", "júní", "júlí", "ágúst", "september", "október", "nóvember", "desember"} + // monthNamesIcelandicAbbr list the month name abbreviations in the Icelandic. + monthNamesIcelandicAbbr = []string{"jan.", "feb.", "mar.", "apr.", "maí", "jún.", "júl.", "ágú.", "sep.", "okt.", "nóv.", "des."} + // monthNamesIgbo list the month names in the Igbo. + monthNamesIgbo = []string{"Jenụwarị", "Febụwarị", "Machị", "Eprelu", "Mey", "Juun", "Julaị", "Ọgọst", "Septemba", "Ọcktọba", "Nọvemba", "Disemba"} + // monthNamesIgboAbbr list the month name abbreviations in the Igbo. + monthNamesIgboAbbr = []string{"Jen", "Feb", "Mac", "Epr", "Mey", "Jun", "Jul", "Ọgọ", "Sep", "Ọkt", "Nọv", "Dis"} + // monthNamesIndonesian list the month names in the Indonesian. + monthNamesIndonesian = []string{"Januari", "Februari", "Maret", "April", "Mei", "Juni", "Juli", "Agustus", "September", "Oktober", "November", "Desember"} + // monthNamesIndonesianAbbr list the month name abbreviations in the Indonesian. + monthNamesIndonesianAbbr = []string{"Jan", "Feb", "Mar", "Apr", "Mei", "Jun", "Jul", "Agu", "Sep", "Okt", "Nov", "Des"} + // monthNamesInuktitut list the month names in the Inuktitut. + monthNamesInuktitut = []string{"Jaannuari", "Viivvuari", "Maatsi", "Iipuri", "Mai", "Juuni", "Julai", "Aaggiisi", "Sitipiri", "Utupiri", "Nuvipiri", "Tisipiri"} + // monthNamesInuktitutAbbr list the month name abbreviations in the Inuktitut. + monthNamesInuktitutAbbr = []string{"Jan", "Viv", "Mas", "Ipu", "Mai", "Jun", "Jul", "Agi", "Sii", "Uut", "Nuv", "Tis"} // 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 lists the month abbreviations in the 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 list the month name abbreviations in the Italian. monthNamesItalianAbbr = []string{"gen", "feb", "mar", "apr", "mag", "giu", "lug", "ago", "set", "ott", "nov", "dic"} + // monthNamesKannada list the month names in the Kannada. + monthNamesKannada = []string{ + "\u0C9C\u0CA8\u0CB5\u0CB0\u0CBF", + "\u0CAB\u0CC6\u0CAC\u0CCD\u0CB0\u0CB5\u0CB0\u0CBF", + "\u0CAE\u0CBE\u0CB0\u0CCD\u0C9A\u0CCD", + "\u0C8F\u0C8F\u0CAA\u0CCD\u0CB0\u0CBF\u0CB2\u0CCD", + "\u0CAE\u0CC7", + "\u0C9C\u0CC2\u0CA8\u0CCD", + "\u0C9C\u0CC1\u0CB2\u0CC8", + "\u0C86\u0C97\u0CB8\u0CCD\u0C9F\u0CCD", + "\u0CB8\u0CC6\u0CAA\u0CCD\u0C9F\u0C82\u0CAC\u0CB0\u0CCD", + "\u0C85\u0C95\u0CCD\u0C9F\u0CCB\u0CAC\u0CB0\u0CCD", + "\u0CA8\u0CB5\u0CC6\u0C82\u0CAC\u0CB0\u0CCD", + "\u0CA1\u0CBF\u0CB8\u0CC6\u0C82\u0CAC\u0CB0\u0CCD", + } + // monthNamesKannadaAbbr lists the month abbreviations in the Kannada. + monthNamesKannadaAbbr = []string{ + "\u0C9C\u0CA8\u0CB5\u0CB0\u0CBF", + "\u0CAB\u0CC6\u0CAC\u0CCD\u0CB0\u0CB5\u0CB0\u0CBF", + "\u0CAE\u0CBE\u0CB0\u0CCD\u0C9A\u0CCD", + "\u0C8E\u0CAA\u0CCD\u0CB0\u0CBF\u0CB2\u0CCD", + "\u0CAE\u0CC7", + "\u0C9C\u0CC2\u0CA8\u0CCD", + "\u0C9C\u0CC1\u0CB2\u0CC8", + "\u0C86\u0C97\u0CB8\u0CCD\u0C9F\u0CCD", + "\u0CB8\u0CC6\u0CAA\u0CCD\u0C9F\u0C82\u0CAC\u0CB0\u0CCD", + "\u0C85\u0C95\u0CCD\u0C9F\u0CCB\u0CAC\u0CB0\u0CCD", + "\u0CA8\u0CB5\u0CC6\u0C82\u0CAC\u0CB0\u0CCD", + "\u0CA1\u0CBF\u0CB8\u0CC6\u0C82\u0CAC\u0CB0\u0CCD", + } + // monthNamesKashmiri list the month names in the Kashmiri. + monthNamesKashmiri = []string{ + "\u062C\u0646\u0624\u0631\u06CC", + "\u0641\u0631\u0624\u0631\u06CC", + "\u0645\u0627\u0631\u0655\u0686", + "\u0627\u067E\u0631\u06CC\u0644", + "\u0645\u06CC\u0654", + "\u062C\u0648\u0657\u0646", + "\u062C\u0648\u0657\u0644\u0627\u06CC\u06CC", + "\u0627\u06AF\u0633\u062A", + "\u0633\u062A\u0645\u0628\u0631", + "\u0627\u06A9\u062A\u0648\u0657\u0628\u0631", + "\u0646\u0648\u0645\u0628\u0631", + "\u062F\u0633\u0645\u0628\u0631", + } + // monthNamesKazakh list the month names in the Kazakh. + monthNamesKazakh = []string{ + "\u049A\u0430\u04A3\u0442\u0430\u0440", + "\u0410\u049B\u043F\u0430\u043D", + "\u041D\u0430\u0443\u0440\u044B\u0437", + "\u0421\u04D9\u0443\u0456\u0440", + "\u041C\u0430\u043C\u044B\u0440", + "\u041C\u0430\u0443\u0441\u044B\u043C", + "\u0428\u0456\u043B\u0434\u0435", + "\u0422\u0430\u043C\u044B\u0437", + "\u049A\u044B\u0440\u043A\u04AF\u0439\u0435\u043A", + "\u049A\u0430\u0437\u0430\u043D", + "\u049A\u0430\u0440\u0430\u0448\u0430", + "\u0416\u0435\u043B\u0442\u043E\u049B\u0441\u0430\u043D", + } + // monthNamesKazakhAbbr list the month name abbreviations in the Kazakh. + monthNamesKazakhAbbr = []string{ + "\u049B\u0430\u04A3", + "\u0430\u049B\u043F", + "\u043D\u0430\u0443", + "\u0441\u04D9\u0443", + "\u043C\u0430\u043C", + "\u043C\u0430\u0443", + "\u0448\u0456\u043B", + "\u0442\u0430\u043C", + "\u049B\u044B\u0440", + "\u049B\u0430\u0437", + "\u049B\u0430\u0440", + "\u0436\u0435\u043B", + } + // monthNamesKhmer list the month names in the Khmer. + monthNamesKhmer = []string{ + "\u1798\u1780\u179A\u17B6", + "\u1780\u17BB\u1798\u17D2\u1797\u17C8", + "\u1798\u17B7\u1793\u17B6", + "\u1798\u17C1\u179F\u17B6", + "\u17A7\u179F\u1797\u17B6", + "\u1798\u17B7\u1790\u17BB\u1793\u17B6", + "\u1780\u1780\u17D2\u1780\u178A\u17B6", + "\u179F\u17B8\u17A0\u17B6", + "\u1780\u1789\u17D2\u1789\u17B6", + "\u178F\u17BB\u179B\u17B6", + "\u179C\u17B7\u1785\u17D2\u1786\u17B7\u1780\u17B6", + "\u1792\u17D2\u1793\u17BC", + } + // monthNamesKhmerAbbr list the month name abbreviations in the Khmer. + monthNamesKhmerAbbr = []string{ + "\u17E1", "\u17E2", "\u17E3", "\u17E4", "\u17E5", "\u17E6", "\u17E7", "\u17E8", "\u17E9", "\u17E1\u17E0", "\u17E1\u17E1", "\u17E1\u17E2", + "\u1798", "\u1780", "\u1798", "\u1798", "\u17A7", "\u1798", "\u1780", "\u179F", "\u1780", "\u178F", "\u179C", "\u1792", + } + // monthNamesKiche list the month names in the Kiche. + monthNamesKiche = []string{"nab'e ik'", "ukab' ik'", "urox ik'", "ukaj ik'", "uro ik'", "uwaq ik'", "uwuq ik'", "uwajxaq ik'", "ub'elej ik'", "ulaj ik'", "ujulaj ik'", "ukab'laj ik'"} + // monthNamesKicheAbbr list the month name abbreviations in the Kiche. + monthNamesKicheAbbr = []string{"nab'e", "ukab'", "urox", "ukaj", "uro", "uwaq", "uwuq", "uwajxaq", "ub'elej", "ulaj", "ujulaj", "ukab'laj"} + // monthNamesKinyarwanda list the month names in the Kinyarwanda. + monthNamesKinyarwanda = []string{"Mutarama", "Gashyantare", "Werurwe", "Mata", "Gicuransi", "Kamena", "Nyakanga", "Kanama", "Nzeli", "Ukwakira", "Ugushyingo", "Ukuboza"} + // monthNamesKinyarwandaAbbr list the month name abbreviations in the Kinyarwanda. + monthNamesKinyarwandaAbbr = []string{"mut.", "gas.", "wer.", "mat.", "gic.", "kam.", "Nyak", "kan.", "nze.", "Ukwak", "Ugus", "Ukub"} + // monthNamesKiswahili list the month names in the Kiswahili. + monthNamesKiswahili = []string{"Januari", "Februari", "Machi", "Aprili", "Mei", "Juni", "Julai", "Agosti", "Septemba", "Oktoba", "Novemba", "Desemba"} + // monthNamesKiswahiliAbbr list the month name abbreviations in the Kiswahili. + monthNamesKiswahiliAbbr = []string{"Jan", "Feb", "Mac", "Apr", "Mei", "Jun", "Jul", "Ago", "Sep", "Okt", "Nov", "Des"} + // monthNamesKonkani list the month names in the Konkani. + monthNamesKonkani = []string{ + "\u091C\u093E\u0928\u0947\u0935\u093E\u0930\u0940", + "\u092B\u0947\u092C\u094D\u0930\u0941\u0935\u093E\u0930\u0940", + "\u092E\u093E\u0930\u094D\u091A", + "\u090F\u092A\u094D\u0930\u093F\u0932", + "\u092E\u0947", + "\u091C\u0942\u0928", + "\u091C\u0941\u0932\u0948", + "\u0911\u0917\u0938\u094D\u091F", + "\u0938\u092A\u094D\u091F\u0947\u0902\u092C\u0930", + "\u0911\u0915\u094D\u091F\u094B\u092C\u0930", + "\u0928\u094B\u0935\u0947\u092E\u094D\u092C\u0930", + "\u0921\u093F\u0938\u0947\u0902\u092C\u0930", + } + // monthNamesKonkaniAbbr list the month name abbreviations in the Konkani. + monthNamesKonkaniAbbr = []string{ + "\u091C\u093E\u0928\u0947", + "\u092B\u0947\u092C\u094D\u0930\u0941", + "\u092E\u093E\u0930\u094D\u091A", + "\u090F\u092A\u094D\u0930\u093F\u0932", + "\u092E\u0947", + "\u091C\u0942\u0928", + "\u091C\u0941\u0932\u0948", + "\u0911\u0917.", + "\u0938\u092A\u094D\u091F\u0947\u0902.", + "\u0911\u0915\u094D\u091F\u094B.", + "\u0928\u094B\u0935\u0947.", + "\u0921\u093F\u0938\u0947\u0902", + } + // monthNamesKoreanAbbr lists out the month number plus 월 for the Korean language. + monthNamesKoreanAbbr = []string{"1월", "2월", "3월", "4월", "5월", "6월", "7월", "8월", "9월", "10월", "11월", "12월"} + // monthNamesKyrgyz list the month names in the Kyrgyz. + monthNamesKyrgyz = []string{ + "\u042F\u043D\u0432\u0430\u0440\u044C", + "\u0424\u0435\u0432\u0440\u0430\u043B\u044C", + "\u041C\u0430\u0440\u0442", + "\u0410\u043F\u0440\u0435\u043B\u044C", + "\u041C\u0430\u0439", + "\u0418\u044E\u043D\u044C", + "\u0418\u044E\u043B\u044C", + "\u0410\u0432\u0433\u0443\u0441\u0442", + "\u0421\u0435\u043D\u0442\u044F\u0431\u0440\u044C", + "\u041E\u043A\u0442\u044F\u0431\u0440\u044C", + "\u041D\u043E\u044F\u0431\u0440\u044C", + "\u0414\u0435\u043A\u0430\u0431\u0440\u044C", + } + // monthNamesKyrgyzAbbr lists the month name abbreviations in the Kyrgyz. + monthNamesKyrgyzAbbr = []string{ + "\u042F\u043D\u0432", + "\u0424\u0435\u0432", + "\u041C\u0430\u0440", + "\u0410\u043F\u0440", + "\u041C\u0430\u0439", + "\u0418\u044E\u043D", + "\u0418\u044E\u043B", + "\u0410\u0432\u0433", + "\u0421\u0435\u043D", + "\u041E\u043A\u0442", + "\u041D\u043E\u044F", + "\u0414\u0435\u043A", + } + // monthNamesLao list the month names in the Lao. + monthNamesLao = []string{ + "\u0EA1\u0EB1\u0E87\u0E81\u0EAD\u0E99", + "\u0E81\u0EB8\u0EA1\u0E9E\u0EB2", + "\u0EA1\u0EB5\u0E99\u0EB2", + "\u0EC0\u0EA1\u0EAA\u0EB2", + "\u0E9E\u0EB6\u0E94\u0EAA\u0EB0\u0E9E\u0EB2", + "\u0EA1\u0EB4\u0E96\u0EB8\u0E99\u0EB2", + "\u0E81\u0ECD\u0EA5\u0EB0\u0E81\u0EBB\u0E94", + "\u0EAA\u0EB4\u0E87\u0EAB\u0EB2", + "\u0E81\u0EB1\u0E99\u0E8D\u0EB2", + "\u0E95\u0EB8\u0EA5\u0EB2", + "\u0E9E\u0EB0\u0E88\u0EB4\u0E81", + "\u0E97\u0EB1\u0E99\u0EA7\u0EB2", + } + // monthNamesLaoAbbr lists the month name abbreviations in the Lao. + monthNamesLaoAbbr = []string{ + "\u0EA1.\u0E81.", + "\u0E81.\u0E9E.", + "\u0EA1.\u0E99.", + "\u0EA1.\u0EAA.", + "\u0E9E.\u0E9E.", + "\u0EA1\u0EB4.\u0E96.", + "\u0E81.\u0EA5.", + "\u0EAA.\u0EAB.", + "\u0E81.\u0E8D.", + "\u0E95.\u0EA5.", + "\u0E9E.\u0E88.", + "\u0E97.\u0EA7.", + } + // monthNamesLatin list the month names in the Latin. + monthNamesLatin = []string{"Ianuarius", "Februarius", "Martius", "Aprilis", "Maius", "Iunius", "Quintilis", "Sextilis", "September", "October", "November", "December"} + // monthNamesLatinAbbr list the month name abbreviations in the Latin. + monthNamesLatinAbbr = []string{"Ian", "Feb", "Mar", "Apr", "Mai", "Iun", "Quint", "Sext", "Sept", "Oct", "Nov", "Dec"} + // monthNamesLatvian list the month names in the Latvian. + monthNamesLatvian = []string{"janvāris", "februāris", "marts", "aprīlis", "maijs", "jūnijs", "jūlijs", "augusts", "septembris", "oktobris", "novembris", "decembris"} + // monthNamesLatvianAbbr list the month name abbreviations in the Latvian. + monthNamesLatvianAbbr = []string{"janv.", "febr.", "marts", "apr.", "maijs", "jūn.", "jūl.", "aug.", "sept.", "okt.", "nov.", "dec."} + // monthNamesLithuanian list the month names in the Lithuanian. + monthNamesLithuanian = []string{"sausis", "vasaris", "kovas", "balandis", "gegužė", "birželis", "liepa", "rugpjūtis", "rugsėjis", "spalis", "lapkritis", "gruodis"} + // monthNamesLithuanianAbbr list the month name abbreviations in the Lithuanian. + monthNamesLithuanianAbbr = []string{"saus.", "vas.", "kov.", "bal.", "geg.", "birž.", "liep.", "rugp.", "rugs.", "spal.", "lapkr.", "gruod."} + // monthNamesLowerSorbian list the month names in the Lower Sorbian. + monthNamesLowerSorbian = []string{"januar", "februar", "měrc", "apryl", "maj", "junij", "julij", "awgust", "september", "oktober", "nowember", "december"} + // monthNamesLowerSorbianAbbr list the month name abbreviations in the LowerSorbian. + monthNamesLowerSorbianAbbr = []string{"jan", "feb", "měr", "apr", "maj", "jun", "jul", "awg", "sep", "okt", "now", "dec"} + // monthNamesLuxembourgish list the month names in the Lower Sorbian. + monthNamesLuxembourgish = []string{"Januar", "Februar", "Mäerz", "Abrëll", "Mee", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"} + // monthNamesLuxembourgishAbbr list the month name abbreviations in the Luxembourgish. + monthNamesLuxembourgishAbbr = []string{"Jan", "Feb", "Mäe", "Abr", "Mee", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"} + // monthNamesMacedonian list the month names in the Lower Sorbian. + monthNamesMacedonian = []string{ + "\u0458\u0430\u043D\u0443\u0430\u0440\u0438", + "\u0444\u0435\u0432\u0440\u0443\u0430\u0440\u0438", + "\u043C\u0430\u0440\u0442", + "\u0430\u043F\u0440\u0438\u043B", + "\u043C\u0430\u0458", + "\u0458\u0443\u043D\u0438", + "\u0458\u0443\u043B\u0438", + "\u0430\u0432\u0433\u0443\u0441\u0442", + "\u0441\u0435\u043F\u0442\u0435\u043C\u0432\u0440\u0438", + "\u043E\u043A\u0442\u043E\u043C\u0432\u0440\u0438", + "\u043D\u043E\u0435\u043C\u0432\u0440\u0438", + "\u0434\u0435\u043A\u0435\u043C\u0432\u0440\u0438", + } + // monthNamesMacedonianAbbr list the month name abbreviations in the Macedonian. + monthNamesMacedonianAbbr = []string{ + "\u0458\u0430\u043D.", + "\u0444\u0435\u0432.", + "\u043C\u0430\u0440.", + "\u0430\u043F\u0440.", + "\u043C\u0430\u0458", + "\u0458\u0443\u043D.", + "\u0458\u0443\u043B.", + "\u0430\u0432\u0433.", + "\u0441\u0435\u043F\u0442.", + "\u043E\u043A\u0442.", + "\u043D\u043E\u0435\u043C.", + "\u0434\u0435\u043A.", + } + // monthNamesMalay list the month names in the Malay. + monthNamesMalay = []string{"Januari", "Februari", "Mac", "April", "Mei", "Jun", "Julai", "Ogos", "September", "Oktober", "November", "Disember"} + // monthNamesMalayAbbr list the month name abbreviations in the Malay. + monthNamesMalayAbbr = []string{"Jan", "Feb", "Mac", "Apr", "Mei", "Jun", "Jul", "Ogo", "Sep", "Okt", "Nov", "Dis"} + // monthNamesMalayalam list the month names in the Malayalam. + monthNamesMalayalam = []string{ + "\u0D1C\u0D28\u0D41\u0D35\u0D30\u0D3F", + "\u0D2B\u0D46\u0D2C\u0D4D\u0D30\u0D41\u0D35\u0D30\u0D3F", + "\u0D2E\u0D3E\u0D30\u0D4D\u200D\u200C\u0D1A\u0D4D\u0D1A\u0D4D", + "\u0D0F\u0D2A\u0D4D\u0D30\u0D3F\u0D32\u0D4D\u200D", + "\u0D2E\u0D47\u0D2F\u0D4D", + "\u0D1C\u0D42\u0D7A", + "\u0D1C\u0D42\u0D32\u0D48", + "\u0D06\u0D17\u0D38\u0D4D\u0D31\u0D4D\u0D31\u0D4D", + "\u0D38\u0D46\u0D2A\u0D4D\u200C\u0D31\u0D4D\u0D31\u0D02\u0D2C\u0D30\u0D4D\u200D", + "\u0D12\u0D15\u0D4D\u200C\u0D1F\u0D4B\u0D2C\u0D30\u0D4D\u200D", + "\u0D28\u0D35\u0D02\u0D2C\u0D30\u0D4D\u200D", + "\u0D21\u0D3F\u0D38\u0D02\u0D2C\u0D30\u0D4D\u200D", + } + // monthNamesMalayalamAbbr list the month name abbreviations in the Malayalam. + monthNamesMalayalamAbbr = []string{ + "\u0D1C\u0D28\u0D41", + "\u0D2B\u0D46\u0D2C\u0D4D\u0D30\u0D41", + "\u0D2E\u0D3E\u0D7C", + "\u0D0F\u0D2A\u0D4D\u0D30\u0D3F", + "\u0D2E\u0D47\u0D2F\u0D4D", + "\u0D1C\u0D42\u0D7A", + "\u0D1C\u0D42\u0D32\u0D48", + "\u0D13\u0D17", + "\u0D38\u0D46\u0D2A\u0D4D\u0D31\u0D4D\u0D31\u0D02", + "\u0D12\u0D15\u0D4D\u0D1F\u0D4B", + "\u0D28\u0D35\u0D02", + "\u0D21\u0D3F\u0D38\u0D02", + } + // monthNamesMaltese list the month names in the Maltese. + monthNamesMaltese = []string{"Jannar", "Frar", "Marzu", "April", "Mejju", "Ġunju", "Lulju", "Awwissu", "Settembru", "Ottubru", "Novembru", "Diċembru"} + // monthNamesMalteseAbbr list the month name abbreviations in the Maltese. + monthNamesMalteseAbbr = []string{"Jan", "Fra", "Mar", "Apr", "Mej", "Ġun", "Lul", "Aww", "Set", "Ott", "Nov", "Diċ"} + // monthNamesMaori list the month names in the Maori. + monthNamesMaori = []string{"Kohitātea", "Huitanguru", "Poutūterangi", "Paengawhāwhā", "Haratua", "Pipiri", "Hōngongoi", "Hereturikōkā", "Mahuru", "Whiringa ā-nuku", "Whiringa ā-rangi", "Hakihea"} + // monthNamesMaoriAbbr list the month name abbreviations in the Maori. + monthNamesMaoriAbbr = []string{"Kohi", "Hui", "Pou", "Pae", "Hara", "Pipi", "Hōngo", "Here", "Mahu", "Nuku", "Rangi", "Haki"} + // monthNamesMapudungun list the month name abbreviations in the Mapudungun. + monthNamesMapudungun = []string{"Kiñe Tripantu", "Epu", "Kila", "Meli", "Kechu", "Cayu", "Regle", "Purha", "Aiya", "Marhi", "Marhi Kiñe", "Marhi Epu"} + // monthNamesMarathi list the month names in the Marathi. + monthNamesMarathi = []string{ + "\u091C\u093E\u0928\u0947\u0935\u093E\u0930\u0940", + "\u092B\u0947\u092C\u094D\u0930\u0941\u0935\u093E\u0930\u0940", + "\u092E\u093E\u0930\u094D\u091A", + "\u090F\u092A\u094D\u0930\u093F\u0932", + "\u092E\u0947", + "\u091C\u0942\u0928", + "\u091C\u0941\u0932\u0948", + "\u0911\u0917\u0938\u094D\u091F", + "\u0938\u092A\u094D\u091F\u0947\u0902\u092C\u0930", + "\u0911\u0915\u094D\u091F\u094B\u092C\u0930", + "\u0928\u094B\u0935\u094D\u0939\u0947\u0902\u092C\u0930", + "\u0921\u093F\u0938\u0947\u0902\u092C\u0930", + } + // monthNamesMarathiAbbr lists the month name abbreviations in Marathi. + monthNamesMarathiAbbr = []string{ + "\u091C\u093E\u0928\u0947.", + "\u092B\u0947\u092C\u094D\u0930\u0941.", + "\u092E\u093E\u0930\u094D\u091A", + "\u090F\u092A\u094D\u0930\u093F", + "\u092E\u0947", + "\u091C\u0942\u0928", + "\u091C\u0941\u0932\u0948", + "\u0911\u0917.", + "\u0938\u092A\u094D\u091F\u0947\u0902.", + "\u0911\u0915\u094D\u091F\u094B.", + "\u0928\u094B\u0935\u094D\u0939\u0947\u0902.", + "\u0921\u093F\u0938\u0947\u0902.", + } + // monthNamesMohawk list the month names in the Mohawk. + monthNamesMohawk = []string{"Tsothohrkó:Wa", "Enniska", "Enniskó:Wa", "Onerahtókha", "Onerahtohkó:Wa", "Ohiari:Ha", "Ohiarihkó:Wa", "Seskéha", "Seskehkó:Wa", "Kenténha", "Kentenhkó:Wa", "Tsothóhrha"} + // monthNamesMongolian list the month names in the Mongolian. + monthNamesMongolian = []string{ + "\u041D\u044D\u0433\u0434\u04AF\u0433\u044D\u044D\u0440 \u0441\u0430\u0440", + "\u0425\u043E\u0451\u0440\u0434\u0443\u0433\u0430\u0430\u0440 \u0441\u0430\u0440", + "\u0413\u0443\u0440\u0430\u0432\u0434\u0443\u0433\u0430\u0430\u0440 \u0441\u0430\u0440", + "\u0414\u04E9\u0440\u04E9\u0432\u0434\u04AF\u0433\u044D\u044D\u0440 \u0441\u0430\u0440", + "\u0422\u0430\u0432\u0434\u0443\u0433\u0430\u0430\u0440 \u0441\u0430\u0440", + "\u0417\u0443\u0440\u0433\u0430\u0430\u0434\u0443\u0433\u0430\u0430\u0440 \u0441\u0430\u0440", + "\u0414\u043E\u043B\u043E\u043E\u0434\u0443\u0433\u0430\u0430\u0440 \u0441\u0430\u0440", + "\u041D\u0430\u0439\u043C\u0434\u0443\u0433\u0430\u0430\u0440 \u0441\u0430\u0440", + "\u0415\u0441\u0434\u04AF\u0433\u044D\u044D\u0440 \u0441\u0430\u0440", + "\u0410\u0440\u0430\u0432\u0434\u0443\u0433\u0430\u0430\u0440 \u0441\u0430\u0440", + "\u0410\u0440\u0432\u0430\u043D \u043D\u044D\u0433\u0434\u04AF\u0433\u044D\u044D\u0440 \u0441\u0430\u0440", + "\u0410\u0440\u0432\u0430\u043D \u0445\u043E\u0451\u0440\u0434\u0443\u0433\u0430\u0430\u0440 \u0441\u0430\u0440", + } + // monthNamesMongolianAbbr lists the month name abbreviations in Mongolian. + monthNamesMongolianAbbr = []string{ + "1-р сар", "2-р сар", "3-р сар", "4-р сар", "5-р сар", "6-р сар", "7-р сар", "8-р сар", "9-р сар", "10-р сар", "11-р сар", "12-р сар", + } + // monthNamesMoroccoAbbr lists the month name abbreviations in the Morocco. + monthNamesMoroccoAbbr = []string{"jan.", "fév.", "mar.", "avr.", "mai", "jui.", "juil.", "août", "sept.", "oct.", "nov.", "déc."} + // monthNamesNepali list the month names in the Nepali. + monthNamesNepali = []string{ + "\u091C\u0928\u0935\u0930\u0940", + "\u092B\u0947\u092C\u094D\u0930\u0941\u0905\u0930\u0940", + "\u092E\u093E\u0930\u094D\u091A", + "\u0905\u092A\u094D\u0930\u093F\u0932", + "\u092E\u0947", + "\u091C\u0942\u0928", + "\u091C\u0941\u0932\u093E\u0908", + "\u0905\u0917\u0938\u094D\u0924", + "\u0938\u0947\u092A\u094D\u091F\u0947\u092E\u094D\u092C\u0930", + "\u0905\u0915\u094D\u091F\u094B\u092C\u0930", + "\u0928\u094B\u092D\u0947\u092E\u094D\u092C\u0930", + "\u0921\u093F\u0938\u0947\u092E\u094D\u092C\u0930", + } + // monthNamesNepaliAbbr lists the month name abbreviations in the Nepali. + monthNamesNepaliAbbr = []string{ + "\u091C\u0928", + "\u092B\u0947\u092C", + "\u092E\u093E\u0930\u094D\u091A", + "\u0905\u092A\u094D\u0930\u093F\u0932", + "\u092E\u0947", + "\u091C\u0942\u0928", + "\u091C\u0941\u0932\u093E\u0908", + "\u0905\u0917", + "\u0938\u0947\u092A\u094D\u091F", + "\u0905\u0915\u094D\u091F", + "\u0928\u094B\u092D", + "\u0921\u093F\u0938", + } + // monthNamesNepaliIN list the month names in the India Nepali. + monthNamesNepaliIN = []string{ + "\u091C\u0928\u0935\u0930\u0940", + "\u092B\u0930\u0935\u0930\u0940", + "\u092E\u093E\u0930\u094D\u091A", + "\u0905\u092A\u094D\u0930\u0947\u0932", + "\u092E\u0908", + "\u091C\u0941\u0928", + "\u091C\u0941\u0932\u093E\u0908", + "\u0905\u0917\u0938\u094D\u091F", + "\u0938\u0947\u092A\u094D\u091F\u0947\u092E\u094D\u092C\u0930", + "\u0905\u0915\u094D\u091F\u094B\u092C\u0930", + "\u0928\u094B\u092D\u0947\u092E\u094D\u092C\u0930", + "\u0926\u093F\u0938\u092E\u094D\u092C\u0930", + } + // monthNamesNepaliINAbbr lists the month name abbreviations in the India Nepali. + monthNamesNepaliINAbbr = []string{ + "\u091C\u0928\u0935\u0930\u0940", + "\u092B\u0947\u092C\u094D\u0930\u0941\u0905\u0930\u0940", + "\u092E\u093E\u0930\u094D\u091A", + "\u0905\u092A\u094D\u0930\u093F", + "\u092E\u0947", + "\u091C\u0941\u0928", + "\u091C\u0941\u0932\u093E", + "\u0905\u0917\u0938\u094D\u091F", + "\u0938\u0947\u092A\u094D\u091F\u0947\u092E\u094D\u092C\u0930", + "\u0905\u0915\u094D\u091F\u094B", + "\u0928\u094B\u092D\u0947", + "\u0921\u093F\u0938\u0947", + } + // monthNamesNigeria list the month names in the Nigeria. + monthNamesNigeria = []string{"samwiee", "feeburyee", "marsa", "awril", "me", "suyeŋ", "sulyee", "ut", "satambara", "oktoobar", "nowamburu", "deesamburu"} + // monthNamesNigeriaAbbr lists the month name abbreviations in the Nigeria. + monthNamesNigeriaAbbr = []string{"samw", "feeb", "mar", "awr", "me", "suy", "sul", "ut", "sat", "okt", "now", "dees"} + // monthNamesNorwegian list the month names in the Norwegian. + monthNamesNorwegian = []string{"januar", "februar", "mars", "april", "mai", "juni", "juli", "august", "september", "oktober", "november", "desember"} + // monthNamesOccitan list the month names in the Occitan. + monthNamesOccitan = []string{"genièr", "febrièr", "març", "abril", "mai", "junh", "julhet", "agost", "setembre", "octobre", "novembre", "decembre"} + // monthNamesOccitanAbbr lists the month name abbreviations in the Occitan. + monthNamesOccitanAbbr = []string{"gen.", "feb.", "març", "abr.", "mai", "junh", "julh", "ag.", "set.", "oct.", "nov.", "dec."} + // monthNamesOdia list the month names in the Odia. + monthNamesOdia = []string{ + "\u0B1C\u0B3E\u0B28\u0B41\u0B5F\u0B3E\u0B30\u0B40", + "\u0B2B\u0B47\u0B2C\u0B43\u0B06\u0B30\u0B40", + "\u0B2E\u0B3E\u0B30\u0B4D\u0B1A\u0B4D\u0B1A", + "\u0B0F\u0B2A\u0B4D\u0B30\u0B3F\u0B32\u0B4D\u200C", + "\u0B2E\u0B47", + "\u0B1C\u0B41\u0B28\u0B4D\u200C", + "\u0B1C\u0B41\u0B32\u0B3E\u0B07", + "\u0B05\u0B17\u0B37\u0B4D\u0B1F", + "\u0B38\u0B47\u0B2A\u0B4D\u0B1F\u0B47\u0B2E\u0B4D\u0B2C\u0B30", + "\u0B05\u0B15\u0B4D\u0B1F\u0B4B\u0B2C\u0B30", + "\u0B28\u0B2D\u0B47\u0B2E\u0B4D\u0B2C\u0B30", + "\u0B21\u0B3F\u0B38\u0B47\u0B2E\u0B4D\u0B2C\u0B30", + } + // monthNamesOromo list the month names in the Oromo. + monthNamesOromo = []string{"Amajjii", "Guraandhala", "Bitooteessa", "Elba", "Caamsa", "Waxabajjii", "Adooleessa", "Hagayya", "Fuulbana", "Onkololeessa", "Sadaasa", "Muddee"} + // monthNamesOromoAbbr list the month abbreviations in the Oromo. + monthNamesOromoAbbr = []string{"Ama", "Gur", "Bit", "Elb", "Cam", "Wax", "Ado", "Hag", "Ful", "Onk", "Sad", "Mud"} + // monthNamesPashto list the month names in the Pashto. + monthNamesPashto = []string{ + "\u0633\u0644\u0648\u0627\u063A\u0647", + "\u0643\u0628", + "\u0648\u0631\u0649", + "\u063A\u0648\u064A\u0649", + "\u063A\u0628\u0631\u06AB\u0648\u0644\u0649", + "\u0686\u0646\u06AB\u0627 \u069A\u0632\u0645\u0631\u0649", + "\u0632\u0645\u0631\u0649", + "\u0648\u0696\u0649", + "\u062A\u0644\u0647", + "\u0644\u0693\u0645", + "\u0644\u0646\u0688 \u06CD", + "\u0645\u0631\u063A\u0648\u0645\u0649", + } // monthNamesRussian list the month names in the Russian. - monthNamesRussian = []string{"январь", "февраль", "март", "апрель", "май", "июнь", "июль", "август", "сентябрь", "октябрь", "ноябрь", "декабрь"} - // monthNamesRussianAbbr list the month abbreviations for Russian. - monthNamesRussianAbbr = []string{"янв.", "фев.", "март", "апр.", "май", "июнь", "июль", "авг.", "сен.", "окт.", "ноя.", "дек."} + monthNamesRussian = []string{ + "\u044F\u043D\u0432\u0430\u0440\u044C", + "\u0444\u0435\u0432\u0440\u0430\u043B\u044C", + "\u043C\u0430\u0440\u0442", + "\u0430\u043F\u0440\u0435\u043B\u044C", + "\u043C\u0430\u0439", + "\u0438\u044E\u043D\u044C", + "\u0438\u044E\u043B\u044C", + "\u0430\u0432\u0433\u0443\u0441\u0442", + "\u0441\u0435\u043D\u0442\u044F\u0431\u0440\u044C", + "\u043E\u043A\u0442\u044F\u0431\u0440\u044C", + "\u043D\u043E\u044F\u0431\u0440\u044C", + "\u0434\u0435\u043A\u0430\u0431\u0440\u044C", + } + // monthNamesRussianAbbr list the month abbreviations in the Russian. + monthNamesRussianAbbr = []string{ + "\u044F\u043D\u0432.", + "\u0444\u0435\u0432.", + "\u043C\u0430\u0440\u0442", + "\u0430\u043F\u0440.", + "\u043C\u0430\u0439", + "\u0438\u044E\u043D\u044C", + "\u0438\u044E\u043B\u044C", + "\u0430\u0432\u0433.", + "\u0441\u0435\u043D.", + "\u043E\u043A\u0442.", + "\u043D\u043E\u044F.", + "\u0434\u0435\u043A.", + } // 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 list the month abbreviations in the Spanish. monthNamesSpanishAbbr = []string{"ene", "feb", "mar", "abr", "may", "jun", "jul", "ago", "sep", "oct", "nov", "dic"} + // monthNamesSyllabics list the month names in the Syllabics. + monthNamesSyllabics = []string{ + "\u152E\u14D0\u14C4\u140A\u1546", + "\u1556\u155D\u1557\u140A\u1546", + "\u14AB\u1466\u14EF", + "\u1404\u1433\u1546", + "\u14AA\u1403", + "\u152B\u14C2", + "\u152A\u14DA\u1403", + "\u140B\u14A1\u148C\u14EF", + "\u14EF\u144E\u1431\u1546", + "\u1405\u1450\u1431\u1546", + "\u14C4\u1555\u1431\u1546", + "\u144E\u14EF\u1431\u1546", + } + // monthNamesSyllabicsAbbr lists the month name abbreviations in Syllabics. + monthNamesSyllabicsAbbr = []string{ + "\u152E\u14D0\u14C4", + "\u1556\u155D\u1557", + "\u14AB\u1466\u14EF", + "\u1404\u1433\u1546", + "\u14AA\u1403", + "\u152B\u14C2", + "\u152A\u14DA\u1403", + "\u140B\u14A1\u148C", + "\u14EF\u144E\u1431", + "\u1405\u1450\u1431", + "\u14C4\u1555\u1431", + "\u144E\u14EF\u1431", + } // monthNamesThai list the month names in the Thai. monthNamesThai = []string{ "\u0e21\u0e01\u0e23\u0e32\u0e04\u0e21", @@ -936,7 +1757,7 @@ 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 lists the month name abbreviations in Tibetan. monthNamesTibetanAbbr = []string{ "\u0f5f\u0fb3\u0f0b\u0f21", "\u0f5f\u0fb3\u0f0b\u0f22", @@ -951,6 +1772,8 @@ var ( "\u0f5f\u0fb3\u0f0b\u0f21\u0f21", "\u0f5f\u0fb3\u0f0b\u0f21\u0f22", } + // monthNamesTradMongolian lists the month number for use with traditional Mongolian. + monthNamesTradMongolian = []string{"M01", "M02", "M03", "M04", "M05", "M06", "M07", "M08", "M09", "M10", "M11", "M12"} // 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 @@ -981,16 +1804,64 @@ var ( monthNamesZulu = []string{"Januwari", "Febhuwari", "Mashi", "Ephreli", "Meyi", "Juni", "Julayi", "Agasti", "Septemba", "Okthoba", "Novemba", "Disemba"} // monthNamesZuluAbbr list the month name abbreviations in Zulu monthNamesZuluAbbr = []string{"Jan", "Feb", "Mas", "Eph", "Mey", "Jun", "Jul", "Agas", "Sep", "Okt", "Nov", "Dis"} + // weekdayNamesChinese list the weekday name in Chinese + weekdayNamesChinese = []string{"日", "一", "二", "三", "四", "五", "六"} // apFmtAfrikaans defined the AM/PM name in the Afrikaans. apFmtAfrikaans = "vm./nm." + // apFmtCameroon defined the AM/PM name in the Cameroon. + apFmtCameroon = "mat./soir" // apFmtCuba defined the AM/PM name in the Cuba. apFmtCuba = "a.m./p.m." + // apFmtFaroese defined the AM/PM name in the Faroese. + apFmtFaroese = "um fyr./um sein." + // apFmtFinnish defined the AM/PM name in the Finnish. + apFmtFinnish = "ap./ip." + // apFmtGreek defined the AM/PM name in the Greek. + apFmtGreek = "\u03C0\u03BC/\u03BC\u03BC" + // apFmtGujarati defined the AM/PM name in the Gujarati. + apFmtGujarati = "\u0AAA\u0AC2\u0AB0\u0ACD\u0AB5 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8/\u0A89\u0AA4\u0ACD\u0AA4\u0AB0 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8" + // apFmtHindi defined the AM/PM name in the Hindi. + apFmtHindi = "\u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928/\u0905\u092A\u0930\u093E\u0939\u094D\u0928" + // apFmtHungarian defined the AM/PM name in the Hungarian. + apFmtHungarian = "de./du." + // apFmtIcelandic defined the AM/PM name in the Icelandic. + apFmtIcelandic = "f.h./e.h." + // apFmtIgbo defined the AM/PM name in the Igbo. + apFmtIgbo = "A.M./P.M." // apFmtIrish defined the AM/PM name in the Irish. apFmtIrish = "r.n./i.n." // apFmtJapanese defined the AM/PM name in the Japanese. apFmtJapanese = "午前/午後" + // apFmtKannada defined the AM/PM name in the Kannada. + apFmtKannada = "\u0CAA\u0CC2\u0CB0\u0CCD\u0CB5\u0CBE\u0CB9\u0CCD\u0CA8/\u0C85\u0CAA\u0CB0\u0CBE\u0CB9\u0CCD\u0CA8" + // apFmtKhmer defined the AM/PM name in the Khmer. + apFmtKhmer = "\u1796\u17D2\u179A\u17B9\u1780/\u179B\u17D2\u1784\u17B6\u1785" + // apFmtKonkani defined the AM/PM name in the Konkani. + apFmtKonkani = "\u092E.\u092A\u0942./\u092E.\u0928\u0902." // apFmtKorean defined the AM/PM name in the Korean. apFmtKorean = "오전/오후" + // apFmtKyrgyz defined the AM/PM name in the Kyrgyz. + apFmtKyrgyz = "\u0442\u04A3/\u0442\u043A" + // apFmtLao defined the AM/PM name in the Lao. + apFmtLao = "\u0E81\u0EC8\u0EAD\u0E99\u0E97\u0EC8\u0EBD\u0E87/\u0EAB\u0EBC\u0EB1\u0E87\u0E97\u0EC8\u0EBD\u0E87" + // apFmtLatvian defined the AM/PM name in the Latvian. + apFmtLatvian = "priekšp./pēcp." + // apFmtLithuanian defined the AM/PM name in the Lithuanian. + apFmtLithuanian = "priešpiet/popiet" + // apFmtMacedonian defined the AM/PM name in the Macedonian. + apFmtMacedonian = "\u043F\u0440\u0435\u0442\u043F\u043B./\u043F\u043E\u043F\u043B." + // apFmtMalay defined the AM/PM name in the Malay. + apFmtMalay = "PG/PTG" + // apFmtMongolian defined the AM/PM name in the Mongolian. + apFmtMongolian = "\u04AF.\u04E9./\u04AF.\u0445." + // apFmtNigeria defined the AM/PM name in the Nigeria. + apFmtNigeria = "subaka/kikiiɗe" + // apFmtNorwegian defined the AM/PM name in the Norwegian. + apFmtNorwegian = "f.m./e.m." + // apFmtOromo defined the AM/PM name in the Oromo. + apFmtOromo = "WD/WB" + // apFmtPashto defined the AM/PM name in the Pashto. + apFmtPashto = "\u063A.\u0645./\u063A.\u0648." // apFmtSpanish defined the AM/PM name in the Spanish. apFmtSpanish = "a. m./p. m." // apFmtTibetan defined the AM/PM name in the Tibetan. @@ -999,12 +1870,12 @@ var ( apFmtTurkish = "\u00F6\u00F6/\u00F6\u0053" // apFmtVietnamese defined the AM/PM name in the Vietnamese. apFmtVietnamese = "SA/CH" + // apFmtWelsh defined the AM/PM name in the Welsh. + apFmtWelsh = "yb/yh" // apFmtWolof defined the AM/PM name in the Wolof. apFmtWolof = "Sub/Ngo" // apFmtYi defined the AM/PM name in the Yi. 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 { @@ -1455,17 +2326,6 @@ func (nf *numberFormat) localAmPm(ap string) string { return ap } -// localMonthsNameEnglish returns the English name of the month. -func localMonthsNameEnglish(t time.Time, abbr int) string { - if abbr == 3 { - return t.Month().String()[:3] - } - if abbr == 4 { - return t.Month().String() - } - return t.Month().String()[:1] -} - // localMonthsNameAfrikaans returns the Afrikaans name of the month. func localMonthsNameAfrikaans(t time.Time, abbr int) string { if abbr == 3 { @@ -1496,6 +2356,99 @@ func localMonthsNameBangla(t time.Time, abbr int) string { return string([]rune(monthNamesBangla[int(t.Month())-1])[:1]) } +// localMonthsNameCaribbean returns the Caribbean name of the month. +func localMonthsNameCaribbean(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesCaribbeanAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesCaribbean[int(t.Month())-1] + } + return monthNamesCaribbeanAbbr[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 monthNamesChineseNum[t.Month()] + } + if abbr == 4 { + return monthNamesChinese[int(t.Month())-1] + } + return monthNamesChineseAbbr[int(t.Month())-1] +} + +// 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 monthNamesChineseAbbr[int(t.Month())-1] +} + +// localMonthsNameChinese3 returns the Chinese name of the month. +func localMonthsNameChinese3(t time.Time, abbr int) string { + if abbr == 3 || abbr == 4 { + return monthNamesChineseNum[t.Month()] + } + return strconv.Itoa(int(t.Month())) +} + +// localMonthsNameEnglish returns the English name of the month. +func localMonthsNameEnglish(t time.Time, abbr int) string { + if abbr == 3 { + return t.Month().String()[:3] + } + if abbr == 4 { + return t.Month().String() + } + return t.Month().String()[:1] +} + +// localMonthsNameEstonian returns the Estonian name of the month. +func localMonthsNameEstonian(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesEstonianAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesEstonian[int(t.Month())-1] + } + return monthNamesEstonianAbbr[int(t.Month())-1][:1] +} + +// localMonthsNameFaroese returns the Faroese name of the month. +func localMonthsNameFaroese(t time.Time, abbr int) string { + if abbr == 3 { + return monthsNameFaroeseAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesFaroese[int(t.Month())-1] + } + return monthsNameFaroeseAbbr[int(t.Month())-1][:1] +} + +// localMonthsNameFilipino returns the Filipino name of the month. +func localMonthsNameFilipino(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesFilipinoAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesFilipino[int(t.Month())-1] + } + return fmt.Sprintf("%02d", int(t.Month())) +} + +// localMonthsNameFinnish returns the Finnish name of the month. +func localMonthsNameFinnish(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesFinnishAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesFinnish[int(t.Month())-1] + } + return fmt.Sprintf("%02d", int(t.Month())) +} + // localMonthsNameFrench returns the French name of the month. func localMonthsNameFrench(t time.Time, abbr int) string { if abbr == 3 { @@ -1511,6 +2464,201 @@ func localMonthsNameFrench(t time.Time, abbr int) string { return monthNamesFrenchAbbr[int(t.Month())-1][:1] } +// localMonthsNameFrisian returns the Frisian name of the month. +func localMonthsNameFrisian(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesFrisianAbbr[int(t.Month())-1] + } + if abbr == 4 || abbr > 6 { + return monthNamesFrisian[int(t.Month())-1] + } + return monthNamesFrisian[int(t.Month())-1][:1] +} + +// localMonthsNameFulah returns the Fulah name of the month. +func localMonthsNameFulah(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesFulahAbbr[int(t.Month())-1] + } + if abbr == 4 || abbr > 6 { + return monthNamesFulah[int(t.Month())-1] + } + return monthNamesFulah[int(t.Month())-1][:1] +} + +// localMonthsNameGalician returns the Galician name of the month. +func localMonthsNameGalician(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesGalicianAbbr[int(t.Month())-1] + } + if abbr == 4 || abbr > 6 { + return monthNamesGalician[int(t.Month())-1] + } + return monthNamesGalician[int(t.Month())-1][:1] +} + +// localMonthsNameGeorgian returns the Georgian name of the month. +func localMonthsNameGeorgian(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesGeorgianAbbr[int(t.Month())-1] + } + if abbr == 4 || abbr > 6 { + return monthNamesGeorgian[int(t.Month())-1] + } + return string([]rune(monthNamesGeorgian[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 monthNamesGermanAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesGerman[int(t.Month())-1] + } + return monthNamesGermanAbbr[int(t.Month())-1][:1] +} + +// localMonthsNameGreek returns the Greek name of the month. +func localMonthsNameGreek(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesGreekAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesGreek[int(t.Month())-1] + } + return string([]rune(monthNamesGreekAbbr[int(t.Month())-1])[:1]) +} + +// localMonthsNameGreenlandic returns the Greenlandic name of the month. +func localMonthsNameGreenlandic(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesGreenlandicAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesGreenlandic[int(t.Month())-1] + } + return string([]rune(monthNamesGreenlandicAbbr[int(t.Month())-1])[:1]) +} + +// localMonthsNameGuarani returns the Guarani name of the month. +func localMonthsNameGuarani(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesGuaraniAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesGuarani[int(t.Month())-1] + } + return string([]rune(monthNamesGuaraniAbbr[int(t.Month())-1])[:1]) +} + +// localMonthsNameGujarati returns the Gujarati name of the month. +func localMonthsNameGujarati(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesGujaratiAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesGujarati[int(t.Month())-1] + } + return string([]rune(monthNamesGujaratiAbbr[int(t.Month())-1])[:1]) +} + +// localMonthsNameHausa returns the Hausa name of the month. +func localMonthsNameHausa(t time.Time, abbr int) string { + if abbr == 3 { + return string([]rune(monthNamesHausa[int(t.Month())-1])[:3]) + } + if abbr == 4 { + return monthNamesHausa[int(t.Month())-1] + } + return string([]rune(monthNamesHausa[int(t.Month())-1])[:1]) +} + +// localMonthsNameHawaiian returns the Hawaiian name of the month. +func localMonthsNameHawaiian(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesHawaiianAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesHawaiian[int(t.Month())-1] + } + return string([]rune(monthNamesHawaiianAbbr[int(t.Month())-1])[:1]) +} + +// localMonthsNameHebrew returns the Hebrew name of the month. +func localMonthsNameHebrew(t time.Time, abbr int) string { + if abbr == 3 { + return string([]rune(monthNamesHebrew[int(t.Month())-1])[:3]) + } + if abbr == 4 { + return monthNamesHebrew[int(t.Month())-1] + } + return string([]rune(monthNamesHebrew[int(t.Month())-1])[:1]) +} + +// localMonthsNameHindi returns the Hindi name of the month. +func localMonthsNameHindi(t time.Time, abbr int) string { + if abbr == 3 || abbr == 4 { + return monthNamesHindi[int(t.Month())-1] + } + return string([]rune(monthNamesHindi[int(t.Month())-1])[:1]) +} + +// localMonthsNameHungarian returns the Hungarian name of the month. +func localMonthsNameHungarian(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesHungarianAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesHungarian[int(t.Month())-1] + } + return string([]rune(monthNamesHungarianAbbr[int(t.Month())-1])[:1]) +} + +// localMonthsNameIcelandic returns the Icelandic name of the month. +func localMonthsNameIcelandic(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesIcelandicAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesIcelandic[int(t.Month())-1] + } + return string([]rune(monthNamesIcelandicAbbr[int(t.Month())-1])[:1]) +} + +// localMonthsNameIgbo returns the Igbo name of the month. +func localMonthsNameIgbo(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesIgboAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesIgbo[int(t.Month())-1] + } + return string([]rune(monthNamesIgboAbbr[int(t.Month())-1])[:1]) +} + +// localMonthsNameIndonesian returns the Indonesian name of the month. +func localMonthsNameIndonesian(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesIndonesianAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesIndonesian[int(t.Month())-1] + } + return string([]rune(monthNamesIndonesianAbbr[int(t.Month())-1])[:1]) +} + +// localMonthsNameInuktitut returns the Inuktitut name of the month. +func localMonthsNameInuktitut(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesInuktitutAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesInuktitut[int(t.Month())-1] + } + return string([]rune(monthNamesInuktitutAbbr[int(t.Month())-1])[:1]) +} + // localMonthsNameIrish returns the Irish name of the month. func localMonthsNameIrish(t time.Time, abbr int) string { if abbr == 3 { @@ -1533,59 +2681,368 @@ func localMonthsNameItalian(t time.Time, abbr int) string { return monthNamesItalianAbbr[int(t.Month())-1][:1] } -// localMonthsNameGerman returns the German name of the month. -func localMonthsNameGerman(t time.Time, abbr int) string { +// localMonthsNameKannada returns the Kannada name of the month. +func localMonthsNameKannada(t time.Time, abbr int) string { if abbr == 3 { - return monthNamesGermanAbbr[int(t.Month())-1] + return monthNamesKannadaAbbr[int(t.Month())-1] } - if abbr == 4 { - return monthNamesGerman[int(t.Month())-1] + if abbr == 4 || abbr > 6 { + return monthNamesKannada[int(t.Month())-1] } - return monthNamesGermanAbbr[int(t.Month())-1][:1] + return string([]rune(monthNamesKannada[int(t.Month())-1])[:1]) } -// localMonthsNameChinese1 returns the Chinese name of the month. -func localMonthsNameChinese1(t time.Time, abbr int) string { +// localMonthsNameKashmiri returns the Kashmiri name of the month. +func localMonthsNameKashmiri(t time.Time, abbr int) string { + if abbr == 5 { + return string([]rune(monthNamesKashmiri[int(t.Month())-1])[:1]) + } + return monthNamesKashmiri[int(t.Month())-1] +} + +// localMonthsNameKazakh returns the Kazakh name of the month. +func localMonthsNameKazakh(t time.Time, abbr int) string { if abbr == 3 { - return monthNamesChineseAbbrPlus[t.Month()] + return monthNamesKazakhAbbr[int(t.Month())-1] } - if abbr == 4 { - return monthNamesChinesePlus[int(t.Month())-1] + if abbr == 4 || abbr > 6 { + return monthNamesKazakh[int(t.Month())-1] } - return monthNamesChinese[int(t.Month())-1] + return string([]rune(monthNamesKazakh[int(t.Month())-1])[:1]) } -// localMonthsNameChinese2 returns the Chinese name of the month. -func localMonthsNameChinese2(t time.Time, abbr int) string { - if abbr == 3 || abbr == 4 { - return monthNamesChinesePlus[int(t.Month())-1] +// localMonthsNameKhmer returns the Khmer name of the month. +func localMonthsNameKhmer(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesKhmerAbbr[int(t.Month())-1] + } + if abbr == 4 || abbr > 6 { + return monthNamesKhmer[int(t.Month())-1] } - return monthNamesChinese[int(t.Month())-1] + return string([]rune(monthNamesKhmerAbbr[int(t.Month())+11])[:1]) } -// localMonthsNameChinese3 returns the Chinese name of the month. -func localMonthsNameChinese3(t time.Time, abbr int) string { - if abbr == 3 || abbr == 4 { - return monthNamesChineseAbbrPlus[t.Month()] +// localMonthsNameKiche returns the Kiche name of the month. +func localMonthsNameKiche(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesKicheAbbr[int(t.Month())-1] } - return strconv.Itoa(int(t.Month())) + if abbr == 4 || abbr > 6 { + return monthNamesKiche[int(t.Month())-1] + } + return string([]rune(monthNamesKicheAbbr[int(t.Month()-1)])[:1]) +} + +// localMonthsNameKinyarwanda returns the Kinyarwanda name of the month. +func localMonthsNameKinyarwanda(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesKinyarwandaAbbr[int(t.Month())-1] + } + if abbr == 4 || abbr > 6 { + return monthNamesKinyarwanda[int(t.Month())-1] + } + return string([]rune(monthNamesKinyarwanda[int(t.Month()-1)])[:1]) +} + +// localMonthsNameKiswahili returns the Kiswahili name of the month. +func localMonthsNameKiswahili(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesKiswahiliAbbr[int(t.Month())-1] + } + if abbr == 4 || abbr > 6 { + return monthNamesKiswahili[int(t.Month())-1] + } + return string([]rune(monthNamesKiswahili[int(t.Month()-1)])[:1]) +} + +// localMonthsNameKonkani returns the Konkani name of the month. +func localMonthsNameKonkani(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesKonkaniAbbr[int(t.Month())-1] + } + if abbr == 4 || abbr > 6 { + return monthNamesKonkani[int(t.Month())-1] + } + return string([]rune(monthNamesKonkani[int(t.Month()-1)])[:1]) } // localMonthsNameKorean returns the Korean name of the month. func localMonthsNameKorean(t time.Time, abbr int) string { - if abbr == 3 || abbr == 4 { - return monthNamesKoreanAbbrPlus[t.Month()] + if abbr == 4 || abbr > 6 { + return monthNamesKoreanAbbr[int(t.Month())-1] } return strconv.Itoa(int(t.Month())) } -// localMonthsNameTraditionalMongolian returns the Traditional Mongolian name of -// the month. -func localMonthsNameTraditionalMongolian(t time.Time, abbr int) string { +// localMonthsNameKyrgyz returns the Kyrgyz name of the month. +func localMonthsNameKyrgyz(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesKyrgyzAbbr[int(t.Month())-1] + } + if abbr == 4 || abbr > 6 { + return monthNamesKyrgyz[int(t.Month())-1] + } + return string([]rune(monthNamesKyrgyz[int(t.Month()-1)])[:1]) +} + +// localMonthsNameLao returns the Lao name of the month. +func localMonthsNameLao(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesLaoAbbr[int(t.Month())-1] + } + if abbr == 4 || abbr > 6 { + return monthNamesLao[int(t.Month())-1] + } + return string([]rune(monthNamesLao[int(t.Month()-1)])[:1]) +} + +// localMonthsNameLatin returns the Latin name of the month. +func localMonthsNameLatin(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesLatinAbbr[int(t.Month())-1] + } + if abbr == 4 || abbr > 6 { + return monthNamesLatin[int(t.Month())-1] + } + return string([]rune(monthNamesLatin[int(t.Month()-1)])[:1]) +} + +// localMonthsNameLatvian returns the Latvian name of the month. +func localMonthsNameLatvian(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesLatvianAbbr[int(t.Month())-1] + } + if abbr == 4 || abbr > 6 { + return monthNamesLatvian[int(t.Month())-1] + } + return string([]rune(monthNamesLatvian[int(t.Month()-1)])[:1]) +} + +// localMonthsNameLithuanian returns the Lithuanian name of the month. +func localMonthsNameLithuanian(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesLithuanianAbbr[int(t.Month())-1] + } + if abbr == 4 || abbr > 6 { + return monthNamesLithuanian[int(t.Month())-1] + } + return string([]rune(monthNamesLithuanian[int(t.Month()-1)])[:1]) +} + +// localMonthsNameLowerSorbian returns the LowerSorbian name of the month. +func localMonthsNameLowerSorbian(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesLowerSorbianAbbr[int(t.Month())-1] + } + if abbr == 4 || abbr > 6 { + return monthNamesLowerSorbian[int(t.Month())-1] + } + return string([]rune(monthNamesLowerSorbian[int(t.Month()-1)])[:1]) +} + +// localMonthsNameLuxembourgish returns the Luxembourgish name of the month. +func localMonthsNameLuxembourgish(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesLuxembourgishAbbr[int(t.Month())-1] + } + if abbr == 4 || abbr > 6 { + return monthNamesLuxembourgish[int(t.Month())-1] + } + return string([]rune(monthNamesLuxembourgish[int(t.Month()-1)])[:1]) +} + +// localMonthsNameMacedonian returns the Macedonian name of the month. +func localMonthsNameMacedonian(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesMacedonianAbbr[int(t.Month())-1] + } + if abbr == 4 || abbr > 6 { + return monthNamesMacedonian[int(t.Month())-1] + } + return string([]rune(monthNamesMacedonian[int(t.Month()-1)])[:1]) +} + +// localMonthsNameMalay returns the Malay name of the month. +func localMonthsNameMalay(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesMalayAbbr[int(t.Month())-1] + } + if abbr == 4 || abbr > 6 { + return monthNamesMalay[int(t.Month())-1] + } + return string([]rune(monthNamesMalay[int(t.Month()-1)])[:1]) +} + +// localMonthsNameMalayalam returns the Malayalam name of the month. +func localMonthsNameMalayalam(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesMalayalamAbbr[int(t.Month())-1] + } + if abbr == 4 || abbr > 6 { + return monthNamesMalayalam[int(t.Month())-1] + } + return string([]rune(monthNamesMalayalam[int(t.Month()-1)])[:1]) +} + +// localMonthsNameMaltese returns the Maltese name of the month. +func localMonthsNameMaltese(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesMalteseAbbr[int(t.Month())-1] + } + if abbr == 4 || abbr > 6 { + return monthNamesMaltese[int(t.Month())-1] + } + return string([]rune(monthNamesMaltese[int(t.Month()-1)])[:1]) +} + +// localMonthsNameMaori returns the Maori name of the month. +func localMonthsNameMaori(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesMaoriAbbr[int(t.Month())-1] + } + if abbr == 4 || abbr > 6 { + return monthNamesMaori[int(t.Month())-1] + } + return string([]rune(monthNamesMaori[int(t.Month()-1)])[:1]) +} + +// localMonthsNameMapudungun returns the Mapudungun name of the month. +func localMonthsNameMapudungun(t time.Time, abbr int) string { if abbr == 5 { - return "M" + return string([]rune(monthNamesMapudungun[int(t.Month()-1)])[:1]) } - return monthNamesTradMongolian[t.Month()-1] + return monthNamesMapudungun[int(t.Month())-1] +} + +// localMonthsNameMarathi returns the Marathi name of the month. +func localMonthsNameMarathi(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesMarathiAbbr[int(t.Month())-1] + } + if abbr == 4 || abbr > 6 { + return monthNamesMarathi[int(t.Month())-1] + } + return string([]rune(monthNamesMarathi[int(t.Month()-1)])[:1]) +} + +// localMonthsNameMohawk returns the Mohawk name of the month. +func localMonthsNameMohawk(t time.Time, abbr int) string { + if abbr == 3 { + return t.Month().String()[:3] + } + if abbr == 4 || abbr > 6 { + return monthNamesMohawk[int(t.Month())-1] + } + return string([]rune(monthNamesMohawk[int(t.Month()-1)])[:1]) +} + +// localMonthsNameMongolian returns the Mongolian name of the month. +func localMonthsNameMongolian(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesMongolianAbbr[int(t.Month())-1] + } + if abbr == 4 || abbr > 6 { + return monthNamesMongolian[int(t.Month())-1] + } + return string([]rune(monthNamesMongolian[int(t.Month()-1)])[:1]) +} + +// localMonthsNameMorocco returns the Morocco name of the month. +func localMonthsNameMorocco(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesMoroccoAbbr[int(t.Month())-1] + } + if abbr == 4 || abbr > 6 { + return monthNamesFrench[int(t.Month())-1] + } + return monthNamesFrench[int(t.Month())-1][:1] +} + +// localMonthsNameNepali returns the Nepali name of the month. +func localMonthsNameNepali(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesNepaliAbbr[int(t.Month())-1] + } + if abbr == 4 || abbr > 6 { + return monthNamesNepali[int(t.Month())-1] + } + return string([]rune(monthNamesNepali[int(t.Month()-1)])[:1]) +} + +// localMonthsNameNepaliIN returns the India Nepali name of the month. +func localMonthsNameNepaliIN(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesNepaliINAbbr[int(t.Month())-1] + } + if abbr == 4 || abbr > 6 { + return monthNamesNepaliIN[int(t.Month())-1] + } + return string([]rune(monthNamesNepaliIN[int(t.Month()-1)])[:1]) +} + +// localMonthsNameNigeria returns the Nigeria name of the month. +func localMonthsNameNigeria(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesNigeriaAbbr[int(t.Month())-1] + } + if abbr == 4 || abbr > 6 { + return monthNamesNigeria[int(t.Month())-1] + } + return monthNamesNigeria[int(t.Month())-1][:1] +} + +// localMonthsNameNorwegian returns the Norwegian name of the month. +func localMonthsNameNorwegian(t time.Time, abbr int) string { + if abbr == 3 { + return monthsNameFaroeseAbbr[int(t.Month())-1] + } + if abbr == 4 || abbr > 6 { + return monthNamesNorwegian[int(t.Month())-1] + } + return string([]rune(monthNamesNorwegian[int(t.Month()-1)])[:1]) +} + +// localMonthsNameOccitan returns the Occitan name of the month. +func localMonthsNameOccitan(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesOccitanAbbr[int(t.Month())-1] + } + if abbr == 4 || abbr > 6 { + return monthNamesOccitan[int(t.Month())-1] + } + return string([]rune(monthNamesOccitan[int(t.Month()-1)])[:1]) +} + +// localMonthsNameOdia returns the Odia name of the month. +func localMonthsNameOdia(t time.Time, abbr int) string { + if abbr == 5 { + return string([]rune(monthNamesOdia[int(t.Month()-1)])[:1]) + } + return monthNamesOdia[int(t.Month())-1] +} + +// localMonthsNameOromo returns the Oromo name of the month. +func localMonthsNameOromo(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesOromoAbbr[int(t.Month())-1] + } + if abbr == 4 || abbr > 6 { + return monthNamesOromo[int(t.Month())-1] + } + return string([]rune(monthNamesOromo[int(t.Month()-1)])[:1]) +} + +// localMonthsNamePashto returns the Pashto name of the month. +func localMonthsNamePashto(t time.Time, abbr int) string { + if int(t.Month()) == 6 { + if abbr == 3 { + return "\u0686\u0646\u06AB\u0627 \u069A" + } + if abbr == 4 || abbr > 6 { + return "\u0686\u0646\u06AB\u0627 \u069A\u0632\u0645\u0631\u0649" + } + } + return monthNamesPashto[int(t.Month())-1] } // localMonthsNameRussian returns the Russian name of the month. @@ -1614,6 +3071,17 @@ func localMonthsNameSpanish(t time.Time, abbr int) string { return monthNamesSpanishAbbr[int(t.Month())-1][:1] } +// localMonthsNameSyllabics returns the Syllabics name of the month. +func localMonthsNameSyllabics(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesSyllabicsAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesSyllabics[int(t.Month())-1] + } + return string([]rune(monthNamesSyllabics[int(t.Month())-1])[:1]) +} + // localMonthsNameThai returns the Thai name of the month. func localMonthsNameThai(t time.Time, abbr int) string { if abbr == 3 { @@ -1640,6 +3108,15 @@ func localMonthsNameTibetan(t time.Time, abbr int) string { return monthNamesTibetan[int(t.Month())-1] } +// localMonthsNameTraditionalMongolian returns the Traditional Mongolian name of +// the month. +func localMonthsNameTraditionalMongolian(t time.Time, abbr int) string { + if abbr == 5 { + return "M" + } + return monthNamesTradMongolian[t.Month()-1] +} + // localMonthsNameTurkish returns the Turkish name of the month. func localMonthsNameTurkish(t time.Time, abbr int) string { if abbr == 3 { @@ -1651,17 +3128,6 @@ func localMonthsNameTurkish(t time.Time, abbr int) string { 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 { - return monthNamesWelshAbbr[int(t.Month())-1] - } - if abbr == 4 { - return monthNamesWelsh[int(t.Month())-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 { @@ -1673,6 +3139,17 @@ func localMonthsNameVietnamese(t time.Time, abbr int) string { return monthNamesVietnamese[t.Month()-1] } +// localMonthsNameWelsh returns the Welsh name of the month. +func localMonthsNameWelsh(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesWelshAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesWelsh[int(t.Month())-1] + } + return monthNamesWelshAbbr[int(t.Month())-1][:1] +} + // localMonthsNameWolof returns the Wolof name of the month. func localMonthsNameWolof(t time.Time, abbr int) string { if abbr == 3 { @@ -1714,7 +3191,7 @@ func localMonthsNameZulu(t time.Time, abbr int) string { return monthNamesZuluAbbr[int(t.Month())-1][:1] } -// localMonthName return months name by supported language ID. +// localMonthsName return months name by supported language ID. func (nf *numberFormat) localMonthsName(abbr int) string { if languageInfo, ok := supportedLanguageInfo[nf.localCode]; ok { return languageInfo.localMonth(nf.t, abbr) @@ -1830,6 +3307,22 @@ func (nf *numberFormat) yearsHandler(token nfp.Token) { // daysHandler will be handling days in the date and times types tokens for a // number format expression. func (nf *numberFormat) daysHandler(token nfp.Token) { + if strings.Contains(strings.ToUpper(token.TValue), "A") { + l := len(token.TValue) + if nf.localCode == "804" || nf.localCode == "404" { + var prefix string + if l == 3 { + prefix = map[string]string{"404": "週", "804": "周"}[nf.localCode] + } + if l > 3 { + prefix = "星期" + } + nf.result += prefix + weekdayNamesChinese[int(nf.t.Weekday())] + return + } + nf.result += nf.t.Weekday().String() + return + } if strings.Contains(strings.ToUpper(token.TValue), "D") { switch len(token.TValue) { case 1: @@ -1843,7 +3336,6 @@ func (nf *numberFormat) daysHandler(token nfp.Token) { return default: nf.result += nf.t.Weekday().String() - return } } } @@ -1894,7 +3386,6 @@ func (nf *numberFormat) minutesHandler(token nfp.Token) { return default: nf.result += fmt.Sprintf("%02d", nf.t.Minute()) - return } } } diff --git a/numfmt_test.go b/numfmt_test.go index 971d629965..f6aff17556 100644 --- a/numfmt_test.go +++ b/numfmt_test.go @@ -72,6 +72,12 @@ func TestNumFmt(t *testing.T) { {"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"}, + {"43466.189571759256", "[$-404]aaa;@", "週二"}, + {"43466.189571759256", "[$-404]aaaa;@", "星期二"}, + {"43466.189571759256", "[$-804]aaa;@", "周二"}, + {"43466.189571759256", "[$-804]aaaa;@", "星期二"}, + {"43466.189571759256", "[$-36]aaa;@", "Tuesday"}, + {"43466.189571759256", "[$-36]aaaa;@", "Tuesday"}, {"44562.189571759256", "[$-36]mmm dd yyyy h:mm AM/PM", "Jan. 01 2022 4:32 vm."}, {"44562.189571759256", "[$-36]mmmm dd yyyy h:mm AM/PM", "Januarie 01 2022 4:32 vm."}, {"44562.189571759256", "[$-36]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 vm."}, @@ -117,9 +123,9 @@ func TestNumFmt(t *testing.T) { {"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", "[$-2809]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-2809]mmmm dd yyyy h:mm AM/PM", "March 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-2809]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"}, @@ -165,12 +171,208 @@ func TestNumFmt(t *testing.T) { {"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"}, + {"43543.503206018519", "[$-25]mmm dd yyyy h:mm AM/PM", "märts 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-25]mmmm dd yyyy h:mm AM/PM", "märts 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-25]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-425]mmm dd yyyy h:mm AM/PM", "märts 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-425]mmmm dd yyyy h:mm AM/PM", "märts 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-425]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-38]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 um sein."}, + {"43543.503206018519", "[$-38]mmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 um sein."}, + {"43543.503206018519", "[$-38]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 um sein."}, + {"43543.503206018519", "[$-438]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 um sein."}, + {"43543.503206018519", "[$-438]mmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 um sein."}, + {"43543.503206018519", "[$-438]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 um sein."}, + {"43543.503206018519", "[$-64]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-64]mmmm dd yyyy h:mm AM/PM", "Marso 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-64]mmmmm dd yyyy h:mm AM/PM", "03 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-464]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-464]mmmm dd yyyy h:mm AM/PM", "Marso 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-464]mmmmm dd yyyy h:mm AM/PM", "03 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-B]mmm dd yyyy h:mm AM/PM", "maalis 19 2019 12:04 ip."}, + {"43543.503206018519", "[$-B]mmmm dd yyyy h:mm AM/PM", "maaliskuu 19 2019 12:04 ip."}, + {"43543.503206018519", "[$-B]mmmmm dd yyyy h:mm AM/PM", "03 19 2019 12:04 ip."}, + {"43543.503206018519", "[$-40B]mmm dd yyyy h:mm AM/PM", "maalis 19 2019 12:04 ip."}, + {"43543.503206018519", "[$-40B]mmmm dd yyyy h:mm AM/PM", "maaliskuu 19 2019 12:04 ip."}, + {"43543.503206018519", "[$-40B]mmmmm dd yyyy h:mm AM/PM", "03 19 2019 12:04 ip."}, {"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 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"}, + {"44562.189571759256", "[$-80C]mmm dd yyyy h:mm AM/PM", "janv. 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-80C]mmmm dd yyyy h:mm AM/PM", "janvier 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-80C]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-80C]mmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-80C]mmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-80C]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-2c0C]mmm dd yyyy h:mm AM/PM", "janv. 01 2022 4:32 mat."}, + {"44562.189571759256", "[$-2c0C]mmmm dd yyyy h:mm AM/PM", "janvier 01 2022 4:32 mat."}, + {"44562.189571759256", "[$-2c0C]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 mat."}, + {"43543.503206018519", "[$-2c0C]mmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 soir"}, + {"43543.503206018519", "[$-2c0C]mmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 soir"}, + {"43543.503206018519", "[$-2c0C]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 soir"}, + {"44562.189571759256", "[$-c0C]mmm dd yyyy h:mm AM/PM", "janv. 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-c0C]mmmm dd yyyy h:mm AM/PM", "janvier 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-c0C]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-c0C]mmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-c0C]mmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-c0C]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-1C0C]mmm dd yyyy h:mm AM/PM", "Janv. 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-1C0C]mmmm dd yyyy h:mm AM/PM", "Janvier 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-1C0C]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-1C0C]mmm dd yyyy h:mm AM/PM", "Mars 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-1C0C]mmmm dd yyyy h:mm AM/PM", "Mars 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-1C0C]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-240C]mmm dd yyyy h:mm AM/PM", "janv. 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-240C]mmmm dd yyyy h:mm AM/PM", "janvier 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-240C]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-240C]mmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-240C]mmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-240C]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-300C]mmm dd yyyy h:mm AM/PM", "janv. 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-300C]mmmm dd yyyy h:mm AM/PM", "janvier 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-300C]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-300C]mmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-300C]mmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-300C]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-40C]mmm dd yyyy h:mm AM/PM", "janv. 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-40C]mmmm dd yyyy h:mm AM/PM", "janvier 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-40C]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-40C]mmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-40C]mmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-40C]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-3c0C]mmm dd yyyy h:mm AM/PM", "janv. 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-3c0C]mmmm dd yyyy h:mm AM/PM", "janvier 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-3c0C]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-3c0C]mmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-3c0C]mmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-3c0C]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-140C]mmm dd yyyy h:mm AM/PM", "janv. 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-140C]mmmm dd yyyy h:mm AM/PM", "janvier 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-140C]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-140C]mmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-140C]mmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-140C]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-340C]mmm dd yyyy h:mm AM/PM", "janv. 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-340C]mmmm dd yyyy h:mm AM/PM", "janvier 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-340C]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-340C]mmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-340C]mmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-340C]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-380C]mmm dd yyyy h:mm AM/PM", "jan. 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-380C]mmmm dd yyyy h:mm AM/PM", "janvier 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-380C]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-380C]mmm dd yyyy h:mm AM/PM", "mar. 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-380C]mmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-380C]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-180C]mmm dd yyyy h:mm AM/PM", "janv. 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-180C]mmmm dd yyyy h:mm AM/PM", "janvier 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-180C]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-180C]mmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-180C]mmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-180C]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-200C]mmm dd yyyy h:mm AM/PM", "janv. 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-200C]mmmm dd yyyy h:mm AM/PM", "janvier 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-200C]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-200C]mmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-200C]mmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-200C]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-280C]mmm dd yyyy h:mm AM/PM", "janv. 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-280C]mmmm dd yyyy h:mm AM/PM", "janvier 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-280C]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-280C]mmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-280C]mmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-280C]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-62]m dd yyyy h:mm AM/PM", "1 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-62]mm dd yyyy h:mm AM/PM", "01 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-62]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-62]mmmm dd yyyy h:mm AM/PM", "Jannewaris 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-62]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-62]mmmmmm dd yyyy h:mm AM/PM", "Jannewaris 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-62]m dd yyyy h:mm AM/PM", "3 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-62]mm dd yyyy h:mm AM/PM", "03 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-62]mmm dd yyyy h:mm AM/PM", "Mrt 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-62]mmmm dd yyyy h:mm AM/PM", "Maart 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-62]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-62]mmmmmm dd yyyy h:mm AM/PM", "Maart 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-462]m dd yyyy h:mm AM/PM", "1 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-462]mm dd yyyy h:mm AM/PM", "01 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-462]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-462]mmmm dd yyyy h:mm AM/PM", "Jannewaris 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-462]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-462]mmmmmm dd yyyy h:mm AM/PM", "Jannewaris 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-462]m dd yyyy h:mm AM/PM", "3 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-462]mm dd yyyy h:mm AM/PM", "03 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-462]mmm dd yyyy h:mm AM/PM", "Mrt 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-462]mmmm dd yyyy h:mm AM/PM", "Maart 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-462]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-462]mmmmmm dd yyyy h:mm AM/PM", "Maart 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-67]mmm dd yyyy h:mm AM/PM", "sii 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-67]mmmm dd yyyy h:mm AM/PM", "siilo 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-67]mmmmm dd yyyy h:mm AM/PM", "s 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-67]mmmmmm dd yyyy h:mm AM/PM", "siilo 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-67]mmm dd yyyy h:mm AM/PM", "mbo 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-67]mmmm dd yyyy h:mm AM/PM", "mbooy 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-67]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-67]mmmmmm dd yyyy h:mm AM/PM", "mbooy 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-7C67]mmm dd yyyy h:mm AM/PM", "sii 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-7C67]mmmm dd yyyy h:mm AM/PM", "siilo 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-7C67]mmmmm dd yyyy h:mm AM/PM", "s 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-7C67]mmmmmm dd yyyy h:mm AM/PM", "siilo 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-7C67]mmm dd yyyy h:mm AM/PM", "mbo 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-7C67]mmmm dd yyyy h:mm AM/PM", "mbooy 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-7C67]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-7C67]mmmmmm dd yyyy h:mm AM/PM", "mbooy 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-467]mmm dd yyyy h:mm AM/PM", "samw 01 2022 4:32 subaka"}, + {"44562.189571759256", "[$-467]mmmm dd yyyy h:mm AM/PM", "samwiee 01 2022 4:32 subaka"}, + {"44562.189571759256", "[$-467]mmmmm dd yyyy h:mm AM/PM", "s 01 2022 4:32 subaka"}, + {"44562.189571759256", "[$-467]mmmmmm dd yyyy h:mm AM/PM", "samwiee 01 2022 4:32 subaka"}, + {"43543.503206018519", "[$-467]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 kikiiɗe"}, + {"43543.503206018519", "[$-467]mmmm dd yyyy h:mm AM/PM", "marsa 19 2019 12:04 kikiiɗe"}, + {"43543.503206018519", "[$-467]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 kikiiɗe"}, + {"43543.503206018519", "[$-467]mmmmmm dd yyyy h:mm AM/PM", "marsa 19 2019 12:04 kikiiɗe"}, + {"44562.189571759256", "[$-867]mmm dd yyyy h:mm AM/PM", "samw 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-867]mmmm dd yyyy h:mm AM/PM", "samwiee 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-867]mmmmm dd yyyy h:mm AM/PM", "s 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-867]mmmmmm dd yyyy h:mm AM/PM", "samwiee 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-867]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-867]mmmm dd yyyy h:mm AM/PM", "marsa 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-867]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-867]mmmmmm dd yyyy h:mm AM/PM", "marsa 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-56]mmm dd yyyy h:mm AM/PM", "Xan. 01 2022 4:32 a.m."}, + {"44562.189571759256", "[$-56]mmmm dd yyyy h:mm AM/PM", "Xaneiro 01 2022 4:32 a.m."}, + {"44562.189571759256", "[$-56]mmmmm dd yyyy h:mm AM/PM", "X 01 2022 4:32 a.m."}, + {"44562.189571759256", "[$-56]mmmmmm dd yyyy h:mm AM/PM", "Xaneiro 01 2022 4:32 a.m."}, + {"43543.503206018519", "[$-56]mmm dd yyyy h:mm AM/PM", "Mar. 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-56]mmmm dd yyyy h:mm AM/PM", "Marzo 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-56]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-56]mmmmmm dd yyyy h:mm AM/PM", "Marzo 19 2019 12:04 p.m."}, + {"44562.189571759256", "[$-56]mmm dd yyyy h:mm AM/PM", "Xan. 01 2022 4:32 a.m."}, + {"44562.189571759256", "[$-56]mmmm dd yyyy h:mm AM/PM", "Xaneiro 01 2022 4:32 a.m."}, + {"44562.189571759256", "[$-56]mmmmm dd yyyy h:mm AM/PM", "X 01 2022 4:32 a.m."}, + {"44562.189571759256", "[$-56]mmmmmm dd yyyy h:mm AM/PM", "Xaneiro 01 2022 4:32 a.m."}, + {"43543.503206018519", "[$-56]mmm dd yyyy h:mm AM/PM", "Mar. 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-56]mmmm dd yyyy h:mm AM/PM", "Marzo 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-56]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-56]mmmmmm dd yyyy h:mm AM/PM", "Marzo 19 2019 12:04 p.m."}, + {"44562.189571759256", "[$-37]mmm dd yyyy h:mm AM/PM", "\u10D8\u10D0\u10DC 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-37]mmmm dd yyyy h:mm AM/PM", "\u10D8\u10D0\u10DC\u10D5\u10D0\u10E0\u10D8 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-37]mmmmm dd yyyy h:mm AM/PM", "\u10D8 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-37]mmmmmm dd yyyy h:mm AM/PM", "\u10D8\u10D0\u10DC\u10D5\u10D0\u10E0\u10D8 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-37]mmm dd yyyy h:mm AM/PM", "\u10DB\u10D0\u10E0 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-37]mmmm dd yyyy h:mm AM/PM", "\u10DB\u10D0\u10E0\u10E2\u10D8 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-37]mmmmm dd yyyy h:mm AM/PM", "\u10DB 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-37]mmmmmm dd yyyy h:mm AM/PM", "\u10DB\u10D0\u10E0\u10E2\u10D8 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-437]mmm dd yyyy h:mm AM/PM", "\u10D8\u10D0\u10DC 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-437]mmmm dd yyyy h:mm AM/PM", "\u10D8\u10D0\u10DC\u10D5\u10D0\u10E0\u10D8 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-437]mmmmm dd yyyy h:mm AM/PM", "\u10D8 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-437]mmmmmm dd yyyy h:mm AM/PM", "\u10D8\u10D0\u10DC\u10D5\u10D0\u10E0\u10D8 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-437]mmm dd yyyy h:mm AM/PM", "\u10DB\u10D0\u10E0 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-437]mmmm dd yyyy h:mm AM/PM", "\u10DB\u10D0\u10E0\u10E2\u10D8 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-437]mmmmm dd yyyy h:mm AM/PM", "\u10DB 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-437]mmmmmm dd yyyy h:mm AM/PM", "\u10DB\u10D0\u10E0\u10E2\u10D8 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"}, @@ -180,30 +382,244 @@ func TestNumFmt(t *testing.T) { {"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."}, - {"44652.18957170139", "[$-83C]mmm dd yyyy h:mm AM/PM", "Aib 01 2022 4:32 r.n."}, - {"44682.18957170139", "[$-83C]mmm dd yyyy h:mm AM/PM", "Beal 01 2022 4:32 r.n."}, - {"44713.18957170139", "[$-83C]mmm dd yyyy h:mm AM/PM", "Meith 01 2022 4:32 r.n."}, - {"44743.18957170139", "[$-83C]mmm dd yyyy h:mm AM/PM", "Iúil 01 2022 4:32 r.n."}, - {"44774.18957170139", "[$-83C]mmm dd yyyy h:mm AM/PM", "Lún 01 2022 4:32 r.n."}, - {"44805.18957170139", "[$-83C]mmm dd yyyy h:mm AM/PM", "MFómh 01 2022 4:32 r.n."}, - {"44835.18957170139", "[$-83C]mmm dd yyyy h:mm AM/PM", "DFómh 01 2022 4:32 r.n."}, - {"44866.18957170139", "[$-83C]mmm dd yyyy h:mm AM/PM", "Samh 01 2022 4:32 r.n."}, - {"44896.18957170139", "[$-83C]mmm dd yyyy h:mm AM/PM", "Noll 01 2022 4:32 r.n."}, - {"44562.189571759256", "[$-83C]mmmm dd yyyy h:mm AM/PM", "Eanáir 01 2022 4:32 r.n."}, - {"44593.189571759256", "[$-83C]mmmm dd yyyy h:mm AM/PM", "Feabhra 01 2022 4:32 r.n."}, - {"44621.18957170139", "[$-83C]mmmm dd yyyy h:mm AM/PM", "Márta 01 2022 4:32 r.n."}, - {"44652.18957170139", "[$-83C]mmmm dd yyyy h:mm AM/PM", "Aibreán 01 2022 4:32 r.n."}, - {"44682.18957170139", "[$-83C]mmmm dd yyyy h:mm AM/PM", "Bealtaine 01 2022 4:32 r.n."}, - {"44713.18957170139", "[$-83C]mmmm dd yyyy h:mm AM/PM", "Meitheamh 01 2022 4:32 r.n."}, - {"44743.18957170139", "[$-83C]mmmm dd yyyy h:mm AM/PM", "Iúil 01 2022 4:32 r.n."}, - {"44774.18957170139", "[$-83C]mmmm dd yyyy h:mm AM/PM", "Lúnasa 01 2022 4:32 r.n."}, - {"44805.18957170139", "[$-83C]mmmm dd yyyy h:mm AM/PM", "Meán Fómhair 01 2022 4:32 r.n."}, - {"44835.18957170139", "[$-83C]mmmm dd yyyy h:mm AM/PM", "Deireadh Fómhair 01 2022 4:32 r.n."}, - {"44866.18957170139", "[$-83C]mmmm dd yyyy h:mm AM/PM", "Samhain 01 2022 4:32 r.n."}, - {"44896.18957170139", "[$-83C]mmmm dd yyyy h:mm AM/PM", "Nollaig 01 2022 4:32 r.n."}, + {"43543.503206018519", "[$-1407]mmm dd yyyy h:mm AM/PM", "Mär 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-1407]mmmm dd yyyy h:mm AM/PM", "März 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-1407]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-807]mmm dd yyyy h:mm AM/PM", "Mär 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-807]mmmm dd yyyy h:mm AM/PM", "März 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-807]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-8]mmm dd yyyy h:mm AM/PM", "\u0399\u03B1\u03BD 01 2022 4:32 \u03C0\u03BC"}, + {"44562.189571759256", "[$-8]mmmm dd yyyy h:mm AM/PM", "\u0399\u03B1\u03BD\u03BF\u03C5\u03AC\u03C1\u03B9\u03BF\u03C2 01 2022 4:32 \u03C0\u03BC"}, + {"44562.189571759256", "[$-8]mmmmm dd yyyy h:mm AM/PM", "\u0399 01 2022 4:32 \u03C0\u03BC"}, + {"44562.189571759256", "[$-8]mmmmmm dd yyyy h:mm AM/PM", "\u0399\u03B1\u03BD\u03BF\u03C5\u03AC\u03C1\u03B9\u03BF\u03C2 01 2022 4:32 \u03C0\u03BC"}, + {"43543.503206018519", "[$-8]mmm dd yyyy h:mm AM/PM", "\u039C\u03B1\u03C1 19 2019 12:04 \u03BC\u03BC"}, + {"43543.503206018519", "[$-8]mmmm dd yyyy h:mm AM/PM", "\u039C\u03AC\u03C1\u03C4\u03B9\u03BF\u03C2 19 2019 12:04 \u03BC\u03BC"}, + {"43543.503206018519", "[$-8]mmmmm dd yyyy h:mm AM/PM", "\u039C 19 2019 12:04 \u03BC\u03BC"}, + {"43543.503206018519", "[$-8]mmmmmm dd yyyy h:mm AM/PM", "\u039C\u03AC\u03C1\u03C4\u03B9\u03BF\u03C2 19 2019 12:04 \u03BC\u03BC"}, + {"44562.189571759256", "[$-408]mmm dd yyyy h:mm AM/PM", "\u0399\u03B1\u03BD 01 2022 4:32 \u03C0\u03BC"}, + {"44562.189571759256", "[$-408]mmmm dd yyyy h:mm AM/PM", "\u0399\u03B1\u03BD\u03BF\u03C5\u03AC\u03C1\u03B9\u03BF\u03C2 01 2022 4:32 \u03C0\u03BC"}, + {"44562.189571759256", "[$-408]mmmmm dd yyyy h:mm AM/PM", "\u0399 01 2022 4:32 \u03C0\u03BC"}, + {"44562.189571759256", "[$-408]mmmmmm dd yyyy h:mm AM/PM", "\u0399\u03B1\u03BD\u03BF\u03C5\u03AC\u03C1\u03B9\u03BF\u03C2 01 2022 4:32 \u03C0\u03BC"}, + {"43543.503206018519", "[$-408]mmm dd yyyy h:mm AM/PM", "\u039C\u03B1\u03C1 19 2019 12:04 \u03BC\u03BC"}, + {"43543.503206018519", "[$-408]mmmm dd yyyy h:mm AM/PM", "\u039C\u03AC\u03C1\u03C4\u03B9\u03BF\u03C2 19 2019 12:04 \u03BC\u03BC"}, + {"43543.503206018519", "[$-408]mmmmm dd yyyy h:mm AM/PM", "\u039C 19 2019 12:04 \u03BC\u03BC"}, + {"43543.503206018519", "[$-408]mmmmmm dd yyyy h:mm AM/PM", "\u039C\u03AC\u03C1\u03C4\u03B9\u03BF\u03C2 19 2019 12:04 \u03BC\u03BC"}, + {"44562.189571759256", "[$-6F]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-6F]mmmm dd yyyy h:mm AM/PM", "januaari 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-6F]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-6F]mmmmmm dd yyyy h:mm AM/PM", "januaari 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-6F]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-6F]mmmm dd yyyy h:mm AM/PM", "marsi 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-6F]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-6F]mmmmmm dd yyyy h:mm AM/PM", "marsi 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-46F]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-46F]mmmm dd yyyy h:mm AM/PM", "januaari 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-46F]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-46F]mmmmmm dd yyyy h:mm AM/PM", "januaari 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-46F]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-46F]mmmm dd yyyy h:mm AM/PM", "marsi 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-46F]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-46F]mmmmmm dd yyyy h:mm AM/PM", "marsi 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-74]mmm dd yyyy h:mm AM/PM", "jteĩ 01 2022 4:32 a.m."}, + {"44562.189571759256", "[$-74]mmmm dd yyyy h:mm AM/PM", "jasyteĩ 01 2022 4:32 a.m."}, + {"44562.189571759256", "[$-74]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 a.m."}, + {"44562.189571759256", "[$-74]mmmmmm dd yyyy h:mm AM/PM", "jasyteĩ 01 2022 4:32 a.m."}, + {"43543.503206018519", "[$-74]mmm dd yyyy h:mm AM/PM", "japy 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-74]mmmm dd yyyy h:mm AM/PM", "jasyapy 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-74]mmmmm dd yyyy h:mm AM/PM", "j 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-74]mmmmmm dd yyyy h:mm AM/PM", "jasyapy 19 2019 12:04 p.m."}, + {"44562.189571759256", "[$-474]mmm dd yyyy h:mm AM/PM", "jteĩ 01 2022 4:32 a.m."}, + {"44562.189571759256", "[$-474]mmmm dd yyyy h:mm AM/PM", "jasyteĩ 01 2022 4:32 a.m."}, + {"44562.189571759256", "[$-474]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 a.m."}, + {"44562.189571759256", "[$-474]mmmmmm dd yyyy h:mm AM/PM", "jasyteĩ 01 2022 4:32 a.m."}, + {"43543.503206018519", "[$-474]mmm dd yyyy h:mm AM/PM", "japy 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-474]mmmm dd yyyy h:mm AM/PM", "jasyapy 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-474]mmmmm dd yyyy h:mm AM/PM", "j 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-474]mmmmmm dd yyyy h:mm AM/PM", "jasyapy 19 2019 12:04 p.m."}, + {"44562.189571759256", "[$-47]mmm dd yyyy h:mm AM/PM", "\u0A9C\u0ABE\u0AA8\u0ACD\u0AAF\u0AC1 01 2022 4:32 \u0AAA\u0AC2\u0AB0\u0ACD\u0AB5 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8"}, + {"44562.189571759256", "[$-47]mmmm dd yyyy h:mm AM/PM", "\u0A9C\u0ABE\u0AA8\u0ACD\u0AAF\u0AC1\u0A86\u0AB0\u0AC0 01 2022 4:32 \u0AAA\u0AC2\u0AB0\u0ACD\u0AB5 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8"}, + {"44562.189571759256", "[$-47]mmmmm dd yyyy h:mm AM/PM", "\u0A9C 01 2022 4:32 \u0AAA\u0AC2\u0AB0\u0ACD\u0AB5 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8"}, + {"44562.189571759256", "[$-47]mmmmmm dd yyyy h:mm AM/PM", "\u0A9C\u0ABE\u0AA8\u0ACD\u0AAF\u0AC1\u0A86\u0AB0\u0AC0 01 2022 4:32 \u0AAA\u0AC2\u0AB0\u0ACD\u0AB5 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8"}, + {"43543.503206018519", "[$-47]mmm dd yyyy h:mm AM/PM", "\u0AAE\u0ABE\u0AB0\u0ACD\u0A9A 19 2019 12:04 \u0A89\u0AA4\u0ACD\u0AA4\u0AB0 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8"}, + {"43543.503206018519", "[$-47]mmmm dd yyyy h:mm AM/PM", "\u0AAE\u0ABE\u0AB0\u0ACD\u0A9A 19 2019 12:04 \u0A89\u0AA4\u0ACD\u0AA4\u0AB0 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8"}, + {"43543.503206018519", "[$-47]mmmmm dd yyyy h:mm AM/PM", "\u0AAE 19 2019 12:04 \u0A89\u0AA4\u0ACD\u0AA4\u0AB0 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8"}, + {"43543.503206018519", "[$-47]mmmmmm dd yyyy h:mm AM/PM", "\u0AAE\u0ABE\u0AB0\u0ACD\u0A9A 19 2019 12:04 \u0A89\u0AA4\u0ACD\u0AA4\u0AB0 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8"}, + {"44562.189571759256", "[$-447]mmm dd yyyy h:mm AM/PM", "\u0A9C\u0ABE\u0AA8\u0ACD\u0AAF\u0AC1 01 2022 4:32 \u0AAA\u0AC2\u0AB0\u0ACD\u0AB5 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8"}, + {"44562.189571759256", "[$-447]mmmm dd yyyy h:mm AM/PM", "\u0A9C\u0ABE\u0AA8\u0ACD\u0AAF\u0AC1\u0A86\u0AB0\u0AC0 01 2022 4:32 \u0AAA\u0AC2\u0AB0\u0ACD\u0AB5 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8"}, + {"44562.189571759256", "[$-447]mmmmm dd yyyy h:mm AM/PM", "\u0A9C 01 2022 4:32 \u0AAA\u0AC2\u0AB0\u0ACD\u0AB5 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8"}, + {"44562.189571759256", "[$-447]mmmmmm dd yyyy h:mm AM/PM", "\u0A9C\u0ABE\u0AA8\u0ACD\u0AAF\u0AC1\u0A86\u0AB0\u0AC0 01 2022 4:32 \u0AAA\u0AC2\u0AB0\u0ACD\u0AB5 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8"}, + {"43543.503206018519", "[$-447]mmm dd yyyy h:mm AM/PM", "\u0AAE\u0ABE\u0AB0\u0ACD\u0A9A 19 2019 12:04 \u0A89\u0AA4\u0ACD\u0AA4\u0AB0 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8"}, + {"43543.503206018519", "[$-447]mmmm dd yyyy h:mm AM/PM", "\u0AAE\u0ABE\u0AB0\u0ACD\u0A9A 19 2019 12:04 \u0A89\u0AA4\u0ACD\u0AA4\u0AB0 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8"}, + {"43543.503206018519", "[$-447]mmmmm dd yyyy h:mm AM/PM", "\u0AAE 19 2019 12:04 \u0A89\u0AA4\u0ACD\u0AA4\u0AB0 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8"}, + {"43543.503206018519", "[$-447]mmmmmm dd yyyy h:mm AM/PM", "\u0AAE\u0ABE\u0AB0\u0ACD\u0A9A 19 2019 12:04 \u0A89\u0AA4\u0ACD\u0AA4\u0AB0 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8"}, + {"44562.189571759256", "[$-68]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-68]mmmm dd yyyy h:mm AM/PM", "Janairu 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-68]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-68]mmmmmm dd yyyy h:mm AM/PM", "Janairu 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-68]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-68]mmmm dd yyyy h:mm AM/PM", "Maris 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-68]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-68]mmmmmm dd yyyy h:mm AM/PM", "Maris 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-7C68]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-7C68]mmmm dd yyyy h:mm AM/PM", "Janairu 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-7C68]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-7C68]mmmmmm dd yyyy h:mm AM/PM", "Janairu 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-7C68]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-7C68]mmmm dd yyyy h:mm AM/PM", "Maris 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-7C68]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-7C68]mmmmmm dd yyyy h:mm AM/PM", "Maris 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-468]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-468]mmmm dd yyyy h:mm AM/PM", "Janairu 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-468]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-468]mmmmmm dd yyyy h:mm AM/PM", "Janairu 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-468]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-468]mmmm dd yyyy h:mm AM/PM", "Maris 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-468]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-468]mmmmmm dd yyyy h:mm AM/PM", "Maris 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-75]mmm dd yyyy h:mm AM/PM", "Ian. 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-75]mmmm dd yyyy h:mm AM/PM", "Ianuali 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-75]mmmmm dd yyyy h:mm AM/PM", "I 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-75]mmmmmm dd yyyy h:mm AM/PM", "Ianuali 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-75]mmm dd yyyy h:mm AM/PM", "Mal. 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-75]mmmm dd yyyy h:mm AM/PM", "Malaki 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-75]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-75]mmmmmm dd yyyy h:mm AM/PM", "Malaki 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-475]mmm dd yyyy h:mm AM/PM", "Ian. 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-475]mmmm dd yyyy h:mm AM/PM", "Ianuali 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-475]mmmmm dd yyyy h:mm AM/PM", "I 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-475]mmmmmm dd yyyy h:mm AM/PM", "Ianuali 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-475]mmm dd yyyy h:mm AM/PM", "Mal. 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-475]mmmm dd yyyy h:mm AM/PM", "Malaki 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-475]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-475]mmmmmm dd yyyy h:mm AM/PM", "Malaki 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-D]mmm dd yyyy h:mm AM/PM", "\u05D9\u05E0\u05D5 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-D]mmmm dd yyyy h:mm AM/PM", "\u05D9\u05E0\u05D5\u05D0\u05E8 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-D]mmmmm dd yyyy h:mm AM/PM", "\u05D9 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-D]mmmmmm dd yyyy h:mm AM/PM", "\u05D9\u05E0\u05D5\u05D0\u05E8 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-D]mmm dd yyyy h:mm AM/PM", "\u05DE\u05E8\u05E5 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-D]mmmm dd yyyy h:mm AM/PM", "\u05DE\u05E8\u05E5 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-D]mmmmm dd yyyy h:mm AM/PM", "\u05DE 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-D]mmmmmm dd yyyy h:mm AM/PM", "\u05DE\u05E8\u05E5 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-40D]mmm dd yyyy h:mm AM/PM", "\u05D9\u05E0\u05D5 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-40D]mmmm dd yyyy h:mm AM/PM", "\u05D9\u05E0\u05D5\u05D0\u05E8 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-40D]mmmmm dd yyyy h:mm AM/PM", "\u05D9 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-40D]mmmmmm dd yyyy h:mm AM/PM", "\u05D9\u05E0\u05D5\u05D0\u05E8 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-40D]mmm dd yyyy h:mm AM/PM", "\u05DE\u05E8\u05E5 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-40D]mmmm dd yyyy h:mm AM/PM", "\u05DE\u05E8\u05E5 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-40D]mmmmm dd yyyy h:mm AM/PM", "\u05DE 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-40D]mmmmmm dd yyyy h:mm AM/PM", "\u05DE\u05E8\u05E5 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-39]mmm dd yyyy h:mm AM/PM", "\u091C\u0928\u0935\u0930\u0940 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"}, + {"44562.189571759256", "[$-39]mmmm dd yyyy h:mm AM/PM", "\u091C\u0928\u0935\u0930\u0940 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"}, + {"44562.189571759256", "[$-39]mmmmm dd yyyy h:mm AM/PM", "\u091C 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"}, + {"44562.189571759256", "[$-39]mmmmmm dd yyyy h:mm AM/PM", "\u091C\u0928\u0935\u0930\u0940 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"}, + {"43543.503206018519", "[$-39]mmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928"}, + {"43543.503206018519", "[$-39]mmmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928"}, + {"43543.503206018519", "[$-39]mmmmm dd yyyy h:mm AM/PM", "\u092E 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928"}, + {"43543.503206018519", "[$-39]mmmmmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928"}, + {"44562.189571759256", "[$-E]mmm dd yyyy h:mm AM/PM", "jan. 01 2022 4:32 de."}, + {"44562.189571759256", "[$-E]mmmm dd yyyy h:mm AM/PM", "január 01 2022 4:32 de."}, + {"44562.189571759256", "[$-E]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 de."}, + {"44562.189571759256", "[$-E]mmmmmm dd yyyy h:mm AM/PM", "január 01 2022 4:32 de."}, + {"43543.503206018519", "[$-E]mmm dd yyyy h:mm AM/PM", "márc. 19 2019 12:04 du."}, + {"43543.503206018519", "[$-E]mmmm dd yyyy h:mm AM/PM", "március 19 2019 12:04 du."}, + {"43543.503206018519", "[$-E]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 du."}, + {"43543.503206018519", "[$-E]mmmmmm dd yyyy h:mm AM/PM", "március 19 2019 12:04 du."}, + {"44562.189571759256", "[$-40E]mmm dd yyyy h:mm AM/PM", "jan. 01 2022 4:32 de."}, + {"44562.189571759256", "[$-40E]mmmm dd yyyy h:mm AM/PM", "január 01 2022 4:32 de."}, + {"44562.189571759256", "[$-40E]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 de."}, + {"44562.189571759256", "[$-40E]mmmmmm dd yyyy h:mm AM/PM", "január 01 2022 4:32 de."}, + {"43543.503206018519", "[$-40E]mmm dd yyyy h:mm AM/PM", "márc. 19 2019 12:04 du."}, + {"43543.503206018519", "[$-40E]mmmm dd yyyy h:mm AM/PM", "március 19 2019 12:04 du."}, + {"43543.503206018519", "[$-40E]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 du."}, + {"43543.503206018519", "[$-40E]mmmmmm dd yyyy h:mm AM/PM", "március 19 2019 12:04 du."}, + {"44562.189571759256", "[$-F]mmm dd yyyy h:mm AM/PM", "jan. 01 2022 4:32 f.h."}, + {"44562.189571759256", "[$-F]mmmm dd yyyy h:mm AM/PM", "janúar 01 2022 4:32 f.h."}, + {"44562.189571759256", "[$-F]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 f.h."}, + {"44562.189571759256", "[$-F]mmmmmm dd yyyy h:mm AM/PM", "janúar 01 2022 4:32 f.h."}, + {"43543.503206018519", "[$-F]mmm dd yyyy h:mm AM/PM", "mar. 19 2019 12:04 e.h."}, + {"43543.503206018519", "[$-F]mmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 e.h."}, + {"43543.503206018519", "[$-F]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 e.h."}, + {"43543.503206018519", "[$-F]mmmmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 e.h."}, + {"44562.189571759256", "[$-40F]mmm dd yyyy h:mm AM/PM", "jan. 01 2022 4:32 f.h."}, + {"44562.189571759256", "[$-40F]mmmm dd yyyy h:mm AM/PM", "janúar 01 2022 4:32 f.h."}, + {"44562.189571759256", "[$-40F]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 f.h."}, + {"44562.189571759256", "[$-40F]mmmmmm dd yyyy h:mm AM/PM", "janúar 01 2022 4:32 f.h."}, + {"43543.503206018519", "[$-40F]mmm dd yyyy h:mm AM/PM", "mar. 19 2019 12:04 e.h."}, + {"43543.503206018519", "[$-40F]mmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 e.h."}, + {"43543.503206018519", "[$-40F]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 e.h."}, + {"43543.503206018519", "[$-40F]mmmmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 e.h."}, + {"44562.189571759256", "[$-70]mmm dd yyyy h:mm AM/PM", "Jen 01 2022 4:32 A.M."}, + {"44562.189571759256", "[$-70]mmmm dd yyyy h:mm AM/PM", "Jenụwarị 01 2022 4:32 A.M."}, + {"44562.189571759256", "[$-70]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 A.M."}, + {"44562.189571759256", "[$-70]mmmmmm dd yyyy h:mm AM/PM", "Jenụwarị 01 2022 4:32 A.M."}, + {"43543.503206018519", "[$-70]mmm dd yyyy h:mm AM/PM", "Mac 19 2019 12:04 P.M."}, + {"43543.503206018519", "[$-70]mmmm dd yyyy h:mm AM/PM", "Machị 19 2019 12:04 P.M."}, + {"43543.503206018519", "[$-70]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 P.M."}, + {"43543.503206018519", "[$-70]mmmmmm dd yyyy h:mm AM/PM", "Machị 19 2019 12:04 P.M."}, + {"44562.189571759256", "[$-470]mmm dd yyyy h:mm AM/PM", "Jen 01 2022 4:32 A.M."}, + {"44562.189571759256", "[$-470]mmmm dd yyyy h:mm AM/PM", "Jenụwarị 01 2022 4:32 A.M."}, + {"44562.189571759256", "[$-470]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 A.M."}, + {"44562.189571759256", "[$-470]mmmmmm dd yyyy h:mm AM/PM", "Jenụwarị 01 2022 4:32 A.M."}, + {"43543.503206018519", "[$-470]mmm dd yyyy h:mm AM/PM", "Mac 19 2019 12:04 P.M."}, + {"43543.503206018519", "[$-470]mmmm dd yyyy h:mm AM/PM", "Machị 19 2019 12:04 P.M."}, + {"43543.503206018519", "[$-470]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 P.M."}, + {"43543.503206018519", "[$-470]mmmmmm dd yyyy h:mm AM/PM", "Machị 19 2019 12:04 P.M."}, + {"44562.189571759256", "[$-21]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-21]mmmm dd yyyy h:mm AM/PM", "Januari 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-21]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-21]mmmmmm dd yyyy h:mm AM/PM", "Januari 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-21]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-21]mmmm dd yyyy h:mm AM/PM", "Maret 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-21]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-21]mmmmmm dd yyyy h:mm AM/PM", "Maret 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-421]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-421]mmmm dd yyyy h:mm AM/PM", "Januari 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-421]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-421]mmmmmm dd yyyy h:mm AM/PM", "Januari 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-421]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-421]mmmm dd yyyy h:mm AM/PM", "Maret 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-421]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-421]mmmmmm dd yyyy h:mm AM/PM", "Maret 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-5D]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-5D]mmmm dd yyyy h:mm AM/PM", "Jaannuari 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-5D]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-5D]mmmmmm dd yyyy h:mm AM/PM", "Jaannuari 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-5D]mmm dd yyyy h:mm AM/PM", "Mas 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-5D]mmmm dd yyyy h:mm AM/PM", "Maatsi 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-5D]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-5D]mmmmmm dd yyyy h:mm AM/PM", "Maatsi 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-7C5D]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-7C5D]mmmm dd yyyy h:mm AM/PM", "Jaannuari 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-7C5D]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-7C5D]mmmmmm dd yyyy h:mm AM/PM", "Jaannuari 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-7C5D]mmm dd yyyy h:mm AM/PM", "Mas 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-7C5D]mmmm dd yyyy h:mm AM/PM", "Maatsi 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-7C5D]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-7C5D]mmmmmm dd yyyy h:mm AM/PM", "Maatsi 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-85D]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-85D]mmmm dd yyyy h:mm AM/PM", "Jaannuari 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-85D]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-85D]mmmmmm dd yyyy h:mm AM/PM", "Jaannuari 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-85D]mmm dd yyyy h:mm AM/PM", "Mas 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-85D]mmmm dd yyyy h:mm AM/PM", "Maatsi 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-85D]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-85D]mmmmmm dd yyyy h:mm AM/PM", "Maatsi 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-785D]mmm dd yyyy h:mm AM/PM", "\u152E\u14D0\u14C4 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-785D]mmmm dd yyyy h:mm AM/PM", "\u152E\u14D0\u14C4\u140A\u1546 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-785D]mmmmm dd yyyy h:mm AM/PM", "\u152E 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-785D]mmmmmm dd yyyy h:mm AM/PM", "\u152E\u14D0\u14C4\u140A\u1546 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-785D]mmm dd yyyy h:mm AM/PM", "\u14AB\u1466\u14EF 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-785D]mmmm dd yyyy h:mm AM/PM", "\u14AB\u1466\u14EF 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-785D]mmmmm dd yyyy h:mm AM/PM", "\u14AB 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-785D]mmmmmm dd yyyy h:mm AM/PM", "\u14AB\u1466\u14EF 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-45D]mmm dd yyyy h:mm AM/PM", "\u152E\u14D0\u14C4 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-45D]mmmm dd yyyy h:mm AM/PM", "\u152E\u14D0\u14C4\u140A\u1546 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-45D]mmmmm dd yyyy h:mm AM/PM", "\u152E 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-45D]mmmmmm dd yyyy h:mm AM/PM", "\u152E\u14D0\u14C4\u140A\u1546 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-45D]mmm dd yyyy h:mm AM/PM", "\u14AB\u1466\u14EF 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-45D]mmmm dd yyyy h:mm AM/PM", "\u14AB\u1466\u14EF 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-45D]mmmmm dd yyyy h:mm AM/PM", "\u14AB 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-45D]mmmmmm dd yyyy h:mm AM/PM", "\u14AB\u1466\u14EF 19 2019 12:04 PM"}, {"44562.189571759256", "[$-3C]mmm dd yyyy h:mm AM/PM", "Ean 01 2022 4:32 r.n."}, {"44593.189571759256", "[$-3C]mmm dd yyyy h:mm AM/PM", "Feabh 01 2022 4:32 r.n."}, {"44621.18957170139", "[$-3C]mmm dd yyyy h:mm AM/PM", "Márta 01 2022 4:32 r.n."}, @@ -240,21 +656,467 @@ 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."}, + {"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."}, + {"44652.18957170139", "[$-83C]mmm dd yyyy h:mm AM/PM", "Aib 01 2022 4:32 r.n."}, + {"44682.18957170139", "[$-83C]mmm dd yyyy h:mm AM/PM", "Beal 01 2022 4:32 r.n."}, + {"44713.18957170139", "[$-83C]mmm dd yyyy h:mm AM/PM", "Meith 01 2022 4:32 r.n."}, + {"44743.18957170139", "[$-83C]mmm dd yyyy h:mm AM/PM", "Iúil 01 2022 4:32 r.n."}, + {"44774.18957170139", "[$-83C]mmm dd yyyy h:mm AM/PM", "Lún 01 2022 4:32 r.n."}, + {"44805.18957170139", "[$-83C]mmm dd yyyy h:mm AM/PM", "MFómh 01 2022 4:32 r.n."}, + {"44835.18957170139", "[$-83C]mmm dd yyyy h:mm AM/PM", "DFómh 01 2022 4:32 r.n."}, + {"44866.18957170139", "[$-83C]mmm dd yyyy h:mm AM/PM", "Samh 01 2022 4:32 r.n."}, + {"44896.18957170139", "[$-83C]mmm dd yyyy h:mm AM/PM", "Noll 01 2022 4:32 r.n."}, + {"44562.189571759256", "[$-83C]mmmm dd yyyy h:mm AM/PM", "Eanáir 01 2022 4:32 r.n."}, + {"44593.189571759256", "[$-83C]mmmm dd yyyy h:mm AM/PM", "Feabhra 01 2022 4:32 r.n."}, + {"44621.18957170139", "[$-83C]mmmm dd yyyy h:mm AM/PM", "Márta 01 2022 4:32 r.n."}, + {"44652.18957170139", "[$-83C]mmmm dd yyyy h:mm AM/PM", "Aibreán 01 2022 4:32 r.n."}, + {"44682.18957170139", "[$-83C]mmmm dd yyyy h:mm AM/PM", "Bealtaine 01 2022 4:32 r.n."}, + {"44713.18957170139", "[$-83C]mmmm dd yyyy h:mm AM/PM", "Meitheamh 01 2022 4:32 r.n."}, + {"44743.18957170139", "[$-83C]mmmm dd yyyy h:mm AM/PM", "Iúil 01 2022 4:32 r.n."}, + {"44774.18957170139", "[$-83C]mmmm dd yyyy h:mm AM/PM", "Lúnasa 01 2022 4:32 r.n."}, + {"44805.18957170139", "[$-83C]mmmm dd yyyy h:mm AM/PM", "Meán Fómhair 01 2022 4:32 r.n."}, + {"44835.18957170139", "[$-83C]mmmm dd yyyy h:mm AM/PM", "Deireadh Fómhair 01 2022 4:32 r.n."}, + {"44866.18957170139", "[$-83C]mmmm dd yyyy h:mm AM/PM", "Samhain 01 2022 4:32 r.n."}, + {"44896.18957170139", "[$-83C]mmmm dd yyyy h:mm AM/PM", "Nollaig 01 2022 4:32 r.n."}, {"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", "[$-410]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-410]mmmm dd yyyy h:mm AM/PM", "marzo 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-410]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-810]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-810]mmmm dd yyyy h:mm AM/PM", "marzo 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-810]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 오후"}, + {"44562.189571759256", "[$-4B]mmm dd yyyy h:mm AM/PM", "\u0C9C\u0CA8\u0CB5\u0CB0\u0CBF 01 2022 4:32 \u0CAA\u0CC2\u0CB0\u0CCD\u0CB5\u0CBE\u0CB9\u0CCD\u0CA8"}, + {"44562.189571759256", "[$-4B]mmmm dd yyyy h:mm AM/PM", "\u0C9C\u0CA8\u0CB5\u0CB0\u0CBF 01 2022 4:32 \u0CAA\u0CC2\u0CB0\u0CCD\u0CB5\u0CBE\u0CB9\u0CCD\u0CA8"}, + {"44562.189571759256", "[$-4B]mmmmm dd yyyy h:mm AM/PM", "\u0C9C 01 2022 4:32 \u0CAA\u0CC2\u0CB0\u0CCD\u0CB5\u0CBE\u0CB9\u0CCD\u0CA8"}, + {"44562.189571759256", "[$-4B]mmmmmm dd yyyy h:mm AM/PM", "\u0C9C\u0CA8\u0CB5\u0CB0\u0CBF 01 2022 4:32 \u0CAA\u0CC2\u0CB0\u0CCD\u0CB5\u0CBE\u0CB9\u0CCD\u0CA8"}, + {"43543.503206018519", "[$-4B]mmm dd yyyy h:mm AM/PM", "\u0CAE\u0CBE\u0CB0\u0CCD\u0C9A\u0CCD 19 2019 12:04 \u0C85\u0CAA\u0CB0\u0CBE\u0CB9\u0CCD\u0CA8"}, + {"43543.503206018519", "[$-4B]mmmm dd yyyy h:mm AM/PM", "\u0CAE\u0CBE\u0CB0\u0CCD\u0C9A\u0CCD 19 2019 12:04 \u0C85\u0CAA\u0CB0\u0CBE\u0CB9\u0CCD\u0CA8"}, + {"43543.503206018519", "[$-4B]mmmmm dd yyyy h:mm AM/PM", "\u0CAE 19 2019 12:04 \u0C85\u0CAA\u0CB0\u0CBE\u0CB9\u0CCD\u0CA8"}, + {"43543.503206018519", "[$-4B]mmmmmm dd yyyy h:mm AM/PM", "\u0CAE\u0CBE\u0CB0\u0CCD\u0C9A\u0CCD 19 2019 12:04 \u0C85\u0CAA\u0CB0\u0CBE\u0CB9\u0CCD\u0CA8"}, + {"44562.189571759256", "[$-44B]mmm dd yyyy h:mm AM/PM", "\u0C9C\u0CA8\u0CB5\u0CB0\u0CBF 01 2022 4:32 \u0CAA\u0CC2\u0CB0\u0CCD\u0CB5\u0CBE\u0CB9\u0CCD\u0CA8"}, + {"44562.189571759256", "[$-44B]mmmm dd yyyy h:mm AM/PM", "\u0C9C\u0CA8\u0CB5\u0CB0\u0CBF 01 2022 4:32 \u0CAA\u0CC2\u0CB0\u0CCD\u0CB5\u0CBE\u0CB9\u0CCD\u0CA8"}, + {"44562.189571759256", "[$-44B]mmmmm dd yyyy h:mm AM/PM", "\u0C9C 01 2022 4:32 \u0CAA\u0CC2\u0CB0\u0CCD\u0CB5\u0CBE\u0CB9\u0CCD\u0CA8"}, + {"44562.189571759256", "[$-44B]mmmmmm dd yyyy h:mm AM/PM", "\u0C9C\u0CA8\u0CB5\u0CB0\u0CBF 01 2022 4:32 \u0CAA\u0CC2\u0CB0\u0CCD\u0CB5\u0CBE\u0CB9\u0CCD\u0CA8"}, + {"43543.503206018519", "[$-44B]mmm dd yyyy h:mm AM/PM", "\u0CAE\u0CBE\u0CB0\u0CCD\u0C9A\u0CCD 19 2019 12:04 \u0C85\u0CAA\u0CB0\u0CBE\u0CB9\u0CCD\u0CA8"}, + {"43543.503206018519", "[$-44B]mmmm dd yyyy h:mm AM/PM", "\u0CAE\u0CBE\u0CB0\u0CCD\u0C9A\u0CCD 19 2019 12:04 \u0C85\u0CAA\u0CB0\u0CBE\u0CB9\u0CCD\u0CA8"}, + {"43543.503206018519", "[$-44B]mmmmm dd yyyy h:mm AM/PM", "\u0CAE 19 2019 12:04 \u0C85\u0CAA\u0CB0\u0CBE\u0CB9\u0CCD\u0CA8"}, + {"43543.503206018519", "[$-44B]mmmmmm dd yyyy h:mm AM/PM", "\u0CAE\u0CBE\u0CB0\u0CCD\u0C9A\u0CCD 19 2019 12:04 \u0C85\u0CAA\u0CB0\u0CBE\u0CB9\u0CCD\u0CA8"}, + {"44562.189571759256", "[$-471]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-471]mmmm dd yyyy h:mm AM/PM", "January 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-471]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-471]mmmmmm dd yyyy h:mm AM/PM", "January 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-471]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-471]mmmm dd yyyy h:mm AM/PM", "March 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-471]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-471]mmmmmm dd yyyy h:mm AM/PM", "March 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-60]mmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0624\u0631\u06CC 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-60]mmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0624\u0631\u06CC 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-60]mmmmm dd yyyy h:mm AM/PM", "\u062C 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-60]mmmmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0624\u0631\u06CC 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-60]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0655\u0686 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-60]mmmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0655\u0686 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-60]mmmmm dd yyyy h:mm AM/PM", "\u0645 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-60]mmmmmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0655\u0686 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-460]mmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0624\u0631\u06CC 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-460]mmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0624\u0631\u06CC 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-460]mmmmm dd yyyy h:mm AM/PM", "\u062C 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-460]mmmmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0624\u0631\u06CC 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-460]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0655\u0686 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-460]mmmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0655\u0686 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-460]mmmmm dd yyyy h:mm AM/PM", "\u0645 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-460]mmmmmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0655\u0686 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-860]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-860]mmmm dd yyyy h:mm AM/PM", "January 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-860]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-860]mmmmmm dd yyyy h:mm AM/PM", "January 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-860]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-860]mmmm dd yyyy h:mm AM/PM", "March 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-860]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-860]mmmmmm dd yyyy h:mm AM/PM", "March 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-3F]mmm dd yyyy h:mm AM/PM", "қаң 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-3F]mmmm dd yyyy h:mm AM/PM", "Қаңтар 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-3F]mmmmm dd yyyy h:mm AM/PM", "Қ 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-3F]mmmmmm dd yyyy h:mm AM/PM", "Қаңтар 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-3F]mmm dd yyyy h:mm AM/PM", "нау 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-3F]mmmm dd yyyy h:mm AM/PM", "Наурыз 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-3F]mmmmm dd yyyy h:mm AM/PM", "Н 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-3F]mmmmmm dd yyyy h:mm AM/PM", "Наурыз 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-43F]mmm dd yyyy h:mm AM/PM", "қаң 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-43F]mmmm dd yyyy h:mm AM/PM", "Қаңтар 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-43F]mmmmm dd yyyy h:mm AM/PM", "Қ 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-43F]mmmmmm dd yyyy h:mm AM/PM", "Қаңтар 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-43F]mmm dd yyyy h:mm AM/PM", "нау 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-43F]mmmm dd yyyy h:mm AM/PM", "Наурыз 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-43F]mmmmm dd yyyy h:mm AM/PM", "Н 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-43F]mmmmmm dd yyyy h:mm AM/PM", "Наурыз 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-53]mmm dd yyyy h:mm AM/PM", "\u17E1 01 2022 4:32 \u1796\u17D2\u179A\u17B9\u1780"}, + {"44562.189571759256", "[$-53]mmmm dd yyyy h:mm AM/PM", "\u1798\u1780\u179A\u17B6 01 2022 4:32 \u1796\u17D2\u179A\u17B9\u1780"}, + {"44562.189571759256", "[$-53]mmmmm dd yyyy h:mm AM/PM", "\u1798 01 2022 4:32 \u1796\u17D2\u179A\u17B9\u1780"}, + {"44562.189571759256", "[$-53]mmmmmm dd yyyy h:mm AM/PM", "\u1798\u1780\u179A\u17B6 01 2022 4:32 \u1796\u17D2\u179A\u17B9\u1780"}, + {"43543.503206018519", "[$-53]mmm dd yyyy h:mm AM/PM", "\u17E3 19 2019 12:04 \u179B\u17D2\u1784\u17B6\u1785"}, + {"43543.503206018519", "[$-53]mmmm dd yyyy h:mm AM/PM", "\u1798\u17B7\u1793\u17B6 19 2019 12:04 \u179B\u17D2\u1784\u17B6\u1785"}, + {"43543.503206018519", "[$-53]mmmmm dd yyyy h:mm AM/PM", "\u1798 19 2019 12:04 \u179B\u17D2\u1784\u17B6\u1785"}, + {"43543.503206018519", "[$-53]mmmmmm dd yyyy h:mm AM/PM", "\u1798\u17B7\u1793\u17B6 19 2019 12:04 \u179B\u17D2\u1784\u17B6\u1785"}, + {"44562.189571759256", "[$-453]mmm dd yyyy h:mm AM/PM", "\u17E1 01 2022 4:32 \u1796\u17D2\u179A\u17B9\u1780"}, + {"44562.189571759256", "[$-453]mmmm dd yyyy h:mm AM/PM", "\u1798\u1780\u179A\u17B6 01 2022 4:32 \u1796\u17D2\u179A\u17B9\u1780"}, + {"44562.189571759256", "[$-453]mmmmm dd yyyy h:mm AM/PM", "\u1798 01 2022 4:32 \u1796\u17D2\u179A\u17B9\u1780"}, + {"44562.189571759256", "[$-453]mmmmmm dd yyyy h:mm AM/PM", "\u1798\u1780\u179A\u17B6 01 2022 4:32 \u1796\u17D2\u179A\u17B9\u1780"}, + {"43543.503206018519", "[$-453]mmm dd yyyy h:mm AM/PM", "\u17E3 19 2019 12:04 \u179B\u17D2\u1784\u17B6\u1785"}, + {"43543.503206018519", "[$-453]mmmm dd yyyy h:mm AM/PM", "\u1798\u17B7\u1793\u17B6 19 2019 12:04 \u179B\u17D2\u1784\u17B6\u1785"}, + {"43543.503206018519", "[$-453]mmmmm dd yyyy h:mm AM/PM", "\u1798 19 2019 12:04 \u179B\u17D2\u1784\u17B6\u1785"}, + {"43543.503206018519", "[$-453]mmmmmm dd yyyy h:mm AM/PM", "\u1798\u17B7\u1793\u17B6 19 2019 12:04 \u179B\u17D2\u1784\u17B6\u1785"}, + {"44562.189571759256", "[$-86]mmm dd yyyy h:mm AM/PM", "nab'e 01 2022 4:32 a.m."}, + {"44562.189571759256", "[$-86]mmmm dd yyyy h:mm AM/PM", "nab'e ik' 01 2022 4:32 a.m."}, + {"44562.189571759256", "[$-86]mmmmm dd yyyy h:mm AM/PM", "n 01 2022 4:32 a.m."}, + {"44562.189571759256", "[$-86]mmmmmm dd yyyy h:mm AM/PM", "nab'e ik' 01 2022 4:32 a.m."}, + {"43543.503206018519", "[$-86]mmm dd yyyy h:mm AM/PM", "urox 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-86]mmmm dd yyyy h:mm AM/PM", "urox ik' 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-86]mmmmm dd yyyy h:mm AM/PM", "u 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-86]mmmmmm dd yyyy h:mm AM/PM", "urox ik' 19 2019 12:04 p.m."}, + {"44562.189571759256", "[$-486]mmm dd yyyy h:mm AM/PM", "nab'e 01 2022 4:32 a.m."}, + {"44562.189571759256", "[$-486]mmmm dd yyyy h:mm AM/PM", "nab'e ik' 01 2022 4:32 a.m."}, + {"44562.189571759256", "[$-486]mmmmm dd yyyy h:mm AM/PM", "n 01 2022 4:32 a.m."}, + {"44562.189571759256", "[$-486]mmmmmm dd yyyy h:mm AM/PM", "nab'e ik' 01 2022 4:32 a.m."}, + {"43543.503206018519", "[$-486]mmm dd yyyy h:mm AM/PM", "urox 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-486]mmmm dd yyyy h:mm AM/PM", "urox ik' 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-486]mmmmm dd yyyy h:mm AM/PM", "u 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-486]mmmmmm dd yyyy h:mm AM/PM", "urox ik' 19 2019 12:04 p.m."}, + {"44562.189571759256", "[$-87]mmm dd yyyy h:mm AM/PM", "mut. 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-87]mmmm dd yyyy h:mm AM/PM", "Mutarama 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-87]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-87]mmmmmm dd yyyy h:mm AM/PM", "Mutarama 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-87]mmm dd yyyy h:mm AM/PM", "wer. 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-87]mmmm dd yyyy h:mm AM/PM", "Werurwe 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-87]mmmmm dd yyyy h:mm AM/PM", "W 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-87]mmmmmm dd yyyy h:mm AM/PM", "Werurwe 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-487]mmm dd yyyy h:mm AM/PM", "mut. 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-487]mmmm dd yyyy h:mm AM/PM", "Mutarama 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-487]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-487]mmmmmm dd yyyy h:mm AM/PM", "Mutarama 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-487]mmm dd yyyy h:mm AM/PM", "wer. 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-487]mmmm dd yyyy h:mm AM/PM", "Werurwe 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-487]mmmmm dd yyyy h:mm AM/PM", "W 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-487]mmmmmm dd yyyy h:mm AM/PM", "Werurwe 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-41]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-41]mmmm dd yyyy h:mm AM/PM", "Januari 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-41]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-41]mmmmmm dd yyyy h:mm AM/PM", "Januari 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-41]mmm dd yyyy h:mm AM/PM", "Mac 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-41]mmmm dd yyyy h:mm AM/PM", "Machi 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-41]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-41]mmmmmm dd yyyy h:mm AM/PM", "Machi 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-441]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-441]mmmm dd yyyy h:mm AM/PM", "Januari 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-441]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-441]mmmmmm dd yyyy h:mm AM/PM", "Januari 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-441]mmm dd yyyy h:mm AM/PM", "Mac 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-441]mmmm dd yyyy h:mm AM/PM", "Machi 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-441]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-441]mmmmmm dd yyyy h:mm AM/PM", "Machi 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-57]mmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u0947 01 2022 4:32 \u092E.\u092A\u0942."}, + {"44562.189571759256", "[$-57]mmmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u0947\u0935\u093E\u0930\u0940 01 2022 4:32 \u092E.\u092A\u0942."}, + {"44562.189571759256", "[$-57]mmmmm dd yyyy h:mm AM/PM", "\u091C 01 2022 4:32 \u092E.\u092A\u0942."}, + {"44562.189571759256", "[$-57]mmmmmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u0947\u0935\u093E\u0930\u0940 01 2022 4:32 \u092E.\u092A\u0942."}, + {"43543.503206018519", "[$-57]mmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E.\u0928\u0902."}, + {"43543.503206018519", "[$-57]mmmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E.\u0928\u0902."}, + {"43543.503206018519", "[$-57]mmmmm dd yyyy h:mm AM/PM", "\u092E 19 2019 12:04 \u092E.\u0928\u0902."}, + {"43543.503206018519", "[$-57]mmmmmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E.\u0928\u0902."}, + {"44562.189571759256", "[$-457]mmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u0947 01 2022 4:32 \u092E.\u092A\u0942."}, + {"44562.189571759256", "[$-457]mmmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u0947\u0935\u093E\u0930\u0940 01 2022 4:32 \u092E.\u092A\u0942."}, + {"44562.189571759256", "[$-457]mmmmm dd yyyy h:mm AM/PM", "\u091C 01 2022 4:32 \u092E.\u092A\u0942."}, + {"44562.189571759256", "[$-457]mmmmmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u0947\u0935\u093E\u0930\u0940 01 2022 4:32 \u092E.\u092A\u0942."}, + {"43543.503206018519", "[$-457]mmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E.\u0928\u0902."}, + {"43543.503206018519", "[$-457]mmmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E.\u0928\u0902."}, + {"43543.503206018519", "[$-457]mmmmm dd yyyy h:mm AM/PM", "\u092E 19 2019 12:04 \u092E.\u0928\u0902."}, + {"43543.503206018519", "[$-457]mmmmmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E.\u0928\u0902."}, + {"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]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", "[$-40]mmm dd yyyy h:mm AM/PM", "\u042F\u043D\u0432 01 2022 4:32 \u0442\u04A3"}, + {"44562.189571759256", "[$-40]mmmm dd yyyy h:mm AM/PM", "\u042F\u043D\u0432\u0430\u0440\u044C 01 2022 4:32 \u0442\u04A3"}, + {"44562.189571759256", "[$-40]mmmmm dd yyyy h:mm AM/PM", "\u042F 01 2022 4:32 \u0442\u04A3"}, + {"44562.189571759256", "[$-40]mmmmmm dd yyyy h:mm AM/PM", "\u042F\u043D\u0432\u0430\u0440\u044C 01 2022 4:32 \u0442\u04A3"}, + {"43543.503206018519", "[$-40]mmm dd yyyy h:mm AM/PM", "\u041C\u0430\u0440 19 2019 12:04 \u0442\u043A"}, + {"43543.503206018519", "[$-40]mmmm dd yyyy h:mm AM/PM", "\u041C\u0430\u0440\u0442 19 2019 12:04 \u0442\u043A"}, + {"43543.503206018519", "[$-40]mmmmm dd yyyy h:mm AM/PM", "\u041C 19 2019 12:04 \u0442\u043A"}, + {"43543.503206018519", "[$-40]mmmmmm dd yyyy h:mm AM/PM", "\u041C\u0430\u0440\u0442 19 2019 12:04 \u0442\u043A"}, + {"44562.189571759256", "[$-440]mmm dd yyyy h:mm AM/PM", "\u042F\u043D\u0432 01 2022 4:32 \u0442\u04A3"}, + {"44562.189571759256", "[$-440]mmmm dd yyyy h:mm AM/PM", "\u042F\u043D\u0432\u0430\u0440\u044C 01 2022 4:32 \u0442\u04A3"}, + {"44562.189571759256", "[$-440]mmmmm dd yyyy h:mm AM/PM", "\u042F 01 2022 4:32 \u0442\u04A3"}, + {"44562.189571759256", "[$-440]mmmmmm dd yyyy h:mm AM/PM", "\u042F\u043D\u0432\u0430\u0440\u044C 01 2022 4:32 \u0442\u04A3"}, + {"43543.503206018519", "[$-440]mmm dd yyyy h:mm AM/PM", "\u041C\u0430\u0440 19 2019 12:04 \u0442\u043A"}, + {"43543.503206018519", "[$-440]mmmm dd yyyy h:mm AM/PM", "\u041C\u0430\u0440\u0442 19 2019 12:04 \u0442\u043A"}, + {"43543.503206018519", "[$-440]mmmmm dd yyyy h:mm AM/PM", "\u041C 19 2019 12:04 \u0442\u043A"}, + {"43543.503206018519", "[$-440]mmmmmm dd yyyy h:mm AM/PM", "\u041C\u0430\u0440\u0442 19 2019 12:04 \u0442\u043A"}, + {"44562.189571759256", "[$-54]mmm dd yyyy h:mm AM/PM", "\u0EA1.\u0E81. 01 2022 4:32 \u0E81\u0EC8\u0EAD\u0E99\u0E97\u0EC8\u0EBD\u0E87"}, + {"44562.189571759256", "[$-54]mmmm dd yyyy h:mm AM/PM", "\u0EA1\u0EB1\u0E87\u0E81\u0EAD\u0E99 01 2022 4:32 \u0E81\u0EC8\u0EAD\u0E99\u0E97\u0EC8\u0EBD\u0E87"}, + {"44562.189571759256", "[$-54]mmmmm dd yyyy h:mm AM/PM", "\u0EA1 01 2022 4:32 \u0E81\u0EC8\u0EAD\u0E99\u0E97\u0EC8\u0EBD\u0E87"}, + {"44562.189571759256", "[$-54]mmmmmm dd yyyy h:mm AM/PM", "\u0EA1\u0EB1\u0E87\u0E81\u0EAD\u0E99 01 2022 4:32 \u0E81\u0EC8\u0EAD\u0E99\u0E97\u0EC8\u0EBD\u0E87"}, + {"43543.503206018519", "[$-54]mmm dd yyyy h:mm AM/PM", "\u0EA1.\u0E99. 19 2019 12:04 \u0EAB\u0EBC\u0EB1\u0E87\u0E97\u0EC8\u0EBD\u0E87"}, + {"43543.503206018519", "[$-54]mmmm dd yyyy h:mm AM/PM", "\u0EA1\u0EB5\u0E99\u0EB2 19 2019 12:04 \u0EAB\u0EBC\u0EB1\u0E87\u0E97\u0EC8\u0EBD\u0E87"}, + {"43543.503206018519", "[$-54]mmmmm dd yyyy h:mm AM/PM", "\u0EA1 19 2019 12:04 \u0EAB\u0EBC\u0EB1\u0E87\u0E97\u0EC8\u0EBD\u0E87"}, + {"43543.503206018519", "[$-54]mmmmmm dd yyyy h:mm AM/PM", "\u0EA1\u0EB5\u0E99\u0EB2 19 2019 12:04 \u0EAB\u0EBC\u0EB1\u0E87\u0E97\u0EC8\u0EBD\u0E87"}, + {"44562.189571759256", "[$-454]mmm dd yyyy h:mm AM/PM", "\u0EA1.\u0E81. 01 2022 4:32 \u0E81\u0EC8\u0EAD\u0E99\u0E97\u0EC8\u0EBD\u0E87"}, + {"44562.189571759256", "[$-454]mmmm dd yyyy h:mm AM/PM", "\u0EA1\u0EB1\u0E87\u0E81\u0EAD\u0E99 01 2022 4:32 \u0E81\u0EC8\u0EAD\u0E99\u0E97\u0EC8\u0EBD\u0E87"}, + {"44562.189571759256", "[$-454]mmmmm dd yyyy h:mm AM/PM", "\u0EA1 01 2022 4:32 \u0E81\u0EC8\u0EAD\u0E99\u0E97\u0EC8\u0EBD\u0E87"}, + {"44562.189571759256", "[$-454]mmmmmm dd yyyy h:mm AM/PM", "\u0EA1\u0EB1\u0E87\u0E81\u0EAD\u0E99 01 2022 4:32 \u0E81\u0EC8\u0EAD\u0E99\u0E97\u0EC8\u0EBD\u0E87"}, + {"43543.503206018519", "[$-454]mmm dd yyyy h:mm AM/PM", "\u0EA1.\u0E99. 19 2019 12:04 \u0EAB\u0EBC\u0EB1\u0E87\u0E97\u0EC8\u0EBD\u0E87"}, + {"43543.503206018519", "[$-454]mmmm dd yyyy h:mm AM/PM", "\u0EA1\u0EB5\u0E99\u0EB2 19 2019 12:04 \u0EAB\u0EBC\u0EB1\u0E87\u0E97\u0EC8\u0EBD\u0E87"}, + {"43543.503206018519", "[$-454]mmmmm dd yyyy h:mm AM/PM", "\u0EA1 19 2019 12:04 \u0EAB\u0EBC\u0EB1\u0E87\u0E97\u0EC8\u0EBD\u0E87"}, + {"43543.503206018519", "[$-454]mmmmmm dd yyyy h:mm AM/PM", "\u0EA1\u0EB5\u0E99\u0EB2 19 2019 12:04 \u0EAB\u0EBC\u0EB1\u0E87\u0E97\u0EC8\u0EBD\u0E87"}, + {"44562.189571759256", "[$-476]mmm dd yyyy h:mm AM/PM", "Ian 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-476]mmmm dd yyyy h:mm AM/PM", "Ianuarius 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-476]mmmmm dd yyyy h:mm AM/PM", "I 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-476]mmmmmm dd yyyy h:mm AM/PM", "Ianuarius 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-476]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-476]mmmm dd yyyy h:mm AM/PM", "Martius 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-476]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-476]mmmmmm dd yyyy h:mm AM/PM", "Martius 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-26]mmm dd yyyy h:mm AM/PM", "janv. 01 2022 4:32 priekšp."}, + {"44562.189571759256", "[$-26]mmmm dd yyyy h:mm AM/PM", "janvāris 01 2022 4:32 priekšp."}, + {"44562.189571759256", "[$-26]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 priekšp."}, + {"44562.189571759256", "[$-26]mmmmmm dd yyyy h:mm AM/PM", "janvāris 01 2022 4:32 priekšp."}, + {"43543.503206018519", "[$-26]mmm dd yyyy h:mm AM/PM", "marts 19 2019 12:04 pēcp."}, + {"43543.503206018519", "[$-26]mmmm dd yyyy h:mm AM/PM", "marts 19 2019 12:04 pēcp."}, + {"43543.503206018519", "[$-26]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 pēcp."}, + {"43543.503206018519", "[$-26]mmmmmm dd yyyy h:mm AM/PM", "marts 19 2019 12:04 pēcp."}, + {"44562.189571759256", "[$-426]mmm dd yyyy h:mm AM/PM", "janv. 01 2022 4:32 priekšp."}, + {"44562.189571759256", "[$-426]mmmm dd yyyy h:mm AM/PM", "janvāris 01 2022 4:32 priekšp."}, + {"44562.189571759256", "[$-426]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 priekšp."}, + {"44562.189571759256", "[$-426]mmmmmm dd yyyy h:mm AM/PM", "janvāris 01 2022 4:32 priekšp."}, + {"43543.503206018519", "[$-426]mmm dd yyyy h:mm AM/PM", "marts 19 2019 12:04 pēcp."}, + {"43543.503206018519", "[$-426]mmmm dd yyyy h:mm AM/PM", "marts 19 2019 12:04 pēcp."}, + {"43543.503206018519", "[$-426]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 pēcp."}, + {"43543.503206018519", "[$-426]mmmmmm dd yyyy h:mm AM/PM", "marts 19 2019 12:04 pēcp."}, + {"44562.189571759256", "[$-27]mmm dd yyyy h:mm AM/PM", "saus. 01 2022 4:32 priešpiet"}, + {"44562.189571759256", "[$-27]mmmm dd yyyy h:mm AM/PM", "sausis 01 2022 4:32 priešpiet"}, + {"44562.189571759256", "[$-27]mmmmm dd yyyy h:mm AM/PM", "s 01 2022 4:32 priešpiet"}, + {"44562.189571759256", "[$-27]mmmmmm dd yyyy h:mm AM/PM", "sausis 01 2022 4:32 priešpiet"}, + {"43543.503206018519", "[$-27]mmm dd yyyy h:mm AM/PM", "kov. 19 2019 12:04 popiet"}, + {"43543.503206018519", "[$-27]mmmm dd yyyy h:mm AM/PM", "kovas 19 2019 12:04 popiet"}, + {"43543.503206018519", "[$-27]mmmmm dd yyyy h:mm AM/PM", "k 19 2019 12:04 popiet"}, + {"43543.503206018519", "[$-27]mmmmmm dd yyyy h:mm AM/PM", "kovas 19 2019 12:04 popiet"}, + {"44562.189571759256", "[$-427]mmm dd yyyy h:mm AM/PM", "saus. 01 2022 4:32 priešpiet"}, + {"44562.189571759256", "[$-427]mmmm dd yyyy h:mm AM/PM", "sausis 01 2022 4:32 priešpiet"}, + {"44562.189571759256", "[$-427]mmmmm dd yyyy h:mm AM/PM", "s 01 2022 4:32 priešpiet"}, + {"44562.189571759256", "[$-427]mmmmmm dd yyyy h:mm AM/PM", "sausis 01 2022 4:32 priešpiet"}, + {"43543.503206018519", "[$-427]mmm dd yyyy h:mm AM/PM", "kov. 19 2019 12:04 popiet"}, + {"43543.503206018519", "[$-427]mmmm dd yyyy h:mm AM/PM", "kovas 19 2019 12:04 popiet"}, + {"43543.503206018519", "[$-427]mmmmm dd yyyy h:mm AM/PM", "k 19 2019 12:04 popiet"}, + {"43543.503206018519", "[$-427]mmmmmm dd yyyy h:mm AM/PM", "kovas 19 2019 12:04 popiet"}, + {"44562.189571759256", "[$-7C2E]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-7C2E]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-7C2E]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-7C2E]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-7C2E]mmm dd yyyy h:mm AM/PM", "měr 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-7C2E]mmmm dd yyyy h:mm AM/PM", "měrc 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-7C2E]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-7C2E]mmmmmm dd yyyy h:mm AM/PM", "měrc 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-82E]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-82E]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-82E]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-82E]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-82E]mmm dd yyyy h:mm AM/PM", "měr 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-82E]mmmm dd yyyy h:mm AM/PM", "měrc 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-82E]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-82E]mmmmmm dd yyyy h:mm AM/PM", "měrc 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-6E]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-6E]mmmm dd yyyy h:mm AM/PM", "Januar 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-6E]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-6E]mmmmmm dd yyyy h:mm AM/PM", "Januar 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-6E]mmm dd yyyy h:mm AM/PM", "Mäe 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-6E]mmmm dd yyyy h:mm AM/PM", "Mäerz 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-6E]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-6E]mmmmmm dd yyyy h:mm AM/PM", "Mäerz 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-46E]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-46E]mmmm dd yyyy h:mm AM/PM", "Januar 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-46E]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-46E]mmmmmm dd yyyy h:mm AM/PM", "Januar 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-46E]mmm dd yyyy h:mm AM/PM", "Mäe 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-46E]mmmm dd yyyy h:mm AM/PM", "Mäerz 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-46E]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-46E]mmmmmm dd yyyy h:mm AM/PM", "Mäerz 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-2F]mmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D. 01 2022 4:32 \u043F\u0440\u0435\u0442\u043F\u043B."}, + {"44562.189571759256", "[$-2F]mmmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D\u0443\u0430\u0440\u0438 01 2022 4:32 \u043F\u0440\u0435\u0442\u043F\u043B."}, + {"44562.189571759256", "[$-2F]mmmmm dd yyyy h:mm AM/PM", "\u0458 01 2022 4:32 \u043F\u0440\u0435\u0442\u043F\u043B."}, + {"44562.189571759256", "[$-2F]mmmmmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D\u0443\u0430\u0440\u0438 01 2022 4:32 \u043F\u0440\u0435\u0442\u043F\u043B."}, + {"43543.503206018519", "[$-2F]mmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440. 19 2019 12:04 \u043F\u043E\u043F\u043B."}, + {"43543.503206018519", "[$-2F]mmmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440\u0442 19 2019 12:04 \u043F\u043E\u043F\u043B."}, + {"43543.503206018519", "[$-2F]mmmmm dd yyyy h:mm AM/PM", "\u043C 19 2019 12:04 \u043F\u043E\u043F\u043B."}, + {"43543.503206018519", "[$-2F]mmmmmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440\u0442 19 2019 12:04 \u043F\u043E\u043F\u043B."}, + {"44562.189571759256", "[$-42F]mmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D. 01 2022 4:32 \u043F\u0440\u0435\u0442\u043F\u043B."}, + {"44562.189571759256", "[$-42F]mmmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D\u0443\u0430\u0440\u0438 01 2022 4:32 \u043F\u0440\u0435\u0442\u043F\u043B."}, + {"44562.189571759256", "[$-42F]mmmmm dd yyyy h:mm AM/PM", "\u0458 01 2022 4:32 \u043F\u0440\u0435\u0442\u043F\u043B."}, + {"44562.189571759256", "[$-42F]mmmmmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D\u0443\u0430\u0440\u0438 01 2022 4:32 \u043F\u0440\u0435\u0442\u043F\u043B."}, + {"43543.503206018519", "[$-42F]mmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440. 19 2019 12:04 \u043F\u043E\u043F\u043B."}, + {"43543.503206018519", "[$-42F]mmmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440\u0442 19 2019 12:04 \u043F\u043E\u043F\u043B."}, + {"43543.503206018519", "[$-42F]mmmmm dd yyyy h:mm AM/PM", "\u043C 19 2019 12:04 \u043F\u043E\u043F\u043B."}, + {"43543.503206018519", "[$-42F]mmmmmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440\u0442 19 2019 12:04 \u043F\u043E\u043F\u043B."}, + {"44562.189571759256", "[$-3E]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 PG"}, + {"44562.189571759256", "[$-3E]mmmm dd yyyy h:mm AM/PM", "Januari 01 2022 4:32 PG"}, + {"44562.189571759256", "[$-3E]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 PG"}, + {"44562.189571759256", "[$-3E]mmmmmm dd yyyy h:mm AM/PM", "Januari 01 2022 4:32 PG"}, + {"43543.503206018519", "[$-3E]mmm dd yyyy h:mm AM/PM", "Mac 19 2019 12:04 PTG"}, + {"43543.503206018519", "[$-3E]mmmm dd yyyy h:mm AM/PM", "Mac 19 2019 12:04 PTG"}, + {"43543.503206018519", "[$-3E]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PTG"}, + {"43543.503206018519", "[$-3E]mmmmmm dd yyyy h:mm AM/PM", "Mac 19 2019 12:04 PTG"}, + {"44562.189571759256", "[$-83E]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 PG"}, + {"44562.189571759256", "[$-83E]mmmm dd yyyy h:mm AM/PM", "Januari 01 2022 4:32 PG"}, + {"44562.189571759256", "[$-83E]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 PG"}, + {"44562.189571759256", "[$-83E]mmmmmm dd yyyy h:mm AM/PM", "Januari 01 2022 4:32 PG"}, + {"43543.503206018519", "[$-83E]mmm dd yyyy h:mm AM/PM", "Mac 19 2019 12:04 PTG"}, + {"43543.503206018519", "[$-83E]mmmm dd yyyy h:mm AM/PM", "Mac 19 2019 12:04 PTG"}, + {"43543.503206018519", "[$-83E]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PTG"}, + {"43543.503206018519", "[$-83E]mmmmmm dd yyyy h:mm AM/PM", "Mac 19 2019 12:04 PTG"}, + {"44562.189571759256", "[$-43E]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 PG"}, + {"44562.189571759256", "[$-43E]mmmm dd yyyy h:mm AM/PM", "Januari 01 2022 4:32 PG"}, + {"44562.189571759256", "[$-43E]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 PG"}, + {"44562.189571759256", "[$-43E]mmmmmm dd yyyy h:mm AM/PM", "Januari 01 2022 4:32 PG"}, + {"43543.503206018519", "[$-43E]mmm dd yyyy h:mm AM/PM", "Mac 19 2019 12:04 PTG"}, + {"43543.503206018519", "[$-43E]mmmm dd yyyy h:mm AM/PM", "Mac 19 2019 12:04 PTG"}, + {"43543.503206018519", "[$-43E]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PTG"}, + {"43543.503206018519", "[$-43E]mmmmmm dd yyyy h:mm AM/PM", "Mac 19 2019 12:04 PTG"}, + {"44562.189571759256", "[$-4C]mmm dd yyyy h:mm AM/PM", "\u0D1C\u0D28\u0D41 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-4C]mmmm dd yyyy h:mm AM/PM", "\u0D1C\u0D28\u0D41\u0D35\u0D30\u0D3F 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-4C]mmmmm dd yyyy h:mm AM/PM", "\u0D1C 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-4C]mmmmmm dd yyyy h:mm AM/PM", "\u0D1C\u0D28\u0D41\u0D35\u0D30\u0D3F 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-4C]mmm dd yyyy h:mm AM/PM", "\u0D2E\u0D3E\u0D7C 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-4C]mmmm dd yyyy h:mm AM/PM", "\u0D2E\u0D3E\u0D30\u0D4D\u200D\u200C\u0D1A\u0D4D\u0D1A\u0D4D 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-4C]mmmmm dd yyyy h:mm AM/PM", "\u0D2E 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-4C]mmmmmm dd yyyy h:mm AM/PM", "\u0D2E\u0D3E\u0D30\u0D4D\u200D\u200C\u0D1A\u0D4D\u0D1A\u0D4D 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-44C]mmm dd yyyy h:mm AM/PM", "\u0D1C\u0D28\u0D41 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-44C]mmmm dd yyyy h:mm AM/PM", "\u0D1C\u0D28\u0D41\u0D35\u0D30\u0D3F 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-44C]mmmmm dd yyyy h:mm AM/PM", "\u0D1C 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-44C]mmmmmm dd yyyy h:mm AM/PM", "\u0D1C\u0D28\u0D41\u0D35\u0D30\u0D3F 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-44C]mmm dd yyyy h:mm AM/PM", "\u0D2E\u0D3E\u0D7C 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-44C]mmmm dd yyyy h:mm AM/PM", "\u0D2E\u0D3E\u0D30\u0D4D\u200D\u200C\u0D1A\u0D4D\u0D1A\u0D4D 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-44C]mmmmm dd yyyy h:mm AM/PM", "\u0D2E 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-44C]mmmmmm dd yyyy h:mm AM/PM", "\u0D2E\u0D3E\u0D30\u0D4D\u200D\u200C\u0D1A\u0D4D\u0D1A\u0D4D 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-3A]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-3A]mmmm dd yyyy h:mm AM/PM", "Jannar 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-3A]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-3A]mmmmmm dd yyyy h:mm AM/PM", "Jannar 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-3A]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-3A]mmmm dd yyyy h:mm AM/PM", "Marzu 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-3A]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-3A]mmmmmm dd yyyy h:mm AM/PM", "Marzu 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-43A]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-43A]mmmm dd yyyy h:mm AM/PM", "Jannar 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-43A]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-43A]mmmmmm dd yyyy h:mm AM/PM", "Jannar 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-43A]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-43A]mmmm dd yyyy h:mm AM/PM", "Marzu 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-43A]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-43A]mmmmmm dd yyyy h:mm AM/PM", "Marzu 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-81]mmm dd yyyy h:mm AM/PM", "Kohi 01 2022 4:32 a.m."}, + {"44562.189571759256", "[$-81]mmmm dd yyyy h:mm AM/PM", "Kohitātea 01 2022 4:32 a.m."}, + {"44562.189571759256", "[$-81]mmmmm dd yyyy h:mm AM/PM", "K 01 2022 4:32 a.m."}, + {"44562.189571759256", "[$-81]mmmmmm dd yyyy h:mm AM/PM", "Kohitātea 01 2022 4:32 a.m."}, + {"43543.503206018519", "[$-81]mmm dd yyyy h:mm AM/PM", "Pou 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-81]mmmm dd yyyy h:mm AM/PM", "Poutūterangi 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-81]mmmmm dd yyyy h:mm AM/PM", "P 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-81]mmmmmm dd yyyy h:mm AM/PM", "Poutūterangi 19 2019 12:04 p.m."}, + {"44562.189571759256", "[$-481]mmm dd yyyy h:mm AM/PM", "Kohi 01 2022 4:32 a.m."}, + {"44562.189571759256", "[$-481]mmmm dd yyyy h:mm AM/PM", "Kohitātea 01 2022 4:32 a.m."}, + {"44562.189571759256", "[$-481]mmmmm dd yyyy h:mm AM/PM", "K 01 2022 4:32 a.m."}, + {"44562.189571759256", "[$-481]mmmmmm dd yyyy h:mm AM/PM", "Kohitātea 01 2022 4:32 a.m."}, + {"43543.503206018519", "[$-481]mmm dd yyyy h:mm AM/PM", "Pou 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-481]mmmm dd yyyy h:mm AM/PM", "Poutūterangi 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-481]mmmmm dd yyyy h:mm AM/PM", "P 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-481]mmmmmm dd yyyy h:mm AM/PM", "Poutūterangi 19 2019 12:04 p.m."}, + {"44562.189571759256", "[$-7A]mmm dd yyyy h:mm AM/PM", "Kiñe Tripantu 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-7A]mmmm dd yyyy h:mm AM/PM", "Kiñe Tripantu 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-7A]mmmmm dd yyyy h:mm AM/PM", "K 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-7A]mmmmmm dd yyyy h:mm AM/PM", "Kiñe Tripantu 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-7A]mmm dd yyyy h:mm AM/PM", "Kila 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-7A]mmmm dd yyyy h:mm AM/PM", "Kila 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-7A]mmmmm dd yyyy h:mm AM/PM", "K 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-7A]mmmmmm dd yyyy h:mm AM/PM", "Kila 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-47A]mmm dd yyyy h:mm AM/PM", "Kiñe Tripantu 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-47A]mmmm dd yyyy h:mm AM/PM", "Kiñe Tripantu 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-47A]mmmmm dd yyyy h:mm AM/PM", "K 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-47A]mmmmmm dd yyyy h:mm AM/PM", "Kiñe Tripantu 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-47A]mmm dd yyyy h:mm AM/PM", "Kila 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-47A]mmmm dd yyyy h:mm AM/PM", "Kila 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-47A]mmmmm dd yyyy h:mm AM/PM", "K 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-47A]mmmmmm dd yyyy h:mm AM/PM", "Kila 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-4E]mmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u0947. 01 2022 4:32 \u092E.\u092A\u0942."}, + {"44562.189571759256", "[$-4E]mmmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u0947\u0935\u093E\u0930\u0940 01 2022 4:32 \u092E.\u092A\u0942."}, + {"44562.189571759256", "[$-4E]mmmmm dd yyyy h:mm AM/PM", "\u091C 01 2022 4:32 \u092E.\u092A\u0942."}, + {"44562.189571759256", "[$-4E]mmmmmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u0947\u0935\u093E\u0930\u0940 01 2022 4:32 \u092E.\u092A\u0942."}, + {"43543.503206018519", "[$-4E]mmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E.\u0928\u0902."}, + {"43543.503206018519", "[$-4E]mmmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E.\u0928\u0902."}, + {"43543.503206018519", "[$-4E]mmmmm dd yyyy h:mm AM/PM", "\u092E 19 2019 12:04 \u092E.\u0928\u0902."}, + {"43543.503206018519", "[$-4E]mmmmmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E.\u0928\u0902."}, + {"44562.189571759256", "[$-44E]mmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u0947. 01 2022 4:32 \u092E.\u092A\u0942."}, + {"44562.189571759256", "[$-44E]mmmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u0947\u0935\u093E\u0930\u0940 01 2022 4:32 \u092E.\u092A\u0942."}, + {"44562.189571759256", "[$-44E]mmmmm dd yyyy h:mm AM/PM", "\u091C 01 2022 4:32 \u092E.\u092A\u0942."}, + {"44562.189571759256", "[$-44E]mmmmmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u0947\u0935\u093E\u0930\u0940 01 2022 4:32 \u092E.\u092A\u0942."}, + {"43543.503206018519", "[$-44E]mmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E.\u0928\u0902."}, + {"43543.503206018519", "[$-44E]mmmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E.\u0928\u0902."}, + {"43543.503206018519", "[$-44E]mmmmm dd yyyy h:mm AM/PM", "\u092E 19 2019 12:04 \u092E.\u0928\u0902."}, + {"43543.503206018519", "[$-44E]mmmmmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E.\u0928\u0902."}, + {"44562.189571759256", "[$-7C]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-7C]mmmm dd yyyy h:mm AM/PM", "Tsothohrkó:Wa 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-7C]mmmmm dd yyyy h:mm AM/PM", "T 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-7C]mmmmmm dd yyyy h:mm AM/PM", "Tsothohrkó:Wa 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-7C]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-7C]mmmm dd yyyy h:mm AM/PM", "Enniskó:Wa 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-7C]mmmmm dd yyyy h:mm AM/PM", "E 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-7C]mmmmmm dd yyyy h:mm AM/PM", "Enniskó:Wa 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-47C]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-47C]mmmm dd yyyy h:mm AM/PM", "Tsothohrkó:Wa 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-47C]mmmmm dd yyyy h:mm AM/PM", "T 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-47C]mmmmmm dd yyyy h:mm AM/PM", "Tsothohrkó:Wa 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-47C]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-47C]mmmm dd yyyy h:mm AM/PM", "Enniskó:Wa 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-47C]mmmmm dd yyyy h:mm AM/PM", "E 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-47C]mmmmmm dd yyyy h:mm AM/PM", "Enniskó:Wa 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-44E]mmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u0947. 01 2022 4:32 \u092E.\u092A\u0942."}, + {"44562.189571759256", "[$-44E]mmmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u0947\u0935\u093E\u0930\u0940 01 2022 4:32 \u092E.\u092A\u0942."}, + {"44562.189571759256", "[$-44E]mmmmm dd yyyy h:mm AM/PM", "\u091C 01 2022 4:32 \u092E.\u092A\u0942."}, + {"44562.189571759256", "[$-44E]mmmmmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u0947\u0935\u093E\u0930\u0940 01 2022 4:32 \u092E.\u092A\u0942."}, + {"43543.503206018519", "[$-44E]mmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E.\u0928\u0902."}, + {"43543.503206018519", "[$-44E]mmmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E.\u0928\u0902."}, + {"43543.503206018519", "[$-44E]mmmmm dd yyyy h:mm AM/PM", "\u092E 19 2019 12:04 \u092E.\u0928\u0902."}, + {"43543.503206018519", "[$-44E]mmmmmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E.\u0928\u0902."}, + {"44562.189571759256", "[$-50]mmm dd yyyy h:mm AM/PM", "1-р сар 01 2022 4:32 \u04AF.\u04E9."}, + {"44562.189571759256", "[$-50]mmmm dd yyyy h:mm AM/PM", "\u041D\u044D\u0433\u0434\u04AF\u0433\u044D\u044D\u0440 \u0441\u0430\u0440 01 2022 4:32 \u04AF.\u04E9."}, + {"44562.189571759256", "[$-50]mmmmm dd yyyy h:mm AM/PM", "\u041D 01 2022 4:32 \u04AF.\u04E9."}, + {"44562.189571759256", "[$-50]mmmmmm dd yyyy h:mm AM/PM", "\u041D\u044D\u0433\u0434\u04AF\u0433\u044D\u044D\u0440 \u0441\u0430\u0440 01 2022 4:32 \u04AF.\u04E9."}, + {"43543.503206018519", "[$-50]mmm dd yyyy h:mm AM/PM", "3-р сар 19 2019 12:04 \u04AF.\u0445."}, + {"43543.503206018519", "[$-50]mmmm dd yyyy h:mm AM/PM", "\u0413\u0443\u0440\u0430\u0432\u0434\u0443\u0433\u0430\u0430\u0440 \u0441\u0430\u0440 19 2019 12:04 \u04AF.\u0445."}, + {"43543.503206018519", "[$-50]mmmmm dd yyyy h:mm AM/PM", "\u0413 19 2019 12:04 \u04AF.\u0445."}, + {"43543.503206018519", "[$-50]mmmmmm dd yyyy h:mm AM/PM", "\u0413\u0443\u0440\u0430\u0432\u0434\u0443\u0433\u0430\u0430\u0440 \u0441\u0430\u0440 19 2019 12:04 \u04AF.\u0445."}, + {"44562.189571759256", "[$-7850]mmm dd yyyy h:mm AM/PM", "1-р сар 01 2022 4:32 \u04AF.\u04E9."}, + {"44562.189571759256", "[$-7850]mmmm dd yyyy h:mm AM/PM", "\u041D\u044D\u0433\u0434\u04AF\u0433\u044D\u044D\u0440 \u0441\u0430\u0440 01 2022 4:32 \u04AF.\u04E9."}, + {"44562.189571759256", "[$-7850]mmmmm dd yyyy h:mm AM/PM", "\u041D 01 2022 4:32 \u04AF.\u04E9."}, + {"44562.189571759256", "[$-7850]mmmmmm dd yyyy h:mm AM/PM", "\u041D\u044D\u0433\u0434\u04AF\u0433\u044D\u044D\u0440 \u0441\u0430\u0440 01 2022 4:32 \u04AF.\u04E9."}, + {"43543.503206018519", "[$-7850]mmm dd yyyy h:mm AM/PM", "3-р сар 19 2019 12:04 \u04AF.\u0445."}, + {"43543.503206018519", "[$-7850]mmmm dd yyyy h:mm AM/PM", "\u0413\u0443\u0440\u0430\u0432\u0434\u0443\u0433\u0430\u0430\u0440 \u0441\u0430\u0440 19 2019 12:04 \u04AF.\u0445."}, + {"43543.503206018519", "[$-7850]mmmmm dd yyyy h:mm AM/PM", "\u0413 19 2019 12:04 \u04AF.\u0445."}, + {"43543.503206018519", "[$-7850]mmmmmm dd yyyy h:mm AM/PM", "\u0413\u0443\u0440\u0430\u0432\u0434\u0443\u0433\u0430\u0430\u0440 \u0441\u0430\u0440 19 2019 12:04 \u04AF.\u0445."}, + {"44562.189571759256", "[$-450]mmm dd yyyy h:mm AM/PM", "1-р сар 01 2022 4:32 \u04AF.\u04E9."}, + {"44562.189571759256", "[$-450]mmmm dd yyyy h:mm AM/PM", "\u041D\u044D\u0433\u0434\u04AF\u0433\u044D\u044D\u0440 \u0441\u0430\u0440 01 2022 4:32 \u04AF.\u04E9."}, + {"44562.189571759256", "[$-450]mmmmm dd yyyy h:mm AM/PM", "\u041D 01 2022 4:32 \u04AF.\u04E9."}, + {"44562.189571759256", "[$-450]mmmmmm dd yyyy h:mm AM/PM", "\u041D\u044D\u0433\u0434\u04AF\u0433\u044D\u044D\u0440 \u0441\u0430\u0440 01 2022 4:32 \u04AF.\u04E9."}, + {"43543.503206018519", "[$-450]mmm dd yyyy h:mm AM/PM", "3-р сар 19 2019 12:04 \u04AF.\u0445."}, + {"43543.503206018519", "[$-450]mmmm dd yyyy h:mm AM/PM", "\u0413\u0443\u0440\u0430\u0432\u0434\u0443\u0433\u0430\u0430\u0440 \u0441\u0430\u0440 19 2019 12:04 \u04AF.\u0445."}, + {"43543.503206018519", "[$-450]mmmmm dd yyyy h:mm AM/PM", "\u0413 19 2019 12:04 \u04AF.\u0445."}, + {"43543.503206018519", "[$-450]mmmmmm dd yyyy h:mm AM/PM", "\u0413\u0443\u0440\u0430\u0432\u0434\u0443\u0433\u0430\u0430\u0440 \u0441\u0430\u0440 19 2019 12:04 \u04AF.\u0445."}, {"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"}, @@ -273,6 +1135,142 @@ func TestNumFmt(t *testing.T) { {"44896.18957170139", "[$-C50]mmmm dd yyyy h:mm AM/PM", "M12 01 2022 4:32 AM"}, {"44562.189571759256", "[$-C50]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 AM"}, {"44896.18957170139", "[$-C50]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-61]mmm dd yyyy h:mm AM/PM", "\u091C\u0928 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"}, + {"44562.189571759256", "[$-61]mmmm dd yyyy h:mm AM/PM", "\u091C\u0928\u0935\u0930\u0940 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"}, + {"44562.189571759256", "[$-61]mmmmm dd yyyy h:mm AM/PM", "\u091C 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"}, + {"44562.189571759256", "[$-61]mmmmmm dd yyyy h:mm AM/PM", "\u091C\u0928\u0935\u0930\u0940 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"}, + {"43543.503206018519", "[$-61]mmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928"}, + {"43543.503206018519", "[$-61]mmmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928"}, + {"43543.503206018519", "[$-61]mmmmm dd yyyy h:mm AM/PM", "\u092E 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928"}, + {"43543.503206018519", "[$-61]mmmmmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928"}, + {"44562.189571759256", "[$-861]mmm dd yyyy h:mm AM/PM", "\u091C\u0928\u0935\u0930\u0940 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"}, + {"44562.189571759256", "[$-861]mmmm dd yyyy h:mm AM/PM", "\u091C\u0928\u0935\u0930\u0940 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"}, + {"44562.189571759256", "[$-861]mmmmm dd yyyy h:mm AM/PM", "\u091C 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"}, + {"44562.189571759256", "[$-861]mmmmmm dd yyyy h:mm AM/PM", "\u091C\u0928\u0935\u0930\u0940 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"}, + {"43543.503206018519", "[$-861]mmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928"}, + {"43543.503206018519", "[$-861]mmmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928"}, + {"43543.503206018519", "[$-861]mmmmm dd yyyy h:mm AM/PM", "\u092E 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928"}, + {"43543.503206018519", "[$-861]mmmmmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928"}, + {"44562.189571759256", "[$-461]mmm dd yyyy h:mm AM/PM", "\u091C\u0928 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"}, + {"44562.189571759256", "[$-461]mmmm dd yyyy h:mm AM/PM", "\u091C\u0928\u0935\u0930\u0940 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"}, + {"44562.189571759256", "[$-461]mmmmm dd yyyy h:mm AM/PM", "\u091C 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"}, + {"44562.189571759256", "[$-461]mmmmmm dd yyyy h:mm AM/PM", "\u091C\u0928\u0935\u0930\u0940 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"}, + {"43543.503206018519", "[$-461]mmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928"}, + {"43543.503206018519", "[$-461]mmmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928"}, + {"43543.503206018519", "[$-461]mmmmm dd yyyy h:mm AM/PM", "\u092E 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928"}, + {"43543.503206018519", "[$-461]mmmmmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928"}, + {"44562.189571759256", "[$-14]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 a.m."}, + {"44562.189571759256", "[$-14]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 a.m."}, + {"44562.189571759256", "[$-14]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 a.m."}, + {"44562.189571759256", "[$-14]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 a.m."}, + {"43543.503206018519", "[$-14]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-14]mmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-14]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-14]mmmmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 p.m."}, + {"44562.189571759256", "[$-7C14]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 a.m."}, + {"44562.189571759256", "[$-7C14]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 a.m."}, + {"44562.189571759256", "[$-7C14]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 a.m."}, + {"44562.189571759256", "[$-7C14]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 a.m."}, + {"43543.503206018519", "[$-7C14]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-7C14]mmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-7C14]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-7C14]mmmmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 p.m."}, + {"44562.189571759256", "[$-414]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 a.m."}, + {"44562.189571759256", "[$-414]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 a.m."}, + {"44562.189571759256", "[$-414]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 a.m."}, + {"44562.189571759256", "[$-414]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 a.m."}, + {"43543.503206018519", "[$-414]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-414]mmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-414]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-414]mmmmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 p.m."}, + {"44562.189571759256", "[$-7814]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 f.m."}, + {"44562.189571759256", "[$-7814]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 f.m."}, + {"44562.189571759256", "[$-7814]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 f.m."}, + {"44562.189571759256", "[$-7814]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 f.m."}, + {"43543.503206018519", "[$-7814]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 e.m."}, + {"43543.503206018519", "[$-7814]mmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 e.m."}, + {"43543.503206018519", "[$-7814]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 e.m."}, + {"43543.503206018519", "[$-7814]mmmmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 e.m."}, + {"44562.189571759256", "[$-814]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 f.m."}, + {"44562.189571759256", "[$-814]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 f.m."}, + {"44562.189571759256", "[$-814]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 f.m."}, + {"44562.189571759256", "[$-814]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 f.m."}, + {"43543.503206018519", "[$-814]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 e.m."}, + {"43543.503206018519", "[$-814]mmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 e.m."}, + {"43543.503206018519", "[$-814]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 e.m."}, + {"43543.503206018519", "[$-814]mmmmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 e.m."}, + {"44562.189571759256", "[$-82]mmm dd yyyy h:mm AM/PM", "gen. 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-82]mmmm dd yyyy h:mm AM/PM", "genièr 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-82]mmmmm dd yyyy h:mm AM/PM", "g 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-82]mmmmmm dd yyyy h:mm AM/PM", "genièr 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-82]mmm dd yyyy h:mm AM/PM", "març 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-82]mmmm dd yyyy h:mm AM/PM", "març 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-82]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-82]mmmmmm dd yyyy h:mm AM/PM", "març 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-482]mmm dd yyyy h:mm AM/PM", "gen. 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-482]mmmm dd yyyy h:mm AM/PM", "genièr 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-482]mmmmm dd yyyy h:mm AM/PM", "g 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-482]mmmmmm dd yyyy h:mm AM/PM", "genièr 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-482]mmm dd yyyy h:mm AM/PM", "març 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-482]mmmm dd yyyy h:mm AM/PM", "març 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-482]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-482]mmmmmm dd yyyy h:mm AM/PM", "març 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-48]mmm dd yyyy h:mm AM/PM", "\u0B1C\u0B3E\u0B28\u0B41\u0B5F\u0B3E\u0B30\u0B40 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-48]mmmm dd yyyy h:mm AM/PM", "\u0B1C\u0B3E\u0B28\u0B41\u0B5F\u0B3E\u0B30\u0B40 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-48]mmmmm dd yyyy h:mm AM/PM", "\u0B1C 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-48]mmmmmm dd yyyy h:mm AM/PM", "\u0B1C\u0B3E\u0B28\u0B41\u0B5F\u0B3E\u0B30\u0B40 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-48]mmm dd yyyy h:mm AM/PM", "\u0B2E\u0B3E\u0B30\u0B4D\u0B1A\u0B4D\u0B1A 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-48]mmmm dd yyyy h:mm AM/PM", "\u0B2E\u0B3E\u0B30\u0B4D\u0B1A\u0B4D\u0B1A 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-48]mmmmm dd yyyy h:mm AM/PM", "\u0B2E 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-48]mmmmmm dd yyyy h:mm AM/PM", "\u0B2E\u0B3E\u0B30\u0B4D\u0B1A\u0B4D\u0B1A 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-448]mmm dd yyyy h:mm AM/PM", "\u0B1C\u0B3E\u0B28\u0B41\u0B5F\u0B3E\u0B30\u0B40 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-448]mmmm dd yyyy h:mm AM/PM", "\u0B1C\u0B3E\u0B28\u0B41\u0B5F\u0B3E\u0B30\u0B40 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-448]mmmmm dd yyyy h:mm AM/PM", "\u0B1C 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-448]mmmmmm dd yyyy h:mm AM/PM", "\u0B1C\u0B3E\u0B28\u0B41\u0B5F\u0B3E\u0B30\u0B40 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-448]mmm dd yyyy h:mm AM/PM", "\u0B2E\u0B3E\u0B30\u0B4D\u0B1A\u0B4D\u0B1A 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-448]mmmm dd yyyy h:mm AM/PM", "\u0B2E\u0B3E\u0B30\u0B4D\u0B1A\u0B4D\u0B1A 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-448]mmmmm dd yyyy h:mm AM/PM", "\u0B2E 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-448]mmmmmm dd yyyy h:mm AM/PM", "\u0B2E\u0B3E\u0B30\u0B4D\u0B1A\u0B4D\u0B1A 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-72]mmm dd yyyy h:mm AM/PM", "Ama 01 2022 4:32 WD"}, + {"44562.189571759256", "[$-72]mmmm dd yyyy h:mm AM/PM", "Amajjii 01 2022 4:32 WD"}, + {"44562.189571759256", "[$-72]mmmmm dd yyyy h:mm AM/PM", "A 01 2022 4:32 WD"}, + {"44562.189571759256", "[$-72]mmmmmm dd yyyy h:mm AM/PM", "Amajjii 01 2022 4:32 WD"}, + {"43543.503206018519", "[$-72]mmm dd yyyy h:mm AM/PM", "Bit 19 2019 12:04 WB"}, + {"43543.503206018519", "[$-72]mmmm dd yyyy h:mm AM/PM", "Bitooteessa 19 2019 12:04 WB"}, + {"43543.503206018519", "[$-72]mmmmm dd yyyy h:mm AM/PM", "B 19 2019 12:04 WB"}, + {"43543.503206018519", "[$-72]mmmmmm dd yyyy h:mm AM/PM", "Bitooteessa 19 2019 12:04 WB"}, + {"44562.189571759256", "[$-472]mmm dd yyyy h:mm AM/PM", "Ama 01 2022 4:32 WD"}, + {"44562.189571759256", "[$-472]mmmm dd yyyy h:mm AM/PM", "Amajjii 01 2022 4:32 WD"}, + {"44562.189571759256", "[$-472]mmmmm dd yyyy h:mm AM/PM", "A 01 2022 4:32 WD"}, + {"44562.189571759256", "[$-472]mmmmmm dd yyyy h:mm AM/PM", "Amajjii 01 2022 4:32 WD"}, + {"43543.503206018519", "[$-472]mmm dd yyyy h:mm AM/PM", "Bit 19 2019 12:04 WB"}, + {"43543.503206018519", "[$-472]mmmm dd yyyy h:mm AM/PM", "Bitooteessa 19 2019 12:04 WB"}, + {"43543.503206018519", "[$-472]mmmmm dd yyyy h:mm AM/PM", "B 19 2019 12:04 WB"}, + {"43543.503206018519", "[$-472]mmmmmm dd yyyy h:mm AM/PM", "Bitooteessa 19 2019 12:04 WB"}, + {"44562.189571759256", "[$-63]mmm dd yyyy h:mm AM/PM", "\u0633\u0644\u0648\u0627\u063A\u0647 01 2022 4:32 \u063A.\u0645."}, + {"44562.189571759256", "[$-63]mmmm dd yyyy h:mm AM/PM", "\u0633\u0644\u0648\u0627\u063A\u0647 01 2022 4:32 \u063A.\u0645."}, + {"44562.189571759256", "[$-63]mmmmm dd yyyy h:mm AM/PM", "\u0633\u0644\u0648\u0627\u063A\u0647 01 2022 4:32 \u063A.\u0645."}, + {"44562.189571759256", "[$-63]mmmmmm dd yyyy h:mm AM/PM", "\u0633\u0644\u0648\u0627\u063A\u0647 01 2022 4:32 \u063A.\u0645."}, + {"44713.188888888886", "[$-63]mmm dd yyyy h:mm AM/PM", "\u0686\u0646\u06AB\u0627 \u069A 01 2022 4:32 \u063A.\u0645."}, + {"44713.188888888886", "[$-63]mmmm dd yyyy h:mm AM/PM", "\u0686\u0646\u06AB\u0627 \u069A\u0632\u0645\u0631\u0649 01 2022 4:32 \u063A.\u0645."}, + {"44713.188888888886", "[$-63]mmmmm dd yyyy h:mm AM/PM", "\u0686\u0646\u06AB\u0627 \u069A\u0632\u0645\u0631\u0649 01 2022 4:32 \u063A.\u0645."}, + {"44713.188888888886", "[$-63]mmmmmm dd yyyy h:mm AM/PM", "\u0686\u0646\u06AB\u0627 \u069A\u0632\u0645\u0631\u0649 01 2022 4:32 \u063A.\u0645."}, + {"43543.503206018519", "[$-63]mmm dd yyyy h:mm AM/PM", "\u0648\u0631\u0649 19 2019 12:04 \u063A.\u0648."}, + {"43543.503206018519", "[$-63]mmmm dd yyyy h:mm AM/PM", "\u0648\u0631\u0649 19 2019 12:04 \u063A.\u0648."}, + {"43543.503206018519", "[$-63]mmmmm dd yyyy h:mm AM/PM", "\u0648\u0631\u0649 19 2019 12:04 \u063A.\u0648."}, + {"43543.503206018519", "[$-63]mmmmmm dd yyyy h:mm AM/PM", "\u0648\u0631\u0649 19 2019 12:04 \u063A.\u0648."}, + {"44562.189571759256", "[$-463]mmm dd yyyy h:mm AM/PM", "\u0633\u0644\u0648\u0627\u063A\u0647 01 2022 4:32 \u063A.\u0645."}, + {"44562.189571759256", "[$-463]mmmm dd yyyy h:mm AM/PM", "\u0633\u0644\u0648\u0627\u063A\u0647 01 2022 4:32 \u063A.\u0645."}, + {"44562.189571759256", "[$-463]mmmmm dd yyyy h:mm AM/PM", "\u0633\u0644\u0648\u0627\u063A\u0647 01 2022 4:32 \u063A.\u0645."}, + {"44562.189571759256", "[$-463]mmmmmm dd yyyy h:mm AM/PM", "\u0633\u0644\u0648\u0627\u063A\u0647 01 2022 4:32 \u063A.\u0645."}, + {"44713.188888888886", "[$-463]mmm dd yyyy h:mm AM/PM", "\u0686\u0646\u06AB\u0627 \u069A 01 2022 4:32 \u063A.\u0645."}, + {"44713.188888888886", "[$-463]mmmm dd yyyy h:mm AM/PM", "\u0686\u0646\u06AB\u0627 \u069A\u0632\u0645\u0631\u0649 01 2022 4:32 \u063A.\u0645."}, + {"44713.188888888886", "[$-463]mmmmm dd yyyy h:mm AM/PM", "\u0686\u0646\u06AB\u0627 \u069A\u0632\u0645\u0631\u0649 01 2022 4:32 \u063A.\u0645."}, + {"44713.188888888886", "[$-463]mmmmmm dd yyyy h:mm AM/PM", "\u0686\u0646\u06AB\u0627 \u069A\u0632\u0645\u0631\u0649 01 2022 4:32 \u063A.\u0645."}, + {"43543.503206018519", "[$-463]mmm dd yyyy h:mm AM/PM", "\u0648\u0631\u0649 19 2019 12:04 \u063A.\u0648."}, + {"43543.503206018519", "[$-463]mmmm dd yyyy h:mm AM/PM", "\u0648\u0631\u0649 19 2019 12:04 \u063A.\u0648."}, + {"43543.503206018519", "[$-463]mmmmm dd yyyy h:mm AM/PM", "\u0648\u0631\u0649 19 2019 12:04 \u063A.\u0648."}, + {"43543.503206018519", "[$-463]mmmmmm dd yyyy h:mm AM/PM", "\u0648\u0631\u0649 19 2019 12:04 \u063A.\u0648."}, {"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"}, @@ -1073,8 +2071,8 @@ func TestNumFmt(t *testing.T) { {"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", "[$x.-unknown]#,##0.00", "123"}, + {"123", "[$x.-unknown]MM/DD/YYYY", "123"}, {"123", "[DBNum4][$-804]yyyy\"年\"m\"月\";@", "123"}, // Invalid number format {"123", "x0.00s", "123"}, From ae17fa87d506cc5c2ccda7ff280567c491de3ea9 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 9 Aug 2023 00:11:06 +0800 Subject: [PATCH 777/957] This ref #1585, support to read one cell anchor pictures and improve date and time number format - Support apply date and time number format with 16 languages: Persian, Polish, Portuguese, Punjabi, Quechua, Romanian, Romansh, Sakha, Sami, Sanskrit, Scottish Gaelic, Serbian, Sesotho sa Leboa, Setswana, Sindhi, Sinhala and Slovak - Update the unit test and dependencies modules --- chart_test.go | 6 +- drawing.go | 14 +- go.mod | 6 +- go.sum | 16 +- numfmt.go | 809 ++++++++++++++++++++++++++++++++++++++++---- numfmt_test.go | 537 +++++++++++++++++++++++++++++ picture.go | 32 +- picture_test.go | 8 + xmlDecodeDrawing.go | 12 +- 9 files changed, 1320 insertions(+), 120 deletions(-) diff --git a/chart_test.go b/chart_test.go index 4d61c9be39..0fa66580f4 100644 --- a/chart_test.go +++ b/chart_test.go @@ -70,7 +70,7 @@ func TestChartSize(t *testing.T) { var ( workdir decodeWsDr - anchor decodeTwoCellAnchor + anchor decodeCellAnchor ) content, ok := newFile.Pkg.Load("xl/drawings/drawing1.xml") @@ -81,8 +81,8 @@ func TestChartSize(t *testing.T) { t.FailNow() } - err = xml.Unmarshal([]byte(""+ - workdir.TwoCellAnchor[0].Content+""), &anchor) + err = xml.Unmarshal([]byte(""+ + workdir.TwoCellAnchor[0].Content+""), &anchor) if !assert.NoError(t, err) { t.FailNow() } diff --git a/drawing.go b/drawing.go index 400c990248..9f0f4b6b69 100644 --- a/drawing.go +++ b/drawing.go @@ -1394,15 +1394,15 @@ func (f *File) deleteDrawing(col, row int, drawingXML, drawingType string) error var ( err error wsDr *xlsxWsDr - deTwoCellAnchor *decodeTwoCellAnchor + deTwoCellAnchor *decodeCellAnchor ) xdrCellAnchorFuncs := map[string]func(anchor *xdrCellAnchor) bool{ "Chart": func(anchor *xdrCellAnchor) bool { return anchor.Pic == nil }, "Pic": func(anchor *xdrCellAnchor) bool { return anchor.Pic != nil }, } - decodeTwoCellAnchorFuncs := map[string]func(anchor *decodeTwoCellAnchor) bool{ - "Chart": func(anchor *decodeTwoCellAnchor) bool { return anchor.Pic == nil }, - "Pic": func(anchor *decodeTwoCellAnchor) bool { return anchor.Pic != nil }, + decodeCellAnchorFuncs := map[string]func(anchor *decodeCellAnchor) bool{ + "Chart": func(anchor *decodeCellAnchor) bool { return anchor.Pic == nil }, + "Pic": func(anchor *decodeCellAnchor) bool { return anchor.Pic != nil }, } if wsDr, _, err = f.drawingParser(drawingXML); err != nil { return err @@ -1416,12 +1416,12 @@ func (f *File) deleteDrawing(col, row int, drawingXML, drawingType string) error } } for idx := 0; idx < len(wsDr.TwoCellAnchor); idx++ { - deTwoCellAnchor = new(decodeTwoCellAnchor) - if err = f.xmlNewDecoder(strings.NewReader("" + wsDr.TwoCellAnchor[idx].GraphicFrame + "")). + deTwoCellAnchor = new(decodeCellAnchor) + if err = f.xmlNewDecoder(strings.NewReader("" + wsDr.TwoCellAnchor[idx].GraphicFrame + "")). Decode(deTwoCellAnchor); err != nil && err != io.EOF { return err } - if err = nil; deTwoCellAnchor.From != nil && decodeTwoCellAnchorFuncs[drawingType](deTwoCellAnchor) { + if err = nil; deTwoCellAnchor.From != nil && decodeCellAnchorFuncs[drawingType](deTwoCellAnchor) { if deTwoCellAnchor.From.Col == col && deTwoCellAnchor.From.Row == row { wsDr.TwoCellAnchor = append(wsDr.TwoCellAnchor[:idx], wsDr.TwoCellAnchor[idx+1:]...) idx-- diff --git a/go.mod b/go.mod index 11d3adafd6..562ffdc3ed 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-20230802181842-ad255f2331ca github.com/xuri/nfp v0.0.0-20230802015359-2d5eeba905e9 - golang.org/x/crypto v0.11.0 + golang.org/x/crypto v0.12.0 golang.org/x/image v0.5.0 - golang.org/x/net v0.13.0 - golang.org/x/text v0.11.0 + golang.org/x/net v0.14.0 + golang.org/x/text v0.12.0 ) require github.com/richardlehane/msoleps v1.0.3 // indirect diff --git a/go.sum b/go.sum index ce3d29d310..f11f4dfe7b 100644 --- a/go.sum +++ b/go.sum @@ -22,8 +22,8 @@ github.com/xuri/nfp v0.0.0-20230802015359-2d5eeba905e9/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.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= -golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= 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= @@ -33,8 +33,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v 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.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.13.0 h1:Nvo8UFsZ8X3BhAC9699Z1j7XQ3rsZnUUm7jfBEk1ueY= -golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= +golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= 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= @@ -45,19 +45,19 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc 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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.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.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= 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.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= -golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 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/numfmt.go b/numfmt.go index c82fe7c81d..abeb7e6423 100644 --- a/numfmt.go +++ b/numfmt.go @@ -87,10 +87,10 @@ var ( 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\);_("$"* "-"??_);_(@_)`, + 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", @@ -102,86 +102,86 @@ var ( langNumFmt = map[string]map[int]string{ "zh-tw": { 27: "[$-404]e/m/d", - 28: `[$-404]e"年"m"月"d"日"`, - 29: `[$-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"秒"`, + 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"秒"`, + 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"日"`, + 58: "[$-404]e\"年\"m\"月\"d\"日\"", }, "zh-cn": { - 27: `yyyy"年"m"月"`, - 28: `m"月"d"日"`, - 29: `m"月"d"日"`, + 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"日"`, + 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"日"`, + 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"日"`, + 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"日"`, + 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"日"`, + 58: "[$-411]ggge\"年\"m\"月\"d\"日\"", }, "ko-kr": { - 27: `yyyy"年" mm"月" dd"日"`, + 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"日"`, + 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"日"`, + 57: "yyyy\"年\" mm\"月\" dd\"日\"", 58: "mm-dd", }, "th-th": { @@ -193,22 +193,22 @@ var ( 68: "t0.00%", 69: "t# ?/?", 70: "t# ??/??", - 71: "ว/ด/ปปปป", - 72: "ว-ดดด-ปป", - 73: "ว-ดดด", - 74: "ดดด-ปป", - 75: "ช:นน", - 76: "ช:นน:ทท", - 77: "ว/ด/ปปปป ช:นน", - 78: "นน:ทท", - 79: "[ช]:นน:ทท", - 80: "นน:ทท.0", + 71: "\u0E27/\u0E14/\u0E1B\u0E1B\u0E1B\u0E1B", + 72: "\u0E27-\u0E14\u0E14\u0E14-\u0E1B\u0E1B", + 73: "\u0E27-\u0E14\u0E14\u0E14", + 74: "\u0E14\u0E14\u0E14-\u0E1B\u0E1B", + 75: "\u0E0A:\u0E19\u0E19", + 76: "\u0E0A:\u0E19\u0E19:\u0E17\u0E17", + 77: "\u0E27/\u0E14/\u0E1B\u0E1B\u0E1B\u0E1B \u0E0A:\u0E19\u0E19", + 78: "\u0E19\u0E19:\u0E17\u0E17", + 79: "[\u0E0A%5D]\u0E19\u0E19:\u0E17\u0E17", + 80: "\u0E19\u0E19:\u0E17\u0E17.0", 81: "d/m/bb", }, } // currencyNumFmt defined the currency number format map. currencyNumFmt = map[int]string{ - 164: `"¥"#,##0.00`, + 164: "\"¥\"#,##0.00", 165: "[$$-409]#,##0.00", 166: "[$$-45C]#,##0.00", 167: "[$$-1004]#,##0.00", @@ -222,18 +222,18 @@ var ( 175: "[$$-2C09]#,##0.00", 176: "[$$-2409]#,##0.00", 177: "[$$-1000]#,##0.00", - 178: `#,##0.00\ [$$-C0C]`, + 178: "#,##0.00\\ [$$-C0C]", 179: "[$$-475]#,##0.00", 180: "[$$-83E]#,##0.00", - 181: `[$$-86B]\ #,##0.00`, - 182: `[$$-340A]\ #,##0.00`, + 181: "[$$-86B]\\ #,##0.00", + 182: "[$$-340A]\\ #,##0.00", 183: "[$$-240A]#,##0.00", - 184: `[$$-300A]\ #,##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`, + 189: "[$$-380A]\\ #,##0.00", 190: "[$£-809]#,##0.00", 191: "[$£-491]#,##0.00", 192: "[$£-452]#,##0.00", @@ -937,9 +937,76 @@ var ( "472": {tags: []string{"om-ET"}, localMonth: localMonthsNameOromo, apFmt: apFmtOromo}, "63": {tags: []string{"ps"}, localMonth: localMonthsNamePashto, apFmt: apFmtPashto}, "463": {tags: []string{"ps-AF"}, localMonth: localMonthsNamePashto, apFmt: apFmtPashto}, + "29": {tags: []string{"fa"}, localMonth: localMonthsNamePersian, apFmt: apFmtPersian}, + "429": {tags: []string{"fa-IR"}, localMonth: localMonthsNamePersian, apFmt: apFmtPersian}, + "15": {tags: []string{"pl"}, localMonth: localMonthsNamePolish, apFmt: nfp.AmPm[0]}, + "415": {tags: []string{"pl-PL"}, localMonth: localMonthsNamePolish, apFmt: nfp.AmPm[0]}, + "16": {tags: []string{"pt"}, localMonth: localMonthsNamePortuguese, apFmt: nfp.AmPm[0]}, + "416": {tags: []string{"pt-BR"}, localMonth: localMonthsNamePortuguese, apFmt: nfp.AmPm[0]}, + "816": {tags: []string{"pt-BR"}, localMonth: localMonthsNamePortuguese, apFmt: nfp.AmPm[0]}, + "46": {tags: []string{"pa"}, localMonth: localMonthsNamePunjabi, apFmt: apFmtPunjabi}, + "7C46": {tags: []string{"pa-Arab"}, localMonth: localMonthsNamePunjabiArab, apFmt: nfp.AmPm[0]}, + "446": {tags: []string{"pa-IN"}, localMonth: localMonthsNamePunjabi, apFmt: apFmtPunjabi}, + "846": {tags: []string{"pa-Arab-PK"}, localMonth: localMonthsNamePunjabiArab, apFmt: nfp.AmPm[0]}, + "6B": {tags: []string{"quz"}, localMonth: localMonthsNameQuechua, apFmt: apFmtCuba}, + "46B": {tags: []string{"quz-BO"}, localMonth: localMonthsNameQuechua, apFmt: apFmtCuba}, + "86B": {tags: []string{"quz-EC"}, localMonth: localMonthsNameQuechuaEcuador, apFmt: nfp.AmPm[0]}, + "C6B": {tags: []string{"quz-PE"}, localMonth: localMonthsNameQuechua, apFmt: apFmtCuba}, + "18": {tags: []string{"ro"}, localMonth: localMonthsNameRomanian, apFmt: apFmtCuba}, + "818": {tags: []string{"ro-MD"}, localMonth: localMonthsNameRomanian, apFmt: apFmtCuba}, + "418": {tags: []string{"ro-RO"}, localMonth: localMonthsNameRomanian, apFmt: apFmtCuba}, + "17": {tags: []string{"rm"}, localMonth: localMonthsNameRomansh, apFmt: nfp.AmPm[0]}, + "417": {tags: []string{"rm-CH"}, localMonth: localMonthsNameRomansh, apFmt: nfp.AmPm[0]}, "19": {tags: []string{"ru"}, localMonth: localMonthsNameRussian, apFmt: nfp.AmPm[0]}, "819": {tags: []string{"ru-MD"}, localMonth: localMonthsNameRussian, apFmt: nfp.AmPm[0]}, "419": {tags: []string{"ru-RU"}, localMonth: localMonthsNameRussian, apFmt: nfp.AmPm[0]}, + "85": {tags: []string{"sah"}, localMonth: localMonthsNameSakha, apFmt: apFmtSakha}, + "485": {tags: []string{"sah-RU"}, localMonth: localMonthsNameSakha, apFmt: apFmtSakha}, + "703B": {tags: []string{"smn"}, localMonth: localMonthsNameSami, apFmt: nfp.AmPm[0]}, + "243B": {tags: []string{"smn-FI"}, localMonth: localMonthsNameSami, apFmt: nfp.AmPm[0]}, + "7C3B": {tags: []string{"smj"}, localMonth: localMonthsNameSamiLule, apFmt: nfp.AmPm[0]}, + "103B": {tags: []string{"smj-NO"}, localMonth: localMonthsNameSamiLule, apFmt: nfp.AmPm[0]}, + "143B": {tags: []string{"smj-SE"}, localMonth: localMonthsNameSamiLule, apFmt: nfp.AmPm[0]}, + "3B": {tags: []string{"se"}, localMonth: localMonthsNameSamiNorthern, apFmt: apFmtSamiNorthern}, + "C3B": {tags: []string{"se-FI"}, localMonth: localMonthsNameSamiNorthernFI, apFmt: nfp.AmPm[0]}, + "43B": {tags: []string{"se-NO"}, localMonth: localMonthsNameSamiNorthern, apFmt: apFmtSamiNorthern}, + "83B": {tags: []string{"se-SE"}, localMonth: localMonthsNameSamiNorthern, apFmt: nfp.AmPm[0]}, + "743B": {tags: []string{"sms"}, localMonth: localMonthsNameSamiSkolt, apFmt: nfp.AmPm[0]}, + "203B": {tags: []string{"sms-FI"}, localMonth: localMonthsNameSamiSkolt, apFmt: nfp.AmPm[0]}, + "783B": {tags: []string{"sma"}, localMonth: localMonthsNameSamiSouthern, apFmt: nfp.AmPm[0]}, + "183B": {tags: []string{"sma-NO"}, localMonth: localMonthsNameSamiSouthern, apFmt: nfp.AmPm[0]}, + "1C3B": {tags: []string{"sma-SE"}, localMonth: localMonthsNameSamiSouthern, apFmt: nfp.AmPm[0]}, + "4F": {tags: []string{"sa"}, localMonth: localMonthsNameSanskrit, apFmt: apFmtSanskrit}, + "44F": {tags: []string{"sa-IN"}, localMonth: localMonthsNameSanskrit, apFmt: apFmtSanskrit}, + "91": {tags: []string{"gd"}, localMonth: localMonthsNameScottishGaelic, apFmt: apFmtScottishGaelic}, + "491": {tags: []string{"gd-GB"}, localMonth: localMonthsNameScottishGaelic, apFmt: apFmtScottishGaelic}, + "6C1A": {tags: []string{"sr-Cyrl"}, localMonth: localMonthsNameSerbian, apFmt: nfp.AmPm[0]}, + "1C1A": {tags: []string{"sr-Cyrl-BA"}, localMonth: localMonthsNameSerbianBA, apFmt: nfp.AmPm[0]}, + "301A": {tags: []string{"sr-Cyrl-ME"}, localMonth: localMonthsNameSerbian, apFmt: nfp.AmPm[0]}, + "281A": {tags: []string{"sr-Cyrl-RS"}, localMonth: localMonthsNameSerbian, apFmt: nfp.AmPm[0]}, + "C1A": {tags: []string{"sr-Cyrl-CS"}, localMonth: localMonthsNameSerbian, apFmt: nfp.AmPm[0]}, + "701A": {tags: []string{"sr-Latn"}, localMonth: localMonthsNameSerbianLatin, apFmt: apFmtSerbianLatin}, + "7C1A": {tags: []string{"sr"}, localMonth: localMonthsNameSerbianLatin, apFmt: apFmtSerbianLatin}, + "181A": {tags: []string{"sr-Latn-BA"}, localMonth: localMonthsNameSerbianLatin, apFmt: apFmtSerbianLatinBA}, + "2C1A": {tags: []string{"sr-Latn-ME"}, localMonth: localMonthsNameSerbianLatin, apFmt: apFmtSerbianLatinBA}, + "241A": {tags: []string{"sr-Latn-RS"}, localMonth: localMonthsNameSerbianLatin, apFmt: apFmtSerbianLatin}, + "81A": {tags: []string{"sr-Latn-CS"}, localMonth: localMonthsNameSerbianLatinCS, apFmt: nfp.AmPm[0]}, + "6C": {tags: []string{"nso"}, localMonth: localMonthsNameSesothoSaLeboa, apFmt: nfp.AmPm[0]}, + "46C": {tags: []string{"nso-ZA"}, localMonth: localMonthsNameSesothoSaLeboa, apFmt: nfp.AmPm[0]}, + "32": {tags: []string{"tn"}, localMonth: localMonthsNameSetswana, apFmt: nfp.AmPm[0]}, + "832": {tags: []string{"tn-BW"}, localMonth: localMonthsNameSetswana, apFmt: nfp.AmPm[0]}, + "432": {tags: []string{"tn-ZA"}, localMonth: localMonthsNameSetswana, apFmt: nfp.AmPm[0]}, + "59": {tags: []string{"sd"}, localMonth: localMonthsNameSindhi, apFmt: nfp.AmPm[0]}, + "7C59": {tags: []string{"sd-Arab"}, localMonth: localMonthsNameSindhi, apFmt: nfp.AmPm[0]}, + "859": {tags: []string{"sd-Arab-PK"}, localMonth: localMonthsNameSindhi, apFmt: nfp.AmPm[0]}, + "5B": {tags: []string{"si"}, localMonth: localMonthsNameSinhala, apFmt: apFmtSinhala}, + "45B": {tags: []string{"si-LK"}, localMonth: localMonthsNameSinhala, apFmt: apFmtSinhala}, + "1B": {tags: []string{"sk"}, localMonth: localMonthsNameSlovak, apFmt: nfp.AmPm[0]}, + "41B": {tags: []string{"sk-SK"}, localMonth: localMonthsNameSlovak, apFmt: nfp.AmPm[0]}, + "24": {tags: []string{"sl"}, localMonth: localMonthsNameSlovenian, apFmt: apFmtSlovenian}, + "424": {tags: []string{"sl-SI"}, localMonth: localMonthsNameSlovenian, apFmt: apFmtSlovenian}, + "77": {tags: []string{"so"}, localMonth: localMonthsNameSomali, apFmt: apFmtSomali}, + "477": {tags: []string{"so-SO"}, localMonth: localMonthsNameSomali, apFmt: apFmtSomali}, "A": {tags: []string{"es"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, "2C0A": {tags: []string{"es-AR"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, "200A": {tags: []string{"es-VE"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, @@ -1663,6 +1730,67 @@ var ( "\u0644\u0646\u0688 \u06CD", "\u0645\u0631\u063A\u0648\u0645\u0649", } + // monthNamesPersian list the month names in the Persian. + monthNamesPersian = []string{ + "\u0698\u0627\u0646\u0648\u064A\u0647", + "\u0641\u0648\u0631\u064A\u0647", + "\u0645\u0627\u0631\u0633", + "\u0622\u0648\u0631\u064A\u0644", + "\u0645\u0647", + "\u0698\u0648\u0626\u0646", + "\u0698\u0648\u0626\u064A\u0647", + "\u0627\u0648\u062A", + "\u0633\u067E\u062A\u0627\u0645\u0628\u0631", + "\u0627\u064F\u0643\u062A\u0628\u0631", + "\u0646\u0648\u0627\u0645\u0628\u0631", + "\u062F\u0633\u0627\u0645\u0628\u0631", + } + // monthNamesPolish list the month names in the Polish. + monthNamesPolish = []string{"styczeń", "luty", "marzec", "kwiecień", "maj", "czerwiec", "lipiec", "sierpień", "wrzesień", "październik", "listopad", "grudzień"} + // monthNamesPortuguese list the month names in the Portuguese. + monthNamesPortuguese = []string{"janeiro", "fevereiro", "março", "abril", "maio", "junho", "julho", "agosto", "setembro", "outubro", "novembro", "dezembro"} + // monthNamesPunjabi list the month names in the Punjabi. + monthNamesPunjabi = []string{ + "\u0A1C\u0A28\u0A35\u0A30\u0A40", + "\u0A2B\u0A3C\u0A30\u0A35\u0A30\u0A40", + "\u0A2E\u0A3E\u0A30\u0A1A", + "\u0A05\u0A2A\u0A4D\u0A30\u0A48\u0A32", + "\u0A2E\u0A08", + "\u0A1C\u0A42\u0A28", + "\u0A1C\u0A41\u0A32\u0A3E\u0A08", + "\u0A05\u0A17\u0A38\u0A24", + "\u0A38\u0A24\u0A70\u0A2C\u0A30", + "\u0A05\u0A15\u0A24\u0A42\u0A2C\u0A30", + "\u0A28\u0A35\u0A70\u0A2C\u0A30", + "\u0A26\u0A38\u0A70\u0A2C\u0A30", + } + // monthNamesPunjabiArab list the month names in the Punjabi Arab. + monthNamesPunjabiArab = []string{ + "\u062C\u0646\u0648\u0631\u06CC", + "\u0641\u0631\u0648\u0631\u06CC", + "\u0645\u0627\u0631\u0686", + "\u0627\u067E\u0631\u06CC\u0644", + "\u0645\u0626\u06CC", + "\u062C\u0648\u0646", + "\u062C\u0648\u0644\u0627\u0626\u06CC", + "\u0627\u06AF\u0633\u062A", + "\u0633\u062A\u0645\u0628\u0631", + "\u0627\u06A9\u062A\u0648\u0628\u0631", + "\u0646\u0648\u0645\u0628\u0631", + "\u062F\u0633\u0645\u0628\u0631", + } + // monthNamesQuechua list the month names in the Quechua. + monthNamesQuechua = []string{"Qulla puquy", "Hatun puquy", "Pauqar waray", "ayriwa", "Aymuray", "Inti raymi", "Anta Sitwa", "Qhapaq Sitwa", "Uma raymi", "Kantaray", "Ayamarq'a", "Kapaq Raymi"} + // monthNamesQuechuaEcuador list the month names in the Quechua Ecuador. + monthNamesQuechuaEcuador = []string{"kulla", "panchi", "pawkar", "ayriwa", "aymuray", "raymi", "sitwa", "karwa", "kuski", "wayru", "sasi", "kapak"} + // monthNamesRomanian list the month names in the Romanian. + monthNamesRomanian = []string{"ianuarie", "februarie", "martie", "aprilie", "mai", "iunie", "iulie", "august", "septembrie", "octombrie", "noiembrie", "decembrie"} + // monthNamesRomanianAbbr list the month abbreviations in the Romanian. + monthNamesRomanianAbbr = []string{"ian.", "feb.", "mar.", "apr.", "mai", "iun.", "iul.", "aug.", "sept.", "oct.", "nov.", "dec."} + // monthNamesRomansh list the month names in the Romansh. + monthNamesRomansh = []string{"schaner", "favrer", "mars", "avrigl", "matg", "zercladur", "fanadur", "avust", "settember", "october", "november", "december"} + // monthNamesRomanshAbbr list the month abbreviations in the Romansh. + monthNamesRomanshAbbr = []string{"schan.", "favr.", "mars", "avr.", "matg", "zercl.", "fan.", "avust", "sett.", "oct.", "nov.", "dec."} // monthNamesRussian list the month names in the Russian. monthNamesRussian = []string{ "\u044F\u043D\u0432\u0430\u0440\u044C", @@ -1693,6 +1821,205 @@ var ( "\u043D\u043E\u044F.", "\u0434\u0435\u043A.", } + // monthNamesSakha list the month names in the Sakha. + monthNamesSakha = []string{ + "\u0422\u043E\u0445\u0441\u0443\u043D\u043D\u044C\u0443", + "\u041E\u043B\u0443\u043D\u043D\u044C\u0443", + "\u041A\u0443\u043B\u0443\u043D \u0442\u0443\u0442\u0430\u0440", + "\u041C\u0443\u0443\u0441 \u0443\u0441\u0442\u0430\u0440", + "\u042B\u0430\u043C \u044B\u0439\u0430", + "\u0411\u044D\u0441 \u044B\u0439\u0430", + "\u041E\u0442 \u044B\u0439\u0430", + "\u0410\u0442\u044B\u0440\u0434\u044C\u0430\u0445 \u044B\u0439\u0430", + "\u0411\u0430\u043B\u0430\u0495\u0430\u043D \u044B\u0439\u0430", + "\u0410\u043B\u0442\u044B\u043D\u043D\u044C\u044B", + "\u0421\u044D\u0442\u0438\u043D\u043D\u044C\u0438", + "\u0410\u0445\u0441\u044B\u043D\u043D\u044C\u044B", + } + // monthNamesSakhaAbbr list the month abbreviations in the Sakha. + monthNamesSakhaAbbr = []string{ + "\u0422\u0445\u0441", + "\u041E\u043B\u043D", + "\u041A\u043B\u043D", + "\u041C\u0441\u0443", + "\u042B\u0430\u043C", + "\u0411\u044D\u0441", + "\u041E\u0442\u044B", + "\u0410\u0442\u0440", + "\u0411\u043B\u0495", + "\u0410\u043B\u0442", + "\u0421\u044D\u0442", + "\u0410\u0445\u0441", + } + // monthNamesSami list the month names in the Sami. + monthNamesSami = []string{"uđđâivemáánu", "kuovâmáánu", "njuhčâmáánu", "cuáŋuimáánu", "vyesimáánu", "kesimáánu", "syeinimáánu", "porgemáánu", "čohčâmáánu", "roovvâdmáánu", "skammâmáánu", "juovlâmáánu"} + // monthNamesSamiAbbr list the month abbreviations in the Sami. + monthNamesSamiAbbr = []string{"uđiv", "kuov", "njuh", "cuáŋ", "vyes", "kesi", "syei", "porg", "čohč", "roov", "skam", "juov"} + // monthNamesSamiLule list the month names in the Sami (Lule). + monthNamesSamiLule = []string{"ådåjakmánno", "guovvamánno", "sjnjuktjamánno", "vuoratjismánno", "moarmesmánno", "biehtsemánno", "sjnjilltjamánno", "bårggemánno", "ragátmánno", "gålgådismánno", "basádismánno", "javllamánno"} + // monthNamesSamiLuleAbbr list the month abbreviations in the Sami (Lule). + monthNamesSamiLuleAbbr = []string{"ådåj", "guov", "snju", "vuor", "moar", "bieh", "snji", "bårg", "ragá", "gålg", "basá", "javl"} + // monthNamesSamiNorthern list the month names in the Sami (Northern). + monthNamesSamiNorthern = []string{"ođđajagemánnu", "guovvamánnu", "njukčamánnu", "cuoŋománnu", "miessemánnu", "geassemánnu", "suoidnemánnu", "borgemánnu", "čakčamánnu", "golggotmánnu", "skábmamánnu", "juovlamánnu"} + // monthNamesSamiNorthernAbbr list the month abbreviations in the Sami (Northern). + monthNamesSamiNorthernAbbr = []string{"ođđj", "guov", "njuk", "cuoŋ", "mies", "geas", "suoi", "borg", "čakč", "golg", "skáb", "juov"} + // monthNamesSamiSkolt list the month names in the Sami (Skolt). + monthNamesSamiSkolt = []string{"ođđee´jjmään", "tä´lvvmään", "pâ´zzlâšttam-mään", "njuhččmään", "vue´ssmään", "ǩie´ssmään", "suei´nnmään", "på´rǧǧmään", "čõhččmään", "kålggmään", "skamm-mään", "rosttovmään"} + // monthNamesSamiSouthern list the month names in the Sami (Southern). + monthNamesSamiSouthern = []string{"tsïengele", "goevte", "njoktje", "voerhtje", "suehpede", "ruffie", "snjaltje", "mïetske", "skïerede", "golke", "rahka", "goeve"} + // monthNamesSamiSouthernAbbr list the month abbreviations in the Sami (Southern). + monthNamesSamiSouthernAbbr = []string{"tsïen", "goevt", "njok", "voer", "sueh", "ruff", "snja", "mïet", "skïer", "golk", "rahk", "goev"} + // monthNamesSanskrit list the month names in the Sanskrit. + monthNamesSanskrit = []string{ + "\u091C\u093E\u0928\u094D\u092F\u0941\u0905\u0930\u0940", + "\u092B\u0947\u092C\u094D\u0930\u0941\u0905\u0930\u0940", + "\u092E\u093E\u0930\u094D\u091A", + "\u090F\u092A\u094D\u0930\u093F\u0932", + "\u092E\u0947", + "\u091C\u0942\u0928", + "\u091C\u0941\u0932\u0948", + "\u0911\u0917\u0938\u094D\u091F", + "\u0938\u092A\u094D\u091F\u0947\u0902\u092C\u0930", + "\u0911\u0915\u094D\u091F\u094B\u092C\u0930", + "\u0928\u094B\u0935\u094D\u0939\u0947\u0902\u092C\u0930", + "\u0921\u093F\u0938\u0947\u0902\u092C\u0930", + } + // monthNamesScottishGaelic list the month names in the Scottish Gaelic. + monthNamesScottishGaelic = []string{"Am Faoilleach", "An Gearran", "Am Màrt", "An Giblean", "An Cèitean", "An t-Ògmhios", "An t-Iuchar", "An Lùnastal", "An t-Sultain", "An Dàmhair", "An t-Samhain", "An Dùbhlachd"} + // monthNamesScottishGaelicAbbr list the month abbreviations in the ScottishGaelic. + monthNamesScottishGaelicAbbr = []string{"Faoi", "Gear", "Màrt", "Gibl", "Cèit", "Ògmh", "Iuch", "Lùna", "Sult", "Dàmh", "Samh", "Dùbh"} + // monthNamesSerbian list the month names in the Serbian (Cyrillic). + monthNamesSerbian = []string{ + "\u0458\u0430\u043D\u0443\u0430\u0440", + "\u0444\u0435\u0431\u0440\u0443\u0430\u0440", + "\u043C\u0430\u0440\u0442", + "\u0430\u043F\u0440\u0438\u043B", + "\u043C\u0430\u0458", + "\u0458\u0443\u043D", + "\u0458\u0443\u043B", + "\u0430\u0432\u0433\u0443\u0441\u0442", + "\u0441\u0435\u043F\u0442\u0435\u043C\u0431\u0430\u0440", + "\u043E\u043A\u0442\u043E\u0431\u0430\u0440", + "\u043D\u043E\u0432\u0435\u043C\u0431\u0430\u0440", + "\u0434\u0435\u0446\u0435\u043C\u0431\u0430\u0440", + } + // monthNamesSerbianAbbr lists the month name abbreviations in the Serbian + // (Cyrillic). + monthNamesSerbianAbbr = []string{ + "\u0458\u0430\u043D.", + "\u0444\u0435\u0431.", + "\u043C\u0430\u0440\u0442", + "\u0430\u043F\u0440.", + "\u043C\u0430\u0458", + "\u0458\u0443\u043D", + "\u0458\u0443\u043B", + "\u0430\u0432\u0433.", + "\u0441\u0435\u043F\u0442.", + "\u043E\u043A\u0442.", + "\u043D\u043E\u0432.", + "\u0434\u0435\u0446.", + } + // monthNamesSerbianBA list the month names in the Serbian (Cyrillic) Bosnia + // and Herzegovina. + monthNamesSerbianBA = []string{ + "\u0458\u0430\u043D\u0443\u0430\u0440", + "\u0444\u0435\u0431\u0440\u0443\u0430\u0440", + "\u043C\u0430\u0440\u0442", + "\u0430\u043F\u0440\u0438\u043B", + "\u043C\u0430\u0458", + "\u0458\u0443\u043D\u0438", + "\u0458\u0443\u043B\u0438", + "\u0430\u0432\u0433\u0443\u0441\u0442", + "\u0441\u0435\u043F\u0442\u0435\u043C\u0431\u0430\u0440", + "\u043E\u043A\u0442\u043E\u0431\u0430\u0440", + "\u043D\u043E\u0432\u0435\u043C\u0431\u0430\u0440", + "\u0434\u0435\u0446\u0435\u043C\u0431\u0430\u0440", + } + // monthNamesSerbianBAAbbr lists the month name abbreviations in the Serbian + // (Cyrillic) Bosnia and Herzegovina. + monthNamesSerbianBAAbbr = []string{ + "\u0458\u0430\u043D", + "\u0444\u0435\u0431", + "\u043C\u0430\u0440", + "\u0430\u043F\u0440", + "\u043C\u0430\u0458", + "\u0458\u0443\u043D", + "\u0458\u0443\u043B", + "\u0430\u0432\u0433", + "\u0441\u0435\u043F", + "\u043E\u043A\u0442", + "\u043D\u043E\u0432", + "\u0434\u0435\u0446", + } + // monthNamesSerbianLatin list the month names in the Serbian (Latin). + monthNamesSerbianLatin = []string{"januar", "februar", "mart", "april", "maj", "jun", "jul", "avgust", "septembar", "oktobar", "novembar", "decembar"} + // monthNamesSerbianLatinAbbr lists the month name abbreviations in the + // Serbian(Latin) and Montenegro (Former). + monthNamesSerbianLatinAbbr = []string{"jan.", "feb.", "mart", "apr.", "maj", "jun", "jul", "avg.", "sept.", "okt.", "nov.", "dec."} + // monthNamesSesothoSaLeboa list the month names in the Sesotho sa Leboa. + monthNamesSesothoSaLeboa = []string{"Janaware", "Feberware", "Matšhe", "Aprele", "Mei", "June", "Julae", "Agostose", "Setemere", "Oktoboro", "Nofemere", "Disemere"} + // monthNamesSesothoSaLeboaAbbr lists the month name abbreviations in the + // Sesotho sa Leboa. + monthNamesSesothoSaLeboaAbbr = []string{"Jan", "Feb", "Matš", "Apr", "Mei", "June", "Julae", "Agost", "Set", "Oky", "Nof", "Dis"} + // monthNamesSetswana list the month names in the Setswana. + monthNamesSetswana = []string{"Ferikgong", "Tlhakole", "Mopitlwe", "Moranang", "Motsheganang", "Seetebosigo", "Phukwi", "Phatwe", "Lwetse", "Diphalane", "Ngwanatsele", "Sedimonthole"} + // monthNamesSetswanaAbbr lists the month name abbreviations in the Setswana. + monthNamesSetswanaAbbr = []string{"Fer.", "Tlh.", "Mop.", "Mor.", "Motsh.", "Seet.", "Phk.", "Pht.", "Lwetse.", "Diph.", "Ngwn.", "Sed."} + // monthNamesSindhi list the month names in the Sindhi. + monthNamesSindhi = []string{ + "\u062C\u0646\u0648\u0631\u064A", + "\u0641\u0631\u0648\u0631\u064A", + "\u0645\u0627\u0631\u0686", + "\u0627\u067E\u0631\u064A\u0644", + "\u0645\u0654\u064A", + "\u062C\u0648\u0646", + "\u062C\u0648\u0644\u0627\u0621\u0650", + "\u0622\u06AF\u0633\u062A", + "\u0633\u062A\u0645\u0628\u0631", + "\u0622\u06A9\u062A\u0648\u0628\u0631", + "\u0646\u0648\u0645\u0628\u0631", + "\u068A\u0633\u0645\u0628\u0631", + } + // monthNamesSinhala list the month names in the Sinhala. + monthNamesSinhala = []string{ + "\u0DA2\u0DB1\u0DC0\u0DCF\u0DBB\u0DD2", + "\u0DB4\u0DD9\u0DB6\u0DBB\u0DC0\u0DCF\u0DBB\u0DD2", + "\u0DB8\u0DCF\u0DBB\u0DCA\u0DAD\u0DD4", + "\u0D85\u0DB4\u0DCA\u200D\u0DBB\u0DDA\u0DBD\u0DCA", + "\u0DB8\u0DD0\u0DBA\u0DD2", + "\u0DA2\u0DD6\u0DB1\u0DD2", + "\u0DA2\u0DD6\u0DBD\u0DD2", + "\u0D85\u0D9C\u0DDD\u0DC3\u0DCA\u0DAD\u0DD4", + "\u0DC3\u0DD0\u0DB4\u0DCA\u0DAD\u0DD0\u0DB8\u0DCA\u0DB6\u0DBB\u0DCA", + "\u0D94\u0D9A\u0DCA\u0DAD\u0DDD\u0DB6\u0DBB\u0DCA", + "\u0DB1\u0DDC\u0DC0\u0DD0\u0DB8\u0DCA\u0DB6\u0DBB\u0DCA", + "\u0DAF\u0DD9\u0DC3\u0DD0\u0DB8\u0DCA\u0DB6\u0DBB\u0DCA", + } + // monthNamesSinhalaAbbr lists the month name abbreviations in Sinhala. + monthNamesSinhalaAbbr = []string{ + "\u0DA2\u0DB1.", + "\u0DB4\u0DD9\u0DB6.", + "\u0DB8\u0DCF\u0DBB\u0DCA\u0DAD\u0DD4.", + "\u0D85\u0DB4\u0DCA\u200D\u0DBB\u0DDA\u0DBD\u0DCA.", + "\u0DB8\u0DD0\u0DBA\u0DD2", + "\u0DA2\u0DD6\u0DB1\u0DD2", + "\u0DA2\u0DD6\u0DBD\u0DD2", + "\u0D85\u0D9C\u0DDD.", + "\u0DC3\u0DD0\u0DB4\u0DCA.", + "\u0D94\u0D9A\u0DCA.", + "\u0DB1\u0DDC\u0DC0\u0DD0.", + "\u0DAF\u0DD9\u0DC3\u0DD0.", + } + // monthNamesSlovak list the month names in the Slovak. + monthNamesSlovak = []string{"január", "február", "marec", "apríl", "máj", "jún", "júl", "august", "september", "október", "november", "december"} + // monthNamesSlovenian list the month names in the Slovenian. + monthNamesSlovenian = []string{"januar", "februar", "marec", "april", "maj", "junij", "julij", "avgust", "september", "oktober", "november", "december"} + // monthNamesSlovenianAbbr list the month abbreviations in the Slovenian. + monthNamesSlovenianAbbr = []string{"jan.", "feb.", "mar.", "apr.", "maj", "jun.", "jul.", "avg.", "sep.", "okt.", "nov.", "dec."} + // monthNamesSomali list the month names in the Somali. + monthNamesSomali = []string{"Jannaayo", "Febraayo", "Maarso", "Abriil", "May", "Juun", "Luuliyo", "Ogost", "Sebtembar", "Oktoobar", "Nofembar", "Desembar"} + // monthNamesSomaliAbbr list the month abbreviations in the Somali. + monthNamesSomaliAbbr = []string{"Jan", "Feb", "Mar", "Abr", "May", "Jun", "Lul", "Ogs", "Seb", "Okt", "Nof", "Dis"} // 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 the Spanish. @@ -1862,6 +2189,29 @@ var ( apFmtOromo = "WD/WB" // apFmtPashto defined the AM/PM name in the Pashto. apFmtPashto = "\u063A.\u0645./\u063A.\u0648." + // apFmtPersian defined the AM/PM name in the Persian. + apFmtPersian = "\u0642.\u0638/\u0628.\u0638" + // apFmtPunjabi defined the AM/PM name in the Punjabi. + apFmtPunjabi = "\u0A38\u0A35\u0A47\u0A30/\u0A38\u0A3C\u0A3E\u0A2E" + // apFmtSakha defined the AM/PM name in the Sakha. + apFmtSakha = "\u041A\u0418/\u041A\u041A" + // apFmtSamiNorthern defined the AM/PM name in the Sami (Northern). + apFmtSamiNorthern = "i.b./e.b." + // apFmtSanskrit defined the AM/PM name in the Sanskrit. + apFmtSanskrit = "\u092E\u0927\u094D\u092F\u093E\u0928\u092A\u0942\u0930\u094D\u0935/\u092E\u0927\u094D\u092F\u093E\u0928\u092A\u091A\u094D\u092F\u093E\u0924" + // apFmtScottishGaelic defined the AM/PM name in the Scottish Gaelic. + apFmtScottishGaelic = "m/f" + // apFmtSerbianLatin defined the AM/PM name in the Serbian (Latin). + apFmtSerbianLatin = "pre podne/po podne" + // apFmtSerbianLatinBA defined the AM/PM name in the Serbian (Latin) Bosnia + // and Herzegovina. + apFmtSerbianLatinBA = "prije podne/po podne" + // apFmtSinhala defined the AM/PM name in the Sinhala. + apFmtSinhala = "\u0DB4\u0DD9.\u0DC0./\u0DB4.\u0DC0." + // apFmtSlovenian defined the AM/PM name in the Slovenian. + apFmtSlovenian = "dop./pop." + // apFmtSomali defined the AM/PM name in the Somali. + apFmtSomali = "GH/GD" // apFmtSpanish defined the AM/PM name in the Spanish. apFmtSpanish = "a. m./p. m." // apFmtTibetan defined the AM/PM name in the Tibetan. @@ -3045,6 +3395,99 @@ func localMonthsNamePashto(t time.Time, abbr int) string { return monthNamesPashto[int(t.Month())-1] } +// localMonthsNamePersian returns the Persian name of the month. +func localMonthsNamePersian(t time.Time, abbr int) string { + if abbr == 5 { + return string([]rune(monthNamesPersian[int(t.Month()-1)])[:1]) + } + return monthNamesPersian[int(t.Month())-1] +} + +// localMonthsNamePolish returns the Polish name of the month. +func localMonthsNamePolish(t time.Time, abbr int) string { + if abbr == 3 { + return string([]rune(monthNamesPolish[int(t.Month()-1)])[:3]) + } + if abbr == 4 || abbr > 6 { + return monthNamesPolish[int(t.Month())-1] + } + return string([]rune(monthNamesPolish[int(t.Month()-1)])[:1]) +} + +// localMonthsNamePortuguese returns the Portuguese name of the month. +func localMonthsNamePortuguese(t time.Time, abbr int) string { + if abbr == 3 { + return string([]rune(monthNamesPortuguese[int(t.Month()-1)])[:3]) + } + if abbr == 4 || abbr > 6 { + return monthNamesPortuguese[int(t.Month())-1] + } + return string([]rune(monthNamesPortuguese[int(t.Month()-1)])[:1]) +} + +// localMonthsNamePunjabi returns the Punjabi name of the month. +func localMonthsNamePunjabi(t time.Time, abbr int) string { + if abbr == 5 { + return string([]rune(monthNamesPunjabi[int(t.Month()-1)])[:1]) + } + return monthNamesPunjabi[int(t.Month())-1] +} + +// localMonthsNamePunjabiArab returns the Punjabi Arab name of the month. +func localMonthsNamePunjabiArab(t time.Time, abbr int) string { + if abbr == 5 { + return string([]rune(monthNamesPunjabiArab[int(t.Month()-1)])[:1]) + } + return monthNamesPunjabiArab[int(t.Month())-1] +} + +// localMonthsNameQuechua returns the Quechua name of the month. +func localMonthsNameQuechua(t time.Time, abbr int) string { + if abbr == 3 { + return string([]rune(monthNamesQuechua[int(t.Month()-1)])[:3]) + } + if abbr == 4 || abbr > 6 { + return monthNamesQuechua[int(t.Month())-1] + } + return string([]rune(monthNamesQuechua[int(t.Month()-1)])[:1]) +} + +// localMonthsNameQuechuaEcuador returns the QuechuaEcuador name of the month. +func localMonthsNameQuechuaEcuador(t time.Time, abbr int) string { + if abbr == 3 { + if int(t.Month()) == 1 { + return string([]rune(monthNamesQuechuaEcuador[int(t.Month()-1)])[:4]) + } + return string([]rune(monthNamesQuechuaEcuador[int(t.Month()-1)])[:3]) + } + if abbr == 4 || abbr > 6 { + return monthNamesQuechuaEcuador[int(t.Month())-1] + } + return string([]rune(monthNamesQuechuaEcuador[int(t.Month()-1)])[:1]) +} + +// localMonthsNameRomanian returns the Romanian name of the month. +func localMonthsNameRomanian(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesRomanianAbbr[int(t.Month())-1] + } + if abbr == 4 || abbr > 6 { + return monthNamesRomanian[int(t.Month())-1] + } + return string([]rune(monthNamesRomanian[int(t.Month()-1)])[:1]) +} + +// localMonthsNameRomansh returns the Romansh name of the month. +func localMonthsNameRomansh(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesRomanshAbbr[int(t.Month())-1] + } + if abbr == 4 || abbr > 6 { + return monthNamesRomansh[int(t.Month())-1] + } + return string([]rune(monthNamesRomansh[int(t.Month()-1)])[:1]) +} + // localMonthsNameRussian returns the Russian name of the month. func localMonthsNameRussian(t time.Time, abbr int) string { if abbr == 3 { @@ -3060,6 +3503,222 @@ func localMonthsNameRussian(t time.Time, abbr int) string { return string([]rune(monthNamesRussian[int(t.Month())-1])[:1]) } +// localMonthsNameSakha returns the Sakha name of the month. +func localMonthsNameSakha(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesSakhaAbbr[int(t.Month())-1] + } + if abbr == 4 || abbr > 6 { + return monthNamesSakha[int(t.Month())-1] + } + return string([]rune(monthNamesSakha[int(t.Month()-1)])[:1]) +} + +// localMonthsNameSami returns the Sami name of the month. +func localMonthsNameSami(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesSamiAbbr[int(t.Month())-1] + } + if abbr == 4 || abbr > 6 { + return monthNamesSami[int(t.Month())-1] + } + return string([]rune(monthNamesSami[int(t.Month()-1)])[:1]) +} + +// localMonthsNameSamiLule returns the Sami (Lule) name of the month. +func localMonthsNameSamiLule(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesSamiLuleAbbr[int(t.Month())-1] + } + if abbr == 4 || abbr > 6 { + return monthNamesSamiLule[int(t.Month())-1] + } + return string([]rune(monthNamesSamiLule[int(t.Month()-1)])[:1]) +} + +// localMonthsNameSamiNorthern returns the Sami (Northern) name of the month. +func localMonthsNameSamiNorthern(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesSamiNorthernAbbr[int(t.Month()-1)] + } + if abbr == 4 || abbr > 6 { + return monthNamesSamiNorthern[int(t.Month())-1] + } + return string([]rune(monthNamesSamiNorthern[int(t.Month()-1)])[:1]) +} + +// localMonthsNameSamiNorthernFI returns the Sami (Northern) Finland name of the +// month. +func localMonthsNameSamiNorthernFI(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesSamiNorthernAbbr[int(t.Month()-1)] + } + if abbr == 4 || abbr > 6 { + if int(t.Month()) == 1 { + return "ođđajagemánu" + } + return monthNamesSamiNorthern[int(t.Month())-1] + } + return string([]rune(monthNamesSamiNorthern[int(t.Month()-1)])[:1]) +} + +// localMonthsNameSamiSkolt returns the Sami (Skolt) name of the month. +func localMonthsNameSamiSkolt(t time.Time, abbr int) string { + if abbr == 5 { + return string([]rune(monthNamesSamiSkolt[int(t.Month()-1)])[:1]) + } + return monthNamesSamiSkolt[int(t.Month())-1] +} + +// localMonthsNameSamiSouthern returns the Sami (Southern) name of the month. +func localMonthsNameSamiSouthern(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesSamiSouthernAbbr[int(t.Month()-1)] + } + if abbr == 4 || abbr > 6 { + return monthNamesSamiSouthern[int(t.Month())-1] + } + return string([]rune(monthNamesSamiSouthern[int(t.Month()-1)])[:1]) +} + +// localMonthsNameSanskrit returns the Sanskrit name of the month. +func localMonthsNameSanskrit(t time.Time, abbr int) string { + if abbr == 5 { + return string([]rune(monthNamesSanskrit[int(t.Month()-1)])[:1]) + } + return monthNamesSanskrit[int(t.Month())-1] +} + +// localMonthsNameScottishGaelic returns the Scottish Gaelic name of the month. +func localMonthsNameScottishGaelic(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesScottishGaelicAbbr[int(t.Month()-1)] + } + if abbr == 4 || abbr > 6 { + return monthNamesScottishGaelic[int(t.Month())-1] + } + return string([]rune(monthNamesScottishGaelic[int(t.Month()-1)])[:1]) +} + +// localMonthsNameSerbian returns the Serbian (Cyrillic) name of the month. +func localMonthsNameSerbian(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesSerbianAbbr[int(t.Month()-1)] + } + if abbr == 4 || abbr > 6 { + return monthNamesSerbian[int(t.Month())-1] + } + return string([]rune(monthNamesSerbian[int(t.Month()-1)])[:1]) +} + +// localMonthsNameSerbianBA returns the Serbian (Cyrillic) Bosnia and +// Herzegovina name of the month. +func localMonthsNameSerbianBA(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesSerbianBAAbbr[int(t.Month()-1)] + } + if abbr == 4 || abbr > 6 { + return monthNamesSerbianBA[int(t.Month())-1] + } + return string([]rune(monthNamesSerbianBA[int(t.Month()-1)])[:1]) +} + +// localMonthsNameSerbianLatin returns the Serbian (Latin) name of the month. +func localMonthsNameSerbianLatin(t time.Time, abbr int) string { + if abbr == 3 { + return string([]rune(monthNamesSerbianLatin[int(t.Month()-1)])[:3]) + } + if abbr == 4 || abbr > 6 { + return monthNamesSerbianLatin[int(t.Month())-1] + } + return string([]rune(monthNamesSerbianLatin[int(t.Month()-1)])[:1]) +} + +// localMonthsNameSerbianLatinCS returns the Serbian (Latin) name of the month. +func localMonthsNameSerbianLatinCS(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesSerbianLatinAbbr[int(t.Month()-1)] + } + if abbr == 4 || abbr > 6 { + return monthNamesSerbianLatin[int(t.Month())-1] + } + return string([]rune(monthNamesSerbianLatin[int(t.Month()-1)])[:1]) +} + +// localMonthsNameSesothoSaLeboa returns the Sesotho sa Leboa name of the month. +func localMonthsNameSesothoSaLeboa(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesSesothoSaLeboaAbbr[int(t.Month()-1)] + } + if abbr == 4 || abbr > 6 { + return monthNamesSesothoSaLeboa[int(t.Month())-1] + } + return string([]rune(monthNamesSesothoSaLeboa[int(t.Month()-1)])[:1]) +} + +// localMonthsNameSetswana returns the Setswana name of the month. +func localMonthsNameSetswana(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesSetswanaAbbr[int(t.Month()-1)] + } + if abbr == 4 || abbr > 6 { + return monthNamesSetswana[int(t.Month())-1] + } + return string([]rune(monthNamesSetswana[int(t.Month()-1)])[:1]) +} + +// localMonthsNameSindhi returns the Sindhi name of the month. +func localMonthsNameSindhi(t time.Time, abbr int) string { + if abbr == 5 { + return string([]rune(monthNamesSindhi[int(t.Month()-1)])[:1]) + } + return monthNamesSindhi[int(t.Month())-1] +} + +// localMonthsNameSinhala returns the Sinhala name of the month. +func localMonthsNameSinhala(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesSinhalaAbbr[int(t.Month()-1)] + } + if abbr == 4 || abbr > 6 { + return monthNamesSinhala[int(t.Month())-1] + } + return string([]rune(monthNamesSinhala[int(t.Month()-1)])[:1]) +} + +// localMonthsNameSlovak returns the Slovak name of the month. +func localMonthsNameSlovak(t time.Time, abbr int) string { + if abbr == 3 { + return strconv.Itoa(int(t.Month())) + } + if abbr == 4 || abbr > 6 { + return monthNamesSlovak[int(t.Month())-1] + } + return string([]rune(monthNamesSlovak[int(t.Month()-1)])[:1]) +} + +// localMonthsNameSlovenian returns the Slovenian name of the month. +func localMonthsNameSlovenian(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesSlovenianAbbr[int(t.Month()-1)] + } + if abbr == 4 || abbr > 6 { + return monthNamesSlovenian[int(t.Month())-1] + } + return string([]rune(monthNamesSlovenian[int(t.Month()-1)])[:1]) +} + +// localMonthsNameSomali returns the Somali name of the month. +func localMonthsNameSomali(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesSomaliAbbr[int(t.Month()-1)] + } + if abbr == 4 || abbr > 6 { + return monthNamesSomali[int(t.Month())-1] + } + return string([]rune(monthNamesSomali[int(t.Month()-1)])[:1]) +} + // localMonthsNameSpanish returns the Spanish name of the month. func localMonthsNameSpanish(t time.Time, abbr int) string { if abbr == 3 { diff --git a/numfmt_test.go b/numfmt_test.go index f6aff17556..746bd3d0e6 100644 --- a/numfmt_test.go +++ b/numfmt_test.go @@ -1271,12 +1271,549 @@ func TestNumFmt(t *testing.T) { {"43543.503206018519", "[$-463]mmmm dd yyyy h:mm AM/PM", "\u0648\u0631\u0649 19 2019 12:04 \u063A.\u0648."}, {"43543.503206018519", "[$-463]mmmmm dd yyyy h:mm AM/PM", "\u0648\u0631\u0649 19 2019 12:04 \u063A.\u0648."}, {"43543.503206018519", "[$-463]mmmmmm dd yyyy h:mm AM/PM", "\u0648\u0631\u0649 19 2019 12:04 \u063A.\u0648."}, + {"44562.189571759256", "[$-29]mmm dd yyyy h:mm AM/PM", "\u0698\u0627\u0646\u0648\u064A\u0647 01 2022 4:32 \u0642.\u0638"}, + {"44562.189571759256", "[$-29]mmmm dd yyyy h:mm AM/PM", "\u0698\u0627\u0646\u0648\u064A\u0647 01 2022 4:32 \u0642.\u0638"}, + {"44562.189571759256", "[$-29]mmmmm dd yyyy h:mm AM/PM", "\u0698 01 2022 4:32 \u0642.\u0638"}, + {"44562.189571759256", "[$-29]mmmmmm dd yyyy h:mm AM/PM", "\u0698\u0627\u0646\u0648\u064A\u0647 01 2022 4:32 \u0642.\u0638"}, + {"43543.503206018519", "[$-29]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0628.\u0638"}, + {"43543.503206018519", "[$-29]mmmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0628.\u0638"}, + {"43543.503206018519", "[$-29]mmmmm dd yyyy h:mm AM/PM", "\u0645 19 2019 12:04 \u0628.\u0638"}, + {"43543.503206018519", "[$-29]mmmmmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0628.\u0638"}, + {"44562.189571759256", "[$-429]mmm dd yyyy h:mm AM/PM", "\u0698\u0627\u0646\u0648\u064A\u0647 01 2022 4:32 \u0642.\u0638"}, + {"44562.189571759256", "[$-429]mmmm dd yyyy h:mm AM/PM", "\u0698\u0627\u0646\u0648\u064A\u0647 01 2022 4:32 \u0642.\u0638"}, + {"44562.189571759256", "[$-429]mmmmm dd yyyy h:mm AM/PM", "\u0698 01 2022 4:32 \u0642.\u0638"}, + {"44562.189571759256", "[$-429]mmmmmm dd yyyy h:mm AM/PM", "\u0698\u0627\u0646\u0648\u064A\u0647 01 2022 4:32 \u0642.\u0638"}, + {"43543.503206018519", "[$-429]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0628.\u0638"}, + {"43543.503206018519", "[$-429]mmmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0628.\u0638"}, + {"43543.503206018519", "[$-429]mmmmm dd yyyy h:mm AM/PM", "\u0645 19 2019 12:04 \u0628.\u0638"}, + {"43543.503206018519", "[$-429]mmmmmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0628.\u0638"}, + {"44562.189571759256", "[$-15]mmm dd yyyy h:mm AM/PM", "sty 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-15]mmmm dd yyyy h:mm AM/PM", "styczeń 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-15]mmmmm dd yyyy h:mm AM/PM", "s 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-15]mmmmmm dd yyyy h:mm AM/PM", "styczeń 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-15]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-15]mmmm dd yyyy h:mm AM/PM", "marzec 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-15]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-15]mmmmmm dd yyyy h:mm AM/PM", "marzec 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-415]mmm dd yyyy h:mm AM/PM", "sty 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-415]mmmm dd yyyy h:mm AM/PM", "styczeń 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-415]mmmmm dd yyyy h:mm AM/PM", "s 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-415]mmmmmm dd yyyy h:mm AM/PM", "styczeń 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-415]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-415]mmmm dd yyyy h:mm AM/PM", "marzec 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-415]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-415]mmmmmm dd yyyy h:mm AM/PM", "marzec 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-16]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-16]mmmm dd yyyy h:mm AM/PM", "janeiro 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-16]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-16]mmmmmm dd yyyy h:mm AM/PM", "janeiro 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-16]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-16]mmmm dd yyyy h:mm AM/PM", "março 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-16]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-16]mmmmmm dd yyyy h:mm AM/PM", "março 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-416]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-416]mmmm dd yyyy h:mm AM/PM", "janeiro 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-416]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-416]mmmmmm dd yyyy h:mm AM/PM", "janeiro 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-416]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-416]mmmm dd yyyy h:mm AM/PM", "março 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-416]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-416]mmmmmm dd yyyy h:mm AM/PM", "março 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-816]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-816]mmmm dd yyyy h:mm AM/PM", "janeiro 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-816]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-816]mmmmmm dd yyyy h:mm AM/PM", "janeiro 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-816]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-816]mmmm dd yyyy h:mm AM/PM", "março 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-816]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-816]mmmmmm dd yyyy h:mm AM/PM", "março 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-46]mmm dd yyyy h:mm AM/PM", "\u0A1C\u0A28\u0A35\u0A30\u0A40 01 2022 4:32 \u0A38\u0A35\u0A47\u0A30"}, + {"44562.189571759256", "[$-46]mmmm dd yyyy h:mm AM/PM", "\u0A1C\u0A28\u0A35\u0A30\u0A40 01 2022 4:32 \u0A38\u0A35\u0A47\u0A30"}, + {"44562.189571759256", "[$-46]mmmmm dd yyyy h:mm AM/PM", "\u0A1C 01 2022 4:32 \u0A38\u0A35\u0A47\u0A30"}, + {"44562.189571759256", "[$-46]mmmmmm dd yyyy h:mm AM/PM", "\u0A1C\u0A28\u0A35\u0A30\u0A40 01 2022 4:32 \u0A38\u0A35\u0A47\u0A30"}, + {"43543.503206018519", "[$-46]mmm dd yyyy h:mm AM/PM", "\u0A2E\u0A3E\u0A30\u0A1A 19 2019 12:04 \u0A38\u0A3C\u0A3E\u0A2E"}, + {"43543.503206018519", "[$-46]mmmm dd yyyy h:mm AM/PM", "\u0A2E\u0A3E\u0A30\u0A1A 19 2019 12:04 \u0A38\u0A3C\u0A3E\u0A2E"}, + {"43543.503206018519", "[$-46]mmmmm dd yyyy h:mm AM/PM", "\u0A2E 19 2019 12:04 \u0A38\u0A3C\u0A3E\u0A2E"}, + {"43543.503206018519", "[$-46]mmmmmm dd yyyy h:mm AM/PM", "\u0A2E\u0A3E\u0A30\u0A1A 19 2019 12:04 \u0A38\u0A3C\u0A3E\u0A2E"}, + {"44562.189571759256", "[$-7C46]mmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u06CC 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-7C46]mmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u06CC 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-7C46]mmmmm dd yyyy h:mm AM/PM", "\u062C 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-7C46]mmmmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u06CC 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-7C46]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-7C46]mmmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-7C46]mmmmm dd yyyy h:mm AM/PM", "\u0645 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-7C46]mmmmmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-446]mmm dd yyyy h:mm AM/PM", "\u0A1C\u0A28\u0A35\u0A30\u0A40 01 2022 4:32 \u0A38\u0A35\u0A47\u0A30"}, + {"44562.189571759256", "[$-446]mmmm dd yyyy h:mm AM/PM", "\u0A1C\u0A28\u0A35\u0A30\u0A40 01 2022 4:32 \u0A38\u0A35\u0A47\u0A30"}, + {"44562.189571759256", "[$-446]mmmmm dd yyyy h:mm AM/PM", "\u0A1C 01 2022 4:32 \u0A38\u0A35\u0A47\u0A30"}, + {"44562.189571759256", "[$-446]mmmmmm dd yyyy h:mm AM/PM", "\u0A1C\u0A28\u0A35\u0A30\u0A40 01 2022 4:32 \u0A38\u0A35\u0A47\u0A30"}, + {"43543.503206018519", "[$-446]mmm dd yyyy h:mm AM/PM", "\u0A2E\u0A3E\u0A30\u0A1A 19 2019 12:04 \u0A38\u0A3C\u0A3E\u0A2E"}, + {"43543.503206018519", "[$-446]mmmm dd yyyy h:mm AM/PM", "\u0A2E\u0A3E\u0A30\u0A1A 19 2019 12:04 \u0A38\u0A3C\u0A3E\u0A2E"}, + {"43543.503206018519", "[$-446]mmmmm dd yyyy h:mm AM/PM", "\u0A2E 19 2019 12:04 \u0A38\u0A3C\u0A3E\u0A2E"}, + {"43543.503206018519", "[$-446]mmmmmm dd yyyy h:mm AM/PM", "\u0A2E\u0A3E\u0A30\u0A1A 19 2019 12:04 \u0A38\u0A3C\u0A3E\u0A2E"}, + {"44562.189571759256", "[$-846]mmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u06CC 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-846]mmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u06CC 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-846]mmmmm dd yyyy h:mm AM/PM", "\u062C 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-846]mmmmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u06CC 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-846]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-846]mmmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-846]mmmmm dd yyyy h:mm AM/PM", "\u0645 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-846]mmmmmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-6B]mmm dd yyyy h:mm AM/PM", "Qul 01 2022 4:32 a.m."}, + {"44562.189571759256", "[$-6B]mmmm dd yyyy h:mm AM/PM", "Qulla puquy 01 2022 4:32 a.m."}, + {"44562.189571759256", "[$-6B]mmmmm dd yyyy h:mm AM/PM", "Q 01 2022 4:32 a.m."}, + {"44562.189571759256", "[$-6B]mmmmmm dd yyyy h:mm AM/PM", "Qulla puquy 01 2022 4:32 a.m."}, + {"43543.503206018519", "[$-6B]mmm dd yyyy h:mm AM/PM", "Pau 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-6B]mmmm dd yyyy h:mm AM/PM", "Pauqar waray 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-6B]mmmmm dd yyyy h:mm AM/PM", "P 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-6B]mmmmmm dd yyyy h:mm AM/PM", "Pauqar waray 19 2019 12:04 p.m."}, + {"44562.189571759256", "[$-46B]mmm dd yyyy h:mm AM/PM", "Qul 01 2022 4:32 a.m."}, + {"44562.189571759256", "[$-46B]mmmm dd yyyy h:mm AM/PM", "Qulla puquy 01 2022 4:32 a.m."}, + {"44562.189571759256", "[$-46B]mmmmm dd yyyy h:mm AM/PM", "Q 01 2022 4:32 a.m."}, + {"44562.189571759256", "[$-46B]mmmmmm dd yyyy h:mm AM/PM", "Qulla puquy 01 2022 4:32 a.m."}, + {"43543.503206018519", "[$-46B]mmm dd yyyy h:mm AM/PM", "Pau 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-46B]mmmm dd yyyy h:mm AM/PM", "Pauqar waray 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-46B]mmmmm dd yyyy h:mm AM/PM", "P 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-46B]mmmmmm dd yyyy h:mm AM/PM", "Pauqar waray 19 2019 12:04 p.m."}, + {"44562.189571759256", "[$-86B]mmm dd yyyy h:mm AM/PM", "kull 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-86B]mmmm dd yyyy h:mm AM/PM", "kulla 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-86B]mmmmm dd yyyy h:mm AM/PM", "k 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-86B]mmmmmm dd yyyy h:mm AM/PM", "kulla 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-86B]mmm dd yyyy h:mm AM/PM", "paw 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-86B]mmmm dd yyyy h:mm AM/PM", "pawkar 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-86B]mmmmm dd yyyy h:mm AM/PM", "p 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-86B]mmmmmm dd yyyy h:mm AM/PM", "pawkar 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-C6B]mmm dd yyyy h:mm AM/PM", "Qul 01 2022 4:32 a.m."}, + {"44562.189571759256", "[$-C6B]mmmm dd yyyy h:mm AM/PM", "Qulla puquy 01 2022 4:32 a.m."}, + {"44562.189571759256", "[$-C6B]mmmmm dd yyyy h:mm AM/PM", "Q 01 2022 4:32 a.m."}, + {"44562.189571759256", "[$-C6B]mmmmmm dd yyyy h:mm AM/PM", "Qulla puquy 01 2022 4:32 a.m."}, + {"43543.503206018519", "[$-C6B]mmm dd yyyy h:mm AM/PM", "Pau 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-C6B]mmmm dd yyyy h:mm AM/PM", "Pauqar waray 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-C6B]mmmmm dd yyyy h:mm AM/PM", "P 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-C6B]mmmmmm dd yyyy h:mm AM/PM", "Pauqar waray 19 2019 12:04 p.m."}, + {"44562.189571759256", "[$-18]mmm dd yyyy h:mm AM/PM", "ian. 01 2022 4:32 a.m."}, + {"44562.189571759256", "[$-18]mmmm dd yyyy h:mm AM/PM", "ianuarie 01 2022 4:32 a.m."}, + {"44562.189571759256", "[$-18]mmmmm dd yyyy h:mm AM/PM", "i 01 2022 4:32 a.m."}, + {"44562.189571759256", "[$-18]mmmmmm dd yyyy h:mm AM/PM", "ianuarie 01 2022 4:32 a.m."}, + {"43543.503206018519", "[$-18]mmm dd yyyy h:mm AM/PM", "mar. 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-18]mmmm dd yyyy h:mm AM/PM", "martie 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-18]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-18]mmmmmm dd yyyy h:mm AM/PM", "martie 19 2019 12:04 p.m."}, + {"44562.189571759256", "[$-818]mmm dd yyyy h:mm AM/PM", "ian. 01 2022 4:32 a.m."}, + {"44562.189571759256", "[$-818]mmmm dd yyyy h:mm AM/PM", "ianuarie 01 2022 4:32 a.m."}, + {"44562.189571759256", "[$-818]mmmmm dd yyyy h:mm AM/PM", "i 01 2022 4:32 a.m."}, + {"44562.189571759256", "[$-818]mmmmmm dd yyyy h:mm AM/PM", "ianuarie 01 2022 4:32 a.m."}, + {"43543.503206018519", "[$-818]mmm dd yyyy h:mm AM/PM", "mar. 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-818]mmmm dd yyyy h:mm AM/PM", "martie 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-818]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-818]mmmmmm dd yyyy h:mm AM/PM", "martie 19 2019 12:04 p.m."}, + {"44562.189571759256", "[$-418]mmm dd yyyy h:mm AM/PM", "ian. 01 2022 4:32 a.m."}, + {"44562.189571759256", "[$-418]mmmm dd yyyy h:mm AM/PM", "ianuarie 01 2022 4:32 a.m."}, + {"44562.189571759256", "[$-418]mmmmm dd yyyy h:mm AM/PM", "i 01 2022 4:32 a.m."}, + {"44562.189571759256", "[$-418]mmmmmm dd yyyy h:mm AM/PM", "ianuarie 01 2022 4:32 a.m."}, + {"43543.503206018519", "[$-418]mmm dd yyyy h:mm AM/PM", "mar. 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-418]mmmm dd yyyy h:mm AM/PM", "martie 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-418]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-418]mmmmmm dd yyyy h:mm AM/PM", "martie 19 2019 12:04 p.m."}, + {"44562.189571759256", "[$-17]mmm dd yyyy h:mm AM/PM", "schan. 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-17]mmmm dd yyyy h:mm AM/PM", "schaner 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-17]mmmmm dd yyyy h:mm AM/PM", "s 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-17]mmmmmm dd yyyy h:mm AM/PM", "schaner 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-17]mmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-17]mmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-17]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-17]mmmmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-417]mmm dd yyyy h:mm AM/PM", "schan. 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-417]mmmm dd yyyy h:mm AM/PM", "schaner 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-417]mmmmm dd yyyy h:mm AM/PM", "s 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-417]mmmmmm dd yyyy h:mm AM/PM", "schaner 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-417]mmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-417]mmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-417]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-417]mmmmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, {"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 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", "[$-85]mmm dd yyyy h:mm AM/PM", "\u0422\u0445\u0441 01 2022 4:32 \u041A\u0418"}, + {"44562.189571759256", "[$-85]mmmm dd yyyy h:mm AM/PM", "\u0422\u043E\u0445\u0441\u0443\u043D\u043D\u044C\u0443 01 2022 4:32 \u041A\u0418"}, + {"44562.189571759256", "[$-85]mmmmm dd yyyy h:mm AM/PM", "\u0422 01 2022 4:32 \u041A\u0418"}, + {"44562.189571759256", "[$-85]mmmmmm dd yyyy h:mm AM/PM", "\u0422\u043E\u0445\u0441\u0443\u043D\u043D\u044C\u0443 01 2022 4:32 \u041A\u0418"}, + {"43543.503206018519", "[$-85]mmm dd yyyy h:mm AM/PM", "\u041A\u043B\u043D 19 2019 12:04 \u041A\u041A"}, + {"43543.503206018519", "[$-85]mmmm dd yyyy h:mm AM/PM", "\u041A\u0443\u043B\u0443\u043D \u0442\u0443\u0442\u0430\u0440 19 2019 12:04 \u041A\u041A"}, + {"43543.503206018519", "[$-85]mmmmm dd yyyy h:mm AM/PM", "\u041A 19 2019 12:04 \u041A\u041A"}, + {"43543.503206018519", "[$-85]mmmmmm dd yyyy h:mm AM/PM", "\u041A\u0443\u043B\u0443\u043D \u0442\u0443\u0442\u0430\u0440 19 2019 12:04 \u041A\u041A"}, + {"44562.189571759256", "[$-485]mmm dd yyyy h:mm AM/PM", "\u0422\u0445\u0441 01 2022 4:32 \u041A\u0418"}, + {"44562.189571759256", "[$-485]mmmm dd yyyy h:mm AM/PM", "\u0422\u043E\u0445\u0441\u0443\u043D\u043D\u044C\u0443 01 2022 4:32 \u041A\u0418"}, + {"44562.189571759256", "[$-485]mmmmm dd yyyy h:mm AM/PM", "\u0422 01 2022 4:32 \u041A\u0418"}, + {"44562.189571759256", "[$-485]mmmmmm dd yyyy h:mm AM/PM", "\u0422\u043E\u0445\u0441\u0443\u043D\u043D\u044C\u0443 01 2022 4:32 \u041A\u0418"}, + {"43543.503206018519", "[$-485]mmm dd yyyy h:mm AM/PM", "\u041A\u043B\u043D 19 2019 12:04 \u041A\u041A"}, + {"43543.503206018519", "[$-485]mmmm dd yyyy h:mm AM/PM", "\u041A\u0443\u043B\u0443\u043D \u0442\u0443\u0442\u0430\u0440 19 2019 12:04 \u041A\u041A"}, + {"43543.503206018519", "[$-485]mmmmm dd yyyy h:mm AM/PM", "\u041A 19 2019 12:04 \u041A\u041A"}, + {"43543.503206018519", "[$-485]mmmmmm dd yyyy h:mm AM/PM", "\u041A\u0443\u043B\u0443\u043D \u0442\u0443\u0442\u0430\u0440 19 2019 12:04 \u041A\u041A"}, + {"44562.189571759256", "[$-703B]mmm dd yyyy h:mm AM/PM", "uđiv 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-703B]mmmm dd yyyy h:mm AM/PM", "uđđâivemáánu 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-703B]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-703B]mmmmmm dd yyyy h:mm AM/PM", "uđđâivemáánu 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-703B]mmm dd yyyy h:mm AM/PM", "njuh 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-703B]mmmm dd yyyy h:mm AM/PM", "njuhčâmáánu 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-703B]mmmmm dd yyyy h:mm AM/PM", "n 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-703B]mmmmmm dd yyyy h:mm AM/PM", "njuhčâmáánu 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-243B]mmm dd yyyy h:mm AM/PM", "uđiv 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-243B]mmmm dd yyyy h:mm AM/PM", "uđđâivemáánu 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-243B]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-243B]mmmmmm dd yyyy h:mm AM/PM", "uđđâivemáánu 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-243B]mmm dd yyyy h:mm AM/PM", "njuh 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-243B]mmmm dd yyyy h:mm AM/PM", "njuhčâmáánu 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-243B]mmmmm dd yyyy h:mm AM/PM", "n 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-243B]mmmmmm dd yyyy h:mm AM/PM", "njuhčâmáánu 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-7C3B]mmm dd yyyy h:mm AM/PM", "ådåj 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-7C3B]mmmm dd yyyy h:mm AM/PM", "ådåjakmánno 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-7C3B]mmmmm dd yyyy h:mm AM/PM", "å 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-7C3B]mmmmmm dd yyyy h:mm AM/PM", "ådåjakmánno 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-7C3B]mmm dd yyyy h:mm AM/PM", "snju 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-7C3B]mmmm dd yyyy h:mm AM/PM", "sjnjuktjamánno 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-7C3B]mmmmm dd yyyy h:mm AM/PM", "s 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-7C3B]mmmmmm dd yyyy h:mm AM/PM", "sjnjuktjamánno 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-103B]mmm dd yyyy h:mm AM/PM", "ådåj 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-103B]mmmm dd yyyy h:mm AM/PM", "ådåjakmánno 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-103B]mmmmm dd yyyy h:mm AM/PM", "å 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-103B]mmmmmm dd yyyy h:mm AM/PM", "ådåjakmánno 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-103B]mmm dd yyyy h:mm AM/PM", "snju 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-103B]mmmm dd yyyy h:mm AM/PM", "sjnjuktjamánno 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-103B]mmmmm dd yyyy h:mm AM/PM", "s 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-103B]mmmmmm dd yyyy h:mm AM/PM", "sjnjuktjamánno 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-143B]mmm dd yyyy h:mm AM/PM", "ådåj 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-143B]mmmm dd yyyy h:mm AM/PM", "ådåjakmánno 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-143B]mmmmm dd yyyy h:mm AM/PM", "å 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-143B]mmmmmm dd yyyy h:mm AM/PM", "ådåjakmánno 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-143B]mmm dd yyyy h:mm AM/PM", "snju 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-143B]mmmm dd yyyy h:mm AM/PM", "sjnjuktjamánno 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-143B]mmmmm dd yyyy h:mm AM/PM", "s 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-143B]mmmmmm dd yyyy h:mm AM/PM", "sjnjuktjamánno 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-3B]mmm dd yyyy h:mm AM/PM", "ođđj 01 2022 4:32 i.b."}, + {"44562.189571759256", "[$-3B]mmmm dd yyyy h:mm AM/PM", "ođđajagemánnu 01 2022 4:32 i.b."}, + {"44562.189571759256", "[$-3B]mmmmm dd yyyy h:mm AM/PM", "o 01 2022 4:32 i.b."}, + {"44562.189571759256", "[$-3B]mmmmmm dd yyyy h:mm AM/PM", "ođđajagemánnu 01 2022 4:32 i.b."}, + {"43543.503206018519", "[$-3B]mmm dd yyyy h:mm AM/PM", "njuk 19 2019 12:04 e.b."}, + {"43543.503206018519", "[$-3B]mmmm dd yyyy h:mm AM/PM", "njukčamánnu 19 2019 12:04 e.b."}, + {"43543.503206018519", "[$-3B]mmmmm dd yyyy h:mm AM/PM", "n 19 2019 12:04 e.b."}, + {"43543.503206018519", "[$-3B]mmmmmm dd yyyy h:mm AM/PM", "njukčamánnu 19 2019 12:04 e.b."}, + {"44562.189571759256", "[$-C3B]mmm dd yyyy h:mm AM/PM", "ođđj 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-C3B]mmmm dd yyyy h:mm AM/PM", "ođđajagemánu 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-C3B]mmmmm dd yyyy h:mm AM/PM", "o 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-C3B]mmmmmm dd yyyy h:mm AM/PM", "ođđajagemánu 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-C3B]mmm dd yyyy h:mm AM/PM", "njuk 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-C3B]mmmm dd yyyy h:mm AM/PM", "njukčamánnu 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-C3B]mmmmm dd yyyy h:mm AM/PM", "n 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-C3B]mmmmmm dd yyyy h:mm AM/PM", "njukčamánnu 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-43B]mmm dd yyyy h:mm AM/PM", "ođđj 01 2022 4:32 i.b."}, + {"44562.189571759256", "[$-43B]mmmm dd yyyy h:mm AM/PM", "ođđajagemánnu 01 2022 4:32 i.b."}, + {"44562.189571759256", "[$-43B]mmmmm dd yyyy h:mm AM/PM", "o 01 2022 4:32 i.b."}, + {"44562.189571759256", "[$-43B]mmmmmm dd yyyy h:mm AM/PM", "ođđajagemánnu 01 2022 4:32 i.b."}, + {"43543.503206018519", "[$-43B]mmm dd yyyy h:mm AM/PM", "njuk 19 2019 12:04 e.b."}, + {"43543.503206018519", "[$-43B]mmmm dd yyyy h:mm AM/PM", "njukčamánnu 19 2019 12:04 e.b."}, + {"43543.503206018519", "[$-43B]mmmmm dd yyyy h:mm AM/PM", "n 19 2019 12:04 e.b."}, + {"43543.503206018519", "[$-43B]mmmmmm dd yyyy h:mm AM/PM", "njukčamánnu 19 2019 12:04 e.b."}, + {"44562.189571759256", "[$-83B]mmm dd yyyy h:mm AM/PM", "ođđj 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-83B]mmmm dd yyyy h:mm AM/PM", "ođđajagemánnu 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-83B]mmmmm dd yyyy h:mm AM/PM", "o 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-83B]mmmmmm dd yyyy h:mm AM/PM", "ođđajagemánnu 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-83B]mmm dd yyyy h:mm AM/PM", "njuk 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-83B]mmmm dd yyyy h:mm AM/PM", "njukčamánnu 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-83B]mmmmm dd yyyy h:mm AM/PM", "n 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-83B]mmmmmm dd yyyy h:mm AM/PM", "njukčamánnu 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-743B]mmm dd yyyy h:mm AM/PM", "ođđee´jjmään 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-743B]mmmm dd yyyy h:mm AM/PM", "ođđee´jjmään 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-743B]mmmmm dd yyyy h:mm AM/PM", "o 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-743B]mmmmmm dd yyyy h:mm AM/PM", "ođđee´jjmään 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-743B]mmm dd yyyy h:mm AM/PM", "pâ´zzlâšttam-mään 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-743B]mmmm dd yyyy h:mm AM/PM", "pâ´zzlâšttam-mään 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-743B]mmmmm dd yyyy h:mm AM/PM", "p 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-743B]mmmmmm dd yyyy h:mm AM/PM", "pâ´zzlâšttam-mään 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-203B]mmm dd yyyy h:mm AM/PM", "ođđee´jjmään 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-203B]mmmm dd yyyy h:mm AM/PM", "ođđee´jjmään 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-203B]mmmmm dd yyyy h:mm AM/PM", "o 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-203B]mmmmmm dd yyyy h:mm AM/PM", "ođđee´jjmään 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-203B]mmm dd yyyy h:mm AM/PM", "pâ´zzlâšttam-mään 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-203B]mmmm dd yyyy h:mm AM/PM", "pâ´zzlâšttam-mään 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-203B]mmmmm dd yyyy h:mm AM/PM", "p 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-203B]mmmmmm dd yyyy h:mm AM/PM", "pâ´zzlâšttam-mään 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-783B]mmm dd yyyy h:mm AM/PM", "tsïen 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-783B]mmmm dd yyyy h:mm AM/PM", "tsïengele 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-783B]mmmmm dd yyyy h:mm AM/PM", "t 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-783B]mmmmmm dd yyyy h:mm AM/PM", "tsïengele 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-783B]mmm dd yyyy h:mm AM/PM", "njok 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-783B]mmmm dd yyyy h:mm AM/PM", "njoktje 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-783B]mmmmm dd yyyy h:mm AM/PM", "n 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-783B]mmmmmm dd yyyy h:mm AM/PM", "njoktje 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-183B]mmm dd yyyy h:mm AM/PM", "tsïen 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-183B]mmmm dd yyyy h:mm AM/PM", "tsïengele 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-183B]mmmmm dd yyyy h:mm AM/PM", "t 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-183B]mmmmmm dd yyyy h:mm AM/PM", "tsïengele 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-183B]mmm dd yyyy h:mm AM/PM", "njok 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-183B]mmmm dd yyyy h:mm AM/PM", "njoktje 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-183B]mmmmm dd yyyy h:mm AM/PM", "n 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-183B]mmmmmm dd yyyy h:mm AM/PM", "njoktje 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-1C3B]mmm dd yyyy h:mm AM/PM", "tsïen 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-1C3B]mmmm dd yyyy h:mm AM/PM", "tsïengele 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-1C3B]mmmmm dd yyyy h:mm AM/PM", "t 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-1C3B]mmmmmm dd yyyy h:mm AM/PM", "tsïengele 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-1C3B]mmm dd yyyy h:mm AM/PM", "njok 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-1C3B]mmmm dd yyyy h:mm AM/PM", "njoktje 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-1C3B]mmmmm dd yyyy h:mm AM/PM", "n 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-1C3B]mmmmmm dd yyyy h:mm AM/PM", "njoktje 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-4F]mmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u094D\u092F\u0941\u0905\u0930\u0940 01 2022 4:32 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u0942\u0930\u094D\u0935"}, + {"44562.189571759256", "[$-4F]mmmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u094D\u092F\u0941\u0905\u0930\u0940 01 2022 4:32 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u0942\u0930\u094D\u0935"}, + {"44562.189571759256", "[$-4F]mmmmm dd yyyy h:mm AM/PM", "\u091C 01 2022 4:32 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u0942\u0930\u094D\u0935"}, + {"44562.189571759256", "[$-4F]mmmmmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u094D\u092F\u0941\u0905\u0930\u0940 01 2022 4:32 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u0942\u0930\u094D\u0935"}, + {"43543.503206018519", "[$-4F]mmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u091A\u094D\u092F\u093E\u0924"}, + {"43543.503206018519", "[$-4F]mmmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u091A\u094D\u092F\u093E\u0924"}, + {"43543.503206018519", "[$-4F]mmmmm dd yyyy h:mm AM/PM", "\u092E 19 2019 12:04 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u091A\u094D\u092F\u093E\u0924"}, + {"43543.503206018519", "[$-4F]mmmmmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u091A\u094D\u092F\u093E\u0924"}, + {"44562.189571759256", "[$-44F]mmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u094D\u092F\u0941\u0905\u0930\u0940 01 2022 4:32 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u0942\u0930\u094D\u0935"}, + {"44562.189571759256", "[$-44F]mmmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u094D\u092F\u0941\u0905\u0930\u0940 01 2022 4:32 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u0942\u0930\u094D\u0935"}, + {"44562.189571759256", "[$-44F]mmmmm dd yyyy h:mm AM/PM", "\u091C 01 2022 4:32 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u0942\u0930\u094D\u0935"}, + {"44562.189571759256", "[$-44F]mmmmmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u094D\u092F\u0941\u0905\u0930\u0940 01 2022 4:32 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u0942\u0930\u094D\u0935"}, + {"43543.503206018519", "[$-44F]mmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u091A\u094D\u092F\u093E\u0924"}, + {"43543.503206018519", "[$-44F]mmmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u091A\u094D\u092F\u093E\u0924"}, + {"43543.503206018519", "[$-44F]mmmmm dd yyyy h:mm AM/PM", "\u092E 19 2019 12:04 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u091A\u094D\u092F\u093E\u0924"}, + {"43543.503206018519", "[$-44F]mmmmmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u091A\u094D\u092F\u093E\u0924"}, + {"44562.189571759256", "[$-91]mmm dd yyyy h:mm AM/PM", "Faoi 01 2022 4:32 m"}, + {"44562.189571759256", "[$-91]mmmm dd yyyy h:mm AM/PM", "Am Faoilleach 01 2022 4:32 m"}, + {"44562.189571759256", "[$-91]mmmmm dd yyyy h:mm AM/PM", "A 01 2022 4:32 m"}, + {"44562.189571759256", "[$-91]mmmmmm dd yyyy h:mm AM/PM", "Am Faoilleach 01 2022 4:32 m"}, + {"43543.503206018519", "[$-91]mmm dd yyyy h:mm AM/PM", "Màrt 19 2019 12:04 f"}, + {"43543.503206018519", "[$-91]mmmm dd yyyy h:mm AM/PM", "Am Màrt 19 2019 12:04 f"}, + {"43543.503206018519", "[$-91]mmmmm dd yyyy h:mm AM/PM", "A 19 2019 12:04 f"}, + {"43543.503206018519", "[$-91]mmmmmm dd yyyy h:mm AM/PM", "Am Màrt 19 2019 12:04 f"}, + {"44562.189571759256", "[$-491]mmm dd yyyy h:mm AM/PM", "Faoi 01 2022 4:32 m"}, + {"44562.189571759256", "[$-491]mmmm dd yyyy h:mm AM/PM", "Am Faoilleach 01 2022 4:32 m"}, + {"44562.189571759256", "[$-491]mmmmm dd yyyy h:mm AM/PM", "A 01 2022 4:32 m"}, + {"44562.189571759256", "[$-491]mmmmmm dd yyyy h:mm AM/PM", "Am Faoilleach 01 2022 4:32 m"}, + {"43543.503206018519", "[$-491]mmm dd yyyy h:mm AM/PM", "Màrt 19 2019 12:04 f"}, + {"43543.503206018519", "[$-491]mmmm dd yyyy h:mm AM/PM", "Am Màrt 19 2019 12:04 f"}, + {"43543.503206018519", "[$-491]mmmmm dd yyyy h:mm AM/PM", "A 19 2019 12:04 f"}, + {"43543.503206018519", "[$-491]mmmmmm dd yyyy h:mm AM/PM", "Am Màrt 19 2019 12:04 f"}, + {"44562.189571759256", "[$-6C1A]mmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D. 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-6C1A]mmmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D\u0443\u0430\u0440 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-6C1A]mmmmm dd yyyy h:mm AM/PM", "\u0458 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-6C1A]mmmmmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D\u0443\u0430\u0440 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-6C1A]mmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-6C1A]mmmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-6C1A]mmmmm dd yyyy h:mm AM/PM", "\u043C 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-6C1A]mmmmmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-1C1A]mmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-1C1A]mmmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D\u0443\u0430\u0440 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-1C1A]mmmmm dd yyyy h:mm AM/PM", "\u0458 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-1C1A]mmmmmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D\u0443\u0430\u0440 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-1C1A]mmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-1C1A]mmmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-1C1A]mmmmm dd yyyy h:mm AM/PM", "\u043C 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-1C1A]mmmmmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-301A]mmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D. 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-301A]mmmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D\u0443\u0430\u0440 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-301A]mmmmm dd yyyy h:mm AM/PM", "\u0458 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-301A]mmmmmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D\u0443\u0430\u0440 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-301A]mmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-301A]mmmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-301A]mmmmm dd yyyy h:mm AM/PM", "\u043C 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-301A]mmmmmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-281A]mmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D. 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-281A]mmmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D\u0443\u0430\u0440 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-281A]mmmmm dd yyyy h:mm AM/PM", "\u0458 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-281A]mmmmmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D\u0443\u0430\u0440 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-281A]mmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-281A]mmmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-281A]mmmmm dd yyyy h:mm AM/PM", "\u043C 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-281A]mmmmmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-C1A]mmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D. 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-C1A]mmmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D\u0443\u0430\u0440 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-C1A]mmmmm dd yyyy h:mm AM/PM", "\u0458 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-C1A]mmmmmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D\u0443\u0430\u0440 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-C1A]mmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-C1A]mmmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-C1A]mmmmm dd yyyy h:mm AM/PM", "\u043C 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-C1A]mmmmmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-701A]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 pre podne"}, + {"44562.189571759256", "[$-701A]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 pre podne"}, + {"44562.189571759256", "[$-701A]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 pre podne"}, + {"44562.189571759256", "[$-701A]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 pre podne"}, + {"43543.503206018519", "[$-701A]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 po podne"}, + {"43543.503206018519", "[$-701A]mmmm dd yyyy h:mm AM/PM", "mart 19 2019 12:04 po podne"}, + {"43543.503206018519", "[$-701A]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 po podne"}, + {"43543.503206018519", "[$-701A]mmmmmm dd yyyy h:mm AM/PM", "mart 19 2019 12:04 po podne"}, + {"44562.189571759256", "[$-7C1A]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 pre podne"}, + {"44562.189571759256", "[$-7C1A]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 pre podne"}, + {"44562.189571759256", "[$-7C1A]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 pre podne"}, + {"44562.189571759256", "[$-7C1A]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 pre podne"}, + {"43543.503206018519", "[$-7C1A]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 po podne"}, + {"43543.503206018519", "[$-7C1A]mmmm dd yyyy h:mm AM/PM", "mart 19 2019 12:04 po podne"}, + {"43543.503206018519", "[$-7C1A]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 po podne"}, + {"43543.503206018519", "[$-7C1A]mmmmmm dd yyyy h:mm AM/PM", "mart 19 2019 12:04 po podne"}, + {"44562.189571759256", "[$-181A]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 prije podne"}, + {"44562.189571759256", "[$-181A]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 prije podne"}, + {"44562.189571759256", "[$-181A]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 prije podne"}, + {"44562.189571759256", "[$-181A]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 prije podne"}, + {"43543.503206018519", "[$-181A]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 po podne"}, + {"43543.503206018519", "[$-181A]mmmm dd yyyy h:mm AM/PM", "mart 19 2019 12:04 po podne"}, + {"43543.503206018519", "[$-181A]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 po podne"}, + {"43543.503206018519", "[$-181A]mmmmmm dd yyyy h:mm AM/PM", "mart 19 2019 12:04 po podne"}, + {"44562.189571759256", "[$-2C1A]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 prije podne"}, + {"44562.189571759256", "[$-2C1A]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 prije podne"}, + {"44562.189571759256", "[$-2C1A]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 prije podne"}, + {"44562.189571759256", "[$-2C1A]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 prije podne"}, + {"43543.503206018519", "[$-2C1A]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 po podne"}, + {"43543.503206018519", "[$-2C1A]mmmm dd yyyy h:mm AM/PM", "mart 19 2019 12:04 po podne"}, + {"43543.503206018519", "[$-2C1A]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 po podne"}, + {"43543.503206018519", "[$-2C1A]mmmmmm dd yyyy h:mm AM/PM", "mart 19 2019 12:04 po podne"}, + {"44562.189571759256", "[$-241A]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 pre podne"}, + {"44562.189571759256", "[$-241A]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 pre podne"}, + {"44562.189571759256", "[$-241A]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 pre podne"}, + {"44562.189571759256", "[$-241A]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 pre podne"}, + {"43543.503206018519", "[$-241A]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 po podne"}, + {"43543.503206018519", "[$-241A]mmmm dd yyyy h:mm AM/PM", "mart 19 2019 12:04 po podne"}, + {"43543.503206018519", "[$-241A]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 po podne"}, + {"43543.503206018519", "[$-241A]mmmmmm dd yyyy h:mm AM/PM", "mart 19 2019 12:04 po podne"}, + {"44562.189571759256", "[$-81A]mmm dd yyyy h:mm AM/PM", "jan. 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-81A]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-81A]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-81A]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-81A]mmm dd yyyy h:mm AM/PM", "mart 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-81A]mmmm dd yyyy h:mm AM/PM", "mart 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-81A]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-81A]mmmmmm dd yyyy h:mm AM/PM", "mart 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-6C]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-6C]mmmm dd yyyy h:mm AM/PM", "Janaware 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-6C]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-6C]mmmmmm dd yyyy h:mm AM/PM", "Janaware 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-6C]mmm dd yyyy h:mm AM/PM", "Matš 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-6C]mmmm dd yyyy h:mm AM/PM", "Matšhe 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-6C]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-6C]mmmmmm dd yyyy h:mm AM/PM", "Matšhe 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-46C]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-46C]mmmm dd yyyy h:mm AM/PM", "Janaware 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-46C]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-46C]mmmmmm dd yyyy h:mm AM/PM", "Janaware 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-46C]mmm dd yyyy h:mm AM/PM", "Matš 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-46C]mmmm dd yyyy h:mm AM/PM", "Matšhe 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-46C]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-46C]mmmmmm dd yyyy h:mm AM/PM", "Matšhe 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-32]mmm dd yyyy h:mm AM/PM", "Fer. 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-32]mmmm dd yyyy h:mm AM/PM", "Ferikgong 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-32]mmmmm dd yyyy h:mm AM/PM", "F 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-32]mmmmmm dd yyyy h:mm AM/PM", "Ferikgong 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-32]mmm dd yyyy h:mm AM/PM", "Mop. 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-32]mmmm dd yyyy h:mm AM/PM", "Mopitlwe 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-32]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-32]mmmmmm dd yyyy h:mm AM/PM", "Mopitlwe 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-832]mmm dd yyyy h:mm AM/PM", "Fer. 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-832]mmmm dd yyyy h:mm AM/PM", "Ferikgong 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-832]mmmmm dd yyyy h:mm AM/PM", "F 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-832]mmmmmm dd yyyy h:mm AM/PM", "Ferikgong 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-832]mmm dd yyyy h:mm AM/PM", "Mop. 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-832]mmmm dd yyyy h:mm AM/PM", "Mopitlwe 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-832]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-832]mmmmmm dd yyyy h:mm AM/PM", "Mopitlwe 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-432]mmm dd yyyy h:mm AM/PM", "Fer. 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-432]mmmm dd yyyy h:mm AM/PM", "Ferikgong 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-432]mmmmm dd yyyy h:mm AM/PM", "F 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-432]mmmmmm dd yyyy h:mm AM/PM", "Ferikgong 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-432]mmm dd yyyy h:mm AM/PM", "Mop. 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-432]mmmm dd yyyy h:mm AM/PM", "Mopitlwe 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-432]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-432]mmmmmm dd yyyy h:mm AM/PM", "Mopitlwe 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-59]mmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u064A 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-59]mmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u064A 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-59]mmmmm dd yyyy h:mm AM/PM", "\u062C 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-59]mmmmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u064A 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-59]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-59]mmmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-59]mmmmm dd yyyy h:mm AM/PM", "\u0645 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-59]mmmmmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-7C59]mmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u064A 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-7C59]mmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u064A 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-7C59]mmmmm dd yyyy h:mm AM/PM", "\u062C 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-7C59]mmmmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u064A 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-7C59]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-7C59]mmmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-7C59]mmmmm dd yyyy h:mm AM/PM", "\u0645 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-7C59]mmmmmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-859]mmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u064A 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-859]mmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u064A 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-859]mmmmm dd yyyy h:mm AM/PM", "\u062C 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-859]mmmmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u064A 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-859]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-859]mmmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-859]mmmmm dd yyyy h:mm AM/PM", "\u0645 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-859]mmmmmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-5B]mmm dd yyyy h:mm AM/PM", "\u0DA2\u0DB1. 01 2022 4:32 \u0DB4\u0DD9.\u0DC0."}, + {"44562.189571759256", "[$-5B]mmmm dd yyyy h:mm AM/PM", "\u0DA2\u0DB1\u0DC0\u0DCF\u0DBB\u0DD2 01 2022 4:32 \u0DB4\u0DD9.\u0DC0."}, + {"44562.189571759256", "[$-5B]mmmmm dd yyyy h:mm AM/PM", "\u0DA2 01 2022 4:32 \u0DB4\u0DD9.\u0DC0."}, + {"44562.189571759256", "[$-5B]mmmmmm dd yyyy h:mm AM/PM", "\u0DA2\u0DB1\u0DC0\u0DCF\u0DBB\u0DD2 01 2022 4:32 \u0DB4\u0DD9.\u0DC0."}, + {"43543.503206018519", "[$-5B]mmm dd yyyy h:mm AM/PM", "\u0DB8\u0DCF\u0DBB\u0DCA\u0DAD\u0DD4. 19 2019 12:04 \u0DB4.\u0DC0."}, + {"43543.503206018519", "[$-5B]mmmm dd yyyy h:mm AM/PM", "\u0DB8\u0DCF\u0DBB\u0DCA\u0DAD\u0DD4 19 2019 12:04 \u0DB4.\u0DC0."}, + {"43543.503206018519", "[$-5B]mmmmm dd yyyy h:mm AM/PM", "\u0DB8 19 2019 12:04 \u0DB4.\u0DC0."}, + {"43543.503206018519", "[$-5B]mmmmmm dd yyyy h:mm AM/PM", "\u0DB8\u0DCF\u0DBB\u0DCA\u0DAD\u0DD4 19 2019 12:04 \u0DB4.\u0DC0."}, + + {"44562.189571759256", "[$-45B]mmm dd yyyy h:mm AM/PM", "\u0DA2\u0DB1. 01 2022 4:32 \u0DB4\u0DD9.\u0DC0."}, + {"44562.189571759256", "[$-45B]mmmm dd yyyy h:mm AM/PM", "\u0DA2\u0DB1\u0DC0\u0DCF\u0DBB\u0DD2 01 2022 4:32 \u0DB4\u0DD9.\u0DC0."}, + {"44562.189571759256", "[$-45B]mmmmm dd yyyy h:mm AM/PM", "\u0DA2 01 2022 4:32 \u0DB4\u0DD9.\u0DC0."}, + {"44562.189571759256", "[$-45B]mmmmmm dd yyyy h:mm AM/PM", "\u0DA2\u0DB1\u0DC0\u0DCF\u0DBB\u0DD2 01 2022 4:32 \u0DB4\u0DD9.\u0DC0."}, + {"43543.503206018519", "[$-45B]mmm dd yyyy h:mm AM/PM", "\u0DB8\u0DCF\u0DBB\u0DCA\u0DAD\u0DD4. 19 2019 12:04 \u0DB4.\u0DC0."}, + {"43543.503206018519", "[$-45B]mmmm dd yyyy h:mm AM/PM", "\u0DB8\u0DCF\u0DBB\u0DCA\u0DAD\u0DD4 19 2019 12:04 \u0DB4.\u0DC0."}, + {"43543.503206018519", "[$-45B]mmmmm dd yyyy h:mm AM/PM", "\u0DB8 19 2019 12:04 \u0DB4.\u0DC0."}, + {"43543.503206018519", "[$-45B]mmmmmm dd yyyy h:mm AM/PM", "\u0DB8\u0DCF\u0DBB\u0DCA\u0DAD\u0DD4 19 2019 12:04 \u0DB4.\u0DC0."}, + {"44562.189571759256", "[$-1B]mmm dd yyyy h:mm AM/PM", "1 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-1B]mmmm dd yyyy h:mm AM/PM", "január 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-1B]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-1B]mmmmmm dd yyyy h:mm AM/PM", "január 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-1B]mmm dd yyyy h:mm AM/PM", "3 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-1B]mmmm dd yyyy h:mm AM/PM", "marec 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-1B]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-1B]mmmmmm dd yyyy h:mm AM/PM", "marec 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-41B]mmm dd yyyy h:mm AM/PM", "1 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-41B]mmmm dd yyyy h:mm AM/PM", "január 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-41B]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-41B]mmmmmm dd yyyy h:mm AM/PM", "január 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-41B]mmm dd yyyy h:mm AM/PM", "3 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-41B]mmmm dd yyyy h:mm AM/PM", "marec 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-41B]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-41B]mmmmmm dd yyyy h:mm AM/PM", "marec 19 2019 12:04 PM"}, + {"44562.189571759256", "[$-24]mmm dd yyyy h:mm AM/PM", "jan. 01 2022 4:32 dop."}, + {"44562.189571759256", "[$-24]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 dop."}, + {"44562.189571759256", "[$-24]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 dop."}, + {"44562.189571759256", "[$-24]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 dop."}, + {"43543.503206018519", "[$-24]mmm dd yyyy h:mm AM/PM", "mar. 19 2019 12:04 pop."}, + {"43543.503206018519", "[$-24]mmmm dd yyyy h:mm AM/PM", "marec 19 2019 12:04 pop."}, + {"43543.503206018519", "[$-24]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 pop."}, + {"43543.503206018519", "[$-24]mmmmmm dd yyyy h:mm AM/PM", "marec 19 2019 12:04 pop."}, + {"44562.189571759256", "[$-424]mmm dd yyyy h:mm AM/PM", "jan. 01 2022 4:32 dop."}, + {"44562.189571759256", "[$-424]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 dop."}, + {"44562.189571759256", "[$-424]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 dop."}, + {"44562.189571759256", "[$-424]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 dop."}, + {"43543.503206018519", "[$-424]mmm dd yyyy h:mm AM/PM", "mar. 19 2019 12:04 pop."}, + {"43543.503206018519", "[$-424]mmmm dd yyyy h:mm AM/PM", "marec 19 2019 12:04 pop."}, + {"43543.503206018519", "[$-424]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 pop."}, + {"43543.503206018519", "[$-424]mmmmmm dd yyyy h:mm AM/PM", "marec 19 2019 12:04 pop."}, + {"44562.189571759256", "[$-77]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 GH"}, + {"44562.189571759256", "[$-77]mmmm dd yyyy h:mm AM/PM", "Jannaayo 01 2022 4:32 GH"}, + {"44562.189571759256", "[$-77]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 GH"}, + {"44562.189571759256", "[$-77]mmmmmm dd yyyy h:mm AM/PM", "Jannaayo 01 2022 4:32 GH"}, + {"43543.503206018519", "[$-77]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 GD"}, + {"43543.503206018519", "[$-77]mmmm dd yyyy h:mm AM/PM", "Maarso 19 2019 12:04 GD"}, + {"43543.503206018519", "[$-77]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 GD"}, + {"43543.503206018519", "[$-77]mmmmmm dd yyyy h:mm AM/PM", "Maarso 19 2019 12:04 GD"}, + {"44562.189571759256", "[$-477]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 GH"}, + {"44562.189571759256", "[$-477]mmmm dd yyyy h:mm AM/PM", "Jannaayo 01 2022 4:32 GH"}, + {"44562.189571759256", "[$-477]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 GH"}, + {"44562.189571759256", "[$-477]mmmmmm dd yyyy h:mm AM/PM", "Jannaayo 01 2022 4:32 GH"}, + {"43543.503206018519", "[$-477]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 GD"}, + {"43543.503206018519", "[$-477]mmmm dd yyyy h:mm AM/PM", "Maarso 19 2019 12:04 GD"}, + {"43543.503206018519", "[$-477]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 GD"}, + {"43543.503206018519", "[$-477]mmmmmm dd yyyy h:mm AM/PM", "Maarso 19 2019 12:04 GD"}, {"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."}, diff --git a/picture.go b/picture.go index 802f515a9a..1a9eeeb652 100644 --- a/picture.go +++ b/picture.go @@ -639,11 +639,11 @@ func (f *File) DeletePicture(sheet, cell string) error { // embed in spreadsheet by given coordinates and drawing relationships. func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) (pics []Picture, err error) { var ( - wsDr *xlsxWsDr - ok bool - deWsDr *decodeWsDr - drawRel *xlsxRelationship - deTwoCellAnchor *decodeTwoCellAnchor + ok bool + deWsDr *decodeWsDr + deCellAnchor *decodeCellAnchor + drawRel *xlsxRelationship + wsDr *xlsxWsDr ) if wsDr, _, err = f.drawingParser(drawingXML); err != nil { @@ -658,26 +658,32 @@ func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) return } err = nil - for _, anchor := range deWsDr.TwoCellAnchor { - deTwoCellAnchor = new(decodeTwoCellAnchor) - if err = f.xmlNewDecoder(strings.NewReader("" + anchor.Content + "")). - Decode(deTwoCellAnchor); err != nil && err != io.EOF { + extractAnchor := func(anchor *decodeCellAnchor) { + deCellAnchor = new(decodeCellAnchor) + if err := f.xmlNewDecoder(strings.NewReader("" + anchor.Content + "")). + Decode(deCellAnchor); err != nil && err != io.EOF { return } - 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 err = nil; deCellAnchor.From != nil && deCellAnchor.Pic != nil { + if deCellAnchor.From.Col == col && deCellAnchor.From.Row == row { + drawRel = f.getDrawingRelationships(drawingRelationships, deCellAnchor.Pic.BlipFill.Blip.Embed) 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) - pic.Format.AltText = deTwoCellAnchor.Pic.NvPicPr.CNvPr.Descr + pic.Format.AltText = deCellAnchor.Pic.NvPicPr.CNvPr.Descr pics = append(pics, pic) } } } } } + for _, anchor := range deWsDr.TwoCellAnchor { + extractAnchor(anchor) + } + for _, anchor := range deWsDr.OneCellAnchor { + extractAnchor(anchor) + } return } diff --git a/picture_test.go b/picture_test.go index c3c0d6e0d9..7eb21cbf5c 100644 --- a/picture_test.go +++ b/picture_test.go @@ -169,6 +169,14 @@ func TestGetPicture(t *testing.T) { assert.Len(t, pics, 0) assert.NoError(t, f.Close()) + // Try to get picture with one cell anchor + f, err = OpenFile(filepath.Join("test", "TestGetPicture.xlsx")) + assert.NoError(t, err) + f.Pkg.Store("xl/drawings/drawing2.xml", []byte(`10151322`)) + pics, err = f.GetPictures("Sheet2", "K16") + assert.NoError(t, err) + assert.Len(t, pics, 1) + // Test get picture from none drawing worksheet f = NewFile() pics, err = f.GetPictures("Sheet1", "F22") diff --git a/xmlDecodeDrawing.go b/xmlDecodeDrawing.go index c737ac08f6..a8b39d5c24 100644 --- a/xmlDecodeDrawing.go +++ b/xmlDecodeDrawing.go @@ -22,6 +22,7 @@ type decodeCellAnchor struct { From *decodeFrom `xml:"from"` To *decodeTo `xml:"to"` Sp *decodeSp `xml:"sp"` + Pic *decodePic `xml:"pic"` ClientData *decodeClientData `xml:"clientData"` Content string `xml:",innerxml"` } @@ -72,17 +73,6 @@ type decodeWsDr struct { TwoCellAnchor []*decodeCellAnchor `xml:"twoCellAnchor,omitempty"` } -// decodeTwoCellAnchor directly maps the oneCellAnchor (One Cell Anchor Shape -// Size) and twoCellAnchor (Two Cell Anchor Shape Size). This element -// specifies a two cell anchor placeholder for a group, a shape, or a drawing -// element. It moves with cells and its extents are in EMU units. -type decodeTwoCellAnchor struct { - From *decodeFrom `xml:"from"` - To *decodeTo `xml:"to"` - Pic *decodePic `xml:"pic"` - ClientData *decodeClientData `xml:"clientData"` -} - // decodeCNvPr 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 From a1810aa056ad9158c8054cb6c6f216d4ad8674b8 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 15 Aug 2023 00:01:57 +0800 Subject: [PATCH 778/957] This improves the date and time number formats - Now support applying date and time number format for 812 language tags - Fix panic on getting merged cells with the same start and end axis --- merge.go | 6 +- merge_test.go | 7 + numfmt.go | 3466 +++++++++++++++++++++++++++++++++++++++++++----- numfmt_test.go | 2616 ++++++++++++++++++++++++------------ 4 files changed, 4932 insertions(+), 1163 deletions(-) diff --git a/merge.go b/merge.go index af4e6420c0..48783d1e2c 100644 --- a/merge.go +++ b/merge.go @@ -289,5 +289,9 @@ func (m *MergeCell) GetStartAxis() string { // GetEndAxis returns the bottom right cell reference of merged range, for // example: "D4". func (m *MergeCell) GetEndAxis() string { - return strings.Split((*m)[0], ":")[1] + coordinates := strings.Split((*m)[0], ":") + if len(coordinates) == 2 { + return coordinates[1] + } + return coordinates[0] } diff --git a/merge_test.go b/merge_test.go index 18fa0f9372..fcdbcfd647 100644 --- a/merge_test.go +++ b/merge_test.go @@ -80,6 +80,13 @@ func TestMergeCell(t *testing.T) { assert.True(t, ok) ws.(*xlsxWorksheet).MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{nil, nil}} assert.NoError(t, f.MergeCell("Sheet1", "A2", "B3")) + // Test getting merged cells with the same start and end axis + ws.(*xlsxWorksheet).MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A1"}}} + mergedCells, err := f.GetMergeCells("Sheet1") + assert.NoError(t, err) + assert.Equal(t, "A1", mergedCells[0].GetStartAxis()) + assert.Equal(t, "A1", mergedCells[0].GetEndAxis()) + assert.Empty(t, mergedCells[0].GetCellValue()) } func TestMergeCellOverlap(t *testing.T) { diff --git a/numfmt.go b/numfmt.go index abeb7e6423..6a1a0ab4dc 100644 --- a/numfmt.go +++ b/numfmt.go @@ -24,10 +24,10 @@ import ( // languageInfo defined the required fields of localization support for number // format. type languageInfo struct { - apFmt string - tags []string - useGannen bool - localMonth func(t time.Time, abbr int) string + apFmt string + tags, weekdayNames, weekdayNamesAbbr []string + useGannen bool + localMonth func(t time.Time, abbr int) string } // numberFormat directly maps the number format parser runtime required @@ -713,17 +713,77 @@ var ( } // supportedLanguageInfo directly maps the supported language ID and tags. supportedLanguageInfo = map[string]languageInfo{ - "36": {tags: []string{"af"}, localMonth: localMonthsNameAfrikaans, apFmt: apFmtAfrikaans}, - "445": {tags: []string{"bn-IN"}, localMonth: localMonthsNameBangla, apFmt: nfp.AmPm[0]}, - "4": {tags: []string{"zh-Hans"}, localMonth: localMonthsNameChinese1, apFmt: nfp.AmPm[2]}, - "7804": {tags: []string{"zh"}, localMonth: localMonthsNameChinese1, apFmt: nfp.AmPm[2]}, - "804": {tags: []string{"zh-CN"}, localMonth: localMonthsNameChinese1, apFmt: nfp.AmPm[2]}, - "1004": {tags: []string{"zh-SG"}, localMonth: localMonthsNameChinese2, apFmt: nfp.AmPm[2]}, - "7C04": {tags: []string{"zh-Hant"}, localMonth: localMonthsNameChinese3, apFmt: nfp.AmPm[2]}, - "C04": {tags: []string{"zh-HK"}, localMonth: localMonthsNameChinese2, apFmt: nfp.AmPm[2]}, - "1404": {tags: []string{"zh-MO"}, localMonth: localMonthsNameChinese3, apFmt: nfp.AmPm[2]}, - "404": {tags: []string{"zh-TW"}, localMonth: localMonthsNameChinese3, apFmt: nfp.AmPm[2]}, - "9": {tags: []string{"en"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, + "36": {tags: []string{"af"}, localMonth: localMonthsNameAfrikaans, apFmt: apFmtAfrikaans, weekdayNames: weekdayNamesAfrikaans, weekdayNamesAbbr: weekdayNamesAfrikaansAbbr}, + "436": {tags: []string{"af-ZA"}, localMonth: localMonthsNameAfrikaans, apFmt: apFmtAfrikaans, weekdayNames: weekdayNamesAfrikaans, weekdayNamesAbbr: weekdayNamesAfrikaansAbbr}, + "1C": {tags: []string{"sq"}, localMonth: localMonthsNameAlbanian, apFmt: apFmtAlbanian, weekdayNames: weekdayNamesAlbanian, weekdayNamesAbbr: weekdayNamesAlbanianAbbr}, + "41C": {tags: []string{"sq-AL"}, localMonth: localMonthsNameAlbanian, apFmt: apFmtAlbanian, weekdayNames: weekdayNamesAlbanian, weekdayNamesAbbr: weekdayNamesAlbanianAbbr}, + "84": {tags: []string{"gsw"}, localMonth: localMonthsNameAlsatian, apFmt: apFmtAlsatian, weekdayNames: weekdayNamesAlsatian, weekdayNamesAbbr: weekdayNamesAlsatianAbbr}, + "484": {tags: []string{"gsw-FR"}, localMonth: localMonthsNameAlsatianFrance, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesAlsatianFrance, weekdayNamesAbbr: weekdayNamesAlsatianFranceAbbr}, + "5E": {tags: []string{"am"}, localMonth: localMonthsNameAmharic, apFmt: apFmtAmharic, weekdayNames: weekdayNamesAmharic, weekdayNamesAbbr: weekdayNamesAmharicAbbr}, + "45E": {tags: []string{"am-ET"}, localMonth: localMonthsNameAmharic, apFmt: apFmtAmharic, weekdayNames: weekdayNamesAmharic, weekdayNamesAbbr: weekdayNamesAmharicAbbr}, + "1": {tags: []string{"ar"}, localMonth: localMonthsNameArabic, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr}, + "1401": {tags: []string{"ar-DZ"}, localMonth: localMonthsNameArabic, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr}, + "3C01": {tags: []string{"ar-BH"}, localMonth: localMonthsNameArabic, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr}, + "C01": {tags: []string{"ar-EG"}, localMonth: localMonthsNameArabic, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr}, + "801": {tags: []string{"ar-IQ"}, localMonth: localMonthsNameArabicIraq, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr}, + "2C01": {tags: []string{"ar-JO"}, localMonth: localMonthsNameArabicIraq, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr}, + "3401": {tags: []string{"ar-KW"}, localMonth: localMonthsNameArabic, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr}, + "3001": {tags: []string{"ar-LB"}, localMonth: localMonthsNameArabicIraq, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr}, + "1801": {tags: []string{"ar-MA"}, localMonth: localMonthsNameArabic, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr}, + "2001": {tags: []string{"ar-OM"}, localMonth: localMonthsNameArabic, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr}, + "4001": {tags: []string{"ar-QA"}, localMonth: localMonthsNameArabic, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr}, + "401": {tags: []string{"ar-SA"}, localMonth: localMonthsNameArabic, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr}, + "2801": {tags: []string{"ar-SY"}, localMonth: localMonthsNameArabicIraq, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr}, + "1C01": {tags: []string{"ar-TN"}, localMonth: localMonthsNameArabic, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr}, + "3801": {tags: []string{"ar-AE"}, localMonth: localMonthsNameArabic, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr}, + "2401": {tags: []string{"ar-YE"}, localMonth: localMonthsNameArabic, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr}, + "2B": {tags: []string{"hy"}, localMonth: localMonthsNameArmenian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesArmenian, weekdayNamesAbbr: weekdayNamesArmenianAbbr}, + "42B": {tags: []string{"hy-AM"}, localMonth: localMonthsNameArmenian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesArmenian, weekdayNamesAbbr: weekdayNamesArmenianAbbr}, + "4D": {tags: []string{"as"}, localMonth: localMonthsNameAssamese, apFmt: apFmtAssamese, weekdayNames: weekdayNamesAssamese, weekdayNamesAbbr: weekdayNamesAssameseAbbr}, + "44D": {tags: []string{"as-IN"}, localMonth: localMonthsNameAssamese, apFmt: apFmtAssamese, weekdayNames: weekdayNamesAssamese, weekdayNamesAbbr: weekdayNamesAssameseAbbr}, + "742C": {tags: []string{"az-Cyrl"}, localMonth: localMonthsNameAzerbaijaniCyrillic, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesAzerbaijaniCyrillic, weekdayNamesAbbr: weekdayNamesAzerbaijaniCyrillicAbbr}, + "82C": {tags: []string{"az-Cyrl-AZ"}, localMonth: localMonthsNameAzerbaijaniCyrillic, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesAzerbaijaniCyrillic, weekdayNamesAbbr: weekdayNamesAzerbaijaniCyrillicAbbr}, + "2C": {tags: []string{"az"}, localMonth: localMonthsNameAzerbaijani, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesAzerbaijani, weekdayNamesAbbr: weekdayNamesAzerbaijaniAbbr}, + "782C": {tags: []string{"az-Latn"}, localMonth: localMonthsNameAzerbaijani, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesAzerbaijani, weekdayNamesAbbr: weekdayNamesAzerbaijaniAbbr}, + "42C": {tags: []string{"az-Latn-AZ"}, localMonth: localMonthsNameAzerbaijani, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesAzerbaijani, weekdayNamesAbbr: weekdayNamesAzerbaijaniAbbr}, + "45": {tags: []string{"bn"}, localMonth: localMonthsNameBangla, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBangla, weekdayNamesAbbr: weekdayNamesBanglaAbbr}, + "845": {tags: []string{"bn-BD"}, localMonth: localMonthsNameBangla, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBangla, weekdayNamesAbbr: weekdayNamesBanglaAbbr}, + "445": {tags: []string{"bn-IN"}, localMonth: localMonthsNameBangla, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBangla, weekdayNamesAbbr: weekdayNamesBanglaAbbr}, + "6D": {tags: []string{"ba"}, localMonth: localMonthsNameBashkir, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBashkir, weekdayNamesAbbr: weekdayNamesBashkirAbbr}, + "46D": {tags: []string{"ba-RU"}, localMonth: localMonthsNameBashkir, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBashkir, weekdayNamesAbbr: weekdayNamesBashkirAbbr}, + "2D": {tags: []string{"eu"}, localMonth: localMonthsNameBasque, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBasque, weekdayNamesAbbr: weekdayNamesBasqueAbbr}, + "42D": {tags: []string{"eu-ES"}, localMonth: localMonthsNameBasque, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBasque, weekdayNamesAbbr: weekdayNamesBasqueAbbr}, + "23": {tags: []string{"be"}, localMonth: localMonthsNameBelarusian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBelarusian, weekdayNamesAbbr: weekdayNamesBelarusianAbbr}, + "423": {tags: []string{"be-BY"}, localMonth: localMonthsNameBelarusian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBelarusian, weekdayNamesAbbr: weekdayNamesBelarusianAbbr}, + "641A": {tags: []string{"bs-Cyrl"}, localMonth: localMonthsNameBosnianCyrillic, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBosnianCyrillic, weekdayNamesAbbr: weekdayNamesBosnianCyrillicAbbr}, + "201A": {tags: []string{"bs-Cyrl-BA"}, localMonth: localMonthsNameBosnianCyrillic, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBosnianCyrillic, weekdayNamesAbbr: weekdayNamesBosnianCyrillicAbbr}, + "681A": {tags: []string{"bs-Latn"}, localMonth: localMonthsNameBosnian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBosnian, weekdayNamesAbbr: weekdayNamesBosnianAbbr}, + "781A": {tags: []string{"bs"}, localMonth: localMonthsNameBosnian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBosnian, weekdayNamesAbbr: weekdayNamesBosnianAbbr}, + "141A": {tags: []string{"bs-Latn-BA"}, localMonth: localMonthsNameBosnian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBosnian, weekdayNamesAbbr: weekdayNamesBosnianAbbr}, + "7E": {tags: []string{"br"}, localMonth: localMonthsNameBreton, apFmt: apFmtBreton, weekdayNames: weekdayNamesBreton, weekdayNamesAbbr: weekdayNamesBretonAbbr}, + "47E": {tags: []string{"br-FR"}, localMonth: localMonthsNameBreton, apFmt: apFmtBreton, weekdayNames: weekdayNamesBreton, weekdayNamesAbbr: weekdayNamesBretonAbbr}, + "2": {tags: []string{"bg"}, localMonth: localMonthsNameBulgarian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBulgarian, weekdayNamesAbbr: weekdayNamesBulgarianAbbr}, + "402": {tags: []string{"bg-BG"}, localMonth: localMonthsNameBulgarian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBulgarian, weekdayNamesAbbr: weekdayNamesBulgarianAbbr}, + "55": {tags: []string{"my"}, localMonth: localMonthsNameBurmese, apFmt: apFmtBurmese, weekdayNames: weekdayNamesBurmese, weekdayNamesAbbr: weekdayNamesBurmese}, + "455": {tags: []string{"my-MM"}, localMonth: localMonthsNameBurmese, apFmt: apFmtBurmese, weekdayNames: weekdayNamesBurmese, weekdayNamesAbbr: weekdayNamesBurmese}, + "3": {tags: []string{"ca"}, localMonth: localMonthsNameValencian, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesValencian, weekdayNamesAbbr: weekdayNamesValencianAbbr}, + "403": {tags: []string{"ca-ES"}, localMonth: localMonthsNameValencian, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesValencian, weekdayNamesAbbr: weekdayNamesValencianAbbr}, + "45F": {tags: []string{"tzm-Arab-MA"}, localMonth: localMonthsNameArabicIraq, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr}, + "92": {tags: []string{"ku"}, localMonth: localMonthsNameCentralKurdish, apFmt: apFmtCentralKurdish, weekdayNames: weekdayNamesCentralKurdish, weekdayNamesAbbr: weekdayNamesCentralKurdish}, + "7C92": {tags: []string{"ku-Arab"}, localMonth: localMonthsNameCentralKurdish, apFmt: apFmtCentralKurdish, weekdayNames: weekdayNamesCentralKurdish, weekdayNamesAbbr: weekdayNamesCentralKurdish}, + "492": {tags: []string{"ku-Arab-IQ"}, localMonth: localMonthsNameCentralKurdish, apFmt: apFmtCentralKurdish, weekdayNames: weekdayNamesCentralKurdish, weekdayNamesAbbr: weekdayNamesCentralKurdish}, + "5C": {tags: []string{"chr"}, localMonth: localMonthsNameCherokee, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesCherokee, weekdayNamesAbbr: weekdayNamesCherokeeAbbr}, + "7C5C": {tags: []string{"chr-Cher"}, localMonth: localMonthsNameCherokee, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesCherokee, weekdayNamesAbbr: weekdayNamesCherokeeAbbr}, + "45C": {tags: []string{"chr-Cher-US"}, localMonth: localMonthsNameCherokee, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesCherokee, weekdayNamesAbbr: weekdayNamesCherokeeAbbr}, + "4": {tags: []string{"zh-Hans"}, localMonth: localMonthsNameChinese1, apFmt: nfp.AmPm[2], weekdayNames: weekdayNamesChinese, weekdayNamesAbbr: weekdayNamesChineseAbbr2}, + "7804": {tags: []string{"zh"}, localMonth: localMonthsNameChinese1, apFmt: nfp.AmPm[2], weekdayNames: weekdayNamesChinese, weekdayNamesAbbr: weekdayNamesChineseAbbr2}, + "804": {tags: []string{"zh-CN"}, localMonth: localMonthsNameChinese1, apFmt: nfp.AmPm[2], weekdayNames: weekdayNamesChinese, weekdayNamesAbbr: weekdayNamesChineseAbbr}, + "1004": {tags: []string{"zh-SG"}, localMonth: localMonthsNameChinese2, apFmt: nfp.AmPm[2], weekdayNames: weekdayNamesChinese, weekdayNamesAbbr: weekdayNamesChineseAbbr}, + "7C04": {tags: []string{"zh-Hant"}, localMonth: localMonthsNameChinese3, apFmt: nfp.AmPm[2], weekdayNames: weekdayNamesChinese, weekdayNamesAbbr: weekdayNamesChineseAbbr2}, + "C04": {tags: []string{"zh-HK"}, localMonth: localMonthsNameChinese2, apFmt: nfp.AmPm[2], weekdayNames: weekdayNamesChinese, weekdayNamesAbbr: weekdayNamesChineseAbbr2}, + "1404": {tags: []string{"zh-MO"}, localMonth: localMonthsNameChinese3, apFmt: nfp.AmPm[2], weekdayNames: weekdayNamesChinese, weekdayNamesAbbr: weekdayNamesChineseAbbr2}, + "404": {tags: []string{"zh-TW"}, localMonth: localMonthsNameChinese3, apFmt: nfp.AmPm[2], weekdayNames: weekdayNamesChinese, weekdayNamesAbbr: weekdayNamesChineseAbbr2}, + "9": {tags: []string{"en"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, "1000": {tags: []string{ "aa", "aa-DJ", "aa-ER", "aa-ER", "aa-NA", "agq", "agq-CM", "ak", "ak-GH", "sq-ML", "gsw-LI", "gsw-CH", "ar-TD", "ar-KM", "ar-DJ", "ar-ER", "ar-IL", "ar-MR", "ar-PS", @@ -768,274 +828,334 @@ var ( "tig-ER", "to", "to-TO", "tr-CY", "uz-Arab", "us-Arab-AF", "vai", "vai-Vaii", "vai-Vaii-LR", "vai-Latn-LR", "vai-Latn", "vo", "vo-001", "vun", "vun-TZ", "wae", "wae-CH", "wal", "wae-ET", "yav", "yav-CM", "yo-BJ", "dje", "dje-NE", - }, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, - "C09": {tags: []string{"en-AU"}, localMonth: localMonthsNameEnglish, apFmt: strings.ToLower(nfp.AmPm[0])}, - "2809": {tags: []string{"en-BZ"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, - "1009": {tags: []string{"en-CA"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, - "2409": {tags: []string{"en-029"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, - "3C09": {tags: []string{"en-HK"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, - "4009": {tags: []string{"en-IN"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, - "1809": {tags: []string{"en-IE"}, localMonth: localMonthsNameEnglish, apFmt: strings.ToLower(nfp.AmPm[0])}, - "2009": {tags: []string{"en-JM"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, - "4409": {tags: []string{"en-MY"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, - "1409": {tags: []string{"en-NZ"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, - "3409": {tags: []string{"en-PH"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, - "4809": {tags: []string{"en-SG"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, - "1C09": {tags: []string{"en-ZA"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, - "2C09": {tags: []string{"en-TT"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, - "4C09": {tags: []string{"en-AE"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, - "809": {tags: []string{"en-GB"}, localMonth: localMonthsNameEnglish, apFmt: strings.ToLower(nfp.AmPm[0])}, - "409": {tags: []string{"en-US"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, - "3009": {tags: []string{"en-ZW"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, - "25": {tags: []string{"et"}, localMonth: localMonthsNameEstonian, apFmt: nfp.AmPm[0]}, - "425": {tags: []string{"et-EE"}, localMonth: localMonthsNameEstonian, apFmt: nfp.AmPm[0]}, - "38": {tags: []string{"fo"}, localMonth: localMonthsNameFaroese, apFmt: apFmtFaroese}, - "438": {tags: []string{"fo-FO"}, localMonth: localMonthsNameFaroese, apFmt: apFmtFaroese}, - "64": {tags: []string{"fil"}, localMonth: localMonthsNameFilipino, apFmt: nfp.AmPm[0]}, - "464": {tags: []string{"fil-PH"}, localMonth: localMonthsNameFilipino, apFmt: nfp.AmPm[0]}, - "B": {tags: []string{"fi"}, localMonth: localMonthsNameFinnish, apFmt: apFmtFinnish}, - "40B": {tags: []string{"fi-FI"}, localMonth: localMonthsNameFinnish, apFmt: apFmtFinnish}, - "C": {tags: []string{"fr"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0]}, - "80C": {tags: []string{"fr-BE"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0]}, - "2C0C": {tags: []string{"fr-CM"}, localMonth: localMonthsNameFrench, apFmt: apFmtCameroon}, - "C0C": {tags: []string{"fr-CA"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0]}, - "1C0C": {tags: []string{"fr-029"}, localMonth: localMonthsNameCaribbean, apFmt: nfp.AmPm[0]}, - "240C": {tags: []string{"fr-CD"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0]}, - "300C": {tags: []string{"fr-CI"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0]}, - "40C": {tags: []string{"fr-FR"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0]}, - "3C0C": {tags: []string{"fr-HT"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0]}, - "140C": {tags: []string{"fr-LU"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0]}, - "340C": {tags: []string{"fr-ML"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0]}, - "380C": {tags: []string{"fr-MA"}, localMonth: localMonthsNameMorocco, apFmt: nfp.AmPm[0]}, - "180C": {tags: []string{"fr-MC"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0]}, - "200C": {tags: []string{"fr-RE"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0]}, - "280C": {tags: []string{"fr-SN"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0]}, - "62": {tags: []string{"fy"}, localMonth: localMonthsNameFrisian, apFmt: nfp.AmPm[0]}, - "462": {tags: []string{"fy-NL"}, localMonth: localMonthsNameFrisian, apFmt: nfp.AmPm[0]}, - "67": {tags: []string{"ff"}, localMonth: localMonthsNameFulah, apFmt: nfp.AmPm[0]}, - "7C67": {tags: []string{"ff-Latn"}, localMonth: localMonthsNameFulah, apFmt: nfp.AmPm[0]}, - "467": {tags: []string{"ff-NG", "ff-Latn-NG"}, localMonth: localMonthsNameNigeria, apFmt: apFmtNigeria}, - "867": {tags: []string{"ff-SN"}, localMonth: localMonthsNameNigeria, apFmt: nfp.AmPm[0]}, - "56": {tags: []string{"gl"}, localMonth: localMonthsNameGalician, apFmt: apFmtCuba}, - "456": {tags: []string{"gl-ES"}, localMonth: localMonthsNameGalician, apFmt: apFmtCuba}, - "37": {tags: []string{"ka"}, localMonth: localMonthsNameGeorgian, apFmt: nfp.AmPm[0]}, - "437": {tags: []string{"ka-GE"}, localMonth: localMonthsNameGeorgian, apFmt: nfp.AmPm[0]}, - "7": {tags: []string{"de"}, localMonth: localMonthsNameGerman, apFmt: nfp.AmPm[0]}, - "C07": {tags: []string{"de-AT"}, localMonth: localMonthsNameAustria, apFmt: nfp.AmPm[0]}, - "407": {tags: []string{"de-DE"}, localMonth: localMonthsNameGerman, apFmt: nfp.AmPm[0]}, - "1407": {tags: []string{"de-LI"}, localMonth: localMonthsNameGerman, apFmt: nfp.AmPm[0]}, - "807": {tags: []string{"de-CH"}, localMonth: localMonthsNameGerman, apFmt: nfp.AmPm[0]}, - "8": {tags: []string{"el"}, localMonth: localMonthsNameGreek, apFmt: apFmtGreek}, - "408": {tags: []string{"el-GR"}, localMonth: localMonthsNameGreek, apFmt: apFmtGreek}, - "6F": {tags: []string{"kl"}, localMonth: localMonthsNameGreenlandic, apFmt: nfp.AmPm[0]}, - "46F": {tags: []string{"kl-GL"}, localMonth: localMonthsNameGreenlandic, apFmt: nfp.AmPm[0]}, - "74": {tags: []string{"gn"}, localMonth: localMonthsNameGuarani, apFmt: apFmtCuba}, - "474": {tags: []string{"gn-PY"}, localMonth: localMonthsNameGuarani, apFmt: apFmtCuba}, - "47": {tags: []string{"gu"}, localMonth: localMonthsNameGujarati, apFmt: apFmtGujarati}, - "447": {tags: []string{"gu-IN"}, localMonth: localMonthsNameGujarati, apFmt: apFmtGujarati}, - "68": {tags: []string{"ha"}, localMonth: localMonthsNameHausa, apFmt: nfp.AmPm[0]}, - "7C68": {tags: []string{"ha-Latn"}, localMonth: localMonthsNameHausa, apFmt: nfp.AmPm[0]}, - "468": {tags: []string{"ha-Latn-NG"}, localMonth: localMonthsNameHausa, apFmt: nfp.AmPm[0]}, - "75": {tags: []string{"haw"}, localMonth: localMonthsNameHawaiian, apFmt: nfp.AmPm[0]}, - "475": {tags: []string{"haw-US"}, localMonth: localMonthsNameHawaiian, apFmt: nfp.AmPm[0]}, - "D": {tags: []string{"he"}, localMonth: localMonthsNameHebrew, apFmt: nfp.AmPm[0]}, - "40D": {tags: []string{"he-IL"}, localMonth: localMonthsNameHebrew, apFmt: nfp.AmPm[0]}, - "39": {tags: []string{"hi"}, localMonth: localMonthsNameHindi, apFmt: apFmtHindi}, - "439": {tags: []string{"hi-IN"}, localMonth: localMonthsNameHindi, apFmt: apFmtHindi}, - "E": {tags: []string{"hu"}, localMonth: localMonthsNameHungarian, apFmt: apFmtHungarian}, - "40E": {tags: []string{"hu-HU"}, localMonth: localMonthsNameHungarian, apFmt: apFmtHungarian}, - "F": {tags: []string{"is"}, localMonth: localMonthsNameIcelandic, apFmt: apFmtIcelandic}, - "40F": {tags: []string{"is-IS"}, localMonth: localMonthsNameIcelandic, apFmt: apFmtIcelandic}, - "70": {tags: []string{"ig"}, localMonth: localMonthsNameIgbo, apFmt: apFmtIgbo}, - "470": {tags: []string{"ig-NG"}, localMonth: localMonthsNameIgbo, apFmt: apFmtIgbo}, - "21": {tags: []string{"id"}, localMonth: localMonthsNameIndonesian, apFmt: nfp.AmPm[0]}, - "421": {tags: []string{"id-ID"}, localMonth: localMonthsNameIndonesian, apFmt: nfp.AmPm[0]}, - "5D": {tags: []string{"iu"}, localMonth: localMonthsNameInuktitut, apFmt: nfp.AmPm[0]}, - "7C5D": {tags: []string{"iu-Latn"}, localMonth: localMonthsNameInuktitut, apFmt: nfp.AmPm[0]}, - "85D": {tags: []string{"iu-Latn-CA"}, localMonth: localMonthsNameInuktitut, apFmt: nfp.AmPm[0]}, - "785D": {tags: []string{"iu-Cans"}, localMonth: localMonthsNameSyllabics, apFmt: nfp.AmPm[0]}, - "45D": {tags: []string{"iu-Cans-CA"}, localMonth: localMonthsNameSyllabics, apFmt: nfp.AmPm[0]}, - "3C": {tags: []string{"ga"}, localMonth: localMonthsNameIrish, apFmt: apFmtIrish}, - "83C": {tags: []string{"ga-IE"}, localMonth: localMonthsNameIrish, apFmt: apFmtIrish}, - "10": {tags: []string{"it"}, localMonth: localMonthsNameItalian, apFmt: nfp.AmPm[0]}, - "410": {tags: []string{"it-IT"}, localMonth: localMonthsNameItalian, apFmt: nfp.AmPm[0]}, - "810": {tags: []string{"it-CH"}, localMonth: localMonthsNameItalian, apFmt: nfp.AmPm[0]}, - "11": {tags: []string{"ja"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese}, - "411": {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese}, - "800411": {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese}, - "JP-X-GANNEN": {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese}, - "JP-X-GANNEN,80": {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese, useGannen: true}, - "4B": {tags: []string{"kn"}, localMonth: localMonthsNameKannada, apFmt: apFmtKannada}, - "44B": {tags: []string{"kn-IN"}, localMonth: localMonthsNameKannada, apFmt: apFmtKannada}, - "471": {tags: []string{"kr-Latn-NG"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, - "60": {tags: []string{"ks"}, localMonth: localMonthsNameKashmiri, apFmt: nfp.AmPm[0]}, - "460": {tags: []string{"ks-Arab"}, localMonth: localMonthsNameKashmiri, apFmt: nfp.AmPm[0]}, - "860": {tags: []string{"ks-Deva-IN"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0]}, - "3F": {tags: []string{"kk"}, localMonth: localMonthsNameKazakh, apFmt: nfp.AmPm[0]}, - "43F": {tags: []string{"kk-KZ"}, localMonth: localMonthsNameKazakh, apFmt: nfp.AmPm[0]}, - "53": {tags: []string{"km"}, localMonth: localMonthsNameKhmer, apFmt: apFmtKhmer}, - "453": {tags: []string{"km-KH"}, localMonth: localMonthsNameKhmer, apFmt: apFmtKhmer}, - "86": {tags: []string{"quc"}, localMonth: localMonthsNameKiche, apFmt: apFmtCuba}, - "486": {tags: []string{"quc-Latn-GT"}, localMonth: localMonthsNameKiche, apFmt: apFmtCuba}, - "87": {tags: []string{"rw"}, localMonth: localMonthsNameKinyarwanda, apFmt: nfp.AmPm[0]}, - "487": {tags: []string{"rw-RW"}, localMonth: localMonthsNameKinyarwanda, apFmt: nfp.AmPm[0]}, - "41": {tags: []string{"sw"}, localMonth: localMonthsNameKiswahili, apFmt: nfp.AmPm[0]}, - "441": {tags: []string{"sw-KE"}, localMonth: localMonthsNameKiswahili, apFmt: nfp.AmPm[0]}, - "57": {tags: []string{"kok"}, localMonth: localMonthsNameKonkani, apFmt: apFmtKonkani}, - "457": {tags: []string{"kok-IN"}, localMonth: localMonthsNameKonkani, apFmt: apFmtKonkani}, - "12": {tags: []string{"ko"}, localMonth: localMonthsNameKorean, apFmt: apFmtKorean}, - "412": {tags: []string{"ko-KR"}, localMonth: localMonthsNameKorean, apFmt: apFmtKorean}, - "40": {tags: []string{"ky"}, localMonth: localMonthsNameKyrgyz, apFmt: apFmtKyrgyz}, - "440": {tags: []string{"ky-KG"}, localMonth: localMonthsNameKyrgyz, apFmt: apFmtKyrgyz}, - "54": {tags: []string{"lo"}, localMonth: localMonthsNameLao, apFmt: apFmtLao}, - "454": {tags: []string{"lo-LA"}, localMonth: localMonthsNameLao, apFmt: apFmtLao}, - "476": {tags: []string{"la-VA"}, localMonth: localMonthsNameLatin, apFmt: nfp.AmPm[0]}, - "26": {tags: []string{"lv"}, localMonth: localMonthsNameLatvian, apFmt: apFmtLatvian}, - "426": {tags: []string{"lv-LV"}, localMonth: localMonthsNameLatvian, apFmt: apFmtLatvian}, - "27": {tags: []string{"lt"}, localMonth: localMonthsNameLithuanian, apFmt: apFmtLithuanian}, - "427": {tags: []string{"lt-LT"}, localMonth: localMonthsNameLithuanian, apFmt: apFmtLithuanian}, - "7C2E": {tags: []string{"dsb"}, localMonth: localMonthsNameLowerSorbian, apFmt: nfp.AmPm[0]}, - "82E": {tags: []string{"dsb-DE"}, localMonth: localMonthsNameLowerSorbian, apFmt: nfp.AmPm[0]}, - "6E": {tags: []string{"lb"}, localMonth: localMonthsNameLuxembourgish, apFmt: nfp.AmPm[0]}, - "46E": {tags: []string{"lb-LU"}, localMonth: localMonthsNameLuxembourgish, apFmt: nfp.AmPm[0]}, - "2F": {tags: []string{"mk"}, localMonth: localMonthsNameMacedonian, apFmt: apFmtMacedonian}, - "42F": {tags: []string{"mk-MK"}, localMonth: localMonthsNameMacedonian, apFmt: apFmtMacedonian}, - "3E": {tags: []string{"ms"}, localMonth: localMonthsNameMalay, apFmt: apFmtMalay}, - "83E": {tags: []string{"ms-BN"}, localMonth: localMonthsNameMalay, apFmt: apFmtMalay}, - "43E": {tags: []string{"ms-MY"}, localMonth: localMonthsNameMalay, apFmt: apFmtMalay}, - "4C": {tags: []string{"ml"}, localMonth: localMonthsNameMalayalam, apFmt: nfp.AmPm[0]}, - "44C": {tags: []string{"ml-IN"}, localMonth: localMonthsNameMalayalam, apFmt: nfp.AmPm[0]}, - "3A": {tags: []string{"mt"}, localMonth: localMonthsNameMaltese, apFmt: nfp.AmPm[0]}, - "43A": {tags: []string{"mt-MT"}, localMonth: localMonthsNameMaltese, apFmt: nfp.AmPm[0]}, - "81": {tags: []string{"mi"}, localMonth: localMonthsNameMaori, apFmt: apFmtCuba}, - "481": {tags: []string{"mi-NZ"}, localMonth: localMonthsNameMaori, apFmt: apFmtCuba}, - "7A": {tags: []string{"arn"}, localMonth: localMonthsNameMapudungun, apFmt: nfp.AmPm[0]}, - "47A": {tags: []string{"arn-CL"}, localMonth: localMonthsNameMapudungun, apFmt: nfp.AmPm[0]}, - "4E": {tags: []string{"mr"}, localMonth: localMonthsNameMarathi, apFmt: apFmtKonkani}, - "44E": {tags: []string{"mr-IN"}, localMonth: localMonthsNameMarathi, apFmt: apFmtKonkani}, - "7C": {tags: []string{"moh"}, localMonth: localMonthsNameMohawk, apFmt: nfp.AmPm[0]}, - "47C": {tags: []string{"moh-CA"}, localMonth: localMonthsNameMohawk, apFmt: nfp.AmPm[0]}, - "50": {tags: []string{"mn"}, localMonth: localMonthsNameMongolian, apFmt: apFmtMongolian}, - "7850": {tags: []string{"mn-Cyrl"}, localMonth: localMonthsNameMongolian, apFmt: apFmtMongolian}, - "450": {tags: []string{"mn-MN"}, localMonth: localMonthsNameMongolian, apFmt: apFmtMongolian}, - "7C50": {tags: []string{"mn-Mong"}, localMonth: localMonthsNameTraditionalMongolian, apFmt: nfp.AmPm[0]}, - "850": {tags: []string{"mn-Mong-CN"}, localMonth: localMonthsNameTraditionalMongolian, apFmt: nfp.AmPm[0]}, - "C50": {tags: []string{"mn-Mong-MN"}, localMonth: localMonthsNameTraditionalMongolian, apFmt: nfp.AmPm[0]}, - "61": {tags: []string{"ne"}, localMonth: localMonthsNameNepali, apFmt: apFmtHindi}, - "861": {tags: []string{"ne-IN"}, localMonth: localMonthsNameNepaliIN, apFmt: apFmtHindi}, - "461": {tags: []string{"ne-NP"}, localMonth: localMonthsNameNepali, apFmt: apFmtHindi}, - "14": {tags: []string{"no"}, localMonth: localMonthsNameNorwegian, apFmt: apFmtCuba}, - "7C14": {tags: []string{"nb-NO"}, localMonth: localMonthsNameNorwegian, apFmt: apFmtCuba}, - "414": {tags: []string{"nn"}, localMonth: localMonthsNameNorwegian, apFmt: apFmtCuba}, - "7814": {tags: []string{"nn"}, localMonth: localMonthsNameNorwegian, apFmt: apFmtNorwegian}, - "814": {tags: []string{"nn-NO"}, localMonth: localMonthsNameNorwegian, apFmt: apFmtNorwegian}, - "82": {tags: []string{"oc"}, localMonth: localMonthsNameOccitan, apFmt: nfp.AmPm[0]}, - "482": {tags: []string{"oc-FR"}, localMonth: localMonthsNameOccitan, apFmt: nfp.AmPm[0]}, - "48": {tags: []string{"or"}, localMonth: localMonthsNameOdia, apFmt: nfp.AmPm[0]}, - "448": {tags: []string{"or-IN"}, localMonth: localMonthsNameOdia, apFmt: nfp.AmPm[0]}, - "72": {tags: []string{"om"}, localMonth: localMonthsNameOromo, apFmt: apFmtOromo}, - "472": {tags: []string{"om-ET"}, localMonth: localMonthsNameOromo, apFmt: apFmtOromo}, - "63": {tags: []string{"ps"}, localMonth: localMonthsNamePashto, apFmt: apFmtPashto}, - "463": {tags: []string{"ps-AF"}, localMonth: localMonthsNamePashto, apFmt: apFmtPashto}, - "29": {tags: []string{"fa"}, localMonth: localMonthsNamePersian, apFmt: apFmtPersian}, - "429": {tags: []string{"fa-IR"}, localMonth: localMonthsNamePersian, apFmt: apFmtPersian}, - "15": {tags: []string{"pl"}, localMonth: localMonthsNamePolish, apFmt: nfp.AmPm[0]}, - "415": {tags: []string{"pl-PL"}, localMonth: localMonthsNamePolish, apFmt: nfp.AmPm[0]}, - "16": {tags: []string{"pt"}, localMonth: localMonthsNamePortuguese, apFmt: nfp.AmPm[0]}, - "416": {tags: []string{"pt-BR"}, localMonth: localMonthsNamePortuguese, apFmt: nfp.AmPm[0]}, - "816": {tags: []string{"pt-BR"}, localMonth: localMonthsNamePortuguese, apFmt: nfp.AmPm[0]}, - "46": {tags: []string{"pa"}, localMonth: localMonthsNamePunjabi, apFmt: apFmtPunjabi}, - "7C46": {tags: []string{"pa-Arab"}, localMonth: localMonthsNamePunjabiArab, apFmt: nfp.AmPm[0]}, - "446": {tags: []string{"pa-IN"}, localMonth: localMonthsNamePunjabi, apFmt: apFmtPunjabi}, - "846": {tags: []string{"pa-Arab-PK"}, localMonth: localMonthsNamePunjabiArab, apFmt: nfp.AmPm[0]}, - "6B": {tags: []string{"quz"}, localMonth: localMonthsNameQuechua, apFmt: apFmtCuba}, - "46B": {tags: []string{"quz-BO"}, localMonth: localMonthsNameQuechua, apFmt: apFmtCuba}, - "86B": {tags: []string{"quz-EC"}, localMonth: localMonthsNameQuechuaEcuador, apFmt: nfp.AmPm[0]}, - "C6B": {tags: []string{"quz-PE"}, localMonth: localMonthsNameQuechua, apFmt: apFmtCuba}, - "18": {tags: []string{"ro"}, localMonth: localMonthsNameRomanian, apFmt: apFmtCuba}, - "818": {tags: []string{"ro-MD"}, localMonth: localMonthsNameRomanian, apFmt: apFmtCuba}, - "418": {tags: []string{"ro-RO"}, localMonth: localMonthsNameRomanian, apFmt: apFmtCuba}, - "17": {tags: []string{"rm"}, localMonth: localMonthsNameRomansh, apFmt: nfp.AmPm[0]}, - "417": {tags: []string{"rm-CH"}, localMonth: localMonthsNameRomansh, apFmt: nfp.AmPm[0]}, - "19": {tags: []string{"ru"}, localMonth: localMonthsNameRussian, apFmt: nfp.AmPm[0]}, - "819": {tags: []string{"ru-MD"}, localMonth: localMonthsNameRussian, apFmt: nfp.AmPm[0]}, - "419": {tags: []string{"ru-RU"}, localMonth: localMonthsNameRussian, apFmt: nfp.AmPm[0]}, - "85": {tags: []string{"sah"}, localMonth: localMonthsNameSakha, apFmt: apFmtSakha}, - "485": {tags: []string{"sah-RU"}, localMonth: localMonthsNameSakha, apFmt: apFmtSakha}, - "703B": {tags: []string{"smn"}, localMonth: localMonthsNameSami, apFmt: nfp.AmPm[0]}, - "243B": {tags: []string{"smn-FI"}, localMonth: localMonthsNameSami, apFmt: nfp.AmPm[0]}, - "7C3B": {tags: []string{"smj"}, localMonth: localMonthsNameSamiLule, apFmt: nfp.AmPm[0]}, - "103B": {tags: []string{"smj-NO"}, localMonth: localMonthsNameSamiLule, apFmt: nfp.AmPm[0]}, - "143B": {tags: []string{"smj-SE"}, localMonth: localMonthsNameSamiLule, apFmt: nfp.AmPm[0]}, - "3B": {tags: []string{"se"}, localMonth: localMonthsNameSamiNorthern, apFmt: apFmtSamiNorthern}, - "C3B": {tags: []string{"se-FI"}, localMonth: localMonthsNameSamiNorthernFI, apFmt: nfp.AmPm[0]}, - "43B": {tags: []string{"se-NO"}, localMonth: localMonthsNameSamiNorthern, apFmt: apFmtSamiNorthern}, - "83B": {tags: []string{"se-SE"}, localMonth: localMonthsNameSamiNorthern, apFmt: nfp.AmPm[0]}, - "743B": {tags: []string{"sms"}, localMonth: localMonthsNameSamiSkolt, apFmt: nfp.AmPm[0]}, - "203B": {tags: []string{"sms-FI"}, localMonth: localMonthsNameSamiSkolt, apFmt: nfp.AmPm[0]}, - "783B": {tags: []string{"sma"}, localMonth: localMonthsNameSamiSouthern, apFmt: nfp.AmPm[0]}, - "183B": {tags: []string{"sma-NO"}, localMonth: localMonthsNameSamiSouthern, apFmt: nfp.AmPm[0]}, - "1C3B": {tags: []string{"sma-SE"}, localMonth: localMonthsNameSamiSouthern, apFmt: nfp.AmPm[0]}, - "4F": {tags: []string{"sa"}, localMonth: localMonthsNameSanskrit, apFmt: apFmtSanskrit}, - "44F": {tags: []string{"sa-IN"}, localMonth: localMonthsNameSanskrit, apFmt: apFmtSanskrit}, - "91": {tags: []string{"gd"}, localMonth: localMonthsNameScottishGaelic, apFmt: apFmtScottishGaelic}, - "491": {tags: []string{"gd-GB"}, localMonth: localMonthsNameScottishGaelic, apFmt: apFmtScottishGaelic}, - "6C1A": {tags: []string{"sr-Cyrl"}, localMonth: localMonthsNameSerbian, apFmt: nfp.AmPm[0]}, - "1C1A": {tags: []string{"sr-Cyrl-BA"}, localMonth: localMonthsNameSerbianBA, apFmt: nfp.AmPm[0]}, - "301A": {tags: []string{"sr-Cyrl-ME"}, localMonth: localMonthsNameSerbian, apFmt: nfp.AmPm[0]}, - "281A": {tags: []string{"sr-Cyrl-RS"}, localMonth: localMonthsNameSerbian, apFmt: nfp.AmPm[0]}, - "C1A": {tags: []string{"sr-Cyrl-CS"}, localMonth: localMonthsNameSerbian, apFmt: nfp.AmPm[0]}, - "701A": {tags: []string{"sr-Latn"}, localMonth: localMonthsNameSerbianLatin, apFmt: apFmtSerbianLatin}, - "7C1A": {tags: []string{"sr"}, localMonth: localMonthsNameSerbianLatin, apFmt: apFmtSerbianLatin}, - "181A": {tags: []string{"sr-Latn-BA"}, localMonth: localMonthsNameSerbianLatin, apFmt: apFmtSerbianLatinBA}, - "2C1A": {tags: []string{"sr-Latn-ME"}, localMonth: localMonthsNameSerbianLatin, apFmt: apFmtSerbianLatinBA}, - "241A": {tags: []string{"sr-Latn-RS"}, localMonth: localMonthsNameSerbianLatin, apFmt: apFmtSerbianLatin}, - "81A": {tags: []string{"sr-Latn-CS"}, localMonth: localMonthsNameSerbianLatinCS, apFmt: nfp.AmPm[0]}, - "6C": {tags: []string{"nso"}, localMonth: localMonthsNameSesothoSaLeboa, apFmt: nfp.AmPm[0]}, - "46C": {tags: []string{"nso-ZA"}, localMonth: localMonthsNameSesothoSaLeboa, apFmt: nfp.AmPm[0]}, - "32": {tags: []string{"tn"}, localMonth: localMonthsNameSetswana, apFmt: nfp.AmPm[0]}, - "832": {tags: []string{"tn-BW"}, localMonth: localMonthsNameSetswana, apFmt: nfp.AmPm[0]}, - "432": {tags: []string{"tn-ZA"}, localMonth: localMonthsNameSetswana, apFmt: nfp.AmPm[0]}, - "59": {tags: []string{"sd"}, localMonth: localMonthsNameSindhi, apFmt: nfp.AmPm[0]}, - "7C59": {tags: []string{"sd-Arab"}, localMonth: localMonthsNameSindhi, apFmt: nfp.AmPm[0]}, - "859": {tags: []string{"sd-Arab-PK"}, localMonth: localMonthsNameSindhi, apFmt: nfp.AmPm[0]}, - "5B": {tags: []string{"si"}, localMonth: localMonthsNameSinhala, apFmt: apFmtSinhala}, - "45B": {tags: []string{"si-LK"}, localMonth: localMonthsNameSinhala, apFmt: apFmtSinhala}, - "1B": {tags: []string{"sk"}, localMonth: localMonthsNameSlovak, apFmt: nfp.AmPm[0]}, - "41B": {tags: []string{"sk-SK"}, localMonth: localMonthsNameSlovak, apFmt: nfp.AmPm[0]}, - "24": {tags: []string{"sl"}, localMonth: localMonthsNameSlovenian, apFmt: apFmtSlovenian}, - "424": {tags: []string{"sl-SI"}, localMonth: localMonthsNameSlovenian, apFmt: apFmtSlovenian}, - "77": {tags: []string{"so"}, localMonth: localMonthsNameSomali, apFmt: apFmtSomali}, - "477": {tags: []string{"so-SO"}, localMonth: localMonthsNameSomali, apFmt: apFmtSomali}, - "A": {tags: []string{"es"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, - "2C0A": {tags: []string{"es-AR"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, - "200A": {tags: []string{"es-VE"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, - "400A": {tags: []string{"es-BO"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, - "340A": {tags: []string{"es-CL"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, - "240A": {tags: []string{"es-CO"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, - "140A": {tags: []string{"es-CR"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, - "5C0A": {tags: []string{"es-CU"}, localMonth: localMonthsNameSpanish, apFmt: apFmtCuba}, - "1C0A": {tags: []string{"es-DO"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, - "300A": {tags: []string{"es-EC"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, - "440A": {tags: []string{"es-SV"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish}, - "1E": {tags: []string{"th"}, localMonth: localMonthsNameThai, apFmt: nfp.AmPm[0]}, - "41E": {tags: []string{"th-TH"}, localMonth: localMonthsNameThai, apFmt: nfp.AmPm[0]}, - "51": {tags: []string{"bo"}, localMonth: localMonthsNameTibetan, apFmt: apFmtTibetan}, - "451": {tags: []string{"bo-CN"}, localMonth: localMonthsNameTibetan, apFmt: apFmtTibetan}, - "1F": {tags: []string{"tr"}, localMonth: localMonthsNameTurkish, apFmt: apFmtTurkish}, - "41F": {tags: []string{"tr-TR"}, localMonth: localMonthsNameTurkish, apFmt: apFmtTurkish}, - "52": {tags: []string{"cy"}, localMonth: localMonthsNameWelsh, apFmt: apFmtWelsh}, - "452": {tags: []string{"cy-GB"}, localMonth: localMonthsNameWelsh, apFmt: apFmtWelsh}, - "2A": {tags: []string{"vi"}, localMonth: localMonthsNameVietnamese, apFmt: apFmtVietnamese}, - "42A": {tags: []string{"vi-VN"}, localMonth: localMonthsNameVietnamese, apFmt: apFmtVietnamese}, - "88": {tags: []string{"wo"}, localMonth: localMonthsNameWolof, apFmt: apFmtWolof}, - "488": {tags: []string{"wo-SN"}, localMonth: localMonthsNameWolof, apFmt: apFmtWolof}, - "34": {tags: []string{"xh"}, localMonth: localMonthsNameXhosa, apFmt: nfp.AmPm[0]}, - "434": {tags: []string{"xh-ZA"}, localMonth: localMonthsNameXhosa, apFmt: nfp.AmPm[0]}, - "78": {tags: []string{"ii"}, localMonth: localMonthsNameYi, apFmt: apFmtYi}, - "478": {tags: []string{"ii-CN"}, localMonth: localMonthsNameYi, apFmt: apFmtYi}, - "35": {tags: []string{"zu"}, localMonth: localMonthsNameZulu, apFmt: nfp.AmPm[0]}, - "435": {tags: []string{"zu-ZA"}, localMonth: localMonthsNameZulu, apFmt: nfp.AmPm[0]}, + }, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + "C09": {tags: []string{"en-AU"}, localMonth: localMonthsNameEnglish, apFmt: strings.ToLower(nfp.AmPm[0]), weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + "2809": {tags: []string{"en-BZ"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + "1009": {tags: []string{"en-CA"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + "2409": {tags: []string{"en-029"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + "3C09": {tags: []string{"en-HK"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + "4009": {tags: []string{"en-IN"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + "1809": {tags: []string{"en-IE"}, localMonth: localMonthsNameEnglish, apFmt: strings.ToLower(nfp.AmPm[0]), weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + "2009": {tags: []string{"en-JM"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + "4409": {tags: []string{"en-MY"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + "1409": {tags: []string{"en-NZ"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + "3409": {tags: []string{"en-PH"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + "4809": {tags: []string{"en-SG"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + "1C09": {tags: []string{"en-ZA"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + "2C09": {tags: []string{"en-TT"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + "4C09": {tags: []string{"en-AE"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + "809": {tags: []string{"en-GB"}, localMonth: localMonthsNameEnglish, apFmt: strings.ToLower(nfp.AmPm[0]), weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + "409": {tags: []string{"en-US"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + "3009": {tags: []string{"en-ZW"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + "25": {tags: []string{"et"}, localMonth: localMonthsNameEstonian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEstonian, weekdayNamesAbbr: weekdayNamesEstonianAbbr}, + "425": {tags: []string{"et-EE"}, localMonth: localMonthsNameEstonian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEstonian, weekdayNamesAbbr: weekdayNamesEstonianAbbr}, + "38": {tags: []string{"fo"}, localMonth: localMonthsNameFaroese, apFmt: apFmtFaroese, weekdayNames: weekdayNamesFaroese, weekdayNamesAbbr: weekdayNamesFaroeseAbbr}, + "438": {tags: []string{"fo-FO"}, localMonth: localMonthsNameFaroese, apFmt: apFmtFaroese, weekdayNames: weekdayNamesFaroese, weekdayNamesAbbr: weekdayNamesFaroeseAbbr}, + "64": {tags: []string{"fil"}, localMonth: localMonthsNameFilipino, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFilipino, weekdayNamesAbbr: weekdayNamesFilipinoAbbr}, + "464": {tags: []string{"fil-PH"}, localMonth: localMonthsNameFilipino, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFilipino, weekdayNamesAbbr: weekdayNamesFilipinoAbbr}, + "B": {tags: []string{"fi"}, localMonth: localMonthsNameFinnish, apFmt: apFmtFinnish, weekdayNames: weekdayNamesFinnish, weekdayNamesAbbr: weekdayNamesFinnishAbbr}, + "40B": {tags: []string{"fi-FI"}, localMonth: localMonthsNameFinnish, apFmt: apFmtFinnish, weekdayNames: weekdayNamesFinnish, weekdayNamesAbbr: weekdayNamesFinnishAbbr}, + "C": {tags: []string{"fr"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, + "80C": {tags: []string{"fr-BE"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, + "2C0C": {tags: []string{"fr-CM"}, localMonth: localMonthsNameFrench, apFmt: apFmtCameroon, weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, + "C0C": {tags: []string{"fr-CA"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, + "1C0C": {tags: []string{"fr-029"}, localMonth: localMonthsNameCaribbean, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, + "240C": {tags: []string{"fr-CD"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, + "300C": {tags: []string{"fr-CI"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, + "40C": {tags: []string{"fr-FR"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, + "3C0C": {tags: []string{"fr-HT"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, + "140C": {tags: []string{"fr-LU"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, + "340C": {tags: []string{"fr-ML"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, + "380C": {tags: []string{"fr-MA"}, localMonth: localMonthsNameMorocco, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, + "180C": {tags: []string{"fr-MC"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, + "200C": {tags: []string{"fr-RE"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, + "280C": {tags: []string{"fr-SN"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, + "62": {tags: []string{"fy"}, localMonth: localMonthsNameFrisian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrisian, weekdayNamesAbbr: weekdayNamesFrisianAbbr}, + "462": {tags: []string{"fy-NL"}, localMonth: localMonthsNameFrisian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrisian, weekdayNamesAbbr: weekdayNamesFrisianAbbr}, + "67": {tags: []string{"ff"}, localMonth: localMonthsNameFulah, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFulah, weekdayNamesAbbr: weekdayNamesFulahAbbr}, + "7C67": {tags: []string{"ff-Latn"}, localMonth: localMonthsNameFulah, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFulah, weekdayNamesAbbr: weekdayNamesFulahAbbr}, + "467": {tags: []string{"ff-NG", "ff-Latn-NG"}, localMonth: localMonthsNameNigeria, apFmt: apFmtNigeria, weekdayNames: weekdayNamesNigeria, weekdayNamesAbbr: weekdayNamesNigeriaAbbr}, + "867": {tags: []string{"ff-SN"}, localMonth: localMonthsNameNigeria, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesNigeria, weekdayNamesAbbr: weekdayNamesNigeriaAbbr}, + "56": {tags: []string{"gl"}, localMonth: localMonthsNameGalician, apFmt: apFmtCuba, weekdayNames: weekdayNamesGalician, weekdayNamesAbbr: weekdayNamesGalicianAbbr}, + "456": {tags: []string{"gl-ES"}, localMonth: localMonthsNameGalician, apFmt: apFmtCuba, weekdayNames: weekdayNamesGalician, weekdayNamesAbbr: weekdayNamesGalicianAbbr}, + "37": {tags: []string{"ka"}, localMonth: localMonthsNameGeorgian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGeorgian, weekdayNamesAbbr: weekdayNamesGeorgianAbbr}, + "437": {tags: []string{"ka-GE"}, localMonth: localMonthsNameGeorgian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGeorgian, weekdayNamesAbbr: weekdayNamesGeorgianAbbr}, + "7": {tags: []string{"de"}, localMonth: localMonthsNameGerman, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGerman, weekdayNamesAbbr: weekdayNamesGermanAbbr}, + "C07": {tags: []string{"de-AT"}, localMonth: localMonthsNameAustria, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGerman, weekdayNamesAbbr: weekdayNamesGermanAbbr}, + "407": {tags: []string{"de-DE"}, localMonth: localMonthsNameGerman, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGerman, weekdayNamesAbbr: weekdayNamesGermanAbbr}, + "1407": {tags: []string{"de-LI"}, localMonth: localMonthsNameGerman, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGerman, weekdayNamesAbbr: weekdayNamesGermanAbbr}, + "807": {tags: []string{"de-CH"}, localMonth: localMonthsNameGerman, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGerman, weekdayNamesAbbr: weekdayNamesGermanAbbr}, + "8": {tags: []string{"el"}, localMonth: localMonthsNameGreek, apFmt: apFmtGreek, weekdayNames: weekdayNamesGreek, weekdayNamesAbbr: weekdayNamesGreekAbbr}, + "408": {tags: []string{"el-GR"}, localMonth: localMonthsNameGreek, apFmt: apFmtGreek, weekdayNames: weekdayNamesGreek, weekdayNamesAbbr: weekdayNamesGreekAbbr}, + "6F": {tags: []string{"kl"}, localMonth: localMonthsNameGreenlandic, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGreenlandic, weekdayNamesAbbr: weekdayNamesGreenlandicAbbr}, + "46F": {tags: []string{"kl-GL"}, localMonth: localMonthsNameGreenlandic, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGreenlandic, weekdayNamesAbbr: weekdayNamesGreenlandicAbbr}, + "74": {tags: []string{"gn"}, localMonth: localMonthsNameGuarani, apFmt: apFmtCuba, weekdayNames: weekdayNamesGuarani, weekdayNamesAbbr: weekdayNamesGuaraniAbbr}, + "474": {tags: []string{"gn-PY"}, localMonth: localMonthsNameGuarani, apFmt: apFmtCuba, weekdayNames: weekdayNamesGuarani, weekdayNamesAbbr: weekdayNamesGuaraniAbbr}, + "47": {tags: []string{"gu"}, localMonth: localMonthsNameGujarati, apFmt: apFmtGujarati, weekdayNames: weekdayNamesGujarati, weekdayNamesAbbr: weekdayNamesGujaratiAbbr}, + "447": {tags: []string{"gu-IN"}, localMonth: localMonthsNameGujarati, apFmt: apFmtGujarati, weekdayNames: weekdayNamesGujarati, weekdayNamesAbbr: weekdayNamesGujaratiAbbr}, + "68": {tags: []string{"ha"}, localMonth: localMonthsNameHausa, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesHausa, weekdayNamesAbbr: weekdayNamesHausaAbbr}, + "7C68": {tags: []string{"ha-Latn"}, localMonth: localMonthsNameHausa, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesHausa, weekdayNamesAbbr: weekdayNamesHausaAbbr}, + "468": {tags: []string{"ha-Latn-NG"}, localMonth: localMonthsNameHausa, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesHausa, weekdayNamesAbbr: weekdayNamesHausaAbbr}, + "75": {tags: []string{"haw"}, localMonth: localMonthsNameHawaiian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesHawaiian, weekdayNamesAbbr: weekdayNamesHawaiianAbbr}, + "475": {tags: []string{"haw-US"}, localMonth: localMonthsNameHawaiian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesHawaiian, weekdayNamesAbbr: weekdayNamesHawaiianAbbr}, + "D": {tags: []string{"he"}, localMonth: localMonthsNameHebrew, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesHebrew, weekdayNamesAbbr: weekdayNamesHebrewAbbr}, + "40D": {tags: []string{"he-IL"}, localMonth: localMonthsNameHebrew, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesHebrew, weekdayNamesAbbr: weekdayNamesHebrewAbbr}, + "39": {tags: []string{"hi"}, localMonth: localMonthsNameHindi, apFmt: apFmtHindi, weekdayNames: weekdayNamesHindi, weekdayNamesAbbr: weekdayNamesHindiAbbr}, + "439": {tags: []string{"hi-IN"}, localMonth: localMonthsNameHindi, apFmt: apFmtHindi, weekdayNames: weekdayNamesHindi, weekdayNamesAbbr: weekdayNamesHindiAbbr}, + "E": {tags: []string{"hu"}, localMonth: localMonthsNameHungarian, apFmt: apFmtHungarian, weekdayNames: weekdayNamesHungarian, weekdayNamesAbbr: weekdayNamesHungarianAbbr}, + "40E": {tags: []string{"hu-HU"}, localMonth: localMonthsNameHungarian, apFmt: apFmtHungarian, weekdayNames: weekdayNamesHungarian, weekdayNamesAbbr: weekdayNamesHungarianAbbr}, + "F": {tags: []string{"is"}, localMonth: localMonthsNameIcelandic, apFmt: apFmtIcelandic, weekdayNames: weekdayNamesIcelandic, weekdayNamesAbbr: weekdayNamesIcelandicAbbr}, + "40F": {tags: []string{"is-IS"}, localMonth: localMonthsNameIcelandic, apFmt: apFmtIcelandic, weekdayNames: weekdayNamesIcelandic, weekdayNamesAbbr: weekdayNamesIcelandicAbbr}, + "70": {tags: []string{"ig"}, localMonth: localMonthsNameIgbo, apFmt: apFmtIgbo, weekdayNames: weekdayNamesIgbo, weekdayNamesAbbr: weekdayNamesIgboAbbr}, + "470": {tags: []string{"ig-NG"}, localMonth: localMonthsNameIgbo, apFmt: apFmtIgbo, weekdayNames: weekdayNamesIgbo, weekdayNamesAbbr: weekdayNamesIgboAbbr}, + "21": {tags: []string{"id"}, localMonth: localMonthsNameIndonesian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesIndonesian, weekdayNamesAbbr: weekdayNamesIndonesianAbbr}, + "421": {tags: []string{"id-ID"}, localMonth: localMonthsNameIndonesian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesIndonesian, weekdayNamesAbbr: weekdayNamesIndonesianAbbr}, + "5D": {tags: []string{"iu"}, localMonth: localMonthsNameInuktitut, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesInuktitut, weekdayNamesAbbr: weekdayNamesInuktitutAbbr}, + "7C5D": {tags: []string{"iu-Latn"}, localMonth: localMonthsNameInuktitut, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesInuktitut, weekdayNamesAbbr: weekdayNamesInuktitutAbbr}, + "85D": {tags: []string{"iu-Latn-CA"}, localMonth: localMonthsNameInuktitut, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesInuktitut, weekdayNamesAbbr: weekdayNamesInuktitutAbbr}, + "785D": {tags: []string{"iu-Cans"}, localMonth: localMonthsNameSyllabics, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSyllabics, weekdayNamesAbbr: weekdayNamesSyllabicsAbbr}, + "45D": {tags: []string{"iu-Cans-CA"}, localMonth: localMonthsNameSyllabics, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSyllabics, weekdayNamesAbbr: weekdayNamesSyllabicsAbbr}, + "3C": {tags: []string{"ga"}, localMonth: localMonthsNameIrish, apFmt: apFmtIrish, weekdayNames: weekdayNamesIrish, weekdayNamesAbbr: weekdayNamesIrishAbbr}, + "83C": {tags: []string{"ga-IE"}, localMonth: localMonthsNameIrish, apFmt: apFmtIrish, weekdayNames: weekdayNamesIrish, weekdayNamesAbbr: weekdayNamesIrishAbbr}, + "10": {tags: []string{"it"}, localMonth: localMonthsNameItalian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesItalian, weekdayNamesAbbr: weekdayNamesItalianAbbr}, + "410": {tags: []string{"it-IT"}, localMonth: localMonthsNameItalian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesItalian, weekdayNamesAbbr: weekdayNamesItalianAbbr}, + "810": {tags: []string{"it-CH"}, localMonth: localMonthsNameItalian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesItalian, weekdayNamesAbbr: weekdayNamesItalianAbbr}, + "11": {tags: []string{"ja"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese, weekdayNames: weekdayNamesJapanese, weekdayNamesAbbr: weekdayNamesJapaneseAbbr}, + "411": {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese, weekdayNames: weekdayNamesJapanese, weekdayNamesAbbr: weekdayNamesJapaneseAbbr}, + "800411": {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese, weekdayNames: weekdayNamesJapanese, weekdayNamesAbbr: weekdayNamesJapaneseAbbr}, + "JP-X-GANNEN": {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese, weekdayNames: weekdayNamesJapanese, weekdayNamesAbbr: weekdayNamesJapaneseAbbr}, + "JP-X-GANNEN,80": {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese, weekdayNames: weekdayNamesJapanese, weekdayNamesAbbr: weekdayNamesJapaneseAbbr, useGannen: true}, + "4B": {tags: []string{"kn"}, localMonth: localMonthsNameKannada, apFmt: apFmtKannada, weekdayNames: weekdayNamesKannada, weekdayNamesAbbr: weekdayNamesKannadaAbbr}, + "44B": {tags: []string{"kn-IN"}, localMonth: localMonthsNameKannada, apFmt: apFmtKannada, weekdayNames: weekdayNamesKannada, weekdayNamesAbbr: weekdayNamesKannadaAbbr}, + "471": {tags: []string{"kr-Latn-NG"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + "60": {tags: []string{"ks"}, localMonth: localMonthsNameKashmiri, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesKashmiri, weekdayNamesAbbr: weekdayNamesKashmiriAbbr}, + "460": {tags: []string{"ks-Arab"}, localMonth: localMonthsNameKashmiri, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesKashmiri, weekdayNamesAbbr: weekdayNamesKashmiriAbbr}, + "860": {tags: []string{"ks-Deva-IN"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + "3F": {tags: []string{"kk"}, localMonth: localMonthsNameKazakh, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesKazakh, weekdayNamesAbbr: weekdayNamesKazakhAbbr}, + "43F": {tags: []string{"kk-KZ"}, localMonth: localMonthsNameKazakh, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesKazakh, weekdayNamesAbbr: weekdayNamesKazakhAbbr}, + "53": {tags: []string{"km"}, localMonth: localMonthsNameKhmer, apFmt: apFmtKhmer, weekdayNames: weekdayNamesKhmer, weekdayNamesAbbr: weekdayNamesKhmerAbbr}, + "453": {tags: []string{"km-KH"}, localMonth: localMonthsNameKhmer, apFmt: apFmtKhmer, weekdayNames: weekdayNamesKhmer, weekdayNamesAbbr: weekdayNamesKhmerAbbr}, + "86": {tags: []string{"quc"}, localMonth: localMonthsNameKiche, apFmt: apFmtCuba, weekdayNames: weekdayNamesKiche, weekdayNamesAbbr: weekdayNamesKicheAbbr}, + "486": {tags: []string{"quc-Latn-GT"}, localMonth: localMonthsNameKiche, apFmt: apFmtCuba, weekdayNames: weekdayNamesKiche, weekdayNamesAbbr: weekdayNamesKicheAbbr}, + "87": {tags: []string{"rw"}, localMonth: localMonthsNameKinyarwanda, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesKinyarwanda, weekdayNamesAbbr: weekdayNamesKinyarwandaAbbr}, + "487": {tags: []string{"rw-RW"}, localMonth: localMonthsNameKinyarwanda, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesKinyarwanda, weekdayNamesAbbr: weekdayNamesKinyarwandaAbbr}, + "41": {tags: []string{"sw"}, localMonth: localMonthsNameKiswahili, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesKiswahili, weekdayNamesAbbr: weekdayNamesKiswahiliAbbr}, + "441": {tags: []string{"sw-KE"}, localMonth: localMonthsNameKiswahili, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesKiswahili, weekdayNamesAbbr: weekdayNamesKiswahiliAbbr}, + "57": {tags: []string{"kok"}, localMonth: localMonthsNameKonkani, apFmt: apFmtKonkani, weekdayNames: weekdayNamesKonkani, weekdayNamesAbbr: weekdayNamesKonkaniAbbr}, + "457": {tags: []string{"kok-IN"}, localMonth: localMonthsNameKonkani, apFmt: apFmtKonkani, weekdayNames: weekdayNamesKonkani, weekdayNamesAbbr: weekdayNamesKonkaniAbbr}, + "12": {tags: []string{"ko"}, localMonth: localMonthsNameKorean, apFmt: apFmtKorean, weekdayNames: weekdayNamesKorean, weekdayNamesAbbr: weekdayNamesKoreanAbbr}, + "412": {tags: []string{"ko-KR"}, localMonth: localMonthsNameKorean, apFmt: apFmtKorean, weekdayNames: weekdayNamesKorean, weekdayNamesAbbr: weekdayNamesKoreanAbbr}, + "40": {tags: []string{"ky"}, localMonth: localMonthsNameKyrgyz, apFmt: apFmtKyrgyz, weekdayNames: weekdayNamesKyrgyz, weekdayNamesAbbr: weekdayNamesKyrgyzAbbr}, + "440": {tags: []string{"ky-KG"}, localMonth: localMonthsNameKyrgyz, apFmt: apFmtKyrgyz, weekdayNames: weekdayNamesKyrgyz, weekdayNamesAbbr: weekdayNamesKyrgyzAbbr}, + "54": {tags: []string{"lo"}, localMonth: localMonthsNameLao, apFmt: apFmtLao, weekdayNames: weekdayNamesLao, weekdayNamesAbbr: weekdayNamesLaoAbbr}, + "454": {tags: []string{"lo-LA"}, localMonth: localMonthsNameLao, apFmt: apFmtLao, weekdayNames: weekdayNamesLao, weekdayNamesAbbr: weekdayNamesLaoAbbr}, + "476": {tags: []string{"la-VA"}, localMonth: localMonthsNameLatin, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesLatin, weekdayNamesAbbr: weekdayNamesLatinAbbr}, + "26": {tags: []string{"lv"}, localMonth: localMonthsNameLatvian, apFmt: apFmtLatvian, weekdayNames: weekdayNamesLatvian, weekdayNamesAbbr: weekdayNamesLatvianAbbr}, + "426": {tags: []string{"lv-LV"}, localMonth: localMonthsNameLatvian, apFmt: apFmtLatvian, weekdayNames: weekdayNamesLatvian, weekdayNamesAbbr: weekdayNamesLatvianAbbr}, + "27": {tags: []string{"lt"}, localMonth: localMonthsNameLithuanian, apFmt: apFmtLithuanian, weekdayNames: weekdayNamesLithuanian, weekdayNamesAbbr: weekdayNamesLithuanianAbbr}, + "427": {tags: []string{"lt-LT"}, localMonth: localMonthsNameLithuanian, apFmt: apFmtLithuanian, weekdayNames: weekdayNamesLithuanian, weekdayNamesAbbr: weekdayNamesLithuanianAbbr}, + "7C2E": {tags: []string{"dsb"}, localMonth: localMonthsNameLowerSorbian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesLowerSorbian, weekdayNamesAbbr: weekdayNamesLowerSorbianAbbr}, + "82E": {tags: []string{"dsb-DE"}, localMonth: localMonthsNameLowerSorbian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesLowerSorbian, weekdayNamesAbbr: weekdayNamesLowerSorbianAbbr}, + "6E": {tags: []string{"lb"}, localMonth: localMonthsNameLuxembourgish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesLuxembourgish, weekdayNamesAbbr: weekdayNamesLuxembourgishAbbr}, + "46E": {tags: []string{"lb-LU"}, localMonth: localMonthsNameLuxembourgish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesLuxembourgish, weekdayNamesAbbr: weekdayNamesLuxembourgishAbbr}, + "2F": {tags: []string{"mk"}, localMonth: localMonthsNameMacedonian, apFmt: apFmtMacedonian, weekdayNames: weekdayNamesMacedonian, weekdayNamesAbbr: weekdayNamesMacedonianAbbr}, + "42F": {tags: []string{"mk-MK"}, localMonth: localMonthsNameMacedonian, apFmt: apFmtMacedonian, weekdayNames: weekdayNamesMacedonian, weekdayNamesAbbr: weekdayNamesMacedonianAbbr}, + "3E": {tags: []string{"ms"}, localMonth: localMonthsNameMalay, apFmt: apFmtMalay, weekdayNames: weekdayNamesMalay, weekdayNamesAbbr: weekdayNamesMalayAbbr}, + "83E": {tags: []string{"ms-BN"}, localMonth: localMonthsNameMalay, apFmt: apFmtMalay, weekdayNames: weekdayNamesMalay, weekdayNamesAbbr: weekdayNamesMalayAbbr}, + "43E": {tags: []string{"ms-MY"}, localMonth: localMonthsNameMalay, apFmt: apFmtMalay, weekdayNames: weekdayNamesMalay, weekdayNamesAbbr: weekdayNamesMalayAbbr}, + "4C": {tags: []string{"ml"}, localMonth: localMonthsNameMalayalam, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesMalayalam, weekdayNamesAbbr: weekdayNamesMalayalamAbbr}, + "44C": {tags: []string{"ml-IN"}, localMonth: localMonthsNameMalayalam, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesMalayalam, weekdayNamesAbbr: weekdayNamesMalayalamAbbr}, + "3A": {tags: []string{"mt"}, localMonth: localMonthsNameMaltese, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesMaltese, weekdayNamesAbbr: weekdayNamesMalteseAbbr}, + "43A": {tags: []string{"mt-MT"}, localMonth: localMonthsNameMaltese, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesMaltese, weekdayNamesAbbr: weekdayNamesMalteseAbbr}, + "81": {tags: []string{"mi"}, localMonth: localMonthsNameMaori, apFmt: apFmtCuba, weekdayNames: weekdayNamesMaori, weekdayNamesAbbr: weekdayNamesMaoriAbbr}, + "481": {tags: []string{"mi-NZ"}, localMonth: localMonthsNameMaori, apFmt: apFmtCuba, weekdayNames: weekdayNamesMaori, weekdayNamesAbbr: weekdayNamesMaoriAbbr}, + "7A": {tags: []string{"arn"}, localMonth: localMonthsNameMapudungun, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesMapudungun, weekdayNamesAbbr: weekdayNamesMapudungunAbbr}, + "47A": {tags: []string{"arn-CL"}, localMonth: localMonthsNameMapudungun, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesMapudungun, weekdayNamesAbbr: weekdayNamesMapudungunAbbr}, + "4E": {tags: []string{"mr"}, localMonth: localMonthsNameMarathi, apFmt: apFmtKonkani, weekdayNames: weekdayNamesMarathi, weekdayNamesAbbr: weekdayNamesMarathiAbbr}, + "44E": {tags: []string{"mr-IN"}, localMonth: localMonthsNameMarathi, apFmt: apFmtKonkani, weekdayNames: weekdayNamesMarathi, weekdayNamesAbbr: weekdayNamesMarathiAbbr}, + "7C": {tags: []string{"moh"}, localMonth: localMonthsNameMohawk, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesMohawk, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + "47C": {tags: []string{"moh-CA"}, localMonth: localMonthsNameMohawk, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesMohawk, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + "50": {tags: []string{"mn"}, localMonth: localMonthsNameMongolian, apFmt: apFmtMongolian, weekdayNames: weekdayNamesMongolian, weekdayNamesAbbr: weekdayNamesMongolianAbbr}, + "7850": {tags: []string{"mn-Cyrl"}, localMonth: localMonthsNameMongolian, apFmt: apFmtMongolian, weekdayNames: weekdayNamesMongolian, weekdayNamesAbbr: weekdayNamesMongolianCyrlAbbr}, + "450": {tags: []string{"mn-MN"}, localMonth: localMonthsNameMongolian, apFmt: apFmtMongolian, weekdayNames: weekdayNamesMongolian, weekdayNamesAbbr: weekdayNamesMongolianCyrlAbbr}, + "7C50": {tags: []string{"mn-Mong"}, localMonth: localMonthsNameTraditionalMongolian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTraditionalMongolian, weekdayNamesAbbr: weekdayNamesTraditionalMongolian}, + "850": {tags: []string{"mn-Mong-CN"}, localMonth: localMonthsNameTraditionalMongolian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTraditionalMongolian, weekdayNamesAbbr: weekdayNamesTraditionalMongolian}, + "C50": {tags: []string{"mn-Mong-MN"}, localMonth: localMonthsNameTraditionalMongolian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTraditionalMongolianMN, weekdayNamesAbbr: weekdayNamesTraditionalMongolianMN}, + "61": {tags: []string{"ne"}, localMonth: localMonthsNameNepali, apFmt: apFmtHindi, weekdayNames: weekdayNamesNepali, weekdayNamesAbbr: weekdayNamesNepaliAbbr}, + "861": {tags: []string{"ne-IN"}, localMonth: localMonthsNameNepaliIN, apFmt: apFmtHindi, weekdayNames: weekdayNamesNepaliIN, weekdayNamesAbbr: weekdayNamesNepaliINAbbr}, + "461": {tags: []string{"ne-NP"}, localMonth: localMonthsNameNepali, apFmt: apFmtHindi, weekdayNames: weekdayNamesNepali, weekdayNamesAbbr: weekdayNamesNepaliAbbr}, + "14": {tags: []string{"no"}, localMonth: localMonthsNameNorwegian, apFmt: apFmtCuba, weekdayNames: weekdayNamesNorwegian, weekdayNamesAbbr: weekdayNamesNorwegianAbbr}, + "7C14": {tags: []string{"nb"}, localMonth: localMonthsNameNorwegian, apFmt: apFmtCuba, weekdayNames: weekdayNamesNorwegian, weekdayNamesAbbr: weekdayNamesNorwegianNOAbbr}, + "414": {tags: []string{"nb-NO"}, localMonth: localMonthsNameNorwegian, apFmt: apFmtCuba, weekdayNames: weekdayNamesNorwegian, weekdayNamesAbbr: weekdayNamesNorwegianNOAbbr}, + "7814": {tags: []string{"nn"}, localMonth: localMonthsNameNorwegian, apFmt: apFmtNorwegian, weekdayNames: weekdayNamesNorwegianNynorsk, weekdayNamesAbbr: weekdayNamesNorwegianNynorskAbbr}, + "814": {tags: []string{"nn-NO"}, localMonth: localMonthsNameNorwegian, apFmt: apFmtNorwegian, weekdayNames: weekdayNamesNorwegianNynorsk, weekdayNamesAbbr: weekdayNamesNorwegianNynorskAbbr}, + "82": {tags: []string{"oc"}, localMonth: localMonthsNameOccitan, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesOccitan, weekdayNamesAbbr: weekdayNamesOccitanAbbr}, + "482": {tags: []string{"oc-FR"}, localMonth: localMonthsNameOccitan, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesOccitan, weekdayNamesAbbr: weekdayNamesOccitanAbbr}, + "48": {tags: []string{"or"}, localMonth: localMonthsNameOdia, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesOdia, weekdayNamesAbbr: weekdayNamesOdiaAbbr}, + "448": {tags: []string{"or-IN"}, localMonth: localMonthsNameOdia, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesOdia, weekdayNamesAbbr: weekdayNamesOdiaAbbr}, + "72": {tags: []string{"om"}, localMonth: localMonthsNameOromo, apFmt: apFmtOromo, weekdayNames: weekdayNamesOromo, weekdayNamesAbbr: weekdayNamesOromoAbbr}, + "472": {tags: []string{"om-ET"}, localMonth: localMonthsNameOromo, apFmt: apFmtOromo, weekdayNames: weekdayNamesOromo, weekdayNamesAbbr: weekdayNamesOromoAbbr}, + "63": {tags: []string{"ps"}, localMonth: localMonthsNamePashto, apFmt: apFmtPashto, weekdayNames: weekdayNamesPashto, weekdayNamesAbbr: weekdayNamesPashto}, + "463": {tags: []string{"ps-AF"}, localMonth: localMonthsNamePashto, apFmt: apFmtPashto, weekdayNames: weekdayNamesPashto, weekdayNamesAbbr: weekdayNamesPashto}, + "29": {tags: []string{"fa"}, localMonth: localMonthsNamePersian, apFmt: apFmtPersian, weekdayNames: weekdayNamesPersian, weekdayNamesAbbr: weekdayNamesPersian}, + "429": {tags: []string{"fa-IR"}, localMonth: localMonthsNamePersian, apFmt: apFmtPersian, weekdayNames: weekdayNamesPersian, weekdayNamesAbbr: weekdayNamesPersian}, + "15": {tags: []string{"pl"}, localMonth: localMonthsNamePolish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesPolish, weekdayNamesAbbr: weekdayNamesPolishAbbr}, + "415": {tags: []string{"pl-PL"}, localMonth: localMonthsNamePolish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesPolish, weekdayNamesAbbr: weekdayNamesPolishAbbr}, + "16": {tags: []string{"pt"}, localMonth: localMonthsNamePortuguese, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesPortuguese, weekdayNamesAbbr: weekdayNamesPortugueseAbbr}, + "416": {tags: []string{"pt-BR"}, localMonth: localMonthsNamePortuguese, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesPortuguese, weekdayNamesAbbr: weekdayNamesPortugueseAbbr}, + "816": {tags: []string{"pt-PT"}, localMonth: localMonthsNamePortuguese, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesPortuguese, weekdayNamesAbbr: weekdayNamesPortugueseAbbr}, + "46": {tags: []string{"pa"}, localMonth: localMonthsNamePunjabi, apFmt: apFmtPunjabi, weekdayNames: weekdayNamesPunjabi, weekdayNamesAbbr: weekdayNamesPunjabiAbbr}, + "7C46": {tags: []string{"pa-Arab"}, localMonth: localMonthsNamePunjabiArab, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesPunjabiArab, weekdayNamesAbbr: weekdayNamesPunjabiArab}, + "446": {tags: []string{"pa-IN"}, localMonth: localMonthsNamePunjabi, apFmt: apFmtPunjabi, weekdayNames: weekdayNamesPunjabi, weekdayNamesAbbr: weekdayNamesPunjabiAbbr}, + "846": {tags: []string{"pa-Arab-PK"}, localMonth: localMonthsNamePunjabiArab, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesPunjabiArab, weekdayNamesAbbr: weekdayNamesPunjabiArab}, + "6B": {tags: []string{"quz"}, localMonth: localMonthsNameQuechua, apFmt: apFmtCuba, weekdayNames: weekdayNamesQuechua, weekdayNamesAbbr: weekdayNamesQuechuaAbbr}, + "46B": {tags: []string{"quz-BO"}, localMonth: localMonthsNameQuechua, apFmt: apFmtCuba, weekdayNames: weekdayNamesQuechua, weekdayNamesAbbr: weekdayNamesQuechuaAbbr}, + "86B": {tags: []string{"quz-EC"}, localMonth: localMonthsNameQuechuaEcuador, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesQuechuaEcuador, weekdayNamesAbbr: weekdayNamesQuechuaEcuadorAbbr}, + "C6B": {tags: []string{"quz-PE"}, localMonth: localMonthsNameQuechua, apFmt: apFmtCuba, weekdayNames: weekdayNamesQuechuaPeru, weekdayNamesAbbr: weekdayNamesQuechuaPeruAbbr}, + "18": {tags: []string{"ro"}, localMonth: localMonthsNameRomanian, apFmt: apFmtCuba, weekdayNames: weekdayNamesRomanian, weekdayNamesAbbr: weekdayNamesRomanianAbbr}, + "818": {tags: []string{"ro-MD"}, localMonth: localMonthsNameRomanian, apFmt: apFmtCuba, weekdayNames: weekdayNamesRomanian, weekdayNamesAbbr: weekdayNamesRomanianMoldovaAbbr}, + "418": {tags: []string{"ro-RO"}, localMonth: localMonthsNameRomanian, apFmt: apFmtCuba, weekdayNames: weekdayNamesRomanian, weekdayNamesAbbr: weekdayNamesRomanianAbbr}, + "17": {tags: []string{"rm"}, localMonth: localMonthsNameRomansh, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesRomansh, weekdayNamesAbbr: weekdayNamesRomanshAbbr}, + "417": {tags: []string{"rm-CH"}, localMonth: localMonthsNameRomansh, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesRomansh, weekdayNamesAbbr: weekdayNamesRomanshAbbr}, + "19": {tags: []string{"ru"}, localMonth: localMonthsNameRussian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesRussian, weekdayNamesAbbr: weekdayNamesRussianAbbr}, + "819": {tags: []string{"ru-MD"}, localMonth: localMonthsNameRussian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesRussian, weekdayNamesAbbr: weekdayNamesRussianAbbr}, + "419": {tags: []string{"ru-RU"}, localMonth: localMonthsNameRussian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesRussian, weekdayNamesAbbr: weekdayNamesRussianAbbr}, + "85": {tags: []string{"sah"}, localMonth: localMonthsNameSakha, apFmt: apFmtSakha, weekdayNames: weekdayNamesSakha, weekdayNamesAbbr: weekdayNamesSakhaAbbr}, + "485": {tags: []string{"sah-RU"}, localMonth: localMonthsNameSakha, apFmt: apFmtSakha, weekdayNames: weekdayNamesSakha, weekdayNamesAbbr: weekdayNamesSakhaAbbr}, + "703B": {tags: []string{"smn"}, localMonth: localMonthsNameSami, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSami, weekdayNamesAbbr: weekdayNamesSamiAbbr}, + "243B": {tags: []string{"smn-FI"}, localMonth: localMonthsNameSami, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSami, weekdayNamesAbbr: weekdayNamesSamiAbbr}, + "7C3B": {tags: []string{"smj"}, localMonth: localMonthsNameSamiLule, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiSamiLule, weekdayNamesAbbr: weekdayNamesSamiSwedenAbbr}, + "103B": {tags: []string{"smj-NO"}, localMonth: localMonthsNameSamiLule, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiSamiLule, weekdayNamesAbbr: weekdayNamesSamiSamiLuleAbbr}, + "143B": {tags: []string{"smj-SE"}, localMonth: localMonthsNameSamiLule, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiSweden, weekdayNamesAbbr: weekdayNamesSamiSwedenAbbr}, + "3B": {tags: []string{"se"}, localMonth: localMonthsNameSamiNorthern, apFmt: apFmtSamiNorthern, weekdayNames: weekdayNamesSamiNorthern, weekdayNamesAbbr: weekdayNamesSamiNorthernAbbr}, + "C3B": {tags: []string{"se-FI"}, localMonth: localMonthsNameSamiNorthernFI, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiNorthernFI, weekdayNamesAbbr: weekdayNamesSamiNorthernFIAbbr}, + "43B": {tags: []string{"se-NO"}, localMonth: localMonthsNameSamiNorthern, apFmt: apFmtSamiNorthern, weekdayNames: weekdayNamesSamiNorthern, weekdayNamesAbbr: weekdayNamesSamiNorthernAbbr}, + "83B": {tags: []string{"se-SE"}, localMonth: localMonthsNameSamiNorthern, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiNorthernSE, weekdayNamesAbbr: weekdayNamesSamiNorthernSEAbbr}, + "743B": {tags: []string{"sms"}, localMonth: localMonthsNameSamiSkolt, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiSkolt, weekdayNamesAbbr: weekdayNamesSamiSkoltAbbr}, + "203B": {tags: []string{"sms-FI"}, localMonth: localMonthsNameSamiSkolt, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiSkolt, weekdayNamesAbbr: weekdayNamesSamiSkoltAbbr}, + "783B": {tags: []string{"sma"}, localMonth: localMonthsNameSamiSouthern, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiSouthern, weekdayNamesAbbr: weekdayNamesSamiSouthernAbbr}, + "183B": {tags: []string{"sma-NO"}, localMonth: localMonthsNameSamiSouthern, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiSouthern, weekdayNamesAbbr: weekdayNamesSamiSouthernAbbr}, + "1C3B": {tags: []string{"sma-SE"}, localMonth: localMonthsNameSamiSouthern, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiSouthern, weekdayNamesAbbr: weekdayNamesSamiSouthernAbbr}, + "4F": {tags: []string{"sa"}, localMonth: localMonthsNameSanskrit, apFmt: apFmtSanskrit, weekdayNames: weekdayNamesSanskrit, weekdayNamesAbbr: weekdayNamesSanskritAbbr}, + "44F": {tags: []string{"sa-IN"}, localMonth: localMonthsNameSanskrit, apFmt: apFmtSanskrit, weekdayNames: weekdayNamesSanskrit, weekdayNamesAbbr: weekdayNamesSanskritAbbr}, + "91": {tags: []string{"gd"}, localMonth: localMonthsNameScottishGaelic, apFmt: apFmtScottishGaelic, weekdayNames: weekdayNamesGaelic, weekdayNamesAbbr: weekdayNamesGaelicAbbr}, + "491": {tags: []string{"gd-GB"}, localMonth: localMonthsNameScottishGaelic, apFmt: apFmtScottishGaelic, weekdayNames: weekdayNamesGaelic, weekdayNamesAbbr: weekdayNamesGaelicAbbr}, + "6C1A": {tags: []string{"sr-Cyrl"}, localMonth: localMonthsNameSerbian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSerbian, weekdayNamesAbbr: weekdayNamesSerbianAbbr}, + "1C1A": {tags: []string{"sr-Cyrl-BA"}, localMonth: localMonthsNameSerbianBA, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSerbianBA, weekdayNamesAbbr: weekdayNamesSerbianBAAbbr}, + "301A": {tags: []string{"sr-Cyrl-ME"}, localMonth: localMonthsNameSerbian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSerbianME, weekdayNamesAbbr: weekdayNamesSerbianBAAbbr}, + "281A": {tags: []string{"sr-Cyrl-RS"}, localMonth: localMonthsNameSerbian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSerbian, weekdayNamesAbbr: weekdayNamesSerbianAbbr}, + "C1A": {tags: []string{"sr-Cyrl-CS"}, localMonth: localMonthsNameSerbian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSerbian, weekdayNamesAbbr: weekdayNamesSerbianAbbr}, + "701A": {tags: []string{"sr-Latn"}, localMonth: localMonthsNameSerbianLatin, apFmt: apFmtSerbianLatin, weekdayNames: weekdayNamesSerbianLatin, weekdayNamesAbbr: weekdayNamesSerbianLatinAbbr}, + "7C1A": {tags: []string{"sr"}, localMonth: localMonthsNameSerbianLatin, apFmt: apFmtSerbianLatin, weekdayNames: weekdayNamesSerbianLatin, weekdayNamesAbbr: weekdayNamesSerbianLatinAbbr}, + "181A": {tags: []string{"sr-Latn-BA"}, localMonth: localMonthsNameSerbianLatin, apFmt: apFmtSerbianLatinBA, weekdayNames: weekdayNamesSerbianLatinBA, weekdayNamesAbbr: weekdayNamesSerbianLatinBAAbbr}, + "2C1A": {tags: []string{"sr-Latn-ME"}, localMonth: localMonthsNameSerbianLatin, apFmt: apFmtSerbianLatinBA, weekdayNames: weekdayNamesSerbianLatinME, weekdayNamesAbbr: weekdayNamesSerbianLatinAbbr}, + "241A": {tags: []string{"sr-Latn-RS"}, localMonth: localMonthsNameSerbianLatin, apFmt: apFmtSerbianLatin, weekdayNames: weekdayNamesSerbianLatin, weekdayNamesAbbr: weekdayNamesSerbianLatinAbbr}, + "81A": {tags: []string{"sr-Latn-CS"}, localMonth: localMonthsNameSerbianLatinCS, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSerbianLatin, weekdayNamesAbbr: weekdayNamesSerbianLatinCSAbbr}, + "6C": {tags: []string{"nso"}, localMonth: localMonthsNameSesothoSaLeboa, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSesothoSaLeboa, weekdayNamesAbbr: weekdayNamesSesothoSaLeboaAbbr}, + "46C": {tags: []string{"nso-ZA"}, localMonth: localMonthsNameSesothoSaLeboa, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSesothoSaLeboa, weekdayNamesAbbr: weekdayNamesSesothoSaLeboaAbbr}, + "32": {tags: []string{"tn"}, localMonth: localMonthsNameSetswana, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSetswana, weekdayNamesAbbr: weekdayNamesSetswanaAbbr}, + "832": {tags: []string{"tn-BW"}, localMonth: localMonthsNameSetswana, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSetswana, weekdayNamesAbbr: weekdayNamesSetswanaAbbr}, + "432": {tags: []string{"tn-ZA"}, localMonth: localMonthsNameSetswana, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSetswana, weekdayNamesAbbr: weekdayNamesSetswanaAbbr}, + "59": {tags: []string{"sd"}, localMonth: localMonthsNameSindhi, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSindhi, weekdayNamesAbbr: weekdayNamesSindhiAbbr}, + "7C59": {tags: []string{"sd-Arab"}, localMonth: localMonthsNameSindhi, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSindhi, weekdayNamesAbbr: weekdayNamesSindhiAbbr}, + "859": {tags: []string{"sd-Arab-PK"}, localMonth: localMonthsNameSindhi, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSindhi, weekdayNamesAbbr: weekdayNamesSindhiAbbr}, + "5B": {tags: []string{"si"}, localMonth: localMonthsNameSinhala, apFmt: apFmtSinhala, weekdayNames: weekdayNamesSindhi, weekdayNamesAbbr: weekdayNamesSindhiAbbr}, + "45B": {tags: []string{"si-LK"}, localMonth: localMonthsNameSinhala, apFmt: apFmtSinhala, weekdayNames: weekdayNamesSindhi, weekdayNamesAbbr: weekdayNamesSindhiAbbr}, + "1B": {tags: []string{"sk"}, localMonth: localMonthsNameSlovak, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSlovak, weekdayNamesAbbr: weekdayNamesSlovakAbbr}, + "41B": {tags: []string{"sk-SK"}, localMonth: localMonthsNameSlovak, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSlovak, weekdayNamesAbbr: weekdayNamesSlovakAbbr}, + "24": {tags: []string{"sl"}, localMonth: localMonthsNameSlovenian, apFmt: apFmtSlovenian, weekdayNames: weekdayNamesSlovenian, weekdayNamesAbbr: weekdayNamesSlovenianAbbr}, + "424": {tags: []string{"sl-SI"}, localMonth: localMonthsNameSlovenian, apFmt: apFmtSlovenian, weekdayNames: weekdayNamesSlovenian, weekdayNamesAbbr: weekdayNamesSlovenianAbbr}, + "77": {tags: []string{"so"}, localMonth: localMonthsNameSomali, apFmt: apFmtSomali, weekdayNames: weekdayNamesSomali, weekdayNamesAbbr: weekdayNamesSomaliAbbr}, + "477": {tags: []string{"so-SO"}, localMonth: localMonthsNameSomali, apFmt: apFmtSomali, weekdayNames: weekdayNamesSomali, weekdayNamesAbbr: weekdayNamesSomaliAbbr}, + "30": {tags: []string{"st"}, localMonth: localMonthsNameSotho, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSotho, weekdayNamesAbbr: weekdayNamesSothoAbbr}, + "430": {tags: []string{"st-ZA"}, localMonth: localMonthsNameSotho, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSotho, weekdayNamesAbbr: weekdayNamesSothoAbbr}, + "A": {tags: []string{"es"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishAbbr}, + "2C0A": {tags: []string{"es-AR"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + "200A": {tags: []string{"es-VE"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + "400A": {tags: []string{"es-BO"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + "340A": {tags: []string{"es-CL"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + "240A": {tags: []string{"es-CO"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + "140A": {tags: []string{"es-CR"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + "5C0A": {tags: []string{"es-CU"}, localMonth: localMonthsNameSpanish, apFmt: apFmtCuba, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + "1C0A": {tags: []string{"es-DO"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + "300A": {tags: []string{"es-EC"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + "440A": {tags: []string{"es-SV"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + "100A": {tags: []string{"es-GT"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + "480A": {tags: []string{"es-HN"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + "580A": {tags: []string{"es-419"}, localMonth: localMonthsNameSpanish, apFmt: apFmtCuba, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + "80A": {tags: []string{"es-MX"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + "4C0A": {tags: []string{"es-NI"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + "180A": {tags: []string{"es-PA"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + "3C0A": {tags: []string{"es-PY"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + "280A": {tags: []string{"es-PE"}, localMonth: localMonthsNameSpanishPE, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + "500A": {tags: []string{"es-PR"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + "40A": {tags: []string{"es-ES_tradnl"}, localMonth: localMonthsNameSpanish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishAbbr}, + "C0A": {tags: []string{"es-ES"}, localMonth: localMonthsNameSpanish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishAbbr}, + "540A": {tags: []string{"es-US"}, localMonth: localMonthsNameSpanish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishUSAbbr}, + "380A": {tags: []string{"es-UY"}, localMonth: localMonthsNameSpanishPE, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + "1D": {tags: []string{"sv"}, localMonth: localMonthsNameSwedish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSwedish, weekdayNamesAbbr: weekdayNamesSwedishAbbr}, + "81D": {tags: []string{"sv-FI"}, localMonth: localMonthsNameSwedishFI, apFmt: apFmtSwedish, weekdayNames: weekdayNamesSwedish, weekdayNamesAbbr: weekdayNamesSwedishAbbr}, + "41D": {tags: []string{"sv-SE"}, localMonth: localMonthsNameSwedishFI, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSwedish, weekdayNamesAbbr: weekdayNamesSwedishAbbr}, + "5A": {tags: []string{"syr"}, localMonth: localMonthsNameSyriac, apFmt: apFmtSyriac, weekdayNames: weekdayNamesSyriac, weekdayNamesAbbr: weekdayNamesSyriacAbbr}, + "45A": {tags: []string{"syr-SY"}, localMonth: localMonthsNameSyriac, apFmt: apFmtSyriac, weekdayNames: weekdayNamesSyriac, weekdayNamesAbbr: weekdayNamesSyriacAbbr}, + "28": {tags: []string{"tg"}, localMonth: localMonthsNameTajik, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTajik, weekdayNamesAbbr: weekdayNamesTajikAbbr}, + "7C28": {tags: []string{"tg-Cyrl"}, localMonth: localMonthsNameTajik, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTajik, weekdayNamesAbbr: weekdayNamesTajikAbbr}, + "428": {tags: []string{"tg-Cyrl-TJ"}, localMonth: localMonthsNameTajik, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTajik, weekdayNamesAbbr: weekdayNamesTajikAbbr}, + "5F": {tags: []string{"tzm"}, localMonth: localMonthsNameTamazight, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTamazight, weekdayNamesAbbr: weekdayNamesTamazightAbbr}, + "7C5F": {tags: []string{"tzm-Latn"}, localMonth: localMonthsNameTamazight, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTamazight, weekdayNamesAbbr: weekdayNamesTamazightAbbr}, + "85F": {tags: []string{"tzm-Latn-DZ"}, localMonth: localMonthsNameTamazight, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTamazight, weekdayNamesAbbr: weekdayNamesTamazightAbbr}, + "49": {tags: []string{"ta"}, localMonth: localMonthsNameTamil, apFmt: apFmtTamil, weekdayNames: weekdayNamesTamil, weekdayNamesAbbr: weekdayNamesTamilAbbr}, + "449": {tags: []string{"ta-IN"}, localMonth: localMonthsNameTamil, apFmt: apFmtTamil, weekdayNames: weekdayNamesTamil, weekdayNamesAbbr: weekdayNamesTamilAbbr}, + "849": {tags: []string{"ta-LK"}, localMonth: localMonthsNameTamilLK, apFmt: apFmtTamil, weekdayNames: weekdayNamesTamilLK, weekdayNamesAbbr: weekdayNamesTamilLKAbbr}, + "44": {tags: []string{"tt"}, localMonth: localMonthsNameTatar, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTatar, weekdayNamesAbbr: weekdayNamesTatarAbbr}, + "444": {tags: []string{"tt-RU"}, localMonth: localMonthsNameTatar, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTatar, weekdayNamesAbbr: weekdayNamesTatarAbbr}, + "4A": {tags: []string{"te"}, localMonth: localMonthsNameTelugu, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTelugu, weekdayNamesAbbr: weekdayNamesTeluguAbbr}, + "44A": {tags: []string{"te-IN"}, localMonth: localMonthsNameTelugu, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTelugu, weekdayNamesAbbr: weekdayNamesTeluguAbbr}, + "1E": {tags: []string{"th"}, localMonth: localMonthsNameThai, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesThai, weekdayNamesAbbr: weekdayNamesThaiAbbr}, + "41E": {tags: []string{"th-TH"}, localMonth: localMonthsNameThai, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesThai, weekdayNamesAbbr: weekdayNamesThaiAbbr}, + "51": {tags: []string{"bo"}, localMonth: localMonthsNameTibetan, apFmt: apFmtTibetan, weekdayNames: weekdayNamesTibetan, weekdayNamesAbbr: weekdayNamesTibetanAbbr}, + "451": {tags: []string{"bo-CN"}, localMonth: localMonthsNameTibetan, apFmt: apFmtTibetan, weekdayNames: weekdayNamesTibetan, weekdayNamesAbbr: weekdayNamesTibetanAbbr}, + "73": {tags: []string{"ti"}, localMonth: localMonthsNameTigrinya, apFmt: apFmtTigrinya, weekdayNames: weekdayNamesTigrinya, weekdayNamesAbbr: weekdayNamesTigrinyaAbbr}, + "873": {tags: []string{"ti-ER"}, localMonth: localMonthsNameTigrinya, apFmt: apFmtTigrinyaER, weekdayNames: weekdayNamesTigrinya, weekdayNamesAbbr: weekdayNamesTigrinyaAbbr}, + "473": {tags: []string{"ti-ET"}, localMonth: localMonthsNameTigrinya, apFmt: apFmtTigrinya, weekdayNames: weekdayNamesTigrinya, weekdayNamesAbbr: weekdayNamesTigrinyaAbbr}, + "31": {tags: []string{"ts"}, localMonth: localMonthsNameTsonga, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTsonga, weekdayNamesAbbr: weekdayNamesTsongaAbbr}, + "431": {tags: []string{"ts-ZA"}, localMonth: localMonthsNameTsonga, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTsonga, weekdayNamesAbbr: weekdayNamesTsongaAbbr}, + "1F": {tags: []string{"tr"}, localMonth: localMonthsNameTurkish, apFmt: apFmtTurkish, weekdayNames: weekdayNamesTurkish, weekdayNamesAbbr: weekdayNamesTurkishAbbr}, + "41F": {tags: []string{"tr-TR"}, localMonth: localMonthsNameTurkish, apFmt: apFmtTurkish, weekdayNames: weekdayNamesTurkish, weekdayNamesAbbr: weekdayNamesTurkishAbbr}, + "42": {tags: []string{"tk"}, localMonth: localMonthsNameTurkmen, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTurkmen, weekdayNamesAbbr: weekdayNamesTurkmenAbbr}, + "442": {tags: []string{"tk-TM"}, localMonth: localMonthsNameTurkmen, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTurkmen, weekdayNamesAbbr: weekdayNamesTurkmenAbbr}, + "22": {tags: []string{"uk"}, localMonth: localMonthsNameUkrainian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesUkrainian, weekdayNamesAbbr: weekdayNamesUkrainianAbbr}, + "422": {tags: []string{"uk-UA"}, localMonth: localMonthsNameUkrainian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesUkrainian, weekdayNamesAbbr: weekdayNamesUkrainianAbbr}, + "2E": {tags: []string{"hsb"}, localMonth: localMonthsNameUpperSorbian, apFmt: apFmtUpperSorbian, weekdayNames: weekdayNamesSorbian, weekdayNamesAbbr: weekdayNamesSorbianAbbr}, + "42E": {tags: []string{"hsb-DE"}, localMonth: localMonthsNameUpperSorbian, apFmt: apFmtUpperSorbian, weekdayNames: weekdayNamesSorbian, weekdayNamesAbbr: weekdayNamesSorbianAbbr}, + "20": {tags: []string{"ur"}, localMonth: localMonthsNamePunjabiArab, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesUrdu, weekdayNamesAbbr: weekdayNamesUrdu}, + "820": {tags: []string{"ur-IN"}, localMonth: localMonthsNamePunjabiArab, apFmt: apFmtUrdu, weekdayNames: weekdayNamesUrduIN, weekdayNamesAbbr: weekdayNamesUrduIN}, + "420": {tags: []string{"ur-PK"}, localMonth: localMonthsNamePunjabiArab, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesUrdu, weekdayNamesAbbr: weekdayNamesUrdu}, + "80": {tags: []string{"ug"}, localMonth: localMonthsNameUyghur, apFmt: apFmtUyghur, weekdayNames: weekdayNamesUyghur, weekdayNamesAbbr: weekdayNamesUyghurAbbr}, + "480": {tags: []string{"ug-CN"}, localMonth: localMonthsNameUyghur, apFmt: apFmtUyghur, weekdayNames: weekdayNamesUyghur, weekdayNamesAbbr: weekdayNamesUyghurAbbr}, + "7843": {tags: []string{"uz-Cyrl"}, localMonth: localMonthsNameUzbekCyrillic, apFmt: apFmtUzbekCyrillic, weekdayNames: weekdayNamesUzbekCyrillic, weekdayNamesAbbr: weekdayNamesUzbekCyrillicAbbr}, + "843": {tags: []string{"uz-Cyrl-UZ"}, localMonth: localMonthsNameUzbekCyrillic, apFmt: apFmtUzbekCyrillic, weekdayNames: weekdayNamesUzbekCyrillic, weekdayNamesAbbr: weekdayNamesUzbekCyrillicAbbr}, + "43": {tags: []string{"uz"}, localMonth: localMonthsNameUzbek, apFmt: apFmtUzbek, weekdayNames: weekdayNamesUzbek, weekdayNamesAbbr: weekdayNamesUzbekAbbr}, + "7C43": {tags: []string{"uz-Latn"}, localMonth: localMonthsNameUzbek, apFmt: apFmtUzbek, weekdayNames: weekdayNamesUzbek, weekdayNamesAbbr: weekdayNamesUzbekAbbr}, + "443": {tags: []string{"uz-Latn-UZ"}, localMonth: localMonthsNameUzbek, apFmt: apFmtUzbek, weekdayNames: weekdayNamesUzbek, weekdayNamesAbbr: weekdayNamesUzbekAbbr}, + "803": {tags: []string{"ca-ES-valencia"}, localMonth: localMonthsNameValencian, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesValencian, weekdayNamesAbbr: weekdayNamesValencianAbbr}, + "33": {tags: []string{"ve"}, localMonth: localMonthsNameVenda, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesVenda, weekdayNamesAbbr: weekdayNamesVendaAbbr}, + "433": {tags: []string{"ve-ZA"}, localMonth: localMonthsNameVenda, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesVenda, weekdayNamesAbbr: weekdayNamesVendaAbbr}, + "2A": {tags: []string{"vi"}, localMonth: localMonthsNameVietnamese, apFmt: apFmtVietnamese, weekdayNames: weekdayNamesVietnamese, weekdayNamesAbbr: weekdayNamesVietnameseAbbr}, + "42A": {tags: []string{"vi-VN"}, localMonth: localMonthsNameVietnamese, apFmt: apFmtVietnamese, weekdayNames: weekdayNamesVietnamese, weekdayNamesAbbr: weekdayNamesVietnameseAbbr}, + "52": {tags: []string{"cy"}, localMonth: localMonthsNameWelsh, apFmt: apFmtWelsh, weekdayNames: weekdayNamesWelsh, weekdayNamesAbbr: weekdayNamesWelshAbbr}, + "452": {tags: []string{"cy-GB"}, localMonth: localMonthsNameWelsh, apFmt: apFmtWelsh, weekdayNames: weekdayNamesWelsh, weekdayNamesAbbr: weekdayNamesWelshAbbr}, + "88": {tags: []string{"wo"}, localMonth: localMonthsNameWolof, apFmt: apFmtWolof, weekdayNames: weekdayNamesWolof, weekdayNamesAbbr: weekdayNamesWolofAbbr}, + "488": {tags: []string{"wo-SN"}, localMonth: localMonthsNameWolof, apFmt: apFmtWolof, weekdayNames: weekdayNamesWolof, weekdayNamesAbbr: weekdayNamesWolofAbbr}, + "34": {tags: []string{"xh"}, localMonth: localMonthsNameXhosa, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesXhosa, weekdayNamesAbbr: weekdayNamesXhosaAbbr}, + "434": {tags: []string{"xh-ZA"}, localMonth: localMonthsNameXhosa, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesXhosa, weekdayNamesAbbr: weekdayNamesXhosaAbbr}, + "78": {tags: []string{"ii"}, localMonth: localMonthsNameYi, apFmt: apFmtYi, weekdayNames: weekdayNamesYi, weekdayNamesAbbr: weekdayNamesYiAbbr}, + "478": {tags: []string{"ii-CN"}, localMonth: localMonthsNameYi, apFmt: apFmtYi, weekdayNames: weekdayNamesYi, weekdayNamesAbbr: weekdayNamesYiAbbr}, + "43D": {tags: []string{"yi-001"}, localMonth: localMonthsNameYiddish, apFmt: apFmtYiddish, weekdayNames: weekdayNamesYiddish, weekdayNamesAbbr: weekdayNamesYiddishAbbr}, + "6A": {tags: []string{"yo"}, localMonth: localMonthsNameYoruba, apFmt: apFmtYoruba, weekdayNames: weekdayNamesYoruba, weekdayNamesAbbr: weekdayNamesYorubaAbbr}, + "46A": {tags: []string{"yo-NG"}, localMonth: localMonthsNameYoruba, apFmt: apFmtYoruba, weekdayNames: weekdayNamesYoruba, weekdayNamesAbbr: weekdayNamesYorubaAbbr}, + "35": {tags: []string{"zu"}, localMonth: localMonthsNameZulu, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesZulu, weekdayNamesAbbr: weekdayNamesZuluAbbr}, + "435": {tags: []string{"zu-ZA"}, localMonth: localMonthsNameZulu, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesZulu, weekdayNamesAbbr: weekdayNamesZuluAbbr}, } // japaneseEraYears list the Japanese era name periods. japaneseEraYears = []time.Time{ @@ -1053,6 +1173,172 @@ var ( monthNamesAfrikaans = []string{"Januarie", "Februarie", "Maart", "April", "Mei", "Junie", "Julie", "Augustus", "September", "Oktober", "November", "Desember"} // monthNamesAfrikaansAbbr lists the month name abbreviations in the Afrikaans. monthNamesAfrikaansAbbr = []string{"Jan.", "Feb.", "Maa.", "Apr.", "Mei", "Jun.", "Jul.", "Aug.", "Sep.", "Okt.", "Nov.", "Des."} + // monthNamesAlbanian list the month names in the Albanian. + monthNamesAlbanian = []string{"janar", "shkurt", "mars", "prill", "maj", "qershor", "korrik", "gusht", "shtator", "tetor", "nëntor", "dhjetor"} + // monthNamesAlbanianAbbr lists the month name abbreviations in the Albanian. + monthNamesAlbanianAbbr = []string{"jan", "shk", "mar", "pri", "maj", "qer", "krr", "gush", "sht", "tet", "nën", "dhj"} + // monthNamesAlsatian list the month names in the Alsatian. + monthNamesAlsatian = []string{"Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "Auguscht", "Septämber", "Oktoober", "Novämber", "Dezämber"} + // monthNamesAlsatianAbbr lists the month name abbreviations in the Alsatian France. + monthNamesAlsatianAbbr = []string{"Jan", "Feb", "Mär", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"} + // monthNamesAlsatianFrance list the month names in the Alsatian. + monthNamesAlsatianFrance = []string{"Jänner", "Feverje", "März", "Àpril", "Mai", "Jüni", "Jüli", "Augscht", "September", "Oktower", "Nowember", "Dezember"} + // monthNamesAlsatianFranceAbbr lists the month name abbreviations in the Alsatian France. + monthNamesAlsatianFranceAbbr = []string{"Jän.", "Fev.", "März", "Apr.", "Mai", "Jüni", "Jüli", "Aug.", "Sept.", "Okt.", "Now.", "Dez."} + // monthNamesAmharic list the month names in the Amharic. + monthNamesAmharic = []string{ + "\u1303\u1295\u12E9\u12C8\u122A", + "\u134C\u1265\u1229\u12C8\u122A", + "\u121B\u122D\u127D", + "\u12A4\u1355\u122A\u120D", + "\u121C\u12ED", + "\u1301\u1295", + "\u1301\u120B\u12ED", + "\u12A6\u1308\u1235\u1275", + "\u1234\u1355\u1274\u121D\u1260\u122D", + "\u12A6\u12AD\u1276\u1260\u122D", + "\u1296\u126C\u121D\u1260\u122D", + "\u12F2\u1234\u121D\u1260\u122D", + } + // monthNamesAmharicAbbr lists the month name abbreviations in the Amharic. + monthNamesAmharicAbbr = []string{ + "\u1303\u1295\u12E9", + "\u134C\u1265\u1229", + "\u121B\u122D\u127D", + "\u12A4\u1355\u122A", + "\u121C\u12ED", + "\u1301\u1295", + "\u1301\u120B\u12ED", + "\u12A6\u1308\u1235", + "\u1234\u1355\u1274", + "\u12A6\u12AD\u1276", + "\u1296\u126C\u121D", + "\u12F2\u1234\u121D", + } + // monthNamesArabic list the month names in the Arabic. + monthNamesArabic = []string{ + "\u064A\u0646\u0627\u064A\u0631", + "\u0641\u0628\u0631\u0627\u064A\u0631", + "\u0645\u0627\u0631\u0633", + "\u0623\u0628\u0631\u064A\u0644", + "\u0645\u0627\u064A\u0648", + "\u064A\u0648\u0646\u064A\u0648", + "\u064A\u0648\u0644\u064A\u0648", + "\u0623\u063A\u0633\u0637\u0633", + "\u0633\u0628\u062A\u0645\u0628\u0631", + "\u0623\u0643\u062A\u0648\u0628\u0631", + "\u0646\u0648\u0641\u0645\u0628\u0631", + "\u062F\u064A\u0633\u0645\u0628\u0631", + } + // monthNamesArabicIraq list the month names in the Arabic Iraq. + monthNamesArabicIraq = []string{ + "\u0643\u0627\u0646\u0648\u0646%A0\u0627\u0644\u062B\u0627\u0646\u064A", + "\u0634\u0628\u0627\u0637", + "\u0622\u0630\u0627\u0631", + "\u0646\u064A\u0633\u0627\u0646", + "\u0623\u064A\u0627\u0631", + "\u062D\u0632\u064A\u0631\u0627\u0646", + "\u062A\u0645\u0648\u0632", + "\u0622\u0628", + "\u0623\u064A\u0644\u0648\u0644", + "\u062A\u0634\u0631\u064A\u0646%A0\u0627\u0644\u0623\u0648\u0644", + "\u062A\u0634\u0631\u064A\u0646%A0\u0627\u0644\u062B\u0627\u0646\u064A", + "\u0643\u0627\u0646\u0648\u0646%A0\u0627\u0644\u0623\u0648\u0644", + } + // monthNamesArmenian list the month names in the Armenian. + monthNamesArmenian = []string{ + "\u0540\u0578\u0582\u0576\u057E\u0561\u0580", + "\u0553\u0565\u057F\u0580\u057E\u0561\u0580", + "\u0544\u0561\u0580\u057F", + "\u0531\u057A\u0580\u056B\u056C", + "\u0544\u0561\u0575\u056B\u057D", + "\u0540\u0578\u0582\u0576\u056B\u057D", + "\u0540\u0578\u0582\u056C\u056B\u057D", + "\u0555\u0563\u0578\u057D\u057F\u0578\u057D", + "\u054D\u0565\u057A\u057F\u0565\u0574\u0562\u0565\u0580", + "\u0540\u0578\u056F\u057F\u0565\u0574\u0562\u0565\u0580", + "\u0546\u0578\u0575\u0565\u0574\u0562\u0565\u0580", + "\u0534\u0565\u056F\u057F\u0565\u0574\u0562\u0565\u0580", + } + // monthNamesArmenianAbbr lists the month name abbreviations in the Armenian. + monthNamesArmenianAbbr = []string{ + "\u0540\u0576\u057E", + "\u0553\u057F\u057E", + "\u0544\u0580\u057F", + "\u0531\u057A\u0580", + "\u0544\u0575\u057D", + "\u0540\u0576\u057D", + "\u0540\u056C\u057D", + "\u0555\u0563\u057D", + "\u054D\u057A\u057F", + "\u0540\u056F\u057F", + "\u0546\u0575\u0574", + "\u0534\u056F\u057F", + } + // monthNamesAssamese list the month names in the Assamese. + monthNamesAssamese = []string{ + "\u099C\u09BE\u09A8\u09C1\u09F1\u09BE\u09F0\u09C0", + "\u09AB\u09C7\u09AC\u09CD\u09B0\u09C1\u09F1\u09BE\u09F0\u09C0", + "\u09AE\u09BE\u09B0\u09CD\u099A", + "\u098F\u09AA\u09CD\u09B0\u09BF\u09B2", + "\u09AE\u09C7", + "\u099C\u09C1\u09A8", + "\u099C\u09C1\u09B2\u09BE\u0987", + "\u0986\u0997\u09B7\u09CD\u099F", + "\u099A\u09C7\u09AA\u09CD\u099F\u09C7\u09AE\u09CD\u09AC\u09F0", + "\u0985\u0995\u09CD\u099F\u09CB\u09AC\u09F0", + "\u09A8\u09AC\u09C7\u09AE\u09CD\u09AC\u09F0", + "\u09A1\u09BF\u099A\u09C7\u09AE\u09CD\u09AC\u09F0", + } + // monthNamesAssameseAbbr lists the month name abbreviations in the Assamese. + monthNamesAssameseAbbr = []string{ + "\u099C\u09BE\u09A8\u09C1", + "\u09AB\u09C7\u09AC\u09CD\u09B0\u09C1", + "\u09AE\u09BE\u09B0\u09CD\u099A", + "\u098F\u09AA\u09CD\u09B0\u09BF\u09B2", + "\u09AE\u09C7", + "\u099C\u09C1\u09A8", + "\u099C\u09C1\u09B2\u09BE\u0987", + "\u0986\u0997\u09B7\u09CD\u099F", + "\u099A\u09C7\u09AA\u09CD\u099F\u09C7", + "\u0985\u0995\u09CD\u099F\u09CB", + "\u09A8\u09AC\u09C7", + "\u09A1\u09BF\u099A\u09C7", + } + // monthNamesAzerbaijaniCyrillic list the month names in the Azerbaijani (Cyrillic). + monthNamesAzerbaijaniCyrillic = []string{ + "j\u0430\u043D\u0432\u0430\u0440", + "\u0444\u0435\u0432\u0440\u0430\u043B", + "\u043C\u0430\u0440\u0442", + "\u0430\u043F\u0440\u0435\u043B", + "\u043C\u0430\u0458", + "\u0438\u0458\u0443\u043D", + "\u0438\u0458\u0443\u043B", + "\u0430\u0432\u0433\u0443\u0441\u0442", + "\u0441\u0435\u043D\u0442\u0458\u0430\u0431\u0440", + "\u043E\u043A\u0442\u0458\u0430\u0431\u0440", + "\u043D\u043E\u0458\u0430\u0431\u0440", + "\u0434\u0435\u043A\u0430\u0431\u0440", + } + // monthNamesAzerbaijaniCyrillicAbbr lists the month name abbreviations in the Azerbaijani (Cyrillic). + monthNamesAzerbaijaniCyrillicAbbr = []string{ + "\u0408\u0430\u043D", + "\u0424\u0435\u0432", + "\u041C\u0430\u0440", + "\u0410\u043F\u0440", + "\u041C\u0430\u0458", + "\u0418\u0458\u0443\u043D", + "\u0418\u0458\u0443\u043B", + "\u0410\u0432\u0433", + "\u0421\u0435\u043D", + "\u041E\u043A\u0442", + "\u041D\u043E\u044F", + "\u0414\u0435\u043A", + } + // monthNamesAzerbaijani list the month names in the Azerbaijani. + monthNamesAzerbaijani = []string{"yanvar", "fevral", "mart", "aprel", "may", "iyun", "iyul", "avgust", "sentyabr", "oktyabr", "noyabr", "dekabr"} + // monthNamesAzerbaijaniAbbr lists the month name abbreviations in the Azerbaijani. + monthNamesAzerbaijaniAbbr = []string{"yan", "fev", "mar", "apr", "may", "iyn", "iyl", "avq", "sen", "okt", "noy", "dek"} // monthNamesAustria list the month names in the Austrian. monthNamesAustria = []string{"Jänner", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"} // monthNamesAustriaAbbr list the month name abbreviations in the Austrian. @@ -1072,10 +1358,202 @@ var ( "\u09A8\u09AD\u09C7\u09AE\u09CD\u09AC\u09B0", "\u09A1\u09BF\u09B8\u09C7\u09AE\u09CD\u09AC\u09B0", } + // monthNamesBashkir list the month names in the Bashkir. + monthNamesBashkir = []string{ + "\u0493\u0438\u043D\u0443\u0430\u0440", + "\u0444\u0435\u0432\u0440\u0430\u043B\u044C", + "\u043C\u0430\u0440\u0442", + "\u0430\u043F\u0440\u0435\u043B\u044C", + "\u043C\u0430\u0439", + "\u0438\u044E\u043D\u044C", + "\u0438\u044E\u043B\u044C", + "\u0430\u0432\u0433\u0443\u0441\u0442", + "\u0441\u0435\u043D\u0442\u044F\u0431\u0440\u044C", + "\u043E\u043A\u0442\u044F\u0431\u0440\u044C", + "\u043D\u043E\u044F\u0431\u0440\u044C", + "\u0434\u0435\u043A\u0430\u0431\u0440\u044C", + } + // monthNamesBashkirAbbr lists the month name abbreviations in the Bashkir. + monthNamesBashkirAbbr = []string{ + "\u0493\u0438\u043D", + "\u0444\u0435\u0432", + "\u043C\u0430\u0440", + "\u0430\u043F\u0440", + "\u043C\u0430\u0439", + "\u0438\u044E\u043D", + "\u0438\u044E\u043B", + "\u0430\u0432\u0433", + "\u0441\u0435\u043D", + "\u043E\u043A\u0442", + "\u043D\u043E\u044F", + "\u0434\u0435\u043A", + } + // monthNamesBasque list the month names in the Basque. + monthNamesBasque = []string{"urtarrila", "otsaila", "martxoa", "apirila", "maiatza", "ekaina", "uztaila", "abuztua", "iraila", "urria", "azaroa", "abendua"} + // monthNamesBasqueAbbr lists the month name abbreviations in the Basque. + monthNamesBasqueAbbr = []string{"urt.", "ots.", "mar.", "api.", "mai.", "eka.", "uzt.", "abu.", "ira.", "urr.", "aza.", "abe."} + // monthNamesBelarusian list the month names in the Belarusian. + monthNamesBelarusian = []string{ + "\u0441\u0442\u0443\u0434\u0437\u0435\u043D\u044C", + "\u043B\u044E\u0442\u044B", + "\u0441\u0430\u043A\u0430\u0432\u0456\u043A", + "\u043A\u0440\u0430\u0441\u0430\u0432\u0456\u043A", + "\u043C\u0430\u0439", + "\u0447\u044D\u0440\u0432\u0435\u043D\u044C", + "\u043B\u0456\u043F\u0435\u043D\u044C", + "\u0436\u043D\u0456\u0432\u0435\u043D\u044C", + "\u0432\u0435\u0440\u0430\u0441\u0435\u043D\u044C", + "\u043A\u0430\u0441\u0442\u0440\u044B\u0447\u043D\u0456\u043A", + "\u043B\u0456\u0441\u0442\u0430\u043F\u0430\u0434", + "\u0441\u043D\u0435\u0436\u0430\u043D\u044C", + } + // monthNamesBelarusianAbbr lists the month name abbreviations in the Belarusian. + monthNamesBelarusianAbbr = []string{ + "\u0441\u0442\u0443\u0434\u0437", + "\u043B\u044E\u0442", + "\u0441\u0430\u043A", + "\u043A\u0440\u0430\u0441", + "\u043C\u0430\u0439", + "\u0447\u044D\u0440\u0432", + "\u043B\u0456\u043F", + "\u0436\u043D", + "\u0432\u0435\u0440", + "\u043A\u0430\u0441\u0442\u0440", + "\u043B\u0456\u0441\u0442", + "\u0441\u043D\u0435\u0436", + } + // monthNamesBosnianCyrillic list the month names in the Bosnian (Cyrillic). + monthNamesBosnianCyrillic = []string{ + "\u0458\u0430\u043D\u0443\u0430\u0440", + "\u0444\u0435\u0431\u0440\u0443\u0430\u0440", + "\u043C\u0430\u0440\u0442", + "\u0430\u043F\u0440\u0438\u043B", + "\u043C\u0430\u0458", + "\u0458\u0443\u043D", + "\u0458\u0443\u043B", + "\u0430\u0432\u0433\u0443\u0441\u0442", + "\u0441\u0435\u043F\u0442\u0435\u043C\u0431\u0430\u0440", + "\u043E\u043A\u0442\u043E\u0431\u0430\u0440", + "\u043D\u043E\u0432\u0435\u043C\u0431\u0430\u0440", + "\u0434\u0435\u0446\u0435\u043C\u0431\u0430\u0440", + } + // monthNamesBosnianCyrillicAbbr lists the month name abbreviations in the Bosnian (Cyrillic). + monthNamesBosnianCyrillicAbbr = []string{ + "\u0458\u0430\u043D", + "\u0444\u0435\u0431", + "\u043C\u0430\u0440", + "\u0430\u043F\u0440", + "\u043C\u0430\u0458", + "\u0458\u0443\u043D", + "\u0458\u0443\u043B", + "\u0430\u0432\u0433", + "\u0441\u0435\u043F", + "\u043E\u043A\u0442", + "\u043D\u043E\u0432", + "\u0434\u0435\u0446", + } + // monthNamesBosnian list the month names in the Bosnian. + monthNamesBosnian = []string{"januar", "februar", "mart", "april", "maj", "juni", "juli", "august", "septembar", "oktobar", "novembar", "decembar"} + // monthNamesBosnianAbbr lists the month name abbreviations in the Bosnian. + monthNamesBosnianAbbr = []string{"jan", "feb", "mar", "apr", "maj", "jun", "jul", "aug", "sep", "okt", "nov", "dec"} + // monthNamesBreton list the month names in the Breton. + monthNamesBreton = []string{"Genver", "Cʼhwevrer", "Meurzh", "Ebrel", "Mae", "Mezheven", "Gouere", "Eost", "Gwengolo", "Here", "Du", "Kerzu"} + // monthNamesBretonAbbr lists the month name abbreviations in the Breton. + monthNamesBretonAbbr = []string{"Gen.", "Cʼhwe.", "Meur.", "Ebr.", "Mae", "Mezh.", "Goue.", "Eost", "Gwen.", "Here", "Du", "Kzu."} + // monthNamesBulgarian list the month names in the Bulgarian. + monthNamesBulgarian = []string{ + "\u044F\u043D\u0443\u0430\u0440\u0438", + "\u0444\u0435\u0432\u0440\u0443\u0430\u0440\u0438", + "\u043C\u0430\u0440\u0442", + "\u0430\u043F\u0440\u0438\u043B", + "\u043C\u0430\u0439", + "\u044E\u043D\u0438", + "\u044E\u043B\u0438", + "\u0430\u0432\u0433\u0443\u0441\u0442", + "\u0441\u0435\u043F\u0442\u0435\u043C\u0432\u0440\u0438", + "\u043E\u043A\u0442\u043E\u043C\u0432\u0440\u0438", + "\u043D\u043E\u0435\u043C\u0432\u0440\u0438", + "\u0434\u0435\u043A\u0435\u043C\u0432\u0440\u0438", + } + // monthNamesBurmese list the month names in the Burmese. + monthNamesBurmese = []string{ + "\u1007\u1014\u103A\u1014\u101D\u102B\u101B\u102E", + "\u1016\u1031\u1016\u1031\u102C\u103A\u101D\u102B\u101B\u102E", + "\u1019\u1010\u103A", + "\u1027\u1015\u103C\u102E", + "\u1019\u1031", + "\u1007\u103D\u1014\u103A", + "\u1007\u1030\u101C\u102D\u102F\u1004\u103A", + "\u1029\u1002\u102F\u1010\u103A", + "\u1005\u1000\u103A\u1010\u1004\u103A\u1018\u102C", + "\u1021\u1031\u102C\u1000\u103A\u1010\u102D\u102F\u1018\u102C", + "\u1014\u102D\u102F\u101D\u1004\u103A\u1018\u102C", + "\u1012\u102E\u1007\u1004\u103A\u1018\u102C", + } + // monthNamesBurmeseAbbr lists the month name abbreviations in the Burmese. + monthNamesBurmeseAbbr = []string{ + "\u1007\u1014\u103A", + "\u1016\u1031", + "\u1019\u1010\u103A", + "\u1027", + "\u1019\u1031", + "\u1007\u103D\u1014\u103A", + "\u1007\u1030", + "\u1029", + "\u1005\u1000\u103A", + "\u1021\u1031\u102C\u1000\u103A", + "\u1014\u102D\u102F", + "\u1012\u102E", + } // monthNamesCaribbean list the month names in the Caribbean. monthNamesCaribbean = []string{"Janvier", "Février", "Mars", "Avril", "Mai", "Juin", "Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre"} // monthNamesCaribbeanAbbr lists the month name abbreviations in the Caribbean. monthNamesCaribbeanAbbr = []string{"Janv.", "Févr.", "Mars", "Avr.", "Mai", "Juin", "Juil.", "Août", "Sept.", "Oct.", "Nov.", "Déc."} + // monthNamesCentralKurdish list the month names in the Central Kurdish. + monthNamesCentralKurdish = []string{ + "\u06A9\u0627\u0646\u0648\u0648\u0646\u06CC%20\u062F\u0648\u0648\u06D5\u0645", + "\u0634\u0648\u0628\u0627\u062A", + "\u0626\u0627\u0632\u0627\u0631", + "\u0646\u06CC\u0633\u0627\u0646", + "\u0626\u0627\u06CC\u0627\u0631", + "\u062D\u0648\u0632\u06D5\u06CC\u0631\u0627\u0646", + "\u062A\u06D5\u0645\u0648\u0648\u0632", + "\u0626\u0627\u0628", + "\u0626\u06D5\u06CC\u0644\u0648\u0648\u0644", + "\u062A\u0634\u0631\u06CC\u0646\u06CC%20\u06CC\u06D5\u06A9\u06D5\u0645", + "\u062A\u0634\u0631\u06CC\u0646\u06CC%20\u062F\u0648\u0648\u06D5\u0645", + "\u06A9\u0627\u0646\u0648\u0646\u06CC%20\u06CC\u06D5\u06A9\u06D5\u0645", + } + // monthNamesCherokee list the month names in the Cherokee. + monthNamesCherokee = []string{ + "\u13A4\u13C3\u13B8\u13D4\u13C5", + "\u13A7\u13A6\u13B5", + "\u13A0\u13C5\u13F1", + "\u13DD\u13EC\u13C2", + "\u13A0\u13C2\u13CD\u13AC\u13D8", + "\u13D5\u13AD\u13B7\u13F1", + "\u13AB\u13F0\u13C9\u13C2", + "\u13A6\u13B6\u13C2", + "\u13DA\u13B5\u13CD\u13D7", + "\u13DA\u13C2\u13C5\u13D7", + "\u13C5\u13D3\u13D5\u13C6", + "\u13A4\u13CD\u13A9\u13F1", + } + // monthNamesCherokeeAbbr lists the month name abbreviations in the Cherokee. + monthNamesCherokeeAbbr = []string{ + "\u13A4\u13C3\u13B8", + "\u13A7\u13A6\u13B5", + "\u13A0\u13C5\u13F1", + "\u13DD\u13EC\u13C2", + "\u13A0\u13C2\u13CD", + "\u13D5\u13AD\u13B7", + "\u13AB\u13F0\u13C9", + "\u13A6\u13B6\u13C2", + "\u13DA\u13B5\u13CD", + "\u13DA\u13C2\u13C5", + "\u13C5\u13D3\u13D5", + "\u13A4\u13CD\u13A9", + } // monthNamesChinese list the month names in the Chinese. monthNamesChinese = []string{"一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"} // monthNamesChineseAbbr lists the month name abbreviations in the Chinese. @@ -1621,9 +2099,7 @@ var ( "\u0410\u0440\u0432\u0430\u043D \u0445\u043E\u0451\u0440\u0434\u0443\u0433\u0430\u0430\u0440 \u0441\u0430\u0440", } // monthNamesMongolianAbbr lists the month name abbreviations in Mongolian. - monthNamesMongolianAbbr = []string{ - "1-р сар", "2-р сар", "3-р сар", "4-р сар", "5-р сар", "6-р сар", "7-р сар", "8-р сар", "9-р сар", "10-р сар", "11-р сар", "12-р сар", - } + monthNamesMongolianAbbr = []string{"1-р сар", "2-р сар", "3-р сар", "4-р сар", "5-р сар", "6-р сар", "7-р сар", "8-р сар", "9-р сар", "10-р сар", "11-р сар", "12-р сар"} // monthNamesMoroccoAbbr lists the month name abbreviations in the Morocco. monthNamesMoroccoAbbr = []string{"jan.", "fév.", "mar.", "avr.", "mai", "jui.", "juil.", "août", "sept.", "oct.", "nov.", "déc."} // monthNamesNepali list the month names in the Nepali. @@ -2020,10 +2496,54 @@ var ( monthNamesSomali = []string{"Jannaayo", "Febraayo", "Maarso", "Abriil", "May", "Juun", "Luuliyo", "Ogost", "Sebtembar", "Oktoobar", "Nofembar", "Desembar"} // monthNamesSomaliAbbr list the month abbreviations in the Somali. monthNamesSomaliAbbr = []string{"Jan", "Feb", "Mar", "Abr", "May", "Jun", "Lul", "Ogs", "Seb", "Okt", "Nof", "Dis"} + // monthNamesSotho list the month names in the Sotho. + monthNamesSotho = []string{"Phesekgong", "Hlakola", "Hlakubele", "Mmese", "Motsheanong", "Phupjane", "Phupu", "Phata", "Leotshe", "Mphalane", "Pundungwane", "Tshitwe"} + // monthNamesSothoAbbr list the month abbreviations in the Sotho. + monthNamesSothoAbbr = []string{"Phe", "Kol", "Ube", "Mme", "Mot", "Jan", "Upu", "Pha", "Leo", "Mph", "Pun", "Tsh"} // 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 the Spanish. monthNamesSpanishAbbr = []string{"ene", "feb", "mar", "abr", "may", "jun", "jul", "ago", "sep", "oct", "nov", "dic"} + // monthNamesSpanishPE list the month names in the Spanish Peru. + monthNamesSpanishPE = []string{"Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto", "Setiembre", "Octubre", "Noviembre", "Diciembre"} + // monthNamesSpanishPEAbbr list the month abbreviations in the Spanish Peru. + monthNamesSpanishPEAbbr = []string{"Ene.", "Feb.", "Mar.", "Abr.", "May.", "Jun.", "Jul.", "Ago.", "Set.", "Oct.", "Nov.", "Dic."} + // monthNamesSwedish list the month names in the Swedish. + monthNamesSwedish = []string{"januari", "februari", "mars", "april", "maj", "juni", "juli", "augusti", "september", "oktober", "november", "december"} + // monthNamesSwedishAbbr list the month abbreviations in the Swedish. + monthNamesSwedishAbbr = []string{"jan", "feb", "mar", "apr", "maj", "jun", "jul", "aug", "sep", "okt", "nov", "dec"} + // monthNamesSwedishFIAbbr list the month abbreviations in the Swedish Finland. + monthNamesSwedishFIAbbr = []string{"jan.", "feb.", "mars", "apr.", "maj", "juni", "juli", "aug.", "sep.", "okt.", "nov.", "dec."} + // monthNamesSyriac list the month names in the Syriac. + monthNamesSyriac = []string{ + "\u071F\u0722\u0718\u0722%A0\u0710\u071A\u072A\u071D", + "\u072B\u0712\u071B", + "\u0710\u0715\u072A", + "\u0722\u071D\u0723\u0722", + "\u0710\u071D\u072A", + "\u071A\u0719\u071D\u072A\u0722", + "\u072C\u0721\u0718\u0719", + "\u0710\u0712", + "\u0710\u071D\u0720\u0718\u0720", + "\u072C\u072B\u072A\u071D%A0\u0729\u0715\u071D\u0721", + "\u072C\u072B\u072A\u071D%A0\u0710\u071A\u072A\u071D", + "\u071F\u0722\u0718\u0722%A0\u0729\u0715\u071D\u0721", + } + // monthNamesSyriacAbbr lists the month name abbreviations in the Syriac. + monthNamesSyriacAbbr = []string{ + "\u071F\u0722%A0\u070F\u0712", + "\u072B\u0712\u071B", + "\u0710\u0715\u072A", + "\u0722\u071D\u0723\u0722", + "\u0710\u071D\u072A", + "\u071A\u0719\u071D\u072A\u0722", + "\u072C\u0721\u0718\u0719", + "\u0710\u0712", + "\u0710\u071D\u0720\u0718\u0720", + "\u070F\u072C\u072B%A0\u070F\u0710", + "\u070F\u072C\u072B%A0\u070F\u0712", + "\u070F\u071F\u0722%A0\u070F\u0710", + } // monthNamesSyllabics list the month names in the Syllabics. monthNamesSyllabics = []string{ "\u152E\u14D0\u14C4\u140A\u1546", @@ -2039,7 +2559,7 @@ var ( "\u14C4\u1555\u1431\u1546", "\u144E\u14EF\u1431\u1546", } - // monthNamesSyllabicsAbbr lists the month name abbreviations in Syllabics. + // monthNamesSyllabicsAbbr lists the month name abbreviations in the Syllabics. monthNamesSyllabicsAbbr = []string{ "\u152E\u14D0\u14C4", "\u1556\u155D\u1557", @@ -2054,6 +2574,130 @@ var ( "\u14C4\u1555\u1431", "\u144E\u14EF\u1431", } + // monthNamesTajik list the month names in the Tajik. + monthNamesTajik = []string{ + "\u044F\u043D\u0432\u0430\u0440", + "\u0444\u0435\u0432\u0440\u0430\u043B", + "\u043C\u0430\u0440\u0442", + "\u0430\u043F\u0440\u0435\u043B", + "\u043C\u0430\u0439", + "\u0438\u044E\u043D", + "\u0438\u044E\u043B", + "\u0430\u0432\u0433\u0443\u0441\u0442", + "\u0441\u0435\u043D\u0442\u044F\u0431\u0440", + "\u043E\u043A\u0442\u044F\u0431\u0440", + "\u043D\u043E\u044F\u0431\u0440", + "\u0434\u0435\u043A\u0430\u0431\u0440", + } + // monthNamesTajikAbbr lists the month name abbreviations in Tajik. + monthNamesTajikAbbr = []string{ + "\u044F\u043D\u0432", + "\u0444\u0435\u0432", + "\u043C\u0430\u0440", + "\u0430\u043F\u0440", + "\u043C\u0430\u0439", + "\u0438\u044E\u043D", + "\u0438\u044E\u043B", + "\u0430\u0432\u0433", + "\u0441\u0435\u043D", + "\u043E\u043A\u0442", + "\u043D\u043E\u044F", + "\u0434\u0435\u043A", + } + // monthNamesTamazight list the month names in the Tamazight. + monthNamesTamazight = []string{"Yennayer", "Furar", "Meghres", "Yebrir", "Magu", "Yunyu", "Yulyu", "Ghuct", "Cutenber", "Tuber", "Nunember", "Dujanbir"} + // monthNamesTamazightAbbr list the month abbreviations in the Tamazight. + monthNamesTamazightAbbr = []string{"Yen", "Fur", "Megh", "Yeb", "May", "Yun", "Yul", "Ghu", "Cut", "Tub", "Nun", "Duj"} + // monthNamesTamil list the month names in the Tamil. + monthNamesTamil = []string{ + "\u0B9C\u0BA9\u0BB5\u0BB0\u0BBF", + "\u0BAA\u0BBF\u0BAA\u0BCD\u0BB0\u0BB5\u0BB0\u0BBF", + "\u0BAE\u0BBE\u0BB0\u0BCD\u0B9A\u0BCD", + "\u0B8F\u0BAA\u0BCD\u0BB0\u0BB2\u0BCD", + "\u0BAE\u0BC7", + "\u0B9C\u0BC2\u0BA9\u0BCD", + "\u0B9C\u0BC2\u0BB2\u0BC8", + "\u0B86\u0B95\u0BB8\u0BCD\u0B9F\u0BCD", + "\u0B9A\u0BC6\u0BAA\u0BCD\u0B9F\u0BAE\u0BCD\u0BAA\u0BB0\u0BCD", + "\u0B85\u0B95\u0BCD\u0B9F\u0BCB\u0BAA\u0BB0\u0BCD", + "\u0BA8\u0BB5\u0BAE\u0BCD\u0BAA\u0BB0\u0BCD", + "\u0B9F\u0BBF\u0B9A\u0BAE\u0BCD\u0BAA\u0BB0\u0BCD", + } + // monthNamesTamilAbbr lists the month name abbreviations in Tamil. + monthNamesTamilAbbr = []string{ + "\u0B9C\u0BA9.", + "\u0BAA\u0BBF\u0BAA\u0BCD.", + "\u0BAE\u0BBE\u0BB0\u0BCD.", + "\u0B8F\u0BAA\u0BCD.", + "\u0BAE\u0BC7", + "\u0B9C\u0BC2\u0BA9\u0BCD", + "\u0B9C\u0BC2\u0BB2\u0BC8", + "\u0B86\u0B95.", + "\u0B9A\u0BC6\u0BAA\u0BCD.", + "\u0B85\u0B95\u0BCD.", + "\u0BA8\u0BB5.", + "\u0B9F\u0BBF\u0B9A.", + } + // monthNamesTatar list the month names in the Tatar. + monthNamesTatar = []string{ + "\u0433\u044B\u0439\u043D\u0432\u0430\u0440", + "\u0444\u0435\u0432\u0440\u0430\u043B\u044C", + "\u043C\u0430\u0440\u0442", + "\u0430\u043F\u0440\u0435\u043B\u044C", + "\u043C\u0430\u0439", + "\u0438\u044E\u043D\u044C", + "\u0438\u044E\u043B\u044C", + "\u0430\u0432\u0433\u0443\u0441\u0442", + "\u0441\u0435\u043D\u0442\u044F\u0431\u0440\u044C", + "\u043E\u043A\u0442\u044F\u0431\u0440\u044C", + "\u043D\u043E\u044F\u0431\u0440\u044C", + "\u0434\u0435\u043A\u0430\u0431\u0440\u044C", + } + // monthNamesTatarAbbr lists the month name abbreviations in the Tatar. + monthNamesTatarAbbr = []string{ + "\u0433\u044B\u0439\u043D.", + "\u0444\u0435\u0432.", + "\u043C\u0430\u0440.", + "\u0430\u043F\u0440.", + "\u043C\u0430\u0439", + "\u0438\u044E\u043D\u044C", + "\u0438\u044E\u043B\u044C", + "\u0430\u0432\u0433.", + "\u0441\u0435\u043D.", + "\u043E\u043A\u0442.", + "\u043D\u043E\u044F\u0431.", + "\u0434\u0435\u043A.", + } + // monthNamesTelugu list the month names in the Telugu. + monthNamesTelugu = []string{ + "\u0C1C\u0C28\u0C35\u0C30\u0C3F", + "\u0C2B\u0C3F\u0C2C\u0C4D\u0C30\u0C35\u0C30\u0C3F", + "\u0C2E\u0C3E\u0C30\u0C4D\u0C1A\u0C3F", + "\u0C0F\u0C2A\u0C4D\u0C30\u0C3F\u0C32\u0C4D", + "\u0C2E\u0C47", + "\u0C1C\u0C42\u0C28\u0C4D", + "\u0C1C\u0C41\u0C32\u0C48", + "\u0C06\u0C17\u0C38\u0C4D\u0C1F\u0C41", + "\u0C38\u0C46\u0C2A\u0C4D\u0C1F\u0C46\u0C02\u0C2C\u0C30\u0C4D", + "\u0C05\u0C15\u0C4D\u0C1F\u0C4B\u0C2C\u0C30\u0C4D", + "\u0C28\u0C35\u0C02\u0C2C\u0C30\u0C4D", + "\u0C21\u0C3F\u0C38\u0C46\u0C02\u0C2C\u0C30\u0C4D", + } + // monthNamesTeluguAbbr lists the month name abbreviations in the Telugu. + monthNamesTeluguAbbr = []string{ + "\u0C1C\u0C28", + "\u0C2B\u0C3F\u0C2C\u0C4D\u0C30", + "\u0C2E\u0C3E\u0C30\u0C4D\u0C1A\u0C3F", + "\u0C0F\u0C2A\u0C4D\u0C30\u0C3F", + "\u0C2E\u0C47", + "\u0C1C\u0C42\u0C28\u0C4D", + "\u0C1C\u0C41\u0C32\u0C48", + "\u0C06\u0C17", + "\u0C38\u0C46\u0C2A\u0C4D\u0C1F\u0C46\u0C02", + "\u0C05\u0C15\u0C4D\u0C1F\u0C4B", + "\u0C28\u0C35\u0C02", + "\u0C21\u0C3F\u0C38\u0C46\u0C02", + } // monthNamesThai list the month names in the Thai. monthNamesThai = []string{ "\u0e21\u0e01\u0e23\u0e32\u0e04\u0e21", @@ -2084,7 +2728,7 @@ 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 lists the month name abbreviations in the Tibetan. monthNamesTibetanAbbr = []string{ "\u0f5f\u0fb3\u0f0b\u0f21", "\u0f5f\u0fb3\u0f0b\u0f22", @@ -2099,44 +2743,1747 @@ var ( "\u0f5f\u0fb3\u0f0b\u0f21\u0f21", "\u0f5f\u0fb3\u0f0b\u0f21\u0f22", } + // monthNamesTigrinya list the month names in the Tigrinya. + monthNamesTigrinya = []string{ + "\u1325\u122A", + "\u1208\u12AB\u1272\u1275", + "\u1218\u130B\u1262\u1275", + "\u121A\u12EB\u12DD\u12EB", + "\u130D\u1295\u1266\u1275", + "\u1230\u1290", + "\u1213\u121D\u1208", + "\u1290\u1213\u1230", + "\u1218\u1235\u12A8\u1228\u121D", + "\u1325\u1245\u121D\u1272", + "\u1215\u12F3\u122D", + "\u1273\u1215\u1233\u1235", + } + // monthNamesTigrinyaAbbr lists the month name abbreviations in the Tigrinya + monthNamesTigrinyaAbbr = []string{ + "\u1325\u122A", + "\u1208\u12AB", + "\u1218\u130B", + "\u121A\u12EB", + "\u130D\u1295", + "\u1230\u1290", + "\u1213\u121D", + "\u1290\u1213", + "\u1218\u1235", + "\u1325\u1245", + "\u1215\u12F3", + "\u1273\u1215", + } + // monthNamesTsonga list the month names in the Tsonga. + monthNamesTsonga = []string{"Sunguti", "Nyenyenyani", "Nyenyankulu", "Dzivamisoko", "Mudyaxihi", "Khotavuxika", "Mawuwani", "Mhawuri", "Ndzhati", "Nhlangula", "Hukuri", "N’wendzamhala"} + // monthNamesTsongaAbbr lists the month name abbreviations in Tsonga, this prevents string concatenation. + monthNamesTsongaAbbr = []string{"Sun", "Yan", "Kul", "Dzi", "Mud", "Kho", "Maw", "Mha", "Ndz", "Nhl", "Huk", "N’w"} // monthNamesTradMongolian lists the month number for use with traditional Mongolian. monthNamesTradMongolian = []string{"M01", "M02", "M03", "M04", "M05", "M06", "M07", "M08", "M09", "M10", "M11", "M12"} // 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 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"} + // monthNamesTurkmen list the month names in the Turkmen. + monthNamesTurkmen = []string{"Ýanwar", "Fewral", "Mart", "Aprel", "Maý", "lýun", "lýul", "Awgust", "Sentýabr", "Oktýabr", "Noýabr", "Dekabr"} + // monthNamesTurkmenAbbr lists the month name abbreviations in Turkmen, this prevents string concatenation. + monthNamesTurkmenAbbr = []string{"Ýan", "Few", "Mart", "Apr", "Maý", "lýun", "lýul", "Awg", "Sen", "Okt", "Noý", "Dek"} + // monthNamesUkrainian list the month names in the Ukrainian. + monthNamesUkrainian = []string{ + "\u0441\u0456\u0447\u0435\u043D\u044C", + "\u043B\u044E\u0442\u0438\u0439", + "\u0431\u0435\u0440\u0435\u0437\u0435\u043D\u044C", + "\u043A\u0432\u0456\u0442\u0435\u043D\u044C", + "\u0442\u0440\u0430\u0432\u0435\u043D\u044C", + "\u0447\u0435\u0440\u0432\u0435\u043D\u044C", + "\u043B\u0438\u043F\u0435\u043D\u044C", + "\u0441\u0435\u0440\u043F\u0435\u043D\u044C", + "\u0432\u0435\u0440\u0435\u0441\u0435\u043D\u044C", + "\u0436\u043E\u0432\u0442\u0435\u043D\u044C", + "\u043B\u0438\u0441\u0442\u043E\u043F\u0430\u0434", + "\u0433\u0440\u0443\u0434\u0435\u043D\u044C", + } + // monthNamesUkrainianAbbr lists the month name abbreviations in Ukrainian. + monthNamesUkrainianAbbr = []string{ + "\u0421\u0456\u0447", + "\u041B\u044E\u0442", + "\u0411\u0435\u0440", + "\u041A\u0432\u0456", + "\u0422\u0440\u0430", + "\u0427\u0435\u0440", + "\u041B\u0438\u043F", + "\u0421\u0435\u0440", + "\u0412\u0435\u0440", + "\u0416\u043E\u0432", + "\u041B\u0438\u0441", + "\u0413\u0440\u0443", + } + // monthNamesUpperSorbian list the month names in the Upper Sorbian. + monthNamesUpperSorbian = []string{"januar", "februar", "měrc", "apryl", "meja", "junij", "julij", "awgust", "september", "oktober", "nowember", "december"} + // monthNamesUpperSorbianAbbr lists the month name abbreviations in the Upper Sorbian, this prevents string concatenation. + monthNamesUpperSorbianAbbr = []string{"jan", "feb", "měr", "apr", "mej", "jun", "jul", "awg", "sep", "okt", "now", "dec"} + // monthNamesUyghur list the month names in the Uyghur. + monthNamesUyghur = []string{ + "\u064A\u0627\u0646\u06CB\u0627\u0631", + "\u0641\u06D0\u06CB\u0631\u0627\u0644", + "\u0645\u0627\u0631\u062A", + "\u0626\u0627\u067E\u0631\u06D0\u0644", + "\u0645\u0627\u064A", + "\u0626\u0649\u064A\u06C7\u0646", + "\u0626\u0649\u064A\u06C7\u0644", + "\u0626\u0627\u06CB\u063A\u06C7\u0633\u062A", + "\u0633\u06D0\u0646\u062A\u06D5\u0628\u0649\u0631", + "\u0626\u06C6\u0643\u062A\u06D5\u0628\u0649\u0631", + "\u0646\u0648\u064A\u0627\u0628\u0649\u0631", + "\u062F\u06D0\u0643\u0627\u0628\u0649\u0631", + } + // monthNamesUzbek list the month names in the Uzbek. + monthNamesUzbek = []string{"Yanvar", "Fevral", "Mart", "Aprel", "May", "Iyun", "Iyul", "Avgust", "Sentabr", "Oktabr", "Noyabr", "Dekabr"} + // monthNamesUzbekAbbr lists the month name abbreviations in the Uzbek, this prevents string concatenation. + monthNamesUzbekAbbr = []string{"Yan", "Fev", "Mar", "Apr", "May", "Iyn", "Iyl", "Avg", "Sen", "Okt", "Noy", "Dek"} + // monthNamesValencian list the month names in the Valencian. + monthNamesValencian = []string{"gener", "febrer", "març", "abril", "maig", "juny", "juliol", "agost", "setembre", "octubre", "novembre", "desembre"} + // monthNamesValencianAbbr lists the month name abbreviations in the Valencian, this prevents string concatenation. + monthNamesValencianAbbr = []string{"gen.", "febr.", "març", "abr.", "maig", "juny", "jul.", "ag.", "set.", "oct.", "nov.", "des."} + // monthNamesVenda list the month names in the Venda. + monthNamesVenda = []string{"Phando", "Luhuhi", "Ṱhafamuhwe", "Lambamai", "Shundunthule", "Fulwi", "Fulwana", "Ṱhangule", "Khubvumedzi", "Tshimedzi", "Ḽara", "Nyendavhusiku"} + // monthNamesVendaAbbr lists the month name abbreviations in the Venda, this prevents string concatenation. + monthNamesVendaAbbr = []string{"Pha", "Luh", "Ṱhf", "Lam", "Shu", "Lwi", "Lwa", "Ṱha", "Khu", "Tsh", "Ḽar", "Nye"} // 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 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 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 lists the month name abbreviations in the 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 list the month name abbreviations in the 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 month 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 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"} + // monthNamesYiddish list the month names in the Yiddish. + monthNamesYiddish = []string{ + "\u05D9\u05D0\u05B7\u05E0\u05D5\u05D0\u05B7\u05E8", + "\u05E4\u05BF\u05E2\u05D1\u05E8\u05D5\u05D0\u05B7\u05E8", + "\u05DE\u05E2\u05E8\u05E5", + "\u05D0\u05B7\u05E4\u05BC\u05E8\u05D9\u05DC", + "\u05DE\u05D9\u05D9", + "\u05D9\u05D5\u05E0\u05D9", + "\u05D9\u05D5\u05DC\u05D9", + "\u05D0\u05D5\u05D9\u05D2\u05D5\u05E1\u05D8", + "\u05E1\u05E2\u05E4\u05BC\u05D8\u05E2\u05DE\u05D1\u05E2\u05E8", + "\u05D0\u05E7\u05D8\u05D0\u05D1\u05E2\u05E8", + "\u05E0\u05D0\u05D5\u05D5\u05E2\u05DE\u05D1\u05E2\u05E8", + "\u05D3\u05E2\u05E6\u05E2\u05DE\u05D1\u05E2\u05E8", + } + // monthNamesYiddishAbbr lists the month name abbreviations in Yiddish. + monthNamesYiddishAbbr = []string{ + "\u05D9\u05D0\u05B7\u05E0", + "\u05E4\u05BF\u05E2\u05D1", + "\u05DE\u05E2\u05E8\u05E5", + "\u05D0\u05B7\u05E4\u05BC\u05E8", + "\u05DE\u05D9\u05D9", + "\u05D9\u05D5\u05E0\u05D9", + "\u05D9\u05D5\u05DC\u05D9", + "\u05D0\u05D5\u05D9\u05D2", + "\u05E1\u05E2\u05E4\u05BC", + "\u05D0\u05E7\u05D8", + "\u05E0\u05D0\u05D5\u05D5", + "\u05D3\u05E2\u05E6", + } + // monthNamesYoruba list the month names in the Yoruba. + monthNamesYoruba = []string{ + "\u1E62\u1EB9\u0301r\u1EB9\u0301", + "%C8r%E8l%E8", + "\u1EB8r\u1EB9\u0300n%E0", + "%CCgb%E9", + "\u1EB8\u0300bibi", + "%D2k%FAdu", + "Ag\u1EB9m\u1ECD", + "%D2g%FAn", + "Owewe", + "\u1ECC\u0300w%E0r%E0", + "B%E9l%FA", + "\u1ECC\u0300p\u1EB9\u0300", + } + // monthNamesYorubaAbbr lists the month name abbreviations in the Yoruba. + monthNamesYorubaAbbr = []string{ + "\u1E62\u1EB9\u0301", + "%C8r", + "\u1EB8r", + "%CCg", + "\u1EB8\u0300b", + "%D2k", + "Ag", + "%D2g", + "Ow", + "\u1ECC\u0300w", + "B%E9", + "\u1ECC\u0300p", + } // monthNamesZulu list the month names in the Zulu. monthNamesZulu = []string{"Januwari", "Febhuwari", "Mashi", "Ephreli", "Meyi", "Juni", "Julayi", "Agasti", "Septemba", "Okthoba", "Novemba", "Disemba"} - // monthNamesZuluAbbr list the month name abbreviations in Zulu + // monthNamesZuluAbbr list the month name abbreviations in the Zulu. monthNamesZuluAbbr = []string{"Jan", "Feb", "Mas", "Eph", "Mey", "Jun", "Jul", "Agas", "Sep", "Okt", "Nov", "Dis"} - // weekdayNamesChinese list the weekday name in Chinese - weekdayNamesChinese = []string{"日", "一", "二", "三", "四", "五", "六"} + // weekdayNamesAfrikaans list the weekday name in the Afrikaans. + weekdayNamesAfrikaans = []string{"Sondag", "Maandag", "Dinsdag", "Woensdag", "Donderdag", "Vrydag", "Saterdag"} + // weekdayNamesAfrikaansAbbr list the weekday name abbreviations in the Afrikaans. + weekdayNamesAfrikaansAbbr = []string{"So.", "Ma.", "Di.", "Wo.", "Do.", "Vr.", "Sa."} + // weekdayNamesAlbanian list the weekday name in the Albanian. + weekdayNamesAlbanian = []string{"e diel", "e hënë", "e martë", "e mërkurë", "e enjte", "e premte", "e shtunë"} + // weekdayNamesAlbanianAbbr list the weekday name abbreviations in the Albanian. + weekdayNamesAlbanianAbbr = []string{"die", "hën", "mar", "mër", "enj", "pre", "sht"} + // weekdayNamesAlsatian list the weekday name in the Alsatian. + weekdayNamesAlsatian = []string{"Sunntig", "Määntig", "Ziischtig", "Mittwuch", "Dunschtig", "Friitig", "Samschtig"} + // weekdayNamesAlsatianAbbr list the weekday name abbreviations in the Alsatian. + weekdayNamesAlsatianAbbr = []string{"Su.", "Mä.", "Zi.", "Mi.", "Du.", "Fr.", "Sa."} + // weekdayNamesAlsatianFrance list the weekday name in the Alsatian France. + weekdayNamesAlsatianFrance = []string{"Sundi", "Manti", "Zischti", "Mettwuch", "Dunnerschti", "Friti", "Sàmschti"} + // weekdayNamesAlsatianFranceAbbr list the weekday name abbreviations in the Alsatian France. + weekdayNamesAlsatianFranceAbbr = []string{"Su.", "Ma.", "Zi.", "Me.", "Du.", "Fr.", "Sà."} + // weekdayNamesAmharic list the weekday name in the Amharic. + weekdayNamesAmharic = []string{ + "\u12A5\u1211\u12F5", + "\u1230\u129E", + "\u121B\u12AD\u1230\u129E", + "\u1228\u1261\u12D5", + "\u1210\u1219\u1235", + "\u12D3\u122D\u1265", + "\u1245\u12F3\u121C", + } + // weekdayNamesAmharicAbbr list the weekday name abbreviations in the Amharic. + weekdayNamesAmharicAbbr = []string{ + "\u12A5\u1211\u12F5", + "\u1230\u129E", + "\u121B\u12AD\u1230", + "\u1228\u1261\u12D5", + "\u1210\u1219\u1235", + "\u12D3\u122D\u1265", + "\u1245\u12F3\u121C", + } + // weekdayNamesArabic list the weekday name in the Arabic. + weekdayNamesArabic = []string{ + "\u0627\u0644\u0623\u062D\u062F", + "\u0627\u0644\u0625\u062B\u0646\u064A\u0646", + "\u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621", + "\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621", + "\u0627\u0644\u062E\u0645\u064A\u0633", + "\u0627\u0644\u062C\u0645\u0639\u0629", + "\u0627\u0644\u0633\u0628\u062A", + } + // weekdayNamesArabicAbbr list the weekday name abbreviations in the Arabic. + weekdayNamesArabicAbbr = []string{ + "\u0627\u0644\u0623\u062D\u062F", + "\u0627\u0644\u0625\u062B\u0646\u064A\u0646", + "\u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621", + "\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621", + "\u0627\u0644\u062E\u0645\u064A\u0633", + "\u0627\u0644\u062C\u0645\u0639\u0629", + "\u0627\u0644\u0633\u0628\u062A", + } + // weekdayNamesArmenian list the weekday name in the Armenian. + weekdayNamesArmenian = []string{ + "\u053F\u056B\u0580\u0561\u056F\u056B", + "\u0535\u0580\u056F\u0578\u0582\u0577\u0561\u0562\u0569\u056B", + "\u0535\u0580\u0565\u0584\u0577\u0561\u0562\u0569\u056B", + "\u0549\u0578\u0580\u0565\u0584\u0577\u0561\u0562\u0569\u056B", + "\u0540\u056B\u0576\u0563\u0577\u0561\u0562\u0569\u056B", + "\u0548\u0582\u0580\u0562\u0561\u0569", + "\u0547\u0561\u0562\u0561\u0569", + } + // weekdayNamesArmenianAbbr list the weekday name abbreviations in the Armenian. + weekdayNamesArmenianAbbr = []string{ + "\u053F\u056B\u0580", + "\u0535\u0580\u056F", + "\u0535\u0580\u0584", + "\u0549\u0580\u0584", + "\u0540\u0576\u0563", + "\u0548\u0582\u0580", + "\u0547\u0562\u0569", + } + // weekdayNamesAssamese list the weekday name in the Assamese. + weekdayNamesAssamese = []string{ + "\u09F0\u09AC\u09BF\u09AC\u09BE\u09F0", + "\u09B8\u09CB\u09AE\u09AC\u09BE\u09F0", + "\u09AE\u0999\u09CD\u0997\u09B2\u09AC\u09BE\u09F0", + "\u09AC\u09C1\u09A7\u09AC\u09BE\u09F0", + "\u09AC\u09C3\u09B9\u09B8\u09CD\u09AA\u09A4\u09BF\u09AC\u09BE\u09F0", + "\u09B6\u09C1\u0995\u09CD\u09B0\u09AC\u09BE\u09F0", + "\u09B6\u09A8\u09BF\u09AC\u09BE\u09F0", + } + // weekdayNamesAssameseAbbr list the weekday name abbreviations in the Assamese. + weekdayNamesAssameseAbbr = []string{ + "\u09F0\u09AC\u09BF.", + "\u09B8\u09CB\u09AE.", + "\u09AE\u0999\u09CD\u0997\u09B2.", + "\u09AC\u09C1\u09A7.", + "\u09AC\u09C3\u09B9.", + "\u09B6\u09C1\u0995\u09CD\u09B0.", + "\u09B6\u09A8\u09BF.", + } + // weekdayNamesAzerbaijaniCyrillic list the weekday name in the Azerbaijani (Cyrillic). + weekdayNamesAzerbaijaniCyrillic = []string{ + "\u0431\u0430\u0437\u0430\u0440", + "\u0431\u0430\u0437\u0430\u0440%A0\u0435\u0440\u0442\u04D9\u0441\u0438", + "\u0447\u04D9\u0440\u0448\u04D9\u043D\u0431\u04D9%A0\u0430\u0445\u0448\u0430\u043C\u044B", + "\u0447\u04D9\u0440\u0448\u04D9\u043D\u0431\u04D9", + "\u04B9\u04AF\u043C\u04D9%A0\u0430\u0445\u0448\u0430\u043C\u044B", + "\u04B9\u04AF\u043C\u04D9", + "\u0448\u04D9\u043D\u0431\u04D9", + } + // weekdayNamesAzerbaijaniCyrillicAbbr list the weekday name abbreviations in the Azerbaijani (Cyrillic). + weekdayNamesAzerbaijaniCyrillicAbbr = []string{"\u0411", "\u0411\u0435", "\u0427\u0430", "\u0427", "\u04B8\u0430", "\u04B8", "\u0428"} + // weekdayNamesAzerbaijani list the weekday name in the Azerbaijani. + weekdayNamesAzerbaijani = []string{ + "bazar", + "bazar%E7ert\u0259si", + "%E7\u0259r\u015F\u0259nb\u0259%A0ax\u015Fam\u0131", + "%E7\u0259r\u015F\u0259nb\u0259", + "c%FCm\u0259%20ax\u015Fam\u0131", + "c%FCm\u0259", + "\u015F\u0259nb\u0259", + } + // weekdayNamesAzerbaijaniAbbr list the weekday name abbreviations in the Azerbaijani. + weekdayNamesAzerbaijaniAbbr = []string{"B.", "B.E.", "%C7.A.", "%C7.", "C.A.", "C.", "\u015E."} + // weekdayNamesBangla list the weekday name in the Bangla. + weekdayNamesBangla = []string{ + "\u09B0\u09AC\u09BF\u09AC\u09BE\u09B0", + "\u09B8\u09CB\u09AE\u09AC\u09BE\u09B0", + "\u09AE\u0999\u09CD\u0997\u09B2\u09AC\u09BE\u09B0", + "\u09AC\u09C1\u09A7\u09AC\u09BE\u09B0", + "\u09AC\u09C3\u09B9\u09B8\u09CD\u09AA\u09A4\u09BF\u09AC\u09BE\u09B0", + "\u09B6\u09C1\u0995\u09CD\u09B0\u09AC\u09BE\u09B0", + "\u09B6\u09A8\u09BF\u09AC\u09BE\u09B0", + } + // weekdayNamesBanglaAbbr list the weekday name abbreviations in the Bangla. + weekdayNamesBanglaAbbr = []string{ + "\u09B0\u09AC\u09BF.", + "\u09B8\u09CB\u09AE.", + "\u09AE\u0999\u09CD\u0997\u09B2.", + "\u09AC\u09C1\u09A7.", + "\u09AC\u09C3\u09B9\u09B8\u09CD\u09AA\u09A4\u09BF.", + "\u09B6\u09C1\u0995\u09CD\u09B0.", + "\u09B6\u09A8\u09BF.", + } + // weekdayNamesBashkir list the weekday name in the Bashkir. + weekdayNamesBashkir = []string{ + "\u0419\u04D9\u043A\u0448\u04D9\u043C\u0431\u0435", + "\u0414\u04AF\u0448\u04D9\u043C\u0431\u0435", + "\u0428\u0438\u0448\u04D9\u043C\u0431\u0435", + "\u0428\u0430\u0440\u0448\u0430\u043C\u0431\u044B", + "\u041A\u0435\u0441\u0430\u0499\u043D\u0430", + "\u0419\u043E\u043C\u0430", + "\u0428\u04D9\u043C\u0431\u0435", + } + // weekdayNamesBashkirAbbr list the weekday name abbreviations in the Bashkir. + weekdayNamesBashkirAbbr = []string{ + "\u0419\u0448", + "\u0414\u0448", + "\u0428\u0448", + "\u0428\u0440", + "\u041A\u0441", + "\u0419\u043C", + "\u0428\u0431", + } + // weekdayNamesBasque list the weekday name in the Basque. + weekdayNamesBasque = []string{"igandea", "astelehena", "asteartea", "asteazkena", "osteguna", "ostirala", "larunbata"} + // weekdayNamesBasqueAbbr list the weekday name abbreviations in the Basque. + weekdayNamesBasqueAbbr = []string{"ig.", "al.", "ar.", "az.", "og.", "or.", "lr."} + // weekdayNamesBelarusian list the weekday name in the Belarusian. + weekdayNamesBelarusian = []string{ + "\u043D\u044F\u0434\u0437\u0435\u043B\u044F", + "\u043F\u0430\u043D\u044F\u0434\u0437\u0435\u043B\u0430\u043A", + "\u0430\u045E\u0442\u043E\u0440\u0430\u043A", + "\u0441\u0435\u0440\u0430\u0434\u0430", + "\u0447\u0430\u0446\u0432\u0435\u0440", + "\u043F\u044F\u0442\u043D\u0456\u0446\u0430", + "\u0441\u0443\u0431\u043E\u0442\u0430", + } + // weekdayNamesBelarusianAbbr list the weekday name abbreviations in the Belarusian. + weekdayNamesBelarusianAbbr = []string{ + "\u043D\u0434", + "\u043F\u043D", + "\u0430\u045E\u0442", + "\u0441\u0440", + "\u0447\u0446", + "\u043F\u0442", + "\u0441\u0431", + } + // weekdayNamesBosnianCyrillic list the weekday name in the Bosnian (Cyrillic). + weekdayNamesBosnianCyrillic = []string{ + "\u043D\u0435\u0434\u0458\u0435\u0459\u0430", + "\u043F\u043E\u043D\u0435\u0434\u0458\u0435\u0459\u0430\u043A", + "\u0443\u0442\u043E\u0440\u0430\u043A", + "\u0441\u0440\u0438\u0458\u0435\u0434\u0430", + "\u0447\u0435\u0442\u0432\u0440\u0442\u0430\u043A", + "\u043F\u0435\u0442\u0430\u043A", + "\u0441\u0443\u0431\u043E\u0442\u0430", + } + // weekdayNamesBosnianCyrillicAbbr list the weekday name abbreviations in the Bosnian (Cyrillic). + weekdayNamesBosnianCyrillicAbbr = []string{ + "\u043D\u0435\u0434", + "\u043F\u043E\u043D", + "\u0443\u0442\u043E", + "\u0441\u0440\u0435", + "\u0447\u0435\u0442", + "\u043F\u0435\u0442", + "\u0441\u0443\u0431", + } + // weekdayNamesBosnian list the weekday name in the Bosnian. + weekdayNamesBosnian = []string{"nedjelja", "ponedjeljak", "utorak", "srijeda", "četvrtak", "petak", "subota"} + // weekdayNamesBosnianAbbr list the weekday name abbreviations in the Bosnian. + weekdayNamesBosnianAbbr = []string{"ned", "pon", "uto", "sri", "čet", "pet", "sub"} + // weekdayNamesBreton list the weekday name in the Breton. + weekdayNamesBreton = []string{"Sul", "Lun", "Meurzh", "Merc'her", "Yaou", "Gwener", "Sadorn"} + // weekdayNamesBretonAbbr list the weekday name abbreviations in the Breton. + weekdayNamesBretonAbbr = []string{"Sul", "Lun", "Meu.", "Mer.", "Yaou", "Gwe.", "Sad."} + // weekdayNamesBulgarian list the weekday name in the Bulgarian. + weekdayNamesBulgarian = []string{ + "\u043D\u0435\u0434\u0435\u043B\u044F", + "\u043F\u043E\u043D\u0435\u0434\u0435\u043B\u043D\u0438\u043A", + "\u0432\u0442\u043E\u0440\u043D\u0438\u043A", + "\u0441\u0440\u044F\u0434\u0430", + "\u0447\u0435\u0442\u0432\u044A\u0440\u0442\u044A\u043A", + "\u043F\u0435\u0442\u044A\u043A", + "\u0441\u044A\u0431\u043E\u0442\u0430", + } + // weekdayNamesBulgarianAbbr list the weekday name abbreviations in the Bulgarian. + weekdayNamesBulgarianAbbr = []string{ + "\u043D\u0435\u0434", + "\u043F\u043E\u043D", + "\u0432\u0442", + "\u0441\u0440", + "\u0447\u0435\u0442\u0432", + "\u043F\u0435\u0442", + "\u0441\u044A\u0431", + } + // weekdayNamesBurmese list the weekday name in the Burmese. + weekdayNamesBurmese = []string{ + "\u1010\u1014\u1004\u103A\u1039\u1002\u1014\u103D\u1031", + "\u1010\u1014\u1004\u103A\u1039\u101C\u102C", + "\u1021\u1004\u103A\u1039\u1002\u102B", + "\u1017\u102F\u1012\u1039\u1013\u101F\u1030\u1038", + "\u1000\u103C\u102C\u101E\u1015\u1010\u1031\u1038", + "\u101E\u1031\u102C\u1000\u103C\u102C", + "\u1005\u1014\u1031", + } + // weekdayNamesCentralKurdish list the weekday name in the Central Kurdish. + weekdayNamesCentralKurdish = []string{ + "\u06CC\u06D5\u06A9\u0634\u06D5\u0645\u0645\u06D5", + "\u062F\u0648\u0648\u0634\u06D5\u0645\u0645\u06D5", + "\u0633\u06CE\u0634\u06D5\u0645\u0645\u06D5", + "\u0686\u0648\u0627\u0631\u0634\u06D5\u0645\u0645\u06D5", + "\u067E\u06CE\u0646\u062C\u0634\u06D5\u0645\u0645\u06D5", + "\u06BE\u06D5\u06CC\u0646\u06CC", + "\u0634\u06D5\u0645\u0645\u06D5", + } + // weekdayNamesCherokee list the weekday name in the Cherokee. + weekdayNamesCherokee = []string{ + "\u13A4\u13BE\u13D9\u13D3\u13C6\u13CD\u13AC", + "\u13A4\u13BE\u13D9\u13D3\u13C9\u13C5\u13AF", + "\u13D4\u13B5\u13C1\u13A2\u13A6", + "\u13E6\u13A2\u13C1\u13A2\u13A6", + "\u13C5\u13A9\u13C1\u13A2\u13A6", + "\u13E7\u13BE\u13A9\u13B6\u13CD\u13D7", + "\u13A4\u13BE\u13D9\u13D3\u13C8\u13D5\u13BE", + } + // weekdayNamesCherokeeAbbr list the weekday name abbreviations in the Cherokee. + weekdayNamesCherokeeAbbr = []string{ + "\u13C6\u13CD\u13AC", + "\u13C9\u13C5\u13AF", + "\u13D4\u13B5\u13C1", + "\u13E6\u13A2\u13C1", + "\u13C5\u13A9\u13C1", + "\u13E7\u13BE\u13A9", + "\u13C8\u13D5\u13BE", + } + // weekdayNamesChinese list the weekday name in the Chinese. + weekdayNamesChinese = []string{"星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"} + // weekdayNamesChineseAbbr list the weekday name abbreviations in the Chinese. + weekdayNamesChineseAbbr = []string{"周日", "周一", "周二", "周三", "周四", "周五", "周六"} + // weekdayNamesChineseAbbr list the weekday name abbreviations in the Chinese. + weekdayNamesChineseAbbr2 = []string{"週日", "週一", "週二", "週三", "週四", "週五", "週六"} + // weekdayNamesEnglish list the weekday name in the English. + weekdayNamesEnglish = []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"} + // weekdayNamesEnglishAbbr list the weekday name abbreviations in the English. + weekdayNamesEnglishAbbr = []string{"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"} + // weekdayNamesEstonian list the weekday name in the Estonian. + weekdayNamesEstonian = []string{"pühapäev", "esmaspäev", "teisipäev", "kolmapäev", "neljapäev", "reede", "laupäev"} + // weekdayNamesEstonianAbbr list the weekday name abbreviations in the Estonian. + weekdayNamesEstonianAbbr = []string{"P", "E", "T", "K", "N", "R", "L"} + // weekdayNamesFaroese list the weekday name in the Faroese. + weekdayNamesFaroese = []string{"sunnudagur", "mánadagur", "týsdagur", "mikudagur", "hósdagur", "fríggjadagur", "leygardagur"} + // weekdayNamesFaroeseAbbr list the weekday name abbreviations in the Faroese. + weekdayNamesFaroeseAbbr = []string{"sun.", "mán.", "týs.", "mik.", "hós.", "frí.", "ley."} + // weekdayNamesFilipino list the weekday name in the Filipino. + weekdayNamesFilipino = []string{"Linggo", "Lunes", "Martes", "Miyerkules", "Huwebes", "Biyernes", "Sabado"} + // weekdayNamesFilipinoAbbr list the weekday name abbreviations in the Filipino. + weekdayNamesFilipinoAbbr = []string{"Lin", "Lun", "Mar", "Miy", "Huw", "Biy", "Sab"} + // weekdayNamesFinnish list the weekday name in the Finnish + weekdayNamesFinnish = []string{"sunnuntai", "maanantai", "tiistai", "keskiviikko", "torstai", "perjantai", "lauantai"} + // weekdayNamesFinnishAbbr list the weekday name abbreviations in the Finnish + weekdayNamesFinnishAbbr = []string{"su", "ma", "ti", "ke", "to", "pe", "la"} + // weekdayNamesFrench list the weekday name in the French. + weekdayNamesFrench = []string{"dimanche", "lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi"} + // weekdayNamesFrenchAbbr list the weekday name abbreviations in the French. + weekdayNamesFrenchAbbr = []string{"dim.", "lun.", "mar.", "mer.", "jeu.", "ven.", "sam."} + // weekdayNamesFrisian list the weekday name in the Frisian. + weekdayNamesFrisian = []string{"snein", "moandei", "tiisdei", "woansdei", "tongersdei", "freed", "sneon"} + // weekdayNamesFrisianAbbr list the weekday name abbreviations in the Frisian. + weekdayNamesFrisianAbbr = []string{"sni", "moa", "tii", "woa", "ton", "fre", "sno"} + // weekdayNamesFulah list the weekday name in the Fulah. + weekdayNamesFulah = []string{"dewo", "aaɓnde", "mawbaare", "njeslaare", "naasaande", "mawnde", "hoore-biir"} + // weekdayNamesFulahAbbr list the weekday name abbreviations in the Fulah + weekdayNamesFulahAbbr = []string{"dew", "aaɓ", "maw", "nje", "naa", "mwd", "hbi"} + // weekdayNamesNigeria list the weekday name in the Nigeria + weekdayNamesNigeria = []string{"alete", "altine", "talaata", "alarba", "alkamiisa", "aljumaa", "asete"} + // weekdayNamesNigeriaAbbr list the weekday name abbreviations in the Nigeria. + weekdayNamesNigeriaAbbr = []string{"alet", "alt.", "tal.", "alar.", "alk.", "alj.", "aset"} + // weekdayNamesGalician list the weekday name in the Galician. + weekdayNamesGalician = []string{"domingo", "luns", "martes", "mércores", "xoves", "venres", "sábado"} + // weekdayNamesGalicianAbbr list the weekday name abbreviations in the Galician. + weekdayNamesGalicianAbbr = []string{"dom.", "luns", "mar.", "mér.", "xov.", "ven.", "sáb."} + // weekdayNamesGeorgian list the weekday name in the Georgian. + weekdayNamesGeorgian = []string{ + "\u10D9\u10D5\u10D8\u10E0\u10D0", + "\u10DD\u10E0\u10E8\u10D0\u10D1\u10D0\u10D7\u10D8", + "\u10E1\u10D0\u10DB\u10E8\u10D0\u10D1\u10D0\u10D7\u10D8", + "\u10DD\u10D7\u10EE\u10E8\u10D0\u10D1\u10D0\u10D7\u10D8", + "\u10EE\u10E3\u10D7\u10E8\u10D0\u10D1\u10D0\u10D7\u10D8", + "\u10DE\u10D0\u10E0\u10D0\u10E1\u10D9\u10D4\u10D5\u10D8", + "\u10E8\u10D0\u10D1\u10D0\u10D7\u10D8", + } + // weekdayNamesGeorgianAbbr list the weekday name abbreviations in the Georgian. + weekdayNamesGeorgianAbbr = []string{ + "\u10D9\u10D5.", + "\u10DD\u10E0\u10E8.", + "\u10E1\u10D0\u10DB\u10E8.", + "\u10DD\u10D7\u10EE\u10E8.", + "\u10EE\u10E3\u10D7\u10E8.", + "\u10DE\u10D0\u10E0.", + "\u10E8\u10D0\u10D1.", + } + // weekdayNamesGerman list the weekday name in the German. + weekdayNamesGerman = []string{"Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"} + // weekdayNamesGermanAbbr list the weekday name abbreviations in the German. + weekdayNamesGermanAbbr = []string{"So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"} + // weekdayNamesGreek list the weekday name in the Greek. + weekdayNamesGreek = []string{ + "\u039A\u03C5\u03C1\u03B9\u03B1\u03BA\u03AE", + "\u0394\u03B5\u03C5\u03C4\u03AD\u03C1\u03B1", + "\u03A4\u03C1\u03AF\u03C4\u03B7", + "\u03A4\u03B5\u03C4\u03AC\u03C1\u03C4\u03B7", + "\u03A0\u03AD\u03BC\u03C0\u03C4\u03B7", + "\u03A0\u03B1\u03C1\u03B1\u03C3\u03BA\u03B5\u03C5\u03AE", + "\u03A3\u03AC\u03B2\u03B2\u03B1\u03C4\u03BF", + } + // weekdayNamesGreekAbbr list the weekday name abbreviations in the Greek. + weekdayNamesGreekAbbr = []string{ + "\u039A\u03C5\u03C1", + "\u0394\u03B5\u03C5", + "\u03A4\u03C1\u03B9", + "\u03A4\u03B5\u03C4", + "\u03A0\u03B5\u03BC", + "\u03A0\u03B1\u03C1", + "\u03A3\u03B1\u03B2", + } + // weekdayNamesGreenlandic list the weekday name in the Greenlandic. + weekdayNamesGreenlandic = []string{"sapaat", "ataasinngorneq", "marlunngorneq", "pingasunngorneq", "sisamanngorneq", "tallimanngorneq", "arfininngorneq"} + // weekdayNamesGreenlandicAbbr list the weekday name abbreviations in the Greenlandic. + weekdayNamesGreenlandicAbbr = []string{"sap.", "at.", "marl.", "ping.", "sis.", "tall.", "arf."} + // weekdayNamesGuarani list the weekday name in the Guarani. + weekdayNamesGuarani = []string{"arate\u0129", "arak%F5i", "araapy", "ararundy", "arapo", "arapote\u0129", "arapok%F5i"} + // weekdayNamesGuaraniAbbr list the weekday name abbreviations in the Guarani. + weekdayNamesGuaraniAbbr = []string{"te\u0129", "k%F5i", "apy", "ndy", "po", "ote\u0129", "ok%F5i"} + // weekdayNamesGujarati list the weekday name in the Gujarati. + weekdayNamesGujarati = []string{ + "\u0AB0\u0AB5\u0ABF\u0AB5\u0ABE\u0AB0", + "\u0AB8\u0ACB\u0AAE\u0AB5\u0ABE\u0AB0", + "\u0AAE\u0A82\u0A97\u0AB3\u0AB5\u0ABE\u0AB0", + "\u0AAC\u0AC1\u0AA7\u0AB5\u0ABE\u0AB0", + "\u0A97\u0AC1\u0AB0\u0AC1\u0AB5\u0ABE\u0AB0", + "\u0AB6\u0AC1\u0A95\u0ACD\u0AB0\u0AB5\u0ABE\u0AB0", + "\u0AB6\u0AA8\u0ABF\u0AB5\u0ABE\u0AB0", + } + // weekdayNamesGujaratiAbbr list the weekday name abbreviations in the Gujarati. + weekdayNamesGujaratiAbbr = []string{ + "\u0AB0\u0AB5\u0ABF", + "\u0AB8\u0ACB\u0AAE", + "\u0AAE\u0A82\u0A97\u0AB3", + "\u0AAC\u0AC1\u0AA7", + "\u0A97\u0AC1\u0AB0\u0AC1", + "\u0AB6\u0AC1\u0A95\u0ACD\u0AB0", + "\u0AB6\u0AA8\u0ABF", + } + // weekdayNamesHausa list the weekday name in the Hausa. + weekdayNamesHausa = []string{"Lahadi", "Litinin", "Talata", "Laraba", "Alhamis", "Jummaʼa", "Asabar"} + // weekdayNamesHausaAbbr list the weekday name abbreviations in the Hausa. + weekdayNamesHausaAbbr = []string{"Lah", "Lit", "Tal", "Lar", "Alh", "Jum", "Asa"} + // weekdayNamesHawaiian list the weekday name in the Hawaiian. + weekdayNamesHawaiian = []string{"Lāpule", "Poʻakahi", "Poʻalua", "Poʻakolu", "Poʻahā", "Poʻalima", "Poʻaono"} + // weekdayNamesHawaiianAbbr list the weekday name abbreviations in the Hawaiian. + weekdayNamesHawaiianAbbr = []string{"LP", "P1", "P2", "P3", "P4", "P5", "P6"} + // weekdayNamesHebrew list the weekday name in the Hebrew. + weekdayNamesHebrew = []string{ + "\u05D9\u05D5\u05DD%A0\u05E8\u05D0\u05E9\u05D5\u05DF", + "\u05D9\u05D5\u05DD%A0\u05E9\u05E0\u05D9", + "\u05D9\u05D5\u05DD%A0\u05E9\u05DC\u05D9\u05E9\u05D9", + "\u05D9\u05D5\u05DD%A0\u05E8\u05D1\u05D9\u05E2\u05D9", + "\u05D9\u05D5\u05DD%A0\u05D7\u05DE\u05D9\u05E9\u05D9", + "\u05D9\u05D5\u05DD%A0\u05E9\u05D9\u05E9\u05D9", + "\u05E9\u05D1\u05EA", + } + // weekdayNamesHebrewAbbr list the weekday name abbreviations in the Hebrew. + weekdayNamesHebrewAbbr = []string{ + "\u05D9\u05D5\u05DD%A0\u05D0", + "\u05D9\u05D5\u05DD%A0\u05D1", + "\u05D9\u05D5\u05DD%A0\u05D2", + "\u05D9\u05D5\u05DD%A0\u05D3", + "\u05D9\u05D5\u05DD%A0\u05D4", + "\u05D9\u05D5\u05DD%A0\u05D5", + "\u05E9\u05D1\u05EA", + } + // weekdayNamesHindi list the weekday name in the Hindi. + weekdayNamesHindi = []string{ + "\u0930\u0935\u093F\u0935\u093E\u0930", + "\u0938\u094B\u092E\u0935\u093E\u0930", + "\u092E\u0902\u0917\u0932\u0935\u093E\u0930", + "\u092C\u0941\u0927\u0935\u093E\u0930", + "\u0917\u0941\u0930\u0941\u0935\u093E\u0930", + "\u0936\u0941\u0915\u094D\u0930\u0935\u093E\u0930", + "\u0936\u0928\u093F\u0935\u093E\u0930", + } + // weekdayNamesHindiAbbr list the weekday name abbreviations in the Hindi. + weekdayNamesHindiAbbr = []string{ + "\u0930\u0935\u093F.", + "\u0938\u094B\u092E.", + "\u092E\u0902\u0917\u0932.", + "\u092C\u0941\u0927.", + "\u0917\u0941\u0930\u0941.", + "\u0936\u0941\u0915\u094D\u0930.", + "\u0936\u0928\u093F.", + } + // weekdayNamesHungarian list the weekday name in the Hungarian. + weekdayNamesHungarian = []string{"vasárnap", "hétfő", "kedd", "szerda", "csütörtök", "péntek", "szombat"} + // weekdayNamesHungarianAbbr list the weekday name abbreviations in the Hungarian. + weekdayNamesHungarianAbbr = []string{"V", "H", "K", "Sze", "Cs", "P", "Szo"} + // weekdayNamesIcelandic list the weekday name in the Icelandic. + weekdayNamesIcelandic = []string{"sunnudagur", "mánudagur", "þriðjudagur", "miðvikudagur", "fimmtudagur", "föstudagur", "laugardagur"} + // weekdayNamesIcelandicAbbr list the weekday name abbreviations in the Icelandic. + weekdayNamesIcelandicAbbr = []string{"sun.", "mán.", "þri.", "mið.", "fim.", "fös.", "lau."} + // weekdayNamesIgbo list the weekday name in the Igbo. + weekdayNamesIgbo = []string{"Ụbọchị Ụka", "Mọnde", "Tiuzdee", "Wenezdee", "Tọọzdee", "Fraịdee", "Satọdee"} + // weekdayNamesIgboAbbr list the weekday name abbreviations in the Igbo. + weekdayNamesIgboAbbr = []string{"Ụka", "Mọn", "Tiu", "Wen", "Tọọ", "Fraị", "Satọdee"} + // weekdayNamesIndonesian list the weekday name in the Indonesian. + weekdayNamesIndonesian = []string{"Minggu", "Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu"} + // weekdayNamesIndonesianAbbr list the weekday name abbreviations in the Indonesian. + weekdayNamesIndonesianAbbr = []string{"Mgg", "Sen", "Sel", "Rab", "Kam", "Jum", "Sab"} + // weekdayNamesInuktitut list the weekday name in the Inuktitut. + weekdayNamesInuktitut = []string{"Naattiinguja", "Naggajjau", "Aippiq", "Pingatsiq", "Sitammiq", "Tallirmiq", "Sivataarvik"} + // weekdayNamesInuktitutAbbr list the weekday name abbreviations in the Inuktitut. + weekdayNamesInuktitutAbbr = []string{"Nat", "Nag", "Aip", "Pi", "Sit", "Tal", "Siv"} + // weekdayNamesSyllabics list the weekday name in the Syllabics. + weekdayNamesSyllabics = []string{ + "\u14C8\u1466\u144F\u1591\u152D", + "\u14C7\u14A1\u1490\u153E\u152D\u1405", + "\u140A\u1403\u1449\u1431\u1585", + "\u1431\u1593\u1466\u14EF\u1585", + "\u14EF\u1455\u14BB\u14A5\u1585", + "\u1455\u14EA\u14D5\u1550\u14A5\u1585", + "\u14EF\u1559\u1456\u1550\u1555\u1483", + } + // weekdayNamesSyllabicsAbbr list the weekday name abbreviations in the Syllabics. + weekdayNamesSyllabicsAbbr = []string{ + "\u14C8\u1466\u144F", + "\u14C7\u14A1\u1490", + "\u140A\u1403\u1449\u1431", + "\u1431\u1593\u1466\u14EF", + "\u14EF\u1455", + "\u1455\u14EA\u14D5", + "\u14EF\u1559\u1456\u1550\u1555\u1483", + } + // weekdayNamesIrish list the weekday name in the Irish. + weekdayNamesIrish = []string{"Dé Domhnaigh", "Dé Luain", "Dé Máirt", "Dé Céadaoin", "Déardaoin", "Dé hAoine", "Dé Sathairn"} + // weekdayNamesIrishAbbr list the weekday name abbreviations in the Irish. + weekdayNamesIrishAbbr = []string{"Domh", "Luan", "Máirt", "Céad", "Déar", "Aoine", "Sath"} + // weekdayNamesItalian list the weekday name in the Italian. + weekdayNamesItalian = []string{"domenica", "lunedì", "martedì", "mercoledì", "giovedì", "venerdì", "sabato"} + // weekdayNamesItalianAbbr list the weekday name abbreviations in the Italian. + weekdayNamesItalianAbbr = []string{"dom", "lun", "mar", "mer", "gio", "ven", "sab"} + // weekdayNamesJapanese list the weekday name in the Japanese. + weekdayNamesJapanese = []string{"日曜日", "月曜日", "火曜日", "水曜日", "木曜日", "金曜日", "土曜日"} + // weekdayNamesJapaneseAbbr list the weekday name abbreviations in the Japanese. + weekdayNamesJapaneseAbbr = []string{"日", "月", "火", "水", "木", "金", "土"} + // weekdayNamesKannada list the weekday name in the Kannada. + weekdayNamesKannada = []string{ + "\u0CAD\u0CBE\u0CA8\u0CC1\u0CB5\u0CBE\u0CB0", + "\u0CB8\u0CCB\u0CAE\u0CB5\u0CBE\u0CB0", + "\u0CAE\u0C82\u0C97\u0CB3\u0CB5\u0CBE\u0CB0", + "\u0CAC\u0CC1\u0CA7\u0CB5\u0CBE\u0CB0", + "\u0C97\u0CC1\u0CB0\u0CC1\u0CB5\u0CBE\u0CB0", + "\u0CB6\u0CC1\u0C95\u0CCD\u0CB0\u0CB5\u0CBE\u0CB0", + "\u0CB6\u0CA8\u0CBF\u0CB5\u0CBE\u0CB0", + } + // weekdayNamesKannadaAbbr list the weekday name abbreviations in the Kannada. + weekdayNamesKannadaAbbr = []string{ + "\u0CAD\u0CBE\u0CA8\u0CC1.", + "\u0CB8\u0CCB\u0CAE.", + "\u0CAE\u0C82\u0C97\u0CB3.", + "\u0CAC\u0CC1\u0CA7.", + "\u0C97\u0CC1\u0CB0\u0CC1.", + "\u0CB6\u0CC1\u0C95\u0CCD\u0CB0.", + "\u0CB6\u0CA8\u0CBF.", + } + // weekdayNamesKashmiri list the weekday name in the Kashmiri. + weekdayNamesKashmiri = []string{ + "\u0627\u064E\u062A\u06BE\u0648\u0627\u0631", + "\u0698\u0654\u0646\u062F\u0631\u0655\u0631\u0648\u0627\u0631", + "\u0628\u06C6\u0645\u0648\u0627\u0631", + "\u0628\u0648\u062F\u0648\u0627\u0631", + "\u0628\u0631\u0620\u0633\u0648\u0627\u0631", + "\u062C\u064F\u0645\u06C1", + "\u0628\u0679\u0648\u0627\u0631", + } + // weekdayNamesKashmiriAbbr list the weekday name abbreviations in the Kashmiri. + weekdayNamesKashmiriAbbr = []string{ + "\u0622\u062A\u06BE\u0648\u0627\u0631", + "\u0698\u0654\u0646\u062F\u0655\u0631\u0648\u0627\u0631", + "\u0628\u06C6\u0645\u0648\u0627\u0631", + "\u0628\u0648\u062F\u0648\u0627\u0631", + "\u0628\u0631\u0620\u0633\u0648\u0627\u0631", + "\u062C\u064F\u0645\u06C1", + "\u0628\u0679\u0648\u0627\u0631", + } + // weekdayNamesKazakh list the weekday name in the Kazakh. + weekdayNamesKazakh = []string{ + "\u0436\u0435\u043A\u0441\u0435\u043D\u0431\u0456", + "\u0434\u04AF\u0439\u0441\u0435\u043D\u0431\u0456", + "\u0441\u0435\u0439\u0441\u0435\u043D\u0431\u0456", + "\u0441\u04D9\u0440\u0441\u0435\u043D\u0431\u0456", + "\u0431\u0435\u0439\u0441\u0435\u043D\u0431\u0456", + "\u0436\u04B1\u043C\u0430", + "\u0441\u0435\u043D\u0431\u0456", + } + // weekdayNamesKazakhAbbr list the weekday name abbreviations in the Kazakh. + weekdayNamesKazakhAbbr = []string{ + "\u0436\u0435\u043A", + "\u0434\u04AF\u0439", + "\u0441\u0435\u0439", + "\u0441\u04D9\u0440", + "\u0431\u0435\u0439", + "\u0436\u04B1\u043C", + "\u0441\u0435\u043D", + } + // weekdayNamesKhmer list the weekday name in the Khmer. + weekdayNamesKhmer = []string{ + "\u1790\u17D2\u1784\u17C3\u17A2\u17B6\u1791\u17B7\u178F\u17D2\u1799", + "\u1790\u17D2\u1784\u17C3\u1785\u17D0\u1793\u17D2\u1791", + "\u1790\u17D2\u1784\u17C3\u17A2\u1784\u17D2\u1782\u17B6\u179A", + "\u1790\u17D2\u1784\u17C3\u1796\u17BB\u1792", + "\u1790\u17D2\u1784\u17C3\u1796\u17D2\u179A\u17A0\u179F\u17D2\u1794\u178F\u17B7\u17CD", + "\u1790\u17D2\u1784\u17C3\u179F\u17BB\u1780\u17D2\u179A", + "\u1790\u17D2\u1784\u17C3\u179F\u17C5\u179A\u17CD", + } + // weekdayNamesKhmerAbbr list the weekday name abbreviations in the Khmer. + weekdayNamesKhmerAbbr = []string{ + "\u17A2\u17B6\u1791\u17B7.", + "\u1785.", + "\u17A2.", + "\u1796\u17BB", + "\u1796\u17D2\u179A\u17A0.", + "\u179F\u17BB.", + "\u179F.", + } + // weekdayNamesKiche list the weekday name in the Kiche. + weekdayNamesKiche = []string{"juq'ij", "kaq'ij", "oxq'ij", "kajq'ij", "joq'ij", "waqq'ij", "wuqq'ij"} + // weekdayNamesKicheAbbr list the weekday name abbreviations in the Kiche. + weekdayNamesKicheAbbr = []string{"juq'", "kaq'", "oxq'", "kajq'", "joq'", "waqq'", "wuqq'"} + // weekdayNamesKinyarwanda list the weekday name in the Kinyarwanda. + weekdayNamesKinyarwanda = []string{"Ku cyumweru", "Ku wa mbere", "Ku wa kabiri", "Ku wa gatatu", "Ku wa kane", "Ku wa gatanu", "Ku wa gatandatu"} + // weekdayNamesKinyarwandaAbbr list the weekday name abbreviations in the Kinyarwanda. + weekdayNamesKinyarwandaAbbr = []string{"cyu.", "mbe.", "kab.", "gat.", "kan.", "gnu.", "gat."} + // weekdayNamesKiswahili list the weekday name in the Kiswahili. + weekdayNamesKiswahili = []string{"Jumapili", "Jumatatu", "Jumanne", "Jumatano", "Alhamisi", "Ijumaa", "Jumamosi"} + // weekdayNamesKiswahiliAbbr list the weekday name abbreviations in the Kiswahili. + weekdayNamesKiswahiliAbbr = []string{"Jpl", "Jtt", "Jnn", "Jtn", "Alh", "Ijm", "Jms"} + // weekdayNamesKonkani list the weekday name in the Konkani. + weekdayNamesKonkani = []string{ + "\u0906\u092F\u0924\u093E\u0930", + "\u0938\u094B\u092E\u093E\u0930", + "\u092E\u0902\u0917\u0933\u093E\u0930", + "\u092C\u0941\u0927\u0935\u093E\u0930", + "\u092C\u093F\u0930\u0947\u0938\u094D\u0924\u093E\u0930", + "\u0938\u0941\u0915\u094D\u0930\u093E\u0930", + "\u0936\u0947\u0928\u0935\u093E\u0930", + } + // weekdayNamesKonkaniAbbr list the weekday name abbreviations in the Konkani. + weekdayNamesKonkaniAbbr = []string{ + "\u0906\u092F.", + "\u0938\u094B\u092E.", + "\u092E\u0902\u0917\u0933.", + "\u092C\u0941\u0927.", + "\u092C\u093F\u0930\u0947.", + "\u0938\u0941\u0915\u094D\u0930.", + "\u0936\u0947\u0928.", + } + // weekdayNamesKorean list the weekday name in the Korean. + weekdayNamesKorean = []string{"일요일", "월요일", "화요일", "수요일", "목요일", "금요일", "토요일"} + // weekdayNamesKoreanAbbr list the weekday name abbreviations in the Korean. + weekdayNamesKoreanAbbr = []string{"일", "월", "화", "수", "목", "금", "토"} + // weekdayNamesKyrgyz list the weekday name in the Kyrgyz. + weekdayNamesKyrgyz = []string{ + "\u0436\u0435\u043A\u0448\u0435\u043C\u0431\u0438", + "\u0434\u04AF\u0439\u0448\u04E9\u043C\u0431\u04AF", + "\u0448\u0435\u0439\u0448\u0435\u043C\u0431\u0438", + "\u0448\u0430\u0440\u0448\u0435\u043C\u0431\u0438", + "\u0431\u0435\u0439\u0448\u0435\u043C\u0431\u0438", + "\u0436\u0443\u043C\u0430", + "\u0438\u0448\u0435\u043C\u0431\u0438", + } + // weekdayNamesKyrgyzAbbr list the weekday name abbreviations in the Kyrgyz. + weekdayNamesKyrgyzAbbr = []string{ + "\u0436\u0435\u043A.", + "\u0434\u04AF\u0439.", + "\u0448\u0435\u0439\u0448.", + "\u0448\u0430\u0440\u0448.", + "\u0431\u0435\u0439\u0448.", + "\u0436\u0443\u043C\u0430", + "\u0438\u0448\u043C.", + } + // weekdayNamesLao list the weekday name in the Lao. + weekdayNamesLao = []string{ + "\u0EA7\u0EB1\u0E99\u0EAD\u0EB2\u0E97\u0EB4\u0E94", + "\u0EA7\u0EB1\u0E99\u0E88\u0EB1\u0E99", + "\u0EA7\u0EB1\u0E99\u0EAD\u0EB1\u0E87\u0E84\u0EB2\u0E99", + "\u0EA7\u0EB1\u0E99\u0E9E\u0EB8\u0E94", + "\u0EA7\u0EB1\u0E99\u0E9E\u0EB0\u0EAB\u0EB1\u0E94", + "\u0EA7\u0EB1\u0E99\u0EAA\u0EB8\u0E81", + "\u0EA7\u0EB1\u0E99\u0EC0\u0EAA\u0EBB\u0EB2", + } + // weekdayNamesLaoAbbr list the weekday name abbreviations in the Lao. + weekdayNamesLaoAbbr = []string{ + "\u0EAD\u0EB2\u0E97\u0EB4\u0E94", + "\u0E88\u0EB1\u0E99", + "\u0EAD\u0EB1\u0E87\u0E84\u0EB2\u0E99", + "\u0E9E\u0EB8\u0E94", + "\u0E9E\u0EB0\u0EAB\u0EB1\u0E94", + "\u0EAA\u0EB8\u0E81", + "\u0EC0\u0EAA\u0EBB\u0EB2", + } + // weekdayNamesLatin list the weekday name in the Latin. + weekdayNamesLatin = []string{"Solis", "Lunae", "Martis", "Mercurii", "Jovis", "Veneris", "Saturni"} + // weekdayNamesLatinAbbr list the weekday name abbreviations in the Latin. + weekdayNamesLatinAbbr = []string{"Sol", "Lun", "Mar", "Mer", "Jov", "Ven", "Sat"} + // weekdayNamesLatvian list the weekday name in the Latvian. + weekdayNamesLatvian = []string{"svētdiena", "pirmdiena", "otrdiena", "trešdiena", "ceturtdiena", "piektdiena", "sestdiena"} + // weekdayNamesLatvianAbbr list the weekday name abbreviations in the Latvian. + weekdayNamesLatvianAbbr = []string{"svētd.", "pirmd.", "otrd.", "trešd.", "ceturtd.", "piektd.", "sestd."} + // weekdayNamesLithuanian list the weekday name in the Lithuanian. + weekdayNamesLithuanian = []string{"sekmadienis", "pirmadienis", "antradienis", "trečiadienis", "ketvirtadienis", "penktadienis", "šeštadienis"} + // weekdayNamesLithuanianAbbr list the weekday name abbreviations in the Lithuanian. + weekdayNamesLithuanianAbbr = []string{"sk", "pr", "an", "tr", "kt", "pn", "št"} + // weekdayNamesLowerSorbian list the weekday name in the Lower Sorbian. + weekdayNamesLowerSorbian = []string{"nje\u017Aela", "ponje\u017Aele", "wa\u0142tora", "srjoda", "stw%F3rtk", "p\u011Btk", "sobota"} + // weekdayNamesLowerSorbianAbbr list the weekday name abbreviations in the Luxembourgish. + weekdayNamesLowerSorbianAbbr = []string{"nje", "pon", "wa\u0142", "srj", "stw", "p\u011Bt", "sob"} + // weekdayNamesLuxembourgish list the weekday name in the Luxembourgish + weekdayNamesLuxembourgish = []string{"Sonndeg", "Méindeg", "Dënschdeg", "Mëttwoch", "Donneschdeg", "Freideg", "Samschdeg"} + // weekdayNamesLuxembourgishAbbr list the weekday name abbreviations in the Lower Sorbian. + weekdayNamesLuxembourgishAbbr = []string{"Son", "Méi", "Dën", "Mët", "Don", "Fre", "Sam"} + // weekdayNamesMacedonian list the weekday name in the Macedonian. + weekdayNamesMacedonian = []string{ + "\u043D\u0435\u0434\u0435\u043B\u0430", + "\u043F\u043E\u043D\u0435\u0434\u0435\u043B\u043D\u0438\u043A", + "\u0432\u0442\u043E\u0440\u043D\u0438\u043A", + "\u0441\u0440\u0435\u0434\u0430", + "\u0447\u0435\u0442\u0432\u0440\u0442\u043E\u043A", + "\u043F\u0435\u0442\u043E\u043A", + "\u0441\u0430\u0431\u043E\u0442\u0430", + } + // weekdayNamesMacedonianAbbr list the weekday name abbreviations in the Macedonian. + weekdayNamesMacedonianAbbr = []string{ + "\u043D\u0435\u0434.", + "\u043F\u043E\u043D.", + "\u0432\u0442.", + "\u0441\u0440\u0435.", + "\u0447\u0435\u0442.", + "\u043F\u0435\u0442.", + "\u0441\u0430\u0431.", + } + // weekdayNamesMalay list the weekday name in the Malay. + weekdayNamesMalay = []string{"Ahad", "Isnin", "Selasa", "Rabu", "Khamis", "Jumaat", "Sabtu"} + // weekdayNamesMalayAbbr list the weekday name abbreviations in the Lower Sorbian. + weekdayNamesMalayAbbr = []string{"Ahd", "Isn", "Sel", "Rab", "Kha", "Jum", "Sab"} + // weekdayNamesMalayalam list the weekday name in the Malayalam. + weekdayNamesMalayalam = []string{ + "\u0D1E\u0D3E\u0D2F\u0D31\u0D3E\u0D34\u0D4D\u200C\u0D1A", + "\u0D24\u0D3F\u0D19\u0D4D\u0D15\u0D33\u0D3E\u0D34\u0D4D\u200C\u0D1A", + "\u0D1A\u0D4A\u0D35\u0D4D\u0D35\u0D3E\u0D34\u0D4D\u0D1A", + "\u0D2C\u0D41\u0D27\u0D28\u0D3E\u0D34\u0D4D\u200C\u0D1A", + "\u0D35\u0D4D\u0D2F\u0D3E\u0D34\u0D3E\u0D34\u0D4D\u200C\u0D1A", + "\u0D35\u0D46\u0D33\u0D4D\u0D33\u0D3F\u0D2F\u0D3E\u0D34\u0D4D\u200C\u0D1A", + "\u0D36\u0D28\u0D3F\u0D2F\u0D3E\u0D34\u0D4D\u200C\u0D1A", + } + // weekdayNamesMalayalamAbbr list the weekday name abbreviations in the Malayalam. + weekdayNamesMalayalamAbbr = []string{ + "\u0D1E\u0D3E\u0D2F\u0D7C", + "\u0D24\u0D3F\u0D19\u0D4D\u0D15\u0D7E", + "\u0D1A\u0D4A\u0D35\u0D4D\u0D35", + "\u0D2C\u0D41\u0D27\u0D7B", + "\u0D35\u0D4D\u0D2F\u0D3E\u0D34\u0D02", + "\u0D35\u0D46\u0D33\u0D4D\u0D33\u0D3F", + "\u0D36\u0D28\u0D3F", + } + // weekdayNamesMaltese list the weekday name in the Maltese. + weekdayNamesMaltese = []string{"Il-\u0126add", "It-Tnejn", "It-Tlieta", "L-Erbg\u0127a", "Il-\u0126amis", "Il-\u0120img\u0127a", "Is-Sibt"} + // weekdayNamesMalteseAbbr list the weekday name abbreviations in the Maltese. + weekdayNamesMalteseAbbr = []string{"\u0126ad", "Tne", "Tli", "Erb", "\u0126am", "\u0120im", "Sib"} + // weekdayNamesMaori list the weekday name in the Maori. + weekdayNamesMaori = []string{"Rātapu", "Rāhina", "Rātū", "Rāapa", "Rāpare", "Rāmere", "Rāhoroi"} + // weekdayNamesMaoriAbbr list the weekday name abbreviations in the Maori. + weekdayNamesMaoriAbbr = []string{"Ta", "Hi", "Tū", "Apa", "Pa", "Me", "Ho"} + // weekdayNamesMapudungun list the weekday name in the Mapudungun. + weekdayNamesMapudungun = []string{"Kiñe Ante", "Epu Ante", "Kila Ante", "Meli Ante", "Kechu Ante", "Cayu Ante", "Regle Ante"} + // weekdayNamesMapudungunAbbr list the weekday name abbreviations in the Mapudungun. + weekdayNamesMapudungunAbbr = []string{"Kiñe", "Epu", "Kila", "Meli", "Kechu", "Cayu", "Regle"} + // weekdayNamesMarathi list the weekday name in the Marathi. + weekdayNamesMarathi = []string{ + "\u0930\u0935\u093F\u0935\u093E\u0930", + "\u0938\u094B\u092E\u0935\u093E\u0930", + "\u092E\u0902\u0917\u0933\u0935\u093E\u0930", + "\u092C\u0941\u0927\u0935\u093E\u0930", + "\u0917\u0941\u0930\u0941\u0935\u093E\u0930", + "\u0936\u0941\u0915\u094D\u0930\u0935\u093E\u0930", + "\u0936\u0928\u093F\u0935\u093E\u0930", + } + // weekdayNamesMarathiAbbr list the weekday name abbreviations in the Marathi. + weekdayNamesMarathiAbbr = []string{ + "\u0930\u0935\u093F.", + "\u0938\u094B\u092E.", + "\u092E\u0902\u0917\u0933.", + "\u092C\u0941\u0927.", + "\u0917\u0941\u0930\u0941.", + "\u0936\u0941\u0915\u094D\u0930.", + "\u0936\u0928\u093F.", + } + // weekdayNamesMohawk list the weekday name in the Mohawk. + weekdayNamesMohawk = []string{"Awentatokentì:ke", "Awentataón'ke", "Ratironhia'kehronòn:ke", "Soséhne", "Okaristiiáhne", "Ronwaia'tanentaktonhne", "Entákta"} + // weekdayNamesMongolian list the weekday name in the Mongolian. + weekdayNamesMongolian = []string{ + "\u043D\u044F\u043C", + "\u0434\u0430\u0432\u0430\u0430", + "\u043C\u044F\u0433\u043C\u0430\u0440", + "\u043B\u0445\u0430\u0433\u0432\u0430", + "\u043F\u04AF\u0440\u044D\u0432", + "\u0431\u0430\u0430\u0441\u0430\u043D", + "\u0431\u044F\u043C\u0431\u0430", + } + // weekdayNamesMongolianAbbr list the weekday name abbreviations in the Mongolian. + weekdayNamesMongolianAbbr = []string{ + "\u041D\u044F", + "\u0414\u0430", + "\u041C\u044F", + "\u041B\u0445", + "\u041F\u04AF", + "\u0411\u0430", + "\u0411\u044F", + } + // weekdayNamesMongolianCyrlAbbr list the weekday name abbreviations in the Mongolian (Cyrillic). + weekdayNamesMongolianCyrlAbbr = []string{ + "\u041D\u044F", + "\u0414\u0430", + "\u041C\u044F", + "\u041B\u0445\u0430", + "\u041F\u04AF", + "\u0411\u0430", + "\u0411\u044F", + } + // weekdayNamesTraditionalMongolian list the weekday name abbreviations in the Traditional Mongolian. + weekdayNamesTraditionalMongolian = []string{ + "\u182D\u1820\u1837\u1820\u182D\u202F\u1824\u1828%20\u1821\u1833\u1826\u1837", + "\u182D\u1820\u1837\u1820\u182D\u202F\u1824\u1828%20\u1828\u1822\u182D\u1821\u1828", + "\u182D\u1820\u1837\u1820\u182D\u202F\u1824\u1828%20\u182C\u1823\u1836\u1820\u1837", + "\u182D\u1820\u1837\u1820\u182D\u202F\u1824\u1828%20\u182D\u1824\u1837\u182A\u1820\u1828", + "\u182D\u1820\u1837\u1820\u182D\u202F\u1824\u1828%20\u1833\u1825\u1837\u182A\u1821\u1828", + "\u182D\u1820\u1837\u1820\u182D\u202F\u1824\u1828%20\u1832\u1820\u182A\u1824\u1828", + "\u182D\u1820\u1837\u1820\u182D\u202F\u1824\u1828%20\u1835\u1822\u1837\u182D\u1824\u182D\u1820\u1828", + } + // weekdayNamesTraditionalMongolianMN list the weekday name abbreviations in the Traditional Mongolian MN. + weekdayNamesTraditionalMongolianMN = []string{ + "\u1828\u1822\u182E\u180E\u1820", + "\u1833\u1820\u1838\u1820", + "\u182E\u1822\u182D\u182E\u1820\u1837", + "\u1840\u1820\u182D\u182A\u1820", + "\u182B\u1826\u1837\u182A\u1826", + "\u182A\u1820\u1830\u1820\u1829", + "\u182A\u1822\u182E\u182A\u1820", + } + // weekdayNamesNepali list the weekday name in the Nepali. + weekdayNamesNepali = []string{ + "\u0906\u0907\u0924\u0935\u093E\u0930", + "\u0938\u094B\u092E\u0935\u093E\u0930", + "\u092E\u0919\u094D\u0917\u0932\u0935\u093E\u0930", + "\u092C\u0941\u0927\u0935\u093E\u0930", + "\u092C\u093F\u0939\u0940\u0935\u093E\u0930", + "\u0936\u0941\u0915\u094D\u0930\u0935\u093E\u0930", + "\u0936\u0928\u093F\u0935\u093E\u0930", + } + // weekdayNamesNepaliAbbr list the weekday name abbreviations in the Nepali. + weekdayNamesNepaliAbbr = []string{ + "\u0906\u0907\u0924", + "\u0938\u094B\u092E", + "\u092E\u0919\u094D\u0917\u0932", + "\u092C\u0941\u0927", + "\u092C\u093F\u0939\u0940", + "\u0936\u0941\u0915\u094D\u0930", + "\u0936\u0928\u093F", + } + // weekdayNamesNepaliIN list the weekday name in the Nepali India. + weekdayNamesNepaliIN = []string{ + "\u0906\u0907\u0924\u092C\u093E\u0930", + "\u0938\u094B\u092E\u092C\u093E\u0930", + "\u092E\u0919\u094D\u0917\u0932\u092C\u093E\u0930", + "\u092C\u0941\u0927\u092C\u093E\u0930", + "\u092C\u093F\u0939\u093F\u092C\u093E\u0930", + "\u0936\u0941\u0915\u094D\u0930\u092C\u093E\u0930", + "\u0936\u0928\u093F\u092C\u093E\u0930", + } + // weekdayNamesNepaliINAbbr list the weekday name abbreviations in the Nepali India. + weekdayNamesNepaliINAbbr = []string{ + "\u0906\u0907\u0924", + "\u0938\u094B\u092E", + "\u092E\u0919\u094D\u0917\u0932", + "\u092C\u0941\u0927", + "\u092C\u093F\u0939\u093F", + "\u0936\u0941\u0915\u094D\u0930", + "\u0936\u0928\u093F", + } + // weekdayNamesNorwegian list the weekday name in the Norwegian. + weekdayNamesNorwegian = []string{"s%F8ndag", "mandag", "tirsdag", "onsdag", "torsdag", "fredag", "l%F8rdag"} + // weekdayNamesNorwegianAbbr list the weekday name abbreviations in the Norwegian. + weekdayNamesNorwegianAbbr = []string{"s%F8n.", "man.", "tir.", "ons.", "tor.", "fre.", "l%F8r."} + // weekdayNamesNorwegianNOAbbr list the weekday name abbreviations in the Norwegian Norway. + weekdayNamesNorwegianNOAbbr = []string{"s%F8n", "man", "tir", "ons", "tor", "fre", "l%F8r"} + // weekdayNamesNorwegianNynorsk list the weekday name abbreviations in the Norwegian Nynorsk. + weekdayNamesNorwegianNynorsk = []string{"s%F8ndag", "m%E5ndag", "tysdag", "onsdag", "torsdag", "fredag", "laurdag"} + // weekdayNamesNorwegianNynorskAbbr list the weekday name abbreviations in the Norwegian Nynorsk. + weekdayNamesNorwegianNynorskAbbr = []string{"s%F8n", "m%E5n", "tys", "ons", "tor", "fre", "lau"} + // weekdayNamesOccitan list the weekday name abbreviations in the Occitan. + weekdayNamesOccitan = []string{"dimenge", "diluns", "dimarts", "dimècres", "dijòus", "divendres", "dissabte"} + // weekdayNamesOccitanAbbr list the weekday name abbreviations in the Occitan. + weekdayNamesOccitanAbbr = []string{"dg.", "dl.", "dma.", "dmc.", "dj.", "dv.", "ds."} + // weekdayNamesOdia list the weekday name in the Odia. + weekdayNamesOdia = []string{ + "\u0B30\u0B2C\u0B3F\u0B2C\u0B3E\u0B30", + "\u0B38\u0B4B\u0B2E\u0B2C\u0B3E\u0B30", + "\u0B2E\u0B19\u0B4D\u0B17\u0B33\u0B2C\u0B3E\u0B30", + "\u0B2C\u0B41\u0B27\u0B2C\u0B3E\u0B30", + "\u0B17\u0B41\u0B30\u0B41\u0B2C\u0B3E\u0B30", + "\u0B36\u0B41\u0B15\u0B4D\u0B30\u0B2C\u0B3E\u0B30", + "\u0B36\u0B28\u0B3F\u0B2C\u0B3E\u0B30", + } + // weekdayNamesOdiaAbbr list the weekday name abbreviations in the Odia. + weekdayNamesOdiaAbbr = []string{ + "\u0B30\u0B2C\u0B3F.", + "\u0B38\u0B4B\u0B2E.", + "\u0B2E\u0B19\u0B4D\u0B17\u0B33.", + "\u0B2C\u0B41\u0B27.", + "\u0B17\u0B41\u0B30\u0B41.", + "\u0B36\u0B41\u0B15\u0B4D\u0B30.", + "\u0B36\u0B28\u0B3F.", + } + // weekdayNamesOromo list the weekday name abbreviations in the Oromo. + weekdayNamesOromo = []string{"Dilbata", "Wiixata", "Qibxata", "Roobii", "Kamiisa", "Jimaata", "Sanbata"} + // weekdayNamesOromoAbbr list the weekday name abbreviations in the Oromo. + weekdayNamesOromoAbbr = []string{"Dil", "Wix", "Qib", "Rob", "Kam", "Jim", "San"} + // weekdayNamesPashto list the weekday name in the Pashto. + weekdayNamesPashto = []string{ + "\u064A\u0648\u0646\u06CD", + "\u062F\u0648\u0646\u06CD", + "\u062F\u0631\u06D0\u0646\u06CD", + "\u0685\u0644\u0631\u0646\u06CD", + "\u067E\u064A\u0646\u0681\u0646\u06CD", + "\u062C\u0645\u0639\u0647", + "\u0627\u0648\u0646\u06CD", + } + // weekdayNamesPersian list the weekday name in the Persian. + weekdayNamesPersian = []string{ + "\u064A\u0643\u0634\u0646\u0628\u0647", + "\u062F\u0648\u0634\u0646\u0628\u0647", + "\u0633\u0647%A0\u0634\u0646\u0628\u0647", + "\u0686\u0647\u0627\u0631\u0634\u0646\u0628\u0647", + "\u067E\u0646\u062C\u0634\u0646\u0628\u0647", + "\u062C\u0645\u0639\u0647", + "\u0634\u0646\u0628\u0647", + } + // weekdayNamesPolish list the weekday name abbreviations in the Polish. + weekdayNamesPolish = []string{"niedziela", "poniedziałek", "wtorek", "środa", "czwartek", "piątek", "sobota"} + // weekdayNamesPolishAbbr list the weekday name abbreviations in the Polish. + weekdayNamesPolishAbbr = []string{"niedz.", "pon.", "wt.", "śr.", "czw.", "pt.", "sob."} + // weekdayNamesPortuguese list the weekday name abbreviations in the Portuguese. + weekdayNamesPortuguese = []string{"domingo", "segunda-feira", "terça-feira", "quarta-feira", "quinta-feira", "sexta-feira", "sábado"} + // weekdayNamesPortugueseAbbr list the weekday name abbreviations in the Portuguese. + weekdayNamesPortugueseAbbr = []string{"dom", "seg", "ter", "qua", "qui", "sex", "sáb"} + // weekdayNamesPunjabi list the weekday name in the Punjabi. + weekdayNamesPunjabi = []string{ + "\u0A10\u0A24\u0A35\u0A3E\u0A30", + "\u0A38\u0A4B\u0A2E\u0A35\u0A3E\u0A30", + "\u0A2E\u0A70\u0A17\u0A32\u0A35\u0A3E\u0A30", + "\u0A2C\u0A41\u0A71\u0A27\u0A35\u0A3E\u0A30", + "\u0A35\u0A40\u0A30\u0A35\u0A3E\u0A30", + "\u0A38\u0A3C\u0A41\u0A71\u0A15\u0A30\u0A35\u0A3E\u0A30", + "\u0A38\u0A3C\u0A28\u0A3F\u0A71\u0A1A\u0A30\u0A35\u0A3E\u0A30", + } + // weekdayNamesPunjabiAbbr list the weekday name abbreviations in the Punjabi. + weekdayNamesPunjabiAbbr = []string{ + "\u0A10\u0A24.", + "\u0A38\u0A4B\u0A2E.", + "\u0A2E\u0A70\u0A17\u0A32.", + "\u0A2C\u0A41\u0A71\u0A27.", + "\u0A35\u0A40\u0A30.", + "\u0A38\u0A3C\u0A41\u0A15\u0A30.", + "\u0A38\u0A3C\u0A28\u0A3F\u0A71\u0A1A\u0A30.", + } + // weekdayNamesPunjabiArab list the weekday name in the Punjabi Arab. + weekdayNamesPunjabiArab = []string{ + "\u067E\u064A\u0631", + "\u0645\u0646\u06AF\u0644", + "\u0628\u062F\u06BE", + "\u062C\u0645\u0639\u0631\u0627\u062A", + "\u062C\u0645\u0639\u0647", + "\u0647\u0641\u062A\u0647", + "\u0627\u062A\u0648\u0627\u0631", + } + // weekdayNamesQuechua list the weekday name abbreviations in the Quechua. + weekdayNamesQuechua = []string{"intichaw", "killachaw", "atipachaw", "quyllurchaw", "Ch' askachaw", "Illapachaw", "k'uychichaw"} + // weekdayNamesQuechuaAbbr list the weekday name abbreviations in the Quechua. + weekdayNamesQuechuaAbbr = []string{"int", "kil", "ati", "quy", "Ch'", "Ill", "k'u"} + // weekdayNamesQuechuaEcuador list the weekday name abbreviations in the Quechua Ecuador. + weekdayNamesQuechuaEcuador = []string{"inti", "awaki", "wanra", "chillay", "kullka", "chaska", "wakma"} + // weekdayNamesQuechuaEcuadorAbbr list the weekday name abbreviations in the Quechua Ecuador. + weekdayNamesQuechuaEcuadorAbbr = []string{"int", "awk", "wan", "chy", "kuk", "cha", "wak"} + // weekdayNamesQuechuaPeru list the weekday name abbreviations in the Quechua Peru. + weekdayNamesQuechuaPeru = []string{"Domingo", "Lunes", "Martes", "Miércoles", "Jueves", "Viernes", "Sábado"} + // weekdayNamesQuechuaPeruAbbr list the weekday name abbreviations in the Quechua Peru. + weekdayNamesQuechuaPeruAbbr = []string{"Dom", "Lun", "Mar", "Mié", "Jue", "Vie", "Sab"} + // weekdayNamesRomanian list the weekday name abbreviations in the Romanian. + weekdayNamesRomanian = []string{"duminică", "luni", "marți", "miercuri", "joi", "vineri", "sâmbătă"} + // weekdayNamesRomanianAbbr list the weekday name abbreviations in the Romanian. + weekdayNamesRomanianAbbr = []string{"dum.", "lun.", "mar.", "mie.", "joi", "vin.", "sâm."} + // weekdayNamesRomanianMoldovaAbbr list the weekday name abbreviations in the Romanian Moldova. + weekdayNamesRomanianMoldovaAbbr = []string{"Du", "Lu", "Mar", "Mie", "Jo", "Vi", "Sâ"} + // weekdayNamesRomansh list the weekday name abbreviations in the Romansh. + weekdayNamesRomansh = []string{"dumengia", "glindesdi", "mardi", "mesemna", "gievgia", "venderdi", "sonda"} + // weekdayNamesRomanshAbbr list the weekday name abbreviations in the Romansh. + weekdayNamesRomanshAbbr = []string{"du", "gli", "ma", "me", "gie", "ve", "so"} + // weekdayNamesRussian list the weekday name abbreviations in the Russian. + weekdayNamesRussian = []string{ + "\u0432\u043E\u0441\u043A\u0440\u0435\u0441\u0435\u043D\u044C\u0435", + "\u043F\u043E\u043D\u0435\u0434\u0435\u043B\u044C\u043D\u0438\u043A", + "\u0432\u0442\u043E\u0440\u043D\u0438\u043A", + "\u0441\u0440\u0435\u0434\u0430", + "\u0447\u0435\u0442\u0432\u0435\u0440\u0433", + "\u043F\u044F\u0442\u043D\u0438\u0446\u0430", + "\u0441\u0443\u0431\u0431\u043E\u0442\u0430", + } + // weekdayNamesRussianAbbr list the weekday name abbreviations in the Russian. + weekdayNamesRussianAbbr = []string{ + "\u0412\u0441", + "\u041F\u043D", + "\u0412\u0442", + "\u0421\u0440", + "\u0427\u0442", + "\u041F\u0442", + "\u0421\u0431", + } + // weekdayNamesSakha list the weekday name abbreviations in the Sakha. + weekdayNamesSakha = []string{ + "\u04E8\u0440\u04E9\u0431\u04AF\u043B", + "\u044D\u043D\u0438\u0434\u0438\u044D\u043D\u043D\u044C\u0438\u043A", + "\u041E\u043F\u0442\u0443\u043E\u0440\u0443\u043D\u043D\u044C\u0443\u043A", + "\u0421\u044D\u0440\u044D\u0434\u044D\u044D", + "\u0427\u044D\u043F\u043F\u0438\u044D\u0440", + "\u0411\u044D\u044D\u0442\u0438\u043D\u0441\u044D", + "\u0421\u0443\u0431\u0443\u043E\u0442\u0430", + } + // weekdayNamesSakhaAbbr list the weekday name abbreviations in the Sakha. + weekdayNamesSakhaAbbr = []string{ + "\u04E8\u0440", + "\u0431\u043D", + "\u043E\u043F", + "\u0441\u044D", + "\u0447\u043F", + "\u0431\u044D", + "\u0441\u0431", + } + // weekdayNamesSami list the weekday name abbreviations in the Sami. + weekdayNamesSami = []string{"pasepeivi", "vuossargâ", "majebargâ", "koskokko", "tuorâstâh", "vástuppeivi", "lávurdâh"} + // weekdayNamesSamiAbbr list the weekday name abbreviations in the Sami. + weekdayNamesSamiAbbr = []string{"pas", "vuo", "maj", "kos", "tuo", "vás", "láv"} + // weekdayNamesSamiSamiLule list the weekday name abbreviations in the Sami (SamiLule). + weekdayNamesSamiSamiLule = []string{"ájllek", "mánnodahka", "dijstahka", "gasskavahkko", "duorastahka", "bierjjedahka", "lávvodahka"} + // weekdayNamesSamiSamiLuleAbbr list the weekday name abbreviations in the Sami (SamiLule). + weekdayNamesSamiSamiLuleAbbr = []string{"ájl", "mán", "dis", "gas", "duor", "bier", "láv"} + // weekdayNamesSamiSweden list the weekday name abbreviations in the Sami (Lule) Sweden. + weekdayNamesSamiSweden = []string{"sådnåbiejvve", "mánnodahka", "dijstahka", "gasskavahkko", "duorastahka", "bierjjedahka", "lávvodahka"} + // weekdayNamesSamiSwedenAbbr list the weekday name abbreviations in the Sami (Lule) Sweden. + weekdayNamesSamiSwedenAbbr = []string{"såd", "mán", "dis", "gas", "duor", "bier", "láv"} + // weekdayNamesSamiNorthern list the weekday name abbreviations in the Sami (Northern). + weekdayNamesSamiNorthern = []string{"sotnabeaivi", "vuossárga", "maŋŋebárga", "gaskavahkku", "duorasdat ", "bearjadat", "lávvardat"} + // weekdayNamesSamiNorthernFIAbbr list the weekday name abbreviations in the Sami (Northern). + weekdayNamesSamiNorthernAbbr = []string{"sotn", "vuos", "maŋ", "gask", "duor", "bear", "láv"} + // weekdayNamesSamiNorthernFI list the weekday name abbreviations in the Sami (Northern) Finland. + weekdayNamesSamiNorthernFI = []string{"sotnabeaivi", "vuossárga", "maŋŋebárga", "gaskavahkku", "duorastat", "bearjadat", "lávvardat"} + // weekdayNamesSamiNorthernFIAbbr list the weekday name abbreviations in the Sami (Northern) Finland. + weekdayNamesSamiNorthernFIAbbr = []string{"so", "má", "di", "ga", "du", "be", "lá"} + // weekdayNamesSamiNorthernSE list the weekday name abbreviations in the Sami (Northern) Sweden. + weekdayNamesSamiNorthernSE = []string{"sotnabeaivi", "mánnodat", "disdat", "gaskavahkku", "duorastat", "bearjadat", "lávvardat"} + // weekdayNamesSamiNorthernSEAbbr list the weekday name abbreviations in the Sami (Northern) Sweden. + weekdayNamesSamiNorthernSEAbbr = []string{"sotn", "mán", "dis", "gask", "duor", "bear", "láv"} + // weekdayNamesSamiSkolt list the weekday name abbreviations in the Sami (Skolt). + weekdayNamesSamiSkolt = []string{"p%E2%B4sspei%B4vv", "vu%F5ssargg", "m%E2%E2ibargg", "se%E4rad", "neljdpei%B4vv", "pi%E2tn%E2c", "sue%B4vet"} + // weekdayNamesSamiSkoltAbbr list the weekday name abbreviations in the Sami (Skolt). + weekdayNamesSamiSkoltAbbr = []string{"p%E2", "vu", "m%E2", "se", "ne", "pi", "su"} + // weekdayNamesSamiSouthern list the weekday name abbreviations in the Sami (Southern). + weekdayNamesSamiSouthern = []string{"aejlege", "m%E5anta", "d%E6jsta", "gaskev%E5hkoe", "duarsta", "bearjadahke", "laavvardahke"} + // weekdayNamesSamiSouthernAbbr list the weekday name abbreviations in the Sami (Southern). + weekdayNamesSamiSouthernAbbr = []string{"aej", "m%E5a", "d%E6j", "gask", "duar", "bearj", "laav"} + // weekdayNamesSanskrit list the weekday name abbreviations in the Sanskrit. + weekdayNamesSanskrit = []string{ + "\u0930\u0935\u093F\u0935\u093E\u0938\u0930\u0903", + "\u0938\u094B\u092E\u0935\u093E\u0938\u0930\u0903", + "\u092E\u0902\u0917\u0932\u0935\u093E\u0938\u0930\u0903", + "\u092C\u0941\u0927\u0935\u093E\u0938\u0930\u0903", + "\u0917\u0941\u0930\u0941\u0935\u093E\u0938\u0930%3A", + "\u0936\u0941\u0915\u094D\u0930\u0935\u093E\u0938\u0930\u0903", + "\u0936\u0928\u093F\u0935\u093E\u0938\u0930\u0903", + } + // weekdayNamesSanskritAbbr list the weekday name abbreviations in the Sanskrit. + weekdayNamesSanskritAbbr = []string{ + "\u0930\u0935\u093F", + "\u0938\u094B\u092E", + "\u092E\u0919\u094D\u0917", + "\u092C\u0941\u0927", + "\u0917\u0941\u0930\u0941", + "\u0936\u0941\u0915\u094D\u0930", + "\u0936\u0928\u093F", + } + // weekdayNamesGaelic list the weekday name abbreviations in the Gaelic. + weekdayNamesGaelic = []string{"DiDòmhnaich", "DiLuain", "DiMàirt", "DiCiadain", "DiarDaoin", "DihAoine", "DiSathairne"} + // weekdayNamesGaelicAbbr list the weekday name abbreviations in the Gaelic + weekdayNamesGaelicAbbr = []string{"DiD", "DiL", "DiM", "DiC", "Dia", "Dih", "DiS"} + // weekdayNamesSerbian list the weekday name abbreviations in the Serbian. + weekdayNamesSerbian = []string{ + "\u043D\u0435\u0434\u0435\u0459\u0430", + "\u043F\u043E\u043D\u0435\u0434\u0435\u0459\u0430\u043A", + "\u0443\u0442\u043E\u0440\u0430\u043A", + "\u0441\u0440\u0435\u0434\u0430", + "\u0447\u0435\u0442\u0432\u0440\u0442\u0430\u043A", + "\u043F\u0435\u0442\u0430\u043A", + "\u0441\u0443\u0431\u043E\u0442\u0430", + } + // weekdayNamesSerbianAbbr list the weekday name abbreviations in the Serbian. + weekdayNamesSerbianAbbr = []string{ + "\u043D\u0435\u0434.", + "\u043F\u043E\u043D.", + "\u0443\u0442.", + "\u0441\u0440.", + "\u0447\u0435\u0442.", + "\u043F\u0435\u0442.", + "\u0441\u0443\u0431.", + } + // weekdayNamesSerbianBA list the weekday name abbreviations in the Serbian (Cyrillic) Bosnia and Herzegovina. + weekdayNamesSerbianBA = []string{ + "\u043D\u0435\u0434\u0458\u0435\u0459\u0430", + "\u043F\u043E\u043D\u0435\u0434\u0458\u0435\u0459\u0430\u043A", + "\u0443\u0442\u043E\u0440\u0430\u043A", + "\u0441\u0440\u0438\u0458\u0435\u0434\u0430", + "\u0447\u0435\u0442\u0432\u0440\u0442\u0430\u043A", + "\u043F\u0435\u0442\u0430\u043A", + "\u0441\u0443\u0431\u043E\u0442\u0430", + } + // weekdayNamesSerbianBAAbbr list the weekday name abbreviations in the Serbian (Cyrillic) Bosnia and Herzegovina. + weekdayNamesSerbianBAAbbr = []string{ + "\u043D\u0435\u0434", + "\u043F\u043E\u043D", + "\u0443\u0442\u043E", + "\u0441\u0440\u0438", + "\u0447\u0435\u0442", + "\u043F\u0435\u0442", + "\u0441\u0443\u0431", + } + // weekdayNamesSerbianLatin list the weekday name abbreviations in the Serbian (Latin). + weekdayNamesSerbianLatin = []string{"nedelja", "ponedeljak", "utorak", "sreda", "četvrtak", "petak", "subota"} + // weekdayNamesSerbianLatinAbbr list the weekday name abbreviations in the Serbian (Latin). + weekdayNamesSerbianLatinAbbr = []string{"ned", "pon", "uto", "sre", "čet", "pet", "sub"} + // weekdayNamesSerbianLatinBA list the weekday name abbreviations in the Serbian (Latin) Bosnia and Herzegovina. + weekdayNamesSerbianLatinBA = []string{"nedjelja", "ponedjeljak", "utorak", "srijeda", "četvrtak", "petak", "subota"} + // weekdayNamesSerbianLatinBAAbbr list the weekday name abbreviations in the Serbian (Latin) Bosnia and Herzegovina. + weekdayNamesSerbianLatinBAAbbr = []string{"ned", "pon", "uto", "sri", "čet", "pet", "sub"} + // weekdayNamesSerbianLatinCSAbbr list the weekday name abbreviations in the Serbian (Latin) Serbia and Montenegro (Former). + weekdayNamesSerbianLatinCSAbbr = []string{"ned.", "pon.", "uto.", "sre.", "čet.", "pet.", "sub."} + // weekdayNamesSerbianLatinME list the weekday name abbreviations in the Serbian (Latin) Montenegro. + weekdayNamesSerbianLatinME = []string{"nedjelja", "ponedeljak", "utorak", "srijeda", "četvrtak", "petak", "subota"} + // weekdayNamesSerbianME list the weekday name abbreviations in the Serbian (Cyrillic) Montenegro. + weekdayNamesSerbianME = []string{ + "\u043D\u0435\u0434\u0435\u0459\u0430", + "\u043F\u043E\u043D\u0435\u0434\u0458\u0435\u0459\u0430\u043A", + "\u0443\u0442\u043E\u0440\u0430\u043A", + "\u0441\u0440\u0438\u0458\u0435\u0434\u0430", + "\u0447\u0435\u0442\u0432\u0440\u0442\u0430\u043A", + "\u043F\u0435\u0442\u0430\u043A", + "\u0441\u0443\u0431\u043E\u0442\u0430", + } + // weekdayNamesSesothoSaLeboa list the weekday name abbreviations in the Sesotho sa Leboa. + weekdayNamesSesothoSaLeboa = []string{"Lamorena", "Musopologo", "Labobedi", "Laboraro", "Labone", "Labohlano", "Mokibelo"} + // weekdayNamesSesothoSaLeboaAbbr list the weekday name abbreviations in the Sesotho sa Leboa. + weekdayNamesSesothoSaLeboaAbbr = []string{"Lam", "Moš", "Lbb", "Lbr", "Lbn", "Lbh", "Mok"} + // weekdayNamesSetswana list the weekday name abbreviations in the Setswana. + weekdayNamesSetswana = []string{"Sontaga", "Mosopulogo", "Labobedi", "Laboraro", "Labone", "Labotlhano", "Matlhatso"} + // weekdayNamesSetswanaAbbr list the weekday name abbreviations in the Setswana. + weekdayNamesSetswanaAbbr = []string{"Sont.", "Mos.", "Lab.", "Labr.", "Labn.", "Labt.", "Matlh."} + // weekdayNamesSindhi list the weekday name abbreviations in the Sindhi. + weekdayNamesSindhi = []string{ + "\u0633\u0648\u0645\u0631", + "\u0627\u06B1\u0627\u0631\u0648", + "\u0627\u0631\u0628\u0639", + "\u062E\u0645\u064A\u0633", + "\u062C\u0645\u0639\u0648", + "\u0687\u0646\u0687\u0631", + "\u0622\u0686\u0631", + } + // weekdayNamesSindhiAbbr list the weekday name abbreviations in the Sindhi. + weekdayNamesSindhiAbbr = []string{ + "\u0633\u0648", + "\u0627\u06B1", + "\u0627\u0631", + "\u062E\u0645", + "\u062C\u0645\u0639\u0648", + "\u0687\u0646", + "\u0622\u0686", + } + // weekdayNamesSlovak list the weekday name abbreviations in the Slovak. + weekdayNamesSlovak = []string{"nedeľa", "pondelok", "utorok", "streda", "štvrtok", "piatok", "sobota"} + // weekdayNamesSlovakAbbr list the weekday name abbreviations in the Slovak. + weekdayNamesSlovakAbbr = []string{"ne", "po", "ut", "st", "št", "pi", "so"} + // weekdayNamesSlovenian list the weekday name abbreviations in the Slovenian. + weekdayNamesSlovenian = []string{"nedelja", "ponedeljek", "torek", "sreda", "četrtek", "petek", "sobota"} + // weekdayNamesSlovenianAbbr list the weekday name abbreviations in the Slovenian. + weekdayNamesSlovenianAbbr = []string{"ned.", "pon.", "tor.", "sre.", "čet.", "pet.", "sob."} + // weekdayNamesSomali list the weekday name abbreviations in the Somali. + weekdayNamesSomali = []string{"Axad", "Isniin", "Talaado", "Arbaco", "Khamiis", "Jimco", "Sabti"} + // weekdayNamesSomaliAbbr list the weekday name abbreviations in the Somali. + weekdayNamesSomaliAbbr = []string{"Axd", "Isn", "Tldo", "Arbc", "Khms", "Jmc", "Sbti"} + // weekdayNamesSotho list the weekday name abbreviations in the Sotho. + weekdayNamesSotho = []string{"Sontaha", "Mmantaha", "Labobedi", "Laboraru", "Labone", "Labohlane", "Moqebelo"} + // weekdayNamesSothoAbbr list the weekday name abbreviations in the Sotho. + weekdayNamesSothoAbbr = []string{"Son", "Mma", "Bed", "Rar", "Ne", "Hla", "Moq"} + // weekdayNamesSpanish list the weekday name abbreviations in the Spanish. + weekdayNamesSpanish = []string{"domingo", "lunes", "martes", "miércoles", "jueves", "viernes", "sábado"} + // weekdayNamesSpanishAbbr list the weekday name abbreviations in the Spanish Argentina. + weekdayNamesSpanishAbbr = []string{"do.", "lu.", "ma.", "mi.", "ju.", "vi.", "sá."} + // weekdayNamesSpanishARAbbr list the weekday name abbreviations in the Spanish Argentina. + weekdayNamesSpanishARAbbr = []string{"dom.", "lun.", "mar.", "mié.", "jue.", "vie.", "sáb."} + // weekdayNamesSpanishUSAbbr list the weekday name abbreviations in the Spanish United States. + weekdayNamesSpanishUSAbbr = []string{"dom", "lun", "mar", "mié", "jue", "vie", "sáb"} + // weekdayNamesSwedish list the weekday name abbreviations in the Swedish. + weekdayNamesSwedish = []string{"söndag", "måndag", "tisdag", "onsdag", "torsdag", "fredag", "lördag"} + // weekdayNamesSwedishAbbr list the weekday name abbreviations in the Swedish Argentina. + weekdayNamesSwedishAbbr = []string{"sön", "mån", "tis", "ons", "tor", "fre", "lör"} + // weekdayNamesSyriac list the weekday name abbreviations in the Syriac. + weekdayNamesSyriac = []string{ + "\u071A\u0715%A0\u0712\u072B\u0712\u0710", + "\u072C\u072A\u071D\u0722%A0\u0712\u072B\u0712\u0710", + "\u072C\u0720\u072C\u0710%A0\u0712\u072B\u0712\u0710", + "\u0710\u072A\u0712\u0725\u0710%A0\u0712\u072B\u0712\u0710", + "\u071A\u0721\u072B\u0710%A0\u0712\u072B\u0712\u0710", + "\u0725\u072A\u0718\u0712\u072C\u0710", + "\u072B\u0712\u072C\u0710", + } + // weekdayNamesSyriacAbbr list the weekday name abbreviations in the Syriac. + weekdayNamesSyriacAbbr = []string{ + "\u070F\u0710%A0\u070F\u0712\u072B", + "\u070F\u0712%A0\u070F\u0712\u072B", + "\u070F\u0713%A0\u070F\u0712\u072B", + "\u070F\u0715%A0\u070F\u0712\u072B", + "\u070F\u0717%A0\u070F\u0712\u072B", + "\u070F\u0725\u072A\u0718\u0712", + "\u070F\u072B\u0712", + } + // weekdayNamesTajik list the weekday name abbreviations in the Tajik. + weekdayNamesTajik = []string{ + "\u042F\u043A\u0448\u0430\u043D\u0431\u0435", + "\u0434\u0443\u0448\u0430\u043D\u0431\u0435", + "\u0441\u0435\u0448\u0430\u043D\u0431\u0435", + "\u0447\u043E\u0440\u0448\u0430\u043D\u0431\u0435", + "\u043F\u0430\u043D\u04B7\u0448\u0430\u043D\u0431\u0435", + "\u04B7\u0443\u043C\u044A\u0430", + "\u0448\u0430\u043D\u0431\u0435", + } + // weekdayNamesTajikAbbr list the weekday name abbreviations in the Tajik. + weekdayNamesTajikAbbr = []string{ + "\u043F\u043A\u0448", + "\u0434\u0448\u0431", + "\u0441\u0448\u0431", + "\u0447\u0448\u0431", + "\u043F\u0448\u0431", + "\u04B7\u0443\u043C", + "\u0448\u043D\u0431", + } + // weekdayNamesTamazight list the weekday name abbreviations in the Tamazight. + weekdayNamesTamazight = []string{"lh'ed", "letnayen", "ttlata", "larebâa", "lexmis", "ldjemâa", "ssebt"} + // weekdayNamesTamazightAbbr list the weekday name abbreviations in the Tamazight Argentina. + weekdayNamesTamazightAbbr = []string{"lh'd", "let", "ttl", "lar", "lex", "ldj", "sse"} + // weekdayNamesTamil list the weekday name abbreviations in the Tamil. + weekdayNamesTamil = []string{ + "\u0B9E\u0BBE\u0BAF\u0BBF\u0BB1\u0BCD\u0BB1\u0BC1\u0B95\u0BCD\u0B95\u0BBF\u0BB4\u0BAE\u0BC8", + "\u0BA4\u0BBF\u0B99\u0BCD\u0B95\u0BB3\u0BCD\u0B95\u0BBF\u0BB4\u0BAE\u0BC8", + "\u0B9A\u0BC6\u0BB5\u0BCD\u0BB5\u0BBE\u0BAF\u0BCD\u0B95\u0BCD\u0B95\u0BBF\u0BB4\u0BAE\u0BC8", + "\u0BAA\u0BC1\u0BA4\u0BA9\u0BCD\u0B95\u0BBF\u0BB4\u0BAE\u0BC8", + "\u0BB5\u0BBF\u0BAF\u0BBE\u0BB4\u0B95\u0BCD\u0B95\u0BBF\u0BB4\u0BAE\u0BC8", + "\u0BB5\u0BC6\u0BB3\u0BCD\u0BB3\u0BBF\u0B95\u0BCD\u0B95\u0BBF\u0BB4\u0BAE\u0BC8", + "\u0B9A\u0BA9\u0BBF\u0B95\u0BCD\u0B95\u0BBF\u0BB4\u0BAE\u0BC8", + } + // weekdayNamesTamilAbbr list the weekday name abbreviations in the Tamil. + weekdayNamesTamilAbbr = []string{ + "\u0B9E\u0BBE\u0BAF\u0BBF\u0BB1\u0BC1", + "\u0BA4\u0BBF\u0B99\u0BCD\u0B95\u0BB3\u0BCD", + "\u0B9A\u0BC6\u0BB5\u0BCD\u0BB5\u0BBE\u0BAF\u0BCD", + "\u0BAA\u0BC1\u0BA4\u0BA9\u0BCD", + "\u0BB5\u0BBF\u0BAF\u0BBE\u0BB4\u0BA9\u0BCD", + "\u0BB5\u0BC6\u0BB3\u0BCD\u0BB3\u0BBF", + "\u0B9A\u0BA9\u0BBF", + } + // weekdayNamesTamilLK list the weekday name abbreviations in the Tamil Sri Lanka. + weekdayNamesTamilLK = []string{ + "\u0B9E\u0BBE\u0BAF\u0BBF\u0BB1\u0BC1", + "\u0BA4\u0BBF\u0B99\u0BCD\u0B95\u0BB3\u0BCD", + "\u0B9A\u0BC6\u0BB5\u0BCD\u0BB5\u0BBE\u0BAF\u0BCD", + "\u0BAA\u0BC1\u0BA4\u0BA9\u0BCD", + "\u0BB5\u0BBF\u0BAF\u0BBE\u0BB4\u0BA9\u0BCD", + "\u0BB5\u0BC6\u0BB3\u0BCD\u0BB3\u0BBF", + "\u0B9A\u0BA9\u0BBF", + } + // weekdayNamesTamilLKAbbr list the weekday name abbreviations in the Tamil Sri Lanka. + weekdayNamesTamilLKAbbr = []string{ + "\u0B9E\u0BBE\u0BAF\u0BBF.", + "\u0BA4\u0BBF\u0B99\u0BCD.", + "\u0B9A\u0BC6\u0BB5\u0BCD.", + "\u0BAA\u0BC1\u0BA4.", + "\u0BB5\u0BBF\u0BAF\u0BBE.", + "\u0BB5\u0BC6\u0BB3\u0BCD.", + "\u0B9A\u0BA9\u0BBF", + } + // weekdayNamesTatar list the weekday name abbreviations in the Tatar. + weekdayNamesTatar = []string{ + "\u044F\u043A\u0448\u04D9\u043C\u0431\u0435", + "\u0434\u04AF\u0448\u04D9\u043C\u0431\u0435", + "\u0441\u0438\u0448\u04D9\u043C\u0431\u0435", + "\u0447\u04D9\u0440\u0448\u04D9\u043C\u0431\u0435", + "\u043F\u04D9\u043D\u0497\u0435\u0448\u04D9\u043C\u0431\u0435", + "\u0497\u043E\u043C\u0433\u0430", + "\u0448\u0438\u043C\u0431\u04D9", + } + // weekdayNamesTatarAbbr list the weekday name abbreviations in the Tatar. + weekdayNamesTatarAbbr = []string{ + "\u044F\u043A\u0448.", + "\u0434\u04AF\u0448.", + "\u0441\u0438\u0448.", + "\u0447\u04D9\u0440\u0448.", + "\u043F\u04D9\u043D\u0497.", + "\u0497\u043E\u043C.", + "\u0448\u0438\u043C.", + } + // weekdayNamesTelugu list the weekday name abbreviations in the Telugu. + weekdayNamesTelugu = []string{ + "\u0C06\u0C26\u0C3F\u0C35\u0C3E\u0C30\u0C02", + "\u0C38\u0C4B\u0C2E\u0C35\u0C3E\u0C30\u0C02", + "\u0C2E\u0C02\u0C17\u0C33\u0C35\u0C3E\u0C30\u0C02", + "\u0C2C\u0C41\u0C27\u0C35\u0C3E\u0C30\u0C02", + "\u0C17\u0C41\u0C30\u0C41\u0C35\u0C3E\u0C30\u0C02", + "\u0C36\u0C41\u0C15\u0C4D\u0C30\u0C35\u0C3E\u0C30\u0C02", + "\u0C36\u0C28\u0C3F\u0C35\u0C3E\u0C30\u0C02", + } + // weekdayNamesTeluguAbbr list the weekday name abbreviations in the Telugu. + weekdayNamesTeluguAbbr = []string{ + "\u0C06\u0C26\u0C3F", + "\u0C38\u0C4B\u0C2E", + "\u0C2E\u0C02\u0C17\u0C33", + "\u0C2C\u0C41\u0C27", + "\u0C17\u0C41\u0C30\u0C41", + "\u0C36\u0C41\u0C15\u0C4D\u0C30", + "\u0C36\u0C28\u0C3F", + } + // weekdayNamesThai list the weekday name abbreviations in the Thai. + weekdayNamesThai = []string{ + "\u0E2D\u0E32\u0E17\u0E34\u0E15\u0E22\u0E4C", + "\u0E08\u0E31\u0E19\u0E17\u0E23\u0E4C", + "\u0E2D\u0E31\u0E07\u0E04\u0E32\u0E23", + "\u0E1E\u0E38\u0E18", + "\u0E1E\u0E24\u0E2B\u0E31\u0E2A\u0E1A\u0E14\u0E35", + "\u0E28\u0E38\u0E01\u0E23\u0E4C", + "\u0E40\u0E2A\u0E32\u0E23\u0E4C", + } + // weekdayNamesThaiAbbr list the weekday name abbreviations in the Thai. + weekdayNamesThaiAbbr = []string{ + "\u0E2D\u0E32.", + "\u0E08.", + "\u0E2D.", + "\u0E1E.", + "\u0E1E\u0E24.", + "\u0E28.", + "\u0E2A.", + } + // weekdayNamesTibetan list the weekday name abbreviations in the Tibetan. + weekdayNamesTibetan = []string{ + "\u0F42\u0F5F\u0F60\u0F0B\u0F49\u0F72\u0F0B\u0F58\u0F0D", + "\u0F42\u0F5F\u0F60\u0F0B\u0F5F\u0FB3\u0F0B\u0F56\u0F0D", + "\u0F42\u0F5F\u0F60\u0F0B\u0F58\u0F72\u0F42\u0F0B\u0F51\u0F58\u0F62\u0F0D", + "\u0F42\u0F5F\u0F60\u0F0B\u0F63\u0FB7\u0F42\u0F0B\u0F54\u0F0D", + "\u0F42\u0F5F\u0F60\u0F0B\u0F55\u0F74\u0F62\u0F0B\u0F56\u0F74\u0F0D", + "\u0F42\u0F5F\u0F60\u0F0B\u0F54\u0F0B\u0F66\u0F44\u0F66\u0F0D", + "\u0F42\u0F5F\u0F60\u0F0B\u0F66\u0FA4\u0F7A\u0F53\u0F0B\u0F54\u0F0D", + } + // weekdayNamesTibetanAbbr list the weekday name abbreviations in the Tibetan. + weekdayNamesTibetanAbbr = []string{ + "\u0F49\u0F72\u0F0B\u0F58\u0F0D", + "\u0F5F\u0FB3\u0F0B\u0F56\u0F0D", + "\u0F58\u0F72\u0F42\u0F0B\u0F51\u0F58\u0F62\u0F0D", + "\u0F63\u0FB7\u0F42\u0F0B\u0F54\u0F0D", + "\u0F55\u0F74\u0F62\u0F0B\u0F56\u0F74\u0F0D", + "\u0F54\u0F0B\u0F66\u0F44\u0F66\u0F0D", + "\u0F66\u0FA4\u0F7A\u0F53\u0F0B\u0F54\u0F0D", + } + // weekdayNamesTigrinya list the weekday name abbreviations in the Tigrinya. + weekdayNamesTigrinya = []string{ + "\u1230\u1295\u1260\u1275", + "\u1230\u1291\u12ED", + "\u1220\u1209\u1235", + "\u1228\u1261\u12D5", + "\u1283\u1219\u1235", + "\u12D3\u122D\u1262", + "\u1240\u12F3\u121D", + } + // weekdayNamesTigrinyaAbbr list the weekday name abbreviations in the Tigrinya. + weekdayNamesTigrinyaAbbr = []string{ + "\u1230\u1295", + "\u1230\u1291", + "\u1230\u1209", + "\u1228\u1261", + "\u1213\u1219", + "\u12D3\u122D", + "\u1240\u12F3", + } + // weekdayNamesTsonga list the weekday name abbreviations in the Tsonga. + weekdayNamesTsonga = []string{"Sonta", "Musumbhunuku", "Ravumbirhi", "Ravunharhu", "Ravumune", "Ravuntlhanu", "Mugqivela"} + // weekdayNamesTsongaAbbr list the weekday name abbreviations in the Tsonga. + weekdayNamesTsongaAbbr = []string{"Son", "Mus", "Bir", "Har", "Ne", "Tlh", "Mug"} + // weekdayNamesTurkish list the weekday name abbreviations in the Turkish. + weekdayNamesTurkish = []string{"Pazar", "Pazartesi", "Salı", "Çarşamba", "Perşembe", "Cuma", "Cumartesi"} + // weekdayNamesTurkishAbbr list the weekday name abbreviations in the Turkish. + weekdayNamesTurkishAbbr = []string{"Paz", "Pzt", "Sal", "Çar", "Per", "Cum", "Cmt"} + // weekdayNamesTurkmen list the weekday name abbreviations in the Turkmen. + weekdayNamesTurkmen = []string{"Ýekşenbe", "Duşenbe", "Sişenbe", "Çarşenbe", "Penşenbe", "Anna", "Şenbe"} + // weekdayNamesTurkmenAbbr list the weekday name abbreviations in the Turkmen. + weekdayNamesTurkmenAbbr = []string{"Ýb", "Db", "Sb", "Çb", "Pb", "An", "Şb"} + // weekdayNamesUkrainian list the weekday name abbreviations in the Ukrainian. + weekdayNamesUkrainian = []string{ + "\u043D\u0435\u0434\u0456\u043B\u044F", + "\u043F\u043E\u043D\u0435\u0434\u0456\u043B\u043E\u043A", + "\u0432\u0456\u0432\u0442\u043E\u0440\u043E\u043A", + "\u0441\u0435\u0440\u0435\u0434\u0430", + "\u0447\u0435\u0442\u0432\u0435\u0440", + "\u043F%27\u044F\u0442\u043D\u0438\u0446\u044F", + "\u0441\u0443\u0431\u043E\u0442\u0430", + } + // weekdayNamesUkrainianAbbr list the weekday name abbreviations in the Ukrainian. + weekdayNamesUkrainianAbbr = []string{ + "\u041D\u0434", + "\u041F\u043D", + "\u0412\u0442", + "\u0421\u0440", + "\u0427\u0442", + "\u041F\u0442", + "\u0421\u0431", + } + // weekdayNamesSorbian list the weekday name abbreviations in the Sorbian. + weekdayNamesSorbian = []string{"njedźela", "póndźela", "wutora", "srjeda", "štwórtk", "pjatk", "sobota"} + // weekdayNamesSorbianAbbr list the weekday name abbreviations in the Sorbian. + weekdayNamesSorbianAbbr = []string{"nje", "pón", "wut", "srj", "štw", "pja", "sob"} + // weekdayNamesUrdu list the weekday name abbreviations in the Urdu. + weekdayNamesUrdu = []string{ + "\u0627\u062A\u0648\u0627\u0631", + "\u067E\u064A\u0631", + "\u0645\u0646\u06AF\u0644", + "\u0628\u062F\u06BE", + "\u062C\u0645\u0639\u0631\u0627\u062A", + "\u062C\u0645\u0639\u0647", + "\u0647\u0641\u062A\u0647", + } + // weekdayNamesUrduIN list the weekday name abbreviations in the Urdu India. + weekdayNamesUrduIN = []string{ + "\u0627\u062A\u0648\u0627\u0631", + "\u067E\u06CC\u0631", + "\u0645\u0646\u06AF\u0644", + "\u0628\u062F\u06BE", + "\u062C\u0645\u0639\u0631\u0627\u062A", + "\u062C\u0645\u0639\u06C1", + "\u06C1\u0641\u062A\u06C1", + } + // weekdayNamesUyghur list the weekday name abbreviations in the Uyghur. + weekdayNamesUyghur = []string{ + "\u064A\u06D5\u0643\u0634\u06D5\u0646\u0628\u06D5", + "\u062F\u06C8\u0634\u06D5\u0646\u0628\u06D5", + "\u0633\u06D5\u064A\u0634\u06D5\u0646\u0628\u06D5", + "\u0686\u0627\u0631\u0634\u06D5\u0646\u0628\u06D5", + "\u067E\u06D5\u064A\u0634\u06D5\u0646\u0628\u06D5", + "\u062C\u06C8\u0645\u06D5", + "\u0634\u06D5\u0646\u0628\u06D5", + } + // weekdayNamesUyghurAbbr list the weekday name abbreviations in the Uyghur. + weekdayNamesUyghurAbbr = []string{ + "\u064A\u06D5", + "\u062F\u06C8", + "\u0633\u06D5", + "\u0686\u0627", + "\u067E\u06D5", + "\u062C\u06C8", + "\u0634\u06D5", + } + // weekdayNamesUzbekCyrillic list the weekday name abbreviations in the Uzbek Cyrillic. + weekdayNamesUzbekCyrillic = []string{ + "\u044F\u043A\u0448\u0430\u043D\u0431\u0430", + "\u0434\u0443\u0448\u0430\u043D\u0431\u0430", + "\u0441\u0435\u0448\u0430\u043D\u0431\u0430", + "\u0447\u043E\u0440\u0448\u0430\u043D\u0431\u0430", + "\u043F\u0430\u0439\u0448\u0430\u043D\u0431\u0430", + "\u0436\u0443\u043C\u0430", + "\u0448\u0430\u043D\u0431\u0430", + } + // weekdayNamesUzbekCyrillicAbbr list the weekday name abbreviations in the Uzbek Cyrillic. + weekdayNamesUzbekCyrillicAbbr = []string{ + "\u044F\u043A\u0448", + "\u0434\u0443\u0448", + "\u0441\u0435\u0448", + "\u0447\u043E\u0440", + "\u043F\u0430\u0439", + "\u0436\u0443\u043C", + "\u0448\u0430\u043D", + } + // weekdayNamesUzbek list the weekday name abbreviations in the Uzbek. + weekdayNamesUzbek = []string{"yakshanba", "dushanba", "seshanba", "chorshanba", "payshanba", "juma", "shanba"} + // weekdayNamesUzbekAbbr list the weekday name abbreviations in the Uzbek. + weekdayNamesUzbekAbbr = []string{"Yak", "Dush", "Sesh", "Chor", "Pay", "Jum", "Shan"} + // weekdayNamesValencian list the weekday name abbreviations in the Valencian. + weekdayNamesValencian = []string{"diumenge", "dilluns", "dimarts", "dimecres", "dijous", "divendres", "dissabte"} + // weekdayNamesValencianAbbr list the weekday name abbreviations in the Valencian. + weekdayNamesValencianAbbr = []string{"dg.", "dl.", "dt.", "dc.", "dj.", "dv.", "ds."} + // weekdayNamesVenda list the weekday name abbreviations in the Venda. + weekdayNamesVenda = []string{"Swondaha", "Musumbuluwo", "Ḽavhuvhili", "Ḽavhuraru", "Ḽavhuṋa", "Ḽavhuṱanu", "Mugivhela"} + // weekdayNamesVendaAbbr list the weekday name abbreviations in the Venda. + weekdayNamesVendaAbbr = []string{"Swo", "Mus", "Vhi", "Rar", "Ṋa", "Ṱan", "Mug"} + // weekdayNamesVietnamese list the weekday name abbreviations in the Vietnamese. + weekdayNamesVietnamese = []string{"Ch\u1EE7%20Nh\u1EADt", "Th\u1EE9%20Hai", "Th\u1EE9%20Ba", "Th\u1EE9%20T\u01B0", "Th\u1EE9%20N\u0103m", "Th\u1EE9%20S%E1u", "Th\u1EE9%20B\u1EA3y"} + // weekdayNamesVietnameseAbbr list the weekday name abbreviations in the Vietnamese. + weekdayNamesVietnameseAbbr = []string{"CN", "T2", "T3", "T4", "T5", "T6", "T7"} + // weekdayNamesWelsh list the weekday name abbreviations in the Welsh. + weekdayNamesWelsh = []string{"Dydd Sul", "Dydd Llun", "Dydd Mawrth", "Dydd Mercher", "Dydd Iau", "Dydd Gwener", "Dydd Sadwrn"} + // weekdayNamesWelshAbbr list the weekday name abbreviations in the Welsh. + weekdayNamesWelshAbbr = []string{"Sul", "Llun", "Maw", "Mer", "Iau", "Gwe", "Sad"} + // weekdayNamesWolof list the weekday name abbreviations in the Wolof. + weekdayNamesWolof = []string{"Dib%E9er", "Altine", "Talaata", "%C0llarba", "Alxames", "%C0jjuma", "Gaawu"} + // weekdayNamesWolofAbbr list the weekday name abbreviations in the Wolof. + weekdayNamesWolofAbbr = []string{"Dib.", "Alt.", "Tal.", "%C0ll.", "Alx.", "%C0jj.", "Gaa."} + // weekdayNamesXhosa list the weekday name abbreviations in the Xhosa. + weekdayNamesXhosa = []string{"Cawe", "Mvulo", "Lwesibini", "Lwesithathu", "Lwesine", "Lwesihlanu", "Mgqibelo"} + // weekdayNamesXhosaAbbr list the weekday name abbreviations in the Xhosa. + weekdayNamesXhosaAbbr = []string{"iCa.", "uMv.", "uLwesib.", "uLwesith.", "uLwesin.", "uLwesihl.", "uMgq."} + // weekdayNamesYi list the weekday name abbreviations in the Yi. + weekdayNamesYi = []string{ + "\uA46D\uA18F\uA44D", + "\uA18F\uA282\uA494", + "\uA18F\uA282\uA44D", + "\uA18F\uA282\uA315", + "\uA18F\uA282\uA1D6", + "\uA18F\uA282\uA26C", + "\uA18F\uA282\uA0D8", + } + // weekdayNamesYiAbbr list the weekday name abbreviations in the Yi. + weekdayNamesYiAbbr = []string{ + "\uA46D\uA18F", + "\uA18F\uA494", + "\uA18F\uA44D", + "\uA18F\uA315", + "\uA18F\uA1D6", + "\uA18F\uA26C", + "\uA18F\uA0D8", + } + // weekdayNamesYiddish list the weekday name abbreviations in the Yiddish. + weekdayNamesYiddish = []string{ + "\u05D6\u05D5\u05E0\u05D8\u05D9\u05E7", + "\u05DE\u05D0\u05B8\u05E0\u05D8\u05D9\u05E7", + "\u05D3\u05D9\u05E0\u05E1\u05D8\u05D9\u05E7", + "\u05DE\u05D9\u05D8\u05D5\u05D5\u05D0\u05DA", + "\u05D3\u05D0\u05E0\u05E2\u05E8\u05E9\u05D8\u05D9\u05E7", + "\u05E4\u05BF\u05E8\u05F2\u05B7\u05D8\u05D9\u05E7", + "\u05E9\u05D1\u05EA", + } + // weekdayNamesYiddishAbbr list the weekday name abbreviations in the Yiddish. + weekdayNamesYiddishAbbr = []string{ + "\u05D9\u05D5\u05DD%A0\u05D0", + "\u05D9\u05D5\u05DD%A0\u05D1", + "\u05D9\u05D5\u05DD%A0\u05D2", + "\u05D9\u05D5\u05DD%A0\u05D3", + "\u05D9\u05D5\u05DD%A0\u05D4", + "\u05D9\u05D5\u05DD%A0\u05D5", + "\u05E9\u05D1\u05EA", + } + // weekdayNamesYoruba list the weekday name abbreviations in the Yoruba. + weekdayNamesYoruba = []string{ + "\u1ECCj\u1ECD\u0301%20%C0%ECk%FA", + "\u1ECCj\u1ECD\u0301%20Aj%E9", + "\u1ECCj\u1ECD\u0301%20%CCs\u1EB9\u0301gun", + "\u1ECCj\u1ECD\u0301r%FA", + "\u1ECCj\u1ECD\u0301b\u1ECD", + "\u1ECCj\u1ECD\u0301%20\u1EB8t%EC", + "\u1ECCj\u1ECD\u0301%20%C0b%E1m\u1EB9\u0301ta", + } + // weekdayNamesYorubaAbbr list the weekday name abbreviations in the Yoruba. + weekdayNamesYorubaAbbr = []string{"%C0%ECk", "Aj", "%CC\u1E63g", "\u1ECCjr", "\u1ECCjb", "\u1EB8t", "%C0b%E1"} + // weekdayNamesZulu list the weekday name abbreviations in the Zulu. + weekdayNamesZulu = []string{"ISonto", "UMsombuluko", "ULwesibili", "ULwesithathu", "ULwesine", "ULwesihlanu", "UMgqibelo"} + // weekdayNamesZuluAbbr list the weekday name abbreviations in the Zulu. + weekdayNamesZuluAbbr = []string{"Son.", "Mso.", "Bi.", "Tha.", "Ne.", "Hla.", "Mgq."} // apFmtAfrikaans defined the AM/PM name in the Afrikaans. apFmtAfrikaans = "vm./nm." + // apFmtAlbanian defined the AM/PM name in the Albanian. + apFmtAlbanian = "p.d./m.d." + // apFmtAlsatian defined the AM/PM name in the Alsatian. + apFmtAlsatian = "vorm./nam." + // apFmtAmharic defined the AM/PM name in the Amharic. + apFmtAmharic = "\u1325\u12CB\u1275/\u12A8\u1230\u12D3\u1275" + // apFmtArabic defined the AM/PM name in the Arabic. + apFmtArabic = "\u0635/\u0645" + // apFmtAssamese defined the AM/PM name in the Assamese. + apFmtAssamese = "\u09F0\u09BE\u09A4\u09BF\u09AA\u09C1/\u0986\u09AC\u09C7\u09B2\u09BF" + // apFmtBreton defined the AM/PM name in the Assamese. + apFmtBreton = "A.M./G.M." + // apFmtBurmese defined the AM/PM name in the Assamese. + apFmtBurmese = "\u1014\u1036\u1014\u1000\u103A/\u100A\u1014\u1031" // apFmtCameroon defined the AM/PM name in the Cameroon. apFmtCameroon = "mat./soir" + // apFmtCentralKurdish defined the AM/PM name in the Central Kurdish. + apFmtCentralKurdish = "\u067E.\u0646/\u062F.\u0646" // apFmtCuba defined the AM/PM name in the Cuba. apFmtCuba = "a.m./p.m." // apFmtFaroese defined the AM/PM name in the Faroese. @@ -2214,10 +4561,32 @@ var ( apFmtSomali = "GH/GD" // apFmtSpanish defined the AM/PM name in the Spanish. apFmtSpanish = "a. m./p. m." + // apFmtSpanishAR defined the AM/PM name in the Spanish Argentina. + apFmtSpanishAR = "a.%A0m./p.%A0m." + // apFmtSwedish defined the AM/PM name in the Swedish. + apFmtSwedish = "fm/em" + // apFmtSyriac defined the AM/PM name in the Syriac. + apFmtSyriac = "\u0729.\u071B/\u0712.\u071B" + // apFmtTamil defined the AM/PM name in the Tamil. + apFmtTamil = "\u0B95\u0BBE\u0BB2\u0BC8/\u0BAE\u0BBE\u0BB2\u0BC8" // apFmtTibetan defined the AM/PM name in the Tibetan. apFmtTibetan = "\u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b/\u0f55\u0fb1\u0f72\u0f0b\u0f51\u0fb2\u0f7c\u0f0b" + // apFmtTigrinya defined the AM/PM name in the Tigrinya. + apFmtTigrinya = "\u1295\u1309\u1206/\u12F5\u1215\u122A%20\u1250\u1275\u122A" + // apFmtTigrinyaER defined the AM/PM name in the Tigrinya Eritrea. + apFmtTigrinyaER = "\u1295\u1309\u1206%20\u1230\u12D3\u1270/\u12F5\u1215\u122D%20\u1230\u12D3\u1275" // apFmtTurkish defined the AM/PM name in the Turkish. apFmtTurkish = "\u00F6\u00F6/\u00F6\u0053" + // apFmtUpperSorbian defined the AM/PM name in the Upper Sorbian. + apFmtUpperSorbian = "dopołdnja/popołdnju" + // apFmtUrdu defined the AM/PM name in the Urdu. + apFmtUrdu = "\u062F\u0646/\u0631\u0627\u062A" + // apFmtUyghur defined the AM/PM name in the Uyghur. + apFmtUyghur = "\u0686\u06C8\u0634\u062A\u0649\u0646%20\u0628\u06C7\u0631\u06C7\u0646/\u0686\u06C8\u0634\u062A\u0649\u0646%20\u0643\u06D0\u064A\u0649\u0646" + // apFmtUzbek defined the AM/PM name in the Uzbek. + apFmtUzbek = "TO/TK" + // apFmtUzbekCyrillic defined the AM/PM name in the Uzbek Cyrillic. + apFmtUzbekCyrillic = "\u0422\u041E/\u0422\u041A" // apFmtVietnamese defined the AM/PM name in the Vietnamese. apFmtVietnamese = "SA/CH" // apFmtWelsh defined the AM/PM name in the Welsh. @@ -2226,7 +4595,11 @@ var ( apFmtWolof = "Sub/Ngo" // apFmtYi defined the AM/PM name in the Yi. apFmtYi = "\ua3b8\ua111/\ua06f\ua2d2" - // switchArgumentFunc defined the switch argument printer function + // apFmtYiddish defined the AM/PM name in the Yiddish. + apFmtYiddish = "\u05E4\u05BF\u05D0\u05B7\u05E8\u05DE\u05D9\u05D8\u05D0\u05B8\u05D2/\u05E0\u05D0\u05B8\u05DB\u05DE\u05D9\u05D8\u05D0\u05B8\u05D2" + // apFmtYoruba defined the AM/PM name in the Yoruba. + apFmtYoruba = "%C0%E1r\u1ECD\u0300/\u1ECC\u0300s%E1n" + // switchArgumentFunc defined the switch argument printer function. switchArgumentFunc = map[string]func(s string) string{ "[DBNum1]": func(s string) string { r := strings.NewReplacer( @@ -2687,6 +5060,110 @@ func localMonthsNameAfrikaans(t time.Time, abbr int) string { return monthNamesAfrikaansAbbr[int(t.Month())-1][:1] } +// localMonthsNameAlbanian returns the Albanian name of the month. +func localMonthsNameAlbanian(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesAlbanianAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesAlbanian[int(t.Month())-1] + } + return monthNamesAlbanianAbbr[int(t.Month())-1][:1] +} + +// localMonthsNameAlsatian returns the Alsatian name of the month. +func localMonthsNameAlsatian(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesAlsatianAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesAlsatian[int(t.Month())-1] + } + return monthNamesAlsatianAbbr[int(t.Month())-1][:1] +} + +// localMonthsNameAlsatianFrance returns the Alsatian France name of the month. +func localMonthsNameAlsatianFrance(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesAlsatianFranceAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesAlsatianFrance[int(t.Month())-1] + } + return monthNamesAlsatianFranceAbbr[int(t.Month())-1][:1] +} + +// localMonthsNameAmharic returns the Amharic name of the month. +func localMonthsNameAmharic(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesAmharicAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesAmharic[int(t.Month())-1] + } + return string([]rune(monthNamesAmharic[int(t.Month())-1])[:1]) +} + +// localMonthsNameArabic returns the Arabic name of the month. +func localMonthsNameArabic(t time.Time, abbr int) string { + if abbr == 5 { + return string([]rune(monthNamesArabic[int(t.Month())-1])[:1]) + } + return monthNamesArabic[int(t.Month())-1] +} + +// localMonthsNameArabicIraq returns the Arabic Iraq name of the month. +func localMonthsNameArabicIraq(t time.Time, abbr int) string { + if abbr == 5 { + return string([]rune(monthNamesArabicIraq[int(t.Month())-1])[:1]) + } + return monthNamesArabicIraq[int(t.Month())-1] +} + +// localMonthsNameArmenian returns the Armenian name of the month. +func localMonthsNameArmenian(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesArmenianAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesArmenian[int(t.Month())-1] + } + return string([]rune(monthNamesArmenianAbbr[int(t.Month())-1])[:1]) +} + +// localMonthsNameAssamese returns the Assamese name of the month. +func localMonthsNameAssamese(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesAssameseAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesAssamese[int(t.Month())-1] + } + return string([]rune(monthNamesAssameseAbbr[int(t.Month())-1])[:1]) +} + +// localMonthsNameAzerbaijaniCyrillic returns the Azerbaijani (Cyrillic) name of the month. +func localMonthsNameAzerbaijaniCyrillic(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesAzerbaijaniCyrillicAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesAzerbaijaniCyrillic[int(t.Month())-1] + } + return string([]rune(monthNamesAzerbaijaniCyrillic[int(t.Month())-1])[:1]) +} + +// localMonthsNameAzerbaijani returns the Azerbaijani name of the month. +func localMonthsNameAzerbaijani(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesAzerbaijaniAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesAzerbaijani[int(t.Month())-1] + } + return string([]rune(monthNamesAzerbaijani[int(t.Month())-1])[:1]) +} + // localMonthsNameAustria returns the Austria name of the month. func localMonthsNameAustria(t time.Time, abbr int) string { if abbr == 3 { @@ -2706,6 +5183,94 @@ func localMonthsNameBangla(t time.Time, abbr int) string { return string([]rune(monthNamesBangla[int(t.Month())-1])[:1]) } +// localMonthsNameBashkir returns the Bashkir name of the month. +func localMonthsNameBashkir(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesBashkirAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesBashkir[int(t.Month())-1] + } + return string([]rune(monthNamesBashkir[int(t.Month())-1])[:1]) +} + +// localMonthsNameBasque returns the Basque name of the month. +func localMonthsNameBasque(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesBasqueAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesBasque[int(t.Month())-1] + } + return string([]rune(monthNamesBasque[int(t.Month())-1])[:1]) +} + +// localMonthsNameBelarusian returns the Belarusian name of the month. +func localMonthsNameBelarusian(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesBelarusianAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesBelarusian[int(t.Month())-1] + } + return string([]rune(monthNamesBelarusian[int(t.Month())-1])[:1]) +} + +// localMonthsNameBosnianCyrillic returns the Bosnian (Cyrillic) name of the month. +func localMonthsNameBosnianCyrillic(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesBosnianCyrillicAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesBosnianCyrillic[int(t.Month())-1] + } + return string([]rune(monthNamesBosnianCyrillic[int(t.Month())-1])[:1]) +} + +// localMonthsNameBosnian returns the Bosnian name of the month. +func localMonthsNameBosnian(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesBosnianAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesBosnian[int(t.Month())-1] + } + return string([]rune(monthNamesBosnian[int(t.Month())-1])[:1]) +} + +// localMonthsNameBreton returns the Breton name of the month. +func localMonthsNameBreton(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesBretonAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesBreton[int(t.Month())-1] + } + return string([]rune(monthNamesBreton[int(t.Month())-1])[:1]) +} + +// localMonthsNameBulgarian returns the Bulgarian name of the month. +func localMonthsNameBulgarian(t time.Time, abbr int) string { + if abbr == 3 { + return string([]rune(monthNamesBulgarian[int(t.Month())-1])[:3]) + } + if abbr == 4 { + return monthNamesBulgarian[int(t.Month())-1] + } + return string([]rune(monthNamesBulgarian[int(t.Month())-1])[:1]) +} + +// localMonthsNameBurmese returns the Burmese name of the month. +func localMonthsNameBurmese(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesBurmeseAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesBurmese[int(t.Month())-1] + } + return string([]rune(monthNamesBurmese[int(t.Month())-1])[:1]) +} + // localMonthsNameCaribbean returns the Caribbean name of the month. func localMonthsNameCaribbean(t time.Time, abbr int) string { if abbr == 3 { @@ -2717,6 +5282,25 @@ func localMonthsNameCaribbean(t time.Time, abbr int) string { return monthNamesCaribbeanAbbr[int(t.Month())-1][:1] } +// localMonthsNameCentralKurdish returns the Central Kurdish name of the month. +func localMonthsNameCentralKurdish(t time.Time, abbr int) string { + if abbr == 5 { + return string([]rune(monthNamesCentralKurdish[int(t.Month())-1])[:1]) + } + return monthNamesCentralKurdish[int(t.Month())-1] +} + +// localMonthsNameCherokee returns the Cherokee name of the month. +func localMonthsNameCherokee(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesCherokeeAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesCherokee[int(t.Month())-1] + } + return string([]rune(monthNamesCherokee[int(t.Month())-1])[:1]) +} + // localMonthsNameChinese1 returns the Chinese name of the month. func localMonthsNameChinese1(t time.Time, abbr int) string { if abbr == 3 { @@ -3719,6 +6303,17 @@ func localMonthsNameSomali(t time.Time, abbr int) string { return string([]rune(monthNamesSomali[int(t.Month()-1)])[:1]) } +// localMonthsNameSotho returns the Sotho name of the month. +func localMonthsNameSotho(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesSothoAbbr[int(t.Month()-1)] + } + if abbr == 4 || abbr > 6 { + return monthNamesSotho[int(t.Month())-1] + } + return string([]rune(monthNamesSotho[int(t.Month()-1)])[:1]) +} + // localMonthsNameSpanish returns the Spanish name of the month. func localMonthsNameSpanish(t time.Time, abbr int) string { if abbr == 3 { @@ -3730,6 +6325,113 @@ func localMonthsNameSpanish(t time.Time, abbr int) string { return monthNamesSpanishAbbr[int(t.Month())-1][:1] } +// localMonthsNameSpanishPE returns the Spanish Peru name of the month. +func localMonthsNameSpanishPE(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesSpanishPEAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesSpanishPE[int(t.Month())-1] + } + return monthNamesSpanishPEAbbr[int(t.Month())-1][:1] +} + +// localMonthsNameSwedish returns the Swedish name of the month. +func localMonthsNameSwedish(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesSwedishAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesSwedish[int(t.Month())-1] + } + return monthNamesSwedishAbbr[int(t.Month())-1][:1] +} + +// localMonthsNameSwedishFI returns the Swedish Finland name of the month. +func localMonthsNameSwedishFI(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesSwedishFIAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesSwedish[int(t.Month())-1] + } + return monthNamesSwedishFIAbbr[int(t.Month())-1][:1] +} + +// localMonthsNameSyriac returns the Syriac name of the month. +func localMonthsNameSyriac(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesSyriacAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesSyriac[int(t.Month())-1] + } + return string([]rune(monthNamesSyriac[int(t.Month()-1)])[:1]) +} + +// localMonthsNameTajik returns the Tajik name of the month. +func localMonthsNameTajik(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesTajikAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesTajik[int(t.Month())-1] + } + return string([]rune(monthNamesTajik[int(t.Month()-1)])[:1]) +} + +// localMonthsNameTamazight returns the Tamazight name of the month. +func localMonthsNameTamazight(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesTamazightAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesTamazight[int(t.Month())-1] + } + return string([]rune(monthNamesTamazight[int(t.Month()-1)])[:1]) +} + +// localMonthsNameTamil returns the Tamil name of the month. +func localMonthsNameTamil(t time.Time, abbr int) string { + if abbr == 5 { + return string([]rune(monthNamesTamil[int(t.Month()-1)])[:1]) + } + return monthNamesTamil[int(t.Month())-1] +} + +// localMonthsNameTamilLK returns the Tamil Sri Lanka name of the month. +func localMonthsNameTamilLK(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesTamilAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesTamil[int(t.Month())-1] + } + return string([]rune(monthNamesTamil[int(t.Month()-1)])[:1]) +} + +// localMonthsNameTatar returns the Tatar name of the month. +func localMonthsNameTatar(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesTatarAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesTatar[int(t.Month())-1] + } + return string([]rune(monthNamesTatar[int(t.Month()-1)])[:1]) +} + +// localMonthsNameTelugu returns the Telugu name of the month. +func localMonthsNameTelugu(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesTeluguAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesTelugu[int(t.Month())-1] + } + return string([]rune(monthNamesTelugu[int(t.Month()-1)])[:1]) +} + // localMonthsNameSyllabics returns the Syllabics name of the month. func localMonthsNameSyllabics(t time.Time, abbr int) string { if abbr == 3 { @@ -3767,6 +6469,28 @@ func localMonthsNameTibetan(t time.Time, abbr int) string { return monthNamesTibetan[int(t.Month())-1] } +// localMonthsNameTigrinya returns the Tigrinya name of the month. +func localMonthsNameTigrinya(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesTigrinyaAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesTigrinya[int(t.Month())-1] + } + return string([]rune(monthNamesTigrinya[int(t.Month()-1)])[:1]) +} + +// localMonthsNameTsonga returns the Tsonga name of the month. +func localMonthsNameTsonga(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesTsongaAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesTsonga[int(t.Month())-1] + } + return string([]rune(monthNamesTsonga[int(t.Month()-1)])[:1]) +} + // localMonthsNameTraditionalMongolian returns the Traditional Mongolian name of // the month. func localMonthsNameTraditionalMongolian(t time.Time, abbr int) string { @@ -3787,6 +6511,94 @@ func localMonthsNameTurkish(t time.Time, abbr int) string { return string([]rune(monthNamesTurkishAbbr[int(t.Month())-1])[:1]) } +// localMonthsNameTurkmen returns the Turkmen name of the month. +func localMonthsNameTurkmen(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesTurkmenAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesTurkmen[int(t.Month())-1] + } + return string([]rune(monthNamesTurkmenAbbr[int(t.Month())-1])[:1]) +} + +// localMonthsNameUkrainian returns the Ukrainian name of the month. +func localMonthsNameUkrainian(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesUkrainianAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesUkrainian[int(t.Month())-1] + } + return string([]rune(monthNamesUkrainian[int(t.Month())-1])[:1]) +} + +// localMonthsNameUpperSorbian returns the Upper Sorbian name of the month. +func localMonthsNameUpperSorbian(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesUpperSorbianAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesUpperSorbian[int(t.Month())-1] + } + return string([]rune(monthNamesUpperSorbian[int(t.Month())-1])[:1]) +} + +// localMonthsNameUyghur returns the Uyghur name of the month. +func localMonthsNameUyghur(t time.Time, abbr int) string { + if abbr == 3 { + return fmt.Sprintf("%d-\u0626\u0627\u064A", int(t.Month())) + } + if abbr == 4 { + return monthNamesUyghur[int(t.Month())-1] + } + return string([]rune(monthNamesUyghur[int(t.Month())-1])[:1]) +} + +// localMonthsNameUzbek returns the Uzbek name of the month. +func localMonthsNameUzbek(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesUzbekAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesUzbek[int(t.Month())-1] + } + return string([]rune(monthNamesUzbek[int(t.Month())-1])[:1]) +} + +// localMonthsNameUzbekCyrillic returns the Uzbek (Cyrillic) name of the month. +func localMonthsNameUzbekCyrillic(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesTajikAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesTajik[int(t.Month())-1] + } + return string([]rune(monthNamesTajik[int(t.Month())-1])[:1]) +} + +// localMonthsNameValencian returns the Valencian name of the month. +func localMonthsNameValencian(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesValencianAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesValencian[int(t.Month())-1] + } + return string([]rune(monthNamesValencian[int(t.Month())-1])[:1]) +} + +// localMonthsNameVenda returns the Venda name of the month. +func localMonthsNameVenda(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesVendaAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesVenda[int(t.Month())-1] + } + return string([]rune(monthNamesVenda[int(t.Month())-1])[:1]) +} + // localMonthsNameVietnamese returns the Vietnamese name of the month. func localMonthsNameVietnamese(t time.Time, abbr int) string { if abbr == 3 { @@ -3839,6 +6651,28 @@ func localMonthsNameYi(t time.Time, abbr int) string { return string([]rune(monthNamesYi[int(t.Month())-1])[:1]) } +// localMonthsNameYiddish returns the Yiddish name of the month. +func localMonthsNameYiddish(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesYiddishAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesYiddish[int(t.Month())-1] + } + return string([]rune(monthNamesYiddish[int(t.Month())-1])[:1]) +} + +// localMonthsNameYoruba returns the Yoruba name of the month. +func localMonthsNameYoruba(t time.Time, abbr int) string { + if abbr == 3 { + return monthNamesYorubaAbbr[int(t.Month())-1] + } + if abbr == 4 { + return monthNamesYoruba[int(t.Month())-1] + } + return string([]rune(monthNamesYoruba[int(t.Month())-1])[:1]) +} + // localMonthsNameZulu returns the Zulu name of the month. func localMonthsNameZulu(t time.Time, abbr int) string { if abbr == 3 { @@ -3966,35 +6800,33 @@ func (nf *numberFormat) yearsHandler(token nfp.Token) { // daysHandler will be handling days in the date and times types tokens for a // number format expression. func (nf *numberFormat) daysHandler(token nfp.Token) { + info, l := supportedLanguageInfo[nf.localCode], len(token.TValue) + weekdayNames, weekdayNamesAbbr := info.weekdayNames, info.weekdayNamesAbbr + if len(weekdayNames) != 7 { + weekdayNames = weekdayNamesEnglish + } + if len(weekdayNamesAbbr) != 7 { + weekdayNamesAbbr = weekdayNamesEnglishAbbr + } if strings.Contains(strings.ToUpper(token.TValue), "A") { - l := len(token.TValue) - if nf.localCode == "804" || nf.localCode == "404" { - var prefix string - if l == 3 { - prefix = map[string]string{"404": "週", "804": "周"}[nf.localCode] - } - if l > 3 { - prefix = "星期" - } - nf.result += prefix + weekdayNamesChinese[int(nf.t.Weekday())] - return + if l == 3 { + nf.result += weekdayNamesAbbr[int(nf.t.Weekday())] + } + if l > 3 { + nf.result += weekdayNames[int(nf.t.Weekday())] } - nf.result += nf.t.Weekday().String() return } if strings.Contains(strings.ToUpper(token.TValue), "D") { - switch len(token.TValue) { + switch l { case 1: nf.result += strconv.Itoa(nf.t.Day()) - return case 2: nf.result += fmt.Sprintf("%02d", nf.t.Day()) - return case 3: - nf.result += nf.t.Weekday().String()[:3] - return + nf.result += weekdayNamesAbbr[int(nf.t.Weekday())] default: - nf.result += nf.t.Weekday().String() + nf.result += weekdayNames[int(nf.t.Weekday())] } } } diff --git a/numfmt_test.go b/numfmt_test.go index 746bd3d0e6..106d82c61b 100644 --- a/numfmt_test.go +++ b/numfmt_test.go @@ -76,215 +76,690 @@ func TestNumFmt(t *testing.T) { {"43466.189571759256", "[$-404]aaaa;@", "星期二"}, {"43466.189571759256", "[$-804]aaa;@", "周二"}, {"43466.189571759256", "[$-804]aaaa;@", "星期二"}, - {"43466.189571759256", "[$-36]aaa;@", "Tuesday"}, - {"43466.189571759256", "[$-36]aaaa;@", "Tuesday"}, - {"44562.189571759256", "[$-36]mmm dd yyyy h:mm AM/PM", "Jan. 01 2022 4:32 vm."}, - {"44562.189571759256", "[$-36]mmmm dd yyyy h:mm AM/PM", "Januarie 01 2022 4:32 vm."}, - {"44562.189571759256", "[$-36]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 vm."}, - {"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 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", "[$-2809]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-2809]mmmm dd yyyy h:mm AM/PM", "March 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-2809]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"}, - {"43543.503206018519", "[$-25]mmm dd yyyy h:mm AM/PM", "märts 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-25]mmmm dd yyyy h:mm AM/PM", "märts 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-25]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-425]mmm dd yyyy h:mm AM/PM", "märts 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-425]mmmm dd yyyy h:mm AM/PM", "märts 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-425]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-38]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 um sein."}, - {"43543.503206018519", "[$-38]mmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 um sein."}, - {"43543.503206018519", "[$-38]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 um sein."}, - {"43543.503206018519", "[$-438]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 um sein."}, - {"43543.503206018519", "[$-438]mmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 um sein."}, - {"43543.503206018519", "[$-438]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 um sein."}, - {"43543.503206018519", "[$-64]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-64]mmmm dd yyyy h:mm AM/PM", "Marso 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-64]mmmmm dd yyyy h:mm AM/PM", "03 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-464]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-464]mmmm dd yyyy h:mm AM/PM", "Marso 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-464]mmmmm dd yyyy h:mm AM/PM", "03 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-B]mmm dd yyyy h:mm AM/PM", "maalis 19 2019 12:04 ip."}, - {"43543.503206018519", "[$-B]mmmm dd yyyy h:mm AM/PM", "maaliskuu 19 2019 12:04 ip."}, - {"43543.503206018519", "[$-B]mmmmm dd yyyy h:mm AM/PM", "03 19 2019 12:04 ip."}, - {"43543.503206018519", "[$-40B]mmm dd yyyy h:mm AM/PM", "maalis 19 2019 12:04 ip."}, - {"43543.503206018519", "[$-40B]mmmm dd yyyy h:mm AM/PM", "maaliskuu 19 2019 12:04 ip."}, - {"43543.503206018519", "[$-40B]mmmmm dd yyyy h:mm AM/PM", "03 19 2019 12:04 ip."}, + {"43466.189571759256", "[$-435]aaa;@", "Bi."}, + {"43466.189571759256", "[$-435]aaaa;@", "ULwesibili"}, + {"43466.189571759256", "[$-404]ddd;@", "週二"}, + {"43466.189571759256", "[$-404]dddd;@", "星期二"}, + {"43466.189571759256", "[$-804]ddd;@", "周二"}, + {"43466.189571759256", "[$-804]dddd;@", "星期二"}, + {"43466.189571759256", "[$-435]ddd;@", "Bi."}, + {"43466.189571759256", "[$-435]dddd;@", "ULwesibili"}, + {"44562.189571759256", "[$-36]mmm dd yyyy h:mm AM/PM d", "Jan. 01 2022 4:32 vm. 1"}, + {"44562.189571759256", "[$-36]mmmm dd yyyy h:mm AM/PM dd", "Januarie 01 2022 4:32 vm. 01"}, + {"44562.189571759256", "[$-36]mmmmm dd yyyy h:mm AM/PM ddd", "J 01 2022 4:32 vm. Sa."}, + {"44682.18957170139", "[$-36]mmm dd yyyy h:mm AM/PM dddd", "Mei 01 2022 4:32 vm. Sondag"}, + {"44682.18957170139", "[$-36]mmmm dd yyyy h:mm AM/PM aaa", "Mei 01 2022 4:32 vm. So."}, + {"44682.18957170139", "[$-36]mmmmm dd yyyy h:mm AM/PM aaaa", "M 01 2022 4:32 vm. Sondag"}, + {"44562.189571759256", "[$-436]mmm dd yyyy h:mm AM/PM d", "Jan. 01 2022 4:32 vm. 1"}, + {"44562.189571759256", "[$-436]mmmm dd yyyy h:mm AM/PM dd", "Januarie 01 2022 4:32 vm. 01"}, + {"44562.189571759256", "[$-436]mmmmm dd yyyy h:mm AM/PM ddd", "J 01 2022 4:32 vm. Sa."}, + {"44682.18957170139", "[$-436]mmm dd yyyy h:mm AM/PM dddd", "Mei 01 2022 4:32 vm. Sondag"}, + {"44682.18957170139", "[$-436]mmmm dd yyyy h:mm AM/PM aaa", "Mei 01 2022 4:32 vm. So."}, + {"44682.18957170139", "[$-436]mmmmm dd yyyy h:mm AM/PM aaaa", "M 01 2022 4:32 vm. Sondag"}, + {"44562.189571759256", "[$-1C]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 p.d."}, + {"44562.189571759256", "[$-1C]mmmm dd yyyy h:mm AM/PM", "janar 01 2022 4:32 p.d."}, + {"44562.189571759256", "[$-1C]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 p.d."}, + {"44562.189571759256", "[$-1C]mmmmmm dd yyyy h:mm AM/PM", "janar 01 2022 4:32 p.d."}, + {"43543.503206018519", "[$-1C]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 m.d."}, + {"43543.503206018519", "[$-1C]mmmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 m.d. mar"}, + {"43543.503206018519", "[$-1C]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 m.d. mar"}, + {"43543.503206018519", "[$-1C]mmmmmm dd yyyy h:mm AM/PM dddd", "mars 19 2019 12:04 m.d. e martë"}, + {"44562.189571759256", "[$-41C]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 p.d."}, + {"44562.189571759256", "[$-41C]mmmm dd yyyy h:mm AM/PM", "janar 01 2022 4:32 p.d."}, + {"44562.189571759256", "[$-41C]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 p.d."}, + {"44562.189571759256", "[$-41C]mmmmmm dd yyyy h:mm AM/PM", "janar 01 2022 4:32 p.d."}, + {"43543.503206018519", "[$-41C]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 m.d."}, + {"43543.503206018519", "[$-41C]mmmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 m.d. mar"}, + {"43543.503206018519", "[$-41C]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 m.d. mar"}, + {"43543.503206018519", "[$-41C]mmmmmm dd yyyy h:mm AM/PM dddd", "mars 19 2019 12:04 m.d. e martë"}, + {"44562.189571759256", "[$-84]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 vorm."}, + {"44562.189571759256", "[$-84]mmmm dd yyyy h:mm AM/PM", "Januar 01 2022 4:32 vorm."}, + {"44562.189571759256", "[$-84]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 vorm."}, + {"44562.189571759256", "[$-84]mmmmmm dd yyyy h:mm AM/PM", "Januar 01 2022 4:32 vorm."}, + {"43543.503206018519", "[$-84]mmm dd yyyy h:mm AM/PM", "Mär 19 2019 12:04 nam."}, + {"43543.503206018519", "[$-84]mmmm dd yyyy h:mm AM/PM aaa", "März 19 2019 12:04 nam. Zi."}, + {"43543.503206018519", "[$-84]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 nam. Zi."}, + {"43543.503206018519", "[$-84]mmmmmm dd yyyy h:mm AM/PM dddd", "März 19 2019 12:04 nam. Ziischtig"}, + {"44562.189571759256", "[$-484]mmm dd yyyy h:mm AM/PM", "Jän. 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-484]mmmm dd yyyy h:mm AM/PM", "Jänner 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-484]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-484]mmmmmm dd yyyy h:mm AM/PM", "Jänner 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-484]mmm dd yyyy h:mm AM/PM", "März 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-484]mmmm dd yyyy h:mm AM/PM aaa", "März 19 2019 12:04 PM Zi."}, + {"43543.503206018519", "[$-484]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM Zi."}, + {"43543.503206018519", "[$-484]mmmmmm dd yyyy h:mm AM/PM dddd", "März 19 2019 12:04 PM Zischti"}, + {"44562.189571759256", "[$-5E]mmm dd yyyy h:mm AM/PM", "\u1303\u1295\u12E9 01 2022 4:32 \u1325\u12CB\u1275"}, + {"44562.189571759256", "[$-5E]mmmm dd yyyy h:mm AM/PM", "\u1303\u1295\u12E9\u12C8\u122A 01 2022 4:32 \u1325\u12CB\u1275"}, + {"44562.189571759256", "[$-5E]mmmmm dd yyyy h:mm AM/PM", "\u1303 01 2022 4:32 \u1325\u12CB\u1275"}, + {"44562.189571759256", "[$-5E]mmmmmm dd yyyy h:mm AM/PM", "\u1303\u1295\u12E9\u12C8\u122A 01 2022 4:32 \u1325\u12CB\u1275"}, + {"43543.503206018519", "[$-5E]mmm dd yyyy h:mm AM/PM", "\u121B\u122D\u127D 19 2019 12:04 \u12A8\u1230\u12D3\u1275"}, + {"43543.503206018519", "[$-5E]mmmm dd yyyy h:mm AM/PM aaa", "\u121B\u122D\u127D 19 2019 12:04 \u12A8\u1230\u12D3\u1275 \u121B\u12AD\u1230"}, + {"43543.503206018519", "[$-5E]mmmmm dd yyyy h:mm AM/PM ddd", "\u121B 19 2019 12:04 \u12A8\u1230\u12D3\u1275 \u121B\u12AD\u1230"}, + {"43543.503206018519", "[$-5E]mmmmmm dd yyyy h:mm AM/PM dddd", "\u121B\u122D\u127D 19 2019 12:04 \u12A8\u1230\u12D3\u1275 \u121B\u12AD\u1230\u129E"}, + {"44562.189571759256", "[$-45E]mmm dd yyyy h:mm AM/PM", "\u1303\u1295\u12E9 01 2022 4:32 \u1325\u12CB\u1275"}, + {"44562.189571759256", "[$-45E]mmmm dd yyyy h:mm AM/PM", "\u1303\u1295\u12E9\u12C8\u122A 01 2022 4:32 \u1325\u12CB\u1275"}, + {"44562.189571759256", "[$-45E]mmmmm dd yyyy h:mm AM/PM", "\u1303 01 2022 4:32 \u1325\u12CB\u1275"}, + {"44562.189571759256", "[$-45E]mmmmmm dd yyyy h:mm AM/PM", "\u1303\u1295\u12E9\u12C8\u122A 01 2022 4:32 \u1325\u12CB\u1275"}, + {"43543.503206018519", "[$-45E]mmm dd yyyy h:mm AM/PM", "\u121B\u122D\u127D 19 2019 12:04 \u12A8\u1230\u12D3\u1275"}, + {"43543.503206018519", "[$-45E]mmmm dd yyyy h:mm AM/PM aaa", "\u121B\u122D\u127D 19 2019 12:04 \u12A8\u1230\u12D3\u1275 \u121B\u12AD\u1230"}, + {"43543.503206018519", "[$-45E]mmmmm dd yyyy h:mm AM/PM ddd", "\u121B 19 2019 12:04 \u12A8\u1230\u12D3\u1275 \u121B\u12AD\u1230"}, + {"43543.503206018519", "[$-45E]mmmmmm dd yyyy h:mm AM/PM dddd", "\u121B\u122D\u127D 19 2019 12:04 \u12A8\u1230\u12D3\u1275 \u121B\u12AD\u1230\u129E"}, + {"44562.189571759256", "[$-1]mmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-1]mmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-1]mmmmm dd yyyy h:mm AM/PM", "\u064A 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-1]mmmmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"}, + {"43543.503206018519", "[$-1]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645"}, + {"43543.503206018519", "[$-1]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"43543.503206018519", "[$-1]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"43543.503206018519", "[$-1]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"44562.189571759256", "[$-1401]mmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-1401]mmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-1401]mmmmm dd yyyy h:mm AM/PM", "\u064A 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-1401]mmmmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"}, + {"43543.503206018519", "[$-1401]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645"}, + {"43543.503206018519", "[$-1401]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"43543.503206018519", "[$-1401]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"43543.503206018519", "[$-1401]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"44562.189571759256", "[$-3C01]mmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-3C01]mmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-3C01]mmmmm dd yyyy h:mm AM/PM", "\u064A 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-3C01]mmmmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"}, + {"43543.503206018519", "[$-3C01]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645"}, + {"43543.503206018519", "[$-3C01]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"43543.503206018519", "[$-3C01]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"43543.503206018519", "[$-3C01]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"44562.189571759256", "[$-c01]mmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-c01]mmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-c01]mmmmm dd yyyy h:mm AM/PM", "\u064A 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-c01]mmmmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"}, + {"43543.503206018519", "[$-c01]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645"}, + {"43543.503206018519", "[$-c01]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"43543.503206018519", "[$-c01]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"43543.503206018519", "[$-c01]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"44562.189571759256", "[$-801]mmm dd yyyy h:mm AM/PM", "\u0643\u0627\u0646\u0648\u0646%A0\u0627\u0644\u062B\u0627\u0646\u064A 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-801]mmmm dd yyyy h:mm AM/PM", "\u0643\u0627\u0646\u0648\u0646%A0\u0627\u0644\u062B\u0627\u0646\u064A 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-801]mmmmm dd yyyy h:mm AM/PM", "\u0643 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-801]mmmmmm dd yyyy h:mm AM/PM", "\u0643\u0627\u0646\u0648\u0646%A0\u0627\u0644\u062B\u0627\u0646\u064A 01 2022 4:32 \u0635"}, + {"43543.503206018519", "[$-801]mmm dd yyyy h:mm AM/PM", "\u0622\u0630\u0627\u0631 19 2019 12:04 \u0645"}, + {"43543.503206018519", "[$-801]mmmm dd yyyy h:mm AM/PM aaa", "\u0622\u0630\u0627\u0631 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"43543.503206018519", "[$-801]mmmmm dd yyyy h:mm AM/PM ddd", "\u0622 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"43543.503206018519", "[$-801]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0622\u0630\u0627\u0631 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"44562.189571759256", "[$-2C01]mmm dd yyyy h:mm AM/PM", "\u0643\u0627\u0646\u0648\u0646%A0\u0627\u0644\u062B\u0627\u0646\u064A 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-2C01]mmmm dd yyyy h:mm AM/PM", "\u0643\u0627\u0646\u0648\u0646%A0\u0627\u0644\u062B\u0627\u0646\u064A 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-2C01]mmmmm dd yyyy h:mm AM/PM", "\u0643 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-2C01]mmmmmm dd yyyy h:mm AM/PM", "\u0643\u0627\u0646\u0648\u0646%A0\u0627\u0644\u062B\u0627\u0646\u064A 01 2022 4:32 \u0635"}, + {"43543.503206018519", "[$-2C01]mmm dd yyyy h:mm AM/PM", "\u0622\u0630\u0627\u0631 19 2019 12:04 \u0645"}, + {"43543.503206018519", "[$-2C01]mmmm dd yyyy h:mm AM/PM aaa", "\u0622\u0630\u0627\u0631 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"43543.503206018519", "[$-2C01]mmmmm dd yyyy h:mm AM/PM ddd", "\u0622 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"43543.503206018519", "[$-2C01]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0622\u0630\u0627\u0631 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"44562.189571759256", "[$-3401]mmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-3401]mmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-3401]mmmmm dd yyyy h:mm AM/PM", "\u064A 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-3401]mmmmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"}, + {"43543.503206018519", "[$-3401]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645"}, + {"43543.503206018519", "[$-3401]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"43543.503206018519", "[$-3401]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"43543.503206018519", "[$-3401]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"44562.189571759256", "[$-3001]mmm dd yyyy h:mm AM/PM", "\u0643\u0627\u0646\u0648\u0646%A0\u0627\u0644\u062B\u0627\u0646\u064A 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-3001]mmmm dd yyyy h:mm AM/PM", "\u0643\u0627\u0646\u0648\u0646%A0\u0627\u0644\u062B\u0627\u0646\u064A 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-3001]mmmmm dd yyyy h:mm AM/PM", "\u0643 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-3001]mmmmmm dd yyyy h:mm AM/PM", "\u0643\u0627\u0646\u0648\u0646%A0\u0627\u0644\u062B\u0627\u0646\u064A 01 2022 4:32 \u0635"}, + {"43543.503206018519", "[$-3001]mmm dd yyyy h:mm AM/PM", "\u0622\u0630\u0627\u0631 19 2019 12:04 \u0645"}, + {"43543.503206018519", "[$-3001]mmmm dd yyyy h:mm AM/PM aaa", "\u0622\u0630\u0627\u0631 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"43543.503206018519", "[$-3001]mmmmm dd yyyy h:mm AM/PM ddd", "\u0622 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"43543.503206018519", "[$-3001]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0622\u0630\u0627\u0631 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"44562.189571759256", "[$-1801]mmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-1801]mmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-1801]mmmmm dd yyyy h:mm AM/PM", "\u064A 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-1801]mmmmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"}, + {"43543.503206018519", "[$-1801]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645"}, + {"43543.503206018519", "[$-1801]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"43543.503206018519", "[$-1801]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"43543.503206018519", "[$-1801]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"44562.189571759256", "[$-2001]mmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-2001]mmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-2001]mmmmm dd yyyy h:mm AM/PM", "\u064A 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-2001]mmmmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"}, + {"43543.503206018519", "[$-2001]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645"}, + {"43543.503206018519", "[$-2001]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"43543.503206018519", "[$-2001]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"43543.503206018519", "[$-2001]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"44562.189571759256", "[$-4001]mmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-4001]mmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-4001]mmmmm dd yyyy h:mm AM/PM", "\u064A 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-4001]mmmmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"}, + {"43543.503206018519", "[$-4001]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645"}, + {"43543.503206018519", "[$-4001]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"43543.503206018519", "[$-4001]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"43543.503206018519", "[$-4001]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"44562.189571759256", "[$-401]mmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-401]mmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-401]mmmmm dd yyyy h:mm AM/PM", "\u064A 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-401]mmmmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"}, + {"43543.503206018519", "[$-401]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645"}, + {"43543.503206018519", "[$-401]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"43543.503206018519", "[$-401]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"43543.503206018519", "[$-401]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"44562.189571759256", "[$-2801]mmm dd yyyy h:mm AM/PM", "\u0643\u0627\u0646\u0648\u0646%A0\u0627\u0644\u062B\u0627\u0646\u064A 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-2801]mmmm dd yyyy h:mm AM/PM", "\u0643\u0627\u0646\u0648\u0646%A0\u0627\u0644\u062B\u0627\u0646\u064A 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-2801]mmmmm dd yyyy h:mm AM/PM", "\u0643 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-2801]mmmmmm dd yyyy h:mm AM/PM", "\u0643\u0627\u0646\u0648\u0646%A0\u0627\u0644\u062B\u0627\u0646\u064A 01 2022 4:32 \u0635"}, + {"43543.503206018519", "[$-2801]mmm dd yyyy h:mm AM/PM", "\u0622\u0630\u0627\u0631 19 2019 12:04 \u0645"}, + {"43543.503206018519", "[$-2801]mmmm dd yyyy h:mm AM/PM aaa", "\u0622\u0630\u0627\u0631 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"43543.503206018519", "[$-2801]mmmmm dd yyyy h:mm AM/PM ddd", "\u0622 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"43543.503206018519", "[$-2801]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0622\u0630\u0627\u0631 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"44562.189571759256", "[$-1C01]mmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-1C01]mmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-1C01]mmmmm dd yyyy h:mm AM/PM", "\u064A 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-1C01]mmmmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"}, + {"43543.503206018519", "[$-1C01]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645"}, + {"43543.503206018519", "[$-1C01]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"43543.503206018519", "[$-1C01]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"43543.503206018519", "[$-1C01]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"44562.189571759256", "[$-3801]mmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-3801]mmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-3801]mmmmm dd yyyy h:mm AM/PM", "\u064A 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-3801]mmmmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"}, + {"43543.503206018519", "[$-3801]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645"}, + {"43543.503206018519", "[$-3801]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"43543.503206018519", "[$-3801]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"43543.503206018519", "[$-3801]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"44562.189571759256", "[$-2401]mmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-2401]mmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-2401]mmmmm dd yyyy h:mm AM/PM", "\u064A 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-2401]mmmmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"}, + {"43543.503206018519", "[$-2401]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645"}, + {"43543.503206018519", "[$-2401]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"43543.503206018519", "[$-2401]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"43543.503206018519", "[$-2401]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"44562.189571759256", "[$-2B]mmm dd yyyy h:mm AM/PM", "\u0540\u0576\u057E 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-2B]mmmm dd yyyy h:mm AM/PM", "\u0540\u0578\u0582\u0576\u057E\u0561\u0580 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-2B]mmmmm dd yyyy h:mm AM/PM", "\u0540 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-2B]mmmmmm dd yyyy h:mm AM/PM", "\u0540\u0578\u0582\u0576\u057E\u0561\u0580 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-2B]mmm dd yyyy h:mm AM/PM", "\u0544\u0580\u057F 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-2B]mmmm dd yyyy h:mm AM/PM aaa", "\u0544\u0561\u0580\u057F 19 2019 12:04 PM \u0535\u0580\u0584"}, + {"43543.503206018519", "[$-2B]mmmmm dd yyyy h:mm AM/PM ddd", "\u0544 19 2019 12:04 PM \u0535\u0580\u0584"}, + {"43543.503206018519", "[$-2B]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0544\u0561\u0580\u057F 19 2019 12:04 PM \u0535\u0580\u0565\u0584\u0577\u0561\u0562\u0569\u056B"}, + {"44562.189571759256", "[$-42B]mmm dd yyyy h:mm AM/PM", "\u0540\u0576\u057E 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-42B]mmmm dd yyyy h:mm AM/PM", "\u0540\u0578\u0582\u0576\u057E\u0561\u0580 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-42B]mmmmm dd yyyy h:mm AM/PM", "\u0540 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-42B]mmmmmm dd yyyy h:mm AM/PM", "\u0540\u0578\u0582\u0576\u057E\u0561\u0580 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-42B]mmm dd yyyy h:mm AM/PM", "\u0544\u0580\u057F 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-42B]mmmm dd yyyy h:mm AM/PM aaa", "\u0544\u0561\u0580\u057F 19 2019 12:04 PM \u0535\u0580\u0584"}, + {"43543.503206018519", "[$-42B]mmmmm dd yyyy h:mm AM/PM ddd", "\u0544 19 2019 12:04 PM \u0535\u0580\u0584"}, + {"43543.503206018519", "[$-42B]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0544\u0561\u0580\u057F 19 2019 12:04 PM \u0535\u0580\u0565\u0584\u0577\u0561\u0562\u0569\u056B"}, + {"44562.189571759256", "[$-4D]mmm dd yyyy h:mm AM/PM", "\u099C\u09BE\u09A8\u09C1 01 2022 4:32 \u09F0\u09BE\u09A4\u09BF\u09AA\u09C1"}, + {"44562.189571759256", "[$-4D]mmmm dd yyyy h:mm AM/PM", "\u099C\u09BE\u09A8\u09C1\u09F1\u09BE\u09F0\u09C0 01 2022 4:32 \u09F0\u09BE\u09A4\u09BF\u09AA\u09C1"}, + {"44562.189571759256", "[$-4D]mmmmm dd yyyy h:mm AM/PM", "\u099C 01 2022 4:32 \u09F0\u09BE\u09A4\u09BF\u09AA\u09C1"}, + {"44562.189571759256", "[$-4D]mmmmmm dd yyyy h:mm AM/PM", "\u099C\u09BE\u09A8\u09C1\u09F1\u09BE\u09F0\u09C0 01 2022 4:32 \u09F0\u09BE\u09A4\u09BF\u09AA\u09C1"}, + {"43543.503206018519", "[$-4D]mmm dd yyyy h:mm AM/PM", "\u09AE\u09BE\u09B0\u09CD\u099A 19 2019 12:04 \u0986\u09AC\u09C7\u09B2\u09BF"}, + {"43543.503206018519", "[$-4D]mmmm dd yyyy h:mm AM/PM aaa", "\u09AE\u09BE\u09B0\u09CD\u099A 19 2019 12:04 \u0986\u09AC\u09C7\u09B2\u09BF \u09AE\u0999\u09CD\u0997\u09B2."}, + {"43543.503206018519", "[$-4D]mmmmm dd yyyy h:mm AM/PM ddd", "\u09AE 19 2019 12:04 \u0986\u09AC\u09C7\u09B2\u09BF \u09AE\u0999\u09CD\u0997\u09B2."}, + {"43543.503206018519", "[$-4D]mmmmmm dd yyyy h:mm AM/PM dddd", "\u09AE\u09BE\u09B0\u09CD\u099A 19 2019 12:04 \u0986\u09AC\u09C7\u09B2\u09BF \u09AE\u0999\u09CD\u0997\u09B2\u09AC\u09BE\u09F0"}, + {"44562.189571759256", "[$-44D]mmm dd yyyy h:mm AM/PM", "\u099C\u09BE\u09A8\u09C1 01 2022 4:32 \u09F0\u09BE\u09A4\u09BF\u09AA\u09C1"}, + {"44562.189571759256", "[$-44D]mmmm dd yyyy h:mm AM/PM", "\u099C\u09BE\u09A8\u09C1\u09F1\u09BE\u09F0\u09C0 01 2022 4:32 \u09F0\u09BE\u09A4\u09BF\u09AA\u09C1"}, + {"44562.189571759256", "[$-44D]mmmmm dd yyyy h:mm AM/PM", "\u099C 01 2022 4:32 \u09F0\u09BE\u09A4\u09BF\u09AA\u09C1"}, + {"44562.189571759256", "[$-44D]mmmmmm dd yyyy h:mm AM/PM", "\u099C\u09BE\u09A8\u09C1\u09F1\u09BE\u09F0\u09C0 01 2022 4:32 \u09F0\u09BE\u09A4\u09BF\u09AA\u09C1"}, + {"43543.503206018519", "[$-44D]mmm dd yyyy h:mm AM/PM", "\u09AE\u09BE\u09B0\u09CD\u099A 19 2019 12:04 \u0986\u09AC\u09C7\u09B2\u09BF"}, + {"43543.503206018519", "[$-44D]mmmm dd yyyy h:mm AM/PM aaa", "\u09AE\u09BE\u09B0\u09CD\u099A 19 2019 12:04 \u0986\u09AC\u09C7\u09B2\u09BF \u09AE\u0999\u09CD\u0997\u09B2."}, + {"43543.503206018519", "[$-44D]mmmmm dd yyyy h:mm AM/PM ddd", "\u09AE 19 2019 12:04 \u0986\u09AC\u09C7\u09B2\u09BF \u09AE\u0999\u09CD\u0997\u09B2."}, + {"43543.503206018519", "[$-44D]mmmmmm dd yyyy h:mm AM/PM dddd", "\u09AE\u09BE\u09B0\u09CD\u099A 19 2019 12:04 \u0986\u09AC\u09C7\u09B2\u09BF \u09AE\u0999\u09CD\u0997\u09B2\u09AC\u09BE\u09F0"}, + {"44562.189571759256", "[$-742C]mmm dd yyyy h:mm AM/PM", "\u0408\u0430\u043D 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-742C]mmmm dd yyyy h:mm AM/PM", "j\u0430\u043D\u0432\u0430\u0440 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-742C]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-742C]mmmmmm dd yyyy h:mm AM/PM", "j\u0430\u043D\u0432\u0430\u0440 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-742C]mmm dd yyyy h:mm AM/PM", "\u041C\u0430\u0440 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-742C]mmmm dd yyyy h:mm AM/PM aaa", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0427\u0430"}, + {"43543.503206018519", "[$-742C]mmmmm dd yyyy h:mm AM/PM ddd", "\u043C 19 2019 12:04 PM \u0427\u0430"}, + {"43543.503206018519", "[$-742C]mmmmmm dd yyyy h:mm AM/PM dddd", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0447\u04D9\u0440\u0448\u04D9\u043D\u0431\u04D9%A0\u0430\u0445\u0448\u0430\u043C\u044B"}, + {"44562.189571759256", "[$-82C]mmm dd yyyy h:mm AM/PM", "\u0408\u0430\u043D 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-82C]mmmm dd yyyy h:mm AM/PM", "j\u0430\u043D\u0432\u0430\u0440 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-82C]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-82C]mmmmmm dd yyyy h:mm AM/PM", "j\u0430\u043D\u0432\u0430\u0440 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-82C]mmm dd yyyy h:mm AM/PM", "\u041C\u0430\u0440 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-82C]mmmm dd yyyy h:mm AM/PM aaa", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0427\u0430"}, + {"43543.503206018519", "[$-82C]mmmmm dd yyyy h:mm AM/PM ddd", "\u043C 19 2019 12:04 PM \u0427\u0430"}, + {"43543.503206018519", "[$-82C]mmmmmm dd yyyy h:mm AM/PM dddd", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0447\u04D9\u0440\u0448\u04D9\u043D\u0431\u04D9%A0\u0430\u0445\u0448\u0430\u043C\u044B"}, + {"44562.189571759256", "[$-2C]mmm dd yyyy h:mm AM/PM", "yan 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-2C]mmmm dd yyyy h:mm AM/PM", "yanvar 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-2C]mmmmm dd yyyy h:mm AM/PM", "y 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-2C]mmmmmm dd yyyy h:mm AM/PM", "yanvar 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-2C]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-2C]mmmm dd yyyy h:mm AM/PM aaa", "mart 19 2019 12:04 PM %C7.A."}, + {"43543.503206018519", "[$-2C]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM %C7.A."}, + {"43543.503206018519", "[$-2C]mmmmmm dd yyyy h:mm AM/PM dddd", "mart 19 2019 12:04 PM %E7\u0259r\u015F\u0259nb\u0259%A0ax\u015Fam\u0131"}, + {"44562.189571759256", "[$-782C]mmm dd yyyy h:mm AM/PM", "yan 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-782C]mmmm dd yyyy h:mm AM/PM", "yanvar 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-782C]mmmmm dd yyyy h:mm AM/PM", "y 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-782C]mmmmmm dd yyyy h:mm AM/PM", "yanvar 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-782C]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-782C]mmmm dd yyyy h:mm AM/PM aaa", "mart 19 2019 12:04 PM %C7.A."}, + {"43543.503206018519", "[$-782C]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM %C7.A."}, + {"43543.503206018519", "[$-782C]mmmmmm dd yyyy h:mm AM/PM dddd", "mart 19 2019 12:04 PM %E7\u0259r\u015F\u0259nb\u0259%A0ax\u015Fam\u0131"}, + {"44562.189571759256", "[$-42C]mmm dd yyyy h:mm AM/PM", "yan 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-42C]mmmm dd yyyy h:mm AM/PM", "yanvar 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-42C]mmmmm dd yyyy h:mm AM/PM", "y 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-42C]mmmmmm dd yyyy h:mm AM/PM", "yanvar 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-42C]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-42C]mmmm dd yyyy h:mm AM/PM aaa", "mart 19 2019 12:04 PM %C7.A."}, + {"43543.503206018519", "[$-42C]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM %C7.A."}, + {"43543.503206018519", "[$-42C]mmmmmm dd yyyy h:mm AM/PM dddd", "mart 19 2019 12:04 PM %E7\u0259r\u015F\u0259nb\u0259%A0ax\u015Fam\u0131"}, + {"43543.503206018519", "[$-45]mmm dd yyyy h:mm AM/PM aaa", "\u09AE\u09BE\u09B0\u09CD\u099A 19 2019 12:04 PM \u09AE\u0999\u09CD\u0997\u09B2."}, + {"43543.503206018519", "[$-45]mmmm dd yyyy h:mm AM/PM ddd", "\u09AE\u09BE\u09B0\u09CD\u099A 19 2019 12:04 PM \u09AE\u0999\u09CD\u0997\u09B2."}, + {"43543.503206018519", "[$-45]mmmmm dd yyyy h:mm AM/PM dddd", "\u09AE 19 2019 12:04 PM \u09AE\u0999\u09CD\u0997\u09B2\u09AC\u09BE\u09B0"}, + {"43543.503206018519", "[$-845]mmm dd yyyy h:mm AM/PM aaa", "\u09AE\u09BE\u09B0\u09CD\u099A 19 2019 12:04 PM \u09AE\u0999\u09CD\u0997\u09B2."}, + {"43543.503206018519", "[$-845]mmmm dd yyyy h:mm AM/PM ddd", "\u09AE\u09BE\u09B0\u09CD\u099A 19 2019 12:04 PM \u09AE\u0999\u09CD\u0997\u09B2."}, + {"43543.503206018519", "[$-845]mmmmm dd yyyy h:mm AM/PM dddd", "\u09AE 19 2019 12:04 PM \u09AE\u0999\u09CD\u0997\u09B2\u09AC\u09BE\u09B0"}, + {"43543.503206018519", "[$-445]mmm dd yyyy h:mm AM/PM aaa", "\u09AE\u09BE\u09B0\u09CD\u099A 19 2019 12:04 PM \u09AE\u0999\u09CD\u0997\u09B2."}, + {"43543.503206018519", "[$-445]mmmm dd yyyy h:mm AM/PM ddd", "\u09AE\u09BE\u09B0\u09CD\u099A 19 2019 12:04 PM \u09AE\u0999\u09CD\u0997\u09B2."}, + {"43543.503206018519", "[$-445]mmmmm dd yyyy h:mm AM/PM dddd", "\u09AE 19 2019 12:04 PM \u09AE\u0999\u09CD\u0997\u09B2\u09AC\u09BE\u09B0"}, + {"44562.189571759256", "[$-6D]mmm dd yyyy h:mm AM/PM", "\u0493\u0438\u043D 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-6D]mmmm dd yyyy h:mm AM/PM", "\u0493\u0438\u043D\u0443\u0430\u0440 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-6D]mmmmm dd yyyy h:mm AM/PM", "\u0493 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-6D]mmmmmm dd yyyy h:mm AM/PM", "\u0493\u0438\u043D\u0443\u0430\u0440 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-6D]mmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-6D]mmmm dd yyyy h:mm AM/PM aaa", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0428\u0448"}, + {"43543.503206018519", "[$-6D]mmmmm dd yyyy h:mm AM/PM ddd", "\u043C 19 2019 12:04 PM \u0428\u0448"}, + {"43543.503206018519", "[$-6D]mmmmmm dd yyyy h:mm AM/PM dddd", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0428\u0438\u0448\u04D9\u043C\u0431\u0435"}, + {"44562.189571759256", "[$-46D]mmm dd yyyy h:mm AM/PM", "\u0493\u0438\u043D 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-46D]mmmm dd yyyy h:mm AM/PM", "\u0493\u0438\u043D\u0443\u0430\u0440 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-46D]mmmmm dd yyyy h:mm AM/PM", "\u0493 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-46D]mmmmmm dd yyyy h:mm AM/PM", "\u0493\u0438\u043D\u0443\u0430\u0440 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-46D]mmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-46D]mmmm dd yyyy h:mm AM/PM aaa", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0428\u0448"}, + {"43543.503206018519", "[$-46D]mmmmm dd yyyy h:mm AM/PM ddd", "\u043C 19 2019 12:04 PM \u0428\u0448"}, + {"43543.503206018519", "[$-46D]mmmmmm dd yyyy h:mm AM/PM dddd", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0428\u0438\u0448\u04D9\u043C\u0431\u0435"}, + {"44562.189571759256", "[$-2D]mmm dd yyyy h:mm AM/PM", "urt. 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-2D]mmmm dd yyyy h:mm AM/PM", "urtarrila 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-2D]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-2D]mmmmmm dd yyyy h:mm AM/PM", "urtarrila 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-2D]mmm dd yyyy h:mm AM/PM", "mar. 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-2D]mmmm dd yyyy h:mm AM/PM aaa", "martxoa 19 2019 12:04 PM ar."}, + {"43543.503206018519", "[$-2D]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM ar."}, + {"43543.503206018519", "[$-2D]mmmmmm dd yyyy h:mm AM/PM dddd", "martxoa 19 2019 12:04 PM asteartea"}, + {"44562.189571759256", "[$-42D]mmm dd yyyy h:mm AM/PM", "urt. 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-42D]mmmm dd yyyy h:mm AM/PM", "urtarrila 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-42D]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-42D]mmmmmm dd yyyy h:mm AM/PM", "urtarrila 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-42D]mmm dd yyyy h:mm AM/PM", "mar. 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-42D]mmmm dd yyyy h:mm AM/PM aaa", "martxoa 19 2019 12:04 PM ar."}, + {"43543.503206018519", "[$-42D]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM ar."}, + {"43543.503206018519", "[$-42D]mmmmmm dd yyyy h:mm AM/PM dddd", "martxoa 19 2019 12:04 PM asteartea"}, + {"44562.189571759256", "[$-23]mmm dd yyyy h:mm AM/PM", "\u0441\u0442\u0443\u0434\u0437 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-23]mmmm dd yyyy h:mm AM/PM", "\u0441\u0442\u0443\u0434\u0437\u0435\u043D\u044C 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-23]mmmmm dd yyyy h:mm AM/PM", "\u0441 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-23]mmmmmm dd yyyy h:mm AM/PM", "\u0441\u0442\u0443\u0434\u0437\u0435\u043D\u044C 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-23]mmm dd yyyy h:mm AM/PM", "\u0441\u0430\u043A 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-23]mmmm dd yyyy h:mm AM/PM aaa", "\u0441\u0430\u043A\u0430\u0432\u0456\u043A 19 2019 12:04 PM \u0430\u045E\u0442"}, + {"43543.503206018519", "[$-23]mmmmm dd yyyy h:mm AM/PM ddd", "\u0441 19 2019 12:04 PM \u0430\u045E\u0442"}, + {"43543.503206018519", "[$-23]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0441\u0430\u043A\u0430\u0432\u0456\u043A 19 2019 12:04 PM \u0430\u045E\u0442\u043E\u0440\u0430\u043A"}, + {"44562.189571759256", "[$-423]mmm dd yyyy h:mm AM/PM", "\u0441\u0442\u0443\u0434\u0437 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-423]mmmm dd yyyy h:mm AM/PM", "\u0441\u0442\u0443\u0434\u0437\u0435\u043D\u044C 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-423]mmmmm dd yyyy h:mm AM/PM", "\u0441 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-423]mmmmmm dd yyyy h:mm AM/PM", "\u0441\u0442\u0443\u0434\u0437\u0435\u043D\u044C 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-423]mmm dd yyyy h:mm AM/PM", "\u0441\u0430\u043A 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-423]mmmm dd yyyy h:mm AM/PM aaa", "\u0441\u0430\u043A\u0430\u0432\u0456\u043A 19 2019 12:04 PM \u0430\u045E\u0442"}, + {"43543.503206018519", "[$-423]mmmmm dd yyyy h:mm AM/PM ddd", "\u0441 19 2019 12:04 PM \u0430\u045E\u0442"}, + {"43543.503206018519", "[$-423]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0441\u0430\u043A\u0430\u0432\u0456\u043A 19 2019 12:04 PM \u0430\u045E\u0442\u043E\u0440\u0430\u043A"}, + {"44562.189571759256", "[$-641A]mmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-641A]mmmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D\u0443\u0430\u0440 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-641A]mmmmm dd yyyy h:mm AM/PM", "\u0458 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-641A]mmmmmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D\u0443\u0430\u0440 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-641A]mmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-641A]mmmm dd yyyy h:mm AM/PM aaa", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0443\u0442\u043E"}, + {"43543.503206018519", "[$-641A]mmmmm dd yyyy h:mm AM/PM ddd", "\u043C 19 2019 12:04 PM \u0443\u0442\u043E"}, + {"43543.503206018519", "[$-641A]mmmmmm dd yyyy h:mm AM/PM dddd", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0443\u0442\u043E\u0440\u0430\u043A"}, + {"44562.189571759256", "[$-201A]mmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-201A]mmmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D\u0443\u0430\u0440 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-201A]mmmmm dd yyyy h:mm AM/PM", "\u0458 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-201A]mmmmmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D\u0443\u0430\u0440 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-201A]mmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-201A]mmmm dd yyyy h:mm AM/PM aaa", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0443\u0442\u043E"}, + {"43543.503206018519", "[$-201A]mmmmm dd yyyy h:mm AM/PM ddd", "\u043C 19 2019 12:04 PM \u0443\u0442\u043E"}, + {"43543.503206018519", "[$-201A]mmmmmm dd yyyy h:mm AM/PM dddd", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0443\u0442\u043E\u0440\u0430\u043A"}, + {"44562.189571759256", "[$-681A]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-681A]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-681A]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-681A]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-681A]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-681A]mmmm dd yyyy h:mm AM/PM aaa", "mart 19 2019 12:04 PM uto"}, + {"43543.503206018519", "[$-681A]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM uto"}, + {"43543.503206018519", "[$-681A]mmmmmm dd yyyy h:mm AM/PM dddd", "mart 19 2019 12:04 PM utorak"}, + {"44562.189571759256", "[$-781A]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-781A]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-781A]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-781A]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-781A]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-781A]mmmm dd yyyy h:mm AM/PM aaa", "mart 19 2019 12:04 PM uto"}, + {"43543.503206018519", "[$-781A]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM uto"}, + {"43543.503206018519", "[$-781A]mmmmmm dd yyyy h:mm AM/PM dddd", "mart 19 2019 12:04 PM utorak"}, + {"44562.189571759256", "[$-141A]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-141A]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-141A]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-141A]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-141A]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-141A]mmmm dd yyyy h:mm AM/PM aaa", "mart 19 2019 12:04 PM uto"}, + {"43543.503206018519", "[$-141A]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM uto"}, + {"43543.503206018519", "[$-141A]mmmmmm dd yyyy h:mm AM/PM dddd", "mart 19 2019 12:04 PM utorak"}, + {"44562.189571759256", "[$-7E]mmm dd yyyy h:mm AM/PM", "Gen. 01 2022 4:32 A.M."}, + {"44562.189571759256", "[$-7E]mmmm dd yyyy h:mm AM/PM", "Genver 01 2022 4:32 A.M."}, + {"44562.189571759256", "[$-7E]mmmmm dd yyyy h:mm AM/PM", "G 01 2022 4:32 A.M."}, + {"44562.189571759256", "[$-7E]mmmmmm dd yyyy h:mm AM/PM", "Genver 01 2022 4:32 A.M."}, + {"43543.503206018519", "[$-7E]mmm dd yyyy h:mm AM/PM", "Meur. 19 2019 12:04 G.M."}, + {"43543.503206018519", "[$-7E]mmmm dd yyyy h:mm AM/PM aaa", "Meurzh 19 2019 12:04 G.M. Meu."}, + {"43543.503206018519", "[$-7E]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 G.M. Meu."}, + {"43543.503206018519", "[$-7E]mmmmmm dd yyyy h:mm AM/PM dddd", "Meurzh 19 2019 12:04 G.M. Meurzh"}, + {"44562.189571759256", "[$-47E]mmm dd yyyy h:mm AM/PM", "Gen. 01 2022 4:32 A.M."}, + {"44562.189571759256", "[$-47E]mmmm dd yyyy h:mm AM/PM", "Genver 01 2022 4:32 A.M."}, + {"44562.189571759256", "[$-47E]mmmmm dd yyyy h:mm AM/PM", "G 01 2022 4:32 A.M."}, + {"44562.189571759256", "[$-47E]mmmmmm dd yyyy h:mm AM/PM", "Genver 01 2022 4:32 A.M."}, + {"43543.503206018519", "[$-47E]mmm dd yyyy h:mm AM/PM", "Meur. 19 2019 12:04 G.M."}, + {"43543.503206018519", "[$-47E]mmmm dd yyyy h:mm AM/PM aaa", "Meurzh 19 2019 12:04 G.M. Meu."}, + {"43543.503206018519", "[$-47E]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 G.M. Meu."}, + {"43543.503206018519", "[$-47E]mmmmmm dd yyyy h:mm AM/PM dddd", "Meurzh 19 2019 12:04 G.M. Meurzh"}, + {"44562.189571759256", "[$-2]mmm dd yyyy h:mm AM/PM", "\u044F\u043D\u0443 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-2]mmmm dd yyyy h:mm AM/PM", "\u044F\u043D\u0443\u0430\u0440\u0438 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-2]mmmmm dd yyyy h:mm AM/PM", "\u044F 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-2]mmmmmm dd yyyy h:mm AM/PM", "\u044F\u043D\u0443\u0430\u0440\u0438 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-2]mmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-2]mmmm dd yyyy h:mm AM/PM aaa", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0432\u0442"}, + {"43543.503206018519", "[$-2]mmmmm dd yyyy h:mm AM/PM ddd", "\u043C 19 2019 12:04 PM \u0432\u0442"}, + {"43543.503206018519", "[$-2]mmmmmm dd yyyy h:mm AM/PM dddd", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0432\u0442\u043E\u0440\u043D\u0438\u043A"}, + {"44562.189571759256", "[$-402]mmm dd yyyy h:mm AM/PM", "\u044F\u043D\u0443 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-402]mmmm dd yyyy h:mm AM/PM", "\u044F\u043D\u0443\u0430\u0440\u0438 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-402]mmmmm dd yyyy h:mm AM/PM", "\u044F 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-402]mmmmmm dd yyyy h:mm AM/PM", "\u044F\u043D\u0443\u0430\u0440\u0438 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-402]mmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-402]mmmm dd yyyy h:mm AM/PM aaa", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0432\u0442"}, + {"43543.503206018519", "[$-402]mmmmm dd yyyy h:mm AM/PM ddd", "\u043C 19 2019 12:04 PM \u0432\u0442"}, + {"43543.503206018519", "[$-402]mmmmmm dd yyyy h:mm AM/PM dddd", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0432\u0442\u043E\u0440\u043D\u0438\u043A"}, + {"44562.189571759256", "[$-55]mmm dd yyyy h:mm AM/PM", "\u1007\u1014\u103A 01 2022 4:32 \u1014\u1036\u1014\u1000\u103A"}, + {"44562.189571759256", "[$-55]mmmm dd yyyy h:mm AM/PM", "\u1007\u1014\u103A\u1014\u101D\u102B\u101B\u102E 01 2022 4:32 \u1014\u1036\u1014\u1000\u103A"}, + {"44562.189571759256", "[$-55]mmmmm dd yyyy h:mm AM/PM", "\u1007 01 2022 4:32 \u1014\u1036\u1014\u1000\u103A"}, + {"44562.189571759256", "[$-55]mmmmmm dd yyyy h:mm AM/PM", "\u1007\u1014\u103A\u1014\u101D\u102B\u101B\u102E 01 2022 4:32 \u1014\u1036\u1014\u1000\u103A"}, + {"43543.503206018519", "[$-55]mmm dd yyyy h:mm AM/PM", "\u1019\u1010\u103A 19 2019 12:04 \u100A\u1014\u1031"}, + {"43543.503206018519", "[$-55]mmmm dd yyyy h:mm AM/PM aaa", "\u1019\u1010\u103A 19 2019 12:04 \u100A\u1014\u1031 \u1021\u1004\u103A\u1039\u1002\u102B"}, + {"43543.503206018519", "[$-55]mmmmm dd yyyy h:mm AM/PM ddd", "\u1019 19 2019 12:04 \u100A\u1014\u1031 \u1021\u1004\u103A\u1039\u1002\u102B"}, + {"43543.503206018519", "[$-55]mmmmmm dd yyyy h:mm AM/PM dddd", "\u1019\u1010\u103A 19 2019 12:04 \u100A\u1014\u1031 \u1021\u1004\u103A\u1039\u1002\u102B"}, + {"44562.189571759256", "[$-455]mmm dd yyyy h:mm AM/PM", "\u1007\u1014\u103A 01 2022 4:32 \u1014\u1036\u1014\u1000\u103A"}, + {"44562.189571759256", "[$-455]mmmm dd yyyy h:mm AM/PM", "\u1007\u1014\u103A\u1014\u101D\u102B\u101B\u102E 01 2022 4:32 \u1014\u1036\u1014\u1000\u103A"}, + {"44562.189571759256", "[$-455]mmmmm dd yyyy h:mm AM/PM", "\u1007 01 2022 4:32 \u1014\u1036\u1014\u1000\u103A"}, + {"44562.189571759256", "[$-455]mmmmmm dd yyyy h:mm AM/PM", "\u1007\u1014\u103A\u1014\u101D\u102B\u101B\u102E 01 2022 4:32 \u1014\u1036\u1014\u1000\u103A"}, + {"43543.503206018519", "[$-455]mmm dd yyyy h:mm AM/PM", "\u1019\u1010\u103A 19 2019 12:04 \u100A\u1014\u1031"}, + {"43543.503206018519", "[$-455]mmmm dd yyyy h:mm AM/PM aaa", "\u1019\u1010\u103A 19 2019 12:04 \u100A\u1014\u1031 \u1021\u1004\u103A\u1039\u1002\u102B"}, + {"43543.503206018519", "[$-455]mmmmm dd yyyy h:mm AM/PM ddd", "\u1019 19 2019 12:04 \u100A\u1014\u1031 \u1021\u1004\u103A\u1039\u1002\u102B"}, + {"43543.503206018519", "[$-455]mmmmmm dd yyyy h:mm AM/PM dddd", "\u1019\u1010\u103A 19 2019 12:04 \u100A\u1014\u1031 \u1021\u1004\u103A\u1039\u1002\u102B"}, + {"44562.189571759256", "[$-3]mmm dd yyyy h:mm AM/PM", "gen. 01 2022 4:32 a.%A0m."}, + {"44562.189571759256", "[$-3]mmmm dd yyyy h:mm AM/PM", "gener 01 2022 4:32 a.%A0m."}, + {"44562.189571759256", "[$-3]mmmmm dd yyyy h:mm AM/PM", "g 01 2022 4:32 a.%A0m."}, + {"44562.189571759256", "[$-3]mmmmmm dd yyyy h:mm AM/PM", "gener 01 2022 4:32 a.%A0m."}, + {"43543.503206018519", "[$-3]mmm dd yyyy h:mm AM/PM", "març 19 2019 12:04 p.%A0m."}, + {"43543.503206018519", "[$-3]mmmm dd yyyy h:mm AM/PM aaa", "març 19 2019 12:04 p.%A0m. dt."}, + {"43543.503206018519", "[$-3]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 p.%A0m. dt."}, + {"43543.503206018519", "[$-3]mmmmmm dd yyyy h:mm AM/PM dddd", "març 19 2019 12:04 p.%A0m. dimarts"}, + {"44562.189571759256", "[$-403]mmm dd yyyy h:mm AM/PM", "gen. 01 2022 4:32 a.%A0m."}, + {"44562.189571759256", "[$-403]mmmm dd yyyy h:mm AM/PM", "gener 01 2022 4:32 a.%A0m."}, + {"44562.189571759256", "[$-403]mmmmm dd yyyy h:mm AM/PM", "g 01 2022 4:32 a.%A0m."}, + {"44562.189571759256", "[$-403]mmmmmm dd yyyy h:mm AM/PM", "gener 01 2022 4:32 a.%A0m."}, + {"43543.503206018519", "[$-403]mmm dd yyyy h:mm AM/PM", "març 19 2019 12:04 p.%A0m."}, + {"43543.503206018519", "[$-403]mmmm dd yyyy h:mm AM/PM aaa", "març 19 2019 12:04 p.%A0m. dt."}, + {"43543.503206018519", "[$-403]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 p.%A0m. dt."}, + {"43543.503206018519", "[$-403]mmmmmm dd yyyy h:mm AM/PM dddd", "març 19 2019 12:04 p.%A0m. dimarts"}, + {"44562.189571759256", "[$-45F]mmm dd yyyy h:mm AM/PM", "\u0643\u0627\u0646\u0648\u0646%A0\u0627\u0644\u062B\u0627\u0646\u064A 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-45F]mmmm dd yyyy h:mm AM/PM", "\u0643\u0627\u0646\u0648\u0646%A0\u0627\u0644\u062B\u0627\u0646\u064A 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-45F]mmmmm dd yyyy h:mm AM/PM", "\u0643 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-45F]mmmmmm dd yyyy h:mm AM/PM", "\u0643\u0627\u0646\u0648\u0646%A0\u0627\u0644\u062B\u0627\u0646\u064A 01 2022 4:32 \u0635"}, + {"43543.503206018519", "[$-45F]mmm dd yyyy h:mm AM/PM", "\u0622\u0630\u0627\u0631 19 2019 12:04 \u0645"}, + {"43543.503206018519", "[$-45F]mmmm dd yyyy h:mm AM/PM aaa", "\u0622\u0630\u0627\u0631 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"43543.503206018519", "[$-45F]mmmmm dd yyyy h:mm AM/PM ddd", "\u0622 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"43543.503206018519", "[$-45F]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0622\u0630\u0627\u0631 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"44562.189571759256", "[$-92]mmm dd yyyy h:mm AM/PM", "\u06A9\u0627\u0646\u0648\u0648\u0646\u06CC%20\u062F\u0648\u0648\u06D5\u0645 01 2022 4:32 \u067E.\u0646"}, + {"44562.189571759256", "[$-92]mmmm dd yyyy h:mm AM/PM", "\u06A9\u0627\u0646\u0648\u0648\u0646\u06CC%20\u062F\u0648\u0648\u06D5\u0645 01 2022 4:32 \u067E.\u0646"}, + {"44562.189571759256", "[$-92]mmmmm dd yyyy h:mm AM/PM", "\u06A9 01 2022 4:32 \u067E.\u0646"}, + {"44562.189571759256", "[$-92]mmmmmm dd yyyy h:mm AM/PM", "\u06A9\u0627\u0646\u0648\u0648\u0646\u06CC%20\u062F\u0648\u0648\u06D5\u0645 01 2022 4:32 \u067E.\u0646"}, + {"43543.503206018519", "[$-92]mmm dd yyyy h:mm AM/PM", "\u0626\u0627\u0632\u0627\u0631 19 2019 12:04 \u062F.\u0646"}, + {"43543.503206018519", "[$-92]mmmm dd yyyy h:mm AM/PM aaa", "\u0626\u0627\u0632\u0627\u0631 19 2019 12:04 \u062F.\u0646 \u0633\u06CE\u0634\u06D5\u0645\u0645\u06D5"}, + {"43543.503206018519", "[$-92]mmmmm dd yyyy h:mm AM/PM ddd", "\u0626 19 2019 12:04 \u062F.\u0646 \u0633\u06CE\u0634\u06D5\u0645\u0645\u06D5"}, + {"43543.503206018519", "[$-92]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0626\u0627\u0632\u0627\u0631 19 2019 12:04 \u062F.\u0646 \u0633\u06CE\u0634\u06D5\u0645\u0645\u06D5"}, + {"44562.189571759256", "[$-7C92]mmm dd yyyy h:mm AM/PM", "\u06A9\u0627\u0646\u0648\u0648\u0646\u06CC%20\u062F\u0648\u0648\u06D5\u0645 01 2022 4:32 \u067E.\u0646"}, + {"44562.189571759256", "[$-7C92]mmmm dd yyyy h:mm AM/PM", "\u06A9\u0627\u0646\u0648\u0648\u0646\u06CC%20\u062F\u0648\u0648\u06D5\u0645 01 2022 4:32 \u067E.\u0646"}, + {"44562.189571759256", "[$-7C92]mmmmm dd yyyy h:mm AM/PM", "\u06A9 01 2022 4:32 \u067E.\u0646"}, + {"44562.189571759256", "[$-7C92]mmmmmm dd yyyy h:mm AM/PM", "\u06A9\u0627\u0646\u0648\u0648\u0646\u06CC%20\u062F\u0648\u0648\u06D5\u0645 01 2022 4:32 \u067E.\u0646"}, + {"43543.503206018519", "[$-7C92]mmm dd yyyy h:mm AM/PM", "\u0626\u0627\u0632\u0627\u0631 19 2019 12:04 \u062F.\u0646"}, + {"43543.503206018519", "[$-7C92]mmmm dd yyyy h:mm AM/PM aaa", "\u0626\u0627\u0632\u0627\u0631 19 2019 12:04 \u062F.\u0646 \u0633\u06CE\u0634\u06D5\u0645\u0645\u06D5"}, + {"43543.503206018519", "[$-7C92]mmmmm dd yyyy h:mm AM/PM ddd", "\u0626 19 2019 12:04 \u062F.\u0646 \u0633\u06CE\u0634\u06D5\u0645\u0645\u06D5"}, + {"43543.503206018519", "[$-7C92]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0626\u0627\u0632\u0627\u0631 19 2019 12:04 \u062F.\u0646 \u0633\u06CE\u0634\u06D5\u0645\u0645\u06D5"}, + + {"44562.189571759256", "[$-492]mmm dd yyyy h:mm AM/PM", "\u06A9\u0627\u0646\u0648\u0648\u0646\u06CC%20\u062F\u0648\u0648\u06D5\u0645 01 2022 4:32 \u067E.\u0646"}, + {"44562.189571759256", "[$-492]mmmm dd yyyy h:mm AM/PM", "\u06A9\u0627\u0646\u0648\u0648\u0646\u06CC%20\u062F\u0648\u0648\u06D5\u0645 01 2022 4:32 \u067E.\u0646"}, + {"44562.189571759256", "[$-492]mmmmm dd yyyy h:mm AM/PM", "\u06A9 01 2022 4:32 \u067E.\u0646"}, + {"44562.189571759256", "[$-492]mmmmmm dd yyyy h:mm AM/PM", "\u06A9\u0627\u0646\u0648\u0648\u0646\u06CC%20\u062F\u0648\u0648\u06D5\u0645 01 2022 4:32 \u067E.\u0646"}, + {"43543.503206018519", "[$-492]mmm dd yyyy h:mm AM/PM", "\u0626\u0627\u0632\u0627\u0631 19 2019 12:04 \u062F.\u0646"}, + {"43543.503206018519", "[$-492]mmmm dd yyyy h:mm AM/PM aaa", "\u0626\u0627\u0632\u0627\u0631 19 2019 12:04 \u062F.\u0646 \u0633\u06CE\u0634\u06D5\u0645\u0645\u06D5"}, + {"43543.503206018519", "[$-492]mmmmm dd yyyy h:mm AM/PM ddd", "\u0626 19 2019 12:04 \u062F.\u0646 \u0633\u06CE\u0634\u06D5\u0645\u0645\u06D5"}, + {"43543.503206018519", "[$-492]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0626\u0627\u0632\u0627\u0631 19 2019 12:04 \u062F.\u0646 \u0633\u06CE\u0634\u06D5\u0645\u0645\u06D5"}, + {"44562.189571759256", "[$-5C]mmm dd yyyy h:mm AM/PM", "\u13A4\u13C3\u13B8 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-5C]mmmm dd yyyy h:mm AM/PM", "\u13A4\u13C3\u13B8\u13D4\u13C5 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-5C]mmmmm dd yyyy h:mm AM/PM", "\u13A4 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-5C]mmmmmm dd yyyy h:mm AM/PM", "\u13A4\u13C3\u13B8\u13D4\u13C5 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-5C]mmm dd yyyy h:mm AM/PM", "\u13A0\u13C5\u13F1 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-5C]mmmm dd yyyy h:mm AM/PM aaa", "\u13A0\u13C5\u13F1 19 2019 12:04 PM \u13D4\u13B5\u13C1"}, + {"43543.503206018519", "[$-5C]mmmmm dd yyyy h:mm AM/PM ddd", "\u13A0 19 2019 12:04 PM \u13D4\u13B5\u13C1"}, + {"43543.503206018519", "[$-5C]mmmmmm dd yyyy h:mm AM/PM dddd", "\u13A0\u13C5\u13F1 19 2019 12:04 PM \u13D4\u13B5\u13C1\u13A2\u13A6"}, + {"44562.189571759256", "[$-7C5C]mmm dd yyyy h:mm AM/PM", "\u13A4\u13C3\u13B8 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-7C5C]mmmm dd yyyy h:mm AM/PM", "\u13A4\u13C3\u13B8\u13D4\u13C5 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-7C5C]mmmmm dd yyyy h:mm AM/PM", "\u13A4 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-7C5C]mmmmmm dd yyyy h:mm AM/PM", "\u13A4\u13C3\u13B8\u13D4\u13C5 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-7C5C]mmm dd yyyy h:mm AM/PM", "\u13A0\u13C5\u13F1 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-7C5C]mmmm dd yyyy h:mm AM/PM aaa", "\u13A0\u13C5\u13F1 19 2019 12:04 PM \u13D4\u13B5\u13C1"}, + {"43543.503206018519", "[$-7C5C]mmmmm dd yyyy h:mm AM/PM ddd", "\u13A0 19 2019 12:04 PM \u13D4\u13B5\u13C1"}, + {"43543.503206018519", "[$-7C5C]mmmmmm dd yyyy h:mm AM/PM dddd", "\u13A0\u13C5\u13F1 19 2019 12:04 PM \u13D4\u13B5\u13C1\u13A2\u13A6"}, + {"44562.189571759256", "[$-45C]mmm dd yyyy h:mm AM/PM", "\u13A4\u13C3\u13B8 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-45C]mmmm dd yyyy h:mm AM/PM", "\u13A4\u13C3\u13B8\u13D4\u13C5 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-45C]mmmmm dd yyyy h:mm AM/PM", "\u13A4 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-45C]mmmmmm dd yyyy h:mm AM/PM", "\u13A4\u13C3\u13B8\u13D4\u13C5 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-45C]mmm dd yyyy h:mm AM/PM", "\u13A0\u13C5\u13F1 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-45C]mmmm dd yyyy h:mm AM/PM aaa", "\u13A0\u13C5\u13F1 19 2019 12:04 PM \u13D4\u13B5\u13C1"}, + {"43543.503206018519", "[$-45C]mmmmm dd yyyy h:mm AM/PM ddd", "\u13A0 19 2019 12:04 PM \u13D4\u13B5\u13C1"}, + {"43543.503206018519", "[$-45C]mmmmmm dd yyyy h:mm AM/PM dddd", "\u13A0\u13C5\u13F1 19 2019 12:04 PM \u13D4\u13B5\u13C1\u13A2\u13A6"}, + {"43543.503206018519", "[$-4]mmm dd yyyy h:mm AM/PM aaa", "3月 19 2019 12:04 下午 週二"}, + {"43543.503206018519", "[$-4]mmmm dd yyyy h:mm AM/PM ddd", "三月 19 2019 12:04 下午 週二"}, + {"43543.503206018519", "[$-4]mmmmm dd yyyy h:mm AM/PM dddd", "三 19 2019 12:04 下午 星期二"}, + {"43543.503206018519", "[$-7804]mmm dd yyyy h:mm AM/PM aaa", "3月 19 2019 12:04 下午 週二"}, + {"43543.503206018519", "[$-7804]mmmm dd yyyy h:mm AM/PM ddd", "三月 19 2019 12:04 下午 週二"}, + {"43543.503206018519", "[$-7804]mmmmm dd yyyy h:mm AM/PM dddd", "三 19 2019 12:04 下午 星期二"}, + {"43543.503206018519", "[$-804]mmm dd yyyy h:mm AM/PM aaa", "3月 19 2019 12:04 下午 周二"}, + {"43543.503206018519", "[$-804]mmmm dd yyyy h:mm AM/PM ddd", "三月 19 2019 12:04 下午 周二"}, + {"43543.503206018519", "[$-804]mmmmm dd yyyy h:mm AM/PM dddd", "三 19 2019 12:04 下午 星期二"}, + {"43543.503206018519", "[$-1004]mmm dd yyyy h:mm AM/PM aaa", "三月 19 2019 12:04 下午 周二"}, + {"43543.503206018519", "[$-1004]mmmm dd yyyy h:mm AM/PM ddd", "三月 19 2019 12:04 下午 周二"}, + {"43543.503206018519", "[$-1004]mmmmm dd yyyy h:mm AM/PM dddd", "三 19 2019 12:04 下午 星期二"}, + {"43543.503206018519", "[$-7C04]mmm dd yyyy h:mm AM/PM aaa", "3月 19 2019 12:04 下午 週二"}, + {"43543.503206018519", "[$-7C04]mmmm dd yyyy h:mm AM/PM ddd", "3月 19 2019 12:04 下午 週二"}, + {"43543.503206018519", "[$-7C04]mmmmm dd yyyy h:mm AM/PM dddd", "3 19 2019 12:04 下午 星期二"}, + {"43543.503206018519", "[$-C04]mmm dd yyyy h:mm AM/PM aaa", "三月 19 2019 12:04 下午 週二"}, + {"43543.503206018519", "[$-C04]mmmm dd yyyy h:mm AM/PM ddd", "三月 19 2019 12:04 下午 週二"}, + {"43543.503206018519", "[$-C04]mmmmm dd yyyy h:mm AM/PM dddd", "三 19 2019 12:04 下午 星期二"}, + {"43543.503206018519", "[$-1404]mmm dd yyyy h:mm AM/PM aaa", "3月 19 2019 12:04 下午 週二"}, + {"43543.503206018519", "[$-1404]mmmm dd yyyy h:mm AM/PM ddd", "3月 19 2019 12:04 下午 週二"}, + {"43543.503206018519", "[$-1404]mmmmm dd yyyy h:mm AM/PM dddd", "3 19 2019 12:04 下午 星期二"}, + {"43543.503206018519", "[$-404]mmm dd yyyy h:mm AM/PM aaa", "3月 19 2019 12:04 下午 週二"}, + {"43543.503206018519", "[$-404]mmmm dd yyyy h:mm AM/PM ddd", "3月 19 2019 12:04 下午 週二"}, + {"43543.503206018519", "[$-404]mmmmm dd yyyy h:mm AM/PM dddd", "3 19 2019 12:04 下午 星期二"}, + {"43543.503206018519", "[$-9]mmm dd yyyy h:mm AM/PM aaa", "Mar 19 2019 12:04 PM Tue"}, + {"43543.503206018519", "[$-9]mmmm dd yyyy h:mm AM/PM ddd", "March 19 2019 12:04 PM Tue"}, + {"43543.503206018519", "[$-9]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 PM Tuesday"}, + {"43543.503206018519", "[$-1000]mmm dd yyyy h:mm AM/PM aaa", "Mar 19 2019 12:04 PM Tue"}, + {"43543.503206018519", "[$-1000]mmmm dd yyyy h:mm AM/PM ddd", "March 19 2019 12:04 PM Tue"}, + {"43543.503206018519", "[$-1000]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 PM Tuesday"}, + {"43543.503206018519", "[$-C09]mmm dd yyyy h:mm AM/PM aaa", "Mar 19 2019 12:04 pm Tue"}, + {"43543.503206018519", "[$-C09]mmmm dd yyyy h:mm AM/PM ddd", "March 19 2019 12:04 pm Tue"}, + {"43543.503206018519", "[$-C09]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 pm Tuesday"}, + {"43543.503206018519", "[$-c09]mmm dd yyyy h:mm AM/PM aaa", "Mar 19 2019 12:04 pm Tue"}, + {"43543.503206018519", "[$-c09]mmmm dd yyyy h:mm AM/PM ddd", "March 19 2019 12:04 pm Tue"}, + {"43543.503206018519", "[$-c09]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 pm Tuesday"}, + {"43543.503206018519", "[$-2809]mmm dd yyyy h:mm AM/PM aaa", "Mar 19 2019 12:04 PM Tue"}, + {"43543.503206018519", "[$-2809]mmmm dd yyyy h:mm AM/PM ddd", "March 19 2019 12:04 PM Tue"}, + {"43543.503206018519", "[$-2809]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 PM Tuesday"}, + {"43543.503206018519", "[$-1009]mmm dd yyyy h:mm AM/PM aaa", "Mar 19 2019 12:04 PM Tue"}, + {"43543.503206018519", "[$-1009]mmmm dd yyyy h:mm AM/PM ddd", "March 19 2019 12:04 PM Tue"}, + {"43543.503206018519", "[$-1009]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 PM Tuesday"}, + {"43543.503206018519", "[$-2409]mmm dd yyyy h:mm AM/PM aaa", "Mar 19 2019 12:04 PM Tue"}, + {"43543.503206018519", "[$-2409]mmmm dd yyyy h:mm AM/PM ddd", "March 19 2019 12:04 PM Tue"}, + {"43543.503206018519", "[$-2409]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 PM Tuesday"}, + {"43543.503206018519", "[$-3C09]mmm dd yyyy h:mm AM/PM aaa", "Mar 19 2019 12:04 PM Tue"}, + {"43543.503206018519", "[$-3C09]mmmm dd yyyy h:mm AM/PM ddd", "March 19 2019 12:04 PM Tue"}, + {"43543.503206018519", "[$-3C09]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 PM Tuesday"}, + {"43543.503206018519", "[$-4009]mmm dd yyyy h:mm AM/PM aaa", "Mar 19 2019 12:04 PM Tue"}, + {"43543.503206018519", "[$-4009]mmmm dd yyyy h:mm AM/PM ddd", "March 19 2019 12:04 PM Tue"}, + {"43543.503206018519", "[$-4009]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 PM Tuesday"}, + {"43543.503206018519", "[$-1809]mmm dd yyyy h:mm AM/PM aaa", "Mar 19 2019 12:04 pm Tue"}, + {"43543.503206018519", "[$-1809]mmmm dd yyyy h:mm AM/PM ddd", "March 19 2019 12:04 pm Tue"}, + {"43543.503206018519", "[$-1809]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 pm Tuesday"}, + {"43543.503206018519", "[$-2009]mmm dd yyyy h:mm AM/PM aaa", "Mar 19 2019 12:04 PM Tue"}, + {"43543.503206018519", "[$-2009]mmmm dd yyyy h:mm AM/PM ddd", "March 19 2019 12:04 PM Tue"}, + {"43543.503206018519", "[$-2009]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 PM Tuesday"}, + {"43543.503206018519", "[$-4409]mmm dd yyyy h:mm AM/PM aaa", "Mar 19 2019 12:04 PM Tue"}, + {"43543.503206018519", "[$-4409]mmmm dd yyyy h:mm AM/PM ddd", "March 19 2019 12:04 PM Tue"}, + {"43543.503206018519", "[$-4409]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 PM Tuesday"}, + {"43543.503206018519", "[$-1409]mmm dd yyyy h:mm AM/PM aaa", "Mar 19 2019 12:04 PM Tue"}, + {"43543.503206018519", "[$-1409]mmmm dd yyyy h:mm AM/PM ddd", "March 19 2019 12:04 PM Tue"}, + {"43543.503206018519", "[$-1409]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 PM Tuesday"}, + {"43543.503206018519", "[$-3409]mmm dd yyyy h:mm AM/PM aaa", "Mar 19 2019 12:04 PM Tue"}, + {"43543.503206018519", "[$-3409]mmmm dd yyyy h:mm AM/PM ddd", "March 19 2019 12:04 PM Tue"}, + {"43543.503206018519", "[$-3409]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 PM Tuesday"}, + {"43543.503206018519", "[$-4809]mmm dd yyyy h:mm AM/PM aaa", "Mar 19 2019 12:04 PM Tue"}, + {"43543.503206018519", "[$-4809]mmmm dd yyyy h:mm AM/PM ddd", "March 19 2019 12:04 PM Tue"}, + {"43543.503206018519", "[$-4809]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 PM Tuesday"}, + {"43543.503206018519", "[$-1C09]mmm dd yyyy h:mm AM/PM aaa", "Mar 19 2019 12:04 PM Tue"}, + {"43543.503206018519", "[$-1C09]mmmm dd yyyy h:mm AM/PM ddd", "March 19 2019 12:04 PM Tue"}, + {"43543.503206018519", "[$-1C09]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 PM Tuesday"}, + {"43543.503206018519", "[$-2C09]mmm dd yyyy h:mm AM/PM aaa", "Mar 19 2019 12:04 PM Tue"}, + {"43543.503206018519", "[$-2C09]mmmm dd yyyy h:mm AM/PM ddd", "March 19 2019 12:04 PM Tue"}, + {"43543.503206018519", "[$-2C09]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 PM Tuesday"}, + {"43543.503206018519", "[$-4C09]mmm dd yyyy h:mm AM/PM aaa", "Mar 19 2019 12:04 PM Tue"}, + {"43543.503206018519", "[$-4C09]mmmm dd yyyy h:mm AM/PM ddd", "March 19 2019 12:04 PM Tue"}, + {"43543.503206018519", "[$-4C09]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 PM Tuesday"}, + {"43543.503206018519", "[$-809]mmm dd yyyy h:mm AM/PM aaa", "Mar 19 2019 12:04 pm Tue"}, + {"43543.503206018519", "[$-809]mmmm dd yyyy h:mm AM/PM ddd", "March 19 2019 12:04 pm Tue"}, + {"43543.503206018519", "[$-809]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 pm Tuesday"}, + {"43543.503206018519", "[$-3009]mmm dd yyyy h:mm AM/PM aaa", "Mar 19 2019 12:04 PM Tue"}, + {"43543.503206018519", "[$-3009]mmmm dd yyyy h:mm AM/PM ddd", "March 19 2019 12:04 PM Tue"}, + {"43543.503206018519", "[$-3009]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 PM Tuesday"}, + {"43543.503206018519", "[$-25]mmm dd yyyy h:mm AM/PM aaa", "märts 19 2019 12:04 PM T"}, + {"43543.503206018519", "[$-25]mmmm dd yyyy h:mm AM/PM ddd", "märts 19 2019 12:04 PM T"}, + {"43543.503206018519", "[$-25]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 PM teisipäev"}, + {"43543.503206018519", "[$-425]mmm dd yyyy h:mm AM/PM aaa", "märts 19 2019 12:04 PM T"}, + {"43543.503206018519", "[$-425]mmmm dd yyyy h:mm AM/PM ddd", "märts 19 2019 12:04 PM T"}, + {"43543.503206018519", "[$-425]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 PM teisipäev"}, + {"43543.503206018519", "[$-38]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 um sein. týs."}, + {"43543.503206018519", "[$-38]mmmm dd yyyy h:mm AM/PM ddd", "mars 19 2019 12:04 um sein. týs."}, + {"43543.503206018519", "[$-38]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 um sein. týsdagur"}, + {"43543.503206018519", "[$-438]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 um sein. týs."}, + {"43543.503206018519", "[$-438]mmmm dd yyyy h:mm AM/PM ddd", "mars 19 2019 12:04 um sein. týs."}, + {"43543.503206018519", "[$-438]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 um sein. týsdagur"}, + {"43543.503206018519", "[$-64]mmm dd yyyy h:mm AM/PM aaa", "Mar 19 2019 12:04 PM Mar"}, + {"43543.503206018519", "[$-64]mmmm dd yyyy h:mm AM/PM ddd", "Marso 19 2019 12:04 PM Mar"}, + {"43543.503206018519", "[$-64]mmmmm dd yyyy h:mm AM/PM dddd", "03 19 2019 12:04 PM Martes"}, + {"43543.503206018519", "[$-464]mmm dd yyyy h:mm AM/PM ddd", "Mar 19 2019 12:04 PM Mar"}, + {"43543.503206018519", "[$-464]mmmm dd yyyy h:mm AM/PM ddd", "Marso 19 2019 12:04 PM Mar"}, + {"43543.503206018519", "[$-464]mmmmm dd yyyy h:mm AM/PM dddd", "03 19 2019 12:04 PM Martes"}, + {"43543.503206018519", "[$-B]mmm dd yyyy h:mm AM/PM aaa", "maalis 19 2019 12:04 ip. ti"}, + {"43543.503206018519", "[$-B]mmmm dd yyyy h:mm AM/PM ddd", "maaliskuu 19 2019 12:04 ip. ti"}, + {"43543.503206018519", "[$-B]mmmmm dd yyyy h:mm AM/PM dddd", "03 19 2019 12:04 ip. tiistai"}, + {"43543.503206018519", "[$-40B]mmm dd yyyy h:mm AM/PM aaa", "maalis 19 2019 12:04 ip. ti"}, + {"43543.503206018519", "[$-40B]mmmm dd yyyy h:mm AM/PM ddd", "maaliskuu 19 2019 12:04 ip. ti"}, + {"43543.503206018519", "[$-40B]mmmmm dd yyyy h:mm AM/PM dddd", "03 19 2019 12:04 ip. tiistai"}, {"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 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", "[$-C]mmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 PM mar."}, + {"43543.503206018519", "[$-C]mmmm dd yyyy h:mm AM/PM ddd", "mars 19 2019 12:04 PM mar."}, + {"43543.503206018519", "[$-C]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 PM mardi"}, {"44562.189571759256", "[$-80C]mmm dd yyyy h:mm AM/PM", "janv. 01 2022 4:32 AM"}, {"44562.189571759256", "[$-80C]mmmm dd yyyy h:mm AM/PM", "janvier 01 2022 4:32 AM"}, {"44562.189571759256", "[$-80C]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"}, - {"43543.503206018519", "[$-80C]mmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-80C]mmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-80C]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-80C]mmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 PM mar."}, + {"43543.503206018519", "[$-80C]mmmm dd yyyy h:mm AM/PM ddd", "mars 19 2019 12:04 PM mar."}, + {"43543.503206018519", "[$-80C]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 PM mardi"}, {"44562.189571759256", "[$-2c0C]mmm dd yyyy h:mm AM/PM", "janv. 01 2022 4:32 mat."}, {"44562.189571759256", "[$-2c0C]mmmm dd yyyy h:mm AM/PM", "janvier 01 2022 4:32 mat."}, {"44562.189571759256", "[$-2c0C]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 mat."}, - {"43543.503206018519", "[$-2c0C]mmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 soir"}, - {"43543.503206018519", "[$-2c0C]mmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 soir"}, - {"43543.503206018519", "[$-2c0C]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 soir"}, + {"43543.503206018519", "[$-2c0C]mmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 soir mar."}, + {"43543.503206018519", "[$-2c0C]mmmm dd yyyy h:mm AM/PM ddd", "mars 19 2019 12:04 soir mar."}, + {"43543.503206018519", "[$-2c0C]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 soir mardi"}, {"44562.189571759256", "[$-c0C]mmm dd yyyy h:mm AM/PM", "janv. 01 2022 4:32 AM"}, {"44562.189571759256", "[$-c0C]mmmm dd yyyy h:mm AM/PM", "janvier 01 2022 4:32 AM"}, {"44562.189571759256", "[$-c0C]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"}, - {"43543.503206018519", "[$-c0C]mmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-c0C]mmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-c0C]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-c0C]mmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 PM mar."}, + {"43543.503206018519", "[$-c0C]mmmm dd yyyy h:mm AM/PM ddd", "mars 19 2019 12:04 PM mar."}, + {"43543.503206018519", "[$-c0C]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 PM mardi"}, {"44562.189571759256", "[$-1C0C]mmm dd yyyy h:mm AM/PM", "Janv. 01 2022 4:32 AM"}, {"44562.189571759256", "[$-1C0C]mmmm dd yyyy h:mm AM/PM", "Janvier 01 2022 4:32 AM"}, {"44562.189571759256", "[$-1C0C]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"}, - {"43543.503206018519", "[$-1C0C]mmm dd yyyy h:mm AM/PM", "Mars 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-1C0C]mmmm dd yyyy h:mm AM/PM", "Mars 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-1C0C]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-1C0C]mmm dd yyyy h:mm AM/PM aaa", "Mars 19 2019 12:04 PM mar."}, + {"43543.503206018519", "[$-1C0C]mmmm dd yyyy h:mm AM/PM ddd", "Mars 19 2019 12:04 PM mar."}, + {"43543.503206018519", "[$-1C0C]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 PM mardi"}, {"44562.189571759256", "[$-240C]mmm dd yyyy h:mm AM/PM", "janv. 01 2022 4:32 AM"}, {"44562.189571759256", "[$-240C]mmmm dd yyyy h:mm AM/PM", "janvier 01 2022 4:32 AM"}, {"44562.189571759256", "[$-240C]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"}, - {"43543.503206018519", "[$-240C]mmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-240C]mmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-240C]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-240C]mmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 PM mar."}, + {"43543.503206018519", "[$-240C]mmmm dd yyyy h:mm AM/PM ddd", "mars 19 2019 12:04 PM mar."}, + {"43543.503206018519", "[$-240C]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 PM mardi"}, {"44562.189571759256", "[$-300C]mmm dd yyyy h:mm AM/PM", "janv. 01 2022 4:32 AM"}, {"44562.189571759256", "[$-300C]mmmm dd yyyy h:mm AM/PM", "janvier 01 2022 4:32 AM"}, {"44562.189571759256", "[$-300C]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"}, - {"43543.503206018519", "[$-300C]mmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-300C]mmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-300C]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-300C]mmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 PM mar."}, + {"43543.503206018519", "[$-300C]mmmm dd yyyy h:mm AM/PM ddd", "mars 19 2019 12:04 PM mar."}, + {"43543.503206018519", "[$-300C]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 PM mardi"}, {"44562.189571759256", "[$-40C]mmm dd yyyy h:mm AM/PM", "janv. 01 2022 4:32 AM"}, {"44562.189571759256", "[$-40C]mmmm dd yyyy h:mm AM/PM", "janvier 01 2022 4:32 AM"}, {"44562.189571759256", "[$-40C]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"}, - {"43543.503206018519", "[$-40C]mmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-40C]mmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-40C]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-40C]mmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 PM mar."}, + {"43543.503206018519", "[$-40C]mmmm dd yyyy h:mm AM/PM ddd", "mars 19 2019 12:04 PM mar."}, + {"43543.503206018519", "[$-40C]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 PM mardi"}, {"44562.189571759256", "[$-3c0C]mmm dd yyyy h:mm AM/PM", "janv. 01 2022 4:32 AM"}, {"44562.189571759256", "[$-3c0C]mmmm dd yyyy h:mm AM/PM", "janvier 01 2022 4:32 AM"}, {"44562.189571759256", "[$-3c0C]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"}, - {"43543.503206018519", "[$-3c0C]mmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-3c0C]mmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-3c0C]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-3c0C]mmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 PM mar."}, + {"43543.503206018519", "[$-3c0C]mmmm dd yyyy h:mm AM/PM ddd", "mars 19 2019 12:04 PM mar."}, + {"43543.503206018519", "[$-3c0C]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 PM mardi"}, {"44562.189571759256", "[$-140C]mmm dd yyyy h:mm AM/PM", "janv. 01 2022 4:32 AM"}, {"44562.189571759256", "[$-140C]mmmm dd yyyy h:mm AM/PM", "janvier 01 2022 4:32 AM"}, {"44562.189571759256", "[$-140C]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"}, - {"43543.503206018519", "[$-140C]mmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-140C]mmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-140C]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-140C]mmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 PM mar."}, + {"43543.503206018519", "[$-140C]mmmm dd yyyy h:mm AM/PM ddd", "mars 19 2019 12:04 PM mar."}, + {"43543.503206018519", "[$-140C]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 PM mardi"}, {"44562.189571759256", "[$-340C]mmm dd yyyy h:mm AM/PM", "janv. 01 2022 4:32 AM"}, {"44562.189571759256", "[$-340C]mmmm dd yyyy h:mm AM/PM", "janvier 01 2022 4:32 AM"}, {"44562.189571759256", "[$-340C]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"}, - {"43543.503206018519", "[$-340C]mmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-340C]mmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-340C]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-340C]mmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 PM mar."}, + {"43543.503206018519", "[$-340C]mmmm dd yyyy h:mm AM/PM ddd", "mars 19 2019 12:04 PM mar."}, + {"43543.503206018519", "[$-340C]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 PM mardi"}, {"44562.189571759256", "[$-380C]mmm dd yyyy h:mm AM/PM", "jan. 01 2022 4:32 AM"}, {"44562.189571759256", "[$-380C]mmmm dd yyyy h:mm AM/PM", "janvier 01 2022 4:32 AM"}, {"44562.189571759256", "[$-380C]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"}, - {"43543.503206018519", "[$-380C]mmm dd yyyy h:mm AM/PM", "mar. 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-380C]mmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-380C]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-380C]mmm dd yyyy h:mm AM/PM aaa", "mar. 19 2019 12:04 PM mar."}, + {"43543.503206018519", "[$-380C]mmmm dd yyyy h:mm AM/PM ddd", "mars 19 2019 12:04 PM mar."}, + {"43543.503206018519", "[$-380C]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 PM mardi"}, {"44562.189571759256", "[$-180C]mmm dd yyyy h:mm AM/PM", "janv. 01 2022 4:32 AM"}, {"44562.189571759256", "[$-180C]mmmm dd yyyy h:mm AM/PM", "janvier 01 2022 4:32 AM"}, {"44562.189571759256", "[$-180C]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"}, - {"43543.503206018519", "[$-180C]mmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-180C]mmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-180C]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-180C]mmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 PM mar."}, + {"43543.503206018519", "[$-180C]mmmm dd yyyy h:mm AM/PM ddd", "mars 19 2019 12:04 PM mar."}, + {"43543.503206018519", "[$-180C]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 PM mardi"}, {"44562.189571759256", "[$-200C]mmm dd yyyy h:mm AM/PM", "janv. 01 2022 4:32 AM"}, {"44562.189571759256", "[$-200C]mmmm dd yyyy h:mm AM/PM", "janvier 01 2022 4:32 AM"}, {"44562.189571759256", "[$-200C]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"}, - {"43543.503206018519", "[$-200C]mmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-200C]mmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-200C]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-200C]mmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 PM mar."}, + {"43543.503206018519", "[$-200C]mmmm dd yyyy h:mm AM/PM ddd", "mars 19 2019 12:04 PM mar."}, + {"43543.503206018519", "[$-200C]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 PM mardi"}, {"44562.189571759256", "[$-280C]mmm dd yyyy h:mm AM/PM", "janv. 01 2022 4:32 AM"}, {"44562.189571759256", "[$-280C]mmmm dd yyyy h:mm AM/PM", "janvier 01 2022 4:32 AM"}, {"44562.189571759256", "[$-280C]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"}, - {"43543.503206018519", "[$-280C]mmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-280C]mmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-280C]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-280C]mmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 PM mar."}, + {"43543.503206018519", "[$-280C]mmmm dd yyyy h:mm AM/PM ddd", "mars 19 2019 12:04 PM mar."}, + {"43543.503206018519", "[$-280C]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 PM mardi"}, {"44562.189571759256", "[$-62]m dd yyyy h:mm AM/PM", "1 01 2022 4:32 AM"}, {"44562.189571759256", "[$-62]mm dd yyyy h:mm AM/PM", "01 01 2022 4:32 AM"}, {"44562.189571759256", "[$-62]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 AM"}, @@ -294,9 +769,9 @@ func TestNumFmt(t *testing.T) { {"43543.503206018519", "[$-62]m dd yyyy h:mm AM/PM", "3 19 2019 12:04 PM"}, {"43543.503206018519", "[$-62]mm dd yyyy h:mm AM/PM", "03 19 2019 12:04 PM"}, {"43543.503206018519", "[$-62]mmm dd yyyy h:mm AM/PM", "Mrt 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-62]mmmm dd yyyy h:mm AM/PM", "Maart 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-62]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-62]mmmmmm dd yyyy h:mm AM/PM", "Maart 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-62]mmmm dd yyyy h:mm AM/PM aaa", "Maart 19 2019 12:04 PM tii"}, + {"43543.503206018519", "[$-62]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM tii"}, + {"43543.503206018519", "[$-62]mmmmmm dd yyyy h:mm AM/PM dddd", "Maart 19 2019 12:04 PM tiisdei"}, {"44562.189571759256", "[$-462]m dd yyyy h:mm AM/PM", "1 01 2022 4:32 AM"}, {"44562.189571759256", "[$-462]mm dd yyyy h:mm AM/PM", "01 01 2022 4:32 AM"}, {"44562.189571759256", "[$-462]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 AM"}, @@ -306,41 +781,41 @@ func TestNumFmt(t *testing.T) { {"43543.503206018519", "[$-462]m dd yyyy h:mm AM/PM", "3 19 2019 12:04 PM"}, {"43543.503206018519", "[$-462]mm dd yyyy h:mm AM/PM", "03 19 2019 12:04 PM"}, {"43543.503206018519", "[$-462]mmm dd yyyy h:mm AM/PM", "Mrt 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-462]mmmm dd yyyy h:mm AM/PM", "Maart 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-462]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-462]mmmmmm dd yyyy h:mm AM/PM", "Maart 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-462]mmmm dd yyyy h:mm AM/PM aaa", "Maart 19 2019 12:04 PM tii"}, + {"43543.503206018519", "[$-462]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM tii"}, + {"43543.503206018519", "[$-462]mmmmmm dd yyyy h:mm AM/PM dddd", "Maart 19 2019 12:04 PM tiisdei"}, {"44562.189571759256", "[$-67]mmm dd yyyy h:mm AM/PM", "sii 01 2022 4:32 AM"}, {"44562.189571759256", "[$-67]mmmm dd yyyy h:mm AM/PM", "siilo 01 2022 4:32 AM"}, {"44562.189571759256", "[$-67]mmmmm dd yyyy h:mm AM/PM", "s 01 2022 4:32 AM"}, {"44562.189571759256", "[$-67]mmmmmm dd yyyy h:mm AM/PM", "siilo 01 2022 4:32 AM"}, {"43543.503206018519", "[$-67]mmm dd yyyy h:mm AM/PM", "mbo 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-67]mmmm dd yyyy h:mm AM/PM", "mbooy 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-67]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-67]mmmmmm dd yyyy h:mm AM/PM", "mbooy 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-67]mmmm dd yyyy h:mm AM/PM aaa", "mbooy 19 2019 12:04 PM maw"}, + {"43543.503206018519", "[$-67]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM maw"}, + {"43543.503206018519", "[$-67]mmmmmm dd yyyy h:mm AM/PM dddd", "mbooy 19 2019 12:04 PM mawbaare"}, {"44562.189571759256", "[$-7C67]mmm dd yyyy h:mm AM/PM", "sii 01 2022 4:32 AM"}, {"44562.189571759256", "[$-7C67]mmmm dd yyyy h:mm AM/PM", "siilo 01 2022 4:32 AM"}, {"44562.189571759256", "[$-7C67]mmmmm dd yyyy h:mm AM/PM", "s 01 2022 4:32 AM"}, {"44562.189571759256", "[$-7C67]mmmmmm dd yyyy h:mm AM/PM", "siilo 01 2022 4:32 AM"}, {"43543.503206018519", "[$-7C67]mmm dd yyyy h:mm AM/PM", "mbo 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-7C67]mmmm dd yyyy h:mm AM/PM", "mbooy 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-7C67]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-7C67]mmmmmm dd yyyy h:mm AM/PM", "mbooy 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-7C67]mmmm dd yyyy h:mm AM/PM aaa", "mbooy 19 2019 12:04 PM maw"}, + {"43543.503206018519", "[$-7C67]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM maw"}, + {"43543.503206018519", "[$-7C67]mmmmmm dd yyyy h:mm AM/PM dddd", "mbooy 19 2019 12:04 PM mawbaare"}, {"44562.189571759256", "[$-467]mmm dd yyyy h:mm AM/PM", "samw 01 2022 4:32 subaka"}, {"44562.189571759256", "[$-467]mmmm dd yyyy h:mm AM/PM", "samwiee 01 2022 4:32 subaka"}, {"44562.189571759256", "[$-467]mmmmm dd yyyy h:mm AM/PM", "s 01 2022 4:32 subaka"}, {"44562.189571759256", "[$-467]mmmmmm dd yyyy h:mm AM/PM", "samwiee 01 2022 4:32 subaka"}, {"43543.503206018519", "[$-467]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 kikiiɗe"}, - {"43543.503206018519", "[$-467]mmmm dd yyyy h:mm AM/PM", "marsa 19 2019 12:04 kikiiɗe"}, - {"43543.503206018519", "[$-467]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 kikiiɗe"}, - {"43543.503206018519", "[$-467]mmmmmm dd yyyy h:mm AM/PM", "marsa 19 2019 12:04 kikiiɗe"}, + {"43543.503206018519", "[$-467]mmmm dd yyyy h:mm AM/PM aaa", "marsa 19 2019 12:04 kikiiɗe tal."}, + {"43543.503206018519", "[$-467]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 kikiiɗe tal."}, + {"43543.503206018519", "[$-467]mmmmmm dd yyyy h:mm AM/PM dddd", "marsa 19 2019 12:04 kikiiɗe talaata"}, {"44562.189571759256", "[$-867]mmm dd yyyy h:mm AM/PM", "samw 01 2022 4:32 AM"}, {"44562.189571759256", "[$-867]mmmm dd yyyy h:mm AM/PM", "samwiee 01 2022 4:32 AM"}, {"44562.189571759256", "[$-867]mmmmm dd yyyy h:mm AM/PM", "s 01 2022 4:32 AM"}, {"44562.189571759256", "[$-867]mmmmmm dd yyyy h:mm AM/PM", "samwiee 01 2022 4:32 AM"}, {"43543.503206018519", "[$-867]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-867]mmmm dd yyyy h:mm AM/PM", "marsa 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-867]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-867]mmmmmm dd yyyy h:mm AM/PM", "marsa 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-867]mmmm dd yyyy h:mm AM/PM aaa", "marsa 19 2019 12:04 PM tal."}, + {"43543.503206018519", "[$-867]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM tal."}, + {"43543.503206018519", "[$-867]mmmmmm dd yyyy h:mm AM/PM dddd", "marsa 19 2019 12:04 PM talaata"}, {"44562.189571759256", "[$-56]mmm dd yyyy h:mm AM/PM", "Xan. 01 2022 4:32 a.m."}, {"44562.189571759256", "[$-56]mmmm dd yyyy h:mm AM/PM", "Xaneiro 01 2022 4:32 a.m."}, {"44562.189571759256", "[$-56]mmmmm dd yyyy h:mm AM/PM", "X 01 2022 4:32 a.m."}, @@ -353,273 +828,284 @@ func TestNumFmt(t *testing.T) { {"44562.189571759256", "[$-56]mmmm dd yyyy h:mm AM/PM", "Xaneiro 01 2022 4:32 a.m."}, {"44562.189571759256", "[$-56]mmmmm dd yyyy h:mm AM/PM", "X 01 2022 4:32 a.m."}, {"44562.189571759256", "[$-56]mmmmmm dd yyyy h:mm AM/PM", "Xaneiro 01 2022 4:32 a.m."}, - {"43543.503206018519", "[$-56]mmm dd yyyy h:mm AM/PM", "Mar. 19 2019 12:04 p.m."}, - {"43543.503206018519", "[$-56]mmmm dd yyyy h:mm AM/PM", "Marzo 19 2019 12:04 p.m."}, - {"43543.503206018519", "[$-56]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-56]mmm dd yyyy h:mm AM/PM aaa", "Mar. 19 2019 12:04 p.m. mar."}, + {"43543.503206018519", "[$-56]mmmm dd yyyy h:mm AM/PM ddd", "Marzo 19 2019 12:04 p.m. mar."}, + {"43543.503206018519", "[$-56]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 p.m. martes"}, {"43543.503206018519", "[$-56]mmmmmm dd yyyy h:mm AM/PM", "Marzo 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-456]mmm dd yyyy h:mm AM/PM aaa", "Mar. 19 2019 12:04 p.m. mar."}, + {"43543.503206018519", "[$-456]mmmm dd yyyy h:mm AM/PM ddd", "Marzo 19 2019 12:04 p.m. mar."}, + {"43543.503206018519", "[$-456]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 p.m. martes"}, {"44562.189571759256", "[$-37]mmm dd yyyy h:mm AM/PM", "\u10D8\u10D0\u10DC 01 2022 4:32 AM"}, {"44562.189571759256", "[$-37]mmmm dd yyyy h:mm AM/PM", "\u10D8\u10D0\u10DC\u10D5\u10D0\u10E0\u10D8 01 2022 4:32 AM"}, {"44562.189571759256", "[$-37]mmmmm dd yyyy h:mm AM/PM", "\u10D8 01 2022 4:32 AM"}, {"44562.189571759256", "[$-37]mmmmmm dd yyyy h:mm AM/PM", "\u10D8\u10D0\u10DC\u10D5\u10D0\u10E0\u10D8 01 2022 4:32 AM"}, {"43543.503206018519", "[$-37]mmm dd yyyy h:mm AM/PM", "\u10DB\u10D0\u10E0 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-37]mmmm dd yyyy h:mm AM/PM", "\u10DB\u10D0\u10E0\u10E2\u10D8 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-37]mmmmm dd yyyy h:mm AM/PM", "\u10DB 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-37]mmmmmm dd yyyy h:mm AM/PM", "\u10DB\u10D0\u10E0\u10E2\u10D8 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-37]mmmm dd yyyy h:mm AM/PM aaa", "\u10DB\u10D0\u10E0\u10E2\u10D8 19 2019 12:04 PM \u10E1\u10D0\u10DB\u10E8."}, + {"43543.503206018519", "[$-37]mmmmm dd yyyy h:mm AM/PM ddd", "\u10DB 19 2019 12:04 PM \u10E1\u10D0\u10DB\u10E8."}, + {"43543.503206018519", "[$-37]mmmmmm dd yyyy h:mm AM/PM dddd", "\u10DB\u10D0\u10E0\u10E2\u10D8 19 2019 12:04 PM \u10E1\u10D0\u10DB\u10E8\u10D0\u10D1\u10D0\u10D7\u10D8"}, {"44562.189571759256", "[$-437]mmm dd yyyy h:mm AM/PM", "\u10D8\u10D0\u10DC 01 2022 4:32 AM"}, {"44562.189571759256", "[$-437]mmmm dd yyyy h:mm AM/PM", "\u10D8\u10D0\u10DC\u10D5\u10D0\u10E0\u10D8 01 2022 4:32 AM"}, {"44562.189571759256", "[$-437]mmmmm dd yyyy h:mm AM/PM", "\u10D8 01 2022 4:32 AM"}, {"44562.189571759256", "[$-437]mmmmmm dd yyyy h:mm AM/PM", "\u10D8\u10D0\u10DC\u10D5\u10D0\u10E0\u10D8 01 2022 4:32 AM"}, {"43543.503206018519", "[$-437]mmm dd yyyy h:mm AM/PM", "\u10DB\u10D0\u10E0 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-437]mmmm dd yyyy h:mm AM/PM", "\u10DB\u10D0\u10E0\u10E2\u10D8 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-437]mmmmm dd yyyy h:mm AM/PM", "\u10DB 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-437]mmmmmm dd yyyy h:mm AM/PM", "\u10DB\u10D0\u10E0\u10E2\u10D8 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 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"}, - {"43543.503206018519", "[$-1407]mmm dd yyyy h:mm AM/PM", "Mär 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-1407]mmmm dd yyyy h:mm AM/PM", "März 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-1407]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-807]mmm dd yyyy h:mm AM/PM", "Mär 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-807]mmmm dd yyyy h:mm AM/PM", "März 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-807]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-437]mmmm dd yyyy h:mm AM/PM aaa", "\u10DB\u10D0\u10E0\u10E2\u10D8 19 2019 12:04 PM \u10E1\u10D0\u10DB\u10E8."}, + {"43543.503206018519", "[$-437]mmmmm dd yyyy h:mm AM/PM ddd", "\u10DB 19 2019 12:04 PM \u10E1\u10D0\u10DB\u10E8."}, + {"43543.503206018519", "[$-437]mmmmmm dd yyyy h:mm AM/PM dddd", "\u10DB\u10D0\u10E0\u10E2\u10D8 19 2019 12:04 PM \u10E1\u10D0\u10DB\u10E8\u10D0\u10D1\u10D0\u10D7\u10D8"}, + {"43543.503206018519", "[$-7]mmm dd yyyy h:mm AM/PM aaa", "Mär 19 2019 12:04 PM Di"}, + {"43543.503206018519", "[$-7]mmmm dd yyyy h:mm AM/PM ddd", "März 19 2019 12:04 PM Di"}, + {"43543.503206018519", "[$-7]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 PM Dienstag"}, + {"44562.189571759256", "[$-C07]mmm dd yyyy h:mm AM/PM aaa", "Jän 01 2022 4:32 AM Sa"}, + {"44562.189571759256", "[$-C07]mmmm dd yyyy h:mm AM/PM ddd", "Jänner 01 2022 4:32 AM Sa"}, + {"44562.189571759256", "[$-C07]mmmmm dd yyyy h:mm AM/PM dddd", "J 01 2022 4:32 AM Samstag"}, + {"43543.503206018519", "[$-407]mmm dd yyyy h:mm AM/PM aaa", "Mär 19 2019 12:04 PM Di"}, + {"43543.503206018519", "[$-407]mmmm dd yyyy h:mm AM/PM ddd", "März 19 2019 12:04 PM Di"}, + {"43543.503206018519", "[$-407]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 PM Dienstag"}, + {"43543.503206018519", "[$-1407]mmm dd yyyy h:mm AM/PM aaa", "Mär 19 2019 12:04 PM Di"}, + {"43543.503206018519", "[$-1407]mmmm dd yyyy h:mm AM/PM ddd", "März 19 2019 12:04 PM Di"}, + {"43543.503206018519", "[$-1407]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 PM Dienstag"}, + {"43543.503206018519", "[$-807]mmm dd yyyy h:mm AM/PM aaa", "Mär 19 2019 12:04 PM Di"}, + {"43543.503206018519", "[$-807]mmmm dd yyyy h:mm AM/PM ddd", "März 19 2019 12:04 PM Di"}, + {"43543.503206018519", "[$-807]mmmmm dd yyyy h:mm AM/PM dddd", "M 19 2019 12:04 PM Dienstag"}, {"44562.189571759256", "[$-8]mmm dd yyyy h:mm AM/PM", "\u0399\u03B1\u03BD 01 2022 4:32 \u03C0\u03BC"}, {"44562.189571759256", "[$-8]mmmm dd yyyy h:mm AM/PM", "\u0399\u03B1\u03BD\u03BF\u03C5\u03AC\u03C1\u03B9\u03BF\u03C2 01 2022 4:32 \u03C0\u03BC"}, {"44562.189571759256", "[$-8]mmmmm dd yyyy h:mm AM/PM", "\u0399 01 2022 4:32 \u03C0\u03BC"}, {"44562.189571759256", "[$-8]mmmmmm dd yyyy h:mm AM/PM", "\u0399\u03B1\u03BD\u03BF\u03C5\u03AC\u03C1\u03B9\u03BF\u03C2 01 2022 4:32 \u03C0\u03BC"}, {"43543.503206018519", "[$-8]mmm dd yyyy h:mm AM/PM", "\u039C\u03B1\u03C1 19 2019 12:04 \u03BC\u03BC"}, - {"43543.503206018519", "[$-8]mmmm dd yyyy h:mm AM/PM", "\u039C\u03AC\u03C1\u03C4\u03B9\u03BF\u03C2 19 2019 12:04 \u03BC\u03BC"}, - {"43543.503206018519", "[$-8]mmmmm dd yyyy h:mm AM/PM", "\u039C 19 2019 12:04 \u03BC\u03BC"}, - {"43543.503206018519", "[$-8]mmmmmm dd yyyy h:mm AM/PM", "\u039C\u03AC\u03C1\u03C4\u03B9\u03BF\u03C2 19 2019 12:04 \u03BC\u03BC"}, + {"43543.503206018519", "[$-8]mmmm dd yyyy h:mm AM/PM aaa", "\u039C\u03AC\u03C1\u03C4\u03B9\u03BF\u03C2 19 2019 12:04 \u03BC\u03BC \u03A4\u03C1\u03B9"}, + {"43543.503206018519", "[$-8]mmmmm dd yyyy h:mm AM/PM ddd", "\u039C 19 2019 12:04 \u03BC\u03BC \u03A4\u03C1\u03B9"}, + {"43543.503206018519", "[$-8]mmmmmm dd yyyy h:mm AM/PM dddd", "\u039C\u03AC\u03C1\u03C4\u03B9\u03BF\u03C2 19 2019 12:04 \u03BC\u03BC \u03A4\u03C1\u03AF\u03C4\u03B7"}, {"44562.189571759256", "[$-408]mmm dd yyyy h:mm AM/PM", "\u0399\u03B1\u03BD 01 2022 4:32 \u03C0\u03BC"}, {"44562.189571759256", "[$-408]mmmm dd yyyy h:mm AM/PM", "\u0399\u03B1\u03BD\u03BF\u03C5\u03AC\u03C1\u03B9\u03BF\u03C2 01 2022 4:32 \u03C0\u03BC"}, {"44562.189571759256", "[$-408]mmmmm dd yyyy h:mm AM/PM", "\u0399 01 2022 4:32 \u03C0\u03BC"}, {"44562.189571759256", "[$-408]mmmmmm dd yyyy h:mm AM/PM", "\u0399\u03B1\u03BD\u03BF\u03C5\u03AC\u03C1\u03B9\u03BF\u03C2 01 2022 4:32 \u03C0\u03BC"}, {"43543.503206018519", "[$-408]mmm dd yyyy h:mm AM/PM", "\u039C\u03B1\u03C1 19 2019 12:04 \u03BC\u03BC"}, - {"43543.503206018519", "[$-408]mmmm dd yyyy h:mm AM/PM", "\u039C\u03AC\u03C1\u03C4\u03B9\u03BF\u03C2 19 2019 12:04 \u03BC\u03BC"}, - {"43543.503206018519", "[$-408]mmmmm dd yyyy h:mm AM/PM", "\u039C 19 2019 12:04 \u03BC\u03BC"}, - {"43543.503206018519", "[$-408]mmmmmm dd yyyy h:mm AM/PM", "\u039C\u03AC\u03C1\u03C4\u03B9\u03BF\u03C2 19 2019 12:04 \u03BC\u03BC"}, + {"43543.503206018519", "[$-408]mmmm dd yyyy h:mm AM/PM aaa", "\u039C\u03AC\u03C1\u03C4\u03B9\u03BF\u03C2 19 2019 12:04 \u03BC\u03BC \u03A4\u03C1\u03B9"}, + {"43543.503206018519", "[$-408]mmmmm dd yyyy h:mm AM/PM ddd", "\u039C 19 2019 12:04 \u03BC\u03BC \u03A4\u03C1\u03B9"}, + {"43543.503206018519", "[$-408]mmmmmm dd yyyy h:mm AM/PM dddd", "\u039C\u03AC\u03C1\u03C4\u03B9\u03BF\u03C2 19 2019 12:04 \u03BC\u03BC \u03A4\u03C1\u03AF\u03C4\u03B7"}, {"44562.189571759256", "[$-6F]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 AM"}, {"44562.189571759256", "[$-6F]mmmm dd yyyy h:mm AM/PM", "januaari 01 2022 4:32 AM"}, {"44562.189571759256", "[$-6F]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"}, {"44562.189571759256", "[$-6F]mmmmmm dd yyyy h:mm AM/PM", "januaari 01 2022 4:32 AM"}, {"43543.503206018519", "[$-6F]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-6F]mmmm dd yyyy h:mm AM/PM", "marsi 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-6F]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-6F]mmmmmm dd yyyy h:mm AM/PM", "marsi 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-6F]mmmm dd yyyy h:mm AM/PM aaa", "marsi 19 2019 12:04 PM marl."}, + {"43543.503206018519", "[$-6F]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM marl."}, + {"43543.503206018519", "[$-6F]mmmmmm dd yyyy h:mm AM/PM dddd", "marsi 19 2019 12:04 PM marlunngorneq"}, {"44562.189571759256", "[$-46F]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 AM"}, {"44562.189571759256", "[$-46F]mmmm dd yyyy h:mm AM/PM", "januaari 01 2022 4:32 AM"}, {"44562.189571759256", "[$-46F]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"}, {"44562.189571759256", "[$-46F]mmmmmm dd yyyy h:mm AM/PM", "januaari 01 2022 4:32 AM"}, {"43543.503206018519", "[$-46F]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-46F]mmmm dd yyyy h:mm AM/PM", "marsi 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-46F]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-46F]mmmmmm dd yyyy h:mm AM/PM", "marsi 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-46F]mmmm dd yyyy h:mm AM/PM aaa", "marsi 19 2019 12:04 PM marl."}, + {"43543.503206018519", "[$-46F]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM marl."}, + {"43543.503206018519", "[$-46F]mmmmmm dd yyyy h:mm AM/PM dddd", "marsi 19 2019 12:04 PM marlunngorneq"}, {"44562.189571759256", "[$-74]mmm dd yyyy h:mm AM/PM", "jteĩ 01 2022 4:32 a.m."}, {"44562.189571759256", "[$-74]mmmm dd yyyy h:mm AM/PM", "jasyteĩ 01 2022 4:32 a.m."}, {"44562.189571759256", "[$-74]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 a.m."}, {"44562.189571759256", "[$-74]mmmmmm dd yyyy h:mm AM/PM", "jasyteĩ 01 2022 4:32 a.m."}, {"43543.503206018519", "[$-74]mmm dd yyyy h:mm AM/PM", "japy 19 2019 12:04 p.m."}, - {"43543.503206018519", "[$-74]mmmm dd yyyy h:mm AM/PM", "jasyapy 19 2019 12:04 p.m."}, - {"43543.503206018519", "[$-74]mmmmm dd yyyy h:mm AM/PM", "j 19 2019 12:04 p.m."}, - {"43543.503206018519", "[$-74]mmmmmm dd yyyy h:mm AM/PM", "jasyapy 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-74]mmmm dd yyyy h:mm AM/PM aaa", "jasyapy 19 2019 12:04 p.m. apy"}, + {"43543.503206018519", "[$-74]mmmmm dd yyyy h:mm AM/PM ddd", "j 19 2019 12:04 p.m. apy"}, + {"43543.503206018519", "[$-74]mmmmmm dd yyyy h:mm AM/PM dddd", "jasyapy 19 2019 12:04 p.m. araapy"}, {"44562.189571759256", "[$-474]mmm dd yyyy h:mm AM/PM", "jteĩ 01 2022 4:32 a.m."}, {"44562.189571759256", "[$-474]mmmm dd yyyy h:mm AM/PM", "jasyteĩ 01 2022 4:32 a.m."}, {"44562.189571759256", "[$-474]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 a.m."}, {"44562.189571759256", "[$-474]mmmmmm dd yyyy h:mm AM/PM", "jasyteĩ 01 2022 4:32 a.m."}, {"43543.503206018519", "[$-474]mmm dd yyyy h:mm AM/PM", "japy 19 2019 12:04 p.m."}, - {"43543.503206018519", "[$-474]mmmm dd yyyy h:mm AM/PM", "jasyapy 19 2019 12:04 p.m."}, - {"43543.503206018519", "[$-474]mmmmm dd yyyy h:mm AM/PM", "j 19 2019 12:04 p.m."}, - {"43543.503206018519", "[$-474]mmmmmm dd yyyy h:mm AM/PM", "jasyapy 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-474]mmmm dd yyyy h:mm AM/PM aaa", "jasyapy 19 2019 12:04 p.m. apy"}, + {"43543.503206018519", "[$-474]mmmmm dd yyyy h:mm AM/PM ddd", "j 19 2019 12:04 p.m. apy"}, + {"43543.503206018519", "[$-474]mmmmmm dd yyyy h:mm AM/PM dddd", "jasyapy 19 2019 12:04 p.m. araapy"}, {"44562.189571759256", "[$-47]mmm dd yyyy h:mm AM/PM", "\u0A9C\u0ABE\u0AA8\u0ACD\u0AAF\u0AC1 01 2022 4:32 \u0AAA\u0AC2\u0AB0\u0ACD\u0AB5 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8"}, {"44562.189571759256", "[$-47]mmmm dd yyyy h:mm AM/PM", "\u0A9C\u0ABE\u0AA8\u0ACD\u0AAF\u0AC1\u0A86\u0AB0\u0AC0 01 2022 4:32 \u0AAA\u0AC2\u0AB0\u0ACD\u0AB5 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8"}, {"44562.189571759256", "[$-47]mmmmm dd yyyy h:mm AM/PM", "\u0A9C 01 2022 4:32 \u0AAA\u0AC2\u0AB0\u0ACD\u0AB5 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8"}, {"44562.189571759256", "[$-47]mmmmmm dd yyyy h:mm AM/PM", "\u0A9C\u0ABE\u0AA8\u0ACD\u0AAF\u0AC1\u0A86\u0AB0\u0AC0 01 2022 4:32 \u0AAA\u0AC2\u0AB0\u0ACD\u0AB5 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8"}, {"43543.503206018519", "[$-47]mmm dd yyyy h:mm AM/PM", "\u0AAE\u0ABE\u0AB0\u0ACD\u0A9A 19 2019 12:04 \u0A89\u0AA4\u0ACD\u0AA4\u0AB0 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8"}, - {"43543.503206018519", "[$-47]mmmm dd yyyy h:mm AM/PM", "\u0AAE\u0ABE\u0AB0\u0ACD\u0A9A 19 2019 12:04 \u0A89\u0AA4\u0ACD\u0AA4\u0AB0 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8"}, - {"43543.503206018519", "[$-47]mmmmm dd yyyy h:mm AM/PM", "\u0AAE 19 2019 12:04 \u0A89\u0AA4\u0ACD\u0AA4\u0AB0 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8"}, - {"43543.503206018519", "[$-47]mmmmmm dd yyyy h:mm AM/PM", "\u0AAE\u0ABE\u0AB0\u0ACD\u0A9A 19 2019 12:04 \u0A89\u0AA4\u0ACD\u0AA4\u0AB0 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8"}, + {"43543.503206018519", "[$-47]mmmm dd yyyy h:mm AM/PM aaa", "\u0AAE\u0ABE\u0AB0\u0ACD\u0A9A 19 2019 12:04 \u0A89\u0AA4\u0ACD\u0AA4\u0AB0 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8 \u0AAE\u0A82\u0A97\u0AB3"}, + {"43543.503206018519", "[$-47]mmmmm dd yyyy h:mm AM/PM ddd", "\u0AAE 19 2019 12:04 \u0A89\u0AA4\u0ACD\u0AA4\u0AB0 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8 \u0AAE\u0A82\u0A97\u0AB3"}, + {"43543.503206018519", "[$-47]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0AAE\u0ABE\u0AB0\u0ACD\u0A9A 19 2019 12:04 \u0A89\u0AA4\u0ACD\u0AA4\u0AB0 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8 \u0AAE\u0A82\u0A97\u0AB3\u0AB5\u0ABE\u0AB0"}, {"44562.189571759256", "[$-447]mmm dd yyyy h:mm AM/PM", "\u0A9C\u0ABE\u0AA8\u0ACD\u0AAF\u0AC1 01 2022 4:32 \u0AAA\u0AC2\u0AB0\u0ACD\u0AB5 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8"}, {"44562.189571759256", "[$-447]mmmm dd yyyy h:mm AM/PM", "\u0A9C\u0ABE\u0AA8\u0ACD\u0AAF\u0AC1\u0A86\u0AB0\u0AC0 01 2022 4:32 \u0AAA\u0AC2\u0AB0\u0ACD\u0AB5 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8"}, {"44562.189571759256", "[$-447]mmmmm dd yyyy h:mm AM/PM", "\u0A9C 01 2022 4:32 \u0AAA\u0AC2\u0AB0\u0ACD\u0AB5 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8"}, {"44562.189571759256", "[$-447]mmmmmm dd yyyy h:mm AM/PM", "\u0A9C\u0ABE\u0AA8\u0ACD\u0AAF\u0AC1\u0A86\u0AB0\u0AC0 01 2022 4:32 \u0AAA\u0AC2\u0AB0\u0ACD\u0AB5 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8"}, {"43543.503206018519", "[$-447]mmm dd yyyy h:mm AM/PM", "\u0AAE\u0ABE\u0AB0\u0ACD\u0A9A 19 2019 12:04 \u0A89\u0AA4\u0ACD\u0AA4\u0AB0 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8"}, - {"43543.503206018519", "[$-447]mmmm dd yyyy h:mm AM/PM", "\u0AAE\u0ABE\u0AB0\u0ACD\u0A9A 19 2019 12:04 \u0A89\u0AA4\u0ACD\u0AA4\u0AB0 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8"}, - {"43543.503206018519", "[$-447]mmmmm dd yyyy h:mm AM/PM", "\u0AAE 19 2019 12:04 \u0A89\u0AA4\u0ACD\u0AA4\u0AB0 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8"}, - {"43543.503206018519", "[$-447]mmmmmm dd yyyy h:mm AM/PM", "\u0AAE\u0ABE\u0AB0\u0ACD\u0A9A 19 2019 12:04 \u0A89\u0AA4\u0ACD\u0AA4\u0AB0 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8"}, + {"43543.503206018519", "[$-447]mmmm dd yyyy h:mm AM/PM aaa", "\u0AAE\u0ABE\u0AB0\u0ACD\u0A9A 19 2019 12:04 \u0A89\u0AA4\u0ACD\u0AA4\u0AB0 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8 \u0AAE\u0A82\u0A97\u0AB3"}, + {"43543.503206018519", "[$-447]mmmmm dd yyyy h:mm AM/PM ddd", "\u0AAE 19 2019 12:04 \u0A89\u0AA4\u0ACD\u0AA4\u0AB0 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8 \u0AAE\u0A82\u0A97\u0AB3"}, + {"43543.503206018519", "[$-447]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0AAE\u0ABE\u0AB0\u0ACD\u0A9A 19 2019 12:04 \u0A89\u0AA4\u0ACD\u0AA4\u0AB0 \u0AAE\u0AA7\u0ACD\u0AAF\u0ABE\u0AB9\u0ACD\u0AA8 \u0AAE\u0A82\u0A97\u0AB3\u0AB5\u0ABE\u0AB0"}, {"44562.189571759256", "[$-68]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"}, {"44562.189571759256", "[$-68]mmmm dd yyyy h:mm AM/PM", "Janairu 01 2022 4:32 AM"}, {"44562.189571759256", "[$-68]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"}, {"44562.189571759256", "[$-68]mmmmmm dd yyyy h:mm AM/PM", "Janairu 01 2022 4:32 AM"}, {"43543.503206018519", "[$-68]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-68]mmmm dd yyyy h:mm AM/PM", "Maris 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-68]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-68]mmmmmm dd yyyy h:mm AM/PM", "Maris 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-68]mmmm dd yyyy h:mm AM/PM aaa", "Maris 19 2019 12:04 PM Tal"}, + {"43543.503206018519", "[$-68]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM Tal"}, + {"43543.503206018519", "[$-68]mmmmmm dd yyyy h:mm AM/PM dddd", "Maris 19 2019 12:04 PM Talata"}, {"44562.189571759256", "[$-7C68]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"}, {"44562.189571759256", "[$-7C68]mmmm dd yyyy h:mm AM/PM", "Janairu 01 2022 4:32 AM"}, {"44562.189571759256", "[$-7C68]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"}, {"44562.189571759256", "[$-7C68]mmmmmm dd yyyy h:mm AM/PM", "Janairu 01 2022 4:32 AM"}, {"43543.503206018519", "[$-7C68]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-7C68]mmmm dd yyyy h:mm AM/PM", "Maris 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-7C68]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-7C68]mmmmmm dd yyyy h:mm AM/PM", "Maris 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-7C68]mmmm dd yyyy h:mm AM/PM aaa", "Maris 19 2019 12:04 PM Tal"}, + {"43543.503206018519", "[$-7C68]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM Tal"}, + {"43543.503206018519", "[$-7C68]mmmmmm dd yyyy h:mm AM/PM dddd", "Maris 19 2019 12:04 PM Talata"}, {"44562.189571759256", "[$-468]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"}, {"44562.189571759256", "[$-468]mmmm dd yyyy h:mm AM/PM", "Janairu 01 2022 4:32 AM"}, {"44562.189571759256", "[$-468]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"}, {"44562.189571759256", "[$-468]mmmmmm dd yyyy h:mm AM/PM", "Janairu 01 2022 4:32 AM"}, {"43543.503206018519", "[$-468]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-468]mmmm dd yyyy h:mm AM/PM", "Maris 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-468]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-468]mmmmmm dd yyyy h:mm AM/PM", "Maris 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-468]mmmm dd yyyy h:mm AM/PM aaa", "Maris 19 2019 12:04 PM Tal"}, + {"43543.503206018519", "[$-468]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM Tal"}, + {"43543.503206018519", "[$-468]mmmmmm dd yyyy h:mm AM/PM dddd", "Maris 19 2019 12:04 PM Talata"}, {"44562.189571759256", "[$-75]mmm dd yyyy h:mm AM/PM", "Ian. 01 2022 4:32 AM"}, {"44562.189571759256", "[$-75]mmmm dd yyyy h:mm AM/PM", "Ianuali 01 2022 4:32 AM"}, {"44562.189571759256", "[$-75]mmmmm dd yyyy h:mm AM/PM", "I 01 2022 4:32 AM"}, {"44562.189571759256", "[$-75]mmmmmm dd yyyy h:mm AM/PM", "Ianuali 01 2022 4:32 AM"}, {"43543.503206018519", "[$-75]mmm dd yyyy h:mm AM/PM", "Mal. 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-75]mmmm dd yyyy h:mm AM/PM", "Malaki 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-75]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-75]mmmmmm dd yyyy h:mm AM/PM", "Malaki 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-75]mmmm dd yyyy h:mm AM/PM aaa", "Malaki 19 2019 12:04 PM P2"}, + {"43543.503206018519", "[$-75]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM P2"}, + {"43543.503206018519", "[$-75]mmmmmm dd yyyy h:mm AM/PM dddd", "Malaki 19 2019 12:04 PM Poʻalua"}, {"44562.189571759256", "[$-475]mmm dd yyyy h:mm AM/PM", "Ian. 01 2022 4:32 AM"}, {"44562.189571759256", "[$-475]mmmm dd yyyy h:mm AM/PM", "Ianuali 01 2022 4:32 AM"}, {"44562.189571759256", "[$-475]mmmmm dd yyyy h:mm AM/PM", "I 01 2022 4:32 AM"}, {"44562.189571759256", "[$-475]mmmmmm dd yyyy h:mm AM/PM", "Ianuali 01 2022 4:32 AM"}, {"43543.503206018519", "[$-475]mmm dd yyyy h:mm AM/PM", "Mal. 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-475]mmmm dd yyyy h:mm AM/PM", "Malaki 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-475]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-475]mmmmmm dd yyyy h:mm AM/PM", "Malaki 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-475]mmmm dd yyyy h:mm AM/PM aaa", "Malaki 19 2019 12:04 PM P2"}, + {"43543.503206018519", "[$-475]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM P2"}, + {"43543.503206018519", "[$-475]mmmmmm dd yyyy h:mm AM/PM dddd", "Malaki 19 2019 12:04 PM Poʻalua"}, {"44562.189571759256", "[$-D]mmm dd yyyy h:mm AM/PM", "\u05D9\u05E0\u05D5 01 2022 4:32 AM"}, {"44562.189571759256", "[$-D]mmmm dd yyyy h:mm AM/PM", "\u05D9\u05E0\u05D5\u05D0\u05E8 01 2022 4:32 AM"}, {"44562.189571759256", "[$-D]mmmmm dd yyyy h:mm AM/PM", "\u05D9 01 2022 4:32 AM"}, {"44562.189571759256", "[$-D]mmmmmm dd yyyy h:mm AM/PM", "\u05D9\u05E0\u05D5\u05D0\u05E8 01 2022 4:32 AM"}, {"43543.503206018519", "[$-D]mmm dd yyyy h:mm AM/PM", "\u05DE\u05E8\u05E5 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-D]mmmm dd yyyy h:mm AM/PM", "\u05DE\u05E8\u05E5 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-D]mmmmm dd yyyy h:mm AM/PM", "\u05DE 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-D]mmmmmm dd yyyy h:mm AM/PM", "\u05DE\u05E8\u05E5 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-D]mmmm dd yyyy h:mm AM/PM aaa", "\u05DE\u05E8\u05E5 19 2019 12:04 PM \u05D9\u05D5\u05DD%A0\u05D2"}, + {"43543.503206018519", "[$-D]mmmmm dd yyyy h:mm AM/PM ddd", "\u05DE 19 2019 12:04 PM \u05D9\u05D5\u05DD%A0\u05D2"}, + {"43543.503206018519", "[$-D]mmmmmm dd yyyy h:mm AM/PM dddd", "\u05DE\u05E8\u05E5 19 2019 12:04 PM \u05D9\u05D5\u05DD%A0\u05E9\u05DC\u05D9\u05E9\u05D9"}, {"44562.189571759256", "[$-40D]mmm dd yyyy h:mm AM/PM", "\u05D9\u05E0\u05D5 01 2022 4:32 AM"}, {"44562.189571759256", "[$-40D]mmmm dd yyyy h:mm AM/PM", "\u05D9\u05E0\u05D5\u05D0\u05E8 01 2022 4:32 AM"}, {"44562.189571759256", "[$-40D]mmmmm dd yyyy h:mm AM/PM", "\u05D9 01 2022 4:32 AM"}, {"44562.189571759256", "[$-40D]mmmmmm dd yyyy h:mm AM/PM", "\u05D9\u05E0\u05D5\u05D0\u05E8 01 2022 4:32 AM"}, {"43543.503206018519", "[$-40D]mmm dd yyyy h:mm AM/PM", "\u05DE\u05E8\u05E5 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-40D]mmmm dd yyyy h:mm AM/PM", "\u05DE\u05E8\u05E5 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-40D]mmmmm dd yyyy h:mm AM/PM", "\u05DE 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-40D]mmmmmm dd yyyy h:mm AM/PM", "\u05DE\u05E8\u05E5 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-40D]mmmm dd yyyy h:mm AM/PM aaa", "\u05DE\u05E8\u05E5 19 2019 12:04 PM \u05D9\u05D5\u05DD%A0\u05D2"}, + {"43543.503206018519", "[$-40D]mmmmm dd yyyy h:mm AM/PM ddd", "\u05DE 19 2019 12:04 PM \u05D9\u05D5\u05DD%A0\u05D2"}, + {"43543.503206018519", "[$-40D]mmmmmm dd yyyy h:mm AM/PM dddd", "\u05DE\u05E8\u05E5 19 2019 12:04 PM \u05D9\u05D5\u05DD%A0\u05E9\u05DC\u05D9\u05E9\u05D9"}, {"44562.189571759256", "[$-39]mmm dd yyyy h:mm AM/PM", "\u091C\u0928\u0935\u0930\u0940 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"}, {"44562.189571759256", "[$-39]mmmm dd yyyy h:mm AM/PM", "\u091C\u0928\u0935\u0930\u0940 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"}, {"44562.189571759256", "[$-39]mmmmm dd yyyy h:mm AM/PM", "\u091C 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"}, {"44562.189571759256", "[$-39]mmmmmm dd yyyy h:mm AM/PM", "\u091C\u0928\u0935\u0930\u0940 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"}, {"43543.503206018519", "[$-39]mmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928"}, - {"43543.503206018519", "[$-39]mmmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928"}, - {"43543.503206018519", "[$-39]mmmmm dd yyyy h:mm AM/PM", "\u092E 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928"}, - {"43543.503206018519", "[$-39]mmmmmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928"}, + {"43543.503206018519", "[$-39]mmmm dd yyyy h:mm AM/PM aaa", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928 \u092E\u0902\u0917\u0932."}, + {"43543.503206018519", "[$-39]mmmmm dd yyyy h:mm AM/PM ddd", "\u092E 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928 \u092E\u0902\u0917\u0932."}, + {"43543.503206018519", "[$-39]mmmmmm dd yyyy h:mm AM/PM dddd", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928 \u092E\u0902\u0917\u0932\u0935\u093E\u0930"}, + {"44562.189571759256", "[$-439]mmm dd yyyy h:mm AM/PM", "\u091C\u0928\u0935\u0930\u0940 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"}, + {"44562.189571759256", "[$-439]mmmm dd yyyy h:mm AM/PM", "\u091C\u0928\u0935\u0930\u0940 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"}, + {"44562.189571759256", "[$-439]mmmmm dd yyyy h:mm AM/PM", "\u091C 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"}, + {"44562.189571759256", "[$-439]mmmmmm dd yyyy h:mm AM/PM", "\u091C\u0928\u0935\u0930\u0940 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"}, + {"43543.503206018519", "[$-439]mmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928"}, + {"43543.503206018519", "[$-439]mmmm dd yyyy h:mm AM/PM aaa", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928 \u092E\u0902\u0917\u0932."}, + {"43543.503206018519", "[$-439]mmmmm dd yyyy h:mm AM/PM ddd", "\u092E 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928 \u092E\u0902\u0917\u0932."}, + {"43543.503206018519", "[$-439]mmmmmm dd yyyy h:mm AM/PM dddd", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928 \u092E\u0902\u0917\u0932\u0935\u093E\u0930"}, {"44562.189571759256", "[$-E]mmm dd yyyy h:mm AM/PM", "jan. 01 2022 4:32 de."}, {"44562.189571759256", "[$-E]mmmm dd yyyy h:mm AM/PM", "január 01 2022 4:32 de."}, {"44562.189571759256", "[$-E]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 de."}, {"44562.189571759256", "[$-E]mmmmmm dd yyyy h:mm AM/PM", "január 01 2022 4:32 de."}, {"43543.503206018519", "[$-E]mmm dd yyyy h:mm AM/PM", "márc. 19 2019 12:04 du."}, - {"43543.503206018519", "[$-E]mmmm dd yyyy h:mm AM/PM", "március 19 2019 12:04 du."}, - {"43543.503206018519", "[$-E]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 du."}, - {"43543.503206018519", "[$-E]mmmmmm dd yyyy h:mm AM/PM", "március 19 2019 12:04 du."}, + {"43543.503206018519", "[$-E]mmmm dd yyyy h:mm AM/PM aaa", "március 19 2019 12:04 du. K"}, + {"43543.503206018519", "[$-E]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 du. K"}, + {"43543.503206018519", "[$-E]mmmmmm dd yyyy h:mm AM/PM dddd", "március 19 2019 12:04 du. kedd"}, {"44562.189571759256", "[$-40E]mmm dd yyyy h:mm AM/PM", "jan. 01 2022 4:32 de."}, {"44562.189571759256", "[$-40E]mmmm dd yyyy h:mm AM/PM", "január 01 2022 4:32 de."}, {"44562.189571759256", "[$-40E]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 de."}, {"44562.189571759256", "[$-40E]mmmmmm dd yyyy h:mm AM/PM", "január 01 2022 4:32 de."}, {"43543.503206018519", "[$-40E]mmm dd yyyy h:mm AM/PM", "márc. 19 2019 12:04 du."}, - {"43543.503206018519", "[$-40E]mmmm dd yyyy h:mm AM/PM", "március 19 2019 12:04 du."}, - {"43543.503206018519", "[$-40E]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 du."}, - {"43543.503206018519", "[$-40E]mmmmmm dd yyyy h:mm AM/PM", "március 19 2019 12:04 du."}, + {"43543.503206018519", "[$-40E]mmmm dd yyyy h:mm AM/PM aaa", "március 19 2019 12:04 du. K"}, + {"43543.503206018519", "[$-40E]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 du. K"}, + {"43543.503206018519", "[$-40E]mmmmmm dd yyyy h:mm AM/PM dddd", "március 19 2019 12:04 du. kedd"}, {"44562.189571759256", "[$-F]mmm dd yyyy h:mm AM/PM", "jan. 01 2022 4:32 f.h."}, {"44562.189571759256", "[$-F]mmmm dd yyyy h:mm AM/PM", "janúar 01 2022 4:32 f.h."}, {"44562.189571759256", "[$-F]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 f.h."}, {"44562.189571759256", "[$-F]mmmmmm dd yyyy h:mm AM/PM", "janúar 01 2022 4:32 f.h."}, {"43543.503206018519", "[$-F]mmm dd yyyy h:mm AM/PM", "mar. 19 2019 12:04 e.h."}, - {"43543.503206018519", "[$-F]mmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 e.h."}, - {"43543.503206018519", "[$-F]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 e.h."}, - {"43543.503206018519", "[$-F]mmmmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 e.h."}, + {"43543.503206018519", "[$-F]mmmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 e.h. þri."}, + {"43543.503206018519", "[$-F]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 e.h. þri."}, + {"43543.503206018519", "[$-F]mmmmmm dd yyyy h:mm AM/PM dddd", "mars 19 2019 12:04 e.h. þriðjudagur"}, {"44562.189571759256", "[$-40F]mmm dd yyyy h:mm AM/PM", "jan. 01 2022 4:32 f.h."}, {"44562.189571759256", "[$-40F]mmmm dd yyyy h:mm AM/PM", "janúar 01 2022 4:32 f.h."}, {"44562.189571759256", "[$-40F]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 f.h."}, {"44562.189571759256", "[$-40F]mmmmmm dd yyyy h:mm AM/PM", "janúar 01 2022 4:32 f.h."}, {"43543.503206018519", "[$-40F]mmm dd yyyy h:mm AM/PM", "mar. 19 2019 12:04 e.h."}, - {"43543.503206018519", "[$-40F]mmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 e.h."}, - {"43543.503206018519", "[$-40F]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 e.h."}, - {"43543.503206018519", "[$-40F]mmmmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 e.h."}, + {"43543.503206018519", "[$-40F]mmmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 e.h. þri."}, + {"43543.503206018519", "[$-40F]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 e.h. þri."}, + {"43543.503206018519", "[$-40F]mmmmmm dd yyyy h:mm AM/PM dddd", "mars 19 2019 12:04 e.h. þriðjudagur"}, {"44562.189571759256", "[$-70]mmm dd yyyy h:mm AM/PM", "Jen 01 2022 4:32 A.M."}, {"44562.189571759256", "[$-70]mmmm dd yyyy h:mm AM/PM", "Jenụwarị 01 2022 4:32 A.M."}, {"44562.189571759256", "[$-70]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 A.M."}, {"44562.189571759256", "[$-70]mmmmmm dd yyyy h:mm AM/PM", "Jenụwarị 01 2022 4:32 A.M."}, {"43543.503206018519", "[$-70]mmm dd yyyy h:mm AM/PM", "Mac 19 2019 12:04 P.M."}, - {"43543.503206018519", "[$-70]mmmm dd yyyy h:mm AM/PM", "Machị 19 2019 12:04 P.M."}, - {"43543.503206018519", "[$-70]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 P.M."}, - {"43543.503206018519", "[$-70]mmmmmm dd yyyy h:mm AM/PM", "Machị 19 2019 12:04 P.M."}, + {"43543.503206018519", "[$-70]mmmm dd yyyy h:mm AM/PM aaa", "Machị 19 2019 12:04 P.M. Tiu"}, + {"43543.503206018519", "[$-70]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 P.M. Tiu"}, + {"43543.503206018519", "[$-70]mmmmmm dd yyyy h:mm AM/PM dddd", "Machị 19 2019 12:04 P.M. Tiuzdee"}, {"44562.189571759256", "[$-470]mmm dd yyyy h:mm AM/PM", "Jen 01 2022 4:32 A.M."}, {"44562.189571759256", "[$-470]mmmm dd yyyy h:mm AM/PM", "Jenụwarị 01 2022 4:32 A.M."}, {"44562.189571759256", "[$-470]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 A.M."}, {"44562.189571759256", "[$-470]mmmmmm dd yyyy h:mm AM/PM", "Jenụwarị 01 2022 4:32 A.M."}, {"43543.503206018519", "[$-470]mmm dd yyyy h:mm AM/PM", "Mac 19 2019 12:04 P.M."}, - {"43543.503206018519", "[$-470]mmmm dd yyyy h:mm AM/PM", "Machị 19 2019 12:04 P.M."}, - {"43543.503206018519", "[$-470]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 P.M."}, - {"43543.503206018519", "[$-470]mmmmmm dd yyyy h:mm AM/PM", "Machị 19 2019 12:04 P.M."}, + {"43543.503206018519", "[$-470]mmmm dd yyyy h:mm AM/PM aaa", "Machị 19 2019 12:04 P.M. Tiu"}, + {"43543.503206018519", "[$-470]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 P.M. Tiu"}, + {"43543.503206018519", "[$-470]mmmmmm dd yyyy h:mm AM/PM dddd", "Machị 19 2019 12:04 P.M. Tiuzdee"}, {"44562.189571759256", "[$-21]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"}, {"44562.189571759256", "[$-21]mmmm dd yyyy h:mm AM/PM", "Januari 01 2022 4:32 AM"}, {"44562.189571759256", "[$-21]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"}, {"44562.189571759256", "[$-21]mmmmmm dd yyyy h:mm AM/PM", "Januari 01 2022 4:32 AM"}, {"43543.503206018519", "[$-21]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-21]mmmm dd yyyy h:mm AM/PM", "Maret 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-21]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-21]mmmmmm dd yyyy h:mm AM/PM", "Maret 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-21]mmmm dd yyyy h:mm AM/PM aaa", "Maret 19 2019 12:04 PM Sel"}, + {"43543.503206018519", "[$-21]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM Sel"}, + {"43543.503206018519", "[$-21]mmmmmm dd yyyy h:mm AM/PM dddd", "Maret 19 2019 12:04 PM Selasa"}, {"44562.189571759256", "[$-421]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"}, {"44562.189571759256", "[$-421]mmmm dd yyyy h:mm AM/PM", "Januari 01 2022 4:32 AM"}, {"44562.189571759256", "[$-421]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"}, {"44562.189571759256", "[$-421]mmmmmm dd yyyy h:mm AM/PM", "Januari 01 2022 4:32 AM"}, {"43543.503206018519", "[$-421]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-421]mmmm dd yyyy h:mm AM/PM", "Maret 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-421]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-421]mmmmmm dd yyyy h:mm AM/PM", "Maret 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-421]mmmm dd yyyy h:mm AM/PM aaa", "Maret 19 2019 12:04 PM Sel"}, + {"43543.503206018519", "[$-421]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM Sel"}, + {"43543.503206018519", "[$-421]mmmmmm dd yyyy h:mm AM/PM dddd", "Maret 19 2019 12:04 PM Selasa"}, {"44562.189571759256", "[$-5D]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"}, {"44562.189571759256", "[$-5D]mmmm dd yyyy h:mm AM/PM", "Jaannuari 01 2022 4:32 AM"}, {"44562.189571759256", "[$-5D]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"}, {"44562.189571759256", "[$-5D]mmmmmm dd yyyy h:mm AM/PM", "Jaannuari 01 2022 4:32 AM"}, {"43543.503206018519", "[$-5D]mmm dd yyyy h:mm AM/PM", "Mas 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-5D]mmmm dd yyyy h:mm AM/PM", "Maatsi 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-5D]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-5D]mmmmmm dd yyyy h:mm AM/PM", "Maatsi 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-5D]mmmm dd yyyy h:mm AM/PM aaa", "Maatsi 19 2019 12:04 PM Aip"}, + {"43543.503206018519", "[$-5D]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM Aip"}, + {"43543.503206018519", "[$-5D]mmmmmm dd yyyy h:mm AM/PM dddd", "Maatsi 19 2019 12:04 PM Aippiq"}, {"44562.189571759256", "[$-7C5D]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"}, {"44562.189571759256", "[$-7C5D]mmmm dd yyyy h:mm AM/PM", "Jaannuari 01 2022 4:32 AM"}, {"44562.189571759256", "[$-7C5D]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"}, {"44562.189571759256", "[$-7C5D]mmmmmm dd yyyy h:mm AM/PM", "Jaannuari 01 2022 4:32 AM"}, {"43543.503206018519", "[$-7C5D]mmm dd yyyy h:mm AM/PM", "Mas 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-7C5D]mmmm dd yyyy h:mm AM/PM", "Maatsi 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-7C5D]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-7C5D]mmmmmm dd yyyy h:mm AM/PM", "Maatsi 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-7C5D]mmmm dd yyyy h:mm AM/PM aaa", "Maatsi 19 2019 12:04 PM Aip"}, + {"43543.503206018519", "[$-7C5D]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM Aip"}, + {"43543.503206018519", "[$-7C5D]mmmmmm dd yyyy h:mm AM/PM dddd", "Maatsi 19 2019 12:04 PM Aippiq"}, {"44562.189571759256", "[$-85D]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"}, {"44562.189571759256", "[$-85D]mmmm dd yyyy h:mm AM/PM", "Jaannuari 01 2022 4:32 AM"}, {"44562.189571759256", "[$-85D]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"}, {"44562.189571759256", "[$-85D]mmmmmm dd yyyy h:mm AM/PM", "Jaannuari 01 2022 4:32 AM"}, {"43543.503206018519", "[$-85D]mmm dd yyyy h:mm AM/PM", "Mas 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-85D]mmmm dd yyyy h:mm AM/PM", "Maatsi 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-85D]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-85D]mmmmmm dd yyyy h:mm AM/PM", "Maatsi 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-85D]mmmm dd yyyy h:mm AM/PM aaa", "Maatsi 19 2019 12:04 PM Aip"}, + {"43543.503206018519", "[$-85D]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM Aip"}, + {"43543.503206018519", "[$-85D]mmmmmm dd yyyy h:mm AM/PM dddd", "Maatsi 19 2019 12:04 PM Aippiq"}, {"44562.189571759256", "[$-785D]mmm dd yyyy h:mm AM/PM", "\u152E\u14D0\u14C4 01 2022 4:32 AM"}, {"44562.189571759256", "[$-785D]mmmm dd yyyy h:mm AM/PM", "\u152E\u14D0\u14C4\u140A\u1546 01 2022 4:32 AM"}, {"44562.189571759256", "[$-785D]mmmmm dd yyyy h:mm AM/PM", "\u152E 01 2022 4:32 AM"}, {"44562.189571759256", "[$-785D]mmmmmm dd yyyy h:mm AM/PM", "\u152E\u14D0\u14C4\u140A\u1546 01 2022 4:32 AM"}, {"43543.503206018519", "[$-785D]mmm dd yyyy h:mm AM/PM", "\u14AB\u1466\u14EF 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-785D]mmmm dd yyyy h:mm AM/PM", "\u14AB\u1466\u14EF 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-785D]mmmmm dd yyyy h:mm AM/PM", "\u14AB 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-785D]mmmmmm dd yyyy h:mm AM/PM", "\u14AB\u1466\u14EF 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-785D]mmmm dd yyyy h:mm AM/PM aaa", "\u14AB\u1466\u14EF 19 2019 12:04 PM \u140A\u1403\u1449\u1431"}, + {"43543.503206018519", "[$-785D]mmmmm dd yyyy h:mm AM/PM ddd", "\u14AB 19 2019 12:04 PM \u140A\u1403\u1449\u1431"}, + {"43543.503206018519", "[$-785D]mmmmmm dd yyyy h:mm AM/PM dddd", "\u14AB\u1466\u14EF 19 2019 12:04 PM \u140A\u1403\u1449\u1431\u1585"}, {"44562.189571759256", "[$-45D]mmm dd yyyy h:mm AM/PM", "\u152E\u14D0\u14C4 01 2022 4:32 AM"}, {"44562.189571759256", "[$-45D]mmmm dd yyyy h:mm AM/PM", "\u152E\u14D0\u14C4\u140A\u1546 01 2022 4:32 AM"}, {"44562.189571759256", "[$-45D]mmmmm dd yyyy h:mm AM/PM", "\u152E 01 2022 4:32 AM"}, {"44562.189571759256", "[$-45D]mmmmmm dd yyyy h:mm AM/PM", "\u152E\u14D0\u14C4\u140A\u1546 01 2022 4:32 AM"}, {"43543.503206018519", "[$-45D]mmm dd yyyy h:mm AM/PM", "\u14AB\u1466\u14EF 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-45D]mmmm dd yyyy h:mm AM/PM", "\u14AB\u1466\u14EF 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-45D]mmmmm dd yyyy h:mm AM/PM", "\u14AB 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-45D]mmmmmm dd yyyy h:mm AM/PM", "\u14AB\u1466\u14EF 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-45D]mmmm dd yyyy h:mm AM/PM aaa", "\u14AB\u1466\u14EF 19 2019 12:04 PM \u140A\u1403\u1449\u1431"}, + {"43543.503206018519", "[$-45D]mmmmm dd yyyy h:mm AM/PM ddd", "\u14AB 19 2019 12:04 PM \u140A\u1403\u1449\u1431"}, + {"43543.503206018519", "[$-45D]mmmmmm dd yyyy h:mm AM/PM dddd", "\u14AB\u1466\u14EF 19 2019 12:04 PM \u140A\u1403\u1449\u1431\u1585"}, {"44562.189571759256", "[$-3C]mmm dd yyyy h:mm AM/PM", "Ean 01 2022 4:32 r.n."}, {"44593.189571759256", "[$-3C]mmm dd yyyy h:mm AM/PM", "Feabh 01 2022 4:32 r.n."}, {"44621.18957170139", "[$-3C]mmm dd yyyy h:mm AM/PM", "Márta 01 2022 4:32 r.n."}, @@ -653,9 +1139,9 @@ func TestNumFmt(t *testing.T) { {"44743.18957170139", "[$-3C]mmmmm dd yyyy h:mm AM/PM", "I 01 2022 4:32 r.n."}, {"44774.18957170139", "[$-3C]mmmmm dd yyyy h:mm AM/PM", "L 01 2022 4:32 r.n."}, {"44805.18957170139", "[$-3C]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 r.n."}, - {"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."}, + {"44835.18957170139", "[$-3C]mmmmm dd yyyy h:mm AM/PM aaa", "D 01 2022 4:32 r.n. Sath"}, + {"44866.18957170139", "[$-3C]mmmmm dd yyyy h:mm AM/PM ddd", "S 01 2022 4:32 r.n. Máirt"}, + {"44896.18957170139", "[$-3C]mmmmm dd yyyy h:mm AM/PM dddd", "N 01 2022 4:32 r.n. Déardaoin"}, {"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."}, @@ -677,395 +1163,398 @@ func TestNumFmt(t *testing.T) { {"44743.18957170139", "[$-83C]mmmm dd yyyy h:mm AM/PM", "Iúil 01 2022 4:32 r.n."}, {"44774.18957170139", "[$-83C]mmmm dd yyyy h:mm AM/PM", "Lúnasa 01 2022 4:32 r.n."}, {"44805.18957170139", "[$-83C]mmmm dd yyyy h:mm AM/PM", "Meán Fómhair 01 2022 4:32 r.n."}, - {"44835.18957170139", "[$-83C]mmmm dd yyyy h:mm AM/PM", "Deireadh Fómhair 01 2022 4:32 r.n."}, - {"44866.18957170139", "[$-83C]mmmm dd yyyy h:mm AM/PM", "Samhain 01 2022 4:32 r.n."}, - {"44896.18957170139", "[$-83C]mmmm dd yyyy h:mm AM/PM", "Nollaig 01 2022 4:32 r.n."}, - {"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", "[$-410]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-410]mmmm dd yyyy h:mm AM/PM", "marzo 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-410]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-810]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-810]mmmm dd yyyy h:mm AM/PM", "marzo 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-810]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 午後"}, + {"44835.18957170139", "[$-83C]mmmm dd yyyy h:mm AM/PM aaa", "Deireadh Fómhair 01 2022 4:32 r.n. Sath"}, + {"44866.18957170139", "[$-83C]mmmm dd yyyy h:mm AM/PM ddd", "Samhain 01 2022 4:32 r.n. Máirt"}, + {"44896.18957170139", "[$-83C]mmmm dd yyyy h:mm AM/PM dddd", "Nollaig 01 2022 4:32 r.n. Déardaoin"}, + {"43543.503206018519", "[$-10]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 PM mar"}, + {"43543.503206018519", "[$-10]mmmm dd yyyy h:mm AM/PM ddd", "marzo 19 2019 12:04 PM mar"}, + {"43543.503206018519", "[$-10]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 PM martedì"}, + {"43543.503206018519", "[$-410]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 PM mar"}, + {"43543.503206018519", "[$-410]mmmm dd yyyy h:mm AM/PM ddd", "marzo 19 2019 12:04 PM mar"}, + {"43543.503206018519", "[$-410]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 PM martedì"}, + {"43543.503206018519", "[$-810]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 PM mar"}, + {"43543.503206018519", "[$-810]mmmm dd yyyy h:mm AM/PM ddd", "marzo 19 2019 12:04 PM mar"}, + {"43543.503206018519", "[$-810]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 PM martedì"}, + {"43543.503206018519", "[$-11]mmm dd yyyy h:mm AM/PM aaa", "3月 19 2019 12:04 午後 火"}, + {"43543.503206018519", "[$-11]mmmm dd yyyy h:mm AM/PM ddd", "3月 19 2019 12:04 午後 火"}, + {"43543.503206018519", "[$-11]mmmmm dd yyyy h:mm AM/PM dddd", "3 19 2019 12:04 午後 火曜日"}, + {"43543.503206018519", "[$-411]mmm dd yyyy h:mm AM/PM aaa", "3月 19 2019 12:04 午後 火"}, + {"43543.503206018519", "[$-411]mmmm dd yyyy h:mm AM/PM ddd", "3月 19 2019 12:04 午後 火"}, + {"43543.503206018519", "[$-411]mmmmm dd yyyy h:mm AM/PM dddd", "3 19 2019 12:04 午後 火曜日"}, {"44562.189571759256", "[$-4B]mmm dd yyyy h:mm AM/PM", "\u0C9C\u0CA8\u0CB5\u0CB0\u0CBF 01 2022 4:32 \u0CAA\u0CC2\u0CB0\u0CCD\u0CB5\u0CBE\u0CB9\u0CCD\u0CA8"}, {"44562.189571759256", "[$-4B]mmmm dd yyyy h:mm AM/PM", "\u0C9C\u0CA8\u0CB5\u0CB0\u0CBF 01 2022 4:32 \u0CAA\u0CC2\u0CB0\u0CCD\u0CB5\u0CBE\u0CB9\u0CCD\u0CA8"}, {"44562.189571759256", "[$-4B]mmmmm dd yyyy h:mm AM/PM", "\u0C9C 01 2022 4:32 \u0CAA\u0CC2\u0CB0\u0CCD\u0CB5\u0CBE\u0CB9\u0CCD\u0CA8"}, {"44562.189571759256", "[$-4B]mmmmmm dd yyyy h:mm AM/PM", "\u0C9C\u0CA8\u0CB5\u0CB0\u0CBF 01 2022 4:32 \u0CAA\u0CC2\u0CB0\u0CCD\u0CB5\u0CBE\u0CB9\u0CCD\u0CA8"}, {"43543.503206018519", "[$-4B]mmm dd yyyy h:mm AM/PM", "\u0CAE\u0CBE\u0CB0\u0CCD\u0C9A\u0CCD 19 2019 12:04 \u0C85\u0CAA\u0CB0\u0CBE\u0CB9\u0CCD\u0CA8"}, - {"43543.503206018519", "[$-4B]mmmm dd yyyy h:mm AM/PM", "\u0CAE\u0CBE\u0CB0\u0CCD\u0C9A\u0CCD 19 2019 12:04 \u0C85\u0CAA\u0CB0\u0CBE\u0CB9\u0CCD\u0CA8"}, - {"43543.503206018519", "[$-4B]mmmmm dd yyyy h:mm AM/PM", "\u0CAE 19 2019 12:04 \u0C85\u0CAA\u0CB0\u0CBE\u0CB9\u0CCD\u0CA8"}, - {"43543.503206018519", "[$-4B]mmmmmm dd yyyy h:mm AM/PM", "\u0CAE\u0CBE\u0CB0\u0CCD\u0C9A\u0CCD 19 2019 12:04 \u0C85\u0CAA\u0CB0\u0CBE\u0CB9\u0CCD\u0CA8"}, + {"43543.503206018519", "[$-4B]mmmm dd yyyy h:mm AM/PM aaa", "\u0CAE\u0CBE\u0CB0\u0CCD\u0C9A\u0CCD 19 2019 12:04 \u0C85\u0CAA\u0CB0\u0CBE\u0CB9\u0CCD\u0CA8 \u0CAE\u0C82\u0C97\u0CB3."}, + {"43543.503206018519", "[$-4B]mmmmm dd yyyy h:mm AM/PM ddd", "\u0CAE 19 2019 12:04 \u0C85\u0CAA\u0CB0\u0CBE\u0CB9\u0CCD\u0CA8 \u0CAE\u0C82\u0C97\u0CB3."}, + {"43543.503206018519", "[$-4B]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0CAE\u0CBE\u0CB0\u0CCD\u0C9A\u0CCD 19 2019 12:04 \u0C85\u0CAA\u0CB0\u0CBE\u0CB9\u0CCD\u0CA8 \u0CAE\u0C82\u0C97\u0CB3\u0CB5\u0CBE\u0CB0"}, {"44562.189571759256", "[$-44B]mmm dd yyyy h:mm AM/PM", "\u0C9C\u0CA8\u0CB5\u0CB0\u0CBF 01 2022 4:32 \u0CAA\u0CC2\u0CB0\u0CCD\u0CB5\u0CBE\u0CB9\u0CCD\u0CA8"}, {"44562.189571759256", "[$-44B]mmmm dd yyyy h:mm AM/PM", "\u0C9C\u0CA8\u0CB5\u0CB0\u0CBF 01 2022 4:32 \u0CAA\u0CC2\u0CB0\u0CCD\u0CB5\u0CBE\u0CB9\u0CCD\u0CA8"}, {"44562.189571759256", "[$-44B]mmmmm dd yyyy h:mm AM/PM", "\u0C9C 01 2022 4:32 \u0CAA\u0CC2\u0CB0\u0CCD\u0CB5\u0CBE\u0CB9\u0CCD\u0CA8"}, {"44562.189571759256", "[$-44B]mmmmmm dd yyyy h:mm AM/PM", "\u0C9C\u0CA8\u0CB5\u0CB0\u0CBF 01 2022 4:32 \u0CAA\u0CC2\u0CB0\u0CCD\u0CB5\u0CBE\u0CB9\u0CCD\u0CA8"}, {"43543.503206018519", "[$-44B]mmm dd yyyy h:mm AM/PM", "\u0CAE\u0CBE\u0CB0\u0CCD\u0C9A\u0CCD 19 2019 12:04 \u0C85\u0CAA\u0CB0\u0CBE\u0CB9\u0CCD\u0CA8"}, - {"43543.503206018519", "[$-44B]mmmm dd yyyy h:mm AM/PM", "\u0CAE\u0CBE\u0CB0\u0CCD\u0C9A\u0CCD 19 2019 12:04 \u0C85\u0CAA\u0CB0\u0CBE\u0CB9\u0CCD\u0CA8"}, - {"43543.503206018519", "[$-44B]mmmmm dd yyyy h:mm AM/PM", "\u0CAE 19 2019 12:04 \u0C85\u0CAA\u0CB0\u0CBE\u0CB9\u0CCD\u0CA8"}, - {"43543.503206018519", "[$-44B]mmmmmm dd yyyy h:mm AM/PM", "\u0CAE\u0CBE\u0CB0\u0CCD\u0C9A\u0CCD 19 2019 12:04 \u0C85\u0CAA\u0CB0\u0CBE\u0CB9\u0CCD\u0CA8"}, + {"43543.503206018519", "[$-44B]mmmm dd yyyy h:mm AM/PM aaa", "\u0CAE\u0CBE\u0CB0\u0CCD\u0C9A\u0CCD 19 2019 12:04 \u0C85\u0CAA\u0CB0\u0CBE\u0CB9\u0CCD\u0CA8 \u0CAE\u0C82\u0C97\u0CB3."}, + {"43543.503206018519", "[$-44B]mmmmm dd yyyy h:mm AM/PM ddd", "\u0CAE 19 2019 12:04 \u0C85\u0CAA\u0CB0\u0CBE\u0CB9\u0CCD\u0CA8 \u0CAE\u0C82\u0C97\u0CB3."}, + {"43543.503206018519", "[$-44B]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0CAE\u0CBE\u0CB0\u0CCD\u0C9A\u0CCD 19 2019 12:04 \u0C85\u0CAA\u0CB0\u0CBE\u0CB9\u0CCD\u0CA8 \u0CAE\u0C82\u0C97\u0CB3\u0CB5\u0CBE\u0CB0"}, {"44562.189571759256", "[$-471]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"}, {"44562.189571759256", "[$-471]mmmm dd yyyy h:mm AM/PM", "January 01 2022 4:32 AM"}, {"44562.189571759256", "[$-471]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"}, {"44562.189571759256", "[$-471]mmmmmm dd yyyy h:mm AM/PM", "January 01 2022 4:32 AM"}, {"43543.503206018519", "[$-471]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-471]mmmm dd yyyy h:mm AM/PM", "March 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-471]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-471]mmmmmm dd yyyy h:mm AM/PM", "March 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-471]mmmm dd yyyy h:mm AM/PM aaa", "March 19 2019 12:04 PM Tue"}, + {"43543.503206018519", "[$-471]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM Tue"}, + {"43543.503206018519", "[$-471]mmmmmm dd yyyy h:mm AM/PM dddd", "March 19 2019 12:04 PM Tuesday"}, {"44562.189571759256", "[$-60]mmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0624\u0631\u06CC 01 2022 4:32 AM"}, {"44562.189571759256", "[$-60]mmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0624\u0631\u06CC 01 2022 4:32 AM"}, {"44562.189571759256", "[$-60]mmmmm dd yyyy h:mm AM/PM", "\u062C 01 2022 4:32 AM"}, {"44562.189571759256", "[$-60]mmmmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0624\u0631\u06CC 01 2022 4:32 AM"}, {"43543.503206018519", "[$-60]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0655\u0686 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-60]mmmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0655\u0686 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-60]mmmmm dd yyyy h:mm AM/PM", "\u0645 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-60]mmmmmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0655\u0686 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-60]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u0655\u0686 19 2019 12:04 PM \u0628\u06C6\u0645\u0648\u0627\u0631"}, + {"43543.503206018519", "[$-60]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 PM \u0628\u06C6\u0645\u0648\u0627\u0631"}, + {"43543.503206018519", "[$-60]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u0655\u0686 19 2019 12:04 PM \u0628\u06C6\u0645\u0648\u0627\u0631"}, {"44562.189571759256", "[$-460]mmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0624\u0631\u06CC 01 2022 4:32 AM"}, {"44562.189571759256", "[$-460]mmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0624\u0631\u06CC 01 2022 4:32 AM"}, {"44562.189571759256", "[$-460]mmmmm dd yyyy h:mm AM/PM", "\u062C 01 2022 4:32 AM"}, {"44562.189571759256", "[$-460]mmmmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0624\u0631\u06CC 01 2022 4:32 AM"}, {"43543.503206018519", "[$-460]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0655\u0686 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-460]mmmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0655\u0686 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-460]mmmmm dd yyyy h:mm AM/PM", "\u0645 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-460]mmmmmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0655\u0686 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-460]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u0655\u0686 19 2019 12:04 PM \u0628\u06C6\u0645\u0648\u0627\u0631"}, + {"43543.503206018519", "[$-460]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 PM \u0628\u06C6\u0645\u0648\u0627\u0631"}, + {"43543.503206018519", "[$-460]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u0655\u0686 19 2019 12:04 PM \u0628\u06C6\u0645\u0648\u0627\u0631"}, {"44562.189571759256", "[$-860]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"}, {"44562.189571759256", "[$-860]mmmm dd yyyy h:mm AM/PM", "January 01 2022 4:32 AM"}, {"44562.189571759256", "[$-860]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"}, {"44562.189571759256", "[$-860]mmmmmm dd yyyy h:mm AM/PM", "January 01 2022 4:32 AM"}, {"43543.503206018519", "[$-860]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-860]mmmm dd yyyy h:mm AM/PM", "March 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-860]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-860]mmmmmm dd yyyy h:mm AM/PM", "March 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-860]mmmm dd yyyy h:mm AM/PM aaa", "March 19 2019 12:04 PM Tue"}, + {"43543.503206018519", "[$-860]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM Tue"}, + {"43543.503206018519", "[$-860]mmmmmm dd yyyy h:mm AM/PM dddd", "March 19 2019 12:04 PM Tuesday"}, {"44562.189571759256", "[$-3F]mmm dd yyyy h:mm AM/PM", "қаң 01 2022 4:32 AM"}, {"44562.189571759256", "[$-3F]mmmm dd yyyy h:mm AM/PM", "Қаңтар 01 2022 4:32 AM"}, {"44562.189571759256", "[$-3F]mmmmm dd yyyy h:mm AM/PM", "Қ 01 2022 4:32 AM"}, {"44562.189571759256", "[$-3F]mmmmmm dd yyyy h:mm AM/PM", "Қаңтар 01 2022 4:32 AM"}, {"43543.503206018519", "[$-3F]mmm dd yyyy h:mm AM/PM", "нау 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-3F]mmmm dd yyyy h:mm AM/PM", "Наурыз 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-3F]mmmmm dd yyyy h:mm AM/PM", "Н 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-3F]mmmmmm dd yyyy h:mm AM/PM", "Наурыз 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-3F]mmmm dd yyyy h:mm AM/PM aaa", "Наурыз 19 2019 12:04 PM \u0441\u0435\u0439"}, + {"43543.503206018519", "[$-3F]mmmmm dd yyyy h:mm AM/PM ddd", "Н 19 2019 12:04 PM \u0441\u0435\u0439"}, + {"43543.503206018519", "[$-3F]mmmmmm dd yyyy h:mm AM/PM dddd", "Наурыз 19 2019 12:04 PM \u0441\u0435\u0439\u0441\u0435\u043D\u0431\u0456"}, {"44562.189571759256", "[$-43F]mmm dd yyyy h:mm AM/PM", "қаң 01 2022 4:32 AM"}, {"44562.189571759256", "[$-43F]mmmm dd yyyy h:mm AM/PM", "Қаңтар 01 2022 4:32 AM"}, {"44562.189571759256", "[$-43F]mmmmm dd yyyy h:mm AM/PM", "Қ 01 2022 4:32 AM"}, {"44562.189571759256", "[$-43F]mmmmmm dd yyyy h:mm AM/PM", "Қаңтар 01 2022 4:32 AM"}, {"43543.503206018519", "[$-43F]mmm dd yyyy h:mm AM/PM", "нау 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-43F]mmmm dd yyyy h:mm AM/PM", "Наурыз 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-43F]mmmmm dd yyyy h:mm AM/PM", "Н 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-43F]mmmmmm dd yyyy h:mm AM/PM", "Наурыз 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-43F]mmmm dd yyyy h:mm AM/PM aaa", "Наурыз 19 2019 12:04 PM \u0441\u0435\u0439"}, + {"43543.503206018519", "[$-43F]mmmmm dd yyyy h:mm AM/PM ddd", "Н 19 2019 12:04 PM \u0441\u0435\u0439"}, + {"43543.503206018519", "[$-43F]mmmmmm dd yyyy h:mm AM/PM dddd", "Наурыз 19 2019 12:04 PM \u0441\u0435\u0439\u0441\u0435\u043D\u0431\u0456"}, {"44562.189571759256", "[$-53]mmm dd yyyy h:mm AM/PM", "\u17E1 01 2022 4:32 \u1796\u17D2\u179A\u17B9\u1780"}, {"44562.189571759256", "[$-53]mmmm dd yyyy h:mm AM/PM", "\u1798\u1780\u179A\u17B6 01 2022 4:32 \u1796\u17D2\u179A\u17B9\u1780"}, {"44562.189571759256", "[$-53]mmmmm dd yyyy h:mm AM/PM", "\u1798 01 2022 4:32 \u1796\u17D2\u179A\u17B9\u1780"}, {"44562.189571759256", "[$-53]mmmmmm dd yyyy h:mm AM/PM", "\u1798\u1780\u179A\u17B6 01 2022 4:32 \u1796\u17D2\u179A\u17B9\u1780"}, {"43543.503206018519", "[$-53]mmm dd yyyy h:mm AM/PM", "\u17E3 19 2019 12:04 \u179B\u17D2\u1784\u17B6\u1785"}, - {"43543.503206018519", "[$-53]mmmm dd yyyy h:mm AM/PM", "\u1798\u17B7\u1793\u17B6 19 2019 12:04 \u179B\u17D2\u1784\u17B6\u1785"}, - {"43543.503206018519", "[$-53]mmmmm dd yyyy h:mm AM/PM", "\u1798 19 2019 12:04 \u179B\u17D2\u1784\u17B6\u1785"}, - {"43543.503206018519", "[$-53]mmmmmm dd yyyy h:mm AM/PM", "\u1798\u17B7\u1793\u17B6 19 2019 12:04 \u179B\u17D2\u1784\u17B6\u1785"}, + {"43543.503206018519", "[$-53]mmmm dd yyyy h:mm AM/PM aaa", "\u1798\u17B7\u1793\u17B6 19 2019 12:04 \u179B\u17D2\u1784\u17B6\u1785 \u17A2."}, + {"43543.503206018519", "[$-53]mmmmm dd yyyy h:mm AM/PM ddd", "\u1798 19 2019 12:04 \u179B\u17D2\u1784\u17B6\u1785 \u17A2."}, + {"43543.503206018519", "[$-53]mmmmmm dd yyyy h:mm AM/PM dddd", "\u1798\u17B7\u1793\u17B6 19 2019 12:04 \u179B\u17D2\u1784\u17B6\u1785 \u1790\u17D2\u1784\u17C3\u17A2\u1784\u17D2\u1782\u17B6\u179A"}, {"44562.189571759256", "[$-453]mmm dd yyyy h:mm AM/PM", "\u17E1 01 2022 4:32 \u1796\u17D2\u179A\u17B9\u1780"}, {"44562.189571759256", "[$-453]mmmm dd yyyy h:mm AM/PM", "\u1798\u1780\u179A\u17B6 01 2022 4:32 \u1796\u17D2\u179A\u17B9\u1780"}, {"44562.189571759256", "[$-453]mmmmm dd yyyy h:mm AM/PM", "\u1798 01 2022 4:32 \u1796\u17D2\u179A\u17B9\u1780"}, {"44562.189571759256", "[$-453]mmmmmm dd yyyy h:mm AM/PM", "\u1798\u1780\u179A\u17B6 01 2022 4:32 \u1796\u17D2\u179A\u17B9\u1780"}, {"43543.503206018519", "[$-453]mmm dd yyyy h:mm AM/PM", "\u17E3 19 2019 12:04 \u179B\u17D2\u1784\u17B6\u1785"}, - {"43543.503206018519", "[$-453]mmmm dd yyyy h:mm AM/PM", "\u1798\u17B7\u1793\u17B6 19 2019 12:04 \u179B\u17D2\u1784\u17B6\u1785"}, - {"43543.503206018519", "[$-453]mmmmm dd yyyy h:mm AM/PM", "\u1798 19 2019 12:04 \u179B\u17D2\u1784\u17B6\u1785"}, - {"43543.503206018519", "[$-453]mmmmmm dd yyyy h:mm AM/PM", "\u1798\u17B7\u1793\u17B6 19 2019 12:04 \u179B\u17D2\u1784\u17B6\u1785"}, + {"43543.503206018519", "[$-453]mmmm dd yyyy h:mm AM/PM aaa", "\u1798\u17B7\u1793\u17B6 19 2019 12:04 \u179B\u17D2\u1784\u17B6\u1785 \u17A2."}, + {"43543.503206018519", "[$-453]mmmmm dd yyyy h:mm AM/PM ddd", "\u1798 19 2019 12:04 \u179B\u17D2\u1784\u17B6\u1785 \u17A2."}, + {"43543.503206018519", "[$-453]mmmmmm dd yyyy h:mm AM/PM dddd", "\u1798\u17B7\u1793\u17B6 19 2019 12:04 \u179B\u17D2\u1784\u17B6\u1785 \u1790\u17D2\u1784\u17C3\u17A2\u1784\u17D2\u1782\u17B6\u179A"}, {"44562.189571759256", "[$-86]mmm dd yyyy h:mm AM/PM", "nab'e 01 2022 4:32 a.m."}, {"44562.189571759256", "[$-86]mmmm dd yyyy h:mm AM/PM", "nab'e ik' 01 2022 4:32 a.m."}, {"44562.189571759256", "[$-86]mmmmm dd yyyy h:mm AM/PM", "n 01 2022 4:32 a.m."}, {"44562.189571759256", "[$-86]mmmmmm dd yyyy h:mm AM/PM", "nab'e ik' 01 2022 4:32 a.m."}, {"43543.503206018519", "[$-86]mmm dd yyyy h:mm AM/PM", "urox 19 2019 12:04 p.m."}, - {"43543.503206018519", "[$-86]mmmm dd yyyy h:mm AM/PM", "urox ik' 19 2019 12:04 p.m."}, - {"43543.503206018519", "[$-86]mmmmm dd yyyy h:mm AM/PM", "u 19 2019 12:04 p.m."}, - {"43543.503206018519", "[$-86]mmmmmm dd yyyy h:mm AM/PM", "urox ik' 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-86]mmmm dd yyyy h:mm AM/PM aaa", "urox ik' 19 2019 12:04 p.m. oxq'"}, + {"43543.503206018519", "[$-86]mmmmm dd yyyy h:mm AM/PM ddd", "u 19 2019 12:04 p.m. oxq'"}, + {"43543.503206018519", "[$-86]mmmmmm dd yyyy h:mm AM/PM dddd", "urox ik' 19 2019 12:04 p.m. oxq'ij"}, {"44562.189571759256", "[$-486]mmm dd yyyy h:mm AM/PM", "nab'e 01 2022 4:32 a.m."}, {"44562.189571759256", "[$-486]mmmm dd yyyy h:mm AM/PM", "nab'e ik' 01 2022 4:32 a.m."}, {"44562.189571759256", "[$-486]mmmmm dd yyyy h:mm AM/PM", "n 01 2022 4:32 a.m."}, {"44562.189571759256", "[$-486]mmmmmm dd yyyy h:mm AM/PM", "nab'e ik' 01 2022 4:32 a.m."}, {"43543.503206018519", "[$-486]mmm dd yyyy h:mm AM/PM", "urox 19 2019 12:04 p.m."}, - {"43543.503206018519", "[$-486]mmmm dd yyyy h:mm AM/PM", "urox ik' 19 2019 12:04 p.m."}, - {"43543.503206018519", "[$-486]mmmmm dd yyyy h:mm AM/PM", "u 19 2019 12:04 p.m."}, - {"43543.503206018519", "[$-486]mmmmmm dd yyyy h:mm AM/PM", "urox ik' 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-486]mmmm dd yyyy h:mm AM/PM aaa", "urox ik' 19 2019 12:04 p.m. oxq'"}, + {"43543.503206018519", "[$-486]mmmmm dd yyyy h:mm AM/PM ddd", "u 19 2019 12:04 p.m. oxq'"}, + {"43543.503206018519", "[$-486]mmmmmm dd yyyy h:mm AM/PM dddd", "urox ik' 19 2019 12:04 p.m. oxq'ij"}, {"44562.189571759256", "[$-87]mmm dd yyyy h:mm AM/PM", "mut. 01 2022 4:32 AM"}, {"44562.189571759256", "[$-87]mmmm dd yyyy h:mm AM/PM", "Mutarama 01 2022 4:32 AM"}, {"44562.189571759256", "[$-87]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 AM"}, {"44562.189571759256", "[$-87]mmmmmm dd yyyy h:mm AM/PM", "Mutarama 01 2022 4:32 AM"}, {"43543.503206018519", "[$-87]mmm dd yyyy h:mm AM/PM", "wer. 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-87]mmmm dd yyyy h:mm AM/PM", "Werurwe 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-87]mmmmm dd yyyy h:mm AM/PM", "W 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-87]mmmmmm dd yyyy h:mm AM/PM", "Werurwe 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-87]mmmm dd yyyy h:mm AM/PM aaa", "Werurwe 19 2019 12:04 PM kab."}, + {"43543.503206018519", "[$-87]mmmmm dd yyyy h:mm AM/PM ddd", "W 19 2019 12:04 PM kab."}, + {"43543.503206018519", "[$-87]mmmmmm dd yyyy h:mm AM/PM dddd", "Werurwe 19 2019 12:04 PM Ku wa kabiri"}, {"44562.189571759256", "[$-487]mmm dd yyyy h:mm AM/PM", "mut. 01 2022 4:32 AM"}, {"44562.189571759256", "[$-487]mmmm dd yyyy h:mm AM/PM", "Mutarama 01 2022 4:32 AM"}, {"44562.189571759256", "[$-487]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 AM"}, {"44562.189571759256", "[$-487]mmmmmm dd yyyy h:mm AM/PM", "Mutarama 01 2022 4:32 AM"}, {"43543.503206018519", "[$-487]mmm dd yyyy h:mm AM/PM", "wer. 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-487]mmmm dd yyyy h:mm AM/PM", "Werurwe 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-487]mmmmm dd yyyy h:mm AM/PM", "W 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-487]mmmmmm dd yyyy h:mm AM/PM", "Werurwe 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-487]mmmm dd yyyy h:mm AM/PM aaa", "Werurwe 19 2019 12:04 PM kab."}, + {"43543.503206018519", "[$-487]mmmmm dd yyyy h:mm AM/PM ddd", "W 19 2019 12:04 PM kab."}, + {"43543.503206018519", "[$-487]mmmmmm dd yyyy h:mm AM/PM dddd", "Werurwe 19 2019 12:04 PM Ku wa kabiri"}, {"44562.189571759256", "[$-41]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"}, {"44562.189571759256", "[$-41]mmmm dd yyyy h:mm AM/PM", "Januari 01 2022 4:32 AM"}, {"44562.189571759256", "[$-41]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"}, {"44562.189571759256", "[$-41]mmmmmm dd yyyy h:mm AM/PM", "Januari 01 2022 4:32 AM"}, {"43543.503206018519", "[$-41]mmm dd yyyy h:mm AM/PM", "Mac 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-41]mmmm dd yyyy h:mm AM/PM", "Machi 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-41]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-41]mmmmmm dd yyyy h:mm AM/PM", "Machi 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-41]mmmm dd yyyy h:mm AM/PM aaa", "Machi 19 2019 12:04 PM Jnn"}, + {"43543.503206018519", "[$-41]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM Jnn"}, + {"43543.503206018519", "[$-41]mmmmmm dd yyyy h:mm AM/PM dddd", "Machi 19 2019 12:04 PM Jumanne"}, {"44562.189571759256", "[$-441]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"}, {"44562.189571759256", "[$-441]mmmm dd yyyy h:mm AM/PM", "Januari 01 2022 4:32 AM"}, {"44562.189571759256", "[$-441]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"}, {"44562.189571759256", "[$-441]mmmmmm dd yyyy h:mm AM/PM", "Januari 01 2022 4:32 AM"}, {"43543.503206018519", "[$-441]mmm dd yyyy h:mm AM/PM", "Mac 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-441]mmmm dd yyyy h:mm AM/PM", "Machi 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-441]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-441]mmmmmm dd yyyy h:mm AM/PM", "Machi 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-441]mmmm dd yyyy h:mm AM/PM aaa", "Machi 19 2019 12:04 PM Jnn"}, + {"43543.503206018519", "[$-441]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM Jnn"}, + {"43543.503206018519", "[$-441]mmmmmm dd yyyy h:mm AM/PM dddd", "Machi 19 2019 12:04 PM Jumanne"}, {"44562.189571759256", "[$-57]mmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u0947 01 2022 4:32 \u092E.\u092A\u0942."}, {"44562.189571759256", "[$-57]mmmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u0947\u0935\u093E\u0930\u0940 01 2022 4:32 \u092E.\u092A\u0942."}, {"44562.189571759256", "[$-57]mmmmm dd yyyy h:mm AM/PM", "\u091C 01 2022 4:32 \u092E.\u092A\u0942."}, {"44562.189571759256", "[$-57]mmmmmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u0947\u0935\u093E\u0930\u0940 01 2022 4:32 \u092E.\u092A\u0942."}, {"43543.503206018519", "[$-57]mmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E.\u0928\u0902."}, - {"43543.503206018519", "[$-57]mmmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E.\u0928\u0902."}, - {"43543.503206018519", "[$-57]mmmmm dd yyyy h:mm AM/PM", "\u092E 19 2019 12:04 \u092E.\u0928\u0902."}, - {"43543.503206018519", "[$-57]mmmmmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E.\u0928\u0902."}, + {"43543.503206018519", "[$-57]mmmm dd yyyy h:mm AM/PM aaa", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E.\u0928\u0902. \u092E\u0902\u0917\u0933."}, + {"43543.503206018519", "[$-57]mmmmm dd yyyy h:mm AM/PM ddd", "\u092E 19 2019 12:04 \u092E.\u0928\u0902. \u092E\u0902\u0917\u0933."}, + {"43543.503206018519", "[$-57]mmmmmm dd yyyy h:mm AM/PM dddd", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E.\u0928\u0902. \u092E\u0902\u0917\u0933\u093E\u0930"}, {"44562.189571759256", "[$-457]mmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u0947 01 2022 4:32 \u092E.\u092A\u0942."}, {"44562.189571759256", "[$-457]mmmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u0947\u0935\u093E\u0930\u0940 01 2022 4:32 \u092E.\u092A\u0942."}, {"44562.189571759256", "[$-457]mmmmm dd yyyy h:mm AM/PM", "\u091C 01 2022 4:32 \u092E.\u092A\u0942."}, {"44562.189571759256", "[$-457]mmmmmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u0947\u0935\u093E\u0930\u0940 01 2022 4:32 \u092E.\u092A\u0942."}, {"43543.503206018519", "[$-457]mmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E.\u0928\u0902."}, - {"43543.503206018519", "[$-457]mmmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E.\u0928\u0902."}, - {"43543.503206018519", "[$-457]mmmmm dd yyyy h:mm AM/PM", "\u092E 19 2019 12:04 \u092E.\u0928\u0902."}, - {"43543.503206018519", "[$-457]mmmmmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E.\u0928\u0902."}, - {"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", "[$-457]mmmm dd yyyy h:mm AM/PM aaa", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E.\u0928\u0902. \u092E\u0902\u0917\u0933."}, + {"43543.503206018519", "[$-457]mmmmm dd yyyy h:mm AM/PM ddd", "\u092E 19 2019 12:04 \u092E.\u0928\u0902. \u092E\u0902\u0917\u0933."}, + {"43543.503206018519", "[$-457]mmmmmm dd yyyy h:mm AM/PM dddd", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E.\u0928\u0902. \u092E\u0902\u0917\u0933\u093E\u0930"}, + {"43543.503206018519", "[$-12]mmm dd yyyy h:mm AM/PM aaa", "3 19 2019 12:04 오후 화"}, + {"43543.503206018519", "[$-12]mmmm dd yyyy h:mm AM/PM ddd", "3월 19 2019 12:04 오후 화"}, + {"43543.503206018519", "[$-12]mmmmm dd yyyy h:mm AM/PM dddd", "3 19 2019 12:04 오후 화요일"}, + {"43543.503206018519", "[$-412]mmm dd yyyy h:mm AM/PM aaa", "3 19 2019 12:04 오후 화"}, + {"43543.503206018519", "[$-412]mmmm dd yyyy h:mm AM/PM ddd", "3월 19 2019 12:04 오후 화"}, + {"43543.503206018519", "[$-412]mmmmm dd yyyy h:mm AM/PM dddd", "3 19 2019 12:04 오후 화요일"}, {"44562.189571759256", "[$-40]mmm dd yyyy h:mm AM/PM", "\u042F\u043D\u0432 01 2022 4:32 \u0442\u04A3"}, {"44562.189571759256", "[$-40]mmmm dd yyyy h:mm AM/PM", "\u042F\u043D\u0432\u0430\u0440\u044C 01 2022 4:32 \u0442\u04A3"}, {"44562.189571759256", "[$-40]mmmmm dd yyyy h:mm AM/PM", "\u042F 01 2022 4:32 \u0442\u04A3"}, {"44562.189571759256", "[$-40]mmmmmm dd yyyy h:mm AM/PM", "\u042F\u043D\u0432\u0430\u0440\u044C 01 2022 4:32 \u0442\u04A3"}, {"43543.503206018519", "[$-40]mmm dd yyyy h:mm AM/PM", "\u041C\u0430\u0440 19 2019 12:04 \u0442\u043A"}, - {"43543.503206018519", "[$-40]mmmm dd yyyy h:mm AM/PM", "\u041C\u0430\u0440\u0442 19 2019 12:04 \u0442\u043A"}, - {"43543.503206018519", "[$-40]mmmmm dd yyyy h:mm AM/PM", "\u041C 19 2019 12:04 \u0442\u043A"}, - {"43543.503206018519", "[$-40]mmmmmm dd yyyy h:mm AM/PM", "\u041C\u0430\u0440\u0442 19 2019 12:04 \u0442\u043A"}, + {"43543.503206018519", "[$-40]mmmm dd yyyy h:mm AM/PM aaa", "\u041C\u0430\u0440\u0442 19 2019 12:04 \u0442\u043A \u0448\u0435\u0439\u0448."}, + {"43543.503206018519", "[$-40]mmmmm dd yyyy h:mm AM/PM ddd", "\u041C 19 2019 12:04 \u0442\u043A \u0448\u0435\u0439\u0448."}, + {"43543.503206018519", "[$-40]mmmmmm dd yyyy h:mm AM/PM dddd", "\u041C\u0430\u0440\u0442 19 2019 12:04 \u0442\u043A \u0448\u0435\u0439\u0448\u0435\u043C\u0431\u0438"}, {"44562.189571759256", "[$-440]mmm dd yyyy h:mm AM/PM", "\u042F\u043D\u0432 01 2022 4:32 \u0442\u04A3"}, {"44562.189571759256", "[$-440]mmmm dd yyyy h:mm AM/PM", "\u042F\u043D\u0432\u0430\u0440\u044C 01 2022 4:32 \u0442\u04A3"}, {"44562.189571759256", "[$-440]mmmmm dd yyyy h:mm AM/PM", "\u042F 01 2022 4:32 \u0442\u04A3"}, {"44562.189571759256", "[$-440]mmmmmm dd yyyy h:mm AM/PM", "\u042F\u043D\u0432\u0430\u0440\u044C 01 2022 4:32 \u0442\u04A3"}, {"43543.503206018519", "[$-440]mmm dd yyyy h:mm AM/PM", "\u041C\u0430\u0440 19 2019 12:04 \u0442\u043A"}, - {"43543.503206018519", "[$-440]mmmm dd yyyy h:mm AM/PM", "\u041C\u0430\u0440\u0442 19 2019 12:04 \u0442\u043A"}, - {"43543.503206018519", "[$-440]mmmmm dd yyyy h:mm AM/PM", "\u041C 19 2019 12:04 \u0442\u043A"}, - {"43543.503206018519", "[$-440]mmmmmm dd yyyy h:mm AM/PM", "\u041C\u0430\u0440\u0442 19 2019 12:04 \u0442\u043A"}, + {"43543.503206018519", "[$-440]mmmm dd yyyy h:mm AM/PM aaa", "\u041C\u0430\u0440\u0442 19 2019 12:04 \u0442\u043A \u0448\u0435\u0439\u0448."}, + {"43543.503206018519", "[$-440]mmmmm dd yyyy h:mm AM/PM ddd", "\u041C 19 2019 12:04 \u0442\u043A \u0448\u0435\u0439\u0448."}, + {"43543.503206018519", "[$-440]mmmmmm dd yyyy h:mm AM/PM dddd", "\u041C\u0430\u0440\u0442 19 2019 12:04 \u0442\u043A \u0448\u0435\u0439\u0448\u0435\u043C\u0431\u0438"}, {"44562.189571759256", "[$-54]mmm dd yyyy h:mm AM/PM", "\u0EA1.\u0E81. 01 2022 4:32 \u0E81\u0EC8\u0EAD\u0E99\u0E97\u0EC8\u0EBD\u0E87"}, {"44562.189571759256", "[$-54]mmmm dd yyyy h:mm AM/PM", "\u0EA1\u0EB1\u0E87\u0E81\u0EAD\u0E99 01 2022 4:32 \u0E81\u0EC8\u0EAD\u0E99\u0E97\u0EC8\u0EBD\u0E87"}, {"44562.189571759256", "[$-54]mmmmm dd yyyy h:mm AM/PM", "\u0EA1 01 2022 4:32 \u0E81\u0EC8\u0EAD\u0E99\u0E97\u0EC8\u0EBD\u0E87"}, {"44562.189571759256", "[$-54]mmmmmm dd yyyy h:mm AM/PM", "\u0EA1\u0EB1\u0E87\u0E81\u0EAD\u0E99 01 2022 4:32 \u0E81\u0EC8\u0EAD\u0E99\u0E97\u0EC8\u0EBD\u0E87"}, {"43543.503206018519", "[$-54]mmm dd yyyy h:mm AM/PM", "\u0EA1.\u0E99. 19 2019 12:04 \u0EAB\u0EBC\u0EB1\u0E87\u0E97\u0EC8\u0EBD\u0E87"}, - {"43543.503206018519", "[$-54]mmmm dd yyyy h:mm AM/PM", "\u0EA1\u0EB5\u0E99\u0EB2 19 2019 12:04 \u0EAB\u0EBC\u0EB1\u0E87\u0E97\u0EC8\u0EBD\u0E87"}, - {"43543.503206018519", "[$-54]mmmmm dd yyyy h:mm AM/PM", "\u0EA1 19 2019 12:04 \u0EAB\u0EBC\u0EB1\u0E87\u0E97\u0EC8\u0EBD\u0E87"}, - {"43543.503206018519", "[$-54]mmmmmm dd yyyy h:mm AM/PM", "\u0EA1\u0EB5\u0E99\u0EB2 19 2019 12:04 \u0EAB\u0EBC\u0EB1\u0E87\u0E97\u0EC8\u0EBD\u0E87"}, + {"43543.503206018519", "[$-54]mmmm dd yyyy h:mm AM/PM aaa", "\u0EA1\u0EB5\u0E99\u0EB2 19 2019 12:04 \u0EAB\u0EBC\u0EB1\u0E87\u0E97\u0EC8\u0EBD\u0E87 \u0EAD\u0EB1\u0E87\u0E84\u0EB2\u0E99"}, + {"43543.503206018519", "[$-54]mmmmm dd yyyy h:mm AM/PM ddd", "\u0EA1 19 2019 12:04 \u0EAB\u0EBC\u0EB1\u0E87\u0E97\u0EC8\u0EBD\u0E87 \u0EAD\u0EB1\u0E87\u0E84\u0EB2\u0E99"}, + {"43543.503206018519", "[$-54]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0EA1\u0EB5\u0E99\u0EB2 19 2019 12:04 \u0EAB\u0EBC\u0EB1\u0E87\u0E97\u0EC8\u0EBD\u0E87 \u0EA7\u0EB1\u0E99\u0EAD\u0EB1\u0E87\u0E84\u0EB2\u0E99"}, {"44562.189571759256", "[$-454]mmm dd yyyy h:mm AM/PM", "\u0EA1.\u0E81. 01 2022 4:32 \u0E81\u0EC8\u0EAD\u0E99\u0E97\u0EC8\u0EBD\u0E87"}, {"44562.189571759256", "[$-454]mmmm dd yyyy h:mm AM/PM", "\u0EA1\u0EB1\u0E87\u0E81\u0EAD\u0E99 01 2022 4:32 \u0E81\u0EC8\u0EAD\u0E99\u0E97\u0EC8\u0EBD\u0E87"}, {"44562.189571759256", "[$-454]mmmmm dd yyyy h:mm AM/PM", "\u0EA1 01 2022 4:32 \u0E81\u0EC8\u0EAD\u0E99\u0E97\u0EC8\u0EBD\u0E87"}, {"44562.189571759256", "[$-454]mmmmmm dd yyyy h:mm AM/PM", "\u0EA1\u0EB1\u0E87\u0E81\u0EAD\u0E99 01 2022 4:32 \u0E81\u0EC8\u0EAD\u0E99\u0E97\u0EC8\u0EBD\u0E87"}, {"43543.503206018519", "[$-454]mmm dd yyyy h:mm AM/PM", "\u0EA1.\u0E99. 19 2019 12:04 \u0EAB\u0EBC\u0EB1\u0E87\u0E97\u0EC8\u0EBD\u0E87"}, - {"43543.503206018519", "[$-454]mmmm dd yyyy h:mm AM/PM", "\u0EA1\u0EB5\u0E99\u0EB2 19 2019 12:04 \u0EAB\u0EBC\u0EB1\u0E87\u0E97\u0EC8\u0EBD\u0E87"}, - {"43543.503206018519", "[$-454]mmmmm dd yyyy h:mm AM/PM", "\u0EA1 19 2019 12:04 \u0EAB\u0EBC\u0EB1\u0E87\u0E97\u0EC8\u0EBD\u0E87"}, - {"43543.503206018519", "[$-454]mmmmmm dd yyyy h:mm AM/PM", "\u0EA1\u0EB5\u0E99\u0EB2 19 2019 12:04 \u0EAB\u0EBC\u0EB1\u0E87\u0E97\u0EC8\u0EBD\u0E87"}, + {"43543.503206018519", "[$-454]mmmm dd yyyy h:mm AM/PM aaa", "\u0EA1\u0EB5\u0E99\u0EB2 19 2019 12:04 \u0EAB\u0EBC\u0EB1\u0E87\u0E97\u0EC8\u0EBD\u0E87 \u0EAD\u0EB1\u0E87\u0E84\u0EB2\u0E99"}, + {"43543.503206018519", "[$-454]mmmmm dd yyyy h:mm AM/PM ddd", "\u0EA1 19 2019 12:04 \u0EAB\u0EBC\u0EB1\u0E87\u0E97\u0EC8\u0EBD\u0E87 \u0EAD\u0EB1\u0E87\u0E84\u0EB2\u0E99"}, + {"43543.503206018519", "[$-454]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0EA1\u0EB5\u0E99\u0EB2 19 2019 12:04 \u0EAB\u0EBC\u0EB1\u0E87\u0E97\u0EC8\u0EBD\u0E87 \u0EA7\u0EB1\u0E99\u0EAD\u0EB1\u0E87\u0E84\u0EB2\u0E99"}, {"44562.189571759256", "[$-476]mmm dd yyyy h:mm AM/PM", "Ian 01 2022 4:32 AM"}, {"44562.189571759256", "[$-476]mmmm dd yyyy h:mm AM/PM", "Ianuarius 01 2022 4:32 AM"}, {"44562.189571759256", "[$-476]mmmmm dd yyyy h:mm AM/PM", "I 01 2022 4:32 AM"}, {"44562.189571759256", "[$-476]mmmmmm dd yyyy h:mm AM/PM", "Ianuarius 01 2022 4:32 AM"}, {"43543.503206018519", "[$-476]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-476]mmmm dd yyyy h:mm AM/PM", "Martius 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-476]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-476]mmmmmm dd yyyy h:mm AM/PM", "Martius 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-476]mmmm dd yyyy h:mm AM/PM aaa", "Martius 19 2019 12:04 PM Mar"}, + {"43543.503206018519", "[$-476]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM Mar"}, + {"43543.503206018519", "[$-476]mmmmmm dd yyyy h:mm AM/PM dddd", "Martius 19 2019 12:04 PM Martis"}, {"44562.189571759256", "[$-26]mmm dd yyyy h:mm AM/PM", "janv. 01 2022 4:32 priekšp."}, {"44562.189571759256", "[$-26]mmmm dd yyyy h:mm AM/PM", "janvāris 01 2022 4:32 priekšp."}, {"44562.189571759256", "[$-26]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 priekšp."}, {"44562.189571759256", "[$-26]mmmmmm dd yyyy h:mm AM/PM", "janvāris 01 2022 4:32 priekšp."}, {"43543.503206018519", "[$-26]mmm dd yyyy h:mm AM/PM", "marts 19 2019 12:04 pēcp."}, - {"43543.503206018519", "[$-26]mmmm dd yyyy h:mm AM/PM", "marts 19 2019 12:04 pēcp."}, - {"43543.503206018519", "[$-26]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 pēcp."}, - {"43543.503206018519", "[$-26]mmmmmm dd yyyy h:mm AM/PM", "marts 19 2019 12:04 pēcp."}, + {"43543.503206018519", "[$-26]mmmm dd yyyy h:mm AM/PM aaa", "marts 19 2019 12:04 pēcp. otrd."}, + {"43543.503206018519", "[$-26]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 pēcp. otrd."}, + {"43543.503206018519", "[$-26]mmmmmm dd yyyy h:mm AM/PM dddd", "marts 19 2019 12:04 pēcp. otrdiena"}, {"44562.189571759256", "[$-426]mmm dd yyyy h:mm AM/PM", "janv. 01 2022 4:32 priekšp."}, {"44562.189571759256", "[$-426]mmmm dd yyyy h:mm AM/PM", "janvāris 01 2022 4:32 priekšp."}, {"44562.189571759256", "[$-426]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 priekšp."}, {"44562.189571759256", "[$-426]mmmmmm dd yyyy h:mm AM/PM", "janvāris 01 2022 4:32 priekšp."}, {"43543.503206018519", "[$-426]mmm dd yyyy h:mm AM/PM", "marts 19 2019 12:04 pēcp."}, - {"43543.503206018519", "[$-426]mmmm dd yyyy h:mm AM/PM", "marts 19 2019 12:04 pēcp."}, - {"43543.503206018519", "[$-426]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 pēcp."}, - {"43543.503206018519", "[$-426]mmmmmm dd yyyy h:mm AM/PM", "marts 19 2019 12:04 pēcp."}, + {"43543.503206018519", "[$-426]mmmm dd yyyy h:mm AM/PM aaa", "marts 19 2019 12:04 pēcp. otrd."}, + {"43543.503206018519", "[$-426]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 pēcp. otrd."}, + {"43543.503206018519", "[$-426]mmmmmm dd yyyy h:mm AM/PM dddd", "marts 19 2019 12:04 pēcp. otrdiena"}, {"44562.189571759256", "[$-27]mmm dd yyyy h:mm AM/PM", "saus. 01 2022 4:32 priešpiet"}, {"44562.189571759256", "[$-27]mmmm dd yyyy h:mm AM/PM", "sausis 01 2022 4:32 priešpiet"}, {"44562.189571759256", "[$-27]mmmmm dd yyyy h:mm AM/PM", "s 01 2022 4:32 priešpiet"}, {"44562.189571759256", "[$-27]mmmmmm dd yyyy h:mm AM/PM", "sausis 01 2022 4:32 priešpiet"}, {"43543.503206018519", "[$-27]mmm dd yyyy h:mm AM/PM", "kov. 19 2019 12:04 popiet"}, - {"43543.503206018519", "[$-27]mmmm dd yyyy h:mm AM/PM", "kovas 19 2019 12:04 popiet"}, - {"43543.503206018519", "[$-27]mmmmm dd yyyy h:mm AM/PM", "k 19 2019 12:04 popiet"}, - {"43543.503206018519", "[$-27]mmmmmm dd yyyy h:mm AM/PM", "kovas 19 2019 12:04 popiet"}, + {"43543.503206018519", "[$-27]mmmm dd yyyy h:mm AM/PM aaa", "kovas 19 2019 12:04 popiet an"}, + {"43543.503206018519", "[$-27]mmmmm dd yyyy h:mm AM/PM ddd", "k 19 2019 12:04 popiet an"}, + {"43543.503206018519", "[$-27]mmmmmm dd yyyy h:mm AM/PM dddd", "kovas 19 2019 12:04 popiet antradienis"}, {"44562.189571759256", "[$-427]mmm dd yyyy h:mm AM/PM", "saus. 01 2022 4:32 priešpiet"}, {"44562.189571759256", "[$-427]mmmm dd yyyy h:mm AM/PM", "sausis 01 2022 4:32 priešpiet"}, {"44562.189571759256", "[$-427]mmmmm dd yyyy h:mm AM/PM", "s 01 2022 4:32 priešpiet"}, {"44562.189571759256", "[$-427]mmmmmm dd yyyy h:mm AM/PM", "sausis 01 2022 4:32 priešpiet"}, {"43543.503206018519", "[$-427]mmm dd yyyy h:mm AM/PM", "kov. 19 2019 12:04 popiet"}, - {"43543.503206018519", "[$-427]mmmm dd yyyy h:mm AM/PM", "kovas 19 2019 12:04 popiet"}, - {"43543.503206018519", "[$-427]mmmmm dd yyyy h:mm AM/PM", "k 19 2019 12:04 popiet"}, - {"43543.503206018519", "[$-427]mmmmmm dd yyyy h:mm AM/PM", "kovas 19 2019 12:04 popiet"}, + {"43543.503206018519", "[$-427]mmmm dd yyyy h:mm AM/PM aaa", "kovas 19 2019 12:04 popiet an"}, + {"43543.503206018519", "[$-427]mmmmm dd yyyy h:mm AM/PM ddd", "k 19 2019 12:04 popiet an"}, + {"43543.503206018519", "[$-427]mmmmmm dd yyyy h:mm AM/PM dddd", "kovas 19 2019 12:04 popiet antradienis"}, {"44562.189571759256", "[$-7C2E]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 AM"}, {"44562.189571759256", "[$-7C2E]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 AM"}, {"44562.189571759256", "[$-7C2E]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"}, {"44562.189571759256", "[$-7C2E]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 AM"}, {"43543.503206018519", "[$-7C2E]mmm dd yyyy h:mm AM/PM", "měr 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-7C2E]mmmm dd yyyy h:mm AM/PM", "měrc 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-7C2E]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-7C2E]mmmmmm dd yyyy h:mm AM/PM", "měrc 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-7C2E]mmmm dd yyyy h:mm AM/PM aaa", "měrc 19 2019 12:04 PM wa\u0142"}, + {"43543.503206018519", "[$-7C2E]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM wa\u0142"}, + {"43543.503206018519", "[$-7C2E]mmmmmm dd yyyy h:mm AM/PM dddd", "měrc 19 2019 12:04 PM wa\u0142tora"}, {"44562.189571759256", "[$-82E]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 AM"}, {"44562.189571759256", "[$-82E]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 AM"}, {"44562.189571759256", "[$-82E]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"}, {"44562.189571759256", "[$-82E]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 AM"}, {"43543.503206018519", "[$-82E]mmm dd yyyy h:mm AM/PM", "měr 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-82E]mmmm dd yyyy h:mm AM/PM", "měrc 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-82E]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-82E]mmmmmm dd yyyy h:mm AM/PM", "měrc 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-82E]mmmm dd yyyy h:mm AM/PM aaa", "měrc 19 2019 12:04 PM wa\u0142"}, + {"43543.503206018519", "[$-82E]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM wa\u0142"}, + {"43543.503206018519", "[$-82E]mmmmmm dd yyyy h:mm AM/PM dddd", "měrc 19 2019 12:04 PM wa\u0142tora"}, {"44562.189571759256", "[$-6E]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"}, {"44562.189571759256", "[$-6E]mmmm dd yyyy h:mm AM/PM", "Januar 01 2022 4:32 AM"}, {"44562.189571759256", "[$-6E]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"}, {"44562.189571759256", "[$-6E]mmmmmm dd yyyy h:mm AM/PM", "Januar 01 2022 4:32 AM"}, {"43543.503206018519", "[$-6E]mmm dd yyyy h:mm AM/PM", "Mäe 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-6E]mmmm dd yyyy h:mm AM/PM", "Mäerz 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-6E]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-6E]mmmmmm dd yyyy h:mm AM/PM", "Mäerz 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-6E]mmmm dd yyyy h:mm AM/PM aaa", "Mäerz 19 2019 12:04 PM Dën"}, + {"43543.503206018519", "[$-6E]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM Dën"}, + {"43543.503206018519", "[$-6E]mmmmmm dd yyyy h:mm AM/PM dddd", "Mäerz 19 2019 12:04 PM Dënschdeg"}, {"44562.189571759256", "[$-46E]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"}, {"44562.189571759256", "[$-46E]mmmm dd yyyy h:mm AM/PM", "Januar 01 2022 4:32 AM"}, {"44562.189571759256", "[$-46E]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"}, {"44562.189571759256", "[$-46E]mmmmmm dd yyyy h:mm AM/PM", "Januar 01 2022 4:32 AM"}, {"43543.503206018519", "[$-46E]mmm dd yyyy h:mm AM/PM", "Mäe 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-46E]mmmm dd yyyy h:mm AM/PM", "Mäerz 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-46E]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-46E]mmmmmm dd yyyy h:mm AM/PM", "Mäerz 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-46E]mmmm dd yyyy h:mm AM/PM aaa", "Mäerz 19 2019 12:04 PM Dën"}, + {"43543.503206018519", "[$-46E]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM Dën"}, + {"43543.503206018519", "[$-46E]mmmmmm dd yyyy h:mm AM/PM dddd", "Mäerz 19 2019 12:04 PM Dënschdeg"}, {"44562.189571759256", "[$-2F]mmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D. 01 2022 4:32 \u043F\u0440\u0435\u0442\u043F\u043B."}, {"44562.189571759256", "[$-2F]mmmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D\u0443\u0430\u0440\u0438 01 2022 4:32 \u043F\u0440\u0435\u0442\u043F\u043B."}, {"44562.189571759256", "[$-2F]mmmmm dd yyyy h:mm AM/PM", "\u0458 01 2022 4:32 \u043F\u0440\u0435\u0442\u043F\u043B."}, {"44562.189571759256", "[$-2F]mmmmmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D\u0443\u0430\u0440\u0438 01 2022 4:32 \u043F\u0440\u0435\u0442\u043F\u043B."}, {"43543.503206018519", "[$-2F]mmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440. 19 2019 12:04 \u043F\u043E\u043F\u043B."}, - {"43543.503206018519", "[$-2F]mmmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440\u0442 19 2019 12:04 \u043F\u043E\u043F\u043B."}, - {"43543.503206018519", "[$-2F]mmmmm dd yyyy h:mm AM/PM", "\u043C 19 2019 12:04 \u043F\u043E\u043F\u043B."}, - {"43543.503206018519", "[$-2F]mmmmmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440\u0442 19 2019 12:04 \u043F\u043E\u043F\u043B."}, + {"43543.503206018519", "[$-2F]mmmm dd yyyy h:mm AM/PM aaa", "\u043C\u0430\u0440\u0442 19 2019 12:04 \u043F\u043E\u043F\u043B. \u0432\u0442."}, + {"43543.503206018519", "[$-2F]mmmmm dd yyyy h:mm AM/PM ddd", "\u043C 19 2019 12:04 \u043F\u043E\u043F\u043B. \u0432\u0442."}, + {"43543.503206018519", "[$-2F]mmmmmm dd yyyy h:mm AM/PM dddd", "\u043C\u0430\u0440\u0442 19 2019 12:04 \u043F\u043E\u043F\u043B. \u0432\u0442\u043E\u0440\u043D\u0438\u043A"}, {"44562.189571759256", "[$-42F]mmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D. 01 2022 4:32 \u043F\u0440\u0435\u0442\u043F\u043B."}, {"44562.189571759256", "[$-42F]mmmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D\u0443\u0430\u0440\u0438 01 2022 4:32 \u043F\u0440\u0435\u0442\u043F\u043B."}, {"44562.189571759256", "[$-42F]mmmmm dd yyyy h:mm AM/PM", "\u0458 01 2022 4:32 \u043F\u0440\u0435\u0442\u043F\u043B."}, {"44562.189571759256", "[$-42F]mmmmmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D\u0443\u0430\u0440\u0438 01 2022 4:32 \u043F\u0440\u0435\u0442\u043F\u043B."}, {"43543.503206018519", "[$-42F]mmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440. 19 2019 12:04 \u043F\u043E\u043F\u043B."}, - {"43543.503206018519", "[$-42F]mmmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440\u0442 19 2019 12:04 \u043F\u043E\u043F\u043B."}, - {"43543.503206018519", "[$-42F]mmmmm dd yyyy h:mm AM/PM", "\u043C 19 2019 12:04 \u043F\u043E\u043F\u043B."}, - {"43543.503206018519", "[$-42F]mmmmmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440\u0442 19 2019 12:04 \u043F\u043E\u043F\u043B."}, + {"43543.503206018519", "[$-42F]mmmm dd yyyy h:mm AM/PM aaa", "\u043C\u0430\u0440\u0442 19 2019 12:04 \u043F\u043E\u043F\u043B. \u0432\u0442."}, + {"43543.503206018519", "[$-42F]mmmmm dd yyyy h:mm AM/PM ddd", "\u043C 19 2019 12:04 \u043F\u043E\u043F\u043B. \u0432\u0442."}, + {"43543.503206018519", "[$-42F]mmmmmm dd yyyy h:mm AM/PM dddd", "\u043C\u0430\u0440\u0442 19 2019 12:04 \u043F\u043E\u043F\u043B. \u0432\u0442\u043E\u0440\u043D\u0438\u043A"}, {"44562.189571759256", "[$-3E]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 PG"}, {"44562.189571759256", "[$-3E]mmmm dd yyyy h:mm AM/PM", "Januari 01 2022 4:32 PG"}, {"44562.189571759256", "[$-3E]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 PG"}, {"44562.189571759256", "[$-3E]mmmmmm dd yyyy h:mm AM/PM", "Januari 01 2022 4:32 PG"}, {"43543.503206018519", "[$-3E]mmm dd yyyy h:mm AM/PM", "Mac 19 2019 12:04 PTG"}, - {"43543.503206018519", "[$-3E]mmmm dd yyyy h:mm AM/PM", "Mac 19 2019 12:04 PTG"}, - {"43543.503206018519", "[$-3E]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PTG"}, - {"43543.503206018519", "[$-3E]mmmmmm dd yyyy h:mm AM/PM", "Mac 19 2019 12:04 PTG"}, + {"43543.503206018519", "[$-3E]mmmm dd yyyy h:mm AM/PM aaa", "Mac 19 2019 12:04 PTG Sel"}, + {"43543.503206018519", "[$-3E]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PTG Sel"}, + {"43543.503206018519", "[$-3E]mmmmmm dd yyyy h:mm AM/PM dddd", "Mac 19 2019 12:04 PTG Selasa"}, {"44562.189571759256", "[$-83E]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 PG"}, {"44562.189571759256", "[$-83E]mmmm dd yyyy h:mm AM/PM", "Januari 01 2022 4:32 PG"}, {"44562.189571759256", "[$-83E]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 PG"}, {"44562.189571759256", "[$-83E]mmmmmm dd yyyy h:mm AM/PM", "Januari 01 2022 4:32 PG"}, {"43543.503206018519", "[$-83E]mmm dd yyyy h:mm AM/PM", "Mac 19 2019 12:04 PTG"}, - {"43543.503206018519", "[$-83E]mmmm dd yyyy h:mm AM/PM", "Mac 19 2019 12:04 PTG"}, - {"43543.503206018519", "[$-83E]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PTG"}, - {"43543.503206018519", "[$-83E]mmmmmm dd yyyy h:mm AM/PM", "Mac 19 2019 12:04 PTG"}, + {"43543.503206018519", "[$-83E]mmmm dd yyyy h:mm AM/PM aaa", "Mac 19 2019 12:04 PTG Sel"}, + {"43543.503206018519", "[$-83E]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PTG Sel"}, + {"43543.503206018519", "[$-83E]mmmmmm dd yyyy h:mm AM/PM dddd", "Mac 19 2019 12:04 PTG Selasa"}, {"44562.189571759256", "[$-43E]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 PG"}, {"44562.189571759256", "[$-43E]mmmm dd yyyy h:mm AM/PM", "Januari 01 2022 4:32 PG"}, {"44562.189571759256", "[$-43E]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 PG"}, {"44562.189571759256", "[$-43E]mmmmmm dd yyyy h:mm AM/PM", "Januari 01 2022 4:32 PG"}, {"43543.503206018519", "[$-43E]mmm dd yyyy h:mm AM/PM", "Mac 19 2019 12:04 PTG"}, - {"43543.503206018519", "[$-43E]mmmm dd yyyy h:mm AM/PM", "Mac 19 2019 12:04 PTG"}, - {"43543.503206018519", "[$-43E]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PTG"}, - {"43543.503206018519", "[$-43E]mmmmmm dd yyyy h:mm AM/PM", "Mac 19 2019 12:04 PTG"}, + {"43543.503206018519", "[$-43E]mmmm dd yyyy h:mm AM/PM aaa", "Mac 19 2019 12:04 PTG Sel"}, + {"43543.503206018519", "[$-43E]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PTG Sel"}, + {"43543.503206018519", "[$-43E]mmmmmm dd yyyy h:mm AM/PM dddd", "Mac 19 2019 12:04 PTG Selasa"}, {"44562.189571759256", "[$-4C]mmm dd yyyy h:mm AM/PM", "\u0D1C\u0D28\u0D41 01 2022 4:32 AM"}, {"44562.189571759256", "[$-4C]mmmm dd yyyy h:mm AM/PM", "\u0D1C\u0D28\u0D41\u0D35\u0D30\u0D3F 01 2022 4:32 AM"}, {"44562.189571759256", "[$-4C]mmmmm dd yyyy h:mm AM/PM", "\u0D1C 01 2022 4:32 AM"}, {"44562.189571759256", "[$-4C]mmmmmm dd yyyy h:mm AM/PM", "\u0D1C\u0D28\u0D41\u0D35\u0D30\u0D3F 01 2022 4:32 AM"}, {"43543.503206018519", "[$-4C]mmm dd yyyy h:mm AM/PM", "\u0D2E\u0D3E\u0D7C 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-4C]mmmm dd yyyy h:mm AM/PM", "\u0D2E\u0D3E\u0D30\u0D4D\u200D\u200C\u0D1A\u0D4D\u0D1A\u0D4D 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-4C]mmmmm dd yyyy h:mm AM/PM", "\u0D2E 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-4C]mmmmmm dd yyyy h:mm AM/PM", "\u0D2E\u0D3E\u0D30\u0D4D\u200D\u200C\u0D1A\u0D4D\u0D1A\u0D4D 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-4C]mmmm dd yyyy h:mm AM/PM aaa", "\u0D2E\u0D3E\u0D30\u0D4D\u200D\u200C\u0D1A\u0D4D\u0D1A\u0D4D 19 2019 12:04 PM \u0D1A\u0D4A\u0D35\u0D4D\u0D35"}, + {"43543.503206018519", "[$-4C]mmmmm dd yyyy h:mm AM/PM ddd", "\u0D2E 19 2019 12:04 PM \u0D1A\u0D4A\u0D35\u0D4D\u0D35"}, + {"43543.503206018519", "[$-4C]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0D2E\u0D3E\u0D30\u0D4D\u200D\u200C\u0D1A\u0D4D\u0D1A\u0D4D 19 2019 12:04 PM \u0D1A\u0D4A\u0D35\u0D4D\u0D35\u0D3E\u0D34\u0D4D\u0D1A"}, {"44562.189571759256", "[$-44C]mmm dd yyyy h:mm AM/PM", "\u0D1C\u0D28\u0D41 01 2022 4:32 AM"}, {"44562.189571759256", "[$-44C]mmmm dd yyyy h:mm AM/PM", "\u0D1C\u0D28\u0D41\u0D35\u0D30\u0D3F 01 2022 4:32 AM"}, {"44562.189571759256", "[$-44C]mmmmm dd yyyy h:mm AM/PM", "\u0D1C 01 2022 4:32 AM"}, {"44562.189571759256", "[$-44C]mmmmmm dd yyyy h:mm AM/PM", "\u0D1C\u0D28\u0D41\u0D35\u0D30\u0D3F 01 2022 4:32 AM"}, {"43543.503206018519", "[$-44C]mmm dd yyyy h:mm AM/PM", "\u0D2E\u0D3E\u0D7C 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-44C]mmmm dd yyyy h:mm AM/PM", "\u0D2E\u0D3E\u0D30\u0D4D\u200D\u200C\u0D1A\u0D4D\u0D1A\u0D4D 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-44C]mmmmm dd yyyy h:mm AM/PM", "\u0D2E 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-44C]mmmmmm dd yyyy h:mm AM/PM", "\u0D2E\u0D3E\u0D30\u0D4D\u200D\u200C\u0D1A\u0D4D\u0D1A\u0D4D 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-44C]mmmm dd yyyy h:mm AM/PM aaa", "\u0D2E\u0D3E\u0D30\u0D4D\u200D\u200C\u0D1A\u0D4D\u0D1A\u0D4D 19 2019 12:04 PM \u0D1A\u0D4A\u0D35\u0D4D\u0D35"}, + {"43543.503206018519", "[$-44C]mmmmm dd yyyy h:mm AM/PM ddd", "\u0D2E 19 2019 12:04 PM \u0D1A\u0D4A\u0D35\u0D4D\u0D35"}, + {"43543.503206018519", "[$-44C]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0D2E\u0D3E\u0D30\u0D4D\u200D\u200C\u0D1A\u0D4D\u0D1A\u0D4D 19 2019 12:04 PM \u0D1A\u0D4A\u0D35\u0D4D\u0D35\u0D3E\u0D34\u0D4D\u0D1A"}, {"44562.189571759256", "[$-3A]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"}, {"44562.189571759256", "[$-3A]mmmm dd yyyy h:mm AM/PM", "Jannar 01 2022 4:32 AM"}, {"44562.189571759256", "[$-3A]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"}, {"44562.189571759256", "[$-3A]mmmmmm dd yyyy h:mm AM/PM", "Jannar 01 2022 4:32 AM"}, {"43543.503206018519", "[$-3A]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-3A]mmmm dd yyyy h:mm AM/PM", "Marzu 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-3A]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-3A]mmmmmm dd yyyy h:mm AM/PM", "Marzu 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-3A]mmmm dd yyyy h:mm AM/PM aaa", "Marzu 19 2019 12:04 PM Tli"}, + {"43543.503206018519", "[$-3A]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM Tli"}, + {"43543.503206018519", "[$-3A]mmmmmm dd yyyy h:mm AM/PM dddd", "Marzu 19 2019 12:04 PM It-Tlieta"}, {"44562.189571759256", "[$-43A]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"}, {"44562.189571759256", "[$-43A]mmmm dd yyyy h:mm AM/PM", "Jannar 01 2022 4:32 AM"}, {"44562.189571759256", "[$-43A]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"}, {"44562.189571759256", "[$-43A]mmmmmm dd yyyy h:mm AM/PM", "Jannar 01 2022 4:32 AM"}, {"43543.503206018519", "[$-43A]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-43A]mmmm dd yyyy h:mm AM/PM", "Marzu 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-43A]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-43A]mmmmmm dd yyyy h:mm AM/PM", "Marzu 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-43A]mmmm dd yyyy h:mm AM/PM aaa", "Marzu 19 2019 12:04 PM Tli"}, + {"43543.503206018519", "[$-43A]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM Tli"}, + {"43543.503206018519", "[$-43A]mmmmmm dd yyyy h:mm AM/PM dddd", "Marzu 19 2019 12:04 PM It-Tlieta"}, {"44562.189571759256", "[$-81]mmm dd yyyy h:mm AM/PM", "Kohi 01 2022 4:32 a.m."}, {"44562.189571759256", "[$-81]mmmm dd yyyy h:mm AM/PM", "Kohitātea 01 2022 4:32 a.m."}, {"44562.189571759256", "[$-81]mmmmm dd yyyy h:mm AM/PM", "K 01 2022 4:32 a.m."}, {"44562.189571759256", "[$-81]mmmmmm dd yyyy h:mm AM/PM", "Kohitātea 01 2022 4:32 a.m."}, {"43543.503206018519", "[$-81]mmm dd yyyy h:mm AM/PM", "Pou 19 2019 12:04 p.m."}, - {"43543.503206018519", "[$-81]mmmm dd yyyy h:mm AM/PM", "Poutūterangi 19 2019 12:04 p.m."}, - {"43543.503206018519", "[$-81]mmmmm dd yyyy h:mm AM/PM", "P 19 2019 12:04 p.m."}, - {"43543.503206018519", "[$-81]mmmmmm dd yyyy h:mm AM/PM", "Poutūterangi 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-81]mmmm dd yyyy h:mm AM/PM aaa", "Poutūterangi 19 2019 12:04 p.m. Tū"}, + {"43543.503206018519", "[$-81]mmmmm dd yyyy h:mm AM/PM ddd", "P 19 2019 12:04 p.m. Tū"}, + {"43543.503206018519", "[$-81]mmmmmm dd yyyy h:mm AM/PM dddd", "Poutūterangi 19 2019 12:04 p.m. Rātū"}, {"44562.189571759256", "[$-481]mmm dd yyyy h:mm AM/PM", "Kohi 01 2022 4:32 a.m."}, {"44562.189571759256", "[$-481]mmmm dd yyyy h:mm AM/PM", "Kohitātea 01 2022 4:32 a.m."}, {"44562.189571759256", "[$-481]mmmmm dd yyyy h:mm AM/PM", "K 01 2022 4:32 a.m."}, {"44562.189571759256", "[$-481]mmmmmm dd yyyy h:mm AM/PM", "Kohitātea 01 2022 4:32 a.m."}, {"43543.503206018519", "[$-481]mmm dd yyyy h:mm AM/PM", "Pou 19 2019 12:04 p.m."}, - {"43543.503206018519", "[$-481]mmmm dd yyyy h:mm AM/PM", "Poutūterangi 19 2019 12:04 p.m."}, - {"43543.503206018519", "[$-481]mmmmm dd yyyy h:mm AM/PM", "P 19 2019 12:04 p.m."}, - {"43543.503206018519", "[$-481]mmmmmm dd yyyy h:mm AM/PM", "Poutūterangi 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-481]mmmm dd yyyy h:mm AM/PM aaa", "Poutūterangi 19 2019 12:04 p.m. Tū"}, + {"43543.503206018519", "[$-481]mmmmm dd yyyy h:mm AM/PM ddd", "P 19 2019 12:04 p.m. Tū"}, + {"43543.503206018519", "[$-481]mmmmmm dd yyyy h:mm AM/PM dddd", "Poutūterangi 19 2019 12:04 p.m. Rātū"}, {"44562.189571759256", "[$-7A]mmm dd yyyy h:mm AM/PM", "Kiñe Tripantu 01 2022 4:32 AM"}, {"44562.189571759256", "[$-7A]mmmm dd yyyy h:mm AM/PM", "Kiñe Tripantu 01 2022 4:32 AM"}, {"44562.189571759256", "[$-7A]mmmmm dd yyyy h:mm AM/PM", "K 01 2022 4:32 AM"}, {"44562.189571759256", "[$-7A]mmmmmm dd yyyy h:mm AM/PM", "Kiñe Tripantu 01 2022 4:32 AM"}, {"43543.503206018519", "[$-7A]mmm dd yyyy h:mm AM/PM", "Kila 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-7A]mmmm dd yyyy h:mm AM/PM", "Kila 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-7A]mmmmm dd yyyy h:mm AM/PM", "K 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-7A]mmmmmm dd yyyy h:mm AM/PM", "Kila 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-7A]mmmm dd yyyy h:mm AM/PM aaa", "Kila 19 2019 12:04 PM Kila"}, + {"43543.503206018519", "[$-7A]mmmmm dd yyyy h:mm AM/PM ddd", "K 19 2019 12:04 PM Kila"}, + {"43543.503206018519", "[$-7A]mmmmmm dd yyyy h:mm AM/PM dddd", "Kila 19 2019 12:04 PM Kila Ante"}, {"44562.189571759256", "[$-47A]mmm dd yyyy h:mm AM/PM", "Kiñe Tripantu 01 2022 4:32 AM"}, {"44562.189571759256", "[$-47A]mmmm dd yyyy h:mm AM/PM", "Kiñe Tripantu 01 2022 4:32 AM"}, {"44562.189571759256", "[$-47A]mmmmm dd yyyy h:mm AM/PM", "K 01 2022 4:32 AM"}, {"44562.189571759256", "[$-47A]mmmmmm dd yyyy h:mm AM/PM", "Kiñe Tripantu 01 2022 4:32 AM"}, {"43543.503206018519", "[$-47A]mmm dd yyyy h:mm AM/PM", "Kila 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-47A]mmmm dd yyyy h:mm AM/PM", "Kila 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-47A]mmmmm dd yyyy h:mm AM/PM", "K 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-47A]mmmmmm dd yyyy h:mm AM/PM", "Kila 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-47A]mmmm dd yyyy h:mm AM/PM aaa", "Kila 19 2019 12:04 PM Kila"}, + {"43543.503206018519", "[$-47A]mmmmm dd yyyy h:mm AM/PM ddd", "K 19 2019 12:04 PM Kila"}, + {"43543.503206018519", "[$-47A]mmmmmm dd yyyy h:mm AM/PM dddd", "Kila 19 2019 12:04 PM Kila Ante"}, {"44562.189571759256", "[$-4E]mmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u0947. 01 2022 4:32 \u092E.\u092A\u0942."}, {"44562.189571759256", "[$-4E]mmmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u0947\u0935\u093E\u0930\u0940 01 2022 4:32 \u092E.\u092A\u0942."}, {"44562.189571759256", "[$-4E]mmmmm dd yyyy h:mm AM/PM", "\u091C 01 2022 4:32 \u092E.\u092A\u0942."}, {"44562.189571759256", "[$-4E]mmmmmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u0947\u0935\u093E\u0930\u0940 01 2022 4:32 \u092E.\u092A\u0942."}, {"43543.503206018519", "[$-4E]mmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E.\u0928\u0902."}, - {"43543.503206018519", "[$-4E]mmmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E.\u0928\u0902."}, - {"43543.503206018519", "[$-4E]mmmmm dd yyyy h:mm AM/PM", "\u092E 19 2019 12:04 \u092E.\u0928\u0902."}, - {"43543.503206018519", "[$-4E]mmmmmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E.\u0928\u0902."}, + {"43543.503206018519", "[$-4E]mmmm dd yyyy h:mm AM/PM aaa", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E.\u0928\u0902. \u092E\u0902\u0917\u0933."}, + {"43543.503206018519", "[$-4E]mmmmm dd yyyy h:mm AM/PM ddd", "\u092E 19 2019 12:04 \u092E.\u0928\u0902. \u092E\u0902\u0917\u0933."}, + {"43543.503206018519", "[$-4E]mmmmmm dd yyyy h:mm AM/PM dddd", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E.\u0928\u0902. \u092E\u0902\u0917\u0933\u0935\u093E\u0930"}, {"44562.189571759256", "[$-44E]mmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u0947. 01 2022 4:32 \u092E.\u092A\u0942."}, {"44562.189571759256", "[$-44E]mmmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u0947\u0935\u093E\u0930\u0940 01 2022 4:32 \u092E.\u092A\u0942."}, {"44562.189571759256", "[$-44E]mmmmm dd yyyy h:mm AM/PM", "\u091C 01 2022 4:32 \u092E.\u092A\u0942."}, {"44562.189571759256", "[$-44E]mmmmmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u0947\u0935\u093E\u0930\u0940 01 2022 4:32 \u092E.\u092A\u0942."}, {"43543.503206018519", "[$-44E]mmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E.\u0928\u0902."}, + {"43543.503206018519", "[$-44E]mmmm dd yyyy h:mm AM/PM aaa", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E.\u0928\u0902. \u092E\u0902\u0917\u0933."}, + {"43543.503206018519", "[$-44E]mmmmm dd yyyy h:mm AM/PM ddd", "\u092E 19 2019 12:04 \u092E.\u0928\u0902. \u092E\u0902\u0917\u0933."}, + {"43543.503206018519", "[$-44E]mmmmmm dd yyyy h:mm AM/PM dddd", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E.\u0928\u0902. \u092E\u0902\u0917\u0933\u0935\u093E\u0930"}, {"43543.503206018519", "[$-44E]mmmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E.\u0928\u0902."}, {"43543.503206018519", "[$-44E]mmmmm dd yyyy h:mm AM/PM", "\u092E 19 2019 12:04 \u092E.\u0928\u0902."}, {"43543.503206018519", "[$-44E]mmmmmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E.\u0928\u0902."}, @@ -1074,179 +1563,171 @@ func TestNumFmt(t *testing.T) { {"44562.189571759256", "[$-7C]mmmmm dd yyyy h:mm AM/PM", "T 01 2022 4:32 AM"}, {"44562.189571759256", "[$-7C]mmmmmm dd yyyy h:mm AM/PM", "Tsothohrkó:Wa 01 2022 4:32 AM"}, {"43543.503206018519", "[$-7C]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-7C]mmmm dd yyyy h:mm AM/PM", "Enniskó:Wa 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-7C]mmmmm dd yyyy h:mm AM/PM", "E 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-7C]mmmmmm dd yyyy h:mm AM/PM", "Enniskó:Wa 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-7C]mmmm dd yyyy h:mm AM/PM aaa", "Enniskó:Wa 19 2019 12:04 PM Tue"}, + {"43543.503206018519", "[$-7C]mmmmm dd yyyy h:mm AM/PM ddd", "E 19 2019 12:04 PM Tue"}, + {"43543.503206018519", "[$-7C]mmmmmm dd yyyy h:mm AM/PM dddd", "Enniskó:Wa 19 2019 12:04 PM Ratironhia'kehronòn:ke"}, {"44562.189571759256", "[$-47C]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"}, {"44562.189571759256", "[$-47C]mmmm dd yyyy h:mm AM/PM", "Tsothohrkó:Wa 01 2022 4:32 AM"}, {"44562.189571759256", "[$-47C]mmmmm dd yyyy h:mm AM/PM", "T 01 2022 4:32 AM"}, {"44562.189571759256", "[$-47C]mmmmmm dd yyyy h:mm AM/PM", "Tsothohrkó:Wa 01 2022 4:32 AM"}, {"43543.503206018519", "[$-47C]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-47C]mmmm dd yyyy h:mm AM/PM", "Enniskó:Wa 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-47C]mmmmm dd yyyy h:mm AM/PM", "E 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-47C]mmmmmm dd yyyy h:mm AM/PM", "Enniskó:Wa 19 2019 12:04 PM"}, - {"44562.189571759256", "[$-44E]mmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u0947. 01 2022 4:32 \u092E.\u092A\u0942."}, - {"44562.189571759256", "[$-44E]mmmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u0947\u0935\u093E\u0930\u0940 01 2022 4:32 \u092E.\u092A\u0942."}, - {"44562.189571759256", "[$-44E]mmmmm dd yyyy h:mm AM/PM", "\u091C 01 2022 4:32 \u092E.\u092A\u0942."}, - {"44562.189571759256", "[$-44E]mmmmmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u0947\u0935\u093E\u0930\u0940 01 2022 4:32 \u092E.\u092A\u0942."}, - {"43543.503206018519", "[$-44E]mmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E.\u0928\u0902."}, - {"43543.503206018519", "[$-44E]mmmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E.\u0928\u0902."}, - {"43543.503206018519", "[$-44E]mmmmm dd yyyy h:mm AM/PM", "\u092E 19 2019 12:04 \u092E.\u0928\u0902."}, - {"43543.503206018519", "[$-44E]mmmmmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E.\u0928\u0902."}, + {"43543.503206018519", "[$-47C]mmmm dd yyyy h:mm AM/PM aaa", "Enniskó:Wa 19 2019 12:04 PM Tue"}, + {"43543.503206018519", "[$-47C]mmmmm dd yyyy h:mm AM/PM ddd", "E 19 2019 12:04 PM Tue"}, + {"43543.503206018519", "[$-47C]mmmmmm dd yyyy h:mm AM/PM dddd", "Enniskó:Wa 19 2019 12:04 PM Ratironhia'kehronòn:ke"}, {"44562.189571759256", "[$-50]mmm dd yyyy h:mm AM/PM", "1-р сар 01 2022 4:32 \u04AF.\u04E9."}, {"44562.189571759256", "[$-50]mmmm dd yyyy h:mm AM/PM", "\u041D\u044D\u0433\u0434\u04AF\u0433\u044D\u044D\u0440 \u0441\u0430\u0440 01 2022 4:32 \u04AF.\u04E9."}, {"44562.189571759256", "[$-50]mmmmm dd yyyy h:mm AM/PM", "\u041D 01 2022 4:32 \u04AF.\u04E9."}, {"44562.189571759256", "[$-50]mmmmmm dd yyyy h:mm AM/PM", "\u041D\u044D\u0433\u0434\u04AF\u0433\u044D\u044D\u0440 \u0441\u0430\u0440 01 2022 4:32 \u04AF.\u04E9."}, {"43543.503206018519", "[$-50]mmm dd yyyy h:mm AM/PM", "3-р сар 19 2019 12:04 \u04AF.\u0445."}, - {"43543.503206018519", "[$-50]mmmm dd yyyy h:mm AM/PM", "\u0413\u0443\u0440\u0430\u0432\u0434\u0443\u0433\u0430\u0430\u0440 \u0441\u0430\u0440 19 2019 12:04 \u04AF.\u0445."}, - {"43543.503206018519", "[$-50]mmmmm dd yyyy h:mm AM/PM", "\u0413 19 2019 12:04 \u04AF.\u0445."}, - {"43543.503206018519", "[$-50]mmmmmm dd yyyy h:mm AM/PM", "\u0413\u0443\u0440\u0430\u0432\u0434\u0443\u0433\u0430\u0430\u0440 \u0441\u0430\u0440 19 2019 12:04 \u04AF.\u0445."}, + {"43543.503206018519", "[$-50]mmmm dd yyyy h:mm AM/PM aaa", "\u0413\u0443\u0440\u0430\u0432\u0434\u0443\u0433\u0430\u0430\u0440 \u0441\u0430\u0440 19 2019 12:04 \u04AF.\u0445. \u041C\u044F"}, + {"43543.503206018519", "[$-50]mmmmm dd yyyy h:mm AM/PM ddd", "\u0413 19 2019 12:04 \u04AF.\u0445. \u041C\u044F"}, + {"43543.503206018519", "[$-50]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0413\u0443\u0440\u0430\u0432\u0434\u0443\u0433\u0430\u0430\u0440 \u0441\u0430\u0440 19 2019 12:04 \u04AF.\u0445. \u043C\u044F\u0433\u043C\u0430\u0440"}, {"44562.189571759256", "[$-7850]mmm dd yyyy h:mm AM/PM", "1-р сар 01 2022 4:32 \u04AF.\u04E9."}, {"44562.189571759256", "[$-7850]mmmm dd yyyy h:mm AM/PM", "\u041D\u044D\u0433\u0434\u04AF\u0433\u044D\u044D\u0440 \u0441\u0430\u0440 01 2022 4:32 \u04AF.\u04E9."}, {"44562.189571759256", "[$-7850]mmmmm dd yyyy h:mm AM/PM", "\u041D 01 2022 4:32 \u04AF.\u04E9."}, {"44562.189571759256", "[$-7850]mmmmmm dd yyyy h:mm AM/PM", "\u041D\u044D\u0433\u0434\u04AF\u0433\u044D\u044D\u0440 \u0441\u0430\u0440 01 2022 4:32 \u04AF.\u04E9."}, {"43543.503206018519", "[$-7850]mmm dd yyyy h:mm AM/PM", "3-р сар 19 2019 12:04 \u04AF.\u0445."}, - {"43543.503206018519", "[$-7850]mmmm dd yyyy h:mm AM/PM", "\u0413\u0443\u0440\u0430\u0432\u0434\u0443\u0433\u0430\u0430\u0440 \u0441\u0430\u0440 19 2019 12:04 \u04AF.\u0445."}, - {"43543.503206018519", "[$-7850]mmmmm dd yyyy h:mm AM/PM", "\u0413 19 2019 12:04 \u04AF.\u0445."}, - {"43543.503206018519", "[$-7850]mmmmmm dd yyyy h:mm AM/PM", "\u0413\u0443\u0440\u0430\u0432\u0434\u0443\u0433\u0430\u0430\u0440 \u0441\u0430\u0440 19 2019 12:04 \u04AF.\u0445."}, + {"43543.503206018519", "[$-7850]mmmm dd yyyy h:mm AM/PM aaa", "\u0413\u0443\u0440\u0430\u0432\u0434\u0443\u0433\u0430\u0430\u0440 \u0441\u0430\u0440 19 2019 12:04 \u04AF.\u0445. \u041C\u044F"}, + {"43543.503206018519", "[$-7850]mmmmm dd yyyy h:mm AM/PM ddd", "\u0413 19 2019 12:04 \u04AF.\u0445. \u041C\u044F"}, + {"43543.503206018519", "[$-7850]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0413\u0443\u0440\u0430\u0432\u0434\u0443\u0433\u0430\u0430\u0440 \u0441\u0430\u0440 19 2019 12:04 \u04AF.\u0445. \u043C\u044F\u0433\u043C\u0430\u0440"}, {"44562.189571759256", "[$-450]mmm dd yyyy h:mm AM/PM", "1-р сар 01 2022 4:32 \u04AF.\u04E9."}, {"44562.189571759256", "[$-450]mmmm dd yyyy h:mm AM/PM", "\u041D\u044D\u0433\u0434\u04AF\u0433\u044D\u044D\u0440 \u0441\u0430\u0440 01 2022 4:32 \u04AF.\u04E9."}, {"44562.189571759256", "[$-450]mmmmm dd yyyy h:mm AM/PM", "\u041D 01 2022 4:32 \u04AF.\u04E9."}, {"44562.189571759256", "[$-450]mmmmmm dd yyyy h:mm AM/PM", "\u041D\u044D\u0433\u0434\u04AF\u0433\u044D\u044D\u0440 \u0441\u0430\u0440 01 2022 4:32 \u04AF.\u04E9."}, {"43543.503206018519", "[$-450]mmm dd yyyy h:mm AM/PM", "3-р сар 19 2019 12:04 \u04AF.\u0445."}, - {"43543.503206018519", "[$-450]mmmm dd yyyy h:mm AM/PM", "\u0413\u0443\u0440\u0430\u0432\u0434\u0443\u0433\u0430\u0430\u0440 \u0441\u0430\u0440 19 2019 12:04 \u04AF.\u0445."}, - {"43543.503206018519", "[$-450]mmmmm dd yyyy h:mm AM/PM", "\u0413 19 2019 12:04 \u04AF.\u0445."}, - {"43543.503206018519", "[$-450]mmmmmm dd yyyy h:mm AM/PM", "\u0413\u0443\u0440\u0430\u0432\u0434\u0443\u0433\u0430\u0430\u0440 \u0441\u0430\u0440 19 2019 12:04 \u04AF.\u0445."}, + {"43543.503206018519", "[$-450]mmmm dd yyyy h:mm AM/PM aaa", "\u0413\u0443\u0440\u0430\u0432\u0434\u0443\u0433\u0430\u0430\u0440 \u0441\u0430\u0440 19 2019 12:04 \u04AF.\u0445. \u041C\u044F"}, + {"43543.503206018519", "[$-450]mmmmm dd yyyy h:mm AM/PM ddd", "\u0413 19 2019 12:04 \u04AF.\u0445. \u041C\u044F"}, + {"43543.503206018519", "[$-450]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0413\u0443\u0440\u0430\u0432\u0434\u0443\u0433\u0430\u0430\u0440 \u0441\u0430\u0440 19 2019 12:04 \u04AF.\u0445. \u043C\u044F\u0433\u043C\u0430\u0440"}, {"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"}, - {"44896.18957170139", "[$-7C50]mmmm dd yyyy h:mm AM/PM", "M12 01 2022 4:32 AM"}, - {"44562.189571759256", "[$-7C50]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 AM"}, - {"44896.18957170139", "[$-7C50]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 AM"}, + {"44896.18957170139", "[$-7C50]mmmm dd yyyy h:mm AM/PM aaa", "M12 01 2022 4:32 AM \u182D\u1820\u1837\u1820\u182D\u202F\u1824\u1828%20\u1833\u1825\u1837\u182A\u1821\u1828"}, + {"44562.189571759256", "[$-7C50]mmmmm dd yyyy h:mm AM/PM ddd", "M 01 2022 4:32 AM \u182D\u1820\u1837\u1820\u182D\u202F\u1824\u1828%20\u1835\u1822\u1837\u182D\u1824\u182D\u1820\u1828"}, + {"44896.18957170139", "[$-7C50]mmmmm dd yyyy h:mm AM/PM dddd", "M 01 2022 4:32 AM \u182D\u1820\u1837\u1820\u182D\u202F\u1824\u1828%20\u1833\u1825\u1837\u182A\u1821\u1828"}, {"44562.189571759256", "[$-850]mmm dd yyyy h:mm AM/PM", "M01 01 2022 4:32 AM"}, {"44896.18957170139", "[$-850]mmm dd yyyy h:mm AM/PM", "M12 01 2022 4:32 AM"}, {"44562.189571759256", "[$-850]mmmm dd yyyy h:mm AM/PM", "M01 01 2022 4:32 AM"}, - {"44896.18957170139", "[$-850]mmmm dd yyyy h:mm AM/PM", "M12 01 2022 4:32 AM"}, - {"44562.189571759256", "[$-850]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 AM"}, - {"44896.18957170139", "[$-850]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 AM"}, + {"44896.18957170139", "[$-850]mmmm dd yyyy h:mm AM/PM aaa", "M12 01 2022 4:32 AM \u182D\u1820\u1837\u1820\u182D\u202F\u1824\u1828%20\u1833\u1825\u1837\u182A\u1821\u1828"}, + {"44562.189571759256", "[$-850]mmmmm dd yyyy h:mm AM/PM ddd", "M 01 2022 4:32 AM \u182D\u1820\u1837\u1820\u182D\u202F\u1824\u1828%20\u1835\u1822\u1837\u182D\u1824\u182D\u1820\u1828"}, + {"44896.18957170139", "[$-850]mmmmm dd yyyy h:mm AM/PM dddd", "M 01 2022 4:32 AM \u182D\u1820\u1837\u1820\u182D\u202F\u1824\u1828%20\u1833\u1825\u1837\u182A\u1821\u1828"}, {"44562.189571759256", "[$-C50]mmm dd yyyy h:mm AM/PM", "M01 01 2022 4:32 AM"}, {"44896.18957170139", "[$-C50]mmm dd yyyy h:mm AM/PM", "M12 01 2022 4:32 AM"}, {"44562.189571759256", "[$-C50]mmmm dd yyyy h:mm AM/PM", "M01 01 2022 4:32 AM"}, - {"44896.18957170139", "[$-C50]mmmm dd yyyy h:mm AM/PM", "M12 01 2022 4:32 AM"}, - {"44562.189571759256", "[$-C50]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 AM"}, - {"44896.18957170139", "[$-C50]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 AM"}, + {"44896.18957170139", "[$-C50]mmmm dd yyyy h:mm AM/PM aaa", "M12 01 2022 4:32 AM \u182B\u1826\u1837\u182A\u1826"}, + {"44562.189571759256", "[$-C50]mmmmm dd yyyy h:mm AM/PM ddd", "M 01 2022 4:32 AM \u182A\u1822\u182E\u182A\u1820"}, + {"44896.18957170139", "[$-C50]mmmmm dd yyyy h:mm AM/PM dddd", "M 01 2022 4:32 AM \u182B\u1826\u1837\u182A\u1826"}, {"44562.189571759256", "[$-61]mmm dd yyyy h:mm AM/PM", "\u091C\u0928 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"}, {"44562.189571759256", "[$-61]mmmm dd yyyy h:mm AM/PM", "\u091C\u0928\u0935\u0930\u0940 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"}, {"44562.189571759256", "[$-61]mmmmm dd yyyy h:mm AM/PM", "\u091C 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"}, {"44562.189571759256", "[$-61]mmmmmm dd yyyy h:mm AM/PM", "\u091C\u0928\u0935\u0930\u0940 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"}, {"43543.503206018519", "[$-61]mmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928"}, - {"43543.503206018519", "[$-61]mmmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928"}, - {"43543.503206018519", "[$-61]mmmmm dd yyyy h:mm AM/PM", "\u092E 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928"}, - {"43543.503206018519", "[$-61]mmmmmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928"}, + {"43543.503206018519", "[$-61]mmmm dd yyyy h:mm AM/PM aaa", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928 \u092E\u0919\u094D\u0917\u0932"}, + {"43543.503206018519", "[$-61]mmmmm dd yyyy h:mm AM/PM ddd", "\u092E 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928 \u092E\u0919\u094D\u0917\u0932"}, + {"43543.503206018519", "[$-61]mmmmmm dd yyyy h:mm AM/PM dddd", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928 \u092E\u0919\u094D\u0917\u0932\u0935\u093E\u0930"}, {"44562.189571759256", "[$-861]mmm dd yyyy h:mm AM/PM", "\u091C\u0928\u0935\u0930\u0940 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"}, {"44562.189571759256", "[$-861]mmmm dd yyyy h:mm AM/PM", "\u091C\u0928\u0935\u0930\u0940 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"}, {"44562.189571759256", "[$-861]mmmmm dd yyyy h:mm AM/PM", "\u091C 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"}, {"44562.189571759256", "[$-861]mmmmmm dd yyyy h:mm AM/PM", "\u091C\u0928\u0935\u0930\u0940 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"}, {"43543.503206018519", "[$-861]mmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928"}, - {"43543.503206018519", "[$-861]mmmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928"}, - {"43543.503206018519", "[$-861]mmmmm dd yyyy h:mm AM/PM", "\u092E 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928"}, - {"43543.503206018519", "[$-861]mmmmmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928"}, + {"43543.503206018519", "[$-861]mmmm dd yyyy h:mm AM/PM aaa", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928 \u092E\u0919\u094D\u0917\u0932"}, + {"43543.503206018519", "[$-861]mmmmm dd yyyy h:mm AM/PM ddd", "\u092E 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928 \u092E\u0919\u094D\u0917\u0932"}, + {"43543.503206018519", "[$-861]mmmmmm dd yyyy h:mm AM/PM dddd", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928 \u092E\u0919\u094D\u0917\u0932\u092C\u093E\u0930"}, {"44562.189571759256", "[$-461]mmm dd yyyy h:mm AM/PM", "\u091C\u0928 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"}, {"44562.189571759256", "[$-461]mmmm dd yyyy h:mm AM/PM", "\u091C\u0928\u0935\u0930\u0940 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"}, {"44562.189571759256", "[$-461]mmmmm dd yyyy h:mm AM/PM", "\u091C 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"}, {"44562.189571759256", "[$-461]mmmmmm dd yyyy h:mm AM/PM", "\u091C\u0928\u0935\u0930\u0940 01 2022 4:32 \u092A\u0942\u0930\u094D\u0935\u093E\u0939\u094D\u0928"}, {"43543.503206018519", "[$-461]mmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928"}, - {"43543.503206018519", "[$-461]mmmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928"}, - {"43543.503206018519", "[$-461]mmmmm dd yyyy h:mm AM/PM", "\u092E 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928"}, - {"43543.503206018519", "[$-461]mmmmmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928"}, + {"43543.503206018519", "[$-461]mmmm dd yyyy h:mm AM/PM aaa", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928 \u092E\u0919\u094D\u0917\u0932"}, + {"43543.503206018519", "[$-461]mmmmm dd yyyy h:mm AM/PM ddd", "\u092E 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928 \u092E\u0919\u094D\u0917\u0932"}, + {"43543.503206018519", "[$-461]mmmmmm dd yyyy h:mm AM/PM dddd", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u0905\u092A\u0930\u093E\u0939\u094D\u0928 \u092E\u0919\u094D\u0917\u0932\u0935\u093E\u0930"}, {"44562.189571759256", "[$-14]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 a.m."}, {"44562.189571759256", "[$-14]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 a.m."}, {"44562.189571759256", "[$-14]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 a.m."}, {"44562.189571759256", "[$-14]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 a.m."}, {"43543.503206018519", "[$-14]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 p.m."}, - {"43543.503206018519", "[$-14]mmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 p.m."}, - {"43543.503206018519", "[$-14]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 p.m."}, - {"43543.503206018519", "[$-14]mmmmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-14]mmmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 p.m. tir."}, + {"43543.503206018519", "[$-14]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 p.m. tir."}, + {"43543.503206018519", "[$-14]mmmmmm dd yyyy h:mm AM/PM dddd", "mars 19 2019 12:04 p.m. tirsdag"}, {"44562.189571759256", "[$-7C14]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 a.m."}, {"44562.189571759256", "[$-7C14]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 a.m."}, {"44562.189571759256", "[$-7C14]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 a.m."}, {"44562.189571759256", "[$-7C14]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 a.m."}, {"43543.503206018519", "[$-7C14]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 p.m."}, - {"43543.503206018519", "[$-7C14]mmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 p.m."}, - {"43543.503206018519", "[$-7C14]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 p.m."}, - {"43543.503206018519", "[$-7C14]mmmmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-7C14]mmmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 p.m. tir"}, + {"43543.503206018519", "[$-7C14]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 p.m. tir"}, + {"43543.503206018519", "[$-7C14]mmmmmm dd yyyy h:mm AM/PM dddd", "mars 19 2019 12:04 p.m. tirsdag"}, {"44562.189571759256", "[$-414]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 a.m."}, {"44562.189571759256", "[$-414]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 a.m."}, {"44562.189571759256", "[$-414]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 a.m."}, {"44562.189571759256", "[$-414]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 a.m."}, {"43543.503206018519", "[$-414]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 p.m."}, - {"43543.503206018519", "[$-414]mmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 p.m."}, - {"43543.503206018519", "[$-414]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 p.m."}, - {"43543.503206018519", "[$-414]mmmmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-414]mmmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 p.m. tir"}, + {"43543.503206018519", "[$-414]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 p.m. tir"}, + {"43543.503206018519", "[$-414]mmmmmm dd yyyy h:mm AM/PM dddd", "mars 19 2019 12:04 p.m. tirsdag"}, {"44562.189571759256", "[$-7814]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 f.m."}, {"44562.189571759256", "[$-7814]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 f.m."}, {"44562.189571759256", "[$-7814]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 f.m."}, {"44562.189571759256", "[$-7814]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 f.m."}, {"43543.503206018519", "[$-7814]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 e.m."}, - {"43543.503206018519", "[$-7814]mmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 e.m."}, - {"43543.503206018519", "[$-7814]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 e.m."}, - {"43543.503206018519", "[$-7814]mmmmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 e.m."}, + {"43543.503206018519", "[$-7814]mmmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 e.m. tys"}, + {"43543.503206018519", "[$-7814]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 e.m. tys"}, + {"43543.503206018519", "[$-7814]mmmmmm dd yyyy h:mm AM/PM dddd", "mars 19 2019 12:04 e.m. tysdag"}, {"44562.189571759256", "[$-814]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 f.m."}, {"44562.189571759256", "[$-814]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 f.m."}, {"44562.189571759256", "[$-814]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 f.m."}, {"44562.189571759256", "[$-814]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 f.m."}, {"43543.503206018519", "[$-814]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 e.m."}, - {"43543.503206018519", "[$-814]mmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 e.m."}, - {"43543.503206018519", "[$-814]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 e.m."}, - {"43543.503206018519", "[$-814]mmmmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 e.m."}, + {"43543.503206018519", "[$-814]mmmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 e.m. tys"}, + {"43543.503206018519", "[$-814]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 e.m. tys"}, + {"43543.503206018519", "[$-814]mmmmmm dd yyyy h:mm AM/PM dddd", "mars 19 2019 12:04 e.m. tysdag"}, {"44562.189571759256", "[$-82]mmm dd yyyy h:mm AM/PM", "gen. 01 2022 4:32 AM"}, {"44562.189571759256", "[$-82]mmmm dd yyyy h:mm AM/PM", "genièr 01 2022 4:32 AM"}, {"44562.189571759256", "[$-82]mmmmm dd yyyy h:mm AM/PM", "g 01 2022 4:32 AM"}, {"44562.189571759256", "[$-82]mmmmmm dd yyyy h:mm AM/PM", "genièr 01 2022 4:32 AM"}, {"43543.503206018519", "[$-82]mmm dd yyyy h:mm AM/PM", "març 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-82]mmmm dd yyyy h:mm AM/PM", "març 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-82]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-82]mmmmmm dd yyyy h:mm AM/PM", "març 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-82]mmmm dd yyyy h:mm AM/PM aaa", "març 19 2019 12:04 PM dma."}, + {"43543.503206018519", "[$-82]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM dma."}, + {"43543.503206018519", "[$-82]mmmmmm dd yyyy h:mm AM/PM dddd", "març 19 2019 12:04 PM dimarts"}, {"44562.189571759256", "[$-482]mmm dd yyyy h:mm AM/PM", "gen. 01 2022 4:32 AM"}, {"44562.189571759256", "[$-482]mmmm dd yyyy h:mm AM/PM", "genièr 01 2022 4:32 AM"}, {"44562.189571759256", "[$-482]mmmmm dd yyyy h:mm AM/PM", "g 01 2022 4:32 AM"}, {"44562.189571759256", "[$-482]mmmmmm dd yyyy h:mm AM/PM", "genièr 01 2022 4:32 AM"}, {"43543.503206018519", "[$-482]mmm dd yyyy h:mm AM/PM", "març 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-482]mmmm dd yyyy h:mm AM/PM", "març 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-482]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-482]mmmmmm dd yyyy h:mm AM/PM", "març 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-482]mmmm dd yyyy h:mm AM/PM aaa", "març 19 2019 12:04 PM dma."}, + {"43543.503206018519", "[$-482]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM dma."}, + {"43543.503206018519", "[$-482]mmmmmm dd yyyy h:mm AM/PM dddd", "març 19 2019 12:04 PM dimarts"}, {"44562.189571759256", "[$-48]mmm dd yyyy h:mm AM/PM", "\u0B1C\u0B3E\u0B28\u0B41\u0B5F\u0B3E\u0B30\u0B40 01 2022 4:32 AM"}, {"44562.189571759256", "[$-48]mmmm dd yyyy h:mm AM/PM", "\u0B1C\u0B3E\u0B28\u0B41\u0B5F\u0B3E\u0B30\u0B40 01 2022 4:32 AM"}, {"44562.189571759256", "[$-48]mmmmm dd yyyy h:mm AM/PM", "\u0B1C 01 2022 4:32 AM"}, {"44562.189571759256", "[$-48]mmmmmm dd yyyy h:mm AM/PM", "\u0B1C\u0B3E\u0B28\u0B41\u0B5F\u0B3E\u0B30\u0B40 01 2022 4:32 AM"}, {"43543.503206018519", "[$-48]mmm dd yyyy h:mm AM/PM", "\u0B2E\u0B3E\u0B30\u0B4D\u0B1A\u0B4D\u0B1A 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-48]mmmm dd yyyy h:mm AM/PM", "\u0B2E\u0B3E\u0B30\u0B4D\u0B1A\u0B4D\u0B1A 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-48]mmmmm dd yyyy h:mm AM/PM", "\u0B2E 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-48]mmmmmm dd yyyy h:mm AM/PM", "\u0B2E\u0B3E\u0B30\u0B4D\u0B1A\u0B4D\u0B1A 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-48]mmmm dd yyyy h:mm AM/PM aaa", "\u0B2E\u0B3E\u0B30\u0B4D\u0B1A\u0B4D\u0B1A 19 2019 12:04 PM \u0B2E\u0B19\u0B4D\u0B17\u0B33."}, + {"43543.503206018519", "[$-48]mmmmm dd yyyy h:mm AM/PM ddd", "\u0B2E 19 2019 12:04 PM \u0B2E\u0B19\u0B4D\u0B17\u0B33."}, + {"43543.503206018519", "[$-48]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0B2E\u0B3E\u0B30\u0B4D\u0B1A\u0B4D\u0B1A 19 2019 12:04 PM \u0B2E\u0B19\u0B4D\u0B17\u0B33\u0B2C\u0B3E\u0B30"}, {"44562.189571759256", "[$-448]mmm dd yyyy h:mm AM/PM", "\u0B1C\u0B3E\u0B28\u0B41\u0B5F\u0B3E\u0B30\u0B40 01 2022 4:32 AM"}, {"44562.189571759256", "[$-448]mmmm dd yyyy h:mm AM/PM", "\u0B1C\u0B3E\u0B28\u0B41\u0B5F\u0B3E\u0B30\u0B40 01 2022 4:32 AM"}, {"44562.189571759256", "[$-448]mmmmm dd yyyy h:mm AM/PM", "\u0B1C 01 2022 4:32 AM"}, {"44562.189571759256", "[$-448]mmmmmm dd yyyy h:mm AM/PM", "\u0B1C\u0B3E\u0B28\u0B41\u0B5F\u0B3E\u0B30\u0B40 01 2022 4:32 AM"}, {"43543.503206018519", "[$-448]mmm dd yyyy h:mm AM/PM", "\u0B2E\u0B3E\u0B30\u0B4D\u0B1A\u0B4D\u0B1A 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-448]mmmm dd yyyy h:mm AM/PM", "\u0B2E\u0B3E\u0B30\u0B4D\u0B1A\u0B4D\u0B1A 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-448]mmmmm dd yyyy h:mm AM/PM", "\u0B2E 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-448]mmmmmm dd yyyy h:mm AM/PM", "\u0B2E\u0B3E\u0B30\u0B4D\u0B1A\u0B4D\u0B1A 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-448]mmmm dd yyyy h:mm AM/PM aaa", "\u0B2E\u0B3E\u0B30\u0B4D\u0B1A\u0B4D\u0B1A 19 2019 12:04 PM \u0B2E\u0B19\u0B4D\u0B17\u0B33."}, + {"43543.503206018519", "[$-448]mmmmm dd yyyy h:mm AM/PM ddd", "\u0B2E 19 2019 12:04 PM \u0B2E\u0B19\u0B4D\u0B17\u0B33."}, + {"43543.503206018519", "[$-448]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0B2E\u0B3E\u0B30\u0B4D\u0B1A\u0B4D\u0B1A 19 2019 12:04 PM \u0B2E\u0B19\u0B4D\u0B17\u0B33\u0B2C\u0B3E\u0B30"}, {"44562.189571759256", "[$-72]mmm dd yyyy h:mm AM/PM", "Ama 01 2022 4:32 WD"}, {"44562.189571759256", "[$-72]mmmm dd yyyy h:mm AM/PM", "Amajjii 01 2022 4:32 WD"}, {"44562.189571759256", "[$-72]mmmmm dd yyyy h:mm AM/PM", "A 01 2022 4:32 WD"}, {"44562.189571759256", "[$-72]mmmmmm dd yyyy h:mm AM/PM", "Amajjii 01 2022 4:32 WD"}, {"43543.503206018519", "[$-72]mmm dd yyyy h:mm AM/PM", "Bit 19 2019 12:04 WB"}, - {"43543.503206018519", "[$-72]mmmm dd yyyy h:mm AM/PM", "Bitooteessa 19 2019 12:04 WB"}, - {"43543.503206018519", "[$-72]mmmmm dd yyyy h:mm AM/PM", "B 19 2019 12:04 WB"}, - {"43543.503206018519", "[$-72]mmmmmm dd yyyy h:mm AM/PM", "Bitooteessa 19 2019 12:04 WB"}, + {"43543.503206018519", "[$-72]mmmm dd yyyy h:mm AM/PM aaa", "Bitooteessa 19 2019 12:04 WB Qib"}, + {"43543.503206018519", "[$-72]mmmmm dd yyyy h:mm AM/PM ddd", "B 19 2019 12:04 WB Qib"}, + {"43543.503206018519", "[$-72]mmmmmm dd yyyy h:mm AM/PM dddd", "Bitooteessa 19 2019 12:04 WB Qibxata"}, {"44562.189571759256", "[$-472]mmm dd yyyy h:mm AM/PM", "Ama 01 2022 4:32 WD"}, {"44562.189571759256", "[$-472]mmmm dd yyyy h:mm AM/PM", "Amajjii 01 2022 4:32 WD"}, {"44562.189571759256", "[$-472]mmmmm dd yyyy h:mm AM/PM", "A 01 2022 4:32 WD"}, {"44562.189571759256", "[$-472]mmmmmm dd yyyy h:mm AM/PM", "Amajjii 01 2022 4:32 WD"}, {"43543.503206018519", "[$-472]mmm dd yyyy h:mm AM/PM", "Bit 19 2019 12:04 WB"}, - {"43543.503206018519", "[$-472]mmmm dd yyyy h:mm AM/PM", "Bitooteessa 19 2019 12:04 WB"}, - {"43543.503206018519", "[$-472]mmmmm dd yyyy h:mm AM/PM", "B 19 2019 12:04 WB"}, - {"43543.503206018519", "[$-472]mmmmmm dd yyyy h:mm AM/PM", "Bitooteessa 19 2019 12:04 WB"}, + {"43543.503206018519", "[$-472]mmmm dd yyyy h:mm AM/PM aaa", "Bitooteessa 19 2019 12:04 WB Qib"}, + {"43543.503206018519", "[$-472]mmmmm dd yyyy h:mm AM/PM ddd", "B 19 2019 12:04 WB Qib"}, + {"43543.503206018519", "[$-472]mmmmmm dd yyyy h:mm AM/PM dddd", "Bitooteessa 19 2019 12:04 WB Qibxata"}, {"44562.189571759256", "[$-63]mmm dd yyyy h:mm AM/PM", "\u0633\u0644\u0648\u0627\u063A\u0647 01 2022 4:32 \u063A.\u0645."}, {"44562.189571759256", "[$-63]mmmm dd yyyy h:mm AM/PM", "\u0633\u0644\u0648\u0627\u063A\u0647 01 2022 4:32 \u063A.\u0645."}, {"44562.189571759256", "[$-63]mmmmm dd yyyy h:mm AM/PM", "\u0633\u0644\u0648\u0627\u063A\u0647 01 2022 4:32 \u063A.\u0645."}, @@ -1256,9 +1737,9 @@ func TestNumFmt(t *testing.T) { {"44713.188888888886", "[$-63]mmmmm dd yyyy h:mm AM/PM", "\u0686\u0646\u06AB\u0627 \u069A\u0632\u0645\u0631\u0649 01 2022 4:32 \u063A.\u0645."}, {"44713.188888888886", "[$-63]mmmmmm dd yyyy h:mm AM/PM", "\u0686\u0646\u06AB\u0627 \u069A\u0632\u0645\u0631\u0649 01 2022 4:32 \u063A.\u0645."}, {"43543.503206018519", "[$-63]mmm dd yyyy h:mm AM/PM", "\u0648\u0631\u0649 19 2019 12:04 \u063A.\u0648."}, - {"43543.503206018519", "[$-63]mmmm dd yyyy h:mm AM/PM", "\u0648\u0631\u0649 19 2019 12:04 \u063A.\u0648."}, - {"43543.503206018519", "[$-63]mmmmm dd yyyy h:mm AM/PM", "\u0648\u0631\u0649 19 2019 12:04 \u063A.\u0648."}, - {"43543.503206018519", "[$-63]mmmmmm dd yyyy h:mm AM/PM", "\u0648\u0631\u0649 19 2019 12:04 \u063A.\u0648."}, + {"43543.503206018519", "[$-63]mmmm dd yyyy h:mm AM/PM aaa", "\u0648\u0631\u0649 19 2019 12:04 \u063A.\u0648. \u062F\u0631\u06D0\u0646\u06CD"}, + {"43543.503206018519", "[$-63]mmmmm dd yyyy h:mm AM/PM ddd", "\u0648\u0631\u0649 19 2019 12:04 \u063A.\u0648. \u062F\u0631\u06D0\u0646\u06CD"}, + {"43543.503206018519", "[$-63]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0648\u0631\u0649 19 2019 12:04 \u063A.\u0648. \u062F\u0631\u06D0\u0646\u06CD"}, {"44562.189571759256", "[$-463]mmm dd yyyy h:mm AM/PM", "\u0633\u0644\u0648\u0627\u063A\u0647 01 2022 4:32 \u063A.\u0645."}, {"44562.189571759256", "[$-463]mmmm dd yyyy h:mm AM/PM", "\u0633\u0644\u0648\u0627\u063A\u0647 01 2022 4:32 \u063A.\u0645."}, {"44562.189571759256", "[$-463]mmmmm dd yyyy h:mm AM/PM", "\u0633\u0644\u0648\u0627\u063A\u0647 01 2022 4:32 \u063A.\u0645."}, @@ -1268,552 +1749,573 @@ func TestNumFmt(t *testing.T) { {"44713.188888888886", "[$-463]mmmmm dd yyyy h:mm AM/PM", "\u0686\u0646\u06AB\u0627 \u069A\u0632\u0645\u0631\u0649 01 2022 4:32 \u063A.\u0645."}, {"44713.188888888886", "[$-463]mmmmmm dd yyyy h:mm AM/PM", "\u0686\u0646\u06AB\u0627 \u069A\u0632\u0645\u0631\u0649 01 2022 4:32 \u063A.\u0645."}, {"43543.503206018519", "[$-463]mmm dd yyyy h:mm AM/PM", "\u0648\u0631\u0649 19 2019 12:04 \u063A.\u0648."}, - {"43543.503206018519", "[$-463]mmmm dd yyyy h:mm AM/PM", "\u0648\u0631\u0649 19 2019 12:04 \u063A.\u0648."}, - {"43543.503206018519", "[$-463]mmmmm dd yyyy h:mm AM/PM", "\u0648\u0631\u0649 19 2019 12:04 \u063A.\u0648."}, - {"43543.503206018519", "[$-463]mmmmmm dd yyyy h:mm AM/PM", "\u0648\u0631\u0649 19 2019 12:04 \u063A.\u0648."}, + {"43543.503206018519", "[$-463]mmmm dd yyyy h:mm AM/PM aaa", "\u0648\u0631\u0649 19 2019 12:04 \u063A.\u0648. \u062F\u0631\u06D0\u0646\u06CD"}, + {"43543.503206018519", "[$-463]mmmmm dd yyyy h:mm AM/PM ddd", "\u0648\u0631\u0649 19 2019 12:04 \u063A.\u0648. \u062F\u0631\u06D0\u0646\u06CD"}, + {"43543.503206018519", "[$-463]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0648\u0631\u0649 19 2019 12:04 \u063A.\u0648. \u062F\u0631\u06D0\u0646\u06CD"}, {"44562.189571759256", "[$-29]mmm dd yyyy h:mm AM/PM", "\u0698\u0627\u0646\u0648\u064A\u0647 01 2022 4:32 \u0642.\u0638"}, {"44562.189571759256", "[$-29]mmmm dd yyyy h:mm AM/PM", "\u0698\u0627\u0646\u0648\u064A\u0647 01 2022 4:32 \u0642.\u0638"}, {"44562.189571759256", "[$-29]mmmmm dd yyyy h:mm AM/PM", "\u0698 01 2022 4:32 \u0642.\u0638"}, {"44562.189571759256", "[$-29]mmmmmm dd yyyy h:mm AM/PM", "\u0698\u0627\u0646\u0648\u064A\u0647 01 2022 4:32 \u0642.\u0638"}, {"43543.503206018519", "[$-29]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0628.\u0638"}, - {"43543.503206018519", "[$-29]mmmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0628.\u0638"}, - {"43543.503206018519", "[$-29]mmmmm dd yyyy h:mm AM/PM", "\u0645 19 2019 12:04 \u0628.\u0638"}, - {"43543.503206018519", "[$-29]mmmmmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0628.\u0638"}, + {"43543.503206018519", "[$-29]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0628.\u0638 \u0633\u0647%A0\u0634\u0646\u0628\u0647"}, + {"43543.503206018519", "[$-29]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 \u0628.\u0638 \u0633\u0647%A0\u0634\u0646\u0628\u0647"}, + {"43543.503206018519", "[$-29]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0628.\u0638 \u0633\u0647%A0\u0634\u0646\u0628\u0647"}, {"44562.189571759256", "[$-429]mmm dd yyyy h:mm AM/PM", "\u0698\u0627\u0646\u0648\u064A\u0647 01 2022 4:32 \u0642.\u0638"}, {"44562.189571759256", "[$-429]mmmm dd yyyy h:mm AM/PM", "\u0698\u0627\u0646\u0648\u064A\u0647 01 2022 4:32 \u0642.\u0638"}, {"44562.189571759256", "[$-429]mmmmm dd yyyy h:mm AM/PM", "\u0698 01 2022 4:32 \u0642.\u0638"}, {"44562.189571759256", "[$-429]mmmmmm dd yyyy h:mm AM/PM", "\u0698\u0627\u0646\u0648\u064A\u0647 01 2022 4:32 \u0642.\u0638"}, {"43543.503206018519", "[$-429]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0628.\u0638"}, - {"43543.503206018519", "[$-429]mmmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0628.\u0638"}, - {"43543.503206018519", "[$-429]mmmmm dd yyyy h:mm AM/PM", "\u0645 19 2019 12:04 \u0628.\u0638"}, - {"43543.503206018519", "[$-429]mmmmmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0628.\u0638"}, + {"43543.503206018519", "[$-429]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0628.\u0638 \u0633\u0647%A0\u0634\u0646\u0628\u0647"}, + {"43543.503206018519", "[$-429]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 \u0628.\u0638 \u0633\u0647%A0\u0634\u0646\u0628\u0647"}, + {"43543.503206018519", "[$-429]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0628.\u0638 \u0633\u0647%A0\u0634\u0646\u0628\u0647"}, {"44562.189571759256", "[$-15]mmm dd yyyy h:mm AM/PM", "sty 01 2022 4:32 AM"}, {"44562.189571759256", "[$-15]mmmm dd yyyy h:mm AM/PM", "styczeń 01 2022 4:32 AM"}, {"44562.189571759256", "[$-15]mmmmm dd yyyy h:mm AM/PM", "s 01 2022 4:32 AM"}, {"44562.189571759256", "[$-15]mmmmmm dd yyyy h:mm AM/PM", "styczeń 01 2022 4:32 AM"}, {"43543.503206018519", "[$-15]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-15]mmmm dd yyyy h:mm AM/PM", "marzec 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-15]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-15]mmmmmm dd yyyy h:mm AM/PM", "marzec 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-15]mmmm dd yyyy h:mm AM/PM aaa", "marzec 19 2019 12:04 PM wt."}, + {"43543.503206018519", "[$-15]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM wt."}, + {"43543.503206018519", "[$-15]mmmmmm dd yyyy h:mm AM/PM dddd", "marzec 19 2019 12:04 PM wtorek"}, {"44562.189571759256", "[$-415]mmm dd yyyy h:mm AM/PM", "sty 01 2022 4:32 AM"}, {"44562.189571759256", "[$-415]mmmm dd yyyy h:mm AM/PM", "styczeń 01 2022 4:32 AM"}, {"44562.189571759256", "[$-415]mmmmm dd yyyy h:mm AM/PM", "s 01 2022 4:32 AM"}, {"44562.189571759256", "[$-415]mmmmmm dd yyyy h:mm AM/PM", "styczeń 01 2022 4:32 AM"}, {"43543.503206018519", "[$-415]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-415]mmmm dd yyyy h:mm AM/PM", "marzec 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-415]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-415]mmmmmm dd yyyy h:mm AM/PM", "marzec 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-415]mmmm dd yyyy h:mm AM/PM aaa", "marzec 19 2019 12:04 PM wt."}, + {"43543.503206018519", "[$-415]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM wt."}, + {"43543.503206018519", "[$-415]mmmmmm dd yyyy h:mm AM/PM dddd", "marzec 19 2019 12:04 PM wtorek"}, {"44562.189571759256", "[$-16]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 AM"}, {"44562.189571759256", "[$-16]mmmm dd yyyy h:mm AM/PM", "janeiro 01 2022 4:32 AM"}, {"44562.189571759256", "[$-16]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"}, {"44562.189571759256", "[$-16]mmmmmm dd yyyy h:mm AM/PM", "janeiro 01 2022 4:32 AM"}, {"43543.503206018519", "[$-16]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-16]mmmm dd yyyy h:mm AM/PM", "março 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-16]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-16]mmmmmm dd yyyy h:mm AM/PM", "março 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-16]mmmm dd yyyy h:mm AM/PM aaa", "março 19 2019 12:04 PM ter"}, + {"43543.503206018519", "[$-16]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM ter"}, + {"43543.503206018519", "[$-16]mmmmmm dd yyyy h:mm AM/PM dddd", "março 19 2019 12:04 PM terça-feira"}, {"44562.189571759256", "[$-416]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 AM"}, {"44562.189571759256", "[$-416]mmmm dd yyyy h:mm AM/PM", "janeiro 01 2022 4:32 AM"}, {"44562.189571759256", "[$-416]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"}, {"44562.189571759256", "[$-416]mmmmmm dd yyyy h:mm AM/PM", "janeiro 01 2022 4:32 AM"}, {"43543.503206018519", "[$-416]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-416]mmmm dd yyyy h:mm AM/PM", "março 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-416]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-416]mmmmmm dd yyyy h:mm AM/PM", "março 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-416]mmmm dd yyyy h:mm AM/PM aaa", "março 19 2019 12:04 PM ter"}, + {"43543.503206018519", "[$-416]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM ter"}, + {"43543.503206018519", "[$-416]mmmmmm dd yyyy h:mm AM/PM dddd", "março 19 2019 12:04 PM terça-feira"}, {"44562.189571759256", "[$-816]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 AM"}, {"44562.189571759256", "[$-816]mmmm dd yyyy h:mm AM/PM", "janeiro 01 2022 4:32 AM"}, {"44562.189571759256", "[$-816]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"}, {"44562.189571759256", "[$-816]mmmmmm dd yyyy h:mm AM/PM", "janeiro 01 2022 4:32 AM"}, {"43543.503206018519", "[$-816]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-816]mmmm dd yyyy h:mm AM/PM", "março 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-816]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-816]mmmmmm dd yyyy h:mm AM/PM", "março 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-816]mmmm dd yyyy h:mm AM/PM aaa", "março 19 2019 12:04 PM ter"}, + {"43543.503206018519", "[$-816]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM ter"}, + {"43543.503206018519", "[$-816]mmmmmm dd yyyy h:mm AM/PM dddd", "março 19 2019 12:04 PM terça-feira"}, {"44562.189571759256", "[$-46]mmm dd yyyy h:mm AM/PM", "\u0A1C\u0A28\u0A35\u0A30\u0A40 01 2022 4:32 \u0A38\u0A35\u0A47\u0A30"}, {"44562.189571759256", "[$-46]mmmm dd yyyy h:mm AM/PM", "\u0A1C\u0A28\u0A35\u0A30\u0A40 01 2022 4:32 \u0A38\u0A35\u0A47\u0A30"}, {"44562.189571759256", "[$-46]mmmmm dd yyyy h:mm AM/PM", "\u0A1C 01 2022 4:32 \u0A38\u0A35\u0A47\u0A30"}, {"44562.189571759256", "[$-46]mmmmmm dd yyyy h:mm AM/PM", "\u0A1C\u0A28\u0A35\u0A30\u0A40 01 2022 4:32 \u0A38\u0A35\u0A47\u0A30"}, {"43543.503206018519", "[$-46]mmm dd yyyy h:mm AM/PM", "\u0A2E\u0A3E\u0A30\u0A1A 19 2019 12:04 \u0A38\u0A3C\u0A3E\u0A2E"}, - {"43543.503206018519", "[$-46]mmmm dd yyyy h:mm AM/PM", "\u0A2E\u0A3E\u0A30\u0A1A 19 2019 12:04 \u0A38\u0A3C\u0A3E\u0A2E"}, - {"43543.503206018519", "[$-46]mmmmm dd yyyy h:mm AM/PM", "\u0A2E 19 2019 12:04 \u0A38\u0A3C\u0A3E\u0A2E"}, - {"43543.503206018519", "[$-46]mmmmmm dd yyyy h:mm AM/PM", "\u0A2E\u0A3E\u0A30\u0A1A 19 2019 12:04 \u0A38\u0A3C\u0A3E\u0A2E"}, + {"43543.503206018519", "[$-46]mmmm dd yyyy h:mm AM/PM aaa", "\u0A2E\u0A3E\u0A30\u0A1A 19 2019 12:04 \u0A38\u0A3C\u0A3E\u0A2E \u0A2E\u0A70\u0A17\u0A32."}, + {"43543.503206018519", "[$-46]mmmmm dd yyyy h:mm AM/PM ddd", "\u0A2E 19 2019 12:04 \u0A38\u0A3C\u0A3E\u0A2E \u0A2E\u0A70\u0A17\u0A32."}, + {"43543.503206018519", "[$-46]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0A2E\u0A3E\u0A30\u0A1A 19 2019 12:04 \u0A38\u0A3C\u0A3E\u0A2E \u0A2E\u0A70\u0A17\u0A32\u0A35\u0A3E\u0A30"}, {"44562.189571759256", "[$-7C46]mmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u06CC 01 2022 4:32 AM"}, {"44562.189571759256", "[$-7C46]mmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u06CC 01 2022 4:32 AM"}, {"44562.189571759256", "[$-7C46]mmmmm dd yyyy h:mm AM/PM", "\u062C 01 2022 4:32 AM"}, {"44562.189571759256", "[$-7C46]mmmmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u06CC 01 2022 4:32 AM"}, {"43543.503206018519", "[$-7C46]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-7C46]mmmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-7C46]mmmmm dd yyyy h:mm AM/PM", "\u0645 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-7C46]mmmmmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-7C46]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM \u0628\u062F\u06BE"}, + {"43543.503206018519", "[$-7C46]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 PM \u0628\u062F\u06BE"}, + {"43543.503206018519", "[$-7C46]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM \u0628\u062F\u06BE"}, {"44562.189571759256", "[$-446]mmm dd yyyy h:mm AM/PM", "\u0A1C\u0A28\u0A35\u0A30\u0A40 01 2022 4:32 \u0A38\u0A35\u0A47\u0A30"}, {"44562.189571759256", "[$-446]mmmm dd yyyy h:mm AM/PM", "\u0A1C\u0A28\u0A35\u0A30\u0A40 01 2022 4:32 \u0A38\u0A35\u0A47\u0A30"}, {"44562.189571759256", "[$-446]mmmmm dd yyyy h:mm AM/PM", "\u0A1C 01 2022 4:32 \u0A38\u0A35\u0A47\u0A30"}, {"44562.189571759256", "[$-446]mmmmmm dd yyyy h:mm AM/PM", "\u0A1C\u0A28\u0A35\u0A30\u0A40 01 2022 4:32 \u0A38\u0A35\u0A47\u0A30"}, {"43543.503206018519", "[$-446]mmm dd yyyy h:mm AM/PM", "\u0A2E\u0A3E\u0A30\u0A1A 19 2019 12:04 \u0A38\u0A3C\u0A3E\u0A2E"}, - {"43543.503206018519", "[$-446]mmmm dd yyyy h:mm AM/PM", "\u0A2E\u0A3E\u0A30\u0A1A 19 2019 12:04 \u0A38\u0A3C\u0A3E\u0A2E"}, - {"43543.503206018519", "[$-446]mmmmm dd yyyy h:mm AM/PM", "\u0A2E 19 2019 12:04 \u0A38\u0A3C\u0A3E\u0A2E"}, - {"43543.503206018519", "[$-446]mmmmmm dd yyyy h:mm AM/PM", "\u0A2E\u0A3E\u0A30\u0A1A 19 2019 12:04 \u0A38\u0A3C\u0A3E\u0A2E"}, + {"43543.503206018519", "[$-446]mmmm dd yyyy h:mm AM/PM aaa", "\u0A2E\u0A3E\u0A30\u0A1A 19 2019 12:04 \u0A38\u0A3C\u0A3E\u0A2E \u0A2E\u0A70\u0A17\u0A32."}, + {"43543.503206018519", "[$-446]mmmmm dd yyyy h:mm AM/PM ddd", "\u0A2E 19 2019 12:04 \u0A38\u0A3C\u0A3E\u0A2E \u0A2E\u0A70\u0A17\u0A32."}, + {"43543.503206018519", "[$-446]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0A2E\u0A3E\u0A30\u0A1A 19 2019 12:04 \u0A38\u0A3C\u0A3E\u0A2E \u0A2E\u0A70\u0A17\u0A32\u0A35\u0A3E\u0A30"}, {"44562.189571759256", "[$-846]mmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u06CC 01 2022 4:32 AM"}, {"44562.189571759256", "[$-846]mmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u06CC 01 2022 4:32 AM"}, {"44562.189571759256", "[$-846]mmmmm dd yyyy h:mm AM/PM", "\u062C 01 2022 4:32 AM"}, {"44562.189571759256", "[$-846]mmmmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u06CC 01 2022 4:32 AM"}, {"43543.503206018519", "[$-846]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-846]mmmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-846]mmmmm dd yyyy h:mm AM/PM", "\u0645 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-846]mmmmmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-846]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM \u0628\u062F\u06BE"}, + {"43543.503206018519", "[$-846]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 PM \u0628\u062F\u06BE"}, + {"43543.503206018519", "[$-846]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM \u0628\u062F\u06BE"}, {"44562.189571759256", "[$-6B]mmm dd yyyy h:mm AM/PM", "Qul 01 2022 4:32 a.m."}, {"44562.189571759256", "[$-6B]mmmm dd yyyy h:mm AM/PM", "Qulla puquy 01 2022 4:32 a.m."}, {"44562.189571759256", "[$-6B]mmmmm dd yyyy h:mm AM/PM", "Q 01 2022 4:32 a.m."}, {"44562.189571759256", "[$-6B]mmmmmm dd yyyy h:mm AM/PM", "Qulla puquy 01 2022 4:32 a.m."}, {"43543.503206018519", "[$-6B]mmm dd yyyy h:mm AM/PM", "Pau 19 2019 12:04 p.m."}, - {"43543.503206018519", "[$-6B]mmmm dd yyyy h:mm AM/PM", "Pauqar waray 19 2019 12:04 p.m."}, - {"43543.503206018519", "[$-6B]mmmmm dd yyyy h:mm AM/PM", "P 19 2019 12:04 p.m."}, - {"43543.503206018519", "[$-6B]mmmmmm dd yyyy h:mm AM/PM", "Pauqar waray 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-6B]mmmm dd yyyy h:mm AM/PM aaa", "Pauqar waray 19 2019 12:04 p.m. ati"}, + {"43543.503206018519", "[$-6B]mmmmm dd yyyy h:mm AM/PM ddd", "P 19 2019 12:04 p.m. ati"}, + {"43543.503206018519", "[$-6B]mmmmmm dd yyyy h:mm AM/PM dddd", "Pauqar waray 19 2019 12:04 p.m. atipachaw"}, {"44562.189571759256", "[$-46B]mmm dd yyyy h:mm AM/PM", "Qul 01 2022 4:32 a.m."}, {"44562.189571759256", "[$-46B]mmmm dd yyyy h:mm AM/PM", "Qulla puquy 01 2022 4:32 a.m."}, {"44562.189571759256", "[$-46B]mmmmm dd yyyy h:mm AM/PM", "Q 01 2022 4:32 a.m."}, {"44562.189571759256", "[$-46B]mmmmmm dd yyyy h:mm AM/PM", "Qulla puquy 01 2022 4:32 a.m."}, {"43543.503206018519", "[$-46B]mmm dd yyyy h:mm AM/PM", "Pau 19 2019 12:04 p.m."}, - {"43543.503206018519", "[$-46B]mmmm dd yyyy h:mm AM/PM", "Pauqar waray 19 2019 12:04 p.m."}, - {"43543.503206018519", "[$-46B]mmmmm dd yyyy h:mm AM/PM", "P 19 2019 12:04 p.m."}, - {"43543.503206018519", "[$-46B]mmmmmm dd yyyy h:mm AM/PM", "Pauqar waray 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-46B]mmmm dd yyyy h:mm AM/PM aaa", "Pauqar waray 19 2019 12:04 p.m. ati"}, + {"43543.503206018519", "[$-46B]mmmmm dd yyyy h:mm AM/PM ddd", "P 19 2019 12:04 p.m. ati"}, + {"43543.503206018519", "[$-46B]mmmmmm dd yyyy h:mm AM/PM dddd", "Pauqar waray 19 2019 12:04 p.m. atipachaw"}, {"44562.189571759256", "[$-86B]mmm dd yyyy h:mm AM/PM", "kull 01 2022 4:32 AM"}, {"44562.189571759256", "[$-86B]mmmm dd yyyy h:mm AM/PM", "kulla 01 2022 4:32 AM"}, {"44562.189571759256", "[$-86B]mmmmm dd yyyy h:mm AM/PM", "k 01 2022 4:32 AM"}, {"44562.189571759256", "[$-86B]mmmmmm dd yyyy h:mm AM/PM", "kulla 01 2022 4:32 AM"}, {"43543.503206018519", "[$-86B]mmm dd yyyy h:mm AM/PM", "paw 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-86B]mmmm dd yyyy h:mm AM/PM", "pawkar 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-86B]mmmmm dd yyyy h:mm AM/PM", "p 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-86B]mmmmmm dd yyyy h:mm AM/PM", "pawkar 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-86B]mmmm dd yyyy h:mm AM/PM aaa", "pawkar 19 2019 12:04 PM wan"}, + {"43543.503206018519", "[$-86B]mmmmm dd yyyy h:mm AM/PM ddd", "p 19 2019 12:04 PM wan"}, + {"43543.503206018519", "[$-86B]mmmmmm dd yyyy h:mm AM/PM dddd", "pawkar 19 2019 12:04 PM wanra"}, {"44562.189571759256", "[$-C6B]mmm dd yyyy h:mm AM/PM", "Qul 01 2022 4:32 a.m."}, {"44562.189571759256", "[$-C6B]mmmm dd yyyy h:mm AM/PM", "Qulla puquy 01 2022 4:32 a.m."}, {"44562.189571759256", "[$-C6B]mmmmm dd yyyy h:mm AM/PM", "Q 01 2022 4:32 a.m."}, {"44562.189571759256", "[$-C6B]mmmmmm dd yyyy h:mm AM/PM", "Qulla puquy 01 2022 4:32 a.m."}, {"43543.503206018519", "[$-C6B]mmm dd yyyy h:mm AM/PM", "Pau 19 2019 12:04 p.m."}, - {"43543.503206018519", "[$-C6B]mmmm dd yyyy h:mm AM/PM", "Pauqar waray 19 2019 12:04 p.m."}, - {"43543.503206018519", "[$-C6B]mmmmm dd yyyy h:mm AM/PM", "P 19 2019 12:04 p.m."}, - {"43543.503206018519", "[$-C6B]mmmmmm dd yyyy h:mm AM/PM", "Pauqar waray 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-C6B]mmmm dd yyyy h:mm AM/PM aaa", "Pauqar waray 19 2019 12:04 p.m. Mar"}, + {"43543.503206018519", "[$-C6B]mmmmm dd yyyy h:mm AM/PM ddd", "P 19 2019 12:04 p.m. Mar"}, + {"43543.503206018519", "[$-C6B]mmmmmm dd yyyy h:mm AM/PM dddd", "Pauqar waray 19 2019 12:04 p.m. Martes"}, {"44562.189571759256", "[$-18]mmm dd yyyy h:mm AM/PM", "ian. 01 2022 4:32 a.m."}, {"44562.189571759256", "[$-18]mmmm dd yyyy h:mm AM/PM", "ianuarie 01 2022 4:32 a.m."}, {"44562.189571759256", "[$-18]mmmmm dd yyyy h:mm AM/PM", "i 01 2022 4:32 a.m."}, {"44562.189571759256", "[$-18]mmmmmm dd yyyy h:mm AM/PM", "ianuarie 01 2022 4:32 a.m."}, {"43543.503206018519", "[$-18]mmm dd yyyy h:mm AM/PM", "mar. 19 2019 12:04 p.m."}, - {"43543.503206018519", "[$-18]mmmm dd yyyy h:mm AM/PM", "martie 19 2019 12:04 p.m."}, - {"43543.503206018519", "[$-18]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 p.m."}, - {"43543.503206018519", "[$-18]mmmmmm dd yyyy h:mm AM/PM", "martie 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-18]mmmm dd yyyy h:mm AM/PM aaa", "martie 19 2019 12:04 p.m. mar."}, + {"43543.503206018519", "[$-18]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 p.m. mar."}, + {"43543.503206018519", "[$-18]mmmmmm dd yyyy h:mm AM/PM dddd", "martie 19 2019 12:04 p.m. marți"}, {"44562.189571759256", "[$-818]mmm dd yyyy h:mm AM/PM", "ian. 01 2022 4:32 a.m."}, {"44562.189571759256", "[$-818]mmmm dd yyyy h:mm AM/PM", "ianuarie 01 2022 4:32 a.m."}, {"44562.189571759256", "[$-818]mmmmm dd yyyy h:mm AM/PM", "i 01 2022 4:32 a.m."}, {"44562.189571759256", "[$-818]mmmmmm dd yyyy h:mm AM/PM", "ianuarie 01 2022 4:32 a.m."}, {"43543.503206018519", "[$-818]mmm dd yyyy h:mm AM/PM", "mar. 19 2019 12:04 p.m."}, - {"43543.503206018519", "[$-818]mmmm dd yyyy h:mm AM/PM", "martie 19 2019 12:04 p.m."}, - {"43543.503206018519", "[$-818]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 p.m."}, - {"43543.503206018519", "[$-818]mmmmmm dd yyyy h:mm AM/PM", "martie 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-818]mmmm dd yyyy h:mm AM/PM aaa", "martie 19 2019 12:04 p.m. Mar"}, + {"43543.503206018519", "[$-818]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 p.m. Mar"}, + {"43543.503206018519", "[$-818]mmmmmm dd yyyy h:mm AM/PM dddd", "martie 19 2019 12:04 p.m. marți"}, {"44562.189571759256", "[$-418]mmm dd yyyy h:mm AM/PM", "ian. 01 2022 4:32 a.m."}, {"44562.189571759256", "[$-418]mmmm dd yyyy h:mm AM/PM", "ianuarie 01 2022 4:32 a.m."}, {"44562.189571759256", "[$-418]mmmmm dd yyyy h:mm AM/PM", "i 01 2022 4:32 a.m."}, {"44562.189571759256", "[$-418]mmmmmm dd yyyy h:mm AM/PM", "ianuarie 01 2022 4:32 a.m."}, {"43543.503206018519", "[$-418]mmm dd yyyy h:mm AM/PM", "mar. 19 2019 12:04 p.m."}, - {"43543.503206018519", "[$-418]mmmm dd yyyy h:mm AM/PM", "martie 19 2019 12:04 p.m."}, - {"43543.503206018519", "[$-418]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 p.m."}, - {"43543.503206018519", "[$-418]mmmmmm dd yyyy h:mm AM/PM", "martie 19 2019 12:04 p.m."}, + {"43543.503206018519", "[$-418]mmmm dd yyyy h:mm AM/PM aaa", "martie 19 2019 12:04 p.m. mar."}, + {"43543.503206018519", "[$-418]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 p.m. mar."}, + {"43543.503206018519", "[$-418]mmmmmm dd yyyy h:mm AM/PM dddd", "martie 19 2019 12:04 p.m. marți"}, {"44562.189571759256", "[$-17]mmm dd yyyy h:mm AM/PM", "schan. 01 2022 4:32 AM"}, {"44562.189571759256", "[$-17]mmmm dd yyyy h:mm AM/PM", "schaner 01 2022 4:32 AM"}, {"44562.189571759256", "[$-17]mmmmm dd yyyy h:mm AM/PM", "s 01 2022 4:32 AM"}, {"44562.189571759256", "[$-17]mmmmmm dd yyyy h:mm AM/PM", "schaner 01 2022 4:32 AM"}, {"43543.503206018519", "[$-17]mmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-17]mmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-17]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-17]mmmmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-17]mmmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 PM ma"}, + {"43543.503206018519", "[$-17]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM ma"}, + {"43543.503206018519", "[$-17]mmmmmm dd yyyy h:mm AM/PM dddd", "mars 19 2019 12:04 PM mardi"}, {"44562.189571759256", "[$-417]mmm dd yyyy h:mm AM/PM", "schan. 01 2022 4:32 AM"}, {"44562.189571759256", "[$-417]mmmm dd yyyy h:mm AM/PM", "schaner 01 2022 4:32 AM"}, {"44562.189571759256", "[$-417]mmmmm dd yyyy h:mm AM/PM", "s 01 2022 4:32 AM"}, {"44562.189571759256", "[$-417]mmmmmm dd yyyy h:mm AM/PM", "schaner 01 2022 4:32 AM"}, {"43543.503206018519", "[$-417]mmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-417]mmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-417]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-417]mmmmmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-417]mmmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 PM ma"}, + {"43543.503206018519", "[$-417]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM ma"}, + {"43543.503206018519", "[$-417]mmmmmm dd yyyy h:mm AM/PM dddd", "mars 19 2019 12:04 PM mardi"}, {"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 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"}, + {"43543.503206018519", "[$-19]mmm dd yyyy h:mm AM/PM aaa", "март 19 2019 12:04 PM \u0412\u0442"}, + {"43543.503206018519", "[$-19]mmmm dd yyyy h:mm AM/PM ddd", "март 19 2019 12:04 PM \u0412\u0442"}, + {"43543.503206018519", "[$-19]mmmmm dd yyyy h:mm AM/PM dddd", "м 19 2019 12:04 PM \u0432\u0442\u043E\u0440\u043D\u0438\u043A"}, + {"43543.503206018519", "[$-819]mmm dd yyyy h:mm AM/PM aaa", "март 19 2019 12:04 PM \u0412\u0442"}, + {"43543.503206018519", "[$-819]mmmm dd yyyy h:mm AM/PM ddd", "март 19 2019 12:04 PM \u0412\u0442"}, + {"43543.503206018519", "[$-819]mmmmm dd yyyy h:mm AM/PM dddd", "м 19 2019 12:04 PM \u0432\u0442\u043E\u0440\u043D\u0438\u043A"}, + {"43543.503206018519", "[$-419]mmm dd yyyy h:mm AM/PM aaa", "март 19 2019 12:04 PM \u0412\u0442"}, + {"43543.503206018519", "[$-419]mmmm dd yyyy h:mm AM/PM ddd", "март 19 2019 12:04 PM \u0412\u0442"}, + {"43543.503206018519", "[$-419]mmmmm dd yyyy h:mm AM/PM dddd", "м 19 2019 12:04 PM \u0432\u0442\u043E\u0440\u043D\u0438\u043A"}, {"44562.189571759256", "[$-85]mmm dd yyyy h:mm AM/PM", "\u0422\u0445\u0441 01 2022 4:32 \u041A\u0418"}, {"44562.189571759256", "[$-85]mmmm dd yyyy h:mm AM/PM", "\u0422\u043E\u0445\u0441\u0443\u043D\u043D\u044C\u0443 01 2022 4:32 \u041A\u0418"}, {"44562.189571759256", "[$-85]mmmmm dd yyyy h:mm AM/PM", "\u0422 01 2022 4:32 \u041A\u0418"}, {"44562.189571759256", "[$-85]mmmmmm dd yyyy h:mm AM/PM", "\u0422\u043E\u0445\u0441\u0443\u043D\u043D\u044C\u0443 01 2022 4:32 \u041A\u0418"}, {"43543.503206018519", "[$-85]mmm dd yyyy h:mm AM/PM", "\u041A\u043B\u043D 19 2019 12:04 \u041A\u041A"}, - {"43543.503206018519", "[$-85]mmmm dd yyyy h:mm AM/PM", "\u041A\u0443\u043B\u0443\u043D \u0442\u0443\u0442\u0430\u0440 19 2019 12:04 \u041A\u041A"}, - {"43543.503206018519", "[$-85]mmmmm dd yyyy h:mm AM/PM", "\u041A 19 2019 12:04 \u041A\u041A"}, - {"43543.503206018519", "[$-85]mmmmmm dd yyyy h:mm AM/PM", "\u041A\u0443\u043B\u0443\u043D \u0442\u0443\u0442\u0430\u0440 19 2019 12:04 \u041A\u041A"}, + {"43543.503206018519", "[$-85]mmmm dd yyyy h:mm AM/PM aaa", "\u041A\u0443\u043B\u0443\u043D \u0442\u0443\u0442\u0430\u0440 19 2019 12:04 \u041A\u041A \u043E\u043F"}, + {"43543.503206018519", "[$-85]mmmmm dd yyyy h:mm AM/PM ddd", "\u041A 19 2019 12:04 \u041A\u041A \u043E\u043F"}, + {"43543.503206018519", "[$-85]mmmmmm dd yyyy h:mm AM/PM dddd", "\u041A\u0443\u043B\u0443\u043D \u0442\u0443\u0442\u0430\u0440 19 2019 12:04 \u041A\u041A \u041E\u043F\u0442\u0443\u043E\u0440\u0443\u043D\u043D\u044C\u0443\u043A"}, {"44562.189571759256", "[$-485]mmm dd yyyy h:mm AM/PM", "\u0422\u0445\u0441 01 2022 4:32 \u041A\u0418"}, {"44562.189571759256", "[$-485]mmmm dd yyyy h:mm AM/PM", "\u0422\u043E\u0445\u0441\u0443\u043D\u043D\u044C\u0443 01 2022 4:32 \u041A\u0418"}, {"44562.189571759256", "[$-485]mmmmm dd yyyy h:mm AM/PM", "\u0422 01 2022 4:32 \u041A\u0418"}, {"44562.189571759256", "[$-485]mmmmmm dd yyyy h:mm AM/PM", "\u0422\u043E\u0445\u0441\u0443\u043D\u043D\u044C\u0443 01 2022 4:32 \u041A\u0418"}, {"43543.503206018519", "[$-485]mmm dd yyyy h:mm AM/PM", "\u041A\u043B\u043D 19 2019 12:04 \u041A\u041A"}, - {"43543.503206018519", "[$-485]mmmm dd yyyy h:mm AM/PM", "\u041A\u0443\u043B\u0443\u043D \u0442\u0443\u0442\u0430\u0440 19 2019 12:04 \u041A\u041A"}, - {"43543.503206018519", "[$-485]mmmmm dd yyyy h:mm AM/PM", "\u041A 19 2019 12:04 \u041A\u041A"}, - {"43543.503206018519", "[$-485]mmmmmm dd yyyy h:mm AM/PM", "\u041A\u0443\u043B\u0443\u043D \u0442\u0443\u0442\u0430\u0440 19 2019 12:04 \u041A\u041A"}, + {"43543.503206018519", "[$-485]mmmm dd yyyy h:mm AM/PM aaa", "\u041A\u0443\u043B\u0443\u043D \u0442\u0443\u0442\u0430\u0440 19 2019 12:04 \u041A\u041A \u043E\u043F"}, + {"43543.503206018519", "[$-485]mmmmm dd yyyy h:mm AM/PM ddd", "\u041A 19 2019 12:04 \u041A\u041A \u043E\u043F"}, + {"43543.503206018519", "[$-485]mmmmmm dd yyyy h:mm AM/PM dddd", "\u041A\u0443\u043B\u0443\u043D \u0442\u0443\u0442\u0430\u0440 19 2019 12:04 \u041A\u041A \u041E\u043F\u0442\u0443\u043E\u0440\u0443\u043D\u043D\u044C\u0443\u043A"}, {"44562.189571759256", "[$-703B]mmm dd yyyy h:mm AM/PM", "uđiv 01 2022 4:32 AM"}, {"44562.189571759256", "[$-703B]mmmm dd yyyy h:mm AM/PM", "uđđâivemáánu 01 2022 4:32 AM"}, {"44562.189571759256", "[$-703B]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"}, {"44562.189571759256", "[$-703B]mmmmmm dd yyyy h:mm AM/PM", "uđđâivemáánu 01 2022 4:32 AM"}, {"43543.503206018519", "[$-703B]mmm dd yyyy h:mm AM/PM", "njuh 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-703B]mmmm dd yyyy h:mm AM/PM", "njuhčâmáánu 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-703B]mmmmm dd yyyy h:mm AM/PM", "n 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-703B]mmmmmm dd yyyy h:mm AM/PM", "njuhčâmáánu 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-703B]mmmm dd yyyy h:mm AM/PM aaa", "njuhčâmáánu 19 2019 12:04 PM maj"}, + {"43543.503206018519", "[$-703B]mmmmm dd yyyy h:mm AM/PM ddd", "n 19 2019 12:04 PM maj"}, + {"43543.503206018519", "[$-703B]mmmmmm dd yyyy h:mm AM/PM dddd", "njuhčâmáánu 19 2019 12:04 PM majebargâ"}, {"44562.189571759256", "[$-243B]mmm dd yyyy h:mm AM/PM", "uđiv 01 2022 4:32 AM"}, {"44562.189571759256", "[$-243B]mmmm dd yyyy h:mm AM/PM", "uđđâivemáánu 01 2022 4:32 AM"}, {"44562.189571759256", "[$-243B]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"}, {"44562.189571759256", "[$-243B]mmmmmm dd yyyy h:mm AM/PM", "uđđâivemáánu 01 2022 4:32 AM"}, {"43543.503206018519", "[$-243B]mmm dd yyyy h:mm AM/PM", "njuh 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-243B]mmmm dd yyyy h:mm AM/PM", "njuhčâmáánu 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-243B]mmmmm dd yyyy h:mm AM/PM", "n 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-243B]mmmmmm dd yyyy h:mm AM/PM", "njuhčâmáánu 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-243B]mmmm dd yyyy h:mm AM/PM aaa", "njuhčâmáánu 19 2019 12:04 PM maj"}, + {"43543.503206018519", "[$-243B]mmmmm dd yyyy h:mm AM/PM ddd", "n 19 2019 12:04 PM maj"}, + {"43543.503206018519", "[$-243B]mmmmmm dd yyyy h:mm AM/PM dddd", "njuhčâmáánu 19 2019 12:04 PM majebargâ"}, {"44562.189571759256", "[$-7C3B]mmm dd yyyy h:mm AM/PM", "ådåj 01 2022 4:32 AM"}, {"44562.189571759256", "[$-7C3B]mmmm dd yyyy h:mm AM/PM", "ådåjakmánno 01 2022 4:32 AM"}, {"44562.189571759256", "[$-7C3B]mmmmm dd yyyy h:mm AM/PM", "å 01 2022 4:32 AM"}, {"44562.189571759256", "[$-7C3B]mmmmmm dd yyyy h:mm AM/PM", "ådåjakmánno 01 2022 4:32 AM"}, {"43543.503206018519", "[$-7C3B]mmm dd yyyy h:mm AM/PM", "snju 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-7C3B]mmmm dd yyyy h:mm AM/PM", "sjnjuktjamánno 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-7C3B]mmmmm dd yyyy h:mm AM/PM", "s 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-7C3B]mmmmmm dd yyyy h:mm AM/PM", "sjnjuktjamánno 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-7C3B]mmmm dd yyyy h:mm AM/PM aaa", "sjnjuktjamánno 19 2019 12:04 PM dis"}, + {"43543.503206018519", "[$-7C3B]mmmmm dd yyyy h:mm AM/PM ddd", "s 19 2019 12:04 PM dis"}, + {"43543.503206018519", "[$-7C3B]mmmmmm dd yyyy h:mm AM/PM dddd", "sjnjuktjamánno 19 2019 12:04 PM dijstahka"}, {"44562.189571759256", "[$-103B]mmm dd yyyy h:mm AM/PM", "ådåj 01 2022 4:32 AM"}, {"44562.189571759256", "[$-103B]mmmm dd yyyy h:mm AM/PM", "ådåjakmánno 01 2022 4:32 AM"}, {"44562.189571759256", "[$-103B]mmmmm dd yyyy h:mm AM/PM", "å 01 2022 4:32 AM"}, {"44562.189571759256", "[$-103B]mmmmmm dd yyyy h:mm AM/PM", "ådåjakmánno 01 2022 4:32 AM"}, {"43543.503206018519", "[$-103B]mmm dd yyyy h:mm AM/PM", "snju 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-103B]mmmm dd yyyy h:mm AM/PM", "sjnjuktjamánno 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-103B]mmmmm dd yyyy h:mm AM/PM", "s 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-103B]mmmmmm dd yyyy h:mm AM/PM", "sjnjuktjamánno 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-103B]mmmm dd yyyy h:mm AM/PM aaa", "sjnjuktjamánno 19 2019 12:04 PM dis"}, + {"43543.503206018519", "[$-103B]mmmmm dd yyyy h:mm AM/PM ddd", "s 19 2019 12:04 PM dis"}, + {"43543.503206018519", "[$-103B]mmmmmm dd yyyy h:mm AM/PM dddd", "sjnjuktjamánno 19 2019 12:04 PM dijstahka"}, {"44562.189571759256", "[$-143B]mmm dd yyyy h:mm AM/PM", "ådåj 01 2022 4:32 AM"}, {"44562.189571759256", "[$-143B]mmmm dd yyyy h:mm AM/PM", "ådåjakmánno 01 2022 4:32 AM"}, {"44562.189571759256", "[$-143B]mmmmm dd yyyy h:mm AM/PM", "å 01 2022 4:32 AM"}, {"44562.189571759256", "[$-143B]mmmmmm dd yyyy h:mm AM/PM", "ådåjakmánno 01 2022 4:32 AM"}, {"43543.503206018519", "[$-143B]mmm dd yyyy h:mm AM/PM", "snju 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-143B]mmmm dd yyyy h:mm AM/PM", "sjnjuktjamánno 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-143B]mmmmm dd yyyy h:mm AM/PM", "s 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-143B]mmmmmm dd yyyy h:mm AM/PM", "sjnjuktjamánno 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-143B]mmmm dd yyyy h:mm AM/PM aaa", "sjnjuktjamánno 19 2019 12:04 PM dis"}, + {"43543.503206018519", "[$-143B]mmmmm dd yyyy h:mm AM/PM ddd", "s 19 2019 12:04 PM dis"}, + {"43543.503206018519", "[$-143B]mmmmmm dd yyyy h:mm AM/PM dddd", "sjnjuktjamánno 19 2019 12:04 PM dijstahka"}, {"44562.189571759256", "[$-3B]mmm dd yyyy h:mm AM/PM", "ođđj 01 2022 4:32 i.b."}, {"44562.189571759256", "[$-3B]mmmm dd yyyy h:mm AM/PM", "ođđajagemánnu 01 2022 4:32 i.b."}, {"44562.189571759256", "[$-3B]mmmmm dd yyyy h:mm AM/PM", "o 01 2022 4:32 i.b."}, {"44562.189571759256", "[$-3B]mmmmmm dd yyyy h:mm AM/PM", "ođđajagemánnu 01 2022 4:32 i.b."}, {"43543.503206018519", "[$-3B]mmm dd yyyy h:mm AM/PM", "njuk 19 2019 12:04 e.b."}, - {"43543.503206018519", "[$-3B]mmmm dd yyyy h:mm AM/PM", "njukčamánnu 19 2019 12:04 e.b."}, - {"43543.503206018519", "[$-3B]mmmmm dd yyyy h:mm AM/PM", "n 19 2019 12:04 e.b."}, - {"43543.503206018519", "[$-3B]mmmmmm dd yyyy h:mm AM/PM", "njukčamánnu 19 2019 12:04 e.b."}, + {"43543.503206018519", "[$-3B]mmmm dd yyyy h:mm AM/PM aaa", "njukčamánnu 19 2019 12:04 e.b. maŋ"}, + {"43543.503206018519", "[$-3B]mmmmm dd yyyy h:mm AM/PM ddd", "n 19 2019 12:04 e.b. maŋ"}, + {"43543.503206018519", "[$-3B]mmmmmm dd yyyy h:mm AM/PM dddd", "njukčamánnu 19 2019 12:04 e.b. maŋŋebárga"}, {"44562.189571759256", "[$-C3B]mmm dd yyyy h:mm AM/PM", "ođđj 01 2022 4:32 AM"}, {"44562.189571759256", "[$-C3B]mmmm dd yyyy h:mm AM/PM", "ođđajagemánu 01 2022 4:32 AM"}, {"44562.189571759256", "[$-C3B]mmmmm dd yyyy h:mm AM/PM", "o 01 2022 4:32 AM"}, {"44562.189571759256", "[$-C3B]mmmmmm dd yyyy h:mm AM/PM", "ođđajagemánu 01 2022 4:32 AM"}, {"43543.503206018519", "[$-C3B]mmm dd yyyy h:mm AM/PM", "njuk 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-C3B]mmmm dd yyyy h:mm AM/PM", "njukčamánnu 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-C3B]mmmmm dd yyyy h:mm AM/PM", "n 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-C3B]mmmmmm dd yyyy h:mm AM/PM", "njukčamánnu 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-C3B]mmmm dd yyyy h:mm AM/PM aaa", "njukčamánnu 19 2019 12:04 PM di"}, + {"43543.503206018519", "[$-C3B]mmmmm dd yyyy h:mm AM/PM ddd", "n 19 2019 12:04 PM di"}, + {"43543.503206018519", "[$-C3B]mmmmmm dd yyyy h:mm AM/PM dddd", "njukčamánnu 19 2019 12:04 PM maŋŋebárga"}, {"44562.189571759256", "[$-43B]mmm dd yyyy h:mm AM/PM", "ođđj 01 2022 4:32 i.b."}, {"44562.189571759256", "[$-43B]mmmm dd yyyy h:mm AM/PM", "ođđajagemánnu 01 2022 4:32 i.b."}, {"44562.189571759256", "[$-43B]mmmmm dd yyyy h:mm AM/PM", "o 01 2022 4:32 i.b."}, {"44562.189571759256", "[$-43B]mmmmmm dd yyyy h:mm AM/PM", "ođđajagemánnu 01 2022 4:32 i.b."}, {"43543.503206018519", "[$-43B]mmm dd yyyy h:mm AM/PM", "njuk 19 2019 12:04 e.b."}, - {"43543.503206018519", "[$-43B]mmmm dd yyyy h:mm AM/PM", "njukčamánnu 19 2019 12:04 e.b."}, - {"43543.503206018519", "[$-43B]mmmmm dd yyyy h:mm AM/PM", "n 19 2019 12:04 e.b."}, - {"43543.503206018519", "[$-43B]mmmmmm dd yyyy h:mm AM/PM", "njukčamánnu 19 2019 12:04 e.b."}, + {"43543.503206018519", "[$-43B]mmmm dd yyyy h:mm AM/PM aaa", "njukčamánnu 19 2019 12:04 e.b. maŋ"}, + {"43543.503206018519", "[$-43B]mmmmm dd yyyy h:mm AM/PM ddd", "n 19 2019 12:04 e.b. maŋ"}, + {"43543.503206018519", "[$-43B]mmmmmm dd yyyy h:mm AM/PM dddd", "njukčamánnu 19 2019 12:04 e.b. maŋŋebárga"}, {"44562.189571759256", "[$-83B]mmm dd yyyy h:mm AM/PM", "ođđj 01 2022 4:32 AM"}, {"44562.189571759256", "[$-83B]mmmm dd yyyy h:mm AM/PM", "ođđajagemánnu 01 2022 4:32 AM"}, {"44562.189571759256", "[$-83B]mmmmm dd yyyy h:mm AM/PM", "o 01 2022 4:32 AM"}, {"44562.189571759256", "[$-83B]mmmmmm dd yyyy h:mm AM/PM", "ođđajagemánnu 01 2022 4:32 AM"}, {"43543.503206018519", "[$-83B]mmm dd yyyy h:mm AM/PM", "njuk 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-83B]mmmm dd yyyy h:mm AM/PM", "njukčamánnu 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-83B]mmmmm dd yyyy h:mm AM/PM", "n 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-83B]mmmmmm dd yyyy h:mm AM/PM", "njukčamánnu 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-83B]mmmm dd yyyy h:mm AM/PM aaa", "njukčamánnu 19 2019 12:04 PM dis"}, + {"43543.503206018519", "[$-83B]mmmmm dd yyyy h:mm AM/PM ddd", "n 19 2019 12:04 PM dis"}, + {"43543.503206018519", "[$-83B]mmmmmm dd yyyy h:mm AM/PM dddd", "njukčamánnu 19 2019 12:04 PM disdat"}, {"44562.189571759256", "[$-743B]mmm dd yyyy h:mm AM/PM", "ođđee´jjmään 01 2022 4:32 AM"}, {"44562.189571759256", "[$-743B]mmmm dd yyyy h:mm AM/PM", "ođđee´jjmään 01 2022 4:32 AM"}, {"44562.189571759256", "[$-743B]mmmmm dd yyyy h:mm AM/PM", "o 01 2022 4:32 AM"}, {"44562.189571759256", "[$-743B]mmmmmm dd yyyy h:mm AM/PM", "ođđee´jjmään 01 2022 4:32 AM"}, {"43543.503206018519", "[$-743B]mmm dd yyyy h:mm AM/PM", "pâ´zzlâšttam-mään 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-743B]mmmm dd yyyy h:mm AM/PM", "pâ´zzlâšttam-mään 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-743B]mmmmm dd yyyy h:mm AM/PM", "p 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-743B]mmmmmm dd yyyy h:mm AM/PM", "pâ´zzlâšttam-mään 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-743B]mmmm dd yyyy h:mm AM/PM aaa", "pâ´zzlâšttam-mään 19 2019 12:04 PM m%E2"}, + {"43543.503206018519", "[$-743B]mmmmm dd yyyy h:mm AM/PM ddd", "p 19 2019 12:04 PM m%E2"}, + {"43543.503206018519", "[$-743B]mmmmmm dd yyyy h:mm AM/PM dddd", "pâ´zzlâšttam-mään 19 2019 12:04 PM m%E2%E2ibargg"}, {"44562.189571759256", "[$-203B]mmm dd yyyy h:mm AM/PM", "ođđee´jjmään 01 2022 4:32 AM"}, {"44562.189571759256", "[$-203B]mmmm dd yyyy h:mm AM/PM", "ođđee´jjmään 01 2022 4:32 AM"}, {"44562.189571759256", "[$-203B]mmmmm dd yyyy h:mm AM/PM", "o 01 2022 4:32 AM"}, {"44562.189571759256", "[$-203B]mmmmmm dd yyyy h:mm AM/PM", "ođđee´jjmään 01 2022 4:32 AM"}, {"43543.503206018519", "[$-203B]mmm dd yyyy h:mm AM/PM", "pâ´zzlâšttam-mään 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-203B]mmmm dd yyyy h:mm AM/PM", "pâ´zzlâšttam-mään 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-203B]mmmmm dd yyyy h:mm AM/PM", "p 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-203B]mmmmmm dd yyyy h:mm AM/PM", "pâ´zzlâšttam-mään 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-203B]mmmm dd yyyy h:mm AM/PM aaa", "pâ´zzlâšttam-mään 19 2019 12:04 PM m%E2"}, + {"43543.503206018519", "[$-203B]mmmmm dd yyyy h:mm AM/PM ddd", "p 19 2019 12:04 PM m%E2"}, + {"43543.503206018519", "[$-203B]mmmmmm dd yyyy h:mm AM/PM dddd", "pâ´zzlâšttam-mään 19 2019 12:04 PM m%E2%E2ibargg"}, {"44562.189571759256", "[$-783B]mmm dd yyyy h:mm AM/PM", "tsïen 01 2022 4:32 AM"}, {"44562.189571759256", "[$-783B]mmmm dd yyyy h:mm AM/PM", "tsïengele 01 2022 4:32 AM"}, {"44562.189571759256", "[$-783B]mmmmm dd yyyy h:mm AM/PM", "t 01 2022 4:32 AM"}, {"44562.189571759256", "[$-783B]mmmmmm dd yyyy h:mm AM/PM", "tsïengele 01 2022 4:32 AM"}, {"43543.503206018519", "[$-783B]mmm dd yyyy h:mm AM/PM", "njok 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-783B]mmmm dd yyyy h:mm AM/PM", "njoktje 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-783B]mmmmm dd yyyy h:mm AM/PM", "n 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-783B]mmmmmm dd yyyy h:mm AM/PM", "njoktje 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-783B]mmmm dd yyyy h:mm AM/PM aaa", "njoktje 19 2019 12:04 PM d%E6j"}, + {"43543.503206018519", "[$-783B]mmmmm dd yyyy h:mm AM/PM ddd", "n 19 2019 12:04 PM d%E6j"}, + {"43543.503206018519", "[$-783B]mmmmmm dd yyyy h:mm AM/PM dddd", "njoktje 19 2019 12:04 PM d%E6jsta"}, {"44562.189571759256", "[$-183B]mmm dd yyyy h:mm AM/PM", "tsïen 01 2022 4:32 AM"}, {"44562.189571759256", "[$-183B]mmmm dd yyyy h:mm AM/PM", "tsïengele 01 2022 4:32 AM"}, {"44562.189571759256", "[$-183B]mmmmm dd yyyy h:mm AM/PM", "t 01 2022 4:32 AM"}, {"44562.189571759256", "[$-183B]mmmmmm dd yyyy h:mm AM/PM", "tsïengele 01 2022 4:32 AM"}, {"43543.503206018519", "[$-183B]mmm dd yyyy h:mm AM/PM", "njok 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-183B]mmmm dd yyyy h:mm AM/PM", "njoktje 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-183B]mmmmm dd yyyy h:mm AM/PM", "n 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-183B]mmmmmm dd yyyy h:mm AM/PM", "njoktje 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-183B]mmmm dd yyyy h:mm AM/PM aaa", "njoktje 19 2019 12:04 PM d%E6j"}, + {"43543.503206018519", "[$-183B]mmmmm dd yyyy h:mm AM/PM ddd", "n 19 2019 12:04 PM d%E6j"}, + {"43543.503206018519", "[$-183B]mmmmmm dd yyyy h:mm AM/PM dddd", "njoktje 19 2019 12:04 PM d%E6jsta"}, {"44562.189571759256", "[$-1C3B]mmm dd yyyy h:mm AM/PM", "tsïen 01 2022 4:32 AM"}, {"44562.189571759256", "[$-1C3B]mmmm dd yyyy h:mm AM/PM", "tsïengele 01 2022 4:32 AM"}, {"44562.189571759256", "[$-1C3B]mmmmm dd yyyy h:mm AM/PM", "t 01 2022 4:32 AM"}, {"44562.189571759256", "[$-1C3B]mmmmmm dd yyyy h:mm AM/PM", "tsïengele 01 2022 4:32 AM"}, {"43543.503206018519", "[$-1C3B]mmm dd yyyy h:mm AM/PM", "njok 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-1C3B]mmmm dd yyyy h:mm AM/PM", "njoktje 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-1C3B]mmmmm dd yyyy h:mm AM/PM", "n 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-1C3B]mmmmmm dd yyyy h:mm AM/PM", "njoktje 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-1C3B]mmmm dd yyyy h:mm AM/PM aaa", "njoktje 19 2019 12:04 PM d%E6j"}, + {"43543.503206018519", "[$-1C3B]mmmmm dd yyyy h:mm AM/PM ddd", "n 19 2019 12:04 PM d%E6j"}, + {"43543.503206018519", "[$-1C3B]mmmmmm dd yyyy h:mm AM/PM dddd", "njoktje 19 2019 12:04 PM d%E6jsta"}, {"44562.189571759256", "[$-4F]mmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u094D\u092F\u0941\u0905\u0930\u0940 01 2022 4:32 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u0942\u0930\u094D\u0935"}, {"44562.189571759256", "[$-4F]mmmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u094D\u092F\u0941\u0905\u0930\u0940 01 2022 4:32 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u0942\u0930\u094D\u0935"}, {"44562.189571759256", "[$-4F]mmmmm dd yyyy h:mm AM/PM", "\u091C 01 2022 4:32 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u0942\u0930\u094D\u0935"}, {"44562.189571759256", "[$-4F]mmmmmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u094D\u092F\u0941\u0905\u0930\u0940 01 2022 4:32 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u0942\u0930\u094D\u0935"}, {"43543.503206018519", "[$-4F]mmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u091A\u094D\u092F\u093E\u0924"}, - {"43543.503206018519", "[$-4F]mmmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u091A\u094D\u092F\u093E\u0924"}, - {"43543.503206018519", "[$-4F]mmmmm dd yyyy h:mm AM/PM", "\u092E 19 2019 12:04 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u091A\u094D\u092F\u093E\u0924"}, - {"43543.503206018519", "[$-4F]mmmmmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u091A\u094D\u092F\u093E\u0924"}, + {"43543.503206018519", "[$-4F]mmmm dd yyyy h:mm AM/PM aaa", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u091A\u094D\u092F\u093E\u0924 \u092E\u0919\u094D\u0917"}, + {"43543.503206018519", "[$-4F]mmmmm dd yyyy h:mm AM/PM ddd", "\u092E 19 2019 12:04 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u091A\u094D\u092F\u093E\u0924 \u092E\u0919\u094D\u0917"}, + {"43543.503206018519", "[$-4F]mmmmmm dd yyyy h:mm AM/PM dddd", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u091A\u094D\u092F\u093E\u0924 \u092E\u0902\u0917\u0932\u0935\u093E\u0938\u0930\u0903"}, {"44562.189571759256", "[$-44F]mmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u094D\u092F\u0941\u0905\u0930\u0940 01 2022 4:32 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u0942\u0930\u094D\u0935"}, {"44562.189571759256", "[$-44F]mmmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u094D\u092F\u0941\u0905\u0930\u0940 01 2022 4:32 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u0942\u0930\u094D\u0935"}, {"44562.189571759256", "[$-44F]mmmmm dd yyyy h:mm AM/PM", "\u091C 01 2022 4:32 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u0942\u0930\u094D\u0935"}, {"44562.189571759256", "[$-44F]mmmmmm dd yyyy h:mm AM/PM", "\u091C\u093E\u0928\u094D\u092F\u0941\u0905\u0930\u0940 01 2022 4:32 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u0942\u0930\u094D\u0935"}, {"43543.503206018519", "[$-44F]mmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u091A\u094D\u092F\u093E\u0924"}, - {"43543.503206018519", "[$-44F]mmmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u091A\u094D\u092F\u093E\u0924"}, - {"43543.503206018519", "[$-44F]mmmmm dd yyyy h:mm AM/PM", "\u092E 19 2019 12:04 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u091A\u094D\u092F\u093E\u0924"}, - {"43543.503206018519", "[$-44F]mmmmmm dd yyyy h:mm AM/PM", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u091A\u094D\u092F\u093E\u0924"}, + {"43543.503206018519", "[$-44F]mmmm dd yyyy h:mm AM/PM aaa", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u091A\u094D\u092F\u093E\u0924 \u092E\u0919\u094D\u0917"}, + {"43543.503206018519", "[$-44F]mmmmm dd yyyy h:mm AM/PM ddd", "\u092E 19 2019 12:04 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u091A\u094D\u092F\u093E\u0924 \u092E\u0919\u094D\u0917"}, + {"43543.503206018519", "[$-44F]mmmmmm dd yyyy h:mm AM/PM dddd", "\u092E\u093E\u0930\u094D\u091A 19 2019 12:04 \u092E\u0927\u094D\u092F\u093E\u0928\u092A\u091A\u094D\u092F\u093E\u0924 \u092E\u0902\u0917\u0932\u0935\u093E\u0938\u0930\u0903"}, {"44562.189571759256", "[$-91]mmm dd yyyy h:mm AM/PM", "Faoi 01 2022 4:32 m"}, {"44562.189571759256", "[$-91]mmmm dd yyyy h:mm AM/PM", "Am Faoilleach 01 2022 4:32 m"}, {"44562.189571759256", "[$-91]mmmmm dd yyyy h:mm AM/PM", "A 01 2022 4:32 m"}, {"44562.189571759256", "[$-91]mmmmmm dd yyyy h:mm AM/PM", "Am Faoilleach 01 2022 4:32 m"}, {"43543.503206018519", "[$-91]mmm dd yyyy h:mm AM/PM", "Màrt 19 2019 12:04 f"}, - {"43543.503206018519", "[$-91]mmmm dd yyyy h:mm AM/PM", "Am Màrt 19 2019 12:04 f"}, - {"43543.503206018519", "[$-91]mmmmm dd yyyy h:mm AM/PM", "A 19 2019 12:04 f"}, - {"43543.503206018519", "[$-91]mmmmmm dd yyyy h:mm AM/PM", "Am Màrt 19 2019 12:04 f"}, + {"43543.503206018519", "[$-91]mmmm dd yyyy h:mm AM/PM aaa", "Am Màrt 19 2019 12:04 f DiM"}, + {"43543.503206018519", "[$-91]mmmmm dd yyyy h:mm AM/PM ddd", "A 19 2019 12:04 f DiM"}, + {"43543.503206018519", "[$-91]mmmmmm dd yyyy h:mm AM/PM dddd", "Am Màrt 19 2019 12:04 f DiMàirt"}, {"44562.189571759256", "[$-491]mmm dd yyyy h:mm AM/PM", "Faoi 01 2022 4:32 m"}, {"44562.189571759256", "[$-491]mmmm dd yyyy h:mm AM/PM", "Am Faoilleach 01 2022 4:32 m"}, {"44562.189571759256", "[$-491]mmmmm dd yyyy h:mm AM/PM", "A 01 2022 4:32 m"}, {"44562.189571759256", "[$-491]mmmmmm dd yyyy h:mm AM/PM", "Am Faoilleach 01 2022 4:32 m"}, {"43543.503206018519", "[$-491]mmm dd yyyy h:mm AM/PM", "Màrt 19 2019 12:04 f"}, - {"43543.503206018519", "[$-491]mmmm dd yyyy h:mm AM/PM", "Am Màrt 19 2019 12:04 f"}, - {"43543.503206018519", "[$-491]mmmmm dd yyyy h:mm AM/PM", "A 19 2019 12:04 f"}, - {"43543.503206018519", "[$-491]mmmmmm dd yyyy h:mm AM/PM", "Am Màrt 19 2019 12:04 f"}, + {"43543.503206018519", "[$-491]mmmm dd yyyy h:mm AM/PM aaa", "Am Màrt 19 2019 12:04 f DiM"}, + {"43543.503206018519", "[$-491]mmmmm dd yyyy h:mm AM/PM ddd", "A 19 2019 12:04 f DiM"}, + {"43543.503206018519", "[$-491]mmmmmm dd yyyy h:mm AM/PM dddd", "Am Màrt 19 2019 12:04 f DiMàirt"}, {"44562.189571759256", "[$-6C1A]mmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D. 01 2022 4:32 AM"}, {"44562.189571759256", "[$-6C1A]mmmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D\u0443\u0430\u0440 01 2022 4:32 AM"}, {"44562.189571759256", "[$-6C1A]mmmmm dd yyyy h:mm AM/PM", "\u0458 01 2022 4:32 AM"}, {"44562.189571759256", "[$-6C1A]mmmmmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D\u0443\u0430\u0440 01 2022 4:32 AM"}, {"43543.503206018519", "[$-6C1A]mmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-6C1A]mmmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-6C1A]mmmmm dd yyyy h:mm AM/PM", "\u043C 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-6C1A]mmmmmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-6C1A]mmmm dd yyyy h:mm AM/PM aaa", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0443\u0442."}, + {"43543.503206018519", "[$-6C1A]mmmmm dd yyyy h:mm AM/PM ddd", "\u043C 19 2019 12:04 PM \u0443\u0442."}, + {"43543.503206018519", "[$-6C1A]mmmmmm dd yyyy h:mm AM/PM dddd", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0443\u0442\u043E\u0440\u0430\u043A"}, {"44562.189571759256", "[$-1C1A]mmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D 01 2022 4:32 AM"}, {"44562.189571759256", "[$-1C1A]mmmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D\u0443\u0430\u0440 01 2022 4:32 AM"}, {"44562.189571759256", "[$-1C1A]mmmmm dd yyyy h:mm AM/PM", "\u0458 01 2022 4:32 AM"}, {"44562.189571759256", "[$-1C1A]mmmmmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D\u0443\u0430\u0440 01 2022 4:32 AM"}, {"43543.503206018519", "[$-1C1A]mmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-1C1A]mmmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-1C1A]mmmmm dd yyyy h:mm AM/PM", "\u043C 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-1C1A]mmmmmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-1C1A]mmmm dd yyyy h:mm AM/PM aaa", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0443\u0442\u043E"}, + {"43543.503206018519", "[$-1C1A]mmmmm dd yyyy h:mm AM/PM ddd", "\u043C 19 2019 12:04 PM \u0443\u0442\u043E"}, + {"43543.503206018519", "[$-1C1A]mmmmmm dd yyyy h:mm AM/PM dddd", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0443\u0442\u043E\u0440\u0430\u043A"}, {"44562.189571759256", "[$-301A]mmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D. 01 2022 4:32 AM"}, {"44562.189571759256", "[$-301A]mmmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D\u0443\u0430\u0440 01 2022 4:32 AM"}, {"44562.189571759256", "[$-301A]mmmmm dd yyyy h:mm AM/PM", "\u0458 01 2022 4:32 AM"}, {"44562.189571759256", "[$-301A]mmmmmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D\u0443\u0430\u0440 01 2022 4:32 AM"}, {"43543.503206018519", "[$-301A]mmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-301A]mmmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-301A]mmmmm dd yyyy h:mm AM/PM", "\u043C 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-301A]mmmmmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-301A]mmmm dd yyyy h:mm AM/PM aaa", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0443\u0442\u043E"}, + {"43543.503206018519", "[$-301A]mmmmm dd yyyy h:mm AM/PM ddd", "\u043C 19 2019 12:04 PM \u0443\u0442\u043E"}, + {"43543.503206018519", "[$-301A]mmmmmm dd yyyy h:mm AM/PM dddd", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0443\u0442\u043E\u0440\u0430\u043A"}, {"44562.189571759256", "[$-281A]mmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D. 01 2022 4:32 AM"}, {"44562.189571759256", "[$-281A]mmmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D\u0443\u0430\u0440 01 2022 4:32 AM"}, {"44562.189571759256", "[$-281A]mmmmm dd yyyy h:mm AM/PM", "\u0458 01 2022 4:32 AM"}, {"44562.189571759256", "[$-281A]mmmmmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D\u0443\u0430\u0440 01 2022 4:32 AM"}, {"43543.503206018519", "[$-281A]mmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-281A]mmmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-281A]mmmmm dd yyyy h:mm AM/PM", "\u043C 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-281A]mmmmmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-281A]mmmm dd yyyy h:mm AM/PM aaa", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0443\u0442."}, + {"43543.503206018519", "[$-281A]mmmmm dd yyyy h:mm AM/PM ddd", "\u043C 19 2019 12:04 PM \u0443\u0442."}, + {"43543.503206018519", "[$-281A]mmmmmm dd yyyy h:mm AM/PM dddd", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0443\u0442\u043E\u0440\u0430\u043A"}, {"44562.189571759256", "[$-C1A]mmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D. 01 2022 4:32 AM"}, {"44562.189571759256", "[$-C1A]mmmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D\u0443\u0430\u0440 01 2022 4:32 AM"}, {"44562.189571759256", "[$-C1A]mmmmm dd yyyy h:mm AM/PM", "\u0458 01 2022 4:32 AM"}, {"44562.189571759256", "[$-C1A]mmmmmm dd yyyy h:mm AM/PM", "\u0458\u0430\u043D\u0443\u0430\u0440 01 2022 4:32 AM"}, {"43543.503206018519", "[$-C1A]mmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-C1A]mmmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-C1A]mmmmm dd yyyy h:mm AM/PM", "\u043C 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-C1A]mmmmmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-C1A]mmmm dd yyyy h:mm AM/PM aaa", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0443\u0442."}, + {"43543.503206018519", "[$-C1A]mmmmm dd yyyy h:mm AM/PM ddd", "\u043C 19 2019 12:04 PM \u0443\u0442."}, + {"43543.503206018519", "[$-C1A]mmmmmm dd yyyy h:mm AM/PM dddd", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0443\u0442\u043E\u0440\u0430\u043A"}, {"44562.189571759256", "[$-701A]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 pre podne"}, {"44562.189571759256", "[$-701A]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 pre podne"}, {"44562.189571759256", "[$-701A]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 pre podne"}, {"44562.189571759256", "[$-701A]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 pre podne"}, {"43543.503206018519", "[$-701A]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 po podne"}, - {"43543.503206018519", "[$-701A]mmmm dd yyyy h:mm AM/PM", "mart 19 2019 12:04 po podne"}, - {"43543.503206018519", "[$-701A]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 po podne"}, - {"43543.503206018519", "[$-701A]mmmmmm dd yyyy h:mm AM/PM", "mart 19 2019 12:04 po podne"}, + {"43543.503206018519", "[$-701A]mmmm dd yyyy h:mm AM/PM aaa", "mart 19 2019 12:04 po podne uto"}, + {"43543.503206018519", "[$-701A]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 po podne uto"}, + {"43543.503206018519", "[$-701A]mmmmmm dd yyyy h:mm AM/PM dddd", "mart 19 2019 12:04 po podne utorak"}, {"44562.189571759256", "[$-7C1A]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 pre podne"}, {"44562.189571759256", "[$-7C1A]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 pre podne"}, {"44562.189571759256", "[$-7C1A]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 pre podne"}, {"44562.189571759256", "[$-7C1A]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 pre podne"}, {"43543.503206018519", "[$-7C1A]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 po podne"}, - {"43543.503206018519", "[$-7C1A]mmmm dd yyyy h:mm AM/PM", "mart 19 2019 12:04 po podne"}, - {"43543.503206018519", "[$-7C1A]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 po podne"}, - {"43543.503206018519", "[$-7C1A]mmmmmm dd yyyy h:mm AM/PM", "mart 19 2019 12:04 po podne"}, + {"43543.503206018519", "[$-7C1A]mmmm dd yyyy h:mm AM/PM aaa", "mart 19 2019 12:04 po podne uto"}, + {"43543.503206018519", "[$-7C1A]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 po podne uto"}, + {"43543.503206018519", "[$-7C1A]mmmmmm dd yyyy h:mm AM/PM dddd", "mart 19 2019 12:04 po podne utorak"}, {"44562.189571759256", "[$-181A]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 prije podne"}, {"44562.189571759256", "[$-181A]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 prije podne"}, {"44562.189571759256", "[$-181A]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 prije podne"}, {"44562.189571759256", "[$-181A]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 prije podne"}, {"43543.503206018519", "[$-181A]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 po podne"}, - {"43543.503206018519", "[$-181A]mmmm dd yyyy h:mm AM/PM", "mart 19 2019 12:04 po podne"}, - {"43543.503206018519", "[$-181A]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 po podne"}, - {"43543.503206018519", "[$-181A]mmmmmm dd yyyy h:mm AM/PM", "mart 19 2019 12:04 po podne"}, + {"43543.503206018519", "[$-181A]mmmm dd yyyy h:mm AM/PM aaa", "mart 19 2019 12:04 po podne uto"}, + {"43543.503206018519", "[$-181A]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 po podne uto"}, + {"43543.503206018519", "[$-181A]mmmmmm dd yyyy h:mm AM/PM dddd", "mart 19 2019 12:04 po podne utorak"}, {"44562.189571759256", "[$-2C1A]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 prije podne"}, {"44562.189571759256", "[$-2C1A]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 prije podne"}, {"44562.189571759256", "[$-2C1A]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 prije podne"}, {"44562.189571759256", "[$-2C1A]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 prije podne"}, {"43543.503206018519", "[$-2C1A]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 po podne"}, - {"43543.503206018519", "[$-2C1A]mmmm dd yyyy h:mm AM/PM", "mart 19 2019 12:04 po podne"}, - {"43543.503206018519", "[$-2C1A]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 po podne"}, - {"43543.503206018519", "[$-2C1A]mmmmmm dd yyyy h:mm AM/PM", "mart 19 2019 12:04 po podne"}, + {"43543.503206018519", "[$-2C1A]mmmm dd yyyy h:mm AM/PM aaa", "mart 19 2019 12:04 po podne uto"}, + {"43543.503206018519", "[$-2C1A]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 po podne uto"}, + {"43543.503206018519", "[$-2C1A]mmmmmm dd yyyy h:mm AM/PM dddd", "mart 19 2019 12:04 po podne utorak"}, {"44562.189571759256", "[$-241A]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 pre podne"}, {"44562.189571759256", "[$-241A]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 pre podne"}, {"44562.189571759256", "[$-241A]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 pre podne"}, {"44562.189571759256", "[$-241A]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 pre podne"}, {"43543.503206018519", "[$-241A]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 po podne"}, - {"43543.503206018519", "[$-241A]mmmm dd yyyy h:mm AM/PM", "mart 19 2019 12:04 po podne"}, - {"43543.503206018519", "[$-241A]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 po podne"}, - {"43543.503206018519", "[$-241A]mmmmmm dd yyyy h:mm AM/PM", "mart 19 2019 12:04 po podne"}, + {"43543.503206018519", "[$-241A]mmmm dd yyyy h:mm AM/PM aaa", "mart 19 2019 12:04 po podne uto"}, + {"43543.503206018519", "[$-241A]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 po podne uto"}, + {"43543.503206018519", "[$-241A]mmmmmm dd yyyy h:mm AM/PM dddd", "mart 19 2019 12:04 po podne utorak"}, {"44562.189571759256", "[$-81A]mmm dd yyyy h:mm AM/PM", "jan. 01 2022 4:32 AM"}, {"44562.189571759256", "[$-81A]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 AM"}, {"44562.189571759256", "[$-81A]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"}, {"44562.189571759256", "[$-81A]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 AM"}, {"43543.503206018519", "[$-81A]mmm dd yyyy h:mm AM/PM", "mart 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-81A]mmmm dd yyyy h:mm AM/PM", "mart 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-81A]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-81A]mmmmmm dd yyyy h:mm AM/PM", "mart 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-81A]mmmm dd yyyy h:mm AM/PM aaa", "mart 19 2019 12:04 PM uto."}, + {"43543.503206018519", "[$-81A]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM uto."}, + {"43543.503206018519", "[$-81A]mmmmmm dd yyyy h:mm AM/PM dddd", "mart 19 2019 12:04 PM utorak"}, {"44562.189571759256", "[$-6C]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"}, {"44562.189571759256", "[$-6C]mmmm dd yyyy h:mm AM/PM", "Janaware 01 2022 4:32 AM"}, {"44562.189571759256", "[$-6C]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"}, {"44562.189571759256", "[$-6C]mmmmmm dd yyyy h:mm AM/PM", "Janaware 01 2022 4:32 AM"}, {"43543.503206018519", "[$-6C]mmm dd yyyy h:mm AM/PM", "Matš 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-6C]mmmm dd yyyy h:mm AM/PM", "Matšhe 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-6C]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-6C]mmmmmm dd yyyy h:mm AM/PM", "Matšhe 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-6C]mmmm dd yyyy h:mm AM/PM aaa", "Matšhe 19 2019 12:04 PM Lbb"}, + {"43543.503206018519", "[$-6C]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM Lbb"}, + {"43543.503206018519", "[$-6C]mmmmmm dd yyyy h:mm AM/PM dddd", "Matšhe 19 2019 12:04 PM Labobedi"}, {"44562.189571759256", "[$-46C]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"}, {"44562.189571759256", "[$-46C]mmmm dd yyyy h:mm AM/PM", "Janaware 01 2022 4:32 AM"}, {"44562.189571759256", "[$-46C]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"}, {"44562.189571759256", "[$-46C]mmmmmm dd yyyy h:mm AM/PM", "Janaware 01 2022 4:32 AM"}, {"43543.503206018519", "[$-46C]mmm dd yyyy h:mm AM/PM", "Matš 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-46C]mmmm dd yyyy h:mm AM/PM", "Matšhe 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-46C]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-46C]mmmmmm dd yyyy h:mm AM/PM", "Matšhe 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-46C]mmmm dd yyyy h:mm AM/PM aaa", "Matšhe 19 2019 12:04 PM Lbb"}, + {"43543.503206018519", "[$-46C]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM Lbb"}, + {"43543.503206018519", "[$-46C]mmmmmm dd yyyy h:mm AM/PM dddd", "Matšhe 19 2019 12:04 PM Labobedi"}, {"44562.189571759256", "[$-32]mmm dd yyyy h:mm AM/PM", "Fer. 01 2022 4:32 AM"}, {"44562.189571759256", "[$-32]mmmm dd yyyy h:mm AM/PM", "Ferikgong 01 2022 4:32 AM"}, {"44562.189571759256", "[$-32]mmmmm dd yyyy h:mm AM/PM", "F 01 2022 4:32 AM"}, {"44562.189571759256", "[$-32]mmmmmm dd yyyy h:mm AM/PM", "Ferikgong 01 2022 4:32 AM"}, {"43543.503206018519", "[$-32]mmm dd yyyy h:mm AM/PM", "Mop. 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-32]mmmm dd yyyy h:mm AM/PM", "Mopitlwe 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-32]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-32]mmmmmm dd yyyy h:mm AM/PM", "Mopitlwe 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-32]mmmm dd yyyy h:mm AM/PM aaa", "Mopitlwe 19 2019 12:04 PM Lab."}, + {"43543.503206018519", "[$-32]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM Lab."}, + {"43543.503206018519", "[$-32]mmmmmm dd yyyy h:mm AM/PM dddd", "Mopitlwe 19 2019 12:04 PM Labobedi"}, {"44562.189571759256", "[$-832]mmm dd yyyy h:mm AM/PM", "Fer. 01 2022 4:32 AM"}, {"44562.189571759256", "[$-832]mmmm dd yyyy h:mm AM/PM", "Ferikgong 01 2022 4:32 AM"}, {"44562.189571759256", "[$-832]mmmmm dd yyyy h:mm AM/PM", "F 01 2022 4:32 AM"}, {"44562.189571759256", "[$-832]mmmmmm dd yyyy h:mm AM/PM", "Ferikgong 01 2022 4:32 AM"}, {"43543.503206018519", "[$-832]mmm dd yyyy h:mm AM/PM", "Mop. 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-832]mmmm dd yyyy h:mm AM/PM", "Mopitlwe 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-832]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-832]mmmmmm dd yyyy h:mm AM/PM", "Mopitlwe 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-832]mmmm dd yyyy h:mm AM/PM aaa", "Mopitlwe 19 2019 12:04 PM Lab."}, + {"43543.503206018519", "[$-832]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM Lab."}, + {"43543.503206018519", "[$-832]mmmmmm dd yyyy h:mm AM/PM dddd", "Mopitlwe 19 2019 12:04 PM Labobedi"}, {"44562.189571759256", "[$-432]mmm dd yyyy h:mm AM/PM", "Fer. 01 2022 4:32 AM"}, {"44562.189571759256", "[$-432]mmmm dd yyyy h:mm AM/PM", "Ferikgong 01 2022 4:32 AM"}, {"44562.189571759256", "[$-432]mmmmm dd yyyy h:mm AM/PM", "F 01 2022 4:32 AM"}, {"44562.189571759256", "[$-432]mmmmmm dd yyyy h:mm AM/PM", "Ferikgong 01 2022 4:32 AM"}, {"43543.503206018519", "[$-432]mmm dd yyyy h:mm AM/PM", "Mop. 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-432]mmmm dd yyyy h:mm AM/PM", "Mopitlwe 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-432]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-432]mmmmmm dd yyyy h:mm AM/PM", "Mopitlwe 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-432]mmmm dd yyyy h:mm AM/PM aaa", "Mopitlwe 19 2019 12:04 PM Lab."}, + {"43543.503206018519", "[$-432]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM Lab."}, + {"43543.503206018519", "[$-432]mmmmmm dd yyyy h:mm AM/PM dddd", "Mopitlwe 19 2019 12:04 PM Labobedi"}, {"44562.189571759256", "[$-59]mmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u064A 01 2022 4:32 AM"}, {"44562.189571759256", "[$-59]mmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u064A 01 2022 4:32 AM"}, {"44562.189571759256", "[$-59]mmmmm dd yyyy h:mm AM/PM", "\u062C 01 2022 4:32 AM"}, {"44562.189571759256", "[$-59]mmmmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u064A 01 2022 4:32 AM"}, {"43543.503206018519", "[$-59]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-59]mmmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-59]mmmmm dd yyyy h:mm AM/PM", "\u0645 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-59]mmmmmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-59]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM \u0627\u0631"}, + {"43543.503206018519", "[$-59]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 PM \u0627\u0631"}, + {"43543.503206018519", "[$-59]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM \u0627\u0631\u0628\u0639"}, {"44562.189571759256", "[$-7C59]mmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u064A 01 2022 4:32 AM"}, {"44562.189571759256", "[$-7C59]mmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u064A 01 2022 4:32 AM"}, {"44562.189571759256", "[$-7C59]mmmmm dd yyyy h:mm AM/PM", "\u062C 01 2022 4:32 AM"}, {"44562.189571759256", "[$-7C59]mmmmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u064A 01 2022 4:32 AM"}, {"43543.503206018519", "[$-7C59]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-7C59]mmmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-7C59]mmmmm dd yyyy h:mm AM/PM", "\u0645 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-7C59]mmmmmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-7C59]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM \u0627\u0631"}, + {"43543.503206018519", "[$-7C59]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 PM \u0627\u0631"}, + {"43543.503206018519", "[$-7C59]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM \u0627\u0631\u0628\u0639"}, {"44562.189571759256", "[$-859]mmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u064A 01 2022 4:32 AM"}, {"44562.189571759256", "[$-859]mmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u064A 01 2022 4:32 AM"}, {"44562.189571759256", "[$-859]mmmmm dd yyyy h:mm AM/PM", "\u062C 01 2022 4:32 AM"}, {"44562.189571759256", "[$-859]mmmmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u064A 01 2022 4:32 AM"}, {"43543.503206018519", "[$-859]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-859]mmmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-859]mmmmm dd yyyy h:mm AM/PM", "\u0645 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-859]mmmmmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-859]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM \u0627\u0631"}, + {"43543.503206018519", "[$-859]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 PM \u0627\u0631"}, + {"43543.503206018519", "[$-859]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM \u0627\u0631\u0628\u0639"}, {"44562.189571759256", "[$-5B]mmm dd yyyy h:mm AM/PM", "\u0DA2\u0DB1. 01 2022 4:32 \u0DB4\u0DD9.\u0DC0."}, {"44562.189571759256", "[$-5B]mmmm dd yyyy h:mm AM/PM", "\u0DA2\u0DB1\u0DC0\u0DCF\u0DBB\u0DD2 01 2022 4:32 \u0DB4\u0DD9.\u0DC0."}, {"44562.189571759256", "[$-5B]mmmmm dd yyyy h:mm AM/PM", "\u0DA2 01 2022 4:32 \u0DB4\u0DD9.\u0DC0."}, {"44562.189571759256", "[$-5B]mmmmmm dd yyyy h:mm AM/PM", "\u0DA2\u0DB1\u0DC0\u0DCF\u0DBB\u0DD2 01 2022 4:32 \u0DB4\u0DD9.\u0DC0."}, {"43543.503206018519", "[$-5B]mmm dd yyyy h:mm AM/PM", "\u0DB8\u0DCF\u0DBB\u0DCA\u0DAD\u0DD4. 19 2019 12:04 \u0DB4.\u0DC0."}, - {"43543.503206018519", "[$-5B]mmmm dd yyyy h:mm AM/PM", "\u0DB8\u0DCF\u0DBB\u0DCA\u0DAD\u0DD4 19 2019 12:04 \u0DB4.\u0DC0."}, - {"43543.503206018519", "[$-5B]mmmmm dd yyyy h:mm AM/PM", "\u0DB8 19 2019 12:04 \u0DB4.\u0DC0."}, - {"43543.503206018519", "[$-5B]mmmmmm dd yyyy h:mm AM/PM", "\u0DB8\u0DCF\u0DBB\u0DCA\u0DAD\u0DD4 19 2019 12:04 \u0DB4.\u0DC0."}, - + {"43543.503206018519", "[$-5B]mmmm dd yyyy h:mm AM/PM aaa", "\u0DB8\u0DCF\u0DBB\u0DCA\u0DAD\u0DD4 19 2019 12:04 \u0DB4.\u0DC0. \u0627\u0631"}, + {"43543.503206018519", "[$-5B]mmmmm dd yyyy h:mm AM/PM ddd", "\u0DB8 19 2019 12:04 \u0DB4.\u0DC0. \u0627\u0631"}, + {"43543.503206018519", "[$-5B]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0DB8\u0DCF\u0DBB\u0DCA\u0DAD\u0DD4 19 2019 12:04 \u0DB4.\u0DC0. \u0627\u0631\u0628\u0639"}, {"44562.189571759256", "[$-45B]mmm dd yyyy h:mm AM/PM", "\u0DA2\u0DB1. 01 2022 4:32 \u0DB4\u0DD9.\u0DC0."}, {"44562.189571759256", "[$-45B]mmmm dd yyyy h:mm AM/PM", "\u0DA2\u0DB1\u0DC0\u0DCF\u0DBB\u0DD2 01 2022 4:32 \u0DB4\u0DD9.\u0DC0."}, {"44562.189571759256", "[$-45B]mmmmm dd yyyy h:mm AM/PM", "\u0DA2 01 2022 4:32 \u0DB4\u0DD9.\u0DC0."}, {"44562.189571759256", "[$-45B]mmmmmm dd yyyy h:mm AM/PM", "\u0DA2\u0DB1\u0DC0\u0DCF\u0DBB\u0DD2 01 2022 4:32 \u0DB4\u0DD9.\u0DC0."}, {"43543.503206018519", "[$-45B]mmm dd yyyy h:mm AM/PM", "\u0DB8\u0DCF\u0DBB\u0DCA\u0DAD\u0DD4. 19 2019 12:04 \u0DB4.\u0DC0."}, - {"43543.503206018519", "[$-45B]mmmm dd yyyy h:mm AM/PM", "\u0DB8\u0DCF\u0DBB\u0DCA\u0DAD\u0DD4 19 2019 12:04 \u0DB4.\u0DC0."}, - {"43543.503206018519", "[$-45B]mmmmm dd yyyy h:mm AM/PM", "\u0DB8 19 2019 12:04 \u0DB4.\u0DC0."}, - {"43543.503206018519", "[$-45B]mmmmmm dd yyyy h:mm AM/PM", "\u0DB8\u0DCF\u0DBB\u0DCA\u0DAD\u0DD4 19 2019 12:04 \u0DB4.\u0DC0."}, + {"43543.503206018519", "[$-45B]mmmm dd yyyy h:mm AM/PM aaa", "\u0DB8\u0DCF\u0DBB\u0DCA\u0DAD\u0DD4 19 2019 12:04 \u0DB4.\u0DC0. \u0627\u0631"}, + {"43543.503206018519", "[$-45B]mmmmm dd yyyy h:mm AM/PM ddd", "\u0DB8 19 2019 12:04 \u0DB4.\u0DC0. \u0627\u0631"}, + {"43543.503206018519", "[$-45B]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0DB8\u0DCF\u0DBB\u0DCA\u0DAD\u0DD4 19 2019 12:04 \u0DB4.\u0DC0. \u0627\u0631\u0628\u0639"}, {"44562.189571759256", "[$-1B]mmm dd yyyy h:mm AM/PM", "1 01 2022 4:32 AM"}, {"44562.189571759256", "[$-1B]mmmm dd yyyy h:mm AM/PM", "január 01 2022 4:32 AM"}, {"44562.189571759256", "[$-1B]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"}, {"44562.189571759256", "[$-1B]mmmmmm dd yyyy h:mm AM/PM", "január 01 2022 4:32 AM"}, {"43543.503206018519", "[$-1B]mmm dd yyyy h:mm AM/PM", "3 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-1B]mmmm dd yyyy h:mm AM/PM", "marec 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-1B]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-1B]mmmmmm dd yyyy h:mm AM/PM", "marec 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-1B]mmmm dd yyyy h:mm AM/PM aaa", "marec 19 2019 12:04 PM ut"}, + {"43543.503206018519", "[$-1B]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM ut"}, + {"43543.503206018519", "[$-1B]mmmmmm dd yyyy h:mm AM/PM dddd", "marec 19 2019 12:04 PM utorok"}, {"44562.189571759256", "[$-41B]mmm dd yyyy h:mm AM/PM", "1 01 2022 4:32 AM"}, {"44562.189571759256", "[$-41B]mmmm dd yyyy h:mm AM/PM", "január 01 2022 4:32 AM"}, {"44562.189571759256", "[$-41B]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"}, {"44562.189571759256", "[$-41B]mmmmmm dd yyyy h:mm AM/PM", "január 01 2022 4:32 AM"}, {"43543.503206018519", "[$-41B]mmm dd yyyy h:mm AM/PM", "3 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-41B]mmmm dd yyyy h:mm AM/PM", "marec 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-41B]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 PM"}, - {"43543.503206018519", "[$-41B]mmmmmm dd yyyy h:mm AM/PM", "marec 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-41B]mmmm dd yyyy h:mm AM/PM aaa", "marec 19 2019 12:04 PM ut"}, + {"43543.503206018519", "[$-41B]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM ut"}, + {"43543.503206018519", "[$-41B]mmmmmm dd yyyy h:mm AM/PM dddd", "marec 19 2019 12:04 PM utorok"}, {"44562.189571759256", "[$-24]mmm dd yyyy h:mm AM/PM", "jan. 01 2022 4:32 dop."}, {"44562.189571759256", "[$-24]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 dop."}, {"44562.189571759256", "[$-24]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 dop."}, {"44562.189571759256", "[$-24]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 dop."}, {"43543.503206018519", "[$-24]mmm dd yyyy h:mm AM/PM", "mar. 19 2019 12:04 pop."}, - {"43543.503206018519", "[$-24]mmmm dd yyyy h:mm AM/PM", "marec 19 2019 12:04 pop."}, - {"43543.503206018519", "[$-24]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 pop."}, - {"43543.503206018519", "[$-24]mmmmmm dd yyyy h:mm AM/PM", "marec 19 2019 12:04 pop."}, + {"43543.503206018519", "[$-24]mmmm dd yyyy h:mm AM/PM aaa", "marec 19 2019 12:04 pop. tor."}, + {"43543.503206018519", "[$-24]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 pop. tor."}, + {"43543.503206018519", "[$-24]mmmmmm dd yyyy h:mm AM/PM dddd", "marec 19 2019 12:04 pop. torek"}, {"44562.189571759256", "[$-424]mmm dd yyyy h:mm AM/PM", "jan. 01 2022 4:32 dop."}, {"44562.189571759256", "[$-424]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 dop."}, {"44562.189571759256", "[$-424]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 dop."}, {"44562.189571759256", "[$-424]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 dop."}, {"43543.503206018519", "[$-424]mmm dd yyyy h:mm AM/PM", "mar. 19 2019 12:04 pop."}, - {"43543.503206018519", "[$-424]mmmm dd yyyy h:mm AM/PM", "marec 19 2019 12:04 pop."}, - {"43543.503206018519", "[$-424]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 pop."}, - {"43543.503206018519", "[$-424]mmmmmm dd yyyy h:mm AM/PM", "marec 19 2019 12:04 pop."}, + {"43543.503206018519", "[$-424]mmmm dd yyyy h:mm AM/PM aaa", "marec 19 2019 12:04 pop. tor."}, + {"43543.503206018519", "[$-424]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 pop. tor."}, + {"43543.503206018519", "[$-424]mmmmmm dd yyyy h:mm AM/PM dddd", "marec 19 2019 12:04 pop. torek"}, {"44562.189571759256", "[$-77]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 GH"}, {"44562.189571759256", "[$-77]mmmm dd yyyy h:mm AM/PM", "Jannaayo 01 2022 4:32 GH"}, {"44562.189571759256", "[$-77]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 GH"}, {"44562.189571759256", "[$-77]mmmmmm dd yyyy h:mm AM/PM", "Jannaayo 01 2022 4:32 GH"}, {"43543.503206018519", "[$-77]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 GD"}, - {"43543.503206018519", "[$-77]mmmm dd yyyy h:mm AM/PM", "Maarso 19 2019 12:04 GD"}, - {"43543.503206018519", "[$-77]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 GD"}, - {"43543.503206018519", "[$-77]mmmmmm dd yyyy h:mm AM/PM", "Maarso 19 2019 12:04 GD"}, + {"43543.503206018519", "[$-77]mmmm dd yyyy h:mm AM/PM aaa", "Maarso 19 2019 12:04 GD Tldo"}, + {"43543.503206018519", "[$-77]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 GD Tldo"}, + {"43543.503206018519", "[$-77]mmmmmm dd yyyy h:mm AM/PM dddd", "Maarso 19 2019 12:04 GD Talaado"}, {"44562.189571759256", "[$-477]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 GH"}, {"44562.189571759256", "[$-477]mmmm dd yyyy h:mm AM/PM", "Jannaayo 01 2022 4:32 GH"}, {"44562.189571759256", "[$-477]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 GH"}, {"44562.189571759256", "[$-477]mmmmmm dd yyyy h:mm AM/PM", "Jannaayo 01 2022 4:32 GH"}, {"43543.503206018519", "[$-477]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 GD"}, - {"43543.503206018519", "[$-477]mmmm dd yyyy h:mm AM/PM", "Maarso 19 2019 12:04 GD"}, - {"43543.503206018519", "[$-477]mmmmm dd yyyy h:mm AM/PM", "M 19 2019 12:04 GD"}, - {"43543.503206018519", "[$-477]mmmmmm dd yyyy h:mm AM/PM", "Maarso 19 2019 12:04 GD"}, + {"43543.503206018519", "[$-477]mmmm dd yyyy h:mm AM/PM aaa", "Maarso 19 2019 12:04 GD Tldo"}, + {"43543.503206018519", "[$-477]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 GD Tldo"}, + {"43543.503206018519", "[$-477]mmmmmm dd yyyy h:mm AM/PM dddd", "Maarso 19 2019 12:04 GD Talaado"}, + {"44562.189571759256", "[$-30]mmm dd yyyy h:mm AM/PM", "Phe 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-30]mmmm dd yyyy h:mm AM/PM", "Phesekgong 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-30]mmmmm dd yyyy h:mm AM/PM", "P 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-30]mmmmmm dd yyyy h:mm AM/PM", "Phesekgong 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-30]mmm dd yyyy h:mm AM/PM", "Ube 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-30]mmmm dd yyyy h:mm AM/PM aaa", "Hlakubele 19 2019 12:04 PM Bed"}, + {"43543.503206018519", "[$-30]mmmmm dd yyyy h:mm AM/PM ddd", "H 19 2019 12:04 PM Bed"}, + {"43543.503206018519", "[$-30]mmmmmm dd yyyy h:mm AM/PM dddd", "Hlakubele 19 2019 12:04 PM Labobedi"}, + {"44562.189571759256", "[$-430]mmm dd yyyy h:mm AM/PM", "Phe 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-430]mmmm dd yyyy h:mm AM/PM", "Phesekgong 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-430]mmmmm dd yyyy h:mm AM/PM", "P 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-430]mmmmmm dd yyyy h:mm AM/PM", "Phesekgong 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-430]mmm dd yyyy h:mm AM/PM", "Ube 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-430]mmmm dd yyyy h:mm AM/PM aaa", "Hlakubele 19 2019 12:04 PM Bed"}, + {"43543.503206018519", "[$-430]mmmmm dd yyyy h:mm AM/PM ddd", "H 19 2019 12:04 PM Bed"}, + {"43543.503206018519", "[$-430]mmmmmm dd yyyy h:mm AM/PM dddd", "Hlakubele 19 2019 12:04 PM Labobedi"}, {"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."}, @@ -1829,60 +2331,268 @@ func TestNumFmt(t *testing.T) { {"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 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 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 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 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 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 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 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."}, + {"43543.503206018519", "[$-A]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 p. m. ma."}, + {"43543.503206018519", "[$-A]mmmm dd yyyy h:mm AM/PM ddd", "marzo 19 2019 12:04 p. m. ma."}, + {"43543.503206018519", "[$-A]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 p. m. martes"}, + {"44562.189571759256", "[$-2C0A]mmm dd yyyy h:mm AM/PM", "ene 01 2022 4:32 a.%A0m."}, + {"44562.189571759256", "[$-2C0A]mmmm dd yyyy h:mm AM/PM", "enero 01 2022 4:32 a.%A0m."}, + {"44562.189571759256", "[$-2C0A]mmmmm dd yyyy h:mm AM/PM", "e 01 2022 4:32 a.%A0m."}, + {"43543.503206018519", "[$-2C0A]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 p.%A0m. mar."}, + {"43543.503206018519", "[$-2C0A]mmmm dd yyyy h:mm AM/PM ddd", "marzo 19 2019 12:04 p.%A0m. mar."}, + {"43543.503206018519", "[$-2C0A]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 p.%A0m. martes"}, + {"44562.189571759256", "[$-200A]mmm dd yyyy h:mm AM/PM", "ene 01 2022 4:32 a.%A0m."}, + {"44562.189571759256", "[$-200A]mmmm dd yyyy h:mm AM/PM", "enero 01 2022 4:32 a.%A0m."}, + {"44562.189571759256", "[$-200A]mmmmm dd yyyy h:mm AM/PM", "e 01 2022 4:32 a.%A0m."}, + {"43543.503206018519", "[$-200A]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 p.%A0m. mar."}, + {"43543.503206018519", "[$-200A]mmmm dd yyyy h:mm AM/PM ddd", "marzo 19 2019 12:04 p.%A0m. mar."}, + {"43543.503206018519", "[$-200A]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 p.%A0m. martes"}, + {"44562.189571759256", "[$-400A]mmm dd yyyy h:mm AM/PM", "ene 01 2022 4:32 a.%A0m."}, + {"44562.189571759256", "[$-400A]mmmm dd yyyy h:mm AM/PM", "enero 01 2022 4:32 a.%A0m."}, + {"44562.189571759256", "[$-400A]mmmmm dd yyyy h:mm AM/PM", "e 01 2022 4:32 a.%A0m."}, + {"43543.503206018519", "[$-400A]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 p.%A0m. mar."}, + {"43543.503206018519", "[$-400A]mmmm dd yyyy h:mm AM/PM ddd", "marzo 19 2019 12:04 p.%A0m. mar."}, + {"43543.503206018519", "[$-400A]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 p.%A0m. martes"}, + {"44562.189571759256", "[$-340A]mmm dd yyyy h:mm AM/PM", "ene 01 2022 4:32 a.%A0m."}, + {"44562.189571759256", "[$-340A]mmmm dd yyyy h:mm AM/PM", "enero 01 2022 4:32 a.%A0m."}, + {"44562.189571759256", "[$-340A]mmmmm dd yyyy h:mm AM/PM", "e 01 2022 4:32 a.%A0m."}, + {"43543.503206018519", "[$-340A]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 p.%A0m. mar."}, + {"43543.503206018519", "[$-340A]mmmm dd yyyy h:mm AM/PM ddd", "marzo 19 2019 12:04 p.%A0m. mar."}, + {"43543.503206018519", "[$-340A]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 p.%A0m. martes"}, + {"44562.189571759256", "[$-240A]mmm dd yyyy h:mm AM/PM", "ene 01 2022 4:32 a.%A0m."}, + {"44562.189571759256", "[$-240A]mmmm dd yyyy h:mm AM/PM", "enero 01 2022 4:32 a.%A0m."}, + {"44562.189571759256", "[$-240A]mmmmm dd yyyy h:mm AM/PM", "e 01 2022 4:32 a.%A0m."}, + {"43543.503206018519", "[$-240A]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 p.%A0m. mar."}, + {"43543.503206018519", "[$-240A]mmmm dd yyyy h:mm AM/PM ddd", "marzo 19 2019 12:04 p.%A0m. mar."}, + {"43543.503206018519", "[$-240A]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 p.%A0m. martes"}, + {"44562.189571759256", "[$-140A]mmm dd yyyy h:mm AM/PM", "ene 01 2022 4:32 a.%A0m."}, + {"44562.189571759256", "[$-140A]mmmm dd yyyy h:mm AM/PM", "enero 01 2022 4:32 a.%A0m."}, + {"44562.189571759256", "[$-140A]mmmmm dd yyyy h:mm AM/PM", "e 01 2022 4:32 a.%A0m."}, + {"43543.503206018519", "[$-140A]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 p.%A0m. mar."}, + {"43543.503206018519", "[$-140A]mmmm dd yyyy h:mm AM/PM ddd", "marzo 19 2019 12:04 p.%A0m. mar."}, + {"43543.503206018519", "[$-140A]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 p.%A0m. martes"}, {"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 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."}, + {"43543.503206018519", "[$-5C0A]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 p.m. mar."}, + {"43543.503206018519", "[$-5C0A]mmmm dd yyyy h:mm AM/PM ddd", "marzo 19 2019 12:04 p.m. mar."}, + {"43543.503206018519", "[$-5C0A]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 p.m. martes"}, + {"43543.503206018519", "[$-1C0A]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 p.%A0m. mar."}, + {"43543.503206018519", "[$-1C0A]mmmm dd yyyy h:mm AM/PM ddd", "marzo 19 2019 12:04 p.%A0m. mar."}, + {"43543.503206018519", "[$-1C0A]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 p.%A0m. martes"}, + {"43543.503206018519", "[$-300A]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 p.%A0m. mar."}, + {"43543.503206018519", "[$-300A]mmmm dd yyyy h:mm AM/PM ddd", "marzo 19 2019 12:04 p.%A0m. mar."}, + {"43543.503206018519", "[$-300A]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 p.%A0m. martes"}, + {"43543.503206018519", "[$-440A]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 p.%A0m. mar."}, + {"43543.503206018519", "[$-440A]mmmm dd yyyy h:mm AM/PM ddd", "marzo 19 2019 12:04 p.%A0m. mar."}, + {"43543.503206018519", "[$-440A]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 p.%A0m. martes"}, + {"43543.503206018519", "[$-100A]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 p.%A0m. mar."}, + {"43543.503206018519", "[$-100A]mmmm dd yyyy h:mm AM/PM ddd", "marzo 19 2019 12:04 p.%A0m. mar."}, + {"43543.503206018519", "[$-100A]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 p.%A0m. martes"}, + {"43543.503206018519", "[$-480A]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 p.%A0m. mar."}, + {"43543.503206018519", "[$-480A]mmmm dd yyyy h:mm AM/PM ddd", "marzo 19 2019 12:04 p.%A0m. mar."}, + {"43543.503206018519", "[$-480A]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 p.%A0m. martes"}, + {"43543.503206018519", "[$-580A]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 p.m. mar."}, + {"43543.503206018519", "[$-580A]mmmm dd yyyy h:mm AM/PM ddd", "marzo 19 2019 12:04 p.m. mar."}, + {"43543.503206018519", "[$-580A]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 p.m. martes"}, + {"44562.189571759256", "[$-80A]mmm dd yyyy h:mm AM/PM", "ene 01 2022 4:32 a. m."}, + {"44562.189571759256", "[$-80A]mmmm dd yyyy h:mm AM/PM", "enero 01 2022 4:32 a. m."}, + {"44562.189571759256", "[$-80A]mmmmm dd yyyy h:mm AM/PM", "e 01 2022 4:32 a. m."}, + {"43543.503206018519", "[$-80A]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 p. m."}, + {"43543.503206018519", "[$-80A]mmmm dd yyyy h:mm AM/PM", "marzo 19 2019 12:04 p. m."}, + {"43543.503206018519", "[$-80A]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 p. m."}, + {"44562.189571759256", "[$-80A]mmm dd yyyy h:mm AM/PM", "ene 01 2022 4:32 a. m."}, + {"44562.189571759256", "[$-80A]mmmm dd yyyy h:mm AM/PM", "enero 01 2022 4:32 a. m."}, + {"44562.189571759256", "[$-80A]mmmmm dd yyyy h:mm AM/PM", "e 01 2022 4:32 a. m."}, + {"43543.503206018519", "[$-80A]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 p. m."}, + {"43543.503206018519", "[$-80A]mmmm dd yyyy h:mm AM/PM", "marzo 19 2019 12:04 p. m."}, + {"43543.503206018519", "[$-80A]mmmmm dd yyyy h:mm AM/PM", "m 19 2019 12:04 p. m."}, + {"44562.189571759256", "[$-80A]mmm dd yyyy h:mm AM/PM", "ene 01 2022 4:32 a. m."}, + {"44562.189571759256", "[$-80A]mmmm dd yyyy h:mm AM/PM", "enero 01 2022 4:32 a. m."}, + {"44562.189571759256", "[$-80A]mmmmm dd yyyy h:mm AM/PM", "e 01 2022 4:32 a. m."}, + {"43543.503206018519", "[$-80A]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 p. m. mar."}, + {"43543.503206018519", "[$-80A]mmmm dd yyyy h:mm AM/PM ddd", "marzo 19 2019 12:04 p. m. mar."}, + {"43543.503206018519", "[$-80A]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 p. m. martes"}, + {"43543.503206018519", "[$-4C0A]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 p.%A0m. mar."}, + {"43543.503206018519", "[$-4C0A]mmmm dd yyyy h:mm AM/PM ddd", "marzo 19 2019 12:04 p.%A0m. mar."}, + {"43543.503206018519", "[$-4C0A]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 p.%A0m. martes"}, + {"43543.503206018519", "[$-180A]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 p.%A0m. mar."}, + {"43543.503206018519", "[$-180A]mmmm dd yyyy h:mm AM/PM ddd", "marzo 19 2019 12:04 p.%A0m. mar."}, + {"43543.503206018519", "[$-180A]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 p.%A0m. martes"}, + {"43543.503206018519", "[$-3C0A]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 p.%A0m. mar."}, + {"43543.503206018519", "[$-3C0A]mmmm dd yyyy h:mm AM/PM ddd", "marzo 19 2019 12:04 p.%A0m. mar."}, + {"43543.503206018519", "[$-3C0A]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 p.%A0m. martes"}, + {"44562.189571759256", "[$-280A]mmm dd yyyy h:mm AM/PM", "Ene. 01 2022 4:32 a.%A0m."}, + {"44562.189571759256", "[$-280A]mmmm dd yyyy h:mm AM/PM", "Enero 01 2022 4:32 a.%A0m."}, + {"44562.189571759256", "[$-280A]mmmmm dd yyyy h:mm AM/PM", "E 01 2022 4:32 a.%A0m."}, + {"44562.189571759256", "[$-280A]mmmmmm dd yyyy h:mm AM/PM", "Enero 01 2022 4:32 a.%A0m."}, + {"43543.503206018519", "[$-280A]mmm dd yyyy h:mm AM/PM", "Mar. 19 2019 12:04 p.%A0m."}, + {"43543.503206018519", "[$-280A]mmmm dd yyyy h:mm AM/PM aaa", "Marzo 19 2019 12:04 p.%A0m. mar."}, + {"43543.503206018519", "[$-280A]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 p.%A0m. mar."}, + {"43543.503206018519", "[$-280A]mmmmmm dd yyyy h:mm AM/PM dddd", "Marzo 19 2019 12:04 p.%A0m. martes"}, + {"43543.503206018519", "[$-500A]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 p.%A0m. mar."}, + {"43543.503206018519", "[$-500A]mmmm dd yyyy h:mm AM/PM ddd", "marzo 19 2019 12:04 p.%A0m. mar."}, + {"43543.503206018519", "[$-500A]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 p.%A0m. martes"}, + {"43543.503206018519", "[$-40A]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 PM ma."}, + {"43543.503206018519", "[$-40A]mmmm dd yyyy h:mm AM/PM ddd", "marzo 19 2019 12:04 PM ma."}, + {"43543.503206018519", "[$-40A]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 PM martes"}, + {"43543.503206018519", "[$-C0A]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 PM ma."}, + {"43543.503206018519", "[$-C0A]mmmm dd yyyy h:mm AM/PM ddd", "marzo 19 2019 12:04 PM ma."}, + {"43543.503206018519", "[$-C0A]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 PM martes"}, + {"43543.503206018519", "[$-540A]mmm dd yyyy h:mm AM/PM aaa", "mar 19 2019 12:04 PM mar"}, + {"43543.503206018519", "[$-540A]mmmm dd yyyy h:mm AM/PM ddd", "marzo 19 2019 12:04 PM mar"}, + {"43543.503206018519", "[$-540A]mmmmm dd yyyy h:mm AM/PM dddd", "m 19 2019 12:04 PM martes"}, + {"44562.189571759256", "[$-380A]mmm dd yyyy h:mm AM/PM", "Ene. 01 2022 4:32 a.%A0m."}, + {"44562.189571759256", "[$-380A]mmmm dd yyyy h:mm AM/PM", "Enero 01 2022 4:32 a.%A0m."}, + {"44562.189571759256", "[$-380A]mmmmm dd yyyy h:mm AM/PM", "E 01 2022 4:32 a.%A0m."}, + {"44562.189571759256", "[$-380A]mmmmmm dd yyyy h:mm AM/PM", "Enero 01 2022 4:32 a.%A0m."}, + {"43543.503206018519", "[$-380A]mmm dd yyyy h:mm AM/PM", "Mar. 19 2019 12:04 p.%A0m."}, + {"43543.503206018519", "[$-380A]mmmm dd yyyy h:mm AM/PM aaa", "Marzo 19 2019 12:04 p.%A0m. mar."}, + {"43543.503206018519", "[$-380A]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 p.%A0m. mar."}, + {"43543.503206018519", "[$-380A]mmmmmm dd yyyy h:mm AM/PM dddd", "Marzo 19 2019 12:04 p.%A0m. martes"}, + {"44562.189571759256", "[$-1D]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-1D]mmmm dd yyyy h:mm AM/PM", "januari 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-1D]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-1D]mmmmmm dd yyyy h:mm AM/PM", "januari 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-1D]mmm dd yyyy h:mm AM/PM", "mar 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-1D]mmmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 PM tis"}, + {"43543.503206018519", "[$-1D]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM tis"}, + {"43543.503206018519", "[$-1D]mmmmmm dd yyyy h:mm AM/PM dddd", "mars 19 2019 12:04 PM tisdag"}, + {"44562.189571759256", "[$-81D]mmm dd yyyy h:mm AM/PM", "jan. 01 2022 4:32 fm"}, + {"44562.189571759256", "[$-81D]mmmm dd yyyy h:mm AM/PM", "januari 01 2022 4:32 fm"}, + {"44562.189571759256", "[$-81D]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 fm"}, + {"44562.189571759256", "[$-81D]mmmmmm dd yyyy h:mm AM/PM", "januari 01 2022 4:32 fm"}, + {"43543.503206018519", "[$-81D]mmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 em"}, + {"43543.503206018519", "[$-81D]mmmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 em tis"}, + {"43543.503206018519", "[$-81D]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 em tis"}, + {"43543.503206018519", "[$-81D]mmmmmm dd yyyy h:mm AM/PM dddd", "mars 19 2019 12:04 em tisdag"}, + {"44562.189571759256", "[$-41D]mmm dd yyyy h:mm AM/PM", "jan. 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-41D]mmmm dd yyyy h:mm AM/PM", "januari 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-41D]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-41D]mmmmmm dd yyyy h:mm AM/PM", "januari 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-41D]mmm dd yyyy h:mm AM/PM", "mars 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-41D]mmmm dd yyyy h:mm AM/PM aaa", "mars 19 2019 12:04 PM tis"}, + {"43543.503206018519", "[$-41D]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 PM tis"}, + {"43543.503206018519", "[$-41D]mmmmmm dd yyyy h:mm AM/PM dddd", "mars 19 2019 12:04 PM tisdag"}, + {"44562.189571759256", "[$-5A]mmm dd yyyy h:mm AM/PM", "\u071F\u0722%A0\u070F\u0712 01 2022 4:32 \u0729.\u071B"}, + {"44562.189571759256", "[$-5A]mmmm dd yyyy h:mm AM/PM", "\u071F\u0722\u0718\u0722%A0\u0710\u071A\u072A\u071D 01 2022 4:32 \u0729.\u071B"}, + {"44562.189571759256", "[$-5A]mmmmm dd yyyy h:mm AM/PM", "\u071F 01 2022 4:32 \u0729.\u071B"}, + {"44562.189571759256", "[$-5A]mmmmmm dd yyyy h:mm AM/PM", "\u071F\u0722\u0718\u0722%A0\u0710\u071A\u072A\u071D 01 2022 4:32 \u0729.\u071B"}, + {"43543.503206018519", "[$-5A]mmm dd yyyy h:mm AM/PM", "\u0710\u0715\u072A 19 2019 12:04 \u0712.\u071B"}, + {"43543.503206018519", "[$-5A]mmmm dd yyyy h:mm AM/PM aaa", "\u0710\u0715\u072A 19 2019 12:04 \u0712.\u071B \u070F\u0713%A0\u070F\u0712\u072B"}, + {"43543.503206018519", "[$-5A]mmmmm dd yyyy h:mm AM/PM ddd", "\u0710 19 2019 12:04 \u0712.\u071B \u070F\u0713%A0\u070F\u0712\u072B"}, + {"43543.503206018519", "[$-5A]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0710\u0715\u072A 19 2019 12:04 \u0712.\u071B \u072C\u0720\u072C\u0710%A0\u0712\u072B\u0712\u0710"}, + {"44562.189571759256", "[$-45A]mmm dd yyyy h:mm AM/PM", "\u071F\u0722%A0\u070F\u0712 01 2022 4:32 \u0729.\u071B"}, + {"44562.189571759256", "[$-45A]mmmm dd yyyy h:mm AM/PM", "\u071F\u0722\u0718\u0722%A0\u0710\u071A\u072A\u071D 01 2022 4:32 \u0729.\u071B"}, + {"44562.189571759256", "[$-45A]mmmmm dd yyyy h:mm AM/PM", "\u071F 01 2022 4:32 \u0729.\u071B"}, + {"44562.189571759256", "[$-45A]mmmmmm dd yyyy h:mm AM/PM", "\u071F\u0722\u0718\u0722%A0\u0710\u071A\u072A\u071D 01 2022 4:32 \u0729.\u071B"}, + {"43543.503206018519", "[$-45A]mmm dd yyyy h:mm AM/PM", "\u0710\u0715\u072A 19 2019 12:04 \u0712.\u071B"}, + {"43543.503206018519", "[$-45A]mmmm dd yyyy h:mm AM/PM aaa", "\u0710\u0715\u072A 19 2019 12:04 \u0712.\u071B \u070F\u0713%A0\u070F\u0712\u072B"}, + {"43543.503206018519", "[$-45A]mmmmm dd yyyy h:mm AM/PM ddd", "\u0710 19 2019 12:04 \u0712.\u071B \u070F\u0713%A0\u070F\u0712\u072B"}, + {"43543.503206018519", "[$-45A]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0710\u0715\u072A 19 2019 12:04 \u0712.\u071B \u072C\u0720\u072C\u0710%A0\u0712\u072B\u0712\u0710"}, + {"44562.189571759256", "[$-28]mmm dd yyyy h:mm AM/PM", "\u044F\u043D\u0432 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-28]mmmm dd yyyy h:mm AM/PM", "\u044F\u043D\u0432\u0430\u0440 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-28]mmmmm dd yyyy h:mm AM/PM", "\u044F 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-28]mmmmmm dd yyyy h:mm AM/PM", "\u044F\u043D\u0432\u0430\u0440 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-28]mmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-28]mmmm dd yyyy h:mm AM/PM aaa", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0441\u0448\u0431"}, + {"43543.503206018519", "[$-28]mmmmm dd yyyy h:mm AM/PM ddd", "\u043C 19 2019 12:04 PM \u0441\u0448\u0431"}, + {"43543.503206018519", "[$-28]mmmmmm dd yyyy h:mm AM/PM dddd", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0441\u0435\u0448\u0430\u043D\u0431\u0435"}, + {"44562.189571759256", "[$-7C28]mmm dd yyyy h:mm AM/PM", "\u044F\u043D\u0432 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-7C28]mmmm dd yyyy h:mm AM/PM", "\u044F\u043D\u0432\u0430\u0440 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-7C28]mmmmm dd yyyy h:mm AM/PM", "\u044F 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-7C28]mmmmmm dd yyyy h:mm AM/PM", "\u044F\u043D\u0432\u0430\u0440 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-7C28]mmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-7C28]mmmm dd yyyy h:mm AM/PM aaa", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0441\u0448\u0431"}, + {"43543.503206018519", "[$-7C28]mmmmm dd yyyy h:mm AM/PM ddd", "\u043C 19 2019 12:04 PM \u0441\u0448\u0431"}, + {"43543.503206018519", "[$-7C28]mmmmmm dd yyyy h:mm AM/PM dddd", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0441\u0435\u0448\u0430\u043D\u0431\u0435"}, + {"44562.189571759256", "[$-428]mmm dd yyyy h:mm AM/PM", "\u044F\u043D\u0432 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-428]mmmm dd yyyy h:mm AM/PM", "\u044F\u043D\u0432\u0430\u0440 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-428]mmmmm dd yyyy h:mm AM/PM", "\u044F 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-428]mmmmmm dd yyyy h:mm AM/PM", "\u044F\u043D\u0432\u0430\u0440 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-428]mmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-428]mmmm dd yyyy h:mm AM/PM aaa", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0441\u0448\u0431"}, + {"43543.503206018519", "[$-428]mmmmm dd yyyy h:mm AM/PM ddd", "\u043C 19 2019 12:04 PM \u0441\u0448\u0431"}, + {"43543.503206018519", "[$-428]mmmmmm dd yyyy h:mm AM/PM dddd", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0441\u0435\u0448\u0430\u043D\u0431\u0435"}, + {"44562.189571759256", "[$-5F]mmm dd yyyy h:mm AM/PM", "Yen 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-5F]mmmm dd yyyy h:mm AM/PM", "Yennayer 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-5F]mmmmm dd yyyy h:mm AM/PM", "Y 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-5F]mmmmmm dd yyyy h:mm AM/PM", "Yennayer 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-5F]mmm dd yyyy h:mm AM/PM", "Megh 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-5F]mmmm dd yyyy h:mm AM/PM aaa", "Meghres 19 2019 12:04 PM ttl"}, + {"43543.503206018519", "[$-5F]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM ttl"}, + {"43543.503206018519", "[$-5F]mmmmmm dd yyyy h:mm AM/PM dddd", "Meghres 19 2019 12:04 PM ttlata"}, + {"44562.189571759256", "[$-7C5F]mmm dd yyyy h:mm AM/PM", "Yen 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-7C5F]mmmm dd yyyy h:mm AM/PM", "Yennayer 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-7C5F]mmmmm dd yyyy h:mm AM/PM", "Y 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-7C5F]mmmmmm dd yyyy h:mm AM/PM", "Yennayer 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-7C5F]mmm dd yyyy h:mm AM/PM", "Megh 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-7C5F]mmmm dd yyyy h:mm AM/PM aaa", "Meghres 19 2019 12:04 PM ttl"}, + {"43543.503206018519", "[$-7C5F]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM ttl"}, + {"43543.503206018519", "[$-7C5F]mmmmmm dd yyyy h:mm AM/PM dddd", "Meghres 19 2019 12:04 PM ttlata"}, + {"44562.189571759256", "[$-85F]mmm dd yyyy h:mm AM/PM", "Yen 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-85F]mmmm dd yyyy h:mm AM/PM", "Yennayer 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-85F]mmmmm dd yyyy h:mm AM/PM", "Y 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-85F]mmmmmm dd yyyy h:mm AM/PM", "Yennayer 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-85F]mmm dd yyyy h:mm AM/PM", "Megh 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-85F]mmmm dd yyyy h:mm AM/PM aaa", "Meghres 19 2019 12:04 PM ttl"}, + {"43543.503206018519", "[$-85F]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM ttl"}, + {"43543.503206018519", "[$-85F]mmmmmm dd yyyy h:mm AM/PM dddd", "Meghres 19 2019 12:04 PM ttlata"}, + {"44562.189571759256", "[$-49]mmm dd yyyy h:mm AM/PM", "\u0B9C\u0BA9\u0BB5\u0BB0\u0BBF 01 2022 4:32 \u0B95\u0BBE\u0BB2\u0BC8"}, + {"44562.189571759256", "[$-49]mmmm dd yyyy h:mm AM/PM", "\u0B9C\u0BA9\u0BB5\u0BB0\u0BBF 01 2022 4:32 \u0B95\u0BBE\u0BB2\u0BC8"}, + {"44562.189571759256", "[$-49]mmmmm dd yyyy h:mm AM/PM", "\u0B9C 01 2022 4:32 \u0B95\u0BBE\u0BB2\u0BC8"}, + {"44562.189571759256", "[$-49]mmmmmm dd yyyy h:mm AM/PM", "\u0B9C\u0BA9\u0BB5\u0BB0\u0BBF 01 2022 4:32 \u0B95\u0BBE\u0BB2\u0BC8"}, + {"43543.503206018519", "[$-49]mmm dd yyyy h:mm AM/PM", "\u0BAE\u0BBE\u0BB0\u0BCD\u0B9A\u0BCD 19 2019 12:04 \u0BAE\u0BBE\u0BB2\u0BC8"}, + {"43543.503206018519", "[$-49]mmmm dd yyyy h:mm AM/PM aaa", "\u0BAE\u0BBE\u0BB0\u0BCD\u0B9A\u0BCD 19 2019 12:04 \u0BAE\u0BBE\u0BB2\u0BC8 \u0B9A\u0BC6\u0BB5\u0BCD\u0BB5\u0BBE\u0BAF\u0BCD"}, + {"43543.503206018519", "[$-49]mmmmm dd yyyy h:mm AM/PM ddd", "\u0BAE 19 2019 12:04 \u0BAE\u0BBE\u0BB2\u0BC8 \u0B9A\u0BC6\u0BB5\u0BCD\u0BB5\u0BBE\u0BAF\u0BCD"}, + {"43543.503206018519", "[$-49]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0BAE\u0BBE\u0BB0\u0BCD\u0B9A\u0BCD 19 2019 12:04 \u0BAE\u0BBE\u0BB2\u0BC8 \u0B9A\u0BC6\u0BB5\u0BCD\u0BB5\u0BBE\u0BAF\u0BCD\u0B95\u0BCD\u0B95\u0BBF\u0BB4\u0BAE\u0BC8"}, + {"44562.189571759256", "[$-449]mmm dd yyyy h:mm AM/PM", "\u0B9C\u0BA9\u0BB5\u0BB0\u0BBF 01 2022 4:32 \u0B95\u0BBE\u0BB2\u0BC8"}, + {"44562.189571759256", "[$-449]mmmm dd yyyy h:mm AM/PM", "\u0B9C\u0BA9\u0BB5\u0BB0\u0BBF 01 2022 4:32 \u0B95\u0BBE\u0BB2\u0BC8"}, + {"44562.189571759256", "[$-449]mmmmm dd yyyy h:mm AM/PM", "\u0B9C 01 2022 4:32 \u0B95\u0BBE\u0BB2\u0BC8"}, + {"44562.189571759256", "[$-449]mmmmmm dd yyyy h:mm AM/PM", "\u0B9C\u0BA9\u0BB5\u0BB0\u0BBF 01 2022 4:32 \u0B95\u0BBE\u0BB2\u0BC8"}, + {"43543.503206018519", "[$-449]mmm dd yyyy h:mm AM/PM", "\u0BAE\u0BBE\u0BB0\u0BCD\u0B9A\u0BCD 19 2019 12:04 \u0BAE\u0BBE\u0BB2\u0BC8"}, + {"43543.503206018519", "[$-449]mmmm dd yyyy h:mm AM/PM aaa", "\u0BAE\u0BBE\u0BB0\u0BCD\u0B9A\u0BCD 19 2019 12:04 \u0BAE\u0BBE\u0BB2\u0BC8 \u0B9A\u0BC6\u0BB5\u0BCD\u0BB5\u0BBE\u0BAF\u0BCD"}, + {"43543.503206018519", "[$-449]mmmmm dd yyyy h:mm AM/PM ddd", "\u0BAE 19 2019 12:04 \u0BAE\u0BBE\u0BB2\u0BC8 \u0B9A\u0BC6\u0BB5\u0BCD\u0BB5\u0BBE\u0BAF\u0BCD"}, + {"43543.503206018519", "[$-449]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0BAE\u0BBE\u0BB0\u0BCD\u0B9A\u0BCD 19 2019 12:04 \u0BAE\u0BBE\u0BB2\u0BC8 \u0B9A\u0BC6\u0BB5\u0BCD\u0BB5\u0BBE\u0BAF\u0BCD\u0B95\u0BCD\u0B95\u0BBF\u0BB4\u0BAE\u0BC8"}, + {"44562.189571759256", "[$-849]mmm dd yyyy h:mm AM/PM", "\u0B9C\u0BA9. 01 2022 4:32 \u0B95\u0BBE\u0BB2\u0BC8"}, + {"44562.189571759256", "[$-849]mmmm dd yyyy h:mm AM/PM", "\u0B9C\u0BA9\u0BB5\u0BB0\u0BBF 01 2022 4:32 \u0B95\u0BBE\u0BB2\u0BC8"}, + {"44562.189571759256", "[$-849]mmmmm dd yyyy h:mm AM/PM", "\u0B9C 01 2022 4:32 \u0B95\u0BBE\u0BB2\u0BC8"}, + {"44562.189571759256", "[$-849]mmmmmm dd yyyy h:mm AM/PM", "\u0B9C\u0BA9\u0BB5\u0BB0\u0BBF 01 2022 4:32 \u0B95\u0BBE\u0BB2\u0BC8"}, + {"43543.503206018519", "[$-849]mmm dd yyyy h:mm AM/PM", "\u0BAE\u0BBE\u0BB0\u0BCD. 19 2019 12:04 \u0BAE\u0BBE\u0BB2\u0BC8"}, + {"43543.503206018519", "[$-849]mmmm dd yyyy h:mm AM/PM aaa", "\u0BAE\u0BBE\u0BB0\u0BCD\u0B9A\u0BCD 19 2019 12:04 \u0BAE\u0BBE\u0BB2\u0BC8 \u0B9A\u0BC6\u0BB5\u0BCD."}, + {"43543.503206018519", "[$-849]mmmmm dd yyyy h:mm AM/PM ddd", "\u0BAE 19 2019 12:04 \u0BAE\u0BBE\u0BB2\u0BC8 \u0B9A\u0BC6\u0BB5\u0BCD."}, + {"43543.503206018519", "[$-849]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0BAE\u0BBE\u0BB0\u0BCD\u0B9A\u0BCD 19 2019 12:04 \u0BAE\u0BBE\u0BB2\u0BC8 \u0B9A\u0BC6\u0BB5\u0BCD\u0BB5\u0BBE\u0BAF\u0BCD"}, + {"44562.189571759256", "[$-44]mmm dd yyyy h:mm AM/PM", "\u0433\u044B\u0439\u043D. 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-44]mmmm dd yyyy h:mm AM/PM", "\u0433\u044B\u0439\u043D\u0432\u0430\u0440 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-44]mmmmm dd yyyy h:mm AM/PM", "\u0433 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-44]mmmmmm dd yyyy h:mm AM/PM", "\u0433\u044B\u0439\u043D\u0432\u0430\u0440 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-44]mmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440. 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-44]mmmm dd yyyy h:mm AM/PM aaa", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0441\u0438\u0448."}, + {"43543.503206018519", "[$-44]mmmmm dd yyyy h:mm AM/PM ddd", "\u043C 19 2019 12:04 PM \u0441\u0438\u0448."}, + {"43543.503206018519", "[$-44]mmmmmm dd yyyy h:mm AM/PM dddd", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0441\u0438\u0448\u04D9\u043C\u0431\u0435"}, + {"44562.189571759256", "[$-444]mmm dd yyyy h:mm AM/PM", "\u0433\u044B\u0439\u043D. 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-444]mmmm dd yyyy h:mm AM/PM", "\u0433\u044B\u0439\u043D\u0432\u0430\u0440 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-444]mmmmm dd yyyy h:mm AM/PM", "\u0433 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-444]mmmmmm dd yyyy h:mm AM/PM", "\u0433\u044B\u0439\u043D\u0432\u0430\u0440 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-444]mmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440. 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-444]mmmm dd yyyy h:mm AM/PM aaa", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0441\u0438\u0448."}, + {"43543.503206018519", "[$-444]mmmmm dd yyyy h:mm AM/PM ddd", "\u043C 19 2019 12:04 PM \u0441\u0438\u0448."}, + {"43543.503206018519", "[$-444]mmmmmm dd yyyy h:mm AM/PM dddd", "\u043C\u0430\u0440\u0442 19 2019 12:04 PM \u0441\u0438\u0448\u04D9\u043C\u0431\u0435"}, + {"44562.189571759256", "[$-4A]mmm dd yyyy h:mm AM/PM", "\u0C1C\u0C28 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-4A]mmmm dd yyyy h:mm AM/PM", "\u0C1C\u0C28\u0C35\u0C30\u0C3F 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-4A]mmmmm dd yyyy h:mm AM/PM", "\u0C1C 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-4A]mmmmmm dd yyyy h:mm AM/PM", "\u0C1C\u0C28\u0C35\u0C30\u0C3F 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-4A]mmm dd yyyy h:mm AM/PM", "\u0C2E\u0C3E\u0C30\u0C4D\u0C1A\u0C3F 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-4A]mmmm dd yyyy h:mm AM/PM aaa", "\u0C2E\u0C3E\u0C30\u0C4D\u0C1A\u0C3F 19 2019 12:04 PM \u0C2E\u0C02\u0C17\u0C33"}, + {"43543.503206018519", "[$-4A]mmmmm dd yyyy h:mm AM/PM ddd", "\u0C2E 19 2019 12:04 PM \u0C2E\u0C02\u0C17\u0C33"}, + {"43543.503206018519", "[$-4A]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0C2E\u0C3E\u0C30\u0C4D\u0C1A\u0C3F 19 2019 12:04 PM \u0C2E\u0C02\u0C17\u0C33\u0C35\u0C3E\u0C30\u0C02"}, + {"44562.189571759256", "[$-44A]mmm dd yyyy h:mm AM/PM", "\u0C1C\u0C28 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-44A]mmmm dd yyyy h:mm AM/PM", "\u0C1C\u0C28\u0C35\u0C30\u0C3F 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-44A]mmmmm dd yyyy h:mm AM/PM", "\u0C1C 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-44A]mmmmmm dd yyyy h:mm AM/PM", "\u0C1C\u0C28\u0C35\u0C30\u0C3F 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-44A]mmm dd yyyy h:mm AM/PM", "\u0C2E\u0C3E\u0C30\u0C4D\u0C1A\u0C3F 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-44A]mmmm dd yyyy h:mm AM/PM aaa", "\u0C2E\u0C3E\u0C30\u0C4D\u0C1A\u0C3F 19 2019 12:04 PM \u0C2E\u0C02\u0C17\u0C33"}, + {"43543.503206018519", "[$-44A]mmmmm dd yyyy h:mm AM/PM ddd", "\u0C2E 19 2019 12:04 PM \u0C2E\u0C02\u0C17\u0C33"}, + {"43543.503206018519", "[$-44A]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0C2E\u0C3E\u0C30\u0C4D\u0C1A\u0C3F 19 2019 12:04 PM \u0C2E\u0C02\u0C17\u0C33\u0C35\u0C3E\u0C30\u0C02"}, {"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"}, @@ -1916,9 +2626,9 @@ func TestNumFmt(t *testing.T) { {"44743.18957170139", "[$-1E]mmmmm dd yyyy h:mm AM/PM", "\u0e01 01 2022 4:32 AM"}, {"44774.18957170139", "[$-1E]mmmmm dd yyyy h:mm AM/PM", "\u0e2a 01 2022 4:32 AM"}, {"44805.18957170139", "[$-1E]mmmmm dd yyyy h:mm AM/PM", "\u0e01 01 2022 4:32 AM"}, - {"44835.18957170139", "[$-1E]mmmmm dd yyyy h:mm AM/PM", "\u0e15 01 2022 4:32 AM"}, - {"44866.18957170139", "[$-1E]mmmmm dd yyyy h:mm AM/PM", "\u0e1e 01 2022 4:32 AM"}, - {"44896.18957170139", "[$-1E]mmmmm dd yyyy h:mm AM/PM", "\u0e18 01 2022 4:32 AM"}, + {"44835.18957170139", "[$-1E]mmmmm dd yyyy h:mm AM/PM aaa", "\u0e15 01 2022 4:32 AM \u0E2A."}, + {"44866.18957170139", "[$-1E]mmmmm dd yyyy h:mm AM/PM ddd", "\u0e1e 01 2022 4:32 AM \u0E2D."}, + {"44896.18957170139", "[$-1E]mmmmm dd yyyy h:mm AM/PM dddd", "\u0e18 01 2022 4:32 AM \u0E1E\u0E24\u0E2B\u0E31\u0E2A\u0E1A\u0E14\u0E35"}, {"44562.189571759256", "[$-41E]mmm dd yyyy h:mm AM/PM", "\u0e21.\u0e04. 01 2022 4:32 AM"}, {"44593.189571759256", "[$-41E]mmm dd yyyy h:mm AM/PM", "\u0e01.\u0e18. 01 2022 4:32 AM"}, {"44621.18957170139", "[$-41E]mmm dd yyyy h:mm AM/PM", "\u0e21.\u0e04. 01 2022 4:32 AM"}, @@ -1952,9 +2662,9 @@ func TestNumFmt(t *testing.T) { {"44743.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM", "\u0e01 01 2022 4:32 AM"}, {"44774.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM", "\u0e2a 01 2022 4:32 AM"}, {"44805.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM", "\u0e01 01 2022 4:32 AM"}, - {"44835.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM", "\u0e15 01 2022 4:32 AM"}, - {"44866.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM", "\u0e1e 01 2022 4:32 AM"}, - {"44896.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM", "\u0e18 01 2022 4:32 AM"}, + {"44835.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM aaa", "\u0e15 01 2022 4:32 AM \u0E2A."}, + {"44866.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM ddd", "\u0e1e 01 2022 4:32 AM \u0E2D."}, + {"44896.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM dddd", "\u0e18 01 2022 4:32 AM \u0E1E\u0E24\u0E2B\u0E31\u0E2A\u0E1A\u0E14\u0E35"}, {"100", "[$-411]ge\"年\"m\"月\"d\"日\";@", "1900年4月9日"}, {"43709", "[$-411]ge\"年\"m\"月\"d\"日\";@", "R1年9月1日"}, {"43709", "[$-411]gge\"年\"m\"月\"d\"日\";@", "\u4EE41年9月1日"}, @@ -2013,9 +2723,9 @@ func TestNumFmt(t *testing.T) { {"44743.18957170139", "[$-51]mmmmm dd yyyy h:mm AM/PM", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, {"44774.18957170139", "[$-51]mmmmm dd yyyy h:mm AM/PM", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, {"44805.18957170139", "[$-51]mmmmm dd yyyy h:mm AM/PM", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, - {"44835.18957170139", "[$-51]mmmmm dd yyyy h:mm AM/PM", "\u0f66 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, - {"44866.18957170139", "[$-51]mmmmm dd yyyy h:mm AM/PM", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, - {"44896.18957170139", "[$-51]mmmmm dd yyyy h:mm AM/PM", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44835.18957170139", "[$-51]mmmmm dd yyyy h:mm AM/PM aaa", "\u0f66 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b \u0F66\u0FA4\u0F7A\u0F53\u0F0B\u0F54\u0F0D"}, + {"44866.18957170139", "[$-51]mmmmm dd yyyy h:mm AM/PM ddd", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b \u0F58\u0F72\u0F42\u0F0B\u0F51\u0F58\u0F62\u0F0D"}, + {"44896.18957170139", "[$-51]mmmmm dd yyyy h:mm AM/PM dddd", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b \u0F42\u0F5F\u0F60\u0F0B\u0F55\u0F74\u0F62\u0F0B\u0F56\u0F74\u0F0D"}, {"44562.189571759256", "[$-451]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f21 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, {"44593.189571759256", "[$-451]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f22 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, {"44621.18957170139", "[$-451]mmm dd yyyy h:mm AM/PM", "\u0f5f\u0fb3\u0f0b\u0f23 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, @@ -2049,9 +2759,49 @@ func TestNumFmt(t *testing.T) { {"44743.18957170139", "[$-451]mmmmm dd yyyy h:mm AM/PM", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, {"44774.18957170139", "[$-451]mmmmm dd yyyy h:mm AM/PM", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, {"44805.18957170139", "[$-451]mmmmm dd yyyy h:mm AM/PM", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, - {"44835.18957170139", "[$-451]mmmmm dd yyyy h:mm AM/PM", "\u0f66 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, - {"44866.18957170139", "[$-451]mmmmm dd yyyy h:mm AM/PM", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, - {"44896.18957170139", "[$-451]mmmmm dd yyyy h:mm AM/PM", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b"}, + {"44835.18957170139", "[$-451]mmmmm dd yyyy h:mm AM/PM aaa", "\u0f66 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b \u0F66\u0FA4\u0F7A\u0F53\u0F0B\u0F54\u0F0D"}, + {"44866.18957170139", "[$-451]mmmmm dd yyyy h:mm AM/PM ddd", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b \u0F58\u0F72\u0F42\u0F0B\u0F51\u0F58\u0F62\u0F0D"}, + {"44896.18957170139", "[$-451]mmmmm dd yyyy h:mm AM/PM dddd", "\u0f5f 01 2022 4:32 \u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b \u0F42\u0F5F\u0F60\u0F0B\u0F55\u0F74\u0F62\u0F0B\u0F56\u0F74\u0F0D"}, + {"44562.189571759256", "[$-73]mmm dd yyyy h:mm AM/PM", "\u1325\u122A 01 2022 4:32 \u1295\u1309\u1206"}, + {"44562.189571759256", "[$-73]mmmm dd yyyy h:mm AM/PM", "\u1325\u122A 01 2022 4:32 \u1295\u1309\u1206"}, + {"44562.189571759256", "[$-73]mmmmm dd yyyy h:mm AM/PM", "\u1325 01 2022 4:32 \u1295\u1309\u1206"}, + {"44562.189571759256", "[$-73]mmmmmm dd yyyy h:mm AM/PM", "\u1325\u122A 01 2022 4:32 \u1295\u1309\u1206"}, + {"43543.503206018519", "[$-73]mmm dd yyyy h:mm AM/PM", "\u1218\u130B 19 2019 12:04 \u12F5\u1215\u122A%20\u1250\u1275\u122A"}, + {"43543.503206018519", "[$-73]mmmm dd yyyy h:mm AM/PM aaa", "\u1218\u130B\u1262\u1275 19 2019 12:04 \u12F5\u1215\u122A%20\u1250\u1275\u122A \u1230\u1209"}, + {"43543.503206018519", "[$-73]mmmmm dd yyyy h:mm AM/PM ddd", "\u1218 19 2019 12:04 \u12F5\u1215\u122A%20\u1250\u1275\u122A \u1230\u1209"}, + {"43543.503206018519", "[$-73]mmmmmm dd yyyy h:mm AM/PM dddd", "\u1218\u130B\u1262\u1275 19 2019 12:04 \u12F5\u1215\u122A%20\u1250\u1275\u122A \u1220\u1209\u1235"}, + {"44562.189571759256", "[$-873]mmm dd yyyy h:mm AM/PM", "\u1325\u122A 01 2022 4:32 \u1295\u1309\u1206%20\u1230\u12D3\u1270"}, + {"44562.189571759256", "[$-873]mmmm dd yyyy h:mm AM/PM", "\u1325\u122A 01 2022 4:32 \u1295\u1309\u1206%20\u1230\u12D3\u1270"}, + {"44562.189571759256", "[$-873]mmmmm dd yyyy h:mm AM/PM", "\u1325 01 2022 4:32 \u1295\u1309\u1206%20\u1230\u12D3\u1270"}, + {"44562.189571759256", "[$-873]mmmmmm dd yyyy h:mm AM/PM", "\u1325\u122A 01 2022 4:32 \u1295\u1309\u1206%20\u1230\u12D3\u1270"}, + {"43543.503206018519", "[$-873]mmm dd yyyy h:mm AM/PM", "\u1218\u130B 19 2019 12:04 \u12F5\u1215\u122D%20\u1230\u12D3\u1275"}, + {"43543.503206018519", "[$-873]mmmm dd yyyy h:mm AM/PM aaa", "\u1218\u130B\u1262\u1275 19 2019 12:04 \u12F5\u1215\u122D%20\u1230\u12D3\u1275 \u1230\u1209"}, + {"43543.503206018519", "[$-873]mmmmm dd yyyy h:mm AM/PM ddd", "\u1218 19 2019 12:04 \u12F5\u1215\u122D%20\u1230\u12D3\u1275 \u1230\u1209"}, + {"43543.503206018519", "[$-873]mmmmmm dd yyyy h:mm AM/PM dddd", "\u1218\u130B\u1262\u1275 19 2019 12:04 \u12F5\u1215\u122D%20\u1230\u12D3\u1275 \u1220\u1209\u1235"}, + {"44562.189571759256", "[$-473]mmm dd yyyy h:mm AM/PM", "\u1325\u122A 01 2022 4:32 \u1295\u1309\u1206"}, + {"44562.189571759256", "[$-473]mmmm dd yyyy h:mm AM/PM", "\u1325\u122A 01 2022 4:32 \u1295\u1309\u1206"}, + {"44562.189571759256", "[$-473]mmmmm dd yyyy h:mm AM/PM", "\u1325 01 2022 4:32 \u1295\u1309\u1206"}, + {"44562.189571759256", "[$-473]mmmmmm dd yyyy h:mm AM/PM", "\u1325\u122A 01 2022 4:32 \u1295\u1309\u1206"}, + {"43543.503206018519", "[$-473]mmm dd yyyy h:mm AM/PM", "\u1218\u130B 19 2019 12:04 \u12F5\u1215\u122A%20\u1250\u1275\u122A"}, + {"43543.503206018519", "[$-473]mmmm dd yyyy h:mm AM/PM aaa", "\u1218\u130B\u1262\u1275 19 2019 12:04 \u12F5\u1215\u122A%20\u1250\u1275\u122A \u1230\u1209"}, + {"43543.503206018519", "[$-473]mmmmm dd yyyy h:mm AM/PM ddd", "\u1218 19 2019 12:04 \u12F5\u1215\u122A%20\u1250\u1275\u122A \u1230\u1209"}, + {"43543.503206018519", "[$-473]mmmmmm dd yyyy h:mm AM/PM dddd", "\u1218\u130B\u1262\u1275 19 2019 12:04 \u12F5\u1215\u122A%20\u1250\u1275\u122A \u1220\u1209\u1235"}, + {"44562.189571759256", "[$-31]mmm dd yyyy h:mm AM/PM", "Sun 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-31]mmmm dd yyyy h:mm AM/PM", "Sunguti 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-31]mmmmm dd yyyy h:mm AM/PM", "S 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-31]mmmmmm dd yyyy h:mm AM/PM", "Sunguti 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-31]mmm dd yyyy h:mm AM/PM", "Kul 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-31]mmmm dd yyyy h:mm AM/PM aaa", "Nyenyankulu 19 2019 12:04 PM Bir"}, + {"43543.503206018519", "[$-31]mmmmm dd yyyy h:mm AM/PM ddd", "N 19 2019 12:04 PM Bir"}, + {"43543.503206018519", "[$-31]mmmmmm dd yyyy h:mm AM/PM dddd", "Nyenyankulu 19 2019 12:04 PM Ravumbirhi"}, + {"44562.189571759256", "[$-431]mmm dd yyyy h:mm AM/PM", "Sun 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-431]mmmm dd yyyy h:mm AM/PM", "Sunguti 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-431]mmmmm dd yyyy h:mm AM/PM", "S 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-431]mmmmmm dd yyyy h:mm AM/PM", "Sunguti 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-431]mmm dd yyyy h:mm AM/PM", "Kul 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-431]mmmm dd yyyy h:mm AM/PM aaa", "Nyenyankulu 19 2019 12:04 PM Bir"}, + {"43543.503206018519", "[$-431]mmmmm dd yyyy h:mm AM/PM ddd", "N 19 2019 12:04 PM Bir"}, + {"43543.503206018519", "[$-431]mmmmmm dd yyyy h:mm AM/PM dddd", "Nyenyankulu 19 2019 12:04 PM Ravumbirhi"}, {"44562.189571759256", "[$-1F]mmm dd yyyy h:mm AM/PM", "Oca 01 2022 4:32 \u00F6\u00F6"}, {"44593.189571759256", "[$-1F]mmm dd yyyy h:mm AM/PM", "Şub 01 2022 4:32 \u00F6\u00F6"}, {"44621.18957170139", "[$-1F]mmm dd yyyy h:mm AM/PM", "Mar 01 2022 4:32 \u00F6\u00F6"}, @@ -2085,9 +2835,9 @@ func TestNumFmt(t *testing.T) { {"44743.18957170139", "[$-1F]mmmmm dd yyyy h:mm AM/PM", "T 01 2022 4:32 \u00F6\u00F6"}, {"44774.18957170139", "[$-1F]mmmmm dd yyyy h:mm AM/PM", "A 01 2022 4:32 \u00F6\u00F6"}, {"44805.18957170139", "[$-1F]mmmmm dd yyyy h:mm AM/PM", "E 01 2022 4:32 \u00F6\u00F6"}, - {"44835.18957170139", "[$-1F]mmmmm dd yyyy h:mm AM/PM", "E 01 2022 4:32 \u00F6\u00F6"}, - {"44866.18957170139", "[$-1F]mmmmm dd yyyy h:mm AM/PM", "K 01 2022 4:32 \u00F6\u00F6"}, - {"44896.18957170139", "[$-1F]mmmmm dd yyyy h:mm AM/PM", "A 01 2022 4:32 \u00F6\u00F6"}, + {"44835.18957170139", "[$-1F]mmmmm dd yyyy h:mm AM/PM aaa", "E 01 2022 4:32 \u00F6\u00F6 Cmt"}, + {"44866.18957170139", "[$-1F]mmmmm dd yyyy h:mm AM/PM ddd", "K 01 2022 4:32 \u00F6\u00F6 Sal"}, + {"44896.18957170139", "[$-1F]mmmmm dd yyyy h:mm AM/PM dddd", "A 01 2022 4:32 \u00F6\u00F6 Perşembe"}, {"44562.189571759256", "[$-41F]mmm dd yyyy h:mm AM/PM", "Oca 01 2022 4:32 \u00F6\u00F6"}, {"44593.189571759256", "[$-41F]mmm dd yyyy h:mm AM/PM", "Şub 01 2022 4:32 \u00F6\u00F6"}, {"44621.18957170139", "[$-41F]mmm dd yyyy h:mm AM/PM", "Mar 01 2022 4:32 \u00F6\u00F6"}, @@ -2121,9 +2871,161 @@ func TestNumFmt(t *testing.T) { {"44743.18957170139", "[$-41F]mmmmm dd yyyy h:mm AM/PM", "T 01 2022 4:32 \u00F6\u00F6"}, {"44774.18957170139", "[$-41F]mmmmm dd yyyy h:mm AM/PM", "A 01 2022 4:32 \u00F6\u00F6"}, {"44805.18957170139", "[$-41F]mmmmm dd yyyy h:mm AM/PM", "E 01 2022 4:32 \u00F6\u00F6"}, - {"44835.18957170139", "[$-41F]mmmmm dd yyyy h:mm AM/PM", "E 01 2022 4:32 \u00F6\u00F6"}, - {"44866.18957170139", "[$-41F]mmmmm dd yyyy h:mm AM/PM", "K 01 2022 4:32 \u00F6\u00F6"}, - {"44896.18957170139", "[$-41F]mmmmm dd yyyy h:mm AM/PM", "A 01 2022 4:32 \u00F6\u00F6"}, + {"44835.18957170139", "[$-41F]mmmmm dd yyyy h:mm AM/PM aaa", "E 01 2022 4:32 \u00F6\u00F6 Cmt"}, + {"44866.18957170139", "[$-41F]mmmmm dd yyyy h:mm AM/PM ddd", "K 01 2022 4:32 \u00F6\u00F6 Sal"}, + {"44896.18957170139", "[$-41F]mmmmm dd yyyy h:mm AM/PM dddd", "A 01 2022 4:32 \u00F6\u00F6 Perşembe"}, + {"44562.189571759256", "[$-42]mmm dd yyyy h:mm AM/PM", "Ýan 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-42]mmmm dd yyyy h:mm AM/PM", "Ýanwar 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-42]mmmmm dd yyyy h:mm AM/PM", "Ý 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-42]mmmmmm dd yyyy h:mm AM/PM", "Ýanwar 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-42]mmm dd yyyy h:mm AM/PM", "Mart 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-42]mmmm dd yyyy h:mm AM/PM aaa", "Mart 19 2019 12:04 PM Sb"}, + {"43543.503206018519", "[$-42]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM Sb"}, + {"43543.503206018519", "[$-42]mmmmmm dd yyyy h:mm AM/PM dddd", "Mart 19 2019 12:04 PM Sişenbe"}, + {"44562.189571759256", "[$-442]mmm dd yyyy h:mm AM/PM", "Ýan 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-442]mmmm dd yyyy h:mm AM/PM", "Ýanwar 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-442]mmmmm dd yyyy h:mm AM/PM", "Ý 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-442]mmmmmm dd yyyy h:mm AM/PM", "Ýanwar 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-442]mmm dd yyyy h:mm AM/PM", "Mart 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-442]mmmm dd yyyy h:mm AM/PM aaa", "Mart 19 2019 12:04 PM Sb"}, + {"43543.503206018519", "[$-442]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 PM Sb"}, + {"43543.503206018519", "[$-442]mmmmmm dd yyyy h:mm AM/PM dddd", "Mart 19 2019 12:04 PM Sişenbe"}, + {"44562.189571759256", "[$-22]mmm dd yyyy h:mm AM/PM", "\u0421\u0456\u0447 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-22]mmmm dd yyyy h:mm AM/PM", "\u0441\u0456\u0447\u0435\u043D\u044C 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-22]mmmmm dd yyyy h:mm AM/PM", "\u0441 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-22]mmmmmm dd yyyy h:mm AM/PM", "\u0441\u0456\u0447\u0435\u043D\u044C 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-22]mmm dd yyyy h:mm AM/PM", "\u0411\u0435\u0440 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-22]mmmm dd yyyy h:mm AM/PM aaa", "\u0431\u0435\u0440\u0435\u0437\u0435\u043D\u044C 19 2019 12:04 PM \u0412\u0442"}, + {"43543.503206018519", "[$-22]mmmmm dd yyyy h:mm AM/PM ddd", "\u0431 19 2019 12:04 PM \u0412\u0442"}, + {"43543.503206018519", "[$-22]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0431\u0435\u0440\u0435\u0437\u0435\u043D\u044C 19 2019 12:04 PM \u0432\u0456\u0432\u0442\u043E\u0440\u043E\u043A"}, + {"44562.189571759256", "[$-422]mmm dd yyyy h:mm AM/PM", "\u0421\u0456\u0447 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-422]mmmm dd yyyy h:mm AM/PM", "\u0441\u0456\u0447\u0435\u043D\u044C 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-422]mmmmm dd yyyy h:mm AM/PM", "\u0441 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-422]mmmmmm dd yyyy h:mm AM/PM", "\u0441\u0456\u0447\u0435\u043D\u044C 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-422]mmm dd yyyy h:mm AM/PM", "\u0411\u0435\u0440 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-422]mmmm dd yyyy h:mm AM/PM aaa", "\u0431\u0435\u0440\u0435\u0437\u0435\u043D\u044C 19 2019 12:04 PM \u0412\u0442"}, + {"43543.503206018519", "[$-422]mmmmm dd yyyy h:mm AM/PM ddd", "\u0431 19 2019 12:04 PM \u0412\u0442"}, + {"43543.503206018519", "[$-422]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0431\u0435\u0440\u0435\u0437\u0435\u043D\u044C 19 2019 12:04 PM \u0432\u0456\u0432\u0442\u043E\u0440\u043E\u043A"}, + {"44562.189571759256", "[$-2E]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 dopołdnja"}, + {"44562.189571759256", "[$-2E]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 dopołdnja"}, + {"44562.189571759256", "[$-2E]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 dopołdnja"}, + {"44562.189571759256", "[$-2E]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 dopołdnja"}, + {"43543.503206018519", "[$-2E]mmm dd yyyy h:mm AM/PM", "měr 19 2019 12:04 popołdnju"}, + {"43543.503206018519", "[$-2E]mmmm dd yyyy h:mm AM/PM aaa", "měrc 19 2019 12:04 popołdnju wut"}, + {"43543.503206018519", "[$-2E]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 popołdnju wut"}, + {"43543.503206018519", "[$-2E]mmmmmm dd yyyy h:mm AM/PM dddd", "měrc 19 2019 12:04 popołdnju wutora"}, + {"44562.189571759256", "[$-42E]mmm dd yyyy h:mm AM/PM", "jan 01 2022 4:32 dopołdnja"}, + {"44562.189571759256", "[$-42E]mmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 dopołdnja"}, + {"44562.189571759256", "[$-42E]mmmmm dd yyyy h:mm AM/PM", "j 01 2022 4:32 dopołdnja"}, + {"44562.189571759256", "[$-42E]mmmmmm dd yyyy h:mm AM/PM", "januar 01 2022 4:32 dopołdnja"}, + {"43543.503206018519", "[$-42E]mmm dd yyyy h:mm AM/PM", "měr 19 2019 12:04 popołdnju"}, + {"43543.503206018519", "[$-42E]mmmm dd yyyy h:mm AM/PM aaa", "měrc 19 2019 12:04 popołdnju wut"}, + {"43543.503206018519", "[$-42E]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 popołdnju wut"}, + {"43543.503206018519", "[$-42E]mmmmmm dd yyyy h:mm AM/PM dddd", "měrc 19 2019 12:04 popołdnju wutora"}, + {"44562.189571759256", "[$-20]mmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u06CC 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-20]mmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u06CC 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-20]mmmmm dd yyyy h:mm AM/PM", "\u062C 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-20]mmmmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u06CC 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-20]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-20]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM \u0645\u0646\u06AF\u0644"}, + {"43543.503206018519", "[$-20]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 PM \u0645\u0646\u06AF\u0644"}, + {"43543.503206018519", "[$-20]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM \u0645\u0646\u06AF\u0644"}, + {"44562.189571759256", "[$-820]mmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u06CC 01 2022 4:32 \u062F\u0646"}, + {"44562.189571759256", "[$-820]mmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u06CC 01 2022 4:32 \u062F\u0646"}, + {"44562.189571759256", "[$-820]mmmmm dd yyyy h:mm AM/PM", "\u062C 01 2022 4:32 \u062F\u0646"}, + {"44562.189571759256", "[$-820]mmmmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u06CC 01 2022 4:32 \u062F\u0646"}, + {"43543.503206018519", "[$-820]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0686 19 2019 12:04 \u0631\u0627\u062A"}, + {"43543.503206018519", "[$-820]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u0686 19 2019 12:04 \u0631\u0627\u062A \u0645\u0646\u06AF\u0644"}, + {"43543.503206018519", "[$-820]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 \u0631\u0627\u062A \u0645\u0646\u06AF\u0644"}, + {"43543.503206018519", "[$-820]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u0686 19 2019 12:04 \u0631\u0627\u062A \u0645\u0646\u06AF\u0644"}, + {"44562.189571759256", "[$-420]mmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u06CC 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-420]mmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u06CC 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-420]mmmmm dd yyyy h:mm AM/PM", "\u062C 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-420]mmmmmm dd yyyy h:mm AM/PM", "\u062C\u0646\u0648\u0631\u06CC 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-420]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-420]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM \u0645\u0646\u06AF\u0644"}, + {"43543.503206018519", "[$-420]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 PM \u0645\u0646\u06AF\u0644"}, + {"43543.503206018519", "[$-420]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u0686 19 2019 12:04 PM \u0645\u0646\u06AF\u0644"}, + {"44562.189571759256", "[$-80]mmm dd yyyy h:mm AM/PM", "1-\u0626\u0627\u064A 01 2022 4:32 \u0686\u06C8\u0634\u062A\u0649\u0646%20\u0628\u06C7\u0631\u06C7\u0646"}, + {"44562.189571759256", "[$-80]mmmm dd yyyy h:mm AM/PM", "\u064A\u0627\u0646\u06CB\u0627\u0631 01 2022 4:32 \u0686\u06C8\u0634\u062A\u0649\u0646%20\u0628\u06C7\u0631\u06C7\u0646"}, + {"44562.189571759256", "[$-80]mmmmm dd yyyy h:mm AM/PM", "\u064A 01 2022 4:32 \u0686\u06C8\u0634\u062A\u0649\u0646%20\u0628\u06C7\u0631\u06C7\u0646"}, + {"44562.189571759256", "[$-80]mmmmmm dd yyyy h:mm AM/PM", "\u064A\u0627\u0646\u06CB\u0627\u0631 01 2022 4:32 \u0686\u06C8\u0634\u062A\u0649\u0646%20\u0628\u06C7\u0631\u06C7\u0646"}, + {"43543.503206018519", "[$-80]mmm dd yyyy h:mm AM/PM", "3-\u0626\u0627\u064A 19 2019 12:04 \u0686\u06C8\u0634\u062A\u0649\u0646%20\u0643\u06D0\u064A\u0649\u0646"}, + {"43543.503206018519", "[$-80]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u062A 19 2019 12:04 \u0686\u06C8\u0634\u062A\u0649\u0646%20\u0643\u06D0\u064A\u0649\u0646 \u0633\u06D5"}, + {"43543.503206018519", "[$-80]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 \u0686\u06C8\u0634\u062A\u0649\u0646%20\u0643\u06D0\u064A\u0649\u0646 \u0633\u06D5"}, + {"43543.503206018519", "[$-80]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u062A 19 2019 12:04 \u0686\u06C8\u0634\u062A\u0649\u0646%20\u0643\u06D0\u064A\u0649\u0646 \u0633\u06D5\u064A\u0634\u06D5\u0646\u0628\u06D5"}, + {"44562.189571759256", "[$-480]mmm dd yyyy h:mm AM/PM", "1-\u0626\u0627\u064A 01 2022 4:32 \u0686\u06C8\u0634\u062A\u0649\u0646%20\u0628\u06C7\u0631\u06C7\u0646"}, + {"44562.189571759256", "[$-480]mmmm dd yyyy h:mm AM/PM", "\u064A\u0627\u0646\u06CB\u0627\u0631 01 2022 4:32 \u0686\u06C8\u0634\u062A\u0649\u0646%20\u0628\u06C7\u0631\u06C7\u0646"}, + {"44562.189571759256", "[$-480]mmmmm dd yyyy h:mm AM/PM", "\u064A 01 2022 4:32 \u0686\u06C8\u0634\u062A\u0649\u0646%20\u0628\u06C7\u0631\u06C7\u0646"}, + {"44562.189571759256", "[$-480]mmmmmm dd yyyy h:mm AM/PM", "\u064A\u0627\u0646\u06CB\u0627\u0631 01 2022 4:32 \u0686\u06C8\u0634\u062A\u0649\u0646%20\u0628\u06C7\u0631\u06C7\u0646"}, + {"43543.503206018519", "[$-480]mmm dd yyyy h:mm AM/PM", "3-\u0626\u0627\u064A 19 2019 12:04 \u0686\u06C8\u0634\u062A\u0649\u0646%20\u0643\u06D0\u064A\u0649\u0646"}, + {"43543.503206018519", "[$-480]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u062A 19 2019 12:04 \u0686\u06C8\u0634\u062A\u0649\u0646%20\u0643\u06D0\u064A\u0649\u0646 \u0633\u06D5"}, + {"43543.503206018519", "[$-480]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 \u0686\u06C8\u0634\u062A\u0649\u0646%20\u0643\u06D0\u064A\u0649\u0646 \u0633\u06D5"}, + {"43543.503206018519", "[$-480]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u062A 19 2019 12:04 \u0686\u06C8\u0634\u062A\u0649\u0646%20\u0643\u06D0\u064A\u0649\u0646 \u0633\u06D5\u064A\u0634\u06D5\u0646\u0628\u06D5"}, + {"44562.189571759256", "[$-7843]mmm dd yyyy h:mm AM/PM", "\u044F\u043D\u0432 01 2022 4:32 \u0422\u041E"}, + {"44562.189571759256", "[$-7843]mmmm dd yyyy h:mm AM/PM", "\u044F\u043D\u0432\u0430\u0440 01 2022 4:32 \u0422\u041E"}, + {"44562.189571759256", "[$-7843]mmmmm dd yyyy h:mm AM/PM", "\u044F 01 2022 4:32 \u0422\u041E"}, + {"44562.189571759256", "[$-7843]mmmmmm dd yyyy h:mm AM/PM", "\u044F\u043D\u0432\u0430\u0440 01 2022 4:32 \u0422\u041E"}, + {"43543.503206018519", "[$-7843]mmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440 19 2019 12:04 \u0422\u041A"}, + {"43543.503206018519", "[$-7843]mmmm dd yyyy h:mm AM/PM aaa", "\u043C\u0430\u0440\u0442 19 2019 12:04 \u0422\u041A \u0441\u0435\u0448"}, + {"43543.503206018519", "[$-7843]mmmmm dd yyyy h:mm AM/PM ddd", "\u043C 19 2019 12:04 \u0422\u041A \u0441\u0435\u0448"}, + {"43543.503206018519", "[$-7843]mmmmmm dd yyyy h:mm AM/PM dddd", "\u043C\u0430\u0440\u0442 19 2019 12:04 \u0422\u041A \u0441\u0435\u0448\u0430\u043D\u0431\u0430"}, + {"44562.189571759256", "[$-843]mmm dd yyyy h:mm AM/PM", "\u044F\u043D\u0432 01 2022 4:32 \u0422\u041E"}, + {"44562.189571759256", "[$-843]mmmm dd yyyy h:mm AM/PM", "\u044F\u043D\u0432\u0430\u0440 01 2022 4:32 \u0422\u041E"}, + {"44562.189571759256", "[$-843]mmmmm dd yyyy h:mm AM/PM", "\u044F 01 2022 4:32 \u0422\u041E"}, + {"44562.189571759256", "[$-843]mmmmmm dd yyyy h:mm AM/PM", "\u044F\u043D\u0432\u0430\u0440 01 2022 4:32 \u0422\u041E"}, + {"43543.503206018519", "[$-843]mmm dd yyyy h:mm AM/PM", "\u043C\u0430\u0440 19 2019 12:04 \u0422\u041A"}, + {"43543.503206018519", "[$-843]mmmm dd yyyy h:mm AM/PM aaa", "\u043C\u0430\u0440\u0442 19 2019 12:04 \u0422\u041A \u0441\u0435\u0448"}, + {"43543.503206018519", "[$-843]mmmmm dd yyyy h:mm AM/PM ddd", "\u043C 19 2019 12:04 \u0422\u041A \u0441\u0435\u0448"}, + {"43543.503206018519", "[$-843]mmmmmm dd yyyy h:mm AM/PM dddd", "\u043C\u0430\u0440\u0442 19 2019 12:04 \u0422\u041A \u0441\u0435\u0448\u0430\u043D\u0431\u0430"}, + {"44562.189571759256", "[$-43]mmm dd yyyy h:mm AM/PM", "Yan 01 2022 4:32 TO"}, + {"44562.189571759256", "[$-43]mmmm dd yyyy h:mm AM/PM", "Yanvar 01 2022 4:32 TO"}, + {"44562.189571759256", "[$-43]mmmmm dd yyyy h:mm AM/PM", "Y 01 2022 4:32 TO"}, + {"44562.189571759256", "[$-43]mmmmmm dd yyyy h:mm AM/PM", "Yanvar 01 2022 4:32 TO"}, + {"43543.503206018519", "[$-43]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 TK"}, + {"43543.503206018519", "[$-43]mmmm dd yyyy h:mm AM/PM aaa", "Mart 19 2019 12:04 TK Sesh"}, + {"43543.503206018519", "[$-43]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 TK Sesh"}, + {"43543.503206018519", "[$-43]mmmmmm dd yyyy h:mm AM/PM dddd", "Mart 19 2019 12:04 TK seshanba"}, + {"44562.189571759256", "[$-7C43]mmm dd yyyy h:mm AM/PM", "Yan 01 2022 4:32 TO"}, + {"44562.189571759256", "[$-7C43]mmmm dd yyyy h:mm AM/PM", "Yanvar 01 2022 4:32 TO"}, + {"44562.189571759256", "[$-7C43]mmmmm dd yyyy h:mm AM/PM", "Y 01 2022 4:32 TO"}, + {"44562.189571759256", "[$-7C43]mmmmmm dd yyyy h:mm AM/PM", "Yanvar 01 2022 4:32 TO"}, + {"43543.503206018519", "[$-7C43]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 TK"}, + {"43543.503206018519", "[$-7C43]mmmm dd yyyy h:mm AM/PM aaa", "Mart 19 2019 12:04 TK Sesh"}, + {"43543.503206018519", "[$-7C43]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 TK Sesh"}, + {"43543.503206018519", "[$-7C43]mmmmmm dd yyyy h:mm AM/PM dddd", "Mart 19 2019 12:04 TK seshanba"}, + {"44562.189571759256", "[$-443]mmm dd yyyy h:mm AM/PM", "Yan 01 2022 4:32 TO"}, + {"44562.189571759256", "[$-443]mmmm dd yyyy h:mm AM/PM", "Yanvar 01 2022 4:32 TO"}, + {"44562.189571759256", "[$-443]mmmmm dd yyyy h:mm AM/PM", "Y 01 2022 4:32 TO"}, + {"44562.189571759256", "[$-443]mmmmmm dd yyyy h:mm AM/PM", "Yanvar 01 2022 4:32 TO"}, + {"43543.503206018519", "[$-443]mmm dd yyyy h:mm AM/PM", "Mar 19 2019 12:04 TK"}, + {"43543.503206018519", "[$-443]mmmm dd yyyy h:mm AM/PM aaa", "Mart 19 2019 12:04 TK Sesh"}, + {"43543.503206018519", "[$-443]mmmmm dd yyyy h:mm AM/PM ddd", "M 19 2019 12:04 TK Sesh"}, + {"43543.503206018519", "[$-443]mmmmmm dd yyyy h:mm AM/PM dddd", "Mart 19 2019 12:04 TK seshanba"}, + {"44562.189571759256", "[$-803]mmm dd yyyy h:mm AM/PM", "gen. 01 2022 4:32 a.%A0m."}, + {"44562.189571759256", "[$-803]mmmm dd yyyy h:mm AM/PM", "gener 01 2022 4:32 a.%A0m."}, + {"44562.189571759256", "[$-803]mmmmm dd yyyy h:mm AM/PM", "g 01 2022 4:32 a.%A0m."}, + {"44562.189571759256", "[$-803]mmmmmm dd yyyy h:mm AM/PM", "gener 01 2022 4:32 a.%A0m."}, + {"43543.503206018519", "[$-803]mmm dd yyyy h:mm AM/PM", "març 19 2019 12:04 p.%A0m."}, + {"43543.503206018519", "[$-803]mmmm dd yyyy h:mm AM/PM aaa", "març 19 2019 12:04 p.%A0m. dt."}, + {"43543.503206018519", "[$-803]mmmmm dd yyyy h:mm AM/PM ddd", "m 19 2019 12:04 p.%A0m. dt."}, + {"43543.503206018519", "[$-803]mmmmmm dd yyyy h:mm AM/PM dddd", "març 19 2019 12:04 p.%A0m. dimarts"}, + {"44562.189571759256", "[$-33]mmm dd yyyy h:mm AM/PM", "Pha 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-33]mmmm dd yyyy h:mm AM/PM", "Phando 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-33]mmmmm dd yyyy h:mm AM/PM", "P 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-33]mmmmmm dd yyyy h:mm AM/PM", "Phando 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-33]mmm dd yyyy h:mm AM/PM", "Ṱhf 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-33]mmmm dd yyyy h:mm AM/PM aaa", "Ṱhafamuhwe 19 2019 12:04 PM Vhi"}, + {"43543.503206018519", "[$-33]mmmmm dd yyyy h:mm AM/PM ddd", "Ṱ 19 2019 12:04 PM Vhi"}, + {"43543.503206018519", "[$-33]mmmmmm dd yyyy h:mm AM/PM dddd", "Ṱhafamuhwe 19 2019 12:04 PM Ḽavhuvhili"}, + {"44562.189571759256", "[$-433]mmm dd yyyy h:mm AM/PM", "Pha 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-433]mmmm dd yyyy h:mm AM/PM", "Phando 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-433]mmmmm dd yyyy h:mm AM/PM", "P 01 2022 4:32 AM"}, + {"44562.189571759256", "[$-433]mmmmmm dd yyyy h:mm AM/PM", "Phando 01 2022 4:32 AM"}, + {"43543.503206018519", "[$-433]mmm dd yyyy h:mm AM/PM", "Ṱhf 19 2019 12:04 PM"}, + {"43543.503206018519", "[$-433]mmmm dd yyyy h:mm AM/PM aaa", "Ṱhafamuhwe 19 2019 12:04 PM Vhi"}, + {"43543.503206018519", "[$-433]mmmmm dd yyyy h:mm AM/PM ddd", "Ṱ 19 2019 12:04 PM Vhi"}, + {"43543.503206018519", "[$-433]mmmmmm dd yyyy h:mm AM/PM dddd", "Ṱhafamuhwe 19 2019 12:04 PM Ḽavhuvhili"}, {"44562.189571759256", "[$-2A]mmm dd yyyy h:mm AM/PM", "Thg 1 01 2022 4:32 SA"}, {"44593.189571759256", "[$-2A]mmm dd yyyy h:mm AM/PM", "Thg 2 01 2022 4:32 SA"}, {"44621.18957170139", "[$-2A]mmm dd yyyy h:mm AM/PM", "Thg 3 01 2022 4:32 SA"}, @@ -2157,9 +3059,9 @@ func TestNumFmt(t *testing.T) { {"44743.18957170139", "[$-2A]mmmmm dd yyyy h:mm AM/PM", "T 7 01 2022 4:32 SA"}, {"44774.18957170139", "[$-2A]mmmmm dd yyyy h:mm AM/PM", "T 8 01 2022 4:32 SA"}, {"44805.18957170139", "[$-2A]mmmmm dd yyyy h:mm AM/PM", "T 9 01 2022 4:32 SA"}, - {"44835.18957170139", "[$-2A]mmmmm dd yyyy h:mm AM/PM", "T 10 01 2022 4:32 SA"}, - {"44866.18957170139", "[$-2A]mmmmm dd yyyy h:mm AM/PM", "T 11 01 2022 4:32 SA"}, - {"44896.18957170139", "[$-2A]mmmmm dd yyyy h:mm AM/PM", "T 12 01 2022 4:32 SA"}, + {"44835.18957170139", "[$-2A]mmmmm dd yyyy h:mm AM/PM aaa", "T 10 01 2022 4:32 SA T7"}, + {"44866.18957170139", "[$-2A]mmmmm dd yyyy h:mm AM/PM ddd", "T 11 01 2022 4:32 SA T3"}, + {"44896.18957170139", "[$-2A]mmmmm dd yyyy h:mm AM/PM dddd", "T 12 01 2022 4:32 SA Th\u1EE9%20N\u0103m"}, {"44562.189571759256", "[$-42A]mmm dd yyyy h:mm AM/PM", "Thg 1 01 2022 4:32 SA"}, {"44593.189571759256", "[$-42A]mmm dd yyyy h:mm AM/PM", "Thg 2 01 2022 4:32 SA"}, {"44621.18957170139", "[$-42A]mmm dd yyyy h:mm AM/PM", "Thg 3 01 2022 4:32 SA"}, @@ -2193,9 +3095,9 @@ func TestNumFmt(t *testing.T) { {"44743.18957170139", "[$-42A]mmmmm dd yyyy h:mm AM/PM", "T 7 01 2022 4:32 SA"}, {"44774.18957170139", "[$-42A]mmmmm dd yyyy h:mm AM/PM", "T 8 01 2022 4:32 SA"}, {"44805.18957170139", "[$-42A]mmmmm dd yyyy h:mm AM/PM", "T 9 01 2022 4:32 SA"}, - {"44835.18957170139", "[$-42A]mmmmm dd yyyy h:mm AM/PM", "T 10 01 2022 4:32 SA"}, - {"44866.18957170139", "[$-42A]mmmmm dd yyyy h:mm AM/PM", "T 11 01 2022 4:32 SA"}, - {"44896.18957170139", "[$-42A]mmmmm dd yyyy h:mm AM/PM", "T 12 01 2022 4:32 SA"}, + {"44835.18957170139", "[$-42A]mmmmm dd yyyy h:mm AM/PM aaa", "T 10 01 2022 4:32 SA T7"}, + {"44866.18957170139", "[$-42A]mmmmm dd yyyy h:mm AM/PM ddd", "T 11 01 2022 4:32 SA T3"}, + {"44896.18957170139", "[$-42A]mmmmm dd yyyy h:mm AM/PM dddd", "T 12 01 2022 4:32 SA Th\u1EE9%20N\u0103m"}, {"44562.189571759256", "[$-52]mmm dd yyyy h:mm AM/PM", "Ion 01 2022 4:32 yb"}, {"44593.189571759256", "[$-52]mmm dd yyyy h:mm AM/PM", "Chwef 01 2022 4:32 yb"}, {"44621.18957170139", "[$-52]mmm dd yyyy h:mm AM/PM", "Maw 01 2022 4:32 yb"}, @@ -2229,9 +3131,9 @@ func TestNumFmt(t *testing.T) { {"44743.18957170139", "[$-52]mmmmm dd yyyy h:mm AM/PM", "G 01 2022 4:32 yb"}, {"44774.18957170139", "[$-52]mmmmm dd yyyy h:mm AM/PM", "A 01 2022 4:32 yb"}, {"44805.18957170139", "[$-52]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 yb"}, - {"44835.18957170139", "[$-52]mmmmm dd yyyy h:mm AM/PM", "H 01 2022 4:32 yb"}, - {"44866.18957170139", "[$-52]mmmmm dd yyyy h:mm AM/PM", "T 01 2022 4:32 yb"}, - {"44896.18957170139", "[$-52]mmmmm dd yyyy h:mm AM/PM", "R 01 2022 4:32 yb"}, + {"44835.18957170139", "[$-52]mmmmm dd yyyy h:mm AM/PM aaa", "H 01 2022 4:32 yb Sad"}, + {"44866.18957170139", "[$-52]mmmmm dd yyyy h:mm AM/PM ddd", "T 01 2022 4:32 yb Maw"}, + {"44896.18957170139", "[$-52]mmmmm dd yyyy h:mm AM/PM dddd", "R 01 2022 4:32 yb Dydd Iau"}, {"44562.189571759256", "[$-452]mmm dd yyyy h:mm AM/PM", "Ion 01 2022 4:32 yb"}, {"44593.189571759256", "[$-452]mmm dd yyyy h:mm AM/PM", "Chwef 01 2022 4:32 yb"}, {"44621.18957170139", "[$-452]mmm dd yyyy h:mm AM/PM", "Maw 01 2022 4:32 yb"}, @@ -2265,9 +3167,9 @@ func TestNumFmt(t *testing.T) { {"44743.18957170139", "[$-452]mmmmm dd yyyy h:mm AM/PM", "G 01 2022 4:32 yb"}, {"44774.18957170139", "[$-452]mmmmm dd yyyy h:mm AM/PM", "A 01 2022 4:32 yb"}, {"44805.18957170139", "[$-452]mmmmm dd yyyy h:mm AM/PM", "M 01 2022 4:32 yb"}, - {"44835.18957170139", "[$-452]mmmmm dd yyyy h:mm AM/PM", "H 01 2022 4:32 yb"}, - {"44866.18957170139", "[$-452]mmmmm dd yyyy h:mm AM/PM", "T 01 2022 4:32 yb"}, - {"44896.18957170139", "[$-452]mmmmm dd yyyy h:mm AM/PM", "R 01 2022 4:32 yb"}, + {"44835.18957170139", "[$-452]mmmmm dd yyyy h:mm AM/PM aaa", "H 01 2022 4:32 yb Sad"}, + {"44866.18957170139", "[$-452]mmmmm dd yyyy h:mm AM/PM ddd", "T 01 2022 4:32 yb Maw"}, + {"44896.18957170139", "[$-452]mmmmm dd yyyy h:mm AM/PM dddd", "R 01 2022 4:32 yb Dydd Iau"}, {"44562.189571759256", "[$-88]mmm dd yyyy h:mm AM/PM", "Sam. 01 2022 4:32 Sub"}, {"44593.189571759256", "[$-88]mmm dd yyyy h:mm AM/PM", "Few. 01 2022 4:32 Sub"}, {"44621.18957170139", "[$-88]mmm dd yyyy h:mm AM/PM", "Maa 01 2022 4:32 Sub"}, @@ -2301,9 +3203,9 @@ func TestNumFmt(t *testing.T) { {"44743.18957170139", "[$-88]mmmmm dd yyyy h:mm AM/PM", "S 01 2022 4:32 Sub"}, {"44774.18957170139", "[$-88]mmmmm dd yyyy h:mm AM/PM", "U 01 2022 4:32 Sub"}, {"44805.18957170139", "[$-88]mmmmm dd yyyy h:mm AM/PM", "S 01 2022 4:32 Sub"}, - {"44835.18957170139", "[$-88]mmmmm dd yyyy h:mm AM/PM", "O 01 2022 4:32 Sub"}, - {"44866.18957170139", "[$-88]mmmmm dd yyyy h:mm AM/PM", "N 01 2022 4:32 Sub"}, - {"44896.18957170139", "[$-88]mmmmm dd yyyy h:mm AM/PM", "D 01 2022 4:32 Sub"}, + {"44835.18957170139", "[$-88]mmmmm dd yyyy h:mm AM/PM aaa", "O 01 2022 4:32 Sub Gaa."}, + {"44866.18957170139", "[$-88]mmmmm dd yyyy h:mm AM/PM ddd", "N 01 2022 4:32 Sub Tal."}, + {"44896.18957170139", "[$-88]mmmmm dd yyyy h:mm AM/PM dddd", "D 01 2022 4:32 Sub Alxames"}, {"44562.189571759256", "[$-488]mmm dd yyyy h:mm AM/PM", "Sam. 01 2022 4:32 Sub"}, {"44593.189571759256", "[$-488]mmm dd yyyy h:mm AM/PM", "Few. 01 2022 4:32 Sub"}, {"44621.18957170139", "[$-488]mmm dd yyyy h:mm AM/PM", "Maa 01 2022 4:32 Sub"}, @@ -2337,9 +3239,9 @@ func TestNumFmt(t *testing.T) { {"44743.18957170139", "[$-488]mmmmm dd yyyy h:mm AM/PM", "S 01 2022 4:32 Sub"}, {"44774.18957170139", "[$-488]mmmmm dd yyyy h:mm AM/PM", "U 01 2022 4:32 Sub"}, {"44805.18957170139", "[$-488]mmmmm dd yyyy h:mm AM/PM", "S 01 2022 4:32 Sub"}, - {"44835.18957170139", "[$-488]mmmmm dd yyyy h:mm AM/PM", "O 01 2022 4:32 Sub"}, - {"44866.18957170139", "[$-488]mmmmm dd yyyy h:mm AM/PM", "N 01 2022 4:32 Sub"}, - {"44896.18957170139", "[$-488]mmmmm dd yyyy h:mm AM/PM", "D 01 2022 4:32 Sub"}, + {"44835.18957170139", "[$-488]mmmmm dd yyyy h:mm AM/PM aaa", "O 01 2022 4:32 Sub Gaa."}, + {"44866.18957170139", "[$-488]mmmmm dd yyyy h:mm AM/PM ddd", "N 01 2022 4:32 Sub Tal."}, + {"44896.18957170139", "[$-488]mmmmm dd yyyy h:mm AM/PM dddd", "D 01 2022 4:32 Sub Alxames"}, {"44562.189571759256", "[$-34]mmm dd yyyy h:mm AM/PM", "uJan. 01 2022 4:32 AM"}, {"44593.189571759256", "[$-34]mmm dd yyyy h:mm AM/PM", "uFeb. 01 2022 4:32 AM"}, {"44621.18957170139", "[$-34]mmm dd yyyy h:mm AM/PM", "uMat. 01 2022 4:32 AM"}, @@ -2373,9 +3275,9 @@ func TestNumFmt(t *testing.T) { {"44743.18957170139", "[$-34]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"}, {"44774.18957170139", "[$-34]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"}, {"44805.18957170139", "[$-34]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"}, - {"44835.18957170139", "[$-34]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"}, - {"44866.18957170139", "[$-34]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"}, - {"44896.18957170139", "[$-34]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"}, + {"44835.18957170139", "[$-34]mmmmm dd yyyy h:mm AM/PM aaa", "u 01 2022 4:32 AM uMgq."}, + {"44866.18957170139", "[$-34]mmmmm dd yyyy h:mm AM/PM ddd", "u 01 2022 4:32 AM uLwesib."}, + {"44896.18957170139", "[$-34]mmmmm dd yyyy h:mm AM/PM dddd", "u 01 2022 4:32 AM Lwesine"}, {"44562.189571759256", "[$-434]mmm dd yyyy h:mm AM/PM", "uJan. 01 2022 4:32 AM"}, {"44593.189571759256", "[$-434]mmm dd yyyy h:mm AM/PM", "uFeb. 01 2022 4:32 AM"}, {"44621.18957170139", "[$-434]mmm dd yyyy h:mm AM/PM", "uMat. 01 2022 4:32 AM"}, @@ -2409,9 +3311,9 @@ func TestNumFmt(t *testing.T) { {"44743.18957170139", "[$-434]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"}, {"44774.18957170139", "[$-434]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"}, {"44805.18957170139", "[$-434]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"}, - {"44835.18957170139", "[$-434]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"}, - {"44866.18957170139", "[$-434]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"}, - {"44896.18957170139", "[$-434]mmmmm dd yyyy h:mm AM/PM", "u 01 2022 4:32 AM"}, + {"44835.18957170139", "[$-434]mmmmm dd yyyy h:mm AM/PM aaa", "u 01 2022 4:32 AM uMgq."}, + {"44866.18957170139", "[$-434]mmmmm dd yyyy h:mm AM/PM ddd", "u 01 2022 4:32 AM uLwesib."}, + {"44896.18957170139", "[$-434]mmmmm dd yyyy h:mm AM/PM dddd", "u 01 2022 4:32 AM Lwesine"}, {"44562.189571759256", "[$-78]mmm dd yyyy h:mm AM/PM", "\ua2cd\ua1aa 01 2022 4:32 \ua3b8\ua111"}, {"44593.189571759256", "[$-78]mmm dd yyyy h:mm AM/PM", "\ua44d\ua1aa 01 2022 4:32 \ua3b8\ua111"}, {"44621.18957170139", "[$-78]mmm dd yyyy h:mm AM/PM", "\ua315\ua1aa 01 2022 4:32 \ua3b8\ua111"}, @@ -2445,9 +3347,9 @@ func TestNumFmt(t *testing.T) { {"44743.18957170139", "[$-78]mmmmm dd yyyy h:mm AM/PM", "\ua3c3 01 2022 4:32 \ua3b8\ua111"}, {"44774.18957170139", "[$-78]mmmmm dd yyyy h:mm AM/PM", "\ua246 01 2022 4:32 \ua3b8\ua111"}, {"44805.18957170139", "[$-78]mmmmm dd yyyy h:mm AM/PM", "\ua22c 01 2022 4:32 \ua3b8\ua111"}, - {"44835.18957170139", "[$-78]mmmmm dd yyyy h:mm AM/PM", "\ua2b0 01 2022 4:32 \ua3b8\ua111"}, - {"44866.18957170139", "[$-78]mmmmm dd yyyy h:mm AM/PM", "\ua2b0 01 2022 4:32 \ua3b8\ua111"}, - {"44896.18957170139", "[$-78]mmmmm dd yyyy h:mm AM/PM", "\ua2b0 01 2022 4:32 \ua3b8\ua111"}, + {"44835.18957170139", "[$-78]mmmmm dd yyyy h:mm AM/PM aaa", "\ua2b0 01 2022 4:32 \ua3b8\ua111 \uA18F\uA0D8"}, + {"44866.18957170139", "[$-78]mmmmm dd yyyy h:mm AM/PM ddd", "\ua2b0 01 2022 4:32 \ua3b8\ua111 \uA18F\uA44D"}, + {"44896.18957170139", "[$-78]mmmmm dd yyyy h:mm AM/PM dddd", "\ua2b0 01 2022 4:32 \ua3b8\ua111 \uA18F\uA282\uA1D6"}, {"44562.189571759256", "[$-478]mmm dd yyyy h:mm AM/PM", "\ua2cd\ua1aa 01 2022 4:32 \ua3b8\ua111"}, {"44593.189571759256", "[$-478]mmm dd yyyy h:mm AM/PM", "\ua44d\ua1aa 01 2022 4:32 \ua3b8\ua111"}, {"44621.18957170139", "[$-478]mmm dd yyyy h:mm AM/PM", "\ua315\ua1aa 01 2022 4:32 \ua3b8\ua111"}, @@ -2481,9 +3383,33 @@ func TestNumFmt(t *testing.T) { {"44743.18957170139", "[$-478]mmmmm dd yyyy h:mm AM/PM", "\ua3c3 01 2022 4:32 \ua3b8\ua111"}, {"44774.18957170139", "[$-478]mmmmm dd yyyy h:mm AM/PM", "\ua246 01 2022 4:32 \ua3b8\ua111"}, {"44805.18957170139", "[$-478]mmmmm dd yyyy h:mm AM/PM", "\ua22c 01 2022 4:32 \ua3b8\ua111"}, - {"44835.18957170139", "[$-478]mmmmm dd yyyy h:mm AM/PM", "\ua2b0 01 2022 4:32 \ua3b8\ua111"}, - {"44866.18957170139", "[$-478]mmmmm dd yyyy h:mm AM/PM", "\ua2b0 01 2022 4:32 \ua3b8\ua111"}, - {"44896.18957170139", "[$-478]mmmmm dd yyyy h:mm AM/PM", "\ua2b0 01 2022 4:32 \ua3b8\ua111"}, + {"44835.18957170139", "[$-478]mmmmm dd yyyy h:mm AM/PM aaa", "\ua2b0 01 2022 4:32 \ua3b8\ua111 \uA18F\uA0D8"}, + {"44866.18957170139", "[$-478]mmmmm dd yyyy h:mm AM/PM ddd", "\ua2b0 01 2022 4:32 \ua3b8\ua111 \uA18F\uA44D"}, + {"44896.18957170139", "[$-478]mmmmm dd yyyy h:mm AM/PM dddd", "\ua2b0 01 2022 4:32 \ua3b8\ua111 \uA18F\uA282\uA1D6"}, + {"44562.189571759256", "[$-43D]mmm dd yyyy h:mm AM/PM", "\u05D9\u05D0\u05B7\u05E0 01 2022 4:32 \u05E4\u05BF\u05D0\u05B7\u05E8\u05DE\u05D9\u05D8\u05D0\u05B8\u05D2"}, + {"44562.189571759256", "[$-43D]mmmm dd yyyy h:mm AM/PM", "\u05D9\u05D0\u05B7\u05E0\u05D5\u05D0\u05B7\u05E8 01 2022 4:32 \u05E4\u05BF\u05D0\u05B7\u05E8\u05DE\u05D9\u05D8\u05D0\u05B8\u05D2"}, + {"44562.189571759256", "[$-43D]mmmmm dd yyyy h:mm AM/PM", "\u05D9 01 2022 4:32 \u05E4\u05BF\u05D0\u05B7\u05E8\u05DE\u05D9\u05D8\u05D0\u05B8\u05D2"}, + {"44562.189571759256", "[$-43D]mmmmmm dd yyyy h:mm AM/PM", "\u05D9\u05D0\u05B7\u05E0\u05D5\u05D0\u05B7\u05E8 01 2022 4:32 \u05E4\u05BF\u05D0\u05B7\u05E8\u05DE\u05D9\u05D8\u05D0\u05B8\u05D2"}, + {"43543.503206018519", "[$-43D]mmm dd yyyy h:mm AM/PM", "\u05DE\u05E2\u05E8\u05E5 19 2019 12:04 \u05E0\u05D0\u05B8\u05DB\u05DE\u05D9\u05D8\u05D0\u05B8\u05D2"}, + {"43543.503206018519", "[$-43D]mmmm dd yyyy h:mm AM/PM aaa", "\u05DE\u05E2\u05E8\u05E5 19 2019 12:04 \u05E0\u05D0\u05B8\u05DB\u05DE\u05D9\u05D8\u05D0\u05B8\u05D2 \u05D9\u05D5\u05DD%A0\u05D2"}, + {"43543.503206018519", "[$-43D]mmmmm dd yyyy h:mm AM/PM ddd", "\u05DE 19 2019 12:04 \u05E0\u05D0\u05B8\u05DB\u05DE\u05D9\u05D8\u05D0\u05B8\u05D2 \u05D9\u05D5\u05DD%A0\u05D2"}, + {"43543.503206018519", "[$-43D]mmmmmm dd yyyy h:mm AM/PM dddd", "\u05DE\u05E2\u05E8\u05E5 19 2019 12:04 \u05E0\u05D0\u05B8\u05DB\u05DE\u05D9\u05D8\u05D0\u05B8\u05D2 \u05D3\u05D9\u05E0\u05E1\u05D8\u05D9\u05E7"}, + {"44562.189571759256", "[$-6A]mmm dd yyyy h:mm AM/PM", "\u1E62\u1EB9\u0301 01 2022 4:32 %C0%E1r\u1ECD\u0300"}, + {"44562.189571759256", "[$-6A]mmmm dd yyyy h:mm AM/PM", "\u1E62\u1EB9\u0301r\u1EB9\u0301 01 2022 4:32 %C0%E1r\u1ECD\u0300"}, + {"44562.189571759256", "[$-6A]mmmmm dd yyyy h:mm AM/PM", "\u1E62 01 2022 4:32 %C0%E1r\u1ECD\u0300"}, + {"44562.189571759256", "[$-6A]mmmmmm dd yyyy h:mm AM/PM", "\u1E62\u1EB9\u0301r\u1EB9\u0301 01 2022 4:32 %C0%E1r\u1ECD\u0300"}, + {"43543.503206018519", "[$-6A]mmm dd yyyy h:mm AM/PM", "\u1EB8r 19 2019 12:04 \u1ECC\u0300s%E1n"}, + {"43543.503206018519", "[$-6A]mmmm dd yyyy h:mm AM/PM aaa", "\u1EB8r\u1EB9\u0300n%E0 19 2019 12:04 \u1ECC\u0300s%E1n %CC\u1E63g"}, + {"43543.503206018519", "[$-6A]mmmmm dd yyyy h:mm AM/PM ddd", "\u1EB8 19 2019 12:04 \u1ECC\u0300s%E1n %CC\u1E63g"}, + {"43543.503206018519", "[$-6A]mmmmmm dd yyyy h:mm AM/PM dddd", "\u1EB8r\u1EB9\u0300n%E0 19 2019 12:04 \u1ECC\u0300s%E1n \u1ECCj\u1ECD\u0301%20%CCs\u1EB9\u0301gun"}, + {"44562.189571759256", "[$-46A]mmm dd yyyy h:mm AM/PM", "\u1E62\u1EB9\u0301 01 2022 4:32 %C0%E1r\u1ECD\u0300"}, + {"44562.189571759256", "[$-46A]mmmm dd yyyy h:mm AM/PM", "\u1E62\u1EB9\u0301r\u1EB9\u0301 01 2022 4:32 %C0%E1r\u1ECD\u0300"}, + {"44562.189571759256", "[$-46A]mmmmm dd yyyy h:mm AM/PM", "\u1E62 01 2022 4:32 %C0%E1r\u1ECD\u0300"}, + {"44562.189571759256", "[$-46A]mmmmmm dd yyyy h:mm AM/PM", "\u1E62\u1EB9\u0301r\u1EB9\u0301 01 2022 4:32 %C0%E1r\u1ECD\u0300"}, + {"43543.503206018519", "[$-46A]mmm dd yyyy h:mm AM/PM", "\u1EB8r 19 2019 12:04 \u1ECC\u0300s%E1n"}, + {"43543.503206018519", "[$-46A]mmmm dd yyyy h:mm AM/PM aaa", "\u1EB8r\u1EB9\u0300n%E0 19 2019 12:04 \u1ECC\u0300s%E1n %CC\u1E63g"}, + {"43543.503206018519", "[$-46A]mmmmm dd yyyy h:mm AM/PM ddd", "\u1EB8 19 2019 12:04 \u1ECC\u0300s%E1n %CC\u1E63g"}, + {"43543.503206018519", "[$-46A]mmmmmm dd yyyy h:mm AM/PM dddd", "\u1EB8r\u1EB9\u0300n%E0 19 2019 12:04 \u1ECC\u0300s%E1n \u1ECCj\u1ECD\u0301%20%CCs\u1EB9\u0301gun"}, {"44562.189571759256", "[$-35]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"}, {"44593.189571759256", "[$-35]mmm dd yyyy h:mm AM/PM", "Feb 01 2022 4:32 AM"}, {"44621.18957170139", "[$-35]mmm dd yyyy h:mm AM/PM", "Mas 01 2022 4:32 AM"}, @@ -2517,9 +3443,9 @@ func TestNumFmt(t *testing.T) { {"44743.18957170139", "[$-35]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"}, {"44774.18957170139", "[$-35]mmmmm dd yyyy h:mm AM/PM", "A 01 2022 4:32 AM"}, {"44805.18957170139", "[$-35]mmmmm dd yyyy h:mm AM/PM", "S 01 2022 4:32 AM"}, - {"44835.18957170139", "[$-35]mmmmm dd yyyy h:mm AM/PM", "O 01 2022 4:32 AM"}, - {"44866.18957170139", "[$-35]mmmmm dd yyyy h:mm AM/PM", "N 01 2022 4:32 AM"}, - {"44896.18957170139", "[$-35]mmmmm dd yyyy h:mm AM/PM", "D 01 2022 4:32 AM"}, + {"44835.18957170139", "[$-35]mmmmm dd yyyy h:mm AM/PM aaa", "O 01 2022 4:32 AM Mgq."}, + {"44866.18957170139", "[$-35]mmmmm dd yyyy h:mm AM/PM ddd", "N 01 2022 4:32 AM Bi."}, + {"44896.18957170139", "[$-35]mmmmm dd yyyy h:mm AM/PM dddd", "D 01 2022 4:32 AM ULwesine"}, {"44562.189571759256", "[$-435]mmm dd yyyy h:mm AM/PM", "Jan 01 2022 4:32 AM"}, {"44593.189571759256", "[$-435]mmm dd yyyy h:mm AM/PM", "Feb 01 2022 4:32 AM"}, {"44621.18957170139", "[$-435]mmm dd yyyy h:mm AM/PM", "Mas 01 2022 4:32 AM"}, @@ -2553,9 +3479,9 @@ func TestNumFmt(t *testing.T) { {"44743.18957170139", "[$-435]mmmmm dd yyyy h:mm AM/PM", "J 01 2022 4:32 AM"}, {"44774.18957170139", "[$-435]mmmmm dd yyyy h:mm AM/PM", "A 01 2022 4:32 AM"}, {"44805.18957170139", "[$-435]mmmmm dd yyyy h:mm AM/PM", "S 01 2022 4:32 AM"}, - {"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"}, + {"44835.18957170139", "[$-435]mmmmm dd yyyy h:mm AM/PM aaa", "O 01 2022 4:32 AM Mgq."}, + {"44866.18957170139", "[$-435]mmmmm dd yyyy h:mm AM/PM ddd", "N 01 2022 4:32 AM Bi."}, + {"44896.18957170139", "[$-435]mmmmm dd yyyy h:mm AM/PM dddd", "D 01 2022 4:32 AM ULwesine"}, {"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_"}, From c63ae6d2627e3295b5f48fe96c1491bc576ee317 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 17 Aug 2023 11:34:28 +0800 Subject: [PATCH 779/957] This fixed #1610, support to create a conditional format with number format and protection --- styles.go | 52 +++++++++++++++++++++++++++++++++++++++++++++----- styles_test.go | 18 ++++++++++++++++- xmlStyles.go | 5 ----- 3 files changed, 64 insertions(+), 11 deletions(-) diff --git a/styles.go b/styles.go index a2d021f267..c1587aea0e 100644 --- a/styles.go +++ b/styles.go @@ -1032,6 +1032,7 @@ func (f *File) NewStyle(style *Style) (int, error) { return setCellXfs(s, fontID, numFmtID, fillID, borderID, applyAlignment, applyProtection, alignment, protection) } +// getXfIDFuncs provides a function to get xfID by given style. var getXfIDFuncs = map[string]func(int, xlsxXf, *Style) bool{ "numFmt": func(numFmtID int, xf xlsxXf, style *Style) bool { if style.CustomNumFmt == nil && numFmtID == -1 { @@ -1121,7 +1122,10 @@ func (f *File) NewConditionalStyle(style *Style) (int, error) { if err != nil { return 0, err } - dxf := dxf{ + if fs.DecimalPlaces != nil && (*fs.DecimalPlaces < 0 || *fs.DecimalPlaces > 30) { + fs.DecimalPlaces = intPtr(2) + } + dxf := xlsxDxf{ Fill: newFills(fs, false), } if fs.Alignment != nil { @@ -1133,17 +1137,55 @@ func (f *File) NewConditionalStyle(style *Style) (int, error) { if fs.Font != nil { dxf.Font, _ = f.newFont(fs) } - dxfStr, _ := xml.Marshal(dxf) + if fs.Protection != nil { + dxf.Protection = newProtection(fs) + } + dxf.NumFmt = newDxfNumFmt(s, style, &dxf) if s.Dxfs == nil { s.Dxfs = &xlsxDxfs{} } s.Dxfs.Count++ - s.Dxfs.Dxfs = append(s.Dxfs.Dxfs, &xlsxDxf{ - Dxf: string(dxfStr[5 : len(dxfStr)-6]), - }) + s.Dxfs.Dxfs = append(s.Dxfs.Dxfs, &dxf) return s.Dxfs.Count - 1, nil } +// newDxfNumFmt provides a function to create number format for conditional +// format styles. +func newDxfNumFmt(styleSheet *xlsxStyleSheet, style *Style, dxf *xlsxDxf) *xlsxNumFmt { + 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 styleSheet.Dxfs != nil { + for _, d := range styleSheet.Dxfs.Dxfs { + if d != nil && d.NumFmt != nil && d.NumFmt.NumFmtID > numFmtID { + numFmtID = d.NumFmt.NumFmtID + } + } + } + return &xlsxNumFmt{NumFmtID: numFmtID + 1, FormatCode: *style.CustomNumFmt} + } + numFmtCode, ok := builtInNumFmt[style.NumFmt] + if style.NumFmt > 0 && ok { + return &xlsxNumFmt{NumFmtID: style.NumFmt, FormatCode: numFmtCode} + } + fc, currency := currencyNumFmt[style.NumFmt] + if !currency { + return nil + } + if style.DecimalPlaces != nil { + fc = strings.ReplaceAll(fc, "0.00", dp) + } + if style.NegRed { + fc = fc + ";[Red]" + fc + } + return &xlsxNumFmt{NumFmtID: numFmtID, FormatCode: fc} +} + // 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, error) { diff --git a/styles_test.go b/styles_test.go index af9654fba6..856e90f03c 100644 --- a/styles_test.go +++ b/styles_test.go @@ -355,10 +355,26 @@ func TestNewStyle(t *testing.T) { func TestNewConditionalStyle(t *testing.T) { f := NewFile() + _, err := f.NewConditionalStyle(&Style{Protection: &Protection{Hidden: true, Locked: true}}) + assert.NoError(t, err) + _, err = f.NewConditionalStyle(&Style{DecimalPlaces: intPtr(4), NumFmt: 165, NegRed: true}) + assert.NoError(t, err) + _, err = f.NewConditionalStyle(&Style{DecimalPlaces: intPtr(-1)}) + assert.NoError(t, err) + _, err = f.NewConditionalStyle(&Style{NumFmt: 1}) + assert.NoError(t, err) + _, err = f.NewConditionalStyle(&Style{NumFmt: 27}) + assert.NoError(t, err) + numFmt := "general" + _, err = f.NewConditionalStyle(&Style{CustomNumFmt: &numFmt}) + assert.NoError(t, err) + numFmt1 := "0.00" + _, err = f.NewConditionalStyle(&Style{CustomNumFmt: &numFmt1}) + assert.NoError(t, err) // 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/xmlStyles.go b/xmlStyles.go index 80d9959404..067cff92d4 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -251,11 +251,6 @@ type xlsxDxfs struct { // xlsxDxf directly maps the dxf element. A single dxf record, expressing // incremental formatting to be applied. type xlsxDxf struct { - Dxf string `xml:",innerxml"` -} - -// dxf directly maps the dxf element. -type dxf struct { Font *xlsxFont `xml:"font"` NumFmt *xlsxNumFmt `xml:"numFmt"` Fill *xlsxFill `xml:"fill"` From 1b63d098a71d6f88966b52c06d8c027ed5af0a48 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 21 Aug 2023 00:04:28 +0800 Subject: [PATCH 780/957] This improves applying cell value with currency and accounting number format - Update the unit test and dependencies modules --- cell.go | 22 +- cell_test.go | 14 +- excelize_test.go | 8 +- go.mod | 7 +- go.sum | 8 +- numfmt.go | 659 +++++++++++++++++++++++------------------------ numfmt_test.go | 1 + 7 files changed, 361 insertions(+), 358 deletions(-) diff --git a/cell.go b/cell.go index 5842aeec17..c2ac31f49d 100644 --- a/cell.go +++ b/cell.go @@ -1365,27 +1365,29 @@ 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 := styleSheet.getCustomNumFmtCode(numFmtID); ok { + return format(c.V, fmtCode, date1904, cellType, f.options), err + } if fmtCode, ok := f.getBuiltInNumFmtCode(numFmtID); ok { return f.applyBuiltInNumFmt(c, fmtCode, numFmtID, date1904, cellType), err } - return f.applyNumFmt(c, styleSheet, numFmtID, date1904, cellType), err + return c.V, err } -// applyNumFmt provides a function to returns formatted cell value with custom -// number format code. -func (f *File) applyNumFmt(c *xlsxC, styleSheet *xlsxStyleSheet, numFmtID int, date1904 bool, cellType CellType) string { - if styleSheet.NumFmts == nil { - return c.V +// getCustomNumFmtCode provides a function to returns custom number format code. +func (ss *xlsxStyleSheet) getCustomNumFmtCode(numFmtID int) (string, bool) { + if ss.NumFmts == nil { + return "", false } - for _, xlsxFmt := range styleSheet.NumFmts.NumFmt { + for _, xlsxFmt := range ss.NumFmts.NumFmt { if xlsxFmt.NumFmtID == numFmtID { if xlsxFmt.FormatCode16 != "" { - return format(c.V, xlsxFmt.FormatCode16, date1904, cellType, f.options) + return xlsxFmt.FormatCode16, true } - return format(c.V, xlsxFmt.FormatCode, date1904, cellType, f.options) + return xlsxFmt.FormatCode, true } } - return c.V + return "", false } // prepareCellStyle provides a function to prepare style index of cell in diff --git a/cell_test.go b/cell_test.go index 1770e91a67..87399e1f0f 100644 --- a/cell_test.go +++ b/cell_test.go @@ -927,12 +927,14 @@ func TestFormattedValueNilWorkbookPr(t *testing.T) { assert.Equal(t, "43528", result) } -func TestApplyNumFmt(t *testing.T) { - f := NewFile() - assert.Equal(t, "\u4EE4\u548C\u5143年9月1日", f.applyNumFmt(&xlsxC{V: "43709"}, - &xlsxStyleSheet{NumFmts: &xlsxNumFmts{NumFmt: []*xlsxNumFmt{ - {NumFmtID: 164, FormatCode16: "[$-ja-JP-x-gannen,80]ggge\"年\"m\"月\"d\"日\";@"}, - }}}, 164, false, CellTypeNumber)) +func TestGetCustomNumFmtCode(t *testing.T) { + expected := "[$-ja-JP-x-gannen,80]ggge\"年\"m\"月\"d\"日\";@" + styleSheet := &xlsxStyleSheet{NumFmts: &xlsxNumFmts{NumFmt: []*xlsxNumFmt{ + {NumFmtID: 164, FormatCode16: expected}, + }}} + numFmtCode, ok := styleSheet.getCustomNumFmtCode(164) + assert.Equal(t, expected, numFmtCode) + assert.True(t, ok) } func TestSharedStringsError(t *testing.T) { diff --git a/excelize_test.go b/excelize_test.go index 9bc0107db1..6739a7eac3 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -741,10 +741,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", "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", "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"}, + {"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 ", "37,948", "$37,948", "37,947.75", "$37,947.75", "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)", "(37,948)", "$(37,948)", "(37,947.75)", "$(37,947.75)", "-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", "12:10 AM", "12:10:05 AM", "00:10", "00:10:05", "12/30/99 00:10", "0 ", "0 ", "0.01 ", "0.01 ", "0", "$0", "0.01", "$0.01", "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", "$2", "2.10", "$2.10", "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 562ffdc3ed..733011fe2c 100644 --- a/go.mod +++ b/go.mod @@ -5,13 +5,12 @@ go 1.16 require ( 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.8.0 github.com/xuri/efp v0.0.0-20230802181842-ad255f2331ca - github.com/xuri/nfp v0.0.0-20230802015359-2d5eeba905e9 + github.com/xuri/nfp v0.0.0-20230819163627-dc951e3ffe1a golang.org/x/crypto v0.12.0 - golang.org/x/image v0.5.0 + golang.org/x/image v0.11.0 golang.org/x/net v0.14.0 golang.org/x/text v0.12.0 ) - -require github.com/richardlehane/msoleps v1.0.3 // indirect diff --git a/go.sum b/go.sum index f11f4dfe7b..d82097a3b1 100644 --- a/go.sum +++ b/go.sum @@ -17,15 +17,15 @@ 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-20230802181842-ad255f2331ca h1:uvPMDVyP7PXMMioYdyPH+0O+Ta/UO1WFfNYMO3Wz0eg= github.com/xuri/efp v0.0.0-20230802181842-ad255f2331ca/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= -github.com/xuri/nfp v0.0.0-20230802015359-2d5eeba905e9 h1:jmhvNv5by7bXDzzjzBXaIWmEI4lMYfv5iJtI5Pw5/aM= -github.com/xuri/nfp v0.0.0-20230802015359-2d5eeba905e9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= +github.com/xuri/nfp v0.0.0-20230819163627-dc951e3ffe1a h1:Mw2VNrNNNjDtw68VsEj2+st+oCSn4Uz7vZw6TbhcV1o= +github.com/xuri/nfp v0.0.0-20230819163627-dc951e3ffe1a/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= golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= -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/image v0.11.0 h1:ds2RoQvBvYTiJkwpSFDwCcDFNX7DqjL2WsUgTNk0Ooo= +golang.org/x/image v0.11.0/go.mod h1:bglhjqbqVuEb9e9+eNR45Jfu7D+T4Qan+NhQk8Ck2P8= 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= diff --git a/numfmt.go b/numfmt.go index 6a1a0ab4dc..4037f827c2 100644 --- a/numfmt.go +++ b/numfmt.go @@ -694,6 +694,7 @@ var ( nfp.TokenTypeHashPlaceHolder, nfp.TokenTypeLiteral, nfp.TokenTypePercent, + nfp.TokenTypeRepeatsChar, nfp.TokenTypeSwitchArgument, nfp.TokenTypeTextPlaceHolder, nfp.TokenTypeThousandsSeparator, @@ -829,333 +830,333 @@ var ( "vai-Vaii-LR", "vai-Latn-LR", "vai-Latn", "vo", "vo-001", "vun", "vun-TZ", "wae", "wae-CH", "wal", "wae-ET", "yav", "yav-CM", "yo-BJ", "dje", "dje-NE", }, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, - "C09": {tags: []string{"en-AU"}, localMonth: localMonthsNameEnglish, apFmt: strings.ToLower(nfp.AmPm[0]), weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, - "2809": {tags: []string{"en-BZ"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, - "1009": {tags: []string{"en-CA"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, - "2409": {tags: []string{"en-029"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, - "3C09": {tags: []string{"en-HK"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, - "4009": {tags: []string{"en-IN"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, - "1809": {tags: []string{"en-IE"}, localMonth: localMonthsNameEnglish, apFmt: strings.ToLower(nfp.AmPm[0]), weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, - "2009": {tags: []string{"en-JM"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, - "4409": {tags: []string{"en-MY"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, - "1409": {tags: []string{"en-NZ"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, - "3409": {tags: []string{"en-PH"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, - "4809": {tags: []string{"en-SG"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, - "1C09": {tags: []string{"en-ZA"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, - "2C09": {tags: []string{"en-TT"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, - "4C09": {tags: []string{"en-AE"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, - "809": {tags: []string{"en-GB"}, localMonth: localMonthsNameEnglish, apFmt: strings.ToLower(nfp.AmPm[0]), weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, - "409": {tags: []string{"en-US"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, - "3009": {tags: []string{"en-ZW"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, - "25": {tags: []string{"et"}, localMonth: localMonthsNameEstonian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEstonian, weekdayNamesAbbr: weekdayNamesEstonianAbbr}, - "425": {tags: []string{"et-EE"}, localMonth: localMonthsNameEstonian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEstonian, weekdayNamesAbbr: weekdayNamesEstonianAbbr}, - "38": {tags: []string{"fo"}, localMonth: localMonthsNameFaroese, apFmt: apFmtFaroese, weekdayNames: weekdayNamesFaroese, weekdayNamesAbbr: weekdayNamesFaroeseAbbr}, - "438": {tags: []string{"fo-FO"}, localMonth: localMonthsNameFaroese, apFmt: apFmtFaroese, weekdayNames: weekdayNamesFaroese, weekdayNamesAbbr: weekdayNamesFaroeseAbbr}, - "64": {tags: []string{"fil"}, localMonth: localMonthsNameFilipino, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFilipino, weekdayNamesAbbr: weekdayNamesFilipinoAbbr}, - "464": {tags: []string{"fil-PH"}, localMonth: localMonthsNameFilipino, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFilipino, weekdayNamesAbbr: weekdayNamesFilipinoAbbr}, - "B": {tags: []string{"fi"}, localMonth: localMonthsNameFinnish, apFmt: apFmtFinnish, weekdayNames: weekdayNamesFinnish, weekdayNamesAbbr: weekdayNamesFinnishAbbr}, - "40B": {tags: []string{"fi-FI"}, localMonth: localMonthsNameFinnish, apFmt: apFmtFinnish, weekdayNames: weekdayNamesFinnish, weekdayNamesAbbr: weekdayNamesFinnishAbbr}, - "C": {tags: []string{"fr"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, - "80C": {tags: []string{"fr-BE"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, - "2C0C": {tags: []string{"fr-CM"}, localMonth: localMonthsNameFrench, apFmt: apFmtCameroon, weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, - "C0C": {tags: []string{"fr-CA"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, - "1C0C": {tags: []string{"fr-029"}, localMonth: localMonthsNameCaribbean, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, - "240C": {tags: []string{"fr-CD"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, - "300C": {tags: []string{"fr-CI"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, - "40C": {tags: []string{"fr-FR"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, - "3C0C": {tags: []string{"fr-HT"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, - "140C": {tags: []string{"fr-LU"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, - "340C": {tags: []string{"fr-ML"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, - "380C": {tags: []string{"fr-MA"}, localMonth: localMonthsNameMorocco, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, - "180C": {tags: []string{"fr-MC"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, - "200C": {tags: []string{"fr-RE"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, - "280C": {tags: []string{"fr-SN"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, - "62": {tags: []string{"fy"}, localMonth: localMonthsNameFrisian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrisian, weekdayNamesAbbr: weekdayNamesFrisianAbbr}, - "462": {tags: []string{"fy-NL"}, localMonth: localMonthsNameFrisian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrisian, weekdayNamesAbbr: weekdayNamesFrisianAbbr}, - "67": {tags: []string{"ff"}, localMonth: localMonthsNameFulah, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFulah, weekdayNamesAbbr: weekdayNamesFulahAbbr}, - "7C67": {tags: []string{"ff-Latn"}, localMonth: localMonthsNameFulah, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFulah, weekdayNamesAbbr: weekdayNamesFulahAbbr}, - "467": {tags: []string{"ff-NG", "ff-Latn-NG"}, localMonth: localMonthsNameNigeria, apFmt: apFmtNigeria, weekdayNames: weekdayNamesNigeria, weekdayNamesAbbr: weekdayNamesNigeriaAbbr}, - "867": {tags: []string{"ff-SN"}, localMonth: localMonthsNameNigeria, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesNigeria, weekdayNamesAbbr: weekdayNamesNigeriaAbbr}, - "56": {tags: []string{"gl"}, localMonth: localMonthsNameGalician, apFmt: apFmtCuba, weekdayNames: weekdayNamesGalician, weekdayNamesAbbr: weekdayNamesGalicianAbbr}, - "456": {tags: []string{"gl-ES"}, localMonth: localMonthsNameGalician, apFmt: apFmtCuba, weekdayNames: weekdayNamesGalician, weekdayNamesAbbr: weekdayNamesGalicianAbbr}, - "37": {tags: []string{"ka"}, localMonth: localMonthsNameGeorgian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGeorgian, weekdayNamesAbbr: weekdayNamesGeorgianAbbr}, - "437": {tags: []string{"ka-GE"}, localMonth: localMonthsNameGeorgian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGeorgian, weekdayNamesAbbr: weekdayNamesGeorgianAbbr}, - "7": {tags: []string{"de"}, localMonth: localMonthsNameGerman, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGerman, weekdayNamesAbbr: weekdayNamesGermanAbbr}, - "C07": {tags: []string{"de-AT"}, localMonth: localMonthsNameAustria, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGerman, weekdayNamesAbbr: weekdayNamesGermanAbbr}, - "407": {tags: []string{"de-DE"}, localMonth: localMonthsNameGerman, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGerman, weekdayNamesAbbr: weekdayNamesGermanAbbr}, - "1407": {tags: []string{"de-LI"}, localMonth: localMonthsNameGerman, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGerman, weekdayNamesAbbr: weekdayNamesGermanAbbr}, - "807": {tags: []string{"de-CH"}, localMonth: localMonthsNameGerman, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGerman, weekdayNamesAbbr: weekdayNamesGermanAbbr}, - "8": {tags: []string{"el"}, localMonth: localMonthsNameGreek, apFmt: apFmtGreek, weekdayNames: weekdayNamesGreek, weekdayNamesAbbr: weekdayNamesGreekAbbr}, - "408": {tags: []string{"el-GR"}, localMonth: localMonthsNameGreek, apFmt: apFmtGreek, weekdayNames: weekdayNamesGreek, weekdayNamesAbbr: weekdayNamesGreekAbbr}, - "6F": {tags: []string{"kl"}, localMonth: localMonthsNameGreenlandic, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGreenlandic, weekdayNamesAbbr: weekdayNamesGreenlandicAbbr}, - "46F": {tags: []string{"kl-GL"}, localMonth: localMonthsNameGreenlandic, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGreenlandic, weekdayNamesAbbr: weekdayNamesGreenlandicAbbr}, - "74": {tags: []string{"gn"}, localMonth: localMonthsNameGuarani, apFmt: apFmtCuba, weekdayNames: weekdayNamesGuarani, weekdayNamesAbbr: weekdayNamesGuaraniAbbr}, - "474": {tags: []string{"gn-PY"}, localMonth: localMonthsNameGuarani, apFmt: apFmtCuba, weekdayNames: weekdayNamesGuarani, weekdayNamesAbbr: weekdayNamesGuaraniAbbr}, - "47": {tags: []string{"gu"}, localMonth: localMonthsNameGujarati, apFmt: apFmtGujarati, weekdayNames: weekdayNamesGujarati, weekdayNamesAbbr: weekdayNamesGujaratiAbbr}, - "447": {tags: []string{"gu-IN"}, localMonth: localMonthsNameGujarati, apFmt: apFmtGujarati, weekdayNames: weekdayNamesGujarati, weekdayNamesAbbr: weekdayNamesGujaratiAbbr}, - "68": {tags: []string{"ha"}, localMonth: localMonthsNameHausa, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesHausa, weekdayNamesAbbr: weekdayNamesHausaAbbr}, - "7C68": {tags: []string{"ha-Latn"}, localMonth: localMonthsNameHausa, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesHausa, weekdayNamesAbbr: weekdayNamesHausaAbbr}, - "468": {tags: []string{"ha-Latn-NG"}, localMonth: localMonthsNameHausa, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesHausa, weekdayNamesAbbr: weekdayNamesHausaAbbr}, - "75": {tags: []string{"haw"}, localMonth: localMonthsNameHawaiian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesHawaiian, weekdayNamesAbbr: weekdayNamesHawaiianAbbr}, - "475": {tags: []string{"haw-US"}, localMonth: localMonthsNameHawaiian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesHawaiian, weekdayNamesAbbr: weekdayNamesHawaiianAbbr}, - "D": {tags: []string{"he"}, localMonth: localMonthsNameHebrew, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesHebrew, weekdayNamesAbbr: weekdayNamesHebrewAbbr}, - "40D": {tags: []string{"he-IL"}, localMonth: localMonthsNameHebrew, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesHebrew, weekdayNamesAbbr: weekdayNamesHebrewAbbr}, - "39": {tags: []string{"hi"}, localMonth: localMonthsNameHindi, apFmt: apFmtHindi, weekdayNames: weekdayNamesHindi, weekdayNamesAbbr: weekdayNamesHindiAbbr}, - "439": {tags: []string{"hi-IN"}, localMonth: localMonthsNameHindi, apFmt: apFmtHindi, weekdayNames: weekdayNamesHindi, weekdayNamesAbbr: weekdayNamesHindiAbbr}, - "E": {tags: []string{"hu"}, localMonth: localMonthsNameHungarian, apFmt: apFmtHungarian, weekdayNames: weekdayNamesHungarian, weekdayNamesAbbr: weekdayNamesHungarianAbbr}, - "40E": {tags: []string{"hu-HU"}, localMonth: localMonthsNameHungarian, apFmt: apFmtHungarian, weekdayNames: weekdayNamesHungarian, weekdayNamesAbbr: weekdayNamesHungarianAbbr}, - "F": {tags: []string{"is"}, localMonth: localMonthsNameIcelandic, apFmt: apFmtIcelandic, weekdayNames: weekdayNamesIcelandic, weekdayNamesAbbr: weekdayNamesIcelandicAbbr}, - "40F": {tags: []string{"is-IS"}, localMonth: localMonthsNameIcelandic, apFmt: apFmtIcelandic, weekdayNames: weekdayNamesIcelandic, weekdayNamesAbbr: weekdayNamesIcelandicAbbr}, - "70": {tags: []string{"ig"}, localMonth: localMonthsNameIgbo, apFmt: apFmtIgbo, weekdayNames: weekdayNamesIgbo, weekdayNamesAbbr: weekdayNamesIgboAbbr}, - "470": {tags: []string{"ig-NG"}, localMonth: localMonthsNameIgbo, apFmt: apFmtIgbo, weekdayNames: weekdayNamesIgbo, weekdayNamesAbbr: weekdayNamesIgboAbbr}, - "21": {tags: []string{"id"}, localMonth: localMonthsNameIndonesian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesIndonesian, weekdayNamesAbbr: weekdayNamesIndonesianAbbr}, - "421": {tags: []string{"id-ID"}, localMonth: localMonthsNameIndonesian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesIndonesian, weekdayNamesAbbr: weekdayNamesIndonesianAbbr}, - "5D": {tags: []string{"iu"}, localMonth: localMonthsNameInuktitut, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesInuktitut, weekdayNamesAbbr: weekdayNamesInuktitutAbbr}, - "7C5D": {tags: []string{"iu-Latn"}, localMonth: localMonthsNameInuktitut, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesInuktitut, weekdayNamesAbbr: weekdayNamesInuktitutAbbr}, - "85D": {tags: []string{"iu-Latn-CA"}, localMonth: localMonthsNameInuktitut, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesInuktitut, weekdayNamesAbbr: weekdayNamesInuktitutAbbr}, - "785D": {tags: []string{"iu-Cans"}, localMonth: localMonthsNameSyllabics, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSyllabics, weekdayNamesAbbr: weekdayNamesSyllabicsAbbr}, - "45D": {tags: []string{"iu-Cans-CA"}, localMonth: localMonthsNameSyllabics, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSyllabics, weekdayNamesAbbr: weekdayNamesSyllabicsAbbr}, - "3C": {tags: []string{"ga"}, localMonth: localMonthsNameIrish, apFmt: apFmtIrish, weekdayNames: weekdayNamesIrish, weekdayNamesAbbr: weekdayNamesIrishAbbr}, - "83C": {tags: []string{"ga-IE"}, localMonth: localMonthsNameIrish, apFmt: apFmtIrish, weekdayNames: weekdayNamesIrish, weekdayNamesAbbr: weekdayNamesIrishAbbr}, - "10": {tags: []string{"it"}, localMonth: localMonthsNameItalian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesItalian, weekdayNamesAbbr: weekdayNamesItalianAbbr}, - "410": {tags: []string{"it-IT"}, localMonth: localMonthsNameItalian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesItalian, weekdayNamesAbbr: weekdayNamesItalianAbbr}, - "810": {tags: []string{"it-CH"}, localMonth: localMonthsNameItalian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesItalian, weekdayNamesAbbr: weekdayNamesItalianAbbr}, - "11": {tags: []string{"ja"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese, weekdayNames: weekdayNamesJapanese, weekdayNamesAbbr: weekdayNamesJapaneseAbbr}, - "411": {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese, weekdayNames: weekdayNamesJapanese, weekdayNamesAbbr: weekdayNamesJapaneseAbbr}, - "800411": {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese, weekdayNames: weekdayNamesJapanese, weekdayNamesAbbr: weekdayNamesJapaneseAbbr}, - "JP-X-GANNEN": {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese, weekdayNames: weekdayNamesJapanese, weekdayNamesAbbr: weekdayNamesJapaneseAbbr}, - "JP-X-GANNEN,80": {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese, weekdayNames: weekdayNamesJapanese, weekdayNamesAbbr: weekdayNamesJapaneseAbbr, useGannen: true}, - "4B": {tags: []string{"kn"}, localMonth: localMonthsNameKannada, apFmt: apFmtKannada, weekdayNames: weekdayNamesKannada, weekdayNamesAbbr: weekdayNamesKannadaAbbr}, - "44B": {tags: []string{"kn-IN"}, localMonth: localMonthsNameKannada, apFmt: apFmtKannada, weekdayNames: weekdayNamesKannada, weekdayNamesAbbr: weekdayNamesKannadaAbbr}, - "471": {tags: []string{"kr-Latn-NG"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, - "60": {tags: []string{"ks"}, localMonth: localMonthsNameKashmiri, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesKashmiri, weekdayNamesAbbr: weekdayNamesKashmiriAbbr}, - "460": {tags: []string{"ks-Arab"}, localMonth: localMonthsNameKashmiri, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesKashmiri, weekdayNamesAbbr: weekdayNamesKashmiriAbbr}, - "860": {tags: []string{"ks-Deva-IN"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, - "3F": {tags: []string{"kk"}, localMonth: localMonthsNameKazakh, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesKazakh, weekdayNamesAbbr: weekdayNamesKazakhAbbr}, - "43F": {tags: []string{"kk-KZ"}, localMonth: localMonthsNameKazakh, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesKazakh, weekdayNamesAbbr: weekdayNamesKazakhAbbr}, - "53": {tags: []string{"km"}, localMonth: localMonthsNameKhmer, apFmt: apFmtKhmer, weekdayNames: weekdayNamesKhmer, weekdayNamesAbbr: weekdayNamesKhmerAbbr}, - "453": {tags: []string{"km-KH"}, localMonth: localMonthsNameKhmer, apFmt: apFmtKhmer, weekdayNames: weekdayNamesKhmer, weekdayNamesAbbr: weekdayNamesKhmerAbbr}, - "86": {tags: []string{"quc"}, localMonth: localMonthsNameKiche, apFmt: apFmtCuba, weekdayNames: weekdayNamesKiche, weekdayNamesAbbr: weekdayNamesKicheAbbr}, - "486": {tags: []string{"quc-Latn-GT"}, localMonth: localMonthsNameKiche, apFmt: apFmtCuba, weekdayNames: weekdayNamesKiche, weekdayNamesAbbr: weekdayNamesKicheAbbr}, - "87": {tags: []string{"rw"}, localMonth: localMonthsNameKinyarwanda, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesKinyarwanda, weekdayNamesAbbr: weekdayNamesKinyarwandaAbbr}, - "487": {tags: []string{"rw-RW"}, localMonth: localMonthsNameKinyarwanda, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesKinyarwanda, weekdayNamesAbbr: weekdayNamesKinyarwandaAbbr}, - "41": {tags: []string{"sw"}, localMonth: localMonthsNameKiswahili, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesKiswahili, weekdayNamesAbbr: weekdayNamesKiswahiliAbbr}, - "441": {tags: []string{"sw-KE"}, localMonth: localMonthsNameKiswahili, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesKiswahili, weekdayNamesAbbr: weekdayNamesKiswahiliAbbr}, - "57": {tags: []string{"kok"}, localMonth: localMonthsNameKonkani, apFmt: apFmtKonkani, weekdayNames: weekdayNamesKonkani, weekdayNamesAbbr: weekdayNamesKonkaniAbbr}, - "457": {tags: []string{"kok-IN"}, localMonth: localMonthsNameKonkani, apFmt: apFmtKonkani, weekdayNames: weekdayNamesKonkani, weekdayNamesAbbr: weekdayNamesKonkaniAbbr}, - "12": {tags: []string{"ko"}, localMonth: localMonthsNameKorean, apFmt: apFmtKorean, weekdayNames: weekdayNamesKorean, weekdayNamesAbbr: weekdayNamesKoreanAbbr}, - "412": {tags: []string{"ko-KR"}, localMonth: localMonthsNameKorean, apFmt: apFmtKorean, weekdayNames: weekdayNamesKorean, weekdayNamesAbbr: weekdayNamesKoreanAbbr}, - "40": {tags: []string{"ky"}, localMonth: localMonthsNameKyrgyz, apFmt: apFmtKyrgyz, weekdayNames: weekdayNamesKyrgyz, weekdayNamesAbbr: weekdayNamesKyrgyzAbbr}, - "440": {tags: []string{"ky-KG"}, localMonth: localMonthsNameKyrgyz, apFmt: apFmtKyrgyz, weekdayNames: weekdayNamesKyrgyz, weekdayNamesAbbr: weekdayNamesKyrgyzAbbr}, - "54": {tags: []string{"lo"}, localMonth: localMonthsNameLao, apFmt: apFmtLao, weekdayNames: weekdayNamesLao, weekdayNamesAbbr: weekdayNamesLaoAbbr}, - "454": {tags: []string{"lo-LA"}, localMonth: localMonthsNameLao, apFmt: apFmtLao, weekdayNames: weekdayNamesLao, weekdayNamesAbbr: weekdayNamesLaoAbbr}, - "476": {tags: []string{"la-VA"}, localMonth: localMonthsNameLatin, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesLatin, weekdayNamesAbbr: weekdayNamesLatinAbbr}, - "26": {tags: []string{"lv"}, localMonth: localMonthsNameLatvian, apFmt: apFmtLatvian, weekdayNames: weekdayNamesLatvian, weekdayNamesAbbr: weekdayNamesLatvianAbbr}, - "426": {tags: []string{"lv-LV"}, localMonth: localMonthsNameLatvian, apFmt: apFmtLatvian, weekdayNames: weekdayNamesLatvian, weekdayNamesAbbr: weekdayNamesLatvianAbbr}, - "27": {tags: []string{"lt"}, localMonth: localMonthsNameLithuanian, apFmt: apFmtLithuanian, weekdayNames: weekdayNamesLithuanian, weekdayNamesAbbr: weekdayNamesLithuanianAbbr}, - "427": {tags: []string{"lt-LT"}, localMonth: localMonthsNameLithuanian, apFmt: apFmtLithuanian, weekdayNames: weekdayNamesLithuanian, weekdayNamesAbbr: weekdayNamesLithuanianAbbr}, - "7C2E": {tags: []string{"dsb"}, localMonth: localMonthsNameLowerSorbian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesLowerSorbian, weekdayNamesAbbr: weekdayNamesLowerSorbianAbbr}, - "82E": {tags: []string{"dsb-DE"}, localMonth: localMonthsNameLowerSorbian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesLowerSorbian, weekdayNamesAbbr: weekdayNamesLowerSorbianAbbr}, - "6E": {tags: []string{"lb"}, localMonth: localMonthsNameLuxembourgish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesLuxembourgish, weekdayNamesAbbr: weekdayNamesLuxembourgishAbbr}, - "46E": {tags: []string{"lb-LU"}, localMonth: localMonthsNameLuxembourgish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesLuxembourgish, weekdayNamesAbbr: weekdayNamesLuxembourgishAbbr}, - "2F": {tags: []string{"mk"}, localMonth: localMonthsNameMacedonian, apFmt: apFmtMacedonian, weekdayNames: weekdayNamesMacedonian, weekdayNamesAbbr: weekdayNamesMacedonianAbbr}, - "42F": {tags: []string{"mk-MK"}, localMonth: localMonthsNameMacedonian, apFmt: apFmtMacedonian, weekdayNames: weekdayNamesMacedonian, weekdayNamesAbbr: weekdayNamesMacedonianAbbr}, - "3E": {tags: []string{"ms"}, localMonth: localMonthsNameMalay, apFmt: apFmtMalay, weekdayNames: weekdayNamesMalay, weekdayNamesAbbr: weekdayNamesMalayAbbr}, - "83E": {tags: []string{"ms-BN"}, localMonth: localMonthsNameMalay, apFmt: apFmtMalay, weekdayNames: weekdayNamesMalay, weekdayNamesAbbr: weekdayNamesMalayAbbr}, - "43E": {tags: []string{"ms-MY"}, localMonth: localMonthsNameMalay, apFmt: apFmtMalay, weekdayNames: weekdayNamesMalay, weekdayNamesAbbr: weekdayNamesMalayAbbr}, - "4C": {tags: []string{"ml"}, localMonth: localMonthsNameMalayalam, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesMalayalam, weekdayNamesAbbr: weekdayNamesMalayalamAbbr}, - "44C": {tags: []string{"ml-IN"}, localMonth: localMonthsNameMalayalam, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesMalayalam, weekdayNamesAbbr: weekdayNamesMalayalamAbbr}, - "3A": {tags: []string{"mt"}, localMonth: localMonthsNameMaltese, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesMaltese, weekdayNamesAbbr: weekdayNamesMalteseAbbr}, - "43A": {tags: []string{"mt-MT"}, localMonth: localMonthsNameMaltese, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesMaltese, weekdayNamesAbbr: weekdayNamesMalteseAbbr}, - "81": {tags: []string{"mi"}, localMonth: localMonthsNameMaori, apFmt: apFmtCuba, weekdayNames: weekdayNamesMaori, weekdayNamesAbbr: weekdayNamesMaoriAbbr}, - "481": {tags: []string{"mi-NZ"}, localMonth: localMonthsNameMaori, apFmt: apFmtCuba, weekdayNames: weekdayNamesMaori, weekdayNamesAbbr: weekdayNamesMaoriAbbr}, - "7A": {tags: []string{"arn"}, localMonth: localMonthsNameMapudungun, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesMapudungun, weekdayNamesAbbr: weekdayNamesMapudungunAbbr}, - "47A": {tags: []string{"arn-CL"}, localMonth: localMonthsNameMapudungun, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesMapudungun, weekdayNamesAbbr: weekdayNamesMapudungunAbbr}, - "4E": {tags: []string{"mr"}, localMonth: localMonthsNameMarathi, apFmt: apFmtKonkani, weekdayNames: weekdayNamesMarathi, weekdayNamesAbbr: weekdayNamesMarathiAbbr}, - "44E": {tags: []string{"mr-IN"}, localMonth: localMonthsNameMarathi, apFmt: apFmtKonkani, weekdayNames: weekdayNamesMarathi, weekdayNamesAbbr: weekdayNamesMarathiAbbr}, - "7C": {tags: []string{"moh"}, localMonth: localMonthsNameMohawk, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesMohawk, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, - "47C": {tags: []string{"moh-CA"}, localMonth: localMonthsNameMohawk, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesMohawk, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, - "50": {tags: []string{"mn"}, localMonth: localMonthsNameMongolian, apFmt: apFmtMongolian, weekdayNames: weekdayNamesMongolian, weekdayNamesAbbr: weekdayNamesMongolianAbbr}, - "7850": {tags: []string{"mn-Cyrl"}, localMonth: localMonthsNameMongolian, apFmt: apFmtMongolian, weekdayNames: weekdayNamesMongolian, weekdayNamesAbbr: weekdayNamesMongolianCyrlAbbr}, - "450": {tags: []string{"mn-MN"}, localMonth: localMonthsNameMongolian, apFmt: apFmtMongolian, weekdayNames: weekdayNamesMongolian, weekdayNamesAbbr: weekdayNamesMongolianCyrlAbbr}, - "7C50": {tags: []string{"mn-Mong"}, localMonth: localMonthsNameTraditionalMongolian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTraditionalMongolian, weekdayNamesAbbr: weekdayNamesTraditionalMongolian}, - "850": {tags: []string{"mn-Mong-CN"}, localMonth: localMonthsNameTraditionalMongolian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTraditionalMongolian, weekdayNamesAbbr: weekdayNamesTraditionalMongolian}, - "C50": {tags: []string{"mn-Mong-MN"}, localMonth: localMonthsNameTraditionalMongolian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTraditionalMongolianMN, weekdayNamesAbbr: weekdayNamesTraditionalMongolianMN}, - "61": {tags: []string{"ne"}, localMonth: localMonthsNameNepali, apFmt: apFmtHindi, weekdayNames: weekdayNamesNepali, weekdayNamesAbbr: weekdayNamesNepaliAbbr}, - "861": {tags: []string{"ne-IN"}, localMonth: localMonthsNameNepaliIN, apFmt: apFmtHindi, weekdayNames: weekdayNamesNepaliIN, weekdayNamesAbbr: weekdayNamesNepaliINAbbr}, - "461": {tags: []string{"ne-NP"}, localMonth: localMonthsNameNepali, apFmt: apFmtHindi, weekdayNames: weekdayNamesNepali, weekdayNamesAbbr: weekdayNamesNepaliAbbr}, - "14": {tags: []string{"no"}, localMonth: localMonthsNameNorwegian, apFmt: apFmtCuba, weekdayNames: weekdayNamesNorwegian, weekdayNamesAbbr: weekdayNamesNorwegianAbbr}, - "7C14": {tags: []string{"nb"}, localMonth: localMonthsNameNorwegian, apFmt: apFmtCuba, weekdayNames: weekdayNamesNorwegian, weekdayNamesAbbr: weekdayNamesNorwegianNOAbbr}, - "414": {tags: []string{"nb-NO"}, localMonth: localMonthsNameNorwegian, apFmt: apFmtCuba, weekdayNames: weekdayNamesNorwegian, weekdayNamesAbbr: weekdayNamesNorwegianNOAbbr}, - "7814": {tags: []string{"nn"}, localMonth: localMonthsNameNorwegian, apFmt: apFmtNorwegian, weekdayNames: weekdayNamesNorwegianNynorsk, weekdayNamesAbbr: weekdayNamesNorwegianNynorskAbbr}, - "814": {tags: []string{"nn-NO"}, localMonth: localMonthsNameNorwegian, apFmt: apFmtNorwegian, weekdayNames: weekdayNamesNorwegianNynorsk, weekdayNamesAbbr: weekdayNamesNorwegianNynorskAbbr}, - "82": {tags: []string{"oc"}, localMonth: localMonthsNameOccitan, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesOccitan, weekdayNamesAbbr: weekdayNamesOccitanAbbr}, - "482": {tags: []string{"oc-FR"}, localMonth: localMonthsNameOccitan, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesOccitan, weekdayNamesAbbr: weekdayNamesOccitanAbbr}, - "48": {tags: []string{"or"}, localMonth: localMonthsNameOdia, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesOdia, weekdayNamesAbbr: weekdayNamesOdiaAbbr}, - "448": {tags: []string{"or-IN"}, localMonth: localMonthsNameOdia, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesOdia, weekdayNamesAbbr: weekdayNamesOdiaAbbr}, - "72": {tags: []string{"om"}, localMonth: localMonthsNameOromo, apFmt: apFmtOromo, weekdayNames: weekdayNamesOromo, weekdayNamesAbbr: weekdayNamesOromoAbbr}, - "472": {tags: []string{"om-ET"}, localMonth: localMonthsNameOromo, apFmt: apFmtOromo, weekdayNames: weekdayNamesOromo, weekdayNamesAbbr: weekdayNamesOromoAbbr}, - "63": {tags: []string{"ps"}, localMonth: localMonthsNamePashto, apFmt: apFmtPashto, weekdayNames: weekdayNamesPashto, weekdayNamesAbbr: weekdayNamesPashto}, - "463": {tags: []string{"ps-AF"}, localMonth: localMonthsNamePashto, apFmt: apFmtPashto, weekdayNames: weekdayNamesPashto, weekdayNamesAbbr: weekdayNamesPashto}, - "29": {tags: []string{"fa"}, localMonth: localMonthsNamePersian, apFmt: apFmtPersian, weekdayNames: weekdayNamesPersian, weekdayNamesAbbr: weekdayNamesPersian}, - "429": {tags: []string{"fa-IR"}, localMonth: localMonthsNamePersian, apFmt: apFmtPersian, weekdayNames: weekdayNamesPersian, weekdayNamesAbbr: weekdayNamesPersian}, - "15": {tags: []string{"pl"}, localMonth: localMonthsNamePolish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesPolish, weekdayNamesAbbr: weekdayNamesPolishAbbr}, - "415": {tags: []string{"pl-PL"}, localMonth: localMonthsNamePolish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesPolish, weekdayNamesAbbr: weekdayNamesPolishAbbr}, - "16": {tags: []string{"pt"}, localMonth: localMonthsNamePortuguese, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesPortuguese, weekdayNamesAbbr: weekdayNamesPortugueseAbbr}, - "416": {tags: []string{"pt-BR"}, localMonth: localMonthsNamePortuguese, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesPortuguese, weekdayNamesAbbr: weekdayNamesPortugueseAbbr}, - "816": {tags: []string{"pt-PT"}, localMonth: localMonthsNamePortuguese, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesPortuguese, weekdayNamesAbbr: weekdayNamesPortugueseAbbr}, - "46": {tags: []string{"pa"}, localMonth: localMonthsNamePunjabi, apFmt: apFmtPunjabi, weekdayNames: weekdayNamesPunjabi, weekdayNamesAbbr: weekdayNamesPunjabiAbbr}, - "7C46": {tags: []string{"pa-Arab"}, localMonth: localMonthsNamePunjabiArab, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesPunjabiArab, weekdayNamesAbbr: weekdayNamesPunjabiArab}, - "446": {tags: []string{"pa-IN"}, localMonth: localMonthsNamePunjabi, apFmt: apFmtPunjabi, weekdayNames: weekdayNamesPunjabi, weekdayNamesAbbr: weekdayNamesPunjabiAbbr}, - "846": {tags: []string{"pa-Arab-PK"}, localMonth: localMonthsNamePunjabiArab, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesPunjabiArab, weekdayNamesAbbr: weekdayNamesPunjabiArab}, - "6B": {tags: []string{"quz"}, localMonth: localMonthsNameQuechua, apFmt: apFmtCuba, weekdayNames: weekdayNamesQuechua, weekdayNamesAbbr: weekdayNamesQuechuaAbbr}, - "46B": {tags: []string{"quz-BO"}, localMonth: localMonthsNameQuechua, apFmt: apFmtCuba, weekdayNames: weekdayNamesQuechua, weekdayNamesAbbr: weekdayNamesQuechuaAbbr}, - "86B": {tags: []string{"quz-EC"}, localMonth: localMonthsNameQuechuaEcuador, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesQuechuaEcuador, weekdayNamesAbbr: weekdayNamesQuechuaEcuadorAbbr}, - "C6B": {tags: []string{"quz-PE"}, localMonth: localMonthsNameQuechua, apFmt: apFmtCuba, weekdayNames: weekdayNamesQuechuaPeru, weekdayNamesAbbr: weekdayNamesQuechuaPeruAbbr}, - "18": {tags: []string{"ro"}, localMonth: localMonthsNameRomanian, apFmt: apFmtCuba, weekdayNames: weekdayNamesRomanian, weekdayNamesAbbr: weekdayNamesRomanianAbbr}, - "818": {tags: []string{"ro-MD"}, localMonth: localMonthsNameRomanian, apFmt: apFmtCuba, weekdayNames: weekdayNamesRomanian, weekdayNamesAbbr: weekdayNamesRomanianMoldovaAbbr}, - "418": {tags: []string{"ro-RO"}, localMonth: localMonthsNameRomanian, apFmt: apFmtCuba, weekdayNames: weekdayNamesRomanian, weekdayNamesAbbr: weekdayNamesRomanianAbbr}, - "17": {tags: []string{"rm"}, localMonth: localMonthsNameRomansh, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesRomansh, weekdayNamesAbbr: weekdayNamesRomanshAbbr}, - "417": {tags: []string{"rm-CH"}, localMonth: localMonthsNameRomansh, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesRomansh, weekdayNamesAbbr: weekdayNamesRomanshAbbr}, - "19": {tags: []string{"ru"}, localMonth: localMonthsNameRussian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesRussian, weekdayNamesAbbr: weekdayNamesRussianAbbr}, - "819": {tags: []string{"ru-MD"}, localMonth: localMonthsNameRussian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesRussian, weekdayNamesAbbr: weekdayNamesRussianAbbr}, - "419": {tags: []string{"ru-RU"}, localMonth: localMonthsNameRussian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesRussian, weekdayNamesAbbr: weekdayNamesRussianAbbr}, - "85": {tags: []string{"sah"}, localMonth: localMonthsNameSakha, apFmt: apFmtSakha, weekdayNames: weekdayNamesSakha, weekdayNamesAbbr: weekdayNamesSakhaAbbr}, - "485": {tags: []string{"sah-RU"}, localMonth: localMonthsNameSakha, apFmt: apFmtSakha, weekdayNames: weekdayNamesSakha, weekdayNamesAbbr: weekdayNamesSakhaAbbr}, - "703B": {tags: []string{"smn"}, localMonth: localMonthsNameSami, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSami, weekdayNamesAbbr: weekdayNamesSamiAbbr}, - "243B": {tags: []string{"smn-FI"}, localMonth: localMonthsNameSami, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSami, weekdayNamesAbbr: weekdayNamesSamiAbbr}, - "7C3B": {tags: []string{"smj"}, localMonth: localMonthsNameSamiLule, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiSamiLule, weekdayNamesAbbr: weekdayNamesSamiSwedenAbbr}, - "103B": {tags: []string{"smj-NO"}, localMonth: localMonthsNameSamiLule, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiSamiLule, weekdayNamesAbbr: weekdayNamesSamiSamiLuleAbbr}, - "143B": {tags: []string{"smj-SE"}, localMonth: localMonthsNameSamiLule, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiSweden, weekdayNamesAbbr: weekdayNamesSamiSwedenAbbr}, - "3B": {tags: []string{"se"}, localMonth: localMonthsNameSamiNorthern, apFmt: apFmtSamiNorthern, weekdayNames: weekdayNamesSamiNorthern, weekdayNamesAbbr: weekdayNamesSamiNorthernAbbr}, - "C3B": {tags: []string{"se-FI"}, localMonth: localMonthsNameSamiNorthernFI, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiNorthernFI, weekdayNamesAbbr: weekdayNamesSamiNorthernFIAbbr}, - "43B": {tags: []string{"se-NO"}, localMonth: localMonthsNameSamiNorthern, apFmt: apFmtSamiNorthern, weekdayNames: weekdayNamesSamiNorthern, weekdayNamesAbbr: weekdayNamesSamiNorthernAbbr}, - "83B": {tags: []string{"se-SE"}, localMonth: localMonthsNameSamiNorthern, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiNorthernSE, weekdayNamesAbbr: weekdayNamesSamiNorthernSEAbbr}, - "743B": {tags: []string{"sms"}, localMonth: localMonthsNameSamiSkolt, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiSkolt, weekdayNamesAbbr: weekdayNamesSamiSkoltAbbr}, - "203B": {tags: []string{"sms-FI"}, localMonth: localMonthsNameSamiSkolt, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiSkolt, weekdayNamesAbbr: weekdayNamesSamiSkoltAbbr}, - "783B": {tags: []string{"sma"}, localMonth: localMonthsNameSamiSouthern, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiSouthern, weekdayNamesAbbr: weekdayNamesSamiSouthernAbbr}, - "183B": {tags: []string{"sma-NO"}, localMonth: localMonthsNameSamiSouthern, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiSouthern, weekdayNamesAbbr: weekdayNamesSamiSouthernAbbr}, - "1C3B": {tags: []string{"sma-SE"}, localMonth: localMonthsNameSamiSouthern, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiSouthern, weekdayNamesAbbr: weekdayNamesSamiSouthernAbbr}, - "4F": {tags: []string{"sa"}, localMonth: localMonthsNameSanskrit, apFmt: apFmtSanskrit, weekdayNames: weekdayNamesSanskrit, weekdayNamesAbbr: weekdayNamesSanskritAbbr}, - "44F": {tags: []string{"sa-IN"}, localMonth: localMonthsNameSanskrit, apFmt: apFmtSanskrit, weekdayNames: weekdayNamesSanskrit, weekdayNamesAbbr: weekdayNamesSanskritAbbr}, - "91": {tags: []string{"gd"}, localMonth: localMonthsNameScottishGaelic, apFmt: apFmtScottishGaelic, weekdayNames: weekdayNamesGaelic, weekdayNamesAbbr: weekdayNamesGaelicAbbr}, - "491": {tags: []string{"gd-GB"}, localMonth: localMonthsNameScottishGaelic, apFmt: apFmtScottishGaelic, weekdayNames: weekdayNamesGaelic, weekdayNamesAbbr: weekdayNamesGaelicAbbr}, - "6C1A": {tags: []string{"sr-Cyrl"}, localMonth: localMonthsNameSerbian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSerbian, weekdayNamesAbbr: weekdayNamesSerbianAbbr}, - "1C1A": {tags: []string{"sr-Cyrl-BA"}, localMonth: localMonthsNameSerbianBA, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSerbianBA, weekdayNamesAbbr: weekdayNamesSerbianBAAbbr}, - "301A": {tags: []string{"sr-Cyrl-ME"}, localMonth: localMonthsNameSerbian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSerbianME, weekdayNamesAbbr: weekdayNamesSerbianBAAbbr}, - "281A": {tags: []string{"sr-Cyrl-RS"}, localMonth: localMonthsNameSerbian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSerbian, weekdayNamesAbbr: weekdayNamesSerbianAbbr}, - "C1A": {tags: []string{"sr-Cyrl-CS"}, localMonth: localMonthsNameSerbian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSerbian, weekdayNamesAbbr: weekdayNamesSerbianAbbr}, - "701A": {tags: []string{"sr-Latn"}, localMonth: localMonthsNameSerbianLatin, apFmt: apFmtSerbianLatin, weekdayNames: weekdayNamesSerbianLatin, weekdayNamesAbbr: weekdayNamesSerbianLatinAbbr}, - "7C1A": {tags: []string{"sr"}, localMonth: localMonthsNameSerbianLatin, apFmt: apFmtSerbianLatin, weekdayNames: weekdayNamesSerbianLatin, weekdayNamesAbbr: weekdayNamesSerbianLatinAbbr}, - "181A": {tags: []string{"sr-Latn-BA"}, localMonth: localMonthsNameSerbianLatin, apFmt: apFmtSerbianLatinBA, weekdayNames: weekdayNamesSerbianLatinBA, weekdayNamesAbbr: weekdayNamesSerbianLatinBAAbbr}, - "2C1A": {tags: []string{"sr-Latn-ME"}, localMonth: localMonthsNameSerbianLatin, apFmt: apFmtSerbianLatinBA, weekdayNames: weekdayNamesSerbianLatinME, weekdayNamesAbbr: weekdayNamesSerbianLatinAbbr}, - "241A": {tags: []string{"sr-Latn-RS"}, localMonth: localMonthsNameSerbianLatin, apFmt: apFmtSerbianLatin, weekdayNames: weekdayNamesSerbianLatin, weekdayNamesAbbr: weekdayNamesSerbianLatinAbbr}, - "81A": {tags: []string{"sr-Latn-CS"}, localMonth: localMonthsNameSerbianLatinCS, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSerbianLatin, weekdayNamesAbbr: weekdayNamesSerbianLatinCSAbbr}, - "6C": {tags: []string{"nso"}, localMonth: localMonthsNameSesothoSaLeboa, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSesothoSaLeboa, weekdayNamesAbbr: weekdayNamesSesothoSaLeboaAbbr}, - "46C": {tags: []string{"nso-ZA"}, localMonth: localMonthsNameSesothoSaLeboa, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSesothoSaLeboa, weekdayNamesAbbr: weekdayNamesSesothoSaLeboaAbbr}, - "32": {tags: []string{"tn"}, localMonth: localMonthsNameSetswana, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSetswana, weekdayNamesAbbr: weekdayNamesSetswanaAbbr}, - "832": {tags: []string{"tn-BW"}, localMonth: localMonthsNameSetswana, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSetswana, weekdayNamesAbbr: weekdayNamesSetswanaAbbr}, - "432": {tags: []string{"tn-ZA"}, localMonth: localMonthsNameSetswana, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSetswana, weekdayNamesAbbr: weekdayNamesSetswanaAbbr}, - "59": {tags: []string{"sd"}, localMonth: localMonthsNameSindhi, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSindhi, weekdayNamesAbbr: weekdayNamesSindhiAbbr}, - "7C59": {tags: []string{"sd-Arab"}, localMonth: localMonthsNameSindhi, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSindhi, weekdayNamesAbbr: weekdayNamesSindhiAbbr}, - "859": {tags: []string{"sd-Arab-PK"}, localMonth: localMonthsNameSindhi, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSindhi, weekdayNamesAbbr: weekdayNamesSindhiAbbr}, - "5B": {tags: []string{"si"}, localMonth: localMonthsNameSinhala, apFmt: apFmtSinhala, weekdayNames: weekdayNamesSindhi, weekdayNamesAbbr: weekdayNamesSindhiAbbr}, - "45B": {tags: []string{"si-LK"}, localMonth: localMonthsNameSinhala, apFmt: apFmtSinhala, weekdayNames: weekdayNamesSindhi, weekdayNamesAbbr: weekdayNamesSindhiAbbr}, - "1B": {tags: []string{"sk"}, localMonth: localMonthsNameSlovak, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSlovak, weekdayNamesAbbr: weekdayNamesSlovakAbbr}, - "41B": {tags: []string{"sk-SK"}, localMonth: localMonthsNameSlovak, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSlovak, weekdayNamesAbbr: weekdayNamesSlovakAbbr}, - "24": {tags: []string{"sl"}, localMonth: localMonthsNameSlovenian, apFmt: apFmtSlovenian, weekdayNames: weekdayNamesSlovenian, weekdayNamesAbbr: weekdayNamesSlovenianAbbr}, - "424": {tags: []string{"sl-SI"}, localMonth: localMonthsNameSlovenian, apFmt: apFmtSlovenian, weekdayNames: weekdayNamesSlovenian, weekdayNamesAbbr: weekdayNamesSlovenianAbbr}, - "77": {tags: []string{"so"}, localMonth: localMonthsNameSomali, apFmt: apFmtSomali, weekdayNames: weekdayNamesSomali, weekdayNamesAbbr: weekdayNamesSomaliAbbr}, - "477": {tags: []string{"so-SO"}, localMonth: localMonthsNameSomali, apFmt: apFmtSomali, weekdayNames: weekdayNamesSomali, weekdayNamesAbbr: weekdayNamesSomaliAbbr}, - "30": {tags: []string{"st"}, localMonth: localMonthsNameSotho, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSotho, weekdayNamesAbbr: weekdayNamesSothoAbbr}, - "430": {tags: []string{"st-ZA"}, localMonth: localMonthsNameSotho, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSotho, weekdayNamesAbbr: weekdayNamesSothoAbbr}, - "A": {tags: []string{"es"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishAbbr}, - "2C0A": {tags: []string{"es-AR"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, - "200A": {tags: []string{"es-VE"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, - "400A": {tags: []string{"es-BO"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, - "340A": {tags: []string{"es-CL"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, - "240A": {tags: []string{"es-CO"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, - "140A": {tags: []string{"es-CR"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, - "5C0A": {tags: []string{"es-CU"}, localMonth: localMonthsNameSpanish, apFmt: apFmtCuba, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, - "1C0A": {tags: []string{"es-DO"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, - "300A": {tags: []string{"es-EC"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, - "440A": {tags: []string{"es-SV"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, - "100A": {tags: []string{"es-GT"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, - "480A": {tags: []string{"es-HN"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, - "580A": {tags: []string{"es-419"}, localMonth: localMonthsNameSpanish, apFmt: apFmtCuba, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, - "80A": {tags: []string{"es-MX"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, - "4C0A": {tags: []string{"es-NI"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, - "180A": {tags: []string{"es-PA"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, - "3C0A": {tags: []string{"es-PY"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, - "280A": {tags: []string{"es-PE"}, localMonth: localMonthsNameSpanishPE, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, - "500A": {tags: []string{"es-PR"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, - "40A": {tags: []string{"es-ES_tradnl"}, localMonth: localMonthsNameSpanish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishAbbr}, - "C0A": {tags: []string{"es-ES"}, localMonth: localMonthsNameSpanish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishAbbr}, - "540A": {tags: []string{"es-US"}, localMonth: localMonthsNameSpanish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishUSAbbr}, - "380A": {tags: []string{"es-UY"}, localMonth: localMonthsNameSpanishPE, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, - "1D": {tags: []string{"sv"}, localMonth: localMonthsNameSwedish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSwedish, weekdayNamesAbbr: weekdayNamesSwedishAbbr}, - "81D": {tags: []string{"sv-FI"}, localMonth: localMonthsNameSwedishFI, apFmt: apFmtSwedish, weekdayNames: weekdayNamesSwedish, weekdayNamesAbbr: weekdayNamesSwedishAbbr}, - "41D": {tags: []string{"sv-SE"}, localMonth: localMonthsNameSwedishFI, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSwedish, weekdayNamesAbbr: weekdayNamesSwedishAbbr}, - "5A": {tags: []string{"syr"}, localMonth: localMonthsNameSyriac, apFmt: apFmtSyriac, weekdayNames: weekdayNamesSyriac, weekdayNamesAbbr: weekdayNamesSyriacAbbr}, - "45A": {tags: []string{"syr-SY"}, localMonth: localMonthsNameSyriac, apFmt: apFmtSyriac, weekdayNames: weekdayNamesSyriac, weekdayNamesAbbr: weekdayNamesSyriacAbbr}, - "28": {tags: []string{"tg"}, localMonth: localMonthsNameTajik, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTajik, weekdayNamesAbbr: weekdayNamesTajikAbbr}, - "7C28": {tags: []string{"tg-Cyrl"}, localMonth: localMonthsNameTajik, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTajik, weekdayNamesAbbr: weekdayNamesTajikAbbr}, - "428": {tags: []string{"tg-Cyrl-TJ"}, localMonth: localMonthsNameTajik, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTajik, weekdayNamesAbbr: weekdayNamesTajikAbbr}, - "5F": {tags: []string{"tzm"}, localMonth: localMonthsNameTamazight, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTamazight, weekdayNamesAbbr: weekdayNamesTamazightAbbr}, - "7C5F": {tags: []string{"tzm-Latn"}, localMonth: localMonthsNameTamazight, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTamazight, weekdayNamesAbbr: weekdayNamesTamazightAbbr}, - "85F": {tags: []string{"tzm-Latn-DZ"}, localMonth: localMonthsNameTamazight, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTamazight, weekdayNamesAbbr: weekdayNamesTamazightAbbr}, - "49": {tags: []string{"ta"}, localMonth: localMonthsNameTamil, apFmt: apFmtTamil, weekdayNames: weekdayNamesTamil, weekdayNamesAbbr: weekdayNamesTamilAbbr}, - "449": {tags: []string{"ta-IN"}, localMonth: localMonthsNameTamil, apFmt: apFmtTamil, weekdayNames: weekdayNamesTamil, weekdayNamesAbbr: weekdayNamesTamilAbbr}, - "849": {tags: []string{"ta-LK"}, localMonth: localMonthsNameTamilLK, apFmt: apFmtTamil, weekdayNames: weekdayNamesTamilLK, weekdayNamesAbbr: weekdayNamesTamilLKAbbr}, - "44": {tags: []string{"tt"}, localMonth: localMonthsNameTatar, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTatar, weekdayNamesAbbr: weekdayNamesTatarAbbr}, - "444": {tags: []string{"tt-RU"}, localMonth: localMonthsNameTatar, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTatar, weekdayNamesAbbr: weekdayNamesTatarAbbr}, - "4A": {tags: []string{"te"}, localMonth: localMonthsNameTelugu, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTelugu, weekdayNamesAbbr: weekdayNamesTeluguAbbr}, - "44A": {tags: []string{"te-IN"}, localMonth: localMonthsNameTelugu, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTelugu, weekdayNamesAbbr: weekdayNamesTeluguAbbr}, - "1E": {tags: []string{"th"}, localMonth: localMonthsNameThai, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesThai, weekdayNamesAbbr: weekdayNamesThaiAbbr}, - "41E": {tags: []string{"th-TH"}, localMonth: localMonthsNameThai, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesThai, weekdayNamesAbbr: weekdayNamesThaiAbbr}, - "51": {tags: []string{"bo"}, localMonth: localMonthsNameTibetan, apFmt: apFmtTibetan, weekdayNames: weekdayNamesTibetan, weekdayNamesAbbr: weekdayNamesTibetanAbbr}, - "451": {tags: []string{"bo-CN"}, localMonth: localMonthsNameTibetan, apFmt: apFmtTibetan, weekdayNames: weekdayNamesTibetan, weekdayNamesAbbr: weekdayNamesTibetanAbbr}, - "73": {tags: []string{"ti"}, localMonth: localMonthsNameTigrinya, apFmt: apFmtTigrinya, weekdayNames: weekdayNamesTigrinya, weekdayNamesAbbr: weekdayNamesTigrinyaAbbr}, - "873": {tags: []string{"ti-ER"}, localMonth: localMonthsNameTigrinya, apFmt: apFmtTigrinyaER, weekdayNames: weekdayNamesTigrinya, weekdayNamesAbbr: weekdayNamesTigrinyaAbbr}, - "473": {tags: []string{"ti-ET"}, localMonth: localMonthsNameTigrinya, apFmt: apFmtTigrinya, weekdayNames: weekdayNamesTigrinya, weekdayNamesAbbr: weekdayNamesTigrinyaAbbr}, - "31": {tags: []string{"ts"}, localMonth: localMonthsNameTsonga, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTsonga, weekdayNamesAbbr: weekdayNamesTsongaAbbr}, - "431": {tags: []string{"ts-ZA"}, localMonth: localMonthsNameTsonga, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTsonga, weekdayNamesAbbr: weekdayNamesTsongaAbbr}, - "1F": {tags: []string{"tr"}, localMonth: localMonthsNameTurkish, apFmt: apFmtTurkish, weekdayNames: weekdayNamesTurkish, weekdayNamesAbbr: weekdayNamesTurkishAbbr}, - "41F": {tags: []string{"tr-TR"}, localMonth: localMonthsNameTurkish, apFmt: apFmtTurkish, weekdayNames: weekdayNamesTurkish, weekdayNamesAbbr: weekdayNamesTurkishAbbr}, - "42": {tags: []string{"tk"}, localMonth: localMonthsNameTurkmen, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTurkmen, weekdayNamesAbbr: weekdayNamesTurkmenAbbr}, - "442": {tags: []string{"tk-TM"}, localMonth: localMonthsNameTurkmen, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTurkmen, weekdayNamesAbbr: weekdayNamesTurkmenAbbr}, - "22": {tags: []string{"uk"}, localMonth: localMonthsNameUkrainian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesUkrainian, weekdayNamesAbbr: weekdayNamesUkrainianAbbr}, - "422": {tags: []string{"uk-UA"}, localMonth: localMonthsNameUkrainian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesUkrainian, weekdayNamesAbbr: weekdayNamesUkrainianAbbr}, - "2E": {tags: []string{"hsb"}, localMonth: localMonthsNameUpperSorbian, apFmt: apFmtUpperSorbian, weekdayNames: weekdayNamesSorbian, weekdayNamesAbbr: weekdayNamesSorbianAbbr}, - "42E": {tags: []string{"hsb-DE"}, localMonth: localMonthsNameUpperSorbian, apFmt: apFmtUpperSorbian, weekdayNames: weekdayNamesSorbian, weekdayNamesAbbr: weekdayNamesSorbianAbbr}, - "20": {tags: []string{"ur"}, localMonth: localMonthsNamePunjabiArab, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesUrdu, weekdayNamesAbbr: weekdayNamesUrdu}, - "820": {tags: []string{"ur-IN"}, localMonth: localMonthsNamePunjabiArab, apFmt: apFmtUrdu, weekdayNames: weekdayNamesUrduIN, weekdayNamesAbbr: weekdayNamesUrduIN}, - "420": {tags: []string{"ur-PK"}, localMonth: localMonthsNamePunjabiArab, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesUrdu, weekdayNamesAbbr: weekdayNamesUrdu}, - "80": {tags: []string{"ug"}, localMonth: localMonthsNameUyghur, apFmt: apFmtUyghur, weekdayNames: weekdayNamesUyghur, weekdayNamesAbbr: weekdayNamesUyghurAbbr}, - "480": {tags: []string{"ug-CN"}, localMonth: localMonthsNameUyghur, apFmt: apFmtUyghur, weekdayNames: weekdayNamesUyghur, weekdayNamesAbbr: weekdayNamesUyghurAbbr}, - "7843": {tags: []string{"uz-Cyrl"}, localMonth: localMonthsNameUzbekCyrillic, apFmt: apFmtUzbekCyrillic, weekdayNames: weekdayNamesUzbekCyrillic, weekdayNamesAbbr: weekdayNamesUzbekCyrillicAbbr}, - "843": {tags: []string{"uz-Cyrl-UZ"}, localMonth: localMonthsNameUzbekCyrillic, apFmt: apFmtUzbekCyrillic, weekdayNames: weekdayNamesUzbekCyrillic, weekdayNamesAbbr: weekdayNamesUzbekCyrillicAbbr}, - "43": {tags: []string{"uz"}, localMonth: localMonthsNameUzbek, apFmt: apFmtUzbek, weekdayNames: weekdayNamesUzbek, weekdayNamesAbbr: weekdayNamesUzbekAbbr}, - "7C43": {tags: []string{"uz-Latn"}, localMonth: localMonthsNameUzbek, apFmt: apFmtUzbek, weekdayNames: weekdayNamesUzbek, weekdayNamesAbbr: weekdayNamesUzbekAbbr}, - "443": {tags: []string{"uz-Latn-UZ"}, localMonth: localMonthsNameUzbek, apFmt: apFmtUzbek, weekdayNames: weekdayNamesUzbek, weekdayNamesAbbr: weekdayNamesUzbekAbbr}, - "803": {tags: []string{"ca-ES-valencia"}, localMonth: localMonthsNameValencian, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesValencian, weekdayNamesAbbr: weekdayNamesValencianAbbr}, - "33": {tags: []string{"ve"}, localMonth: localMonthsNameVenda, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesVenda, weekdayNamesAbbr: weekdayNamesVendaAbbr}, - "433": {tags: []string{"ve-ZA"}, localMonth: localMonthsNameVenda, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesVenda, weekdayNamesAbbr: weekdayNamesVendaAbbr}, - "2A": {tags: []string{"vi"}, localMonth: localMonthsNameVietnamese, apFmt: apFmtVietnamese, weekdayNames: weekdayNamesVietnamese, weekdayNamesAbbr: weekdayNamesVietnameseAbbr}, - "42A": {tags: []string{"vi-VN"}, localMonth: localMonthsNameVietnamese, apFmt: apFmtVietnamese, weekdayNames: weekdayNamesVietnamese, weekdayNamesAbbr: weekdayNamesVietnameseAbbr}, - "52": {tags: []string{"cy"}, localMonth: localMonthsNameWelsh, apFmt: apFmtWelsh, weekdayNames: weekdayNamesWelsh, weekdayNamesAbbr: weekdayNamesWelshAbbr}, - "452": {tags: []string{"cy-GB"}, localMonth: localMonthsNameWelsh, apFmt: apFmtWelsh, weekdayNames: weekdayNamesWelsh, weekdayNamesAbbr: weekdayNamesWelshAbbr}, - "88": {tags: []string{"wo"}, localMonth: localMonthsNameWolof, apFmt: apFmtWolof, weekdayNames: weekdayNamesWolof, weekdayNamesAbbr: weekdayNamesWolofAbbr}, - "488": {tags: []string{"wo-SN"}, localMonth: localMonthsNameWolof, apFmt: apFmtWolof, weekdayNames: weekdayNamesWolof, weekdayNamesAbbr: weekdayNamesWolofAbbr}, - "34": {tags: []string{"xh"}, localMonth: localMonthsNameXhosa, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesXhosa, weekdayNamesAbbr: weekdayNamesXhosaAbbr}, - "434": {tags: []string{"xh-ZA"}, localMonth: localMonthsNameXhosa, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesXhosa, weekdayNamesAbbr: weekdayNamesXhosaAbbr}, - "78": {tags: []string{"ii"}, localMonth: localMonthsNameYi, apFmt: apFmtYi, weekdayNames: weekdayNamesYi, weekdayNamesAbbr: weekdayNamesYiAbbr}, - "478": {tags: []string{"ii-CN"}, localMonth: localMonthsNameYi, apFmt: apFmtYi, weekdayNames: weekdayNamesYi, weekdayNamesAbbr: weekdayNamesYiAbbr}, - "43D": {tags: []string{"yi-001"}, localMonth: localMonthsNameYiddish, apFmt: apFmtYiddish, weekdayNames: weekdayNamesYiddish, weekdayNamesAbbr: weekdayNamesYiddishAbbr}, - "6A": {tags: []string{"yo"}, localMonth: localMonthsNameYoruba, apFmt: apFmtYoruba, weekdayNames: weekdayNamesYoruba, weekdayNamesAbbr: weekdayNamesYorubaAbbr}, - "46A": {tags: []string{"yo-NG"}, localMonth: localMonthsNameYoruba, apFmt: apFmtYoruba, weekdayNames: weekdayNamesYoruba, weekdayNamesAbbr: weekdayNamesYorubaAbbr}, - "35": {tags: []string{"zu"}, localMonth: localMonthsNameZulu, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesZulu, weekdayNamesAbbr: weekdayNamesZuluAbbr}, - "435": {tags: []string{"zu-ZA"}, localMonth: localMonthsNameZulu, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesZulu, weekdayNamesAbbr: weekdayNamesZuluAbbr}, + "C09": {tags: []string{"en-AU"}, localMonth: localMonthsNameEnglish, apFmt: strings.ToLower(nfp.AmPm[0]), weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + "2809": {tags: []string{"en-BZ"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + "1009": {tags: []string{"en-CA"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + "2409": {tags: []string{"en-029"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + "3C09": {tags: []string{"en-HK"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + "4009": {tags: []string{"en-IN"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + "1809": {tags: []string{"en-IE"}, localMonth: localMonthsNameEnglish, apFmt: strings.ToLower(nfp.AmPm[0]), weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + "2009": {tags: []string{"en-JM"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + "4409": {tags: []string{"en-MY"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + "1409": {tags: []string{"en-NZ"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + "3409": {tags: []string{"en-PH"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + "4809": {tags: []string{"en-SG"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + "1C09": {tags: []string{"en-ZA"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + "2C09": {tags: []string{"en-TT"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + "4C09": {tags: []string{"en-AE"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + "809": {tags: []string{"en-GB"}, localMonth: localMonthsNameEnglish, apFmt: strings.ToLower(nfp.AmPm[0]), weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + "409": {tags: []string{"en-US"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + "3009": {tags: []string{"en-ZW"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + "25": {tags: []string{"et"}, localMonth: localMonthsNameEstonian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEstonian, weekdayNamesAbbr: weekdayNamesEstonianAbbr}, + "425": {tags: []string{"et-EE"}, localMonth: localMonthsNameEstonian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEstonian, weekdayNamesAbbr: weekdayNamesEstonianAbbr}, + "38": {tags: []string{"fo"}, localMonth: localMonthsNameFaroese, apFmt: apFmtFaroese, weekdayNames: weekdayNamesFaroese, weekdayNamesAbbr: weekdayNamesFaroeseAbbr}, + "438": {tags: []string{"fo-FO"}, localMonth: localMonthsNameFaroese, apFmt: apFmtFaroese, weekdayNames: weekdayNamesFaroese, weekdayNamesAbbr: weekdayNamesFaroeseAbbr}, + "64": {tags: []string{"fil"}, localMonth: localMonthsNameFilipino, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFilipino, weekdayNamesAbbr: weekdayNamesFilipinoAbbr}, + "464": {tags: []string{"fil-PH"}, localMonth: localMonthsNameFilipino, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFilipino, weekdayNamesAbbr: weekdayNamesFilipinoAbbr}, + "B": {tags: []string{"fi"}, localMonth: localMonthsNameFinnish, apFmt: apFmtFinnish, weekdayNames: weekdayNamesFinnish, weekdayNamesAbbr: weekdayNamesFinnishAbbr}, + "40B": {tags: []string{"fi-FI"}, localMonth: localMonthsNameFinnish, apFmt: apFmtFinnish, weekdayNames: weekdayNamesFinnish, weekdayNamesAbbr: weekdayNamesFinnishAbbr}, + "C": {tags: []string{"fr"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, + "80C": {tags: []string{"fr-BE"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, + "2C0C": {tags: []string{"fr-CM"}, localMonth: localMonthsNameFrench, apFmt: apFmtCameroon, weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, + "C0C": {tags: []string{"fr-CA"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, + "1C0C": {tags: []string{"fr-029"}, localMonth: localMonthsNameCaribbean, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, + "240C": {tags: []string{"fr-CD"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, + "300C": {tags: []string{"fr-CI"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, + "40C": {tags: []string{"fr-FR"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, + "3C0C": {tags: []string{"fr-HT"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, + "140C": {tags: []string{"fr-LU"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, + "340C": {tags: []string{"fr-ML"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, + "380C": {tags: []string{"fr-MA"}, localMonth: localMonthsNameMorocco, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, + "180C": {tags: []string{"fr-MC"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, + "200C": {tags: []string{"fr-RE"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, + "280C": {tags: []string{"fr-SN"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, + "62": {tags: []string{"fy"}, localMonth: localMonthsNameFrisian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrisian, weekdayNamesAbbr: weekdayNamesFrisianAbbr}, + "462": {tags: []string{"fy-NL"}, localMonth: localMonthsNameFrisian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrisian, weekdayNamesAbbr: weekdayNamesFrisianAbbr}, + "67": {tags: []string{"ff"}, localMonth: localMonthsNameFulah, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFulah, weekdayNamesAbbr: weekdayNamesFulahAbbr}, + "7C67": {tags: []string{"ff-Latn"}, localMonth: localMonthsNameFulah, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFulah, weekdayNamesAbbr: weekdayNamesFulahAbbr}, + "467": {tags: []string{"ff-NG", "ff-Latn-NG"}, localMonth: localMonthsNameNigeria, apFmt: apFmtNigeria, weekdayNames: weekdayNamesNigeria, weekdayNamesAbbr: weekdayNamesNigeriaAbbr}, + "867": {tags: []string{"ff-SN"}, localMonth: localMonthsNameNigeria, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesNigeria, weekdayNamesAbbr: weekdayNamesNigeriaAbbr}, + "56": {tags: []string{"gl"}, localMonth: localMonthsNameGalician, apFmt: apFmtCuba, weekdayNames: weekdayNamesGalician, weekdayNamesAbbr: weekdayNamesGalicianAbbr}, + "456": {tags: []string{"gl-ES"}, localMonth: localMonthsNameGalician, apFmt: apFmtCuba, weekdayNames: weekdayNamesGalician, weekdayNamesAbbr: weekdayNamesGalicianAbbr}, + "37": {tags: []string{"ka"}, localMonth: localMonthsNameGeorgian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGeorgian, weekdayNamesAbbr: weekdayNamesGeorgianAbbr}, + "437": {tags: []string{"ka-GE"}, localMonth: localMonthsNameGeorgian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGeorgian, weekdayNamesAbbr: weekdayNamesGeorgianAbbr}, + "7": {tags: []string{"de"}, localMonth: localMonthsNameGerman, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGerman, weekdayNamesAbbr: weekdayNamesGermanAbbr}, + "C07": {tags: []string{"de-AT"}, localMonth: localMonthsNameAustria, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGerman, weekdayNamesAbbr: weekdayNamesGermanAbbr}, + "407": {tags: []string{"de-DE"}, localMonth: localMonthsNameGerman, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGerman, weekdayNamesAbbr: weekdayNamesGermanAbbr}, + "1407": {tags: []string{"de-LI"}, localMonth: localMonthsNameGerman, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGerman, weekdayNamesAbbr: weekdayNamesGermanAbbr}, + "807": {tags: []string{"de-CH"}, localMonth: localMonthsNameGerman, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGerman, weekdayNamesAbbr: weekdayNamesGermanAbbr}, + "8": {tags: []string{"el"}, localMonth: localMonthsNameGreek, apFmt: apFmtGreek, weekdayNames: weekdayNamesGreek, weekdayNamesAbbr: weekdayNamesGreekAbbr}, + "408": {tags: []string{"el-GR"}, localMonth: localMonthsNameGreek, apFmt: apFmtGreek, weekdayNames: weekdayNamesGreek, weekdayNamesAbbr: weekdayNamesGreekAbbr}, + "6F": {tags: []string{"kl"}, localMonth: localMonthsNameGreenlandic, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGreenlandic, weekdayNamesAbbr: weekdayNamesGreenlandicAbbr}, + "46F": {tags: []string{"kl-GL"}, localMonth: localMonthsNameGreenlandic, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGreenlandic, weekdayNamesAbbr: weekdayNamesGreenlandicAbbr}, + "74": {tags: []string{"gn"}, localMonth: localMonthsNameGuarani, apFmt: apFmtCuba, weekdayNames: weekdayNamesGuarani, weekdayNamesAbbr: weekdayNamesGuaraniAbbr}, + "474": {tags: []string{"gn-PY"}, localMonth: localMonthsNameGuarani, apFmt: apFmtCuba, weekdayNames: weekdayNamesGuarani, weekdayNamesAbbr: weekdayNamesGuaraniAbbr}, + "47": {tags: []string{"gu"}, localMonth: localMonthsNameGujarati, apFmt: apFmtGujarati, weekdayNames: weekdayNamesGujarati, weekdayNamesAbbr: weekdayNamesGujaratiAbbr}, + "447": {tags: []string{"gu-IN"}, localMonth: localMonthsNameGujarati, apFmt: apFmtGujarati, weekdayNames: weekdayNamesGujarati, weekdayNamesAbbr: weekdayNamesGujaratiAbbr}, + "68": {tags: []string{"ha"}, localMonth: localMonthsNameHausa, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesHausa, weekdayNamesAbbr: weekdayNamesHausaAbbr}, + "7C68": {tags: []string{"ha-Latn"}, localMonth: localMonthsNameHausa, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesHausa, weekdayNamesAbbr: weekdayNamesHausaAbbr}, + "468": {tags: []string{"ha-Latn-NG"}, localMonth: localMonthsNameHausa, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesHausa, weekdayNamesAbbr: weekdayNamesHausaAbbr}, + "75": {tags: []string{"haw"}, localMonth: localMonthsNameHawaiian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesHawaiian, weekdayNamesAbbr: weekdayNamesHawaiianAbbr}, + "475": {tags: []string{"haw-US"}, localMonth: localMonthsNameHawaiian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesHawaiian, weekdayNamesAbbr: weekdayNamesHawaiianAbbr}, + "D": {tags: []string{"he"}, localMonth: localMonthsNameHebrew, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesHebrew, weekdayNamesAbbr: weekdayNamesHebrewAbbr}, + "40D": {tags: []string{"he-IL"}, localMonth: localMonthsNameHebrew, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesHebrew, weekdayNamesAbbr: weekdayNamesHebrewAbbr}, + "39": {tags: []string{"hi"}, localMonth: localMonthsNameHindi, apFmt: apFmtHindi, weekdayNames: weekdayNamesHindi, weekdayNamesAbbr: weekdayNamesHindiAbbr}, + "439": {tags: []string{"hi-IN"}, localMonth: localMonthsNameHindi, apFmt: apFmtHindi, weekdayNames: weekdayNamesHindi, weekdayNamesAbbr: weekdayNamesHindiAbbr}, + "E": {tags: []string{"hu"}, localMonth: localMonthsNameHungarian, apFmt: apFmtHungarian, weekdayNames: weekdayNamesHungarian, weekdayNamesAbbr: weekdayNamesHungarianAbbr}, + "40E": {tags: []string{"hu-HU"}, localMonth: localMonthsNameHungarian, apFmt: apFmtHungarian, weekdayNames: weekdayNamesHungarian, weekdayNamesAbbr: weekdayNamesHungarianAbbr}, + "F": {tags: []string{"is"}, localMonth: localMonthsNameIcelandic, apFmt: apFmtIcelandic, weekdayNames: weekdayNamesIcelandic, weekdayNamesAbbr: weekdayNamesIcelandicAbbr}, + "40F": {tags: []string{"is-IS"}, localMonth: localMonthsNameIcelandic, apFmt: apFmtIcelandic, weekdayNames: weekdayNamesIcelandic, weekdayNamesAbbr: weekdayNamesIcelandicAbbr}, + "70": {tags: []string{"ig"}, localMonth: localMonthsNameIgbo, apFmt: apFmtIgbo, weekdayNames: weekdayNamesIgbo, weekdayNamesAbbr: weekdayNamesIgboAbbr}, + "470": {tags: []string{"ig-NG"}, localMonth: localMonthsNameIgbo, apFmt: apFmtIgbo, weekdayNames: weekdayNamesIgbo, weekdayNamesAbbr: weekdayNamesIgboAbbr}, + "21": {tags: []string{"id"}, localMonth: localMonthsNameIndonesian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesIndonesian, weekdayNamesAbbr: weekdayNamesIndonesianAbbr}, + "421": {tags: []string{"id-ID"}, localMonth: localMonthsNameIndonesian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesIndonesian, weekdayNamesAbbr: weekdayNamesIndonesianAbbr}, + "5D": {tags: []string{"iu"}, localMonth: localMonthsNameInuktitut, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesInuktitut, weekdayNamesAbbr: weekdayNamesInuktitutAbbr}, + "7C5D": {tags: []string{"iu-Latn"}, localMonth: localMonthsNameInuktitut, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesInuktitut, weekdayNamesAbbr: weekdayNamesInuktitutAbbr}, + "85D": {tags: []string{"iu-Latn-CA"}, localMonth: localMonthsNameInuktitut, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesInuktitut, weekdayNamesAbbr: weekdayNamesInuktitutAbbr}, + "785D": {tags: []string{"iu-Cans"}, localMonth: localMonthsNameSyllabics, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSyllabics, weekdayNamesAbbr: weekdayNamesSyllabicsAbbr}, + "45D": {tags: []string{"iu-Cans-CA"}, localMonth: localMonthsNameSyllabics, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSyllabics, weekdayNamesAbbr: weekdayNamesSyllabicsAbbr}, + "3C": {tags: []string{"ga"}, localMonth: localMonthsNameIrish, apFmt: apFmtIrish, weekdayNames: weekdayNamesIrish, weekdayNamesAbbr: weekdayNamesIrishAbbr}, + "83C": {tags: []string{"ga-IE"}, localMonth: localMonthsNameIrish, apFmt: apFmtIrish, weekdayNames: weekdayNamesIrish, weekdayNamesAbbr: weekdayNamesIrishAbbr}, + "10": {tags: []string{"it"}, localMonth: localMonthsNameItalian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesItalian, weekdayNamesAbbr: weekdayNamesItalianAbbr}, + "410": {tags: []string{"it-IT"}, localMonth: localMonthsNameItalian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesItalian, weekdayNamesAbbr: weekdayNamesItalianAbbr}, + "810": {tags: []string{"it-CH"}, localMonth: localMonthsNameItalian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesItalian, weekdayNamesAbbr: weekdayNamesItalianAbbr}, + "11": {tags: []string{"ja"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese, weekdayNames: weekdayNamesJapanese, weekdayNamesAbbr: weekdayNamesJapaneseAbbr}, + "411": {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese, weekdayNames: weekdayNamesJapanese, weekdayNamesAbbr: weekdayNamesJapaneseAbbr}, + "800411": {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese, weekdayNames: weekdayNamesJapanese, weekdayNamesAbbr: weekdayNamesJapaneseAbbr}, + "JA-JP-X-GANNEN": {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese, weekdayNames: weekdayNamesJapanese, weekdayNamesAbbr: weekdayNamesJapaneseAbbr}, + "JA-JP-X-GANNEN,80": {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese, weekdayNames: weekdayNamesJapanese, weekdayNamesAbbr: weekdayNamesJapaneseAbbr, useGannen: true}, + "4B": {tags: []string{"kn"}, localMonth: localMonthsNameKannada, apFmt: apFmtKannada, weekdayNames: weekdayNamesKannada, weekdayNamesAbbr: weekdayNamesKannadaAbbr}, + "44B": {tags: []string{"kn-IN"}, localMonth: localMonthsNameKannada, apFmt: apFmtKannada, weekdayNames: weekdayNamesKannada, weekdayNamesAbbr: weekdayNamesKannadaAbbr}, + "471": {tags: []string{"kr-Latn-NG"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + "60": {tags: []string{"ks"}, localMonth: localMonthsNameKashmiri, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesKashmiri, weekdayNamesAbbr: weekdayNamesKashmiriAbbr}, + "460": {tags: []string{"ks-Arab"}, localMonth: localMonthsNameKashmiri, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesKashmiri, weekdayNamesAbbr: weekdayNamesKashmiriAbbr}, + "860": {tags: []string{"ks-Deva-IN"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + "3F": {tags: []string{"kk"}, localMonth: localMonthsNameKazakh, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesKazakh, weekdayNamesAbbr: weekdayNamesKazakhAbbr}, + "43F": {tags: []string{"kk-KZ"}, localMonth: localMonthsNameKazakh, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesKazakh, weekdayNamesAbbr: weekdayNamesKazakhAbbr}, + "53": {tags: []string{"km"}, localMonth: localMonthsNameKhmer, apFmt: apFmtKhmer, weekdayNames: weekdayNamesKhmer, weekdayNamesAbbr: weekdayNamesKhmerAbbr}, + "453": {tags: []string{"km-KH"}, localMonth: localMonthsNameKhmer, apFmt: apFmtKhmer, weekdayNames: weekdayNamesKhmer, weekdayNamesAbbr: weekdayNamesKhmerAbbr}, + "86": {tags: []string{"quc"}, localMonth: localMonthsNameKiche, apFmt: apFmtCuba, weekdayNames: weekdayNamesKiche, weekdayNamesAbbr: weekdayNamesKicheAbbr}, + "486": {tags: []string{"quc-Latn-GT"}, localMonth: localMonthsNameKiche, apFmt: apFmtCuba, weekdayNames: weekdayNamesKiche, weekdayNamesAbbr: weekdayNamesKicheAbbr}, + "87": {tags: []string{"rw"}, localMonth: localMonthsNameKinyarwanda, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesKinyarwanda, weekdayNamesAbbr: weekdayNamesKinyarwandaAbbr}, + "487": {tags: []string{"rw-RW"}, localMonth: localMonthsNameKinyarwanda, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesKinyarwanda, weekdayNamesAbbr: weekdayNamesKinyarwandaAbbr}, + "41": {tags: []string{"sw"}, localMonth: localMonthsNameKiswahili, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesKiswahili, weekdayNamesAbbr: weekdayNamesKiswahiliAbbr}, + "441": {tags: []string{"sw-KE"}, localMonth: localMonthsNameKiswahili, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesKiswahili, weekdayNamesAbbr: weekdayNamesKiswahiliAbbr}, + "57": {tags: []string{"kok"}, localMonth: localMonthsNameKonkani, apFmt: apFmtKonkani, weekdayNames: weekdayNamesKonkani, weekdayNamesAbbr: weekdayNamesKonkaniAbbr}, + "457": {tags: []string{"kok-IN"}, localMonth: localMonthsNameKonkani, apFmt: apFmtKonkani, weekdayNames: weekdayNamesKonkani, weekdayNamesAbbr: weekdayNamesKonkaniAbbr}, + "12": {tags: []string{"ko"}, localMonth: localMonthsNameKorean, apFmt: apFmtKorean, weekdayNames: weekdayNamesKorean, weekdayNamesAbbr: weekdayNamesKoreanAbbr}, + "412": {tags: []string{"ko-KR"}, localMonth: localMonthsNameKorean, apFmt: apFmtKorean, weekdayNames: weekdayNamesKorean, weekdayNamesAbbr: weekdayNamesKoreanAbbr}, + "40": {tags: []string{"ky"}, localMonth: localMonthsNameKyrgyz, apFmt: apFmtKyrgyz, weekdayNames: weekdayNamesKyrgyz, weekdayNamesAbbr: weekdayNamesKyrgyzAbbr}, + "440": {tags: []string{"ky-KG"}, localMonth: localMonthsNameKyrgyz, apFmt: apFmtKyrgyz, weekdayNames: weekdayNamesKyrgyz, weekdayNamesAbbr: weekdayNamesKyrgyzAbbr}, + "54": {tags: []string{"lo"}, localMonth: localMonthsNameLao, apFmt: apFmtLao, weekdayNames: weekdayNamesLao, weekdayNamesAbbr: weekdayNamesLaoAbbr}, + "454": {tags: []string{"lo-LA"}, localMonth: localMonthsNameLao, apFmt: apFmtLao, weekdayNames: weekdayNamesLao, weekdayNamesAbbr: weekdayNamesLaoAbbr}, + "476": {tags: []string{"la-VA"}, localMonth: localMonthsNameLatin, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesLatin, weekdayNamesAbbr: weekdayNamesLatinAbbr}, + "26": {tags: []string{"lv"}, localMonth: localMonthsNameLatvian, apFmt: apFmtLatvian, weekdayNames: weekdayNamesLatvian, weekdayNamesAbbr: weekdayNamesLatvianAbbr}, + "426": {tags: []string{"lv-LV"}, localMonth: localMonthsNameLatvian, apFmt: apFmtLatvian, weekdayNames: weekdayNamesLatvian, weekdayNamesAbbr: weekdayNamesLatvianAbbr}, + "27": {tags: []string{"lt"}, localMonth: localMonthsNameLithuanian, apFmt: apFmtLithuanian, weekdayNames: weekdayNamesLithuanian, weekdayNamesAbbr: weekdayNamesLithuanianAbbr}, + "427": {tags: []string{"lt-LT"}, localMonth: localMonthsNameLithuanian, apFmt: apFmtLithuanian, weekdayNames: weekdayNamesLithuanian, weekdayNamesAbbr: weekdayNamesLithuanianAbbr}, + "7C2E": {tags: []string{"dsb"}, localMonth: localMonthsNameLowerSorbian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesLowerSorbian, weekdayNamesAbbr: weekdayNamesLowerSorbianAbbr}, + "82E": {tags: []string{"dsb-DE"}, localMonth: localMonthsNameLowerSorbian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesLowerSorbian, weekdayNamesAbbr: weekdayNamesLowerSorbianAbbr}, + "6E": {tags: []string{"lb"}, localMonth: localMonthsNameLuxembourgish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesLuxembourgish, weekdayNamesAbbr: weekdayNamesLuxembourgishAbbr}, + "46E": {tags: []string{"lb-LU"}, localMonth: localMonthsNameLuxembourgish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesLuxembourgish, weekdayNamesAbbr: weekdayNamesLuxembourgishAbbr}, + "2F": {tags: []string{"mk"}, localMonth: localMonthsNameMacedonian, apFmt: apFmtMacedonian, weekdayNames: weekdayNamesMacedonian, weekdayNamesAbbr: weekdayNamesMacedonianAbbr}, + "42F": {tags: []string{"mk-MK"}, localMonth: localMonthsNameMacedonian, apFmt: apFmtMacedonian, weekdayNames: weekdayNamesMacedonian, weekdayNamesAbbr: weekdayNamesMacedonianAbbr}, + "3E": {tags: []string{"ms"}, localMonth: localMonthsNameMalay, apFmt: apFmtMalay, weekdayNames: weekdayNamesMalay, weekdayNamesAbbr: weekdayNamesMalayAbbr}, + "83E": {tags: []string{"ms-BN"}, localMonth: localMonthsNameMalay, apFmt: apFmtMalay, weekdayNames: weekdayNamesMalay, weekdayNamesAbbr: weekdayNamesMalayAbbr}, + "43E": {tags: []string{"ms-MY"}, localMonth: localMonthsNameMalay, apFmt: apFmtMalay, weekdayNames: weekdayNamesMalay, weekdayNamesAbbr: weekdayNamesMalayAbbr}, + "4C": {tags: []string{"ml"}, localMonth: localMonthsNameMalayalam, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesMalayalam, weekdayNamesAbbr: weekdayNamesMalayalamAbbr}, + "44C": {tags: []string{"ml-IN"}, localMonth: localMonthsNameMalayalam, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesMalayalam, weekdayNamesAbbr: weekdayNamesMalayalamAbbr}, + "3A": {tags: []string{"mt"}, localMonth: localMonthsNameMaltese, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesMaltese, weekdayNamesAbbr: weekdayNamesMalteseAbbr}, + "43A": {tags: []string{"mt-MT"}, localMonth: localMonthsNameMaltese, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesMaltese, weekdayNamesAbbr: weekdayNamesMalteseAbbr}, + "81": {tags: []string{"mi"}, localMonth: localMonthsNameMaori, apFmt: apFmtCuba, weekdayNames: weekdayNamesMaori, weekdayNamesAbbr: weekdayNamesMaoriAbbr}, + "481": {tags: []string{"mi-NZ"}, localMonth: localMonthsNameMaori, apFmt: apFmtCuba, weekdayNames: weekdayNamesMaori, weekdayNamesAbbr: weekdayNamesMaoriAbbr}, + "7A": {tags: []string{"arn"}, localMonth: localMonthsNameMapudungun, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesMapudungun, weekdayNamesAbbr: weekdayNamesMapudungunAbbr}, + "47A": {tags: []string{"arn-CL"}, localMonth: localMonthsNameMapudungun, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesMapudungun, weekdayNamesAbbr: weekdayNamesMapudungunAbbr}, + "4E": {tags: []string{"mr"}, localMonth: localMonthsNameMarathi, apFmt: apFmtKonkani, weekdayNames: weekdayNamesMarathi, weekdayNamesAbbr: weekdayNamesMarathiAbbr}, + "44E": {tags: []string{"mr-IN"}, localMonth: localMonthsNameMarathi, apFmt: apFmtKonkani, weekdayNames: weekdayNamesMarathi, weekdayNamesAbbr: weekdayNamesMarathiAbbr}, + "7C": {tags: []string{"moh"}, localMonth: localMonthsNameMohawk, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesMohawk, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + "47C": {tags: []string{"moh-CA"}, localMonth: localMonthsNameMohawk, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesMohawk, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + "50": {tags: []string{"mn"}, localMonth: localMonthsNameMongolian, apFmt: apFmtMongolian, weekdayNames: weekdayNamesMongolian, weekdayNamesAbbr: weekdayNamesMongolianAbbr}, + "7850": {tags: []string{"mn-Cyrl"}, localMonth: localMonthsNameMongolian, apFmt: apFmtMongolian, weekdayNames: weekdayNamesMongolian, weekdayNamesAbbr: weekdayNamesMongolianCyrlAbbr}, + "450": {tags: []string{"mn-MN"}, localMonth: localMonthsNameMongolian, apFmt: apFmtMongolian, weekdayNames: weekdayNamesMongolian, weekdayNamesAbbr: weekdayNamesMongolianCyrlAbbr}, + "7C50": {tags: []string{"mn-Mong"}, localMonth: localMonthsNameTraditionalMongolian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTraditionalMongolian, weekdayNamesAbbr: weekdayNamesTraditionalMongolian}, + "850": {tags: []string{"mn-Mong-CN"}, localMonth: localMonthsNameTraditionalMongolian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTraditionalMongolian, weekdayNamesAbbr: weekdayNamesTraditionalMongolian}, + "C50": {tags: []string{"mn-Mong-MN"}, localMonth: localMonthsNameTraditionalMongolian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTraditionalMongolianMN, weekdayNamesAbbr: weekdayNamesTraditionalMongolianMN}, + "61": {tags: []string{"ne"}, localMonth: localMonthsNameNepali, apFmt: apFmtHindi, weekdayNames: weekdayNamesNepali, weekdayNamesAbbr: weekdayNamesNepaliAbbr}, + "861": {tags: []string{"ne-IN"}, localMonth: localMonthsNameNepaliIN, apFmt: apFmtHindi, weekdayNames: weekdayNamesNepaliIN, weekdayNamesAbbr: weekdayNamesNepaliINAbbr}, + "461": {tags: []string{"ne-NP"}, localMonth: localMonthsNameNepali, apFmt: apFmtHindi, weekdayNames: weekdayNamesNepali, weekdayNamesAbbr: weekdayNamesNepaliAbbr}, + "14": {tags: []string{"no"}, localMonth: localMonthsNameNorwegian, apFmt: apFmtCuba, weekdayNames: weekdayNamesNorwegian, weekdayNamesAbbr: weekdayNamesNorwegianAbbr}, + "7C14": {tags: []string{"nb"}, localMonth: localMonthsNameNorwegian, apFmt: apFmtCuba, weekdayNames: weekdayNamesNorwegian, weekdayNamesAbbr: weekdayNamesNorwegianNOAbbr}, + "414": {tags: []string{"nb-NO"}, localMonth: localMonthsNameNorwegian, apFmt: apFmtCuba, weekdayNames: weekdayNamesNorwegian, weekdayNamesAbbr: weekdayNamesNorwegianNOAbbr}, + "7814": {tags: []string{"nn"}, localMonth: localMonthsNameNorwegian, apFmt: apFmtNorwegian, weekdayNames: weekdayNamesNorwegianNynorsk, weekdayNamesAbbr: weekdayNamesNorwegianNynorskAbbr}, + "814": {tags: []string{"nn-NO"}, localMonth: localMonthsNameNorwegian, apFmt: apFmtNorwegian, weekdayNames: weekdayNamesNorwegianNynorsk, weekdayNamesAbbr: weekdayNamesNorwegianNynorskAbbr}, + "82": {tags: []string{"oc"}, localMonth: localMonthsNameOccitan, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesOccitan, weekdayNamesAbbr: weekdayNamesOccitanAbbr}, + "482": {tags: []string{"oc-FR"}, localMonth: localMonthsNameOccitan, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesOccitan, weekdayNamesAbbr: weekdayNamesOccitanAbbr}, + "48": {tags: []string{"or"}, localMonth: localMonthsNameOdia, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesOdia, weekdayNamesAbbr: weekdayNamesOdiaAbbr}, + "448": {tags: []string{"or-IN"}, localMonth: localMonthsNameOdia, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesOdia, weekdayNamesAbbr: weekdayNamesOdiaAbbr}, + "72": {tags: []string{"om"}, localMonth: localMonthsNameOromo, apFmt: apFmtOromo, weekdayNames: weekdayNamesOromo, weekdayNamesAbbr: weekdayNamesOromoAbbr}, + "472": {tags: []string{"om-ET"}, localMonth: localMonthsNameOromo, apFmt: apFmtOromo, weekdayNames: weekdayNamesOromo, weekdayNamesAbbr: weekdayNamesOromoAbbr}, + "63": {tags: []string{"ps"}, localMonth: localMonthsNamePashto, apFmt: apFmtPashto, weekdayNames: weekdayNamesPashto, weekdayNamesAbbr: weekdayNamesPashto}, + "463": {tags: []string{"ps-AF"}, localMonth: localMonthsNamePashto, apFmt: apFmtPashto, weekdayNames: weekdayNamesPashto, weekdayNamesAbbr: weekdayNamesPashto}, + "29": {tags: []string{"fa"}, localMonth: localMonthsNamePersian, apFmt: apFmtPersian, weekdayNames: weekdayNamesPersian, weekdayNamesAbbr: weekdayNamesPersian}, + "429": {tags: []string{"fa-IR"}, localMonth: localMonthsNamePersian, apFmt: apFmtPersian, weekdayNames: weekdayNamesPersian, weekdayNamesAbbr: weekdayNamesPersian}, + "15": {tags: []string{"pl"}, localMonth: localMonthsNamePolish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesPolish, weekdayNamesAbbr: weekdayNamesPolishAbbr}, + "415": {tags: []string{"pl-PL"}, localMonth: localMonthsNamePolish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesPolish, weekdayNamesAbbr: weekdayNamesPolishAbbr}, + "16": {tags: []string{"pt"}, localMonth: localMonthsNamePortuguese, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesPortuguese, weekdayNamesAbbr: weekdayNamesPortugueseAbbr}, + "416": {tags: []string{"pt-BR"}, localMonth: localMonthsNamePortuguese, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesPortuguese, weekdayNamesAbbr: weekdayNamesPortugueseAbbr}, + "816": {tags: []string{"pt-PT"}, localMonth: localMonthsNamePortuguese, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesPortuguese, weekdayNamesAbbr: weekdayNamesPortugueseAbbr}, + "46": {tags: []string{"pa"}, localMonth: localMonthsNamePunjabi, apFmt: apFmtPunjabi, weekdayNames: weekdayNamesPunjabi, weekdayNamesAbbr: weekdayNamesPunjabiAbbr}, + "7C46": {tags: []string{"pa-Arab"}, localMonth: localMonthsNamePunjabiArab, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesPunjabiArab, weekdayNamesAbbr: weekdayNamesPunjabiArab}, + "446": {tags: []string{"pa-IN"}, localMonth: localMonthsNamePunjabi, apFmt: apFmtPunjabi, weekdayNames: weekdayNamesPunjabi, weekdayNamesAbbr: weekdayNamesPunjabiAbbr}, + "846": {tags: []string{"pa-Arab-PK"}, localMonth: localMonthsNamePunjabiArab, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesPunjabiArab, weekdayNamesAbbr: weekdayNamesPunjabiArab}, + "6B": {tags: []string{"quz"}, localMonth: localMonthsNameQuechua, apFmt: apFmtCuba, weekdayNames: weekdayNamesQuechua, weekdayNamesAbbr: weekdayNamesQuechuaAbbr}, + "46B": {tags: []string{"quz-BO"}, localMonth: localMonthsNameQuechua, apFmt: apFmtCuba, weekdayNames: weekdayNamesQuechua, weekdayNamesAbbr: weekdayNamesQuechuaAbbr}, + "86B": {tags: []string{"quz-EC"}, localMonth: localMonthsNameQuechuaEcuador, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesQuechuaEcuador, weekdayNamesAbbr: weekdayNamesQuechuaEcuadorAbbr}, + "C6B": {tags: []string{"quz-PE"}, localMonth: localMonthsNameQuechua, apFmt: apFmtCuba, weekdayNames: weekdayNamesQuechuaPeru, weekdayNamesAbbr: weekdayNamesQuechuaPeruAbbr}, + "18": {tags: []string{"ro"}, localMonth: localMonthsNameRomanian, apFmt: apFmtCuba, weekdayNames: weekdayNamesRomanian, weekdayNamesAbbr: weekdayNamesRomanianAbbr}, + "818": {tags: []string{"ro-MD"}, localMonth: localMonthsNameRomanian, apFmt: apFmtCuba, weekdayNames: weekdayNamesRomanian, weekdayNamesAbbr: weekdayNamesRomanianMoldovaAbbr}, + "418": {tags: []string{"ro-RO"}, localMonth: localMonthsNameRomanian, apFmt: apFmtCuba, weekdayNames: weekdayNamesRomanian, weekdayNamesAbbr: weekdayNamesRomanianAbbr}, + "17": {tags: []string{"rm"}, localMonth: localMonthsNameRomansh, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesRomansh, weekdayNamesAbbr: weekdayNamesRomanshAbbr}, + "417": {tags: []string{"rm-CH"}, localMonth: localMonthsNameRomansh, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesRomansh, weekdayNamesAbbr: weekdayNamesRomanshAbbr}, + "19": {tags: []string{"ru"}, localMonth: localMonthsNameRussian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesRussian, weekdayNamesAbbr: weekdayNamesRussianAbbr}, + "819": {tags: []string{"ru-MD"}, localMonth: localMonthsNameRussian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesRussian, weekdayNamesAbbr: weekdayNamesRussianAbbr}, + "419": {tags: []string{"ru-RU"}, localMonth: localMonthsNameRussian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesRussian, weekdayNamesAbbr: weekdayNamesRussianAbbr}, + "85": {tags: []string{"sah"}, localMonth: localMonthsNameSakha, apFmt: apFmtSakha, weekdayNames: weekdayNamesSakha, weekdayNamesAbbr: weekdayNamesSakhaAbbr}, + "485": {tags: []string{"sah-RU"}, localMonth: localMonthsNameSakha, apFmt: apFmtSakha, weekdayNames: weekdayNamesSakha, weekdayNamesAbbr: weekdayNamesSakhaAbbr}, + "703B": {tags: []string{"smn"}, localMonth: localMonthsNameSami, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSami, weekdayNamesAbbr: weekdayNamesSamiAbbr}, + "243B": {tags: []string{"smn-FI"}, localMonth: localMonthsNameSami, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSami, weekdayNamesAbbr: weekdayNamesSamiAbbr}, + "7C3B": {tags: []string{"smj"}, localMonth: localMonthsNameSamiLule, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiSamiLule, weekdayNamesAbbr: weekdayNamesSamiSwedenAbbr}, + "103B": {tags: []string{"smj-NO"}, localMonth: localMonthsNameSamiLule, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiSamiLule, weekdayNamesAbbr: weekdayNamesSamiSamiLuleAbbr}, + "143B": {tags: []string{"smj-SE"}, localMonth: localMonthsNameSamiLule, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiSweden, weekdayNamesAbbr: weekdayNamesSamiSwedenAbbr}, + "3B": {tags: []string{"se"}, localMonth: localMonthsNameSamiNorthern, apFmt: apFmtSamiNorthern, weekdayNames: weekdayNamesSamiNorthern, weekdayNamesAbbr: weekdayNamesSamiNorthernAbbr}, + "C3B": {tags: []string{"se-FI"}, localMonth: localMonthsNameSamiNorthernFI, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiNorthernFI, weekdayNamesAbbr: weekdayNamesSamiNorthernFIAbbr}, + "43B": {tags: []string{"se-NO"}, localMonth: localMonthsNameSamiNorthern, apFmt: apFmtSamiNorthern, weekdayNames: weekdayNamesSamiNorthern, weekdayNamesAbbr: weekdayNamesSamiNorthernAbbr}, + "83B": {tags: []string{"se-SE"}, localMonth: localMonthsNameSamiNorthern, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiNorthernSE, weekdayNamesAbbr: weekdayNamesSamiNorthernSEAbbr}, + "743B": {tags: []string{"sms"}, localMonth: localMonthsNameSamiSkolt, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiSkolt, weekdayNamesAbbr: weekdayNamesSamiSkoltAbbr}, + "203B": {tags: []string{"sms-FI"}, localMonth: localMonthsNameSamiSkolt, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiSkolt, weekdayNamesAbbr: weekdayNamesSamiSkoltAbbr}, + "783B": {tags: []string{"sma"}, localMonth: localMonthsNameSamiSouthern, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiSouthern, weekdayNamesAbbr: weekdayNamesSamiSouthernAbbr}, + "183B": {tags: []string{"sma-NO"}, localMonth: localMonthsNameSamiSouthern, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiSouthern, weekdayNamesAbbr: weekdayNamesSamiSouthernAbbr}, + "1C3B": {tags: []string{"sma-SE"}, localMonth: localMonthsNameSamiSouthern, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiSouthern, weekdayNamesAbbr: weekdayNamesSamiSouthernAbbr}, + "4F": {tags: []string{"sa"}, localMonth: localMonthsNameSanskrit, apFmt: apFmtSanskrit, weekdayNames: weekdayNamesSanskrit, weekdayNamesAbbr: weekdayNamesSanskritAbbr}, + "44F": {tags: []string{"sa-IN"}, localMonth: localMonthsNameSanskrit, apFmt: apFmtSanskrit, weekdayNames: weekdayNamesSanskrit, weekdayNamesAbbr: weekdayNamesSanskritAbbr}, + "91": {tags: []string{"gd"}, localMonth: localMonthsNameScottishGaelic, apFmt: apFmtScottishGaelic, weekdayNames: weekdayNamesGaelic, weekdayNamesAbbr: weekdayNamesGaelicAbbr}, + "491": {tags: []string{"gd-GB"}, localMonth: localMonthsNameScottishGaelic, apFmt: apFmtScottishGaelic, weekdayNames: weekdayNamesGaelic, weekdayNamesAbbr: weekdayNamesGaelicAbbr}, + "6C1A": {tags: []string{"sr-Cyrl"}, localMonth: localMonthsNameSerbian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSerbian, weekdayNamesAbbr: weekdayNamesSerbianAbbr}, + "1C1A": {tags: []string{"sr-Cyrl-BA"}, localMonth: localMonthsNameSerbianBA, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSerbianBA, weekdayNamesAbbr: weekdayNamesSerbianBAAbbr}, + "301A": {tags: []string{"sr-Cyrl-ME"}, localMonth: localMonthsNameSerbian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSerbianME, weekdayNamesAbbr: weekdayNamesSerbianBAAbbr}, + "281A": {tags: []string{"sr-Cyrl-RS"}, localMonth: localMonthsNameSerbian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSerbian, weekdayNamesAbbr: weekdayNamesSerbianAbbr}, + "C1A": {tags: []string{"sr-Cyrl-CS"}, localMonth: localMonthsNameSerbian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSerbian, weekdayNamesAbbr: weekdayNamesSerbianAbbr}, + "701A": {tags: []string{"sr-Latn"}, localMonth: localMonthsNameSerbianLatin, apFmt: apFmtSerbianLatin, weekdayNames: weekdayNamesSerbianLatin, weekdayNamesAbbr: weekdayNamesSerbianLatinAbbr}, + "7C1A": {tags: []string{"sr"}, localMonth: localMonthsNameSerbianLatin, apFmt: apFmtSerbianLatin, weekdayNames: weekdayNamesSerbianLatin, weekdayNamesAbbr: weekdayNamesSerbianLatinAbbr}, + "181A": {tags: []string{"sr-Latn-BA"}, localMonth: localMonthsNameSerbianLatin, apFmt: apFmtSerbianLatinBA, weekdayNames: weekdayNamesSerbianLatinBA, weekdayNamesAbbr: weekdayNamesSerbianLatinBAAbbr}, + "2C1A": {tags: []string{"sr-Latn-ME"}, localMonth: localMonthsNameSerbianLatin, apFmt: apFmtSerbianLatinBA, weekdayNames: weekdayNamesSerbianLatinME, weekdayNamesAbbr: weekdayNamesSerbianLatinAbbr}, + "241A": {tags: []string{"sr-Latn-RS"}, localMonth: localMonthsNameSerbianLatin, apFmt: apFmtSerbianLatin, weekdayNames: weekdayNamesSerbianLatin, weekdayNamesAbbr: weekdayNamesSerbianLatinAbbr}, + "81A": {tags: []string{"sr-Latn-CS"}, localMonth: localMonthsNameSerbianLatinCS, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSerbianLatin, weekdayNamesAbbr: weekdayNamesSerbianLatinCSAbbr}, + "6C": {tags: []string{"nso"}, localMonth: localMonthsNameSesothoSaLeboa, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSesothoSaLeboa, weekdayNamesAbbr: weekdayNamesSesothoSaLeboaAbbr}, + "46C": {tags: []string{"nso-ZA"}, localMonth: localMonthsNameSesothoSaLeboa, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSesothoSaLeboa, weekdayNamesAbbr: weekdayNamesSesothoSaLeboaAbbr}, + "32": {tags: []string{"tn"}, localMonth: localMonthsNameSetswana, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSetswana, weekdayNamesAbbr: weekdayNamesSetswanaAbbr}, + "832": {tags: []string{"tn-BW"}, localMonth: localMonthsNameSetswana, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSetswana, weekdayNamesAbbr: weekdayNamesSetswanaAbbr}, + "432": {tags: []string{"tn-ZA"}, localMonth: localMonthsNameSetswana, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSetswana, weekdayNamesAbbr: weekdayNamesSetswanaAbbr}, + "59": {tags: []string{"sd"}, localMonth: localMonthsNameSindhi, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSindhi, weekdayNamesAbbr: weekdayNamesSindhiAbbr}, + "7C59": {tags: []string{"sd-Arab"}, localMonth: localMonthsNameSindhi, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSindhi, weekdayNamesAbbr: weekdayNamesSindhiAbbr}, + "859": {tags: []string{"sd-Arab-PK"}, localMonth: localMonthsNameSindhi, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSindhi, weekdayNamesAbbr: weekdayNamesSindhiAbbr}, + "5B": {tags: []string{"si"}, localMonth: localMonthsNameSinhala, apFmt: apFmtSinhala, weekdayNames: weekdayNamesSindhi, weekdayNamesAbbr: weekdayNamesSindhiAbbr}, + "45B": {tags: []string{"si-LK"}, localMonth: localMonthsNameSinhala, apFmt: apFmtSinhala, weekdayNames: weekdayNamesSindhi, weekdayNamesAbbr: weekdayNamesSindhiAbbr}, + "1B": {tags: []string{"sk"}, localMonth: localMonthsNameSlovak, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSlovak, weekdayNamesAbbr: weekdayNamesSlovakAbbr}, + "41B": {tags: []string{"sk-SK"}, localMonth: localMonthsNameSlovak, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSlovak, weekdayNamesAbbr: weekdayNamesSlovakAbbr}, + "24": {tags: []string{"sl"}, localMonth: localMonthsNameSlovenian, apFmt: apFmtSlovenian, weekdayNames: weekdayNamesSlovenian, weekdayNamesAbbr: weekdayNamesSlovenianAbbr}, + "424": {tags: []string{"sl-SI"}, localMonth: localMonthsNameSlovenian, apFmt: apFmtSlovenian, weekdayNames: weekdayNamesSlovenian, weekdayNamesAbbr: weekdayNamesSlovenianAbbr}, + "77": {tags: []string{"so"}, localMonth: localMonthsNameSomali, apFmt: apFmtSomali, weekdayNames: weekdayNamesSomali, weekdayNamesAbbr: weekdayNamesSomaliAbbr}, + "477": {tags: []string{"so-SO"}, localMonth: localMonthsNameSomali, apFmt: apFmtSomali, weekdayNames: weekdayNamesSomali, weekdayNamesAbbr: weekdayNamesSomaliAbbr}, + "30": {tags: []string{"st"}, localMonth: localMonthsNameSotho, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSotho, weekdayNamesAbbr: weekdayNamesSothoAbbr}, + "430": {tags: []string{"st-ZA"}, localMonth: localMonthsNameSotho, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSotho, weekdayNamesAbbr: weekdayNamesSothoAbbr}, + "A": {tags: []string{"es"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishAbbr}, + "2C0A": {tags: []string{"es-AR"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + "200A": {tags: []string{"es-VE"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + "400A": {tags: []string{"es-BO"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + "340A": {tags: []string{"es-CL"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + "240A": {tags: []string{"es-CO"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + "140A": {tags: []string{"es-CR"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + "5C0A": {tags: []string{"es-CU"}, localMonth: localMonthsNameSpanish, apFmt: apFmtCuba, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + "1C0A": {tags: []string{"es-DO"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + "300A": {tags: []string{"es-EC"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + "440A": {tags: []string{"es-SV"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + "100A": {tags: []string{"es-GT"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + "480A": {tags: []string{"es-HN"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + "580A": {tags: []string{"es-419"}, localMonth: localMonthsNameSpanish, apFmt: apFmtCuba, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + "80A": {tags: []string{"es-MX"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + "4C0A": {tags: []string{"es-NI"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + "180A": {tags: []string{"es-PA"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + "3C0A": {tags: []string{"es-PY"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + "280A": {tags: []string{"es-PE"}, localMonth: localMonthsNameSpanishPE, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + "500A": {tags: []string{"es-PR"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + "40A": {tags: []string{"es-ES_tradnl"}, localMonth: localMonthsNameSpanish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishAbbr}, + "C0A": {tags: []string{"es-ES"}, localMonth: localMonthsNameSpanish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishAbbr}, + "540A": {tags: []string{"es-US"}, localMonth: localMonthsNameSpanish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishUSAbbr}, + "380A": {tags: []string{"es-UY"}, localMonth: localMonthsNameSpanishPE, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + "1D": {tags: []string{"sv"}, localMonth: localMonthsNameSwedish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSwedish, weekdayNamesAbbr: weekdayNamesSwedishAbbr}, + "81D": {tags: []string{"sv-FI"}, localMonth: localMonthsNameSwedishFI, apFmt: apFmtSwedish, weekdayNames: weekdayNamesSwedish, weekdayNamesAbbr: weekdayNamesSwedishAbbr}, + "41D": {tags: []string{"sv-SE"}, localMonth: localMonthsNameSwedishFI, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSwedish, weekdayNamesAbbr: weekdayNamesSwedishAbbr}, + "5A": {tags: []string{"syr"}, localMonth: localMonthsNameSyriac, apFmt: apFmtSyriac, weekdayNames: weekdayNamesSyriac, weekdayNamesAbbr: weekdayNamesSyriacAbbr}, + "45A": {tags: []string{"syr-SY"}, localMonth: localMonthsNameSyriac, apFmt: apFmtSyriac, weekdayNames: weekdayNamesSyriac, weekdayNamesAbbr: weekdayNamesSyriacAbbr}, + "28": {tags: []string{"tg"}, localMonth: localMonthsNameTajik, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTajik, weekdayNamesAbbr: weekdayNamesTajikAbbr}, + "7C28": {tags: []string{"tg-Cyrl"}, localMonth: localMonthsNameTajik, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTajik, weekdayNamesAbbr: weekdayNamesTajikAbbr}, + "428": {tags: []string{"tg-Cyrl-TJ"}, localMonth: localMonthsNameTajik, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTajik, weekdayNamesAbbr: weekdayNamesTajikAbbr}, + "5F": {tags: []string{"tzm"}, localMonth: localMonthsNameTamazight, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTamazight, weekdayNamesAbbr: weekdayNamesTamazightAbbr}, + "7C5F": {tags: []string{"tzm-Latn"}, localMonth: localMonthsNameTamazight, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTamazight, weekdayNamesAbbr: weekdayNamesTamazightAbbr}, + "85F": {tags: []string{"tzm-Latn-DZ"}, localMonth: localMonthsNameTamazight, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTamazight, weekdayNamesAbbr: weekdayNamesTamazightAbbr}, + "49": {tags: []string{"ta"}, localMonth: localMonthsNameTamil, apFmt: apFmtTamil, weekdayNames: weekdayNamesTamil, weekdayNamesAbbr: weekdayNamesTamilAbbr}, + "449": {tags: []string{"ta-IN"}, localMonth: localMonthsNameTamil, apFmt: apFmtTamil, weekdayNames: weekdayNamesTamil, weekdayNamesAbbr: weekdayNamesTamilAbbr}, + "849": {tags: []string{"ta-LK"}, localMonth: localMonthsNameTamilLK, apFmt: apFmtTamil, weekdayNames: weekdayNamesTamilLK, weekdayNamesAbbr: weekdayNamesTamilLKAbbr}, + "44": {tags: []string{"tt"}, localMonth: localMonthsNameTatar, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTatar, weekdayNamesAbbr: weekdayNamesTatarAbbr}, + "444": {tags: []string{"tt-RU"}, localMonth: localMonthsNameTatar, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTatar, weekdayNamesAbbr: weekdayNamesTatarAbbr}, + "4A": {tags: []string{"te"}, localMonth: localMonthsNameTelugu, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTelugu, weekdayNamesAbbr: weekdayNamesTeluguAbbr}, + "44A": {tags: []string{"te-IN"}, localMonth: localMonthsNameTelugu, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTelugu, weekdayNamesAbbr: weekdayNamesTeluguAbbr}, + "1E": {tags: []string{"th"}, localMonth: localMonthsNameThai, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesThai, weekdayNamesAbbr: weekdayNamesThaiAbbr}, + "41E": {tags: []string{"th-TH"}, localMonth: localMonthsNameThai, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesThai, weekdayNamesAbbr: weekdayNamesThaiAbbr}, + "51": {tags: []string{"bo"}, localMonth: localMonthsNameTibetan, apFmt: apFmtTibetan, weekdayNames: weekdayNamesTibetan, weekdayNamesAbbr: weekdayNamesTibetanAbbr}, + "451": {tags: []string{"bo-CN"}, localMonth: localMonthsNameTibetan, apFmt: apFmtTibetan, weekdayNames: weekdayNamesTibetan, weekdayNamesAbbr: weekdayNamesTibetanAbbr}, + "73": {tags: []string{"ti"}, localMonth: localMonthsNameTigrinya, apFmt: apFmtTigrinya, weekdayNames: weekdayNamesTigrinya, weekdayNamesAbbr: weekdayNamesTigrinyaAbbr}, + "873": {tags: []string{"ti-ER"}, localMonth: localMonthsNameTigrinya, apFmt: apFmtTigrinyaER, weekdayNames: weekdayNamesTigrinya, weekdayNamesAbbr: weekdayNamesTigrinyaAbbr}, + "473": {tags: []string{"ti-ET"}, localMonth: localMonthsNameTigrinya, apFmt: apFmtTigrinya, weekdayNames: weekdayNamesTigrinya, weekdayNamesAbbr: weekdayNamesTigrinyaAbbr}, + "31": {tags: []string{"ts"}, localMonth: localMonthsNameTsonga, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTsonga, weekdayNamesAbbr: weekdayNamesTsongaAbbr}, + "431": {tags: []string{"ts-ZA"}, localMonth: localMonthsNameTsonga, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTsonga, weekdayNamesAbbr: weekdayNamesTsongaAbbr}, + "1F": {tags: []string{"tr"}, localMonth: localMonthsNameTurkish, apFmt: apFmtTurkish, weekdayNames: weekdayNamesTurkish, weekdayNamesAbbr: weekdayNamesTurkishAbbr}, + "41F": {tags: []string{"tr-TR"}, localMonth: localMonthsNameTurkish, apFmt: apFmtTurkish, weekdayNames: weekdayNamesTurkish, weekdayNamesAbbr: weekdayNamesTurkishAbbr}, + "42": {tags: []string{"tk"}, localMonth: localMonthsNameTurkmen, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTurkmen, weekdayNamesAbbr: weekdayNamesTurkmenAbbr}, + "442": {tags: []string{"tk-TM"}, localMonth: localMonthsNameTurkmen, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTurkmen, weekdayNamesAbbr: weekdayNamesTurkmenAbbr}, + "22": {tags: []string{"uk"}, localMonth: localMonthsNameUkrainian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesUkrainian, weekdayNamesAbbr: weekdayNamesUkrainianAbbr}, + "422": {tags: []string{"uk-UA"}, localMonth: localMonthsNameUkrainian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesUkrainian, weekdayNamesAbbr: weekdayNamesUkrainianAbbr}, + "2E": {tags: []string{"hsb"}, localMonth: localMonthsNameUpperSorbian, apFmt: apFmtUpperSorbian, weekdayNames: weekdayNamesSorbian, weekdayNamesAbbr: weekdayNamesSorbianAbbr}, + "42E": {tags: []string{"hsb-DE"}, localMonth: localMonthsNameUpperSorbian, apFmt: apFmtUpperSorbian, weekdayNames: weekdayNamesSorbian, weekdayNamesAbbr: weekdayNamesSorbianAbbr}, + "20": {tags: []string{"ur"}, localMonth: localMonthsNamePunjabiArab, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesUrdu, weekdayNamesAbbr: weekdayNamesUrdu}, + "820": {tags: []string{"ur-IN"}, localMonth: localMonthsNamePunjabiArab, apFmt: apFmtUrdu, weekdayNames: weekdayNamesUrduIN, weekdayNamesAbbr: weekdayNamesUrduIN}, + "420": {tags: []string{"ur-PK"}, localMonth: localMonthsNamePunjabiArab, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesUrdu, weekdayNamesAbbr: weekdayNamesUrdu}, + "80": {tags: []string{"ug"}, localMonth: localMonthsNameUyghur, apFmt: apFmtUyghur, weekdayNames: weekdayNamesUyghur, weekdayNamesAbbr: weekdayNamesUyghurAbbr}, + "480": {tags: []string{"ug-CN"}, localMonth: localMonthsNameUyghur, apFmt: apFmtUyghur, weekdayNames: weekdayNamesUyghur, weekdayNamesAbbr: weekdayNamesUyghurAbbr}, + "7843": {tags: []string{"uz-Cyrl"}, localMonth: localMonthsNameUzbekCyrillic, apFmt: apFmtUzbekCyrillic, weekdayNames: weekdayNamesUzbekCyrillic, weekdayNamesAbbr: weekdayNamesUzbekCyrillicAbbr}, + "843": {tags: []string{"uz-Cyrl-UZ"}, localMonth: localMonthsNameUzbekCyrillic, apFmt: apFmtUzbekCyrillic, weekdayNames: weekdayNamesUzbekCyrillic, weekdayNamesAbbr: weekdayNamesUzbekCyrillicAbbr}, + "43": {tags: []string{"uz"}, localMonth: localMonthsNameUzbek, apFmt: apFmtUzbek, weekdayNames: weekdayNamesUzbek, weekdayNamesAbbr: weekdayNamesUzbekAbbr}, + "7C43": {tags: []string{"uz-Latn"}, localMonth: localMonthsNameUzbek, apFmt: apFmtUzbek, weekdayNames: weekdayNamesUzbek, weekdayNamesAbbr: weekdayNamesUzbekAbbr}, + "443": {tags: []string{"uz-Latn-UZ"}, localMonth: localMonthsNameUzbek, apFmt: apFmtUzbek, weekdayNames: weekdayNamesUzbek, weekdayNamesAbbr: weekdayNamesUzbekAbbr}, + "803": {tags: []string{"ca-ES-valencia"}, localMonth: localMonthsNameValencian, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesValencian, weekdayNamesAbbr: weekdayNamesValencianAbbr}, + "33": {tags: []string{"ve"}, localMonth: localMonthsNameVenda, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesVenda, weekdayNamesAbbr: weekdayNamesVendaAbbr}, + "433": {tags: []string{"ve-ZA"}, localMonth: localMonthsNameVenda, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesVenda, weekdayNamesAbbr: weekdayNamesVendaAbbr}, + "2A": {tags: []string{"vi"}, localMonth: localMonthsNameVietnamese, apFmt: apFmtVietnamese, weekdayNames: weekdayNamesVietnamese, weekdayNamesAbbr: weekdayNamesVietnameseAbbr}, + "42A": {tags: []string{"vi-VN"}, localMonth: localMonthsNameVietnamese, apFmt: apFmtVietnamese, weekdayNames: weekdayNamesVietnamese, weekdayNamesAbbr: weekdayNamesVietnameseAbbr}, + "52": {tags: []string{"cy"}, localMonth: localMonthsNameWelsh, apFmt: apFmtWelsh, weekdayNames: weekdayNamesWelsh, weekdayNamesAbbr: weekdayNamesWelshAbbr}, + "452": {tags: []string{"cy-GB"}, localMonth: localMonthsNameWelsh, apFmt: apFmtWelsh, weekdayNames: weekdayNamesWelsh, weekdayNamesAbbr: weekdayNamesWelshAbbr}, + "88": {tags: []string{"wo"}, localMonth: localMonthsNameWolof, apFmt: apFmtWolof, weekdayNames: weekdayNamesWolof, weekdayNamesAbbr: weekdayNamesWolofAbbr}, + "488": {tags: []string{"wo-SN"}, localMonth: localMonthsNameWolof, apFmt: apFmtWolof, weekdayNames: weekdayNamesWolof, weekdayNamesAbbr: weekdayNamesWolofAbbr}, + "34": {tags: []string{"xh"}, localMonth: localMonthsNameXhosa, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesXhosa, weekdayNamesAbbr: weekdayNamesXhosaAbbr}, + "434": {tags: []string{"xh-ZA"}, localMonth: localMonthsNameXhosa, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesXhosa, weekdayNamesAbbr: weekdayNamesXhosaAbbr}, + "78": {tags: []string{"ii"}, localMonth: localMonthsNameYi, apFmt: apFmtYi, weekdayNames: weekdayNamesYi, weekdayNamesAbbr: weekdayNamesYiAbbr}, + "478": {tags: []string{"ii-CN"}, localMonth: localMonthsNameYi, apFmt: apFmtYi, weekdayNames: weekdayNamesYi, weekdayNamesAbbr: weekdayNamesYiAbbr}, + "43D": {tags: []string{"yi-001"}, localMonth: localMonthsNameYiddish, apFmt: apFmtYiddish, weekdayNames: weekdayNamesYiddish, weekdayNamesAbbr: weekdayNamesYiddishAbbr}, + "6A": {tags: []string{"yo"}, localMonth: localMonthsNameYoruba, apFmt: apFmtYoruba, weekdayNames: weekdayNamesYoruba, weekdayNamesAbbr: weekdayNamesYorubaAbbr}, + "46A": {tags: []string{"yo-NG"}, localMonth: localMonthsNameYoruba, apFmt: apFmtYoruba, weekdayNames: weekdayNamesYoruba, weekdayNamesAbbr: weekdayNamesYorubaAbbr}, + "35": {tags: []string{"zu"}, localMonth: localMonthsNameZulu, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesZulu, weekdayNamesAbbr: weekdayNamesZuluAbbr}, + "435": {tags: []string{"zu-ZA"}, localMonth: localMonthsNameZulu, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesZulu, weekdayNamesAbbr: weekdayNamesZuluAbbr}, } // japaneseEraYears list the Japanese era name periods. japaneseEraYears = []time.Time{ @@ -4939,9 +4940,7 @@ func (nf *numberFormat) dateTimeHandler() string { if changeNumFmtCode, err := nf.currencyLanguageHandler(token); err != nil || changeNumFmtCode { return nf.value } - if !strings.EqualFold(nf.localCode, "JP-X-GANNEN") && !strings.EqualFold(nf.localCode, "JP-X-GANNEN,80") { - nf.result += nf.currencyString - } + nf.result += nf.currencyString } if token.TType == nfp.TokenTypeDateTimes { nf.dateTimesHandler(i, token) diff --git a/numfmt_test.go b/numfmt_test.go index 106d82c61b..02a73af9ec 100644 --- a/numfmt_test.go +++ b/numfmt_test.go @@ -10,6 +10,7 @@ import ( func TestNumFmt(t *testing.T) { for _, item := range [][]string{ {"123", "general", "123"}, + {"-123", ";general", "-123"}, {"43528", "y", "19"}, {"43528", "Y", "19"}, {"43528", "yy", "19"}, From cb5a8e2d1ed8d0707a411deab4437815ab22750b Mon Sep 17 00:00:00 2001 From: fsfsx <19376131@buaa.edu.cn> Date: Wed, 23 Aug 2023 10:51:11 +0800 Subject: [PATCH 781/957] This closes #674, closes #1454, add new exported functions GetTables and DeleteTable (#1573) --- errors.go | 6 ++++ table.go | 85 +++++++++++++++++++++++++++++++++++++++++++++++++++ table_test.go | 46 ++++++++++++++++++++++++++++ xmlTable.go | 1 + 4 files changed, 138 insertions(+) diff --git a/errors.go b/errors.go index 254890efa8..a5d8814f53 100644 --- a/errors.go +++ b/errors.go @@ -82,6 +82,12 @@ func newNoExistSheetError(name string) error { return fmt.Errorf("sheet %s does not exist", name) } +// newNoExistTableError defined the error message on receiving the non existing +// table name. +func newNoExistTableError(name string) error { + return fmt.Errorf("table %s does not exist", name) +} + // newNotWorksheetError defined the error message on receiving a sheet which // not a worksheet. func newNotWorksheetError(name string) error { diff --git a/table.go b/table.go index d59656daec..6aa1552edd 100644 --- a/table.go +++ b/table.go @@ -125,6 +125,91 @@ func (f *File) AddTable(sheet string, table *Table) error { return f.addContentTypePart(tableID, "table") } +// GetTables provides the method to get all tables in a worksheet by given +// worksheet name. +func (f *File) GetTables(sheet string) ([]Table, error) { + var tables []Table + ws, err := f.workSheetReader(sheet) + if err != nil { + return tables, err + } + if ws.TableParts == nil { + return tables, err + } + for _, tbl := range ws.TableParts.TableParts { + if tbl != nil { + target := f.getSheetRelationshipsTargetByID(sheet, tbl.RID) + tableXML := strings.ReplaceAll(target, "..", "xl") + content, ok := f.Pkg.Load(tableXML) + if !ok { + continue + } + var t xlsxTable + if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(content.([]byte)))). + Decode(&t); err != nil && err != io.EOF { + return tables, err + } + table := Table{ + rID: tbl.RID, + Range: t.Ref, + Name: t.Name, + } + if t.TableStyleInfo != nil { + table.StyleName = t.TableStyleInfo.Name + table.ShowColumnStripes = t.TableStyleInfo.ShowColumnStripes + table.ShowFirstColumn = t.TableStyleInfo.ShowFirstColumn + table.ShowLastColumn = t.TableStyleInfo.ShowLastColumn + table.ShowRowStripes = &t.TableStyleInfo.ShowRowStripes + } + tables = append(tables, table) + } + } + return tables, err +} + +// DeleteTable provides the method to delete table by given table name. +func (f *File) DeleteTable(name string) error { + if err := checkDefinedName(name); err != nil { + return err + } + for _, sheet := range f.GetSheetList() { + tables, err := f.GetTables(sheet) + if err != nil { + return err + } + for _, table := range tables { + if table.Name != name { + continue + } + ws, _ := f.workSheetReader(sheet) + for i, tbl := range ws.TableParts.TableParts { + if tbl.RID == table.rID { + ws.TableParts.TableParts = append(ws.TableParts.TableParts[:i], ws.TableParts.TableParts[i+1:]...) + f.deleteSheetRelationships(sheet, tbl.RID) + break + } + } + if ws.TableParts.Count = len(ws.TableParts.TableParts); ws.TableParts.Count == 0 { + ws.TableParts = nil + } + // Delete cell value in the table header + coordinates, err := rangeRefToCoordinates(table.Range) + if err != nil { + return err + } + _ = sortCoordinates(coordinates) + for col := coordinates[0]; col <= coordinates[2]; col++ { + for row := coordinates[1]; row < coordinates[1]+1; row++ { + cell, _ := CoordinatesToCellName(col, row) + err = f.SetCellValue(sheet, cell, nil) + } + } + return err + } + } + return newNoExistTableError(name) +} + // countTables provides a function to get table files count storage in the // folder xl/tables. func (f *File) countTables() int { diff --git a/table_test.go b/table_test.go index e6a67fb9a9..da4426586b 100644 --- a/table_test.go +++ b/table_test.go @@ -27,6 +27,10 @@ func TestAddTable(t *testing.T) { ShowHeaderRow: boolPtr(false), })) assert.NoError(t, f.AddTable("Sheet2", &Table{Range: "F1:F1", StyleName: "TableStyleMedium8"})) + // Test get tables in worksheet + tables, err := f.GetTables("Sheet2") + assert.Len(t, tables, 3) + assert.NoError(t, err) // Test add table with already exist table name assert.Equal(t, f.AddTable("Sheet2", &Table{Name: "Table1"}), ErrExistsTableName) @@ -74,6 +78,48 @@ func TestAddTable(t *testing.T) { assert.NoError(t, f.AddTable("Sheet1", &Table{Range: "A1:B2"})) } +func TestGetTables(t *testing.T) { + f := NewFile() + // Test get tables in none table worksheet + tables, err := f.GetTables("Sheet1") + assert.Len(t, tables, 0) + assert.NoError(t, err) + // Test get tables in not exist worksheet + _, err = f.GetTables("SheetN") + assert.EqualError(t, err, "sheet SheetN does not exist") + // Test adjust table with unsupported charset + assert.NoError(t, f.AddTable("Sheet1", &Table{Range: "B26:A21"})) + f.Pkg.Store("xl/tables/table1.xml", MacintoshCyrillicCharset) + _, err = f.GetTables("Sheet1") + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") + // Test adjust table with no exist table parts + f.Pkg.Delete("xl/tables/table1.xml") + tables, err = f.GetTables("Sheet1") + assert.Len(t, tables, 0) + assert.NoError(t, err) +} + +func TestDeleteTable(t *testing.T) { + f := NewFile() + assert.NoError(t, f.AddTable("Sheet1", &Table{Range: "A1:B4", Name: "Table1"})) + assert.NoError(t, f.AddTable("Sheet1", &Table{Range: "B26:A21", Name: "Table2"})) + assert.NoError(t, f.DeleteTable("Table2")) + assert.NoError(t, f.DeleteTable("Table1")) + // Test delete table with invalid table name + assert.EqualError(t, f.DeleteTable("Table 1"), newInvalidNameError("Table 1").Error()) + // Test delete table with no exist table name + assert.EqualError(t, f.DeleteTable("Table"), newNoExistTableError("Table").Error()) + // Test delete table with unsupported charset + f.Sheet.Delete("xl/worksheets/sheet1.xml") + f.Pkg.Store("xl/worksheets/sheet1.xml", MacintoshCyrillicCharset) + assert.EqualError(t, f.DeleteTable("Table1"), "XML syntax error on line 1: invalid UTF-8") + // Test delete table with invalid table range + f = NewFile() + assert.NoError(t, f.AddTable("Sheet1", &Table{Range: "A1:B4", Name: "Table1"})) + f.Pkg.Store("xl/tables/table1.xml", []byte("
")) + assert.EqualError(t, f.DeleteTable("Table1"), ErrParameterInvalid.Error()) +} + func TestSetTableHeader(t *testing.T) { f := NewFile() _, err := f.setTableHeader("Sheet1", true, 1, 0, 1) diff --git a/xmlTable.go b/xmlTable.go index 789d4a2873..00fa6748c9 100644 --- a/xmlTable.go +++ b/xmlTable.go @@ -198,6 +198,7 @@ type xlsxTableStyleInfo struct { // Table directly maps the format settings of the table. type Table struct { + rID string Range string Name string StyleName string From db224523987ea14f10fbe638e10f375f6741631c Mon Sep 17 00:00:00 2001 From: cnmlgbgithub Date: Thu, 24 Aug 2023 23:51:07 +0800 Subject: [PATCH 782/957] This closes #314, closes #1520 and closes #1521 (#1574) - Add new function GetStyle support for get style definition --- lib.go | 24 ++++ lib_test.go | 6 + numfmt.go | 2 +- styles.go | 305 ++++++++++++++++++++++++++++++++++++++++++++++++- styles_test.go | 102 +++++++++++++++++ xmlStyles.go | 11 +- 6 files changed, 446 insertions(+), 4 deletions(-) diff --git a/lib.go b/lib.go index 379880755b..25fdc3b16d 100644 --- a/lib.go +++ b/lib.go @@ -430,6 +430,30 @@ 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 } +// Value extracts string data type text from a attribute value. +func (attr *attrValString) Value() string { + if attr != nil && attr.Val != nil { + return *attr.Val + } + return "" +} + +// Value extracts boolean data type value from a attribute value. +func (attr *attrValBool) Value() bool { + if attr != nil && attr.Val != nil { + return *attr.Val + } + return false +} + +// Value extracts float64 data type numeric from a attribute value. +func (attr *attrValFloat) Value() float64 { + if attr != nil && attr.Val != nil { + return *attr.Val + } + return 0 +} + // 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 { diff --git a/lib_test.go b/lib_test.go index fe8d6a8ffb..b41f5f1c48 100644 --- a/lib_test.go +++ b/lib_test.go @@ -238,6 +238,12 @@ func TestInStrSlice(t *testing.T) { assert.EqualValues(t, -1, inStrSlice([]string{}, "", true)) } +func TestAttrValue(t *testing.T) { + assert.Empty(t, (&attrValString{}).Value()) + assert.False(t, (&attrValBool{}).Value()) + assert.Zero(t, (&attrValFloat{}).Value()) +} + func TestBoolValMarshal(t *testing.T) { bold := true node := &xlsxFont{B: &attrValBool{Val: &bold}} diff --git a/numfmt.go b/numfmt.go index 4037f827c2..b63e74a1c8 100644 --- a/numfmt.go +++ b/numfmt.go @@ -4692,7 +4692,7 @@ 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 isLangNumFmt(numFmtID) { if f.options.CultureInfo == CultureNameEnUS { return f.langNumFmtFuncEnUS(numFmtID), true } diff --git a/styles.go b/styles.go index c1587aea0e..05d49511e9 100644 --- a/styles.go +++ b/styles.go @@ -1032,6 +1032,303 @@ func (f *File) NewStyle(style *Style) (int, error) { return setCellXfs(s, fontID, numFmtID, fillID, borderID, applyAlignment, applyProtection, alignment, protection) } +var ( + // styleBorders list all types of the cell border style. + styleBorders = []string{ + "none", + "thin", + "medium", + "dashed", + "dotted", + "thick", + "double", + "hair", + "mediumDashed", + "dashDot", + "mediumDashDot", + "dashDotDot", + "mediumDashDotDot", + "slantDashDot", + } + // styleBorderTypes list all types of the cell border. + styleBorderTypes = []string{ + "left", "right", "top", "bottom", "diagonalUp", "diagonalDown", + } + // styleFillPatterns list all types of the cell fill style. + styleFillPatterns = []string{ + "none", + "solid", + "mediumGray", + "darkGray", + "lightGray", + "darkHorizontal", + "darkVertical", + "darkDown", + "darkUp", + "darkGrid", + "darkTrellis", + "lightHorizontal", + "lightVertical", + "lightDown", + "lightUp", + "lightGrid", + "lightTrellis", + "gray125", + "gray0625", + } + // styleFillVariants list all preset variants of the fill style. + styleFillVariants = []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}, + } +) + +// getThemeColor provides a function to convert theme color or index color to +// RGB color. +func (f *File) getThemeColor(clr *xlsxColor) string { + var RGB string + if clr == nil || f.Theme == nil { + return RGB + } + if clrScheme := f.Theme.ThemeElements.ClrScheme; clr.Theme != nil { + if val, ok := map[int]*string{ + 0: &clrScheme.Lt1.SysClr.LastClr, + 1: &clrScheme.Dk1.SysClr.LastClr, + 2: clrScheme.Lt2.SrgbClr.Val, + 3: clrScheme.Dk2.SrgbClr.Val, + 4: clrScheme.Accent1.SrgbClr.Val, + 5: clrScheme.Accent2.SrgbClr.Val, + 6: clrScheme.Accent3.SrgbClr.Val, + 7: clrScheme.Accent4.SrgbClr.Val, + 8: clrScheme.Accent5.SrgbClr.Val, + 9: clrScheme.Accent6.SrgbClr.Val, + }[*clr.Theme]; ok && val != nil { + return strings.TrimPrefix(ThemeColor(*val, clr.Tint), "FF") + } + } + if len(clr.RGB) == 6 { + return clr.RGB + } + if len(clr.RGB) == 8 { + return strings.TrimPrefix(clr.RGB, "FF") + } + if f.Styles.Colors != nil && clr.Indexed < len(f.Styles.Colors.IndexedColors.RgbColor) { + return strings.TrimPrefix(ThemeColor(strings.TrimPrefix(f.Styles.Colors.IndexedColors.RgbColor[clr.Indexed].RGB, "FF"), clr.Tint), "FF") + } + if clr.Indexed < len(IndexedColorMapping) { + return strings.TrimPrefix(ThemeColor(IndexedColorMapping[clr.Indexed], clr.Tint), "FF") + } + return RGB +} + +// extractBorders provides a function to extract borders styles settings by +// given border styles definition. +func (f *File) extractBorders(xf xlsxXf, s *xlsxStyleSheet, style *Style) { + if xf.ApplyBorder != nil && *xf.ApplyBorder && + xf.BorderID != nil && s.Borders != nil && + *xf.BorderID < len(s.Borders.Border) { + if bdr := s.Borders.Border[*xf.BorderID]; bdr != nil { + + var borders []Border + extractBorder := func(lineType string, line xlsxLine) { + if line.Style != "" { + borders = append(borders, Border{ + Type: lineType, + Color: f.getThemeColor(line.Color), + Style: inStrSlice(styleBorders, line.Style, false), + }) + } + } + for i, line := range []xlsxLine{ + bdr.Left, bdr.Right, bdr.Top, bdr.Bottom, bdr.Diagonal, bdr.Diagonal, + } { + if i < 4 { + extractBorder(styleBorderTypes[i], line) + } + if i == 4 && bdr.DiagonalUp { + extractBorder(styleBorderTypes[i], line) + } + if i == 5 && bdr.DiagonalDown { + extractBorder(styleBorderTypes[i], line) + } + } + style.Border = borders + } + } +} + +// extractFills provides a function to extract fill styles settings by +// given fill styles definition. +func (f *File) extractFills(xf xlsxXf, s *xlsxStyleSheet, style *Style) { + if fl := s.Fills.Fill[*xf.FillID]; fl != nil { + var fill Fill + if fl.GradientFill != nil { + fill.Type = "gradient" + for shading, variants := range styleFillVariants { + if fl.GradientFill.Bottom == variants.Bottom && + fl.GradientFill.Degree == variants.Degree && + fl.GradientFill.Left == variants.Left && + fl.GradientFill.Right == variants.Right && + fl.GradientFill.Top == variants.Top && + fl.GradientFill.Type == variants.Type { + fill.Shading = shading + break + } + } + for _, stop := range fl.GradientFill.Stop { + fill.Color = append(fill.Color, f.getThemeColor(&stop.Color)) + } + } + if fl.PatternFill != nil { + fill.Type = "pattern" + fill.Pattern = inStrSlice(styleFillPatterns, fl.PatternFill.PatternType, false) + if fl.PatternFill.FgColor != nil { + fill.Color = []string{f.getThemeColor(fl.PatternFill.FgColor)} + } + } + style.Fill = fill + } +} + +// extractFont provides a function to extract font styles settings by given +// font styles definition. +func (f *File) extractFont(xf xlsxXf, s *xlsxStyleSheet, style *Style) { + if xf.ApplyFont != nil && *xf.ApplyFont && + xf.FontID != nil && s.Fonts != nil && + *xf.FontID < len(s.Fonts.Font) { + if fnt := s.Fonts.Font[*xf.FontID]; fnt != nil { + var font Font + if fnt.B != nil { + font.Bold = fnt.B.Value() + } + if fnt.I != nil { + font.Italic = fnt.I.Value() + } + if fnt.U != nil { + font.Underline = fnt.U.Value() + } + if fnt.Name != nil { + font.Family = fnt.Name.Value() + } + if fnt.Sz != nil { + font.Size = fnt.Sz.Value() + } + if fnt.Strike != nil { + font.Strike = fnt.Strike.Value() + } + if fnt.Color != nil { + font.Color = strings.TrimPrefix(fnt.Color.RGB, "FF") + font.ColorIndexed = fnt.Color.Indexed + font.ColorTheme = fnt.Color.Theme + font.ColorTint = fnt.Color.Tint + } + style.Font = &font + } + } +} + +// extractNumFmt provides a function to extract number format by given styles +// definition. +func (f *File) extractNumFmt(xf xlsxXf, s *xlsxStyleSheet, style *Style) { + if xf.NumFmtID != nil { + numFmtID := *xf.NumFmtID + if _, ok := builtInNumFmt[numFmtID]; ok || isLangNumFmt(numFmtID) { + style.NumFmt = numFmtID + return + } + if s.NumFmts != nil { + for _, numFmt := range s.NumFmts.NumFmt { + style.CustomNumFmt = &numFmt.FormatCode + if strings.Contains(numFmt.FormatCode, ";[Red]") { + style.NegRed = true + } + for numFmtID, fmtCode := range currencyNumFmt { + if style.NegRed { + fmtCode += ";[Red]" + fmtCode + } + if numFmt.FormatCode == fmtCode { + style.NumFmt = numFmtID + } + } + } + } + } +} + +// extractAlignment provides a function to extract alignment format by +// given style definition. +func (f *File) extractAlignment(xf xlsxXf, s *xlsxStyleSheet, style *Style) { + if xf.ApplyAlignment != nil && *xf.ApplyAlignment && xf.Alignment != nil { + style.Alignment = &Alignment{ + Horizontal: xf.Alignment.Horizontal, + Indent: xf.Alignment.Indent, + JustifyLastLine: xf.Alignment.JustifyLastLine, + ReadingOrder: xf.Alignment.ReadingOrder, + RelativeIndent: xf.Alignment.RelativeIndent, + ShrinkToFit: xf.Alignment.ShrinkToFit, + TextRotation: xf.Alignment.TextRotation, + Vertical: xf.Alignment.Vertical, + WrapText: xf.Alignment.WrapText, + } + } +} + +// extractProtection provides a function to extract protection settings by +// given format definition. +func (f *File) extractProtection(xf xlsxXf, s *xlsxStyleSheet, style *Style) { + if xf.ApplyProtection != nil && *xf.ApplyProtection && xf.Protection != nil { + style.Protection = &Protection{} + if xf.Protection.Hidden != nil { + style.Protection.Hidden = *xf.Protection.Hidden + } + if xf.Protection.Locked != nil { + style.Protection.Locked = *xf.Protection.Locked + } + } +} + +// GetStyle get style details by given style index. +func (f *File) GetStyle(idx int) (*Style, error) { + var style *Style + f.mu.Lock() + s, err := f.stylesReader() + if err != nil { + return style, err + } + f.mu.Unlock() + if idx < 0 || s.CellXfs == nil || len(s.CellXfs.Xf) <= idx { + return style, newInvalidStyleID(idx) + } + style = &Style{} + xf := s.CellXfs.Xf[idx] + if xf.ApplyFill != nil && *xf.ApplyFill && + xf.FillID != nil && s.Fills != nil && + *xf.FillID < len(s.Fills.Fill) { + f.extractFills(xf, s, style) + } + f.extractBorders(xf, s, style) + f.extractFont(xf, s, style) + f.extractAlignment(xf, s, style) + f.extractProtection(xf, s, style) + f.extractNumFmt(xf, s, style) + return style, nil +} + // getXfIDFuncs provides a function to get xfID by given style. var getXfIDFuncs = map[string]func(int, xlsxXf, *Style) bool{ "numFmt": func(numFmtID int, xf xlsxXf, style *Style) bool { @@ -1406,9 +1703,15 @@ func getCustomNumFmtID(styleSheet *xlsxStyleSheet, style *Style) (customNumFmtID return } +// isLangNumFmt provides a function to returns if a given number format ID is a +// built-in language glyphs number format code. +func isLangNumFmt(ID int) bool { + return (27 <= ID && ID <= 36) || (50 <= ID && ID <= 62) || (67 <= ID && ID <= 81) +} + // setLangNumFmt provides a function to set number format code with language. func setLangNumFmt(style *Style) int { - if (27 <= style.NumFmt && style.NumFmt <= 36) || (50 <= style.NumFmt && style.NumFmt <= 81) { + if isLangNumFmt(style.NumFmt) { return style.NumFmt } return 0 diff --git a/styles_test.go b/styles_test.go index 856e90f03c..f202bcf02a 100644 --- a/styles_test.go +++ b/styles_test.go @@ -488,3 +488,105 @@ func TestGetNumFmtID(t *testing.T) { assert.NotEqual(t, id1, id2) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestStyleNumFmt.xlsx"))) } + +func TestGetThemeColor(t *testing.T) { + assert.Empty(t, (&File{}).getThemeColor(&xlsxColor{})) + f := NewFile() + assert.Empty(t, f.getThemeColor(nil)) + var theme int + assert.Equal(t, "FFFFFF", f.getThemeColor(&xlsxColor{Theme: &theme})) + assert.Equal(t, "FFFFFF", f.getThemeColor(&xlsxColor{RGB: "FFFFFF"})) + assert.Equal(t, "FF8080", f.getThemeColor(&xlsxColor{Indexed: 2, Tint: 0.5})) + assert.Empty(t, f.getThemeColor(&xlsxColor{Indexed: len(IndexedColorMapping), Tint: 0.5})) +} + +func TestGetStyle(t *testing.T) { + f := NewFile() + expected := &Style{ + Border: []Border{ + {Type: "left", Color: "0000FF", Style: 3}, + {Type: "right", Color: "FF0000", Style: 6}, + {Type: "top", Color: "00FF00", Style: 4}, + {Type: "bottom", Color: "FFFF00", Style: 5}, + {Type: "diagonalUp", Color: "A020F0", Style: 7}, + {Type: "diagonalDown", Color: "A020F0", Style: 7}, + }, + Fill: Fill{Type: "gradient", Shading: 16, Color: []string{"0000FF", "00FF00"}}, + Font: &Font{ + Bold: true, Italic: true, Underline: "single", Family: "Arial", + Size: 8.5, Strike: true, Color: "777777", ColorIndexed: 1, ColorTint: 0.1, + }, + Alignment: &Alignment{ + Horizontal: "center", + Indent: 1, + JustifyLastLine: true, + ReadingOrder: 1, + RelativeIndent: 1, + ShrinkToFit: true, + TextRotation: 180, + Vertical: "center", + WrapText: true, + }, + Protection: &Protection{Hidden: true, Locked: true}, + NumFmt: 49, + } + styleID, err := f.NewStyle(expected) + assert.NoError(t, err) + style, err := f.GetStyle(styleID) + assert.NoError(t, err) + assert.Equal(t, expected.Border, style.Border) + assert.Equal(t, expected.Fill, style.Fill) + assert.Equal(t, expected.Font, style.Font) + assert.Equal(t, expected.Alignment, style.Alignment) + assert.Equal(t, expected.Protection, style.Protection) + assert.Equal(t, expected.NumFmt, style.NumFmt) + + expected = &Style{ + Fill: Fill{Type: "pattern", Pattern: 1, Color: []string{"0000FF"}}, + } + styleID, err = f.NewStyle(expected) + assert.NoError(t, err) + style, err = f.GetStyle(styleID) + assert.NoError(t, err) + assert.Equal(t, expected.Fill, style.Fill) + + expected = &Style{NumFmt: 27} + styleID, err = f.NewStyle(expected) + assert.NoError(t, err) + style, err = f.GetStyle(styleID) + assert.NoError(t, err) + assert.Equal(t, expected.NumFmt, style.NumFmt) + + expected = &Style{NumFmt: 165} + styleID, err = f.NewStyle(expected) + assert.NoError(t, err) + style, err = f.GetStyle(styleID) + assert.NoError(t, err) + assert.Equal(t, expected.NumFmt, style.NumFmt) + + expected = &Style{NumFmt: 165, NegRed: true} + styleID, err = f.NewStyle(expected) + assert.NoError(t, err) + style, err = f.GetStyle(styleID) + assert.NoError(t, err) + assert.Equal(t, expected.NumFmt, style.NumFmt) + + // Test get style with custom color index + f.Styles.Colors = &xlsxStyleColors{ + IndexedColors: xlsxIndexedColors{ + RgbColor: []xlsxColor{{RGB: "FF012345"}}, + }, + } + assert.Equal(t, "012345", f.getThemeColor(&xlsxColor{Indexed: 0})) + + // Test get style with invalid style index + style, err = f.GetStyle(-1) + assert.Nil(t, style) + assert.Equal(t, err, newInvalidStyleID(-1)) + // Test get style with unsupported charset style sheet + f.Styles = nil + f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset) + style, err = f.GetStyle(1) + assert.Nil(t, style) + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") +} diff --git a/xmlStyles.go b/xmlStyles.go index 067cff92d4..1dda04a0aa 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -300,12 +300,19 @@ type xlsxNumFmt struct { FormatCode16 string `xml:"http://schemas.microsoft.com/office/spreadsheetml/2015/02/main formatCode16,attr,omitempty"` } +// xlsxIndexedColors directly maps the single ARGB entry for the corresponding +// color index. +type xlsxIndexedColors struct { + RgbColor []xlsxColor `xml:"rgbColor"` +} + // xlsxStyleColors directly maps the colors' element. Color information -// associated with this stylesheet. This collection is written whenever the +// associated with this style sheet. 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. type xlsxStyleColors struct { - Color string `xml:",innerxml"` + IndexedColors xlsxIndexedColors `xml:"indexedColors"` + MruColors xlsxInnerXML `xml:"mruColors"` } // Alignment directly maps the alignment settings of the cells. From 15614badfc1b60d727a24af3062a132633f45184 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 25 Aug 2023 01:06:41 +0800 Subject: [PATCH 783/957] This closes #1628, fix the GetPictures function returns pictures doesn't correct in some cases --- picture.go | 4 +--- picture_test.go | 9 +++++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/picture.go b/picture.go index 1a9eeeb652..1d6b5f06f2 100644 --- a/picture.go +++ b/picture.go @@ -649,9 +649,7 @@ func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) if wsDr, _, err = f.drawingParser(drawingXML); err != nil { return } - if pics = f.getPicturesFromWsDr(row, col, drawingRelationships, wsDr); len(pics) > 0 { - return - } + pics = f.getPicturesFromWsDr(row, col, drawingRelationships, wsDr) deWsDr = new(decodeWsDr) if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(drawingXML)))). Decode(deWsDr); err != nil && err != io.EOF { diff --git a/picture_test.go b/picture_test.go index 7eb21cbf5c..4920634514 100644 --- a/picture_test.go +++ b/picture_test.go @@ -70,6 +70,15 @@ func TestAddPicture(t *testing.T) { assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddPicture1.xlsx"))) assert.NoError(t, f.Close()) + // Test get pictures after inserting a new picture from a workbook which contains existing pictures + f, err = OpenFile(filepath.Join("test", "TestAddPicture1.xlsx")) + assert.NoError(t, err) + assert.NoError(t, f.AddPicture("Sheet1", "A30", filepath.Join("test", "images", "excel.jpg"), nil)) + pics, err := f.GetPictures("Sheet1", "A30") + assert.NoError(t, err) + assert.Len(t, pics, 2) + assert.NoError(t, f.Close()) + // Test add picture with unsupported charset content types f = NewFile() f.ContentTypes = nil From 4957ee9abc8b802e147480c83c1dd87d5a791a53 Mon Sep 17 00:00:00 2001 From: fsfsx <19376131@buaa.edu.cn> Date: Sat, 26 Aug 2023 13:14:03 +0800 Subject: [PATCH 784/957] ref #65, add support for 10 formula functions - Add support for 10 formula functions: ARRAYTOTEXT, FORECAST, FORECAST.LINEAR, FREQUENCY, INTERCEPT, ODDFYIELD, ODDLPRICE, ODDLYIELD, PROB and VALUETOTEXT - Update unit tests --- calc.go | 540 +++++++++++++++++++++++++++++++++++++++++++++++---- calc_test.go | 171 +++++++++++++++- 2 files changed, 663 insertions(+), 48 deletions(-) diff --git a/calc.go b/calc.go index 048be02e2a..8560675d55 100644 --- a/calc.go +++ b/calc.go @@ -365,6 +365,7 @@ type formulaFuncs struct { // AMORLINC // AND // ARABIC +// ARRAYTOTEXT // ASIN // ASINH // ATAN @@ -510,7 +511,10 @@ type formulaFuncs struct { // FLOOR // FLOOR.MATH // FLOOR.PRECISE +// FORECAST +// FORECAST.LINEAR // FORMULATEXT +// FREQUENCY // FTEST // FV // FVSCHEDULE @@ -567,6 +571,7 @@ type formulaFuncs struct { // INDEX // INDIRECT // INT +// INTERCEPT // INTRATE // IPMT // IRR @@ -649,6 +654,9 @@ type formulaFuncs struct { // OCT2HEX // ODD // ODDFPRICE +// ODDFYIELD +// ODDLPRICE +// ODDLYIELD // OR // PDURATION // PEARSON @@ -670,6 +678,7 @@ type formulaFuncs struct { // PRICE // PRICEDISC // PRICEMAT +// PROB // PRODUCT // PROPER // PV @@ -763,6 +772,7 @@ type formulaFuncs struct { // UNICODE // UPPER // VALUE +// VALUETOTEXT // VAR // VAR.P // VAR.S @@ -5657,6 +5667,76 @@ func (fn *formulaFuncs) POISSON(argsList *list.List) formulaArg { return newNumberFormulaArg(math.Exp(0-mean.Number) * math.Pow(mean.Number, x.Number) / fact(x.Number)) } +// prepareProbArgs checking and prepare arguments for the formula function +// PROB. +func prepareProbArgs(argsList *list.List) []formulaArg { + if argsList.Len() < 3 { + return []formulaArg{newErrorFormulaArg(formulaErrorVALUE, "PROB requires at least 3 arguments")} + } + if argsList.Len() > 4 { + return []formulaArg{newErrorFormulaArg(formulaErrorVALUE, "PROB requires at most 4 arguments")} + } + var lower, upper formulaArg + xRange := argsList.Front().Value.(formulaArg) + probRange := argsList.Front().Next().Value.(formulaArg) + if lower = argsList.Front().Next().Next().Value.(formulaArg); lower.Type != ArgNumber { + return []formulaArg{newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)} + } + upper = lower + if argsList.Len() == 4 { + if upper = argsList.Back().Value.(formulaArg); upper.Type != ArgNumber { + return []formulaArg{newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE)} + } + } + nR1, nR2 := len(xRange.Matrix), len(probRange.Matrix) + if nR1 == 0 || nR2 == 0 { + return []formulaArg{newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)} + } + if nR1 != nR2 { + return []formulaArg{newErrorFormulaArg(formulaErrorNA, formulaErrorNA)} + } + nC1, nC2 := len(xRange.Matrix[0]), len(probRange.Matrix[0]) + if nC1 != nC2 { + return []formulaArg{newErrorFormulaArg(formulaErrorNA, formulaErrorNA)} + } + return []formulaArg{xRange, probRange, lower, upper} +} + +// PROB function calculates the probability associated with a given range. The +// syntax of the function is: +// +// PROB(x_range,prob_range,lower_limit,[upper_limit]) +func (fn *formulaFuncs) PROB(argsList *list.List) formulaArg { + args := prepareProbArgs(argsList) + if len(args) == 1 { + return args[0] + } + xRange, probRange, lower, upper := args[0], args[1], args[2], args[3] + var sum, res, fP, fW float64 + var stop bool + for r := 0; r < len(xRange.Matrix) && !stop; r++ { + for c := 0; c < len(xRange.Matrix[0]) && !stop; c++ { + p := probRange.Matrix[r][c] + x := xRange.Matrix[r][c] + if p.Type == ArgNumber && x.Type == ArgNumber { + if fP, fW = p.Number, x.Number; fP < 0 || fP > 1 { + stop = true + continue + } + if sum += fP; fW >= lower.Number && fW <= upper.Number { + res += fP + } + continue + } + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + } + if stop || math.Abs(sum-1) > 1.0e-7 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + return newNumberFormulaArg(res) +} + // 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: // @@ -7933,6 +8013,92 @@ func (fn *formulaFuncs) FISHERINV(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorVALUE, "FISHERINV requires 1 numeric argument") } +// FORECAST function predicts a future point on a linear trend line fitted to a +// supplied set of x- and y- values. The syntax of the function is: +// +// FORECAST(x,known_y's,known_x's) +func (fn *formulaFuncs) FORECAST(argsList *list.List) formulaArg { + return fn.pearsonProduct("FORECAST", 3, argsList) +} + +// FORECASTdotLINEAR function predicts a future point on a linear trend line +// fitted to a supplied set of x- and y- values. The syntax of the function is: +// +// FORECAST.LINEAR(x,known_y's,known_x's) +func (fn *formulaFuncs) FORECASTdotLINEAR(argsList *list.List) formulaArg { + return fn.pearsonProduct("FORECAST.LINEAR", 3, argsList) +} + +// maritxToSortedColumnList convert matrix formula arguments to a ascending +// order list by column. +func maritxToSortedColumnList(arg formulaArg) formulaArg { + mtx, cols := []formulaArg{}, len(arg.Matrix[0]) + for colIdx := 0; colIdx < cols; colIdx++ { + for _, row := range arg.Matrix { + cell := row[colIdx] + if cell.Type == ArgError { + return cell + } + if cell.Type == ArgNumber { + mtx = append(mtx, cell) + } + } + } + argsList := newListFormulaArg(mtx) + sort.Slice(argsList.List, func(i, j int) bool { + return argsList.List[i].Number < argsList.List[j].Number + }) + return argsList +} + +// FREQUENCY function to count how many children fall into different age +// ranges. The syntax of the function is: +// +// FREQUENCY(data_array,bins_array) +func (fn *formulaFuncs) FREQUENCY(argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, "FREQUENCY requires 2 arguments") + } + data, bins := argsList.Front().Value.(formulaArg), argsList.Back().Value.(formulaArg) + if len(data.Matrix) == 0 { + data.Matrix = [][]formulaArg{{data}} + } + if len(bins.Matrix) == 0 { + bins.Matrix = [][]formulaArg{{bins}} + } + var ( + dataMtx, binsMtx formulaArg + c [][]formulaArg + i, j int + ) + if dataMtx = maritxToSortedColumnList(data); dataMtx.Type != ArgList { + return dataMtx + } + if binsMtx = maritxToSortedColumnList(bins); binsMtx.Type != ArgList { + return binsMtx + } + for row := 0; row < len(binsMtx.List)+1; row++ { + rows := []formulaArg{} + for col := 0; col < 1; col++ { + rows = append(rows, newNumberFormulaArg(0)) + } + c = append(c, rows) + } + for j = 0; j < len(binsMtx.List); j++ { + n := 0.0 + for i < len(dataMtx.List) && dataMtx.List[i].Number <= binsMtx.List[j].Number { + n++ + i++ + } + c[j] = []formulaArg{newNumberFormulaArg(n)} + } + c[j] = []formulaArg{newNumberFormulaArg(float64(len(dataMtx.List) - i))} + if len(c) > 2 { + c[1], c[2] = c[2], c[1] + } + return newMatrixFormulaArg(c) +} + // GAMMA function returns the value of the Gamma Function, Γ(n), for a // specified number, n. The syntax of the function is: // @@ -8953,6 +9119,15 @@ func (fn *formulaFuncs) HYPGEOMDIST(argsList *list.List) formulaArg { binomCoeff(numberPop.Number, numberSample.Number)) } +// INTERCEPT function calculates the intercept (the value at the intersection +// of the y axis) of the linear regression line through a supplied set of x- +// and y- values. The syntax of the function is: +// +// INTERCEPT(known_y's,known_x's) +func (fn *formulaFuncs) INTERCEPT(argsList *list.List) formulaArg { + return fn.pearsonProduct("INTERCEPT", 2, argsList) +} + // KURT function calculates the kurtosis of a supplied set of values. The // syntax of the function is: // @@ -10013,19 +10188,23 @@ func (fn *formulaFuncs) min(mina bool, argsList *list.List) formulaArg { return newNumberFormulaArg(min) } -// 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)) +// pearsonProduct is an implementation of the formula functions FORECAST, +// FORECAST.LINEAR, INTERCEPT, PEARSON, RSQ and SLOPE. +func (fn *formulaFuncs) pearsonProduct(name string, n int, argsList *list.List) formulaArg { + if argsList.Len() != n { + return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires %d arguments", name, n)) } - 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() + var fx formulaArg + array1 := argsList.Back().Value.(formulaArg).ToList() + array2 := argsList.Front().Value.(formulaArg).ToList() + if name == "PEARSON" || name == "RSQ" { + array1, array2 = array2, array1 + } + if n == 3 { + if fx = argsList.Front().Value.(formulaArg).ToNumber(); fx.Type != ArgNumber { + return fx + } + array2 = argsList.Front().Next().Value.(formulaArg).ToList() } if len(array1) != len(array2) { return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) @@ -10051,16 +10230,17 @@ func (fn *formulaFuncs) pearsonProduct(name string, argsList *list.List) formula deltaX += (num1.Number - x) * (num1.Number - x) deltaY += (num2.Number - y) * (num2.Number - y) } - if deltaX == 0 || deltaY == 0 { + if sum*deltaX*deltaY == 0 { return newErrorFormulaArg(formulaErrorDIV, formulaErrorDIV) } - if name == "RSQ" { - return newNumberFormulaArg(math.Pow(sum/math.Sqrt(deltaX*deltaY), 2)) - } - if name == "PEARSON" { - return newNumberFormulaArg(sum / math.Sqrt(deltaX*deltaY)) - } - return newNumberFormulaArg(sum / deltaX) + return newNumberFormulaArg(map[string]float64{ + "FORECAST": y + sum/deltaX*(fx.Number-x), + "FORECAST.LINEAR": y + sum/deltaX*(fx.Number-x), + "INTERCEPT": y - sum/deltaX*x, + "PEARSON": sum / math.Sqrt(deltaX*deltaY), + "RSQ": math.Pow(sum/math.Sqrt(deltaX*deltaY), 2), + "SLOPE": sum / deltaX, + }[name]) } // PEARSON function calculates the Pearson Product-Moment Correlation @@ -10068,7 +10248,7 @@ func (fn *formulaFuncs) pearsonProduct(name string, argsList *list.List) formula // // PEARSON(array1,array2) func (fn *formulaFuncs) PEARSON(argsList *list.List) formulaArg { - return fn.pearsonProduct("PEARSON", argsList) + return fn.pearsonProduct("PEARSON", 2, argsList) } // PERCENTILEdotEXC function returns the k'th percentile (i.e. the value below @@ -10407,7 +10587,7 @@ func (fn *formulaFuncs) RANK(argsList *list.List) formulaArg { // // RSQ(known_y's,known_x's) func (fn *formulaFuncs) RSQ(argsList *list.List) formulaArg { - return fn.pearsonProduct("RSQ", argsList) + return fn.pearsonProduct("RSQ", 2, argsList) } // skew is an implementation of the formula functions SKEW and SKEW.P. @@ -10475,7 +10655,7 @@ func (fn *formulaFuncs) SKEWdotP(argsList *list.List) formulaArg { // // SLOPE(known_y's,known_x's) func (fn *formulaFuncs) SLOPE(argsList *list.List) formulaArg { - return fn.pearsonProduct("SLOPE", argsList) + return fn.pearsonProduct("SLOPE", 2, argsList) } // SMALL function returns the k'th smallest value from an array of numeric @@ -13203,8 +13383,65 @@ func (fn *formulaFuncs) WEEKNUM(argsList *list.List) formulaArg { // Text Functions +// prepareToText checking and prepare arguments for the formula functions +// ARRAYTOTEXT and VALUETOTEXT. +func prepareToText(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() > 2 { + return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s allows at most 2 arguments", name)) + } + format := newNumberFormulaArg(0) + if argsList.Len() == 2 { + if format = argsList.Back().Value.(formulaArg).ToNumber(); format.Type != ArgNumber { + return format + } + } + if format.Number != 0 && format.Number != 1 { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + return format +} + +// ARRAYTOTEXT function returns an array of text values from any specified +// range. It passes text values unchanged, and converts non-text values to +// text. The syntax of the function is: +// +// ARRAYTOTEXT(array,[format]) +func (fn *formulaFuncs) ARRAYTOTEXT(argsList *list.List) formulaArg { + var mtx [][]string + format := prepareToText("ARRAYTOTEXT", argsList) + if format.Type != ArgNumber { + return format + } + for _, rows := range argsList.Front().Value.(formulaArg).Matrix { + var row []string + for _, cell := range rows { + if num := cell.ToNumber(); num.Type != ArgNumber && format.Number == 1 { + row = append(row, fmt.Sprintf("\"%s\"", cell.Value())) + continue + } + row = append(row, cell.Value()) + } + mtx = append(mtx, row) + } + var text []string + for _, row := range mtx { + if format.Number == 1 { + text = append(text, strings.Join(row, ",")) + continue + } + text = append(text, strings.Join(row, ", ")) + } + if format.Number == 1 { + return newStringFormulaArg(fmt.Sprintf("{%s}", strings.Join(text, ";"))) + } + return newStringFormulaArg(strings.Join(text, ", ")) +} + // CHAR function returns the character relating to a supplied character set -// number (from 1 to 255). syntax of the function is: +// number (from 1 to 255). The syntax of the function is: // // CHAR(number) func (fn *formulaFuncs) CHAR(argsList *list.List) formulaArg { @@ -13873,6 +14110,22 @@ func (fn *formulaFuncs) VALUE(argsList *list.List) formulaArg { return newNumberFormulaArg(dateValue + timeValue) } +// VALUETOTEXT function returns text from any specified value. It passes text +// values unchanged, and converts non-text values to text. +// +// VALUETOTEXT(value,[format]) +func (fn *formulaFuncs) VALUETOTEXT(argsList *list.List) formulaArg { + format := prepareToText("VALUETOTEXT", argsList) + if format.Type != ArgNumber { + return format + } + cell := argsList.Front().Value.(formulaArg) + if num := cell.ToNumber(); num.Type != ArgNumber && format.Number == 1 { + return newStringFormulaArg(fmt.Sprintf("\"%s\"", cell.Value())) + } + return newStringFormulaArg(cell.Value()) +} + // Conditional Functions // IF function tests a supplied condition and returns one result if the @@ -16401,43 +16654,56 @@ func coupNumber(maturity, settlement, numMonths float64) float64 { return result } -// prepareOddfpriceArgs checking and prepare arguments for the formula -// function ODDFPRICE. -func (fn *formulaFuncs) prepareOddfpriceArgs(argsList *list.List) formulaArg { +// prepareOddYldOrPrArg checking and prepare yield or price arguments for the +// formula functions ODDFPRICE, ODDFYIELD, ODDLPRICE and ODDLYIELD. +func prepareOddYldOrPrArg(name string, arg formulaArg) formulaArg { + yldOrPr := arg.ToNumber() + if yldOrPr.Type != ArgNumber { + return yldOrPr + } + if (name == "ODDFPRICE" || name == "ODDLPRICE") && yldOrPr.Number < 0 { + return newErrorFormulaArg(formulaErrorNUM, fmt.Sprintf("%s requires yld >= 0", name)) + } + if (name == "ODDFYIELD" || name == "ODDLYIELD") && yldOrPr.Number <= 0 { + return newErrorFormulaArg(formulaErrorNUM, fmt.Sprintf("%s requires pr > 0", name)) + } + return yldOrPr +} + +// prepareOddfArgs checking and prepare arguments for the formula +// functions ODDFPRICE and ODDFYIELD. +func (fn *formulaFuncs) prepareOddfArgs(name string, argsList *list.List) formulaArg { dateValues := fn.prepareDataValueArgs(4, argsList) if dateValues.Type != ArgList { return dateValues } settlement, maturity, issue, firstCoupon := dateValues.List[0], dateValues.List[1], dateValues.List[2], dateValues.List[3] if issue.Number >= settlement.Number { - return newErrorFormulaArg(formulaErrorNUM, "ODDFPRICE requires settlement > issue") + return newErrorFormulaArg(formulaErrorNUM, fmt.Sprintf("%s requires settlement > issue", name)) } if settlement.Number >= firstCoupon.Number { - return newErrorFormulaArg(formulaErrorNUM, "ODDFPRICE requires first_coupon > settlement") + return newErrorFormulaArg(formulaErrorNUM, fmt.Sprintf("%s requires first_coupon > settlement", name)) } if firstCoupon.Number >= maturity.Number { - return newErrorFormulaArg(formulaErrorNUM, "ODDFPRICE requires maturity > first_coupon") + return newErrorFormulaArg(formulaErrorNUM, fmt.Sprintf("%s requires maturity > first_coupon", name)) } rate := argsList.Front().Next().Next().Next().Next().Value.(formulaArg).ToNumber() if rate.Type != ArgNumber { return rate } if rate.Number < 0 { - return newErrorFormulaArg(formulaErrorNUM, "ODDFPRICE requires rate >= 0") - } - yld := argsList.Front().Next().Next().Next().Next().Next().Value.(formulaArg).ToNumber() - if yld.Type != ArgNumber { - return yld + return newErrorFormulaArg(formulaErrorNUM, fmt.Sprintf("%s requires rate >= 0", name)) } - if yld.Number < 0 { - return newErrorFormulaArg(formulaErrorNUM, "ODDFPRICE requires yld >= 0") + yldOrPr := prepareOddYldOrPrArg(name, argsList.Front().Next().Next().Next().Next().Next().Value.(formulaArg)) + if yldOrPr.Type != ArgNumber { + return yldOrPr } redemption := argsList.Front().Next().Next().Next().Next().Next().Next().Value.(formulaArg).ToNumber() if redemption.Type != ArgNumber { return redemption } if redemption.Number <= 0 { - return newErrorFormulaArg(formulaErrorNUM, "ODDFPRICE requires redemption > 0") + return newErrorFormulaArg(formulaErrorNUM, fmt.Sprintf("%s requires redemption > 0", name)) } frequency := argsList.Front().Next().Next().Next().Next().Next().Next().Next().Value.(formulaArg).ToNumber() if frequency.Type != ArgNumber { @@ -16452,7 +16718,7 @@ func (fn *formulaFuncs) prepareOddfpriceArgs(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) } } - return newListFormulaArg([]formulaArg{settlement, maturity, issue, firstCoupon, rate, yld, redemption, frequency, basis}) + return newListFormulaArg([]formulaArg{settlement, maturity, issue, firstCoupon, rate, yldOrPr, redemption, frequency, basis}) } // ODDFPRICE function calculates the price per $100 face value of a security @@ -16463,7 +16729,7 @@ func (fn *formulaFuncs) ODDFPRICE(argsList *list.List) formulaArg { if argsList.Len() != 8 && argsList.Len() != 9 { return newErrorFormulaArg(formulaErrorVALUE, "ODDFPRICE requires 8 or 9 arguments") } - args := fn.prepareOddfpriceArgs(argsList) + args := fn.prepareOddfArgs("ODDFPRICE", argsList) if args.Type != ArgList { return args } @@ -16583,6 +16849,198 @@ func (fn *formulaFuncs) ODDFPRICE(argsList *list.List) formulaArg { return newNumberFormulaArg(term1 + term2 + term3[0] - term4) } +// getODDFPRICE is a part of implementation of the formula function ODDFPRICE. +func getODDFPRICE(f func(yld float64) float64, x, cnt, prec float64) float64 { + const maxCnt = 20.0 + d := func(f func(yld float64) float64, x float64) float64 { + return (f(x+prec) - f(x-prec)) / (2 * prec) + } + fx, Fx := f(x), d(f, x) + newX := x - (fx / Fx) + if math.Abs(newX-x) < prec { + return newX + } else if cnt > maxCnt { + return newX + } + return getODDFPRICE(f, newX, cnt+1, prec) +} + +// ODDFYIELD function calculates the yield of a security with an odd (short or +// long) first period. The syntax of the function is: +// +// ODDFYIELD(settlement,maturity,issue,first_coupon,rate,pr,redemption,frequency,[basis]) +func (fn *formulaFuncs) ODDFYIELD(argsList *list.List) formulaArg { + if argsList.Len() != 8 && argsList.Len() != 9 { + return newErrorFormulaArg(formulaErrorVALUE, "ODDFYIELD requires 8 or 9 arguments") + } + args := fn.prepareOddfArgs("ODDFYIELD", argsList) + if args.Type != ArgList { + return args + } + settlement, maturity, issue, firstCoupon, rate, pr, redemption, frequency, basisArg := args.List[0], args.List[1], args.List[2], args.List[3], args.List[4], args.List[5], args.List[6], args.List[7], args.List[8] + if basisArg.Number < 0 || basisArg.Number > 4 { + return newErrorFormulaArg(formulaErrorNUM, "invalid basis") + } + settlementTime := timeFromExcelTime(settlement.Number, false) + maturityTime := timeFromExcelTime(maturity.Number, false) + years := coupdays(settlementTime, maturityTime, int(basisArg.Number)) + px := pr.Number - 100 + num := rate.Number*years*100 - px + denum := px/4 + years*px/2 + years*100 + guess := num / denum + f := func(yld float64) float64 { + fnArgs := list.New().Init() + fnArgs.PushBack(settlement) + fnArgs.PushBack(maturity) + fnArgs.PushBack(issue) + fnArgs.PushBack(firstCoupon) + fnArgs.PushBack(rate) + fnArgs.PushBack(newNumberFormulaArg(yld)) + fnArgs.PushBack(redemption) + fnArgs.PushBack(frequency) + fnArgs.PushBack(basisArg) + return pr.Number - fn.ODDFPRICE(fnArgs).Number + } + if result := getODDFPRICE(f, guess, 0, 1e-7); !math.IsInf(result, 0) { + return newNumberFormulaArg(result) + } + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) +} + +// prepareOddlArgs checking and prepare arguments for the formula +// functions ODDLPRICE and ODDLYIELD. +func (fn *formulaFuncs) prepareOddlArgs(name string, argsList *list.List) formulaArg { + dateValues := fn.prepareDataValueArgs(3, argsList) + if dateValues.Type != ArgList { + return dateValues + } + settlement, maturity, lastInterest := dateValues.List[0], dateValues.List[1], dateValues.List[2] + if lastInterest.Number >= settlement.Number { + return newErrorFormulaArg(formulaErrorNUM, fmt.Sprintf("%s requires settlement > last_interest", name)) + } + if settlement.Number >= maturity.Number { + return newErrorFormulaArg(formulaErrorNUM, fmt.Sprintf("%s requires maturity > settlement", name)) + } + rate := argsList.Front().Next().Next().Next().Value.(formulaArg).ToNumber() + if rate.Type != ArgNumber { + return rate + } + if rate.Number < 0 { + return newErrorFormulaArg(formulaErrorNUM, fmt.Sprintf("%s requires rate >= 0", name)) + } + yldOrPr := prepareOddYldOrPrArg(name, argsList.Front().Next().Next().Next().Next().Value.(formulaArg)) + if yldOrPr.Type != ArgNumber { + return yldOrPr + } + redemption := argsList.Front().Next().Next().Next().Next().Next().Value.(formulaArg).ToNumber() + if redemption.Type != ArgNumber { + return redemption + } + if redemption.Number <= 0 { + return newErrorFormulaArg(formulaErrorNUM, fmt.Sprintf("%s requires redemption > 0", name)) + } + frequency := argsList.Front().Next().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() == 8 { + if basis = argsList.Back().Value.(formulaArg).ToNumber(); basis.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } + } + return newListFormulaArg([]formulaArg{settlement, maturity, lastInterest, rate, yldOrPr, redemption, frequency, basis}) +} + +// oddl is an implementation of the formula functions ODDLPRICE and ODDLYIELD. +func (fn *formulaFuncs) oddl(name string, argsList *list.List) formulaArg { + if argsList.Len() != 7 && argsList.Len() != 8 { + return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires 7 or 8 arguments", name)) + } + args := fn.prepareOddlArgs(name, argsList) + if args.Type != ArgList { + return args + } + settlement, maturity, lastInterest, rate, prOrYld, redemption, frequency, basisArg := args.List[0], args.List[1], args.List[2], args.List[3], args.List[4], args.List[5], args.List[6], args.List[7] + if basisArg.Number < 0 || basisArg.Number > 4 { + return newErrorFormulaArg(formulaErrorNUM, "invalid basis") + } + settlementTime := timeFromExcelTime(settlement.Number, false) + maturityTime := timeFromExcelTime(maturity.Number, false) + basis := int(basisArg.Number) + numMonths := 12 / frequency.Number + fnArgs := list.New().Init() + fnArgs.PushBack(lastInterest) + fnArgs.PushBack(maturity) + fnArgs.PushBack(frequency) + fnArgs.PushBack(basisArg) + nc := fn.COUPNUM(fnArgs) + earlyCoupon := lastInterest.Number + aggrFunc := func(acc []float64, index float64) []float64 { + earlyCouponTime := timeFromExcelTime(earlyCoupon, false) + lateCouponTime := changeMonth(earlyCouponTime, numMonths, false) + lateCoupon, _ := timeToExcelTime(lateCouponTime, false) + nl := coupdays(earlyCouponTime, lateCouponTime, basis) + dci := coupdays(earlyCouponTime, maturityTime, basis) + if index < nc.Number { + dci = nl + } + var a float64 + if lateCoupon < settlement.Number { + a = dci + } else if earlyCoupon < settlement.Number { + a = coupdays(earlyCouponTime, settlementTime, basis) + } + startDate := earlyCoupon + if settlement.Number > earlyCoupon { + startDate = settlement.Number + } + endDate := lateCoupon + if maturity.Number < lateCoupon { + endDate = maturity.Number + } + startDateTime := timeFromExcelTime(startDate, false) + endDateTime := timeFromExcelTime(endDate, false) + dsc := coupdays(startDateTime, endDateTime, basis) + earlyCoupon = lateCoupon + dcnl := acc[0] + anl := acc[1] + dscnl := acc[2] + return []float64{dcnl + dci/nl, anl + a/nl, dscnl + dsc/nl} + } + ag := aggrBetween(1, math.Floor(nc.Number), []float64{0, 0, 0}, aggrFunc) + dcnl, anl, dscnl := ag[0], ag[1], ag[2] + x := 100.0 * rate.Number / frequency.Number + term1 := dcnl*x + redemption.Number + if name == "ODDLPRICE" { + term2 := dscnl*prOrYld.Number/frequency.Number + 1 + term3 := anl * x + return newNumberFormulaArg(term1/term2 - term3) + } + term2 := anl*x + prOrYld.Number + term3 := frequency.Number / dscnl + return newNumberFormulaArg((term1 - term2) / term2 * term3) +} + +// ODDLPRICE function calculates the price per $100 face value of a security +// with an odd (short or long) last period. The syntax of the function is: +// +// ODDLPRICE(settlement,maturity,last_interest,rate,yld,redemption,frequency,[basis]) +func (fn *formulaFuncs) ODDLPRICE(argsList *list.List) formulaArg { + return fn.oddl("ODDLPRICE", argsList) +} + +// ODDLYIELD function calculates the yield of a security with an odd (short or +// long) last period. The syntax of the function is: +// +// ODDLYIELD(settlement,maturity,last_interest,rate,pr,redemption,frequency,[basis]) +func (fn *formulaFuncs) ODDLYIELD(argsList *list.List) formulaArg { + return fn.oddl("ODDLYIELD", argsList) +} + // PDURATION function calculates the number of periods required for an // investment to reach a specified future value. The syntax of the function // is: @@ -17743,7 +18201,7 @@ func (fn *formulaFuncs) database(name string, argsList *list.List) formulaArg { // 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: +// user-specified criteria. The syntax of the function is: // // DAVERAGE(database,field,criteria) func (fn *formulaFuncs) DAVERAGE(argsList *list.List) formulaArg { diff --git a/calc_test.go b/calc_test.go index 96dfbdda2f..65ace82095 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1060,6 +1060,13 @@ func TestCalcCellValue(t *testing.T) { "=FISHERINV(INT(0))": "0", "=FISHERINV(\"0\")": "0", "=FISHERINV(2.8)": "0.992631520201128", + // FORECAST + "=FORECAST(7,A1:A7,B1:B7)": "4", + // FORECAST.LINEAR + "=FORECAST.LINEAR(7,A1:A7,B1:B7)": "4", + // FREQUENCY + "=SUM(FREQUENCY(A2,B2))": "1", + "=SUM(FREQUENCY(A1:A5,B1:B2))": "4", // GAMMA "=GAMMA(0.1)": "9.51350769866873", "=GAMMA(INT(1))": "1", @@ -1109,6 +1116,8 @@ func TestCalcCellValue(t *testing.T) { "=HYPGEOMDIST(2,4,4,12)": "0.339393939393939", "=HYPGEOMDIST(3,4,4,12)": "0.0646464646464646", "=HYPGEOMDIST(4,4,4,12)": "0.00202020202020202", + // INTERCEPT + "=INTERCEPT(A1:A4,B1:B4)": "-3", // KURT "=KURT(F1:F9)": "-1.03350350255137", "=KURT(F1,F2:F9)": "-1.03350350255137", @@ -1652,6 +1661,10 @@ func TestCalcCellValue(t *testing.T) { "=WEEKNUM(\"01/01/2017\",21)": "52", "=WEEKNUM(\"01/01/2021\",21)": "53", // Text Functions + // ARRAYTOTEXT + "=ARRAYTOTEXT(A1:D2)": "1, 4, , Month, 2, 5, , Jan", + "=ARRAYTOTEXT(A1:D2,0)": "1, 4, , Month, 2, 5, , Jan", + "=ARRAYTOTEXT(A1:D2,1)": "{1,4,,\"Month\";2,5,,\"Jan\"}", // CHAR "=CHAR(65)": "A", "=CHAR(97)": "a", @@ -1820,6 +1833,13 @@ func TestCalcCellValue(t *testing.T) { "=VALUE(\"20%\")": "0.2", "=VALUE(\"12:00:00\")": "0.5", "=VALUE(\"01/02/2006 15:04:05\")": "38719.6278356481", + // VALUETOTEXT + "=VALUETOTEXT(A1)": "1", + "=VALUETOTEXT(A1,0)": "1", + "=VALUETOTEXT(A1,1)": "1", + "=VALUETOTEXT(D1)": "Month", + "=VALUETOTEXT(D1,0)": "Month", + "=VALUETOTEXT(D1,1)": "\"Month\"", // Conditional Functions // IF "=IF(1=1)": "TRUE", @@ -2050,13 +2070,23 @@ func TestCalcCellValue(t *testing.T) { // NPV "=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.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", + "=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", + // ODDFYIELD + "=ODDFYIELD(\"05/01/2017\",\"06/30/2021\",\"03/15/2017\",\"06/30/2017\",5.5%,102,100,1)": "0.0495998049937776", + "=ODDFYIELD(\"05/01/2017\",\"06/30/2021\",\"03/15/2017\",\"06/30/2017\",5.5%,102,100,2)": "0.0496289417392839", + "=ODDFYIELD(\"05/01/2017\",\"06/30/2021\",\"03/15/2017\",\"06/30/2017\",5.5%,102,100,4,1)": "0.0464750282973541", + // ODDLPRICE + "=ODDLPRICE(\"04/20/2008\",\"06/15/2008\",\"12/24/2007\",3.75%,99.875,100,2)": "5.0517841252892", + "=ODDLPRICE(\"04/20/2008\",\"06/15/2008\",\"12/24/2007\",3.75%,99.875,100,4,1)": "10.3667274303228", + // ODDLYIELD + "=ODDLYIELD(\"04/20/2008\",\"06/15/2008\",\"12/24/2007\",3.75%,99.875,100,2)": "0.0451922356291692", + "=ODDLYIELD(\"04/20/2008\",\"06/15/2008\",\"12/24/2007\",3.75%,99.875,100,4,1)": "0.0882287538349037", // PDURATION "=PDURATION(0.04,10000,15000)": "10.3380350715076", // PMT @@ -2710,6 +2740,15 @@ func TestCalcCellValue(t *testing.T) { "=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"}, + // PROB + "=PROB()": {"#VALUE!", "PROB requires at least 3 arguments"}, + "=PROB(A1:A2,B1:B2,1,1,1)": {"#VALUE!", "PROB requires at most 4 arguments"}, + "=PROB(A1:A2,B1:B2,\"\")": {"#VALUE!", "#VALUE!"}, + "=PROB(A1:A2,B1:B2,1,\"\")": {"#VALUE!", "#VALUE!"}, + "=PROB(A1,B1,1)": {"#NUM!", "#NUM!"}, + "=PROB(A1:A2,B1:B3,1)": {"#N/A", "#N/A"}, + "=PROB(A1:A2,B1:C2,1)": {"#N/A", "#N/A"}, + "=PROB(A1:A2,B1:B2,1)": {"#NUM!", "#NUM!"}, // SUBTOTAL "=SUBTOTAL()": {"#VALUE!", "SUBTOTAL requires at least 2 arguments"}, "=SUBTOTAL(\"\",A4:A5)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, @@ -2967,6 +3006,20 @@ func TestCalcCellValue(t *testing.T) { // FISHERINV "=FISHERINV()": {"#VALUE!", "FISHERINV requires 1 numeric argument"}, "=FISHERINV(F1)": {"#VALUE!", "FISHERINV requires 1 numeric argument"}, + // FORECAST + "=FORECAST()": {"#VALUE!", "FORECAST requires 3 arguments"}, + "=FORECAST(\"\",A1:A7,B1:B7)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=FORECAST(1,A1:A2,B1:B1)": {"#N/A", "#N/A"}, + "=FORECAST(1,A4,A4)": {"#DIV/0!", "#DIV/0!"}, + // FORECAST.LINEAR + "=FORECAST.LINEAR()": {"#VALUE!", "FORECAST.LINEAR requires 3 arguments"}, + "=FORECAST.LINEAR(\"\",A1:A7,B1:B7)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=FORECAST.LINEAR(1,A1:A2,B1:B1)": {"#N/A", "#N/A"}, + "=FORECAST.LINEAR(1,A4,A4)": {"#DIV/0!", "#DIV/0!"}, + // FREQUENCY + "=FREQUENCY()": {"#VALUE!", "FREQUENCY requires 2 arguments"}, + "=FREQUENCY(NA(),A1:A3)": {"#N/A", "#N/A"}, + "=FREQUENCY(A1:A3,NA())": {"#N/A", "#N/A"}, // GAMMA "=GAMMA()": {"#VALUE!", "GAMMA requires 1 numeric argument"}, "=GAMMA(F1)": {"#VALUE!", "GAMMA requires 1 numeric argument"}, @@ -3059,6 +3112,10 @@ func TestCalcCellValue(t *testing.T) { "=HYPGEOMDIST(1,4,4,2)": {"#NUM!", "#NUM!"}, "=HYPGEOMDIST(1,4,0,12)": {"#NUM!", "#NUM!"}, "=HYPGEOMDIST(1,4,4,0)": {"#NUM!", "#NUM!"}, + // INTERCEPT + "=INTERCEPT()": {"#VALUE!", "INTERCEPT requires 2 arguments"}, + "=INTERCEPT(A1:A2,B1:B1)": {"#N/A", "#N/A"}, + "=INTERCEPT(A4,A4)": {"#DIV/0!", "#DIV/0!"}, // KURT "=KURT()": {"#VALUE!", "KURT requires at least 1 argument"}, "=KURT(F1,INT(1))": {"#DIV/0!", "#DIV/0!"}, @@ -3662,6 +3719,11 @@ func TestCalcCellValue(t *testing.T) { "=WEEKNUM(0,0)": {"#NUM!", "#NUM!"}, "=WEEKNUM(-1,1)": {"#NUM!", "#NUM!"}, // Text Functions + // ARRAYTOTEXT + "=ARRAYTOTEXT()": {"#VALUE!", "ARRAYTOTEXT requires at least 1 argument"}, + "=ARRAYTOTEXT(A1,0,0)": {"#VALUE!", "ARRAYTOTEXT allows at most 2 arguments"}, + "=ARRAYTOTEXT(A1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=ARRAYTOTEXT(A1,2)": {"#VALUE!", "#VALUE!"}, // CHAR "=CHAR()": {"#VALUE!", "CHAR requires 1 argument"}, "=CHAR(-1)": {"#VALUE!", "#VALUE!"}, @@ -3779,6 +3841,11 @@ func TestCalcCellValue(t *testing.T) { // VALUE "=VALUE()": {"#VALUE!", "VALUE requires 1 argument"}, "=VALUE(\"\")": {"#VALUE!", "#VALUE!"}, + // VALUETOTEXT + "=VALUETOTEXT()": {"#VALUE!", "VALUETOTEXT requires at least 1 argument"}, + "=VALUETOTEXT(A1,0,0)": {"#VALUE!", "VALUETOTEXT allows at most 2 arguments"}, + "=VALUETOTEXT(A1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=VALUETOTEXT(A1,2)": {"#VALUE!", "#VALUE!"}, // UPPER "=UPPER()": {"#VALUE!", "UPPER requires 1 argument"}, "=UPPER(1,2)": {"#VALUE!", "UPPER requires 1 argument"}, @@ -4187,6 +4254,60 @@ func TestCalcCellValue(t *testing.T) { "=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"}, + // ODDFYIELD + "=ODDFYIELD()": {"#VALUE!", "ODDFYIELD requires 8 or 9 arguments"}, + "=ODDFYIELD(\"\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,2)": {"#VALUE!", "#VALUE!"}, + "=ODDFYIELD(\"02/01/2017\",\"\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,2)": {"#VALUE!", "#VALUE!"}, + "=ODDFYIELD(\"02/01/2017\",\"03/31/2021\",\"\",\"03/31/2017\",5.5%,3.5%,100,2)": {"#VALUE!", "#VALUE!"}, + "=ODDFYIELD(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"\",5.5%,3.5%,100,2)": {"#VALUE!", "#VALUE!"}, + "=ODDFYIELD(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",\"\",3.5%,100,2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=ODDFYIELD(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,\"\",100,2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=ODDFYIELD(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,\"\",2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=ODDFYIELD(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=ODDFYIELD(\"02/01/2017\",\"03/31/2021\",\"02/01/2017\",\"03/31/2017\",5.5%,3.5%,100,2)": {"#NUM!", "ODDFYIELD requires settlement > issue"}, + "=ODDFYIELD(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"02/01/2017\",5.5%,3.5%,100,2)": {"#NUM!", "ODDFYIELD requires first_coupon > settlement"}, + "=ODDFYIELD(\"02/01/2017\",\"02/01/2017\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,2)": {"#NUM!", "ODDFYIELD requires maturity > first_coupon"}, + "=ODDFYIELD(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",-1,3.5%,100,2)": {"#NUM!", "ODDFYIELD requires rate >= 0"}, + "=ODDFYIELD(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,0,100,2)": {"#NUM!", "ODDFYIELD requires pr > 0"}, + "=ODDFYIELD(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,0,2)": {"#NUM!", "ODDFYIELD requires redemption > 0"}, + "=ODDFYIELD(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,2,\"\")": {"#NUM!", "#NUM!"}, + "=ODDFYIELD(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,3)": {"#NUM!", "#NUM!"}, + "=ODDFYIELD(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/30/2017\",5.5%,3.5%,100,4)": {"#NUM!", "#NUM!"}, + "=ODDFYIELD(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,2,5)": {"#NUM!", "invalid basis"}, + // ODDLPRICE + "=ODDLPRICE()": {"#VALUE!", "ODDLPRICE requires 7 or 8 arguments"}, + "=ODDLPRICE(\"\",\"03/31/2021\",\"12/01/2016\",5.5%,3.5%,100,2)": {"#VALUE!", "#VALUE!"}, + "=ODDLPRICE(\"02/01/2017\",\"\",\"12/01/2016\",5.5%,3.5%,100,2)": {"#VALUE!", "#VALUE!"}, + "=ODDLPRICE(\"02/01/2017\",\"03/31/2021\",\"\",5.5%,3.5%,100,2)": {"#VALUE!", "#VALUE!"}, + "=ODDLPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"\",3.5%,100,2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=ODDLPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",5.5%,\"\",100,2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=ODDLPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",5.5%,3.5%,\"\",2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=ODDLPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",5.5%,3.5%,100,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=ODDLPRICE(\"04/20/2008\",\"06/15/2008\",\"04/30/2008\",3.75%,99.875,100,2)": {"#NUM!", "ODDLPRICE requires settlement > last_interest"}, + "=ODDLPRICE(\"06/20/2008\",\"06/15/2008\",\"04/30/2008\",3.75%,99.875,100,2)": {"#NUM!", "ODDLPRICE requires maturity > settlement"}, + "=ODDLPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",-1,3.5%,100,2)": {"#NUM!", "ODDLPRICE requires rate >= 0"}, + "=ODDLPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",5.5%,-1,100,2)": {"#NUM!", "ODDLPRICE requires yld >= 0"}, + "=ODDLPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",5.5%,3.5%,0,2)": {"#NUM!", "ODDLPRICE requires redemption > 0"}, + "=ODDLPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",5.5%,3.5%,100,2,\"\")": {"#NUM!", "#NUM!"}, + "=ODDLPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",5.5%,3.5%,100,3)": {"#NUM!", "#NUM!"}, + "=ODDLPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",5.5%,3.5%,100,2,5)": {"#NUM!", "invalid basis"}, + // ODDLYIELD + "=ODDLYIELD()": {"#VALUE!", "ODDLYIELD requires 7 or 8 arguments"}, + "=ODDLYIELD(\"\",\"03/31/2021\",\"12/01/2016\",5.5%,3.5%,100,2)": {"#VALUE!", "#VALUE!"}, + "=ODDLYIELD(\"02/01/2017\",\"\",\"12/01/2016\",5.5%,3.5%,100,2)": {"#VALUE!", "#VALUE!"}, + "=ODDLYIELD(\"02/01/2017\",\"03/31/2021\",\"\",5.5%,3.5%,100,2)": {"#VALUE!", "#VALUE!"}, + "=ODDLYIELD(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"\",3.5%,100,2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=ODDLYIELD(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",5.5%,\"\",100,2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=ODDLYIELD(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",5.5%,3.5%,\"\",2)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=ODDLYIELD(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",5.5%,3.5%,100,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=ODDLYIELD(\"04/20/2008\",\"06/15/2008\",\"04/30/2008\",3.75%,99.875,100,2)": {"#NUM!", "ODDLYIELD requires settlement > last_interest"}, + "=ODDLYIELD(\"06/20/2008\",\"06/15/2008\",\"04/30/2008\",3.75%,99.875,100,2)": {"#NUM!", "ODDLYIELD requires maturity > settlement"}, + "=ODDLYIELD(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",-1,3.5%,100,2)": {"#NUM!", "ODDLYIELD requires rate >= 0"}, + "=ODDLYIELD(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",5.5%,0,100,2)": {"#NUM!", "ODDLYIELD requires pr > 0"}, + "=ODDLYIELD(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",5.5%,3.5%,0,2)": {"#NUM!", "ODDLYIELD requires redemption > 0"}, + "=ODDLYIELD(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",5.5%,3.5%,100,2,\"\")": {"#NUM!", "#NUM!"}, + "=ODDLYIELD(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",5.5%,3.5%,100,3)": {"#NUM!", "#NUM!"}, + "=ODDLYIELD(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",5.5%,3.5%,100,2,5)": {"#NUM!", "invalid basis"}, // PDURATION "=PDURATION()": {"#VALUE!", "PDURATION requires 3 arguments"}, "=PDURATION(\"\",0,0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, @@ -5500,6 +5621,42 @@ func TestCalcPEARSON(t *testing.T) { } } +func TestCalcPROB(t *testing.T) { + cellData := [][]interface{}{ + {"x", "probability"}, + {0, 0.1}, + {1, 0.15}, + {2, 0.17}, + {3, 0.22}, + {4, 0.21}, + {5, 0.09}, + {6, 0.05}, + {7, 0.01}, + } + f := prepareCalcData(cellData) + formulaList := map[string]string{ + "=PROB(A2:A9,B2:B9,3)": "0.22", + "=PROB(A2:A9,B2:B9,3,5)": "0.52", + "=PROB(A2:A9,B2:B9,8,10)": "0", + } + 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) + } + assert.NoError(t, f.SetCellFormula("Sheet1", "A2", "=NA()")) + calcError := map[string][]string{ + "=PROB(A2:A9,B2:B9,3)": {"#NUM!", "#NUM!"}, + } + for formula, expected := range calcError { + assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) + result, err := f.CalcCellValue("Sheet1", "C1") + assert.Equal(t, expected[0], result, formula) + assert.EqualError(t, err, expected[1], formula) + } +} + func TestCalcRSQ(t *testing.T) { cellData := [][]interface{}{ {"known_y's", "known_x's"}, From 3b2b8ca8d6130723f494b773e462b03cfa011140 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 28 Aug 2023 00:02:25 +0800 Subject: [PATCH 785/957] Update the README and documentation for the data validation functions --- README.md | 2 +- README_zh.md | 2 +- datavalidation.go | 16 ++++++++++++---- styles.go | 2 +- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 460ace4092..8eea55f0f1 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.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/). +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. There are some [incompatible changes](https://github.com/golang/go/issues/61881) in the Go 1.21.0, the Excelize library can not working with that version normally, if you are using the Go 1.21.x, please upgrade to the Go 1.21.1 and later version. 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 8bb3068ca3..5f82bcd6a1 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 等多种文档格式,高度兼容带有样式、图片(表)、透视表、切片器等复杂组件的文档,并提供流式读写函数,用于处理包含大规模数据的工作簿。可应用于各类报表平台、云计算、边缘计算等系统。使用本类库要求使用的 Go 语言为 1.16 或更高版本,完整的使用文档请访问 [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 1.21.0 中存在[不兼容的更改](https://github.com/golang/go/issues/61881),导致 Excelize 基础库无法在该版本上正常工作,如果您使用的是 Go 1.21.x,请升级到 Go 1.21.1 及更高版本。完整的使用文档请访问 [go.dev](https://pkg.go.dev/github.com/xuri/excelize/v2) 或查看 [参考文档](https://xuri.me/excelize/)。 ## 快速上手 diff --git a/datavalidation.go b/datavalidation.go index ac4aaec57c..37e1f4394a 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -112,7 +112,12 @@ func (dv *DataValidation) SetInput(title, msg string) { dv.Prompt = &msg } -// SetDropList data validation list. +// SetDropList data validation list. If you type the items into the data +// validation dialog box (a delimited list), the limit is 255 characters, +// including the separators. If your data validation list source formula is +// over the maximum length limit, please set the allowed values in the +// worksheet cells, and use the SetSqrefDropList function to set the reference +// for their cells. func (dv *DataValidation) SetDropList(keys []string) error { formula := strings.Join(keys, ",") if MaxFieldLength < len(utf16.Encode([]rune(formula))) { @@ -162,9 +167,12 @@ func (dv *DataValidation) SetRange(f1, f2 interface{}, t DataValidationType, o D // SetSqrefDropList provides set data validation on a range with source // reference range of the worksheet by given data validation object and // worksheet name. The data validation object can be created by -// NewDataValidation function. For example, set data validation on -// Sheet1!A7:B8 with validation criteria source Sheet1!E1:E3 settings, create -// in-cell dropdown by allowing list source: +// NewDataValidation function. There are limits to the number of items that +// will show in a data validation drop down list: The list can show up to show +// 32768 items from a list on the worksheet. If you need more items than that, +// you could create a dependent drop down list, broken down by category. For +// example, set data validation on Sheet1!A7:B8 with validation criteria source +// Sheet1!E1:E3 settings, create in-cell dropdown by allowing list source: // // dv := excelize.NewDataValidation(true) // dv.Sqref = "A7:B8" diff --git a/styles.go b/styles.go index 05d49511e9..fe2e3f69fc 100644 --- a/styles.go +++ b/styles.go @@ -1302,7 +1302,7 @@ func (f *File) extractProtection(xf xlsxXf, s *xlsxStyleSheet, style *Style) { } } -// GetStyle get style details by given style index. +// GetStyle provides a function to get style definition by given style index. func (f *File) GetStyle(idx int) (*Style, error) { var style *Style f.mu.Lock() From ff5657ba87f7fbae2c5352fb69e3f975d0e6e2e4 Mon Sep 17 00:00:00 2001 From: Francis Nickels III Date: Sun, 3 Sep 2023 09:11:35 -0700 Subject: [PATCH 786/957] add Footer & Header clarification to docs (#1644) --- sheet.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sheet.go b/sheet.go index bc35aa7e2d..b93c231793 100644 --- a/sheet.go +++ b/sheet.go @@ -1123,8 +1123,8 @@ func attrValToBool(name string, attrs []xml.Attr) (val bool, err error) { // 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 +// OddFooter | Odd Page Footer, or primary Page Footer if 'DifferentOddEven' is 'false' +// OddHeader | Odd Header, or primary Page Header if 'DifferentOddEven' is 'false' // EvenFooter | Even Page Footer // EvenHeader | Even Page Header // FirstFooter | First Page Footer From ae64bcaabea65d9bafa4eed1753ca961f2f5e9c2 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 8 Sep 2023 00:09:41 +0800 Subject: [PATCH 787/957] This fixes #1643, fixes #1647 and fixes #1653 - Correction cell type when formatting date type cell value - Add check for MID and MIDB formula functions num_chars arguments, prevent panic on specifying a negative number - Ref #65, add support for 2 formula functions: SEARCH and SEARCHB - Fix a v2.8.0 regression bug, error on set print area and print titles with built-in special defined name - Add new exported function `GetPivotTables` for get pivot tables - Add a new `Name` field in the `PivotTableOptions` to support specify pivot table name - Using relative cell reference in the pivot table docs and unit tests - Support adding slicer content type part internally - Add new exported source relationship and namespace `NameSpaceSpreadSheetXR10`, `ContentTypeSlicer`, `ContentTypeSlicerCache`, and `SourceRelationshipSlicer` - Add new exported extended URI `ExtURIPivotCacheDefinition` - Fix formula argument wildcard match issues - Update GitHub Actions configuration, test on Go 1.21.x with 1.21.1 and later - Avoid corrupted workbooks generated by improving compatibility with internally indexed color styles --- .github/workflows/go.yml | 2 +- calc.go | 187 +++++++++++++++++++++++++++------------ calc_test.go | 60 ++++++++++--- cell.go | 2 +- picture.go | 4 + pivotTable.go | 183 ++++++++++++++++++++++++++++++++++++-- pivotTable_test.go | 179 ++++++++++++++++++++++++------------- sheet.go | 2 +- sheet_test.go | 12 ++- sparkline_test.go | 6 +- styles.go | 2 +- styles_test.go | 7 +- table.go | 5 +- xmlDrawing.go | 8 ++ xmlStyles.go | 4 +- 15 files changed, 504 insertions(+), 159 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 4f26b1b9dc..9a1633c3ae 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, 1.20.x] + go-version: [1.16.x, 1.17.x, 1.18.x, 1.19.x, 1.20.x, '>=1.21.1'] os: [ubuntu-latest, macos-latest, windows-latest] targetplatform: [x86, x64] diff --git a/calc.go b/calc.go index 8560675d55..459616f752 100644 --- a/calc.go +++ b/calc.go @@ -706,6 +706,8 @@ type formulaFuncs struct { // ROWS // RRI // RSQ +// SEARCH +// SEARCHB // SEC // SECH // SECOND @@ -9303,7 +9305,7 @@ func (fn *formulaFuncs) FdotDISTdotRT(argsList *list.List) formulaArg { return fn.FDIST(argsList) } -// prepareFinvArgs checking and prepare arguments for the formula function +// prepareFinvArgs checking and prepare arguments for the formula functions // F.INV, F.INV.RT and FINV. func (fn *formulaFuncs) prepareFinvArgs(name string, argsList *list.List) formulaArg { if argsList.Len() != 3 { @@ -13612,17 +13614,16 @@ func (fn *formulaFuncs) FINDB(argsList *list.List) formulaArg { return fn.find("FINDB", argsList) } -// find is an implementation of the formula functions FIND and FINDB. -func (fn *formulaFuncs) find(name string, argsList *list.List) formulaArg { +// prepareFindArgs checking and prepare arguments for the formula functions +// FIND, FINDB, SEARCH and SEARCHB. +func (fn *formulaFuncs) prepareFindArgs(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)) } - findText := argsList.Front().Value.(formulaArg).Value() - withinText := argsList.Front().Next().Value.(formulaArg).Value() - startNum, result := 1, 1 + startNum := 1 if argsList.Len() == 3 { numArg := argsList.Back().Value.(formulaArg).ToNumber() if numArg.Type != ArgNumber { @@ -13633,19 +13634,44 @@ func (fn *formulaFuncs) find(name string, argsList *list.List) formulaArg { } startNum = int(numArg.Number) } + return newListFormulaArg([]formulaArg{newNumberFormulaArg(float64(startNum))}) +} + +// find is an implementation of the formula functions FIND, FINDB, SEARCH and +// SEARCHB. +func (fn *formulaFuncs) find(name string, argsList *list.List) formulaArg { + args := fn.prepareFindArgs(name, argsList) + if args.Type != ArgList { + return args + } + findText := argsList.Front().Value.(formulaArg).Value() + withinText := argsList.Front().Next().Value.(formulaArg).Value() + startNum := int(args.List[0].Number) if findText == "" { return newNumberFormulaArg(float64(startNum)) } - for idx := range withinText { - if result < startNum { - result++ - } - if strings.Index(withinText[idx:], findText) == 0 { - return newNumberFormulaArg(float64(result)) + dbcs, search := name == "FINDB" || name == "SEARCHB", name == "SEARCH" || name == "SEARCHB" + if search { + findText, withinText = strings.ToUpper(findText), strings.ToUpper(withinText) + } + offset, ok := matchPattern(findText, withinText, dbcs, startNum) + if !ok { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + result := offset + if dbcs { + var pre int + for idx := range withinText { + if pre > offset { + break + } + if idx-pre > 1 { + result++ + } + pre = idx } - result++ } - return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + return newNumberFormulaArg(float64(result)) } // LEFT function returns a specified number of characters from the start of a @@ -13780,20 +13806,37 @@ func (fn *formulaFuncs) mid(name string, argsList *list.List) formulaArg { return numCharsArg } startNum := int(startNumArg.Number) - if startNum < 0 { + if startNum < 1 || numCharsArg.Number < 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:]) + var result string + var cnt, offset int + for _, char := range text { + offset++ + var dbcs bool + if utf8.RuneLen(char) > 1 { + dbcs = true + offset++ + } + if cnt == int(numCharsArg.Number) { + break + } + if offset+1 > startNum { + if dbcs { + if cnt+2 > int(numCharsArg.Number) { + result += string(char)[:1] + break + } + result += string(char) + cnt += 2 + } else { + result += string(char) + cnt++ + } + } } - return newStringFormulaArg(text[startNum:endNum]) + return newStringFormulaArg(result) } // MID textLen := utf8.RuneCountInString(text) @@ -13922,6 +13965,23 @@ func (fn *formulaFuncs) RIGHTB(argsList *list.List) formulaArg { return fn.leftRight("RIGHTB", argsList) } +// SEARCH function returns the position of a specified character or sub-string +// within a supplied text string. The syntax of the function is: +// +// SEARCH(search_text,within_text,[start_num]) +func (fn *formulaFuncs) SEARCH(argsList *list.List) formulaArg { + return fn.find("SEARCH", argsList) +} + +// SEARCHB functions locate one text string within a second text string, and +// return the number of the starting position of the first text string from the +// first character of the second text string. The syntax of the function is: +// +// SEARCHB(search_text,within_text,[start_num]) +func (fn *formulaFuncs) SEARCHB(argsList *list.List) formulaArg { + return fn.find("SEARCHB", argsList) +} + // SUBSTITUTE function replaces one or more instances of a given text string, // within an original text string. The syntax of the function is: // @@ -14255,46 +14315,57 @@ func (fn *formulaFuncs) CHOOSE(argsList *list.List) formulaArg { return arg.Value.(formulaArg) } -// deepMatchRune finds whether the text deep matches/satisfies the pattern -// string. -func deepMatchRune(str, pattern []rune, simple bool) bool { - for len(pattern) > 0 { - switch pattern[0] { - default: - if len(str) == 0 || str[0] != pattern[0] { - return false - } - case '?': - if len(str) == 0 && !simple { - return false - } - case '*': - return deepMatchRune(str, pattern[1:], simple) || - (len(str) > 0 && deepMatchRune(str[1:], pattern, simple)) +// matchPatternToRegExp convert find text pattern to regular expression. +func matchPatternToRegExp(findText string, dbcs bool) (string, bool) { + var ( + exp string + wildCard bool + mark = "." + ) + if dbcs { + mark = "(?:(?:[\\x00-\\x0081])|(?:[\\xFF61-\\xFFA0])|(?:[\\xF8F1-\\xF8F4])|[0-9A-Za-z])" + } + for _, char := range findText { + if strings.ContainsAny(string(char), ".+$^[](){}|/") { + exp += fmt.Sprintf("\\%s", string(char)) + continue + } + if char == '?' { + wildCard = true + exp += mark + continue + } + if char == '*' { + wildCard = true + exp += ".*" + continue } - str = str[1:] - pattern = pattern[1:] + exp += string(char) } - return len(str) == 0 && len(pattern) == 0 + return fmt.Sprintf("^%s", exp), wildCard } // matchPattern finds whether the text matches or satisfies the pattern // string. The pattern supports '*' and '?' wildcards in the pattern string. -func matchPattern(pattern, name string) (matched bool) { - if pattern == "" { - return name == pattern - } - if pattern == "*" { - return true - } - rName, rPattern := make([]rune, 0, len(name)), make([]rune, 0, len(pattern)) - for _, r := range name { - rName = append(rName, r) - } - for _, r := range pattern { - rPattern = append(rPattern, r) +func matchPattern(findText, withinText string, dbcs bool, startNum int) (int, bool) { + exp, wildCard := matchPatternToRegExp(findText, dbcs) + offset := 1 + for idx := range withinText { + if offset < startNum { + offset++ + continue + } + if wildCard { + if ok, _ := regexp.MatchString(exp, withinText[idx:]); ok { + break + } + } + if strings.Index(withinText[idx:], findText) == 0 { + break + } + offset++ } - return deepMatchRune(rName, rPattern, false) + return offset, utf8.RuneCountInString(withinText) != offset-1 } // compareFormulaArg compares the left-hand sides and the right-hand sides' @@ -14319,7 +14390,7 @@ func compareFormulaArg(lhs, rhs, matchMode formulaArg, caseSensitive bool) byte ls, rs = strings.ToLower(ls), strings.ToLower(rs) } if matchMode.Number == matchModeWildcard { - if matchPattern(rs, ls) { + if _, ok := matchPattern(rs, ls, false, 0); ok { return criteriaEq } } diff --git a/calc_test.go b/calc_test.go index 65ace82095..ba5a35b4f5 100644 --- a/calc_test.go +++ b/calc_test.go @@ -764,6 +764,30 @@ func TestCalcCellValue(t *testing.T) { "=ROUNDUP(-11.111,2)": "-11.12", "=ROUNDUP(-11.111,-1)": "-20", "=ROUNDUP(ROUNDUP(100,1),-1)": "100", + // SEARCH + "=SEARCH(\"s\",F1)": "1", + "=SEARCH(\"s\",F1,2)": "5", + "=SEARCH(\"e\",F1)": "4", + "=SEARCH(\"e*\",F1)": "4", + "=SEARCH(\"?e\",F1)": "3", + "=SEARCH(\"??e\",F1)": "2", + "=SEARCH(6,F2)": "2", + "=SEARCH(\"?\",\"你好world\")": "1", + "=SEARCH(\"?l\",\"你好world\")": "5", + "=SEARCH(\"?+\",\"你好 1+2\")": "4", + "=SEARCH(\" ?+\",\"你好 1+2\")": "3", + // SEARCHB + "=SEARCHB(\"s\",F1)": "1", + "=SEARCHB(\"s\",F1,2)": "5", + "=SEARCHB(\"e\",F1)": "4", + "=SEARCHB(\"e*\",F1)": "4", + "=SEARCHB(\"?e\",F1)": "3", + "=SEARCHB(\"??e\",F1)": "2", + "=SEARCHB(6,F2)": "2", + "=SEARCHB(\"?\",\"你好world\")": "5", + "=SEARCHB(\"?l\",\"你好world\")": "7", + "=SEARCHB(\"?+\",\"你好 1+2\")": "6", + "=SEARCHB(\" ?+\",\"你好 1+2\")": "5", // SEC "=_xlfn.SEC(-3.14159265358979)": "-1", "=_xlfn.SEC(0)": "1", @@ -1707,6 +1731,7 @@ func TestCalcCellValue(t *testing.T) { "=FIND(\"i\",\"Original Text\",4)": "5", "=FIND(\"\",\"Original Text\")": "1", "=FIND(\"\",\"Original Text\",2)": "2", + "=FIND(\"s\",\"Sales\",2)": "5", // FINDB "=FINDB(\"T\",\"Original Text\")": "10", "=FINDB(\"t\",\"Original Text\")": "13", @@ -1714,6 +1739,7 @@ func TestCalcCellValue(t *testing.T) { "=FINDB(\"i\",\"Original Text\",4)": "5", "=FINDB(\"\",\"Original Text\")": "1", "=FINDB(\"\",\"Original Text\",2)": "2", + "=FINDB(\"s\",\"Sales\",2)": "5", // LEFT "=LEFT(\"Original Text\")": "O", "=LEFT(\"Original Text\",4)": "Orig", @@ -1752,14 +1778,18 @@ 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)": "ジナルテキ", + "=MID(\"你好World\",5,1)": "r", + "=MID(\"\u30AA\u30EA\u30B8\u30CA\u30EB\u30C6\u30AD\u30B9\u30C8\",6,4)": "\u30C6\u30AD\u30B9\u30C8", + "=MID(\"\u30AA\u30EA\u30B8\u30CA\u30EB\u30C6\u30AD\u30B9\u30C8\",3,5)": "\u30B8\u30CA\u30EB\u30C6\u30AD", // MIDB "=MIDB(\"Original Text\",7,1)": "a", "=MIDB(\"Original Text\",4,7)": "ginal T", "=MIDB(\"255 years\",3,1)": "5", "=MIDB(\"text\",3,6)": "xt", "=MIDB(\"text\",6,0)": "", + "=MIDB(\"你好World\",5,1)": "W", + "=MIDB(\"\u30AA\u30EA\u30B8\u30CA\u30EB\u30C6\u30AD\u30B9\u30C8\",6,4)": "\u30B8\u30CA", + "=MIDB(\"\u30AA\u30EA\u30B8\u30CA\u30EB\u30C6\u30AD\u30B9\u30C8\",3,5)": "\u30EA\u30B8\xe3", // PROPER "=PROPER(\"this is a test sentence\")": "This Is A Test Sentence", "=PROPER(\"THIS IS A TEST SENTENCE\")": "This Is A Test Sentence", @@ -2695,6 +2725,17 @@ func TestCalcCellValue(t *testing.T) { "=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"}, + // SEARCH + "=SEARCH()": {"#VALUE!", "SEARCH requires at least 2 arguments"}, + "=SEARCH(1,A1,1,1)": {"#VALUE!", "SEARCH allows at most 3 arguments"}, + "=SEARCH(2,A1)": {"#VALUE!", "#VALUE!"}, + "=SEARCH(1,A1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + // SEARCHB + "=SEARCHB()": {"#VALUE!", "SEARCHB requires at least 2 arguments"}, + "=SEARCHB(1,A1,1,1)": {"#VALUE!", "SEARCHB allows at most 3 arguments"}, + "=SEARCHB(2,A1)": {"#VALUE!", "#VALUE!"}, + "=SEARCHB(\"?w\",\"你好world\")": {"#VALUE!", "#VALUE!"}, + "=SEARCHB(1,A1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // SEC "=_xlfn.SEC()": {"#VALUE!", "SEC requires 1 numeric argument"}, `=_xlfn.SEC("X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, @@ -3781,12 +3822,14 @@ func TestCalcCellValue(t *testing.T) { "=LOWER(1,2)": {"#VALUE!", "LOWER requires 1 argument"}, // MID "=MID()": {"#VALUE!", "MID requires 3 arguments"}, - "=MID(\"\",-1,1)": {"#VALUE!", "#VALUE!"}, + "=MID(\"\",0,1)": {"#VALUE!", "#VALUE!"}, + "=MID(\"\",1,-1)": {"#VALUE!", "#VALUE!"}, "=MID(\"\",\"\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, "=MID(\"\",1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // MIDB "=MIDB()": {"#VALUE!", "MIDB requires 3 arguments"}, - "=MIDB(\"\",-1,1)": {"#VALUE!", "#VALUE!"}, + "=MIDB(\"\",0,1)": {"#VALUE!", "#VALUE!"}, + "=MIDB(\"\",1,-1)": {"#VALUE!", "#VALUE!"}, "=MIDB(\"\",\"\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, "=MIDB(\"\",1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // PROPER @@ -4684,14 +4727,6 @@ func TestCalcCompareFormulaArg(t *testing.T) { assert.Equal(t, compareFormulaArg(formulaArg{Type: ArgUnknown}, formulaArg{Type: ArgUnknown}, newNumberFormulaArg(matchModeMaxLess), false), criteriaErr) } -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")) -} - func TestCalcTRANSPOSE(t *testing.T) { cellData := [][]interface{}{ {"a", "d"}, @@ -5376,7 +5411,6 @@ func TestCalcXLOOKUP(t *testing.T) { "=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!"}, diff --git a/cell.go b/cell.go index c2ac31f49d..36265d9fe0 100644 --- a/cell.go +++ b/cell.go @@ -565,7 +565,7 @@ func (c *xlsxC) getCellDate(f *File, raw bool) (string, error) { c.V = strconv.FormatFloat(excelTime, 'G', 15, 64) } } - return f.formattedValue(c, raw, CellTypeBool) + return f.formattedValue(c, raw, CellTypeDate) } // getValueFrom return a value from a column/row cell, this function is diff --git a/picture.go b/picture.go index 1d6b5f06f2..b8978ea950 100644 --- a/picture.go +++ b/picture.go @@ -506,6 +506,8 @@ func (f *File) addContentTypePart(index int, contentType string) error { "pivotTable": "/xl/pivotTables/pivotTable" + strconv.Itoa(index) + ".xml", "pivotCache": "/xl/pivotCache/pivotCacheDefinition" + strconv.Itoa(index) + ".xml", "sharedStrings": "/xl/sharedStrings.xml", + "slicer": "/xl/slicers/slicer" + strconv.Itoa(index) + ".xml", + "slicerCache": "/xl/slicerCaches/slicerCache" + strconv.Itoa(index) + ".xml", } contentTypes := map[string]string{ "chart": ContentTypeDrawingML, @@ -516,6 +518,8 @@ func (f *File) addContentTypePart(index int, contentType string) error { "pivotTable": ContentTypeSpreadSheetMLPivotTable, "pivotCache": ContentTypeSpreadSheetMLPivotCacheDefinition, "sharedStrings": ContentTypeSpreadSheetMLSharedStrings, + "slicer": ContentTypeSlicer, + "slicerCache": ContentTypeSlicerCache, } s, ok := setContentType[contentType] if ok { diff --git a/pivotTable.go b/pivotTable.go index 4c8dee282c..f98e93b8de 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -12,10 +12,17 @@ package excelize import ( + "bytes" "encoding/xml" "fmt" + "io" + "path/filepath" + "reflect" "strconv" "strings" + + "golang.org/x/text/cases" + "golang.org/x/text/language" ) // PivotTableOptions directly maps the format settings of the pivot table. @@ -29,6 +36,7 @@ type PivotTableOptions struct { pivotTableSheetName string DataRange string PivotTableRange string + Name string Rows []PivotTableField Columns []PivotTableField Data []PivotTableField @@ -115,8 +123,8 @@ type PivotTableField struct { // f.SetCellValue("Sheet1", fmt.Sprintf("E%d", row), region[rand.Intn(4)]) // } // if err := f.AddPivotTable(&excelize.PivotTableOptions{ -// DataRange: "Sheet1!$A$1:$E$31", -// PivotTableRange: "Sheet1!$G$2:$M$34", +// DataRange: "Sheet1!A1:E31", +// PivotTableRange: "Sheet1!G2:M34", // Rows: []excelize.PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, // Filter: []excelize.PivotTableField{{Data: "Region"}}, // Columns: []excelize.PivotTableField{{Data: "Type", DefaultSubtotal: true}}, @@ -181,6 +189,9 @@ func (f *File) parseFormatPivotTableSet(opts *PivotTableOptions) (*xlsxWorksheet if err != nil { return nil, "", fmt.Errorf("parameter 'PivotTableRange' parsing error: %s", err.Error()) } + if len(opts.Name) > MaxFieldLength { + return nil, "", ErrNameLength + } opts.pivotTableSheetName = pivotTableSheetName dataRange := f.getDefinedNameRefTo(opts.DataRange, pivotTableSheetName) if dataRange == "" { @@ -334,7 +345,7 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op return opts.PivotTableStyleName } pt := xlsxPivotTableDefinition{ - Name: fmt.Sprintf("Pivot Table%d", pivotTableID), + Name: opts.Name, CacheID: cacheID, RowGrandTotals: &opts.RowGrandTotals, ColGrandTotals: &opts.ColGrandTotals, @@ -376,7 +387,9 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op ShowLastColumn: opts.ShowLastColumn, }, } - + if pt.Name == "" { + pt.Name = fmt.Sprintf("PivotTable%d", pivotTableID) + } // pivot fields _ = f.addPivotFields(&pt, opts) @@ -604,8 +617,8 @@ func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opts *PivotTableOpti return err } -// countPivotTables provides a function to get drawing files count storage in -// the folder xl/pivotTables. +// countPivotTables provides a function to get pivot table files count storage +// in the folder xl/pivotTables. func (f *File) countPivotTables() int { count := 0 f.Pkg.Range(func(k, v interface{}) bool { @@ -617,8 +630,8 @@ func (f *File) countPivotTables() int { return count } -// countPivotCache provides a function to get drawing files count storage in -// the folder xl/pivotCache. +// countPivotCache provides a function to get pivot table cache definition files +// count storage in the folder xl/pivotCache. func (f *File) countPivotCache() int { count := 0 f.Pkg.Range(func(k, v interface{}) bool { @@ -719,3 +732,157 @@ func (f *File) addWorkbookPivotCache(RID int) int { }) return cacheID } + +// GetPivotTables returns all pivot table definitions in a worksheet by given +// worksheet name. +func (f *File) GetPivotTables(sheet string) ([]PivotTableOptions, error) { + var pivotTables []PivotTableOptions + name, ok := f.getSheetXMLPath(sheet) + if !ok { + return pivotTables, newNoExistSheetError(sheet) + } + rels := "xl/worksheets/_rels/" + strings.TrimPrefix(name, "xl/worksheets/") + ".rels" + sheetRels, err := f.relsReader(rels) + if err != nil { + return pivotTables, err + } + if sheetRels == nil { + sheetRels = &xlsxRelationships{} + } + for _, v := range sheetRels.Relationships { + if v.Type == SourceRelationshipPivotTable { + pivotTableXML := strings.ReplaceAll(v.Target, "..", "xl") + pivotCacheRels := "xl/pivotTables/_rels/" + filepath.Base(v.Target) + ".rels" + pivotTable, err := f.getPivotTable(sheet, pivotTableXML, pivotCacheRels) + if err != nil { + return pivotTables, err + } + pivotTables = append(pivotTables, pivotTable) + } + } + return pivotTables, nil +} + +// getPivotTable provides a function to get a pivot table definition by given +// worksheet name, pivot table XML path and pivot cache relationship XML path. +func (f *File) getPivotTable(sheet, pivotTableXML, pivotCacheRels string) (PivotTableOptions, error) { + var opts PivotTableOptions + rels, err := f.relsReader(pivotCacheRels) + if err != nil { + return opts, err + } + var pivotCacheXML string + for _, v := range rels.Relationships { + if v.Type == SourceRelationshipPivotCache { + pivotCacheXML = strings.ReplaceAll(v.Target, "..", "xl") + break + } + } + pc, err := f.pivotCacheReader(pivotCacheXML) + if err != nil { + return opts, err + } + pt, err := f.pivotTableReader(pivotTableXML) + if err != nil { + return opts, err + } + dataRange := fmt.Sprintf("%s!%s", pc.CacheSource.WorksheetSource.Sheet, pc.CacheSource.WorksheetSource.Ref) + opts = PivotTableOptions{ + pivotTableSheetName: sheet, + DataRange: dataRange, + PivotTableRange: fmt.Sprintf("%s!%s", sheet, pt.Location.Ref), + Name: pt.Name, + } + fields := []string{"RowGrandTotals", "ColGrandTotals", "ShowDrill", "UseAutoFormatting", "PageOverThenDown", "MergeItem", "CompactData", "ShowError"} + immutable, mutable := reflect.ValueOf(*pt), reflect.ValueOf(&opts).Elem() + for _, field := range fields { + immutableField := immutable.FieldByName(field) + if immutableField.Kind() == reflect.Ptr && !immutableField.IsNil() && immutableField.Elem().Kind() == reflect.Bool { + mutable.FieldByName(field).SetBool(immutableField.Elem().Bool()) + } + } + if si := pt.PivotTableStyleInfo; si != nil { + opts.ShowRowHeaders = si.ShowRowHeaders + opts.ShowColHeaders = si.ShowColHeaders + opts.ShowRowStripes = si.ShowRowStripes + opts.ShowColStripes = si.ShowColStripes + opts.ShowLastColumn = si.ShowLastColumn + opts.PivotTableStyleName = si.Name + } + order, _ := f.getPivotFieldsOrder(&PivotTableOptions{DataRange: dataRange, pivotTableSheetName: pt.Name}) + f.extractPivotTableFields(order, pt, &opts) + return opts, err +} + +// pivotTableReader provides a function to get the pointer to the structure +// after deserialization of xl/pivotTables/pivotTable%d.xml. +func (f *File) pivotTableReader(path string) (*xlsxPivotTableDefinition, error) { + content, ok := f.Pkg.Load(path) + pivotTable := &xlsxPivotTableDefinition{} + if ok && content != nil { + if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(content.([]byte)))). + Decode(pivotTable); err != nil && err != io.EOF { + return nil, err + } + } + return pivotTable, nil +} + +// pivotCacheReader provides a function to get the pointer to the structure +// after deserialization of xl/pivotCache/pivotCacheDefinition%d.xml. +func (f *File) pivotCacheReader(path string) (*xlsxPivotCacheDefinition, error) { + content, ok := f.Pkg.Load(path) + pivotCache := &xlsxPivotCacheDefinition{} + if ok && content != nil { + if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(content.([]byte)))). + Decode(pivotCache); err != nil && err != io.EOF { + return nil, err + } + } + return pivotCache, nil +} + +// extractPivotTableFields provides a function to extract all pivot table fields +// settings by given pivot table fields. +func (f *File) extractPivotTableFields(order []string, pt *xlsxPivotTableDefinition, opts *PivotTableOptions) { + for fieldIdx, field := range pt.PivotFields.PivotField { + if field.Axis == "axisRow" { + opts.Rows = append(opts.Rows, extractPivotTableField(order[fieldIdx], field)) + } + if field.Axis == "axisCol" { + opts.Columns = append(opts.Columns, extractPivotTableField(order[fieldIdx], field)) + } + if field.Axis == "axisPage" { + opts.Filter = append(opts.Filter, extractPivotTableField(order[fieldIdx], field)) + } + } + if pt.DataFields != nil { + for _, field := range pt.DataFields.DataField { + opts.Data = append(opts.Data, PivotTableField{ + Data: order[field.Fld], + Name: field.Name, + Subtotal: cases.Title(language.English).String(field.Subtotal), + }) + } + } +} + +// extractPivotTableField provides a function to extract pivot table field +// settings by given pivot table fields. +func extractPivotTableField(data string, fld *xlsxPivotField) PivotTableField { + pivotTableField := PivotTableField{ + Data: data, + } + fields := []string{"Compact", "Name", "Outline", "Subtotal", "DefaultSubtotal"} + immutable, mutable := reflect.ValueOf(*fld), reflect.ValueOf(&pivotTableField).Elem() + for _, field := range fields { + immutableField := immutable.FieldByName(field) + if immutableField.Kind() == reflect.String { + mutable.FieldByName(field).SetString(immutableField.String()) + } + if immutableField.Kind() == reflect.Ptr && !immutableField.IsNil() && immutableField.Elem().Kind() == reflect.Bool { + mutable.FieldByName(field).SetBool(immutableField.Elem().Bool()) + } + } + return pivotTableField +} diff --git a/pivotTable_test.go b/pivotTable_test.go index 2de3e07f89..9c45d16e84 100644 --- a/pivotTable_test.go +++ b/pivotTable_test.go @@ -10,7 +10,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestAddPivotTable(t *testing.T) { +func TestPivotTable(t *testing.T) { f := NewFile() // Create some data in a sheet month := []string{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"} @@ -25,25 +25,33 @@ 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(&PivotTableOptions{ - DataRange: "Sheet1!$A$1:$E$31", - PivotTableRange: "Sheet1!$G$2:$M$34", - Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, - Filter: []PivotTableField{{Data: "Region"}}, - Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, - Data: []PivotTableField{{Data: "Sales", Subtotal: "Sum", Name: "Summarize by Sum"}}, - RowGrandTotals: true, - ColGrandTotals: true, - ShowDrill: true, - ShowRowHeaders: true, - ShowColHeaders: true, - ShowLastColumn: true, - ShowError: true, - })) + expected := &PivotTableOptions{ + DataRange: "Sheet1!A1:E31", + PivotTableRange: "Sheet1!G2:M34", + Name: "PivotTable1", + Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, + Filter: []PivotTableField{{Data: "Region"}}, + Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, + Data: []PivotTableField{{Data: "Sales", Subtotal: "Sum", Name: "Summarize by Sum"}}, + RowGrandTotals: true, + ColGrandTotals: true, + ShowDrill: true, + ShowRowHeaders: true, + ShowColHeaders: true, + ShowLastColumn: true, + ShowError: true, + PivotTableStyleName: "PivotStyleLight16", + } + assert.NoError(t, f.AddPivotTable(expected)) + // Test get pivot table + pivotTables, err := f.GetPivotTables("Sheet1") + assert.NoError(t, err) + assert.Len(t, pivotTables, 1) + assert.Equal(t, *expected, pivotTables[0]) // Use different order of coordinate tests assert.NoError(t, f.AddPivotTable(&PivotTableOptions{ - DataRange: "Sheet1!$A$1:$E$31", - PivotTableRange: "Sheet1!$U$34:$O$2", + DataRange: "Sheet1!A1:E31", + PivotTableRange: "Sheet1!U34:O2", Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, Data: []PivotTableField{{Data: "Sales", Subtotal: "Average", Name: "Summarize by Average"}}, @@ -54,10 +62,15 @@ func TestAddPivotTable(t *testing.T) { ShowColHeaders: true, ShowLastColumn: true, })) + // Test get pivot table with default style name + pivotTables, err = f.GetPivotTables("Sheet1") + assert.NoError(t, err) + assert.Len(t, pivotTables, 2) + assert.Equal(t, "PivotStyleLight16", pivotTables[1].PivotTableStyleName) assert.NoError(t, f.AddPivotTable(&PivotTableOptions{ - DataRange: "Sheet1!$A$1:$E$31", - PivotTableRange: "Sheet1!$W$2:$AC$34", + DataRange: "Sheet1!A1:E31", + PivotTableRange: "Sheet1!W2:AC34", Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, Columns: []PivotTableField{{Data: "Region"}}, Data: []PivotTableField{{Data: "Sales", Subtotal: "Count", Name: "Summarize by Count"}}, @@ -69,8 +82,8 @@ func TestAddPivotTable(t *testing.T) { ShowLastColumn: true, })) assert.NoError(t, f.AddPivotTable(&PivotTableOptions{ - DataRange: "Sheet1!$A$1:$E$31", - PivotTableRange: "Sheet1!$G$42:$W$55", + DataRange: "Sheet1!A1:E31", + PivotTableRange: "Sheet1!G42:W55", Rows: []PivotTableField{{Data: "Month"}}, Columns: []PivotTableField{{Data: "Region", DefaultSubtotal: true}, {Data: "Year"}}, Data: []PivotTableField{{Data: "Sales", Subtotal: "CountNums", Name: "Summarize by CountNums"}}, @@ -82,8 +95,8 @@ func TestAddPivotTable(t *testing.T) { ShowLastColumn: true, })) assert.NoError(t, f.AddPivotTable(&PivotTableOptions{ - DataRange: "Sheet1!$A$1:$E$31", - PivotTableRange: "Sheet1!$AE$2:$AG$33", + DataRange: "Sheet1!A1:E31", + PivotTableRange: "Sheet1!AE2:AG33", Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, Data: []PivotTableField{{Data: "Sales", Subtotal: "Max", Name: "Summarize by Max"}, {Data: "Sales", Subtotal: "Average", Name: "Average of Sales"}}, RowGrandTotals: true, @@ -95,8 +108,8 @@ func TestAddPivotTable(t *testing.T) { })) // Create pivot table with empty subtotal field name and specified style assert.NoError(t, f.AddPivotTable(&PivotTableOptions{ - DataRange: "Sheet1!$A$1:$E$31", - PivotTableRange: "Sheet1!$AJ$2:$AP1$35", + DataRange: "Sheet1!A1:E31", + PivotTableRange: "Sheet1!AJ2:AP135", Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, Filter: []PivotTableField{{Data: "Region"}}, Columns: []PivotTableField{}, @@ -109,11 +122,11 @@ func TestAddPivotTable(t *testing.T) { ShowLastColumn: true, PivotTableStyleName: "PivotStyleLight19", })) - _, err := 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:$AN$17", + DataRange: "Sheet1!A1:E31", + PivotTableRange: "Sheet2!A1:AN17", Rows: []PivotTableField{{Data: "Month"}}, Columns: []PivotTableField{{Data: "Region", DefaultSubtotal: true}, {Data: "Type", DefaultSubtotal: true}, {Data: "Year"}}, Data: []PivotTableField{{Data: "Sales", Subtotal: "Min", Name: "Summarize by Min"}}, @@ -125,8 +138,8 @@ func TestAddPivotTable(t *testing.T) { ShowLastColumn: true, })) assert.NoError(t, f.AddPivotTable(&PivotTableOptions{ - DataRange: "Sheet1!$A$1:$E$31", - PivotTableRange: "Sheet2!$A$20:$AR$60", + DataRange: "Sheet1!A1:E31", + PivotTableRange: "Sheet2!A20:AR60", Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Type"}}, Columns: []PivotTableField{{Data: "Region", DefaultSubtotal: true}, {Data: "Year"}}, Data: []PivotTableField{{Data: "Sales", Subtotal: "Product", Name: "Summarize by Product"}}, @@ -140,13 +153,13 @@ func TestAddPivotTable(t *testing.T) { // Create pivot table with many data, many rows, many cols and defined name assert.NoError(t, f.SetDefinedName(&DefinedName{ Name: "dataRange", - RefersTo: "Sheet1!$A$1:$E$31", + RefersTo: "Sheet1!A1:E31", Comment: "Pivot Table Data Range", Scope: "Sheet2", })) assert.NoError(t, f.AddPivotTable(&PivotTableOptions{ DataRange: "dataRange", - PivotTableRange: "Sheet2!$A$65:$AJ$100", + PivotTableRange: "Sheet2!A65:AJ100", Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, Columns: []PivotTableField{{Data: "Region", DefaultSubtotal: true}, {Data: "Type"}}, Data: []PivotTableField{{Data: "Sales", Subtotal: "Sum", Name: "Sum of Sales"}, {Data: "Sales", Subtotal: "Average", Name: "Average of Sales"}}, @@ -160,58 +173,64 @@ func TestAddPivotTable(t *testing.T) { // Test empty pivot table options assert.EqualError(t, f.AddPivotTable(nil), ErrParameterRequired.Error()) + // Test add pivot table with custom name which exceeds the max characters limit + assert.Equal(t, ErrNameLength, f.AddPivotTable(&PivotTableOptions{ + DataRange: "dataRange", + PivotTableRange: "Sheet2!A65:AJ100", + Name: strings.Repeat("c", MaxFieldLength+1), + })) // Test invalid data range assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{ - DataRange: "Sheet1!$A$1:$A$1", - PivotTableRange: "Sheet1!$U$34:$O$2", + DataRange: "Sheet1!A1:A1", + PivotTableRange: "Sheet1!U34:O2", Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, 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(&PivotTableOptions{ - DataRange: "$A$1:$E$31", - PivotTableRange: "Sheet1!$U$34:$O$2", + DataRange: "A1:E31", + PivotTableRange: "Sheet1!U34:O2", Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, 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(&PivotTableOptions{ - DataRange: "SheetN!$A$1:$E$31", - PivotTableRange: "Sheet1!$U$34:$O$2", + DataRange: "SheetN!A1:E31", + PivotTableRange: "Sheet1!U34:O2", Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, 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(&PivotTableOptions{ - DataRange: "Sheet1!$A$1:$E$31", - PivotTableRange: "$U$34:$O$2", + DataRange: "Sheet1!A1:E31", + PivotTableRange: "U34:O2", Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, 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(&PivotTableOptions{ - DataRange: "Sheet1!$A$1:$E$31", - PivotTableRange: "SheetN!$U$34:$O$2", + DataRange: "Sheet1!A1:E31", + PivotTableRange: "SheetN!U34:O2", Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, Data: []PivotTableField{{Data: "Sales"}}, }), "sheet SheetN does not exist") // Test not exists worksheet in data range assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{ - DataRange: "SheetN!$A$1:$E$31", - PivotTableRange: "Sheet1!$U$34:$O$2", + DataRange: "SheetN!A1:E31", + PivotTableRange: "Sheet1!U34:O2", Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, Data: []PivotTableField{{Data: "Sales"}}, }), "sheet SheetN does not exist") // Test invalid row number in data range assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{ - DataRange: "Sheet1!$A$0:$E$31", - PivotTableRange: "Sheet1!$U$34:$O$2", + DataRange: "Sheet1!A0:E31", + PivotTableRange: "Sheet1!U34:O2", Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, Data: []PivotTableField{{Data: "Sales"}}, @@ -219,8 +238,8 @@ func TestAddPivotTable(t *testing.T) { 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(&PivotTableOptions{ - DataRange: "Sheet1!$A$1:$E$31", - PivotTableRange: "Sheet1!$G$2:$M$34", + DataRange: "Sheet1!A1:E31", + PivotTableRange: "Sheet1!G2:M34", Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, Data: []PivotTableField{{Data: "Sales", Subtotal: "-", Name: strings.Repeat("s", MaxFieldLength+1)}}, @@ -228,8 +247,8 @@ func TestAddPivotTable(t *testing.T) { // 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", + DataRange: "Sheet:1!A1:E31", + PivotTableRange: "Sheet:1!G2:M34", Rows: []PivotTableField{{Data: "Year"}}, }), ErrSheetNameInvalid.Error()) // Test adjust range with invalid range @@ -245,8 +264,8 @@ func TestAddPivotTable(t *testing.T) { 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("", &PivotTableOptions{ - DataRange: "$A$1:$E$31", - PivotTableRange: "Sheet1!$U$34:$O$2", + DataRange: "A1:E31", + PivotTableRange: "Sheet1!U34:O2", Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, Data: []PivotTableField{{Data: "Sales"}}, @@ -257,8 +276,8 @@ func TestAddPivotTable(t *testing.T) { 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, &PivotTableOptions{ - DataRange: "$A$1:$E$31", - PivotTableRange: "Sheet1!$U$34:$O$2", + DataRange: "A1:E31", + PivotTableRange: "Sheet1!U34:O2", Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, Data: []PivotTableField{{Data: "Sales"}}, @@ -271,17 +290,53 @@ func TestAddPivotTable(t *testing.T) { 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", + DataRange: "Sheet1!A1:E31", + PivotTableRange: "Sheet1!G2:M34", Rows: []PivotTableField{{Data: "Year"}}, }), "XML syntax error on line 1: invalid UTF-8") + assert.NoError(t, f.Close()) + + // Test get pivot table without pivot table + f = NewFile() + pivotTables, err = f.GetPivotTables("Sheet1") + assert.NoError(t, err) + assert.Len(t, pivotTables, 0) + // Test get pivot table with not exists worksheet + _, err = f.GetPivotTables("SheetN") + assert.EqualError(t, err, "sheet SheetN does not exist") + // Test get pivot table with unsupported charset worksheet relationships + f.Pkg.Store("xl/worksheets/_rels/sheet1.xml.rels", MacintoshCyrillicCharset) + _, err = f.GetPivotTables("Sheet1") + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") + assert.NoError(t, f.Close()) + // Test get pivot table with unsupported charset pivot cache definition + f, err = OpenFile(filepath.Join("test", "TestAddPivotTable1.xlsx")) + assert.NoError(t, err) + f.Pkg.Store("xl/pivotCache/pivotCacheDefinition1.xml", MacintoshCyrillicCharset) + _, err = f.GetPivotTables("Sheet1") + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") + assert.NoError(t, f.Close()) + // Test get pivot table with unsupported charset pivot table relationships + f, err = OpenFile(filepath.Join("test", "TestAddPivotTable1.xlsx")) + assert.NoError(t, err) + f.Pkg.Store("xl/pivotTables/_rels/pivotTable1.xml.rels", MacintoshCyrillicCharset) + _, err = f.GetPivotTables("Sheet1") + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") + assert.NoError(t, f.Close()) + // Test get pivot table with unsupported charset pivot table + f, err = OpenFile(filepath.Join("test", "TestAddPivotTable1.xlsx")) + assert.NoError(t, err) + f.Pkg.Store("xl/pivotTables/pivotTable1.xml", MacintoshCyrillicCharset) + _, err = f.GetPivotTables("Sheet1") + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") + assert.NoError(t, f.Close()) } func TestAddPivotRowFields(t *testing.T) { f := NewFile() // Test invalid data range assert.EqualError(t, f.addPivotRowFields(&xlsxPivotTableDefinition{}, &PivotTableOptions{ - DataRange: "Sheet1!$A$1:$A$1", + DataRange: "Sheet1!A1:A1", }), `parameter 'DataRange' parsing error: parameter is invalid`) } @@ -289,7 +344,7 @@ func TestAddPivotPageFields(t *testing.T) { f := NewFile() // Test invalid data range assert.EqualError(t, f.addPivotPageFields(&xlsxPivotTableDefinition{}, &PivotTableOptions{ - DataRange: "Sheet1!$A$1:$A$1", + DataRange: "Sheet1!A1:A1", }), `parameter 'DataRange' parsing error: parameter is invalid`) } @@ -297,7 +352,7 @@ func TestAddPivotDataFields(t *testing.T) { f := NewFile() // Test invalid data range assert.EqualError(t, f.addPivotDataFields(&xlsxPivotTableDefinition{}, &PivotTableOptions{ - DataRange: "Sheet1!$A$1:$A$1", + DataRange: "Sheet1!A1:A1", }), `parameter 'DataRange' parsing error: parameter is invalid`) } @@ -305,7 +360,7 @@ func TestAddPivotColFields(t *testing.T) { f := NewFile() // Test invalid data range assert.EqualError(t, f.addPivotColFields(&xlsxPivotTableDefinition{}, &PivotTableOptions{ - DataRange: "Sheet1!$A$1:$A$1", + DataRange: "Sheet1!A1:A1", Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, }), `parameter 'DataRange' parsing error: parameter is invalid`) } @@ -313,7 +368,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(&PivotTableOptions{DataRange: "SheetN!$A$1:$E$31"}) + _, err := f.getPivotFieldsOrder(&PivotTableOptions{DataRange: "SheetN!A1:E31"}) assert.EqualError(t, err, "sheet SheetN does not exist") } diff --git a/sheet.go b/sheet.go index b93c231793..00977ec389 100644 --- a/sheet.go +++ b/sheet.go @@ -1595,7 +1595,7 @@ func (f *File) SetDefinedName(definedName *DefinedName) error { if definedName.Name == "" || definedName.RefersTo == "" { return ErrParameterInvalid } - if err := checkDefinedName(definedName.Name); err != nil { + if err := checkDefinedName(definedName.Name); err != nil && inStrSlice(builtInDefinedNames[:2], definedName.Name, false) == -1 { return err } wb, err := f.workbookReader() diff --git a/sheet_test.go b/sheet_test.go index 8d42fd54db..bc4c6fdc7c 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -276,6 +276,16 @@ func TestDefinedName(t *testing.T) { RefersTo: "Sheet1!$A$2:$D$5", Comment: "defined name comment", })) + assert.NoError(t, f.SetDefinedName(&DefinedName{ + Name: builtInDefinedNames[0], + RefersTo: "Sheet1!$A$1:$Z$100", + Scope: "Sheet1", + })) + assert.NoError(t, f.SetDefinedName(&DefinedName{ + Name: builtInDefinedNames[1], + RefersTo: "Sheet1!$A:$A,Sheet1!$1:$1", + Scope: "Sheet1", + })) assert.EqualError(t, f.SetDefinedName(&DefinedName{ Name: "Amount", RefersTo: "Sheet1!$A$2:$D$5", @@ -297,7 +307,7 @@ func TestDefinedName(t *testing.T) { Name: "Amount", })) assert.Exactly(t, "Sheet1!$A$2:$D$5", f.GetDefinedName()[0].RefersTo) - assert.Len(t, f.GetDefinedName(), 1) + assert.Len(t, f.GetDefinedName(), 3) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDefinedName.xlsx"))) // Test set defined name with unsupported charset workbook f.WorkBook = nil diff --git a/sparkline_test.go b/sparkline_test.go index 0d1511d040..048ed2b865 100644 --- a/sparkline_test.go +++ b/sparkline_test.go @@ -267,16 +267,14 @@ func TestAddSparkline(t *testing.T) { // 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 = &xlsxExtLst{Ext: ` - - `} + ws.(*xlsxWorksheet).ExtLst = &xlsxExtLst{Ext: fmt.Sprintf(``, ExtURISlicerListX14, ExtURISparklineGroups)} 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 = `` + ws.(*xlsxWorksheet).ExtLst.Ext = fmt.Sprintf(``, ExtURISparklineGroups) assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOptions{ Location: []string{"A2"}, Range: []string{"Sheet3!A1:J1"}, diff --git a/styles.go b/styles.go index fe2e3f69fc..13904679e1 100644 --- a/styles.go +++ b/styles.go @@ -1127,7 +1127,7 @@ func (f *File) getThemeColor(clr *xlsxColor) string { if len(clr.RGB) == 8 { return strings.TrimPrefix(clr.RGB, "FF") } - if f.Styles.Colors != nil && clr.Indexed < len(f.Styles.Colors.IndexedColors.RgbColor) { + if f.Styles.Colors != nil && f.Styles.Colors.IndexedColors != nil && clr.Indexed < len(f.Styles.Colors.IndexedColors.RgbColor) { return strings.TrimPrefix(ThemeColor(strings.TrimPrefix(f.Styles.Colors.IndexedColors.RgbColor[clr.Indexed].RGB, "FF"), clr.Tint), "FF") } if clr.Indexed < len(IndexedColorMapping) { diff --git a/styles_test.go b/styles_test.go index f202bcf02a..9ef6a894df 100644 --- a/styles_test.go +++ b/styles_test.go @@ -1,6 +1,7 @@ package excelize import ( + "fmt" "math" "path/filepath" "strings" @@ -180,9 +181,7 @@ func TestSetConditionalFormat(t *testing.T) { // 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 = &xlsxExtLst{Ext: ` - - `} + ws.(*xlsxWorksheet).ExtLst = &xlsxExtLst{Ext: fmt.Sprintf(``, ExtURISlicerListX14, ExtURISparklineGroups)} 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 @@ -573,7 +572,7 @@ func TestGetStyle(t *testing.T) { // Test get style with custom color index f.Styles.Colors = &xlsxStyleColors{ - IndexedColors: xlsxIndexedColors{ + IndexedColors: &xlsxIndexedColors{ RgbColor: []xlsxColor{{RGB: "FF012345"}}, }, } diff --git a/table.go b/table.go index 6aa1552edd..ae4fc780fa 100644 --- a/table.go +++ b/table.go @@ -427,7 +427,6 @@ func (f *File) AutoFilter(sheet, rangeRef string, opts []AutoFilterOptions) erro _ = 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 @@ -438,7 +437,7 @@ func (f *File) AutoFilter(sheet, rangeRef string, opts []AutoFilterOptions) erro } filterRange := fmt.Sprintf("'%s'!%s", sheet, ref) d := xlsxDefinedName{ - Name: filterDB, + Name: builtInDefinedNames[2], Hidden: true, LocalSheetID: intPtr(sheetID), Data: filterRange, @@ -451,7 +450,7 @@ func (f *File) AutoFilter(sheet, rangeRef string, opts []AutoFilterOptions) erro var definedNameExists bool for idx := range wb.DefinedNames.DefinedName { definedName := wb.DefinedNames.DefinedName[idx] - if definedName.Name == filterDB && *definedName.LocalSheetID == sheetID && definedName.Hidden { + if definedName.Name == builtInDefinedNames[2] && *definedName.LocalSheetID == sheetID && definedName.Hidden { wb.DefinedNames.DefinedName[idx].Data = filterRange definedNameExists = true } diff --git a/xmlDrawing.go b/xmlDrawing.go index ba38655684..688e601637 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -29,6 +29,7 @@ var ( 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"} + NameSpaceSpreadSheetXR10 = xml.Attr{Name: xml.Name{Local: "xr10", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/spreadsheetml/2016/revision10"} 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"} @@ -43,6 +44,8 @@ const ( 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" + ContentTypeSlicer = "application/vnd.ms-excel.slicer+xml" + ContentTypeSlicerCache = "application/vnd.ms-excel.slicerCache+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" @@ -74,6 +77,7 @@ const ( 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" + SourceRelationshipSlicer = "http://schemas.microsoft.com/office/2007/relationships/slicer" 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" @@ -97,6 +101,7 @@ const ( ExtURIDrawingBlip = "{28A0092B-C50C-407E-A947-70E740481C1C}" ExtURIIgnoredErrors = "{01252117-D84E-4E92-8308-4BE1C098FCBB}" ExtURIMacExcelMX = "{64002731-A6B0-56B0-2670-7721B7C09600}" + ExtURIPivotCacheDefinition = "{725AE2AE-9491-48be-B2B4-4EB974FC3084}" ExtURIProtectedRanges = "{FC87AEE6-9EDD-4A0A-B7FB-166176984837}" ExtURISlicerCachesListX14 = "{BBE1A952-AA13-448e-AADC-164F8A28A991}" ExtURISlicerListX14 = "{A8765BA9-456A-4DAB-B4F3-ACF838C121DE}" @@ -222,6 +227,9 @@ var supportedDrawingUnderlineTypes = []string{ // supportedPositioning defined supported positioning types. var supportedPositioning = []string{"absolute", "oneCell", "twoCell"} +// builtInDefinedNames defined built-in defined names are built with a _xlnm prefix. +var builtInDefinedNames = []string{"_xlnm.Print_Area", "_xlnm.Print_Titles", "_xlnm._FilterDatabase"} + // 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. diff --git a/xmlStyles.go b/xmlStyles.go index 1dda04a0aa..e7de885629 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -311,8 +311,8 @@ type xlsxIndexedColors struct { // legacy color palette has been modified (backwards compatibility settings) or // a custom color has been selected while using this workbook. type xlsxStyleColors struct { - IndexedColors xlsxIndexedColors `xml:"indexedColors"` - MruColors xlsxInnerXML `xml:"mruColors"` + IndexedColors *xlsxIndexedColors `xml:"indexedColors"` + MruColors xlsxInnerXML `xml:"mruColors"` } // Alignment directly maps the alignment settings of the cells. From a0a7d5cdbb092d421bdb0b7bce82a93fe350af5b Mon Sep 17 00:00:00 2001 From: Matthias Endler Date: Tue, 5 Sep 2023 13:43:22 +0200 Subject: [PATCH 788/957] Fix bug in checkDefinedNames --- table.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/table.go b/table.go index ae4fc780fa..a93ca8f959 100644 --- a/table.go +++ b/table.go @@ -289,7 +289,7 @@ func checkDefinedName(name string) error { if unicode.IsLetter(c) { continue } - if i > 0 && unicode.IsDigit(c) { + if i > 0 && (unicode.IsDigit(c) || c == '.') { continue } return newInvalidNameError(name) From 49706c9018fcc363fcff349612fdb1a38bf06412 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 9 Sep 2023 13:51:00 +0800 Subject: [PATCH 789/957] This fixes #1645 and fixes #1655 - Breaking changes, change the data type for the `HeaderFooterOptions` structure fields `AlignWithMargins` and `ScaleWithDoc` as a pointer - Fixed panic on `AutoFilter` by adding nil pointer guard for local sheet ID - Allow dot character in the defined name, table name, or pivot table name - Update the unit tests --- sheet_test.go | 2 +- table.go | 7 +++++-- table_test.go | 4 ++++ xmlWorksheet.go | 8 ++++---- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/sheet_test.go b/sheet_test.go index bc4c6fdc7c..af89c58cc7 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -266,7 +266,7 @@ func TestSetHeaderFooter(t *testing.T) { func TestDefinedName(t *testing.T) { f := NewFile() assert.NoError(t, f.SetDefinedName(&DefinedName{ - Name: "Amount", + Name: "Amount.", RefersTo: "Sheet1!$A$2:$D$5", Comment: "defined name comment", Scope: "Sheet1", diff --git a/table.go b/table.go index a93ca8f959..196be5ef86 100644 --- a/table.go +++ b/table.go @@ -449,8 +449,11 @@ func (f *File) AutoFilter(sheet, rangeRef string, opts []AutoFilterOptions) erro } else { var definedNameExists bool for idx := range wb.DefinedNames.DefinedName { - definedName := wb.DefinedNames.DefinedName[idx] - if definedName.Name == builtInDefinedNames[2] && *definedName.LocalSheetID == sheetID && definedName.Hidden { + definedName, localSheetID := wb.DefinedNames.DefinedName[idx], 0 + if definedName.LocalSheetID != nil { + localSheetID = *definedName.LocalSheetID + } + if definedName.Name == builtInDefinedNames[2] && localSheetID == sheetID && definedName.Hidden { wb.DefinedNames.DefinedName[idx].Data = filterRange definedNameExists = true } diff --git a/table_test.go b/table_test.go index da4426586b..d81a52bee7 100644 --- a/table_test.go +++ b/table_test.go @@ -156,6 +156,10 @@ func TestAutoFilter(t *testing.T) { f.WorkBook = nil f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset) assert.EqualError(t, f.AutoFilter("Sheet1", "D4:B1", nil), "XML syntax error on line 1: invalid UTF-8") + // Test add auto filter with empty local sheet ID + f = NewFile() + f.WorkBook = &xlsxWorkbook{DefinedNames: &xlsxDefinedNames{DefinedName: []xlsxDefinedName{{Name: builtInDefinedNames[2], Hidden: true}}}} + assert.NoError(t, f.AutoFilter("Sheet1", "A1:B1", nil)) } func TestAutoFilterError(t *testing.T) { diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 22ec03e3b1..76ffe52442 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -81,8 +81,8 @@ type xlsxHeaderFooter struct { 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"` + ScaleWithDoc *bool `xml:"scaleWithDoc,attr"` + AlignWithMargins *bool `xml:"alignWithMargins,attr"` OddHeader string `xml:"oddHeader,omitempty"` OddFooter string `xml:"oddFooter,omitempty"` EvenHeader string `xml:"evenHeader,omitempty"` @@ -963,10 +963,10 @@ type SheetProtectionOptions struct { // HeaderFooterOptions directly maps the settings of header and footer. type HeaderFooterOptions struct { - AlignWithMargins bool + AlignWithMargins *bool DifferentFirst bool DifferentOddEven bool - ScaleWithDoc bool + ScaleWithDoc *bool OddHeader string OddFooter string EvenHeader string From 5a039f3045293c58d2383691c30e6ba5ae8da9fb Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 14 Sep 2023 22:56:28 +0800 Subject: [PATCH 790/957] This fixes #1658 - Fix a v2.8.0 regression bug, number format code apply result was empty - Fix calculate formula functions CHITEST and MMULT panic in some cases - Updated unit tests --- calc.go | 12 ++++++++++-- calc_test.go | 23 +++++++++++++---------- numfmt.go | 24 ++++++++++++++---------- numfmt_test.go | 6 ++++++ 4 files changed, 43 insertions(+), 22 deletions(-) diff --git a/calc.go b/calc.go index 459616f752..4ae2b3e8bf 100644 --- a/calc.go +++ b/calc.go @@ -4803,11 +4803,16 @@ 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) + arr1 := argsList.Front().Value.(formulaArg) + arr2 := argsList.Back().Value.(formulaArg) + if arr1.Type == ArgNumber && arr2.Type == ArgNumber { + return newNumberFormulaArg(arr1.Number * arr2.Number) + } + numMtx1, errArg1 := newNumberMatrix(arr1, false) if errArg1.Type == ArgError { return errArg1 } - numMtx2, errArg2 := newNumberMatrix(argsList.Back().Value.(formulaArg), false) + numMtx2, errArg2 := newNumberMatrix(arr2, false) if errArg2.Type == ArgError { return errArg2 } @@ -7191,6 +7196,9 @@ func (fn *formulaFuncs) CHITEST(argsList *list.List) formulaArg { actual, expected := argsList.Front().Value.(formulaArg), argsList.Back().Value.(formulaArg) actualList, expectedList := actual.ToList(), expected.ToList() rows := len(actual.Matrix) + if rows == 0 { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } columns := len(actualList) / rows if len(actualList) != len(expectedList) || len(actualList) == 1 { return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) diff --git a/calc_test.go b/calc_test.go index ba5a35b4f5..6042715206 100644 --- a/calc_test.go +++ b/calc_test.go @@ -673,6 +673,8 @@ func TestCalcCellValue(t *testing.T) { // MINVERSE "=MINVERSE(A1:B2)": "-0", // MMULT + "=MMULT(0,0)": "0", + "=MMULT(2,4)": "8", "=MMULT(A4:A4,A4:A4)": "0", // MOD "=MOD(6,4)": "2", @@ -5139,16 +5141,17 @@ func TestCalcCHITESTandCHISQdotTEST(t *testing.T) { assert.Equal(t, expected, result, formula) } 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!"}, + "=CHITEST()": {"#VALUE!", "CHITEST requires 2 arguments"}, + "=CHITEST(MUNIT(0),MUNIT(0))": {"#VALUE!", "#VALUE!"}, + "=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)) diff --git a/numfmt.go b/numfmt.go index b63e74a1c8..b0fd7c2e01 100644 --- a/numfmt.go +++ b/numfmt.go @@ -4759,9 +4759,9 @@ func (nf *numberFormat) getNumberFmtConf() { if token.TType == nfp.TokenTypeHashPlaceHolder { if nf.usePointer { nf.fracHolder += len(token.TValue) - } else { - nf.intHolder += len(token.TValue) + continue } + nf.intHolder += len(token.TValue) } if token.TType == nfp.TokenTypeExponential { nf.useScientificNotation = true @@ -4779,6 +4779,7 @@ func (nf *numberFormat) getNumberFmtConf() { nf.switchArgument = token.TValue } if token.TType == nfp.TokenTypeZeroPlaceHolder { + nf.intHolder = 0 if nf.usePointer { if nf.useScientificNotation { nf.expBaseLen += len(token.TValue) @@ -4795,7 +4796,7 @@ func (nf *numberFormat) getNumberFmtConf() { // printNumberLiteral apply literal tokens for the pre-formatted text. func (nf *numberFormat) printNumberLiteral(text string) string { var result string - var useLiteral, useZeroPlaceHolder bool + var useLiteral, usePlaceHolder bool if nf.usePositive { result += "-" } @@ -4807,17 +4808,17 @@ func (nf *numberFormat) printNumberLiteral(text string) string { result += nf.currencyString } if token.TType == nfp.TokenTypeLiteral { - if useZeroPlaceHolder { + if usePlaceHolder { useLiteral = true } result += token.TValue } - if token.TType == nfp.TokenTypeZeroPlaceHolder { - if useLiteral && useZeroPlaceHolder { + if token.TType == nfp.TokenTypeHashPlaceHolder || token.TType == nfp.TokenTypeZeroPlaceHolder { + if useLiteral && usePlaceHolder { return nf.value } - if !useZeroPlaceHolder { - useZeroPlaceHolder = true + if !usePlaceHolder { + usePlaceHolder = true result += text } } @@ -4896,8 +4897,11 @@ func (nf *numberFormat) numberHandler() string { result string ) nf.getNumberFmtConf() - if intLen = intPart; nf.intPadding > intPart { - intLen = nf.intPadding + if nf.intHolder > intPart { + nf.intHolder = intPart + } + if intLen = intPart; nf.intPadding+nf.intHolder > intPart { + intLen = nf.intPadding + nf.intHolder } if fracLen = fracPart; fracPart > nf.fracHolder+nf.fracPadding { fracLen = nf.fracHolder + nf.fracPadding diff --git a/numfmt_test.go b/numfmt_test.go index 02a73af9ec..143357c87a 100644 --- a/numfmt_test.go +++ b/numfmt_test.go @@ -3504,6 +3504,12 @@ func TestNumFmt(t *testing.T) { {"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", "#", "1235"}, + {"1234.5678", "#0", "1235"}, + {"1234.5678", "##", "1235"}, + {"1234.5678", "00000.00#", "01234.568"}, + {"1234.5678", "00000####", "000001235"}, + {"1234.5678", "00000######", "000001235"}, {"-1234.5678", "0.00", "-1234.57"}, {"-1234.5678", "0.00;-0.00", "-1234.57"}, {"-1234.5678", "0.00%%", "-12345678.00%%"}, From e3b7dad69a2ece6acc4e3d6a9bf4f354ff523b57 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 16 Sep 2023 12:21:11 +0800 Subject: [PATCH 791/957] Introduce the new exported function `AddSlicer` for adding table slicer - Fix a v2.8.0 regression bug, generate workbook corruption caused by incorrect MRU colors style parts - Fix corrupted workbooks generated when adding tables in some cases - Added several exported extension list child element URI constants - Move part of the internal constant and variables definition to the template source code file - Updated unit tests --- chart.go | 4 +- chart_test.go | 8 +- drawing.go | 2 +- errors.go | 6 + lib.go | 42 +++- lib_test.go | 9 + picture.go | 32 ++- picture_test.go | 11 + pivotTable.go | 26 +-- pivotTable_test.go | 8 +- rows.go | 4 +- shape.go | 41 ++-- slicer.go | 551 ++++++++++++++++++++++++++++++++++++++++++++ slicer_test.go | 247 ++++++++++++++++++++ sparkline.go | 6 +- styles.go | 10 +- table.go | 15 +- templates.go | 237 +++++++++++++++++++ xmlDecodeDrawing.go | 19 +- xmlDrawing.go | 263 +++------------------ xmlSlicers.go | 168 ++++++++++++++ xmlStyles.go | 4 +- xmlTable.go | 1 + xmlWorkbook.go | 60 ++++- xmlWorksheet.go | 30 +-- 25 files changed, 1464 insertions(+), 340 deletions(-) create mode 100644 slicer.go create mode 100644 slicer_test.go create mode 100644 xmlSlicers.go diff --git a/chart.go b/chart.go index ac13729bd7..4cfac96000 100644 --- a/chart.go +++ b/chart.go @@ -499,10 +499,10 @@ func parseChartOptions(opts *Chart) (*Chart, error) { opts.Format.Locked = boolPtr(false) } if opts.Format.ScaleX == 0 { - opts.Format.ScaleX = defaultPictureScale + opts.Format.ScaleX = defaultDrawingScale } if opts.Format.ScaleY == 0 { - opts.Format.ScaleY = defaultPictureScale + opts.Format.ScaleY = defaultDrawingScale } if opts.Legend.Position == "" { opts.Legend.Position = defaultChartLegendPosition diff --git a/chart_test.go b/chart_test.go index 0fa66580f4..a860fa55ef 100644 --- a/chart_test.go +++ b/chart_test.go @@ -184,8 +184,8 @@ func TestAddChart(t *testing.T) { {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, + ScaleX: defaultDrawingScale, + ScaleY: defaultDrawingScale, OffsetX: 15, OffsetY: 10, PrintObject: boolPtr(true), @@ -369,8 +369,8 @@ func TestDeleteChart(t *testing.T) { {Name: "Sheet1!$A$37", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$37:$D$37"}, } format := GraphicOptions{ - ScaleX: defaultPictureScale, - ScaleY: defaultPictureScale, + ScaleX: defaultDrawingScale, + ScaleY: defaultDrawingScale, OffsetX: 15, OffsetY: 10, PrintObject: boolPtr(true), diff --git a/drawing.go b/drawing.go index 9f0f4b6b69..afe9d4d103 100644 --- a/drawing.go +++ b/drawing.go @@ -1356,7 +1356,7 @@ func (f *File) addSheetDrawingChart(drawingXML string, rID int, opts *GraphicOpt absoluteAnchor := xdrCellAnchor{ EditAs: opts.Positioning, Pos: &xlsxPoint2D{}, - Ext: &xlsxExt{}, + Ext: &aExt{}, } graphicFrame := xlsxGraphicFrame{ diff --git a/errors.go b/errors.go index a5d8814f53..59888007bc 100644 --- a/errors.go +++ b/errors.go @@ -34,6 +34,12 @@ func newInvalidCellNameError(cell string) error { return fmt.Errorf("invalid cell name %q", cell) } +// newInvalidSlicerNameError defined the error message on receiving the invalid +// slicer name. +func newInvalidSlicerNameError(name string) error { + return fmt.Errorf("invalid slicer name %q", name) +} + // newInvalidExcelDateError defined the error message on receiving the data // with negative values. func newInvalidExcelDateError(dateValue float64) error { diff --git a/lib.go b/lib.go index 25fdc3b16d..65d5ae04e3 100644 --- a/lib.go +++ b/lib.go @@ -329,7 +329,7 @@ func (f *File) coordinatesToRangeRef(coordinates []int, abs ...bool) (string, er } // getDefinedNameRefTo convert defined name to reference range. -func (f *File) getDefinedNameRefTo(definedNameName string, currentSheet string) (refTo string) { +func (f *File) getDefinedNameRefTo(definedNameName, currentSheet string) (refTo string) { var workbookRefTo, worksheetRefTo string for _, definedName := range f.GetDefinedName() { if definedName.Name == definedNameName { @@ -431,17 +431,17 @@ func float64Ptr(f float64) *float64 { return &f } func stringPtr(s string) *string { return &s } // Value extracts string data type text from a attribute value. -func (attr *attrValString) Value() string { - if attr != nil && attr.Val != nil { - return *attr.Val +func (avb *attrValString) Value() string { + if avb != nil && avb.Val != nil { + return *avb.Val } return "" } // Value extracts boolean data type value from a attribute value. -func (attr *attrValBool) Value() bool { - if attr != nil && attr.Val != nil { - return *attr.Val +func (avb *attrValBool) Value() bool { + if avb != nil && avb.Val != nil { + return *avb.Val } return false } @@ -517,6 +517,34 @@ func (avb *attrValBool) UnmarshalXML(d *xml.Decoder, start xml.StartElement) err return nil } +// MarshalXML encodes ext element with specified namespace attributes on +// serialization. +func (ext xlsxExt) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + start.Attr = ext.xmlns + return e.EncodeElement(decodeExt{URI: ext.URI, Content: ext.Content}, start) +} + +// UnmarshalXML extracts ext element attributes namespace by giving XML decoder +// on deserialization. +func (ext *xlsxExt) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + for _, attr := range start.Attr { + if attr.Name.Local == "uri" { + continue + } + if attr.Name.Space == "xmlns" { + attr.Name.Space = "" + attr.Name.Local = "xmlns:" + attr.Name.Local + } + ext.xmlns = append(ext.xmlns, attr) + } + e := &decodeExt{} + if err := d.DecodeElement(&e, &start); err != nil { + return err + } + ext.URI, ext.Content = e.URI, e.Content + return nil +} + // namespaceStrictToTransitional provides a method to convert Strict and // Transitional namespaces. func namespaceStrictToTransitional(content []byte) []byte { diff --git a/lib_test.go b/lib_test.go index b41f5f1c48..5d54eaf9c1 100644 --- a/lib_test.go +++ b/lib_test.go @@ -274,6 +274,15 @@ func TestBoolValUnmarshalXML(t *testing.T) { assert.EqualError(t, attr.UnmarshalXML(xml.NewDecoder(strings.NewReader("")), xml.StartElement{}), io.EOF.Error()) } +func TestExtUnmarshalXML(t *testing.T) { + f, extLst := NewFile(), decodeExtLst{} + expected := fmt.Sprintf(``, + ExtURISlicerCachesX14, NameSpaceSpreadSheetX14.Value) + assert.NoError(t, f.xmlNewDecoder(strings.NewReader(expected)).Decode(&extLst)) + assert.Len(t, extLst.Ext, 1) + assert.Equal(t, extLst.Ext[0].URI, ExtURISlicerCachesX14) +} + func TestBytesReplace(t *testing.T) { s := []byte{0x01} assert.EqualValues(t, s, bytesReplace(s, []byte{}, []byte{}, 0)) diff --git a/picture.go b/picture.go index b8978ea950..4e646eeac5 100644 --- a/picture.go +++ b/picture.go @@ -30,8 +30,8 @@ func parseGraphicOptions(opts *GraphicOptions) *GraphicOptions { return &GraphicOptions{ PrintObject: boolPtr(true), Locked: boolPtr(true), - ScaleX: defaultPictureScale, - ScaleY: defaultPictureScale, + ScaleX: defaultDrawingScale, + ScaleY: defaultDrawingScale, } } if opts.PrintObject == nil { @@ -41,10 +41,10 @@ func parseGraphicOptions(opts *GraphicOptions) *GraphicOptions { opts.Locked = boolPtr(true) } if opts.ScaleX == 0 { - opts.ScaleX = defaultPictureScale + opts.ScaleX = defaultDrawingScale } if opts.ScaleY == 0 { - opts.ScaleY = defaultPictureScale + opts.ScaleY = defaultDrawingScale } return opts } @@ -440,6 +440,28 @@ func (f *File) addMedia(file []byte, ext string) string { return media } +// setContentTypePartRelsExtensions provides a function to set the content +// type for relationship parts and the Main Document part. +func (f *File) setContentTypePartRelsExtensions() error { + var rels bool + content, err := f.contentTypesReader() + if err != nil { + return err + } + for _, v := range content.Defaults { + if v.Extension == "rels" { + rels = true + } + } + if !rels { + content.Defaults = append(content.Defaults, xlsxDefault{ + Extension: "rels", + ContentType: ContentTypeRelationships, + }) + } + return err +} + // setContentTypePartImageExtensions provides a function to set the content // type for relationship parts and the Main Document part. func (f *File) setContentTypePartImageExtensions() error { @@ -542,7 +564,7 @@ func (f *File) addContentTypePart(index int, contentType string) error { PartName: partNames[contentType], ContentType: contentTypes[contentType], }) - return err + return f.setContentTypePartRelsExtensions() } // getSheetRelationshipsTargetByID provides a function to get Target attribute diff --git a/picture_test.go b/picture_test.go index 4920634514..cd070d5ccf 100644 --- a/picture_test.go +++ b/picture_test.go @@ -269,6 +269,17 @@ func TestDrawingResize(t *testing.T) { assert.EqualError(t, f.AddPicture("Sheet1", "A1", filepath.Join("test", "images", "excel.jpg"), &GraphicOptions{AutoFit: true}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) } +func TestSetContentTypePartRelsExtensions(t *testing.T) { + f := NewFile() + f.ContentTypes = &xlsxTypes{} + assert.NoError(t, f.setContentTypePartRelsExtensions()) + + // Test set content type part relationships extensions with unsupported charset content types + f.ContentTypes = nil + f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) + assert.EqualError(t, f.setContentTypePartRelsExtensions(), "XML syntax error on line 1: invalid UTF-8") +} + func TestSetContentTypePartImageExtensions(t *testing.T) { f := NewFile() // Test set content type part image extensions with unsupported charset content types diff --git a/pivotTable.go b/pivotTable.go index f98e93b8de..2775af7f2b 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -89,9 +89,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 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: +// For example, create a pivot table on the range reference Sheet1!G2:M34 with +// the range reference Sheet1!A1:E31 as the data source, summarize by sum for +// sales: // // package main // @@ -242,15 +242,15 @@ func (f *File) adjustRange(rangeStr string) (string, []int, error) { return rng[0], []int{x1, y1, x2, y2}, nil } -// getPivotFieldsOrder provides a function to get order list of pivot table +// getTableFieldsOrder provides a function to get order list of pivot table // fields. -func (f *File) getPivotFieldsOrder(opts *PivotTableOptions) ([]string, error) { +func (f *File) getTableFieldsOrder(sheetName, dataRange string) ([]string, error) { var order []string - dataRange := f.getDefinedNameRefTo(opts.DataRange, opts.pivotTableSheetName) - if dataRange == "" { - dataRange = opts.DataRange + ref := f.getDefinedNameRefTo(dataRange, sheetName) + if ref == "" { + ref = dataRange } - dataSheet, coordinates, err := f.adjustRange(dataRange) + dataSheet, coordinates, err := f.adjustRange(ref) if err != nil { return order, fmt.Errorf("parameter 'DataRange' parsing error: %s", err.Error()) } @@ -279,7 +279,7 @@ func (f *File) addPivotCache(pivotCacheXML string, opts *PivotTableOptions) erro return fmt.Errorf("parameter 'DataRange' parsing error: %s", err.Error()) } // data range has been checked - order, _ := f.getPivotFieldsOrder(opts) + order, _ := f.getTableFieldsOrder(opts.pivotTableSheetName, opts.DataRange) hCell, _ := CoordinatesToCellName(coordinates[0], coordinates[1]) vCell, _ := CoordinatesToCellName(coordinates[2], coordinates[3]) pc := xlsxPivotCacheDefinition{ @@ -541,7 +541,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 *PivotTableOptions) error { - order, err := f.getPivotFieldsOrder(opts) + order, err := f.getTableFieldsOrder(opts.pivotTableSheetName, opts.DataRange) if err != nil { return err } @@ -647,7 +647,7 @@ func (f *File) countPivotCache() int { // to a sequential index by given fields and pivot option. func (f *File) getPivotFieldsIndex(fields []PivotTableField, opts *PivotTableOptions) ([]int, error) { var pivotFieldsIndex []int - orders, err := f.getPivotFieldsOrder(opts) + orders, err := f.getTableFieldsOrder(opts.pivotTableSheetName, opts.DataRange) if err != nil { return pivotFieldsIndex, err } @@ -809,7 +809,7 @@ func (f *File) getPivotTable(sheet, pivotTableXML, pivotCacheRels string) (Pivot opts.ShowLastColumn = si.ShowLastColumn opts.PivotTableStyleName = si.Name } - order, _ := f.getPivotFieldsOrder(&PivotTableOptions{DataRange: dataRange, pivotTableSheetName: pt.Name}) + order, _ := f.getTableFieldsOrder(pt.Name, dataRange) f.extractPivotTableFields(order, pt, &opts) return opts, err } diff --git a/pivotTable_test.go b/pivotTable_test.go index 9c45d16e84..f6d0707524 100644 --- a/pivotTable_test.go +++ b/pivotTable_test.go @@ -257,8 +257,8 @@ func TestPivotTable(t *testing.T) { // Test adjust range with incorrect range _, _, err = f.adjustRange("sheet1!") assert.EqualError(t, err, "parameter is invalid") - // Test get pivot fields order with empty data range - _, err = f.getPivotFieldsOrder(&PivotTableOptions{}) + // Test get table fields order with empty data range + _, err = f.getTableFieldsOrder("", "") assert.EqualError(t, err, `parameter 'DataRange' parsing error: parameter is required`) // Test add pivot cache with empty data range assert.EqualError(t, f.addPivotCache("", &PivotTableOptions{}), "parameter 'DataRange' parsing error: parameter is required") @@ -367,8 +367,8 @@ func TestAddPivotColFields(t *testing.T) { func TestGetPivotFieldsOrder(t *testing.T) { f := NewFile() - // Test get pivot fields order with not exist worksheet - _, err := f.getPivotFieldsOrder(&PivotTableOptions{DataRange: "SheetN!A1:E31"}) + // Test get table fields order with not exist worksheet + _, err := f.getTableFieldsOrder("", "SheetN!A1:E31") assert.EqualError(t, err, "sheet SheetN does not exist") } diff --git a/rows.go b/rows.go index 332bedda38..adb05a2bfd 100644 --- a/rows.go +++ b/rows.go @@ -708,7 +708,7 @@ 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: // -// +// // // // @@ -717,7 +717,7 @@ func (f *File) duplicateMergeCells(sheet string, ws *xlsxWorksheet, row, row2 in // // in this case, we should to change it to // -// +// // // // diff --git a/shape.go b/shape.go index 53f1649011..6a48f794f7 100644 --- a/shape.go +++ b/shape.go @@ -38,10 +38,10 @@ func parseShapeOptions(opts *Shape) (*Shape, error) { opts.Format.Locked = boolPtr(false) } if opts.Format.ScaleX == 0 { - opts.Format.ScaleX = defaultPictureScale + opts.Format.ScaleX = defaultDrawingScale } if opts.Format.ScaleY == 0 { - opts.Format.ScaleY = defaultPictureScale + opts.Format.ScaleY = defaultDrawingScale } if opts.Line.Width == nil { opts.Line.Width = float64Ptr(defaultShapeLineWidth) @@ -322,29 +322,27 @@ func (f *File) AddShape(sheet string, opts *Shape) error { return f.addContentTypePart(drawingID, "drawings") } -// addDrawingShape provides a function to add preset geometry by given sheet, -// drawingXMLand format sets. -func (f *File) addDrawingShape(sheet, drawingXML, cell string, opts *Shape) error { +// twoCellAnchorShape create a two cell anchor shape size placeholder for a +// group, a shape, or a drawing element. +func (f *File) twoCellAnchorShape(sheet, drawingXML, cell string, width, height uint, format GraphicOptions) (*xlsxWsDr, *xdrCellAnchor, int, error) { fromCol, fromRow, err := CellNameToCoordinates(cell) if err != nil { - return err + return nil, nil, 0, err } - 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, fromCol, fromRow, opts.Format.OffsetX, opts.Format.OffsetY, - width, height) + w := int(float64(width) * format.ScaleX) + h := int(float64(height) * format.ScaleY) + colStart, rowStart, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, fromCol, fromRow, format.OffsetX, format.OffsetY, w, h) content, cNvPrID, err := f.drawingParser(drawingXML) if err != nil { - return err + return content, nil, cNvPrID, err } twoCellAnchor := xdrCellAnchor{} - twoCellAnchor.EditAs = opts.Format.Positioning + twoCellAnchor.EditAs = format.Positioning from := xlsxFrom{} from.Col = colStart - from.ColOff = opts.Format.OffsetX * EMU + from.ColOff = format.OffsetX * EMU from.Row = rowStart - from.RowOff = opts.Format.OffsetY * EMU + from.RowOff = format.OffsetY * EMU to := xlsxTo{} to.Col = colEnd to.ColOff = x2 * EMU @@ -352,6 +350,17 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, opts *Shape) erro to.RowOff = y2 * EMU twoCellAnchor.From = &from twoCellAnchor.To = &to + return content, &twoCellAnchor, cNvPrID, err +} + +// addDrawingShape provides a function to add preset geometry by given sheet, +// drawingXML and format sets. +func (f *File) addDrawingShape(sheet, drawingXML, cell string, opts *Shape) error { + content, twoCellAnchor, cNvPrID, err := f.twoCellAnchorShape( + sheet, drawingXML, cell, opts.Width, opts.Height, opts.Format) + if err != nil { + return err + } var solidColor string if len(opts.Fill.Color) == 1 { solidColor = opts.Fill.Color[0] @@ -462,7 +471,7 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, opts *Shape) erro FLocksWithSheet: *opts.Format.Locked, FPrintsWithSheet: *opts.Format.PrintObject, } - content.TwoCellAnchor = append(content.TwoCellAnchor, &twoCellAnchor) + content.TwoCellAnchor = append(content.TwoCellAnchor, twoCellAnchor) f.Drawings.Store(drawingXML, content) return err } diff --git a/slicer.go b/slicer.go new file mode 100644 index 0000000000..435c0f96f2 --- /dev/null +++ b/slicer.go @@ -0,0 +1,551 @@ +// 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. +// +// Package excelize 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. + +package excelize + +import ( + "bytes" + "encoding/xml" + "fmt" + "io" + "sort" + "strconv" + "strings" + "unicode" +) + +// SlicerOptions represents the settings of the slicer. +// +// Name specifies the slicer name, should be an existing field name of the given +// table or pivot table, this setting is required. +// +// Table specifies the name of the table or pivot table, this setting is +// required. +// +// Cell specifies the left top cell coordinates the position for inserting the +// slicer, this setting is required. +// +// Caption specifies the caption of the slicer, this setting is optional. +// +// Macro used for set macro for the slicer, the workbook extension should be +// XLSM or XLTM +// +// Width specifies the width of the slicer, this setting is optional. +// +// Height specifies the height of the slicer, this setting is optional. +// +// DisplayHeader specifies if display header of the slicer, this setting is +// optional, the default setting is display. +// +// ItemDesc specifies descending (Z-A) item sorting, this setting is optional, +// and the default setting is false (represents ascending). +// +// Format specifies the format of the slicer, this setting is optional. +type SlicerOptions struct { + Name string + Table string + Cell string + Caption string + Macro string + Width uint + Height uint + DisplayHeader *bool + ItemDesc bool + Format GraphicOptions +} + +// AddSlicer function inserts a slicer by giving the worksheet name and slicer +// settings. The pivot table slicer is not supported currently. +// +// For example, insert a slicer on the Sheet1!E1 with field Column1 for the +// table named Table1: +// +// err := f.AddSlicer("Sheet1", &excelize.SlicerOptions{ +// Name: "Column1", +// Table: "Table1", +// Cell: "E1", +// Caption: "Column1", +// Width: 200, +// Height: 200, +// }) +func (f *File) AddSlicer(sheet string, opts *SlicerOptions) error { + opts, err := parseSlicerOptions(opts) + if err != nil { + return err + } + table, colIdx, err := f.getSlicerSource(sheet, opts) + if err != nil { + return err + } + slicerID, err := f.addSheetSlicer(sheet) + if err != nil { + return err + } + slicerCacheName, err := f.setSlicerCache(colIdx, opts, table) + if err != nil { + return err + } + slicerName, err := f.addDrawingSlicer(sheet, opts) + if err != nil { + return err + } + return f.addSlicer(slicerID, xlsxSlicer{ + Name: slicerName, + Cache: slicerCacheName, + Caption: opts.Caption, + ShowCaption: opts.DisplayHeader, + RowHeight: 251883, + }) +} + +// parseSlicerOptions provides a function to parse the format settings of the +// slicer with default value. +func parseSlicerOptions(opts *SlicerOptions) (*SlicerOptions, error) { + if opts == nil { + return nil, ErrParameterRequired + } + if opts.Name == "" || opts.Table == "" || opts.Cell == "" { + return nil, ErrParameterInvalid + } + if opts.Width == 0 { + opts.Width = defaultSlicerWidth + } + if opts.Height == 0 { + opts.Height = defaultSlicerHeight + } + if opts.Format.PrintObject == nil { + opts.Format.PrintObject = boolPtr(true) + } + if opts.Format.Locked == nil { + opts.Format.Locked = boolPtr(false) + } + if opts.Format.ScaleX == 0 { + opts.Format.ScaleX = defaultDrawingScale + } + if opts.Format.ScaleY == 0 { + opts.Format.ScaleY = defaultDrawingScale + } + return opts, nil +} + +// countSlicers provides a function to get slicer files count storage in the +// folder xl/slicers. +func (f *File) countSlicers() int { + count := 0 + f.Pkg.Range(func(k, v interface{}) bool { + if strings.Contains(k.(string), "xl/slicers/slicer") { + count++ + } + return true + }) + return count +} + +// countSlicerCache provides a function to get slicer cache files count storage +// in the folder xl/SlicerCaches. +func (f *File) countSlicerCache() int { + count := 0 + f.Pkg.Range(func(k, v interface{}) bool { + if strings.Contains(k.(string), "xl/slicerCaches/slicerCache") { + count++ + } + return true + }) + return count +} + +// getSlicerSource returns the slicer data source table or pivot table settings +// and the index of the given slicer fields in the table or pivot table +// column. +func (f *File) getSlicerSource(sheet string, opts *SlicerOptions) (*Table, int, error) { + var ( + table *Table + colIdx int + tables, err = f.GetTables(sheet) + ) + if err != nil { + return table, colIdx, err + } + for _, tbl := range tables { + if tbl.Name == opts.Table { + table = &tbl + break + } + } + if table == nil { + return table, colIdx, newNoExistTableError(opts.Table) + } + order, _ := f.getTableFieldsOrder(sheet, fmt.Sprintf("%s!%s", sheet, table.Range)) + if colIdx = inStrSlice(order, opts.Name, true); colIdx == -1 { + return table, colIdx, newInvalidSlicerNameError(opts.Name) + } + return table, colIdx, err +} + +// addSheetSlicer adds a new slicer and updates the namespace and relationships +// parts of the worksheet by giving the worksheet name. +func (f *File) addSheetSlicer(sheet string) (int, error) { + var ( + slicerID = f.countSlicers() + 1 + ws, err = f.workSheetReader(sheet) + decodeExtLst = new(decodeExtLst) + slicerList = new(decodeSlicerList) + ) + if err != nil { + return slicerID, err + } + if ws.ExtLst != nil { + if err = f.xmlNewDecoder(strings.NewReader("" + ws.ExtLst.Ext + "")). + Decode(decodeExtLst); err != nil && err != io.EOF { + return slicerID, err + } + for _, ext := range decodeExtLst.Ext { + if ext.URI == ExtURISlicerListX15 { + _ = f.xmlNewDecoder(strings.NewReader(ext.Content)).Decode(slicerList) + for _, slicer := range slicerList.Slicer { + if slicer.RID != "" { + sheetRelationshipsDrawingXML := f.getSheetRelationshipsTargetByID(sheet, slicer.RID) + slicerID, _ = strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(sheetRelationshipsDrawingXML, "../slicers/slicer"), ".xml")) + return slicerID, err + } + } + } + } + } + sheetRelationshipsSlicerXML := "../slicers/slicer" + strconv.Itoa(slicerID) + ".xml" + sheetXMLPath, _ := f.getSheetXMLPath(sheet) + sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetXMLPath, "xl/worksheets/") + ".rels" + rID := f.addRels(sheetRels, SourceRelationshipSlicer, sheetRelationshipsSlicerXML, "") + f.addSheetNameSpace(sheet, NameSpaceSpreadSheetX14) + return slicerID, f.addSheetTableSlicer(ws, rID) +} + +// addSheetTableSlicer adds a new table slicer for the worksheet by giving the +// worksheet relationships ID. +func (f *File) addSheetTableSlicer(ws *xlsxWorksheet, rID int) error { + var ( + decodeExtLst = new(decodeExtLst) + err error + slicerListBytes, extLstBytes []byte + ) + if ws.ExtLst != nil { + if err = f.xmlNewDecoder(strings.NewReader("" + ws.ExtLst.Ext + "")). + Decode(decodeExtLst); err != nil && err != io.EOF { + return err + } + } + slicerListBytes, _ = xml.Marshal(&xlsxX14SlicerList{ + Slicer: []*xlsxX14Slicer{{RID: "rId" + strconv.Itoa(rID)}}, + }) + decodeExtLst.Ext = append(decodeExtLst.Ext, &xlsxExt{ + xmlns: []xml.Attr{{Name: xml.Name{Local: "xmlns:" + NameSpaceSpreadSheetX15.Name.Local}, Value: NameSpaceSpreadSheetX15.Value}}, + URI: ExtURISlicerListX15, Content: string(slicerListBytes), + }) + 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 +} + +// addSlicer adds a new slicer to the workbook by giving the slicer ID and +// settings. +func (f *File) addSlicer(slicerID int, slicer xlsxSlicer) error { + slicerXML := "xl/slicers/slicer" + strconv.Itoa(slicerID) + ".xml" + slicers, err := f.slicerReader(slicerXML) + if err != nil { + return err + } + if err := f.addContentTypePart(slicerID, "slicer"); err != nil { + return err + } + slicers.Slicer = append(slicers.Slicer, slicer) + output, err := xml.Marshal(slicers) + f.saveFileList(slicerXML, output) + return err +} + +// genSlicerNames generates a unique slicer cache name by giving the slicer name. +func (f *File) genSlicerCacheName(name string) string { + var ( + cnt int + definedNames []string + slicerCacheName string + ) + for _, dn := range f.GetDefinedName() { + if dn.Scope == "Workbook" { + definedNames = append(definedNames, dn.Name) + } + } + for i, c := range name { + if unicode.IsLetter(c) { + slicerCacheName += string(c) + continue + } + if i > 0 && (unicode.IsDigit(c) || c == '.') { + slicerCacheName += string(c) + continue + } + slicerCacheName += "_" + } + slicerCacheName = fmt.Sprintf("Slicer_%s", slicerCacheName) + for { + tmp := slicerCacheName + if cnt > 0 { + tmp = fmt.Sprintf("%s%d", slicerCacheName, cnt) + } + if inStrSlice(definedNames, tmp, true) == -1 { + slicerCacheName = tmp + break + } + cnt++ + } + return slicerCacheName +} + +// setSlicerCache check if a slicer cache already exists or add a new slicer +// cache by giving the column index, slicer, table options, and returns the +// slicer cache name. +func (f *File) setSlicerCache(colIdx int, opts *SlicerOptions, table *Table) (string, error) { + var ok bool + var slicerCacheName string + f.Pkg.Range(func(k, v interface{}) bool { + if strings.Contains(k.(string), "xl/slicerCaches/slicerCache") { + slicerCache := &xlsxSlicerCacheDefinition{} + if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(v.([]byte)))). + Decode(slicerCache); err != nil && err != io.EOF { + return true + } + if slicerCache.ExtLst == nil { + return true + } + ext := new(xlsxExt) + _ = f.xmlNewDecoder(strings.NewReader(slicerCache.ExtLst.Ext)).Decode(ext) + if ext.URI == ExtURISlicerCacheDefinition { + tableSlicerCache := new(decodeTableSlicerCache) + _ = f.xmlNewDecoder(strings.NewReader(ext.Content)).Decode(tableSlicerCache) + if tableSlicerCache.TableID == table.tID && tableSlicerCache.Column == colIdx+1 { + ok, slicerCacheName = true, slicerCache.Name + return false + } + } + } + return true + }) + if ok { + return slicerCacheName, nil + } + slicerCacheName = f.genSlicerCacheName(opts.Name) + return slicerCacheName, f.addSlicerCache(slicerCacheName, colIdx, opts, table) +} + +// slicerReader provides a function to get the pointer to the structure +// after deserialization of xl/slicers/slicer%d.xml. +func (f *File) slicerReader(slicerXML string) (*xlsxSlicers, error) { + content, ok := f.Pkg.Load(slicerXML) + slicer := &xlsxSlicers{ + XMLNSXMC: SourceRelationshipCompatibility.Value, + XMLNSX: NameSpaceSpreadSheet.Value, + XMLNSXR10: NameSpaceSpreadSheetXR10.Value, + } + if ok && content != nil { + if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(content.([]byte)))). + Decode(slicer); err != nil && err != io.EOF { + return nil, err + } + } + return slicer, nil +} + +// addSlicerCache adds a new slicer cache by giving the slicer cache name, +// column index, slicer, and table options. +func (f *File) addSlicerCache(slicerCacheName string, colIdx int, opts *SlicerOptions, table *Table) error { + var ( + slicerCacheBytes, tableSlicerBytes, extLstBytes []byte + slicerCacheID = f.countSlicerCache() + 1 + decodeExtLst = new(decodeExtLst) + slicerCache = xlsxSlicerCacheDefinition{ + XMLNSXMC: SourceRelationshipCompatibility.Value, + XMLNSX: NameSpaceSpreadSheet.Value, + XMLNSX15: NameSpaceSpreadSheetX15.Value, + XMLNSXR10: NameSpaceSpreadSheetXR10.Value, + Name: slicerCacheName, + SourceName: opts.Name, + ExtLst: &xlsxExtLst{}, + } + ) + var sortOrder string + if opts.ItemDesc { + sortOrder = "descending" + } + tableSlicerBytes, _ = xml.Marshal(&xlsxTableSlicerCache{ + TableID: table.tID, + Column: colIdx + 1, + SortOrder: sortOrder, + }) + decodeExtLst.Ext = append(decodeExtLst.Ext, &xlsxExt{ + xmlns: []xml.Attr{{Name: xml.Name{Local: "xmlns:" + NameSpaceSpreadSheetX15.Name.Local}, Value: NameSpaceSpreadSheetX15.Value}}, + URI: ExtURISlicerCacheDefinition, Content: string(tableSlicerBytes), + }) + extLstBytes, _ = xml.Marshal(decodeExtLst) + slicerCache.ExtLst = &xlsxExtLst{Ext: strings.TrimSuffix(strings.TrimPrefix(string(extLstBytes), ""), "")} + slicerCacheXML := "xl/slicerCaches/slicerCache" + strconv.Itoa(slicerCacheID) + ".xml" + slicerCacheBytes, _ = xml.Marshal(slicerCache) + f.saveFileList(slicerCacheXML, slicerCacheBytes) + if err := f.addContentTypePart(slicerCacheID, "slicerCache"); err != nil { + return err + } + if err := f.addWorkbookSlicerCache(slicerCacheID, ExtURISlicerCachesX15); err != nil { + return err + } + return f.SetDefinedName(&DefinedName{Name: slicerCacheName, RefersTo: formulaErrorNA}) +} + +// addDrawingSlicer adds a slicer shape and fallback shape by giving the +// worksheet name, slicer options, and returns slicer name. +func (f *File) addDrawingSlicer(sheet string, opts *SlicerOptions) (string, error) { + var slicerName string + drawingID := f.countDrawings() + 1 + drawingXML := "xl/drawings/drawing" + strconv.Itoa(drawingID) + ".xml" + ws, err := f.workSheetReader(sheet) + if err != nil { + return slicerName, err + } + drawingID, drawingXML = f.prepareDrawing(ws, drawingID, sheet, drawingXML) + content, twoCellAnchor, cNvPrID, err := f.twoCellAnchorShape(sheet, drawingXML, opts.Cell, opts.Width, opts.Height, opts.Format) + if err != nil { + return slicerName, err + } + slicerName = fmt.Sprintf("%s %d", opts.Name, cNvPrID) + graphicFrame := xlsxGraphicFrame{ + NvGraphicFramePr: xlsxNvGraphicFramePr{ + CNvPr: &xlsxCNvPr{ + ID: cNvPrID, + Name: slicerName, + }, + }, + Xfrm: xlsxXfrm{Off: xlsxOff{}, Ext: aExt{}}, + Graphic: &xlsxGraphic{ + GraphicData: &xlsxGraphicData{ + URI: NameSpaceDrawingMLSlicer.Value, + Sle: &xlsxSle{XMLNS: NameSpaceDrawingMLSlicer.Value, Name: slicerName}, + }, + }, + } + graphic, _ := xml.Marshal(graphicFrame) + sp := xdrSp{ + Macro: opts.Macro, + NvSpPr: &xdrNvSpPr{ + CNvPr: &xlsxCNvPr{ + ID: cNvPrID, + }, + CNvSpPr: &xdrCNvSpPr{ + TxBox: true, + }, + }, + SpPr: &xlsxSpPr{ + Xfrm: xlsxXfrm{Off: xlsxOff{X: 2914650, Y: 152400}, Ext: aExt{Cx: 1828800, Cy: 2238375}}, + SolidFill: &xlsxInnerXML{Content: ""}, + PrstGeom: xlsxPrstGeom{ + Prst: "rect", + }, + Ln: xlsxLineProperties{W: 1, SolidFill: &xlsxInnerXML{Content: ""}}, + }, + TxBody: &xdrTxBody{ + BodyPr: &aBodyPr{VertOverflow: "clip", HorzOverflow: "clip"}, + P: []*aP{ + {R: &aR{T: "This shape represents a table slicer. Table slicers are not supported in this version of Excel."}}, + {R: &aR{T: "If the shape was modified in an earlier version of Excel, or if the workbook was saved in Excel 2007 or earlier, the slicer can't be used."}}, + }, + }, + } + shape, _ := xml.Marshal(sp) + twoCellAnchor.ClientData = &xdrClientData{ + FLocksWithSheet: *opts.Format.Locked, + FPrintsWithSheet: *opts.Format.PrintObject, + } + choice := xlsxChoice{ + XMLNSSle15: NameSpaceDrawingMLSlicerX15.Value, + Requires: NameSpaceDrawingMLSlicerX15.Name.Local, + Content: string(graphic), + } + fallback := xlsxFallback{ + Content: string(shape), + } + choiceBytes, _ := xml.Marshal(choice) + shapeBytes, _ := xml.Marshal(fallback) + twoCellAnchor.AlternateContent = append(twoCellAnchor.AlternateContent, &xlsxAlternateContent{ + XMLNSMC: SourceRelationshipCompatibility.Value, + Content: string(choiceBytes) + string(shapeBytes), + }) + content.TwoCellAnchor = append(content.TwoCellAnchor, twoCellAnchor) + f.Drawings.Store(drawingXML, content) + return slicerName, f.addContentTypePart(drawingID, "drawings") +} + +// addWorkbookSlicerCache add the association ID of the slicer cache in +// workbook.xml. +func (f *File) addWorkbookSlicerCache(slicerCacheID int, URI string) error { + var ( + wb *xlsxWorkbook + err error + idx int + appendMode bool + decodeExtLst = new(decodeExtLst) + decodeSlicerCaches *decodeX15SlicerCaches + x15SlicerCaches = new(xlsxX15SlicerCaches) + ext *xlsxExt + slicerCacheBytes, slicerCachesBytes, extLstBytes []byte + ) + if wb, err = f.workbookReader(); err != nil { + return err + } + rID := f.addRels(f.getWorkbookRelsPath(), SourceRelationshipSlicerCache, fmt.Sprintf("/xl/slicerCaches/slicerCache%d.xml", slicerCacheID), "") + if wb.ExtLst != nil { // append mode ext + if err = f.xmlNewDecoder(strings.NewReader("" + wb.ExtLst.Ext + "")). + Decode(decodeExtLst); err != nil && err != io.EOF { + return err + } + for idx, ext = range decodeExtLst.Ext { + if ext.URI == URI { + if URI == ExtURISlicerCachesX15 { + decodeSlicerCaches = new(decodeX15SlicerCaches) + _ = f.xmlNewDecoder(strings.NewReader(ext.Content)).Decode(decodeSlicerCaches) + slicerCache := xlsxX14SlicerCache{RID: fmt.Sprintf("rId%d", rID)} + slicerCacheBytes, _ = xml.Marshal(slicerCache) + x15SlicerCaches.Content = decodeSlicerCaches.Content + string(slicerCacheBytes) + x15SlicerCaches.XMLNS = NameSpaceSpreadSheetX14.Value + slicerCachesBytes, _ = xml.Marshal(x15SlicerCaches) + decodeExtLst.Ext[idx].Content = string(slicerCachesBytes) + appendMode = true + } + } + } + } + if !appendMode { + if URI == ExtURISlicerCachesX15 { + slicerCache := xlsxX14SlicerCache{RID: fmt.Sprintf("rId%d", rID)} + slicerCacheBytes, _ = xml.Marshal(slicerCache) + x15SlicerCaches.Content = string(slicerCacheBytes) + x15SlicerCaches.XMLNS = NameSpaceSpreadSheetX14.Value + slicerCachesBytes, _ = xml.Marshal(x15SlicerCaches) + decodeExtLst.Ext = append(decodeExtLst.Ext, &xlsxExt{ + xmlns: []xml.Attr{{Name: xml.Name{Local: "xmlns:" + NameSpaceSpreadSheetX15.Name.Local}, Value: NameSpaceSpreadSheetX15.Value}}, + URI: ExtURISlicerCachesX15, Content: string(slicerCachesBytes), + }) + } + } + extLstBytes, err = xml.Marshal(decodeExtLst) + wb.ExtLst = &xlsxExtLst{Ext: strings.TrimSuffix(strings.TrimPrefix(string(extLstBytes), ""), "")} + return err +} diff --git a/slicer_test.go b/slicer_test.go new file mode 100644 index 0000000000..663a4e1311 --- /dev/null +++ b/slicer_test.go @@ -0,0 +1,247 @@ +package excelize + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAddSlicer(t *testing.T) { + f := NewFile() + disable, colName := false, "_!@#$%^&*()-+=|\\/<>" + assert.NoError(t, f.SetCellValue("Sheet1", "B1", colName)) + // Create table in a worksheet + assert.NoError(t, f.AddTable("Sheet1", &Table{ + Name: "Table1", + Range: "A1:D5", + })) + assert.NoError(t, f.AddSlicer("Sheet1", &SlicerOptions{ + Name: "Column1", + Table: "Table1", + Cell: "E1", + Caption: "Column1", + })) + assert.NoError(t, f.AddSlicer("Sheet1", &SlicerOptions{ + Name: "Column1", + Table: "Table1", + Cell: "I1", + Caption: "Column1", + })) + assert.NoError(t, f.AddSlicer("Sheet1", &SlicerOptions{ + Name: colName, + Table: "Table1", + Cell: "M1", + Caption: colName, + Macro: "Button1_Click", + Width: 200, + Height: 200, + DisplayHeader: &disable, + ItemDesc: true, + })) + // Test add a table slicer with empty slicer options + assert.Equal(t, ErrParameterRequired, f.AddSlicer("Sheet1", nil)) + // Test add a table slicer with invalid slicer options + for _, opts := range []*SlicerOptions{ + {Table: "Table1", Cell: "Q1"}, + {Name: "Column", Cell: "Q1"}, + {Name: "Column", Table: "Table1"}, + } { + assert.Equal(t, ErrParameterInvalid, f.AddSlicer("Sheet1", opts)) + } + // Test add a table slicer with not exist worksheet + assert.EqualError(t, f.AddSlicer("SheetN", &SlicerOptions{ + Name: "Column2", + Table: "Table1", + Cell: "Q1", + }), "sheet SheetN does not exist") + // Test add a table slicer with not exist table name + assert.Equal(t, newNoExistTableError("Table2"), f.AddSlicer("Sheet1", &SlicerOptions{ + Name: "Column2", + Table: "Table2", + Cell: "Q1", + })) + // Test add a table slicer with invalid slicer name + assert.Equal(t, newInvalidSlicerNameError("Column6"), f.AddSlicer("Sheet1", &SlicerOptions{ + Name: "Column6", + Table: "Table1", + Cell: "Q1", + })) + file, err := os.ReadFile(filepath.Join("test", "vbaProject.bin")) + assert.NoError(t, err) + assert.NoError(t, f.AddVBAProject(file)) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddSlicer.xlsm"))) + assert.NoError(t, f.Close()) + + // Test add a table slicer with invalid worksheet extension list + f = NewFile() + assert.NoError(t, f.AddTable("Sheet1", &Table{ + Name: "Table1", + Range: "A1:D5", + })) + ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") + assert.True(t, ok) + ws.(*xlsxWorksheet).ExtLst = &xlsxExtLst{Ext: "<>"} + assert.Error(t, f.AddSlicer("Sheet1", &SlicerOptions{ + Name: "Column1", + Table: "Table1", + Cell: "E1", + })) + assert.NoError(t, f.Close()) + + // Test add a table slicer with unsupported charset slicer + f = NewFile() + assert.NoError(t, f.AddTable("Sheet1", &Table{ + Name: "Table1", + Range: "A1:D5", + })) + f.Pkg.Store("xl/slicers/slicer2.xml", MacintoshCyrillicCharset) + assert.EqualError(t, f.AddSlicer("Sheet1", &SlicerOptions{ + Name: "Column1", + Table: "Table1", + Cell: "E1", + }), "XML syntax error on line 1: invalid UTF-8") + assert.NoError(t, f.Close()) + + // Test add a table slicer with read workbook error + f = NewFile() + assert.NoError(t, f.AddTable("Sheet1", &Table{ + Name: "Table1", + Range: "A1:D5", + })) + f.WorkBook.ExtLst = &xlsxExtLst{Ext: "<>"} + assert.Error(t, f.AddSlicer("Sheet1", &SlicerOptions{ + Name: "Column1", + Table: "Table1", + Cell: "E1", + })) + assert.NoError(t, f.Close()) + + // Test add a table slicer with unsupported charset content types + f = NewFile() + assert.NoError(t, f.AddTable("Sheet1", &Table{ + Name: "Table1", + Range: "A1:D5", + })) + f.ContentTypes = nil + f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) + assert.EqualError(t, f.AddSlicer("Sheet1", &SlicerOptions{ + Name: "Column1", + Table: "Table1", + Cell: "E1", + }), "XML syntax error on line 1: invalid UTF-8") + f.ContentTypes = nil + f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) + assert.EqualError(t, f.addSlicer(0, xlsxSlicer{}), "XML syntax error on line 1: invalid UTF-8") + assert.NoError(t, f.Close()) + + f = NewFile() + // Create table in a worksheet + assert.NoError(t, f.AddTable("Sheet1", &Table{ + Name: "Table1", + Range: "A1:D5", + })) + f.Pkg.Store("xl/drawings/drawing2.xml", MacintoshCyrillicCharset) + assert.EqualError(t, f.AddSlicer("Sheet1", &SlicerOptions{ + Name: "Column1", + Table: "Table1", + Cell: "E1", + Caption: "Column1", + }), "XML syntax error on line 1: invalid UTF-8") + assert.NoError(t, f.Close()) +} + +func TestAddSheetSlicer(t *testing.T) { + f := NewFile() + // Test add sheet slicer with not exist worksheet name + _, err := f.addSheetSlicer("SheetN") + assert.EqualError(t, err, "sheet SheetN does not exist") + assert.NoError(t, f.Close()) +} + +func TestAddSheetTableSlicer(t *testing.T) { + f := NewFile() + // Test add sheet table slicer with invalid worksheet extension + assert.Error(t, f.addSheetTableSlicer(&xlsxWorksheet{ExtLst: &xlsxExtLst{Ext: "<>"}}, 0)) + // Test add sheet table slicer with existing worksheet extension + assert.NoError(t, f.addSheetTableSlicer(&xlsxWorksheet{ExtLst: &xlsxExtLst{Ext: fmt.Sprintf("", ExtURITimelineRefs)}}, 1)) + assert.NoError(t, f.Close()) +} + +func TestSetSlicerCache(t *testing.T) { + f := NewFile() + f.Pkg.Store("xl/slicerCaches/slicerCache1.xml", MacintoshCyrillicCharset) + _, err := f.setSlicerCache(1, &SlicerOptions{}, &Table{}) + assert.NoError(t, err) + assert.NoError(t, f.Close()) + + f = NewFile() + + f.Pkg.Store("xl/slicerCaches/slicerCache2.xml", []byte(fmt.Sprintf(``, NameSpaceSpreadSheetX14.Value, ExtURISlicerCacheDefinition))) + _, err = f.setSlicerCache(1, &SlicerOptions{}, &Table{}) + assert.NoError(t, err) + assert.NoError(t, f.Close()) + + f = NewFile() + f.Pkg.Store("xl/slicerCaches/slicerCache2.xml", []byte(fmt.Sprintf(``, NameSpaceSpreadSheetX14.Value, ExtURISlicerCacheDefinition))) + _, err = f.setSlicerCache(1, &SlicerOptions{}, &Table{}) + assert.NoError(t, err) + assert.NoError(t, f.Close()) + + f = NewFile() + f.Pkg.Store("xl/slicerCaches/slicerCache2.xml", []byte(fmt.Sprintf(``, NameSpaceSpreadSheetX14.Value, ExtURISlicerCacheDefinition))) + _, err = f.setSlicerCache(1, &SlicerOptions{}, &Table{tID: 1}) + assert.NoError(t, err) + assert.NoError(t, f.Close()) + + f = NewFile() + f.Pkg.Store("xl/slicerCaches/slicerCache2.xml", []byte(fmt.Sprintf(``, NameSpaceSpreadSheetX14.Value))) + _, err = f.setSlicerCache(1, &SlicerOptions{}, &Table{tID: 1}) + assert.NoError(t, err) + assert.NoError(t, f.Close()) +} + +func TestAddSlicerCache(t *testing.T) { + f := NewFile() + f.ContentTypes = nil + f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) + assert.EqualError(t, f.addSlicerCache("Slicer1", 0, &SlicerOptions{}, &Table{}), "XML syntax error on line 1: invalid UTF-8") + assert.NoError(t, f.Close()) +} + +func TestAddDrawingSlicer(t *testing.T) { + f := NewFile() + // Test add a drawing slicer with not exist worksheet + _, err := f.addDrawingSlicer("SheetN", &SlicerOptions{ + Name: "Column2", + Table: "Table1", + Cell: "Q1", + }) + assert.EqualError(t, err, "sheet SheetN does not exist") + // Test add a drawing slicer with invalid cell reference + _, err = f.addDrawingSlicer("Sheet1", &SlicerOptions{ + Name: "Column2", + Table: "Table1", + Cell: "A", + }) + assert.EqualError(t, err, "cannot convert cell \"A\" to coordinates: invalid cell name \"A\"") + assert.NoError(t, f.Close()) +} + +func TestAddWorkbookSlicerCache(t *testing.T) { + // Test add a workbook slicer cache with with unsupported charset workbook + f := NewFile() + f.WorkBook = nil + f.Pkg.Store("xl/workbook.xml", MacintoshCyrillicCharset) + assert.EqualError(t, f.addWorkbookSlicerCache(1, ExtURISlicerCachesX15), "XML syntax error on line 1: invalid UTF-8") + assert.NoError(t, f.Close()) +} + +func TestGenSlicerCacheName(t *testing.T) { + f := NewFile() + assert.NoError(t, f.SetDefinedName(&DefinedName{Name: "Slicer_Column_1", RefersTo: formulaErrorNA})) + assert.Equal(t, "Slicer_Column_11", f.genSlicerCacheName("Column 1")) + assert.NoError(t, f.Close()) +} diff --git a/sparkline.go b/sparkline.go index a208773844..7bb50f0f9e 100644 --- a/sparkline.go +++ b/sparkline.go @@ -489,9 +489,9 @@ func (f *File) appendSparkline(ws *xlsxWorksheet, group *xlsxX14SparklineGroup, err error idx int appendMode bool - decodeExtLst = new(decodeWorksheetExt) + decodeExtLst = new(decodeExtLst) decodeSparklineGroups *decodeX14SparklineGroups - ext *xlsxWorksheetExt + ext *xlsxExt sparklineGroupsBytes, sparklineGroupBytes, extLstBytes []byte ) sparklineGroupBytes, _ = xml.Marshal(group) @@ -523,7 +523,7 @@ func (f *File) appendSparkline(ws *xlsxWorksheet, group *xlsxX14SparklineGroup, XMLNSXM: NameSpaceSpreadSheetExcel2006Main.Value, SparklineGroups: []*xlsxX14SparklineGroup{group}, }) - decodeExtLst.Ext = append(decodeExtLst.Ext, &xlsxWorksheetExt{ + decodeExtLst.Ext = append(decodeExtLst.Ext, &xlsxExt{ URI: ExtURISparklineGroups, Content: string(sparklineGroupsBytes), }) } diff --git a/styles.go b/styles.go index 13904679e1..7f0601ea14 100644 --- a/styles.go +++ b/styles.go @@ -2615,10 +2615,10 @@ func (f *File) appendCfRule(ws *xlsxWorksheet, rule *xlsxX14CfRule) error { err error idx int appendMode bool - decodeExtLst = new(decodeWorksheetExt) + decodeExtLst = new(decodeExtLst) condFmts *xlsxX14ConditionalFormattings decodeCondFmts *decodeX14ConditionalFormattings - ext *xlsxWorksheetExt + ext *xlsxExt condFmtBytes, condFmtsBytes, extLstBytes []byte ) condFmtBytes, _ = xml.Marshal([]*xlsxX14ConditionalFormatting{ @@ -2645,7 +2645,7 @@ func (f *File) appendCfRule(ws *xlsxWorksheet, rule *xlsxX14CfRule) error { } if !appendMode { condFmtsBytes, _ = xml.Marshal(&xlsxX14ConditionalFormattings{Content: string(condFmtBytes)}) - decodeExtLst.Ext = append(decodeExtLst.Ext, &xlsxWorksheetExt{ + decodeExtLst.Ext = append(decodeExtLst.Ext, &xlsxExt{ URI: ExtURIConditionalFormattings, Content: string(condFmtsBytes), }) } @@ -2781,7 +2781,7 @@ func extractCondFmtDataBar(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatO } } } - extractExtLst := func(extLst *decodeWorksheetExt) { + extractExtLst := func(extLst *decodeExtLst) { for _, ext := range extLst.Ext { if ext.URI == ExtURIConditionalFormattings { decodeCondFmts := new(decodeX14ConditionalFormattings) @@ -2797,7 +2797,7 @@ func extractCondFmtDataBar(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatO if c.ExtLst != nil { ext := decodeX14ConditionalFormattingExt{} if err := xml.Unmarshal([]byte(c.ExtLst.Ext), &ext); err == nil && extLst != nil { - decodeExtLst := new(decodeWorksheetExt) + decodeExtLst := new(decodeExtLst) if err = xml.Unmarshal([]byte(""+extLst.Ext+""), decodeExtLst); err == nil { extractExtLst(decodeExtLst) } diff --git a/table.go b/table.go index 196be5ef86..f365a63e59 100644 --- a/table.go +++ b/table.go @@ -151,6 +151,7 @@ func (f *File) GetTables(sheet string) ([]Table, error) { } table := Table{ rID: tbl.RID, + tID: t.ID, Range: t.Ref, Name: t.Name, } @@ -216,7 +217,15 @@ func (f *File) countTables() int { count := 0 f.Pkg.Range(func(k, v interface{}) bool { if strings.Contains(k.(string), "xl/tables/table") { - count++ + var t xlsxTable + if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(v.([]byte)))). + Decode(&t); err != nil && err != io.EOF { + count++ + return true + } + if count < t.ID { + count = t.ID + } } return true }) @@ -343,9 +352,9 @@ func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, opts *Tab t.AutoFilter = nil t.HeaderRowCount = intPtr(0) } - table, _ := xml.Marshal(t) + table, err := xml.Marshal(t) f.saveFileList(tableXML, table) - return nil + return err } // AutoFilter provides the method to add auto filter in a worksheet by given diff --git a/templates.go b/templates.go index 91a7ed80aa..1c6f4ddb16 100644 --- a/templates.go +++ b/templates.go @@ -14,6 +14,190 @@ package excelize +import "encoding/xml" + +// Source relationship and namespace list, associated prefixes and schema in which it was +// introduced. +var ( + 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"} + NameSpaceDrawingMLSlicer = xml.Attr{Name: xml.Name{Local: "sle", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/drawing/2010/slicer"} + NameSpaceDrawingMLSlicerX15 = xml.Attr{Name: xml.Name{Local: "sle15", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/drawing/2012/slicer"} + NameSpaceDrawingMLSpreadSheet = xml.Attr{Name: xml.Name{Local: "xdr", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing"} + NameSpaceMacExcel2008Main = xml.Attr{Name: xml.Name{Local: "mx", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/mac/excel/2008/main"} + 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"} + NameSpaceSpreadSheetXR10 = xml.Attr{Name: xml.Name{Local: "xr10", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/spreadsheetml/2016/revision10"} + 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 ( + 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" + ContentTypeRelationships = "application/vnd.openxmlformats-package.relationships+xml" + ContentTypeSheetML = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml" + ContentTypeSlicer = "application/vnd.ms-excel.slicer+xml" + ContentTypeSlicerCache = "application/vnd.ms-excel.slicerCache+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" + SourceRelationshipSlicer = "http://schemas.microsoft.com/office/2007/relationships/slicer" + SourceRelationshipSlicerCache = "http://schemas.microsoft.com/office/2007/relationships/slicerCache" + 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" + // The following constants defined the extLst child element + // ([ISO/IEC29500-1:2016] section 18.2.10) of the workbook and worksheet + // elements extended by the addition of new child ext elements. + ExtURICalcFeatures = "{B58B0392-4F1F-4190-BB64-5DF3571DCE5F}" + ExtURIConditionalFormattingRuleID = "{B025F937-C7B1-47D3-B67F-A62EFF666E3E}" + ExtURIConditionalFormattings = "{78C0D931-6437-407d-A8EE-F0AAD7539E65}" + ExtURIDataModel = "{FCE2AD5D-F65C-4FA6-A056-5C36A1767C68}" + ExtURIDataValidations = "{CCE6A557-97BC-4B89-ADB6-D9C93CAAB3DF}" + ExtURIDrawingBlip = "{28A0092B-C50C-407E-A947-70E740481C1C}" + ExtURIExternalLinkPr = "{FCE6A71B-6B00-49CD-AB44-F6B1AE7CDE65}" + ExtURIIgnoredErrors = "{01252117-D84E-4E92-8308-4BE1C098FCBB}" + ExtURIMacExcelMX = "{64002731-A6B0-56B0-2670-7721B7C09600}" + ExtURIModelTimeGroupings = "{9835A34E-60A6-4A7C-AAB8-D5F71C897F49}" + ExtURIPivotCacheDefinition = "{725AE2AE-9491-48be-B2B4-4EB974FC3084}" + ExtURIPivotCachesX14 = "{876F7934-8845-4945-9796-88D515C7AA90}" + ExtURIPivotCachesX15 = "{841E416B-1EF1-43b6-AB56-02D37102CBD5}" + ExtURIPivotTableReferences = "{983426D0-5260-488c-9760-48F4B6AC55F4}" + ExtURIProtectedRanges = "{FC87AEE6-9EDD-4A0A-B7FB-166176984837}" + ExtURISlicerCacheDefinition = "{2F2917AC-EB37-4324-AD4E-5DD8C200BD13}" + ExtURISlicerCacheHideItemsWithNoData = "{470722E0-AACD-4C17-9CDC-17EF765DBC7E}" + ExtURISlicerCachesX14 = "{BBE1A952-AA13-448e-AADC-164F8A28A991}" + ExtURISlicerCachesX15 = "{46BE6895-7355-4a93-B00E-2C351335B9C9}" + ExtURISlicerListX14 = "{A8765BA9-456A-4DAB-B4F3-ACF838C121DE}" + ExtURISlicerListX15 = "{3A4CF648-6AED-40f4-86FF-DC5316D8AED3}" + ExtURISparklineGroups = "{05C60535-1F16-4fd2-B633-F4F36F0B64E0}" + ExtURISVG = "{96DAC541-7B7A-43D3-8B79-37D633B846F1}" + ExtURITimelineCachePivotCaches = "{A2CB5862-8E78-49c6-8D9D-AF26E26ADB89}" + ExtURITimelineCacheRefs = "{D0CA8CA8-9F24-4464-BF8E-62219DCF47F9}" + ExtURITimelineRefs = "{7E03D99C-DC04-49d9-9315-930204A7B6E9}" + ExtURIWebExtensions = "{F7C9EE02-42E1-4005-9D12-6889AFFD525C}" + ExtURIWorkbookPrX14 = "{79F54976-1DA5-4618-B147-ACDE4B953A38}" + ExtURIWorkbookPrX15 = "{140A7094-0E35-4892-8432-C4D2E57EDEB5}" +) + +// extensionURIPriority is the priority of URI in the extension lists. +var extensionURIPriority = []string{ + ExtURIConditionalFormattings, + ExtURIDataValidations, + ExtURISparklineGroups, + ExtURISlicerListX14, + ExtURIProtectedRanges, + ExtURIIgnoredErrors, + ExtURIWebExtensions, + ExtURISlicerListX15, + ExtURITimelineRefs, + ExtURIExternalLinkPr, +} + +// Excel specifications and limits +const ( + MaxCellStyles = 65430 + MaxColumns = 16384 + MaxColumnWidth = 255 + MaxFieldLength = 255 + MaxFilePathLength = 207 + MaxFormControlValue = 30000 + MaxFontFamilyLength = 31 + MaxFontSize = 409 + MaxRowHeight = 409 + MaxSheetNameLength = 31 + MinColumns = 1 + MinFontSize = 1 + StreamChunkSize = 1 << 24 + TotalCellChars = 32767 + TotalRows = 1048576 + TotalSheetHyperlinks = 65529 + 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 + // PivotTables created in Excel 2007 or a newer version of Excel. + pivotTableVersion = 3 + defaultDrawingScale = 1.0 + defaultChartDimensionWidth = 480 + defaultChartDimensionHeight = 260 + defaultSlicerWidth = 200 + defaultSlicerHeight = 200 + defaultChartLegendPosition = "bottom" + defaultChartShowBlanksAs = "gap" + defaultShapeSize = 160 + defaultShapeLineWidth = 1 +) + +// 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 +) + const ( defaultXMLPathContentTypes = "[Content_Types].xml" defaultXMLPathDocPropsApp = "docProps/app.xml" @@ -27,6 +211,59 @@ const ( defaultTempFileSST = "sharedStrings" ) +// 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", + "000000", "FFFFFF", +} + +// supportedImageTypes defined supported image types. +var supportedImageTypes = map[string]string{ + ".bmp": ".bmp", ".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{ + ".xlam": ContentTypeAddinMacro, + ".xlsm": ContentTypeMacro, + ".xlsx": ContentTypeSheetML, + ".xltm": ContentTypeTemplateMacro, + ".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", +} + +// supportedPositioning defined supported positioning types. +var supportedPositioning = []string{"absolute", "oneCell", "twoCell"} + +// builtInDefinedNames defined built-in defined names are built with a _xlnm prefix. +var builtInDefinedNames = []string{"_xlnm.Print_Area", "_xlnm.Print_Titles", "_xlnm._FilterDatabase"} + const templateDocpropsApp = `0Go Excelize` const templateContentTypes = `` diff --git a/xmlDecodeDrawing.go b/xmlDecodeDrawing.go index a8b39d5c24..62251deadc 100644 --- a/xmlDecodeDrawing.go +++ b/xmlDecodeDrawing.go @@ -47,7 +47,7 @@ type decodeSp struct { // appearance of the shape to be stored. type decodeNvSpPr struct { CNvPr *decodeCNvPr `xml:"cNvPr"` - ExtLst *decodeExt `xml:"extLst"` + ExtLst *decodeAExt `xml:"extLst"` CNvSpPr *decodeCNvSpPr `xml:"cNvSpPr"` } @@ -78,10 +78,11 @@ type decodeWsDr struct { // information that does not affect the appearance of the picture to be // stored. type decodeCNvPr struct { - ID int `xml:"id,attr"` - Name string `xml:"name,attr"` - Descr string `xml:"descr,attr"` - Title string `xml:"title,attr,omitempty"` + XMLName xml.Name `xml:"cNvPr"` + ID int `xml:"id,attr"` + Name string `xml:"name,attr"` + Descr string `xml:"descr,attr"` + Title string `xml:"title,attr,omitempty"` } // decodePicLocks directly maps the picLocks (Picture Locks). This element @@ -124,8 +125,8 @@ type decodeOff struct { Y int `xml:"y,attr"` } -// decodeExt directly maps the ext element. -type decodeExt struct { +// decodeAExt directly maps the a:ext element. +type decodeAExt struct { Cx int `xml:"cx,attr"` Cy int `xml:"cy,attr"` } @@ -143,8 +144,8 @@ type decodePrstGeom struct { // frame. This transformation is applied to the graphic frame just as it would // be for a shape or group shape. type decodeXfrm struct { - Off decodeOff `xml:"off"` - Ext decodeExt `xml:"ext"` + Off decodeOff `xml:"off"` + Ext decodeAExt `xml:"ext"` } // decodeCNvPicPr directly maps the cNvPicPr (Non-Visual Picture Drawing diff --git a/xmlDrawing.go b/xmlDrawing.go index 688e601637..13ed87f7f3 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -16,220 +16,6 @@ import ( "sync" ) -// Source relationship and namespace list, associated prefixes and schema in which it was -// introduced. -var ( - 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"} - NameSpaceMacExcel2008Main = xml.Attr{Name: xml.Name{Local: "mx", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/mac/excel/2008/main"} - 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"} - NameSpaceSpreadSheetXR10 = xml.Attr{Name: xml.Name{Local: "xr10", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/spreadsheetml/2016/revision10"} - 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 ( - 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" - ContentTypeSlicer = "application/vnd.ms-excel.slicer+xml" - ContentTypeSlicerCache = "application/vnd.ms-excel.slicerCache+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" - SourceRelationshipSlicer = "http://schemas.microsoft.com/office/2007/relationships/slicer" - 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 - // new child ext elements ([ISO/IEC29500-1:2016] section 18.2.7) - 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}" - ExtURIPivotCacheDefinition = "{725AE2AE-9491-48be-B2B4-4EB974FC3084}" - 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}" -) - -// 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 - MaxColumns = 16384 - MaxColumnWidth = 255 - MaxFieldLength = 255 - MaxFilePathLength = 207 - MaxFormControlValue = 30000 - MaxFontFamilyLength = 31 - MaxFontSize = 409 - MaxRowHeight = 409 - MaxSheetNameLength = 31 - MinColumns = 1 - MinFontSize = 1 - StreamChunkSize = 1 << 24 - TotalCellChars = 32767 - TotalRows = 1048576 - TotalSheetHyperlinks = 65529 - 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 - // PivotTables created in Excel 2007 or a newer version of Excel. - pivotTableVersion = 3 - defaultPictureScale = 1.0 - defaultChartDimensionWidth = 480 - defaultChartDimensionHeight = 260 - defaultChartLegendPosition = "bottom" - defaultChartShowBlanksAs = "gap" - defaultShapeSize = 160 - defaultShapeLineWidth = 1 -) - -// 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 -) - -// 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", - "000000", "FFFFFF", -} - -// supportedImageTypes defined supported image types. -var supportedImageTypes = map[string]string{ - ".bmp": ".bmp", ".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{ - ".xlam": ContentTypeAddinMacro, - ".xlsm": ContentTypeMacro, - ".xlsx": ContentTypeSheetML, - ".xltm": ContentTypeTemplateMacro, - ".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", -} - -// supportedPositioning defined supported positioning types. -var supportedPositioning = []string{"absolute", "oneCell", "twoCell"} - -// builtInDefinedNames defined built-in defined names are built with a _xlnm prefix. -var builtInDefinedNames = []string{"_xlnm.Print_Area", "_xlnm.Print_Titles", "_xlnm._FilterDatabase"} - // 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. @@ -297,8 +83,8 @@ type xlsxOff struct { Y int `xml:"y,attr"` } -// xlsxExt directly maps the ext element. -type xlsxExt struct { +// aExt directly maps the a:ext element. +type aExt struct { Cx int `xml:"cx,attr"` Cy int `xml:"cy,attr"` } @@ -317,7 +103,7 @@ type xlsxPrstGeom struct { // be for a shape or group shape. type xlsxXfrm struct { Off xlsxOff `xml:"a:off"` - Ext xlsxExt `xml:"a:ext"` + Ext aExt `xml:"a:ext"` } // xlsxCNvPicPr directly maps the cNvPicPr (Non-Visual Picture Drawing @@ -375,7 +161,8 @@ type xlsxBlipFill struct { // has a minimum value of greater than or equal to 0. This simple type has a // maximum value of less than or equal to 20116800. type xlsxLineProperties struct { - W int `xml:"w,attr,omitempty"` + W int `xml:"w,attr,omitempty"` + SolidFill *xlsxInnerXML `xml:"a:solidFill"` } // xlsxSpPr directly maps the spPr (Shape Properties). This element specifies @@ -384,9 +171,10 @@ type xlsxLineProperties struct { // but are used here to describe the visual appearance of a picture within a // document. type xlsxSpPr struct { - Xfrm xlsxXfrm `xml:"a:xfrm"` - PrstGeom xlsxPrstGeom `xml:"a:prstGeom"` - Ln xlsxLineProperties `xml:"a:ln"` + Xfrm xlsxXfrm `xml:"a:xfrm"` + PrstGeom xlsxPrstGeom `xml:"a:prstGeom"` + SolidFill *xlsxInnerXML `xml:"a:solidFill"` + Ln xlsxLineProperties `xml:"a:ln"` } // xlsxPic elements encompass the definition of pictures within the DrawingML @@ -426,20 +214,20 @@ type xdrClientData struct { FPrintsWithSheet bool `xml:"fPrintsWithSheet,attr"` } -// xdrCellAnchor directly maps the oneCellAnchor (One Cell Anchor Shape Size) -// and twoCellAnchor (Two Cell Anchor Shape Size). This element specifies a two -// cell anchor placeholder for a group, a shape, or a drawing element. It moves -// with cells and its extents are in EMU units. +// xdrCellAnchor specifies a oneCellAnchor (One Cell Anchor Shape Size) and +// twoCellAnchor (Two Cell Anchor Shape Size) placeholder for a group, a shape, +// or a drawing element. It moves with cells and its extents are in EMU units. type xdrCellAnchor struct { - EditAs string `xml:"editAs,attr,omitempty"` - Pos *xlsxPoint2D `xml:"xdr:pos"` - From *xlsxFrom `xml:"xdr:from"` - To *xlsxTo `xml:"xdr:to"` - Ext *xlsxExt `xml:"xdr:ext"` - Sp *xdrSp `xml:"xdr:sp"` - Pic *xlsxPic `xml:"xdr:pic,omitempty"` - GraphicFrame string `xml:",innerxml"` - ClientData *xdrClientData `xml:"xdr:clientData"` + EditAs string `xml:"editAs,attr,omitempty"` + Pos *xlsxPoint2D `xml:"xdr:pos"` + From *xlsxFrom `xml:"xdr:from"` + To *xlsxTo `xml:"xdr:to"` + Ext *aExt `xml:"xdr:ext"` + Sp *xdrSp `xml:"xdr:sp"` + Pic *xlsxPic `xml:"xdr:pic,omitempty"` + GraphicFrame string `xml:",innerxml"` + AlternateContent []*xlsxAlternateContent `xml:"mc:AlternateContent"` + ClientData *xdrClientData `xml:"xdr:clientData"` } // xlsxPoint2D describes the position of a drawing element within a spreadsheet. @@ -503,6 +291,12 @@ type xlsxGraphic struct { type xlsxGraphicData struct { URI string `xml:"uri,attr"` Chart *xlsxChart `xml:"c:chart,omitempty"` + Sle *xlsxSle `xml:"sle:slicer"` +} + +type xlsxSle struct { + XMLNS string `xml:"xmlns:sle,attr"` + Name string `xml:"name,attr"` } // xlsxChart (Chart) directly maps the c:chart element. @@ -520,6 +314,7 @@ type xlsxChart struct { // This shape is specified along with all other shapes within either the shape // tree or group shape elements. type xdrSp struct { + XMLName xml.Name `xml:"xdr:sp"` Macro string `xml:"macro,attr"` Textlink string `xml:"textlink,attr"` NvSpPr *xdrNvSpPr `xml:"xdr:nvSpPr"` diff --git a/xmlSlicers.go b/xmlSlicers.go new file mode 100644 index 0000000000..e259de8990 --- /dev/null +++ b/xmlSlicers.go @@ -0,0 +1,168 @@ +// 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. +// +// Package excelize 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. + +package excelize + +import "encoding/xml" + +// xlsxSlicers directly maps the slicers element that specifies a slicer view on +// the worksheet. +type xlsxSlicers struct { + XMLName xml.Name `xml:"http://schemas.microsoft.com/office/spreadsheetml/2009/9/main slicers"` + XMLNSXMC string `xml:"xmlns:mc,attr"` + XMLNSX string `xml:"xmlns:x,attr"` + XMLNSXR10 string `xml:"xmlns:xr10,attr"` + Slicer []xlsxSlicer `xml:"slicer"` +} + +// xlsxSlicer is a complex type that specifies a slicer view. +type xlsxSlicer struct { + Name string `xml:"name,attr"` + XR10UID string `xml:"xr10:uid,attr,omitempty"` + Cache string `xml:"cache,attr"` + Caption string `xml:"caption,attr,omitempty"` + StartItem *int `xml:"startItem,attr"` + ColumnCount *int `xml:"columnCount,attr"` + ShowCaption *bool `xml:"showCaption,attr"` + Level int `xml:"level,attr,omitempty"` + Style string `xml:"style,attr,omitempty"` + LockedPosition bool `xml:"lockedPosition,attr,omitempty"` + RowHeight int `xml:"rowHeight,attr"` +} + +// slicerCacheDefinition directly maps the slicerCacheDefinition element that +// specifies a slicer cache. +type xlsxSlicerCacheDefinition struct { + XMLName xml.Name `xml:"http://schemas.microsoft.com/office/spreadsheetml/2009/9/main slicerCacheDefinition"` + XMLNSXMC string `xml:"xmlns:mc,attr"` + XMLNSX string `xml:"xmlns:x,attr"` + XMLNSX15 string `xml:"xmlns:x15,attr,omitempty"` + XMLNSXR10 string `xml:"xmlns:xr10,attr"` + Name string `xml:"name,attr"` + XR10UID string `xml:"xr10:uid,attr,omitempty"` + SourceName string `xml:"sourceName,attr"` + PivotTables *xlsxSlicerCachePivotTables `xml:"pivotTables"` + Data *xlsxSlicerCacheData `xml:"data"` + ExtLst *xlsxExtLst `xml:"extLst"` +} + +// xlsxSlicerCachePivotTables is a complex type that specifies a group of +// pivotTable elements that specify the PivotTable views that are filtered by +// the slicer cache. +type xlsxSlicerCachePivotTables struct { + PivotTable []xlsxSlicerCachePivotTable `xml:"pivotTable"` +} + +// xlsxSlicerCachePivotTable is a complex type that specifies a PivotTable view +// filtered by a slicer cache. +type xlsxSlicerCachePivotTable struct { + TabID int `xml:"tabId,attr"` + Name string `xml:"name,attr"` +} + +// xlsxSlicerCacheData is a complex type that specifies a data source for the +// slicer cache. +type xlsxSlicerCacheData struct { + OLAP *xlsxInnerXML `xml:"olap"` + Tabular *xlsxTabularSlicerCache `xml:"tabular"` +} + +// xlsxTabularSlicerCache is a complex type that specifies non-OLAP slicer items +// that are cached within this slicer cache and properties of the slicer cache +// specific to non-OLAP slicer items. +type xlsxTabularSlicerCache struct { + PivotCacheID int `xml:"pivotCacheId,attr"` + SortOrder string `xml:"sortOrder,attr,omitempty"` + CustomListSort *bool `xml:"customListSort,attr"` + ShowMissing *bool `xml:"showMissing,attr"` + CrossFilter string `xml:"crossFilter,attr,omitempty"` + Items *xlsxTabularSlicerCacheItems `xml:"items"` + ExtLst *xlsxExtLst `xml:"extLst"` +} + +// xlsxTabularSlicerCacheItems is a complex type that specifies non-OLAP slicer +// items that are cached within this slicer cache. +type xlsxTabularSlicerCacheItems struct { + Count int `xml:"count,attr,omitempty"` + I []xlsxTabularSlicerCacheItem `xml:"i"` +} + +// xlsxTabularSlicerCacheItem is a complex type that specifies a non-OLAP slicer +// item that is cached within this slicer cache. +type xlsxTabularSlicerCacheItem struct { + X int `xml:"x,attr"` + S bool `xml:"s,attr,omitempty"` + ND bool `xml:"nd,attr,omitempty"` +} + +// xlsxTableSlicerCache specifies a table data source for the slicer cache. +type xlsxTableSlicerCache struct { + XMLName xml.Name `xml:"x15:tableSlicerCache"` + TableID int `xml:"tableId,attr"` + Column int `xml:"column,attr"` + SortOrder string `xml:"sortOrder,attr,omitempty"` + CustomListSort *bool `xml:"customListSort,attr"` + CrossFilter string `xml:"crossFilter,attr,omitempty"` + ExtLst *xlsxExtLst `xml:"extLst"` +} + +// xlsxX14SlicerList specifies a list of slicer. +type xlsxX14SlicerList struct { + XMLName xml.Name `xml:"x14:slicerList"` + Slicer []*xlsxX14Slicer `xml:"x14:slicer"` +} + +// xlsxX14Slicer specifies a slicer view, +type xlsxX14Slicer struct { + XMLName xml.Name `xml:"x14:slicer"` + RID string `xml:"r:id,attr"` +} + +// xlsxX15SlicerCaches directly maps the x14:slicerCache element. +type xlsxX14SlicerCache struct { + XMLName xml.Name `xml:"x14:slicerCache"` + RID string `xml:"r:id,attr"` +} + +// xlsxX15SlicerCaches directly maps the x15:slicerCaches element. +type xlsxX15SlicerCaches struct { + XMLName xml.Name `xml:"x15:slicerCaches"` + XMLNS string `xml:"xmlns:x14,attr"` + Content string `xml:",innerxml"` +} + +// decodeTableSlicerCache defines the structure used to parse the +// x15:tableSlicerCache element of the table slicer cache. +type decodeTableSlicerCache struct { + XMLName xml.Name `xml:"tableSlicerCache"` + TableID int `xml:"tableId,attr"` + Column int `xml:"column,attr"` +} + +// decodeSlicerList defines the structure used to parse the x14:slicerList +// element of a list of slicer. +type decodeSlicerList struct { + XMLName xml.Name `xml:"slicerList"` + Slicer []*decodeSlicer `xml:"slicer"` +} + +// decodeSlicer defines the structure used to parse the x14:slicer element of a +// slicer. +type decodeSlicer struct { + RID string `xml:"id,attr"` +} + +// decodeX15SlicerCaches defines the structure used to parse the +// x15:slicerCaches element of a slicer cache. +type decodeX15SlicerCaches struct { + XMLName xml.Name `xml:"slicerCaches"` + Content string `xml:",innerxml"` +} diff --git a/xmlStyles.go b/xmlStyles.go index e7de885629..3dd61c3307 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -257,7 +257,7 @@ type xlsxDxf struct { Alignment *xlsxAlignment `xml:"alignment"` Border *xlsxBorder `xml:"border"` Protection *xlsxProtection `xml:"protection"` - ExtLst *xlsxExt `xml:"extLst"` + ExtLst *aExt `xml:"extLst"` } // xlsxTableStyles directly maps the tableStyles element. This element @@ -312,7 +312,7 @@ type xlsxIndexedColors struct { // a custom color has been selected while using this workbook. type xlsxStyleColors struct { IndexedColors *xlsxIndexedColors `xml:"indexedColors"` - MruColors xlsxInnerXML `xml:"mruColors"` + MruColors *xlsxInnerXML `xml:"mruColors"` } // Alignment directly maps the alignment settings of the cells. diff --git a/xmlTable.go b/xmlTable.go index 00fa6748c9..16fd284cab 100644 --- a/xmlTable.go +++ b/xmlTable.go @@ -198,6 +198,7 @@ type xlsxTableStyleInfo struct { // Table directly maps the format settings of the table. type Table struct { + tID int rID string Range string Name string diff --git a/xmlWorkbook.go b/xmlWorkbook.go index c00637508c..0a3586f8e7 100644 --- a/xmlWorkbook.go +++ b/xmlWorkbook.go @@ -16,7 +16,8 @@ import ( "sync" ) -// xlsxRelationships describe references from parts to other internal resources in the package or to external resources. +// xlsxRelationships describe references from parts to other internal resources +// in the package or to external resources. type xlsxRelationships struct { mu sync.Mutex XMLName xml.Name `xml:"http://schemas.openxmlformats.org/package/2006/relationships Relationships"` @@ -218,6 +219,63 @@ type xlsxExtLst struct { Ext string `xml:",innerxml"` } +// xlsxExt represents a the future feature data storage area. Each extension +// within an extension list shall be contained within an ext element. +// Extensions shall be versioned by namespace, using the uri attribute, and +// shall be allowed to appear in any order within the extension list. Any +// number of extensions shall be allowed within an extension list. +type xlsxExt struct { + XMLName xml.Name `xml:"ext"` + URI string `xml:"uri,attr"` + Content string `xml:",innerxml"` + xmlns []xml.Attr +} + +// xlsxAlternateContent is a container for a sequence of multiple +// representations of a given piece of content. The program reading the file +// should only process one of these, and the one chosen should be based on +// which conditions match. +type xlsxAlternateContent struct { + XMLNSMC string `xml:"xmlns:mc,attr,omitempty"` + Content string `xml:",innerxml"` +} + +// xlsxChoice element shall be an element in the Markup Compatibility namespace +// with local name "Choice". Parent elements of Choice elements shall be +// AlternateContent elements. +type xlsxChoice struct { + XMLName xml.Name `xml:"mc:Choice"` + XMLNSSle15 string `xml:"xmlns:sle15,attr,omitempty"` + Requires string `xml:"Requires,attr,omitempty"` + Content string `xml:",innerxml"` +} + +// xlsxFallback element shall be an element in the Markup Compatibility +// namespace with local name "Fallback". Parent elements of Fallback elements +// shall be AlternateContent elements. +type xlsxFallback struct { + XMLName xml.Name `xml:"mc:Fallback"` + Content string `xml:",innerxml"` +} + +// xlsxInnerXML holds parts of XML content currently not unmarshal. +type xlsxInnerXML struct { + Content string `xml:",innerxml"` +} + +// decodeExtLst defines the structure used to parse the extLst element +// of the future feature data storage area. +type decodeExtLst struct { + XMLName xml.Name `xml:"extLst"` + Ext []*xlsxExt `xml:"ext"` +} + +// decodeExt defines the structure used to parse the ext element. +type decodeExt struct { + URI string `xml:"uri,attr,omitempty"` + Content string `xml:",innerxml"` +} + // xlsxDefinedNames directly maps the definedNames element. This element defines // the collection of defined names for this workbook. Defined names are // descriptive names to represent cells, ranges of cells, formulas, or constant diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 76ffe52442..07085bbffb 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -699,33 +699,6 @@ type xlsxLegacyDrawingHF struct { RID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr,omitempty"` } -// xlsxAlternateContent is a container for a sequence of multiple -// representations of a given piece of content. The program reading the file -// should only process one of these, and the one chosen should be based on -// which conditions match. -type xlsxAlternateContent struct { - XMLNSMC string `xml:"xmlns:mc,attr,omitempty"` - Content string `xml:",innerxml"` -} - -// xlsxInnerXML holds parts of XML content currently not unmarshal. -type xlsxInnerXML struct { - Content string `xml:",innerxml"` -} - -// xlsxWorksheetExt directly maps the ext element in the worksheet. -type xlsxWorksheetExt struct { - XMLName xml.Name `xml:"ext"` - URI string `xml:"uri,attr"` - Content string `xml:",innerxml"` -} - -// decodeWorksheetExt directly maps the ext element. -type decodeWorksheetExt struct { - XMLName xml.Name `xml:"extLst"` - Ext []*xlsxWorksheetExt `xml:"ext"` -} - // decodeX14SparklineGroups directly maps the sparklineGroups element. type decodeX14SparklineGroups struct { XMLName xml.Name `xml:"sparklineGroups"` @@ -733,8 +706,7 @@ type decodeX14SparklineGroups struct { Content string `xml:",innerxml"` } -// decodeX14ConditionalFormattingExt directly maps the ext -// element. +// decodeX14ConditionalFormattingExt directly maps the ext element. type decodeX14ConditionalFormattingExt struct { XMLName xml.Name `xml:"ext"` ID string `xml:"id"` From 744236b4b840d71cff471e45f8641d0a682d0292 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 17 Sep 2023 18:30:58 +0800 Subject: [PATCH 792/957] This closes #1661, fix incorrect time number format result --- numfmt.go | 36 ++++++++++++++++++++++++------------ numfmt_test.go | 4 ++++ 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/numfmt.go b/numfmt.go index b0fd7c2e01..83f40f1eda 100644 --- a/numfmt.go +++ b/numfmt.go @@ -4939,6 +4939,9 @@ func (nf *numberFormat) numberHandler() string { // positive numeric. func (nf *numberFormat) dateTimeHandler() string { nf.t, nf.hours, nf.seconds = timeFromExcelTime(nf.number, nf.date1904), false, false + if !nf.useMillisecond { + nf.t = nf.t.Add(time.Duration(math.Round(float64(nf.t.Nanosecond())/1e9)) * time.Second) + } for i, token := range nf.section[nf.sectionIdx].Items { if token.TType == nfp.TokenTypeCurrencyLanguage { if changeNumFmtCode, err := nf.currencyLanguageHandler(token); err != nil || changeNumFmtCode { @@ -6712,11 +6715,11 @@ func (nf *numberFormat) dateTimesHandler(i int, token nfp.Token) { } if strings.Contains(strings.ToUpper(token.TValue), "M") { l := len(token.TValue) - if l == 1 && !nf.hours && !nf.secondsNext(i) { + if l == 1 && nf.isMonthToken(i) { nf.result += strconv.Itoa(int(nf.t.Month())) return } - if l == 2 && !nf.hours && !nf.secondsNext(i) { + if l == 2 && nf.isMonthToken(i) { nf.result += fmt.Sprintf("%02d", int(nf.t.Month())) return } @@ -6837,8 +6840,7 @@ func (nf *numberFormat) daysHandler(token nfp.Token) { // hoursHandler will be handling hours in the date and times types tokens for a // number format expression. func (nf *numberFormat) hoursHandler(i int, token nfp.Token) { - nf.hours = strings.Contains(strings.ToUpper(token.TValue), "H") - if nf.hours { + if nf.hours = strings.Contains(strings.ToUpper(token.TValue), "H"); nf.hours { h := nf.t.Hour() ap, ok := nf.apNext(i) if ok { @@ -6890,9 +6892,6 @@ func (nf *numberFormat) secondsHandler(token nfp.Token) { 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 @@ -6947,16 +6946,29 @@ func (nf *numberFormat) apNext(i int) ([]string, bool) { return nil, false } -// secondsNext detects if a token of type seconds exists after a given tokens -// list. -func (nf *numberFormat) secondsNext(i int) bool { +// isMonthToken detects if the given token represents minutes, if no hours and +// seconds tokens before the given token or not seconds after the given token, +// the current token is a minutes token. +func (nf *numberFormat) isMonthToken(i int) bool { tokens := nf.section[nf.sectionIdx].Items + var timePrevious, secondsNext bool + for idx := i - 1; idx >= 0; idx-- { + if tokens[idx].TType == nfp.TokenTypeDateTimes { + timePrevious = strings.ContainsAny(strings.ToUpper(tokens[idx].TValue), "HS") + break + } + if tokens[idx].TType == nfp.TokenTypeElapsedDateTimes { + timePrevious = true + break + } + } for idx := i + 1; idx < len(tokens); idx++ { if tokens[idx].TType == nfp.TokenTypeDateTimes { - return strings.Contains(strings.ToUpper(tokens[idx].TValue), "S") + secondsNext = strings.Contains(strings.ToUpper(tokens[idx].TValue), "S") + break } } - return false + return !(timePrevious || secondsNext) } // negativeHandler will be handling negative selection for a number format diff --git a/numfmt_test.go b/numfmt_test.go index 143357c87a..7b8fa91cac 100644 --- a/numfmt_test.go +++ b/numfmt_test.go @@ -3518,6 +3518,10 @@ func TestNumFmt(t *testing.T) { {"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"}, + {"0.3270833333", "[h]:mm", "7:51"}, + {"0.5347222222", "[h]:mm", "12:50"}, + {"0.5833333333", "[h]:mm", "14:00"}, + {"0.5833333333", "hh", "14"}, {"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"}, From 9c079e5eec19a94553c6a1e5389be375e38a9446 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 21 Sep 2023 00:06:31 +0800 Subject: [PATCH 793/957] This fix #1665, supports getting formula string cell value - Improve compatibility for absolute path drawing part - Fix incorrect table ID generated in the workbook which contains single table cells - Fix missing relationship parts in the content types in some cases - Upgrade number format parser to fix missing literal tokens in some cases - Update built-in zh-cn and zh-tw language number format - Ref #65, init new formula function: TEXT - Remove duplicate style-related variables - Update the unit tests --- calc.go | 23 ++++++ calc_test.go | 13 +++ cell.go | 2 + drawing.go | 13 +-- go.mod | 8 +- go.sum | 19 ++--- numfmt.go | 20 ++--- numfmt_test.go | 1 + slicer.go | 2 +- styles.go | 209 ++++++++++++++++++------------------------------- table.go | 13 +++ table_test.go | 9 +++ xmlDrawing.go | 1 + xmlTable.go | 29 +++++++ 14 files changed, 199 insertions(+), 163 deletions(-) diff --git a/calc.go b/calc.go index 4ae2b3e8bf..bd8776ecf4 100644 --- a/calc.go +++ b/calc.go @@ -757,6 +757,7 @@ type formulaFuncs struct { // TBILLPRICE // TBILLYIELD // TDIST +// TEXT // TEXTJOIN // TIME // TIMEVALUE @@ -14035,6 +14036,28 @@ func (fn *formulaFuncs) SUBSTITUTE(argsList *list.List) formulaArg { return newStringFormulaArg(pre + targetText.Value() + post) } +// TEXT function converts a supplied numeric value into text, in a +// user-specified format. The syntax of the function is: +// +// TEXT(value,format_text) +func (fn *formulaFuncs) TEXT(argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, "TEXT requires 2 arguments") + } + value, fmtText := argsList.Front().Value.(formulaArg), argsList.Back().Value.(formulaArg) + if value.Type == ArgError { + return value + } + if fmtText.Type == ArgError { + return fmtText + } + cellType := CellTypeNumber + if num := value.ToNumber(); num.Type != ArgNumber { + cellType = CellTypeSharedString + } + return newStringFormulaArg(format(value.Value(), fmtText.Value(), false, cellType, nil)) +} + // TEXTJOIN function joins together a series of supplied text strings into one // combined text string. The user can specify a delimiter to add between the // individual text items, if required. The syntax of the function is: diff --git a/calc_test.go b/calc_test.go index 6042715206..0b08db1ac7 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1834,6 +1834,15 @@ func TestCalcCellValue(t *testing.T) { "=SUBSTITUTE(\"abab\",\"x\",\"X\",2)": "abab", "=SUBSTITUTE(\"John is 5 years old\",\"John\",\"Jack\")": "Jack is 5 years old", "=SUBSTITUTE(\"John is 5 years old\",\"5\",\"6\")": "John is 6 years old", + // TEXT + "=TEXT(\"07/07/2015\",\"mm/dd/yyyy\")": "07/07/2015", + "=TEXT(42192,\"mm/dd/yyyy\")": "07/07/2015", + "=TEXT(42192,\"mmm dd yyyy\")": "Jul 07 2015", + "=TEXT(0.75,\"hh:mm\")": "18:00", + "=TEXT(36.363636,\"0.00\")": "36.36", + "=TEXT(567.9,\"$#,##0.00\")": "$567.90", + "=TEXT(-5,\"+ $#,##0.00;- $#,##0.00;$0.00\")": "- $5.00", + "=TEXT(5,\"+ $#,##0.00;- $#,##0.00;$0.00\")": "+ $5.00", // TEXTJOIN "=TEXTJOIN(\"-\",TRUE,1,2,3,4)": "1-2-3-4", "=TEXTJOIN(A4,TRUE,A1:B2)": "1040205", @@ -3866,6 +3875,10 @@ func TestCalcCellValue(t *testing.T) { "=SUBSTITUTE()": {"#VALUE!", "SUBSTITUTE requires 3 or 4 arguments"}, "=SUBSTITUTE(\"\",\"\",\"\",\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, "=SUBSTITUTE(\"\",\"\",\"\",0)": {"#VALUE!", "instance_num should be > 0"}, + // TEXT + "=TEXT()": {"#VALUE!", "TEXT requires 2 arguments"}, + "=TEXT(NA(),\"\")": {"#N/A", "#N/A"}, + "=TEXT(0,NA())": {"#N/A", "#N/A"}, // TEXTJOIN "=TEXTJOIN()": {"#VALUE!", "TEXTJOIN requires at least 3 arguments"}, "=TEXTJOIN(\"\",\"\",1)": {"#VALUE!", "#VALUE!"}, diff --git a/cell.go b/cell.go index 36265d9fe0..53bcc218b0 100644 --- a/cell.go +++ b/cell.go @@ -590,6 +590,8 @@ func (c *xlsxC) getValueFrom(f *File, d *xlsxSST, raw bool) (string, error) { } } return f.formattedValue(c, raw, CellTypeSharedString) + case "str": + return c.V, nil case "inlineStr": if c.IS != nil { return f.formattedValue(&xlsxC{S: c.S, V: c.IS.String()}, raw, CellTypeInlineString) diff --git a/drawing.go b/drawing.go index afe9d4d103..045506c860 100644 --- a/drawing.go +++ b/drawing.go @@ -25,8 +25,9 @@ import ( func (f *File) prepareDrawing(ws *xlsxWorksheet, drawingID int, sheet, drawingXML string) (int, string) { sheetRelationshipsDrawingXML := "../drawings/drawing" + strconv.Itoa(drawingID) + ".xml" if ws.Drawing != nil { - // The worksheet already has a picture or chart relationships, use the relationships drawing ../drawings/drawing%d.xml. - sheetRelationshipsDrawingXML = f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID) + // The worksheet already has a picture or chart relationships, use the + // relationships drawing ../drawings/drawing%d.xml or /xl/drawings/drawing%d.xml. + sheetRelationshipsDrawingXML = strings.ReplaceAll(f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID), "/xl/drawings/", "../drawings/") drawingID, _ = strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(sheetRelationshipsDrawingXML, "../drawings/drawing"), ".xml")) drawingXML = strings.ReplaceAll(sheetRelationshipsDrawingXML, "..", "xl") } else { @@ -1247,9 +1248,11 @@ func (f *File) drawingParser(path string) (*xlsxWsDr, int, error) { ) _, ok = f.Drawings.Load(path) if !ok { - content := xlsxWsDr{} - content.A = NameSpaceDrawingML.Value - content.Xdr = NameSpaceDrawingMLSpreadSheet.Value + content := xlsxWsDr{ + NS: NameSpaceDrawingMLSpreadSheet.Value, + Xdr: NameSpaceDrawingMLSpreadSheet.Value, + A: NameSpaceDrawingML.Value, + } if _, ok = f.Pkg.Load(path); ok { // Append Model decodeWsDr := decodeWsDr{} if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(path)))). diff --git a/go.mod b/go.mod index 733011fe2c..1c30e606e5 100644 --- a/go.mod +++ b/go.mod @@ -8,9 +8,9 @@ require ( github.com/richardlehane/msoleps v1.0.3 // indirect github.com/stretchr/testify v1.8.0 github.com/xuri/efp v0.0.0-20230802181842-ad255f2331ca - github.com/xuri/nfp v0.0.0-20230819163627-dc951e3ffe1a - golang.org/x/crypto v0.12.0 + github.com/xuri/nfp v0.0.0-20230918160701-e5a3f5b24785 + golang.org/x/crypto v0.13.0 golang.org/x/image v0.11.0 - golang.org/x/net v0.14.0 - golang.org/x/text v0.12.0 + golang.org/x/net v0.15.0 + golang.org/x/text v0.13.0 ) diff --git a/go.sum b/go.sum index d82097a3b1..3b314f28e6 100644 --- a/go.sum +++ b/go.sum @@ -17,13 +17,13 @@ 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-20230802181842-ad255f2331ca h1:uvPMDVyP7PXMMioYdyPH+0O+Ta/UO1WFfNYMO3Wz0eg= github.com/xuri/efp v0.0.0-20230802181842-ad255f2331ca/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= -github.com/xuri/nfp v0.0.0-20230819163627-dc951e3ffe1a h1:Mw2VNrNNNjDtw68VsEj2+st+oCSn4Uz7vZw6TbhcV1o= -github.com/xuri/nfp v0.0.0-20230819163627-dc951e3ffe1a/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= +github.com/xuri/nfp v0.0.0-20230918160701-e5a3f5b24785 h1:FG9hcK7lhf3w/Y2NRUKy/mopsH0Oy6P1rib1KWXAie0= +github.com/xuri/nfp v0.0.0-20230918160701-e5a3f5b24785/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= -golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= -golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/image v0.11.0 h1:ds2RoQvBvYTiJkwpSFDwCcDFNX7DqjL2WsUgTNk0Ooo= golang.org/x/image v0.11.0/go.mod h1:bglhjqbqVuEb9e9+eNR45Jfu7D+T4Qan+NhQk8Ck2P8= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -33,8 +33,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v 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.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= -golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= 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= @@ -45,19 +45,20 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc 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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.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.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= 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.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 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/numfmt.go b/numfmt.go index 83f40f1eda..8ef69e0e72 100644 --- a/numfmt.go +++ b/numfmt.go @@ -108,16 +108,16 @@ var ( 31: "yyyy\"年\"m\"月\"d\"日\"", 32: "hh\"時\"mm\"分\"", 33: "hh\"時\"mm\"分\"ss\"秒\"", - 34: "上午/下午 hh\"時\"mm\"分\"", - 35: "上午/下午 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\"秒\"", + 52: "上午/下午hh\"時\"mm\"分\"", + 53: "上午/下午hh\"時\"mm\"分\"ss\"秒\"", 54: "[$-404]e\"年\"m\"月\"d\"日\"", - 55: "上午/下午 hh\"時\"mm\"分\"", - 56: "上午/下午 hh\"時\"mm\"分\"ss\"秒\"", + 55: "上午/下午hh\"時\"mm\"分\"", + 56: "上午/下午hh\"時\"mm\"分\"ss\"秒\"", 57: "[$-404]e/m/d", 58: "[$-404]e\"年\"m\"月\"d\"日\"", }, @@ -129,16 +129,16 @@ var ( 31: "yyyy\"年\"m\"月\"d\"日\"", 32: "h\"时\"mm\"分\"", 33: "h\"时\"mm\"分\"ss\"秒\"", - 34: "上午/下午 h\"时\"mm\"分\"", - 35: "上午/下午 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\"秒\"", + 55: "上午/下午h\"时\"mm\"分\"", + 56: "上午/下午h\"时\"mm\"分\"ss\"秒\"", 57: "yyyy\"年\"m\"月\"", 58: "m\"月\"d\"日\"", }, diff --git a/numfmt_test.go b/numfmt_test.go index 7b8fa91cac..2433364bcb 100644 --- a/numfmt_test.go +++ b/numfmt_test.go @@ -72,6 +72,7 @@ func TestNumFmt(t *testing.T) { {"43528", "[$-111]MM/DD/YYYY", "43528"}, {"43528", "[$US-409]MM/DD/YYYY", "US03/04/2019"}, {"43543.586539351854", "AM/PM h h:mm", "PM 14 2:04"}, + {"45186", "DD.MM.YYYY", "17.09.2023"}, {"text", "AM/PM h h:mm", "text"}, {"43466.189571759256", "[$-404]aaa;@", "週二"}, {"43466.189571759256", "[$-404]aaaa;@", "星期二"}, diff --git a/slicer.go b/slicer.go index 435c0f96f2..c62b33768a 100644 --- a/slicer.go +++ b/slicer.go @@ -36,7 +36,7 @@ import ( // Caption specifies the caption of the slicer, this setting is optional. // // Macro used for set macro for the slicer, the workbook extension should be -// XLSM or XLTM +// XLSM or XLTM. // // Width specifies the width of the slicer, this setting is optional. // diff --git a/styles.go b/styles.go index 7f0601ea14..528529548a 100644 --- a/styles.go +++ b/styles.go @@ -1096,6 +1096,73 @@ var ( {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}, } + // getXfIDFuncs provides a function to get xfID by given style. + getXfIDFuncs = map[string]func(int, xlsxXf, *Style) bool{ + "numFmt": func(numFmtID int, xf xlsxXf, style *Style) bool { + if style.CustomNumFmt == nil && numFmtID == -1 { + return xf.NumFmtID != nil && *xf.NumFmtID == 0 + } + if style.NegRed || (style.DecimalPlaces != nil && *style.DecimalPlaces != 2) { + return false + } + return xf.NumFmtID != nil && *xf.NumFmtID == numFmtID + }, + "font": func(fontID int, xf xlsxXf, style *Style) bool { + if style.Font == nil { + return (xf.FontID == nil || *xf.FontID == 0) && (xf.ApplyFont == nil || !*xf.ApplyFont) + } + return xf.FontID != nil && *xf.FontID == fontID && xf.ApplyFont != nil && *xf.ApplyFont + }, + "fill": func(fillID int, xf xlsxXf, style *Style) bool { + if style.Fill.Type == "" { + return (xf.FillID == nil || *xf.FillID == 0) && (xf.ApplyFill == nil || !*xf.ApplyFill) + } + return xf.FillID != nil && *xf.FillID == fillID && xf.ApplyFill != nil && *xf.ApplyFill + }, + "border": func(borderID int, xf xlsxXf, style *Style) bool { + if len(style.Border) == 0 { + return (xf.BorderID == nil || *xf.BorderID == 0) && (xf.ApplyBorder == nil || !*xf.ApplyBorder) + } + return xf.BorderID != nil && *xf.BorderID == borderID && xf.ApplyBorder != nil && *xf.ApplyBorder + }, + "alignment": func(ID int, xf xlsxXf, style *Style) bool { + if style.Alignment == nil { + return xf.ApplyAlignment == nil || !*xf.ApplyAlignment + } + return reflect.DeepEqual(xf.Alignment, newAlignment(style)) + }, + "protection": func(ID int, xf xlsxXf, style *Style) bool { + if style.Protection == nil { + return xf.ApplyProtection == nil || !*xf.ApplyProtection + } + return reflect.DeepEqual(xf.Protection, newProtection(style)) && xf.ApplyProtection != nil && *xf.ApplyProtection + }, + } + // drawContFmtFunc defines functions to create conditional formats. + drawContFmtFunc = map[string]func(p int, ct, GUID string, fmtCond *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule){ + "cellIs": drawCondFmtCellIs, + "top10": drawCondFmtTop10, + "aboveAverage": drawCondFmtAboveAverage, + "duplicateValues": drawCondFmtDuplicateUniqueValues, + "uniqueValues": drawCondFmtDuplicateUniqueValues, + "2_color_scale": drawCondFmtColorScale, + "3_color_scale": drawCondFmtColorScale, + "dataBar": drawCondFmtDataBar, + "expression": drawCondFmtExp, + "iconSet": drawCondFmtIconSet, + } + // extractContFmtFunc defines functions to get conditional formats. + extractContFmtFunc = map[string]func(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions{ + "cellIs": extractCondFmtCellIs, + "top10": extractCondFmtTop10, + "aboveAverage": extractCondFmtAboveAverage, + "duplicateValues": extractCondFmtDuplicateUniqueValues, + "uniqueValues": extractCondFmtDuplicateUniqueValues, + "colorScale": extractCondFmtColorScale, + "dataBar": extractCondFmtDataBar, + "expression": extractCondFmtExp, + "iconSet": extractCondFmtIconSet, + } ) // getThemeColor provides a function to convert theme color or index color to @@ -1329,49 +1396,6 @@ func (f *File) GetStyle(idx int) (*Style, error) { return style, nil } -// getXfIDFuncs provides a function to get xfID by given style. -var getXfIDFuncs = map[string]func(int, xlsxXf, *Style) bool{ - "numFmt": func(numFmtID int, xf xlsxXf, style *Style) bool { - if style.CustomNumFmt == nil && numFmtID == -1 { - return xf.NumFmtID != nil && *xf.NumFmtID == 0 - } - if style.NegRed || (style.DecimalPlaces != nil && *style.DecimalPlaces != 2) { - return false - } - return xf.NumFmtID != nil && *xf.NumFmtID == numFmtID - }, - "font": func(fontID int, xf xlsxXf, style *Style) bool { - if style.Font == nil { - return (xf.FontID == nil || *xf.FontID == 0) && (xf.ApplyFont == nil || !*xf.ApplyFont) - } - return xf.FontID != nil && *xf.FontID == fontID && xf.ApplyFont != nil && *xf.ApplyFont - }, - "fill": func(fillID int, xf xlsxXf, style *Style) bool { - if style.Fill.Type == "" { - return (xf.FillID == nil || *xf.FillID == 0) && (xf.ApplyFill == nil || !*xf.ApplyFill) - } - return xf.FillID != nil && *xf.FillID == fillID && xf.ApplyFill != nil && *xf.ApplyFill - }, - "border": func(borderID int, xf xlsxXf, style *Style) bool { - if len(style.Border) == 0 { - return (xf.BorderID == nil || *xf.BorderID == 0) && (xf.ApplyBorder == nil || !*xf.ApplyBorder) - } - return xf.BorderID != nil && *xf.BorderID == borderID && xf.ApplyBorder != nil && *xf.ApplyBorder - }, - "alignment": func(ID int, xf xlsxXf, style *Style) bool { - if style.Alignment == nil { - return xf.ApplyAlignment == nil || !*xf.ApplyAlignment - } - return reflect.DeepEqual(xf.Alignment, newAlignment(style)) - }, - "protection": func(ID int, xf xlsxXf, style *Style) bool { - if style.Protection == nil { - return xf.ApplyProtection == nil || !*xf.ApplyProtection - } - return reflect.DeepEqual(xf.Protection, newProtection(style)) && xf.ApplyProtection != nil && *xf.ApplyProtection - }, -} - // 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) (int, error) { @@ -1740,54 +1764,13 @@ func getFillID(styleSheet *xlsxStyleSheet, style *Style) (fillID int) { // newFills provides a function to add fill elements in the styles.xml by // given cell format settings. func newFills(style *Style, fg bool) *xlsxFill { - patterns := []string{ - "none", - "solid", - "mediumGray", - "darkGray", - "lightGray", - "darkHorizontal", - "darkVertical", - "darkDown", - "darkUp", - "darkGrid", - "darkTrellis", - "lightHorizontal", - "lightVertical", - "lightDown", - "lightUp", - "lightGrid", - "lightTrellis", - "gray125", - "gray0625", - } - 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 || style.Fill.Shading < 0 || style.Fill.Shading > 16 { break } - gradient := variants[style.Fill.Shading] + gradient := styleFillVariants[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 { @@ -1802,7 +1785,7 @@ func newFills(style *Style, fg bool) *xlsxFill { break } var pattern xlsxPatternFill - pattern.PatternType = patterns[style.Fill.Pattern] + pattern.PatternType = styleFillPatterns[style.Fill.Pattern] if fg { if pattern.FgColor == nil { pattern.FgColor = new(xlsxColor) @@ -1871,23 +1854,6 @@ func getBorderID(styleSheet *xlsxStyleSheet, style *Style) (borderID int) { // newBorders provides a function to add border elements in the styles.xml by // given borders format settings. func newBorders(style *Style) *xlsxBorder { - styles := []string{ - "none", - "thin", - "medium", - "dashed", - "dotted", - "thick", - "double", - "hair", - "mediumDashed", - "dashDot", - "mediumDashDot", - "dashDotDot", - "mediumDashDotDot", - "slantDashDot", - } - var border xlsxBorder for _, v := range style.Border { if 0 <= v.Style && v.Style < 14 { @@ -1895,23 +1861,23 @@ func newBorders(style *Style) *xlsxBorder { color.RGB = getPaletteColor(v.Color) switch v.Type { case "left": - border.Left.Style = styles[v.Style] + border.Left.Style = styleBorders[v.Style] border.Left.Color = &color case "right": - border.Right.Style = styles[v.Style] + border.Right.Style = styleBorders[v.Style] border.Right.Color = &color case "top": - border.Top.Style = styles[v.Style] + border.Top.Style = styleBorders[v.Style] border.Top.Color = &color case "bottom": - border.Bottom.Style = styles[v.Style] + border.Bottom.Style = styleBorders[v.Style] border.Bottom.Color = &color case "diagonalUp": - border.Diagonal.Style = styles[v.Style] + border.Diagonal.Style = styleBorders[v.Style] border.Diagonal.Color = &color border.DiagonalUp = true case "diagonalDown": - border.Diagonal.Style = styles[v.Style] + border.Diagonal.Style = styleBorders[v.Style] border.Diagonal.Color = &color border.DiagonalDown = true } @@ -2551,19 +2517,6 @@ func (f *File) SetCellStyle(sheet, hCell, vCell string, styleID int) error { // 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, GUID string, fmtCond *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule){ - "cellIs": drawCondFmtCellIs, - "top10": drawCondFmtTop10, - "aboveAverage": drawCondFmtAboveAverage, - "duplicateValues": drawCondFmtDuplicateUniqueValues, - "uniqueValues": drawCondFmtDuplicateUniqueValues, - "2_color_scale": drawCondFmtColorScale, - "3_color_scale": drawCondFmtColorScale, - "dataBar": drawCondFmtDataBar, - "expression": drawCondFmtExp, - "iconSet": drawCondFmtIconSet, - } - ws, err := f.workSheetReader(sheet) if err != nil { return err @@ -2833,18 +2786,6 @@ func extractCondFmtIconSet(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatO // 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, extLst *xlsxExtLst) ConditionalFormatOptions{ - "cellIs": extractCondFmtCellIs, - "top10": extractCondFmtTop10, - "aboveAverage": extractCondFmtAboveAverage, - "duplicateValues": extractCondFmtDuplicateUniqueValues, - "uniqueValues": extractCondFmtDuplicateUniqueValues, - "colorScale": extractCondFmtColorScale, - "dataBar": extractCondFmtDataBar, - "expression": extractCondFmtExp, - "iconSet": extractCondFmtIconSet, - } - conditionalFormats := make(map[string][]ConditionalFormatOptions) ws, err := f.workSheetReader(sheet) if err != nil { diff --git a/table.go b/table.go index f365a63e59..32667cd7ca 100644 --- a/table.go +++ b/table.go @@ -216,6 +216,19 @@ func (f *File) DeleteTable(name string) error { func (f *File) countTables() int { count := 0 f.Pkg.Range(func(k, v interface{}) bool { + if strings.Contains(k.(string), "xl/tables/tableSingleCells") { + var cells xlsxSingleXmlCells + if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(v.([]byte)))). + Decode(&cells); err != nil && err != io.EOF { + count++ + return true + } + for _, cell := range cells.SingleXmlCell { + if count < cell.ID { + count = cell.ID + } + } + } if strings.Contains(k.(string), "xl/tables/table") { var t xlsxTable if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(v.([]byte)))). diff --git a/table_test.go b/table_test.go index d81a52bee7..994cd72fb1 100644 --- a/table_test.go +++ b/table_test.go @@ -76,6 +76,15 @@ func TestAddTable(t *testing.T) { f = NewFile() f.Pkg.Store("xl/tables/table1.xml", MacintoshCyrillicCharset) assert.NoError(t, f.AddTable("Sheet1", &Table{Range: "A1:B2"})) + assert.NoError(t, f.Close()) + f = NewFile() + // Test add table with workbook with single cells parts + f.Pkg.Store("xl/tables/tableSingleCells1.xml", []byte("")) + assert.NoError(t, f.AddTable("Sheet1", &Table{Range: "A1:B2"})) + // Test add table with workbook with unsupported charset single cells parts + f.Pkg.Store("xl/tables/tableSingleCells1.xml", MacintoshCyrillicCharset) + assert.NoError(t, f.AddTable("Sheet1", &Table{Range: "A1:B2"})) + assert.NoError(t, f.Close()) } func TestGetTables(t *testing.T) { diff --git a/xmlDrawing.go b/xmlDrawing.go index 13ed87f7f3..e5dc384ae9 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -242,6 +242,7 @@ type xlsxPoint2D struct { type xlsxWsDr struct { mu sync.Mutex XMLName xml.Name `xml:"xdr:wsDr"` + NS string `xml:"xmlns,attr,omitempty"` A string `xml:"xmlns:a,attr,omitempty"` Xdr string `xml:"xmlns:xdr,attr,omitempty"` R string `xml:"xmlns:r,attr,omitempty"` diff --git a/xmlTable.go b/xmlTable.go index 16fd284cab..7c509ed4e6 100644 --- a/xmlTable.go +++ b/xmlTable.go @@ -196,6 +196,35 @@ type xlsxTableStyleInfo struct { ShowColumnStripes bool `xml:"showColumnStripes,attr"` } +// xlsxSingleXmlCells is a single cell table is generated from an XML mapping. +// These really just look like regular cells to the spreadsheet user, but shall +// be implemented as Tables "under the covers." +type xlsxSingleXmlCells struct { + XMLName xml.Name `xml:"singleXmlCells"` + SingleXmlCell []xlsxSingleXmlCell `xml:"singleXmlCell"` +} + +// xlsxSingleXmlCell is a element represents the table properties for a single +// cell XML table. +type xlsxSingleXmlCell struct { + XMLName xml.Name `xml:"singleXmlCell"` + ID int `xml:"id,attr"` + R string `xml:"r,attr"` + ConnectionID int `xml:"connectionId,attr"` + XMLCellPr xlsxXmlCellPr `xml:"xmlCellPr"` + ExtLst *xlsxInnerXML `xml:"extLst"` +} + +// xlsxXmlCellPr is a element stores the XML properties for the cell of a single +// cell xml table. +type xlsxXmlCellPr struct { + XMLName xml.Name `xml:"xmlCellPr"` + ID int `xml:"id,attr"` + UniqueName string `xml:"uniqueName,attr,omitempty"` + XMLPr *xlsxInnerXML `xml:"xmlPr"` + ExtLst *xlsxInnerXML `xml:"extLst"` +} + // Table directly maps the format settings of the table. type Table struct { tID int From c62d23e0a113f192ce39469a80bb66d67dd7cbec Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 27 Sep 2023 00:05:59 +0800 Subject: [PATCH 794/957] The `AddSlicer` function now support create pivot table slicer --- pivotTable.go | 56 +++++--- pivotTable_test.go | 18 ++- slicer.go | 334 ++++++++++++++++++++++++++++++++++----------- slicer_test.go | 259 +++++++++++++++++++++++++++-------- sparkline.go | 4 +- styles.go | 4 +- templates.go | 25 +++- xmlPivotCache.go | 54 +++++--- xmlSlicers.go | 43 +++++- xmlWorkbook.go | 1 + 10 files changed, 610 insertions(+), 188 deletions(-) diff --git a/pivotTable.go b/pivotTable.go index 2775af7f2b..720f2b193d 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -33,6 +33,8 @@ import ( // PivotStyleMedium1 - PivotStyleMedium28 // PivotStyleDark1 - PivotStyleDark28 type PivotTableOptions struct { + pivotTableXML string + pivotCacheXML string pivotTableSheetName string DataRange string PivotTableRange string @@ -286,7 +288,7 @@ func (f *File) addPivotCache(pivotCacheXML string, opts *PivotTableOptions) erro SaveData: false, RefreshOnLoad: true, CreatedVersion: pivotTableVersion, - RefreshedVersion: pivotTableVersion, + RefreshedVersion: pivotTableRefreshedVersion, MinRefreshableVersion: pivotTableVersion, CacheSource: &xlsxCacheSource{ Type: "worksheet", @@ -301,23 +303,9 @@ func (f *File) addPivotCache(pivotCacheXML string, opts *PivotTableOptions) erro pc.CacheSource.WorksheetSource = &xlsxWorksheetSource{Name: opts.DataRange} } for _, name := range order { - rowOptions, rowOk := f.getPivotTableFieldOptions(name, opts.Rows) - columnOptions, colOk := f.getPivotTableFieldOptions(name, opts.Columns) - sharedItems := xlsxSharedItems{ - Count: 0, - } - s := xlsxString{} - if (rowOk && !rowOptions.DefaultSubtotal) || (colOk && !columnOptions.DefaultSubtotal) { - s = xlsxString{ - V: "", - } - sharedItems.Count++ - sharedItems.S = &s - } - pc.CacheFields.CacheField = append(pc.CacheFields.CacheField, &xlsxCacheField{ Name: name, - SharedItems: &sharedItems, + SharedItems: &xlsxSharedItems{ContainsBlank: true, M: []xlsxMissing{{}}}, }) } pc.CacheFields.Count = len(pc.CacheFields.CacheField) @@ -349,13 +337,13 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op CacheID: cacheID, RowGrandTotals: &opts.RowGrandTotals, ColGrandTotals: &opts.ColGrandTotals, - UpdatedVersion: pivotTableVersion, + UpdatedVersion: pivotTableRefreshedVersion, MinRefreshableVersion: pivotTableVersion, ShowDrill: &opts.ShowDrill, UseAutoFormatting: &opts.UseAutoFormatting, PageOverThenDown: &opts.PageOverThenDown, MergeItem: &opts.MergeItem, - CreatedVersion: pivotTableVersion, + CreatedVersion: 3, CompactData: &opts.CompactData, ShowError: &opts.ShowError, DataCaption: "Values", @@ -788,6 +776,8 @@ func (f *File) getPivotTable(sheet, pivotTableXML, pivotCacheRels string) (Pivot } dataRange := fmt.Sprintf("%s!%s", pc.CacheSource.WorksheetSource.Sheet, pc.CacheSource.WorksheetSource.Ref) opts = PivotTableOptions{ + pivotTableXML: pivotTableXML, + pivotCacheXML: pivotCacheXML, pivotTableSheetName: sheet, DataRange: dataRange, PivotTableRange: fmt.Sprintf("%s!%s", sheet, pt.Location.Ref), @@ -886,3 +876,33 @@ func extractPivotTableField(data string, fld *xlsxPivotField) PivotTableField { } return pivotTableField } + +// genPivotCacheDefinitionID generates a unique pivot table cache definition ID. +func (f *File) genPivotCacheDefinitionID() int { + var ( + ID int + decodeExtLst = new(decodeExtLst) + decodeX14PivotCacheDefinition = new(decodeX14PivotCacheDefinition) + ) + f.Pkg.Range(func(k, v interface{}) bool { + if strings.Contains(k.(string), "xl/pivotCache/pivotCacheDefinition") { + pc, err := f.pivotCacheReader(k.(string)) + if err != nil { + return true + } + if pc.ExtLst != nil { + _ = f.xmlNewDecoder(strings.NewReader("" + pc.ExtLst.Ext + "")).Decode(decodeExtLst) + for _, ext := range decodeExtLst.Ext { + if ext.URI == ExtURIPivotCacheDefinition { + _ = f.xmlNewDecoder(strings.NewReader(ext.Content)).Decode(decodeX14PivotCacheDefinition) + if ID < decodeX14PivotCacheDefinition.PivotCacheID { + ID = decodeX14PivotCacheDefinition.PivotCacheID + } + } + } + } + } + return true + }) + return ID + 1 +} diff --git a/pivotTable_test.go b/pivotTable_test.go index f6d0707524..bba445dc77 100644 --- a/pivotTable_test.go +++ b/pivotTable_test.go @@ -26,6 +26,8 @@ func TestPivotTable(t *testing.T) { assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("E%d", row), region[rand.Intn(4)])) } expected := &PivotTableOptions{ + pivotTableXML: "xl/pivotTables/pivotTable1.xml", + pivotCacheXML: "xl/pivotCache/pivotCacheDefinition1.xml", DataRange: "Sheet1!A1:E31", PivotTableRange: "Sheet1!G2:M34", Name: "PivotTable1", @@ -374,5 +376,19 @@ func TestGetPivotFieldsOrder(t *testing.T) { func TestGetPivotTableFieldName(t *testing.T) { f := NewFile() - f.getPivotTableFieldName("-", []PivotTableField{}) + assert.Empty(t, f.getPivotTableFieldName("-", []PivotTableField{})) +} + +func TestGetPivotTableFieldOptions(t *testing.T) { + f := NewFile() + _, ok := f.getPivotTableFieldOptions("-", []PivotTableField{}) + assert.False(t, ok) +} + +func TestGenPivotCacheDefinitionID(t *testing.T) { + f := NewFile() + // Test generate pivot table cache definition ID with unsupported charset + f.Pkg.Store("xl/pivotCache/pivotCacheDefinition1.xml", MacintoshCyrillicCharset) + assert.Equal(t, 1, f.genPivotCacheDefinitionID()) + assert.NoError(t, f.Close()) } diff --git a/slicer.go b/slicer.go index c62b33768a..63d9dad1f9 100644 --- a/slicer.go +++ b/slicer.go @@ -27,12 +27,15 @@ import ( // Name specifies the slicer name, should be an existing field name of the given // table or pivot table, this setting is required. // -// Table specifies the name of the table or pivot table, this setting is -// required. -// // Cell specifies the left top cell coordinates the position for inserting the // slicer, this setting is required. // +// TableSheet specifies the worksheet name of the table or pivot table, this +// setting is required. +// +// TableName specifies the name of the table or pivot table, this setting is +// required. +// // Caption specifies the caption of the slicer, this setting is optional. // // Macro used for set macro for the slicer, the workbook extension should be @@ -51,8 +54,9 @@ import ( // Format specifies the format of the slicer, this setting is optional. type SlicerOptions struct { Name string - Table string Cell string + TableSheet string + TableName string Caption string Macro string Width uint @@ -63,38 +67,44 @@ type SlicerOptions struct { } // AddSlicer function inserts a slicer by giving the worksheet name and slicer -// settings. The pivot table slicer is not supported currently. +// settings. // // For example, insert a slicer on the Sheet1!E1 with field Column1 for the // table named Table1: // // err := f.AddSlicer("Sheet1", &excelize.SlicerOptions{ -// Name: "Column1", -// Table: "Table1", -// Cell: "E1", -// Caption: "Column1", -// Width: 200, -// Height: 200, +// Name: "Column1", +// Cell: "E1", +// TableSheet: "Sheet1", +// TableName: "Table1", +// Caption: "Column1", +// Width: 200, +// Height: 200, // }) func (f *File) AddSlicer(sheet string, opts *SlicerOptions) error { opts, err := parseSlicerOptions(opts) if err != nil { return err } - table, colIdx, err := f.getSlicerSource(sheet, opts) + table, pivotTable, colIdx, err := f.getSlicerSource(opts) if err != nil { return err } - slicerID, err := f.addSheetSlicer(sheet) + extURI, ns := ExtURISlicerListX14, NameSpaceDrawingMLA14 + if table != nil { + extURI = ExtURISlicerListX15 + ns = NameSpaceDrawingMLSlicerX15 + } + slicerID, err := f.addSheetSlicer(sheet, extURI) if err != nil { return err } - slicerCacheName, err := f.setSlicerCache(colIdx, opts, table) + slicerCacheName, err := f.setSlicerCache(sheet, colIdx, opts, table, pivotTable) if err != nil { return err } - slicerName, err := f.addDrawingSlicer(sheet, opts) - if err != nil { + slicerName := f.genSlicerName(opts.Name) + if err := f.addDrawingSlicer(sheet, slicerName, ns, opts); err != nil { return err } return f.addSlicer(slicerID, xlsxSlicer{ @@ -112,7 +122,7 @@ func parseSlicerOptions(opts *SlicerOptions) (*SlicerOptions, error) { if opts == nil { return nil, ErrParameterRequired } - if opts.Name == "" || opts.Table == "" || opts.Cell == "" { + if opts.Name == "" || opts.Cell == "" || opts.TableSheet == "" || opts.TableName == "" { return nil, ErrParameterInvalid } if opts.Width == 0 { @@ -165,34 +175,51 @@ func (f *File) countSlicerCache() int { // getSlicerSource returns the slicer data source table or pivot table settings // and the index of the given slicer fields in the table or pivot table // column. -func (f *File) getSlicerSource(sheet string, opts *SlicerOptions) (*Table, int, error) { +func (f *File) getSlicerSource(opts *SlicerOptions) (*Table, *PivotTableOptions, int, error) { var ( table *Table + pivotTable *PivotTableOptions colIdx int - tables, err = f.GetTables(sheet) + err error + dataRange string + tables []Table + pivotTables []PivotTableOptions ) - if err != nil { - return table, colIdx, err + if tables, err = f.GetTables(opts.TableSheet); err != nil { + return table, pivotTable, colIdx, err } for _, tbl := range tables { - if tbl.Name == opts.Table { + if tbl.Name == opts.TableName { table = &tbl + dataRange = fmt.Sprintf("%s!%s", opts.TableSheet, tbl.Range) break } } if table == nil { - return table, colIdx, newNoExistTableError(opts.Table) + if pivotTables, err = f.GetPivotTables(opts.TableSheet); err != nil { + return table, pivotTable, colIdx, err + } + for _, tbl := range pivotTables { + if tbl.Name == opts.TableName { + pivotTable = &tbl + dataRange = tbl.DataRange + break + } + } + if pivotTable == nil { + return table, pivotTable, colIdx, newNoExistTableError(opts.TableName) + } } - order, _ := f.getTableFieldsOrder(sheet, fmt.Sprintf("%s!%s", sheet, table.Range)) + order, _ := f.getTableFieldsOrder(opts.TableSheet, dataRange) if colIdx = inStrSlice(order, opts.Name, true); colIdx == -1 { - return table, colIdx, newInvalidSlicerNameError(opts.Name) + return table, pivotTable, colIdx, newInvalidSlicerNameError(opts.Name) } - return table, colIdx, err + return table, pivotTable, colIdx, err } // addSheetSlicer adds a new slicer and updates the namespace and relationships // parts of the worksheet by giving the worksheet name. -func (f *File) addSheetSlicer(sheet string) (int, error) { +func (f *File) addSheetSlicer(sheet, extURI string) (int, error) { var ( slicerID = f.countSlicers() + 1 ws, err = f.workSheetReader(sheet) @@ -208,7 +235,7 @@ func (f *File) addSheetSlicer(sheet string) (int, error) { return slicerID, err } for _, ext := range decodeExtLst.Ext { - if ext.URI == ExtURISlicerListX15 { + if ext.URI == extURI { _ = f.xmlNewDecoder(strings.NewReader(ext.Content)).Decode(slicerList) for _, slicer := range slicerList.Slicer { if slicer.RID != "" { @@ -225,12 +252,12 @@ func (f *File) addSheetSlicer(sheet string) (int, error) { sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetXMLPath, "xl/worksheets/") + ".rels" rID := f.addRels(sheetRels, SourceRelationshipSlicer, sheetRelationshipsSlicerXML, "") f.addSheetNameSpace(sheet, NameSpaceSpreadSheetX14) - return slicerID, f.addSheetTableSlicer(ws, rID) + return slicerID, f.addSheetTableSlicer(ws, rID, extURI) } // addSheetTableSlicer adds a new table slicer for the worksheet by giving the -// worksheet relationships ID. -func (f *File) addSheetTableSlicer(ws *xlsxWorksheet, rID int) error { +// worksheet relationships ID and extension URI. +func (f *File) addSheetTableSlicer(ws *xlsxWorksheet, rID int, extURI string) error { var ( decodeExtLst = new(decodeExtLst) err error @@ -245,13 +272,17 @@ func (f *File) addSheetTableSlicer(ws *xlsxWorksheet, rID int) error { slicerListBytes, _ = xml.Marshal(&xlsxX14SlicerList{ Slicer: []*xlsxX14Slicer{{RID: "rId" + strconv.Itoa(rID)}}, }) - decodeExtLst.Ext = append(decodeExtLst.Ext, &xlsxExt{ - xmlns: []xml.Attr{{Name: xml.Name{Local: "xmlns:" + NameSpaceSpreadSheetX15.Name.Local}, Value: NameSpaceSpreadSheetX15.Value}}, - URI: ExtURISlicerListX15, Content: string(slicerListBytes), - }) + ext := &xlsxExt{ + xmlns: []xml.Attr{{Name: xml.Name{Local: "xmlns:" + NameSpaceSpreadSheetX14.Name.Local}, Value: NameSpaceSpreadSheetX14.Value}}, + URI: extURI, Content: string(slicerListBytes), + } + if extURI == ExtURISlicerListX15 { + ext.xmlns = []xml.Attr{{Name: xml.Name{Local: "xmlns:" + NameSpaceSpreadSheetX15.Name.Local}, Value: NameSpaceSpreadSheetX15.Value}} + } + decodeExtLst.Ext = append(decodeExtLst.Ext, ext) sort.Slice(decodeExtLst.Ext, func(i, j int) bool { - return inStrSlice(extensionURIPriority, decodeExtLst.Ext[i].URI, false) < - inStrSlice(extensionURIPriority, decodeExtLst.Ext[j].URI, false) + return inStrSlice(worksheetExtURIPriority, decodeExtLst.Ext[i].URI, false) < + inStrSlice(worksheetExtURIPriority, decodeExtLst.Ext[j].URI, false) }) extLstBytes, err = xml.Marshal(decodeExtLst) ws.ExtLst = &xlsxExtLst{Ext: strings.TrimSuffix(strings.TrimPrefix(string(extLstBytes), ""), "")} @@ -275,6 +306,49 @@ func (f *File) addSlicer(slicerID int, slicer xlsxSlicer) error { return err } +// genSlicerName generates a unique slicer cache name by giving the slicer name. +func (f *File) genSlicerName(name string) string { + var ( + cnt int + slicerName string + names []string + ) + f.Pkg.Range(func(k, v interface{}) bool { + if strings.Contains(k.(string), "xl/slicers/slicer") { + slicers, err := f.slicerReader(k.(string)) + if err != nil { + return true + } + for _, slicer := range slicers.Slicer { + names = append(names, slicer.Name) + } + } + if strings.Contains(k.(string), "xl/timelines/timeline") { + timelines, err := f.timelineReader(k.(string)) + if err != nil { + return true + } + for _, timeline := range timelines.Timeline { + names = append(names, timeline.Name) + } + } + return true + }) + slicerName = name + for { + tmp := slicerName + if cnt > 0 { + tmp = fmt.Sprintf("%s %d", slicerName, cnt) + } + if inStrSlice(names, tmp, true) == -1 { + slicerName = tmp + break + } + cnt++ + } + return slicerName +} + // genSlicerNames generates a unique slicer cache name by giving the slicer name. func (f *File) genSlicerCacheName(name string) string { var ( @@ -316,7 +390,7 @@ func (f *File) genSlicerCacheName(name string) string { // setSlicerCache check if a slicer cache already exists or add a new slicer // cache by giving the column index, slicer, table options, and returns the // slicer cache name. -func (f *File) setSlicerCache(colIdx int, opts *SlicerOptions, table *Table) (string, error) { +func (f *File) setSlicerCache(sheet string, colIdx int, opts *SlicerOptions, table *Table, pivotTable *PivotTableOptions) (string, error) { var ok bool var slicerCacheName string f.Pkg.Range(func(k, v interface{}) bool { @@ -326,7 +400,15 @@ func (f *File) setSlicerCache(colIdx int, opts *SlicerOptions, table *Table) (st Decode(slicerCache); err != nil && err != io.EOF { return true } - if slicerCache.ExtLst == nil { + if pivotTable != nil && slicerCache.PivotTables != nil { + for _, tbl := range slicerCache.PivotTables.PivotTable { + if tbl.Name == pivotTable.Name { + ok, slicerCacheName = true, slicerCache.Name + return false + } + } + } + if table == nil || slicerCache.ExtLst == nil { return true } ext := new(xlsxExt) @@ -346,7 +428,7 @@ func (f *File) setSlicerCache(colIdx int, opts *SlicerOptions, table *Table) (st return slicerCacheName, nil } slicerCacheName = f.genSlicerCacheName(opts.Name) - return slicerCacheName, f.addSlicerCache(slicerCacheName, colIdx, opts, table) + return slicerCacheName, f.addSlicerCache(slicerCacheName, colIdx, opts, table, pivotTable) } // slicerReader provides a function to get the pointer to the structure @@ -367,11 +449,31 @@ func (f *File) slicerReader(slicerXML string) (*xlsxSlicers, error) { return slicer, nil } +// timelineReader provides a function to get the pointer to the structure +// after deserialization of xl/timelines/timeline%d.xml. +func (f *File) timelineReader(timelineXML string) (*xlsxTimelines, error) { + content, ok := f.Pkg.Load(timelineXML) + timeline := &xlsxTimelines{ + XMLNSXMC: SourceRelationshipCompatibility.Value, + XMLNSX: NameSpaceSpreadSheet.Value, + XMLNSXR10: NameSpaceSpreadSheetXR10.Value, + } + if ok && content != nil { + if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(content.([]byte)))). + Decode(timeline); err != nil && err != io.EOF { + return nil, err + } + } + return timeline, nil +} + // addSlicerCache adds a new slicer cache by giving the slicer cache name, -// column index, slicer, and table options. -func (f *File) addSlicerCache(slicerCacheName string, colIdx int, opts *SlicerOptions, table *Table) error { +// column index, slicer, and table or pivot table options. +func (f *File) addSlicerCache(slicerCacheName string, colIdx int, opts *SlicerOptions, table *Table, pivotTable *PivotTableOptions) error { var ( + sortOrder string slicerCacheBytes, tableSlicerBytes, extLstBytes []byte + extURI = ExtURISlicerCachesX14 slicerCacheID = f.countSlicerCache() + 1 decodeExtLst = new(decodeExtLst) slicerCache = xlsxSlicerCacheDefinition{ @@ -381,52 +483,108 @@ func (f *File) addSlicerCache(slicerCacheName string, colIdx int, opts *SlicerOp XMLNSXR10: NameSpaceSpreadSheetXR10.Value, Name: slicerCacheName, SourceName: opts.Name, - ExtLst: &xlsxExtLst{}, } ) - var sortOrder string if opts.ItemDesc { sortOrder = "descending" } - tableSlicerBytes, _ = xml.Marshal(&xlsxTableSlicerCache{ - TableID: table.tID, - Column: colIdx + 1, - SortOrder: sortOrder, - }) - decodeExtLst.Ext = append(decodeExtLst.Ext, &xlsxExt{ - xmlns: []xml.Attr{{Name: xml.Name{Local: "xmlns:" + NameSpaceSpreadSheetX15.Name.Local}, Value: NameSpaceSpreadSheetX15.Value}}, - URI: ExtURISlicerCacheDefinition, Content: string(tableSlicerBytes), - }) - extLstBytes, _ = xml.Marshal(decodeExtLst) - slicerCache.ExtLst = &xlsxExtLst{Ext: strings.TrimSuffix(strings.TrimPrefix(string(extLstBytes), ""), "")} + if pivotTable != nil { + pivotCacheID, err := f.addPivotCacheSlicer(pivotTable) + if err != nil { + return err + } + slicerCache.PivotTables = &xlsxSlicerCachePivotTables{ + PivotTable: []xlsxSlicerCachePivotTable{ + {TabID: f.getSheetID(opts.TableSheet), Name: pivotTable.Name}, + }, + } + slicerCache.Data = &xlsxSlicerCacheData{ + Tabular: &xlsxTabularSlicerCache{ + PivotCacheID: pivotCacheID, + SortOrder: sortOrder, + ShowMissing: boolPtr(false), + Items: &xlsxTabularSlicerCacheItems{ + Count: 1, I: []xlsxTabularSlicerCacheItem{{S: true}}, + }, + }, + } + } + if table != nil { + tableSlicerBytes, _ = xml.Marshal(&xlsxTableSlicerCache{ + TableID: table.tID, + Column: colIdx + 1, + SortOrder: sortOrder, + }) + decodeExtLst.Ext = append(decodeExtLst.Ext, &xlsxExt{ + xmlns: []xml.Attr{{Name: xml.Name{Local: "xmlns:" + NameSpaceSpreadSheetX15.Name.Local}, Value: NameSpaceSpreadSheetX15.Value}}, + URI: ExtURISlicerCacheDefinition, Content: string(tableSlicerBytes), + }) + extLstBytes, _ = xml.Marshal(decodeExtLst) + slicerCache.ExtLst = &xlsxExtLst{Ext: strings.TrimSuffix(strings.TrimPrefix(string(extLstBytes), ""), "")} + extURI = ExtURISlicerCachesX15 + } slicerCacheXML := "xl/slicerCaches/slicerCache" + strconv.Itoa(slicerCacheID) + ".xml" slicerCacheBytes, _ = xml.Marshal(slicerCache) f.saveFileList(slicerCacheXML, slicerCacheBytes) if err := f.addContentTypePart(slicerCacheID, "slicerCache"); err != nil { return err } - if err := f.addWorkbookSlicerCache(slicerCacheID, ExtURISlicerCachesX15); err != nil { + if err := f.addWorkbookSlicerCache(slicerCacheID, extURI); err != nil { return err } return f.SetDefinedName(&DefinedName{Name: slicerCacheName, RefersTo: formulaErrorNA}) } +// addPivotCacheSlicer adds a new slicer cache by giving the pivot table options +// and returns pivot table cache ID. +func (f *File) addPivotCacheSlicer(opts *PivotTableOptions) (int, error) { + var ( + pivotCacheID int + pivotCacheBytes, extLstBytes []byte + decodeExtLst = new(decodeExtLst) + decodeX14PivotCacheDefinition = new(decodeX14PivotCacheDefinition) + ) + pc, err := f.pivotCacheReader(opts.pivotCacheXML) + if err != nil { + return pivotCacheID, err + } + if pc.ExtLst != nil { + _ = f.xmlNewDecoder(strings.NewReader("" + pc.ExtLst.Ext + "")).Decode(decodeExtLst) + for _, ext := range decodeExtLst.Ext { + if ext.URI == ExtURIPivotCacheDefinition { + _ = f.xmlNewDecoder(strings.NewReader(ext.Content)).Decode(decodeX14PivotCacheDefinition) + return decodeX14PivotCacheDefinition.PivotCacheID, err + } + } + } + pivotCacheID = f.genPivotCacheDefinitionID() + pivotCacheBytes, _ = xml.Marshal(&xlsxX14PivotCacheDefinition{PivotCacheID: pivotCacheID}) + ext := &xlsxExt{ + xmlns: []xml.Attr{{Name: xml.Name{Local: "xmlns:" + NameSpaceSpreadSheetX14.Name.Local}, Value: NameSpaceSpreadSheetX14.Value}}, + URI: ExtURIPivotCacheDefinition, Content: string(pivotCacheBytes), + } + decodeExtLst.Ext = append(decodeExtLst.Ext, ext) + extLstBytes, _ = xml.Marshal(decodeExtLst) + pc.ExtLst = &xlsxExtLst{Ext: strings.TrimSuffix(strings.TrimPrefix(string(extLstBytes), ""), "")} + pivotCache, err := xml.Marshal(pc) + f.saveFileList(opts.pivotCacheXML, pivotCache) + return pivotCacheID, err +} + // addDrawingSlicer adds a slicer shape and fallback shape by giving the -// worksheet name, slicer options, and returns slicer name. -func (f *File) addDrawingSlicer(sheet string, opts *SlicerOptions) (string, error) { - var slicerName string +// worksheet name, slicer name, and slicer options. +func (f *File) addDrawingSlicer(sheet, slicerName string, ns xml.Attr, opts *SlicerOptions) error { drawingID := f.countDrawings() + 1 drawingXML := "xl/drawings/drawing" + strconv.Itoa(drawingID) + ".xml" ws, err := f.workSheetReader(sheet) if err != nil { - return slicerName, err + return err } drawingID, drawingXML = f.prepareDrawing(ws, drawingID, sheet, drawingXML) content, twoCellAnchor, cNvPrID, err := f.twoCellAnchorShape(sheet, drawingXML, opts.Cell, opts.Width, opts.Height, opts.Format) if err != nil { - return slicerName, err + return err } - slicerName = fmt.Sprintf("%s %d", opts.Name, cNvPrID) graphicFrame := xlsxGraphicFrame{ NvGraphicFramePr: xlsxNvGraphicFramePr{ CNvPr: &xlsxCNvPr{ @@ -474,14 +632,14 @@ func (f *File) addDrawingSlicer(sheet string, opts *SlicerOptions) (string, erro FLocksWithSheet: *opts.Format.Locked, FPrintsWithSheet: *opts.Format.PrintObject, } - choice := xlsxChoice{ - XMLNSSle15: NameSpaceDrawingMLSlicerX15.Value, - Requires: NameSpaceDrawingMLSlicerX15.Name.Local, - Content: string(graphic), + choice := xlsxChoice{Requires: ns.Name.Local, Content: string(graphic)} + if ns.Value == NameSpaceDrawingMLA14.Value { // pivot table slicer + choice.XMLNSA14 = ns.Value } - fallback := xlsxFallback{ - Content: string(shape), + if ns.Value == NameSpaceDrawingMLSlicerX15.Value { // table slicer + choice.XMLNSSle15 = ns.Value } + fallback := xlsxFallback{Content: string(shape)} choiceBytes, _ := xml.Marshal(choice) shapeBytes, _ := xml.Marshal(fallback) twoCellAnchor.AlternateContent = append(twoCellAnchor.AlternateContent, &xlsxAlternateContent{ @@ -490,7 +648,7 @@ func (f *File) addDrawingSlicer(sheet string, opts *SlicerOptions) (string, erro }) content.TwoCellAnchor = append(content.TwoCellAnchor, twoCellAnchor) f.Drawings.Store(drawingXML, content) - return slicerName, f.addContentTypePart(drawingID, "drawings") + return f.addContentTypePart(drawingID, "drawings") } // addWorkbookSlicerCache add the association ID of the slicer cache in @@ -502,7 +660,8 @@ func (f *File) addWorkbookSlicerCache(slicerCacheID int, URI string) error { idx int appendMode bool decodeExtLst = new(decodeExtLst) - decodeSlicerCaches *decodeX15SlicerCaches + decodeSlicerCaches = new(decodeSlicerCaches) + x14SlicerCaches = new(xlsxX14SlicerCaches) x15SlicerCaches = new(xlsxX15SlicerCaches) ext *xlsxExt slicerCacheBytes, slicerCachesBytes, extLstBytes []byte @@ -518,24 +677,37 @@ func (f *File) addWorkbookSlicerCache(slicerCacheID int, URI string) error { } for idx, ext = range decodeExtLst.Ext { if ext.URI == URI { - if URI == ExtURISlicerCachesX15 { - decodeSlicerCaches = new(decodeX15SlicerCaches) - _ = f.xmlNewDecoder(strings.NewReader(ext.Content)).Decode(decodeSlicerCaches) - slicerCache := xlsxX14SlicerCache{RID: fmt.Sprintf("rId%d", rID)} - slicerCacheBytes, _ = xml.Marshal(slicerCache) + _ = f.xmlNewDecoder(strings.NewReader(ext.Content)).Decode(decodeSlicerCaches) + slicerCache := xlsxX14SlicerCache{RID: fmt.Sprintf("rId%d", rID)} + slicerCacheBytes, _ = xml.Marshal(slicerCache) + if URI == ExtURISlicerCachesX14 { // pivot table slicer + x14SlicerCaches.Content = decodeSlicerCaches.Content + string(slicerCacheBytes) + x14SlicerCaches.XMLNS = NameSpaceSpreadSheetX14.Value + slicerCachesBytes, _ = xml.Marshal(x14SlicerCaches) + } + if URI == ExtURISlicerCachesX15 { // table slicer x15SlicerCaches.Content = decodeSlicerCaches.Content + string(slicerCacheBytes) x15SlicerCaches.XMLNS = NameSpaceSpreadSheetX14.Value slicerCachesBytes, _ = xml.Marshal(x15SlicerCaches) - decodeExtLst.Ext[idx].Content = string(slicerCachesBytes) - appendMode = true } + decodeExtLst.Ext[idx].Content = string(slicerCachesBytes) + appendMode = true } } } if !appendMode { + slicerCache := xlsxX14SlicerCache{RID: fmt.Sprintf("rId%d", rID)} + slicerCacheBytes, _ = xml.Marshal(slicerCache) + if URI == ExtURISlicerCachesX14 { + x14SlicerCaches.Content = string(slicerCacheBytes) + x14SlicerCaches.XMLNS = NameSpaceSpreadSheetX14.Value + slicerCachesBytes, _ = xml.Marshal(x14SlicerCaches) + decodeExtLst.Ext = append(decodeExtLst.Ext, &xlsxExt{ + xmlns: []xml.Attr{{Name: xml.Name{Local: "xmlns:" + NameSpaceSpreadSheetX14.Name.Local}, Value: NameSpaceSpreadSheetX14.Value}}, + URI: ExtURISlicerCachesX14, Content: string(slicerCachesBytes), + }) + } if URI == ExtURISlicerCachesX15 { - slicerCache := xlsxX14SlicerCache{RID: fmt.Sprintf("rId%d", rID)} - slicerCacheBytes, _ = xml.Marshal(slicerCache) x15SlicerCaches.Content = string(slicerCacheBytes) x15SlicerCaches.XMLNS = NameSpaceSpreadSheetX14.Value slicerCachesBytes, _ = xml.Marshal(x15SlicerCaches) @@ -545,6 +717,10 @@ func (f *File) addWorkbookSlicerCache(slicerCacheID int, URI string) error { }) } } + sort.Slice(decodeExtLst.Ext, func(i, j int) bool { + return inStrSlice(workbookExtURIPriority, decodeExtLst.Ext[i].URI, false) < + inStrSlice(workbookExtURIPriority, decodeExtLst.Ext[j].URI, false) + }) extLstBytes, err = xml.Marshal(decodeExtLst) wb.ExtLst = &xlsxExtLst{Ext: strings.TrimSuffix(strings.TrimPrefix(string(extLstBytes), ""), "")} return err diff --git a/slicer_test.go b/slicer_test.go index 663a4e1311..da6fa915f1 100644 --- a/slicer_test.go +++ b/slicer_test.go @@ -2,6 +2,7 @@ package excelize import ( "fmt" + "math/rand" "os" "path/filepath" "testing" @@ -19,21 +20,24 @@ func TestAddSlicer(t *testing.T) { Range: "A1:D5", })) assert.NoError(t, f.AddSlicer("Sheet1", &SlicerOptions{ - Name: "Column1", - Table: "Table1", - Cell: "E1", - Caption: "Column1", + Name: "Column1", + Cell: "E1", + TableSheet: "Sheet1", + TableName: "Table1", + Caption: "Column1", })) assert.NoError(t, f.AddSlicer("Sheet1", &SlicerOptions{ - Name: "Column1", - Table: "Table1", - Cell: "I1", - Caption: "Column1", + Name: "Column1", + Cell: "I1", + TableSheet: "Sheet1", + TableName: "Table1", + Caption: "Column1", })) assert.NoError(t, f.AddSlicer("Sheet1", &SlicerOptions{ Name: colName, - Table: "Table1", Cell: "M1", + TableSheet: "Sheet1", + TableName: "Table1", Caption: colName, Macro: "Button1_Click", Width: 200, @@ -41,38 +45,152 @@ func TestAddSlicer(t *testing.T) { DisplayHeader: &disable, ItemDesc: true, })) + // Test create two pivot tables in a new worksheet + _, err := f.NewSheet("Sheet2") + assert.NoError(t, 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} + types := []string{"Meat", "Dairy", "Beverages", "Produce"} + region := []string{"East", "West", "North", "South"} + assert.NoError(t, f.SetSheetRow("Sheet2", "A1", &[]string{"Month", "Year", "Type", "Sales", "Region"})) + for row := 2; row < 32; row++ { + assert.NoError(t, f.SetCellValue("Sheet2", fmt.Sprintf("A%d", row), month[rand.Intn(12)])) + assert.NoError(t, f.SetCellValue("Sheet2", fmt.Sprintf("B%d", row), year[rand.Intn(3)])) + assert.NoError(t, f.SetCellValue("Sheet2", fmt.Sprintf("C%d", row), types[rand.Intn(4)])) + assert.NoError(t, f.SetCellValue("Sheet2", fmt.Sprintf("D%d", row), rand.Intn(5000))) + assert.NoError(t, f.SetCellValue("Sheet2", fmt.Sprintf("E%d", row), region[rand.Intn(4)])) + } + assert.NoError(t, f.AddPivotTable(&PivotTableOptions{ + DataRange: "Sheet2!A1:E31", + PivotTableRange: "Sheet2!G2:M34", + Name: "PivotTable1", + Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, + Filter: []PivotTableField{{Data: "Region"}}, + Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, + Data: []PivotTableField{{Data: "Sales", Subtotal: "Sum", Name: "Summarize by Sum"}}, + RowGrandTotals: true, + ColGrandTotals: true, + ShowDrill: true, + ShowRowHeaders: true, + ShowColHeaders: true, + ShowLastColumn: true, + ShowError: true, + PivotTableStyleName: "PivotStyleLight16", + })) + assert.NoError(t, f.AddPivotTable(&PivotTableOptions{ + DataRange: "Sheet2!A1:E31", + PivotTableRange: "Sheet2!U34:O2", + Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, + Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, + Data: []PivotTableField{{Data: "Sales", Subtotal: "Average", Name: "Summarize by Average"}}, + RowGrandTotals: true, + ColGrandTotals: true, + ShowDrill: true, + ShowRowHeaders: true, + ShowColHeaders: true, + ShowLastColumn: true, + })) + // Test add a pivot table slicer + assert.NoError(t, f.AddSlicer("Sheet2", &SlicerOptions{ + Name: "Month", + Cell: "G42", + TableSheet: "Sheet2", + TableName: "PivotTable1", + Caption: "Month", + })) + // Test add a pivot table slicer with duplicate field name + assert.NoError(t, f.AddSlicer("Sheet2", &SlicerOptions{ + Name: "Month", + Cell: "K42", + TableSheet: "Sheet2", + TableName: "PivotTable1", + Caption: "Month", + })) + // Test add a pivot table slicer for another pivot table in a worksheet + assert.NoError(t, f.AddSlicer("Sheet2", &SlicerOptions{ + Name: "Region", + Cell: "O42", + TableSheet: "Sheet2", + TableName: "PivotTable2", + Caption: "Region", + ItemDesc: true, + })) // Test add a table slicer with empty slicer options assert.Equal(t, ErrParameterRequired, f.AddSlicer("Sheet1", nil)) // Test add a table slicer with invalid slicer options for _, opts := range []*SlicerOptions{ - {Table: "Table1", Cell: "Q1"}, - {Name: "Column", Cell: "Q1"}, - {Name: "Column", Table: "Table1"}, + {Cell: "Q1", TableSheet: "Sheet1", TableName: "Table1"}, + {Name: "Column", Cell: "Q1", TableSheet: "Sheet1"}, + {Name: "Column", TableSheet: "Sheet1", TableName: "Table1"}, } { assert.Equal(t, ErrParameterInvalid, f.AddSlicer("Sheet1", opts)) } // Test add a table slicer with not exist worksheet assert.EqualError(t, f.AddSlicer("SheetN", &SlicerOptions{ - Name: "Column2", - Table: "Table1", - Cell: "Q1", + Name: "Column2", + Cell: "Q1", + TableSheet: "SheetN", + TableName: "Table1", }), "sheet SheetN does not exist") // Test add a table slicer with not exist table name assert.Equal(t, newNoExistTableError("Table2"), f.AddSlicer("Sheet1", &SlicerOptions{ - Name: "Column2", - Table: "Table2", - Cell: "Q1", + Name: "Column2", + Cell: "Q1", + TableSheet: "Sheet1", + TableName: "Table2", })) // Test add a table slicer with invalid slicer name assert.Equal(t, newInvalidSlicerNameError("Column6"), f.AddSlicer("Sheet1", &SlicerOptions{ - Name: "Column6", - Table: "Table1", - Cell: "Q1", + Name: "Column6", + Cell: "Q1", + TableSheet: "Sheet1", + TableName: "Table1", })) + workbookPath := filepath.Join("test", "TestAddSlicer.xlsm") file, err := os.ReadFile(filepath.Join("test", "vbaProject.bin")) assert.NoError(t, err) assert.NoError(t, f.AddVBAProject(file)) - assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddSlicer.xlsm"))) + assert.NoError(t, f.SaveAs(workbookPath)) + assert.NoError(t, f.Close()) + + // Test add a pivot table slicer with unsupported charset pivot table + f, err = OpenFile(workbookPath) + assert.NoError(t, err) + f.Pkg.Store("xl/pivotTables/pivotTable2.xml", MacintoshCyrillicCharset) + assert.EqualError(t, f.AddSlicer("Sheet2", &SlicerOptions{ + Name: "Month", + Cell: "G42", + TableSheet: "Sheet2", + TableName: "PivotTable1", + Caption: "Month", + }), "XML syntax error on line 1: invalid UTF-8") + assert.NoError(t, f.Close()) + + // Test add a pivot table slicer with workbook which contains timeline + f, err = OpenFile(workbookPath) + assert.NoError(t, err) + f.Pkg.Store("xl/timelines/timeline1.xml", []byte(fmt.Sprintf(``, NameSpaceSpreadSheetX15.Value))) + assert.NoError(t, f.AddSlicer("Sheet2", &SlicerOptions{ + Name: "Month", + Cell: "G42", + TableSheet: "Sheet2", + TableName: "PivotTable1", + Caption: "Month", + })) + assert.NoError(t, f.Close()) + + // Test add a pivot table slicer with unsupported charset timeline + f, err = OpenFile(workbookPath) + assert.NoError(t, err) + f.Pkg.Store("xl/timelines/timeline1.xml", MacintoshCyrillicCharset) + assert.NoError(t, f.AddSlicer("Sheet2", &SlicerOptions{ + Name: "Month", + Cell: "G42", + TableSheet: "Sheet2", + TableName: "PivotTable1", + Caption: "Month", + })) assert.NoError(t, f.Close()) // Test add a table slicer with invalid worksheet extension list @@ -85,9 +203,10 @@ func TestAddSlicer(t *testing.T) { assert.True(t, ok) ws.(*xlsxWorksheet).ExtLst = &xlsxExtLst{Ext: "<>"} assert.Error(t, f.AddSlicer("Sheet1", &SlicerOptions{ - Name: "Column1", - Table: "Table1", - Cell: "E1", + Name: "Column1", + Cell: "E1", + TableSheet: "Sheet1", + TableName: "Table1", })) assert.NoError(t, f.Close()) @@ -99,9 +218,10 @@ func TestAddSlicer(t *testing.T) { })) f.Pkg.Store("xl/slicers/slicer2.xml", MacintoshCyrillicCharset) assert.EqualError(t, f.AddSlicer("Sheet1", &SlicerOptions{ - Name: "Column1", - Table: "Table1", - Cell: "E1", + Name: "Column1", + Cell: "E1", + TableName: "Table1", + TableSheet: "Sheet1", }), "XML syntax error on line 1: invalid UTF-8") assert.NoError(t, f.Close()) @@ -113,9 +233,10 @@ func TestAddSlicer(t *testing.T) { })) f.WorkBook.ExtLst = &xlsxExtLst{Ext: "<>"} assert.Error(t, f.AddSlicer("Sheet1", &SlicerOptions{ - Name: "Column1", - Table: "Table1", - Cell: "E1", + Name: "Column1", + Cell: "E1", + TableName: "Table1", + TableSheet: "Sheet1", })) assert.NoError(t, f.Close()) @@ -128,9 +249,10 @@ func TestAddSlicer(t *testing.T) { f.ContentTypes = nil f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) assert.EqualError(t, f.AddSlicer("Sheet1", &SlicerOptions{ - Name: "Column1", - Table: "Table1", - Cell: "E1", + Name: "Column1", + Cell: "E1", + TableName: "Table1", + TableSheet: "Sheet1", }), "XML syntax error on line 1: invalid UTF-8") f.ContentTypes = nil f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) @@ -145,10 +267,11 @@ func TestAddSlicer(t *testing.T) { })) f.Pkg.Store("xl/drawings/drawing2.xml", MacintoshCyrillicCharset) assert.EqualError(t, f.AddSlicer("Sheet1", &SlicerOptions{ - Name: "Column1", - Table: "Table1", - Cell: "E1", - Caption: "Column1", + Name: "Column1", + Cell: "E1", + TableSheet: "Sheet1", + TableName: "Table1", + Caption: "Column1", }), "XML syntax error on line 1: invalid UTF-8") assert.NoError(t, f.Close()) } @@ -156,7 +279,7 @@ func TestAddSlicer(t *testing.T) { func TestAddSheetSlicer(t *testing.T) { f := NewFile() // Test add sheet slicer with not exist worksheet name - _, err := f.addSheetSlicer("SheetN") + _, err := f.addSheetSlicer("SheetN", ExtURISlicerListX15) assert.EqualError(t, err, "sheet SheetN does not exist") assert.NoError(t, f.Close()) } @@ -164,41 +287,41 @@ func TestAddSheetSlicer(t *testing.T) { func TestAddSheetTableSlicer(t *testing.T) { f := NewFile() // Test add sheet table slicer with invalid worksheet extension - assert.Error(t, f.addSheetTableSlicer(&xlsxWorksheet{ExtLst: &xlsxExtLst{Ext: "<>"}}, 0)) + assert.Error(t, f.addSheetTableSlicer(&xlsxWorksheet{ExtLst: &xlsxExtLst{Ext: "<>"}}, 0, ExtURISlicerListX15)) // Test add sheet table slicer with existing worksheet extension - assert.NoError(t, f.addSheetTableSlicer(&xlsxWorksheet{ExtLst: &xlsxExtLst{Ext: fmt.Sprintf("", ExtURITimelineRefs)}}, 1)) + assert.NoError(t, f.addSheetTableSlicer(&xlsxWorksheet{ExtLst: &xlsxExtLst{Ext: fmt.Sprintf("", ExtURITimelineRefs)}}, 1, ExtURISlicerListX15)) assert.NoError(t, f.Close()) } func TestSetSlicerCache(t *testing.T) { f := NewFile() f.Pkg.Store("xl/slicerCaches/slicerCache1.xml", MacintoshCyrillicCharset) - _, err := f.setSlicerCache(1, &SlicerOptions{}, &Table{}) + _, err := f.setSlicerCache("Sheet1", 1, &SlicerOptions{}, &Table{}, nil) assert.NoError(t, err) assert.NoError(t, f.Close()) f = NewFile() f.Pkg.Store("xl/slicerCaches/slicerCache2.xml", []byte(fmt.Sprintf(``, NameSpaceSpreadSheetX14.Value, ExtURISlicerCacheDefinition))) - _, err = f.setSlicerCache(1, &SlicerOptions{}, &Table{}) + _, err = f.setSlicerCache("Sheet1", 1, &SlicerOptions{}, &Table{}, nil) assert.NoError(t, err) assert.NoError(t, f.Close()) f = NewFile() f.Pkg.Store("xl/slicerCaches/slicerCache2.xml", []byte(fmt.Sprintf(``, NameSpaceSpreadSheetX14.Value, ExtURISlicerCacheDefinition))) - _, err = f.setSlicerCache(1, &SlicerOptions{}, &Table{}) + _, err = f.setSlicerCache("Sheet1", 1, &SlicerOptions{}, &Table{}, nil) assert.NoError(t, err) assert.NoError(t, f.Close()) f = NewFile() f.Pkg.Store("xl/slicerCaches/slicerCache2.xml", []byte(fmt.Sprintf(``, NameSpaceSpreadSheetX14.Value, ExtURISlicerCacheDefinition))) - _, err = f.setSlicerCache(1, &SlicerOptions{}, &Table{tID: 1}) + _, err = f.setSlicerCache("Sheet1", 1, &SlicerOptions{}, &Table{tID: 1}, nil) assert.NoError(t, err) assert.NoError(t, f.Close()) f = NewFile() f.Pkg.Store("xl/slicerCaches/slicerCache2.xml", []byte(fmt.Sprintf(``, NameSpaceSpreadSheetX14.Value))) - _, err = f.setSlicerCache(1, &SlicerOptions{}, &Table{tID: 1}) + _, err = f.setSlicerCache("Sheet1", 1, &SlicerOptions{}, &Table{tID: 1}, nil) assert.NoError(t, err) assert.NoError(t, f.Close()) } @@ -207,31 +330,36 @@ func TestAddSlicerCache(t *testing.T) { f := NewFile() f.ContentTypes = nil f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) - assert.EqualError(t, f.addSlicerCache("Slicer1", 0, &SlicerOptions{}, &Table{}), "XML syntax error on line 1: invalid UTF-8") + assert.EqualError(t, f.addSlicerCache("Slicer1", 0, &SlicerOptions{}, &Table{}, nil), "XML syntax error on line 1: invalid UTF-8") + // Test add a pivot table cache slicer with unsupported charset + pivotCacheXML := "xl/pivotCache/pivotCacheDefinition1.xml" + f.Pkg.Store(pivotCacheXML, MacintoshCyrillicCharset) + assert.EqualError(t, f.addSlicerCache("Slicer1", 0, &SlicerOptions{}, nil, + &PivotTableOptions{pivotCacheXML: pivotCacheXML}), "XML syntax error on line 1: invalid UTF-8") assert.NoError(t, f.Close()) } func TestAddDrawingSlicer(t *testing.T) { f := NewFile() // Test add a drawing slicer with not exist worksheet - _, err := f.addDrawingSlicer("SheetN", &SlicerOptions{ - Name: "Column2", - Table: "Table1", - Cell: "Q1", - }) - assert.EqualError(t, err, "sheet SheetN does not exist") + assert.EqualError(t, f.addDrawingSlicer("SheetN", "Column2", NameSpaceDrawingMLSlicerX15, &SlicerOptions{ + Name: "Column2", + Cell: "Q1", + TableSheet: "SheetN", + TableName: "Table1", + }), "sheet SheetN does not exist") // Test add a drawing slicer with invalid cell reference - _, err = f.addDrawingSlicer("Sheet1", &SlicerOptions{ - Name: "Column2", - Table: "Table1", - Cell: "A", - }) - assert.EqualError(t, err, "cannot convert cell \"A\" to coordinates: invalid cell name \"A\"") + assert.EqualError(t, f.addDrawingSlicer("Sheet1", "Column2", NameSpaceDrawingMLSlicerX15, &SlicerOptions{ + Name: "Column2", + Cell: "A", + TableSheet: "Sheet1", + TableName: "Table1", + }), "cannot convert cell \"A\" to coordinates: invalid cell name \"A\"") assert.NoError(t, f.Close()) } func TestAddWorkbookSlicerCache(t *testing.T) { - // Test add a workbook slicer cache with with unsupported charset workbook + // Test add a workbook slicer cache with unsupported charset workbook f := NewFile() f.WorkBook = nil f.Pkg.Store("xl/workbook.xml", MacintoshCyrillicCharset) @@ -245,3 +373,14 @@ func TestGenSlicerCacheName(t *testing.T) { assert.Equal(t, "Slicer_Column_11", f.genSlicerCacheName("Column 1")) assert.NoError(t, f.Close()) } + +func TestAddPivotCacheSlicer(t *testing.T) { + f := NewFile() + pivotCacheXML := "xl/pivotCache/pivotCacheDefinition1.xml" + // Test add a pivot table cache slicer with existing extension list + f.Pkg.Store(pivotCacheXML, []byte(fmt.Sprintf(``, NameSpaceSpreadSheet.Value, ExtURIPivotCacheDefinition))) + _, err := f.addPivotCacheSlicer(&PivotTableOptions{ + pivotCacheXML: pivotCacheXML, + }) + assert.NoError(t, err) +} diff --git a/sparkline.go b/sparkline.go index 7bb50f0f9e..810d21365c 100644 --- a/sparkline.go +++ b/sparkline.go @@ -528,8 +528,8 @@ func (f *File) appendSparkline(ws *xlsxWorksheet, group *xlsxX14SparklineGroup, }) } sort.Slice(decodeExtLst.Ext, func(i, j int) bool { - return inStrSlice(extensionURIPriority, decodeExtLst.Ext[i].URI, false) < - inStrSlice(extensionURIPriority, decodeExtLst.Ext[j].URI, false) + return inStrSlice(worksheetExtURIPriority, decodeExtLst.Ext[i].URI, false) < + inStrSlice(worksheetExtURIPriority, decodeExtLst.Ext[j].URI, false) }) extLstBytes, err = xml.Marshal(decodeExtLst) ws.ExtLst = &xlsxExtLst{Ext: strings.TrimSuffix(strings.TrimPrefix(string(extLstBytes), ""), "")} diff --git a/styles.go b/styles.go index 528529548a..1de83bcb03 100644 --- a/styles.go +++ b/styles.go @@ -2603,8 +2603,8 @@ func (f *File) appendCfRule(ws *xlsxWorksheet, rule *xlsxX14CfRule) error { }) } sort.Slice(decodeExtLst.Ext, func(i, j int) bool { - return inStrSlice(extensionURIPriority, decodeExtLst.Ext[i].URI, false) < - inStrSlice(extensionURIPriority, decodeExtLst.Ext[j].URI, false) + return inStrSlice(worksheetExtURIPriority, decodeExtLst.Ext[i].URI, false) < + inStrSlice(worksheetExtURIPriority, decodeExtLst.Ext[j].URI, false) }) extLstBytes, err = xml.Marshal(decodeExtLst) ws.ExtLst = &xlsxExtLst{Ext: strings.TrimSuffix(strings.TrimPrefix(string(extLstBytes), ""), "")} diff --git a/templates.go b/templates.go index 1c6f4ddb16..c94a09b406 100644 --- a/templates.go +++ b/templates.go @@ -22,6 +22,7 @@ var ( 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"} + NameSpaceDrawingMLA14 = xml.Attr{Name: xml.Name{Local: "a14", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/drawing/2010/main"} NameSpaceDrawingMLChart = xml.Attr{Name: xml.Name{Local: "c", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/drawingml/2006/chart"} NameSpaceDrawingMLSlicer = xml.Attr{Name: xml.Name{Local: "sle", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/drawing/2010/slicer"} NameSpaceDrawingMLSlicerX15 = xml.Attr{Name: xml.Name{Local: "sle15", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/drawing/2012/slicer"} @@ -117,7 +118,7 @@ const ( ExtURISlicerCacheHideItemsWithNoData = "{470722E0-AACD-4C17-9CDC-17EF765DBC7E}" ExtURISlicerCachesX14 = "{BBE1A952-AA13-448e-AADC-164F8A28A991}" ExtURISlicerCachesX15 = "{46BE6895-7355-4a93-B00E-2C351335B9C9}" - ExtURISlicerListX14 = "{A8765BA9-456A-4DAB-B4F3-ACF838C121DE}" + ExtURISlicerListX14 = "{A8765BA9-456A-4dab-B4F3-ACF838C121DE}" ExtURISlicerListX15 = "{3A4CF648-6AED-40f4-86FF-DC5316D8AED3}" ExtURISparklineGroups = "{05C60535-1F16-4fd2-B633-F4F36F0B64E0}" ExtURISVG = "{96DAC541-7B7A-43D3-8B79-37D633B846F1}" @@ -129,8 +130,25 @@ const ( ExtURIWorkbookPrX15 = "{140A7094-0E35-4892-8432-C4D2E57EDEB5}" ) -// extensionURIPriority is the priority of URI in the extension lists. -var extensionURIPriority = []string{ +// workbookExtURIPriority is the priority of URI in the workbook extension lists. +var workbookExtURIPriority = []string{ + ExtURIPivotCachesX14, + ExtURISlicerCachesX14, + ExtURISlicerCachesX15, + ExtURIWorkbookPrX14, + ExtURIPivotCachesX15, + ExtURIPivotTableReferences, + ExtURITimelineCachePivotCaches, + ExtURITimelineCacheRefs, + ExtURIWorkbookPrX15, + ExtURIDataModel, + ExtURICalcFeatures, + ExtURIExternalLinkPr, + ExtURIModelTimeGroupings, +} + +// worksheetExtURIPriority is the priority of URI in the worksheet extension lists. +var worksheetExtURIPriority = []string{ ExtURIConditionalFormattings, ExtURIDataValidations, ExtURISparklineGroups, @@ -167,6 +185,7 @@ const ( // 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 + pivotTableRefreshedVersion = 8 defaultDrawingScale = 1.0 defaultChartDimensionWidth = 480 defaultChartDimensionHeight = 260 diff --git a/xmlPivotCache.go b/xmlPivotCache.go index 1925fa4d23..9f5a84165a 100644 --- a/xmlPivotCache.go +++ b/xmlPivotCache.go @@ -120,26 +120,26 @@ type xlsxCacheField struct { // those values that are referenced in multiple places across all the // PivotTable parts. type xlsxSharedItems struct { - ContainsSemiMixedTypes bool `xml:"containsSemiMixedTypes,attr,omitempty"` - ContainsNonDate bool `xml:"containsNonDate,attr,omitempty"` - ContainsDate bool `xml:"containsDate,attr,omitempty"` - ContainsString bool `xml:"containsString,attr,omitempty"` - ContainsBlank bool `xml:"containsBlank,attr,omitempty"` - ContainsMixedTypes bool `xml:"containsMixedTypes,attr,omitempty"` - ContainsNumber bool `xml:"containsNumber,attr,omitempty"` - ContainsInteger bool `xml:"containsInteger,attr,omitempty"` - MinValue float64 `xml:"minValue,attr,omitempty"` - MaxValue float64 `xml:"maxValue,attr,omitempty"` - MinDate string `xml:"minDate,attr,omitempty"` - MaxDate string `xml:"maxDate,attr,omitempty"` - Count int `xml:"count,attr"` - LongText bool `xml:"longText,attr,omitempty"` - M *xlsxMissing `xml:"m"` - N *xlsxNumber `xml:"n"` - B *xlsxBoolean `xml:"b"` - E *xlsxError `xml:"e"` - S *xlsxString `xml:"s"` - D *xlsxDateTime `xml:"d"` + ContainsSemiMixedTypes bool `xml:"containsSemiMixedTypes,attr,omitempty"` + ContainsNonDate bool `xml:"containsNonDate,attr,omitempty"` + ContainsDate bool `xml:"containsDate,attr,omitempty"` + ContainsString bool `xml:"containsString,attr,omitempty"` + ContainsBlank bool `xml:"containsBlank,attr,omitempty"` + ContainsMixedTypes bool `xml:"containsMixedTypes,attr,omitempty"` + ContainsNumber bool `xml:"containsNumber,attr,omitempty"` + ContainsInteger bool `xml:"containsInteger,attr,omitempty"` + MinValue float64 `xml:"minValue,attr,omitempty"` + MaxValue float64 `xml:"maxValue,attr,omitempty"` + MinDate string `xml:"minDate,attr,omitempty"` + MaxDate string `xml:"maxDate,attr,omitempty"` + Count int `xml:"count,attr"` + LongText bool `xml:"longText,attr,omitempty"` + M []xlsxMissing `xml:"m"` + N []xlsxNumber `xml:"n"` + B []xlsxBoolean `xml:"b"` + E []xlsxError `xml:"e"` + S []xlsxString `xml:"s"` + D []xlsxDateTime `xml:"d"` } // xlsxMissing represents a value that was not specified. @@ -226,3 +226,17 @@ type xlsxMeasureGroups struct{} // xlsxMaps represents the PivotTable OLAP measure group - Dimension maps. type xlsxMaps struct{} + +// xlsxX14PivotCacheDefinition specifies the extended properties of a pivot +// table cache definition. +type xlsxX14PivotCacheDefinition struct { + XMLName xml.Name `xml:"x14:pivotCacheDefinition"` + PivotCacheID int `xml:"pivotCacheId,attr"` +} + +// decodeX14PivotCacheDefinition defines the structure used to parse the +// x14:pivotCacheDefinition element of a pivot table cache. +type decodeX14PivotCacheDefinition struct { + XMLName xml.Name `xml:"pivotCacheDefinition"` + PivotCacheID int `xml:"pivotCacheId,attr"` +} diff --git a/xmlSlicers.go b/xmlSlicers.go index e259de8990..56dde04ef9 100644 --- a/xmlSlicers.go +++ b/xmlSlicers.go @@ -126,6 +126,13 @@ type xlsxX14Slicer struct { RID string `xml:"r:id,attr"` } +// xlsxX14SlicerCaches directly maps the x14:slicerCache element. +type xlsxX14SlicerCaches struct { + XMLName xml.Name `xml:"x14:slicerCaches"` + XMLNS string `xml:"xmlns:x14,attr"` + Content string `xml:",innerxml"` +} + // xlsxX15SlicerCaches directly maps the x14:slicerCache element. type xlsxX14SlicerCache struct { XMLName xml.Name `xml:"x14:slicerCache"` @@ -160,9 +167,39 @@ type decodeSlicer struct { RID string `xml:"id,attr"` } -// decodeX15SlicerCaches defines the structure used to parse the -// x15:slicerCaches element of a slicer cache. -type decodeX15SlicerCaches struct { +// decodeSlicerCaches defines the structure used to parse the +// x14:slicerCaches and x15:slicerCaches element of a slicer cache. +type decodeSlicerCaches struct { XMLName xml.Name `xml:"slicerCaches"` Content string `xml:",innerxml"` } + +// xlsxTimelines is a mechanism for filtering data in pivot table views, cube +// functions and charts based on non-worksheet pivot tables. In the case of +// using OLAP Timeline source data, a Timeline is based on a key attribute of +// an OLAP hierarchy. In the case of using native Timeline source data, a +// Timeline is based on a data table column. +type xlsxTimelines struct { + XMLName xml.Name `xml:"http://schemas.microsoft.com/office/spreadsheetml/2010/11/main timelines"` + XMLNSXMC string `xml:"xmlns:mc,attr"` + XMLNSX string `xml:"xmlns:x,attr"` + XMLNSXR10 string `xml:"xmlns:xr10,attr"` + Timeline []xlsxTimeline `xml:"timeline"` +} + +// xlsxTimeline is timeline view specifies the display of a timeline on a +// worksheet. +type xlsxTimeline struct { + Name string `xml:"name,attr"` + XR10UID string `xml:"xr10:uid,attr,omitempty"` + Cache string `xml:"cache,attr"` + Caption string `xml:"caption,attr,omitempty"` + ShowHeader *bool `xml:"showHeader,attr"` + ShowSelectionLabel *bool `xml:"showSelectionLabel,attr"` + ShowTimeLevel *bool `xml:"showTimeLevel,attr"` + ShowHorizontalScrollbar *bool `xml:"showHorizontalScrollbar,attr"` + Level int `xml:"level,attr"` + SelectionLevel int `xml:"selectionLevel,attr"` + ScrollPosition string `xml:"scrollPosition,attr,omitempty"` + Style string `xml:"style,attr,omitempty"` +} diff --git a/xmlWorkbook.go b/xmlWorkbook.go index 0a3586f8e7..cc953975ed 100644 --- a/xmlWorkbook.go +++ b/xmlWorkbook.go @@ -245,6 +245,7 @@ type xlsxAlternateContent struct { // AlternateContent elements. type xlsxChoice struct { XMLName xml.Name `xml:"mc:Choice"` + XMLNSA14 string `xml:"xmlns:a14,attr,omitempty"` XMLNSSle15 string `xml:"xmlns:sle15,attr,omitempty"` Requires string `xml:"Requires,attr,omitempty"` Content string `xml:",innerxml"` From 1c23dc3507c65d5b83de4750541fa15a37e31779 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 28 Sep 2023 08:53:54 +0800 Subject: [PATCH 795/957] This sorted exported error constants by name and listed them in one place --- cell.go | 2 +- errors.go | 440 +++++++++++++++++++++++++-------------------- excelize.go | 2 +- excelize_test.go | 6 +- lib.go | 2 +- lib_test.go | 4 +- pivotTable.go | 14 +- pivotTable_test.go | 28 +-- rows.go | 10 -- rows_test.go | 3 +- sheet_test.go | 2 +- stream.go | 2 +- styles_test.go | 6 +- table.go | 8 +- table_test.go | 24 +-- vml.go | 6 +- vml_test.go | 10 +- xmlTable.go | 16 +- 18 files changed, 310 insertions(+), 275 deletions(-) diff --git a/cell.go b/cell.go index 53bcc218b0..b1f61f675e 100644 --- a/cell.go +++ b/cell.go @@ -923,7 +923,7 @@ func (f *File) SetCellHyperLink(sheet, cell, link, linkType string, opts ...Hype Location: link, } default: - return fmt.Errorf("invalid link type %q", linkType) + return newInvalidLinkTypeError(linkType) } for _, o := range opts { diff --git a/errors.go b/errors.go index 59888007bc..b5fe34eac1 100644 --- a/errors.go +++ b/errors.go @@ -16,16 +16,204 @@ import ( "fmt" ) -// newInvalidColumnNameError defined the error message on receiving the -// invalid column name. -func newInvalidColumnNameError(col string) error { - return fmt.Errorf("invalid column name %q", col) +var ( + // ErrAddVBAProject defined the error message on add the VBA project in + // the workbook. + ErrAddVBAProject = errors.New("unsupported VBA project") + // ErrAttrValBool defined the error message on marshal and unmarshal + // boolean type XML attribute. + ErrAttrValBool = errors.New("unexpected child of attrValBool") + // ErrCellCharsLength defined the error message for receiving a cell + // characters length that exceeds the limit. + ErrCellCharsLength = fmt.Errorf("cell value must be 0-%d characters", TotalCellChars) + // ErrCellStyles defined the error message on cell styles exceeds the limit. + ErrCellStyles = fmt.Errorf("the cell styles exceeds the %d limit", MaxCellStyles) + // 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) + // ErrColumnWidth defined the error message on receive an invalid column + // width. + ErrColumnWidth = fmt.Errorf("the width of the column must be less than or equal to %d characters", MaxColumnWidth) + // ErrCoordinates defined the error message on invalid coordinates tuples + // length. + ErrCoordinates = errors.New("coordinates length must be 4") + // ErrCustomNumFmt defined the error message on receive the empty custom number format. + ErrCustomNumFmt = errors.New("custom number format can not be empty") + // ErrDataValidationFormulaLength defined the error message for receiving a + // data validation formula length that exceeds the limit. + 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") + // 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") + // ErrDefinedNameScope defined the error message on not found defined name + // in the given scope. + ErrDefinedNameScope = errors.New("no defined name on the scope") + // ErrExistsSheet defined the error message on given sheet already exists. + ErrExistsSheet = errors.New("the same name sheet already exists") + // ErrExistsTableName defined the error message on given table already exists. + ErrExistsTableName = errors.New("the same name table already exists") + // 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 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) + // ErrFormControlValue defined the error message for receiving a scroll + // value exceeds limit. + ErrFormControlValue = fmt.Errorf("scroll value must be between 0 and %d", MaxFormControlValue) + // ErrGroupSheets defined the error message on group sheets. + ErrGroupSheets = errors.New("group worksheet must contain an active worksheet") + // ErrImgExt defined the error message on receive an unsupported image + // extension. + ErrImgExt = errors.New("unsupported image extension") + // ErrInvalidFormula defined the error message on receive an invalid + // formula. + ErrInvalidFormula = errors.New("formula not valid") + // ErrMaxFilePathLength defined the error message on receive the file path + // length overflow. + ErrMaxFilePathLength = fmt.Errorf("file path length exceeds maximum limit %d characters", MaxFilePathLength) + // ErrMaxRowHeight defined the error message on receive an invalid row + // height. + ErrMaxRowHeight = fmt.Errorf("the height of the row must be less than or equal to %d points", MaxRowHeight) + // ErrMaxRows defined the error message on receive a row number exceeds maximum limit. + ErrMaxRows = errors.New("row number exceeds maximum limit") + // 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) + // ErrOptionsUnzipSizeLimit defined the error message for receiving + // invalid UnzipSizeLimit and UnzipXMLSizeLimit. + ErrOptionsUnzipSizeLimit = errors.New("the value of UnzipSizeLimit should be greater than or equal to UnzipXMLSizeLimit") + // ErrOutlineLevel defined the error message on receive an invalid outline + // level number. + ErrOutlineLevel = errors.New("invalid outline level") + // ErrParameterInvalid defined the error message on receive the invalid + // parameter. + ErrParameterInvalid = errors.New("parameter is invalid") + // ErrParameterRequired defined the error message on receive the empty + // parameter. + ErrParameterRequired = errors.New("parameter is required") + // ErrPasswordLengthInvalid defined the error message on invalid password + // length. + ErrPasswordLengthInvalid = errors.New("password length invalid") + // ErrSave defined the error message for saving file. + ErrSave = errors.New("no path defined for file, consider File.WriteTo or File.Write") + // ErrSheetIdx defined the error message on receive the invalid worksheet + // index. + ErrSheetIdx = errors.New("invalid worksheet index") + // ErrSheetNameBlank defined the error message on receive the blank sheet + // name. + ErrSheetNameBlank = errors.New("the sheet name can not be blank") + // 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]") + // 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) + // 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") + // ErrSparkline defined the error message on receive the invalid sparkline + // parameters. + ErrSparkline = errors.New("must have the same number of 'Location' and 'Range' parameters") + // ErrSparklineLocation defined the error message on missing Location + // parameters + ErrSparklineLocation = errors.New("parameter 'Location' is required") + // ErrSparklineRange defined the error message on missing sparkline Range + // parameters + ErrSparklineRange = errors.New("parameter 'Range' is required") + // ErrSparklineStyle defined the error message on receive the invalid + // sparkline Style parameters. + ErrSparklineStyle = errors.New("parameter 'Style' must between 0-35") + // ErrSparklineType defined the error message on receive the invalid + // sparkline Type parameters. + ErrSparklineType = errors.New("parameter 'Type' must be 'line', 'column' or 'win_loss'") + // 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") + // ErrTotalSheetHyperlinks defined the error message on hyperlinks count + // overflow. + ErrTotalSheetHyperlinks = errors.New("over maximum limit hyperlinks in a worksheet") + // ErrUnknownEncryptMechanism defined the error message on unsupported + // encryption mechanism. + ErrUnknownEncryptMechanism = errors.New("unknown encryption mechanism") + // ErrUnprotectSheet defined the error message on worksheet has set no + // protection. + ErrUnprotectSheet = errors.New("worksheet has set no protect") + // ErrUnprotectSheetPassword defined the error message on remove sheet + // protection with password verification failed. + ErrUnprotectSheetPassword = errors.New("worksheet protect password not match") + // 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") + // ErrUnsupportedEncryptMechanism defined the error message on unsupported + // encryption mechanism. + ErrUnsupportedEncryptMechanism = errors.New("unsupported encryption mechanism") + // ErrUnsupportedHashAlgorithm defined the error message on unsupported + // hash algorithm. + ErrUnsupportedHashAlgorithm = errors.New("unsupported hash algorithm") + // ErrUnsupportedNumberFormat defined the error message on unsupported number format + // expression. + ErrUnsupportedNumberFormat = errors.New("unsupported number format token") + // ErrWorkbookFileFormat defined the error message on receive an + // unsupported workbook file format. + ErrWorkbookFileFormat = errors.New("unsupported workbook file format") + // ErrWorkbookPassword defined the error message on receiving the incorrect + // workbook password. + ErrWorkbookPassword = errors.New("the supplied open workbook password is not correct") +) + +// ErrSheetNotExist defined an error of sheet that does not exist. +type ErrSheetNotExist struct { + SheetName string } -// newInvalidRowNumberError defined the error message on receiving the invalid -// row number. -func newInvalidRowNumberError(row int) error { - return fmt.Errorf("invalid row number %d", row) +// Error returns the error message on receiving the non existing sheet name. +func (err ErrSheetNotExist) Error() string { + return fmt.Sprintf("sheet %s does not exist", err.SheetName) +} + +// newCellNameToCoordinatesError defined the error message on converts +// alphanumeric cell name to coordinates. +func newCellNameToCoordinatesError(cell string, err error) error { + return fmt.Errorf("cannot convert cell %q to coordinates: %v", cell, err) +} + +// newCoordinatesToCellNameError defined the error message on converts [X, Y] +// coordinates to alpha-numeric cell name. +func newCoordinatesToCellNameError(col, row int) error { + return fmt.Errorf("invalid cell reference [%d, %d]", col, row) +} + +// newFieldLengthError defined the error message on receiving the field length +// overflow. +func newFieldLengthError(name string) error { + return fmt.Errorf("field %s must be less than or equal to 255 characters", name) +} + +// newInvalidAutoFilterColumnError defined the error message on receiving the +// incorrect index of column. +func newInvalidAutoFilterColumnError(col string) error { + return fmt.Errorf("incorrect index of column %q", col) +} + +// newInvalidAutoFilterExpError defined the error message on receiving the +// incorrect number of tokens in criteria expression. +func newInvalidAutoFilterExpError(exp string) error { + return fmt.Errorf("incorrect number of tokens in criteria %q", exp) +} + +// newInvalidAutoFilterOperatorError defined the error message on receiving the +// incorrect expression operator. +func newInvalidAutoFilterOperatorError(op, exp string) error { + return fmt.Errorf("the operator %q in expression %q is not valid in relation to Blanks/NonBlanks", op, exp) } // newInvalidCellNameError defined the error message on receiving the invalid @@ -34,10 +222,10 @@ func newInvalidCellNameError(cell string) error { return fmt.Errorf("invalid cell name %q", cell) } -// newInvalidSlicerNameError defined the error message on receiving the invalid -// slicer name. -func newInvalidSlicerNameError(name string) error { - return fmt.Errorf("invalid slicer name %q", name) +// newInvalidColumnNameError defined the error message on receiving the +// invalid column name. +func newInvalidColumnNameError(col string) error { + return fmt.Errorf("invalid column name %q", col) } // newInvalidExcelDateError defined the error message on receiving the data @@ -46,22 +234,28 @@ func newInvalidExcelDateError(dateValue float64) error { return fmt.Errorf("invalid date value %f, negative values are not supported", dateValue) } +// newInvalidLinkTypeError defined the error message on receiving the invalid +// hyper link type. +func newInvalidLinkTypeError(linkType string) error { + return fmt.Errorf("invalid link type %q", linkType) +} + // 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 -// type are unsupported. -func newUnsupportedChartType(chartType ChartType) error { - return fmt.Errorf("unsupported chart type %d", chartType) +// newInvalidRowNumberError defined the error message on receiving the invalid +// row number. +func newInvalidRowNumberError(row int) error { + return fmt.Errorf("invalid row number %d", row) } -// newUnzipSizeLimitError defined the error message on unzip size exceeds the -// limit. -func newUnzipSizeLimitError(unzipSizeLimit int64) error { - return fmt.Errorf("unzip size exceeds the %d bytes limit", unzipSizeLimit) +// newInvalidSlicerNameError defined the error message on receiving the invalid +// slicer name. +func newInvalidSlicerNameError(name string) error { + return fmt.Errorf("invalid slicer name %q", name) } // newInvalidStyleID defined the error message on receiving the invalid style @@ -70,24 +264,6 @@ func newInvalidStyleID(styleID int) error { return fmt.Errorf("invalid style ID %d", styleID) } -// newFieldLengthError defined the error message on receiving the field length -// overflow. -func newFieldLengthError(name string) error { - return fmt.Errorf("field %s must be less than or equal to 255 characters", name) -} - -// newCellNameToCoordinatesError defined the error message on converts -// alphanumeric cell name to coordinates. -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 non existing -// sheet name. -func newNoExistSheetError(name string) error { - return fmt.Errorf("sheet %s does not exist", name) -} - // newNoExistTableError defined the error message on receiving the non existing // table name. func newNoExistTableError(name string) error { @@ -100,174 +276,44 @@ func newNotWorksheetError(name string) error { return fmt.Errorf("sheet %s is not a worksheet", name) } +// newPivotTableDataRangeError defined the error message on receiving the +// invalid pivot table data range. +func newPivotTableDataRangeError(msg string) error { + return fmt.Errorf("parameter 'DataRange' parsing error: %s", msg) +} + +// newPivotTableRangeError defined the error message on receiving the invalid +// pivot table range. +func newPivotTableRangeError(msg string) error { + return fmt.Errorf("parameter 'PivotTableRange' parsing error: %s", msg) +} + // 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) } -// 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) -} - // 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. - 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) - // ErrColumnWidth defined the error message on receive an invalid column - // width. - 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") - // ErrCoordinates defined the error message on invalid coordinates tuples - // length. - ErrCoordinates = errors.New("coordinates length must be 4") - // 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") - // ErrInvalidFormula defined the error message on receive an invalid - // formula. - 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") - // 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 - // height. - 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") - // 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 = 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") - // ErrUnsupportedEncryptMechanism defined the error message on unsupported - // encryption mechanism. - ErrUnsupportedEncryptMechanism = errors.New("unsupported encryption mechanism") - // ErrUnsupportedHashAlgorithm defined the error message on unsupported - // hash algorithm. - ErrUnsupportedHashAlgorithm = errors.New("unsupported hash algorithm") - // ErrUnsupportedNumberFormat defined the error message on unsupported number format - // expression. - ErrUnsupportedNumberFormat = errors.New("unsupported number format token") - // ErrPasswordLengthInvalid defined the error message on invalid password - // length. - ErrPasswordLengthInvalid = errors.New("password length invalid") - // ErrParameterRequired defined the error message on receive the empty - // parameter. - ErrParameterRequired = errors.New("parameter is required") - // ErrParameterInvalid defined the error message on receive the invalid - // parameter. - ErrParameterInvalid = errors.New("parameter is invalid") - // 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 - // 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 - // family name overflow. - 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 - // index. - ErrSheetIdx = errors.New("invalid worksheet index") - // ErrUnprotectSheet defined the error message on worksheet has set no - // protection. - ErrUnprotectSheet = errors.New("worksheet has set no protect") - // ErrUnprotectSheetPassword defined the error message on remove sheet - // protection with password verification failed. - ErrUnprotectSheetPassword = errors.New("worksheet protect password not match") - // ErrGroupSheets defined the error message on group sheets. - 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 = 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") - // ErrCellCharsLength defined the error message for receiving a cell - // characters length that exceeds the limit. - ErrCellCharsLength = fmt.Errorf("cell value must be 0-%d characters", TotalCellChars) - // ErrOptionsUnzipSizeLimit defined the error message for receiving - // invalid UnzipSizeLimit and UnzipXMLSizeLimit. - ErrOptionsUnzipSizeLimit = errors.New("the value of UnzipSizeLimit should be greater than or equal to UnzipXMLSizeLimit") - // ErrSave defined the error message for saving file. - ErrSave = errors.New("no path defined for file, consider File.WriteTo or File.Write") - // ErrAttrValBool defined the error message on marshal and unmarshal - // boolean type XML attribute. - ErrAttrValBool = errors.New("unexpected child of attrValBool") - // ErrSparklineType defined the error message on receive the invalid - // sparkline Type parameters. - ErrSparklineType = errors.New("parameter 'Type' must be 'line', 'column' or 'win_loss'") - // ErrSparklineLocation defined the error message on missing Location - // parameters - ErrSparklineLocation = errors.New("parameter 'Location' is required") - // ErrSparklineRange defined the error message on missing sparkline Range - // parameters - ErrSparklineRange = errors.New("parameter 'Range' is required") - // ErrSparkline defined the error message on receive the invalid sparkline - // parameters. - ErrSparkline = errors.New("must have the same number of 'Location' and 'Range' parameters") - // 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") - // 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) - // 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. - 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") - // ErrUnprotectWorkbookPassword defined the error message on remove workbook - // protection with password verification failed. - ErrUnprotectWorkbookPassword = errors.New("workbook protect password not match") - // ErrorFormControlValue defined the error message for receiving a scroll - // value exceeds limit. - ErrorFormControlValue = fmt.Errorf("scroll value must be between 0 and %d", MaxFormControlValue) -) +// newUnsupportedChartType defined the error message on receiving the chart +// type are unsupported. +func newUnsupportedChartType(chartType ChartType) error { + return fmt.Errorf("unsupported chart type %d", chartType) +} + +// newUnzipSizeLimitError defined the error message on unzip size exceeds the +// limit. +func newUnzipSizeLimitError(unzipSizeLimit int64) error { + return fmt.Errorf("unzip size exceeds the %d bytes limit", unzipSizeLimit) +} + +// 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) +} diff --git a/excelize.go b/excelize.go index a0eaecae11..0f1073fced 100644 --- a/excelize.go +++ b/excelize.go @@ -261,7 +261,7 @@ func (f *File) workSheetReader(sheet string) (ws *xlsxWorksheet, err error) { return } if name, ok = f.getSheetXMLPath(sheet); !ok { - err = newNoExistSheetError(sheet) + err = ErrSheetNotExist{sheet} return } if worksheet, ok := f.Sheet.Load(name); ok && worksheet != nil { diff --git a/excelize_test.go b/excelize_test.go index 6739a7eac3..51d9206c50 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -399,8 +399,8 @@ func TestSetCellHyperLink(t *testing.T) { 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.Equal(t, ErrSheetNameInvalid, f.SetCellHyperLink("Sheet:1", "A1", "Sheet1!D60", "Location")) + assert.Equal(t, newInvalidLinkTypeError(""), f.SetCellHyperLink("Sheet2", "C3", "Sheet1!D8", "")) 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()) @@ -1236,7 +1236,7 @@ func TestConditionalFormat(t *testing.T) { // Test create conditional format with invalid custom number format var exp string _, err = f.NewConditionalStyle(&Style{CustomNumFmt: &exp}) - assert.EqualError(t, err, ErrCustomNumFmt.Error()) + assert.Equal(t, ErrCustomNumFmt, err) // Set conditional format with file without dxfs element should not return error f, err = OpenFile(filepath.Join("test", "Book1.xlsx")) diff --git a/lib.go b/lib.go index 65d5ae04e3..98f48c80d6 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 reference [%d, %d]", col, row) + return "", newCoordinatesToCellNameError(col, row) } sign := "" for _, a := range abs { diff --git a/lib_test.go b/lib_test.go index 5d54eaf9c1..46650e5fc5 100644 --- a/lib_test.go +++ b/lib_test.go @@ -222,9 +222,9 @@ func TestCoordinatesToRangeRef(t *testing.T) { _, err := f.coordinatesToRangeRef([]int{}) assert.EqualError(t, err, ErrCoordinates.Error()) _, err = f.coordinatesToRangeRef([]int{1, -1, 1, 1}) - assert.EqualError(t, err, "invalid cell reference [1, -1]") + assert.Equal(t, newCoordinatesToCellNameError(1, -1), err) _, err = f.coordinatesToRangeRef([]int{1, 1, 1, -1}) - assert.EqualError(t, err, "invalid cell reference [1, -1]") + assert.Equal(t, newCoordinatesToCellNameError(1, -1), err) ref, err := f.coordinatesToRangeRef([]int{1, 1, 1, 1}) assert.NoError(t, err) assert.EqualValues(t, ref, "A1:A1") diff --git a/pivotTable.go b/pivotTable.go index 720f2b193d..5178b35118 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -189,7 +189,7 @@ func (f *File) parseFormatPivotTableSet(opts *PivotTableOptions) (*xlsxWorksheet } pivotTableSheetName, _, err := f.adjustRange(opts.PivotTableRange) if err != nil { - return nil, "", fmt.Errorf("parameter 'PivotTableRange' parsing error: %s", err.Error()) + return nil, "", newPivotTableRangeError(err.Error()) } if len(opts.Name) > MaxFieldLength { return nil, "", ErrNameLength @@ -201,7 +201,7 @@ func (f *File) parseFormatPivotTableSet(opts *PivotTableOptions) (*xlsxWorksheet } dataSheetName, _, err := f.adjustRange(dataRange) if err != nil { - return nil, "", fmt.Errorf("parameter 'DataRange' parsing error: %s", err.Error()) + return nil, "", newPivotTableDataRangeError(err.Error()) } dataSheet, err := f.workSheetReader(dataSheetName) if err != nil { @@ -209,7 +209,7 @@ func (f *File) parseFormatPivotTableSet(opts *PivotTableOptions) (*xlsxWorksheet } pivotTableSheetPath, ok := f.getSheetXMLPath(pivotTableSheetName) if !ok { - return dataSheet, pivotTableSheetPath, fmt.Errorf("sheet %s does not exist", pivotTableSheetName) + return dataSheet, pivotTableSheetPath, ErrSheetNotExist{pivotTableSheetName} } return dataSheet, pivotTableSheetPath, err } @@ -254,7 +254,7 @@ func (f *File) getTableFieldsOrder(sheetName, dataRange string) ([]string, error } dataSheet, coordinates, err := f.adjustRange(ref) if err != nil { - return order, fmt.Errorf("parameter 'DataRange' parsing error: %s", err.Error()) + return order, newPivotTableDataRangeError(err.Error()) } for col := coordinates[0]; col <= coordinates[2]; col++ { coordinate, _ := CoordinatesToCellName(col, coordinates[1]) @@ -278,7 +278,7 @@ func (f *File) addPivotCache(pivotCacheXML string, opts *PivotTableOptions) erro } dataSheet, coordinates, err := f.adjustRange(dataRange) if err != nil { - return fmt.Errorf("parameter 'DataRange' parsing error: %s", err.Error()) + return newPivotTableDataRangeError(err.Error()) } // data range has been checked order, _ := f.getTableFieldsOrder(opts.pivotTableSheetName, opts.DataRange) @@ -320,7 +320,7 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op // validate pivot table range _, coordinates, err := f.adjustRange(opts.PivotTableRange) if err != nil { - return fmt.Errorf("parameter 'PivotTableRange' parsing error: %s", err.Error()) + return newPivotTableRangeError(err.Error()) } hCell, _ := CoordinatesToCellName(coordinates[0], coordinates[1]) @@ -727,7 +727,7 @@ func (f *File) GetPivotTables(sheet string) ([]PivotTableOptions, error) { var pivotTables []PivotTableOptions name, ok := f.getSheetXMLPath(sheet) if !ok { - return pivotTables, newNoExistSheetError(sheet) + return pivotTables, ErrSheetNotExist{sheet} } rels := "xl/worksheets/_rels/" + strings.TrimPrefix(name, "xl/worksheets/") + ".rels" sheetRels, err := f.relsReader(rels) diff --git a/pivotTable_test.go b/pivotTable_test.go index bba445dc77..2cd56f5fea 100644 --- a/pivotTable_test.go +++ b/pivotTable_test.go @@ -182,61 +182,61 @@ func TestPivotTable(t *testing.T) { Name: strings.Repeat("c", MaxFieldLength+1), })) // Test invalid data range - assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{ + assert.Equal(t, newPivotTableDataRangeError("parameter is invalid"), f.AddPivotTable(&PivotTableOptions{ DataRange: "Sheet1!A1:A1", PivotTableRange: "Sheet1!U34:O2", Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, 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(&PivotTableOptions{ + assert.Equal(t, newPivotTableDataRangeError("parameter is invalid"), f.AddPivotTable(&PivotTableOptions{ DataRange: "A1:E31", PivotTableRange: "Sheet1!U34:O2", Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, 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(&PivotTableOptions{ + assert.Equal(t, ErrSheetNotExist{"SheetN"}, f.AddPivotTable(&PivotTableOptions{ DataRange: "SheetN!A1:E31", PivotTableRange: "Sheet1!U34:O2", Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, 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(&PivotTableOptions{ + assert.Equal(t, newPivotTableRangeError("parameter is invalid"), f.AddPivotTable(&PivotTableOptions{ DataRange: "Sheet1!A1:E31", PivotTableRange: "U34:O2", Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, 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(&PivotTableOptions{ + assert.Equal(t, ErrSheetNotExist{"SheetN"}, f.AddPivotTable(&PivotTableOptions{ DataRange: "Sheet1!A1:E31", PivotTableRange: "SheetN!U34:O2", Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, Data: []PivotTableField{{Data: "Sales"}}, - }), "sheet SheetN does not exist") + })) // Test not exists worksheet in data range - assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{ + assert.Equal(t, ErrSheetNotExist{"SheetN"}, f.AddPivotTable(&PivotTableOptions{ DataRange: "SheetN!A1:E31", PivotTableRange: "Sheet1!U34:O2", Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, Data: []PivotTableField{{Data: "Sales"}}, - }), "sheet SheetN does not exist") + })) // Test invalid row number in data range - assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{ + assert.Equal(t, newPivotTableDataRangeError(newCellNameToCoordinatesError("A0", newInvalidCellNameError("A0")).Error()), f.AddPivotTable(&PivotTableOptions{ DataRange: "Sheet1!A0:E31", PivotTableRange: "Sheet1!U34:O2", Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, Data: []PivotTableField{{Data: "Sales"}}, - }), `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(&PivotTableOptions{ diff --git a/rows.go b/rows.go index adb05a2bfd..972707d38e 100644 --- a/rows.go +++ b/rows.go @@ -14,7 +14,6 @@ package excelize import ( "bytes" "encoding/xml" - "fmt" "io" "math" "os" @@ -202,15 +201,6 @@ func appendSpace(l int, s []string) []string { return s } -// 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 does not exist", err.SheetName) -} - // rowXMLIterator defined runtime use field for the worksheet row SAX parser. type rowXMLIterator struct { err error diff --git a/rows_test.go b/rows_test.go index acf50ff9e8..1cba0a75d3 100644 --- a/rows_test.go +++ b/rows_test.go @@ -932,8 +932,7 @@ func TestGetValueFromNumber(t *testing.T) { } func TestErrSheetNotExistError(t *testing.T) { - err := ErrSheetNotExist{SheetName: "Sheet1"} - assert.EqualValues(t, err.Error(), "sheet Sheet1 does not exist") + assert.Equal(t, "sheet Sheet1 does not exist", ErrSheetNotExist{"Sheet1"}.Error()) } func TestCheckRow(t *testing.T) { diff --git a/sheet_test.go b/sheet_test.go index af89c58cc7..935736d75e 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -185,7 +185,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 reference [1, 0]") + assert.Equal(t, newCoordinatesToCellNameError(1, 0), err) assert.Equal(t, []string(nil), result) // Test search sheet with unsupported charset shared strings table diff --git a/stream.go b/stream.go index 13a14d8561..8e72b91aa7 100644 --- a/stream.go +++ b/stream.go @@ -118,7 +118,7 @@ func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) { } sheetID := f.getSheetID(sheet) if sheetID == -1 { - return nil, newNoExistSheetError(sheet) + return nil, ErrSheetNotExist{sheet} } sw := &StreamWriter{ file: f, diff --git a/styles_test.go b/styles_test.go index 9ef6a894df..7858fecdb7 100644 --- a/styles_test.go +++ b/styles_test.go @@ -265,11 +265,11 @@ func TestNewStyle(t *testing.T) { var exp string _, err = f.NewStyle(&Style{CustomNumFmt: &exp}) - assert.EqualError(t, err, ErrCustomNumFmt.Error()) + assert.Equal(t, ErrCustomNumFmt, err) _, err = f.NewStyle(&Style{Font: &Font{Family: strings.Repeat("s", MaxFontFamilyLength+1)}}) - assert.EqualError(t, err, ErrFontLength.Error()) + assert.Equal(t, ErrFontLength, err) _, err = f.NewStyle(&Style{Font: &Font{Size: MaxFontSize + 1}}) - assert.EqualError(t, err, ErrFontSize.Error()) + assert.Equal(t, ErrFontSize, err) // Test create numeric custom style numFmt := "####;####" diff --git a/table.go b/table.go index 32667cd7ca..d12c33b857 100644 --- a/table.go +++ b/table.go @@ -217,7 +217,7 @@ func (f *File) countTables() int { count := 0 f.Pkg.Range(func(k, v interface{}) bool { if strings.Contains(k.(string), "xl/tables/tableSingleCells") { - var cells xlsxSingleXmlCells + var cells xlsxSingleXMLCells if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(v.([]byte)))). Decode(&cells); err != nil && err != io.EOF { count++ @@ -513,12 +513,12 @@ func (f *File) autoFilter(sheet, ref string, columns, col int, opts []AutoFilter } offset := fsCol - col if offset < 0 || offset > columns { - return fmt.Errorf("incorrect index of column '%s'", opt.Column) + return newInvalidAutoFilterColumnError(opt.Column) } fc := &xlsxFilterColumn{ColID: offset} 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) + return newInvalidAutoFilterExpError(opt.Expression) } expressions, tokens, err := f.parseFilterExpression(opt.Expression, token) if err != nil { @@ -647,7 +647,7 @@ func (f *File) parseFilterTokens(expression string, tokens []string) ([]int, str if re { // Only allow Equals or NotEqual in this context. if operator != 2 && operator != 5 { - return []int{operator}, token, fmt.Errorf("the operator '%s' in expression '%s' is not valid in relation to Blanks/NonBlanks'", tokens[1], expression) + return []int{operator}, token, newInvalidAutoFilterOperatorError(tokens[1], expression) } token = strings.ToLower(token) // The operator should always be 2 (=) to flag a "simple" equality in diff --git a/table_test.go b/table_test.go index 994cd72fb1..aa1980cb87 100644 --- a/table_test.go +++ b/table_test.go @@ -48,8 +48,8 @@ func TestAddTable(t *testing.T) { 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]") - assert.EqualError(t, f.addTable("sheet1", "", 1, 1, 0, 0, 0, nil), "invalid cell reference [0, 0]") + assert.Equal(t, newCoordinatesToCellNameError(0, 0), f.addTable("sheet1", "", 0, 0, 0, 0, 0, nil)) + assert.Equal(t, newCoordinatesToCellNameError(0, 0), f.addTable("sheet1", "", 1, 1, 0, 0, 0, nil)) // Test set defined name and add table with invalid name for _, cases := range []struct { name string @@ -132,7 +132,7 @@ func TestDeleteTable(t *testing.T) { func TestSetTableHeader(t *testing.T) { f := NewFile() _, err := f.setTableHeader("Sheet1", true, 1, 0, 1) - assert.EqualError(t, err, "invalid cell reference [1, 0]") + assert.Equal(t, newCoordinatesToCellNameError(1, 0), err) } func TestAutoFilter(t *testing.T) { @@ -190,22 +190,22 @@ func TestAutoFilterError(t *testing.T) { }) } - assert.EqualError(t, f.autoFilter("SheetN", "A1", 1, 1, []AutoFilterOptions{{ + assert.Equal(t, ErrSheetNotExist{"SheetN"}, 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.Equal(t, newInvalidColumnNameError("-"), f.autoFilter("Sheet1", "A1", 1, 1, []AutoFilterOptions{{ Column: "-", Expression: "-", - }}), newInvalidColumnNameError("-").Error()) - assert.EqualError(t, f.autoFilter("Sheet1", "A1", 1, 100, []AutoFilterOptions{{ + }})) + assert.Equal(t, newInvalidAutoFilterColumnError("A"), 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.Equal(t, newInvalidAutoFilterExpError("-"), f.autoFilter("Sheet1", "A1", 1, 1, []AutoFilterOptions{{ Column: "A", Expression: "-", - }}), `incorrect number of tokens in criteria '-'`) + }})) } func TestParseFilterTokens(t *testing.T) { @@ -215,5 +215,5 @@ func TestParseFilterTokens(t *testing.T) { assert.EqualError(t, err, "unknown operator: !") // 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'") + assert.Equal(t, newInvalidAutoFilterOperatorError("<", ""), err) } diff --git a/vml.go b/vml.go index 844149e5c3..866019376d 100644 --- a/vml.go +++ b/vml.go @@ -41,7 +41,7 @@ func (f *File) GetComments(sheet string) ([]Comment, error) { var comments []Comment sheetXMLPath, ok := f.getSheetXMLPath(sheet) if !ok { - return comments, newNoExistSheetError(sheet) + return comments, ErrSheetNotExist{sheet} } commentsXML := f.getSheetComments(filepath.Base(sheetXMLPath)) if !strings.HasPrefix(commentsXML, "/") { @@ -125,7 +125,7 @@ func (f *File) DeleteComment(sheet, cell string) error { } sheetXMLPath, ok := f.getSheetXMLPath(sheet) if !ok { - return newNoExistSheetError(sheet) + return ErrSheetNotExist{sheet} } commentsXML := f.getSheetComments(filepath.Base(sheetXMLPath)) if !strings.HasPrefix(commentsXML, "/") { @@ -738,7 +738,7 @@ func (sp *encodeShape) addFormCtrl(opts *vmlOptions) error { opts.MaxVal > MaxFormControlValue || opts.IncChange > MaxFormControlValue || opts.PageChange > MaxFormControlValue { - return ErrorFormControlValue + return ErrFormControlValue } if opts.CellLink != "" { if _, _, err := CellNameToCoordinates(opts.CellLink); err != nil { diff --git a/vml_test.go b/vml_test.go index 34d7f25d05..5491454c28 100644 --- a/vml_test.go +++ b/vml_test.go @@ -263,9 +263,9 @@ func TestFormControl(t *testing.T) { Cell: "A1", Type: 0x37, Macro: "Button1_Click", }), ErrParameterInvalid) // Test add form control on not exists worksheet - assert.Equal(t, f.AddFormControl("SheetN", FormControl{ + assert.Equal(t, ErrSheetNotExist{"SheetN"}, f.AddFormControl("SheetN", FormControl{ Cell: "A1", Type: FormControlButton, Macro: "Button1_Click", - }), newNoExistSheetError("SheetN")) + })) // Test add form control with invalid positioning types assert.Equal(t, f.AddFormControl("Sheet1", FormControl{ Cell: "A1", Type: FormControlButton, @@ -278,7 +278,7 @@ func TestFormControl(t *testing.T) { // Test add spin form control with invalid scroll value assert.Equal(t, f.AddFormControl("Sheet1", FormControl{ Cell: "C5", Type: FormControlSpinButton, CurrentVal: MaxFormControlValue + 1, - }), ErrorFormControlValue) + }), ErrFormControlValue) assert.NoError(t, f.Close()) // Test delete form control f, err = OpenFile(filepath.Join("test", "TestAddFormControl.xlsm")) @@ -290,7 +290,7 @@ func TestFormControl(t *testing.T) { assert.NoError(t, err) assert.Len(t, result, 9) // Test delete form control on not exists worksheet - assert.Equal(t, f.DeleteFormControl("SheetN", "A1"), newNoExistSheetError("SheetN")) + assert.Equal(t, ErrSheetNotExist{"SheetN"}, f.DeleteFormControl("SheetN", "A1")) // Test delete form control with illegal cell link reference assert.Equal(t, f.DeleteFormControl("Sheet1", "A"), newCellNameToCoordinatesError("A", newInvalidCellNameError("A"))) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDeleteFormControl.xlsm"))) @@ -314,7 +314,7 @@ func TestFormControl(t *testing.T) { assert.NoError(t, err) // Test get form controls on not exists worksheet _, err = f.GetFormControls("SheetN") - assert.Equal(t, err, newNoExistSheetError("SheetN")) + assert.Equal(t, ErrSheetNotExist{"SheetN"}, err) // Test get form controls with unsupported charset VML drawing f, err = OpenFile(filepath.Join("test", "TestAddFormControl.xlsm")) assert.NoError(t, err) diff --git a/xmlTable.go b/xmlTable.go index 7c509ed4e6..ff97df54c2 100644 --- a/xmlTable.go +++ b/xmlTable.go @@ -196,28 +196,28 @@ type xlsxTableStyleInfo struct { ShowColumnStripes bool `xml:"showColumnStripes,attr"` } -// xlsxSingleXmlCells is a single cell table is generated from an XML mapping. +// xlsxSingleXMLCells is a single cell table is generated from an XML mapping. // These really just look like regular cells to the spreadsheet user, but shall // be implemented as Tables "under the covers." -type xlsxSingleXmlCells struct { +type xlsxSingleXMLCells struct { XMLName xml.Name `xml:"singleXmlCells"` - SingleXmlCell []xlsxSingleXmlCell `xml:"singleXmlCell"` + SingleXmlCell []xlsxSingleXMLCell `xml:"singleXmlCell"` } -// xlsxSingleXmlCell is a element represents the table properties for a single +// xlsxSingleXMLCell is a element represents the table properties for a single // cell XML table. -type xlsxSingleXmlCell struct { +type xlsxSingleXMLCell struct { XMLName xml.Name `xml:"singleXmlCell"` ID int `xml:"id,attr"` R string `xml:"r,attr"` ConnectionID int `xml:"connectionId,attr"` - XMLCellPr xlsxXmlCellPr `xml:"xmlCellPr"` + XMLCellPr xlsxXMLCellPr `xml:"xmlCellPr"` ExtLst *xlsxInnerXML `xml:"extLst"` } -// xlsxXmlCellPr is a element stores the XML properties for the cell of a single +// xlsxXMLCellPr is a element stores the XML properties for the cell of a single // cell xml table. -type xlsxXmlCellPr struct { +type xlsxXMLCellPr struct { XMLName xml.Name `xml:"xmlCellPr"` ID int `xml:"id,attr"` UniqueName string `xml:"uniqueName,attr,omitempty"` From f85770f4c908e8ca9b044a17a5266e3b989a4061 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 1 Oct 2023 13:37:47 +0800 Subject: [PATCH 796/957] ref #65, formula functions TEXTAFTER and TEXTBEFORE (array formula not support yet) --- calc.go | 172 ++++++++++++++++++++++++++++++++++++++++++++++++-- calc_test.go | 65 +++++++++++++++++++ pivotTable.go | 2 +- 3 files changed, 231 insertions(+), 8 deletions(-) diff --git a/calc.go b/calc.go index bd8776ecf4..ed5aeaa5ea 100644 --- a/calc.go +++ b/calc.go @@ -314,7 +314,7 @@ func (fa formulaArg) ToBool() formulaArg { return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } case ArgNumber: - if fa.Boolean && fa.Number == 1 { + if fa.Number == 1 { b = true } } @@ -758,6 +758,8 @@ type formulaFuncs struct { // TBILLYIELD // TDIST // TEXT +// TEXTAFTER +// TEXTBEFORE // TEXTJOIN // TIME // TIMEVALUE @@ -13748,7 +13750,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(utf8.RuneCountInString(argsList.Front().Value.(formulaArg).String))) + return newNumberFormulaArg(float64(utf8.RuneCountInString(argsList.Front().Value.(formulaArg).String))) } // LENB returns the number of bytes used to represent the characters in a text @@ -13770,7 +13772,7 @@ func (fn *formulaFuncs) LENB(argsList *list.List) formulaArg { bytes += 2 } } - return newStringFormulaArg(strconv.Itoa(bytes)) + return newNumberFormulaArg(float64(bytes)) } // LOWER converts all characters in a supplied text string to lower case. The @@ -14058,6 +14060,163 @@ func (fn *formulaFuncs) TEXT(argsList *list.List) formulaArg { return newStringFormulaArg(format(value.Value(), fmtText.Value(), false, cellType, nil)) } +// prepareTextAfterBefore checking and prepare arguments for the formula +// functions TEXTAFTER and TEXTBEFORE. +func (fn *formulaFuncs) prepareTextAfterBefore(name string, argsList *list.List) formulaArg { + argsLen := argsList.Len() + if argsLen < 2 { + return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires at least 2 arguments", name)) + } + if argsLen > 6 { + return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s accepts at most 6 arguments", name)) + } + text, delimiter := argsList.Front().Value.(formulaArg), argsList.Front().Next().Value.(formulaArg) + instanceNum, matchMode, matchEnd, ifNotFound := newNumberFormulaArg(1), newBoolFormulaArg(false), newBoolFormulaArg(false), newEmptyFormulaArg() + if argsLen > 2 { + instanceNum = argsList.Front().Next().Next().Value.(formulaArg).ToNumber() + if instanceNum.Type != ArgNumber { + return instanceNum + } + } + if argsLen > 3 { + matchMode = argsList.Front().Next().Next().Next().Value.(formulaArg).ToBool() + if matchMode.Type != ArgNumber { + return matchMode + } + if matchMode.Number == 1 { + text, delimiter = newStringFormulaArg(strings.ToLower(text.Value())), newStringFormulaArg(strings.ToLower(delimiter.Value())) + } + } + if argsLen > 4 { + matchEnd = argsList.Front().Next().Next().Next().Next().Value.(formulaArg).ToBool() + if matchEnd.Type != ArgNumber { + return matchEnd + } + } + if argsLen > 5 { + ifNotFound = argsList.Back().Value.(formulaArg) + } + if text.Value() == "" { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + } + lenArgsList := list.New().Init() + lenArgsList.PushBack(text) + textLen := fn.LEN(lenArgsList) + if instanceNum.Number == 0 || instanceNum.Number > textLen.Number { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + reverseSearch, startPos := instanceNum.Number < 0, 0.0 + if reverseSearch { + startPos = textLen.Number + } + return newListFormulaArg([]formulaArg{ + text, delimiter, instanceNum, matchMode, matchEnd, ifNotFound, + textLen, newBoolFormulaArg(reverseSearch), newNumberFormulaArg(startPos), + }) +} + +// textAfterBeforeSearch is an implementation of the formula functions TEXTAFTER +// and TEXTBEFORE. +func textAfterBeforeSearch(text string, delimiter []string, startPos int, reverseSearch bool) (int, string) { + idx := -1 + var modifiedDelimiter string + for i := 0; i < len(delimiter); i++ { + nextDelimiter := delimiter[i] + nextIdx := strings.Index(text[startPos:], nextDelimiter) + if nextIdx != -1 { + nextIdx += startPos + } + if reverseSearch { + nextIdx = strings.LastIndex(text[:startPos], nextDelimiter) + } + if idx == -1 || (((nextIdx < idx && !reverseSearch) || (nextIdx > idx && reverseSearch)) && idx != -1) { + idx = nextIdx + modifiedDelimiter = nextDelimiter + } + } + return idx, modifiedDelimiter +} + +// textAfterBeforeResult is an implementation of the formula functions TEXTAFTER +// and TEXTBEFORE. +func textAfterBeforeResult(name, modifiedDelimiter string, text []rune, foundIdx, repeatZero, textLen int, matchEndActive, matchEnd, reverseSearch bool) formulaArg { + if name == "TEXTAFTER" { + endPos := len(modifiedDelimiter) + if (repeatZero > 1 || matchEndActive) && matchEnd && reverseSearch { + endPos = 0 + } + if foundIdx+endPos >= textLen { + return newEmptyFormulaArg() + } + return newStringFormulaArg(string(text[foundIdx+endPos : textLen])) + } + return newStringFormulaArg(string(text[:foundIdx])) +} + +// textAfterBefore is an implementation of the formula functions TEXTAFTER and +// TEXTBEFORE. +func (fn *formulaFuncs) textAfterBefore(name string, argsList *list.List) formulaArg { + args := fn.prepareTextAfterBefore(name, argsList) + if args.Type != ArgList { + return args + } + var ( + text = []rune(argsList.Front().Value.(formulaArg).Value()) + modifiedText = args.List[0].Value() + delimiter = []string{args.List[1].Value()} + instanceNum = args.List[2].Number + matchEnd = args.List[4].Number == 1 + ifNotFound = args.List[5] + textLen = args.List[6] + reverseSearch = args.List[7].Number == 1 + foundIdx = -1 + repeatZero, startPos int + matchEndActive bool + modifiedDelimiter string + ) + if reverseSearch { + startPos = int(args.List[8].Number) + } + for i := 0; i < int(math.Abs(instanceNum)); i++ { + foundIdx, modifiedDelimiter = textAfterBeforeSearch(modifiedText, delimiter, startPos, reverseSearch) + if foundIdx == 0 { + repeatZero++ + } + if foundIdx == -1 { + if matchEnd && i == int(math.Abs(instanceNum))-1 { + if foundIdx = int(textLen.Number); reverseSearch { + foundIdx = 0 + } + matchEndActive = true + } + break + } + if startPos = foundIdx + len(modifiedDelimiter); reverseSearch { + startPos = foundIdx - len(modifiedDelimiter) + } + } + if foundIdx == -1 { + return ifNotFound + } + return textAfterBeforeResult(name, modifiedDelimiter, text, foundIdx, repeatZero, int(textLen.Number), matchEndActive, matchEnd, reverseSearch) +} + +// TEXTAFTER function returns the text that occurs after a given substring or +// delimiter. The syntax of the function is: +// +// TEXTAFTER(text,delimiter,[instance_num],[match_mode],[match_end],[if_not_found]) +func (fn *formulaFuncs) TEXTAFTER(argsList *list.List) formulaArg { + return fn.textAfterBefore("TEXTAFTER", argsList) +} + +// TEXTBEFORE function returns text that occurs before a given character or +// string. The syntax of the function is: +// +// TEXTBEFORE(text,delimiter,[instance_num],[match_mode],[match_end],[if_not_found]) +func (fn *formulaFuncs) TEXTBEFORE(argsList *list.List) formulaArg { + return fn.textAfterBefore("TEXTBEFORE", argsList) +} + // TEXTJOIN function joins together a series of supplied text strings into one // combined text string. The user can specify a delimiter to add between the // individual text items, if required. The syntax of the function is: @@ -14465,8 +14624,7 @@ func compareFormulaArgMatrix(lhs, rhs, matchMode formulaArg, caseSensitive bool) return criteriaG } for i := range lhs.Matrix { - left := lhs.Matrix[i] - right := lhs.Matrix[i] + left, right := lhs.Matrix[i], rhs.Matrix[i] if len(left) < len(right) { return criteriaL } @@ -15289,7 +15447,7 @@ func (fn *formulaFuncs) ROWS(argsList *list.List) formulaArg { } min, max := calcColsRowsMinMax(false, argsList) if max == TotalRows { - return newStringFormulaArg(strconv.Itoa(TotalRows)) + return newNumberFormulaArg(TotalRows) } result := max - min + 1 if max == min { @@ -15298,7 +15456,7 @@ func (fn *formulaFuncs) ROWS(argsList *list.List) formulaArg { } return newNumberFormulaArg(float64(1)) } - return newStringFormulaArg(strconv.Itoa(result)) + return newNumberFormulaArg(float64(result)) } // Web Functions diff --git a/calc_test.go b/calc_test.go index 0b08db1ac7..4bb8660bbc 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1843,6 +1843,35 @@ func TestCalcCellValue(t *testing.T) { "=TEXT(567.9,\"$#,##0.00\")": "$567.90", "=TEXT(-5,\"+ $#,##0.00;- $#,##0.00;$0.00\")": "- $5.00", "=TEXT(5,\"+ $#,##0.00;- $#,##0.00;$0.00\")": "+ $5.00", + // TEXTAFTER + "=TEXTAFTER(\"Red riding hood's, red hood\",\"hood\")": "'s, red hood", + "=TEXTAFTER(\"Red riding hood's, red hood\",\"HOOD\",1,1)": "'s, red hood", + "=TEXTAFTER(\"Red riding hood's, red hood\",\"basket\",1,0,0,\"x\")": "x", + "=TEXTAFTER(\"Red riding hood's, red hood\",\"basket\",1,0,1,\"x\")": "", + "=TEXTAFTER(\"Red riding hood's, red hood\",\"hood\",-1)": "", + "=TEXTAFTER(\"Jones,Bob\",\",\")": "Bob", + "=TEXTAFTER(\"12 ft x 20 ft\",\" x \")": "20 ft", + "=TEXTAFTER(\"ABX-112-Red-Y\",\"-\",1)": "112-Red-Y", + "=TEXTAFTER(\"ABX-112-Red-Y\",\"-\",2)": "Red-Y", + "=TEXTAFTER(\"ABX-112-Red-Y\",\"-\",-1)": "Y", + "=TEXTAFTER(\"ABX-112-Red-Y\",\"-\",-2)": "Red-Y", + "=TEXTAFTER(\"ABX-112-Red-Y\",\"-\",-3)": "112-Red-Y", + "=TEXTAFTER(\"ABX-123-Red-XYZ\",\"-\",-4,0,1)": "ABX-123-Red-XYZ", + "=TEXTAFTER(\"ABX-123-Red-XYZ\",\"A\")": "BX-123-Red-XYZ", + // TEXTBEFORE + "=TEXTBEFORE(\"Red riding hood's, red hood\",\"hood\")": "Red riding ", + "=TEXTBEFORE(\"Red riding hood's, red hood\",\"HOOD\",1,1)": "Red riding ", + "=TEXTBEFORE(\"Red riding hood's, red hood\",\"basket\",1,0,0,\"x\")": "x", + "=TEXTBEFORE(\"Red riding hood's, red hood\",\"basket\",1,0,1,\"x\")": "Red riding hood's, red hood", + "=TEXTBEFORE(\"Red riding hood's, red hood\",\"hood\",-1)": "Red riding hood's, red ", + "=TEXTBEFORE(\"Jones,Bob\",\",\")": "Jones", + "=TEXTBEFORE(\"12 ft x 20 ft\",\" x \")": "12 ft", + "=TEXTBEFORE(\"ABX-112-Red-Y\",\"-\",1)": "ABX", + "=TEXTBEFORE(\"ABX-112-Red-Y\",\"-\",2)": "ABX-112", + "=TEXTBEFORE(\"ABX-112-Red-Y\",\"-\",-1)": "ABX-112-Red", + "=TEXTBEFORE(\"ABX-112-Red-Y\",\"-\",-2)": "ABX-112", + "=TEXTBEFORE(\"ABX-123-Red-XYZ\",\"-\",4,0,1)": "ABX-123-Red-XYZ", + "=TEXTBEFORE(\"ABX-112-Red-Y\",\"A\")": "", // TEXTJOIN "=TEXTJOIN(\"-\",TRUE,1,2,3,4)": "1-2-3-4", "=TEXTJOIN(A4,TRUE,A1:B2)": "1040205", @@ -3879,6 +3908,24 @@ func TestCalcCellValue(t *testing.T) { "=TEXT()": {"#VALUE!", "TEXT requires 2 arguments"}, "=TEXT(NA(),\"\")": {"#N/A", "#N/A"}, "=TEXT(0,NA())": {"#N/A", "#N/A"}, + // TEXTAFTER + "=TEXTAFTER()": {"#VALUE!", "TEXTAFTER requires at least 2 arguments"}, + "=TEXTAFTER(\"Red riding hood's, red hood\",\"hood\",1,0,0,\"\",0)": {"#VALUE!", "TEXTAFTER accepts at most 6 arguments"}, + "=TEXTAFTER(\"Red riding hood's, red hood\",\"hood\",\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=TEXTAFTER(\"Red riding hood's, red hood\",\"hood\",1,\"\")": {"#VALUE!", "strconv.ParseBool: parsing \"\": invalid syntax"}, + "=TEXTAFTER(\"Red riding hood's, red hood\",\"hood\",1,0,\"\")": {"#VALUE!", "strconv.ParseBool: parsing \"\": invalid syntax"}, + "=TEXTAFTER(\"\",\"hood\")": {"#N/A", "#N/A"}, + "=TEXTAFTER(\"Red riding hood's, red hood\",\"hood\",0)": {"#VALUE!", "#VALUE!"}, + "=TEXTAFTER(\"Red riding hood's, red hood\",\"hood\",28)": {"#VALUE!", "#VALUE!"}, + // TEXTBEFORE + "=TEXTBEFORE()": {"#VALUE!", "TEXTBEFORE requires at least 2 arguments"}, + "=TEXTBEFORE(\"Red riding hood's, red hood\",\"hood\",1,0,0,\"\",0)": {"#VALUE!", "TEXTBEFORE accepts at most 6 arguments"}, + "=TEXTBEFORE(\"Red riding hood's, red hood\",\"hood\",\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "=TEXTBEFORE(\"Red riding hood's, red hood\",\"hood\",1,\"\")": {"#VALUE!", "strconv.ParseBool: parsing \"\": invalid syntax"}, + "=TEXTBEFORE(\"Red riding hood's, red hood\",\"hood\",1,0,\"\")": {"#VALUE!", "strconv.ParseBool: parsing \"\": invalid syntax"}, + "=TEXTBEFORE(\"\",\"hood\")": {"#N/A", "#N/A"}, + "=TEXTBEFORE(\"Red riding hood's, red hood\",\"hood\",0)": {"#VALUE!", "#VALUE!"}, + "=TEXTBEFORE(\"Red riding hood's, red hood\",\"hood\",28)": {"#VALUE!", "#VALUE!"}, // TEXTJOIN "=TEXTJOIN()": {"#VALUE!", "TEXTJOIN requires at least 3 arguments"}, "=TEXTJOIN(\"\",\"\",1)": {"#VALUE!", "#VALUE!"}, @@ -4739,9 +4786,27 @@ func TestCalcCompareFormulaArg(t *testing.T) { rhs = newListFormulaArg([]formulaArg{newBoolFormulaArg(true)}) assert.Equal(t, compareFormulaArg(lhs, rhs, newNumberFormulaArg(matchModeMaxLess), false), criteriaEq) + lhs = newListFormulaArg([]formulaArg{newNumberFormulaArg(1)}) + rhs = newListFormulaArg([]formulaArg{newNumberFormulaArg(0)}) + assert.Equal(t, compareFormulaArg(lhs, rhs, newNumberFormulaArg(matchModeMaxLess), false), criteriaG) + assert.Equal(t, compareFormulaArg(formulaArg{Type: ArgUnknown}, formulaArg{Type: ArgUnknown}, newNumberFormulaArg(matchModeMaxLess), false), criteriaErr) } +func TestCalcCompareFormulaArgMatrix(t *testing.T) { + lhs := newMatrixFormulaArg([][]formulaArg{{newEmptyFormulaArg()}}) + rhs := newMatrixFormulaArg([][]formulaArg{{newEmptyFormulaArg(), newEmptyFormulaArg()}}) + assert.Equal(t, compareFormulaArgMatrix(lhs, rhs, newNumberFormulaArg(matchModeMaxLess), false), criteriaL) + + lhs = newMatrixFormulaArg([][]formulaArg{{newEmptyFormulaArg(), newEmptyFormulaArg()}}) + rhs = newMatrixFormulaArg([][]formulaArg{{newEmptyFormulaArg()}}) + assert.Equal(t, compareFormulaArgMatrix(lhs, rhs, newNumberFormulaArg(matchModeMaxLess), false), criteriaG) + + lhs = newMatrixFormulaArg([][]formulaArg{{newNumberFormulaArg(1)}}) + rhs = newMatrixFormulaArg([][]formulaArg{{newNumberFormulaArg(0)}}) + assert.Equal(t, compareFormulaArgMatrix(lhs, rhs, newNumberFormulaArg(matchModeMaxLess), false), criteriaG) +} + func TestCalcTRANSPOSE(t *testing.T) { cellData := [][]interface{}{ {"a", "d"}, diff --git a/pivotTable.go b/pivotTable.go index 5178b35118..7d1be37fdc 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -343,7 +343,7 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op UseAutoFormatting: &opts.UseAutoFormatting, PageOverThenDown: &opts.PageOverThenDown, MergeItem: &opts.MergeItem, - CreatedVersion: 3, + CreatedVersion: pivotTableVersion, CompactData: &opts.CompactData, ShowError: &opts.ShowError, DataCaption: "Values", From 1c7c417c7067ca96844dd7dd4da2d61369c901b4 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 2 Oct 2023 00:06:38 +0800 Subject: [PATCH 797/957] This closes #1677, fix the incorrect custom number format ID allocated - Improve compatibility with empty custom number format code --- styles.go | 22 ++++++++++------------ xmlStyles.go | 2 +- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/styles.go b/styles.go index 1de83bcb03..40505b7b9e 100644 --- a/styles.go +++ b/styles.go @@ -1694,20 +1694,18 @@ func newNumFmt(styleSheet *xlsxStyleSheet, style *Style) int { // setCustomNumFmt provides a function to set custom number format code. func setCustomNumFmt(styleSheet *xlsxStyleSheet, style *Style) int { - nf := xlsxNumFmt{FormatCode: *style.CustomNumFmt} - - 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 = 164 - numFmts := xlsxNumFmts{ - NumFmt: []*xlsxNumFmt{&nf}, - Count: 1, + nf := xlsxNumFmt{NumFmtID: 163, FormatCode: *style.CustomNumFmt} + if styleSheet.NumFmts == nil { + styleSheet.NumFmts = &xlsxNumFmts{} + } + for _, numFmt := range styleSheet.NumFmts.NumFmt { + if numFmt != nil && nf.NumFmtID < numFmt.NumFmtID { + nf.NumFmtID = numFmt.NumFmtID } - styleSheet.NumFmts = &numFmts } + nf.NumFmtID++ + styleSheet.NumFmts.NumFmt = append(styleSheet.NumFmts.NumFmt, &nf) + styleSheet.NumFmts.Count = len(styleSheet.NumFmts.NumFmt) return nf.NumFmtID } diff --git a/xmlStyles.go b/xmlStyles.go index 3dd61c3307..613001c8e4 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -296,7 +296,7 @@ type xlsxNumFmts struct { // of a cell. type xlsxNumFmt struct { NumFmtID int `xml:"numFmtId,attr"` - FormatCode string `xml:"formatCode,attr,omitempty"` + FormatCode string `xml:"formatCode,attr"` FormatCode16 string `xml:"http://schemas.microsoft.com/office/spreadsheetml/2015/02/main formatCode16,attr,omitempty"` } From 0861faf2f2f0de9e86d63f10546819c5993bab05 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 3 Oct 2023 00:59:31 +0800 Subject: [PATCH 798/957] Add new exported function `DeletePivotTable` - Support adding pivot table by specific table name - Update unit tests --- pivotTable.go | 147 +++++++++++++++++++++++++++++++++++++++------ pivotTable_test.go | 119 +++++++++++++++++++++++++++++++++++- sheet.go | 2 +- workbook.go | 20 ++++++ 4 files changed, 266 insertions(+), 22 deletions(-) diff --git a/pivotTable.go b/pivotTable.go index 7d1be37fdc..10ef955433 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -157,20 +157,18 @@ func (f *File) AddPivotTable(opts *PivotTableOptions) 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, opts) - if err != nil { + if err = f.addPivotCache(pivotCacheXML, opts); err != nil { return err } // workbook pivot cache - workBookPivotCacheRID := f.addRels(f.getWorkbookRelsPath(), SourceRelationshipPivotCache, fmt.Sprintf("/xl/pivotCache/pivotCacheDefinition%d.xml", pivotCacheID), "") + workBookPivotCacheRID := f.addRels(f.getWorkbookRelsPath(), SourceRelationshipPivotCache, strings.TrimPrefix(pivotCacheXML, "xl/"), "") cacheID := f.addWorkbookPivotCache(workBookPivotCacheRID) 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, opts) - if err != nil { + if err = f.addPivotTable(cacheID, pivotTableID, pivotTableXML, opts); err != nil { return err } pivotTableSheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(pivotTableSheetPath, "xl/worksheets/") + ".rels" @@ -195,11 +193,14 @@ func (f *File) parseFormatPivotTableSet(opts *PivotTableOptions) (*xlsxWorksheet return nil, "", ErrNameLength } opts.pivotTableSheetName = pivotTableSheetName - dataRange := f.getDefinedNameRefTo(opts.DataRange, pivotTableSheetName) - if dataRange == "" { - dataRange = opts.DataRange + _, dataRangeRef, err := f.getPivotTableDataRange(pivotTableSheetName, opts.DataRange, opts.DataRange) + if err != nil { + return nil, "", err } - dataSheetName, _, err := f.adjustRange(dataRange) + if dataRangeRef == "" { + dataRangeRef = opts.DataRange + } + dataSheetName, _, err := f.adjustRange(dataRangeRef) if err != nil { return nil, "", newPivotTableDataRangeError(err.Error()) } @@ -248,11 +249,17 @@ func (f *File) adjustRange(rangeStr string) (string, []int, error) { // fields. func (f *File) getTableFieldsOrder(sheetName, dataRange string) ([]string, error) { var order []string - ref := f.getDefinedNameRefTo(dataRange, sheetName) - if ref == "" { - ref = dataRange + if dataRange == "" { + return order, newPivotTableDataRangeError(ErrParameterRequired.Error()) } - dataSheet, coordinates, err := f.adjustRange(ref) + _, dataRangeRef, err := f.getPivotTableDataRange(sheetName, dataRange, dataRange) + if err != nil { + return order, err + } + if dataRangeRef == "" { + dataRangeRef = dataRange + } + dataSheet, coordinates, err := f.adjustRange(dataRangeRef) if err != nil { return order, newPivotTableDataRangeError(err.Error()) } @@ -271,17 +278,20 @@ func (f *File) getTableFieldsOrder(sheetName, dataRange string) ([]string, error func (f *File) addPivotCache(pivotCacheXML string, opts *PivotTableOptions) error { // validate data range definedNameRef := true - dataRange := f.getDefinedNameRefTo(opts.DataRange, opts.pivotTableSheetName) - if dataRange == "" { + _, dataRangeRef, err := f.getPivotTableDataRange(opts.pivotTableSheetName, opts.DataRange, opts.DataRange) + if err != nil { + return err + } + if dataRangeRef == "" { definedNameRef = false - dataRange = opts.DataRange + dataRangeRef = opts.DataRange } - dataSheet, coordinates, err := f.adjustRange(dataRange) + dataSheet, coordinates, err := f.adjustRange(dataRangeRef) if err != nil { return newPivotTableDataRangeError(err.Error()) } // data range has been checked - order, _ := f.getTableFieldsOrder(opts.pivotTableSheetName, opts.DataRange) + order, _ := f.getTableFieldsOrder(opts.pivotTableSheetName, dataRangeRef) hCell, _ := CoordinatesToCellName(coordinates[0], coordinates[1]) vCell, _ := CoordinatesToCellName(coordinates[2], coordinates[3]) pc := xlsxPivotCacheDefinition{ @@ -751,6 +761,32 @@ func (f *File) GetPivotTables(sheet string) ([]PivotTableOptions, error) { return pivotTables, nil } +// getPivotTableDataRange returns pivot table data range name and reference from +// cell reference, table name or defined name. +func (f *File) getPivotTableDataRange(sheet, ref, name string) (string, string, error) { + dataRange := fmt.Sprintf("%s!%s", sheet, ref) + dataRangeRef, isTable := dataRange, false + if name != "" { + dataRange = name + for _, sheetName := range f.GetSheetList() { + tables, err := f.GetTables(sheetName) + e := ErrSheetNotExist{sheetName} + if err != nil && err.Error() != newNotWorksheetError(sheetName).Error() && err.Error() != e.Error() { + return dataRange, dataRangeRef, err + } + for _, table := range tables { + if table.Name == name { + dataRangeRef, isTable = fmt.Sprintf("%s!%s", sheetName, table.Range), true + } + } + } + if !isTable { + dataRangeRef = f.getDefinedNameRefTo(name, sheet) + } + } + return dataRange, dataRangeRef, nil +} + // getPivotTable provides a function to get a pivot table definition by given // worksheet name, pivot table XML path and pivot cache relationship XML path. func (f *File) getPivotTable(sheet, pivotTableXML, pivotCacheRels string) (PivotTableOptions, error) { @@ -774,7 +810,10 @@ func (f *File) getPivotTable(sheet, pivotTableXML, pivotCacheRels string) (Pivot if err != nil { return opts, err } - dataRange := fmt.Sprintf("%s!%s", pc.CacheSource.WorksheetSource.Sheet, pc.CacheSource.WorksheetSource.Ref) + dataRange, dataRangeRef, err := f.getPivotTableDataRange(sheet, pc.CacheSource.WorksheetSource.Ref, pc.CacheSource.WorksheetSource.Name) + if err != nil { + return opts, err + } opts = PivotTableOptions{ pivotTableXML: pivotTableXML, pivotCacheXML: pivotCacheXML, @@ -799,7 +838,7 @@ func (f *File) getPivotTable(sheet, pivotTableXML, pivotCacheRels string) (Pivot opts.ShowLastColumn = si.ShowLastColumn opts.PivotTableStyleName = si.Name } - order, _ := f.getTableFieldsOrder(pt.Name, dataRange) + order, err := f.getTableFieldsOrder(pt.Name, dataRangeRef) f.extractPivotTableFields(order, pt, &opts) return opts, err } @@ -906,3 +945,71 @@ func (f *File) genPivotCacheDefinitionID() int { }) return ID + 1 } + +// deleteWorkbookPivotCache remove workbook pivot cache and pivot cache +// relationships. +func (f *File) deleteWorkbookPivotCache(opt PivotTableOptions) error { + rID, err := f.deleteWorkbookRels(SourceRelationshipPivotCache, strings.TrimPrefix(strings.TrimPrefix(opt.pivotCacheXML, "/"), "xl/")) + if err != nil { + return err + } + wb, err := f.workbookReader() + if err != nil { + return err + } + if wb.PivotCaches != nil { + for i, pivotCache := range wb.PivotCaches.PivotCache { + if pivotCache.RID == rID { + wb.PivotCaches.PivotCache = append(wb.PivotCaches.PivotCache[:i], wb.PivotCaches.PivotCache[i+1:]...) + } + } + if len(wb.PivotCaches.PivotCache) == 0 { + wb.PivotCaches = nil + } + } + return err +} + +// DeletePivotTable delete a pivot table by giving the worksheet name and pivot +// table name. Note that this function does not clean cell values in the pivot +// table range. +func (f *File) DeletePivotTable(sheet, name string) error { + sheetXML, ok := f.getSheetXMLPath(sheet) + if !ok { + return ErrSheetNotExist{sheet} + } + rels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetXML, "xl/worksheets/") + ".rels" + sheetRels, err := f.relsReader(rels) + if err != nil { + return err + } + if sheetRels == nil { + sheetRels = &xlsxRelationships{} + } + opts, err := f.GetPivotTables(sheet) + if err != nil { + return err + } + pivotTableCaches := map[string]int{} + for _, sheetName := range f.GetSheetList() { + sheetPivotTables, _ := f.GetPivotTables(sheetName) + for _, sheetPivotTable := range sheetPivotTables { + pivotTableCaches[sheetPivotTable.pivotCacheXML]++ + } + } + for _, v := range sheetRels.Relationships { + for _, opt := range opts { + if v.Type == SourceRelationshipPivotTable { + pivotTableXML := strings.ReplaceAll(v.Target, "..", "xl") + if opt.Name == name && opt.pivotTableXML == pivotTableXML { + if pivotTableCaches[opt.pivotCacheXML] == 1 { + err = f.deleteWorkbookPivotCache(opt) + } + f.deleteSheetRelationships(sheet, v.ID) + return err + } + } + } + } + return newNoExistTableError(name) +} diff --git a/pivotTable_test.go b/pivotTable_test.go index 2cd56f5fea..ec3384a7d7 100644 --- a/pivotTable_test.go +++ b/pivotTable_test.go @@ -246,6 +246,14 @@ func TestPivotTable(t *testing.T) { Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, Data: []PivotTableField{{Data: "Sales", Subtotal: "-", Name: strings.Repeat("s", MaxFieldLength+1)}}, })) + // Test delete pivot table + pivotTables, err = f.GetPivotTables("Sheet1") + assert.Len(t, pivotTables, 7) + assert.NoError(t, err) + assert.NoError(t, f.DeletePivotTable("Sheet1", "PivotTable1")) + pivotTables, err = f.GetPivotTables("Sheet1") + assert.Len(t, pivotTables, 6) + assert.NoError(t, err) // Test add pivot table with invalid sheet name assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{ @@ -253,6 +261,10 @@ func TestPivotTable(t *testing.T) { PivotTableRange: "Sheet:1!G2:M34", Rows: []PivotTableField{{Data: "Year"}}, }), ErrSheetNameInvalid.Error()) + // Test delete pivot table with not exists worksheet + assert.EqualError(t, f.DeletePivotTable("SheetN", "PivotTable1"), "sheet SheetN does not exist") + // Test delete pivot table with not exists pivot table name + assert.EqualError(t, f.DeletePivotTable("Sheet1", "PivotTableN"), "table PivotTableN does not exist") // Test adjust range with invalid range _, _, err = f.adjustRange("") assert.EqualError(t, err, ErrParameterRequired.Error()) @@ -263,7 +275,7 @@ func TestPivotTable(t *testing.T) { _, err = f.getTableFieldsOrder("", "") assert.EqualError(t, err, `parameter 'DataRange' parsing error: parameter is required`) // Test add pivot cache with empty data range - assert.EqualError(t, f.addPivotCache("", &PivotTableOptions{}), "parameter 'DataRange' parsing error: parameter is required") + assert.EqualError(t, f.addPivotCache("", &PivotTableOptions{}), "parameter 'DataRange' parsing error: parameter is invalid") // Test add pivot cache with invalid data range assert.EqualError(t, f.addPivotCache("", &PivotTableOptions{ DataRange: "A1:E31", @@ -334,6 +346,89 @@ func TestPivotTable(t *testing.T) { assert.NoError(t, f.Close()) } +func TestPivotTableDataRange(t *testing.T) { + f := NewFile() + // Create table in a worksheet + assert.NoError(t, f.AddTable("Sheet1", &Table{ + Name: "Table1", + Range: "A1:D5", + })) + for row := 2; row < 6; row++ { + assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("A%d", row), rand.Intn(10))) + assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("B%d", row), rand.Intn(10))) + assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("C%d", row), rand.Intn(10))) + assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("D%d", row), rand.Intn(10))) + } + // Test add pivot table with table data range + opts := PivotTableOptions{ + DataRange: "Table1", + PivotTableRange: "Sheet1!G2:K7", + Rows: []PivotTableField{{Data: "Column1"}}, + Columns: []PivotTableField{{Data: "Column2"}}, + RowGrandTotals: true, + ColGrandTotals: true, + ShowDrill: true, + ShowRowHeaders: true, + ShowColHeaders: true, + ShowLastColumn: true, + ShowError: true, + PivotTableStyleName: "PivotStyleLight16", + } + assert.NoError(t, f.AddPivotTable(&opts)) + assert.NoError(t, f.DeletePivotTable("Sheet1", "PivotTable1")) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddPivotTable2.xlsx"))) + assert.NoError(t, f.Close()) + + assert.NoError(t, f.AddPivotTable(&opts)) + + // Test delete pivot table with unsupported table relationships charset + f.Pkg.Store("xl/tables/table1.xml", MacintoshCyrillicCharset) + assert.EqualError(t, f.DeletePivotTable("Sheet1", "PivotTable1"), "XML syntax error on line 1: invalid UTF-8") + + // Test delete pivot table with unsupported worksheet relationships charset + f.Relationships.Delete("xl/worksheets/_rels/sheet1.xml.rels") + f.Pkg.Store("xl/worksheets/_rels/sheet1.xml.rels", MacintoshCyrillicCharset) + assert.EqualError(t, f.DeletePivotTable("Sheet1", "PivotTable1"), "XML syntax error on line 1: invalid UTF-8") + + // Test delete pivot table without worksheet relationships + f.Relationships.Delete("xl/worksheets/_rels/sheet1.xml.rels") + f.Pkg.Delete("xl/worksheets/_rels/sheet1.xml.rels") + assert.EqualError(t, f.DeletePivotTable("Sheet1", "PivotTable1"), "table PivotTable1 does not exist") +} + +func TestParseFormatPivotTableSet(t *testing.T) { + f := NewFile() + // Create table in a worksheet + assert.NoError(t, f.AddTable("Sheet1", &Table{ + Name: "Table1", + Range: "A1:D5", + })) + // Test parse format pivot table options with unsupported table relationships charset + f.Pkg.Store("xl/tables/table1.xml", MacintoshCyrillicCharset) + _, _, err := f.parseFormatPivotTableSet(&PivotTableOptions{ + DataRange: "Table1", + PivotTableRange: "Sheet1!G2:K7", + Rows: []PivotTableField{{Data: "Column1"}}, + }) + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") +} + +func TestAddPivotCache(t *testing.T) { + f := NewFile() + // Create table in a worksheet + assert.NoError(t, f.AddTable("Sheet1", &Table{ + Name: "Table1", + Range: "A1:D5", + })) + // Test add pivot table cache with unsupported table relationships charset + f.Pkg.Store("xl/tables/table1.xml", MacintoshCyrillicCharset) + assert.EqualError(t, f.addPivotCache("xl/pivotCache/pivotCacheDefinition1.xml", &PivotTableOptions{ + DataRange: "Table1", + PivotTableRange: "Sheet1!G2:K7", + Rows: []PivotTableField{{Data: "Column1"}}, + }), "XML syntax error on line 1: invalid UTF-8") +} + func TestAddPivotRowFields(t *testing.T) { f := NewFile() // Test invalid data range @@ -372,6 +467,15 @@ func TestGetPivotFieldsOrder(t *testing.T) { // Test get table fields order with not exist worksheet _, err := f.getTableFieldsOrder("", "SheetN!A1:E31") assert.EqualError(t, err, "sheet SheetN does not exist") + // Create table in a worksheet + assert.NoError(t, f.AddTable("Sheet1", &Table{ + Name: "Table1", + Range: "A1:D5", + })) + // Test get table fields order with unsupported table relationships charset + f.Pkg.Store("xl/tables/table1.xml", MacintoshCyrillicCharset) + _, err = f.getTableFieldsOrder("Sheet1", "Table") + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") } func TestGetPivotTableFieldName(t *testing.T) { @@ -392,3 +496,16 @@ func TestGenPivotCacheDefinitionID(t *testing.T) { assert.Equal(t, 1, f.genPivotCacheDefinitionID()) assert.NoError(t, f.Close()) } + +func TestDeleteWorkbookPivotCache(t *testing.T) { + f := NewFile() + // Test delete workbook pivot table cache with unsupported workbook charset + f.WorkBook = nil + f.Pkg.Store("xl/workbook.xml", MacintoshCyrillicCharset) + assert.EqualError(t, f.deleteWorkbookPivotCache(PivotTableOptions{pivotCacheXML: "pivotCache/pivotCacheDefinition1.xml"}), "XML syntax error on line 1: invalid UTF-8") + + // Test delete workbook pivot table cache with unsupported workbook relationships charset + f.Relationships.Delete("xl/_rels/workbook.xml.rels") + f.Pkg.Store("xl/_rels/workbook.xml.rels", MacintoshCyrillicCharset) + assert.EqualError(t, f.deleteWorkbookPivotCache(PivotTableOptions{pivotCacheXML: "pivotCache/pivotCacheDefinition1.xml"}), "XML syntax error on line 1: invalid UTF-8") +} diff --git a/sheet.go b/sheet.go index 00977ec389..23ee77ec05 100644 --- a/sheet.go +++ b/sheet.go @@ -1864,7 +1864,7 @@ 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. +// after deserialization of relationships parts. func (f *File) relsReader(path string) (*xlsxRelationships, error) { rels, _ := f.Relationships.Load(path) if rels == nil { diff --git a/workbook.go b/workbook.go index c560d5e30a..2810018c37 100644 --- a/workbook.go +++ b/workbook.go @@ -170,6 +170,26 @@ func (f *File) getWorkbookRelsPath() (path string) { return } +// deleteWorkbookRels provides a function to delete relationships in +// xl/_rels/workbook.xml.rels by given type and target. +func (f *File) deleteWorkbookRels(relType, relTarget string) (string, error) { + var rID string + rels, err := f.relsReader(f.getWorkbookRelsPath()) + if err != nil { + return rID, err + } + if rels == nil { + rels = &xlsxRelationships{} + } + for k, v := range rels.Relationships { + if v.Type == relType && v.Target == relTarget { + rID = v.ID + rels.Relationships = append(rels.Relationships[:k], rels.Relationships[k+1:]...) + } + } + return rID, err +} + // workbookReader provides a function to get the pointer to the workbook.xml // structure after deserialization. func (f *File) workbookReader() (*xlsxWorkbook, error) { From ecb4f62b776aa7c5659e0752785ecf91b6a334a3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Oct 2023 00:11:50 +0800 Subject: [PATCH 799/957] Update actions/checkout from 3 to 4 (#1676) --- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/go.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index c62270a94d..e457ab7e03 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -20,7 +20,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 9a1633c3ae..75b6172c3c 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -19,7 +19,7 @@ jobs: go-version: ${{ matrix.go-version }} - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Get dependencies run: | From df032fcae7d4839de5b72f19beabfd3cab64682a Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 5 Oct 2023 00:08:31 +0800 Subject: [PATCH 800/957] This improves performance for adding and removing pivot table --- pivotTable.go | 139 ++++++++++++++++++++++----------------------- pivotTable_test.go | 36 ++---------- slicer.go | 2 +- workbook_test.go | 10 ++++ 4 files changed, 84 insertions(+), 103 deletions(-) diff --git a/pivotTable.go b/pivotTable.go index 10ef955433..9512c27a94 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -35,7 +35,9 @@ import ( type PivotTableOptions struct { pivotTableXML string pivotCacheXML string - pivotTableSheetName string + pivotSheetName string + pivotDataRange string + namedDataRange bool DataRange string PivotTableRange string Name string @@ -155,20 +157,20 @@ func (f *File) AddPivotTable(opts *PivotTableOptions) error { pivotCacheID := f.countPivotCache() + 1 sheetRelationshipsPivotTableXML := "../pivotTables/pivotTable" + strconv.Itoa(pivotTableID) + ".xml" - pivotTableXML := strings.ReplaceAll(sheetRelationshipsPivotTableXML, "..", "xl") - pivotCacheXML := "xl/pivotCache/pivotCacheDefinition" + strconv.Itoa(pivotCacheID) + ".xml" - if err = f.addPivotCache(pivotCacheXML, opts); err != nil { + opts.pivotTableXML = strings.ReplaceAll(sheetRelationshipsPivotTableXML, "..", "xl") + opts.pivotCacheXML = "xl/pivotCache/pivotCacheDefinition" + strconv.Itoa(pivotCacheID) + ".xml" + if err = f.addPivotCache(opts); err != nil { return err } // workbook pivot cache - workBookPivotCacheRID := f.addRels(f.getWorkbookRelsPath(), SourceRelationshipPivotCache, strings.TrimPrefix(pivotCacheXML, "xl/"), "") + workBookPivotCacheRID := f.addRels(f.getWorkbookRelsPath(), SourceRelationshipPivotCache, strings.TrimPrefix(opts.pivotCacheXML, "xl/"), "") cacheID := f.addWorkbookPivotCache(workBookPivotCacheRID) pivotCacheRels := "xl/pivotTables/_rels/pivotTable" + strconv.Itoa(pivotTableID) + ".xml.rels" // rId not used _ = f.addRels(pivotCacheRels, SourceRelationshipPivotCache, fmt.Sprintf("../pivotCache/pivotCacheDefinition%d.xml", pivotCacheID), "") - if err = f.addPivotTable(cacheID, pivotTableID, pivotTableXML, opts); err != nil { + if err = f.addPivotTable(cacheID, pivotTableID, opts); err != nil { return err } pivotTableSheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(pivotTableSheetPath, "xl/worksheets/") + ".rels" @@ -192,15 +194,11 @@ func (f *File) parseFormatPivotTableSet(opts *PivotTableOptions) (*xlsxWorksheet if len(opts.Name) > MaxFieldLength { return nil, "", ErrNameLength } - opts.pivotTableSheetName = pivotTableSheetName - _, dataRangeRef, err := f.getPivotTableDataRange(pivotTableSheetName, opts.DataRange, opts.DataRange) - if err != nil { + opts.pivotSheetName = pivotTableSheetName + if err = f.getPivotTableDataRange(opts); err != nil { return nil, "", err } - if dataRangeRef == "" { - dataRangeRef = opts.DataRange - } - dataSheetName, _, err := f.adjustRange(dataRangeRef) + dataSheetName, _, err := f.adjustRange(opts.pivotDataRange) if err != nil { return nil, "", newPivotTableDataRangeError(err.Error()) } @@ -247,19 +245,12 @@ func (f *File) adjustRange(rangeStr string) (string, []int, error) { // getTableFieldsOrder provides a function to get order list of pivot table // fields. -func (f *File) getTableFieldsOrder(sheetName, dataRange string) ([]string, error) { +func (f *File) getTableFieldsOrder(opts *PivotTableOptions) ([]string, error) { var order []string - if dataRange == "" { - return order, newPivotTableDataRangeError(ErrParameterRequired.Error()) - } - _, dataRangeRef, err := f.getPivotTableDataRange(sheetName, dataRange, dataRange) - if err != nil { + if err := f.getPivotTableDataRange(opts); err != nil { return order, err } - if dataRangeRef == "" { - dataRangeRef = dataRange - } - dataSheet, coordinates, err := f.adjustRange(dataRangeRef) + dataSheet, coordinates, err := f.adjustRange(opts.pivotDataRange) if err != nil { return order, newPivotTableDataRangeError(err.Error()) } @@ -275,23 +266,14 @@ func (f *File) getTableFieldsOrder(sheetName, dataRange string) ([]string, error } // addPivotCache provides a function to create a pivot cache by given properties. -func (f *File) addPivotCache(pivotCacheXML string, opts *PivotTableOptions) error { +func (f *File) addPivotCache(opts *PivotTableOptions) error { // validate data range - definedNameRef := true - _, dataRangeRef, err := f.getPivotTableDataRange(opts.pivotTableSheetName, opts.DataRange, opts.DataRange) - if err != nil { - return err - } - if dataRangeRef == "" { - definedNameRef = false - dataRangeRef = opts.DataRange - } - dataSheet, coordinates, err := f.adjustRange(dataRangeRef) + dataSheet, coordinates, err := f.adjustRange(opts.pivotDataRange) if err != nil { return newPivotTableDataRangeError(err.Error()) } // data range has been checked - order, _ := f.getTableFieldsOrder(opts.pivotTableSheetName, dataRangeRef) + order, _ := f.getTableFieldsOrder(opts) hCell, _ := CoordinatesToCellName(coordinates[0], coordinates[1]) vCell, _ := CoordinatesToCellName(coordinates[2], coordinates[3]) pc := xlsxPivotCacheDefinition{ @@ -309,7 +291,7 @@ func (f *File) addPivotCache(pivotCacheXML string, opts *PivotTableOptions) erro }, CacheFields: &xlsxCacheFields{}, } - if definedNameRef { + if opts.namedDataRange { pc.CacheSource.WorksheetSource = &xlsxWorksheetSource{Name: opts.DataRange} } for _, name := range order { @@ -320,13 +302,13 @@ func (f *File) addPivotCache(pivotCacheXML string, opts *PivotTableOptions) erro } pc.CacheFields.Count = len(pc.CacheFields.CacheField) pivotCache, err := xml.Marshal(pc) - f.saveFileList(pivotCacheXML, pivotCache) + f.saveFileList(opts.pivotCacheXML, pivotCache) return err } // 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 *PivotTableOptions) error { +func (f *File) addPivotTable(cacheID, pivotTableID int, opts *PivotTableOptions) error { // validate pivot table range _, coordinates, err := f.adjustRange(opts.PivotTableRange) if err != nil { @@ -401,7 +383,7 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op _ = f.addPivotDataFields(&pt, opts) pivotTable, err := xml.Marshal(pt) - f.saveFileList(pivotTableXML, pivotTable) + f.saveFileList(opts.pivotTableXML, pivotTable) return err } @@ -539,7 +521,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 *PivotTableOptions) error { - order, err := f.getTableFieldsOrder(opts.pivotTableSheetName, opts.DataRange) + order, err := f.getTableFieldsOrder(opts) if err != nil { return err } @@ -645,7 +627,7 @@ func (f *File) countPivotCache() int { // to a sequential index by given fields and pivot option. func (f *File) getPivotFieldsIndex(fields []PivotTableField, opts *PivotTableOptions) ([]int, error) { var pivotFieldsIndex []int - orders, err := f.getTableFieldsOrder(opts.pivotTableSheetName, opts.DataRange) + orders, err := f.getTableFieldsOrder(opts) if err != nil { return pivotFieldsIndex, err } @@ -761,30 +743,40 @@ func (f *File) GetPivotTables(sheet string) ([]PivotTableOptions, error) { return pivotTables, nil } -// getPivotTableDataRange returns pivot table data range name and reference from -// cell reference, table name or defined name. -func (f *File) getPivotTableDataRange(sheet, ref, name string) (string, string, error) { - dataRange := fmt.Sprintf("%s!%s", sheet, ref) - dataRangeRef, isTable := dataRange, false - if name != "" { - dataRange = name - for _, sheetName := range f.GetSheetList() { - tables, err := f.GetTables(sheetName) - e := ErrSheetNotExist{sheetName} - if err != nil && err.Error() != newNotWorksheetError(sheetName).Error() && err.Error() != e.Error() { - return dataRange, dataRangeRef, err - } - for _, table := range tables { - if table.Name == name { - dataRangeRef, isTable = fmt.Sprintf("%s!%s", sheetName, table.Range), true - } +// getPivotTableDataRange checking given if data range is a cell reference or +// named reference (defined name or table name), and set pivot table data range. +func (f *File) getPivotTableDataRange(opts *PivotTableOptions) error { + if opts.DataRange == "" { + return newPivotTableDataRangeError(ErrParameterRequired.Error()) + } + if opts.pivotDataRange != "" { + return nil + } + if strings.Contains(opts.DataRange, "!") { + opts.pivotDataRange = opts.DataRange + return nil + } + for _, sheetName := range f.GetSheetList() { + tables, err := f.GetTables(sheetName) + e := ErrSheetNotExist{sheetName} + if err != nil && err.Error() != newNotWorksheetError(sheetName).Error() && err.Error() != e.Error() { + return err + } + for _, table := range tables { + if table.Name == opts.DataRange { + opts.pivotDataRange, opts.namedDataRange = fmt.Sprintf("%s!%s", sheetName, table.Range), true + return err } } - if !isTable { - dataRangeRef = f.getDefinedNameRefTo(name, sheet) + } + if !opts.namedDataRange { + opts.pivotDataRange = f.getDefinedNameRefTo(opts.DataRange, opts.pivotSheetName) + if opts.pivotDataRange != "" { + opts.namedDataRange = true + return nil } } - return dataRange, dataRangeRef, nil + return newPivotTableDataRangeError(ErrParameterInvalid.Error()) } // getPivotTable provides a function to get a pivot table definition by given @@ -810,17 +802,17 @@ func (f *File) getPivotTable(sheet, pivotTableXML, pivotCacheRels string) (Pivot if err != nil { return opts, err } - dataRange, dataRangeRef, err := f.getPivotTableDataRange(sheet, pc.CacheSource.WorksheetSource.Ref, pc.CacheSource.WorksheetSource.Name) - if err != nil { - return opts, err - } opts = PivotTableOptions{ - pivotTableXML: pivotTableXML, - pivotCacheXML: pivotCacheXML, - pivotTableSheetName: sheet, - DataRange: dataRange, - PivotTableRange: fmt.Sprintf("%s!%s", sheet, pt.Location.Ref), - Name: pt.Name, + pivotTableXML: pivotTableXML, + pivotCacheXML: pivotCacheXML, + pivotSheetName: sheet, + DataRange: fmt.Sprintf("%s!%s", sheet, pc.CacheSource.WorksheetSource.Ref), + PivotTableRange: fmt.Sprintf("%s!%s", sheet, pt.Location.Ref), + Name: pt.Name, + } + if pc.CacheSource.WorksheetSource.Name != "" { + opts.DataRange = pc.CacheSource.WorksheetSource.Name + _ = f.getPivotTableDataRange(&opts) } fields := []string{"RowGrandTotals", "ColGrandTotals", "ShowDrill", "UseAutoFormatting", "PageOverThenDown", "MergeItem", "CompactData", "ShowError"} immutable, mutable := reflect.ValueOf(*pt), reflect.ValueOf(&opts).Elem() @@ -838,7 +830,10 @@ func (f *File) getPivotTable(sheet, pivotTableXML, pivotCacheRels string) (Pivot opts.ShowLastColumn = si.ShowLastColumn opts.PivotTableStyleName = si.Name } - order, err := f.getTableFieldsOrder(pt.Name, dataRangeRef) + order, err := f.getTableFieldsOrder(&opts) + if err != nil { + return opts, err + } f.extractPivotTableFields(order, pt, &opts) return opts, err } diff --git a/pivotTable_test.go b/pivotTable_test.go index ec3384a7d7..6cbdf55d77 100644 --- a/pivotTable_test.go +++ b/pivotTable_test.go @@ -272,22 +272,14 @@ func TestPivotTable(t *testing.T) { _, _, err = f.adjustRange("sheet1!") assert.EqualError(t, err, "parameter is invalid") // Test get table fields order with empty data range - _, err = f.getTableFieldsOrder("", "") + _, err = f.getTableFieldsOrder(&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("", &PivotTableOptions{}), "parameter 'DataRange' parsing error: parameter is invalid") - // Test add pivot cache with invalid data range - assert.EqualError(t, f.addPivotCache("", &PivotTableOptions{ - DataRange: "A1:E31", - PivotTableRange: "Sheet1!U34:O2", - Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, - Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, - Data: []PivotTableField{{Data: "Sales"}}, - }), "parameter 'DataRange' parsing error: parameter is invalid") + assert.EqualError(t, f.addPivotCache(&PivotTableOptions{}), "parameter 'DataRange' parsing error: parameter is required") // Test add pivot table with empty options - assert.EqualError(t, f.addPivotTable(0, 0, "", &PivotTableOptions{}), "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, "", &PivotTableOptions{}), "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, &PivotTableOptions{ DataRange: "A1:E31", @@ -413,22 +405,6 @@ func TestParseFormatPivotTableSet(t *testing.T) { assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") } -func TestAddPivotCache(t *testing.T) { - f := NewFile() - // Create table in a worksheet - assert.NoError(t, f.AddTable("Sheet1", &Table{ - Name: "Table1", - Range: "A1:D5", - })) - // Test add pivot table cache with unsupported table relationships charset - f.Pkg.Store("xl/tables/table1.xml", MacintoshCyrillicCharset) - assert.EqualError(t, f.addPivotCache("xl/pivotCache/pivotCacheDefinition1.xml", &PivotTableOptions{ - DataRange: "Table1", - PivotTableRange: "Sheet1!G2:K7", - Rows: []PivotTableField{{Data: "Column1"}}, - }), "XML syntax error on line 1: invalid UTF-8") -} - func TestAddPivotRowFields(t *testing.T) { f := NewFile() // Test invalid data range @@ -465,7 +441,7 @@ func TestAddPivotColFields(t *testing.T) { func TestGetPivotFieldsOrder(t *testing.T) { f := NewFile() // Test get table fields order with not exist worksheet - _, err := f.getTableFieldsOrder("", "SheetN!A1:E31") + _, err := f.getTableFieldsOrder(&PivotTableOptions{DataRange: "SheetN!A1:E31"}) assert.EqualError(t, err, "sheet SheetN does not exist") // Create table in a worksheet assert.NoError(t, f.AddTable("Sheet1", &Table{ @@ -474,7 +450,7 @@ func TestGetPivotFieldsOrder(t *testing.T) { })) // Test get table fields order with unsupported table relationships charset f.Pkg.Store("xl/tables/table1.xml", MacintoshCyrillicCharset) - _, err = f.getTableFieldsOrder("Sheet1", "Table") + _, err = f.getTableFieldsOrder(&PivotTableOptions{DataRange: "Table"}) assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") } diff --git a/slicer.go b/slicer.go index 63d9dad1f9..6dd855eba6 100644 --- a/slicer.go +++ b/slicer.go @@ -210,7 +210,7 @@ func (f *File) getSlicerSource(opts *SlicerOptions) (*Table, *PivotTableOptions, return table, pivotTable, colIdx, newNoExistTableError(opts.TableName) } } - order, _ := f.getTableFieldsOrder(opts.TableSheet, dataRange) + order, _ := f.getTableFieldsOrder(&PivotTableOptions{DataRange: dataRange}) if colIdx = inStrSlice(order, opts.Name, true); colIdx == -1 { return table, pivotTable, colIdx, newInvalidSlicerNameError(opts.Name) } diff --git a/workbook_test.go b/workbook_test.go index 67cf5c81c4..73dda6cef5 100644 --- a/workbook_test.go +++ b/workbook_test.go @@ -31,3 +31,13 @@ func TestWorkbookProps(t *testing.T) { _, err = f.GetWorkbookProps() assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") } + +func TestDeleteWorkbookRels(t *testing.T) { + f := NewFile() + // Test delete pivot table without worksheet relationships + f.Relationships.Delete("xl/_rels/workbook.xml.rels") + f.Pkg.Delete("xl/_rels/workbook.xml.rels") + rID, err := f.deleteWorkbookRels("", "") + assert.Empty(t, rID) + assert.NoError(t, err) +} From 95fc35f46cbf1948198d114fc857f1e66b642ab4 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 6 Oct 2023 00:04:38 +0800 Subject: [PATCH 801/957] This fix #1682, removes table and content type parts when deleting the table - Move worksheet-related functions in one place --- picture.go | 173 -------------------------------------------------- sheet.go | 56 +++++++++++----- sheet_test.go | 4 +- table.go | 11 ++-- workbook.go | 147 ++++++++++++++++++++++++++++++++++++++++++ xmlTable.go | 1 + 6 files changed, 198 insertions(+), 194 deletions(-) diff --git a/picture.go b/picture.go index 4e646eeac5..c289850727 100644 --- a/picture.go +++ b/picture.go @@ -251,29 +251,6 @@ func (f *File) AddPictureFromBytes(sheet, cell string, pic *Picture) error { return err } -// deleteSheetRelationships provides a function to delete relationships in -// xl/worksheets/_rels/sheet%d.xml.rels by given worksheet name and -// relationship index. -func (f *File) deleteSheetRelationships(sheet, rID string) { - name, ok := f.getSheetXMLPath(sheet) - if !ok { - name = strings.ToLower(sheet) + ".xml" - } - rels := "xl/worksheets/_rels/" + strings.TrimPrefix(name, "xl/worksheets/") + ".rels" - sheetRels, _ := f.relsReader(rels) - if sheetRels == nil { - sheetRels = &xlsxRelationships{} - } - 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:]...) - } - } - f.Relationships.Store(rels, sheetRels) -} - // addSheetLegacyDrawing provides a function to add legacy drawing element to // xl/worksheets/sheet%d.xml by given worksheet name and relationship index. func (f *File) addSheetLegacyDrawing(sheet string, rID int) { @@ -440,156 +417,6 @@ func (f *File) addMedia(file []byte, ext string) string { return media } -// setContentTypePartRelsExtensions provides a function to set the content -// type for relationship parts and the Main Document part. -func (f *File) setContentTypePartRelsExtensions() error { - var rels bool - content, err := f.contentTypesReader() - if err != nil { - return err - } - for _, v := range content.Defaults { - if v.Extension == "rels" { - rels = true - } - } - if !rels { - content.Defaults = append(content.Defaults, xlsxDefault{ - Extension: "rels", - ContentType: ContentTypeRelationships, - }) - } - return err -} - -// setContentTypePartImageExtensions provides a function to set the content -// type for relationship parts and the Main Document part. -func (f *File) setContentTypePartImageExtensions() error { - imageTypes := map[string]string{ - "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 { - return err - } - content.mu.Lock() - defer content.mu.Unlock() - for _, file := range content.Defaults { - delete(imageTypes, file.Extension) - } - for extension, prefix := range imageTypes { - content.Defaults = append(content.Defaults, xlsxDefault{ - Extension: extension, - 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() error { - var vml bool - content, err := f.contentTypesReader() - if err != nil { - return err - } - content.mu.Lock() - defer content.mu.Unlock() - for _, v := range content.Defaults { - if v.Extension == "vml" { - vml = true - } - } - if !vml { - content.Defaults = append(content.Defaults, xlsxDefault{ - Extension: "vml", - 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) error { - setContentType := map[string]func() error{ - "comments": f.setContentTypePartVMLExtensions, - "drawings": f.setContentTypePartImageExtensions, - } - partNames := map[string]string{ - "chart": "/xl/charts/chart" + strconv.Itoa(index) + ".xml", - "chartsheet": "/xl/chartsheets/sheet" + strconv.Itoa(index) + ".xml", - "comments": "/xl/comments" + strconv.Itoa(index) + ".xml", - "drawings": "/xl/drawings/drawing" + strconv.Itoa(index) + ".xml", - "table": "/xl/tables/table" + strconv.Itoa(index) + ".xml", - "pivotTable": "/xl/pivotTables/pivotTable" + strconv.Itoa(index) + ".xml", - "pivotCache": "/xl/pivotCache/pivotCacheDefinition" + strconv.Itoa(index) + ".xml", - "sharedStrings": "/xl/sharedStrings.xml", - "slicer": "/xl/slicers/slicer" + strconv.Itoa(index) + ".xml", - "slicerCache": "/xl/slicerCaches/slicerCache" + strconv.Itoa(index) + ".xml", - } - contentTypes := map[string]string{ - "chart": ContentTypeDrawingML, - "chartsheet": ContentTypeSpreadSheetMLChartsheet, - "comments": ContentTypeSpreadSheetMLComments, - "drawings": ContentTypeDrawing, - "table": ContentTypeSpreadSheetMLTable, - "pivotTable": ContentTypeSpreadSheetMLPivotTable, - "pivotCache": ContentTypeSpreadSheetMLPivotCacheDefinition, - "sharedStrings": ContentTypeSpreadSheetMLSharedStrings, - "slicer": ContentTypeSlicer, - "slicerCache": ContentTypeSlicerCache, - } - s, ok := setContentType[contentType] - if ok { - if err := s(); err != nil { - return err - } - } - content, err := f.contentTypesReader() - if err != nil { - return err - } - content.mu.Lock() - defer content.mu.Unlock() - for _, v := range content.Overrides { - if v.PartName == partNames[contentType] { - return err - } - } - content.Overrides = append(content.Overrides, xlsxOverride{ - PartName: partNames[contentType], - ContentType: contentTypes[contentType], - }) - return f.setContentTypePartRelsExtensions() -} - -// getSheetRelationshipsTargetByID provides a function to get Target attribute -// 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.getSheetXMLPath(sheet) - if !ok { - name = strings.ToLower(sheet) + ".xml" - } - rels := "xl/worksheets/_rels/" + strings.TrimPrefix(name, "xl/worksheets/") + ".rels" - sheetRels, _ := f.relsReader(rels) - if sheetRels == nil { - sheetRels = &xlsxRelationships{} - } - sheetRels.mu.Lock() - defer sheetRels.mu.Unlock() - for _, v := range sheetRels.Relationships { - if v.ID == rID { - return v.Target - } - } - return "" -} - // 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 image contents as []byte data types. This function is diff --git a/sheet.go b/sheet.go index 23ee77ec05..8f678b5150 100644 --- a/sheet.go +++ b/sheet.go @@ -576,7 +576,7 @@ func (f *File) DeleteSheet(sheet string) error { } } target := f.deleteSheetFromWorkbookRels(v.ID) - _ = f.deleteSheetFromContentTypes(target) + _ = f.removeContentTypesPart(ContentTypeSpreadSheetMLWorksheet, target) _ = f.deleteCalcChain(f.getSheetID(sheet), "") delete(f.sheetMap, v.Name) f.Pkg.Delete(sheetXML) @@ -626,24 +626,50 @@ func (f *File) deleteSheetFromWorkbookRels(rID string) string { return "" } -// deleteSheetFromContentTypes provides a function to remove worksheet -// relationships by given target name in the file [Content_Types].xml. -func (f *File) deleteSheetFromContentTypes(target string) error { - if !strings.HasPrefix(target, "/") { - target = "/xl/" + target +// deleteSheetRelationships provides a function to delete relationships in +// xl/worksheets/_rels/sheet%d.xml.rels by given worksheet name and +// relationship index. +func (f *File) deleteSheetRelationships(sheet, rID string) { + name, ok := f.getSheetXMLPath(sheet) + if !ok { + name = strings.ToLower(sheet) + ".xml" } - content, err := f.contentTypesReader() - if err != nil { - return err + rels := "xl/worksheets/_rels/" + strings.TrimPrefix(name, "xl/worksheets/") + ".rels" + sheetRels, _ := f.relsReader(rels) + if sheetRels == nil { + sheetRels = &xlsxRelationships{} } - 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:]...) + 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:]...) } } - return err + f.Relationships.Store(rels, sheetRels) +} + +// getSheetRelationshipsTargetByID provides a function to get Target attribute +// 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.getSheetXMLPath(sheet) + if !ok { + name = strings.ToLower(sheet) + ".xml" + } + rels := "xl/worksheets/_rels/" + strings.TrimPrefix(name, "xl/worksheets/") + ".rels" + sheetRels, _ := f.relsReader(rels) + if sheetRels == nil { + sheetRels = &xlsxRelationships{} + } + sheetRels.mu.Lock() + defer sheetRels.mu.Unlock() + for _, v := range sheetRels.Relationships { + if v.ID == rID { + return v.Target + } + } + return "" } // CopySheet provides a function to duplicate a worksheet by gave source and diff --git a/sheet_test.go b/sheet_test.go index 935736d75e..6851dfcc78 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -561,12 +561,12 @@ func TestSetContentTypes(t *testing.T) { assert.EqualError(t, f.setContentTypes("/xl/worksheets/sheet1.xml", ContentTypeSpreadSheetMLWorksheet), "XML syntax error on line 1: invalid UTF-8") } -func TestDeleteSheetFromContentTypes(t *testing.T) { +func TestRemoveContentTypesPart(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") + assert.EqualError(t, f.removeContentTypesPart(ContentTypeSpreadSheetMLWorksheet, "/xl/worksheets/sheet1.xml"), "XML syntax error on line 1: invalid UTF-8") } func BenchmarkNewSheet(b *testing.B) { diff --git a/table.go b/table.go index d12c33b857..1efcd41447 100644 --- a/table.go +++ b/table.go @@ -150,10 +150,11 @@ func (f *File) GetTables(sheet string) ([]Table, error) { return tables, err } table := Table{ - rID: tbl.RID, - tID: t.ID, - Range: t.Ref, - Name: t.Name, + rID: tbl.RID, + tID: t.ID, + tableXML: tableXML, + Range: t.Ref, + Name: t.Name, } if t.TableStyleInfo != nil { table.StyleName = t.TableStyleInfo.Name @@ -186,6 +187,8 @@ func (f *File) DeleteTable(name string) error { for i, tbl := range ws.TableParts.TableParts { if tbl.RID == table.rID { ws.TableParts.TableParts = append(ws.TableParts.TableParts[:i], ws.TableParts.TableParts[i+1:]...) + f.Pkg.Delete(table.tableXML) + _ = f.removeContentTypesPart(ContentTypeSpreadSheetMLTable, "/"+table.tableXML) f.deleteSheetRelationships(sheet, tbl.RID) break } diff --git a/workbook.go b/workbook.go index 2810018c37..0e635d29ce 100644 --- a/workbook.go +++ b/workbook.go @@ -225,3 +225,150 @@ func (f *File) workBookWriter() { f.saveFileList(f.getWorkbookPath(), replaceRelationshipsBytes(f.replaceNameSpaceBytes(f.getWorkbookPath(), output))) } } + +// setContentTypePartRelsExtensions provides a function to set the content type +// for relationship parts and the Main Document part. +func (f *File) setContentTypePartRelsExtensions() error { + var rels bool + content, err := f.contentTypesReader() + if err != nil { + return err + } + for _, v := range content.Defaults { + if v.Extension == "rels" { + rels = true + } + } + if !rels { + content.Defaults = append(content.Defaults, xlsxDefault{ + Extension: "rels", + ContentType: ContentTypeRelationships, + }) + } + return err +} + +// setContentTypePartImageExtensions provides a function to set the content type +// for relationship parts and the Main Document part. +func (f *File) setContentTypePartImageExtensions() error { + imageTypes := map[string]string{ + "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 { + return err + } + content.mu.Lock() + defer content.mu.Unlock() + for _, file := range content.Defaults { + delete(imageTypes, file.Extension) + } + for extension, prefix := range imageTypes { + content.Defaults = append(content.Defaults, xlsxDefault{ + Extension: extension, + 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() error { + var vml bool + content, err := f.contentTypesReader() + if err != nil { + return err + } + content.mu.Lock() + defer content.mu.Unlock() + for _, v := range content.Defaults { + if v.Extension == "vml" { + vml = true + } + } + if !vml { + content.Defaults = append(content.Defaults, xlsxDefault{ + Extension: "vml", + ContentType: ContentTypeVML, + }) + } + return err +} + +// addContentTypePart provides a function to add content type part relationships +// in the file [Content_Types].xml by given index and content type. +func (f *File) addContentTypePart(index int, contentType string) error { + setContentType := map[string]func() error{ + "comments": f.setContentTypePartVMLExtensions, + "drawings": f.setContentTypePartImageExtensions, + } + partNames := map[string]string{ + "chart": "/xl/charts/chart" + strconv.Itoa(index) + ".xml", + "chartsheet": "/xl/chartsheets/sheet" + strconv.Itoa(index) + ".xml", + "comments": "/xl/comments" + strconv.Itoa(index) + ".xml", + "drawings": "/xl/drawings/drawing" + strconv.Itoa(index) + ".xml", + "table": "/xl/tables/table" + strconv.Itoa(index) + ".xml", + "pivotTable": "/xl/pivotTables/pivotTable" + strconv.Itoa(index) + ".xml", + "pivotCache": "/xl/pivotCache/pivotCacheDefinition" + strconv.Itoa(index) + ".xml", + "sharedStrings": "/xl/sharedStrings.xml", + "slicer": "/xl/slicers/slicer" + strconv.Itoa(index) + ".xml", + "slicerCache": "/xl/slicerCaches/slicerCache" + strconv.Itoa(index) + ".xml", + } + contentTypes := map[string]string{ + "chart": ContentTypeDrawingML, + "chartsheet": ContentTypeSpreadSheetMLChartsheet, + "comments": ContentTypeSpreadSheetMLComments, + "drawings": ContentTypeDrawing, + "table": ContentTypeSpreadSheetMLTable, + "pivotTable": ContentTypeSpreadSheetMLPivotTable, + "pivotCache": ContentTypeSpreadSheetMLPivotCacheDefinition, + "sharedStrings": ContentTypeSpreadSheetMLSharedStrings, + "slicer": ContentTypeSlicer, + "slicerCache": ContentTypeSlicerCache, + } + s, ok := setContentType[contentType] + if ok { + if err := s(); err != nil { + return err + } + } + content, err := f.contentTypesReader() + if err != nil { + return err + } + content.mu.Lock() + defer content.mu.Unlock() + for _, v := range content.Overrides { + if v.PartName == partNames[contentType] { + return err + } + } + content.Overrides = append(content.Overrides, xlsxOverride{ + PartName: partNames[contentType], + ContentType: contentTypes[contentType], + }) + return f.setContentTypePartRelsExtensions() +} + +// removeContentTypesPart provides a function to remove relationships by given +// content type and part name in the file [Content_Types].xml. +func (f *File) removeContentTypesPart(contentType, partName string) error { + if !strings.HasPrefix(partName, "/") { + partName = "/xl/" + partName + } + content, err := f.contentTypesReader() + if err != nil { + return err + } + content.mu.Lock() + defer content.mu.Unlock() + for k, v := range content.Overrides { + if v.PartName == partName && v.ContentType == contentType { + content.Overrides = append(content.Overrides[:k], content.Overrides[k+1:]...) + } + } + return err +} diff --git a/xmlTable.go b/xmlTable.go index ff97df54c2..41a5bdb30e 100644 --- a/xmlTable.go +++ b/xmlTable.go @@ -229,6 +229,7 @@ type xlsxXMLCellPr struct { type Table struct { tID int rID string + tableXML string Range string Name string StyleName string From 07f2c6831a85214b2e914f274e746213e3f6d081 Mon Sep 17 00:00:00 2001 From: Abdelaziz-Ouhammou Date: Fri, 6 Oct 2023 19:05:50 +0300 Subject: [PATCH 802/957] Keep all cells value in the table range when deleting table (#1684) --- table.go | 12 ------------ table_test.go | 16 ++++++++++++---- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/table.go b/table.go index 1efcd41447..b88db4cd63 100644 --- a/table.go +++ b/table.go @@ -196,18 +196,6 @@ func (f *File) DeleteTable(name string) error { if ws.TableParts.Count = len(ws.TableParts.TableParts); ws.TableParts.Count == 0 { ws.TableParts = nil } - // Delete cell value in the table header - coordinates, err := rangeRefToCoordinates(table.Range) - if err != nil { - return err - } - _ = sortCoordinates(coordinates) - for col := coordinates[0]; col <= coordinates[2]; col++ { - for row := coordinates[1]; row < coordinates[1]+1; row++ { - cell, _ := CoordinatesToCellName(col, row) - err = f.SetCellValue(sheet, cell, nil) - } - } return err } } diff --git a/table_test.go b/table_test.go index aa1980cb87..0b82b2a10c 100644 --- a/table_test.go +++ b/table_test.go @@ -122,11 +122,19 @@ func TestDeleteTable(t *testing.T) { f.Sheet.Delete("xl/worksheets/sheet1.xml") f.Pkg.Store("xl/worksheets/sheet1.xml", MacintoshCyrillicCharset) assert.EqualError(t, f.DeleteTable("Table1"), "XML syntax error on line 1: invalid UTF-8") - // Test delete table with invalid table range + // Test delete table without deleting table header f = NewFile() - assert.NoError(t, f.AddTable("Sheet1", &Table{Range: "A1:B4", Name: "Table1"})) - f.Pkg.Store("xl/tables/table1.xml", []byte("
")) - assert.EqualError(t, f.DeleteTable("Table1"), ErrParameterInvalid.Error()) + assert.NoError(t, f.SetCellValue("Sheet1", "A1", "Date")) + assert.NoError(t, f.SetCellValue("Sheet1", "B1", "Values")) + assert.NoError(t, f.UpdateLinkedValue()) + assert.NoError(t, f.AddTable("Sheet1", &Table{Range: "A1:B2", Name: "Table1"})) + assert.NoError(t, f.DeleteTable("Table1")) + val, err := f.GetCellValue("Sheet1", "A1") + assert.NoError(t, err) + assert.Equal(t, "Date", val) + val, err = f.GetCellValue("Sheet1", "B1") + assert.NoError(t, err) + assert.Equal(t, "Values", val) } func TestSetTableHeader(t *testing.T) { From 99df1a734314f3bcf1478234c9ec08b0d23707bc Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 8 Oct 2023 00:06:11 +0800 Subject: [PATCH 803/957] This closes #1681, closes #1683 - Fix incorrect formula calculation result - Introduce new exported function `SetCellUint` - Updates unit test - Typo fixed --- calc.go | 175 ++++++++++++++++++++++++++------------------------- calc_test.go | 7 ++- cell.go | 53 ++++++++++++---- cell_test.go | 21 +++++++ col_test.go | 2 +- stream.go | 10 +-- 6 files changed, 162 insertions(+), 106 deletions(-) diff --git a/calc.go b/calc.go index ed5aeaa5ea..1320238827 100644 --- a/calc.go +++ b/calc.go @@ -193,7 +193,7 @@ var ( return fmt.Sprintf("R[%d]C[%d]", row, col), nil }, } - formularFormats = []*regexp.Regexp{ + formulaFormats = []*regexp.Regexp{ regexp.MustCompile(`^(\d+)$`), regexp.MustCompile(`^=(.*)$`), regexp.MustCompile(`^<>(.*)$`), @@ -202,7 +202,7 @@ var ( regexp.MustCompile(`^<(.*)$`), regexp.MustCompile(`^>(.*)$`), } - formularCriterias = []byte{ + formulaCriterias = []byte{ criteriaEq, criteriaEq, criteriaNe, @@ -238,7 +238,7 @@ type cellRange struct { // formulaCriteria defined formula criteria parser result. type formulaCriteria struct { Type byte - Condition string + Condition formulaArg } // ArgType is the type of formula argument type. @@ -1698,65 +1698,66 @@ func callFuncByName(receiver interface{}, name string, params []reflect.Value) ( } // formulaCriteriaParser parse formula criteria. -func formulaCriteriaParser(exp string) (fc *formulaCriteria) { - fc = &formulaCriteria{} - if exp == "" { - 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, "?", ".") - } - if strings.Contains(exp, "*") { - exp = strings.ReplaceAll(exp, "*", ".*") - } - fc.Type, fc.Condition = criteriaRegexp, exp - return -} - -// formulaCriteriaEval evaluate formula criteria expression. -func formulaCriteriaEval(val string, criteria *formulaCriteria) (result bool, err error) { - var value, expected float64 - var e error - prepareValue := func(val, cond string) (value float64, expected float64, err error) { +func formulaCriteriaParser(exp formulaArg) *formulaCriteria { + prepareValue := func(cond string) (expected float64, err error) { percentile := 1.0 if strings.HasSuffix(cond, "%") { cond = strings.TrimSuffix(cond, "%") percentile /= 100 } - if value, err = strconv.ParseFloat(val, 64); err != nil { - return - } if expected, err = strconv.ParseFloat(cond, 64); err != nil { return } expected *= percentile return } + fc, val := &formulaCriteria{}, exp.Value() + if val == "" { + return fc + } + for i, re := range formulaFormats { + if match := re.FindStringSubmatch(val); len(match) > 1 { + fc.Condition = newStringFormulaArg(match[1]) + if num, err := prepareValue(match[1]); err == nil { + fc.Condition = newNumberFormulaArg(num) + } + fc.Type = formulaCriterias[i] + return fc + } + } + if strings.Contains(val, "?") { + val = strings.ReplaceAll(val, "?", ".") + } + if strings.Contains(val, "*") { + val = strings.ReplaceAll(val, "*", ".*") + } + fc.Type, fc.Condition = criteriaRegexp, newStringFormulaArg(val) + if num := fc.Condition.ToNumber(); num.Type == ArgNumber { + fc.Condition = num + } + return fc +} + +// formulaCriteriaEval evaluate formula criteria expression. +func formulaCriteriaEval(val formulaArg, criteria *formulaCriteria) (result bool, err error) { + s := NewStack() + tokenCalcFunc := map[byte]func(rOpd, lOpd formulaArg, opdStack *Stack) error{ + criteriaEq: calcEq, + criteriaNe: calcNEq, + criteriaL: calcL, + criteriaLe: calcLe, + criteriaG: calcG, + criteriaGe: calcGe, + } switch criteria.Type { - case criteriaEq: - return val == criteria.Condition, err - case criteriaLe: - value, expected, e = prepareValue(val, criteria.Condition) - return value <= expected && e == nil, err - case criteriaGe: - value, expected, e = prepareValue(val, criteria.Condition) - return value >= expected && e == nil, err - case criteriaNe: - return val != criteria.Condition, err - case criteriaL: - value, expected, e = prepareValue(val, criteria.Condition) - return value < expected && e == nil, err - case criteriaG: - value, expected, e = prepareValue(val, criteria.Condition) - return value > expected && e == nil, err + case criteriaEq, criteriaLe, criteriaGe, criteriaNe, criteriaL, criteriaG: + if fn, ok := tokenCalcFunc[criteria.Type]; ok { + if _ = fn(criteria.Condition, val, s); s.Len() > 0 { + return s.Pop().(formulaArg).Number == 1, err + } + } case criteriaRegexp: - return regexp.MatchString(criteria.Condition, val) + return regexp.MatchString(criteria.Condition.Value(), val.Value()) } return } @@ -5821,7 +5822,7 @@ func (fn *formulaFuncs) SUMIF(argsList *list.List) formulaArg { if argsList.Len() < 2 { return newErrorFormulaArg(formulaErrorVALUE, "SUMIF requires at least 2 arguments") } - criteria := formulaCriteriaParser(argsList.Front().Next().Value.(formulaArg).String) + criteria := formulaCriteriaParser(argsList.Front().Next().Value.(formulaArg)) rangeMtx := argsList.Front().Value.(formulaArg).Matrix var sumRange [][]formulaArg if argsList.Len() == 3 { @@ -5835,7 +5836,7 @@ func (fn *formulaFuncs) SUMIF(argsList *list.List) formulaArg { if arg.Type == ArgEmpty { continue } - if ok, _ := formulaCriteriaEval(arg.Value(), criteria); ok { + if ok, _ := formulaCriteriaEval(arg, criteria); ok { if argsList.Len() == 3 { if len(sumRange) > rowIdx && len(sumRange[rowIdx]) > colIdx { arg = sumRange[rowIdx][colIdx] @@ -6169,7 +6170,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).Value()) + criteria = formulaCriteriaParser(argsList.Front().Next().Value.(formulaArg)) rangeMtx = argsList.Front().Value.(formulaArg).Matrix cellRange [][]formulaArg args []formulaArg @@ -6183,10 +6184,13 @@ func (fn *formulaFuncs) AVERAGEIF(argsList *list.List) formulaArg { for rowIdx, row := range rangeMtx { for colIdx, col := range row { fromVal := col.Value() - if col.Value() == "" { + if fromVal == "" { continue } - ok, _ = formulaCriteriaEval(fromVal, criteria) + if col.Type == ArgString && criteria.Condition.Type != ArgString { + continue + } + ok, _ = formulaCriteriaEval(col, criteria) if ok { if argsList.Len() == 3 { if len(cellRange) > rowIdx && len(cellRange[rowIdx]) > colIdx { @@ -7880,11 +7884,14 @@ func (fn *formulaFuncs) COUNTIF(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorVALUE, "COUNTIF requires 2 arguments") } var ( - criteria = formulaCriteriaParser(argsList.Front().Next().Value.(formulaArg).String) + criteria = formulaCriteriaParser(argsList.Front().Next().Value.(formulaArg)) count float64 ) for _, cell := range argsList.Front().Value.(formulaArg).ToList() { - if ok, _ := formulaCriteriaEval(cell.Value(), criteria); ok { + if cell.Type == ArgString && criteria.Condition.Type != ArgString { + continue + } + if ok, _ := formulaCriteriaEval(cell, criteria); ok { count++ } } @@ -7895,11 +7902,11 @@ func (fn *formulaFuncs) COUNTIF(argsList *list.List) formulaArg { func formulaIfsMatch(args []formulaArg) (cellRefs []cellRef) { for i := 0; i < len(args)-1; i += 2 { var match []cellRef - matrix, criteria := args[i].Matrix, formulaCriteriaParser(args[i+1].Value()) + matrix, criteria := args[i].Matrix, formulaCriteriaParser(args[i+1]) if i == 0 { for rowIdx, row := range matrix { for colIdx, col := range row { - if ok, _ := formulaCriteriaEval(col.Value(), criteria); ok { + if ok, _ := formulaCriteriaEval(col, criteria); ok { match = append(match, cellRef{Col: colIdx, Row: rowIdx}) } } @@ -7908,7 +7915,7 @@ func formulaIfsMatch(args []formulaArg) (cellRefs []cellRef) { match = []cellRef{} for _, ref := range cellRefs { value := matrix[ref.Row][ref.Col] - if ok, _ := formulaCriteriaEval(value.Value(), criteria); ok { + if ok, _ := formulaCriteriaEval(value, criteria); ok { match = append(match, ref) } } @@ -14830,43 +14837,43 @@ func (fn *formulaFuncs) HYPERLINK(argsList *list.List) formulaArg { // 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 { + idx := -1 switch matchType { case 0: for i, arg := range lookupArray { - if ok, _ := formulaCriteriaEval(arg.Value(), criteria); ok { + if ok, _ := formulaCriteriaEval(arg, criteria); ok { return newNumberFormulaArg(float64(i + 1)) } } case -1: for i, arg := range lookupArray { - if ok, _ := formulaCriteriaEval(arg.Value(), criteria); ok { - return newNumberFormulaArg(float64(i + 1)) - } - if ok, _ := formulaCriteriaEval(arg.Value(), &formulaCriteria{ - Type: criteriaL, Condition: criteria.Condition, + if ok, _ := formulaCriteriaEval(arg, &formulaCriteria{ + Type: criteriaGe, Condition: criteria.Condition, }); ok { - if i == 0 { - return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) - } - return newNumberFormulaArg(float64(i)) + idx = i + continue + } + if criteria.Condition.Type == ArgNumber { + break } } case 1: for i, arg := range lookupArray { - if ok, _ := formulaCriteriaEval(arg.Value(), criteria); ok { - return newNumberFormulaArg(float64(i + 1)) - } - if ok, _ := formulaCriteriaEval(arg.Value(), &formulaCriteria{ - Type: criteriaG, Condition: criteria.Condition, + if ok, _ := formulaCriteriaEval(arg, &formulaCriteria{ + Type: criteriaLe, Condition: criteria.Condition, }); ok { - if i == 0 { - return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) - } - return newNumberFormulaArg(float64(i)) + idx = i + continue + } + if criteria.Condition.Type == ArgNumber { + break } } } - return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + if idx == -1 { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + } + return newNumberFormulaArg(float64(idx + 1)) } // MATCH function looks up a value in an array, and returns the position of @@ -14904,7 +14911,7 @@ func (fn *formulaFuncs) MATCH(argsList *list.List) formulaArg { default: return newErrorFormulaArg(formulaErrorNA, lookupArrayErr) } - return calcMatch(matchType, formulaCriteriaParser(argsList.Front().Value.(formulaArg).Value()), lookupArray) + return calcMatch(matchType, formulaCriteriaParser(argsList.Front().Value.(formulaArg)), lookupArray) } // TRANSPOSE function 'transposes' an array of cells (i.e. the function copies @@ -14970,7 +14977,7 @@ start: } } if matchMode.Number == matchModeMinGreater || matchMode.Number == matchModeMaxLess { - matchIdx = int(calcMatch(int(matchMode.Number), formulaCriteriaParser(lookupValue.Value()), tableArray).Number) + matchIdx = int(calcMatch(int(matchMode.Number), formulaCriteriaParser(lookupValue), tableArray).Number) continue } } @@ -18390,12 +18397,12 @@ func (db *calcDatabase) criteriaEval() bool { for i := 1; !matched && i < rows; i++ { matched = true for j := 0; matched && j < columns; j++ { - criteriaExp := db.criteria[i][j].Value() - if criteriaExp == "" { + criteriaExp := db.criteria[i][j] + if criteriaExp.Value() == "" { continue } criteria := formulaCriteriaParser(criteriaExp) - cell := db.database[db.row][db.indexMap[j]].Value() + cell := db.database[db.row][db.indexMap[j]] matched, _ = formulaCriteriaEval(cell, criteria) } } diff --git a/calc_test.go b/calc_test.go index 4bb8660bbc..5e97a0eff7 100644 --- a/calc_test.go +++ b/calc_test.go @@ -4913,9 +4913,9 @@ func TestCalcAVERAGEIF(t *testing.T) { {4, 50}, {5, 100}, {1, 50}, - {"TRUE", 200}, - {"TRUE", 250}, - {"FALSE", 50}, + {true, 200}, + {true, 250}, + {false, 50}, }) for formula, expected := range map[string]string{ "=AVERAGEIF(A1:A14,\"Thursday\",B1:B14)": "150", @@ -5622,6 +5622,7 @@ func TestCalcMATCH(t *testing.T) { "=MATCH(8,C1:C6,1)": "3", "=MATCH(6,B1:B6,-1)": "1", "=MATCH(10,D1:D6,-1)": "3", + "=MATCH(-10,D1:D6,-1)": "6", } for formula, expected := range formulaList { assert.NoError(t, f.SetCellFormula("Sheet1", "E1", formula)) diff --git a/cell.go b/cell.go index b1f61f675e..c56b58767b 100644 --- a/cell.go +++ b/cell.go @@ -216,15 +216,15 @@ func (f *File) setCellIntFunc(sheet, cell string, value interface{}) error { case int64: err = f.SetCellInt(sheet, cell, int(v)) case uint: - err = f.SetCellInt(sheet, cell, int(v)) + err = f.SetCellUint(sheet, cell, uint64(v)) case uint8: - err = f.SetCellInt(sheet, cell, int(v)) + err = f.SetCellUint(sheet, cell, uint64(v)) case uint16: - err = f.SetCellInt(sheet, cell, int(v)) + err = f.SetCellUint(sheet, cell, uint64(v)) case uint32: - err = f.SetCellInt(sheet, cell, int(v)) + err = f.SetCellUint(sheet, cell, uint64(v)) case uint64: - err = f.SetCellInt(sheet, cell, int(v)) + err = f.SetCellUint(sheet, cell, v) } return err } @@ -307,13 +307,41 @@ func (f *File) SetCellInt(sheet, cell string, value int) error { return f.removeFormula(c, ws, sheet) } -// setCellInt prepares cell type and string type cell value by a given -// integer. +// setCellInt prepares cell type and string type cell value by a given integer. func setCellInt(value int) (t string, v string) { v = strconv.Itoa(value) return } +// SetCellUint provides a function to set uint type value of a cell by given +// worksheet name, cell reference and cell value. +func (f *File) SetCellUint(sheet, cell string, value uint64) 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 + } + c.S = ws.prepareCellStyle(col, row, c.S) + c.T, c.V = setCellUint(value) + c.IS = nil + return f.removeFormula(c, ws, sheet) +} + +// setCellUint prepares cell type and string type cell value by a given unsigned +// integer. +func setCellUint(value uint64) (t string, v string) { + v = strconv.FormatUint(value, 10) + return +} + // 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 { @@ -336,8 +364,8 @@ func (f *File) SetCellBool(sheet, cell string, value bool) error { return f.removeFormula(c, ws, sheet) } -// setCellBool prepares cell type and string type cell value by a given -// boolean value. +// setCellBool prepares cell type and string type cell value by a given boolean +// value. func setCellBool(value bool) (t string, v string) { t = "b" if value { @@ -376,8 +404,8 @@ func (f *File) SetCellFloat(sheet, cell string, value float64, precision, bitSiz return f.removeFormula(c, ws, sheet) } -// setCellFloat prepares cell type and string type cell value by a given -// float value. +// setCellFloat prepares cell type and string type cell value by a given float +// value. func setCellFloat(value float64, precision, bitSize int) (t string, v string) { v = strconv.FormatFloat(value, 'f', precision, bitSize) return @@ -407,8 +435,7 @@ func (f *File) SetCellStr(sheet, cell, value string) error { return f.removeFormula(c, ws, sheet) } -// setCellString provides a function to set string type to shared string -// table. +// setCellString provides a function to set string type to shared string table. func (f *File) setCellString(value string) (t, v string, err error) { if utf8.RuneCountInString(value) > TotalCellChars { value = string([]rune(value)[:TotalCellChars]) diff --git a/cell_test.go b/cell_test.go index 87399e1f0f..a4a2ddf53d 100644 --- a/cell_test.go +++ b/cell_test.go @@ -3,6 +3,7 @@ package excelize import ( "fmt" _ "image/jpeg" + "math" "os" "path/filepath" "reflect" @@ -176,6 +177,26 @@ func TestSetCellFloat(t *testing.T) { assert.EqualError(t, f.SetCellFloat("Sheet:1", "A1", 123.42, -1, 64), ErrSheetNameInvalid.Error()) } +func TestSetCellUint(t *testing.T) { + f := NewFile() + assert.NoError(t, f.SetCellValue("Sheet1", "A1", uint8(math.MaxUint8))) + result, err := f.GetCellValue("Sheet1", "A1") + assert.Equal(t, "255", result) + assert.NoError(t, err) + assert.NoError(t, f.SetCellValue("Sheet1", "A1", uint(math.MaxUint16))) + result, err = f.GetCellValue("Sheet1", "A1") + assert.Equal(t, "65535", result) + assert.NoError(t, err) + assert.NoError(t, f.SetCellValue("Sheet1", "A1", uint(math.MaxUint32))) + result, err = f.GetCellValue("Sheet1", "A1") + assert.Equal(t, "4294967295", result) + assert.NoError(t, err) + // Test uint cell value not exists worksheet + assert.EqualError(t, f.SetCellUint("SheetN", "A1", 1), "sheet SheetN does not exist") + // Test uint cell value with illegal cell reference + assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.SetCellUint("Sheet1", "A", 1)) +} + func TestSetCellValuesMultiByte(t *testing.T) { f := NewFile() row := []interface{}{ diff --git a/col_test.go b/col_test.go index ce7c3808c4..f1fe032f82 100644 --- a/col_test.go +++ b/col_test.go @@ -217,7 +217,7 @@ 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 not exists worksheet _, err = f.GetColVisible("SheetN", "F") assert.EqualError(t, err, "sheet SheetN does not exist") // Test get column visible with invalid sheet name diff --git a/stream.go b/stream.go index 8e72b91aa7..fe637b9650 100644 --- a/stream.go +++ b/stream.go @@ -567,15 +567,15 @@ func setCellIntFunc(c *xlsxC, val interface{}) (err error) { case int64: c.T, c.V = setCellInt(int(val)) case uint: - c.T, c.V = setCellInt(int(val)) + c.T, c.V = setCellUint(uint64(val)) case uint8: - c.T, c.V = setCellInt(int(val)) + c.T, c.V = setCellUint(uint64(val)) case uint16: - c.T, c.V = setCellInt(int(val)) + c.T, c.V = setCellUint(uint64(val)) case uint32: - c.T, c.V = setCellInt(int(val)) + c.T, c.V = setCellUint(uint64(val)) case uint64: - c.T, c.V = setCellInt(int(val)) + c.T, c.V = setCellUint(val) default: } return From 87a00e4f7e0aca8c5c52bbca4c706bf6b7a4a1b7 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 9 Oct 2023 00:14:56 +0800 Subject: [PATCH 804/957] This closed #1680, fixing a potential issue that stream reader temporary files can not be clear - Delete image files from the workbook internally when deleting pictures to reduce generated workbook size and resolve potential security issues --- chart.go | 3 ++- chart_test.go | 8 ++++--- drawing.go | 59 +++++++++++++++++++++++++++++++++++++++++++------ drawing_test.go | 9 ++++++++ lib.go | 10 +++++++-- picture.go | 34 +++++++++++++++++++++++++--- picture_test.go | 45 +++++++++++++++++++++++++++++++++++-- 7 files changed, 150 insertions(+), 18 deletions(-) diff --git a/chart.go b/chart.go index 4cfac96000..f2473f162b 100644 --- a/chart.go +++ b/chart.go @@ -1109,7 +1109,8 @@ func (f *File) DeleteChart(sheet, cell string) error { return err } drawingXML := strings.ReplaceAll(f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID), "..", "xl") - return f.deleteDrawing(col, row, drawingXML, "Chart") + _, err = f.deleteDrawing(col, row, drawingXML, "Chart") + return err } // countCharts provides a function to get chart files count storage in the diff --git a/chart_test.go b/chart_test.go index a860fa55ef..4516f5c56e 100644 --- a/chart_test.go +++ b/chart_test.go @@ -120,13 +120,15 @@ 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") - f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + _, err := f.deleteDrawing(0, 0, path, "Chart") + assert.EqualError(t, err, "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") + _, err = f.deleteDrawing(0, 0, path, "Chart") + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") } func TestAddChart(t *testing.T) { diff --git a/drawing.go b/drawing.go index 045506c860..c7bf88a0c9 100644 --- a/drawing.go +++ b/drawing.go @@ -1391,11 +1391,14 @@ func (f *File) addSheetDrawingChart(drawingXML string, rID int, opts *GraphicOpt return err } -// deleteDrawing provides a function to delete chart graphic frame by given by +// deleteDrawing provides a function to delete the chart graphic frame and +// returns deleted embed relationships ID (for unique picture cell anchor) by // given coordinates and graphic type. -func (f *File) deleteDrawing(col, row int, drawingXML, drawingType string) error { +func (f *File) deleteDrawing(col, row int, drawingXML, drawingType string) (string, error) { var ( err error + rID string + rIDs []string wsDr *xlsxWsDr deTwoCellAnchor *decodeCellAnchor ) @@ -1407,32 +1410,74 @@ func (f *File) deleteDrawing(col, row int, drawingXML, drawingType string) error "Chart": func(anchor *decodeCellAnchor) bool { return anchor.Pic == nil }, "Pic": func(anchor *decodeCellAnchor) bool { return anchor.Pic != nil }, } + onAnchorCell := func(c, r int) bool { return c == col && r == row } if wsDr, _, err = f.drawingParser(drawingXML); err != nil { - return err + return rID, 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 { + if onAnchorCell(wsDr.TwoCellAnchor[idx].From.Col, wsDr.TwoCellAnchor[idx].From.Row) { + rID, _ = extractEmbedRID(wsDr.TwoCellAnchor[idx].Pic, nil, rIDs) wsDr.TwoCellAnchor = append(wsDr.TwoCellAnchor[:idx], wsDr.TwoCellAnchor[idx+1:]...) idx-- + continue } + _, rIDs = extractEmbedRID(wsDr.TwoCellAnchor[idx].Pic, nil, rIDs) } } for idx := 0; idx < len(wsDr.TwoCellAnchor); idx++ { deTwoCellAnchor = new(decodeCellAnchor) if err = f.xmlNewDecoder(strings.NewReader("" + wsDr.TwoCellAnchor[idx].GraphicFrame + "")). Decode(deTwoCellAnchor); err != nil && err != io.EOF { - return err + return rID, err } if err = nil; deTwoCellAnchor.From != nil && decodeCellAnchorFuncs[drawingType](deTwoCellAnchor) { - if deTwoCellAnchor.From.Col == col && deTwoCellAnchor.From.Row == row { + if onAnchorCell(deTwoCellAnchor.From.Col, deTwoCellAnchor.From.Row) { + rID, _ = extractEmbedRID(nil, deTwoCellAnchor.Pic, rIDs) wsDr.TwoCellAnchor = append(wsDr.TwoCellAnchor[:idx], wsDr.TwoCellAnchor[idx+1:]...) idx-- + continue } + _, rIDs = extractEmbedRID(nil, deTwoCellAnchor.Pic, rIDs) } } + if inStrSlice(rIDs, rID, true) != -1 { + rID = "" + } f.Drawings.Store(drawingXML, wsDr) - return err + return rID, err +} + +// extractEmbedRID returns embed relationship ID and all relationship ID lists +// for giving cell anchor. +func extractEmbedRID(pic *xlsxPic, decodePic *decodePic, rIDs []string) (string, []string) { + if pic != nil { + rIDs = append(rIDs, pic.BlipFill.Blip.Embed) + return pic.BlipFill.Blip.Embed, rIDs + } + if decodePic != nil { + rIDs = append(rIDs, decodePic.BlipFill.Blip.Embed) + return decodePic.BlipFill.Blip.Embed, rIDs + } + return "", rIDs +} + +// deleteDrawingRels provides a function to delete relationships in +// xl/drawings/_rels/drawings%d.xml.rels by giving drawings relationships path +// and relationship ID. +func (f *File) deleteDrawingRels(rels, rID string) { + drawingRels, _ := f.relsReader(rels) + if drawingRels == nil { + drawingRels = &xlsxRelationships{} + } + drawingRels.mu.Lock() + defer drawingRels.mu.Unlock() + for k, v := range drawingRels.Relationships { + if v.ID == rID { + drawingRels.Relationships = append(drawingRels.Relationships[:k], drawingRels.Relationships[k+1:]...) + } + } + f.Relationships.Store(rels, drawingRels) } // genAxID provides a function to generate ID for primary and secondary diff --git a/drawing_test.go b/drawing_test.go index 7fcee82970..90846702cd 100644 --- a/drawing_test.go +++ b/drawing_test.go @@ -38,3 +38,12 @@ func TestDrawingParser(t *testing.T) { _, _, err = f.drawingParser("wsDr") assert.NoError(t, err) } + +func TestDeleteDrawingRels(t *testing.T) { + f := NewFile() + // Test delete drawing relationships with unsupported charset + rels := "xl/drawings/_rels/drawing1.xml.rels" + f.Relationships.Delete(rels) + f.Pkg.Store(rels, MacintoshCyrillicCharset) + f.deleteDrawingRels(rels, "") +} diff --git a/lib.go b/lib.go index 98f48c80d6..83c28e7c0b 100644 --- a/lib.go +++ b/lib.go @@ -48,16 +48,22 @@ func (f *File) ReadZipReader(r *zip.Reader) (map[string][]byte, int, error) { fileName = partName } if strings.EqualFold(fileName, defaultXMLPathSharedStrings) && fileSize > f.options.UnzipXMLSizeLimit { - if tempFile, err := f.unzipToTemp(v); err == nil { + tempFile, err := f.unzipToTemp(v) + if tempFile != "" { f.tempFiles.Store(fileName, tempFile) + } + if err == nil { continue } } if strings.HasPrefix(strings.ToLower(fileName), "xl/worksheets/sheet") { worksheets++ if fileSize > f.options.UnzipXMLSizeLimit && !v.FileInfo().IsDir() { - if tempFile, err := f.unzipToTemp(v); err == nil { + tempFile, err := f.unzipToTemp(v) + if tempFile != "" { f.tempFiles.Store(fileName, tempFile) + } + if err == nil { continue } } diff --git a/picture.go b/picture.go index c289850727..b263aa1328 100644 --- a/picture.go +++ b/picture.go @@ -468,8 +468,7 @@ func (f *File) GetPictures(sheet, cell string) ([]Picture, error) { } // 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. +// worksheet name and cell reference. func (f *File) DeletePicture(sheet, cell string) error { col, row, err := CellNameToCoordinates(cell) if err != nil { @@ -485,7 +484,36 @@ func (f *File) DeletePicture(sheet, cell string) error { return err } drawingXML := strings.ReplaceAll(f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID), "..", "xl") - return f.deleteDrawing(col, row, drawingXML, "Pic") + drawingRels := "xl/drawings/_rels/" + filepath.Base(drawingXML) + ".rels" + rID, err := f.deleteDrawing(col, row, drawingXML, "Pic") + if err != nil { + return err + } + rels := f.getDrawingRelationships(drawingRels, rID) + if rels == nil { + return err + } + var used bool + f.Pkg.Range(func(k, v interface{}) bool { + if strings.Contains(k.(string), "xl/drawings/_rels/") { + r, err := f.relsReader(k.(string)) + if err != nil { + return true + } + for _, rel := range r.Relationships { + if rel.ID != rels.ID && rel.Type == SourceRelationshipImage && + filepath.Base(rel.Target) == filepath.Base(rels.Target) { + used = true + } + } + } + return true + }) + if !used { + f.Pkg.Delete(strings.Replace(rels.Target, "../", "xl/", -1)) + } + f.deleteDrawingRels(drawingRels, rID) + return err } // getPicture provides a function to get picture base name and raw content diff --git a/picture_test.go b/picture_test.go index cd070d5ccf..8e7e3a9787 100644 --- a/picture_test.go +++ b/picture_test.go @@ -240,10 +240,25 @@ func TestAddPictureFromBytes(t *testing.T) { func TestDeletePicture(t *testing.T) { f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) assert.NoError(t, err) + // Test delete picture on a worksheet which does not contains any pictures assert.NoError(t, f.DeletePicture("Sheet1", "A1")) - assert.NoError(t, f.AddPicture("Sheet1", "P1", filepath.Join("test", "images", "excel.jpg"), nil)) - assert.NoError(t, f.DeletePicture("Sheet1", "P1")) + // Add same pictures on different worksheets + assert.NoError(t, f.AddPicture("Sheet1", "F20", filepath.Join("test", "images", "excel.jpg"), nil)) + assert.NoError(t, f.AddPicture("Sheet1", "I20", filepath.Join("test", "images", "excel.jpg"), nil)) + assert.NoError(t, f.AddPicture("Sheet2", "F1", filepath.Join("test", "images", "excel.jpg"), nil)) + // Test delete picture on a worksheet, the images should be preserved + assert.NoError(t, f.DeletePicture("Sheet1", "F20")) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDeletePicture.xlsx"))) + assert.NoError(t, f.Close()) + + f, err = OpenFile(filepath.Join("test", "TestDeletePicture.xlsx")) + assert.NoError(t, err) + // Test delete same picture on different worksheet, the images should be removed + assert.NoError(t, f.DeletePicture("Sheet1", "F10")) + assert.NoError(t, f.DeletePicture("Sheet2", "F1")) + assert.NoError(t, f.DeletePicture("Sheet1", "I20")) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDeletePicture2.xlsx"))) + // Test delete picture on not exists worksheet assert.EqualError(t, f.DeletePicture("SheetN", "A1"), "sheet SheetN does not exist") // Test delete picture with invalid sheet name @@ -253,6 +268,32 @@ func TestDeletePicture(t *testing.T) { assert.NoError(t, f.Close()) // Test delete picture on no chart worksheet assert.NoError(t, NewFile().DeletePicture("Sheet1", "A1")) + + f, err = OpenFile(filepath.Join("test", "TestDeletePicture.xlsx")) + assert.NoError(t, err) + // Test delete picture with unsupported charset drawing + f.Pkg.Store("xl/drawings/drawing1.xml", MacintoshCyrillicCharset) + assert.EqualError(t, f.DeletePicture("Sheet1", "F10"), "XML syntax error on line 1: invalid UTF-8") + assert.NoError(t, f.Close()) + + f, err = OpenFile(filepath.Join("test", "TestDeletePicture.xlsx")) + assert.NoError(t, err) + // Test delete picture with unsupported charset drawing relationships + f.Relationships.Delete("xl/drawings/_rels/drawing1.xml.rels") + f.Pkg.Store("xl/drawings/_rels/drawing1.xml.rels", MacintoshCyrillicCharset) + assert.NoError(t, f.DeletePicture("Sheet2", "F1")) + assert.NoError(t, f.Close()) + + f = NewFile() + assert.NoError(t, err) + assert.NoError(t, f.AddPicture("Sheet1", "A1", filepath.Join("test", "images", "excel.jpg"), nil)) + assert.NoError(t, f.AddPicture("Sheet1", "G1", filepath.Join("test", "images", "excel.jpg"), nil)) + drawing, ok := f.Drawings.Load("xl/drawings/drawing1.xml") + assert.True(t, ok) + // Made two picture reference the same drawing relationship ID + drawing.(*xlsxWsDr).TwoCellAnchor[1].Pic.BlipFill.Blip.Embed = "rId1" + assert.NoError(t, f.DeletePicture("Sheet1", "A1")) + assert.NoError(t, f.Close()) } func TestDrawingResize(t *testing.T) { From d133dc12d774dcf944b36720f25e1dd945dda63f Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 10 Oct 2023 00:04:10 +0800 Subject: [PATCH 805/957] Support format cell value with fraction number format code - Fix delete incorrect image files when picture deleting pictures - Update the unit test and dependencies modules --- excelize_test.go | 8 +++--- go.mod | 6 ++-- go.sum | 16 +++++------ lib.go | 25 ++++++++++++++++ numfmt.go | 74 +++++++++++++++++++++++++++++++++++++++--------- numfmt_test.go | 10 +++++++ picture.go | 21 +++++++++++--- 7 files changed, 127 insertions(+), 33 deletions(-) diff --git a/excelize_test.go b/excelize_test.go index 51d9206c50..5eb19aa66d 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -741,10 +741,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 ", "37,948", "$37,948", "37,947.75", "$37,947.75", "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)", "(37,948)", "$(37,948)", "(37,947.75)", "$(37,947.75)", "-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", "12:10 AM", "12:10:05 AM", "00:10", "00:10:05", "12/30/99 00:10", "0 ", "0 ", "0.01 ", "0.01 ", "0", "$0", "0.01", "$0.01", "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", "$2", "2.10", "$2.10", "24:00", "50:24:00", "24:00.0", "2.1", "2.1"}, + {"37947.7500001", "37948", "37947.75", "37,948", "37,947.75", "3794775%", "3794775.00%", "3.79E+04", "37947 3/4", "37947 3/4", "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 ", "37,948", "$37,948", "37,947.75", "$37,947.75", "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 3/4", "-37947 3/4", "-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)", "(37,948)", "$(37,948)", "(37,947.75)", "$(37,947.75)", "-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 ", "0 ", "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", "$0", "0.01", "$0.01", "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/9", "2 1/10", "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", "$2", "2.10", "$2.10", "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 1c30e606e5..4dc20b1d3e 100644 --- a/go.mod +++ b/go.mod @@ -8,9 +8,9 @@ require ( github.com/richardlehane/msoleps v1.0.3 // indirect github.com/stretchr/testify v1.8.0 github.com/xuri/efp v0.0.0-20230802181842-ad255f2331ca - github.com/xuri/nfp v0.0.0-20230918160701-e5a3f5b24785 - golang.org/x/crypto v0.13.0 + github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05 + golang.org/x/crypto v0.14.0 golang.org/x/image v0.11.0 - golang.org/x/net v0.15.0 + golang.org/x/net v0.16.0 golang.org/x/text v0.13.0 ) diff --git a/go.sum b/go.sum index 3b314f28e6..1d4dd9e86d 100644 --- a/go.sum +++ b/go.sum @@ -17,13 +17,13 @@ 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-20230802181842-ad255f2331ca h1:uvPMDVyP7PXMMioYdyPH+0O+Ta/UO1WFfNYMO3Wz0eg= github.com/xuri/efp v0.0.0-20230802181842-ad255f2331ca/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= -github.com/xuri/nfp v0.0.0-20230918160701-e5a3f5b24785 h1:FG9hcK7lhf3w/Y2NRUKy/mopsH0Oy6P1rib1KWXAie0= -github.com/xuri/nfp v0.0.0-20230918160701-e5a3f5b24785/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= +github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05 h1:qhbILQo1K3mphbwKh1vNm4oGezE1eF9fQWmNiIpSfI4= +github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05/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= -golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/image v0.11.0 h1:ds2RoQvBvYTiJkwpSFDwCcDFNX7DqjL2WsUgTNk0Ooo= golang.org/x/image v0.11.0/go.mod h1:bglhjqbqVuEb9e9+eNR45Jfu7D+T4Qan+NhQk8Ck2P8= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -33,8 +33,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v 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.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos= +golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= 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= @@ -45,12 +45,12 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc 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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.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.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= 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/lib.go b/lib.go index 83c28e7c0b..417ce7965e 100644 --- a/lib.go +++ b/lib.go @@ -18,6 +18,7 @@ import ( "encoding/xml" "fmt" "io" + "math" "math/big" "os" "regexp" @@ -825,6 +826,30 @@ func bstrMarshal(s string) (result string) { return result } +// newRat converts decimals to rational fractions with the required precision. +func newRat(n float64, iterations int64, prec float64) *big.Rat { + x := int64(math.Floor(n)) + y := n - float64(x) + rat := continuedFraction(y, 1, iterations, prec) + return rat.Add(rat, new(big.Rat).SetInt64(x)) +} + +// continuedFraction returns rational from decimal with the continued fraction +// algorithm. +func continuedFraction(n float64, i int64, limit int64, prec float64) *big.Rat { + if i >= limit || n <= prec { + return big.NewRat(0, 1) + } + inverted := 1 / n + y := int64(math.Floor(inverted)) + x := inverted - float64(y) + ratY := new(big.Rat).SetInt64(y) + ratNext := continuedFraction(x, i+1, limit, prec) + res := ratY.Add(ratY, ratNext) + res = res.Inv(res) + return res +} + // Stack defined an abstract data type that serves as a collection of elements. type Stack struct { list *list.List diff --git a/numfmt.go b/numfmt.go index 8ef69e0e72..a6f456e203 100644 --- a/numfmt.go +++ b/numfmt.go @@ -33,18 +33,18 @@ 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 - sectionIdx int - date1904, isNumeric, hours, seconds, useMillisecond, useGannen 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 + opts *Options + cellType CellType + section []nfp.Section + t time.Time + sectionIdx int + date1904, isNumeric, hours, seconds, useMillisecond, useGannen bool + number float64 + ap, localCode, result, value, valueSectionType string + switchArgument, currencyString string + fracHolder, fracPadding, intHolder, intPadding, expBaseLen int + percent int + useCommaSep, useFraction, usePointer, usePositive, useScientificNotation bool } // CultureName is the type of supported language country codes types for apply @@ -688,8 +688,11 @@ var ( nfp.TokenTypeCurrencyLanguage, nfp.TokenTypeDateTimes, nfp.TokenTypeDecimalPoint, + nfp.TokenTypeDenominator, + nfp.TokenTypeDigitalPlaceHolder, nfp.TokenTypeElapsedDateTimes, nfp.TokenTypeExponential, + nfp.TokenTypeFraction, nfp.TokenTypeGeneral, nfp.TokenTypeHashPlaceHolder, nfp.TokenTypeLiteral, @@ -702,7 +705,10 @@ var ( } // supportedNumberTokenTypes list the supported number token types. supportedNumberTokenTypes = []string{ + nfp.TokenTypeDenominator, + nfp.TokenTypeDigitalPlaceHolder, nfp.TokenTypeExponential, + nfp.TokenTypeFraction, nfp.TokenTypeHashPlaceHolder, nfp.TokenTypePercent, nfp.TokenTypeZeroPlaceHolder, @@ -4775,6 +4781,9 @@ func (nf *numberFormat) getNumberFmtConf() { if token.TType == nfp.TokenTypeDecimalPoint { nf.usePointer = true } + if token.TType == nfp.TokenTypeFraction { + nf.useFraction = true + } if token.TType == nfp.TokenTypeSwitchArgument { nf.switchArgument = token.TValue } @@ -4795,8 +4804,11 @@ func (nf *numberFormat) getNumberFmtConf() { // printNumberLiteral apply literal tokens for the pre-formatted text. func (nf *numberFormat) printNumberLiteral(text string) string { - var result string - var useLiteral, usePlaceHolder bool + var ( + result string + frac float64 + useFraction, useLiteral, usePlaceHolder bool + ) if nf.usePositive { result += "-" } @@ -4822,10 +4834,41 @@ func (nf *numberFormat) printNumberLiteral(text string) string { result += text } } + if token.TType == nfp.TokenTypeFraction { + _, frac = math.Modf(nf.number) + frac, useFraction = math.Abs(frac), true + } + if useFraction { + result += nf.fractionHandler(frac, token) + } } return nf.printSwitchArgument(result) } +// fractionHandler handling fraction number format expression for positive and +// negative numeric. +func (nf *numberFormat) fractionHandler(frac float64, token nfp.Token) string { + var rat, result string + if token.TType == nfp.TokenTypeDigitalPlaceHolder { + fracPlaceHolder := len(token.TValue) + for i := 0; i < 5000; i++ { + if r := newRat(frac, int64(i), 0); len(r.Denom().String()) <= fracPlaceHolder { + if rat = r.String(); strings.HasPrefix(rat, "0/") { + rat = strings.Repeat(" ", 3) + } + continue + } + break + } + result += rat + } + if token.TType == nfp.TokenTypeDenominator { + denom, _ := strconv.ParseFloat(token.TValue, 64) + result += fmt.Sprintf("%d/%d", int(math.Round(frac*denom)), int(math.Round(denom))) + } + return result +} + // printCommaSep format number with thousands separator. func printCommaSep(text string) string { var ( @@ -4929,6 +4972,9 @@ func (nf *numberFormat) numberHandler() string { if nf.percent > 0 { num *= math.Pow(100, float64(nf.percent)) } + if nf.useFraction { + num = math.Floor(math.Abs(num)) + } if result = fmt.Sprintf(fmtCode, math.Abs(num)); nf.useCommaSep { result = printCommaSep(result) } diff --git a/numfmt_test.go b/numfmt_test.go index 2433364bcb..45d7d0f0d0 100644 --- a/numfmt_test.go +++ b/numfmt_test.go @@ -3543,6 +3543,16 @@ 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%"}, + {"-123.4567", "# ?/?", "-123 1/2"}, + {"123.4567", "# ??/??", "123 37/81"}, + {"123.4567", "#\\ ???/???", "123 58/127"}, + {"123.4567", "#\\ ?/2", "123 1/2"}, + {"123.4567", "#\\ ?/4", "123 2/4"}, + {"123.4567", "#\\ ?/8", "123 4/8"}, + {"123.4567", "#\\ ?/16", "123 7/16"}, + {"123.4567", "#\\ ?/10", "123 5/10"}, + {"-123.4567", "#\\ ?/100", "-123 46/100"}, + {"123.4567", "#\\ ?/1000", "123 457/1000"}, {"1234.5678", "[$$-409]#,##0.00", "$1,234.57"}, // Unsupported number format {"37947.7500001", "0.00000000E+000", "37947.7500001"}, diff --git a/picture.go b/picture.go index b263aa1328..6016b8960f 100644 --- a/picture.go +++ b/picture.go @@ -231,7 +231,18 @@ func (f *File) AddPictureFromBytes(sheet, cell string, pic *Picture) error { drawingID, drawingXML = f.prepareDrawing(ws, drawingID, sheet, drawingXML) drawingRels := "xl/drawings/_rels/drawing" + strconv.Itoa(drawingID) + ".xml.rels" mediaStr := ".." + strings.TrimPrefix(f.addMedia(pic.File, ext), "xl") - drawingRID := f.addRels(drawingRels, SourceRelationshipImage, mediaStr, hyperlinkType) + var drawingRID int + if rels, _ := f.relsReader(drawingRels); rels != nil { + for _, rel := range rels.Relationships { + if rel.Type == SourceRelationshipImage && rel.Target == mediaStr { + drawingRID, _ = strconv.Atoi(strings.TrimPrefix(rel.ID, "rId")) + break + } + } + } + if drawingRID == 0 { + drawingRID = f.addRels(drawingRels, SourceRelationshipImage, mediaStr, hyperlinkType) + } // Add picture with hyperlink. if options.Hyperlink != "" && options.HyperlinkType != "" { if options.HyperlinkType == "External" { @@ -494,8 +505,8 @@ func (f *File) DeletePicture(sheet, cell string) error { return err } var used bool - f.Pkg.Range(func(k, v interface{}) bool { - if strings.Contains(k.(string), "xl/drawings/_rels/") { + checkPicRef := func(k, v interface{}) bool { + if strings.Contains(k.(string), "xl/drawings/_rels/drawing") { r, err := f.relsReader(k.(string)) if err != nil { return true @@ -508,7 +519,9 @@ func (f *File) DeletePicture(sheet, cell string) error { } } return true - }) + } + f.Relationships.Range(checkPicRef) + f.Pkg.Range(checkPicRef) if !used { f.Pkg.Delete(strings.Replace(rels.Target, "../", "xl/", -1)) } From d9a0da7b48bac4175a23193a60f973c64d27835f Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 11 Oct 2023 00:04:38 +0800 Subject: [PATCH 806/957] This closes #1687 and closes #1688 - Using sync map internally to get cell value concurrency safe - Support set the height and width for the comment box - Update the unit test --- cell.go | 9 ++++---- cell_test.go | 14 ++++++------- col_test.go | 3 ++- excelize.go | 23 +++++++++++---------- excelize_test.go | 3 ++- lib.go | 33 +++++++++++++++++++----------- lib_test.go | 6 ++++-- rows_test.go | 2 +- sheet.go | 12 +++++------ sheet_test.go | 7 ++++--- stream.go | 2 +- styles.go | 2 +- vml.go | 53 ++++++++++++++++++++++-------------------------- vmlDrawing.go | 2 -- vml_test.go | 2 +- workbook.go | 8 ++++++-- xmlComments.go | 2 ++ 17 files changed, 99 insertions(+), 84 deletions(-) diff --git a/cell.go b/cell.go index c56b58767b..dd9980d9f4 100644 --- a/cell.go +++ b/cell.go @@ -1317,10 +1317,15 @@ func (ws *xlsxWorksheet) prepareCell(cell string) (*xlsxC, int, int, error) { // 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) { + 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() cell, err = ws.mergeCellsParser(cell) if err != nil { return "", err @@ -1329,10 +1334,6 @@ func (f *File) getCellStringFunc(sheet, cell string, fn func(x *xlsxWorksheet, c if err != nil { return "", err } - - ws.mu.Lock() - defer ws.mu.Unlock() - lastRowNum := 0 if l := len(ws.SheetData.Row); l > 0 { lastRowNum = ws.SheetData.Row[l-1].R diff --git a/cell_test.go b/cell_test.go index a4a2ddf53d..1307688830 100644 --- a/cell_test.go +++ b/cell_test.go @@ -313,7 +313,7 @@ func TestGetCellValue(t *testing.T) { f.Sheet.Delete("xl/worksheets/sheet1.xml") f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `A3A4B4A7B7A8B8`))) - f.checked = nil + f.checked = sync.Map{} cells := []string{"A3", "A4", "B4", "A7", "B7"} rows, err := f.GetRows("Sheet1") assert.Equal(t, [][]string{nil, nil, {"A3"}, {"A4", "B4"}, nil, nil, {"A7", "B7"}, {"A8", "B8"}}, rows) @@ -329,35 +329,35 @@ func TestGetCellValue(t *testing.T) { f.Sheet.Delete("xl/worksheets/sheet1.xml") f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `A2B2`))) - f.checked = nil + f.checked = sync.Map{} 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.checked = nil + f.checked = sync.Map{} 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.checked = nil + f.checked = sync.Map{} 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.checked = nil + f.checked = sync.Map{} 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.checked = nil + f.checked = sync.Map{} cell, err = f.GetCellValue("Sheet1", "H6") assert.Equal(t, "H6", cell) assert.NoError(t, err) @@ -410,7 +410,7 @@ func TestGetCellValue(t *testing.T) { 20221022T150529Z 2022-10-22T15:05:29Z 2020-07-10 15:00:00.000`))) - f.checked = nil + f.checked = sync.Map{} rows, err = f.GetCols("Sheet1") assert.Equal(t, []string{ "2422.3", diff --git a/col_test.go b/col_test.go index f1fe032f82..9af8ca03b9 100644 --- a/col_test.go +++ b/col_test.go @@ -2,6 +2,7 @@ package excelize import ( "path/filepath" + "sync" "testing" "github.com/stretchr/testify/assert" @@ -125,7 +126,7 @@ func TestGetColsError(t *testing.T) { f = NewFile() f.Sheet.Delete("xl/worksheets/sheet1.xml") f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`B`)) - f.checked = nil + f.checked = sync.Map{} _, err = f.GetCols("Sheet1") assert.EqualError(t, err, `strconv.Atoi: parsing "A": invalid syntax`) diff --git a/excelize.go b/excelize.go index 0f1073fced..80ba1d3e9e 100644 --- a/excelize.go +++ b/excelize.go @@ -30,8 +30,8 @@ import ( type File struct { mu sync.Mutex options *Options - xmlAttr map[string][]xml.Attr - checked map[string]bool + xmlAttr sync.Map + checked sync.Map sheetMap map[string]string streams map[string]*StreamWriter tempFiles sync.Map @@ -133,8 +133,8 @@ func OpenFile(filename string, opts ...Options) (*File, error) { func newFile() *File { return &File{ options: &Options{UnzipSizeLimit: UnzipSizeLimit, UnzipXMLSizeLimit: StreamChunkSize}, - xmlAttr: make(map[string][]xml.Attr), - checked: make(map[string]bool), + xmlAttr: sync.Map{}, + checked: sync.Map{}, sheetMap: make(map[string]string), tempFiles: sync.Map{}, Comments: make(map[string]*xlsxComments), @@ -275,24 +275,25 @@ func (f *File) workSheetReader(sheet string) (ws *xlsxWorksheet, err error) { } } ws = new(xlsxWorksheet) - if _, ok := f.xmlAttr[name]; !ok { + if attrs, ok := f.xmlAttr.Load(name); !ok { d := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readBytes(name)))) - f.xmlAttr[name] = append(f.xmlAttr[name], getRootElement(d)...) + if attrs == nil { + attrs = []xml.Attr{} + } + attrs = append(attrs.([]xml.Attr), getRootElement(d)...) + f.xmlAttr.Store(name, attrs) } if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readBytes(name)))). Decode(ws); err != nil && err != io.EOF { return } err = nil - if f.checked == nil { - f.checked = make(map[string]bool) - } - if ok = f.checked[name]; !ok { + if _, ok = f.checked.Load(name); !ok { ws.checkSheet() if err = ws.checkRow(); err != nil { return } - f.checked[name] = true + f.checked.Store(name, true) } f.Sheet.Store(name, ws) return diff --git a/excelize_test.go b/excelize_test.go index 5eb19aa66d..e1c8401cee 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -16,6 +16,7 @@ import ( "path/filepath" "strconv" "strings" + "sync" "testing" "time" @@ -1531,7 +1532,7 @@ func TestWorkSheetReader(t *testing.T) { f = NewFile() f.Sheet.Delete("xl/worksheets/sheet1.xml") f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(``)) - f.checked = nil + f.checked = sync.Map{} _, err = f.workSheetReader("Sheet1") assert.NoError(t, err) } diff --git a/lib.go b/lib.go index 417ce7965e..a69446312f 100644 --- a/lib.go +++ b/lib.go @@ -682,8 +682,8 @@ func getXMLNamespace(space string, attr []xml.Attr) string { func (f *File) replaceNameSpaceBytes(path string, contentMarshal []byte) []byte { sourceXmlns := []byte(`xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">`) targetXmlns := []byte(templateNamespaceIDMap) - if attr, ok := f.xmlAttr[path]; ok { - targetXmlns = []byte(genXMLNamespace(attr)) + if attrs, ok := f.xmlAttr.Load(path); ok { + targetXmlns = []byte(genXMLNamespace(attrs.([]xml.Attr))) } return bytesReplace(contentMarshal, sourceXmlns, bytes.ReplaceAll(targetXmlns, []byte(" mc:Ignorable=\"r\""), []byte{}), -1) } @@ -694,29 +694,36 @@ func (f *File) addNameSpaces(path string, ns xml.Attr) { exist := false mc := false ignore := -1 - if attr, ok := f.xmlAttr[path]; ok { - for i, attribute := range attr { - if attribute.Name.Local == ns.Name.Local && attribute.Name.Space == ns.Name.Space { + if attrs, ok := f.xmlAttr.Load(path); ok { + for i, attr := range attrs.([]xml.Attr) { + if attr.Name.Local == ns.Name.Local && attr.Name.Space == ns.Name.Space { exist = true } - if attribute.Name.Local == "Ignorable" && getXMLNamespace(attribute.Name.Space, attr) == "mc" { + if attr.Name.Local == "Ignorable" && getXMLNamespace(attr.Name.Space, attrs.([]xml.Attr)) == "mc" { ignore = i } - if attribute.Name.Local == "mc" && attribute.Name.Space == "xmlns" { + if attr.Name.Local == "mc" && attr.Name.Space == "xmlns" { mc = true } } } if !exist { - f.xmlAttr[path] = append(f.xmlAttr[path], ns) + attrs, _ := f.xmlAttr.Load(path) + if attrs == nil { + attrs = []xml.Attr{} + } + attrs = append(attrs.([]xml.Attr), ns) + f.xmlAttr.Store(path, attrs) if !mc { - f.xmlAttr[path] = append(f.xmlAttr[path], SourceRelationshipCompatibility) + attrs = append(attrs.([]xml.Attr), SourceRelationshipCompatibility) + f.xmlAttr.Store(path, attrs) } if ignore == -1 { - f.xmlAttr[path] = append(f.xmlAttr[path], xml.Attr{ + attrs = append(attrs.([]xml.Attr), xml.Attr{ Name: xml.Name{Local: "Ignorable", Space: "mc"}, Value: ns.Name.Local, }) + f.xmlAttr.Store(path, attrs) return } f.setIgnorableNameSpace(path, ignore, ns) @@ -727,8 +734,10 @@ func (f *File) addNameSpaces(path string, ns xml.Attr) { // by the given attribute. func (f *File) setIgnorableNameSpace(path string, index int, ns xml.Attr) { ignorableNS := []string{"c14", "cdr14", "a14", "pic14", "x14", "xdr14", "x14ac", "dsp", "mso14", "dgm14", "x15", "x12ac", "x15ac", "xr", "xr2", "xr3", "xr4", "xr5", "xr6", "xr7", "xr8", "xr9", "xr10", "xr11", "xr12", "xr13", "xr14", "xr15", "x15", "x16", "x16r2", "mo", "mx", "mv", "o", "v"} - if inStrSlice(strings.Fields(f.xmlAttr[path][index].Value), ns.Name.Local, true) == -1 && inStrSlice(ignorableNS, ns.Name.Local, true) != -1 { - f.xmlAttr[path][index].Value = strings.TrimSpace(fmt.Sprintf("%s %s", f.xmlAttr[path][index].Value, ns.Name.Local)) + xmlAttrs, _ := f.xmlAttr.Load(path) + if inStrSlice(strings.Fields(xmlAttrs.([]xml.Attr)[index].Value), ns.Name.Local, true) == -1 && inStrSlice(ignorableNS, ns.Name.Local, true) != -1 { + xmlAttrs.([]xml.Attr)[index].Value = strings.TrimSpace(fmt.Sprintf("%s %s", xmlAttrs.([]xml.Attr)[index].Value, ns.Name.Local)) + f.xmlAttr.Store(path, xmlAttrs) } } diff --git a/lib_test.go b/lib_test.go index 46650e5fc5..48e730d0f1 100644 --- a/lib_test.go +++ b/lib_test.go @@ -294,9 +294,11 @@ func TestGetRootElement(t *testing.T) { func TestSetIgnorableNameSpace(t *testing.T) { f := NewFile() - f.xmlAttr["xml_path"] = []xml.Attr{{}} + f.xmlAttr.Store("xml_path", []xml.Attr{{}}) f.setIgnorableNameSpace("xml_path", 0, xml.Attr{Name: xml.Name{Local: "c14"}}) - assert.EqualValues(t, "c14", f.xmlAttr["xml_path"][0].Value) + attrs, ok := f.xmlAttr.Load("xml_path") + assert.EqualValues(t, "c14", attrs.([]xml.Attr)[0].Value) + assert.True(t, ok) } func TestStack(t *testing.T) { diff --git a/rows_test.go b/rows_test.go index 1cba0a75d3..768f8b01c1 100644 --- a/rows_test.go +++ b/rows_test.go @@ -944,7 +944,7 @@ func TestCheckRow(t *testing.T) { f = NewFile() f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(xml.Header+`12345`)) f.Sheet.Delete("xl/worksheets/sheet1.xml") - delete(f.checked, "xl/worksheets/sheet1.xml") + f.checked.Delete("xl/worksheets/sheet1.xml") assert.EqualError(t, f.SetCellValue("Sheet1", "A1", false), newCellNameToCoordinatesError("-", newInvalidCellNameError("-")).Error()) } diff --git a/sheet.go b/sheet.go index 8f678b5150..5672484eb1 100644 --- a/sheet.go +++ b/sheet.go @@ -164,10 +164,10 @@ func (f *File) workSheetWriter() { // reusing buffer _ = encoder.Encode(sheet) f.saveFileList(p.(string), replaceRelationshipsBytes(f.replaceNameSpaceBytes(p.(string), buffer.Bytes()))) - ok := f.checked[p.(string)] + _, ok := f.checked.Load(p.(string)) if ok { f.Sheet.Delete(p.(string)) - f.checked[p.(string)] = false + f.checked.Store(p.(string), false) } buffer.Reset() } @@ -237,7 +237,7 @@ func (f *File) setSheet(index int, name string) { sheetXMLPath := "xl/worksheets/sheet" + strconv.Itoa(index) + ".xml" f.sheetMap[name] = sheetXMLPath f.Sheet.Store(sheetXMLPath, &ws) - f.xmlAttr[sheetXMLPath] = []xml.Attr{NameSpaceSpreadSheet} + f.xmlAttr.Store(sheetXMLPath, []xml.Attr{NameSpaceSpreadSheet}) } // relsWriter provides a function to save relationships after @@ -583,7 +583,7 @@ func (f *File) DeleteSheet(sheet string) error { f.Pkg.Delete(rels) f.Relationships.Delete(rels) f.Sheet.Delete(sheetXML) - delete(f.xmlAttr, sheetXML) + f.xmlAttr.Delete(sheetXML) f.SheetCount-- } index, err := f.GetSheetIndex(activeSheetName) @@ -714,8 +714,8 @@ func (f *File) copySheet(from, to int) error { f.Pkg.Store(toRels, rels.([]byte)) } fromSheetXMLPath, _ := f.getSheetXMLPath(fromSheet) - fromSheetAttr := f.xmlAttr[fromSheetXMLPath] - f.xmlAttr[sheetXMLPath] = fromSheetAttr + fromSheetAttr, _ := f.xmlAttr.Load(fromSheetXMLPath) + f.xmlAttr.Store(sheetXMLPath, fromSheetAttr) return err } diff --git a/sheet_test.go b/sheet_test.go index 6851dfcc78..bb9a7862fa 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -8,6 +8,7 @@ import ( "path/filepath" "strconv" "strings" + "sync" "testing" "github.com/stretchr/testify/assert" @@ -120,7 +121,7 @@ func TestPanes(t *testing.T) { // Test add pane on empty sheet views worksheet f = NewFile() - f.checked = nil + f.checked = sync.Map{} f.Sheet.Delete("xl/worksheets/sheet1.xml") f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(``)) assert.NoError(t, f.SetPanes("Sheet1", @@ -173,7 +174,7 @@ func TestSearchSheet(t *testing.T) { f = NewFile() f.Sheet.Delete("xl/worksheets/sheet1.xml") f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`A`)) - f.checked = nil + f.checked = sync.Map{} result, err = f.SearchSheet("Sheet1", "A") assert.EqualError(t, err, "strconv.Atoi: parsing \"A\": invalid syntax") assert.Equal(t, []string(nil), result) @@ -462,7 +463,7 @@ func TestWorksheetWriter(t *testing.T) { f.Sheet.Delete("xl/worksheets/sheet1.xml") worksheet := xml.Header + `%d` f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(worksheet, 1))) - f.checked = nil + f.checked = sync.Map{} assert.NoError(t, f.SetCellValue("Sheet1", "A1", 2)) f.workSheetWriter() value, ok := f.Pkg.Load("xl/worksheets/sheet1.xml") diff --git a/stream.go b/stream.go index fe637b9650..2247bcb8cc 100644 --- a/stream.go +++ b/stream.go @@ -678,7 +678,7 @@ func (sw *StreamWriter) Flush() error { sheetPath := sw.file.sheetMap[sw.Sheet] sw.file.Sheet.Delete(sheetPath) - delete(sw.file.checked, sheetPath) + sw.file.checked.Delete(sheetPath) sw.file.Pkg.Delete(sheetPath) return nil diff --git a/styles.go b/styles.go index 40505b7b9e..433cb8d19b 100644 --- a/styles.go +++ b/styles.go @@ -2161,7 +2161,7 @@ func (f *File) SetCellStyle(sheet, hCell, vCell string, styleID int) error { // // 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: +// applied to {Type: "cell"} are: // // between | // not between | diff --git a/vml.go b/vml.go index 866019376d..993f2f1c85 100644 --- a/vml.go +++ b/vml.go @@ -94,23 +94,33 @@ func (f *File) getSheetComments(sheetFile string) string { return "" } -// AddComment provides the method to add comment in a sheet by given worksheet -// name, cell reference and format set (such as author and text). Note that the -// max author length is 255 and the max text length is 32512. For example, add -// a comment in Sheet1!$A$30: +// AddComment provides the method to add comments in a sheet by giving the +// worksheet name, cell reference, and format set (such as author and text). +// Note that the maximum author name length is 255 and the max text length is +// 32512. For example, add a rich-text comment with a specified comments box +// size in Sheet1!A5: // // err := f.AddComment("Sheet1", excelize.Comment{ -// Cell: "A12", +// Cell: "A5", // Author: "Excelize", // Paragraph: []excelize.RichTextRun{ // {Text: "Excelize: ", Font: &excelize.Font{Bold: true}}, // {Text: "This is a comment."}, // }, +// Height: 40, +// Width: 180, // }) func (f *File) AddComment(sheet string, opts Comment) error { return f.addVMLObject(vmlOptions{ sheet: sheet, Comment: opts, - FormControl: FormControl{Cell: opts.Cell, Type: FormControlNote}, + FormControl: FormControl{ + Cell: opts.Cell, + Type: FormControlNote, + Text: opts.Text, + Paragraph: opts.Paragraph, + Width: opts.Width, + Height: opts.Height, + }, }) } @@ -529,31 +539,17 @@ func (f *File) addVMLObject(opts vmlOptions) error { // prepareFormCtrlOptions provides a function to parse the format settings of // the form control with default value. func prepareFormCtrlOptions(opts *vmlOptions) *vmlOptions { - for _, runs := range opts.FormControl.Paragraph { - for _, subStr := range strings.Split(runs.Text, "\n") { - opts.rows++ - if chars := len(subStr); chars > opts.cols { - opts.cols = chars - } - } - } - if len(opts.FormControl.Paragraph) == 0 { - opts.rows, opts.cols = 1, len(opts.FormControl.Text) - } if opts.Format.ScaleX == 0 { opts.Format.ScaleX = 1 } if opts.Format.ScaleY == 0 { opts.Format.ScaleY = 1 } - if opts.cols == 0 { - opts.cols = 8 - } - if opts.Width == 0 { - opts.Width = uint(opts.cols * 9) + if opts.FormControl.Width == 0 { + opts.FormControl.Width = 140 } - if opts.Height == 0 { - opts.Height = uint(opts.rows * 25) + if opts.FormControl.Height == 0 { + opts.FormControl.Height = 60 } return opts } @@ -818,15 +814,14 @@ func (f *File) addDrawingVML(dataID int, drawingVML string, opts *vmlOptions) er if err != nil { return err } - anchor := fmt.Sprintf("%d, 23, %d, 0, %d, %d, %d, 5", col, row, col+opts.rows+2, col+opts.cols-1, row+opts.rows+2) - vmlID, vml, preset := 202, f.VMLDrawing[drawingVML], formCtrlPresets[opts.Type] + leftOffset, vmlID, vml, preset := 23, 202, f.VMLDrawing[drawingVML], formCtrlPresets[opts.Type] style := "position:absolute;73.5pt;width:108pt;height:59.25pt;z-index:1;visibility:hidden" if opts.formCtrl { - vmlID = 201 + leftOffset, vmlID = 0, 201 style = "position:absolute;73.5pt;width:108pt;height:59.25pt;z-index:1;mso-wrap-style:tight" - colStart, rowStart, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(opts.sheet, col, row, opts.Format.OffsetX, opts.Format.OffsetY, int(opts.Width), int(opts.Height)) - anchor = fmt.Sprintf("%d, 0, %d, 0, %d, %d, %d, %d", colStart, rowStart, colEnd, x2, rowEnd, y2) } + colStart, rowStart, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(opts.sheet, col, row, opts.Format.OffsetX, opts.Format.OffsetY, int(opts.FormControl.Width), int(opts.FormControl.Height)) + anchor := fmt.Sprintf("%d, %d, %d, 0, %d, %d, %d, %d", colStart, leftOffset, rowStart, colEnd, x2, rowEnd, y2) if vml == nil { vml = &vmlDrawing{ XMLNSv: "urn:schemas-microsoft-com:vml", diff --git a/vmlDrawing.go b/vmlDrawing.go index 7ebedb7aeb..fa293be5cf 100644 --- a/vmlDrawing.go +++ b/vmlDrawing.go @@ -276,8 +276,6 @@ type formCtrlPreset struct { // vmlOptions defines the structure used to internal comments and form controls. type vmlOptions struct { - rows int - cols int formCtrl bool sheet string Comment diff --git a/vml_test.go b/vml_test.go index 5491454c28..8e38fbeb8f 100644 --- a/vml_test.go +++ b/vml_test.go @@ -153,7 +153,7 @@ func TestAddDrawingVML(t *testing.T) { assert.Equal(t, f.addDrawingVML(0, "", &vmlOptions{FormControl: FormControl{Cell: "*"}}), newCellNameToCoordinatesError("*", newInvalidCellNameError("*"))) f.Pkg.Store("xl/drawings/vmlDrawing1.vml", MacintoshCyrillicCharset) - assert.EqualError(t, f.addDrawingVML(0, "xl/drawings/vmlDrawing1.vml", &vmlOptions{FormControl: FormControl{Cell: "A1"}}), "XML syntax error on line 1: invalid UTF-8") + assert.EqualError(t, f.addDrawingVML(0, "xl/drawings/vmlDrawing1.vml", &vmlOptions{sheet: "Sheet1", FormControl: FormControl{Cell: "A1"}}), "XML syntax error on line 1: invalid UTF-8") } func TestFormControl(t *testing.T) { diff --git a/workbook.go b/workbook.go index 0e635d29ce..5dfe9d2555 100644 --- a/workbook.go +++ b/workbook.go @@ -197,9 +197,13 @@ func (f *File) workbookReader() (*xlsxWorkbook, error) { if f.WorkBook == nil { wbPath := f.getWorkbookPath() f.WorkBook = new(xlsxWorkbook) - if _, ok := f.xmlAttr[wbPath]; !ok { + if attrs, ok := f.xmlAttr.Load(wbPath); !ok { d := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(wbPath)))) - f.xmlAttr[wbPath] = append(f.xmlAttr[wbPath], getRootElement(d)...) + if attrs == nil { + attrs = []xml.Attr{} + } + attrs = append(attrs.([]xml.Attr), getRootElement(d)...) + f.xmlAttr.Store(wbPath, attrs) f.addNameSpaces(wbPath, SourceRelationship) } if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(wbPath)))). diff --git a/xmlComments.go b/xmlComments.go index c27cd70e4c..a28f5cc31d 100644 --- a/xmlComments.go +++ b/xmlComments.go @@ -78,5 +78,7 @@ type Comment struct { AuthorID int Cell string Text string + Width uint + Height uint Paragraph []RichTextRun } From f752f2ddf45c3a1655905e663d487a801b0057f3 Mon Sep 17 00:00:00 2001 From: Eng Zer Jun Date: Thu, 12 Oct 2023 09:23:31 +0800 Subject: [PATCH 807/957] Remove redundant `len` check in `GroupSheets` and `UngroupSheets` (#1685) --- sheet.go | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/sheet.go b/sheet.go index 5672484eb1..6166773dc0 100644 --- a/sheet.go +++ b/sheet.go @@ -1739,11 +1739,8 @@ func (f *File) GroupSheets(sheets []string) error { } for _, ws := range wss { sheetViews := ws.SheetViews.SheetView - if len(sheetViews) > 0 { - for idx := range sheetViews { - ws.SheetViews.SheetView[idx].TabSelected = true - } - continue + for idx := range sheetViews { + ws.SheetViews.SheetView[idx].TabSelected = true } } return nil @@ -1758,10 +1755,8 @@ func (f *File) UngroupSheets() error { } ws, _ := f.workSheetReader(sheet) sheetViews := ws.SheetViews.SheetView - if len(sheetViews) > 0 { - for idx := range sheetViews { - ws.SheetViews.SheetView[idx].TabSelected = false - } + for idx := range sheetViews { + ws.SheetViews.SheetView[idx].TabSelected = false } } return nil From 27f105692942db78f83295af6536f33bafdeb726 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: Fri, 13 Oct 2023 00:06:07 +0800 Subject: [PATCH 808/957] This closes #1218 and closes #1689 (#1634) - Introduce new exported function GetPictureCells - Upgrade dependencies module golang.org/x/net from 0.16.0 to 0.17.0 - Update unit tests --- go.mod | 2 +- go.sum | 4 +- picture.go | 166 ++++++++++++++++++++++++++++++++++-------------- picture_test.go | 52 ++++++++++++++- 4 files changed, 173 insertions(+), 51 deletions(-) diff --git a/go.mod b/go.mod index 4dc20b1d3e..32cb416a3d 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,6 @@ require ( github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05 golang.org/x/crypto v0.14.0 golang.org/x/image v0.11.0 - golang.org/x/net v0.16.0 + golang.org/x/net v0.17.0 golang.org/x/text v0.13.0 ) diff --git a/go.sum b/go.sum index 1d4dd9e86d..23e1c02f52 100644 --- a/go.sum +++ b/go.sum @@ -33,8 +33,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v 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.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos= -golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= 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= diff --git a/picture.go b/picture.go index 6016b8960f..46a2c39447 100644 --- a/picture.go +++ b/picture.go @@ -478,6 +478,27 @@ func (f *File) GetPictures(sheet, cell string) ([]Picture, error) { return f.getPicture(row, col, drawingXML, drawingRelationships) } +// GetPictureCells returns all picture cell references in a worksheet by a +// specific worksheet name. +func (f *File) GetPictureCells(sheet string) ([]string, error) { + 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 + } + target := f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID) + drawingXML := strings.ReplaceAll(target, "..", "xl") + drawingRelationships := strings.ReplaceAll( + strings.ReplaceAll(target, "../drawings", "xl/drawings/_rels"), ".xml", ".xml.rels") + + return f.getPictureCells(drawingXML, drawingRelationships) +} + // DeletePicture provides a function to delete all pictures in a cell by given // worksheet name and cell reference. func (f *File) DeletePicture(sheet, cell string) error { @@ -533,58 +554,52 @@ func (f *File) DeletePicture(sheet, cell string) error { // embed in spreadsheet by given coordinates and drawing relationships. func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) (pics []Picture, err error) { var ( - ok bool - deWsDr *decodeWsDr - deCellAnchor *decodeCellAnchor - drawRel *xlsxRelationship - wsDr *xlsxWsDr + deWsDr = new(decodeWsDr) + wsDr *xlsxWsDr ) - if wsDr, _, err = f.drawingParser(drawingXML); err != nil { return } - pics = f.getPicturesFromWsDr(row, col, drawingRelationships, wsDr) - deWsDr = new(decodeWsDr) + anchorCond := func(a *xdrCellAnchor) bool { return a.From.Col == col && a.From.Row == row } + anchorCb := func(a *xdrCellAnchor, r *xlsxRelationship) { + pic := Picture{Extension: filepath.Ext(r.Target), Format: &GraphicOptions{}} + if buffer, _ := f.Pkg.Load(strings.ReplaceAll(r.Target, "..", "xl")); buffer != nil { + pic.File = buffer.([]byte) + pic.Format.AltText = a.Pic.NvPicPr.CNvPr.Descr + pics = append(pics, pic) + } + } + f.extractCellAnchor(drawingRelationships, wsDr, anchorCond, anchorCb) if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(drawingXML)))). Decode(deWsDr); err != nil && err != io.EOF { return } err = nil - extractAnchor := func(anchor *decodeCellAnchor) { - deCellAnchor = new(decodeCellAnchor) - if err := f.xmlNewDecoder(strings.NewReader("" + anchor.Content + "")). - Decode(deCellAnchor); err != nil && err != io.EOF { - return - } - if err = nil; deCellAnchor.From != nil && deCellAnchor.Pic != nil { - if deCellAnchor.From.Col == col && deCellAnchor.From.Row == row { - drawRel = f.getDrawingRelationships(drawingRelationships, deCellAnchor.Pic.BlipFill.Blip.Embed) - 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) - pic.Format.AltText = deCellAnchor.Pic.NvPicPr.CNvPr.Descr - pics = append(pics, pic) - } - } - } + decodeAnchorCond := func(a *decodeCellAnchor) bool { return a.From.Col == col && a.From.Row == row } + decodeAnchorCb := func(a *decodeCellAnchor, r *xlsxRelationship) { + pic := Picture{Extension: filepath.Ext(r.Target), Format: &GraphicOptions{}} + if buffer, _ := f.Pkg.Load(strings.ReplaceAll(r.Target, "..", "xl")); buffer != nil { + pic.File = buffer.([]byte) + pic.Format.AltText = a.Pic.NvPicPr.CNvPr.Descr + pics = append(pics, pic) } } for _, anchor := range deWsDr.TwoCellAnchor { - extractAnchor(anchor) + f.extractDecodeCellAnchor(anchor, drawingRelationships, decodeAnchorCond, decodeAnchorCb) } for _, anchor := range deWsDr.OneCellAnchor { - extractAnchor(anchor) + f.extractDecodeCellAnchor(anchor, drawingRelationships, decodeAnchorCond, decodeAnchorCb) } return } -// getPicturesFromWsDr provides a function to get picture base name and raw -// content in worksheet drawing by given coordinates and drawing -// relationships. -func (f *File) getPicturesFromWsDr(row, col int, drawingRelationships string, wsDr *xlsxWsDr) (pics []Picture) { +// extractCellAnchor extract drawing object from cell anchor by giving drawing +// cell anchor, drawing relationships part path, conditional and callback +// function. +func (f *File) extractCellAnchor(drawingRelationships string, wsDr *xlsxWsDr, + cond func(anchor *xdrCellAnchor) bool, cb func(anchor *xdrCellAnchor, rels *xlsxRelationship), +) { var ( - ok bool anchor *xdrCellAnchor drawRel *xlsxRelationship ) @@ -592,22 +607,40 @@ func (f *File) getPicturesFromWsDr(row, col int, drawingRelationships string, ws 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 { + if cond(anchor) { if drawRel = f.getDrawingRelationships(drawingRelationships, anchor.Pic.BlipFill.Blip.Embed); drawRel != nil { - 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) - pic.Format.AltText = anchor.Pic.NvPicPr.CNvPr.Descr - pics = append(pics, pic) - } + if _, ok := supportedImageTypes[strings.ToLower(filepath.Ext(drawRel.Target))]; ok { + cb(anchor, drawRel) } } } } } - return +} + +// extractDecodeCellAnchor extract drawing object from cell anchor by giving +// decoded drawing cell anchor, drawing relationships part path, conditional and +// callback function. +func (f *File) extractDecodeCellAnchor(anchor *decodeCellAnchor, drawingRelationships string, + cond func(anchor *decodeCellAnchor) bool, cb func(anchor *decodeCellAnchor, rels *xlsxRelationship), +) { + var ( + drawRel *xlsxRelationship + deCellAnchor = new(decodeCellAnchor) + ) + if err := f.xmlNewDecoder(strings.NewReader("" + anchor.Content + "")). + Decode(deCellAnchor); err != nil && err != io.EOF { + return + } + if deCellAnchor.From != nil && deCellAnchor.Pic != nil { + if cond(deCellAnchor) { + drawRel = f.getDrawingRelationships(drawingRelationships, deCellAnchor.Pic.BlipFill.Blip.Embed) + if _, ok := supportedImageTypes[strings.ToLower(filepath.Ext(drawRel.Target))]; ok { + cb(deCellAnchor, drawRel) + } + } + } } // getDrawingRelationships provides a function to get drawing relationships @@ -655,10 +688,7 @@ func (f *File) drawingResize(sheet, cell string, width, height float64, opts *Gr if inMergeCell { continue } - if inMergeCell, err = f.checkCellInRangeRef(cell, mergeCell[0]); err != nil { - return - } - if inMergeCell { + if inMergeCell, err = f.checkCellInRangeRef(cell, mergeCell[0]); err == nil { rng, _ = cellRefsToCoordinates(mergeCell.GetStartAxis(), mergeCell.GetEndAxis()) _ = sortCoordinates(rng) } @@ -685,3 +715,47 @@ func (f *File) drawingResize(sheet, cell string, width, height float64, opts *Gr w, h = int(width*opts.ScaleX), int(height*opts.ScaleY) return } + +// getPictureCells provides a function to get all picture cell references in a +// worksheet by given drawing part path and drawing relationships path. +func (f *File) getPictureCells(drawingXML, drawingRelationships string) ([]string, error) { + var ( + cells []string + err error + deWsDr *decodeWsDr + wsDr *xlsxWsDr + ) + if wsDr, _, err = f.drawingParser(drawingXML); err != nil { + return cells, err + } + anchorCond := func(a *xdrCellAnchor) bool { return true } + anchorCb := func(a *xdrCellAnchor, r *xlsxRelationship) { + if _, ok := f.Pkg.Load(strings.ReplaceAll(r.Target, "..", "xl")); ok { + if cell, err := CoordinatesToCellName(a.From.Col+1, a.From.Row+1); err == nil && inStrSlice(cells, cell, true) == -1 { + cells = append(cells, cell) + } + } + } + f.extractCellAnchor(drawingRelationships, wsDr, anchorCond, anchorCb) + deWsDr = new(decodeWsDr) + if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(drawingXML)))). + Decode(deWsDr); err != nil && err != io.EOF { + return cells, err + } + err = nil + decodeAnchorCond := func(a *decodeCellAnchor) bool { return true } + decodeAnchorCb := func(a *decodeCellAnchor, r *xlsxRelationship) { + if _, ok := f.Pkg.Load(strings.ReplaceAll(r.Target, "..", "xl")); ok { + if cell, err := CoordinatesToCellName(a.From.Col+1, a.From.Row+1); err == nil && inStrSlice(cells, cell, true) == -1 { + cells = append(cells, cell) + } + } + } + for _, anchor := range deWsDr.TwoCellAnchor { + f.extractDecodeCellAnchor(anchor, drawingRelationships, decodeAnchorCond, decodeAnchorCb) + } + for _, anchor := range deWsDr.OneCellAnchor { + f.extractDecodeCellAnchor(anchor, drawingRelationships, decodeAnchorCond, decodeAnchorCb) + } + return cells, err +} diff --git a/picture_test.go b/picture_test.go index 8e7e3a9787..8460c361b7 100644 --- a/picture_test.go +++ b/picture_test.go @@ -62,8 +62,8 @@ 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()) - 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)) + for _, preset := range [][]string{{"Q8", "gif"}, {"Q15", "jpg"}, {"Q22", "tif"}, {"Q28", "bmp"}} { + assert.NoError(t, f.AddPicture("Sheet1", preset[0], filepath.Join("test", "images", fmt.Sprintf("excel.%s", preset[1])), nil)) } // Test write file to given path @@ -77,6 +77,32 @@ func TestAddPicture(t *testing.T) { pics, err := f.GetPictures("Sheet1", "A30") assert.NoError(t, err) assert.Len(t, pics, 2) + + // Test get picture cells + cells, err := f.GetPictureCells("Sheet1") + assert.NoError(t, err) + assert.Equal(t, []string{"A30", "F21", "B30", "Q1", "Q8", "Q15", "Q22", "Q28"}, cells) + assert.NoError(t, f.Close()) + + f, err = OpenFile(filepath.Join("test", "TestAddPicture1.xlsx")) + assert.NoError(t, err) + path := "xl/drawings/drawing1.xml" + f.Drawings.Delete(path) + cells, err = f.GetPictureCells("Sheet1") + assert.NoError(t, err) + assert.Equal(t, []string{"F21", "A30", "B30", "Q1", "Q8", "Q15", "Q22", "Q28"}, cells) + // Test get picture cells with unsupported charset + f.Pkg.Store(path, MacintoshCyrillicCharset) + _, err = f.GetPictureCells("Sheet1") + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") + assert.NoError(t, f.Close()) + + f, err = OpenFile(filepath.Join("test", "TestAddPicture1.xlsx")) + assert.NoError(t, err) + // Test get picture cells with unsupported charset + f.Pkg.Store(path, MacintoshCyrillicCharset) + _, err = f.GetPictureCells("Sheet1") + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") assert.NoError(t, f.Close()) // Test add picture with unsupported charset content types @@ -185,6 +211,10 @@ func TestGetPicture(t *testing.T) { pics, err = f.GetPictures("Sheet2", "K16") assert.NoError(t, err) assert.Len(t, pics, 1) + // Try to get picture cells with one cell anchor + cells, err := f.GetPictureCells("Sheet2") + assert.NoError(t, err) + assert.Equal(t, []string{"K16"}, cells) // Test get picture from none drawing worksheet f = NewFile() @@ -344,3 +374,21 @@ func TestAddContentTypePart(t *testing.T) { f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) assert.EqualError(t, f.addContentTypePart(0, "unknown"), "XML syntax error on line 1: invalid UTF-8") } + +func TestGetPictureCells(t *testing.T) { + f := NewFile() + // Test get picture cells on a worksheet which not contains any pictures + cells, err := f.GetPictureCells("Sheet1") + assert.NoError(t, err) + assert.Empty(t, cells) + // Test get picture cells on not exists worksheet + _, err = f.GetPictureCells("SheetN") + assert.EqualError(t, err, "sheet SheetN does not exist") +} + +func TestExtractDecodeCellAnchor(t *testing.T) { + f := NewFile() + cond := func(a *decodeCellAnchor) bool { return true } + cb := func(a *decodeCellAnchor, r *xlsxRelationship) {} + f.extractDecodeCellAnchor(&decodeCellAnchor{Content: string(MacintoshCyrillicCharset)}, "", cond, cb) +} From b52db71d616e6371a6d54111aa483831dbb1c602 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 17 Oct 2023 08:52:34 +0800 Subject: [PATCH 809/957] This closes #1690, new exported function `GetConditionalStyle` - Support get the conditional format style definition - Update the unit test --- styles.go | 230 ++++++++++++++++++++++++++++++------------------- styles_test.go | 22 ++++- 2 files changed, 158 insertions(+), 94 deletions(-) diff --git a/styles.go b/styles.go index 433cb8d19b..115cad1fac 100644 --- a/styles.go +++ b/styles.go @@ -1138,6 +1138,31 @@ var ( return reflect.DeepEqual(xf.Protection, newProtection(style)) && xf.ApplyProtection != nil && *xf.ApplyProtection }, } + // extractStyleCondFuncs provides a function set to returns if shoudle be + // extract style definition by given style. + extractStyleCondFuncs = map[string]func(xlsxXf, *xlsxStyleSheet) bool{ + "fill": func(xf xlsxXf, s *xlsxStyleSheet) bool { + return xf.ApplyFill != nil && *xf.ApplyFill && + xf.FillID != nil && s.Fills != nil && + *xf.FillID < len(s.Fills.Fill) + }, + "border": func(xf xlsxXf, s *xlsxStyleSheet) bool { + return xf.ApplyBorder != nil && *xf.ApplyBorder && + xf.BorderID != nil && s.Borders != nil && + *xf.BorderID < len(s.Borders.Border) + }, + "font": func(xf xlsxXf, s *xlsxStyleSheet) bool { + return xf.ApplyFont != nil && *xf.ApplyFont && + xf.FontID != nil && s.Fonts != nil && + *xf.FontID < len(s.Fonts.Font) + }, + "alignment": func(xf xlsxXf, s *xlsxStyleSheet) bool { + return xf.ApplyAlignment != nil && *xf.ApplyAlignment + }, + "protection": func(xf xlsxXf, s *xlsxStyleSheet) bool { + return xf.ApplyProtection != nil && *xf.ApplyProtection + }, + } // drawContFmtFunc defines functions to create conditional formats. drawContFmtFunc = map[string]func(p int, ct, GUID string, fmtCond *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule){ "cellIs": drawCondFmtCellIs, @@ -1205,44 +1230,39 @@ func (f *File) getThemeColor(clr *xlsxColor) string { // extractBorders provides a function to extract borders styles settings by // given border styles definition. -func (f *File) extractBorders(xf xlsxXf, s *xlsxStyleSheet, style *Style) { - if xf.ApplyBorder != nil && *xf.ApplyBorder && - xf.BorderID != nil && s.Borders != nil && - *xf.BorderID < len(s.Borders.Border) { - if bdr := s.Borders.Border[*xf.BorderID]; bdr != nil { - - var borders []Border - extractBorder := func(lineType string, line xlsxLine) { - if line.Style != "" { - borders = append(borders, Border{ - Type: lineType, - Color: f.getThemeColor(line.Color), - Style: inStrSlice(styleBorders, line.Style, false), - }) - } +func (f *File) extractBorders(bdr *xlsxBorder, s *xlsxStyleSheet, style *Style) { + if bdr != nil { + var borders []Border + extractBorder := func(lineType string, line xlsxLine) { + if line.Style != "" { + borders = append(borders, Border{ + Type: lineType, + Color: f.getThemeColor(line.Color), + Style: inStrSlice(styleBorders, line.Style, false), + }) } - for i, line := range []xlsxLine{ - bdr.Left, bdr.Right, bdr.Top, bdr.Bottom, bdr.Diagonal, bdr.Diagonal, - } { - if i < 4 { - extractBorder(styleBorderTypes[i], line) - } - if i == 4 && bdr.DiagonalUp { - extractBorder(styleBorderTypes[i], line) - } - if i == 5 && bdr.DiagonalDown { - extractBorder(styleBorderTypes[i], line) - } + } + for i, line := range []xlsxLine{ + bdr.Left, bdr.Right, bdr.Top, bdr.Bottom, bdr.Diagonal, bdr.Diagonal, + } { + if i < 4 { + extractBorder(styleBorderTypes[i], line) + } + if i == 4 && bdr.DiagonalUp { + extractBorder(styleBorderTypes[i], line) + } + if i == 5 && bdr.DiagonalDown { + extractBorder(styleBorderTypes[i], line) } - style.Border = borders } + style.Border = borders } } // extractFills provides a function to extract fill styles settings by // given fill styles definition. -func (f *File) extractFills(xf xlsxXf, s *xlsxStyleSheet, style *Style) { - if fl := s.Fills.Fill[*xf.FillID]; fl != nil { +func (f *File) extractFills(fl *xlsxFill, s *xlsxStyleSheet, style *Style) { + if fl != nil { var fill Fill if fl.GradientFill != nil { fill.Type = "gradient" @@ -1274,46 +1294,42 @@ func (f *File) extractFills(xf xlsxXf, s *xlsxStyleSheet, style *Style) { // extractFont provides a function to extract font styles settings by given // font styles definition. -func (f *File) extractFont(xf xlsxXf, s *xlsxStyleSheet, style *Style) { - if xf.ApplyFont != nil && *xf.ApplyFont && - xf.FontID != nil && s.Fonts != nil && - *xf.FontID < len(s.Fonts.Font) { - if fnt := s.Fonts.Font[*xf.FontID]; fnt != nil { - var font Font - if fnt.B != nil { - font.Bold = fnt.B.Value() - } - if fnt.I != nil { - font.Italic = fnt.I.Value() - } - if fnt.U != nil { - font.Underline = fnt.U.Value() - } - if fnt.Name != nil { - font.Family = fnt.Name.Value() - } - if fnt.Sz != nil { - font.Size = fnt.Sz.Value() - } - if fnt.Strike != nil { - font.Strike = fnt.Strike.Value() - } - if fnt.Color != nil { - font.Color = strings.TrimPrefix(fnt.Color.RGB, "FF") - font.ColorIndexed = fnt.Color.Indexed - font.ColorTheme = fnt.Color.Theme - font.ColorTint = fnt.Color.Tint - } - style.Font = &font +func (f *File) extractFont(fnt *xlsxFont, s *xlsxStyleSheet, style *Style) { + if fnt != nil { + var font Font + if fnt.B != nil { + font.Bold = fnt.B.Value() + } + if fnt.I != nil { + font.Italic = fnt.I.Value() + } + if fnt.U != nil { + font.Underline = fnt.U.Value() + } + if fnt.Name != nil { + font.Family = fnt.Name.Value() } + if fnt.Sz != nil { + font.Size = fnt.Sz.Value() + } + if fnt.Strike != nil { + font.Strike = fnt.Strike.Value() + } + if fnt.Color != nil { + font.Color = strings.TrimPrefix(fnt.Color.RGB, "FF") + font.ColorIndexed = fnt.Color.Indexed + font.ColorTheme = fnt.Color.Theme + font.ColorTint = fnt.Color.Tint + } + style.Font = &font } } // extractNumFmt provides a function to extract number format by given styles // definition. -func (f *File) extractNumFmt(xf xlsxXf, s *xlsxStyleSheet, style *Style) { - if xf.NumFmtID != nil { - numFmtID := *xf.NumFmtID +func (f *File) extractNumFmt(n *int, s *xlsxStyleSheet, style *Style) { + if n != nil { + numFmtID := *n if _, ok := builtInNumFmt[numFmtID]; ok || isLangNumFmt(numFmtID) { style.NumFmt = numFmtID return @@ -1339,32 +1355,32 @@ func (f *File) extractNumFmt(xf xlsxXf, s *xlsxStyleSheet, style *Style) { // extractAlignment provides a function to extract alignment format by // given style definition. -func (f *File) extractAlignment(xf xlsxXf, s *xlsxStyleSheet, style *Style) { - if xf.ApplyAlignment != nil && *xf.ApplyAlignment && xf.Alignment != nil { +func (f *File) extractAlignment(a *xlsxAlignment, s *xlsxStyleSheet, style *Style) { + if a != nil { style.Alignment = &Alignment{ - Horizontal: xf.Alignment.Horizontal, - Indent: xf.Alignment.Indent, - JustifyLastLine: xf.Alignment.JustifyLastLine, - ReadingOrder: xf.Alignment.ReadingOrder, - RelativeIndent: xf.Alignment.RelativeIndent, - ShrinkToFit: xf.Alignment.ShrinkToFit, - TextRotation: xf.Alignment.TextRotation, - Vertical: xf.Alignment.Vertical, - WrapText: xf.Alignment.WrapText, + Horizontal: a.Horizontal, + Indent: a.Indent, + JustifyLastLine: a.JustifyLastLine, + ReadingOrder: a.ReadingOrder, + RelativeIndent: a.RelativeIndent, + ShrinkToFit: a.ShrinkToFit, + TextRotation: a.TextRotation, + Vertical: a.Vertical, + WrapText: a.WrapText, } } } // extractProtection provides a function to extract protection settings by // given format definition. -func (f *File) extractProtection(xf xlsxXf, s *xlsxStyleSheet, style *Style) { - if xf.ApplyProtection != nil && *xf.ApplyProtection && xf.Protection != nil { +func (f *File) extractProtection(p *xlsxProtection, s *xlsxStyleSheet, style *Style) { + if p != nil { style.Protection = &Protection{} - if xf.Protection.Hidden != nil { - style.Protection.Hidden = *xf.Protection.Hidden + if p.Hidden != nil { + style.Protection.Hidden = *p.Hidden } - if xf.Protection.Locked != nil { - style.Protection.Locked = *xf.Protection.Locked + if p.Locked != nil { + style.Protection.Locked = *p.Locked } } } @@ -1383,16 +1399,22 @@ func (f *File) GetStyle(idx int) (*Style, error) { } style = &Style{} xf := s.CellXfs.Xf[idx] - if xf.ApplyFill != nil && *xf.ApplyFill && - xf.FillID != nil && s.Fills != nil && - *xf.FillID < len(s.Fills.Fill) { - f.extractFills(xf, s, style) - } - f.extractBorders(xf, s, style) - f.extractFont(xf, s, style) - f.extractAlignment(xf, s, style) - f.extractProtection(xf, s, style) - f.extractNumFmt(xf, s, style) + if extractStyleCondFuncs["fill"](xf, s) { + f.extractFills(s.Fills.Fill[*xf.FillID], s, style) + } + if extractStyleCondFuncs["border"](xf, s) { + f.extractBorders(s.Borders.Border[*xf.BorderID], s, style) + } + if extractStyleCondFuncs["font"](xf, s) { + f.extractFont(s.Fonts.Font[*xf.FontID], s, style) + } + if extractStyleCondFuncs["alignment"](xf, s) { + f.extractAlignment(xf.Alignment, s, style) + } + if extractStyleCondFuncs["protection"](xf, s) { + f.extractProtection(xf.Protection, s, style) + } + f.extractNumFmt(xf.NumFmtID, s, style) return style, nil } @@ -1470,6 +1492,32 @@ func (f *File) NewConditionalStyle(style *Style) (int, error) { return s.Dxfs.Count - 1, nil } +// GetConditionalStyle returns conditional format style definition by specified +// style index. +func (f *File) GetConditionalStyle(idx int) (*Style, error) { + var style *Style + f.mu.Lock() + s, err := f.stylesReader() + if err != nil { + return style, err + } + f.mu.Unlock() + if idx < 0 || s.Dxfs == nil || len(s.Dxfs.Dxfs) <= idx { + return style, newInvalidStyleID(idx) + } + style = &Style{} + xf := s.Dxfs.Dxfs[idx] + f.extractFills(xf.Fill, s, style) + f.extractBorders(xf.Border, s, style) + f.extractFont(xf.Font, s, style) + f.extractAlignment(xf.Alignment, s, style) + f.extractProtection(xf.Protection, s, style) + if xf.NumFmt != nil { + f.extractNumFmt(&xf.NumFmt.NumFmtID, s, style) + } + return style, nil +} + // newDxfNumFmt provides a function to create number format for conditional // format styles. func newDxfNumFmt(styleSheet *xlsxStyleSheet, style *Style, dxf *xlsxDxf) *xlsxNumFmt { diff --git a/styles_test.go b/styles_test.go index 7858fecdb7..2e91fedf6c 100644 --- a/styles_test.go +++ b/styles_test.go @@ -352,16 +352,24 @@ func TestNewStyle(t *testing.T) { assert.Equal(t, ErrCellStyles, err) } -func TestNewConditionalStyle(t *testing.T) { +func TestConditionalStyle(t *testing.T) { f := NewFile() - _, err := f.NewConditionalStyle(&Style{Protection: &Protection{Hidden: true, Locked: true}}) + expected := &Style{Protection: &Protection{Hidden: true, Locked: true}} + idx, err := f.NewConditionalStyle(expected) assert.NoError(t, err) + style, err := f.GetConditionalStyle(idx) + assert.NoError(t, err) + assert.Equal(t, expected, style) _, err = f.NewConditionalStyle(&Style{DecimalPlaces: intPtr(4), NumFmt: 165, NegRed: true}) assert.NoError(t, err) _, err = f.NewConditionalStyle(&Style{DecimalPlaces: intPtr(-1)}) assert.NoError(t, err) - _, err = f.NewConditionalStyle(&Style{NumFmt: 1}) + expected = &Style{NumFmt: 1} + idx, err = f.NewConditionalStyle(expected) + assert.NoError(t, err) + style, err = f.GetConditionalStyle(idx) assert.NoError(t, err) + assert.Equal(t, expected, style) _, err = f.NewConditionalStyle(&Style{NumFmt: 27}) assert.NoError(t, err) numFmt := "general" @@ -375,6 +383,14 @@ func TestNewConditionalStyle(t *testing.T) { f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset) _, 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") + // Test get conditional style with invalid style index + _, err = f.GetConditionalStyle(1) + assert.Equal(t, newInvalidStyleID(1), err) + // Test get conditional style with unsupported charset style sheet + f.Styles = nil + f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset) + _, err = f.GetConditionalStyle(1) + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") } func TestGetDefaultFont(t *testing.T) { From 05689d6ade5331c8045c7332ef05fb89c4b41b8e Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 20 Oct 2023 00:04:31 +0800 Subject: [PATCH 810/957] This closes #1694, using namespace prefix in workbook theme XML - Improve compatibility with the viewer which doesn't support default theme part namespace - ref #1690, support read background color style, and conditional format with default pattern type - Update the unit tests --- excelize.go | 2 +- file.go | 12 ++-- styles.go | 72 +++++++++++++++++++++- styles_test.go | 14 ++++- xmlTheme.go | 164 ++++++++++++++++++++++++++++++++++++------------- 5 files changed, 210 insertions(+), 54 deletions(-) diff --git a/excelize.go b/excelize.go index 80ba1d3e9e..79b4b687ab 100644 --- a/excelize.go +++ b/excelize.go @@ -47,7 +47,7 @@ type File struct { Sheet sync.Map SheetCount int Styles *xlsxStyleSheet - Theme *xlsxTheme + Theme *decodeTheme DecodeVMLDrawing map[string]*decodeVmlDrawing VMLDrawing map[string]*vmlDrawing WorkBook *xlsxWorkbook diff --git a/file.go b/file.go index 19333ea350..7f2a036c71 100644 --- a/file.go +++ b/file.go @@ -191,13 +191,11 @@ func (f *File) writeToZip(zw *zip.Writer) error { return err } var from io.Reader - from, err = stream.rawData.Reader() - if err != nil { + if from, err = stream.rawData.Reader(); err != nil { _ = stream.rawData.Close() return err } - _, err = io.Copy(fi, from) - if err != nil { + if _, err = io.Copy(fi, from); err != nil { return err } } @@ -210,8 +208,7 @@ func (f *File) writeToZip(zw *zip.Writer) error { return true } var fi io.Writer - fi, err = zw.Create(path.(string)) - if err != nil { + if fi, err = zw.Create(path.(string)); err != nil { return false } _, err = fi.Write(content.([]byte)) @@ -222,8 +219,7 @@ func (f *File) writeToZip(zw *zip.Writer) error { return true } var fi io.Writer - fi, err = zw.Create(path.(string)) - if err != nil { + if fi, err = zw.Create(path.(string)); err != nil { return false } _, err = fi.Write(f.readBytes(path.(string))) diff --git a/styles.go b/styles.go index 115cad1fac..0ca82fd100 100644 --- a/styles.go +++ b/styles.go @@ -129,8 +129,67 @@ func (f *File) styleSheetWriter() { // themeWriter provides a function to save xl/theme/theme1.xml after serialize // structure. func (f *File) themeWriter() { + newColor := func(c *decodeCTColor) xlsxCTColor { + return xlsxCTColor{ + ScrgbClr: c.ScrgbClr, + SrgbClr: c.SrgbClr, + HslClr: c.HslClr, + SysClr: c.SysClr, + SchemeClr: c.SchemeClr, + PrstClr: c.PrstClr, + } + } + newFontScheme := func(c *decodeFontCollection) xlsxFontCollection { + return xlsxFontCollection{ + Latin: c.Latin, + Ea: c.Ea, + Cs: c.Cs, + Font: c.Font, + ExtLst: c.ExtLst, + } + } if f.Theme != nil { - output, _ := xml.Marshal(f.Theme) + output, _ := xml.Marshal(xlsxTheme{ + XMLNSa: NameSpaceDrawingML.Value, + XMLNSr: SourceRelationship.Value, + Name: f.Theme.Name, + ThemeElements: xlsxBaseStyles{ + ClrScheme: xlsxColorScheme{ + Name: f.Theme.ThemeElements.ClrScheme.Name, + Dk1: newColor(&f.Theme.ThemeElements.ClrScheme.Dk1), + Lt1: newColor(&f.Theme.ThemeElements.ClrScheme.Lt1), + Dk2: newColor(&f.Theme.ThemeElements.ClrScheme.Dk2), + Lt2: newColor(&f.Theme.ThemeElements.ClrScheme.Lt2), + Accent1: newColor(&f.Theme.ThemeElements.ClrScheme.Accent1), + Accent2: newColor(&f.Theme.ThemeElements.ClrScheme.Accent2), + Accent3: newColor(&f.Theme.ThemeElements.ClrScheme.Accent3), + Accent4: newColor(&f.Theme.ThemeElements.ClrScheme.Accent4), + Accent5: newColor(&f.Theme.ThemeElements.ClrScheme.Accent5), + Accent6: newColor(&f.Theme.ThemeElements.ClrScheme.Accent6), + Hlink: newColor(&f.Theme.ThemeElements.ClrScheme.Hlink), + FolHlink: newColor(&f.Theme.ThemeElements.ClrScheme.FolHlink), + ExtLst: f.Theme.ThemeElements.ClrScheme.ExtLst, + }, + FontScheme: xlsxFontScheme{ + Name: f.Theme.ThemeElements.FontScheme.Name, + MajorFont: newFontScheme(&f.Theme.ThemeElements.FontScheme.MajorFont), + MinorFont: newFontScheme(&f.Theme.ThemeElements.FontScheme.MinorFont), + ExtLst: f.Theme.ThemeElements.FontScheme.ExtLst, + }, + FmtScheme: xlsxStyleMatrix{ + Name: f.Theme.ThemeElements.FmtScheme.Name, + FillStyleLst: f.Theme.ThemeElements.FmtScheme.FillStyleLst, + LnStyleLst: f.Theme.ThemeElements.FmtScheme.LnStyleLst, + EffectStyleLst: f.Theme.ThemeElements.FmtScheme.EffectStyleLst, + BgFillStyleLst: f.Theme.ThemeElements.FmtScheme.BgFillStyleLst, + }, + ExtLst: f.Theme.ThemeElements.ExtLst, + }, + ObjectDefaults: f.Theme.ObjectDefaults, + ExtraClrSchemeLst: f.Theme.ExtraClrSchemeLst, + CustClrLst: f.Theme.CustClrLst, + ExtLst: f.Theme.ExtLst, + }) f.saveFileList(defaultXMLPathTheme, f.replaceNameSpaceBytes(defaultXMLPathTheme, output)) } } @@ -1284,6 +1343,9 @@ func (f *File) extractFills(fl *xlsxFill, s *xlsxStyleSheet, style *Style) { if fl.PatternFill != nil { fill.Type = "pattern" fill.Pattern = inStrSlice(styleFillPatterns, fl.PatternFill.PatternType, false) + if fl.PatternFill.BgColor != nil { + fill.Color = []string{f.getThemeColor(fl.PatternFill.BgColor)} + } if fl.PatternFill.FgColor != nil { fill.Color = []string{f.getThemeColor(fl.PatternFill.FgColor)} } @@ -1507,6 +1569,10 @@ func (f *File) GetConditionalStyle(idx int) (*Style, error) { } style = &Style{} xf := s.Dxfs.Dxfs[idx] + // The default pattern fill type of conditional format style is solid + if xf.Fill != nil && xf.Fill.PatternFill != nil && xf.Fill.PatternFill.PatternType == "" { + xf.Fill.PatternFill.PatternType = "solid" + } f.extractFills(xf.Fill, s, style) f.extractBorders(xf.Border, s, style) f.extractFont(xf.Font, s, style) @@ -3078,11 +3144,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, error) { +func (f *File) themeReader() (*decodeTheme, error) { if _, ok := f.Pkg.Load(defaultXMLPathTheme); !ok { return nil, nil } - theme := xlsxTheme{XMLNSa: NameSpaceDrawingML.Value, XMLNSr: SourceRelationship.Value} + theme := decodeTheme{} if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathTheme)))). Decode(&theme); err != nil && err != io.EOF { return &theme, err diff --git a/styles_test.go b/styles_test.go index 2e91fedf6c..a886c20f7c 100644 --- a/styles_test.go +++ b/styles_test.go @@ -391,6 +391,18 @@ func TestConditionalStyle(t *testing.T) { f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset) _, err = f.GetConditionalStyle(1) assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") + + f = NewFile() + // Test get conditional style with background color and empty pattern type + idx, err = f.NewConditionalStyle(&Style{Fill: Fill{Type: "pattern", Color: []string{"FEC7CE"}, Pattern: 1}}) + assert.NoError(t, err) + f.Styles.Dxfs.Dxfs[0].Fill.PatternFill.PatternType = "" + f.Styles.Dxfs.Dxfs[0].Fill.PatternFill.FgColor = nil + f.Styles.Dxfs.Dxfs[0].Fill.PatternFill.BgColor = &xlsxColor{Theme: intPtr(6)} + style, err = f.GetConditionalStyle(idx) + assert.NoError(t, err) + assert.Equal(t, "pattern", style.Fill.Type) + assert.Equal(t, []string{"A5A5A5"}, style.Fill.Color) } func TestGetDefaultFont(t *testing.T) { @@ -436,7 +448,7 @@ func TestThemeReader(t *testing.T) { f.Pkg.Store(defaultXMLPathTheme, MacintoshCyrillicCharset) 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) + assert.EqualValues(t, &decodeTheme{}, theme) } func TestSetCellStyle(t *testing.T) { diff --git a/xmlTheme.go b/xmlTheme.go index 3a01221fcc..88cc5730e6 100644 --- a/xmlTheme.go +++ b/xmlTheme.go @@ -16,15 +16,15 @@ import "encoding/xml" // xlsxTheme directly maps the theme element in the namespace // http://schemas.openxmlformats.org/drawingml/2006/main type xlsxTheme struct { - XMLName xml.Name `xml:"http://schemas.openxmlformats.org/drawingml/2006/main theme"` + XMLName xml.Name `xml:"a: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"` + ThemeElements xlsxBaseStyles `xml:"a:themeElements"` + ObjectDefaults xlsxObjectDefaults `xml:"a:objectDefaults"` + ExtraClrSchemeLst xlsxExtraClrSchemeLst `xml:"a:extraClrSchemeLst"` + CustClrLst *xlsxInnerXML `xml:"a:custClrLst"` + ExtLst *xlsxExtLst `xml:"a:extLst"` } // xlsxBaseStyles defines the theme elements for a theme, and is the workhorse @@ -33,40 +33,40 @@ type xlsxTheme struct { // 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"` + ClrScheme xlsxColorScheme `xml:"a:clrScheme"` + FontScheme xlsxFontScheme `xml:"a:fontScheme"` + FmtScheme xlsxStyleMatrix `xml:"a:fmtScheme"` + ExtLst *xlsxExtLst `xml:"a: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"` + ScrgbClr *xlsxInnerXML `xml:"a:scrgbClr"` + SrgbClr *attrValString `xml:"a:srgbClr"` + HslClr *xlsxInnerXML `xml:"a:hslClr"` + SysClr *xlsxSysClr `xml:"a:sysClr"` + SchemeClr *xlsxInnerXML `xml:"a:schemeClr"` + PrstClr *xlsxInnerXML `xml:"a: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"` + Dk1 xlsxCTColor `xml:"a:dk1"` + Lt1 xlsxCTColor `xml:"a:lt1"` + Dk2 xlsxCTColor `xml:"a:dk2"` + Lt2 xlsxCTColor `xml:"a:lt2"` + Accent1 xlsxCTColor `xml:"a:accent1"` + Accent2 xlsxCTColor `xml:"a:accent2"` + Accent3 xlsxCTColor `xml:"a:accent3"` + Accent4 xlsxCTColor `xml:"a:accent4"` + Accent5 xlsxCTColor `xml:"a:accent5"` + Accent6 xlsxCTColor `xml:"a:accent6"` + Hlink xlsxCTColor `xml:"a:hlink"` + FolHlink xlsxCTColor `xml:"a:folHlink"` + ExtLst *xlsxExtLst `xml:"a:extLst"` } // objectDefaults element allows for the definition of default shape, line, @@ -95,11 +95,11 @@ type xlsxCTSupplementalFont struct { // 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"` + Latin *xlsxCTTextFont `xml:"a:latin"` + Ea *xlsxCTTextFont `xml:"a:ea"` + Cs *xlsxCTTextFont `xml:"a:cs"` + Font []xlsxCTSupplementalFont `xml:"a:font"` + ExtLst *xlsxExtLst `xml:"a:extLst"` } // xlsxFontScheme element defines the font scheme within the theme. The font @@ -109,9 +109,9 @@ type xlsxFontCollection struct { // paragraph areas. type xlsxFontScheme struct { Name string `xml:"name,attr"` - MajorFont xlsxFontCollection `xml:"majorFont"` - MinorFont xlsxFontCollection `xml:"minorFont"` - ExtLst *xlsxExtLst `xml:"extLst"` + MajorFont xlsxFontCollection `xml:"a:majorFont"` + MinorFont xlsxFontCollection `xml:"a:minorFont"` + ExtLst *xlsxExtLst `xml:"a:extLst"` } // xlsxStyleMatrix defines a set of formatting options, which can be referenced @@ -121,10 +121,10 @@ type xlsxFontScheme struct { // 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"` - BgFillStyleLst xlsxBgFillStyleLst `xml:"bgFillStyleLst"` + FillStyleLst xlsxFillStyleLst `xml:"a:fillStyleLst"` + LnStyleLst xlsxLnStyleLst `xml:"a:lnStyleLst"` + EffectStyleLst xlsxEffectStyleLst `xml:"a:effectStyleLst"` + BgFillStyleLst xlsxBgFillStyleLst `xml:"a:bgFillStyleLst"` } // xlsxFillStyleLst element defines a set of three fill styles that are used @@ -161,3 +161,85 @@ type xlsxSysClr struct { Val string `xml:"val,attr"` LastClr string `xml:"lastClr,attr"` } + +// decodeTheme defines the structure used to parse the a:theme element for the +// theme. +type decodeTheme struct { + XMLName xml.Name `xml:"http://schemas.openxmlformats.org/drawingml/2006/main theme"` + Name string `xml:"name,attr"` + ThemeElements decodeBaseStyles `xml:"themeElements"` + ObjectDefaults xlsxObjectDefaults `xml:"objectDefaults"` + ExtraClrSchemeLst xlsxExtraClrSchemeLst `xml:"extraClrSchemeLst"` + CustClrLst *xlsxInnerXML `xml:"custClrLst"` + ExtLst *xlsxExtLst `xml:"extLst"` +} + +// decodeBaseStyles defines the structure used to parse the theme elements for a +// theme, and is the workhorse of the theme. +type decodeBaseStyles struct { + ClrScheme decodeColorScheme `xml:"clrScheme"` + FontScheme decodeFontScheme `xml:"fontScheme"` + FmtScheme decodeStyleMatrix `xml:"fmtScheme"` + ExtLst *xlsxExtLst `xml:"extLst"` +} + +// decodeColorScheme defines the structure used to parse a set of colors for the +// theme. +type decodeColorScheme struct { + Name string `xml:"name,attr"` + Dk1 decodeCTColor `xml:"dk1"` + Lt1 decodeCTColor `xml:"lt1"` + Dk2 decodeCTColor `xml:"dk2"` + Lt2 decodeCTColor `xml:"lt2"` + Accent1 decodeCTColor `xml:"accent1"` + Accent2 decodeCTColor `xml:"accent2"` + Accent3 decodeCTColor `xml:"accent3"` + Accent4 decodeCTColor `xml:"accent4"` + Accent5 decodeCTColor `xml:"accent5"` + Accent6 decodeCTColor `xml:"accent6"` + Hlink decodeCTColor `xml:"hlink"` + FolHlink decodeCTColor `xml:"folHlink"` + ExtLst *xlsxExtLst `xml:"extLst"` +} + +// decodeFontScheme defines the structure used to parse font scheme within the +// theme. +type decodeFontScheme struct { + Name string `xml:"name,attr"` + MajorFont decodeFontCollection `xml:"majorFont"` + MinorFont decodeFontCollection `xml:"minorFont"` + ExtLst *xlsxExtLst `xml:"extLst"` +} + +// decodeFontCollection defines the structure used to parse a major and minor +// font which is used in the font scheme. +type decodeFontCollection struct { + Latin *xlsxCTTextFont `xml:"latin"` + Ea *xlsxCTTextFont `xml:"ea"` + Cs *xlsxCTTextFont `xml:"cs"` + Font []xlsxCTSupplementalFont `xml:"font"` + ExtLst *xlsxExtLst `xml:"extLst"` +} + +// decodeCTColor defines the structure used to parse the actual color values +// that are to be applied to a given diagram and how those colors are to be +// applied. +type decodeCTColor 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"` +} + +// decodeStyleMatrix defines the structure used to parse a set of formatting +// options, which can be referenced by documents that apply a certain style to +// a given part of an object. +type decodeStyleMatrix struct { + Name string `xml:"name,attr,omitempty"` + FillStyleLst xlsxFillStyleLst `xml:"fillStyleLst"` + LnStyleLst xlsxLnStyleLst `xml:"lnStyleLst"` + EffectStyleLst xlsxEffectStyleLst `xml:"effectStyleLst"` + BgFillStyleLst xlsxBgFillStyleLst `xml:"bgFillStyleLst"` +} From a8cbcfa39b7cab7e691b3c369b56e98a5a7f848e Mon Sep 17 00:00:00 2001 From: rjtee <62975067+TeeRenJing@users.noreply.github.com> Date: Tue, 24 Oct 2023 00:05:52 +0800 Subject: [PATCH 811/957] This closes #1306 and closes #1615 (#1698) - Support adjust formula on inserting/deleting columns/rows --- adjust.go | 142 ++++++++++++++++++++++++++++++++++++++++-------- adjust_test.go | 54 +++++++++++++++--- calc.go | 2 +- calc_test.go | 2 +- lib.go | 3 + rows.go | 2 +- rows_test.go | 15 +++++ xmlCalcChain.go | 2 +- 8 files changed, 186 insertions(+), 36 deletions(-) diff --git a/adjust.go b/adjust.go index 5f408979b6..3708401072 100644 --- a/adjust.go +++ b/adjust.go @@ -16,6 +16,8 @@ import ( "encoding/xml" "io" "strings" + + "github.com/xuri/efp" ) type adjustDirection bool @@ -42,9 +44,9 @@ func (f *File) adjustHelper(sheet string, dir adjustDirection, num, offset int) } sheetID := f.getSheetID(sheet) if dir == rows { - err = f.adjustRowDimensions(ws, num, offset) + err = f.adjustRowDimensions(sheet, ws, num, offset) } else { - err = f.adjustColDimensions(ws, num, offset) + err = f.adjustColDimensions(sheet, ws, num, offset) } if err != nil { return err @@ -116,7 +118,7 @@ func (f *File) adjustCols(ws *xlsxWorksheet, col, offset int) error { // 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 { +func (f *File) adjustColDimensions(sheet string, 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 { @@ -131,9 +133,11 @@ 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) } } + if err := f.adjustFormula(sheet, ws.SheetData.Row[rowIdx].C[colIdx].F, columns, col, offset, false); err != nil { + return err + } } } return f.adjustCols(ws, col, offset) @@ -141,40 +145,49 @@ 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 { +func (f *File) adjustRowDimensions(sheet string, ws *xlsxWorksheet, row, offset int) error { 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 { + 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 { - f.adjustSingleRowDimensions(r, newRow, offset, false) + if err := f.adjustSingleRowDimensions(sheet, r, row, offset, false); err != nil { + return err + } } } return nil } // adjustSingleRowDimensions provides a function to adjust single row dimensions. -func (f *File) adjustSingleRowDimensions(r *xlsxRow, num, offset int, si bool) { - r.R = num +func (f *File) adjustSingleRowDimensions(sheet string, r *xlsxRow, num, offset int, si bool) error { + r.R += offset for i, col := range r.C { colName, _, _ := SplitCellName(col.R) - r.C[i].R, _ = JoinCellName(colName, num) - _ = f.adjustFormula(col.F, rows, offset, si) + r.C[i].R, _ = JoinCellName(colName, r.R) + if err := f.adjustFormula(sheet, col.F, rows, num, offset, si); err != nil { + return err + } } + return nil } -// 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) +// adjustFormula provides a function to adjust formula reference and shared +// formula reference. +func (f *File) adjustFormula(sheet string, formula *xlsxF, dir adjustDirection, num, offset int, si bool) error { + if formula == nil { + return nil + } + adjustRef := func(ref string) (string, error) { + coordinates, err := rangeRefToCoordinates(ref) if err != nil { - return err + return ref, err } if dir == columns { coordinates[0] += offset @@ -183,16 +196,72 @@ func (f *File) adjustFormula(formula *xlsxF, dir adjustDirection, offset int, si coordinates[1] += offset coordinates[3] += offset } - if formula.Ref, err = f.coordinatesToRangeRef(coordinates); err != nil { + return f.coordinatesToRangeRef(coordinates) + } + var err error + if formula.Ref != "" { + if formula.Ref, err = adjustRef(formula.Ref); err != nil { return err } if si && formula.Si != nil { formula.Si = intPtr(*formula.Si + 1) } } + if formula.T == STCellFormulaTypeArray { + formula.Content, err = adjustRef(strings.TrimPrefix(formula.Content, "=")) + return err + } + if formula.Content != "" && !strings.ContainsAny(formula.Content, "[:]") { + content, err := f.adjustFormulaRef(sheet, formula.Content, dir, num, offset) + if err != nil { + return err + } + formula.Content = content + } return nil } +// adjustFormulaRef returns adjusted formula text by giving adjusting direction +// and the base number of column or row, and offset. +func (f *File) adjustFormulaRef(sheet string, text string, dir adjustDirection, num, offset int) (string, error) { + var ( + formulaText string + definedNames []string + ps = efp.ExcelParser() + ) + for _, definedName := range f.GetDefinedName() { + if definedName.Scope == "Workbook" || definedName.Scope == sheet { + definedNames = append(definedNames, definedName.Name) + } + } + for _, token := range ps.Parse(text) { + if token.TType == efp.TokenTypeOperand && token.TSubType == efp.TokenSubTypeRange { + if inStrSlice(definedNames, token.TValue, true) != -1 { + formulaText += token.TValue + continue + } + c, r, err := CellNameToCoordinates(token.TValue) + if err != nil { + return formulaText, err + } + if dir == columns && c >= num { + c += offset + } + if dir == rows { + r += offset + } + cell, err := CoordinatesToCellName(c, r, strings.Contains(token.TValue, "$")) + if err != nil { + return formulaText, err + } + formulaText += cell + continue + } + formulaText += token.TValue + } + return formulaText, 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) { @@ -260,7 +329,7 @@ func (f *File) adjustTable(ws *xlsxWorksheet, sheet string, dir adjustDirection, return } // Remove the table when deleting the header row of the table - if dir == rows && num == coordinates[0] { + if dir == rows && num == coordinates[0] && offset == -1 { ws.TableParts.TableParts = append(ws.TableParts.TableParts[:idx], ws.TableParts.TableParts[idx+1:]...) ws.TableParts.Count = len(ws.TableParts.TableParts) idx-- @@ -316,8 +385,8 @@ func (f *File) adjustAutoFilter(ws *xlsxWorksheet, dir adjustDirection, num, off } // adjustAutoFilterHelper provides a function for adjusting auto filter to -// compare and calculate cell reference by the given adjust direction, operation -// reference and offset. +// compare and calculate cell reference by the giving adjusting direction, +// operation reference and offset. func (f *File) adjustAutoFilterHelper(dir adjustDirection, coordinates []int, num, offset int) []int { if dir == rows { if coordinates[1] >= num { @@ -422,13 +491,34 @@ func (f *File) deleteMergeCell(ws *xlsxWorksheet, idx int) { } } +// adjustCalcChainRef update the cell reference in calculation chain when +// inserting or deleting rows or columns. +func (f *File) adjustCalcChainRef(i, c, r, offset int, dir adjustDirection) { + if dir == rows { + if rn := r + offset; rn > 0 { + f.CalcChain.C[i].R, _ = CoordinatesToCellName(c, rn) + } + return + } + if nc := c + offset; nc > 0 { + f.CalcChain.C[i].R, _ = CoordinatesToCellName(nc, r) + } +} + // adjustCalcChain provides a function to update the calculation chain when // inserting or deleting rows or columns. func (f *File) adjustCalcChain(dir adjustDirection, num, offset, sheetID int) error { if f.CalcChain == nil { return nil } + // If sheet ID is omitted, it is assumed to be the same as the i value of + // the previous cell. + var prevSheetID int for index, c := range f.CalcChain.C { + if c.I == 0 { + c.I = prevSheetID + } + prevSheetID = c.I if c.I != sheetID { continue } @@ -437,14 +527,18 @@ func (f *File) adjustCalcChain(dir adjustDirection, num, offset, sheetID int) er return err } if dir == rows && num <= rowNum { - if newRow := rowNum + offset; newRow > 0 { - f.CalcChain.C[index].R, _ = CoordinatesToCellName(colNum, newRow) + if num == rowNum && offset == -1 { + _ = f.deleteCalcChain(c.I, c.R) + continue } + f.adjustCalcChainRef(index, colNum, rowNum, offset, dir) } if dir == columns && num <= colNum { - if newCol := colNum + offset; newCol > 0 { - f.CalcChain.C[index].R, _ = CoordinatesToCellName(newCol, rowNum) + if num == colNum && offset == -1 { + _ = f.deleteCalcChain(c.I, c.R) + continue } + f.adjustCalcChainRef(index, colNum, rowNum, offset, dir) } } return nil diff --git a/adjust_test.go b/adjust_test.go index f6147e6486..793659ff67 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -357,13 +357,18 @@ func TestAdjustHelper(t *testing.T) { func TestAdjustCalcChain(t *testing.T) { f := NewFile() f.CalcChain = &xlsxCalcChain{ - C: []xlsxCalcChainC{ - {R: "B2", I: 2}, {R: "B2", I: 1}, - }, + C: []xlsxCalcChainC{{R: "B2", I: 2}, {R: "B2", I: 1}, {R: "A1", I: 1}}, } assert.NoError(t, f.InsertCols("Sheet1", "A", 1)) assert.NoError(t, f.InsertRows("Sheet1", 1, 1)) + f.CalcChain = &xlsxCalcChain{ + C: []xlsxCalcChainC{{R: "B2", I: 1}, {R: "B3"}, {R: "A1"}}, + } + assert.NoError(t, f.RemoveRow("Sheet1", 3)) + assert.NoError(t, f.RemoveCol("Sheet1", "B")) + + f.CalcChain = &xlsxCalcChain{C: []xlsxCalcChainC{{R: "B2", I: 2}, {R: "B2", I: 1}}} f.CalcChain.C[1].R = "invalid coordinates" assert.Equal(t, f.InsertCols("Sheet1", "A", 1), newCellNameToCoordinatesError("invalid coordinates", newInvalidCellNameError("invalid coordinates"))) f.CalcChain = nil @@ -449,11 +454,11 @@ func TestAdjustCols(t *testing.T) { 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.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"} { + for cell, expected := range map[string]string{"D2": "A2+C2", "D3": "A3+C3", "D11": "A11+C11"} { formula, err := f.GetCellFormula("Sheet1", cell) assert.NoError(t, err) assert.Equal(t, expected, formula) @@ -461,7 +466,40 @@ func TestAdjustFormula(t *testing.T) { 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) + assert.NoError(t, f.adjustFormula("Sheet1", nil, rows, 0, 0, false)) + assert.Equal(t, ErrParameterInvalid, f.adjustFormula("Sheet1", &xlsxF{Ref: "-"}, rows, 0, 0, false)) + assert.Equal(t, ErrColumnNumber, f.adjustFormula("Sheet1", &xlsxF{Ref: "XFD1:XFD1"}, columns, 0, 1, false)) + + _, err := f.adjustFormulaRef("Sheet1", "XFE1", columns, 0, 1) + assert.Equal(t, ErrColumnNumber, err) + _, err = f.adjustFormulaRef("Sheet1", "XFD1", columns, 0, 1) + assert.Equal(t, ErrColumnNumber, err) + + f = NewFile() + assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "XFD1")) + assert.Equal(t, ErrColumnNumber, f.InsertCols("Sheet1", "A", 1)) + + assert.NoError(t, f.SetCellFormula("Sheet1", "B2", fmt.Sprintf("A%d", TotalRows))) + assert.Equal(t, ErrMaxRows, f.InsertRows("Sheet1", 1, 1)) + + // Test adjust formula with defined name in formula text + f = NewFile() + assert.NoError(t, f.SetDefinedName(&DefinedName{ + Name: "Amount", + RefersTo: "Sheet1!$B$2", + })) + assert.NoError(t, f.SetCellFormula("Sheet1", "B2", "Amount+B3")) + assert.NoError(t, f.RemoveRow("Sheet1", 1)) + formula, err := f.GetCellFormula("Sheet1", "B1") + assert.NoError(t, err) + assert.Equal(t, "Amount+B2", formula) + + // Test adjust formula with array formula + f = NewFile() + formulaType, reference := STCellFormulaTypeArray, "A3:A3" + assert.NoError(t, f.SetCellFormula("Sheet1", "A3", "=A1:A2", FormulaOpts{Ref: &reference, Type: &formulaType})) + assert.NoError(t, f.InsertRows("Sheet1", 1, 1)) + formula, err = f.GetCellFormula("Sheet1", "A4") + assert.NoError(t, err) + assert.Equal(t, "A2:A3", formula) } diff --git a/calc.go b/calc.go index 1320238827..1c1d8e9597 100644 --- a/calc.go +++ b/calc.go @@ -14454,7 +14454,7 @@ func (fn *formulaFuncs) ADDRESS(argsList *list.List) formulaArg { if rowNum.Type != ArgNumber { return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) } - if rowNum.Number >= TotalRows { + if rowNum.Number > TotalRows { return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) } colNum := argsList.Front().Next().Value.(formulaArg).ToNumber() diff --git a/calc_test.go b/calc_test.go index 5e97a0eff7..336d085b52 100644 --- a/calc_test.go +++ b/calc_test.go @@ -3970,7 +3970,7 @@ func TestCalcCellValue(t *testing.T) { "=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!"}, + "=ADDRESS(1048577,1,1,TRUE)": {"#VALUE!", "#VALUE!"}, // CHOOSE "=CHOOSE()": {"#VALUE!", "CHOOSE requires 2 arguments"}, "=CHOOSE(\"index_num\",0)": {"#VALUE!", "CHOOSE requires first argument of type number"}, diff --git a/lib.go b/lib.go index a69446312f..bc564225e2 100644 --- a/lib.go +++ b/lib.go @@ -270,6 +270,9 @@ func CoordinatesToCellName(col, row int, abs ...bool) (string, error) { if col < 1 || row < 1 { return "", newCoordinatesToCellNameError(col, row) } + if row > TotalRows { + return "", ErrMaxRows + } sign := "" for _, a := range abs { if a { diff --git a/rows.go b/rows.go index 972707d38e..88d1f6660d 100644 --- a/rows.go +++ b/rows.go @@ -652,7 +652,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, row2-row, true) + _ = f.adjustSingleRowDimensions(sheet, &rowCopy, row, row2-row, true) if idx2 != -1 { ws.SheetData.Row[idx2] = rowCopy diff --git a/rows_test.go b/rows_test.go index 768f8b01c1..3e49580293 100644 --- a/rows_test.go +++ b/rows_test.go @@ -870,6 +870,21 @@ 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()) + + f = NewFile() + assert.NoError(t, f.SetDefinedName(&DefinedName{ + Name: "Amount", + RefersTo: "Sheet1!$B$1", + })) + assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "Amount+C1")) + assert.NoError(t, f.SetCellValue("Sheet1", "A10", "A10")) + assert.NoError(t, f.DuplicateRowTo("Sheet1", 1, 10)) + formula, err := f.GetCellFormula("Sheet1", "A10") + assert.NoError(t, err) + assert.Equal(t, "Amount+C10", formula) + value, err := f.GetCellValue("Sheet1", "A11") + assert.NoError(t, err) + assert.Equal(t, "A10", value) } func TestDuplicateRowTo(t *testing.T) { diff --git a/xmlCalcChain.go b/xmlCalcChain.go index 3631565aad..9c1d1ee21f 100644 --- a/xmlCalcChain.go +++ b/xmlCalcChain.go @@ -76,7 +76,7 @@ type xlsxCalcChain struct { // | boolean datatype. type xlsxCalcChainC struct { R string `xml:"r,attr"` - I int `xml:"i,attr"` + I int `xml:"i,attr,omitempty"` L bool `xml:"l,attr,omitempty"` S bool `xml:"s,attr,omitempty"` T bool `xml:"t,attr,omitempty"` From cf3e0164d90d194aa5312fd2ec8fe0765caa54c8 Mon Sep 17 00:00:00 2001 From: rjtee <62975067+TeeRenJing@users.noreply.github.com> Date: Sun, 29 Oct 2023 13:40:21 +0800 Subject: [PATCH 812/957] Support adjust formula for entire cols/rows reference (#1702) - Update the unit tests --- adjust.go | 202 ++++++++++++++++++++++++++++--------- adjust_test.go | 269 ++++++++++++++++++++++++++++++++++++++++++++++++- rows.go | 3 +- 3 files changed, 424 insertions(+), 50 deletions(-) diff --git a/adjust.go b/adjust.go index 3708401072..b90c0db377 100644 --- a/adjust.go +++ b/adjust.go @@ -15,6 +15,7 @@ import ( "bytes" "encoding/xml" "io" + "strconv" "strings" "github.com/xuri/efp" @@ -154,23 +155,31 @@ func (f *File) adjustRowDimensions(sheet string, ws *xlsxWorksheet, row, offset if newRow := lastRow.R + offset; lastRow.R >= row && newRow > 0 && newRow > TotalRows { return ErrMaxRows } - for i := 0; i < len(ws.SheetData.Row); i++ { + numOfRows := len(ws.SheetData.Row) + for i := 0; i < numOfRows; i++ { r := &ws.SheetData.Row[i] if newRow := r.R + offset; r.R >= row && newRow > 0 { - if err := f.adjustSingleRowDimensions(sheet, r, row, offset, false); err != nil { - return err - } + r.adjustSingleRowDimensions(offset) + } + if err := f.adjustSingleRowFormulas(sheet, r, row, offset, false); err != nil { + return err } } return nil } // adjustSingleRowDimensions provides a function to adjust single row dimensions. -func (f *File) adjustSingleRowDimensions(sheet string, r *xlsxRow, num, offset int, si bool) error { +func (r *xlsxRow) adjustSingleRowDimensions(offset int) { r.R += offset for i, col := range r.C { colName, _, _ := SplitCellName(col.R) r.C[i].R, _ = JoinCellName(colName, r.R) + } +} + +// adjustSingleRowFormulas provides a function to adjust single row formulas. +func (f *File) adjustSingleRowFormulas(sheet string, r *xlsxRow, num, offset int, si bool) error { + for _, col := range r.C { if err := f.adjustFormula(sheet, col.F, rows, num, offset, si); err != nil { return err } @@ -178,54 +187,151 @@ func (f *File) adjustSingleRowDimensions(sheet string, r *xlsxRow, num, offset i return nil } -// adjustFormula provides a function to adjust formula reference and shared -// formula reference. -func (f *File) adjustFormula(sheet string, formula *xlsxF, dir adjustDirection, num, offset int, si bool) error { - if formula == nil { - return nil +// adjustCellRef provides a function to adjust cell reference. +func (f *File) adjustCellRef(ref string, dir adjustDirection, num, offset int) (string, error) { + if !strings.Contains(ref, ":") { + ref += ":" + ref } - adjustRef := func(ref string) (string, error) { - coordinates, err := rangeRefToCoordinates(ref) - if err != nil { - return ref, err - } - if dir == columns { + coordinates, err := rangeRefToCoordinates(ref) + if err != nil { + return ref, err + } + if dir == columns { + if coordinates[0] >= num { coordinates[0] += offset + } + if coordinates[2] >= num { coordinates[2] += offset - } else { + } + } else { + if coordinates[1] >= num { coordinates[1] += offset + } + if coordinates[3] >= num { coordinates[3] += offset } - return f.coordinatesToRangeRef(coordinates) + } + return f.coordinatesToRangeRef(coordinates) +} + +// adjustFormula provides a function to adjust formula reference and shared +// formula reference. +func (f *File) adjustFormula(sheet string, formula *xlsxF, dir adjustDirection, num, offset int, si bool) error { + if formula == nil { + return nil } var err error if formula.Ref != "" { - if formula.Ref, err = adjustRef(formula.Ref); err != nil { + if formula.Ref, err = f.adjustCellRef(formula.Ref, dir, num, offset); err != nil { return err } if si && formula.Si != nil { formula.Si = intPtr(*formula.Si + 1) } } - if formula.T == STCellFormulaTypeArray { - formula.Content, err = adjustRef(strings.TrimPrefix(formula.Content, "=")) - return err - } - if formula.Content != "" && !strings.ContainsAny(formula.Content, "[:]") { - content, err := f.adjustFormulaRef(sheet, formula.Content, dir, num, offset) - if err != nil { + if formula.Content != "" { + if formula.Content, err = f.adjustFormulaRef(sheet, formula.Content, dir, num, offset); err != nil { return err } - formula.Content = content } return nil } -// adjustFormulaRef returns adjusted formula text by giving adjusting direction -// and the base number of column or row, and offset. -func (f *File) adjustFormulaRef(sheet string, text string, dir adjustDirection, num, offset int) (string, error) { +// isFunctionStop provides a function to check if token is a function stop. +func isFunctionStop(token efp.Token) bool { + return token.TType == efp.TokenTypeFunction && token.TSubType == efp.TokenSubTypeStop +} + +// isFunctionStart provides a function to check if token is a function start. +func isFunctionStart(token efp.Token) bool { + return token.TType == efp.TokenTypeFunction && token.TSubType == efp.TokenSubTypeStart +} + +// adjustFormulaColumnName adjust column name in the formula reference. +func adjustFormulaColumnName(name string, dir adjustDirection, num, offset int) (string, error) { + if name == "" { + return name, nil + } + col, err := ColumnNameToNumber(name) + if err != nil { + return name, err + } + if dir == columns && col >= num { + col += offset + return ColumnNumberToName(col) + } + return name, nil +} + +// adjustFormulaRowNumber adjust row number in the formula reference. +func adjustFormulaRowNumber(name string, dir adjustDirection, num, offset int) (string, error) { + if name == "" { + return name, nil + } + row, _ := strconv.Atoi(name) + if dir == rows && row >= num { + row += offset + if row > TotalRows { + return name, ErrMaxRows + } + return strconv.Itoa(row), nil + } + return name, nil +} + +// adjustFormulaOperand adjust range operand tokens for the formula. +func (f *File) adjustFormulaOperand(token efp.Token, dir adjustDirection, num int, offset int) (string, error) { + var col, row, operand string + for _, r := range token.TValue { + if ('A' <= r && r <= 'Z') || ('a' <= r && r <= 'z') { + col += string(r) + continue + } + if '0' <= r && r <= '9' { + row += string(r) + if col != "" { + name, err := adjustFormulaColumnName(col, dir, num, offset) + if err != nil { + return operand, err + } + operand += name + col = "" + } + continue + } + if row != "" { + name, err := adjustFormulaRowNumber(row, dir, num, offset) + if err != nil { + return operand, err + } + operand += name + row = "" + } + if col != "" { + name, err := adjustFormulaColumnName(col, dir, num, offset) + if err != nil { + return operand, err + } + operand += name + col = "" + } + operand += string(r) + } + name, err := adjustFormulaColumnName(col, dir, num, offset) + if err != nil { + return operand, err + } + operand += name + name, err = adjustFormulaRowNumber(row, dir, num, offset) + operand += name + return operand, err +} + +// adjustFormulaRef returns adjusted formula by giving adjusting direction and +// the base number of column or row, and offset. +func (f *File) adjustFormulaRef(sheet, formula string, dir adjustDirection, num, offset int) (string, error) { var ( - formulaText string + val string definedNames []string ps = efp.ExcelParser() ) @@ -234,32 +340,34 @@ func (f *File) adjustFormulaRef(sheet string, text string, dir adjustDirection, definedNames = append(definedNames, definedName.Name) } } - for _, token := range ps.Parse(text) { + for _, token := range ps.Parse(formula) { if token.TType == efp.TokenTypeOperand && token.TSubType == efp.TokenSubTypeRange { if inStrSlice(definedNames, token.TValue, true) != -1 { - formulaText += token.TValue + val += token.TValue continue } - c, r, err := CellNameToCoordinates(token.TValue) - if err != nil { - return formulaText, err - } - if dir == columns && c >= num { - c += offset - } - if dir == rows { - r += offset + if strings.ContainsAny(token.TValue, "[]") { + val += token.TValue + continue } - cell, err := CoordinatesToCellName(c, r, strings.Contains(token.TValue, "$")) + operand, err := f.adjustFormulaOperand(token, dir, num, offset) if err != nil { - return formulaText, err + return val, err } - formulaText += cell + val += operand + continue + } + if isFunctionStart(token) { + val += token.TValue + string(efp.ParenOpen) + continue + } + if isFunctionStop(token) { + val += token.TValue + string(efp.ParenClose) continue } - formulaText += token.TValue + val += token.TValue } - return formulaText, nil + return val, nil } // adjustHyperlinks provides a function to update hyperlinks when inserting or diff --git a/adjust_test.go b/adjust_test.go index 793659ff67..b3743d6bd5 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -451,6 +451,63 @@ func TestAdjustCols(t *testing.T) { assert.NoError(t, f.Close()) } +func TestAdjustColDimensions(t *testing.T) { + f := NewFile() + ws, err := f.workSheetReader("Sheet1") + assert.NoError(t, err) + assert.NoError(t, f.SetCellFormula("Sheet1", "C3", "A1+B1")) + assert.Equal(t, ErrColumnNumber, f.adjustColDimensions("Sheet1", ws, 1, MaxColumns)) +} + +func TestAdjustRowDimensions(t *testing.T) { + f := NewFile() + ws, err := f.workSheetReader("Sheet1") + assert.NoError(t, err) + assert.NoError(t, f.SetCellFormula("Sheet1", "C3", "A1+B1")) + assert.Equal(t, ErrMaxRows, f.adjustRowDimensions("Sheet1", ws, 1, TotalRows)) +} + +func TestAdjustHyperlinks(t *testing.T) { + f := NewFile() + ws, err := f.workSheetReader("Sheet1") + assert.NoError(t, err) + assert.NoError(t, f.SetCellFormula("Sheet1", "C3", "A1+B1")) + f.adjustHyperlinks(ws, "Sheet1", rows, 3, -1) + + // Test adjust hyperlinks location with positive offset + assert.NoError(t, f.SetCellHyperLink("Sheet1", "F5", "Sheet1!A1", "Location")) + assert.NoError(t, f.InsertRows("Sheet1", 1, 1)) + link, target, err := f.GetCellHyperLink("Sheet1", "F6") + assert.NoError(t, err) + assert.True(t, link) + assert.Equal(t, target, "Sheet1!A1") + + // Test adjust hyperlinks location with negative offset + assert.NoError(t, f.RemoveRow("Sheet1", 1)) + link, target, err = f.GetCellHyperLink("Sheet1", "F5") + assert.NoError(t, err) + assert.True(t, link) + assert.Equal(t, target, "Sheet1!A1") + + // Test adjust hyperlinks location on remove row + assert.NoError(t, f.RemoveRow("Sheet1", 5)) + link, target, err = f.GetCellHyperLink("Sheet1", "F5") + assert.NoError(t, err) + assert.False(t, link) + assert.Empty(t, target) + + // Test adjust hyperlinks location on remove column + assert.NoError(t, f.SetCellHyperLink("Sheet1", "F5", "Sheet1!A1", "Location")) + assert.NoError(t, f.RemoveCol("Sheet1", "F")) + link, target, err = f.GetCellHyperLink("Sheet1", "F5") + assert.NoError(t, err) + assert.False(t, link) + assert.Empty(t, target) + + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAdjustHyperlinks.xlsx"))) + assert.NoError(t, f.Close()) +} + func TestAdjustFormula(t *testing.T) { f := NewFile() formulaType, ref := STCellFormulaTypeShared, "C1:C5" @@ -467,7 +524,7 @@ func TestAdjustFormula(t *testing.T) { assert.NoError(t, f.Close()) assert.NoError(t, f.adjustFormula("Sheet1", nil, rows, 0, 0, false)) - assert.Equal(t, ErrParameterInvalid, f.adjustFormula("Sheet1", &xlsxF{Ref: "-"}, rows, 0, 0, false)) + assert.Equal(t, newCellNameToCoordinatesError("-", newInvalidCellNameError("-")), f.adjustFormula("Sheet1", &xlsxF{Ref: "-"}, rows, 0, 0, false)) assert.Equal(t, ErrColumnNumber, f.adjustFormula("Sheet1", &xlsxF{Ref: "XFD1:XFD1"}, columns, 0, 1, false)) _, err := f.adjustFormulaRef("Sheet1", "XFE1", columns, 0, 1) @@ -482,6 +539,18 @@ func TestAdjustFormula(t *testing.T) { assert.NoError(t, f.SetCellFormula("Sheet1", "B2", fmt.Sprintf("A%d", TotalRows))) assert.Equal(t, ErrMaxRows, f.InsertRows("Sheet1", 1, 1)) + f = NewFile() + assert.NoError(t, f.SetCellFormula("Sheet1", "B3", "SUM(1048576:1:2)")) + assert.Equal(t, ErrMaxRows, f.InsertRows("Sheet1", 1, 1)) + + f = NewFile() + assert.NoError(t, f.SetCellFormula("Sheet1", "B3", "SUM(XFD:A:B)")) + assert.Equal(t, ErrColumnNumber, f.InsertCols("Sheet1", "A", 1)) + + f = NewFile() + assert.NoError(t, f.SetCellFormula("Sheet1", "B3", "SUM(A:B:XFD)")) + assert.Equal(t, ErrColumnNumber, f.InsertCols("Sheet1", "A", 1)) + // Test adjust formula with defined name in formula text f = NewFile() assert.NoError(t, f.SetDefinedName(&DefinedName{ @@ -497,9 +566,205 @@ func TestAdjustFormula(t *testing.T) { // Test adjust formula with array formula f = NewFile() formulaType, reference := STCellFormulaTypeArray, "A3:A3" - assert.NoError(t, f.SetCellFormula("Sheet1", "A3", "=A1:A2", FormulaOpts{Ref: &reference, Type: &formulaType})) + assert.NoError(t, f.SetCellFormula("Sheet1", "A3", "A1:A2", FormulaOpts{Ref: &reference, Type: &formulaType})) assert.NoError(t, f.InsertRows("Sheet1", 1, 1)) formula, err = f.GetCellFormula("Sheet1", "A4") assert.NoError(t, err) assert.Equal(t, "A2:A3", formula) + + // Test adjust formula on duplicate row with array formula + f = NewFile() + formulaType, reference = STCellFormulaTypeArray, "A3" + assert.NoError(t, f.SetCellFormula("Sheet1", "A3", "A1:A2", FormulaOpts{Ref: &reference, Type: &formulaType})) + assert.NoError(t, f.InsertRows("Sheet1", 1, 1)) + formula, err = f.GetCellFormula("Sheet1", "A4") + assert.NoError(t, err) + assert.Equal(t, "A2:A3", formula) + + // Test adjust formula on duplicate row with relative and absolute cell references + f = NewFile() + assert.NoError(t, f.SetCellFormula("Sheet1", "B10", "A$10+$A11")) + assert.NoError(t, f.DuplicateRowTo("Sheet1", 10, 2)) + formula, err = f.GetCellFormula("Sheet1", "B2") + assert.NoError(t, err) + assert.Equal(t, "A$2+$A3", formula) + + t.Run("for_cells_affected_directly", func(t *testing.T) { + // Test insert row in middle of range with relative and absolute cell references + f := NewFile() + assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "$A1+A$2")) + assert.NoError(t, f.InsertRows("Sheet1", 2, 1)) + formula, err := f.GetCellFormula("Sheet1", "B1") + assert.NoError(t, err) + assert.Equal(t, "$A1+A$3", formula) + assert.NoError(t, f.RemoveRow("Sheet1", 2)) + formula, err = f.GetCellFormula("Sheet1", "B1") + assert.NoError(t, err) + assert.Equal(t, "$A1+A$2", formula) + + // Test insert column in middle of range + f = NewFile() + assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "B1+C1")) + assert.NoError(t, f.InsertCols("Sheet1", "C", 1)) + formula, err = f.GetCellFormula("Sheet1", "A1") + assert.NoError(t, err) + assert.Equal(t, "B1+D1", formula) + assert.NoError(t, f.RemoveCol("Sheet1", "C")) + formula, err = f.GetCellFormula("Sheet1", "A1") + assert.NoError(t, err) + assert.Equal(t, "B1+C1", formula) + + // Test insert row and column in a rectangular range + f = NewFile() + assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "D4+D5+E4+E5")) + assert.NoError(t, f.InsertCols("Sheet1", "E", 1)) + assert.NoError(t, f.InsertRows("Sheet1", 5, 1)) + formula, err = f.GetCellFormula("Sheet1", "A1") + assert.NoError(t, err) + assert.Equal(t, "D4+D6+F4+F6", formula) + + // Test insert row in middle of range + f = NewFile() + formulaType, reference := STCellFormulaTypeArray, "B1:B1" + assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "A1:A2", FormulaOpts{Ref: &reference, Type: &formulaType})) + assert.NoError(t, f.InsertRows("Sheet1", 2, 1)) + formula, err = f.GetCellFormula("Sheet1", "B1") + assert.NoError(t, err) + assert.Equal(t, "A1:A3", formula) + assert.NoError(t, f.RemoveRow("Sheet1", 2)) + formula, err = f.GetCellFormula("Sheet1", "B1") + assert.NoError(t, err) + assert.Equal(t, "A1:A2", formula) + + // Test insert column in middle of range + f = NewFile() + formulaType, reference = STCellFormulaTypeArray, "A1:A1" + assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "B1:C1", FormulaOpts{Ref: &reference, Type: &formulaType})) + assert.NoError(t, f.InsertCols("Sheet1", "C", 1)) + formula, err = f.GetCellFormula("Sheet1", "A1") + assert.NoError(t, err) + assert.Equal(t, "B1:D1", formula) + assert.NoError(t, f.RemoveCol("Sheet1", "C")) + formula, err = f.GetCellFormula("Sheet1", "A1") + assert.NoError(t, err) + assert.Equal(t, "B1:C1", formula) + + // Test insert row and column in a rectangular range + f = NewFile() + formulaType, reference = STCellFormulaTypeArray, "A1:A1" + assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "D4:E5", FormulaOpts{Ref: &reference, Type: &formulaType})) + assert.NoError(t, f.InsertCols("Sheet1", "E", 1)) + assert.NoError(t, f.InsertRows("Sheet1", 5, 1)) + formula, err = f.GetCellFormula("Sheet1", "A1") + assert.NoError(t, err) + assert.Equal(t, "D4:F6", formula) + }) + t.Run("for_cells_affected_indirectly", func(t *testing.T) { + f := NewFile() + assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "A3+A4")) + assert.NoError(t, f.InsertRows("Sheet1", 2, 1)) + formula, err := f.GetCellFormula("Sheet1", "B1") + assert.NoError(t, err) + assert.Equal(t, "A4+A5", formula) + assert.NoError(t, f.RemoveRow("Sheet1", 2)) + formula, err = f.GetCellFormula("Sheet1", "B1") + assert.NoError(t, err) + assert.Equal(t, "A3+A4", formula) + + f = NewFile() + assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "D3+D4")) + assert.NoError(t, f.InsertCols("Sheet1", "C", 1)) + formula, err = f.GetCellFormula("Sheet1", "B1") + assert.NoError(t, err) + assert.Equal(t, "E3+E4", formula) + assert.NoError(t, f.RemoveCol("Sheet1", "C")) + formula, err = f.GetCellFormula("Sheet1", "B1") + assert.NoError(t, err) + assert.Equal(t, "D3+D4", formula) + }) + t.Run("for_entire_cols_rows_reference", func(t *testing.T) { + f := NewFile() + // Test adjust formula on insert row in the middle of the range + assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "SUM(A2:A3:A4,,Table1[])")) + assert.NoError(t, f.InsertRows("Sheet1", 3, 1)) + formula, err := f.GetCellFormula("Sheet1", "B1") + assert.NoError(t, err) + assert.Equal(t, "SUM(A2:A4:A5,,Table1[])", formula) + + // Test adjust formula on insert at the top of the range + assert.NoError(t, f.InsertRows("Sheet1", 2, 1)) + formula, err = f.GetCellFormula("Sheet1", "B1") + assert.NoError(t, err) + assert.Equal(t, "SUM(A3:A5:A6,,Table1[])", formula) + + f = NewFile() + // Test adjust formula on insert row in the middle of the range + assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "SUM(A2,A3)")) + assert.NoError(t, f.InsertRows("Sheet1", 3, 1)) + formula, err = f.GetCellFormula("Sheet1", "B1") + assert.NoError(t, err) + assert.Equal(t, "SUM(A2,A4)", formula) + + // Test adjust formula on insert row at the top of the range + assert.NoError(t, f.InsertRows("Sheet1", 2, 1)) + formula, err = f.GetCellFormula("Sheet1", "B1") + assert.NoError(t, err) + assert.Equal(t, "SUM(A3,A5)", formula) + + f = NewFile() + // Test adjust formula on insert col in the middle of the range + assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "SUM(C3:D3)")) + assert.NoError(t, f.InsertCols("Sheet1", "D", 1)) + formula, err = f.GetCellFormula("Sheet1", "B1") + assert.NoError(t, err) + assert.Equal(t, "SUM(C3:E3)", formula) + + // Test adjust formula on insert at the top of the range + assert.NoError(t, f.InsertCols("Sheet1", "C", 1)) + formula, err = f.GetCellFormula("Sheet1", "B1") + assert.NoError(t, err) + assert.Equal(t, "SUM(D3:F3)", formula) + + f = NewFile() + // Test adjust formula on insert column in the middle of the range + assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "SUM(C3,D3)")) + assert.NoError(t, f.InsertCols("Sheet1", "D", 1)) + formula, err = f.GetCellFormula("Sheet1", "B1") + assert.NoError(t, err) + assert.Equal(t, "SUM(C3,E3)", formula) + + // Test adjust formula on insert column at the top of the range + assert.NoError(t, f.InsertCols("Sheet1", "C", 1)) + formula, err = f.GetCellFormula("Sheet1", "B1") + assert.NoError(t, err) + assert.Equal(t, "SUM(D3,F3)", formula) + + f = NewFile() + // Test adjust formula on insert row in the middle of the range (range of whole row) + assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "SUM(2:3)")) + assert.NoError(t, f.InsertRows("Sheet1", 3, 1)) + formula, err = f.GetCellFormula("Sheet1", "B1") + assert.NoError(t, err) + assert.Equal(t, "SUM(2:4)", formula) + + // Test adjust formula on insert row at the top of the range (range of whole row) + assert.NoError(t, f.InsertRows("Sheet1", 2, 1)) + formula, err = f.GetCellFormula("Sheet1", "B1") + assert.NoError(t, err) + assert.Equal(t, "SUM(3:5)", formula) + + f = NewFile() + // Test adjust formula on insert row in the middle of the range (range of whole column) + assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "SUM(C:D)")) + assert.NoError(t, f.InsertCols("Sheet1", "D", 1)) + formula, err = f.GetCellFormula("Sheet1", "B1") + assert.NoError(t, err) + assert.Equal(t, "SUM(C:E)", formula) + + // Test adjust formula on insert row at the top of the range (range of whole column) + assert.NoError(t, f.InsertCols("Sheet1", "C", 1)) + formula, err = f.GetCellFormula("Sheet1", "B1") + assert.NoError(t, err) + assert.Equal(t, "SUM(D:F)", formula) + }) } diff --git a/rows.go b/rows.go index 88d1f6660d..992a780751 100644 --- a/rows.go +++ b/rows.go @@ -652,7 +652,8 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) error { } rowCopy.C = append(make([]xlsxC, 0, len(rowCopy.C)), rowCopy.C...) - _ = f.adjustSingleRowDimensions(sheet, &rowCopy, row, row2-row, true) + rowCopy.adjustSingleRowDimensions(row2 - row) + _ = f.adjustSingleRowFormulas(sheet, &rowCopy, row, row2-row, true) if idx2 != -1 { ws.SheetData.Row[idx2] = rowCopy From 5bba8f980591da0e5230c4f7b3aa1f406d98417d Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 31 Oct 2023 00:01:57 +0800 Subject: [PATCH 813/957] This improves compatibility for adjusting tables and formula references on inserting/deleting columns or rows --- adjust.go | 74 +++++++++++++++++++++++++++++--------------------- adjust_test.go | 10 +++---- table.go | 43 ++++++++++++++++++++--------- table_test.go | 5 ++-- xmlTable.go | 42 ++++++++++++++++------------ 5 files changed, 104 insertions(+), 70 deletions(-) diff --git a/adjust.go b/adjust.go index b90c0db377..d8defcee6c 100644 --- a/adjust.go +++ b/adjust.go @@ -53,6 +53,8 @@ func (f *File) adjustHelper(sheet string, dir adjustDirection, num, offset int) return err } f.adjustHyperlinks(ws, sheet, dir, num, offset) + ws.checkSheet() + _ = ws.checkRow() f.adjustTable(ws, sheet, dir, num, offset) if err = f.adjustMergeCells(ws, dir, num, offset); err != nil { return err @@ -63,9 +65,6 @@ func (f *File) adjustHelper(sheet string, dir adjustDirection, num, offset int) if err = f.adjustCalcChain(dir, num, offset, sheetID); err != nil { return err } - ws.checkSheet() - _ = ws.checkRow() - if ws.MergeCells != nil && len(ws.MergeCells.Cells) == 0 { ws.MergeCells = nil } @@ -279,10 +278,40 @@ func adjustFormulaRowNumber(name string, dir adjustDirection, num, offset int) ( return name, nil } +// adjustFormulaOperandRef adjust cell reference in the operand tokens for the formula. +func adjustFormulaOperandRef(row, col, operand string, dir adjustDirection, num int, offset int) (string, string, string, error) { + if col != "" { + name, err := adjustFormulaColumnName(col, dir, num, offset) + if err != nil { + return row, col, operand, err + } + operand += name + col = "" + } + if row != "" { + name, err := adjustFormulaRowNumber(row, dir, num, offset) + if err != nil { + return row, col, operand, err + } + operand += name + row = "" + } + return row, col, operand, nil +} + // adjustFormulaOperand adjust range operand tokens for the formula. func (f *File) adjustFormulaOperand(token efp.Token, dir adjustDirection, num int, offset int) (string, error) { - var col, row, operand string - for _, r := range token.TValue { + var ( + err error + sheet, col, row, operand string + cell = token.TValue + tokens = strings.Split(token.TValue, "!") + ) + if len(tokens) == 2 { // have a worksheet + sheet, cell = tokens[0], tokens[1] + operand = string(efp.QuoteSingle) + sheet + string(efp.QuoteSingle) + "!" + } + for _, r := range cell { if ('A' <= r && r <= 'Z') || ('a' <= r && r <= 'z') { col += string(r) continue @@ -299,21 +328,8 @@ func (f *File) adjustFormulaOperand(token efp.Token, dir adjustDirection, num in } continue } - if row != "" { - name, err := adjustFormulaRowNumber(row, dir, num, offset) - if err != nil { - return operand, err - } - operand += name - row = "" - } - if col != "" { - name, err := adjustFormulaColumnName(col, dir, num, offset) - if err != nil { - return operand, err - } - operand += name - col = "" + if row, col, operand, err = adjustFormulaOperandRef(row, col, operand, dir, num, offset); err != nil { + return operand, err } operand += string(r) } @@ -365,6 +381,10 @@ func (f *File) adjustFormulaRef(sheet, formula string, dir adjustDirection, num, val += token.TValue + string(efp.ParenClose) continue } + if token.TType == efp.TokenTypeOperand && token.TSubType == efp.TokenSubTypeText { + val += string(efp.QuoteDouble) + token.TValue + string(efp.QuoteDouble) + continue + } val += token.TValue } return val, nil @@ -400,16 +420,7 @@ func (f *File) adjustHyperlinks(ws *xlsxWorksheet, sheet string, dir adjustDirec } for i := range ws.Hyperlinks.Hyperlink { link := &ws.Hyperlinks.Hyperlink[i] // get reference - colNum, rowNum, _ := CellNameToCoordinates(link.Ref) - if dir == rows { - if rowNum >= num { - link.Ref, _ = CoordinatesToCellName(colNum, rowNum+offset) - } - } else { - if colNum >= num { - link.Ref, _ = CoordinatesToCellName(colNum+offset, rowNum) - } - } + link.Ref, _ = f.adjustFormulaRef(sheet, link.Ref, dir, num, offset) } } @@ -455,7 +466,8 @@ func (f *File) adjustTable(ws *xlsxWorksheet, sheet string, dir adjustDirection, if t.AutoFilter != nil { t.AutoFilter.Ref = t.Ref } - _, _ = f.setTableHeader(sheet, true, x1, y1, x2) + _ = f.setTableColumns(sheet, true, x1, y1, x2, &t) + t.TotalsRowCount = 0 table, _ := xml.Marshal(t) f.saveFileList(tableXML, table) } diff --git a/adjust_test.go b/adjust_test.go index b3743d6bd5..2356b82963 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -583,11 +583,11 @@ func TestAdjustFormula(t *testing.T) { // Test adjust formula on duplicate row with relative and absolute cell references f = NewFile() - assert.NoError(t, f.SetCellFormula("Sheet1", "B10", "A$10+$A11")) + assert.NoError(t, f.SetCellFormula("Sheet1", "B10", "A$10+$A11&\" \"")) assert.NoError(t, f.DuplicateRowTo("Sheet1", 10, 2)) formula, err = f.GetCellFormula("Sheet1", "B2") assert.NoError(t, err) - assert.Equal(t, "A$2+$A3", formula) + assert.Equal(t, "A$2+$A3&\" \"", formula) t.Run("for_cells_affected_directly", func(t *testing.T) { // Test insert row in middle of range with relative and absolute cell references @@ -699,17 +699,17 @@ func TestAdjustFormula(t *testing.T) { f = NewFile() // Test adjust formula on insert row in the middle of the range - assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "SUM(A2,A3)")) + assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "SUM('Sheet1'!A2,A3)")) assert.NoError(t, f.InsertRows("Sheet1", 3, 1)) formula, err = f.GetCellFormula("Sheet1", "B1") assert.NoError(t, err) - assert.Equal(t, "SUM(A2,A4)", formula) + assert.Equal(t, "SUM('Sheet1'!A2,A4)", formula) // Test adjust formula on insert row at the top of the range assert.NoError(t, f.InsertRows("Sheet1", 2, 1)) formula, err = f.GetCellFormula("Sheet1", "B1") assert.NoError(t, err) - assert.Equal(t, "SUM(A3,A5)", formula) + assert.Equal(t, "SUM('Sheet1'!A3,A5)", formula) f = NewFile() // Test adjust formula on insert col in the middle of the range diff --git a/table.go b/table.go index b88db4cd63..b9b32f6696 100644 --- a/table.go +++ b/table.go @@ -254,37 +254,58 @@ func (f *File) addSheetTable(sheet string, rID int) error { return err } -// setTableHeader provides a function to set cells value in header row for the +// setTableColumns provides a function to set cells value in header row for the // table. -func (f *File) setTableHeader(sheet string, showHeaderRow bool, x1, y1, x2 int) ([]*xlsxTableColumn, error) { +func (f *File) setTableColumns(sheet string, showHeaderRow bool, x1, y1, x2 int, tbl *xlsxTable) error { var ( - tableColumns []*xlsxTableColumn - idx int + idx int + header []string + tableColumns []*xlsxTableColumn + getTableColumn = func(name string) *xlsxTableColumn { + if tbl != nil && tbl.TableColumns != nil { + for _, column := range tbl.TableColumns.TableColumn { + if column.Name == name { + return column + } + } + } + return nil + } ) for i := x1; i <= x2; i++ { idx++ cell, err := CoordinatesToCellName(i, y1) if err != nil { - return tableColumns, err + return err } - name, _ := f.GetCellValue(sheet, cell) + name, _ := f.GetCellValue(sheet, cell, Options{RawCellValue: true}) if _, err := strconv.Atoi(name); err == nil { if showHeaderRow { _ = f.SetCellStr(sheet, cell, name) } } - if name == "" { + if name == "" || inStrSlice(header, name, true) != -1 { name = "Column" + strconv.Itoa(idx) if showHeaderRow { _ = f.SetCellStr(sheet, cell, name) } } + header = append(header, name) + if column := getTableColumn(name); column != nil { + column.ID, column.DataDxfID = idx, 0 + tableColumns = append(tableColumns, column) + continue + } tableColumns = append(tableColumns, &xlsxTableColumn{ ID: idx, Name: name, }) } - return tableColumns, nil + tbl.TableColumns = &xlsxTableColumns{ + Count: len(tableColumns), + TableColumn: tableColumns, + } + return nil } // checkDefinedName check whether there are illegal characters in the defined @@ -326,7 +347,6 @@ func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, opts *Tab if err != nil { return err } - tableColumns, _ := f.setTableHeader(sheet, !hideHeaderRow, x1, y1, x2) name := opts.Name if name == "" { name = "Table" + strconv.Itoa(i) @@ -340,10 +360,6 @@ func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, opts *Tab AutoFilter: &xlsxAutoFilter{ Ref: ref, }, - TableColumns: &xlsxTableColumns{ - Count: len(tableColumns), - TableColumn: tableColumns, - }, TableStyleInfo: &xlsxTableStyleInfo{ Name: opts.StyleName, ShowFirstColumn: opts.ShowFirstColumn, @@ -352,6 +368,7 @@ func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, opts *Tab ShowColumnStripes: opts.ShowColumnStripes, }, } + _ = f.setTableColumns(sheet, !hideHeaderRow, x1, y1, x2, &t) if hideHeaderRow { t.AutoFilter = nil t.HeaderRowCount = intPtr(0) diff --git a/table_test.go b/table_test.go index 0b82b2a10c..69e3ad09ee 100644 --- a/table_test.go +++ b/table_test.go @@ -137,10 +137,9 @@ func TestDeleteTable(t *testing.T) { assert.Equal(t, "Values", val) } -func TestSetTableHeader(t *testing.T) { +func TestSetTableColumns(t *testing.T) { f := NewFile() - _, err := f.setTableHeader("Sheet1", true, 1, 0, 1) - assert.Equal(t, newCoordinatesToCellNameError(1, 0), err) + assert.Equal(t, newCoordinatesToCellNameError(1, 0), f.setTableColumns("Sheet1", true, 1, 0, 1, nil)) } func TestAutoFilter(t *testing.T) { diff --git a/xmlTable.go b/xmlTable.go index 41a5bdb30e..193113586d 100644 --- a/xmlTable.go +++ b/xmlTable.go @@ -21,22 +21,28 @@ import "encoding/xml" type xlsxTable struct { XMLName xml.Name `xml:"table"` XMLNS string `xml:"xmlns,attr"` - DataCellStyle string `xml:"dataCellStyle,attr,omitempty"` - DataDxfID int `xml:"dataDxfId,attr,omitempty"` + ID int `xml:"id,attr"` + Name string `xml:"name,attr"` DisplayName string `xml:"displayName,attr,omitempty"` - HeaderRowBorderDxfID int `xml:"headerRowBorderDxfId,attr,omitempty"` - HeaderRowCellStyle string `xml:"headerRowCellStyle,attr,omitempty"` + Comment string `xml:"comment,attr,omitempty"` + Ref string `xml:"ref,attr"` + TableType string `xml:"tableType,attr,omitempty"` HeaderRowCount *int `xml:"headerRowCount,attr"` - HeaderRowDxfID int `xml:"headerRowDxfId,attr,omitempty"` - ID int `xml:"id,attr"` InsertRow bool `xml:"insertRow,attr,omitempty"` InsertRowShift bool `xml:"insertRowShift,attr,omitempty"` - Name string `xml:"name,attr"` - Published bool `xml:"published,attr,omitempty"` - Ref string `xml:"ref,attr"` TotalsRowCount int `xml:"totalsRowCount,attr,omitempty"` + TotalsRowShown *bool `xml:"totalsRowShown,attr"` + Published bool `xml:"published,attr,omitempty"` + HeaderRowDxfID int `xml:"headerRowDxfId,attr,omitempty"` + DataDxfID int `xml:"dataDxfId,attr,omitempty"` TotalsRowDxfID int `xml:"totalsRowDxfId,attr,omitempty"` - TotalsRowShown bool `xml:"totalsRowShown,attr"` + HeaderRowBorderDxfID int `xml:"headerRowBorderDxfId,attr,omitempty"` + TableBorderDxfId int `xml:"tableBorderDxfId,attr,omitempty"` + TotalsRowBorderDxfId int `xml:"totalsRowBorderDxfId,attr,omitempty"` + HeaderRowCellStyle string `xml:"headerRowCellStyle,attr,omitempty"` + DataCellStyle string `xml:"dataCellStyle,attr,omitempty"` + TotalsRowCellStyle string `xml:"totalsRowCellStyle,attr,omitempty"` + ConnectionId int `xml:"connectionId,attr,omitempty"` AutoFilter *xlsxAutoFilter `xml:"autoFilter"` TableColumns *xlsxTableColumns `xml:"tableColumns"` TableStyleInfo *xlsxTableStyleInfo `xml:"tableStyleInfo"` @@ -171,18 +177,18 @@ type xlsxTableColumns struct { // xlsxTableColumn directly maps the element representing a single column for // this table. type xlsxTableColumn struct { - DataCellStyle string `xml:"dataCellStyle,attr,omitempty"` - DataDxfID int `xml:"dataDxfId,attr,omitempty"` - HeaderRowCellStyle string `xml:"headerRowCellStyle,attr,omitempty"` - HeaderRowDxfID int `xml:"headerRowDxfId,attr,omitempty"` ID int `xml:"id,attr"` + UniqueName string `xml:"uniqueName,attr,omitempty"` Name string `xml:"name,attr"` - QueryTableFieldID int `xml:"queryTableFieldId,attr,omitempty"` - TotalsRowCellStyle string `xml:"totalsRowCellStyle,attr,omitempty"` - TotalsRowDxfID int `xml:"totalsRowDxfId,attr,omitempty"` TotalsRowFunction string `xml:"totalsRowFunction,attr,omitempty"` TotalsRowLabel string `xml:"totalsRowLabel,attr,omitempty"` - UniqueName string `xml:"uniqueName,attr,omitempty"` + QueryTableFieldID int `xml:"queryTableFieldId,attr,omitempty"` + HeaderRowDxfID int `xml:"headerRowDxfId,attr,omitempty"` + DataDxfID int `xml:"dataDxfId,attr,omitempty"` + TotalsRowDxfID int `xml:"totalsRowDxfId,attr,omitempty"` + HeaderRowCellStyle string `xml:"headerRowCellStyle,attr,omitempty"` + DataCellStyle string `xml:"dataCellStyle,attr,omitempty"` + TotalsRowCellStyle string `xml:"totalsRowCellStyle,attr,omitempty"` } // xlsxTableStyleInfo directly maps the tableStyleInfo element. This element From b41a6cc3cd37c7c99bae83fa8dd1158e5e8fecc7 Mon Sep 17 00:00:00 2001 From: rjtee <62975067+TeeRenJing@users.noreply.github.com> Date: Wed, 1 Nov 2023 00:52:18 +0800 Subject: [PATCH 814/957] Support to adjust formula cross worksheet on inserting/deleting columns/rows (#1705) --- adjust.go | 107 +++++++++++++++++++----------- adjust_test.go | 175 ++++++++++++++++++++++++++++++++++++++++++++++--- rows.go | 2 +- 3 files changed, 237 insertions(+), 47 deletions(-) diff --git a/adjust.go b/adjust.go index d8defcee6c..608e249af6 100644 --- a/adjust.go +++ b/adjust.go @@ -17,6 +17,7 @@ import ( "io" "strconv" "strings" + "unicode" "github.com/xuri/efp" ) @@ -128,15 +129,24 @@ func (f *File) adjustColDimensions(sheet string, ws *xlsxWorksheet, col, offset } } } - for rowIdx := range ws.SheetData.Row { - for colIdx, v := range ws.SheetData.Row[rowIdx].C { - if cellCol, cellRow, _ := CellNameToCoordinates(v.R); col <= cellCol { - if newCol := cellCol + offset; newCol > 0 { - ws.SheetData.Row[rowIdx].C[colIdx].R, _ = CoordinatesToCellName(newCol, cellRow) - } + for _, sheetN := range f.GetSheetList() { + worksheet, err := f.workSheetReader(sheetN) + if err != nil { + if err.Error() == newNotWorksheetError(sheetN).Error() { + continue } - if err := f.adjustFormula(sheet, ws.SheetData.Row[rowIdx].C[colIdx].F, columns, col, offset, false); err != nil { - return err + return err + } + for rowIdx := range worksheet.SheetData.Row { + for colIdx, v := range worksheet.SheetData.Row[rowIdx].C { + if cellCol, cellRow, _ := CellNameToCoordinates(v.R); sheetN == sheet && col <= cellCol { + if newCol := cellCol + offset; newCol > 0 { + worksheet.SheetData.Row[rowIdx].C[colIdx].R, _ = CoordinatesToCellName(newCol, cellRow) + } + } + if err := f.adjustFormula(sheet, sheetN, worksheet.SheetData.Row[rowIdx].C[colIdx].F, columns, col, offset, false); err != nil { + return err + } } } } @@ -146,6 +156,25 @@ func (f *File) adjustColDimensions(sheet string, ws *xlsxWorksheet, col, offset // adjustRowDimensions provides a function to update row dimensions when // inserting or deleting rows or columns. func (f *File) adjustRowDimensions(sheet string, ws *xlsxWorksheet, row, offset int) error { + for _, sheetN := range f.GetSheetList() { + if sheetN == sheet { + continue + } + worksheet, err := f.workSheetReader(sheetN) + if err != nil { + if err.Error() == newNotWorksheetError(sheetN).Error() { + continue + } + return err + } + numOfRows := len(worksheet.SheetData.Row) + for i := 0; i < numOfRows; i++ { + r := &worksheet.SheetData.Row[i] + if err = f.adjustSingleRowFormulas(sheet, sheetN, r, row, offset, false); err != nil { + return err + } + } + } totalRows := len(ws.SheetData.Row) if totalRows == 0 { return nil @@ -160,7 +189,7 @@ func (f *File) adjustRowDimensions(sheet string, ws *xlsxWorksheet, row, offset if newRow := r.R + offset; r.R >= row && newRow > 0 { r.adjustSingleRowDimensions(offset) } - if err := f.adjustSingleRowFormulas(sheet, r, row, offset, false); err != nil { + if err := f.adjustSingleRowFormulas(sheet, sheet, r, row, offset, false); err != nil { return err } } @@ -177,9 +206,9 @@ func (r *xlsxRow) adjustSingleRowDimensions(offset int) { } // adjustSingleRowFormulas provides a function to adjust single row formulas. -func (f *File) adjustSingleRowFormulas(sheet string, r *xlsxRow, num, offset int, si bool) error { +func (f *File) adjustSingleRowFormulas(sheet, sheetN string, r *xlsxRow, num, offset int, si bool) error { for _, col := range r.C { - if err := f.adjustFormula(sheet, col.F, rows, num, offset, si); err != nil { + if err := f.adjustFormula(sheet, sheetN, col.F, rows, num, offset, si); err != nil { return err } } @@ -215,12 +244,12 @@ func (f *File) adjustCellRef(ref string, dir adjustDirection, num, offset int) ( // adjustFormula provides a function to adjust formula reference and shared // formula reference. -func (f *File) adjustFormula(sheet string, formula *xlsxF, dir adjustDirection, num, offset int, si bool) error { +func (f *File) adjustFormula(sheet, sheetN string, formula *xlsxF, dir adjustDirection, num, offset int, si bool) error { if formula == nil { return nil } var err error - if formula.Ref != "" { + if formula.Ref != "" && sheet == sheetN { if formula.Ref, err = f.adjustCellRef(formula.Ref, dir, num, offset); err != nil { return err } @@ -229,7 +258,7 @@ func (f *File) adjustFormula(sheet string, formula *xlsxF, dir adjustDirection, } } if formula.Content != "" { - if formula.Content, err = f.adjustFormulaRef(sheet, formula.Content, dir, num, offset); err != nil { + if formula.Content, err = f.adjustFormulaRef(sheet, sheetN, formula.Content, dir, num, offset); err != nil { return err } } @@ -246,11 +275,19 @@ func isFunctionStart(token efp.Token) bool { return token.TType == efp.TokenTypeFunction && token.TSubType == efp.TokenSubTypeStart } +// escapeSheetName enclose sheet name in single quotation marks if the giving +// worksheet name includes spaces or non-alphabetical characters. +func escapeSheetName(name string) string { + if strings.IndexFunc(name, func(r rune) bool { + return !unicode.IsLetter(r) && !unicode.IsNumber(r) + }) != -1 { + return string(efp.QuoteSingle) + name + string(efp.QuoteSingle) + } + return name +} + // adjustFormulaColumnName adjust column name in the formula reference. func adjustFormulaColumnName(name string, dir adjustDirection, num, offset int) (string, error) { - if name == "" { - return name, nil - } col, err := ColumnNameToNumber(name) if err != nil { return name, err @@ -264,9 +301,6 @@ func adjustFormulaColumnName(name string, dir adjustDirection, num, offset int) // adjustFormulaRowNumber adjust row number in the formula reference. func adjustFormulaRowNumber(name string, dir adjustDirection, num, offset int) (string, error) { - if name == "" { - return name, nil - } row, _ := strconv.Atoi(name) if dir == rows && row >= num { row += offset @@ -300,16 +334,19 @@ func adjustFormulaOperandRef(row, col, operand string, dir adjustDirection, num } // adjustFormulaOperand adjust range operand tokens for the formula. -func (f *File) adjustFormulaOperand(token efp.Token, dir adjustDirection, num int, offset int) (string, error) { +func (f *File) adjustFormulaOperand(sheet, sheetN string, token efp.Token, dir adjustDirection, num int, offset int) (string, error) { var ( - err error - sheet, col, row, operand string - cell = token.TValue - tokens = strings.Split(token.TValue, "!") + err error + sheetName, col, row, operand string + cell = token.TValue + tokens = strings.Split(token.TValue, "!") ) if len(tokens) == 2 { // have a worksheet - sheet, cell = tokens[0], tokens[1] - operand = string(efp.QuoteSingle) + sheet + string(efp.QuoteSingle) + "!" + sheetName, cell = tokens[0], tokens[1] + operand = escapeSheetName(sheetName) + "!" + } + if sheet != sheetN && sheet != sheetName { + return operand + cell, err } for _, r := range cell { if ('A' <= r && r <= 'Z') || ('a' <= r && r <= 'z') { @@ -333,19 +370,13 @@ func (f *File) adjustFormulaOperand(token efp.Token, dir adjustDirection, num in } operand += string(r) } - name, err := adjustFormulaColumnName(col, dir, num, offset) - if err != nil { - return operand, err - } - operand += name - name, err = adjustFormulaRowNumber(row, dir, num, offset) - operand += name + _, _, operand, err = adjustFormulaOperandRef(row, col, operand, dir, num, offset) return operand, err } // adjustFormulaRef returns adjusted formula by giving adjusting direction and // the base number of column or row, and offset. -func (f *File) adjustFormulaRef(sheet, formula string, dir adjustDirection, num, offset int) (string, error) { +func (f *File) adjustFormulaRef(sheet, sheetN, formula string, dir adjustDirection, num, offset int) (string, error) { var ( val string definedNames []string @@ -366,7 +397,7 @@ func (f *File) adjustFormulaRef(sheet, formula string, dir adjustDirection, num, val += token.TValue continue } - operand, err := f.adjustFormulaOperand(token, dir, num, offset) + operand, err := f.adjustFormulaOperand(sheet, sheetN, token, dir, num, offset) if err != nil { return val, err } @@ -382,7 +413,7 @@ func (f *File) adjustFormulaRef(sheet, formula string, dir adjustDirection, num, continue } if token.TType == efp.TokenTypeOperand && token.TSubType == efp.TokenSubTypeText { - val += string(efp.QuoteDouble) + token.TValue + string(efp.QuoteDouble) + val += string(efp.QuoteDouble) + strings.ReplaceAll(token.TValue, "\"", "\"\"") + string(efp.QuoteDouble) continue } val += token.TValue @@ -420,7 +451,7 @@ func (f *File) adjustHyperlinks(ws *xlsxWorksheet, sheet string, dir adjustDirec } for i := range ws.Hyperlinks.Hyperlink { link := &ws.Hyperlinks.Hyperlink[i] // get reference - link.Ref, _ = f.adjustFormulaRef(sheet, link.Ref, dir, num, offset) + link.Ref, _ = f.adjustFormulaRef(sheet, sheet, link.Ref, dir, num, offset) } } diff --git a/adjust_test.go b/adjust_test.go index 2356b82963..985d759e38 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -457,6 +457,12 @@ func TestAdjustColDimensions(t *testing.T) { assert.NoError(t, err) assert.NoError(t, f.SetCellFormula("Sheet1", "C3", "A1+B1")) assert.Equal(t, ErrColumnNumber, f.adjustColDimensions("Sheet1", ws, 1, MaxColumns)) + + _, err = f.NewSheet("Sheet2") + assert.NoError(t, err) + f.Sheet.Delete("xl/worksheets/sheet2.xml") + f.Pkg.Store("xl/worksheets/sheet2.xml", MacintoshCyrillicCharset) + assert.EqualError(t, f.adjustColDimensions("Sheet2", ws, 2, 1), "XML syntax error on line 1: invalid UTF-8") } func TestAdjustRowDimensions(t *testing.T) { @@ -465,6 +471,20 @@ func TestAdjustRowDimensions(t *testing.T) { assert.NoError(t, err) assert.NoError(t, f.SetCellFormula("Sheet1", "C3", "A1+B1")) assert.Equal(t, ErrMaxRows, f.adjustRowDimensions("Sheet1", ws, 1, TotalRows)) + + _, err = f.NewSheet("Sheet2") + assert.NoError(t, err) + f.Sheet.Delete("xl/worksheets/sheet2.xml") + f.Pkg.Store("xl/worksheets/sheet2.xml", MacintoshCyrillicCharset) + assert.EqualError(t, f.adjustRowDimensions("Sheet1", ws, 2, 1), "XML syntax error on line 1: invalid UTF-8") + + f = NewFile() + _, err = f.NewSheet("Sheet2") + assert.NoError(t, err) + ws, err = f.workSheetReader("Sheet1") + assert.NoError(t, err) + assert.NoError(t, f.SetCellFormula("Sheet1", "B2", fmt.Sprintf("Sheet2!A%d", TotalRows))) + assert.Equal(t, ErrMaxRows, f.adjustRowDimensions("Sheet2", ws, 1, TotalRows)) } func TestAdjustHyperlinks(t *testing.T) { @@ -523,13 +543,13 @@ func TestAdjustFormula(t *testing.T) { assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAdjustFormula.xlsx"))) assert.NoError(t, f.Close()) - assert.NoError(t, f.adjustFormula("Sheet1", nil, rows, 0, 0, false)) - assert.Equal(t, newCellNameToCoordinatesError("-", newInvalidCellNameError("-")), f.adjustFormula("Sheet1", &xlsxF{Ref: "-"}, rows, 0, 0, false)) - assert.Equal(t, ErrColumnNumber, f.adjustFormula("Sheet1", &xlsxF{Ref: "XFD1:XFD1"}, columns, 0, 1, false)) + assert.NoError(t, f.adjustFormula("Sheet1", "Sheet1", nil, rows, 0, 0, false)) + assert.Equal(t, newCellNameToCoordinatesError("-", newInvalidCellNameError("-")), f.adjustFormula("Sheet1", "Sheet1", &xlsxF{Ref: "-"}, rows, 0, 0, false)) + assert.Equal(t, ErrColumnNumber, f.adjustFormula("Sheet1", "Sheet1", &xlsxF{Ref: "XFD1:XFD1"}, columns, 0, 1, false)) - _, err := f.adjustFormulaRef("Sheet1", "XFE1", columns, 0, 1) + _, err := f.adjustFormulaRef("Sheet1", "Sheet1", "XFE1", columns, 0, 1) assert.Equal(t, ErrColumnNumber, err) - _, err = f.adjustFormulaRef("Sheet1", "XFD1", columns, 0, 1) + _, err = f.adjustFormulaRef("Sheet1", "Sheet1", "XFD1", columns, 0, 1) assert.Equal(t, ErrColumnNumber, err) f = NewFile() @@ -699,17 +719,17 @@ func TestAdjustFormula(t *testing.T) { f = NewFile() // Test adjust formula on insert row in the middle of the range - assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "SUM('Sheet1'!A2,A3)")) + assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "SUM('Sheet 1'!A2,A3)")) assert.NoError(t, f.InsertRows("Sheet1", 3, 1)) formula, err = f.GetCellFormula("Sheet1", "B1") assert.NoError(t, err) - assert.Equal(t, "SUM('Sheet1'!A2,A4)", formula) + assert.Equal(t, "SUM('Sheet 1'!A2,A4)", formula) // Test adjust formula on insert row at the top of the range assert.NoError(t, f.InsertRows("Sheet1", 2, 1)) formula, err = f.GetCellFormula("Sheet1", "B1") assert.NoError(t, err) - assert.Equal(t, "SUM('Sheet1'!A3,A5)", formula) + assert.Equal(t, "SUM('Sheet 1'!A3,A5)", formula) f = NewFile() // Test adjust formula on insert col in the middle of the range @@ -767,4 +787,143 @@ func TestAdjustFormula(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "SUM(D:F)", formula) }) + t.Run("for_all_worksheet_cells_with_rows_insert", func(t *testing.T) { + f := NewFile() + _, err := f.NewSheet("Sheet2") + assert.NoError(t, err) + // Tests formulas referencing Sheet2 should update but those referencing the original sheet should not + tbl := [][]string{ + {"B1", "Sheet2!A1+Sheet2!A2", "Sheet2!A1+Sheet2!A3", "Sheet2!A2+Sheet2!A4"}, + {"C1", "A1+A2", "A1+A2", "A1+A2"}, + {"D1", "Sheet2!B1:B2", "Sheet2!B1:B3", "Sheet2!B2:B4"}, + {"E1", "B1:B2", "B1:B2", "B1:B2"}, + {"F1", "SUM(Sheet2!C1:C2)", "SUM(Sheet2!C1:C3)", "SUM(Sheet2!C2:C4)"}, + {"G1", "SUM(C1:C2)", "SUM(C1:C2)", "SUM(C1:C2)"}, + {"H1", "SUM(Sheet2!D1,Sheet2!D2)", "SUM(Sheet2!D1,Sheet2!D3)", "SUM(Sheet2!D2,Sheet2!D4)"}, + {"I1", "SUM(D1,D2)", "SUM(D1,D2)", "SUM(D1,D2)"}, + } + for _, preset := range tbl { + assert.NoError(t, f.SetCellFormula("Sheet1", preset[0], preset[1])) + } + // Test adjust formula on insert row in the middle of the range + assert.NoError(t, f.InsertRows("Sheet2", 2, 1)) + for _, preset := range tbl { + formula, err := f.GetCellFormula("Sheet1", preset[0]) + assert.NoError(t, err) + assert.Equal(t, preset[2], formula) + } + + // Test adjust formula on insert row in the top of the range + assert.NoError(t, f.InsertRows("Sheet2", 1, 1)) + for _, preset := range tbl { + formula, err := f.GetCellFormula("Sheet1", preset[0]) + assert.NoError(t, err) + assert.Equal(t, preset[3], formula) + } + }) + t.Run("for_all_worksheet_cells_with_cols_insert", func(t *testing.T) { + f := NewFile() + _, err := f.NewSheet("Sheet2") + assert.NoError(t, err) + tbl := [][]string{ + {"A1", "Sheet2!A1+Sheet2!B1", "Sheet2!A1+Sheet2!C1", "Sheet2!B1+Sheet2!D1"}, + {"A2", "A1+B1", "A1+B1", "A1+B1"}, + {"A3", "Sheet2!A2:B2", "Sheet2!A2:C2", "Sheet2!B2:D2"}, + {"A4", "A2:B2", "A2:B2", "A2:B2"}, + {"A5", "SUM(Sheet2!A3:B3)", "SUM(Sheet2!A3:C3)", "SUM(Sheet2!B3:D3)"}, + {"A6", "SUM(A3:B3)", "SUM(A3:B3)", "SUM(A3:B3)"}, + {"A7", "SUM(Sheet2!A4,Sheet2!B4)", "SUM(Sheet2!A4,Sheet2!C4)", "SUM(Sheet2!B4,Sheet2!D4)"}, + {"A8", "SUM(A4,B4)", "SUM(A4,B4)", "SUM(A4,B4)"}, + } + for _, preset := range tbl { + assert.NoError(t, f.SetCellFormula("Sheet1", preset[0], preset[1])) + } + // Test adjust formula on insert column in the middle of the range + assert.NoError(t, f.InsertCols("Sheet2", "B", 1)) + for _, preset := range tbl { + formula, err := f.GetCellFormula("Sheet1", preset[0]) + assert.NoError(t, err) + assert.Equal(t, preset[2], formula) + } + // Test adjust formula on insert column in the top of the range + assert.NoError(t, f.InsertCols("Sheet2", "A", 1)) + for _, preset := range tbl { + formula, err := f.GetCellFormula("Sheet1", preset[0]) + assert.NoError(t, err) + assert.Equal(t, preset[3], formula) + } + }) + t.Run("for_cross_sheet_ref_with_rows_insert)", func(t *testing.T) { + f := NewFile() + _, err := f.NewSheet("Sheet2") + assert.NoError(t, err) + _, err = f.NewSheet("Sheet3") + assert.NoError(t, err) + // Tests formulas referencing Sheet2 should update but those referencing + // the original sheet or Sheet 3 should not update + tbl := [][]string{ + {"B1", "Sheet2!A1+Sheet2!A2+Sheet1!A3+Sheet1!A4", "Sheet2!A1+Sheet2!A3+Sheet1!A3+Sheet1!A4", "Sheet2!A2+Sheet2!A4+Sheet1!A3+Sheet1!A4"}, + {"C1", "Sheet2!B1+Sheet2!B2+B3+B4", "Sheet2!B1+Sheet2!B3+B3+B4", "Sheet2!B2+Sheet2!B4+B3+B4"}, + {"D1", "Sheet2!C1+Sheet2!C2+Sheet3!A3+Sheet3!A4", "Sheet2!C1+Sheet2!C3+Sheet3!A3+Sheet3!A4", "Sheet2!C2+Sheet2!C4+Sheet3!A3+Sheet3!A4"}, + {"E1", "SUM(Sheet2!D1:D2,Sheet1!A3:A4)", "SUM(Sheet2!D1:D3,Sheet1!A3:A4)", "SUM(Sheet2!D2:D4,Sheet1!A3:A4)"}, + {"F1", "SUM(Sheet2!E1:E2,A3:A4)", "SUM(Sheet2!E1:E3,A3:A4)", "SUM(Sheet2!E2:E4,A3:A4)"}, + {"G1", "SUM(Sheet2!F1:F2,Sheet3!A3:A4)", "SUM(Sheet2!F1:F3,Sheet3!A3:A4)", "SUM(Sheet2!F2:F4,Sheet3!A3:A4)"}, + } + for _, preset := range tbl { + assert.NoError(t, f.SetCellFormula("Sheet1", preset[0], preset[1])) + } + // Test adjust formula on insert row in the middle of the range + assert.NoError(t, f.InsertRows("Sheet2", 2, 1)) + for _, preset := range tbl { + formula, err := f.GetCellFormula("Sheet1", preset[0]) + assert.NoError(t, err) + assert.Equal(t, preset[2], formula) + } + // Test adjust formula on insert row in the top of the range + assert.NoError(t, f.InsertRows("Sheet2", 1, 1)) + for _, preset := range tbl { + formula, err := f.GetCellFormula("Sheet1", preset[0]) + assert.NoError(t, err) + assert.Equal(t, preset[3], formula) + } + }) + t.Run("for_cross_sheet_ref_with_cols_insert)", func(t *testing.T) { + f := NewFile() + _, err := f.NewSheet("Sheet2") + assert.NoError(t, err) + _, err = f.NewSheet("Sheet3") + assert.NoError(t, err) + // Tests formulas referencing Sheet2 should update but those referencing + // the original sheet or Sheet 3 should not update + tbl := [][]string{ + {"A1", "Sheet2!A1+Sheet2!B1+Sheet1!C1+Sheet1!D1", "Sheet2!A1+Sheet2!C1+Sheet1!C1+Sheet1!D1", "Sheet2!B1+Sheet2!D1+Sheet1!C1+Sheet1!D1"}, + {"A2", "Sheet2!A2+Sheet2!B2+C2+D2", "Sheet2!A2+Sheet2!C2+C2+D2", "Sheet2!B2+Sheet2!D2+C2+D2"}, + {"A3", "Sheet2!A3+Sheet2!B3+Sheet3!C3+Sheet3!D3", "Sheet2!A3+Sheet2!C3+Sheet3!C3+Sheet3!D3", "Sheet2!B3+Sheet2!D3+Sheet3!C3+Sheet3!D3"}, + {"A4", "SUM(Sheet2!A4:B4,Sheet1!C4:D4)", "SUM(Sheet2!A4:C4,Sheet1!C4:D4)", "SUM(Sheet2!B4:D4,Sheet1!C4:D4)"}, + {"A5", "SUM(Sheet2!A5:B5,C5:D5)", "SUM(Sheet2!A5:C5,C5:D5)", "SUM(Sheet2!B5:D5,C5:D5)"}, + {"A6", "SUM(Sheet2!A6:B6,Sheet3!C6:D6)", "SUM(Sheet2!A6:C6,Sheet3!C6:D6)", "SUM(Sheet2!B6:D6,Sheet3!C6:D6)"}, + } + for _, preset := range tbl { + assert.NoError(t, f.SetCellFormula("Sheet1", preset[0], preset[1])) + } + // Test adjust formula on insert row in the middle of the range + assert.NoError(t, f.InsertCols("Sheet2", "B", 1)) + for _, preset := range tbl { + formula, err := f.GetCellFormula("Sheet1", preset[0]) + assert.NoError(t, err) + assert.Equal(t, preset[2], formula) + } + // Test adjust formula on insert row in the top of the range + assert.NoError(t, f.InsertCols("Sheet2", "A", 1)) + for _, preset := range tbl { + formula, err := f.GetCellFormula("Sheet1", preset[0]) + assert.NoError(t, err) + assert.Equal(t, preset[3], formula) + } + }) + t.Run("for_cross_sheet_ref_with_chart_sheet)", func(t *testing.T) { + assert.NoError(t, f.AddChartSheet("Chart1", &Chart{Type: Line})) + assert.NoError(t, f.InsertRows("Sheet1", 2, 1)) + assert.NoError(t, f.InsertCols("Sheet1", "A", 1)) + }) } diff --git a/rows.go b/rows.go index 992a780751..027d44cbc2 100644 --- a/rows.go +++ b/rows.go @@ -653,7 +653,7 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) error { rowCopy.C = append(make([]xlsxC, 0, len(rowCopy.C)), rowCopy.C...) rowCopy.adjustSingleRowDimensions(row2 - row) - _ = f.adjustSingleRowFormulas(sheet, &rowCopy, row, row2-row, true) + _ = f.adjustSingleRowFormulas(sheet, sheet, &rowCopy, row, row2-row, true) if idx2 != -1 { ws.SheetData.Row[idx2] = rowCopy From 7291e787bd11f1634201329f0e1a5bae1fe8f803 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 2 Nov 2023 00:15:41 +0800 Subject: [PATCH 815/957] Support update volatile dependencies on inserting/deleting columns/rows --- adjust.go | 86 ++++++++++++++++++++++++++++++++++++++++--------- adjust_test.go | 20 ++++++++++++ calcchain.go | 36 +++++++++++++++++++++ excelize.go | 19 +++++------ file.go | 1 + table.go | 2 +- templates.go | 5 +-- xmlCalcChain.go | 49 ++++++++++++++++++++++++++-- xmlTable.go | 6 ++-- 9 files changed, 192 insertions(+), 32 deletions(-) diff --git a/adjust.go b/adjust.go index 608e249af6..67e6b85db3 100644 --- a/adjust.go +++ b/adjust.go @@ -38,7 +38,7 @@ const ( // row: Index number of the row we're inserting/deleting before // offset: Number of rows/column to insert/delete negative values indicate deletion // -// TODO: adjustPageBreaks, adjustComments, adjustDataValidations, adjustProtectedCells +// TODO: adjustComments, adjustDataValidations, adjustDrawings, adjustPageBreaks, adjustProtectedCells func (f *File) adjustHelper(sheet string, dir adjustDirection, num, offset int) error { ws, err := f.workSheetReader(sheet) if err != nil { @@ -66,6 +66,9 @@ func (f *File) adjustHelper(sheet string, dir adjustDirection, num, offset int) if err = f.adjustCalcChain(dir, num, offset, sheetID); err != nil { return err } + if err = f.adjustVolatileDeps(dir, num, offset, sheetID); err != nil { + return err + } if ws.MergeCells != nil && len(ws.MergeCells.Cells) == 0 { ws.MergeCells = nil } @@ -498,7 +501,8 @@ func (f *File) adjustTable(ws *xlsxWorksheet, sheet string, dir adjustDirection, t.AutoFilter.Ref = t.Ref } _ = f.setTableColumns(sheet, true, x1, y1, x2, &t) - t.TotalsRowCount = 0 + // Currently doesn't support query table + t.TableType, t.TotalsRowCount, t.ConnectionID = "", 0, 0 table, _ := xml.Marshal(t) f.saveFileList(tableXML, table) } @@ -578,7 +582,6 @@ func (f *File) adjustMergeCells(ws *xlsxWorksheet, dir adjustDirection, num, off if dir == rows { if y1 == num && y2 == num && offset < 0 { f.deleteMergeCell(ws, i) - i-- continue } @@ -586,7 +589,6 @@ func (f *File) adjustMergeCells(ws *xlsxWorksheet, dir adjustDirection, num, off } else { if x1 == num && x2 == num && offset < 0 { f.deleteMergeCell(ws, i) - i-- continue } @@ -642,18 +644,15 @@ func (f *File) deleteMergeCell(ws *xlsxWorksheet, idx int) { } } -// adjustCalcChainRef update the cell reference in calculation chain when -// inserting or deleting rows or columns. -func (f *File) adjustCalcChainRef(i, c, r, offset int, dir adjustDirection) { +// adjustCellName returns updated cell name by giving column/row number and +// offset on inserting or deleting rows or columns. +func adjustCellName(cell string, dir adjustDirection, c, r, offset int) (string, error) { if dir == rows { if rn := r + offset; rn > 0 { - f.CalcChain.C[i].R, _ = CoordinatesToCellName(c, rn) + return CoordinatesToCellName(c, rn) } - return - } - if nc := c + offset; nc > 0 { - f.CalcChain.C[i].R, _ = CoordinatesToCellName(nc, r) } + return CoordinatesToCellName(c+offset, r) } // adjustCalcChain provides a function to update the calculation chain when @@ -665,7 +664,8 @@ func (f *File) adjustCalcChain(dir adjustDirection, num, offset, sheetID int) er // If sheet ID is omitted, it is assumed to be the same as the i value of // the previous cell. var prevSheetID int - for index, c := range f.CalcChain.C { + for i := 0; i < len(f.CalcChain.C); i++ { + c := f.CalcChain.C[i] if c.I == 0 { c.I = prevSheetID } @@ -680,16 +680,72 @@ func (f *File) adjustCalcChain(dir adjustDirection, num, offset, sheetID int) er if dir == rows && num <= rowNum { if num == rowNum && offset == -1 { _ = f.deleteCalcChain(c.I, c.R) + i-- continue } - f.adjustCalcChainRef(index, colNum, rowNum, offset, dir) + f.CalcChain.C[i].R, _ = adjustCellName(c.R, dir, colNum, rowNum, offset) } if dir == columns && num <= colNum { if num == colNum && offset == -1 { _ = f.deleteCalcChain(c.I, c.R) + i-- continue } - f.adjustCalcChainRef(index, colNum, rowNum, offset, dir) + f.CalcChain.C[i].R, _ = adjustCellName(c.R, dir, colNum, rowNum, offset) + } + } + return nil +} + +// adjustVolatileDepsTopic updates the volatile dependencies topic when +// inserting or deleting rows or columns. +func (vt *xlsxVolTypes) adjustVolatileDepsTopic(cell string, dir adjustDirection, indexes []int) (int, error) { + num, offset, i1, i2, i3, i4 := indexes[0], indexes[1], indexes[2], indexes[3], indexes[4], indexes[5] + colNum, rowNum, err := CellNameToCoordinates(cell) + if err != nil { + return i4, err + } + if dir == rows && num <= rowNum { + if num == rowNum && offset == -1 { + vt.deleteVolTopicRef(i1, i2, i3, i4) + i4-- + return i4, err + } + vt.VolType[i1].Main[i2].Tp[i3].Tr[i4].R, _ = adjustCellName(cell, dir, colNum, rowNum, offset) + } + if dir == columns && num <= colNum { + if num == colNum && offset == -1 { + vt.deleteVolTopicRef(i1, i2, i3, i4) + i4-- + return i4, err + } + if name, _ := adjustCellName(cell, dir, colNum, rowNum, offset); name != "" { + vt.VolType[i1].Main[i2].Tp[i3].Tr[i4].R, _ = adjustCellName(cell, dir, colNum, rowNum, offset) + } + } + return i4, err +} + +// adjustVolatileDeps updates the volatile dependencies when inserting or +// deleting rows or columns. +func (f *File) adjustVolatileDeps(dir adjustDirection, num, offset, sheetID int) error { + volTypes, err := f.volatileDepsReader() + if err != nil || volTypes == nil { + return err + } + for i1 := 0; i1 < len(volTypes.VolType); i1++ { + for i2 := 0; i2 < len(volTypes.VolType[i1].Main); i2++ { + for i3 := 0; i3 < len(volTypes.VolType[i1].Main[i2].Tp); i3++ { + for i4 := 0; i4 < len(volTypes.VolType[i1].Main[i2].Tp[i3].Tr); i4++ { + ref := volTypes.VolType[i1].Main[i2].Tp[i3].Tr[i4] + if ref.S != sheetID { + continue + } + if i4, err = volTypes.adjustVolatileDepsTopic(ref.R, dir, []int{num, offset, i1, i2, i3, i4}); err != nil { + return err + } + } + } } } return nil diff --git a/adjust_test.go b/adjust_test.go index 985d759e38..b2106bc0ec 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -927,3 +927,23 @@ func TestAdjustFormula(t *testing.T) { assert.NoError(t, f.InsertCols("Sheet1", "A", 1)) }) } + +func TestAdjustVolatileDeps(t *testing.T) { + f := NewFile() + f.Pkg.Store(defaultXMLPathVolatileDeps, []byte(fmt.Sprintf(`
`, NameSpaceSpreadSheet.Value))) + assert.NoError(t, f.InsertCols("Sheet1", "A", 1)) + assert.NoError(t, f.InsertRows("Sheet1", 2, 1)) + assert.Equal(t, "D3", f.VolatileDeps.VolType[0].Main[0].Tp[0].Tr[1].R) + assert.NoError(t, f.RemoveCol("Sheet1", "D")) + assert.NoError(t, f.RemoveRow("Sheet1", 4)) + assert.Len(t, f.VolatileDeps.VolType[0].Main[0].Tp[0].Tr, 1) + + f = NewFile() + f.Pkg.Store(defaultXMLPathVolatileDeps, MacintoshCyrillicCharset) + assert.EqualError(t, f.InsertRows("Sheet1", 2, 1), "XML syntax error on line 1: invalid UTF-8") + + f = NewFile() + f.Pkg.Store(defaultXMLPathVolatileDeps, []byte(fmt.Sprintf(`
`, NameSpaceSpreadSheet.Value))) + assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.InsertCols("Sheet1", "A", 1)) + f.volatileDepsWriter() +} diff --git a/calcchain.go b/calcchain.go index c35dd7d480..a7cd259cc7 100644 --- a/calcchain.go +++ b/calcchain.go @@ -81,3 +81,39 @@ func (c xlsxCalcChainCollection) Filter(fn func(v xlsxCalcChainC) bool) []xlsxCa } return results } + +// volatileDepsReader provides a function to get the pointer to the structure +// after deserialization of xl/volatileDependencies.xml. +func (f *File) volatileDepsReader() (*xlsxVolTypes, error) { + if f.VolatileDeps == nil { + volatileDeps, ok := f.Pkg.Load(defaultXMLPathVolatileDeps) + if !ok { + return f.VolatileDeps, nil + } + f.VolatileDeps = new(xlsxVolTypes) + if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(volatileDeps.([]byte)))). + Decode(f.VolatileDeps); err != nil && err != io.EOF { + return f.VolatileDeps, err + } + } + return f.VolatileDeps, nil +} + +// volatileDepsWriter provides a function to save xl/volatileDependencies.xml +// after serialize structure. +func (f *File) volatileDepsWriter() { + if f.VolatileDeps != nil { + output, _ := xml.Marshal(f.VolatileDeps) + f.saveFileList(defaultXMLPathVolatileDeps, output) + } +} + +// deleteVolTopicRef provides a function to remove cell reference on the +// volatile dependencies topic. +func (vt *xlsxVolTypes) deleteVolTopicRef(i1, i2, i3, i4 int) { + for i := range vt.VolType[i1].Main[i2].Tp[i3].Tr { + if i == i4 { + vt.VolType[i1].Main[i2].Tp[i3].Tr = append(vt.VolType[i1].Main[i2].Tp[i3].Tr[:i], vt.VolType[i1].Main[i2].Tp[i3].Tr[i+1:]...) + } + } +} diff --git a/excelize.go b/excelize.go index 79b4b687ab..ae80ce0df2 100644 --- a/excelize.go +++ b/excelize.go @@ -29,31 +29,32 @@ import ( // File define a populated spreadsheet file struct. type File struct { mu sync.Mutex - options *Options - xmlAttr sync.Map checked sync.Map + options *Options + sharedStringItem [][]uint + sharedStringsMap map[string]int + sharedStringTemp *os.File sheetMap map[string]string streams map[string]*StreamWriter tempFiles sync.Map - sharedStringsMap map[string]int - sharedStringItem [][]uint - sharedStringTemp *os.File + xmlAttr sync.Map CalcChain *xlsxCalcChain + CharsetReader charsetTranscoderFn Comments map[string]*xlsxComments ContentTypes *xlsxTypes + DecodeVMLDrawing map[string]*decodeVmlDrawing Drawings sync.Map Path string + Pkg sync.Map + Relationships sync.Map SharedStrings *xlsxSST Sheet sync.Map SheetCount int Styles *xlsxStyleSheet Theme *decodeTheme - DecodeVMLDrawing map[string]*decodeVmlDrawing VMLDrawing map[string]*vmlDrawing + VolatileDeps *xlsxVolTypes WorkBook *xlsxWorkbook - Relationships sync.Map - Pkg sync.Map - CharsetReader charsetTranscoderFn } // charsetTranscoderFn set user-defined codepage transcoder function for open diff --git a/file.go b/file.go index 7f2a036c71..dc29b50dfd 100644 --- a/file.go +++ b/file.go @@ -176,6 +176,7 @@ func (f *File) writeToZip(zw *zip.Writer) error { f.commentsWriter() f.contentTypesWriter() f.drawingsWriter() + f.volatileDepsWriter() f.vmlDrawingWriter() f.workBookWriter() f.workSheetWriter() diff --git a/table.go b/table.go index b9b32f6696..9d5820d974 100644 --- a/table.go +++ b/table.go @@ -292,7 +292,7 @@ func (f *File) setTableColumns(sheet string, showHeaderRow bool, x1, y1, x2 int, } header = append(header, name) if column := getTableColumn(name); column != nil { - column.ID, column.DataDxfID = idx, 0 + column.ID, column.DataDxfID, column.QueryTableFieldID = idx, 0, 0 tableColumns = append(tableColumns, column) continue } diff --git a/templates.go b/templates.go index c94a09b406..41a107c474 100644 --- a/templates.go +++ b/templates.go @@ -218,16 +218,17 @@ const ( ) const ( + defaultTempFileSST = "sharedStrings" + defaultXMLPathCalcChain = "xl/calcChain.xml" defaultXMLPathContentTypes = "[Content_Types].xml" defaultXMLPathDocPropsApp = "docProps/app.xml" defaultXMLPathDocPropsCore = "docProps/core.xml" - defaultXMLPathCalcChain = "xl/calcChain.xml" defaultXMLPathSharedStrings = "xl/sharedStrings.xml" defaultXMLPathStyles = "xl/styles.xml" defaultXMLPathTheme = "xl/theme/theme1.xml" + defaultXMLPathVolatileDeps = "xl/volatileDependencies.xml" defaultXMLPathWorkbook = "xl/workbook.xml" defaultXMLPathWorkbookRels = "xl/_rels/workbook.xml.rels" - defaultTempFileSST = "sharedStrings" ) // IndexedColorMapping is the table of default mappings from indexed color value diff --git a/xmlCalcChain.go b/xmlCalcChain.go index 9c1d1ee21f..0941cf5e50 100644 --- a/xmlCalcChain.go +++ b/xmlCalcChain.go @@ -11,10 +11,15 @@ package excelize -import "encoding/xml" +import ( + "encoding/xml" + "sync" +) -// xlsxCalcChain directly maps the calcChain element. This element represents the root of the calculation chain. +// xlsxCalcChain directly maps the calcChain element. This element represents +// the root of the calculation chain. type xlsxCalcChain struct { + mu sync.Mutex XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main calcChain"` C []xlsxCalcChainC `xml:"c"` } @@ -82,3 +87,43 @@ type xlsxCalcChainC struct { T bool `xml:"t,attr,omitempty"` A bool `xml:"a,attr,omitempty"` } + +// xlsxVolTypes maps the volatileDependencies part provides a cache of data that +// supports Real Time Data (RTD) and CUBE functions in the workbook. +type xlsxVolTypes struct { + mu sync.Mutex + XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main volTypes"` + VolType []xlsxVolType `xml:"volType"` + ExtLst *xlsxExtLst `xml:"extLst"` +} + +// xlsxVolType represents dependency information for a specific type of external +// data server. +type xlsxVolType struct { + Type string `xml:"type,attr"` + Main []xlsxVolMain `xml:"main"` +} + +// xlsxVolMain represents dependency information for all topics within a +// volatile dependency type that share the same first string or function +// argument. +type xlsxVolMain struct { + First string `xml:"first,attr"` + Tp []xlsxVolTopic `xml:"tp"` +} + +// xlsxVolTopic represents dependency information for all topics within a +// volatile dependency type that share the same first string or argument. +type xlsxVolTopic struct { + T string `xml:"t,attr,omitempty"` + V string `xml:"v"` + Stp []string `xml:"stp"` + Tr []xlsxVolTopicRef `xml:"tr"` +} + +// xlsxVolTopicRef represents the reference to a cell that depends on this +// topic. Each topic can have one or more cells dependencies. +type xlsxVolTopicRef struct { + R string `xml:"r,attr"` + S int `xml:"s,attr"` +} diff --git a/xmlTable.go b/xmlTable.go index 193113586d..4fc00b1cca 100644 --- a/xmlTable.go +++ b/xmlTable.go @@ -37,12 +37,12 @@ type xlsxTable struct { DataDxfID int `xml:"dataDxfId,attr,omitempty"` TotalsRowDxfID int `xml:"totalsRowDxfId,attr,omitempty"` HeaderRowBorderDxfID int `xml:"headerRowBorderDxfId,attr,omitempty"` - TableBorderDxfId int `xml:"tableBorderDxfId,attr,omitempty"` - TotalsRowBorderDxfId int `xml:"totalsRowBorderDxfId,attr,omitempty"` + TableBorderDxfID int `xml:"tableBorderDxfId,attr,omitempty"` + TotalsRowBorderDxfID int `xml:"totalsRowBorderDxfId,attr,omitempty"` HeaderRowCellStyle string `xml:"headerRowCellStyle,attr,omitempty"` DataCellStyle string `xml:"dataCellStyle,attr,omitempty"` TotalsRowCellStyle string `xml:"totalsRowCellStyle,attr,omitempty"` - ConnectionId int `xml:"connectionId,attr,omitempty"` + ConnectionID int `xml:"connectionId,attr,omitempty"` AutoFilter *xlsxAutoFilter `xml:"autoFilter"` TableColumns *xlsxTableColumns `xml:"tableColumns"` TableStyleInfo *xlsxTableStyleInfo `xml:"tableStyleInfo"` From 4e936dafdd6d89edbe80b508b5fcf4402ab8da4c Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 3 Nov 2023 00:12:43 +0800 Subject: [PATCH 816/957] This closes #1706 and closes #1708 - Add export ChartLineType enumeration to specify the chart line type - Add new Border field in the Chart type to set the chart area border - Add new Type field in the ChartLine type to set the line type - Fix some format missing on get style definition - Update the unit tests --- chart.go | 32 ++++++++++++++++++++++++------- chart_test.go | 6 +++--- drawing.go | 50 +++++++++++++++++++++++++++++++------------------ styles.go | 14 ++++++++------ styles_test.go | 6 ++++++ xmlCalcChain.go | 7 +------ xmlChart.go | 16 +++++++++------- 7 files changed, 84 insertions(+), 47 deletions(-) diff --git a/chart.go b/chart.go index f2473f162b..5b7a5a29ff 100644 --- a/chart.go +++ b/chart.go @@ -80,6 +80,16 @@ const ( Bubble3D ) +// ChartLineType is the type of supported chart line types. +type ChartLineType byte + +// This section defines the currently supported chart line types enumeration. +const ( + ChartLineSolid ChartLineType = iota + ChartLineNone + ChartLineAutomatic +) + // This section defines the default value of chart properties. var ( chartView3DRotX = map[ChartType]int{ @@ -507,6 +517,21 @@ func parseChartOptions(opts *Chart) (*Chart, error) { if opts.Legend.Position == "" { opts.Legend.Position = defaultChartLegendPosition } + opts.parseTitle() + if opts.VaryColors == nil { + opts.VaryColors = boolPtr(true) + } + if opts.Border.Width == 0 { + opts.Border.Width = 0.75 + } + if opts.ShowBlanksAs == "" { + opts.ShowBlanksAs = defaultChartShowBlanksAs + } + return opts, nil +} + +// parseTitle parse the title settings of the chart with default value. +func (opts *Chart) parseTitle() { for i := range opts.Title { if opts.Title[i].Font == nil { opts.Title[i].Font = &Font{} @@ -518,13 +543,6 @@ func parseChartOptions(opts *Chart) (*Chart, error) { opts.Title[i].Font.Size = 14 } } - 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 diff --git a/chart_test.go b/chart_test.go index 4516f5c56e..a988d3f6f5 100644 --- a/chart_test.go +++ b/chart_test.go @@ -208,9 +208,9 @@ 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: []RichTextRun{{Text: "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: []RichTextRun{{Text: "2D Stacked Column Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, - {sheetName: "Sheet1", cell: "P16", opts: &Chart{Type: ColPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "100% Stacked Column Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "P1", opts: &Chart{Type: Col, Series: series, Format: format, Legend: ChartLegend{Position: "none", ShowLegendKey: true}, Title: []RichTextRun{{Text: "2D Column Chart"}}, PlotArea: plotArea, Border: ChartLine{Type: ChartLineNone}, 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: []RichTextRun{{Text: "2D Stacked Column Chart"}}, PlotArea: plotArea, Border: ChartLine{Type: ChartLineAutomatic}, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "P16", opts: &Chart{Type: ColPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "100% Stacked Column Chart"}}, PlotArea: plotArea, Border: ChartLine{Type: ChartLineSolid, Width: 2}, ShowBlanksAs: "zero"}}, {sheetName: "Sheet1", cell: "X16", opts: &Chart{Type: Col3DClustered, Series: series, Format: format, Legend: ChartLegend{Position: "bottom", ShowLegendKey: false}, Title: []RichTextRun{{Text: "3D Clustered Column Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet1", cell: "P30", opts: &Chart{Type: Col3DStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Stacked Column Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet1", cell: "X30", opts: &Chart{Type: Col3DPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D 100% Stacked Column Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, diff --git a/drawing.go b/drawing.go index c7bf88a0c9..a65c88559d 100644 --- a/drawing.go +++ b/drawing.go @@ -94,23 +94,7 @@ func (f *File) addChart(opts *Chart, comboCharts []*Chart) { SolidFill: &aSolidFill{ SchemeClr: &aSchemeClr{Val: "bg1"}, }, - Ln: &aLn{ - W: 9525, - Cap: "flat", - Cmpd: "sng", - Algn: "ctr", - SolidFill: &aSolidFill{ - SchemeClr: &aSchemeClr{ - Val: "tx1", - LumMod: &attrValInt{ - Val: intPtr(15000), - }, - LumOff: &attrValInt{ - Val: intPtr(85000), - }, - }, - }, - }, + Ln: f.drawChartLn(&opts.Border), }, PrintSettings: &cPrintSettings{ PageMargins: &cPageMargins{ @@ -756,7 +740,7 @@ func (f *File) drawChartSeriesSpPr(i int, opts *Chart) *cSpPr { spPrScatter := &cSpPr{ Ln: &aLn{ W: 25400, - NoFill: " ", + NoFill: &attrValString{}, }, } spPrLine := &cSpPr{ @@ -1237,6 +1221,36 @@ func (f *File) drawPlotAreaTxPr(opts *ChartAxis) *cTxPr { return cTxPr } +// drawChartLn provides a function to draw the a:ln element. +func (f *File) drawChartLn(opts *ChartLine) *aLn { + ln := &aLn{ + W: f.ptToEMUs(opts.Width), + Cap: "flat", + Cmpd: "sng", + Algn: "ctr", + } + switch opts.Type { + case ChartLineSolid: + ln.SolidFill = &aSolidFill{ + SchemeClr: &aSchemeClr{ + Val: "tx1", + LumMod: &attrValInt{ + Val: intPtr(15000), + }, + LumOff: &attrValInt{ + Val: intPtr(85000), + }, + }, + } + return ln + case ChartLineNone: + ln.NoFill = &attrValString{} + return ln + default: + return nil + } +} + // drawingParser provides a function to parse drawingXML. In order to solve // the problem that the label structure is changed after serialization and // deserialization, two different structures: decodeWsDr and encodeWsDr are diff --git a/styles.go b/styles.go index 0ca82fd100..ed28234cee 100644 --- a/styles.go +++ b/styles.go @@ -1201,25 +1201,25 @@ var ( // extract style definition by given style. extractStyleCondFuncs = map[string]func(xlsxXf, *xlsxStyleSheet) bool{ "fill": func(xf xlsxXf, s *xlsxStyleSheet) bool { - return xf.ApplyFill != nil && *xf.ApplyFill && + return (xf.ApplyFill == nil || (xf.ApplyFill != nil && *xf.ApplyFill)) && xf.FillID != nil && s.Fills != nil && *xf.FillID < len(s.Fills.Fill) }, "border": func(xf xlsxXf, s *xlsxStyleSheet) bool { - return xf.ApplyBorder != nil && *xf.ApplyBorder && + return (xf.ApplyBorder == nil || (xf.ApplyBorder != nil && *xf.ApplyBorder)) && xf.BorderID != nil && s.Borders != nil && *xf.BorderID < len(s.Borders.Border) }, "font": func(xf xlsxXf, s *xlsxStyleSheet) bool { - return xf.ApplyFont != nil && *xf.ApplyFont && + return (xf.ApplyFont == nil || (xf.ApplyFont != nil && *xf.ApplyFont)) && xf.FontID != nil && s.Fonts != nil && *xf.FontID < len(s.Fonts.Font) }, "alignment": func(xf xlsxXf, s *xlsxStyleSheet) bool { - return xf.ApplyAlignment != nil && *xf.ApplyAlignment + return xf.ApplyAlignment == nil || (xf.ApplyAlignment != nil && *xf.ApplyAlignment) }, "protection": func(xf xlsxXf, s *xlsxStyleSheet) bool { - return xf.ApplyProtection != nil && *xf.ApplyProtection + return xf.ApplyProtection == nil || (xf.ApplyProtection != nil && *xf.ApplyProtection) }, } // drawContFmtFunc defines functions to create conditional formats. @@ -1366,7 +1366,9 @@ func (f *File) extractFont(fnt *xlsxFont, s *xlsxStyleSheet, style *Style) { font.Italic = fnt.I.Value() } if fnt.U != nil { - font.Underline = fnt.U.Value() + if font.Underline = fnt.U.Value(); font.Underline == "" { + font.Underline = "single" + } } if fnt.Name != nil { font.Family = fnt.Name.Value() diff --git a/styles_test.go b/styles_test.go index a886c20f7c..dd7f7189a5 100644 --- a/styles_test.go +++ b/styles_test.go @@ -606,6 +606,12 @@ func TestGetStyle(t *testing.T) { } assert.Equal(t, "012345", f.getThemeColor(&xlsxColor{Indexed: 0})) + f.Styles.Fonts.Font[0].U = &attrValString{} + f.Styles.CellXfs.Xf[0].FontID = intPtr(0) + style, err = f.GetStyle(styleID) + assert.NoError(t, err) + assert.Equal(t, "single", style.Font.Underline) + // Test get style with invalid style index style, err = f.GetStyle(-1) assert.Nil(t, style) diff --git a/xmlCalcChain.go b/xmlCalcChain.go index 0941cf5e50..358cd8d174 100644 --- a/xmlCalcChain.go +++ b/xmlCalcChain.go @@ -11,15 +11,11 @@ package excelize -import ( - "encoding/xml" - "sync" -) +import "encoding/xml" // xlsxCalcChain directly maps the calcChain element. This element represents // the root of the calculation chain. type xlsxCalcChain struct { - mu sync.Mutex XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main calcChain"` C []xlsxCalcChainC `xml:"c"` } @@ -91,7 +87,6 @@ type xlsxCalcChainC struct { // xlsxVolTypes maps the volatileDependencies part provides a cache of data that // supports Real Time Data (RTD) and CUBE functions in the workbook. type xlsxVolTypes struct { - mu sync.Mutex XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main volTypes"` VolType []xlsxVolType `xml:"volType"` ExtLst *xlsxExtLst `xml:"extLst"` diff --git a/xmlChart.go b/xmlChart.go index c69a6afdd8..93dd857843 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -248,13 +248,13 @@ type aContourClr struct { // shapes and text. The line allows for the specifying of many different types // of outlines including even line dashes and bevels. type aLn struct { - Algn string `xml:"algn,attr,omitempty"` - Cap string `xml:"cap,attr,omitempty"` - Cmpd string `xml:"cmpd,attr,omitempty"` - W int `xml:"w,attr,omitempty"` - NoFill string `xml:"a:noFill,omitempty"` - Round string `xml:"a:round,omitempty"` - SolidFill *aSolidFill `xml:"a:solidFill"` + Algn string `xml:"algn,attr,omitempty"` + Cap string `xml:"cap,attr,omitempty"` + Cmpd string `xml:"cmpd,attr,omitempty"` + W int `xml:"w,attr,omitempty"` + NoFill *attrValString `xml:"a:noFill"` + Round string `xml:"a:round,omitempty"` + SolidFill *aSolidFill `xml:"a:solidFill"` } // cTxPr (Text Properties) directly maps the txPr element. This element @@ -575,6 +575,7 @@ type Chart struct { XAxis ChartAxis YAxis ChartAxis PlotArea ChartPlotArea + Border ChartLine ShowBlanksAs string HoleSize int order int @@ -594,6 +595,7 @@ type ChartMarker struct { // ChartLine directly maps the format settings of the chart line. type ChartLine struct { + Type ChartLineType Smooth bool Width float64 } From f753e560fa975f2dd671382ac07203f284ce945c Mon Sep 17 00:00:00 2001 From: magicrabbit <31507468+phperic@users.noreply.github.com> Date: Sat, 4 Nov 2023 08:02:09 +0800 Subject: [PATCH 817/957] Fix number format scientific notation zero fill issues (#1710) --- numfmt.go | 7 +++---- numfmt_test.go | 2 ++ 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/numfmt.go b/numfmt.go index a6f456e203..a303b640f9 100644 --- a/numfmt.go +++ b/numfmt.go @@ -4953,7 +4953,7 @@ func (nf *numberFormat) numberHandler() string { fracLen = nf.fracPadding } if isNum, precision, decimal := isNumeric(nf.value); isNum { - if precision > 15 && intLen+fracLen > 15 { + if precision > 15 && intLen+fracLen > 15 && !nf.useScientificNotation { return nf.printNumberLiteral(nf.printBigNumber(decimal, fracLen)) } } @@ -4961,14 +4961,13 @@ func (nf *numberFormat) numberHandler() string { if fracLen > 0 { paddingLen++ } - flag := "f" + fmtCode := fmt.Sprintf("%%0%d.%df%s", paddingLen, fracLen, strings.Repeat("%%", nf.percent)) if nf.useScientificNotation { if nf.expBaseLen != 2 { return nf.value } - flag = "E" + fmtCode = fmt.Sprintf("%%.%dE%s", fracLen, strings.Repeat("%%", nf.percent)) } - 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)) } diff --git a/numfmt_test.go b/numfmt_test.go index 45d7d0f0d0..b449feb44c 100644 --- a/numfmt_test.go +++ b/numfmt_test.go @@ -3539,6 +3539,8 @@ func TestNumFmt(t *testing.T) { {"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"}, + {"2312312321.1231198", "0.00E+00", "2.31E+09"}, + {"3.2234623764278598E+33", "0.00E+00", "3.22E+33"}, {"1.234E-16", "0.00000000000000000000", "0.00000000000000012340"}, {"1.234E-16", "0.000000000000000000", "0.000000000000000123"}, {"1.234E-16", "0.000000000000000000%", "0.000000000000012340%"}, From fe639faa45b3786778982d158cb9797a20614a23 Mon Sep 17 00:00:00 2001 From: Anton Petrov <55432120+kjushka@users.noreply.github.com> Date: Mon, 6 Nov 2023 04:51:19 +0300 Subject: [PATCH 818/957] This closes #1125, support update drawing objects on inserting/deleting columns/rows (#1127) --- adjust.go | 176 ++++++++++++++++++++++++++++++++++++++------ adjust_test.go | 105 +++++++++++++++++++++++--- picture.go | 95 ++++++++++-------------- picture_test.go | 8 +- xmlDecodeDrawing.go | 33 +++++++-- xmlDrawing.go | 18 +++++ 6 files changed, 335 insertions(+), 100 deletions(-) diff --git a/adjust.go b/adjust.go index 67e6b85db3..34bee6c990 100644 --- a/adjust.go +++ b/adjust.go @@ -29,6 +29,28 @@ const ( rows adjustDirection = true ) +// adjustHelperFunc defines functions to adjust helper. +var adjustHelperFunc = [6]func(*File, *xlsxWorksheet, string, adjustDirection, int, int, int) error{ + func(f *File, ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error { + return f.adjustTable(ws, sheet, dir, num, offset, sheetID) + }, + func(f *File, ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error { + return f.adjustMergeCells(ws, sheet, dir, num, offset, sheetID) + }, + func(f *File, ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error { + return f.adjustAutoFilter(ws, sheet, dir, num, offset, sheetID) + }, + func(f *File, ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error { + return f.adjustCalcChain(ws, sheet, dir, num, offset, sheetID) + }, + func(f *File, ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error { + return f.adjustVolatileDeps(ws, sheet, dir, num, offset, sheetID) + }, + func(f *File, ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error { + return f.adjustDrawings(ws, sheet, dir, num, offset, sheetID) + }, +} + // adjustHelper provides a function to adjust rows and columns dimensions, // hyperlinks, merged cells and auto filter when inserting or deleting rows or // columns. @@ -56,23 +78,14 @@ func (f *File) adjustHelper(sheet string, dir adjustDirection, num, offset int) f.adjustHyperlinks(ws, sheet, dir, num, offset) ws.checkSheet() _ = ws.checkRow() - f.adjustTable(ws, sheet, dir, num, offset) - if err = f.adjustMergeCells(ws, dir, num, offset); err != nil { - return err - } - if err = f.adjustAutoFilter(ws, dir, num, offset); err != nil { - return err - } - if err = f.adjustCalcChain(dir, num, offset, sheetID); err != nil { - return err - } - if err = f.adjustVolatileDeps(dir, num, offset, sheetID); err != nil { - return err + for _, fn := range adjustHelperFunc { + if err := fn(f, ws, sheet, dir, num, offset, sheetID); err != nil { + return err + } } if ws.MergeCells != nil && len(ws.MergeCells.Cells) == 0 { ws.MergeCells = nil } - return nil } @@ -460,9 +473,9 @@ 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) { +func (f *File) adjustTable(ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error { if ws.TableParts == nil || len(ws.TableParts.TableParts) == 0 { - return + return nil } for idx := 0; idx < len(ws.TableParts.TableParts); idx++ { tbl := ws.TableParts.TableParts[idx] @@ -475,11 +488,11 @@ func (f *File) adjustTable(ws *xlsxWorksheet, sheet string, dir adjustDirection, t := xlsxTable{} if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(content.([]byte)))). Decode(&t); err != nil && err != io.EOF { - return + return nil } coordinates, err := rangeRefToCoordinates(t.Ref) if err != nil { - return + return err } // Remove the table when deleting the header row of the table if dir == rows && num == coordinates[0] && offset == -1 { @@ -506,11 +519,12 @@ func (f *File) adjustTable(ws *xlsxWorksheet, sheet string, dir adjustDirection, table, _ := xml.Marshal(t) f.saveFileList(tableXML, table) } + return nil } // 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 { +func (f *File) adjustAutoFilter(ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error { if ws.AutoFilter == nil { return nil } @@ -563,7 +577,7 @@ func (f *File) adjustAutoFilterHelper(dir adjustDirection, coordinates []int, nu // adjustMergeCells provides a function to update merged cells when inserting // or deleting rows or columns. -func (f *File) adjustMergeCells(ws *xlsxWorksheet, dir adjustDirection, num, offset int) error { +func (f *File) adjustMergeCells(ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error { if ws.MergeCells == nil { return nil } @@ -657,7 +671,7 @@ func adjustCellName(cell string, dir adjustDirection, c, r, offset int) (string, // adjustCalcChain provides a function to update the calculation chain when // inserting or deleting rows or columns. -func (f *File) adjustCalcChain(dir adjustDirection, num, offset, sheetID int) error { +func (f *File) adjustCalcChain(ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error { if f.CalcChain == nil { return nil } @@ -728,7 +742,7 @@ func (vt *xlsxVolTypes) adjustVolatileDepsTopic(cell string, dir adjustDirection // adjustVolatileDeps updates the volatile dependencies when inserting or // deleting rows or columns. -func (f *File) adjustVolatileDeps(dir adjustDirection, num, offset, sheetID int) error { +func (f *File) adjustVolatileDeps(ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error { volTypes, err := f.volatileDepsReader() if err != nil || volTypes == nil { return err @@ -750,3 +764,123 @@ func (f *File) adjustVolatileDeps(dir adjustDirection, num, offset, sheetID int) } return nil } + +// adjustDrawings updates the starting anchor of the two cell anchor pictures +// and charts object when inserting or deleting rows or columns. +func (from *xlsxFrom) adjustDrawings(dir adjustDirection, num, offset int, editAs string) (bool, error) { + var ok bool + if dir == columns && from.Col+1 >= num && from.Col+offset >= 0 { + if from.Col+offset >= MaxColumns { + return false, ErrColumnNumber + } + from.Col += offset + ok = editAs == "oneCell" + } + if dir == rows && from.Row+1 >= num && from.Row+offset >= 0 { + if from.Row+offset >= TotalRows { + return false, ErrMaxRows + } + from.Row += offset + ok = editAs == "oneCell" + } + return ok, nil +} + +// adjustDrawings updates the ending anchor of the two cell anchor pictures +// and charts object when inserting or deleting rows or columns. +func (to *xlsxTo) adjustDrawings(dir adjustDirection, num, offset int, editAs string, ok bool) error { + if dir == columns && to.Col+1 >= num && to.Col+offset >= 0 && ok { + if to.Col+offset >= MaxColumns { + return ErrColumnNumber + } + to.Col += offset + } + if dir == rows && to.Row+1 >= num && to.Row+offset >= 0 && ok { + if to.Row+offset >= TotalRows { + return ErrMaxRows + } + to.Row += offset + } + return nil +} + +// adjustDrawings updates the two cell anchor pictures and charts object when +// inserting or deleting rows or columns. +func (a *xdrCellAnchor) adjustDrawings(dir adjustDirection, num, offset int) error { + editAs := a.EditAs + if a.From == nil || a.To == nil || editAs == "absolute" { + return nil + } + ok, err := a.From.adjustDrawings(dir, num, offset, editAs) + if err != nil { + return err + } + return a.To.adjustDrawings(dir, num, offset, editAs, ok || editAs == "") +} + +// adjustDrawings updates the existing two cell anchor pictures and charts +// object when inserting or deleting rows or columns. +func (a *xlsxCellAnchorPos) adjustDrawings(dir adjustDirection, num, offset int, editAs string) error { + if a.From == nil || a.To == nil || editAs == "absolute" { + return nil + } + ok, err := a.From.adjustDrawings(dir, num, offset, editAs) + if err != nil { + return err + } + return a.To.adjustDrawings(dir, num, offset, editAs, ok || editAs == "") +} + +// adjustDrawings updates the pictures and charts object when inserting or +// deleting rows or columns. +func (f *File) adjustDrawings(ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error { + if ws.Drawing == nil { + return nil + } + target := f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID) + drawingXML := strings.TrimPrefix(strings.ReplaceAll(target, "..", "xl"), "/") + var ( + err error + wsDr *xlsxWsDr + ) + if wsDr, _, err = f.drawingParser(drawingXML); err != nil { + return err + } + anchorCb := func(a *xdrCellAnchor) error { + if a.GraphicFrame == "" { + return a.adjustDrawings(dir, num, offset) + } + deCellAnchor := decodeCellAnchor{} + deCellAnchorPos := decodeCellAnchorPos{} + _ = f.xmlNewDecoder(strings.NewReader("" + a.GraphicFrame + "")).Decode(&deCellAnchor) + _ = f.xmlNewDecoder(strings.NewReader("" + a.GraphicFrame + "")).Decode(&deCellAnchorPos) + xlsxCellAnchorPos := xlsxCellAnchorPos(deCellAnchorPos) + for i := 0; i < len(xlsxCellAnchorPos.AlternateContent); i++ { + xlsxCellAnchorPos.AlternateContent[i].XMLNSMC = SourceRelationshipCompatibility.Value + } + if deCellAnchor.From != nil { + xlsxCellAnchorPos.From = &xlsxFrom{ + Col: deCellAnchor.From.Col, ColOff: deCellAnchor.From.ColOff, + Row: deCellAnchor.From.Row, RowOff: deCellAnchor.From.RowOff, + } + } + if deCellAnchor.To != nil { + xlsxCellAnchorPos.To = &xlsxTo{ + Col: deCellAnchor.To.Col, ColOff: deCellAnchor.To.ColOff, + Row: deCellAnchor.To.Row, RowOff: deCellAnchor.To.RowOff, + } + } + if err = xlsxCellAnchorPos.adjustDrawings(dir, num, offset, a.EditAs); err != nil { + return err + } + cellAnchor, _ := xml.Marshal(xlsxCellAnchorPos) + a.GraphicFrame = strings.TrimSuffix(strings.TrimPrefix(string(cellAnchor), ""), "") + return err + } + for _, anchor := range wsDr.TwoCellAnchor { + if err = anchorCb(anchor); err != nil { + return err + } + } + return nil +} diff --git a/adjust_test.go b/adjust_test.go index b2106bc0ec..1cf39fee7a 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -1,10 +1,13 @@ package excelize import ( + "encoding/xml" "fmt" "path/filepath" "testing" + _ "image/jpeg" + "github.com/stretchr/testify/assert" ) @@ -19,7 +22,7 @@ func TestAdjustMergeCells(t *testing.T) { }, }, }, - }, rows, 0, 0), newCellNameToCoordinatesError("A", newInvalidCellNameError("A"))) + }, "Sheet1", rows, 0, 0, 1), newCellNameToCoordinatesError("A", newInvalidCellNameError("A"))) assert.Equal(t, f.adjustMergeCells(&xlsxWorksheet{ MergeCells: &xlsxMergeCells{ Cells: []*xlsxMergeCell{ @@ -28,7 +31,7 @@ func TestAdjustMergeCells(t *testing.T) { }, }, }, - }, rows, 0, 0), newCellNameToCoordinatesError("B", newInvalidCellNameError("B"))) + }, "Sheet1", rows, 0, 0, 1), newCellNameToCoordinatesError("B", newInvalidCellNameError("B"))) assert.NoError(t, f.adjustMergeCells(&xlsxWorksheet{ MergeCells: &xlsxMergeCells{ Cells: []*xlsxMergeCell{ @@ -37,7 +40,7 @@ func TestAdjustMergeCells(t *testing.T) { }, }, }, - }, rows, 1, -1)) + }, "Sheet1", rows, 1, -1, 1)) assert.NoError(t, f.adjustMergeCells(&xlsxWorksheet{ MergeCells: &xlsxMergeCells{ Cells: []*xlsxMergeCell{ @@ -46,7 +49,7 @@ func TestAdjustMergeCells(t *testing.T) { }, }, }, - }, columns, 1, -1)) + }, "Sheet1", columns, 1, -1, 1)) assert.NoError(t, f.adjustMergeCells(&xlsxWorksheet{ MergeCells: &xlsxMergeCells{ Cells: []*xlsxMergeCell{ @@ -55,7 +58,7 @@ func TestAdjustMergeCells(t *testing.T) { }, }, }, - }, columns, 1, -1)) + }, "Sheet1", columns, 1, -1, 1)) // Test adjust merge cells var cases []struct { @@ -134,7 +137,7 @@ func TestAdjustMergeCells(t *testing.T) { }, } for _, c := range cases { - assert.NoError(t, f.adjustMergeCells(c.ws, c.dir, c.num, 1)) + assert.NoError(t, f.adjustMergeCells(c.ws, "Sheet1", c.dir, c.num, 1, 1)) assert.Equal(t, c.expect, c.ws.MergeCells.Cells[0].Ref, c.label) assert.Equal(t, c.expectRect, c.ws.MergeCells.Cells[0].rect, c.label) } @@ -223,7 +226,7 @@ func TestAdjustMergeCells(t *testing.T) { }, } for _, c := range cases { - assert.NoError(t, f.adjustMergeCells(c.ws, c.dir, c.num, -1)) + assert.NoError(t, f.adjustMergeCells(c.ws, "Sheet1", c.dir, c.num, -1, 1)) assert.Equal(t, c.expect, c.ws.MergeCells.Cells[0].Ref, c.label) } @@ -271,7 +274,7 @@ func TestAdjustMergeCells(t *testing.T) { }, } for _, c := range cases { - assert.NoError(t, f.adjustMergeCells(c.ws, c.dir, c.num, -1)) + assert.NoError(t, f.adjustMergeCells(c.ws, "Sheet1", c.dir, c.num, -1, 1)) assert.Len(t, c.ws.MergeCells.Cells, 0, c.label) } @@ -291,18 +294,18 @@ func TestAdjustAutoFilter(t *testing.T) { AutoFilter: &xlsxAutoFilter{ Ref: "A1:A3", }, - }, rows, 1, -1)) + }, "Sheet1", rows, 1, -1, 1)) // Test adjustAutoFilter with illegal cell reference assert.Equal(t, f.adjustAutoFilter(&xlsxWorksheet{ AutoFilter: &xlsxAutoFilter{ Ref: "A:B1", }, - }, rows, 0, 0), newCellNameToCoordinatesError("A", newInvalidCellNameError("A"))) + }, "Sheet1", rows, 0, 0, 1), newCellNameToCoordinatesError("A", newInvalidCellNameError("A"))) assert.Equal(t, f.adjustAutoFilter(&xlsxWorksheet{ AutoFilter: &xlsxAutoFilter{ Ref: "A1:B", }, - }, rows, 0, 0), newCellNameToCoordinatesError("B", newInvalidCellNameError("B"))) + }, "Sheet1", rows, 0, 0, 1), newCellNameToCoordinatesError("B", newInvalidCellNameError("B"))) } func TestAdjustTable(t *testing.T) { @@ -334,7 +337,7 @@ func TestAdjustTable(t *testing.T) { 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)) + assert.Equal(t, ErrParameterInvalid, f.RemoveRow(sheetName, 1)) } func TestAdjustHelper(t *testing.T) { @@ -947,3 +950,81 @@ func TestAdjustVolatileDeps(t *testing.T) { assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.InsertCols("Sheet1", "A", 1)) f.volatileDepsWriter() } + +func TestAdjustDrawings(t *testing.T) { + f := NewFile() + // Test add pictures to sheet with positioning + assert.NoError(t, f.AddPicture("Sheet1", "B2", filepath.Join("test", "images", "excel.jpg"), nil)) + assert.NoError(t, f.AddPicture("Sheet1", "B11", filepath.Join("test", "images", "excel.jpg"), &GraphicOptions{Positioning: "oneCell"})) + assert.NoError(t, f.AddPicture("Sheet1", "B21", filepath.Join("test", "images", "excel.jpg"), &GraphicOptions{Positioning: "absolute"})) + + // Test adjust pictures on inserting columns and rows + assert.NoError(t, f.InsertCols("Sheet1", "A", 1)) + assert.NoError(t, f.InsertRows("Sheet1", 1, 1)) + assert.NoError(t, f.InsertCols("Sheet1", "C", 1)) + assert.NoError(t, f.InsertRows("Sheet1", 5, 1)) + assert.NoError(t, f.InsertRows("Sheet1", 15, 1)) + cells, err := f.GetPictureCells("Sheet1") + assert.NoError(t, err) + assert.Equal(t, []string{"D3", "D13", "B21"}, cells) + wb := filepath.Join("test", "TestAdjustDrawings.xlsx") + assert.NoError(t, f.SaveAs(wb)) + + // Test adjust pictures on deleting columns and rows + assert.NoError(t, f.RemoveCol("Sheet1", "A")) + assert.NoError(t, f.RemoveRow("Sheet1", 1)) + cells, err = f.GetPictureCells("Sheet1") + assert.NoError(t, err) + assert.Equal(t, []string{"C2", "C12", "B21"}, cells) + + // Test adjust existing pictures on inserting columns and rows + f, err = OpenFile(wb) + assert.NoError(t, err) + assert.NoError(t, f.InsertCols("Sheet1", "A", 1)) + assert.NoError(t, f.InsertRows("Sheet1", 1, 1)) + assert.NoError(t, f.InsertCols("Sheet1", "D", 1)) + assert.NoError(t, f.InsertRows("Sheet1", 5, 1)) + assert.NoError(t, f.InsertRows("Sheet1", 16, 1)) + cells, err = f.GetPictureCells("Sheet1") + assert.NoError(t, err) + assert.Equal(t, []string{"F4", "F15", "B21"}, cells) + + // Test adjust drawings with unsupported charset + f, err = OpenFile(wb) + assert.NoError(t, err) + f.Pkg.Store("xl/drawings/drawing1.xml", MacintoshCyrillicCharset) + assert.EqualError(t, f.InsertCols("Sheet1", "A", 1), "XML syntax error on line 1: invalid UTF-8") + + errors := []error{ErrColumnNumber, ErrColumnNumber, ErrMaxRows, ErrMaxRows} + cells = []string{"XFD1", "XFB1"} + for i, cell := range cells { + f = NewFile() + assert.NoError(t, f.AddPicture("Sheet1", cell, filepath.Join("test", "images", "excel.jpg"), nil)) + assert.Equal(t, errors[i], f.InsertCols("Sheet1", "A", 1)) + assert.NoError(t, f.SaveAs(wb)) + f, err = OpenFile(wb) + assert.NoError(t, err) + assert.Equal(t, errors[i], f.InsertCols("Sheet1", "A", 1)) + } + errors = []error{ErrMaxRows, ErrMaxRows} + cells = []string{"A1048576", "A1048570"} + for i, cell := range cells { + f = NewFile() + assert.NoError(t, f.AddPicture("Sheet1", cell, filepath.Join("test", "images", "excel.jpg"), nil)) + assert.Equal(t, errors[i], f.InsertRows("Sheet1", 1, 1)) + assert.NoError(t, f.SaveAs(wb)) + f, err = OpenFile(wb) + assert.NoError(t, err) + assert.Equal(t, errors[i], f.InsertRows("Sheet1", 1, 1)) + } + + a := xdrCellAnchor{} + assert.NoError(t, a.adjustDrawings(columns, 0, 0)) + p := xlsxCellAnchorPos{} + assert.NoError(t, p.adjustDrawings(columns, 0, 0, "")) + + f, err = OpenFile(wb) + assert.NoError(t, err) + f.Pkg.Store("xl/drawings/drawing1.xml", []byte(xml.Header+`00001010`)) + assert.NoError(t, f.InsertCols("Sheet1", "A", 1)) +} diff --git a/picture.go b/picture.go index 46a2c39447..499f4937c2 100644 --- a/picture.go +++ b/picture.go @@ -15,7 +15,6 @@ import ( "bytes" "encoding/xml" "image" - "io" "os" "path" "path/filepath" @@ -471,7 +470,7 @@ func (f *File) GetPictures(sheet, cell string) ([]Picture, error) { return nil, err } target := f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID) - drawingXML := strings.ReplaceAll(target, "..", "xl") + drawingXML := strings.TrimPrefix(strings.ReplaceAll(target, "..", "xl"), "/") drawingRelationships := strings.ReplaceAll( strings.ReplaceAll(target, "../drawings", "xl/drawings/_rels"), ".xml", ".xml.rels") @@ -492,7 +491,7 @@ func (f *File) GetPictureCells(sheet string) ([]string, error) { return nil, err } target := f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID) - drawingXML := strings.ReplaceAll(target, "..", "xl") + drawingXML := strings.TrimPrefix(strings.ReplaceAll(target, "..", "xl"), "/") drawingRelationships := strings.ReplaceAll( strings.ReplaceAll(target, "../drawings", "xl/drawings/_rels"), ".xml", ".xml.rels") @@ -553,15 +552,15 @@ 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) (pics []Picture, err error) { - var ( - deWsDr = new(decodeWsDr) - wsDr *xlsxWsDr - ) + var wsDr *xlsxWsDr if wsDr, _, err = f.drawingParser(drawingXML); err != nil { return } - anchorCond := func(a *xdrCellAnchor) bool { return a.From.Col == col && a.From.Row == row } - anchorCb := func(a *xdrCellAnchor, r *xlsxRelationship) { + wsDr.mu.Lock() + defer wsDr.mu.Unlock() + cond := func(from *xlsxFrom) bool { return from.Col == col && from.Row == row } + cond2 := func(from *decodeFrom) bool { return from.Col == col && from.Row == row } + cb := func(a *xdrCellAnchor, r *xlsxRelationship) { pic := Picture{Extension: filepath.Ext(r.Target), Format: &GraphicOptions{}} if buffer, _ := f.Pkg.Load(strings.ReplaceAll(r.Target, "..", "xl")); buffer != nil { pic.File = buffer.([]byte) @@ -569,14 +568,7 @@ func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) pics = append(pics, pic) } } - f.extractCellAnchor(drawingRelationships, wsDr, anchorCond, anchorCb) - if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(drawingXML)))). - Decode(deWsDr); err != nil && err != io.EOF { - return - } - err = nil - decodeAnchorCond := func(a *decodeCellAnchor) bool { return a.From.Col == col && a.From.Row == row } - decodeAnchorCb := func(a *decodeCellAnchor, r *xlsxRelationship) { + cb2 := func(a *decodeCellAnchor, r *xlsxRelationship) { pic := Picture{Extension: filepath.Ext(r.Target), Format: &GraphicOptions{}} if buffer, _ := f.Pkg.Load(strings.ReplaceAll(r.Target, "..", "xl")); buffer != nil { pic.File = buffer.([]byte) @@ -584,11 +576,11 @@ func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) pics = append(pics, pic) } } - for _, anchor := range deWsDr.TwoCellAnchor { - f.extractDecodeCellAnchor(anchor, drawingRelationships, decodeAnchorCond, decodeAnchorCb) + for _, anchor := range wsDr.TwoCellAnchor { + f.extractCellAnchor(anchor, drawingRelationships, cond, cb, cond2, cb2) } - for _, anchor := range deWsDr.OneCellAnchor { - f.extractDecodeCellAnchor(anchor, drawingRelationships, decodeAnchorCond, decodeAnchorCb) + for _, anchor := range wsDr.OneCellAnchor { + f.extractCellAnchor(anchor, drawingRelationships, cond, cb, cond2, cb2) } return } @@ -596,18 +588,14 @@ func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) // extractCellAnchor extract drawing object from cell anchor by giving drawing // cell anchor, drawing relationships part path, conditional and callback // function. -func (f *File) extractCellAnchor(drawingRelationships string, wsDr *xlsxWsDr, - cond func(anchor *xdrCellAnchor) bool, cb func(anchor *xdrCellAnchor, rels *xlsxRelationship), +func (f *File) extractCellAnchor(anchor *xdrCellAnchor, drawingRelationships string, + cond func(from *xlsxFrom) bool, cb func(anchor *xdrCellAnchor, rels *xlsxRelationship), + cond2 func(from *decodeFrom) bool, cb2 func(anchor *decodeCellAnchor, rels *xlsxRelationship), ) { - var ( - anchor *xdrCellAnchor - drawRel *xlsxRelationship - ) - wsDr.mu.Lock() - defer wsDr.mu.Unlock() - for _, anchor = range wsDr.TwoCellAnchor { + var drawRel *xlsxRelationship + if anchor.GraphicFrame == "" { if anchor.From != nil && anchor.Pic != nil { - if cond(anchor) { + if cond(anchor.From) { if drawRel = f.getDrawingRelationships(drawingRelationships, anchor.Pic.BlipFill.Blip.Embed); drawRel != nil { if _, ok := supportedImageTypes[strings.ToLower(filepath.Ext(drawRel.Target))]; ok { @@ -616,25 +604,24 @@ func (f *File) extractCellAnchor(drawingRelationships string, wsDr *xlsxWsDr, } } } + return } + f.extractDecodeCellAnchor(anchor, drawingRelationships, cond2, cb2) } // extractDecodeCellAnchor extract drawing object from cell anchor by giving // decoded drawing cell anchor, drawing relationships part path, conditional and // callback function. -func (f *File) extractDecodeCellAnchor(anchor *decodeCellAnchor, drawingRelationships string, - cond func(anchor *decodeCellAnchor) bool, cb func(anchor *decodeCellAnchor, rels *xlsxRelationship), +func (f *File) extractDecodeCellAnchor(anchor *xdrCellAnchor, drawingRelationships string, + cond func(from *decodeFrom) bool, cb func(anchor *decodeCellAnchor, rels *xlsxRelationship), ) { var ( drawRel *xlsxRelationship deCellAnchor = new(decodeCellAnchor) ) - if err := f.xmlNewDecoder(strings.NewReader("" + anchor.Content + "")). - Decode(deCellAnchor); err != nil && err != io.EOF { - return - } + _ = f.xmlNewDecoder(strings.NewReader("" + anchor.GraphicFrame + "")).Decode(&deCellAnchor) if deCellAnchor.From != nil && deCellAnchor.Pic != nil { - if cond(deCellAnchor) { + if cond(deCellAnchor.From) { drawRel = f.getDrawingRelationships(drawingRelationships, deCellAnchor.Pic.BlipFill.Blip.Embed) if _, ok := supportedImageTypes[strings.ToLower(filepath.Ext(drawRel.Target))]; ok { cb(deCellAnchor, drawRel) @@ -720,42 +707,36 @@ func (f *File) drawingResize(sheet, cell string, width, height float64, opts *Gr // worksheet by given drawing part path and drawing relationships path. func (f *File) getPictureCells(drawingXML, drawingRelationships string) ([]string, error) { var ( - cells []string - err error - deWsDr *decodeWsDr - wsDr *xlsxWsDr + cells []string + err error + wsDr *xlsxWsDr ) if wsDr, _, err = f.drawingParser(drawingXML); err != nil { return cells, err } - anchorCond := func(a *xdrCellAnchor) bool { return true } - anchorCb := func(a *xdrCellAnchor, r *xlsxRelationship) { + wsDr.mu.Lock() + defer wsDr.mu.Unlock() + cond := func(from *xlsxFrom) bool { return true } + cond2 := func(from *decodeFrom) bool { return true } + cb := func(a *xdrCellAnchor, r *xlsxRelationship) { if _, ok := f.Pkg.Load(strings.ReplaceAll(r.Target, "..", "xl")); ok { if cell, err := CoordinatesToCellName(a.From.Col+1, a.From.Row+1); err == nil && inStrSlice(cells, cell, true) == -1 { cells = append(cells, cell) } } } - f.extractCellAnchor(drawingRelationships, wsDr, anchorCond, anchorCb) - deWsDr = new(decodeWsDr) - if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(drawingXML)))). - Decode(deWsDr); err != nil && err != io.EOF { - return cells, err - } - err = nil - decodeAnchorCond := func(a *decodeCellAnchor) bool { return true } - decodeAnchorCb := func(a *decodeCellAnchor, r *xlsxRelationship) { + cb2 := func(a *decodeCellAnchor, r *xlsxRelationship) { if _, ok := f.Pkg.Load(strings.ReplaceAll(r.Target, "..", "xl")); ok { if cell, err := CoordinatesToCellName(a.From.Col+1, a.From.Row+1); err == nil && inStrSlice(cells, cell, true) == -1 { cells = append(cells, cell) } } } - for _, anchor := range deWsDr.TwoCellAnchor { - f.extractDecodeCellAnchor(anchor, drawingRelationships, decodeAnchorCond, decodeAnchorCb) + for _, anchor := range wsDr.TwoCellAnchor { + f.extractCellAnchor(anchor, drawingRelationships, cond, cb, cond2, cb2) } - for _, anchor := range deWsDr.OneCellAnchor { - f.extractDecodeCellAnchor(anchor, drawingRelationships, decodeAnchorCond, decodeAnchorCb) + for _, anchor := range wsDr.OneCellAnchor { + f.extractCellAnchor(anchor, drawingRelationships, cond, cb, cond2, cb2) } return cells, err } diff --git a/picture_test.go b/picture_test.go index 8460c361b7..5422046b7f 100644 --- a/picture_test.go +++ b/picture_test.go @@ -81,7 +81,7 @@ func TestAddPicture(t *testing.T) { // Test get picture cells cells, err := f.GetPictureCells("Sheet1") assert.NoError(t, err) - assert.Equal(t, []string{"A30", "F21", "B30", "Q1", "Q8", "Q15", "Q22", "Q28"}, cells) + assert.Equal(t, []string{"F21", "A30", "B30", "Q1", "Q8", "Q15", "Q22", "Q28"}, cells) assert.NoError(t, f.Close()) f, err = OpenFile(filepath.Join("test", "TestAddPicture1.xlsx")) @@ -92,6 +92,7 @@ func TestAddPicture(t *testing.T) { assert.NoError(t, err) assert.Equal(t, []string{"F21", "A30", "B30", "Q1", "Q8", "Q15", "Q22", "Q28"}, cells) // Test get picture cells with unsupported charset + f.Drawings.Delete(path) f.Pkg.Store(path, MacintoshCyrillicCharset) _, err = f.GetPictureCells("Sheet1") assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") @@ -226,6 +227,7 @@ func TestGetPicture(t *testing.T) { // Test get pictures with unsupported charset path := "xl/drawings/drawing1.xml" + f.Drawings.Delete(path) 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") @@ -388,7 +390,7 @@ func TestGetPictureCells(t *testing.T) { func TestExtractDecodeCellAnchor(t *testing.T) { f := NewFile() - cond := func(a *decodeCellAnchor) bool { return true } + cond := func(a *decodeFrom) bool { return true } cb := func(a *decodeCellAnchor, r *xlsxRelationship) {} - f.extractDecodeCellAnchor(&decodeCellAnchor{Content: string(MacintoshCyrillicCharset)}, "", cond, cb) + f.extractDecodeCellAnchor(&xdrCellAnchor{GraphicFrame: string(MacintoshCyrillicCharset)}, "", cond, cb) } diff --git a/xmlDecodeDrawing.go b/xmlDecodeDrawing.go index 62251deadc..fb4ea07bd8 100644 --- a/xmlDecodeDrawing.go +++ b/xmlDecodeDrawing.go @@ -18,13 +18,32 @@ import "encoding/xml" // specifies a two cell anchor placeholder for a group, a shape, or a drawing // element. It moves with cells and its extents are in EMU units. type decodeCellAnchor struct { - EditAs string `xml:"editAs,attr,omitempty"` - From *decodeFrom `xml:"from"` - To *decodeTo `xml:"to"` - Sp *decodeSp `xml:"sp"` - Pic *decodePic `xml:"pic"` - ClientData *decodeClientData `xml:"clientData"` - Content string `xml:",innerxml"` + EditAs string `xml:"editAs,attr,omitempty"` + From *decodeFrom `xml:"from"` + To *decodeTo `xml:"to"` + Sp *decodeSp `xml:"sp"` + Pic *decodePic `xml:"pic"` + ClientData *decodeClientData `xml:"clientData"` + AlternateContent []*xlsxAlternateContent `xml:"mc:AlternateContent"` + Content string `xml:",innerxml"` +} + +// decodeCellAnchorPos defines the structure used to deserialize the cell anchor +// for adjust drawing object on inserting/deleting column/rows. +type decodeCellAnchorPos struct { + EditAs string `xml:"editAs,attr,omitempty"` + From *xlsxFrom `xml:"from"` + To *xlsxTo `xml:"to"` + Pos *xlsxInnerXML `xml:"pos"` + Ext *xlsxInnerXML `xml:"ext"` + Sp *xlsxInnerXML `xml:"sp"` + GrpSp *xlsxInnerXML `xml:"grpSp"` + GraphicFrame *xlsxInnerXML `xml:"graphicFrame"` + CxnSp *xlsxInnerXML `xml:"cxnSp"` + Pic *xlsxInnerXML `xml:"pic"` + ContentPart *xlsxInnerXML `xml:"contentPart"` + AlternateContent []*xlsxAlternateContent `xml:"AlternateContent"` + ClientData *xlsxInnerXML `xml:"clientData"` } // xdrSp (Shape) directly maps the sp element. This element specifies the diff --git a/xmlDrawing.go b/xmlDrawing.go index e5dc384ae9..26a8e2f149 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -230,6 +230,24 @@ type xdrCellAnchor struct { ClientData *xdrClientData `xml:"xdr:clientData"` } +// xlsxCellAnchorPos defines the structure used to serialize the cell anchor for +// adjust drawing object on inserting/deleting column/rows. +type xlsxCellAnchorPos struct { + EditAs string `xml:"editAs,attr,omitempty"` + From *xlsxFrom `xml:"xdr:from"` + To *xlsxTo `xml:"xdr:to"` + Pos *xlsxInnerXML `xml:"xdr:pos"` + Ext *xlsxInnerXML `xml:"xdr:ext"` + Sp *xlsxInnerXML `xml:"xdr:sp"` + GrpSp *xlsxInnerXML `xml:"xdr:grpSp"` + GraphicFrame *xlsxInnerXML `xml:"xdr:graphicFrame"` + CxnSp *xlsxInnerXML `xml:"xdr:cxnSp"` + Pic *xlsxInnerXML `xml:"xdr:pic"` + ContentPart *xlsxInnerXML `xml:"xdr:contentPart"` + AlternateContent []*xlsxAlternateContent `xml:"mc:AlternateContent"` + ClientData *xlsxInnerXML `xml:"xdr:clientData"` +} + // xlsxPoint2D describes the position of a drawing element within a spreadsheet. type xlsxPoint2D struct { XMLName xml.Name `xml:"xdr:pos"` From 6cc1a547abd4ed47a61536d97b47e00383faf512 Mon Sep 17 00:00:00 2001 From: Marko Krstic Date: Tue, 7 Nov 2023 01:46:01 +0100 Subject: [PATCH 819/957] This closes #1712, reduce memory consumption (#1713) --- sheet.go | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/sheet.go b/sheet.go index 6166773dc0..daf6f4c813 100644 --- a/sheet.go +++ b/sheet.go @@ -178,36 +178,39 @@ func (f *File) workSheetWriter() { // trimRow provides a function to trim empty rows. func trimRow(sheetData *xlsxSheetData) []xlsxRow { var ( - row xlsxRow - rows []xlsxRow + row xlsxRow + i int ) - for k, v := range sheetData.Row { + + for k := range sheetData.Row { row = sheetData.Row[k] - if row.C = trimCell(v.C); len(row.C) != 0 || row.hasAttr() { - rows = append(rows, row) + if row = trimCell(row); len(row.C) != 0 || row.hasAttr() { + sheetData.Row[i] = row } + i++ } - return rows + return sheetData.Row[:i] } // trimCell provides a function to trim blank cells which created by fillColumns. -func trimCell(column []xlsxC) []xlsxC { +func trimCell(row xlsxRow) xlsxRow { + column := row.C rowFull := true for i := range column { rowFull = column[i].hasValue() && rowFull } if rowFull { - return column + return row } - col := make([]xlsxC, len(column)) i := 0 for _, c := range column { if c.hasValue() { - col[i] = c + row.C[i] = c i++ } } - return col[:i] + row.C = row.C[:i] + return row } // setContentTypes provides a function to read and update property of contents From a0252bd05af1c8c1bc16ecedff79575a912494a1 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 8 Nov 2023 00:01:35 +0800 Subject: [PATCH 820/957] Support update defined names on inserting/deleting columns/rows --- adjust.go | 169 ++++++++++++++++++++++++++++--------------------- adjust_test.go | 81 +++++++++++++++++++++++- 2 files changed, 174 insertions(+), 76 deletions(-) diff --git a/adjust.go b/adjust.go index 34bee6c990..450e49c719 100644 --- a/adjust.go +++ b/adjust.go @@ -30,9 +30,12 @@ const ( ) // adjustHelperFunc defines functions to adjust helper. -var adjustHelperFunc = [6]func(*File, *xlsxWorksheet, string, adjustDirection, int, int, int) error{ +var adjustHelperFunc = [7]func(*File, *xlsxWorksheet, string, adjustDirection, int, int, int) error{ func(f *File, ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error { - return f.adjustTable(ws, sheet, dir, num, offset, sheetID) + return f.adjustDefinedNames(ws, sheet, dir, num, offset, sheetID) + }, + func(f *File, ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error { + return f.adjustDrawings(ws, sheet, dir, num, offset, sheetID) }, func(f *File, ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error { return f.adjustMergeCells(ws, sheet, dir, num, offset, sheetID) @@ -44,10 +47,10 @@ var adjustHelperFunc = [6]func(*File, *xlsxWorksheet, string, adjustDirection, i return f.adjustCalcChain(ws, sheet, dir, num, offset, sheetID) }, func(f *File, ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error { - return f.adjustVolatileDeps(ws, sheet, dir, num, offset, sheetID) + return f.adjustTable(ws, sheet, dir, num, offset, sheetID) }, func(f *File, ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error { - return f.adjustDrawings(ws, sheet, dir, num, offset, sheetID) + return f.adjustVolatileDeps(ws, sheet, dir, num, offset, sheetID) }, } @@ -60,7 +63,7 @@ var adjustHelperFunc = [6]func(*File, *xlsxWorksheet, string, adjustDirection, i // row: Index number of the row we're inserting/deleting before // offset: Number of rows/column to insert/delete negative values indicate deletion // -// TODO: adjustComments, adjustDataValidations, adjustDrawings, adjustPageBreaks, adjustProtectedCells +// TODO: adjustComments, adjustDataValidations, adjustPageBreaks, adjustProtectedCells func (f *File) adjustHelper(sheet string, dir adjustDirection, num, offset int) error { ws, err := f.workSheetReader(sheet) if err != nil { @@ -97,38 +100,34 @@ func (f *File) adjustCols(ws *xlsxWorksheet, col, offset int) error { } 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 { + if ws.Cols.Col[i].Min += offset; ws.Cols.Col[i].Min > MaxColumns { ws.Cols.Col = append(ws.Cols.Col[:i], ws.Cols.Col[i+1:]...) - } else { - ws.Cols.Col = nil + i-- + continue } - 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 + if ws.Cols.Col[i].Max >= col || ws.Cols.Col[i].Max+1 == col { + if ws.Cols.Col[i].Max += offset; ws.Cols.Col[i].Max > MaxColumns { + ws.Cols.Col[i].Max = MaxColumns + } } + continue } + if ws.Cols.Col[i].Min == col && ws.Cols.Col[i].Max == col { + ws.Cols.Col = append(ws.Cols.Col[:i], ws.Cols.Col[i+1:]...) + i-- + continue + } + if ws.Cols.Col[i].Min > col { + ws.Cols.Col[i].Min += offset + } + if ws.Cols.Col[i].Max >= col { + ws.Cols.Col[i].Max += offset + } + } + if len(ws.Cols.Col) == 0 { + ws.Cols = nil } return nil } @@ -274,7 +273,7 @@ func (f *File) adjustFormula(sheet, sheetN string, formula *xlsxF, dir adjustDir } } if formula.Content != "" { - if formula.Content, err = f.adjustFormulaRef(sheet, sheetN, formula.Content, dir, num, offset); err != nil { + if formula.Content, err = f.adjustFormulaRef(sheet, sheetN, formula.Content, false, dir, num, offset); err != nil { return err } } @@ -297,62 +296,60 @@ func escapeSheetName(name string) string { if strings.IndexFunc(name, func(r rune) bool { return !unicode.IsLetter(r) && !unicode.IsNumber(r) }) != -1 { - return string(efp.QuoteSingle) + name + string(efp.QuoteSingle) + return "'" + strings.ReplaceAll(name, "'", "''") + "'" } return name } // adjustFormulaColumnName adjust column name in the formula reference. -func adjustFormulaColumnName(name string, dir adjustDirection, num, offset int) (string, error) { +func adjustFormulaColumnName(name, operand string, abs, keepRelative bool, dir adjustDirection, num, offset int) (string, string, bool, error) { + if name == "" || (!abs && keepRelative) { + return "", operand + name, abs, nil + } col, err := ColumnNameToNumber(name) if err != nil { - return name, err + return "", operand, false, err } if dir == columns && col >= num { col += offset - return ColumnNumberToName(col) + colName, err := ColumnNumberToName(col) + return "", operand + colName, false, err } - return name, nil + return "", operand + name, false, nil } // adjustFormulaRowNumber adjust row number in the formula reference. -func adjustFormulaRowNumber(name string, dir adjustDirection, num, offset int) (string, error) { +func adjustFormulaRowNumber(name, operand string, abs, keepRelative bool, dir adjustDirection, num, offset int) (string, string, bool, error) { + if name == "" || (!abs && keepRelative) { + return "", operand + name, abs, nil + } row, _ := strconv.Atoi(name) if dir == rows && row >= num { row += offset - if row > TotalRows { - return name, ErrMaxRows + if row <= 0 || row > TotalRows { + return "", operand + name, false, ErrMaxRows } - return strconv.Itoa(row), nil + return "", operand + strconv.Itoa(row), false, nil } - return name, nil + return "", operand + name, false, nil } // adjustFormulaOperandRef adjust cell reference in the operand tokens for the formula. -func adjustFormulaOperandRef(row, col, operand string, dir adjustDirection, num int, offset int) (string, string, string, error) { - if col != "" { - name, err := adjustFormulaColumnName(col, dir, num, offset) - if err != nil { - return row, col, operand, err - } - operand += name - col = "" - } - if row != "" { - name, err := adjustFormulaRowNumber(row, dir, num, offset) - if err != nil { - return row, col, operand, err - } - operand += name - row = "" +func adjustFormulaOperandRef(row, col, operand string, abs, keepRelative bool, dir adjustDirection, num int, offset int) (string, string, string, bool, error) { + var err error + col, operand, abs, err = adjustFormulaColumnName(col, operand, abs, keepRelative, dir, num, offset) + if err != nil { + return row, col, operand, abs, err } - return row, col, operand, nil + row, operand, abs, err = adjustFormulaRowNumber(row, operand, abs, keepRelative, dir, num, offset) + return row, col, operand, abs, err } // adjustFormulaOperand adjust range operand tokens for the formula. -func (f *File) adjustFormulaOperand(sheet, sheetN string, token efp.Token, dir adjustDirection, num int, offset int) (string, error) { +func (f *File) adjustFormulaOperand(sheet, sheetN string, keepRelative bool, token efp.Token, dir adjustDirection, num int, offset int) (string, error) { var ( err error + abs bool sheetName, col, row, operand string cell = token.TValue tokens = strings.Split(token.TValue, "!") @@ -365,34 +362,38 @@ func (f *File) adjustFormulaOperand(sheet, sheetN string, token efp.Token, dir a return operand + cell, err } for _, r := range cell { + if r == '$' { + if col, operand, _, err = adjustFormulaColumnName(col, operand, abs, keepRelative, dir, num, offset); err != nil { + return operand, err + } + abs = true + operand += string(r) + continue + } if ('A' <= r && r <= 'Z') || ('a' <= r && r <= 'z') { col += string(r) continue } if '0' <= r && r <= '9' { row += string(r) - if col != "" { - name, err := adjustFormulaColumnName(col, dir, num, offset) - if err != nil { - return operand, err - } - operand += name - col = "" + col, operand, abs, err = adjustFormulaColumnName(col, operand, abs, keepRelative, dir, num, offset) + if err != nil { + return operand, err } continue } - if row, col, operand, err = adjustFormulaOperandRef(row, col, operand, dir, num, offset); err != nil { + if row, col, operand, abs, err = adjustFormulaOperandRef(row, col, operand, abs, keepRelative, dir, num, offset); err != nil { return operand, err } operand += string(r) } - _, _, operand, err = adjustFormulaOperandRef(row, col, operand, dir, num, offset) + _, _, operand, _, err = adjustFormulaOperandRef(row, col, operand, abs, keepRelative, dir, num, offset) return operand, err } // adjustFormulaRef returns adjusted formula by giving adjusting direction and // the base number of column or row, and offset. -func (f *File) adjustFormulaRef(sheet, sheetN, formula string, dir adjustDirection, num, offset int) (string, error) { +func (f *File) adjustFormulaRef(sheet, sheetN, formula string, keepRelative bool, dir adjustDirection, num, offset int) (string, error) { var ( val string definedNames []string @@ -404,6 +405,10 @@ func (f *File) adjustFormulaRef(sheet, sheetN, formula string, dir adjustDirecti } } for _, token := range ps.Parse(formula) { + if token.TType == efp.TokenTypeUnknown { + val = formula + break + } if token.TType == efp.TokenTypeOperand && token.TSubType == efp.TokenSubTypeRange { if inStrSlice(definedNames, token.TValue, true) != -1 { val += token.TValue @@ -413,7 +418,7 @@ func (f *File) adjustFormulaRef(sheet, sheetN, formula string, dir adjustDirecti val += token.TValue continue } - operand, err := f.adjustFormulaOperand(sheet, sheetN, token, dir, num, offset) + operand, err := f.adjustFormulaOperand(sheet, sheetN, keepRelative, token, dir, num, offset) if err != nil { return val, err } @@ -467,7 +472,7 @@ func (f *File) adjustHyperlinks(ws *xlsxWorksheet, sheet string, dir adjustDirec } for i := range ws.Hyperlinks.Hyperlink { link := &ws.Hyperlinks.Hyperlink[i] // get reference - link.Ref, _ = f.adjustFormulaRef(sheet, sheet, link.Ref, dir, num, offset) + link.Ref, _ = f.adjustFormulaRef(sheet, sheet, link.Ref, false, dir, num, offset) } } @@ -488,7 +493,7 @@ func (f *File) adjustTable(ws *xlsxWorksheet, sheet string, dir adjustDirection, t := xlsxTable{} if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(content.([]byte)))). Decode(&t); err != nil && err != io.EOF { - return nil + return err } coordinates, err := rangeRefToCoordinates(t.Ref) if err != nil { @@ -884,3 +889,21 @@ func (f *File) adjustDrawings(ws *xlsxWorksheet, sheet string, dir adjustDirecti } return nil } + +// adjustDefinedNames updates the cell reference of the defined names when +// inserting or deleting rows or columns. +func (f *File) adjustDefinedNames(ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error { + wb, err := f.workbookReader() + if err != nil { + return err + } + if wb.DefinedNames != nil { + for i := 0; i < len(wb.DefinedNames.DefinedName); i++ { + data := wb.DefinedNames.DefinedName[i].Data + if data, err = f.adjustFormulaRef(sheet, "", data, true, dir, num, offset); err == nil { + wb.DefinedNames.DefinedName[i].Data = data + } + } + } + return nil +} diff --git a/adjust_test.go b/adjust_test.go index 1cf39fee7a..c33de8acdc 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -334,7 +334,7 @@ func TestAdjustTable(t *testing.T) { 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)) + assert.EqualError(t, f.RemoveRow(sheetName, 1), "XML syntax error on line 1: invalid UTF-8") // Test adjust table with invalid table range reference f.Pkg.Store("xl/tables/table1.xml", []byte(`
`)) assert.Equal(t, ErrParameterInvalid, f.RemoveRow(sheetName, 1)) @@ -452,6 +452,17 @@ func TestAdjustCols(t *testing.T) { assert.NoError(t, f.RemoveCol(sheetName, "A")) assert.NoError(t, f.Close()) + + f = NewFile() + assert.NoError(t, f.SetColWidth("Sheet1", "XFB", "XFC", 12)) + assert.NoError(t, f.InsertCols("Sheet1", "A", 2)) + ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml") + assert.True(t, ok) + assert.Equal(t, MaxColumns, ws.(*xlsxWorksheet).Cols.Col[0].Min) + assert.Equal(t, MaxColumns, ws.(*xlsxWorksheet).Cols.Col[0].Max) + + assert.NoError(t, f.InsertCols("Sheet1", "A", 2)) + assert.Nil(t, ws.(*xlsxWorksheet).Cols) } func TestAdjustColDimensions(t *testing.T) { @@ -550,9 +561,9 @@ func TestAdjustFormula(t *testing.T) { assert.Equal(t, newCellNameToCoordinatesError("-", newInvalidCellNameError("-")), f.adjustFormula("Sheet1", "Sheet1", &xlsxF{Ref: "-"}, rows, 0, 0, false)) assert.Equal(t, ErrColumnNumber, f.adjustFormula("Sheet1", "Sheet1", &xlsxF{Ref: "XFD1:XFD1"}, columns, 0, 1, false)) - _, err := f.adjustFormulaRef("Sheet1", "Sheet1", "XFE1", columns, 0, 1) + _, err := f.adjustFormulaRef("Sheet1", "Sheet1", "XFE1", false, columns, 0, 1) assert.Equal(t, ErrColumnNumber, err) - _, err = f.adjustFormulaRef("Sheet1", "Sheet1", "XFD1", columns, 0, 1) + _, err = f.adjustFormulaRef("Sheet1", "Sheet1", "XFD1", false, columns, 0, 1) assert.Equal(t, ErrColumnNumber, err) f = NewFile() @@ -1028,3 +1039,67 @@ func TestAdjustDrawings(t *testing.T) { f.Pkg.Store("xl/drawings/drawing1.xml", []byte(xml.Header+`00001010`)) assert.NoError(t, f.InsertCols("Sheet1", "A", 1)) } + +func TestAdjustDefinedNames(t *testing.T) { + f := NewFile() + _, err := f.NewSheet("Sheet2") + assert.NoError(t, err) + for _, dn := range []*DefinedName{ + {Name: "Name1", RefersTo: "Sheet1!$XFD$1"}, + {Name: "Name2", RefersTo: "Sheet2!$C$1", Scope: "Sheet1"}, + {Name: "Name3", RefersTo: "Sheet2!$C$1:$D$2", Scope: "Sheet1"}, + {Name: "Name4", RefersTo: "Sheet2!$C1:D$2"}, + {Name: "Name5", RefersTo: "Sheet2!C$1:$D2"}, + {Name: "Name6", RefersTo: "Sheet2!C:$D"}, + {Name: "Name7", RefersTo: "Sheet2!$C:D"}, + {Name: "Name8", RefersTo: "Sheet2!C:D"}, + {Name: "Name9", RefersTo: "Sheet2!$C:$D"}, + {Name: "Name10", RefersTo: "Sheet2!1:2"}, + } { + assert.NoError(t, f.SetDefinedName(dn)) + } + assert.NoError(t, f.InsertCols("Sheet1", "A", 1)) + assert.NoError(t, f.InsertRows("Sheet1", 1, 1)) + assert.NoError(t, f.InsertCols("Sheet2", "A", 1)) + assert.NoError(t, f.InsertRows("Sheet2", 1, 1)) + definedNames := f.GetDefinedName() + for i, expected := range []string{ + "Sheet1!$XFD$2", + "Sheet2!$D$2", + "Sheet2!$D$2:$E$3", + "Sheet2!$D1:D$3", + "Sheet2!C$2:$E2", + "Sheet2!C:$E", + "Sheet2!$D:D", + "Sheet2!C:D", + "Sheet2!$D:$E", + "Sheet2!1:2", + } { + assert.Equal(t, expected, definedNames[i].RefersTo) + } + + f = NewFile() + assert.NoError(t, f.SetDefinedName(&DefinedName{ + Name: "Name1", + RefersTo: "Sheet1!$A$1", + Scope: "Sheet1", + })) + assert.NoError(t, f.RemoveCol("Sheet1", "A")) + definedNames = f.GetDefinedName() + assert.Equal(t, "Sheet1!$A$1", definedNames[0].RefersTo) + + f = NewFile() + assert.NoError(t, f.SetDefinedName(&DefinedName{ + Name: "Name1", + RefersTo: "'1.A & B C'!#REF!", + Scope: "Sheet1", + })) + assert.NoError(t, f.RemoveCol("Sheet1", "A")) + definedNames = f.GetDefinedName() + assert.Equal(t, "'1.A & B C'!#REF!", definedNames[0].RefersTo) + + f = NewFile() + f.WorkBook = nil + f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset) + assert.EqualError(t, f.adjustDefinedNames(nil, "Sheet1", columns, 0, 0, 1), "XML syntax error on line 1: invalid UTF-8") +} From 134865d9d2e99be0cdb71a1f4a18393ac2570796 Mon Sep 17 00:00:00 2001 From: Nick Date: Thu, 9 Nov 2023 00:15:10 +0800 Subject: [PATCH 821/957] Update data validation type and operator's enumerations (#1714) --- datavalidation.go | 162 ++++++++++++++++++++--------------------- datavalidation_test.go | 6 ++ 2 files changed, 84 insertions(+), 84 deletions(-) diff --git a/datavalidation.go b/datavalidation.go index 37e1f4394a..56740e603d 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -23,15 +23,14 @@ type DataValidationType int // Data validation types. const ( - _DataValidationType = iota - typeNone // inline use + _ DataValidationType = iota + DataValidationTypeNone DataValidationTypeCustom DataValidationTypeDate DataValidationTypeDecimal - typeList // inline use + DataValidationTypeList DataValidationTypeTextLength DataValidationTypeTime - // DataValidationTypeWhole Integer DataValidationTypeWhole ) @@ -58,7 +57,7 @@ type DataValidationOperator int // Data validation operators. const ( - _DataValidationOperator = iota + _ DataValidationOperator = iota DataValidationOperatorBetween DataValidationOperatorEqual DataValidationOperatorGreaterThan @@ -69,13 +68,42 @@ const ( DataValidationOperatorNotEqual ) -// formulaEscaper mimics the Excel escaping rules for data validation, -// which converts `"` to `""` instead of `"`. -var formulaEscaper = strings.NewReplacer( - `&`, `&`, - `<`, `<`, - `>`, `>`, - `"`, `""`, +var ( + // formulaEscaper mimics the Excel escaping rules for data validation, + // which converts `"` to `""` instead of `"`. + formulaEscaper = strings.NewReplacer( + `&`, `&`, + `<`, `<`, + `>`, `>`, + `"`, `""`, + ) + // dataValidationTypeMap defined supported data validation types. + dataValidationTypeMap = map[DataValidationType]string{ + DataValidationTypeNone: "none", + DataValidationTypeCustom: "custom", + DataValidationTypeDate: "date", + DataValidationTypeDecimal: "decimal", + DataValidationTypeList: "list", + DataValidationTypeTextLength: "textLength", + DataValidationTypeTime: "time", + DataValidationTypeWhole: "whole", + } + // dataValidationOperatorMap defined supported data validation operators. + dataValidationOperatorMap = map[DataValidationOperator]string{ + DataValidationOperatorBetween: "between", + DataValidationOperatorEqual: "equal", + DataValidationOperatorGreaterThan: "greaterThan", + DataValidationOperatorGreaterThanOrEqual: "greaterThanOrEqual", + DataValidationOperatorLessThan: "lessThan", + DataValidationOperatorLessThanOrEqual: "lessThanOrEqual", + DataValidationOperatorNotBetween: "notBetween", + DataValidationOperatorNotEqual: "notEqual", + } +) + +const ( + formula1Name = "formula1" + formula2Name = "formula2" ) // NewDataValidation return data validation struct. @@ -123,45 +151,43 @@ func (dv *DataValidation) SetDropList(keys []string) error { if MaxFieldLength < len(utf16.Encode([]rune(formula))) { return ErrDataValidationFormulaLength } - dv.Formula1 = fmt.Sprintf(`"%s"`, formulaEscaper.Replace(formula)) - dv.Type = convDataValidationType(typeList) + dv.Formula1 = fmt.Sprintf(`<%[2]s>"%[1]s"`, formulaEscaper.Replace(formula), formula1Name) + dv.Type = dataValidationTypeMap[DataValidationTypeList] return nil } // SetRange provides function to set data validation range in drop list, only -// accepts int, float64, or string data type formula argument. +// accepts int, float64, string or []string data type formula argument. func (dv *DataValidation) SetRange(f1, f2 interface{}, t DataValidationType, o DataValidationOperator) error { - var formula1, formula2 string - switch v := f1.(type) { - case int: - formula1 = fmt.Sprintf("%d", v) - case float64: - if math.Abs(v) > math.MaxFloat32 { - return ErrDataValidationRange + genFormula := func(name string, val interface{}) (string, error) { + var formula string + switch v := val.(type) { + case int: + formula = fmt.Sprintf("<%s>%d", name, v, name) + case float64: + if math.Abs(v) > math.MaxFloat32 { + return formula, ErrDataValidationRange + } + formula = fmt.Sprintf("<%s>%.17g", name, v, name) + case string: + formula = fmt.Sprintf("<%s>%s", name, v, name) + default: + return formula, ErrParameterInvalid } - formula1 = fmt.Sprintf("%.17g", v) - case string: - formula1 = fmt.Sprintf("%s", v) - default: - return ErrParameterInvalid + return formula, nil } - switch v := f2.(type) { - case int: - formula2 = fmt.Sprintf("%d", v) - case float64: - if math.Abs(v) > math.MaxFloat32 { - return ErrDataValidationRange - } - formula2 = fmt.Sprintf("%.17g", v) - case string: - formula2 = fmt.Sprintf("%s", v) - default: - return ErrParameterInvalid + formula1, err := genFormula(formula1Name, f1) + if err != nil { + return err + } + formula2, err := genFormula(formula2Name, f2) + if err != nil { + return err } dv.Formula1, dv.Formula2 = formula1, formula2 - dv.Type = convDataValidationType(t) - dv.Operator = convDataValidationOperator(o) - return nil + dv.Type = dataValidationTypeMap[t] + dv.Operator = dataValidationOperatorMap[o] + return err } // SetSqrefDropList provides set data validation on a range with source @@ -180,48 +206,16 @@ func (dv *DataValidation) SetRange(f1, f2 interface{}, t DataValidationType, o D // err := f.AddDataValidation("Sheet1", dv) func (dv *DataValidation) SetSqrefDropList(sqref string) { dv.Formula1 = fmt.Sprintf("%s", sqref) - dv.Type = convDataValidationType(typeList) + dv.Type = dataValidationTypeMap[DataValidationTypeList] } // SetSqref provides function to set data validation range in drop list. func (dv *DataValidation) SetSqref(sqref string) { if dv.Sqref == "" { dv.Sqref = sqref - } else { - dv.Sqref = fmt.Sprintf("%s %s", dv.Sqref, sqref) + return } -} - -// convDataValidationType get excel data validation type. -func convDataValidationType(t DataValidationType) string { - typeMap := map[DataValidationType]string{ - typeNone: "none", - DataValidationTypeCustom: "custom", - DataValidationTypeDate: "date", - DataValidationTypeDecimal: "decimal", - typeList: "list", - DataValidationTypeTextLength: "textLength", - DataValidationTypeTime: "time", - DataValidationTypeWhole: "whole", - } - - return typeMap[t] -} - -// convDataValidationOperator get excel data validation operator. -func convDataValidationOperator(o DataValidationOperator) string { - typeMap := map[DataValidationOperator]string{ - DataValidationOperatorBetween: "between", - DataValidationOperatorEqual: "equal", - DataValidationOperatorGreaterThan: "greaterThan", - DataValidationOperatorGreaterThanOrEqual: "greaterThanOrEqual", - DataValidationOperatorLessThan: "lessThan", - DataValidationOperatorLessThanOrEqual: "lessThanOrEqual", - DataValidationOperatorNotBetween: "notBetween", - DataValidationOperatorNotEqual: "notEqual", - } - - return typeMap[o] + dv.Sqref = fmt.Sprintf("%s %s", dv.Sqref, sqref) } // AddDataValidation provides set data validation on a range of the worksheet @@ -337,23 +331,23 @@ func (f *File) squashSqref(cells [][]int) []string { } else if len(cells) == 0 { return []string{} } - var res []string + var refs []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.coordinatesToRangeRef(append(cells[l], cells[r]...)) + ref, _ := f.coordinatesToRangeRef(append(cells[l], cells[r]...)) if l == r { - curr, _ = CoordinatesToCellName(cells[l][0], cells[l][1]) + ref, _ = CoordinatesToCellName(cells[l][0], cells[l][1]) } - res = append(res, curr) + refs = append(refs, ref) l, r = i, i } else { r++ } } - curr, _ := f.coordinatesToRangeRef(append(cells[l], cells[r]...)) + ref, _ := f.coordinatesToRangeRef(append(cells[l], cells[r]...)) if l == r { - curr, _ = CoordinatesToCellName(cells[l][0], cells[l][1]) + ref, _ = CoordinatesToCellName(cells[l][0], cells[l][1]) } - return append(res, curr) + return append(refs, ref) } diff --git a/datavalidation_test.go b/datavalidation_test.go index 2f45fd9963..a2c8d20984 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -67,6 +67,12 @@ func TestDataValidation(t *testing.T) { assert.NoError(t, err) assert.Len(t, dataValidations, 1) + dv = NewDataValidation(true) + dv.Sqref = "A4:A5" + assert.NoError(t, dv.SetRange("Sheet2!$A$2:$A$3", "", DataValidationTypeList, DataValidationOperatorBetween)) + dv.SetError(DataValidationErrorStyleStop, "error title", "error body") + assert.NoError(t, f.AddDataValidation("Sheet2", dv)) + dv = NewDataValidation(true) dv.Sqref = "A5:B6" for _, listValid := range [][]string{ From e014a8bb239a1d3f2e0360635a47591e8cfac225 Mon Sep 17 00:00:00 2001 From: ByteFlyCoding <81181757+ByteFlyCoding@users.noreply.github.com> Date: Fri, 10 Nov 2023 09:25:59 +0800 Subject: [PATCH 822/957] Support update conditional formatting on inserting/deleting columns/rows (#1717) Return error for unsupported conditional formatting rule types --- adjust.go | 44 +++++++++++++++++++++++++++++++++++++----- adjust_test.go | 31 +++++++++++++++++++++++++++++ datavalidation_test.go | 6 ------ excelize_test.go | 2 +- styles.go | 2 ++ styles_test.go | 13 +++++++++++++ 6 files changed, 86 insertions(+), 12 deletions(-) diff --git a/adjust.go b/adjust.go index 450e49c719..11da69271b 100644 --- a/adjust.go +++ b/adjust.go @@ -30,7 +30,10 @@ const ( ) // adjustHelperFunc defines functions to adjust helper. -var adjustHelperFunc = [7]func(*File, *xlsxWorksheet, string, adjustDirection, int, int, int) error{ +var adjustHelperFunc = [8]func(*File, *xlsxWorksheet, string, adjustDirection, int, int, int) error{ + func(f *File, ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error { + return f.adjustConditionalFormats(ws, sheet, dir, num, offset, sheetID) + }, func(f *File, ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error { return f.adjustDefinedNames(ws, sheet, dir, num, offset, sheetID) }, @@ -231,15 +234,19 @@ func (f *File) adjustSingleRowFormulas(sheet, sheetN string, r *xlsxRow, num, of } // adjustCellRef provides a function to adjust cell reference. -func (f *File) adjustCellRef(ref string, dir adjustDirection, num, offset int) (string, error) { +func (f *File) adjustCellRef(ref string, dir adjustDirection, num, offset int) (string, bool, error) { if !strings.Contains(ref, ":") { ref += ":" + ref } + var delete bool coordinates, err := rangeRefToCoordinates(ref) if err != nil { - return ref, err + return ref, delete, err } if dir == columns { + if offset < 0 && coordinates[0] == coordinates[2] { + delete = true + } if coordinates[0] >= num { coordinates[0] += offset } @@ -247,6 +254,9 @@ func (f *File) adjustCellRef(ref string, dir adjustDirection, num, offset int) ( coordinates[2] += offset } } else { + if offset < 0 && coordinates[1] == coordinates[3] { + delete = true + } if coordinates[1] >= num { coordinates[1] += offset } @@ -254,7 +264,8 @@ func (f *File) adjustCellRef(ref string, dir adjustDirection, num, offset int) ( coordinates[3] += offset } } - return f.coordinatesToRangeRef(coordinates) + ref, err = f.coordinatesToRangeRef(coordinates) + return ref, delete, err } // adjustFormula provides a function to adjust formula reference and shared @@ -265,7 +276,7 @@ func (f *File) adjustFormula(sheet, sheetN string, formula *xlsxF, dir adjustDir } var err error if formula.Ref != "" && sheet == sheetN { - if formula.Ref, err = f.adjustCellRef(formula.Ref, dir, num, offset); err != nil { + if formula.Ref, _, err = f.adjustCellRef(formula.Ref, dir, num, offset); err != nil { return err } if si && formula.Si != nil { @@ -770,6 +781,29 @@ func (f *File) adjustVolatileDeps(ws *xlsxWorksheet, sheet string, dir adjustDir return nil } +// adjustConditionalFormats updates the cell reference of the worksheet +// conditional formatting when inserting or deleting rows or columns. +func (f *File) adjustConditionalFormats(ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error { + for i := 0; i < len(ws.ConditionalFormatting); i++ { + cf := ws.ConditionalFormatting[i] + if cf == nil { + continue + } + ref, del, err := f.adjustCellRef(cf.SQRef, dir, num, offset) + if err != nil { + return err + } + if del { + ws.ConditionalFormatting = append(ws.ConditionalFormatting[:i], + ws.ConditionalFormatting[i+1:]...) + i-- + continue + } + ws.ConditionalFormatting[i].SQRef = ref + } + return nil +} + // adjustDrawings updates the starting anchor of the two cell anchor pictures // and charts object when inserting or deleting rows or columns. func (from *xlsxFrom) adjustDrawings(dir adjustDirection, num, offset int, editAs string) (bool, error) { diff --git a/adjust_test.go b/adjust_test.go index c33de8acdc..80c15828be 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -962,6 +962,37 @@ func TestAdjustVolatileDeps(t *testing.T) { f.volatileDepsWriter() } +func TestAdjustConditionalFormats(t *testing.T) { + f := NewFile() + assert.NoError(t, f.SetSheetRow("Sheet1", "B1", &[]interface{}{1, nil, 1, 1})) + formatID, err := f.NewConditionalStyle(&Style{Font: &Font{Color: "09600B"}, Fill: Fill{Type: "pattern", Color: []string{"C7EECF"}, Pattern: 1}}) + assert.NoError(t, err) + format := []ConditionalFormatOptions{ + { + Type: "cell", + Criteria: "greater than", + Format: formatID, + Value: "0", + }, + } + for _, ref := range []string{"B1", "D1:E1"} { + assert.NoError(t, f.SetConditionalFormat("Sheet1", ref, format)) + } + assert.NoError(t, f.RemoveCol("Sheet1", "B")) + opts, err := f.GetConditionalFormats("Sheet1") + assert.NoError(t, err) + assert.Len(t, format, 1) + assert.Equal(t, format, opts["C1:D1"]) + + ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") + assert.True(t, ok) + ws.(*xlsxWorksheet).ConditionalFormatting[0].SQRef = "-" + assert.Equal(t, newCellNameToCoordinatesError("-", newInvalidCellNameError("-")), f.RemoveCol("Sheet1", "B")) + + ws.(*xlsxWorksheet).ConditionalFormatting[0] = nil + assert.NoError(t, f.RemoveCol("Sheet1", "B")) +} + func TestAdjustDrawings(t *testing.T) { f := NewFile() // Test add pictures to sheet with positioning diff --git a/datavalidation_test.go b/datavalidation_test.go index a2c8d20984..2f45fd9963 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -67,12 +67,6 @@ func TestDataValidation(t *testing.T) { assert.NoError(t, err) assert.Len(t, dataValidations, 1) - dv = NewDataValidation(true) - dv.Sqref = "A4:A5" - assert.NoError(t, dv.SetRange("Sheet2!$A$2:$A$3", "", DataValidationTypeList, DataValidationOperatorBetween)) - dv.SetError(DataValidationErrorStyleStop, "error title", "error body") - assert.NoError(t, f.AddDataValidation("Sheet2", dv)) - dv = NewDataValidation(true) dv.Sqref = "A5:B6" for _, listValid := range [][]string{ diff --git a/excelize_test.go b/excelize_test.go index e1c8401cee..117d86a7ef 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -1223,7 +1223,7 @@ func TestConditionalFormat(t *testing.T) { }, )) // Set conditional format with illegal criteria type - assert.NoError(t, f.SetConditionalFormat(sheet1, "K1:K10", + assert.Equal(t, ErrParameterInvalid, f.SetConditionalFormat(sheet1, "K1:K10", []ConditionalFormatOptions{ { Type: "data_bar", diff --git a/styles.go b/styles.go index ed28234cee..311bed87fa 100644 --- a/styles.go +++ b/styles.go @@ -2664,8 +2664,10 @@ func (f *File) SetConditionalFormat(sheet, rangeRef string, opts []ConditionalFo f.addSheetNameSpace(sheet, NameSpaceSpreadSheetX14) } cfRule = append(cfRule, rule) + continue } } + return ErrParameterInvalid } } diff --git a/styles_test.go b/styles_test.go index dd7f7189a5..87efb08fed 100644 --- a/styles_test.go +++ b/styles_test.go @@ -191,6 +191,19 @@ func TestSetConditionalFormat(t *testing.T) { 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: "icon_set", IconStyle: "unknown"}}), ErrParameterInvalid.Error()) + // Test unsupported conditional formatting rule types + for _, val := range []string{ + "date", + "time", + "text", + "time_period", + "blanks", + "no_blanks", + "errors", + "no_errors", + } { + assert.Equal(t, ErrParameterInvalid, f.SetConditionalFormat("Sheet1", "A1", []ConditionalFormatOptions{{Type: val}})) + } } func TestGetConditionalFormats(t *testing.T) { From c7acf4fafef429b67ca79f496d2709271c840dcd Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 11 Nov 2023 00:04:05 +0800 Subject: [PATCH 823/957] Support update data validations on inserting/deleting columns/rows --- adjust.go | 66 +++++++++++++++++++++++++++++-- adjust_test.go | 72 +++++++++++++++++++++++++++++++++- datavalidation.go | 89 ++++++++++++++++++++++++++++++++++-------- datavalidation_test.go | 3 +- picture_test.go | 4 +- pivotTable_test.go | 2 +- shape_test.go | 12 +++--- sheetpr_test.go | 8 ++-- sparkline_test.go | 30 +++++++------- stream_test.go | 28 ++++++------- styles_test.go | 10 ++--- table_test.go | 20 +++++----- vml_test.go | 6 +-- xmlWorksheet.go | 60 ++++++++++++++++++---------- 14 files changed, 308 insertions(+), 102 deletions(-) diff --git a/adjust.go b/adjust.go index 11da69271b..5cdb711c84 100644 --- a/adjust.go +++ b/adjust.go @@ -30,10 +30,13 @@ const ( ) // adjustHelperFunc defines functions to adjust helper. -var adjustHelperFunc = [8]func(*File, *xlsxWorksheet, string, adjustDirection, int, int, int) error{ +var adjustHelperFunc = [9]func(*File, *xlsxWorksheet, string, adjustDirection, int, int, int) error{ func(f *File, ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error { return f.adjustConditionalFormats(ws, sheet, dir, num, offset, sheetID) }, + func(f *File, ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error { + return f.adjustDataValidations(ws, sheet, dir, num, offset, sheetID) + }, func(f *File, ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error { return f.adjustDefinedNames(ws, sheet, dir, num, offset, sheetID) }, @@ -66,7 +69,7 @@ var adjustHelperFunc = [8]func(*File, *xlsxWorksheet, string, adjustDirection, i // row: Index number of the row we're inserting/deleting before // offset: Number of rows/column to insert/delete negative values indicate deletion // -// TODO: adjustComments, adjustDataValidations, adjustPageBreaks, adjustProtectedCells +// TODO: adjustComments, adjustPageBreaks, adjustProtectedCells func (f *File) adjustHelper(sheet string, dir adjustDirection, num, offset int) error { ws, err := f.workSheetReader(sheet) if err != nil { @@ -369,7 +372,10 @@ func (f *File) adjustFormulaOperand(sheet, sheetN string, keepRelative bool, tok sheetName, cell = tokens[0], tokens[1] operand = escapeSheetName(sheetName) + "!" } - if sheet != sheetN && sheet != sheetName { + if sheetName == "" { + sheetName = sheetN + } + if sheet != sheetName { return operand + cell, err } for _, r := range cell { @@ -804,6 +810,60 @@ func (f *File) adjustConditionalFormats(ws *xlsxWorksheet, sheet string, dir adj return nil } +// adjustDataValidations updates the range of data validations for the worksheet +// when inserting or deleting rows or columns. +func (f *File) adjustDataValidations(ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset, sheetID int) error { + for _, sheetN := range f.GetSheetList() { + worksheet, err := f.workSheetReader(sheetN) + if err != nil { + if err.Error() == newNotWorksheetError(sheetN).Error() { + continue + } + return err + } + if worksheet.DataValidations == nil { + return nil + } + for i := 0; i < len(worksheet.DataValidations.DataValidation); i++ { + dv := worksheet.DataValidations.DataValidation[i] + if dv == nil { + continue + } + if sheet == sheetN { + ref, del, err := f.adjustCellRef(dv.Sqref, dir, num, offset) + if err != nil { + return err + } + if del { + worksheet.DataValidations.DataValidation = append(worksheet.DataValidations.DataValidation[:i], + worksheet.DataValidations.DataValidation[i+1:]...) + i-- + continue + } + worksheet.DataValidations.DataValidation[i].Sqref = ref + } + if worksheet.DataValidations.DataValidation[i].Formula1 != nil { + formula := unescapeDataValidationFormula(worksheet.DataValidations.DataValidation[i].Formula1.Content) + if formula, err = f.adjustFormulaRef(sheet, sheetN, formula, false, dir, num, offset); err != nil { + return err + } + worksheet.DataValidations.DataValidation[i].Formula1 = &xlsxInnerXML{Content: formulaEscaper.Replace(formula)} + } + if worksheet.DataValidations.DataValidation[i].Formula2 != nil { + formula := unescapeDataValidationFormula(worksheet.DataValidations.DataValidation[i].Formula2.Content) + if formula, err = f.adjustFormulaRef(sheet, sheetN, formula, false, dir, num, offset); err != nil { + return err + } + worksheet.DataValidations.DataValidation[i].Formula2 = &xlsxInnerXML{Content: formulaEscaper.Replace(formula)} + } + } + if worksheet.DataValidations.Count = len(worksheet.DataValidations.DataValidation); worksheet.DataValidations.Count == 0 { + worksheet.DataValidations = nil + } + } + return nil +} + // adjustDrawings updates the starting anchor of the two cell anchor pictures // and charts object when inserting or deleting rows or columns. func (from *xlsxFrom) adjustDrawings(dir adjustDirection, num, offset int, editAs string) (bool, error) { diff --git a/adjust_test.go b/adjust_test.go index 80c15828be..769affe397 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -743,7 +743,7 @@ func TestAdjustFormula(t *testing.T) { assert.NoError(t, f.InsertRows("Sheet1", 2, 1)) formula, err = f.GetCellFormula("Sheet1", "B1") assert.NoError(t, err) - assert.Equal(t, "SUM('Sheet 1'!A3,A5)", formula) + assert.Equal(t, "SUM('Sheet 1'!A2,A5)", formula) f = NewFile() // Test adjust formula on insert col in the middle of the range @@ -993,6 +993,76 @@ func TestAdjustConditionalFormats(t *testing.T) { assert.NoError(t, f.RemoveCol("Sheet1", "B")) } +func TestAdjustDataValidations(t *testing.T) { + f := NewFile() + dv := NewDataValidation(true) + dv.Sqref = "B1" + assert.NoError(t, dv.SetDropList([]string{"1", "2", "3"})) + assert.NoError(t, f.AddDataValidation("Sheet1", dv)) + assert.NoError(t, f.RemoveCol("Sheet1", "B")) + dvs, err := f.GetDataValidations("Sheet1") + assert.NoError(t, err) + assert.Len(t, dvs, 0) + + assert.NoError(t, f.SetCellValue("Sheet1", "F2", 1)) + assert.NoError(t, f.SetCellValue("Sheet1", "F3", 2)) + dv = NewDataValidation(true) + dv.Sqref = "C2:D3" + dv.SetSqrefDropList("$F$2:$F$3") + assert.NoError(t, f.AddDataValidation("Sheet1", dv)) + + assert.NoError(t, f.AddChartSheet("Chart1", &Chart{Type: Line})) + _, err = f.NewSheet("Sheet2") + assert.NoError(t, err) + assert.NoError(t, f.SetSheetRow("Sheet2", "C1", &[]interface{}{1, 10})) + dv = NewDataValidation(true) + dv.Sqref = "C5:D6" + assert.NoError(t, dv.SetRange("Sheet2!C1", "Sheet2!D1", DataValidationTypeWhole, DataValidationOperatorBetween)) + dv.SetError(DataValidationErrorStyleStop, "error title", "error body") + assert.NoError(t, f.AddDataValidation("Sheet1", dv)) + assert.NoError(t, f.RemoveCol("Sheet1", "B")) + assert.NoError(t, f.RemoveCol("Sheet2", "B")) + dvs, err = f.GetDataValidations("Sheet1") + assert.NoError(t, err) + assert.Equal(t, "B2:C3", dvs[0].Sqref) + assert.Equal(t, "$E$2:$E$3", dvs[0].Formula1) + assert.Equal(t, "B5:C6", dvs[1].Sqref) + assert.Equal(t, "Sheet2!B1", dvs[1].Formula1) + assert.Equal(t, "Sheet2!C1", dvs[1].Formula2) + + dv = NewDataValidation(true) + dv.Sqref = "C8:D10" + assert.NoError(t, dv.SetDropList([]string{`A<`, `B>`, `C"`, "D\t", `E'`, `F`})) + assert.NoError(t, f.AddDataValidation("Sheet1", dv)) + assert.NoError(t, f.RemoveCol("Sheet1", "B")) + dvs, err = f.GetDataValidations("Sheet1") + assert.NoError(t, err) + assert.Equal(t, "\"A<,B>,C\",D\t,E',F\"", dvs[2].Formula1) + + dv = NewDataValidation(true) + dv.Sqref = "C5:D6" + assert.NoError(t, dv.SetRange("Sheet1!A1048576", "Sheet1!XFD1", DataValidationTypeWhole, DataValidationOperatorBetween)) + dv.SetError(DataValidationErrorStyleStop, "error title", "error body") + assert.NoError(t, f.AddDataValidation("Sheet1", dv)) + assert.Equal(t, ErrColumnNumber, f.InsertCols("Sheet1", "A", 1)) + assert.Equal(t, ErrMaxRows, f.InsertRows("Sheet1", 1, 1)) + + ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") + assert.True(t, ok) + ws.(*xlsxWorksheet).DataValidations.DataValidation[0].Sqref = "-" + assert.Equal(t, newCellNameToCoordinatesError("-", newInvalidCellNameError("-")), f.RemoveCol("Sheet1", "B")) + + ws.(*xlsxWorksheet).DataValidations.DataValidation[0] = nil + assert.NoError(t, f.RemoveCol("Sheet1", "B")) + + ws.(*xlsxWorksheet).DataValidations = nil + assert.NoError(t, f.RemoveCol("Sheet1", "B")) + + f.Sheet.Delete("xl/worksheets/sheet1.xml") + f.Pkg.Store("xl/worksheets/sheet1.xml", MacintoshCyrillicCharset) + assert.EqualError(t, f.adjustDataValidations(nil, "Sheet1", columns, 0, 0, 1), "XML syntax error on line 1: invalid UTF-8") +} + func TestAdjustDrawings(t *testing.T) { f := NewFile() // Test add pictures to sheet with positioning diff --git a/datavalidation.go b/datavalidation.go index 56740e603d..4d2f360b28 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -75,7 +75,11 @@ var ( `&`, `&`, `<`, `<`, `>`, `>`, - `"`, `""`, + ) + formulaUnescaper = strings.NewReplacer( + `&`, `&`, + `<`, `<`, + `>`, `>`, ) // dataValidationTypeMap defined supported data validation types. dataValidationTypeMap = map[DataValidationType]string{ @@ -101,11 +105,6 @@ var ( } ) -const ( - formula1Name = "formula1" - formula2Name = "formula2" -) - // NewDataValidation return data validation struct. func NewDataValidation(allowBlank bool) *DataValidation { return &DataValidation{ @@ -151,36 +150,40 @@ func (dv *DataValidation) SetDropList(keys []string) error { if MaxFieldLength < len(utf16.Encode([]rune(formula))) { return ErrDataValidationFormulaLength } - dv.Formula1 = fmt.Sprintf(`<%[2]s>"%[1]s"`, formulaEscaper.Replace(formula), formula1Name) dv.Type = dataValidationTypeMap[DataValidationTypeList] + if strings.HasPrefix(formula, "=") { + dv.Formula1 = formulaEscaper.Replace(formula) + return nil + } + dv.Formula1 = fmt.Sprintf(`"%s"`, strings.NewReplacer(`"`, `""`).Replace(formulaEscaper.Replace(formula))) return nil } // SetRange provides function to set data validation range in drop list, only // accepts int, float64, string or []string data type formula argument. func (dv *DataValidation) SetRange(f1, f2 interface{}, t DataValidationType, o DataValidationOperator) error { - genFormula := func(name string, val interface{}) (string, error) { + genFormula := func(val interface{}) (string, error) { var formula string switch v := val.(type) { case int: - formula = fmt.Sprintf("<%s>%d", name, v, name) + formula = fmt.Sprintf("%d", v) case float64: if math.Abs(v) > math.MaxFloat32 { return formula, ErrDataValidationRange } - formula = fmt.Sprintf("<%s>%.17g", name, v, name) + formula = fmt.Sprintf("%.17g", v) case string: - formula = fmt.Sprintf("<%s>%s", name, v, name) + formula = v default: return formula, ErrParameterInvalid } return formula, nil } - formula1, err := genFormula(formula1Name, f1) + formula1, err := genFormula(f1) if err != nil { return err } - formula2, err := genFormula(formula2Name, f2) + formula2, err := genFormula(f2) if err != nil { return err } @@ -205,7 +208,7 @@ func (dv *DataValidation) SetRange(f1, f2 interface{}, t DataValidationType, o D // dv.SetSqrefDropList("$E$1:$E$3") // err := f.AddDataValidation("Sheet1", dv) func (dv *DataValidation) SetSqrefDropList(sqref string) { - dv.Formula1 = fmt.Sprintf("%s", sqref) + dv.Formula1 = sqref dv.Type = dataValidationTypeMap[DataValidationTypeList] } @@ -256,7 +259,27 @@ func (f *File) AddDataValidation(sheet string, dv *DataValidation) error { if nil == ws.DataValidations { ws.DataValidations = new(xlsxDataValidations) } - ws.DataValidations.DataValidation = append(ws.DataValidations.DataValidation, dv) + dataValidation := &xlsxDataValidation{ + AllowBlank: dv.AllowBlank, + Error: dv.Error, + ErrorStyle: dv.ErrorStyle, + ErrorTitle: dv.ErrorTitle, + Operator: dv.Operator, + Prompt: dv.Prompt, + PromptTitle: dv.PromptTitle, + ShowDropDown: dv.ShowDropDown, + ShowErrorMessage: dv.ShowErrorMessage, + ShowInputMessage: dv.ShowInputMessage, + Sqref: dv.Sqref, + Type: dv.Type, + } + if dv.Formula1 != "" { + dataValidation.Formula1 = &xlsxInnerXML{Content: dv.Formula1} + } + if dv.Formula2 != "" { + dataValidation.Formula2 = &xlsxInnerXML{Content: dv.Formula2} + } + ws.DataValidations.DataValidation = append(ws.DataValidations.DataValidation, dataValidation) ws.DataValidations.Count = len(ws.DataValidations.DataValidation) return err } @@ -270,7 +293,33 @@ func (f *File) GetDataValidations(sheet string) ([]*DataValidation, error) { if ws.DataValidations == nil || len(ws.DataValidations.DataValidation) == 0 { return nil, err } - return ws.DataValidations.DataValidation, err + var dvs []*DataValidation + for _, dv := range ws.DataValidations.DataValidation { + if dv != nil { + dataValidation := &DataValidation{ + AllowBlank: dv.AllowBlank, + Error: dv.Error, + ErrorStyle: dv.ErrorStyle, + ErrorTitle: dv.ErrorTitle, + Operator: dv.Operator, + Prompt: dv.Prompt, + PromptTitle: dv.PromptTitle, + ShowDropDown: dv.ShowDropDown, + ShowErrorMessage: dv.ShowErrorMessage, + ShowInputMessage: dv.ShowInputMessage, + Sqref: dv.Sqref, + Type: dv.Type, + } + if dv.Formula1 != nil { + dataValidation.Formula1 = unescapeDataValidationFormula(dv.Formula1.Content) + } + if dv.Formula2 != nil { + dataValidation.Formula2 = unescapeDataValidationFormula(dv.Formula2.Content) + } + dvs = append(dvs, dataValidation) + } + } + return dvs, err } // DeleteDataValidation delete data validation by given worksheet name and @@ -351,3 +400,11 @@ func (f *File) squashSqref(cells [][]int) []string { } return append(refs, ref) } + +// unescapeDataValidationFormula returns unescaped data validation formula. +func unescapeDataValidationFormula(val string) string { + if strings.HasPrefix(val, "\"") { // Text detection + return strings.NewReplacer(`""`, `"`).Replace(formulaUnescaper.Replace(val)) + } + return formulaUnescaper.Replace(val) +} diff --git a/datavalidation_test.go b/datavalidation_test.go index 2f45fd9963..c331ebe101 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -71,6 +71,7 @@ func TestDataValidation(t *testing.T) { dv.Sqref = "A5:B6" for _, listValid := range [][]string{ {"1", "2", "3"}, + {"=A1"}, {strings.Repeat("&", MaxFieldLength)}, {strings.Repeat("\u4E00", MaxFieldLength)}, {strings.Repeat("\U0001F600", 100), strings.Repeat("\u4E01", 50), "<&>"}, @@ -82,7 +83,7 @@ func TestDataValidation(t *testing.T) { assert.NotEqual(t, "", dv.Formula1, "Formula1 should not be empty for valid input %v", listValid) } - assert.Equal(t, `"A<,B>,C"",D ,E',F"`, dv.Formula1) + assert.Equal(t, `"A<,B>,C"",D ,E',F"`, dv.Formula1) assert.NoError(t, f.AddDataValidation("Sheet1", dv)) dataValidations, err = f.GetDataValidations("Sheet1") diff --git a/picture_test.go b/picture_test.go index 5422046b7f..b98941f4ad 100644 --- a/picture_test.go +++ b/picture_test.go @@ -294,9 +294,9 @@ func TestDeletePicture(t *testing.T) { // Test delete picture on not exists worksheet assert.EqualError(t, f.DeletePicture("SheetN", "A1"), "sheet SheetN does not exist") // Test delete picture with invalid sheet name - assert.EqualError(t, f.DeletePicture("Sheet:1", "A1"), ErrSheetNameInvalid.Error()) + assert.Equal(t, ErrSheetNameInvalid, f.DeletePicture("Sheet:1", "A1")) // Test delete picture with invalid coordinates - assert.EqualError(t, f.DeletePicture("Sheet1", ""), newCellNameToCoordinatesError("", newInvalidCellNameError("")).Error()) + assert.Equal(t, newCellNameToCoordinatesError("", newInvalidCellNameError("")), f.DeletePicture("Sheet1", "")) assert.NoError(t, f.Close()) // Test delete picture on no chart worksheet assert.NoError(t, NewFile().DeletePicture("Sheet1", "A1")) diff --git a/pivotTable_test.go b/pivotTable_test.go index 6cbdf55d77..49bc7d9caa 100644 --- a/pivotTable_test.go +++ b/pivotTable_test.go @@ -174,7 +174,7 @@ func TestPivotTable(t *testing.T) { })) // Test empty pivot table options - assert.EqualError(t, f.AddPivotTable(nil), ErrParameterRequired.Error()) + assert.Equal(t, ErrParameterRequired, f.AddPivotTable(nil)) // Test add pivot table with custom name which exceeds the max characters limit assert.Equal(t, ErrNameLength, f.AddPivotTable(&PivotTableOptions{ DataRange: "dataRange", diff --git a/shape_test.go b/shape_test.go index 2a9fa08cb3..57c7501a3f 100644 --- a/shape_test.go +++ b/shape_test.go @@ -42,16 +42,16 @@ func TestAddShape(t *testing.T) { }, }, ), "sheet Sheet3 does not exist") - assert.EqualError(t, f.AddShape("Sheet3", nil), ErrParameterInvalid.Error()) - assert.EqualError(t, f.AddShape("Sheet1", &Shape{Cell: "A1"}), ErrParameterInvalid.Error()) - assert.EqualError(t, f.AddShape("Sheet1", &Shape{ + assert.Equal(t, ErrParameterInvalid, f.AddShape("Sheet3", nil)) + assert.Equal(t, ErrParameterInvalid, f.AddShape("Sheet1", &Shape{Cell: "A1"})) + assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.AddShape("Sheet1", &Shape{ Cell: "A", Type: "rect", Paragraph: []RichTextRun{ {Text: "Rectangle", Font: &Font{Color: "CD5C5C"}}, {Text: "Shape", Font: &Font{Bold: true, Color: "2980B9"}}, }, - }), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) + })) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddShape1.xlsx"))) // Test add first shape for given sheet @@ -79,14 +79,14 @@ func TestAddShape(t *testing.T) { })) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddShape2.xlsx"))) // Test add shape with invalid sheet name - assert.EqualError(t, f.AddShape("Sheet:1", &Shape{ + assert.Equal(t, ErrSheetNameInvalid, f.AddShape("Sheet:1", &Shape{ Cell: "A30", Type: "rect", Paragraph: []RichTextRun{ {Text: "Rectangle", Font: &Font{Color: "CD5C5C"}}, {Text: "Shape", Font: &Font{Bold: true, Color: "2980B9"}}, }, - }), ErrSheetNameInvalid.Error()) + })) // Test add shape with unsupported charset style sheet f.Styles = nil f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset) diff --git a/sheetpr_test.go b/sheetpr_test.go index 5491e78694..63b732326d 100644 --- a/sheetpr_test.go +++ b/sheetpr_test.go @@ -30,7 +30,7 @@ func TestSetPageMargins(t *testing.T) { // 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()) + assert.Equal(t, ErrSheetNameInvalid, f.SetPageMargins("Sheet:1", nil)) } func TestGetPageMargins(t *testing.T) { @@ -40,7 +40,7 @@ func TestGetPageMargins(t *testing.T) { 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()) + assert.Equal(t, ErrSheetNameInvalid, err) } func TestSetSheetProps(t *testing.T) { @@ -88,7 +88,7 @@ func TestSetSheetProps(t *testing.T) { // 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()) + assert.Equal(t, ErrSheetNameInvalid, f.SetSheetProps("Sheet:1", nil)) } func TestGetSheetProps(t *testing.T) { @@ -98,5 +98,5 @@ func TestGetSheetProps(t *testing.T) { 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()) + assert.Equal(t, ErrSheetNameInvalid, err) } diff --git a/sparkline_test.go b/sparkline_test.go index 048ed2b865..27da1e694e 100644 --- a/sparkline_test.go +++ b/sparkline_test.go @@ -224,46 +224,46 @@ func TestAddSparkline(t *testing.T) { Range: []string{"Sheet2!A3:E3"}, }), "sheet SheetN does not exist") - assert.EqualError(t, f.AddSparkline("Sheet1", nil), ErrParameterRequired.Error()) + assert.Equal(t, ErrParameterRequired, f.AddSparkline("Sheet1", nil)) // Test add sparkline with invalid sheet name - assert.EqualError(t, f.AddSparkline("Sheet:1", &SparklineOptions{ + assert.Equal(t, ErrSheetNameInvalid, 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{ + assert.Equal(t, ErrSparklineLocation, f.AddSparkline("Sheet1", &SparklineOptions{ Range: []string{"Sheet2!A3:E3"}, - }), ErrSparklineLocation.Error()) + })) - assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOptions{ + assert.Equal(t, ErrSparklineRange, f.AddSparkline("Sheet1", &SparklineOptions{ Location: []string{"F3"}, - }), ErrSparklineRange.Error()) + })) - assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOptions{ + assert.Equal(t, ErrSparkline, f.AddSparkline("Sheet1", &SparklineOptions{ Location: []string{"F2", "F3"}, Range: []string{"Sheet2!A3:E3"}, - }), ErrSparkline.Error()) + })) - assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOptions{ + assert.Equal(t, ErrSparklineType, f.AddSparkline("Sheet1", &SparklineOptions{ Location: []string{"F3"}, Range: []string{"Sheet2!A3:E3"}, Type: "unknown_type", - }), ErrSparklineType.Error()) + })) - assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOptions{ + assert.Equal(t, ErrSparklineStyle, f.AddSparkline("Sheet1", &SparklineOptions{ Location: []string{"F3"}, Range: []string{"Sheet2!A3:E3"}, Style: -1, - }), ErrSparklineStyle.Error()) + })) - assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOptions{ + assert.Equal(t, ErrSparklineStyle, f.AddSparkline("Sheet1", &SparklineOptions{ Location: []string{"F3"}, 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) diff --git a/stream_test.go b/stream_test.go index 2f68d62ed7..da3fd14125 100644 --- a/stream_test.go +++ b/stream_test.go @@ -73,7 +73,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("A8", nil, RowOpts{Height: MaxRowHeight + 1}), ErrMaxRowHeight.Error()) + assert.Equal(t, ErrMaxRowHeight, streamWriter.SetRow("A8", nil, RowOpts{Height: MaxRowHeight + 1})) for rowID := 10; rowID <= 51200; rowID++ { row := make([]interface{}, 50) @@ -158,11 +158,11 @@ func TestStreamSetColWidth(t *testing.T) { streamWriter, err := file.NewStreamWriter("Sheet1") assert.NoError(t, err) assert.NoError(t, streamWriter.SetColWidth(3, 2, 20)) - 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.Equal(t, ErrColumnNumber, streamWriter.SetColWidth(0, 3, 20)) + assert.Equal(t, ErrColumnNumber, streamWriter.SetColWidth(MaxColumns+1, 3, 20)) + assert.Equal(t, ErrColumnWidth, streamWriter.SetColWidth(1, 3, MaxColumnWidth+1)) assert.NoError(t, streamWriter.SetRow("A1", []interface{}{"A", "B", "C"})) - assert.ErrorIs(t, streamWriter.SetColWidth(2, 3, 20), ErrStreamSetColWidth) + assert.Equal(t, ErrStreamSetColWidth, streamWriter.SetColWidth(2, 3, 20)) } func TestStreamSetPanes(t *testing.T) { @@ -183,9 +183,9 @@ func TestStreamSetPanes(t *testing.T) { streamWriter, err := file.NewStreamWriter("Sheet1") assert.NoError(t, err) assert.NoError(t, streamWriter.SetPanes(paneOpts)) - assert.EqualError(t, streamWriter.SetPanes(nil), ErrParameterInvalid.Error()) + assert.Equal(t, ErrParameterInvalid, streamWriter.SetPanes(nil)) assert.NoError(t, streamWriter.SetRow("A1", []interface{}{"A", "B", "C"})) - assert.ErrorIs(t, streamWriter.SetPanes(paneOpts), ErrStreamSetPanes) + assert.Equal(t, ErrStreamSetPanes, streamWriter.SetPanes(paneOpts)) } func TestStreamTable(t *testing.T) { @@ -220,10 +220,10 @@ func TestStreamTable(t *testing.T) { assert.NoError(t, streamWriter.AddTable(&Table{Range: "A1:C1"})) // Test add table with illegal cell reference - 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()) + assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), streamWriter.AddTable(&Table{Range: "A:B1"})) + assert.Equal(t, newCellNameToCoordinatesError("B", newInvalidCellNameError("B")), streamWriter.AddTable(&Table{Range: "A1:B"})) // Test add table with invalid table name - assert.EqualError(t, streamWriter.AddTable(&Table{Range: "A:B1", Name: "1Table"}), newInvalidNameError("1Table").Error()) + assert.Equal(t, newInvalidNameError("1Table"), streamWriter.AddTable(&Table{Range: "A:B1", Name: "1Table"})) // Test add table with unsupported charset content types file.ContentTypes = nil file.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) @@ -239,7 +239,7 @@ func TestStreamMergeCells(t *testing.T) { assert.NoError(t, err) assert.NoError(t, streamWriter.MergeCell("A1", "D1")) // Test merge cells with illegal cell reference - assert.EqualError(t, streamWriter.MergeCell("A", "D1"), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) + assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), streamWriter.MergeCell("A", "D1")) assert.NoError(t, streamWriter.Flush()) // Save spreadsheet by the given path assert.NoError(t, file.SaveAs(filepath.Join("test", "TestStreamMergeCells.xlsx"))) @@ -270,7 +270,7 @@ func TestNewStreamWriter(t *testing.T) { 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()) + assert.Equal(t, ErrSheetNameInvalid, err) } func TestStreamMarshalAttrs(t *testing.T) { @@ -288,10 +288,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()) + assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), streamWriter.SetRow("A", []interface{}{})) // 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()) + assert.Equal(t, newStreamSetRowError(1), streamWriter.SetRow("A1", []interface{}{})) // Test set row with unsupported charset workbook file.WorkBook = nil file.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset) diff --git a/styles_test.go b/styles_test.go index 87efb08fed..c3ba1e9539 100644 --- a/styles_test.go +++ b/styles_test.go @@ -190,7 +190,7 @@ 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: "icon_set", IconStyle: "unknown"}}), ErrParameterInvalid.Error()) + assert.Equal(t, ErrParameterInvalid, f.SetConditionalFormat("Sheet1", "A1:A2", []ConditionalFormatOptions{{Type: "icon_set", IconStyle: "unknown"}})) // Test unsupported conditional formatting rule types for _, val := range []string{ "date", @@ -235,7 +235,7 @@ func TestGetConditionalFormats(t *testing.T) { 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()) + assert.Equal(t, ErrSheetNameInvalid, err) } func TestUnsetConditionalFormat(t *testing.T) { @@ -249,7 +249,7 @@ func TestUnsetConditionalFormat(t *testing.T) { // Test unset conditional format on not exists worksheet assert.EqualError(t, f.UnsetConditionalFormat("SheetN", "A1:A10"), "sheet SheetN does not exist") // Test unset conditional format with invalid sheet name - assert.EqualError(t, f.UnsetConditionalFormat("Sheet:1", "A1:A10"), ErrSheetNameInvalid.Error()) + assert.Equal(t, ErrSheetNameInvalid, f.UnsetConditionalFormat("Sheet:1", "A1:A10")) // Save spreadsheet by the given path assert.NoError(t, f.SaveAs(filepath.Join("test", "TestUnsetConditionalFormat.xlsx"))) } @@ -469,9 +469,9 @@ func TestSetCellStyle(t *testing.T) { // 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()) + assert.Equal(t, newInvalidStyleID(-1), f.SetCellStyle("Sheet1", "A1", "A2", -1)) // Test set cell style with not exists style ID - assert.EqualError(t, f.SetCellStyle("Sheet1", "A1", "A2", 10), newInvalidStyleID(10).Error()) + assert.Equal(t, newInvalidStyleID(10), f.SetCellStyle("Sheet1", "A1", "A2", 10)) // Test set cell style with unsupported charset style sheet f.Styles = nil f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset) diff --git a/table_test.go b/table_test.go index 69e3ad09ee..cda9cb07de 100644 --- a/table_test.go +++ b/table_test.go @@ -45,7 +45,7 @@ func TestAddTable(t *testing.T) { assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddTable.xlsx"))) // Test add table with invalid sheet name - assert.EqualError(t, f.AddTable("Sheet:1", &Table{Range: "B26:A21"}), ErrSheetNameInvalid.Error()) + assert.Equal(t, ErrSheetNameInvalid, f.AddTable("Sheet:1", &Table{Range: "B26:A21"})) // Test addTable with illegal cell reference f = NewFile() assert.Equal(t, newCoordinatesToCellNameError(0, 0), f.addTable("sheet1", "", 0, 0, 0, 0, 0, nil)) @@ -64,13 +64,13 @@ func TestAddTable(t *testing.T) { {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{ + assert.Equal(t, cases.err, f.AddTable("Sheet1", &Table{ Range: "A1:B2", Name: cases.name, - }), cases.err.Error()) - assert.EqualError(t, f.SetDefinedName(&DefinedName{ + })) + assert.Equal(t, cases.err, 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() @@ -115,9 +115,9 @@ func TestDeleteTable(t *testing.T) { assert.NoError(t, f.DeleteTable("Table2")) assert.NoError(t, f.DeleteTable("Table1")) // Test delete table with invalid table name - assert.EqualError(t, f.DeleteTable("Table 1"), newInvalidNameError("Table 1").Error()) + assert.Equal(t, newInvalidNameError("Table 1"), f.DeleteTable("Table 1")) // Test delete table with no exist table name - assert.EqualError(t, f.DeleteTable("Table"), newNoExistTableError("Table").Error()) + assert.Equal(t, newNoExistTableError("Table"), f.DeleteTable("Table")) // Test delete table with unsupported charset f.Sheet.Delete("xl/worksheets/sheet1.xml") f.Pkg.Store("xl/worksheets/sheet1.xml", MacintoshCyrillicCharset) @@ -164,10 +164,10 @@ func TestAutoFilter(t *testing.T) { } // Test add auto filter with invalid sheet name - assert.EqualError(t, f.AutoFilter("Sheet:1", "A1:B1", nil), ErrSheetNameInvalid.Error()) + assert.Equal(t, ErrSheetNameInvalid, f.AutoFilter("Sheet:1", "A1:B1", nil)) // Test add auto filter with illegal cell reference - 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()) + assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.AutoFilter("Sheet1", "A:B1", nil)) + assert.Equal(t, newCellNameToCoordinatesError("B", newInvalidCellNameError("B")), f.AutoFilter("Sheet1", "A1:B", nil)) // Test add auto filter with unsupported charset workbook f.WorkBook = nil f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset) diff --git a/vml_test.go b/vml_test.go index 8e38fbeb8f..50e9a04fae 100644 --- a/vml_test.go +++ b/vml_test.go @@ -35,7 +35,7 @@ func TestAddComment(t *testing.T) { // Test add comment on not exists worksheet assert.EqualError(t, f.AddComment("SheetN", Comment{Cell: "B7", Author: "Excelize", Paragraph: []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", Paragraph: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) + assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.AddComment("Sheet1", Comment{Cell: "A", Author: "Excelize", Paragraph: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}})) comments, err := f.GetComments("Sheet1") assert.NoError(t, err) assert.Len(t, comments, 2) @@ -57,7 +57,7 @@ func TestAddComment(t *testing.T) { 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()) + assert.Equal(t, ErrSheetNameInvalid, f.AddComment("Sheet:1", Comment{Cell: "A1", Author: "Excelize", Text: "This is a comment."})) // Test add comments with unsupported charset f.Comments["xl/comments2.xml"] = nil @@ -105,7 +105,7 @@ func TestDeleteComment(t *testing.T) { assert.Len(t, comments, 0) // Test delete comment with invalid sheet name - assert.EqualError(t, f.DeleteComment("Sheet:1", "A1"), ErrSheetNameInvalid.Error()) + assert.Equal(t, ErrSheetNameInvalid, f.DeleteComment("Sheet:1", "A1")) // Test delete all comments in a worksheet assert.NoError(t, f.DeleteComment("Sheet2", "A41")) assert.NoError(t, f.DeleteComment("Sheet2", "C41")) diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 07085bbffb..6eb860f330 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -419,31 +419,31 @@ type xlsxMergeCells struct { // xlsxDataValidations expresses all data validation information for cells in a // sheet which have data validation features applied. type xlsxDataValidations struct { - XMLName xml.Name `xml:"dataValidations"` - Count int `xml:"count,attr,omitempty"` - DisablePrompts bool `xml:"disablePrompts,attr,omitempty"` - XWindow int `xml:"xWindow,attr,omitempty"` - YWindow int `xml:"yWindow,attr,omitempty"` - DataValidation []*DataValidation `xml:"dataValidation"` + XMLName xml.Name `xml:"dataValidations"` + Count int `xml:"count,attr,omitempty"` + DisablePrompts bool `xml:"disablePrompts,attr,omitempty"` + XWindow int `xml:"xWindow,attr,omitempty"` + YWindow int `xml:"yWindow,attr,omitempty"` + DataValidation []*xlsxDataValidation `xml:"dataValidation"` } // DataValidation directly maps the single item of data validation defined // on a range of the worksheet. -type DataValidation struct { - AllowBlank bool `xml:"allowBlank,attr"` - Error *string `xml:"error,attr"` - ErrorStyle *string `xml:"errorStyle,attr"` - ErrorTitle *string `xml:"errorTitle,attr"` - Operator string `xml:"operator,attr,omitempty"` - Prompt *string `xml:"prompt,attr"` - PromptTitle *string `xml:"promptTitle,attr"` - ShowDropDown bool `xml:"showDropDown,attr,omitempty"` - ShowErrorMessage bool `xml:"showErrorMessage,attr,omitempty"` - ShowInputMessage bool `xml:"showInputMessage,attr,omitempty"` - Sqref string `xml:"sqref,attr"` - Type string `xml:"type,attr,omitempty"` - Formula1 string `xml:",innerxml"` - Formula2 string `xml:",innerxml"` +type xlsxDataValidation struct { + AllowBlank bool `xml:"allowBlank,attr"` + Error *string `xml:"error,attr"` + ErrorStyle *string `xml:"errorStyle,attr"` + ErrorTitle *string `xml:"errorTitle,attr"` + Operator string `xml:"operator,attr,omitempty"` + Prompt *string `xml:"prompt,attr"` + PromptTitle *string `xml:"promptTitle,attr"` + ShowDropDown bool `xml:"showDropDown,attr,omitempty"` + ShowErrorMessage bool `xml:"showErrorMessage,attr,omitempty"` + ShowInputMessage bool `xml:"showInputMessage,attr,omitempty"` + Sqref string `xml:"sqref,attr"` + Type string `xml:"type,attr,omitempty"` + Formula1 *xlsxInnerXML `xml:"formula1"` + Formula2 *xlsxInnerXML `xml:"formula2"` } // xlsxC collection represents a cell in the worksheet. Information about the @@ -835,6 +835,24 @@ type xlsxX14Sparkline struct { Sqref string `xml:"xm:sqref"` } +// DataValidation directly maps the settings of the data validation rule. +type DataValidation struct { + AllowBlank bool + Error *string + ErrorStyle *string + ErrorTitle *string + Operator string + Prompt *string + PromptTitle *string + ShowDropDown bool + ShowErrorMessage bool + ShowInputMessage bool + Sqref string + Type string + Formula1 string + Formula2 string +} + // SparklineOptions directly maps the settings of the sparkline. type SparklineOptions struct { Location []string From 2499bf6b5b4d91bece9e5cae9c07197f2908abdd Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 13 Nov 2023 00:16:29 +0800 Subject: [PATCH 824/957] Support 5 new kinds of conditional formatting types - New conditional formatting types: text, blanks, no blanks, errors, and no errors - Support calculate formula with multiple dash arithmetic symbol - Fix empty calculate result with numeric arguments in LEN, LOWER, PROPER, REPT, UPPER, and IF formula functions - Uniform double quote in calculation unit tests - Update unit tests --- calc.go | 45 ++++-- calc_test.go | 364 ++++++++++++++++++++++++----------------------- excelize_test.go | 6 +- styles.go | 234 +++++++++++++++++++++++++----- styles_test.go | 15 +- 5 files changed, 426 insertions(+), 238 deletions(-) diff --git a/calc.go b/calc.go index 1c1d8e9597..a5dbd72b38 100644 --- a/calc.go +++ b/calc.go @@ -844,7 +844,7 @@ func (f *File) calcCellValue(ctx *calcContext, sheet, cell string) (result formu ps := efp.ExcelParser() tokens := ps.Parse(formula) if tokens == nil { - return + return f.cellResolver(ctx, sheet, cell) } result, err = f.evalInfixExp(ctx, sheet, cell, tokens) return @@ -1225,6 +1225,12 @@ func calcAdd(rOpd, lOpd formulaArg, opdStack *Stack) error { // calcSubtract evaluate subtraction arithmetic operations. func calcSubtract(rOpd, lOpd formulaArg, opdStack *Stack) error { + if rOpd.Value() == "" { + rOpd = newNumberFormulaArg(0) + } + if lOpd.Value() == "" { + lOpd = newNumberFormulaArg(0) + } lOpdVal := lOpd.ToNumber() if lOpdVal.Type != ArgNumber { return errors.New(lOpdVal.Value()) @@ -1300,22 +1306,27 @@ func calculate(opdStack *Stack, opt efp.Token) error { ">=": calcGe, "&": calcSplice, } - fn, ok := tokenCalcFunc[opt.TValue] - if ok { + if fn, ok := tokenCalcFunc[opt.TValue]; ok { if opdStack.Len() < 2 { return ErrInvalidFormula } rOpd := opdStack.Pop().(formulaArg) lOpd := opdStack.Pop().(formulaArg) + if opt.TValue != "&" { + if rOpd.Value() == "" { + rOpd = newNumberFormulaArg(0) + } + if lOpd.Value() == "" { + lOpd = newNumberFormulaArg(0) + } + } if rOpd.Type == ArgError { return errors.New(rOpd.Value()) } if lOpd.Type == ArgError { return errors.New(lOpd.Value()) } - if err := fn(rOpd, lOpd, opdStack); err != nil { - return err - } + return fn(rOpd, lOpd, opdStack) } return nil } @@ -1329,6 +1340,10 @@ func (f *File) parseOperatorPrefixToken(optStack, opdStack *Stack, token efp.Tok tokenPriority := getPriority(token) topOpt := optStack.Peek().(efp.Token) topOptPriority := getPriority(topOpt) + if topOpt.TValue == "-" && topOpt.TType == efp.TokenTypeOperatorPrefix && token.TValue == "-" && token.TType == efp.TokenTypeOperatorPrefix { + optStack.Pop() + return + } if tokenPriority > topOptPriority { optStack.Push(token) return @@ -13757,7 +13772,7 @@ func (fn *formulaFuncs) LEN(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "LEN requires 1 string argument") } - return newNumberFormulaArg(float64(utf8.RuneCountInString(argsList.Front().Value.(formulaArg).String))) + return newNumberFormulaArg(float64(utf8.RuneCountInString(argsList.Front().Value.(formulaArg).Value()))) } // LENB returns the number of bytes used to represent the characters in a text @@ -13790,7 +13805,7 @@ func (fn *formulaFuncs) LOWER(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "LOWER requires 1 argument") } - return newStringFormulaArg(strings.ToLower(argsList.Front().Value.(formulaArg).String)) + return newStringFormulaArg(strings.ToLower(argsList.Front().Value.(formulaArg).Value())) } // MID function returns a specified number of characters from the middle of a @@ -13881,7 +13896,7 @@ func (fn *formulaFuncs) PROPER(argsList *list.List) formulaArg { } buf := bytes.Buffer{} isLetter := false - for _, char := range argsList.Front().Value.(formulaArg).String { + for _, char := range argsList.Front().Value.(formulaArg).Value() { if !isLetter && unicode.IsLetter(char) { buf.WriteRune(unicode.ToUpper(char)) } else { @@ -13962,7 +13977,7 @@ func (fn *formulaFuncs) REPT(argsList *list.List) formulaArg { } buf := bytes.Buffer{} for i := 0; i < int(times.Number); i++ { - buf.WriteString(text.String) + buf.WriteString(text.Value()) } return newStringFormulaArg(buf.String()) } @@ -14327,7 +14342,7 @@ func (fn *formulaFuncs) UPPER(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "UPPER requires 1 argument") } - return newStringFormulaArg(strings.ToUpper(argsList.Front().Value.(formulaArg).String)) + return newStringFormulaArg(strings.ToUpper(argsList.Front().Value.(formulaArg).Value())) } // VALUE function converts a text string into a numeric value. The syntax of @@ -14405,7 +14420,7 @@ func (fn *formulaFuncs) IF(argsList *list.List) formulaArg { ) switch token.Type { case ArgString: - if cond, err = strconv.ParseBool(token.String); err != nil { + if cond, err = strconv.ParseBool(token.Value()); err != nil { return newErrorFormulaArg(formulaErrorVALUE, err.Error()) } case ArgNumber: @@ -14421,7 +14436,7 @@ func (fn *formulaFuncs) IF(argsList *list.List) formulaArg { case ArgNumber: result = value.ToNumber() default: - result = newStringFormulaArg(value.String) + result = newStringFormulaArg(value.Value()) } return result } @@ -14431,7 +14446,7 @@ func (fn *formulaFuncs) IF(argsList *list.List) formulaArg { case ArgNumber: result = value.ToNumber() default: - result = newStringFormulaArg(value.String) + result = newStringFormulaArg(value.Value()) } } return result @@ -14582,7 +14597,7 @@ func compareFormulaArg(lhs, rhs, matchMode formulaArg, caseSensitive bool) byte } return criteriaG case ArgString: - ls, rs := lhs.String, rhs.String + ls, rs := lhs.Value(), rhs.Value() if !caseSensitive { ls, rs = strings.ToLower(ls), strings.ToLower(rs) } diff --git a/calc_test.go b/calc_test.go index 336d085b52..71f3396406 100644 --- a/calc_test.go +++ b/calc_test.go @@ -58,12 +58,22 @@ func TestCalcCellValue(t *testing.T) { "=1>=\"-1\"": "FALSE", "=\"-1\">=-1": "TRUE", "=\"-1\">=\"-2\"": "FALSE", + "=-----1+1": "0", + "=------1+1": "2", + "=---1---1": "-2", + "=---1----1": "0", "=1&2": "12", "=15%": "0.15", "=1+20%": "1.2", "={1}+2": "3", "=1+{2}": "3", "={1}+{2}": "3", + "=A1+(B1-C1)": "5", + "=A1+(C1-B1)": "-3", + "=A1&B1&C1": "14", + "=B1+C1": "4", + "=C1+B1": "4", + "=C1+C1": "0", "=\"A\"=\"A\"": "TRUE", "=\"A\"<>\"A\"": "FALSE", "=TRUE()&FALSE()": "TRUEFALSE", @@ -1760,10 +1770,12 @@ func TestCalcCellValue(t *testing.T) { "=LEFTB(\"Original Text\",13)": "Original Text", "=LEFTB(\"Original Text\",20)": "Original Text", // LEN - "=LEN(\"\")": "0", - "=LEN(D1)": "5", - "=LEN(\"テキスト\")": "4", - "=LEN(\"オリジナルテキスト\")": "9", + "=LEN(\"\")": "0", + "=LEN(D1)": "5", + "=LEN(\"テキスト\")": "4", + "=LEN(\"オリジナルテキスト\")": "9", + "=LEN(7+LEN(A1&B1&C1))": "1", + "=LEN(8+LEN(A1+(C1-B1)))": "2", // LENB "=LENB(\"\")": "0", "=LENB(D1)": "5", @@ -2546,146 +2558,146 @@ func TestCalcCellValue(t *testing.T) { "=_xlfn.ARABIC()": {"#VALUE!", "ARABIC requires 1 numeric argument"}, "=_xlfn.ARABIC(\"" + strings.Repeat("I", 256) + "\")": {"#VALUE!", "#VALUE!"}, // ASIN - "=ASIN()": {"#VALUE!", "ASIN requires 1 numeric argument"}, - `=ASIN("X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + "=ASIN()": {"#VALUE!", "ASIN requires 1 numeric argument"}, + "=ASIN(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // ASINH - "=ASINH()": {"#VALUE!", "ASINH requires 1 numeric argument"}, - `=ASINH("X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + "=ASINH()": {"#VALUE!", "ASINH requires 1 numeric argument"}, + "=ASINH(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // ATAN - "=ATAN()": {"#VALUE!", "ATAN requires 1 numeric argument"}, - `=ATAN("X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + "=ATAN()": {"#VALUE!", "ATAN requires 1 numeric argument"}, + "=ATAN(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // ATANH - "=ATANH()": {"#VALUE!", "ATANH requires 1 numeric argument"}, - `=ATANH("X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + "=ATANH()": {"#VALUE!", "ATANH requires 1 numeric argument"}, + "=ATANH(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // ATAN2 - "=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"}, + "=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()": {"#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"}, + "=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()": {"#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"}, + "=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()": {"#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.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()": {"#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"}, + "=_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()": {"#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"}, + "=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()": {"#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"}, + "=_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()": {"#VALUE!", "COS requires 1 numeric argument"}, - `=COS("X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + "=COS()": {"#VALUE!", "COS requires 1 numeric argument"}, + "=COS(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // COSH - "=COSH()": {"#VALUE!", "COSH requires 1 numeric argument"}, - `=COSH("X")`: {"#VALUE!", "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()": {"#VALUE!", "COT requires 1 numeric argument"}, - `=COT("X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, - "=COT(0)": {"#DIV/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()": {"#VALUE!", "COTH requires 1 numeric argument"}, - `=COTH("X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, - "=COTH(0)": {"#DIV/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()": {"#VALUE!", "CSC requires 1 numeric argument"}, - `=_xlfn.CSC("X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, - "=_xlfn.CSC(0)": {"#DIV/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()": {"#VALUE!", "CSCH requires 1 numeric argument"}, - `=_xlfn.CSCH("X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, - "=_xlfn.CSCH(0)": {"#DIV/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()": {"#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"}, + "=_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()": {"#VALUE!", "DEGREES requires 1 numeric argument"}, - `=DEGREES("X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, - "=DEGREES(0)": {"#DIV/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()": {"#VALUE!", "EVEN requires 1 numeric argument"}, - `=EVEN("X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + "=EVEN()": {"#VALUE!", "EVEN requires 1 numeric argument"}, + "=EVEN(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // EXP - "=EXP()": {"#VALUE!", "EXP requires 1 numeric argument"}, - `=EXP("X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + "=EXP()": {"#VALUE!", "EXP requires 1 numeric argument"}, + "=EXP(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // FACT - "=FACT()": {"#VALUE!", "FACT requires 1 numeric argument"}, - `=FACT("X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, - "=FACT(-1)": {"#NUM!", "#NUM!"}, + "=FACT()": {"#VALUE!", "FACT requires 1 numeric argument"}, + "=FACT(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + "=FACT(-1)": {"#NUM!", "#NUM!"}, // FACTDOUBLE - "=FACTDOUBLE()": {"#VALUE!", "FACTDOUBLE requires 1 numeric argument"}, - `=FACTDOUBLE("X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, - "=FACTDOUBLE(-1)": {"#NUM!", "#NUM!"}, + "=FACTDOUBLE()": {"#VALUE!", "FACTDOUBLE requires 1 numeric argument"}, + "=FACTDOUBLE(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + "=FACTDOUBLE(-1)": {"#NUM!", "#NUM!"}, // 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"}, + "=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()": {"#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.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()": {"#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"}, + "=_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()": {"#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"}, + "=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()": {"#VALUE!", "INT requires 1 numeric argument"}, - `=INT("X")`: {"#VALUE!", "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()": {"#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"}, + "=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()": {"#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"}, + "=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()": {"#VALUE!", "LN requires 1 numeric argument"}, "=LN(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // LOG - "=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!"}, + "=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()": {"#VALUE!", "LOG10 requires 1 numeric argument"}, "=LOG10(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, @@ -2702,51 +2714,51 @@ func TestCalcCellValue(t *testing.T) { "=MMULT(B3:C4,A1:B2)": {"#VALUE!", "#VALUE!"}, "=MMULT(A1:A2,B1:B2)": {"#VALUE!", "#VALUE!"}, // MOD - "=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"}, + "=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()": {"#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"}, + "=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")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + "=MULTINOMIAL(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // _xlfn.MUNIT - "=_xlfn.MUNIT()": {"#VALUE!", "MUNIT requires 1 numeric argument"}, - `=_xlfn.MUNIT("X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, - "=_xlfn.MUNIT(-1)": {"#VALUE!", ""}, + "=_xlfn.MUNIT()": {"#VALUE!", "MUNIT requires 1 numeric argument"}, + "=_xlfn.MUNIT(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + "=_xlfn.MUNIT(-1)": {"#VALUE!", ""}, // ODD - "=ODD()": {"#VALUE!", "ODD requires 1 numeric argument"}, - `=ODD("X")`: {"#VALUE!", "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)": {"#VALUE!", "PI accepts no arguments"}, // POWER - `=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"}, + "=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\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, "=PRODUCT(\"\",3,6)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // QUOTIENT - `=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"}, + "=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")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, - "=RADIANS()": {"#VALUE!", "RADIANS requires 1 numeric argument"}, + "=RADIANS(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + "=RADIANS()": {"#VALUE!", "RADIANS requires 1 numeric argument"}, // RAND "=RAND(1)": {"#VALUE!", "RAND accepts no arguments"}, // RANDBETWEEN - `=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!"}, + "=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()": {"#VALUE!", "ROMAN requires at least 1 argument"}, "=ROMAN(1,2,3)": {"#VALUE!", "ROMAN allows at most 2 arguments"}, @@ -2754,17 +2766,17 @@ func TestCalcCellValue(t *testing.T) { "=ROMAN(\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, "=ROMAN(\"\",1)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // ROUND - "=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"}, + "=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()": {"#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"}, + "=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()": {"#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"}, + "=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"}, // SEARCH "=SEARCH()": {"#VALUE!", "SEARCH requires at least 2 arguments"}, "=SEARCH(1,A1,1,1)": {"#VALUE!", "SEARCH allows at most 3 arguments"}, @@ -2777,11 +2789,11 @@ func TestCalcCellValue(t *testing.T) { "=SEARCHB(\"?w\",\"你好world\")": {"#VALUE!", "#VALUE!"}, "=SEARCHB(1,A1,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, // SEC - "=_xlfn.SEC()": {"#VALUE!", "SEC requires 1 numeric argument"}, - `=_xlfn.SEC("X")`: {"#VALUE!", "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()": {"#VALUE!", "SECH requires 1 numeric argument"}, - `=_xlfn.SECH("X")`: {"#VALUE!", "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()": {"#VALUE!", "SERIESSUM requires 4 arguments"}, "=SERIESSUM(\"\",2,3,A1:A4)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, @@ -2789,22 +2801,22 @@ func TestCalcCellValue(t *testing.T) { "=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()": {"#VALUE!", "SIGN requires 1 numeric argument"}, - `=SIGN("X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + "=SIGN()": {"#VALUE!", "SIGN requires 1 numeric argument"}, + "=SIGN(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // SIN - "=SIN()": {"#VALUE!", "SIN requires 1 numeric argument"}, - `=SIN("X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + "=SIN()": {"#VALUE!", "SIN requires 1 numeric argument"}, + "=SIN(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // SINH - "=SINH()": {"#VALUE!", "SINH requires 1 numeric argument"}, - `=SINH("X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + "=SINH()": {"#VALUE!", "SINH requires 1 numeric argument"}, + "=SINH(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // SQRT - "=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!"}, + "=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()": {"#VALUE!", "SQRTPI requires 1 numeric argument"}, - `=SQRTPI("X")`: {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, + "=SQRTPI()": {"#VALUE!", "SQRTPI requires 1 numeric argument"}, + "=SQRTPI(\"X\")": {"#VALUE!", "strconv.ParseFloat: parsing \"X\": invalid syntax"}, // STDEV "=STDEV()": {"#VALUE!", "STDEV requires at least 1 argument"}, "=STDEV(E2:E9)": {"#DIV/0!", "#DIV/0!"}, @@ -4666,7 +4678,7 @@ func TestCalcCellValue(t *testing.T) { f := prepareCalcData(cellData) result, err := f.CalcCellValue("Sheet1", "A1") assert.NoError(t, err) - assert.Equal(t, "", result) + assert.Equal(t, "1", result) // Test get calculated cell value on not exists worksheet f = prepareCalcData(cellData) _, err = f.CalcCellValue("SheetN", "A1") @@ -4718,7 +4730,7 @@ func TestCalcWithDefinedName(t *testing.T) { 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")`) + assert.Equal(t, "YES", result, "=IF(\"B1_as_string\"=defined_name1,\"YES\",\"NO\")") } func TestCalcISBLANK(t *testing.T) { @@ -6140,14 +6152,14 @@ func TestCalcBetainvProbIterator(t *testing.T) { func TestNestedFunctionsWithOperators(t *testing.T) { f := NewFile() formulaList := map[string]string{ - `=LEN("KEEP")`: "4", - `=LEN("REMOVEKEEP") - LEN("REMOVE")`: "4", - `=RIGHT("REMOVEKEEP", 4)`: "KEEP", - `=RIGHT("REMOVEKEEP", 10 - 6))`: "KEEP", - `=RIGHT("REMOVEKEEP", LEN("REMOVEKEEP") - 6)`: "KEEP", - `=RIGHT("REMOVEKEEP", LEN("REMOVEKEEP") - LEN("REMOV") - 1)`: "KEEP", - `=RIGHT("REMOVEKEEP", 10 - LEN("REMOVE"))`: "KEEP", - `=RIGHT("REMOVEKEEP", LEN("REMOVEKEEP") - LEN("REMOVE"))`: "KEEP", + "=LEN(\"KEEP\")": "4", + "=LEN(\"REMOVEKEEP\") - LEN(\"REMOVE\")": "4", + "=RIGHT(\"REMOVEKEEP\", 4)": "KEEP", + "=RIGHT(\"REMOVEKEEP\", 10 - 6))": "KEEP", + "=RIGHT(\"REMOVEKEEP\", LEN(\"REMOVEKEEP\") - 6)": "KEEP", + "=RIGHT(\"REMOVEKEEP\", LEN(\"REMOVEKEEP\") - LEN(\"REMOV\") - 1)": "KEEP", + "=RIGHT(\"REMOVEKEEP\", 10 - LEN(\"REMOVE\"))": "KEEP", + "=RIGHT(\"REMOVEKEEP\", LEN(\"REMOVEKEEP\") - LEN(\"REMOVE\"))": "KEEP", } for formula, expected := range formulaList { assert.NoError(t, f.SetCellFormula("Sheet1", "E1", formula)) diff --git a/excelize_test.go b/excelize_test.go index 117d86a7ef..9879302648 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -1202,16 +1202,18 @@ func TestConditionalFormat(t *testing.T) { }, }, )) + // Test set conditional format with invalid cell reference + assert.Equal(t, newCellNameToCoordinatesError("-", newInvalidCellNameError("-")), f.SetConditionalFormat("Sheet1", "A1:-", nil)) // Test set conditional format on not exists worksheet 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", nil), ErrSheetNameInvalid.Error()) + assert.Equal(t, ErrSheetNameInvalid, f.SetConditionalFormat("Sheet:1", "L1:L10", nil)) err = f.SaveAs(filepath.Join("test", "TestConditionalFormat.xlsx")) assert.NoError(t, err) // Set conditional format with illegal valid type - assert.NoError(t, f.SetConditionalFormat(sheet1, "K1:K10", + assert.Equal(t, ErrParameterInvalid, f.SetConditionalFormat(sheet1, "K1:K10", []ConditionalFormatOptions{ { Type: "", diff --git a/styles.go b/styles.go index 311bed87fa..2be700f7e8 100644 --- a/styles.go +++ b/styles.go @@ -33,12 +33,12 @@ var validType = map[string]string{ "unique": "uniqueValues", "top": "top10", "bottom": "top10", - "text": "text", // Doesn't support currently - "time_period": "timePeriod", // Doesn't support currently - "blanks": "containsBlanks", // Doesn't support currently - "no_blanks": "notContainsBlanks", // Doesn't support currently - "errors": "containsErrors", // Doesn't support currently - "no_errors": "notContainsErrors", // Doesn't support currently + "text": "text", + "time_period": "timePeriod", // Doesn't support currently + "blanks": "containsBlanks", + "no_blanks": "notContainsBlanks", + "errors": "containsErrors", + "no_errors": "notContainsErrors", "2_color_scale": "2_color_scale", "3_color_scale": "3_color_scale", "data_bar": "dataBar", @@ -1223,29 +1223,42 @@ var ( }, } // drawContFmtFunc defines functions to create conditional formats. - drawContFmtFunc = map[string]func(p int, ct, GUID string, fmtCond *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule){ - "cellIs": drawCondFmtCellIs, - "top10": drawCondFmtTop10, - "aboveAverage": drawCondFmtAboveAverage, - "duplicateValues": drawCondFmtDuplicateUniqueValues, - "uniqueValues": drawCondFmtDuplicateUniqueValues, - "2_color_scale": drawCondFmtColorScale, - "3_color_scale": drawCondFmtColorScale, - "dataBar": drawCondFmtDataBar, - "expression": drawCondFmtExp, - "iconSet": drawCondFmtIconSet, + drawContFmtFunc = map[string]func(p int, ct, ref, GUID string, fmtCond *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule){ + "cellIs": drawCondFmtCellIs, + "text": drawCondFmtText, + "top10": drawCondFmtTop10, + "aboveAverage": drawCondFmtAboveAverage, + "duplicateValues": drawCondFmtDuplicateUniqueValues, + "uniqueValues": drawCondFmtDuplicateUniqueValues, + "containsBlanks": drawCondFmtBlanks, + "notContainsBlanks": drawCondFmtNoBlanks, + "containsErrors": drawCondFmtErrors, + "notContainsErrors": drawCondFmtNoErrors, + "2_color_scale": drawCondFmtColorScale, + "3_color_scale": drawCondFmtColorScale, + "dataBar": drawCondFmtDataBar, + "expression": drawCondFmtExp, + "iconSet": drawCondFmtIconSet, } // extractContFmtFunc defines functions to get conditional formats. extractContFmtFunc = map[string]func(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions{ - "cellIs": extractCondFmtCellIs, - "top10": extractCondFmtTop10, - "aboveAverage": extractCondFmtAboveAverage, - "duplicateValues": extractCondFmtDuplicateUniqueValues, - "uniqueValues": extractCondFmtDuplicateUniqueValues, - "colorScale": extractCondFmtColorScale, - "dataBar": extractCondFmtDataBar, - "expression": extractCondFmtExp, - "iconSet": extractCondFmtIconSet, + "cellIs": extractCondFmtCellIs, + "containsText": extractCondFmtText, + "notContainsText": extractCondFmtText, + "beginsWith": extractCondFmtText, + "endsWith": extractCondFmtText, + "top10": extractCondFmtTop10, + "aboveAverage": extractCondFmtAboveAverage, + "duplicateValues": extractCondFmtDuplicateUniqueValues, + "uniqueValues": extractCondFmtDuplicateUniqueValues, + "containsBlanks": extractCondFmtBlanks, + "notContainsBlanks": extractCondFmtNoBlanks, + "containsErrors": extractCondFmtErrors, + "notContainsErrors": extractCondFmtNoErrors, + "colorScale": extractCondFmtColorScale, + "dataBar": extractCondFmtDataBar, + "expression": extractCondFmtExp, + "iconSet": extractCondFmtIconSet, } ) @@ -2635,13 +2648,31 @@ func (f *File) SetConditionalFormat(sheet, rangeRef string, opts []ConditionalFo if err != nil { return err } + if strings.Contains(rangeRef, ":") { + rect, err := rangeRefToCoordinates(rangeRef) + if err != nil { + return err + } + _ = sortCoordinates(rect) + rangeRef, _ = f.coordinatesToRangeRef(rect, strings.Contains(rangeRef, "$")) + } // 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 + var ( + GUID = fmt.Sprintf("{00000000-0000-0000-%04X-%012X}", f.getSheetID(sheet), rules) + cfRule []*xlsxCfRule + noCriteriaTypes = []string{ + "containsBlanks", + "notContainsBlanks", + "containsErrors", + "notContainsErrors", + "expression", + "iconSet", + } + ) for p, v := range opts { var vt, ct string var ok bool @@ -2650,10 +2681,10 @@ 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" || vt == "iconSet" { + if ok || inStrSlice(noCriteriaTypes, vt, true) != -1 { drawFunc, ok := drawContFmtFunc[vt] if ok { - rule, x14rule := drawFunc(p, ct, GUID, &v) + rule, x14rule := drawFunc(p, ct, strings.Split(rangeRef, ":")[0], GUID, &v) if rule == nil { return ErrParameterInvalid } @@ -2669,6 +2700,7 @@ func (f *File) SetConditionalFormat(sheet, rangeRef string, opts []ConditionalFo } return ErrParameterInvalid } + return ErrParameterInvalid } ws.ConditionalFormatting = append(ws.ConditionalFormatting, &xlsxConditionalFormatting{ @@ -2740,6 +2772,12 @@ func extractCondFmtCellIs(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOp return format } +// extractCondFmtText provides a function to extract conditional format +// settings for text cell values by given conditional formatting rule. +func extractCondFmtText(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { + return ConditionalFormatOptions{StopIfTrue: c.StopIfTrue, Type: "text", Criteria: operatorType[c.Operator], Format: *c.DxfID, Value: c.Text} +} + // extractCondFmtTop10 provides a function to extract conditional format // settings for top N (default is top 10) by given conditional formatting // rule. @@ -2786,6 +2824,46 @@ func extractCondFmtDuplicateUniqueValues(c *xlsxCfRule, extLst *xlsxExtLst) Cond } } +// extractCondFmtBlanks provides a function to extract conditional format +// settings for blank cells by given conditional formatting rule. +func extractCondFmtBlanks(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { + return ConditionalFormatOptions{ + StopIfTrue: c.StopIfTrue, + Type: "blanks", + Format: *c.DxfID, + } +} + +// extractCondFmtNoBlanks provides a function to extract conditional format +// settings for no blank cells by given conditional formatting rule. +func extractCondFmtNoBlanks(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { + return ConditionalFormatOptions{ + StopIfTrue: c.StopIfTrue, + Type: "no_blanks", + Format: *c.DxfID, + } +} + +// extractCondFmtErrors provides a function to extract conditional format +// settings for cells with errors by given conditional formatting rule. +func extractCondFmtErrors(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { + return ConditionalFormatOptions{ + StopIfTrue: c.StopIfTrue, + Type: "errors", + Format: *c.DxfID, + } +} + +// extractCondFmtNoErrors provides a function to extract conditional format +// settings for cells without errors by given conditional formatting rule. +func extractCondFmtNoErrors(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { + return ConditionalFormatOptions{ + StopIfTrue: c.StopIfTrue, + Type: "no_errors", + 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. @@ -2938,7 +3016,7 @@ 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, GUID string, format *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule) { +func drawCondFmtCellIs(p int, ct, ref, GUID string, format *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule) { c := &xlsxCfRule{ Priority: p + 1, StopIfTrue: format.StopIfTrue, @@ -2950,16 +3028,46 @@ func drawCondFmtCellIs(p int, ct, GUID string, format *ConditionalFormatOptions) if ct == "between" || ct == "notBetween" { 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 { + if inStrSlice([]string{"equal", "notEqual", "greaterThan", "lessThan", "greaterThanOrEqual", "lessThanOrEqual", "containsText", "notContains", "beginsWith", "endsWith"}, ct, true) != -1 { c.Formula = append(c.Formula, format.Value) } return c, nil } +// drawCondFmtText provides a function to create conditional formatting rule for +// text cell values by given priority, criteria type and format settings. +func drawCondFmtText(p int, ct, ref, GUID string, format *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule) { + return &xlsxCfRule{ + Priority: p + 1, + StopIfTrue: format.StopIfTrue, + Type: map[string]string{ + "containsText": "containsText", + "notContains": "notContainsText", + "beginsWith": "beginsWith", + "endsWith": "endsWith", + }[ct], + Text: format.Value, + Operator: ct, + Formula: []string{ + map[string]string{ + "containsText": fmt.Sprintf("NOT(ISERROR(SEARCH(\"%s\",%s)))", + strings.NewReplacer(`"`, `""`).Replace(format.Value), ref), + "notContains": fmt.Sprintf("ISERROR(SEARCH(\"%s\",%s))", + strings.NewReplacer(`"`, `""`).Replace(format.Value), ref), + "beginsWith": fmt.Sprintf("LEFT(%[2]s,LEN(\"%[1]s\"))=\"%[1]s\"", + strings.NewReplacer(`"`, `""`).Replace(format.Value), ref), + "endsWith": fmt.Sprintf("RIGHT(%[2]s,LEN(\"%[1]s\"))=\"%[1]s\"", + strings.NewReplacer(`"`, `""`).Replace(format.Value), ref), + }[ct], + }, + DxfID: intPtr(format.Format), + }, 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, GUID string, format *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule) { +func drawCondFmtTop10(p int, ct, ref, GUID string, format *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule) { c := &xlsxCfRule{ Priority: p + 1, StopIfTrue: format.StopIfTrue, @@ -2978,7 +3086,7 @@ func drawCondFmtTop10(p int, ct, GUID string, format *ConditionalFormatOptions) // 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, GUID string, format *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule) { +func drawCondFmtAboveAverage(p int, ct, ref, GUID string, format *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule) { return &xlsxCfRule{ Priority: p + 1, StopIfTrue: format.StopIfTrue, @@ -2991,7 +3099,7 @@ func drawCondFmtAboveAverage(p int, ct, GUID string, format *ConditionalFormatOp // 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, GUID string, format *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule) { +func drawCondFmtDuplicateUniqueValues(p int, ct, ref, GUID string, format *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule) { return &xlsxCfRule{ Priority: p + 1, StopIfTrue: format.StopIfTrue, @@ -3003,7 +3111,7 @@ func drawCondFmtDuplicateUniqueValues(p int, ct, GUID string, format *Conditiona // 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, GUID string, format *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule) { +func drawCondFmtColorScale(p int, ct, ref, GUID string, format *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule) { minValue := format.MinValue if minValue == "" { minValue = "0" @@ -3041,7 +3149,7 @@ func drawCondFmtColorScale(p int, ct, GUID string, format *ConditionalFormatOpti // 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, GUID string, format *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule) { +func drawCondFmtDataBar(p int, ct, ref, GUID string, format *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule) { var x14CfRule *xlsxX14CfRule var extLst *xlsxExtLst if format.BarSolid || format.BarDirection == "leftToRight" || format.BarDirection == "rightToLeft" || format.BarBorderColor != "" { @@ -3078,7 +3186,7 @@ func drawCondFmtDataBar(p int, ct, GUID string, format *ConditionalFormatOptions // drawCondFmtExp provides a function to create conditional formatting rule // for expression by given priority, criteria type and format settings. -func drawCondFmtExp(p int, ct, GUID string, format *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule) { +func drawCondFmtExp(p int, ct, ref, GUID string, format *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule) { return &xlsxCfRule{ Priority: p + 1, StopIfTrue: format.StopIfTrue, @@ -3088,9 +3196,57 @@ func drawCondFmtExp(p int, ct, GUID string, format *ConditionalFormatOptions) (* }, nil } +// drawCondFmtErrors provides a function to create conditional formatting rule +// for cells with errors by given priority, criteria type and format settings. +func drawCondFmtErrors(p int, ct, ref, GUID string, format *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule) { + return &xlsxCfRule{ + Priority: p + 1, + StopIfTrue: format.StopIfTrue, + Type: validType[format.Type], + Formula: []string{fmt.Sprintf("ISERROR(%s)", ref)}, + DxfID: intPtr(format.Format), + }, nil +} + +// drawCondFmtErrors provides a function to create conditional formatting rule +// for cells without errors by given priority, criteria type and format settings. +func drawCondFmtNoErrors(p int, ct, ref, GUID string, format *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule) { + return &xlsxCfRule{ + Priority: p + 1, + StopIfTrue: format.StopIfTrue, + Type: validType[format.Type], + Formula: []string{fmt.Sprintf("NOT(ISERROR(%s))", ref)}, + DxfID: intPtr(format.Format), + }, nil +} + +// drawCondFmtErrors provides a function to create conditional formatting rule +// for blank cells by given priority, criteria type and format settings. +func drawCondFmtBlanks(p int, ct, ref, GUID string, format *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule) { + return &xlsxCfRule{ + Priority: p + 1, + StopIfTrue: format.StopIfTrue, + Type: validType[format.Type], + Formula: []string{fmt.Sprintf("LEN(TRIM(%s))=0", ref)}, + DxfID: intPtr(format.Format), + }, nil +} + +// drawCondFmtErrors provides a function to create conditional formatting rule +// for no blanks cells by given priority, criteria type and format settings. +func drawCondFmtNoBlanks(p int, ct, ref, GUID string, format *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule) { + return &xlsxCfRule{ + Priority: p + 1, + StopIfTrue: format.StopIfTrue, + Type: validType[format.Type], + Formula: []string{fmt.Sprintf("LEN(TRIM(%s))>0", ref)}, + DxfID: intPtr(format.Format), + }, 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) { +func drawCondFmtIconSet(p int, ct, ref, GUID string, format *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule) { cfvo3 := &xlsxCfRule{IconSet: &xlsxIconSet{Cfvo: []*xlsxCfvo{ {Type: "percent", Val: "0"}, {Type: "percent", Val: "33"}, diff --git a/styles_test.go b/styles_test.go index c3ba1e9539..928f0c53cf 100644 --- a/styles_test.go +++ b/styles_test.go @@ -195,12 +195,7 @@ func TestSetConditionalFormat(t *testing.T) { for _, val := range []string{ "date", "time", - "text", "time_period", - "blanks", - "no_blanks", - "errors", - "no_errors", } { assert.Equal(t, ErrParameterInvalid, f.SetConditionalFormat("Sheet1", "A1", []ConditionalFormatOptions{{Type: val}})) } @@ -210,6 +205,10 @@ func TestGetConditionalFormats(t *testing.T) { for _, format := range [][]ConditionalFormatOptions{ {{Type: "cell", Format: 1, Criteria: "greater than", Value: "6"}}, {{Type: "cell", Format: 1, Criteria: "between", MinValue: "6", MaxValue: "8"}}, + {{Type: "text", Format: 1, Criteria: "containing", Value: "~!@#$%^&*()_+{}|:<>?\"';"}}, + {{Type: "text", Format: 1, Criteria: "not containing", Value: "text"}}, + {{Type: "text", Format: 1, Criteria: "begins with", Value: "prefix"}}, + {{Type: "text", Format: 1, Criteria: "ends with", Value: "suffix"}}, {{Type: "top", Format: 1, Criteria: "=", Value: "6"}}, {{Type: "bottom", Format: 1, Criteria: "=", Value: "6"}}, {{Type: "average", AboveAverage: true, Format: 1, Criteria: "="}}, @@ -220,10 +219,14 @@ func TestGetConditionalFormats(t *testing.T) { {{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: "blanks", Format: 1}}, + {{Type: "no_blanks", Format: 1}}, + {{Type: "errors", Format: 1}}, + {{Type: "no_errors", Format: 1}}, {{Type: "icon_set", IconStyle: "3Arrows", ReverseIcons: true, IconsOnly: true}}, } { f := NewFile() - err := f.SetConditionalFormat("Sheet1", "A1:A2", format) + err := f.SetConditionalFormat("Sheet1", "A2:A1", format) assert.NoError(t, err) opts, err := f.GetConditionalFormats("Sheet1") assert.NoError(t, err) From 5e247de805ab544075e969a228c7f2bcd2a97c16 Mon Sep 17 00:00:00 2001 From: Yang Li Date: Tue, 14 Nov 2023 09:47:57 +0800 Subject: [PATCH 825/957] Support set time period type conditional formatting (#1718) --- styles.go | 114 +++++++++++++++++++++++++++++++------------------ styles_test.go | 18 +++++--- 2 files changed, 83 insertions(+), 49 deletions(-) diff --git a/styles.go b/styles.go index 2be700f7e8..f58a3ceb3e 100644 --- a/styles.go +++ b/styles.go @@ -26,15 +26,13 @@ import ( // validType defined the list of valid validation types. var validType = map[string]string{ "cell": "cellIs", - "date": "date", // Doesn't support currently - "time": "time", // Doesn't support currently "average": "aboveAverage", "duplicate": "duplicateValues", "unique": "uniqueValues", "top": "top10", "bottom": "top10", "text": "text", - "time_period": "timePeriod", // Doesn't support currently + "time_period": "timePeriod", "blanks": "containsBlanks", "no_blanks": "notContainsBlanks", "errors": "containsErrors", @@ -48,60 +46,62 @@ var validType = map[string]string{ // criteriaType defined the list of valid criteria types. var criteriaType = map[string]string{ - "between": "between", - "not between": "notBetween", - "equal to": "equal", - "=": "equal", - "==": "equal", - "not equal to": "notEqual", "!=": "notEqual", + "<": "lessThan", + "<=": "lessThanOrEqual", "<>": "notEqual", - "greater than": "greaterThan", + "=": "equal", + "==": "equal", ">": "greaterThan", - "less than": "lessThan", - "<": "lessThan", - "greater than or equal to": "greaterThanOrEqual", ">=": "greaterThanOrEqual", - "less than or equal to": "lessThanOrEqual", - "<=": "lessThanOrEqual", - "containing": "containsText", - "not containing": "notContains", "begins with": "beginsWith", + "between": "between", + "containing": "containsText", + "continue month": "nextMonth", + "continue week": "nextWeek", "ends with": "endsWith", - "yesterday": "yesterday", - "today": "today", + "equal to": "equal", + "greater than or equal to": "greaterThanOrEqual", + "greater than": "greaterThan", "last 7 days": "last7Days", - "last week": "lastWeek", - "this week": "thisWeek", - "continue week": "continueWeek", "last month": "lastMonth", + "last week": "lastWeek", + "less than or equal to": "lessThanOrEqual", + "less than": "lessThan", + "not between": "notBetween", + "not containing": "notContains", + "not equal to": "notEqual", "this month": "thisMonth", - "continue month": "continueMonth", + "this week": "thisWeek", + "today": "today", + "tomorrow": "tomorrow", + "yesterday": "yesterday", } // operatorType defined the list of valid operator types. var operatorType = map[string]string{ - "lastMonth": "last month", + "beginsWith": "begins with", "between": "between", - "notEqual": "not equal to", + "containsText": "containing", + "endsWith": "ends with", + "equal": "equal to", "greaterThan": "greater than", + "greaterThanOrEqual": "greater than or equal to", + "last7Days": "last 7 days", + "lastMonth": "last month", + "lastWeek": "last week", + "lessThan": "less than", "lessThanOrEqual": "less than or equal to", - "today": "today", - "equal": "equal to", + "nextMonth": "continue month", + "nextWeek": "continue week", + "notBetween": "not between", "notContains": "not containing", + "notEqual": "not equal to", + "thisMonth": "this month", "thisWeek": "this week", - "endsWith": "ends with", + "today": "today", + "tomorrow": "tomorrow", "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", } // stylesReader provides a function to get the pointer to the structure after @@ -1225,6 +1225,7 @@ var ( // drawContFmtFunc defines functions to create conditional formats. drawContFmtFunc = map[string]func(p int, ct, ref, GUID string, fmtCond *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule){ "cellIs": drawCondFmtCellIs, + "timePeriod": drawCondFmtTimePeriod, "text": drawCondFmtText, "top10": drawCondFmtTop10, "aboveAverage": drawCondFmtAboveAverage, @@ -1243,6 +1244,7 @@ var ( // extractContFmtFunc defines functions to get conditional formats. extractContFmtFunc = map[string]func(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions{ "cellIs": extractCondFmtCellIs, + "timePeriod": extractCondFmtTimePeriod, "containsText": extractCondFmtText, "notContainsText": extractCondFmtText, "beginsWith": extractCondFmtText, @@ -2241,10 +2243,6 @@ func (f *File) SetCellStyle(sheet, hCell, vCell string, styleID int) error { // | Value // | MinValue // | MaxValue -// date | Criteria -// | Value -// | MinValue -// | MaxValue // time_period | Criteria // text | Criteria // | Value @@ -2772,6 +2770,12 @@ func extractCondFmtCellIs(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOp return format } +// extractCondFmtTimePeriod provides a function to extract conditional format +// settings for time period by given conditional formatting rule. +func extractCondFmtTimePeriod(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { + return ConditionalFormatOptions{StopIfTrue: c.StopIfTrue, Type: "time_period", Criteria: operatorType[c.Operator], Format: *c.DxfID} +} + // extractCondFmtText provides a function to extract conditional format // settings for text cell values by given conditional formatting rule. func extractCondFmtText(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { @@ -3034,6 +3038,32 @@ func drawCondFmtCellIs(p int, ct, ref, GUID string, format *ConditionalFormatOpt return c, nil } +// drawCondFmtTimePeriod provides a function to create conditional formatting +// rule for time period by given priority, criteria type and format settings. +func drawCondFmtTimePeriod(p int, ct, ref, GUID string, format *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule) { + return &xlsxCfRule{ + Priority: p + 1, + StopIfTrue: format.StopIfTrue, + Type: "timePeriod", + Operator: ct, + Formula: []string{ + map[string]string{ + "yesterday": fmt.Sprintf("FLOOR(%s,1)=TODAY()-1", ref), + "today": fmt.Sprintf("FLOOR(%s,1)=TODAY()", ref), + "tomorrow": fmt.Sprintf("FLOOR(%s,1)=TODAY()+1", ref), + "last 7 days": fmt.Sprintf("AND(TODAY()-FLOOR(%[1]s,1)<=6,FLOOR(%[1]s,1)<=TODAY())", ref), + "last week": fmt.Sprintf("AND(TODAY()-ROUNDDOWN(%[1]s,0)>=(WEEKDAY(TODAY())),TODAY()-ROUNDDOWN(%[1]s,0)<(WEEKDAY(TODAY())+7))", ref), + "this week": fmt.Sprintf("AND(TODAY()-ROUNDDOWN(%[1]s,0)<=WEEKDAY(TODAY())-1,ROUNDDOWN(%[1]s,0)-TODAY()>=7-WEEKDAY(TODAY()))", ref), + "continue week": fmt.Sprintf("AND(ROUNDDOWN(%[1]s,0)-TODAY()>(7-WEEKDAY(TODAY())),ROUNDDOWN(%[1]s,0)-TODAY()<(15-WEEKDAY(TODAY())))", ref), + "last month": fmt.Sprintf("AND(MONTH(%[1]s)=MONTH(TODAY())-1,OR(YEAR(%[1]s)=YEAR(TODAY()),AND(MONTH(%[1]s)=1,YEAR(%[1]s)=YEAR(TODAY())-1)))", ref), + "this month": fmt.Sprintf("AND(MONTH(%[1]s)=MONTH(TODAY()),YEAR(%[1]s)=YEAR(TODAY()))", ref), + "continue month": fmt.Sprintf("AND(MONTH(%[1]s)=MONTH(TODAY())+1,OR(YEAR(%[1]s)=YEAR(TODAY()),AND(MONTH(%[1]s)=12,YEAR(%[1]s)=YEAR(TODAY())+1)))", ref), + }[ct], + }, + DxfID: intPtr(format.Format), + }, nil +} + // drawCondFmtText provides a function to create conditional formatting rule for // text cell values by given priority, criteria type and format settings. func drawCondFmtText(p int, ct, ref, GUID string, format *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule) { diff --git a/styles_test.go b/styles_test.go index 928f0c53cf..881828f996 100644 --- a/styles_test.go +++ b/styles_test.go @@ -192,19 +192,23 @@ func TestSetConditionalFormat(t *testing.T) { // Test creating a conditional format with invalid icon set style assert.Equal(t, ErrParameterInvalid, f.SetConditionalFormat("Sheet1", "A1:A2", []ConditionalFormatOptions{{Type: "icon_set", IconStyle: "unknown"}})) // Test unsupported conditional formatting rule types - for _, val := range []string{ - "date", - "time", - "time_period", - } { - assert.Equal(t, ErrParameterInvalid, f.SetConditionalFormat("Sheet1", "A1", []ConditionalFormatOptions{{Type: val}})) - } + assert.Equal(t, ErrParameterInvalid, f.SetConditionalFormat("Sheet1", "A1", []ConditionalFormatOptions{{Type: "unsupported"}})) } func TestGetConditionalFormats(t *testing.T) { for _, format := range [][]ConditionalFormatOptions{ {{Type: "cell", Format: 1, Criteria: "greater than", Value: "6"}}, {{Type: "cell", Format: 1, Criteria: "between", MinValue: "6", MaxValue: "8"}}, + {{Type: "time_period", Format: 1, Criteria: "yesterday"}}, + {{Type: "time_period", Format: 1, Criteria: "today"}}, + {{Type: "time_period", Format: 1, Criteria: "tomorrow"}}, + {{Type: "time_period", Format: 1, Criteria: "last 7 days"}}, + {{Type: "time_period", Format: 1, Criteria: "last week"}}, + {{Type: "time_period", Format: 1, Criteria: "this week"}}, + {{Type: "time_period", Format: 1, Criteria: "continue week"}}, + {{Type: "time_period", Format: 1, Criteria: "last month"}}, + {{Type: "time_period", Format: 1, Criteria: "this month"}}, + {{Type: "time_period", Format: 1, Criteria: "continue month"}}, {{Type: "text", Format: 1, Criteria: "containing", Value: "~!@#$%^&*()_+{}|:<>?\"';"}}, {{Type: "text", Format: 1, Criteria: "not containing", Value: "text"}}, {{Type: "text", Format: 1, Criteria: "begins with", Value: "prefix"}}, From 3bdc2c5fc705dce5f1a87bcc1a9e43fc347b50e5 Mon Sep 17 00:00:00 2001 From: 15535382838 <66766230+15535382838@users.noreply.github.com> Date: Tue, 14 Nov 2023 22:49:18 -0600 Subject: [PATCH 826/957] This add new exported function GetHeaderFooter (#1720) --- sheet.go | 26 ++++++++++++++++++++++++++ sheet_test.go | 18 +++++++++++++++--- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/sheet.go b/sheet.go index daf6f4c813..b9201a363d 100644 --- a/sheet.go +++ b/sheet.go @@ -1289,6 +1289,32 @@ func (f *File) SetHeaderFooter(sheet string, opts *HeaderFooterOptions) error { return err } +// GetHeaderFooter provides a function to get worksheet header and footer by +// given worksheet name. +func (f *File) GetHeaderFooter(sheet string) (*HeaderFooterOptions, error) { + var opts *HeaderFooterOptions + ws, err := f.workSheetReader(sheet) + if err != nil { + return opts, err + } + if ws.HeaderFooter == nil { + return opts, err + } + opts = &HeaderFooterOptions{ + AlignWithMargins: ws.HeaderFooter.AlignWithMargins, + DifferentFirst: ws.HeaderFooter.DifferentFirst, + DifferentOddEven: ws.HeaderFooter.DifferentOddEven, + ScaleWithDoc: ws.HeaderFooter.ScaleWithDoc, + OddHeader: ws.HeaderFooter.OddHeader, + OddFooter: ws.HeaderFooter.OddFooter, + EvenHeader: ws.HeaderFooter.EvenHeader, + EvenFooter: ws.HeaderFooter.EvenFooter, + FirstHeader: ws.HeaderFooter.FirstHeader, + FirstFooter: ws.HeaderFooter.FirstFooter, + } + return opts, err +} + // 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, diff --git a/sheet_test.go b/sheet_test.go index bb9a7862fa..044af91708 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -231,8 +231,16 @@ func TestGetPageLayout(t *testing.T) { assert.EqualError(t, err, ErrSheetNameInvalid.Error()) } -func TestSetHeaderFooter(t *testing.T) { +func TestHeaderFooter(t *testing.T) { f := NewFile() + // Test get header and footer with default header and footer settings + opts, err := f.GetHeaderFooter("Sheet1") + assert.NoError(t, err) + assert.Equal(t, (*HeaderFooterOptions)(nil), opts) + // Test get header and footer on not exists worksheet + _, err = f.GetHeaderFooter("SheetN") + assert.EqualError(t, err, "sheet SheetN does not exist") + 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 does not exist") @@ -252,7 +260,7 @@ func TestSetHeaderFooter(t *testing.T) { EvenFooter: text, FirstHeader: text, })) - assert.NoError(t, f.SetHeaderFooter("Sheet1", &HeaderFooterOptions{ + expected := &HeaderFooterOptions{ DifferentFirst: true, DifferentOddEven: true, OddHeader: "&R&P", @@ -260,7 +268,11 @@ func TestSetHeaderFooter(t *testing.T) { EvenHeader: "&L&P", EvenFooter: "&L&D&R&T", FirstHeader: `&CCenter &"-,Bold"Bold&"-,Regular"HeaderU+000A&D`, - })) + } + assert.NoError(t, f.SetHeaderFooter("Sheet1", expected)) + opts, err = f.GetHeaderFooter("Sheet1") + assert.NoError(t, err) + assert.Equal(t, expected, opts) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetHeaderFooter.xlsx"))) } From 57faaf253afb5d6b59b53c2322e9ae42dc3633b4 Mon Sep 17 00:00:00 2001 From: Tajang <63721558+TajangSec@users.noreply.github.com> Date: Thu, 16 Nov 2023 09:34:37 +0800 Subject: [PATCH 827/957] Fix panic on GetPictureCells without drawing relationships parts (#1721) --- picture.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/picture.go b/picture.go index 499f4937c2..9411f07524 100644 --- a/picture.go +++ b/picture.go @@ -622,9 +622,10 @@ func (f *File) extractDecodeCellAnchor(anchor *xdrCellAnchor, drawingRelationshi _ = f.xmlNewDecoder(strings.NewReader("" + anchor.GraphicFrame + "")).Decode(&deCellAnchor) if deCellAnchor.From != nil && deCellAnchor.Pic != nil { if cond(deCellAnchor.From) { - drawRel = f.getDrawingRelationships(drawingRelationships, deCellAnchor.Pic.BlipFill.Blip.Embed) - if _, ok := supportedImageTypes[strings.ToLower(filepath.Ext(drawRel.Target))]; ok { - cb(deCellAnchor, drawRel) + if drawRel = f.getDrawingRelationships(drawingRelationships, deCellAnchor.Pic.BlipFill.Blip.Embed); drawRel != nil { + if _, ok := supportedImageTypes[strings.ToLower(filepath.Ext(drawRel.Target))]; ok { + cb(deCellAnchor, drawRel) + } } } } From 6220a798fd79231bf4b3d7ef587bb2b79d276710 Mon Sep 17 00:00:00 2001 From: lujin <33309882+lujin1@users.noreply.github.com> Date: Sat, 18 Nov 2023 16:44:45 +0800 Subject: [PATCH 828/957] This closes #1723, fix panic on read workbook in some cases (#1692) - Fix panic on read workbook with internal row element without r attribute - Check worksheet XML before get all cell value by column or row - Update the unit tests --- adjust.go | 10 ++--- adjust_test.go | 2 +- cell.go | 6 +-- cell_test.go | 16 ++++++-- col.go | 6 +-- col_test.go | 20 ++++++++-- excelize.go | 97 ++++++++++++++++++++++++++++++------------------- rows.go | 14 +++---- sheet.go | 2 +- xmlWorksheet.go | 2 +- 10 files changed, 110 insertions(+), 65 deletions(-) diff --git a/adjust.go b/adjust.go index 5cdb711c84..b5a99c5316 100644 --- a/adjust.go +++ b/adjust.go @@ -201,13 +201,13 @@ func (f *File) adjustRowDimensions(sheet string, ws *xlsxWorksheet, row, offset return nil } lastRow := &ws.SheetData.Row[totalRows-1] - if newRow := lastRow.R + offset; lastRow.R >= row && newRow > 0 && newRow > TotalRows { + if newRow := *lastRow.R + offset; *lastRow.R >= row && newRow > 0 && newRow > TotalRows { return ErrMaxRows } numOfRows := len(ws.SheetData.Row) for i := 0; i < numOfRows; i++ { r := &ws.SheetData.Row[i] - if newRow := r.R + offset; r.R >= row && newRow > 0 { + if newRow := *r.R + offset; *r.R >= row && newRow > 0 { r.adjustSingleRowDimensions(offset) } if err := f.adjustSingleRowFormulas(sheet, sheet, r, row, offset, false); err != nil { @@ -219,10 +219,10 @@ func (f *File) adjustRowDimensions(sheet string, ws *xlsxWorksheet, row, offset // adjustSingleRowDimensions provides a function to adjust single row dimensions. func (r *xlsxRow) adjustSingleRowDimensions(offset int) { - r.R += offset + r.R = intPtr(*r.R + offset) for i, col := range r.C { colName, _, _ := SplitCellName(col.R) - r.C[i].R, _ = JoinCellName(colName, r.R) + r.C[i].R, _ = JoinCellName(colName, *r.R) } } @@ -561,7 +561,7 @@ func (f *File) adjustAutoFilter(ws *xlsxWorksheet, sheet string, dir adjustDirec ws.AutoFilter = nil for rowIdx := range ws.SheetData.Row { rowData := &ws.SheetData.Row[rowIdx] - if rowData.R > y1 && rowData.R <= y2 { + if rowData.R != nil && *rowData.R > y1 && *rowData.R <= y2 { rowData.Hidden = false } } diff --git a/adjust_test.go b/adjust_test.go index 769affe397..a8dd2ff379 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -289,7 +289,7 @@ func TestAdjustAutoFilter(t *testing.T) { f := NewFile() assert.NoError(t, f.adjustAutoFilter(&xlsxWorksheet{ SheetData: xlsxSheetData{ - Row: []xlsxRow{{Hidden: true, R: 2}}, + Row: []xlsxRow{{Hidden: true, R: intPtr(2)}}, }, AutoFilter: &xlsxAutoFilter{ Ref: "A1:A3", diff --git a/cell.go b/cell.go index dd9980d9f4..80d113ad5a 100644 --- a/cell.go +++ b/cell.go @@ -1335,8 +1335,8 @@ func (f *File) getCellStringFunc(sheet, cell string, fn func(x *xlsxWorksheet, c return "", err } lastRowNum := 0 - if l := len(ws.SheetData.Row); l > 0 { - lastRowNum = ws.SheetData.Row[l-1].R + if l := len(ws.SheetData.Row); l > 0 && ws.SheetData.Row[l-1].R != nil { + lastRowNum = *ws.SheetData.Row[l-1].R } // keep in mind: row starts from 1 @@ -1346,7 +1346,7 @@ func (f *File) getCellStringFunc(sheet, cell string, fn func(x *xlsxWorksheet, c for rowIdx := range ws.SheetData.Row { rowData := &ws.SheetData.Row[rowIdx] - if rowData.R != row { + if rowData.R != nil && *rowData.R != row { continue } for colIdx := range rowData.C { diff --git a/cell_test.go b/cell_test.go index 1307688830..ea802c0e12 100644 --- a/cell_test.go +++ b/cell_test.go @@ -358,9 +358,6 @@ func TestGetCellValue(t *testing.T) { f.Sheet.Delete("xl/worksheets/sheet1.xml") f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `H6r0A6F4A6B6C6100B3`))) f.checked = sync.Map{} - cell, err = f.GetCellValue("Sheet1", "H6") - assert.Equal(t, "H6", cell) - assert.NoError(t, err) rows, err = f.GetRows("Sheet1") assert.Equal(t, [][]string{ {"A6", "B6", "C6"}, @@ -371,6 +368,19 @@ func TestGetCellValue(t *testing.T) { {"", "", "", "", "", "", "", "H6"}, }, rows) assert.NoError(t, err) + cell, err = f.GetCellValue("Sheet1", "H6") + assert.Equal(t, "H6", cell) + assert.NoError(t, err) + + f.Sheet.Delete("xl/worksheets/sheet1.xml") + f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `A1A3`))) + f.checked = sync.Map{} + rows, err = f.GetRows("Sheet1") + assert.Equal(t, [][]string{{"A1"}, nil, {"A3"}}, rows) + assert.NoError(t, err) + cell, err = f.GetCellValue("Sheet1", "A3") + assert.Equal(t, "A3", cell) + assert.NoError(t, err) f.Sheet.Delete("xl/worksheets/sheet1.xml") f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, ` diff --git a/col.go b/col.go index 13bf13978f..c5be1b742f 100644 --- a/col.go +++ b/col.go @@ -63,16 +63,16 @@ type Cols struct { // fmt.Println() // } func (f *File) GetCols(sheet string, opts ...Options) ([][]string, error) { - cols, err := f.Cols(sheet) - if err != nil { + if _, err := f.workSheetReader(sheet); err != nil { return nil, err } + cols, err := f.Cols(sheet) results := make([][]string, 0, 64) for cols.Next() { col, _ := cols.Rows(opts...) results = append(results, col) } - return results, nil + return results, err } // Next will return true if the next column is found. diff --git a/col_test.go b/col_test.go index 9af8ca03b9..ff3892e4fc 100644 --- a/col_test.go +++ b/col_test.go @@ -1,6 +1,7 @@ package excelize import ( + "fmt" "path/filepath" "sync" "testing" @@ -72,6 +73,17 @@ func TestCols(t *testing.T) { cols.Next() _, err = cols.Rows() assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") + + f = NewFile() + f.Sheet.Delete("xl/worksheets/sheet1.xml") + f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`B`)) + f.checked = sync.Map{} + _, err = f.Cols("Sheet1") + assert.EqualError(t, err, `strconv.Atoi: parsing "A": invalid syntax`) + + f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`B`)) + _, err = f.Cols("Sheet1") + assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) } func TestColumnsIterator(t *testing.T) { @@ -125,12 +137,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(fmt.Sprintf(`B`, NameSpaceSpreadSheet.Value))) f.checked = sync.Map{} _, err = f.GetCols("Sheet1") - assert.EqualError(t, err, `strconv.Atoi: parsing "A": invalid syntax`) + assert.EqualError(t, err, `strconv.ParseInt: parsing "A": invalid syntax`) - f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`B`)) + f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(`B`, NameSpaceSpreadSheet.Value))) _, err = f.GetCols("Sheet1") assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) @@ -140,7 +152,7 @@ func TestGetColsError(t *testing.T) { cols.totalRows = 2 cols.totalCols = 2 cols.curCol = 1 - cols.sheetXML = []byte(`A`) + cols.sheetXML = []byte(fmt.Sprintf(`A`, NameSpaceSpreadSheet.Value)) _, err = cols.Rows() assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) diff --git a/excelize.go b/excelize.go index ae80ce0df2..0b85760bda 100644 --- a/excelize.go +++ b/excelize.go @@ -303,60 +303,83 @@ 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 (ws *xlsxWorksheet) checkSheet() { - var row int - var r0 xlsxRow - for i, r := range ws.SheetData.Row { - if i == 0 && r.R == 0 { - r0 = r - ws.SheetData.Row = ws.SheetData.Row[1:] + row, r0 := ws.checkSheetRows() + sheetData := xlsxSheetData{Row: make([]xlsxRow, row)} + row = 0 + for _, r := range ws.SheetData.Row { + if r.R == nil { + row++ + r.R = intPtr(row) + sheetData.Row[row-1] = r continue } - if r.R != 0 && r.R > row { - row = r.R + if *r.R == row && row > 0 { + sheetData.Row[*r.R-1].C = append(sheetData.Row[*r.R-1].C, r.C...) continue } - if r.R != row { - row++ + if *r.R != 0 { + sheetData.Row[*r.R-1] = r + row = *r.R } } - sheetData := xlsxSheetData{Row: make([]xlsxRow, row)} - row = 0 - for _, r := range ws.SheetData.Row { - if r.R == row && row > 0 { - sheetData.Row[r.R-1].C = append(sheetData.Row[r.R-1].C, r.C...) + for i := 1; i <= len(sheetData.Row); i++ { + sheetData.Row[i-1].R = intPtr(i) + } + ws.checkSheetR0(&sheetData, r0) +} + +// checkSheetRows returns the last row number of the worksheet and rows element +// with r="0" attribute. +func (ws *xlsxWorksheet) checkSheetRows() (int, []xlsxRow) { + var ( + row, max int + r0 []xlsxRow + maxRowNum = func(num int, c []xlsxC) int { + for _, cell := range c { + if _, n, err := CellNameToCoordinates(cell.R); err == nil && n > num { + num = n + } + } + return num + } + ) + for i, r := range ws.SheetData.Row { + if r.R == nil { + row++ continue } - if r.R != 0 { - sheetData.Row[r.R-1] = r - row = r.R + if i == 0 && *r.R == 0 { + if num := maxRowNum(row, r.C); num > max { + max = num + } + r0 = append(r0, r) continue } - row++ - r.R = row - sheetData.Row[row-1] = r + if *r.R != 0 && *r.R > row { + row = *r.R + } } - for i := 1; i <= row; i++ { - sheetData.Row[i-1].R = i + if max > row { + row = max } - ws.checkSheetR0(&sheetData, &r0) + return row, 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 (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 - for r := rows; r < row; r++ { - sheetData.Row = append(sheetData.Row, xlsxRow{R: r + 1}) - } - columns, colIdx := len(sheetData.Row[rowIdx].C), col-1 - for c := columns; c < col; c++ { - sheetData.Row[rowIdx].C = append(sheetData.Row[rowIdx].C, xlsxC{}) - } - if !sheetData.Row[rowIdx].C[colIdx].hasValue() { - sheetData.Row[rowIdx].C[colIdx] = cell +func (ws *xlsxWorksheet) checkSheetR0(sheetData *xlsxSheetData, r0s []xlsxRow) { + for _, r0 := range r0s { + for _, cell := range r0.C { + if col, row, err := CellNameToCoordinates(cell.R); err == nil { + rowIdx := row - 1 + columns, colIdx := len(sheetData.Row[rowIdx].C), col-1 + for c := columns; c < col; c++ { + sheetData.Row[rowIdx].C = append(sheetData.Row[rowIdx].C, xlsxC{}) + } + if !sheetData.Row[rowIdx].C[colIdx].hasValue() { + sheetData.Row[rowIdx].C[colIdx] = cell + } } } } diff --git a/rows.go b/rows.go index 027d44cbc2..9c240570ee 100644 --- a/rows.go +++ b/rows.go @@ -45,10 +45,10 @@ import ( // fmt.Println() // } func (f *File) GetRows(sheet string, opts ...Options) ([][]string, error) { - rows, err := f.Rows(sheet) - if err != nil { + if _, err := f.workSheetReader(sheet); err != nil { return nil, err } + rows, _ := f.Rows(sheet) results, cur, max := make([][]string, 0, 64), 0, 0 for rows.Next() { cur++ @@ -368,7 +368,7 @@ func (f *File) getRowHeight(sheet string, row int) int { defer ws.mu.Unlock() for i := range ws.SheetData.Row { v := &ws.SheetData.Row[i] - if v.R == row && v.Ht != nil { + if v.R != nil && *v.R == row && v.Ht != nil { return int(convertRowHeightToPixels(*v.Ht)) } } @@ -399,7 +399,7 @@ 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 != nil { + if v.R != nil && *v.R == row && v.Ht != nil { return *v.Ht, nil } } @@ -554,7 +554,7 @@ func (f *File) RemoveRow(sheet string, row int) error { keep := 0 for rowIdx := 0; rowIdx < len(ws.SheetData.Row); rowIdx++ { v := &ws.SheetData.Row[rowIdx] - if v.R != row { + if v.R != nil && *v.R != row { ws.SheetData.Row[keep] = *v keep++ } @@ -625,7 +625,7 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) error { var rowCopy xlsxRow for i, r := range ws.SheetData.Row { - if r.R == row { + if *r.R == row { rowCopy = deepcopy.Copy(ws.SheetData.Row[i]).(xlsxRow) ok = true break @@ -642,7 +642,7 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) error { idx2 := -1 for i, r := range ws.SheetData.Row { - if r.R == row2 { + if *r.R == row2 { idx2 = i break } diff --git a/sheet.go b/sheet.go index b9201a363d..83ad73e458 100644 --- a/sheet.go +++ b/sheet.go @@ -1951,7 +1951,7 @@ func (ws *xlsxWorksheet) prepareSheetXML(col int, row int) { if rowCount < row { // append missing rows for rowIdx := rowCount; rowIdx < row; rowIdx++ { - ws.SheetData.Row = append(ws.SheetData.Row, xlsxRow{R: rowIdx + 1, CustomHeight: customHeight, Ht: ht, C: make([]xlsxC, 0, sizeHint)}) + ws.SheetData.Row = append(ws.SheetData.Row, xlsxRow{R: intPtr(rowIdx + 1), CustomHeight: customHeight, Ht: ht, C: make([]xlsxC, 0, sizeHint)}) } } rowData := &ws.SheetData.Row[row-1] diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 6eb860f330..177f1363c2 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -308,7 +308,7 @@ type xlsxSheetData struct { // particular row in the worksheet. type xlsxRow struct { C []xlsxC `xml:"c"` - R int `xml:"r,attr,omitempty"` + R *int `xml:"r,attr"` Spans string `xml:"spans,attr,omitempty"` S int `xml:"s,attr,omitempty"` CustomFormat bool `xml:"customFormat,attr,omitempty"` From 55e4d4b2c305a6c43178c19a630e54ba7b2c5446 Mon Sep 17 00:00:00 2001 From: Tian <75908403+parkoo@users.noreply.github.com> Date: Mon, 20 Nov 2023 23:57:45 +0800 Subject: [PATCH 829/957] The GetCellRichText function support to return inline rich text (#1724) Co-authored-by: jintian.wang --- cell.go | 10 ++++++++++ cell_test.go | 27 +++++++++++++++++++++------ 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/cell.go b/cell.go index 80d113ad5a..204b8dfefe 100644 --- a/cell.go +++ b/cell.go @@ -971,6 +971,9 @@ func (f *File) SetCellHyperLink(sheet, cell, link, linkType string, opts ...Hype // getCellRichText returns rich text of cell by given string item. func getCellRichText(si *xlsxSI) (runs []RichTextRun) { + if si.T != nil { + runs = append(runs, RichTextRun{Text: si.T.Val}) + } for _, v := range si.R { run := RichTextRun{ Text: v.T.Val, @@ -994,6 +997,13 @@ func (f *File) GetCellRichText(sheet, cell string) (runs []RichTextRun, err erro if err != nil { return } + if c.T == "inlineStr" && c.IS != nil { + runs = getCellRichText(c.IS) + return + } + if c.T == "" { + return + } siIdx, err := strconv.Atoi(c.V) if err != nil || c.T != "s" { return diff --git a/cell_test.go b/cell_test.go index ea802c0e12..c41bf58410 100644 --- a/cell_test.go +++ b/cell_test.go @@ -685,26 +685,41 @@ 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 with inlineStr ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") assert.True(t, ok) - ws.(*xlsxWorksheet).SheetData.Row[0].C[0].V = "2" + ws.(*xlsxWorksheet).SheetData.Row[0].C[0] = xlsxC{ + T: "inlineStr", + IS: &xlsxSI{ + T: &xlsxT{Val: "A"}, + R: []xlsxR{{T: &xlsxT{Val: "1"}}}, + }, + } + runs, err = f.GetCellRichText("Sheet1", "A1") + assert.NoError(t, err) + assert.Equal(t, []RichTextRun{{Text: "A"}, {Text: "1"}}, runs) + + // 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] = xlsxC{V: "2", IS: &xlsxSI{}} 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 ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml") assert.True(t, ok) - ws.(*xlsxWorksheet).SheetData.Row[0].C[0].V = "-1" + ws.(*xlsxWorksheet).SheetData.Row[0].C[0] = xlsxC{T: "s", V: "-1", IS: &xlsxSI{}} 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 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") + ws.(*xlsxWorksheet).SheetData.Row[0].C[0] = xlsxC{V: "x"} + runs, err = f.GetCellRichText("Sheet1", "A1") + assert.NoError(t, err) + assert.Equal(t, 0, len(runs)) // Test set cell rich text on not exists worksheet _, err = f.GetCellRichText("SheetN", "A1") assert.EqualError(t, err, "sheet SheetN does not exist") From 6251d493b3eb18d9ad4bac519ce26bba0d679e9a Mon Sep 17 00:00:00 2001 From: ZX <89002650+ZhangXiao1024@users.noreply.github.com> Date: Tue, 21 Nov 2023 11:23:54 +0800 Subject: [PATCH 830/957] Fixed invalid shared string table index on set cell value (#1725) --- cell.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cell.go b/cell.go index 204b8dfefe..1a2ef8f029 100644 --- a/cell.go +++ b/cell.go @@ -495,7 +495,7 @@ func (f *File) setSharedString(val string) (int, error) { 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 + return len(sst.SI) - 1, nil } // trimCellValue provides a function to set string type to cell. From 41259b474f9172750195c4910a847f26f6e5d9e7 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 23 Nov 2023 00:03:10 +0800 Subject: [PATCH 831/957] Recalculate and use the same shared string count and unique count to overwrite incorrect existing values --- cell.go | 6 +++--- cell_test.go | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/cell.go b/cell.go index 1a2ef8f029..80853eec94 100644 --- a/cell.go +++ b/cell.go @@ -489,13 +489,13 @@ func (f *File) setSharedString(val string) (int, error) { } sst.mu.Lock() defer sst.mu.Unlock() - sst.Count++ - sst.UniqueCount++ t := xlsxT{Val: val} val, t.Space = trimCellValue(val, false) sst.SI = append(sst.SI, xlsxSI{T: &t}) + sst.Count = len(sst.SI) + sst.UniqueCount = sst.Count f.sharedStringsMap[val] = sst.UniqueCount - 1 - return len(sst.SI) - 1, nil + return sst.UniqueCount - 1, nil } // trimCellValue provides a function to set string type to cell. diff --git a/cell_test.go b/cell_test.go index c41bf58410..c3a622ede8 100644 --- a/cell_test.go +++ b/cell_test.go @@ -260,6 +260,23 @@ func TestSetCellValue(t *testing.T) { 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") + // Test set cell value with the shared string table's count not equal with unique count + f = NewFile() + f.SharedStrings = nil + f.Pkg.Store(defaultXMLPathSharedStrings, []byte(fmt.Sprintf(`aa`, NameSpaceSpreadSheet.Value))) + f.Sheet.Store("xl/worksheets/sheet1.xml", &xlsxWorksheet{ + SheetData: xlsxSheetData{Row: []xlsxRow{ + {R: intPtr(1), C: []xlsxC{{R: "A1", T: "str", V: "1"}}}, + }}, + }) + assert.NoError(t, f.SetCellValue("Sheet1", "A1", "b")) + val, err := f.GetCellValue("Sheet1", "A1") + assert.NoError(t, err) + assert.Equal(t, "b", val) + assert.NoError(t, f.SetCellValue("Sheet1", "B1", "b")) + val, err = f.GetCellValue("Sheet1", "B1") + assert.NoError(t, err) + assert.Equal(t, "b", val) } func TestSetCellValues(t *testing.T) { From bce2789c112b6ab9beb86327170e1bd64ed29740 Mon Sep 17 00:00:00 2001 From: zcgly Date: Sat, 25 Nov 2023 02:03:33 +0800 Subject: [PATCH 832/957] This support set column style with default width in sheet props settings (#1728) --- col.go | 26 +++++++++++++++----------- col_test.go | 10 ++++++++++ 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/col.go b/col.go index c5be1b742f..dd7ffafc4e 100644 --- a/col.go +++ b/col.go @@ -290,7 +290,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 { - min, max, err := f.parseColRange(columns) + minVal, maxVal, err := f.parseColRange(columns) if err != nil { return err } @@ -301,8 +301,8 @@ func (f *File) SetColVisible(sheet, columns string, visible bool) error { ws.mu.Lock() defer ws.mu.Unlock() colData := xlsxCol{ - Min: min, - Max: max, + Min: minVal, + Max: maxVal, Width: float64Ptr(defaultColWidth), Hidden: !visible, CustomWidth: true, @@ -427,7 +427,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 { - min, max, err := f.parseColRange(columns) + minVal, maxVal, err := f.parseColRange(columns) if err != nil { return err } @@ -453,10 +453,14 @@ func (f *File) SetColStyle(sheet, columns string, styleID int) error { if ws.Cols == nil { ws.Cols = &xlsxCols{} } + width := defaultColWidth + if ws.SheetFormatPr != nil && ws.SheetFormatPr.DefaultColWidth > 0 { + width = ws.SheetFormatPr.DefaultColWidth + } ws.Cols.Col = flatCols(xlsxCol{ - Min: min, - Max: max, - Width: float64Ptr(defaultColWidth), + Min: minVal, + Max: maxVal, + Width: float64Ptr(width), Style: styleID, }, ws.Cols.Col, func(fc, c xlsxCol) xlsxCol { fc.BestFit = c.BestFit @@ -470,7 +474,7 @@ func (f *File) SetColStyle(sheet, columns string, styleID int) error { }) ws.mu.Unlock() if rows := len(ws.SheetData.Row); rows > 0 { - for col := min; col <= max; col++ { + for col := minVal; col <= maxVal; col++ { from, _ := CoordinatesToCellName(col, 1) to, _ := CoordinatesToCellName(col, rows) err = f.SetCellStyle(sheet, from, to, styleID) @@ -484,7 +488,7 @@ func (f *File) SetColStyle(sheet, columns string, styleID int) error { // // 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) + minVal, maxVal, err := f.parseColRange(startCol + ":" + endCol) if err != nil { return err } @@ -501,8 +505,8 @@ func (f *File) SetColWidth(sheet, startCol, endCol string, width float64) error ws.mu.Lock() defer ws.mu.Unlock() col := xlsxCol{ - Min: min, - Max: max, + Min: minVal, + Max: maxVal, Width: float64Ptr(width), CustomWidth: true, } diff --git a/col_test.go b/col_test.go index ff3892e4fc..6174254395 100644 --- a/col_test.go +++ b/col_test.go @@ -366,6 +366,16 @@ func TestSetColStyle(t *testing.T) { 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") + + // Test set column style with worksheet properties columns default width settings + f = NewFile() + assert.NoError(t, f.SetSheetProps("Sheet1", &SheetPropsOptions{DefaultColWidth: float64Ptr(20)})) + style, err = f.NewStyle(&Style{Alignment: &Alignment{Vertical: "center"}}) + assert.NoError(t, err) + assert.NoError(t, f.SetColStyle("Sheet1", "A:Z", style)) + width, err := f.GetColWidth("Sheet1", "B") + assert.NoError(t, err) + assert.Equal(t, 20.0, width) } func TestColWidth(t *testing.T) { From 866e7fd9e1d09e79b9ff345f240e10a72ed9f310 Mon Sep 17 00:00:00 2001 From: Bram Vanbilsen Date: Tue, 28 Nov 2023 10:13:39 -0600 Subject: [PATCH 833/957] This closes #1729, support copy conditional format and data validation on duplicate row (#1733) --- rows.go | 90 +++++++++++++++++++++++++++++++++++++++++++++++----- rows_test.go | 43 +++++++++++++++++++++++-- 2 files changed, 123 insertions(+), 10 deletions(-) diff --git a/rows.go b/rows.go index 9c240570ee..b887c96628 100644 --- a/rows.go +++ b/rows.go @@ -18,10 +18,24 @@ import ( "math" "os" "strconv" + "strings" "github.com/mohae/deepcopy" ) +// duplicateHelperFunc defines functions to duplicate helper. +var duplicateHelperFunc = [3]func(*File, *xlsxWorksheet, string, int, int) error{ + func(f *File, ws *xlsxWorksheet, sheet string, row, row2 int) error { + return f.duplicateConditionalFormat(ws, sheet, row, row2) + }, + func(f *File, ws *xlsxWorksheet, sheet string, row, row2 int) error { + return f.duplicateDataValidations(ws, sheet, row, row2) + }, + func(f *File, ws *xlsxWorksheet, sheet string, row, row2 int) error { + return f.duplicateMergeCells(ws, sheet, row, row2) + }, +} + // 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, @@ -618,7 +632,7 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) error { } if row2 < 1 || row == row2 { - return nil + return err } var ok bool @@ -637,7 +651,7 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) error { } if !ok { - return nil + return err } idx2 := -1 @@ -647,10 +661,6 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) error { break } } - if idx2 == -1 && len(ws.SheetData.Row) >= row2 { - return nil - } - rowCopy.C = append(make([]xlsxC, 0, len(rowCopy.C)), rowCopy.C...) rowCopy.adjustSingleRowDimensions(row2 - row) _ = f.adjustSingleRowFormulas(sheet, sheet, &rowCopy, row, row2-row, true) @@ -660,12 +670,76 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) error { } else { ws.SheetData.Row = append(ws.SheetData.Row, rowCopy) } - return f.duplicateMergeCells(sheet, ws, row, row2) + for _, fn := range duplicateHelperFunc { + if err := fn(f, ws, sheet, row, row2); err != nil { + return err + } + } + return err +} + +// duplicateConditionalFormat create conditional formatting for the destination +// row if there are conditional formats in the copied row. +func (f *File) duplicateConditionalFormat(ws *xlsxWorksheet, sheet string, row, row2 int) error { + var cfs []*xlsxConditionalFormatting + for _, cf := range ws.ConditionalFormatting { + if cf != nil { + if !strings.Contains(cf.SQRef, ":") { + cf.SQRef += ":" + cf.SQRef + } + abs := strings.Contains(cf.SQRef, "$") + coordinates, err := rangeRefToCoordinates(cf.SQRef) + if err != nil { + return err + } + x1, y1, x2, y2 := coordinates[0], coordinates[1], coordinates[2], coordinates[3] + if y1 == y2 && y1 == row { + cfCopy := deepcopy.Copy(*cf).(xlsxConditionalFormatting) + if cfCopy.SQRef, err = f.coordinatesToRangeRef([]int{x1, row2, x2, row2}, abs); err != nil { + return err + } + cfs = append(cfs, &cfCopy) + } + } + } + ws.ConditionalFormatting = append(ws.ConditionalFormatting, cfs...) + return nil +} + +// duplicateDataValidations create data validations for the destination row if +// there are data validation rules in the copied row. +func (f *File) duplicateDataValidations(ws *xlsxWorksheet, sheet string, row, row2 int) error { + if ws.DataValidations == nil { + return nil + } + var dvs []*xlsxDataValidation + for _, dv := range ws.DataValidations.DataValidation { + if dv != nil { + if !strings.Contains(dv.Sqref, ":") { + dv.Sqref += ":" + dv.Sqref + } + abs := strings.Contains(dv.Sqref, "$") + coordinates, err := rangeRefToCoordinates(dv.Sqref) + if err != nil { + return err + } + x1, y1, x2, y2 := coordinates[0], coordinates[1], coordinates[2], coordinates[3] + if y1 == y2 && y1 == row { + dvCopy := deepcopy.Copy(*dv).(xlsxDataValidation) + if dvCopy.Sqref, err = f.coordinatesToRangeRef([]int{x1, row2, x2, row2}, abs); err != nil { + return err + } + dvs = append(dvs, &dvCopy) + } + } + } + ws.DataValidations.DataValidation = append(ws.DataValidations.DataValidation, dvs...) + return nil } // duplicateMergeCells merge cells in the destination row if there are single // row merged cells in the copied row. -func (f *File) duplicateMergeCells(sheet string, ws *xlsxWorksheet, row, row2 int) error { +func (f *File) duplicateMergeCells(ws *xlsxWorksheet, sheet string, row, row2 int) error { if ws.MergeCells == nil { return nil } diff --git a/rows_test.go b/rows_test.go index 3e49580293..8fd0bfc5b4 100644 --- a/rows_test.go +++ b/rows_test.go @@ -878,6 +878,23 @@ func TestDuplicateRow(t *testing.T) { })) assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "Amount+C1")) assert.NoError(t, f.SetCellValue("Sheet1", "A10", "A10")) + + format, err := f.NewConditionalStyle(&Style{Font: &Font{Color: "9A0511"}, Fill: Fill{Type: "pattern", Color: []string{"FEC7CE"}, Pattern: 1}}) + assert.NoError(t, err) + + expected := []ConditionalFormatOptions{ + {Type: "cell", Criteria: "greater than", Format: format, Value: "0"}, + } + assert.NoError(t, f.SetConditionalFormat("Sheet1", "A1", expected)) + + dv := NewDataValidation(true) + dv.Sqref = "A1" + assert.NoError(t, dv.SetDropList([]string{"1", "2", "3"})) + assert.NoError(t, f.AddDataValidation("Sheet1", dv)) + ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") + assert.True(t, ok) + ws.(*xlsxWorksheet).DataValidations.DataValidation[0].Sqref = "A1" + assert.NoError(t, f.DuplicateRowTo("Sheet1", 1, 10)) formula, err := f.GetCellFormula("Sheet1", "A10") assert.NoError(t, err) @@ -885,6 +902,28 @@ func TestDuplicateRow(t *testing.T) { value, err := f.GetCellValue("Sheet1", "A11") assert.NoError(t, err) assert.Equal(t, "A10", value) + + cfs, err := f.GetConditionalFormats("Sheet1") + assert.NoError(t, err) + assert.Len(t, cfs, 2) + assert.Equal(t, expected, cfs["A10:A10"]) + + dvs, err := f.GetDataValidations("Sheet1") + assert.NoError(t, err) + assert.Len(t, dvs, 2) + assert.Equal(t, "A10:A10", dvs[1].Sqref) + + // Test duplicate data validation with row number exceeds maximum limit + assert.Equal(t, ErrMaxRows, f.duplicateDataValidations(ws.(*xlsxWorksheet), "Sheet1", 1, TotalRows+1)) + // Test duplicate data validation with invalid range reference + ws.(*xlsxWorksheet).DataValidations.DataValidation[0].Sqref = "A" + assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.duplicateDataValidations(ws.(*xlsxWorksheet), "Sheet1", 1, 10)) + + // Test duplicate conditional formatting with row number exceeds maximum limit + assert.Equal(t, ErrMaxRows, f.duplicateConditionalFormat(ws.(*xlsxWorksheet), "Sheet1", 1, TotalRows+1)) + // Test duplicate conditional formatting with invalid range reference + ws.(*xlsxWorksheet).ConditionalFormatting[0].SQRef = "A" + assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.duplicateConditionalFormat(ws.(*xlsxWorksheet), "Sheet1", 1, 10)) } func TestDuplicateRowTo(t *testing.T) { @@ -911,9 +950,9 @@ func TestDuplicateMergeCells(t *testing.T) { ws := &xlsxWorksheet{MergeCells: &xlsxMergeCells{ Cells: []*xlsxMergeCell{{Ref: "A1:-"}}, }} - assert.EqualError(t, f.duplicateMergeCells("Sheet1", ws, 0, 0), `cannot convert cell "-" to coordinates: invalid cell name "-"`) + assert.EqualError(t, f.duplicateMergeCells(ws, "Sheet1", 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 does not exist") + assert.EqualError(t, f.duplicateMergeCells(ws, "SheetN", 1, 2), "sheet SheetN does not exist") } func TestGetValueFromInlineStr(t *testing.T) { From a16182e004e75cf3ace23c11039395ea7542f2a3 Mon Sep 17 00:00:00 2001 From: user65536 <37108140+user65536@users.noreply.github.com> Date: Fri, 1 Dec 2023 00:31:41 +0800 Subject: [PATCH 834/957] This closes #1732, saving workbook with sorted internal part path (#1735) --- file.go | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/file.go b/file.go index dc29b50dfd..31a96746f4 100644 --- a/file.go +++ b/file.go @@ -18,6 +18,7 @@ import ( "io" "os" "path/filepath" + "sort" "strings" "sync" ) @@ -200,31 +201,40 @@ func (f *File) writeToZip(zw *zip.Writer) error { return err } } - var err error + var ( + err error + files, tempFiles []string + ) f.Pkg.Range(func(path, content interface{}) bool { - if err != nil { - return false - } if _, ok := f.streams[path.(string)]; ok { return true } + files = append(files, path.(string)) + return true + }) + sort.Strings(files) + for _, path := range files { var fi io.Writer - if fi, err = zw.Create(path.(string)); err != nil { - return false + if fi, err = zw.Create(path); err != nil { + break } + content, _ := f.Pkg.Load(path) _, err = fi.Write(content.([]byte)) - return true - }) + } f.tempFiles.Range(func(path, content interface{}) bool { if _, ok := f.Pkg.Load(path); ok { return true } - var fi io.Writer - if fi, err = zw.Create(path.(string)); err != nil { - return false - } - _, err = fi.Write(f.readBytes(path.(string))) + tempFiles = append(tempFiles, path.(string)) return true }) + sort.Strings(tempFiles) + for _, path := range tempFiles { + var fi io.Writer + if fi, err = zw.Create(path); err != nil { + break + } + _, err = fi.Write(f.readBytes(path)) + } return err } From 18a160c5be82f6515f7dd117a47bf58dd3466aaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=A9=E7=88=B1=E6=9C=89=E6=83=85?= Date: Sat, 2 Dec 2023 12:03:09 +0800 Subject: [PATCH 835/957] Support unset custom row height if height value is -1 (#1736) - Return error if get an invalid row height value - Update unit tests and update documentation of the SetRowHeigth function --- rows.go | 16 +++++++++++++--- rows_test.go | 16 ++++++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/rows.go b/rows.go index b887c96628..894b1ba77d 100644 --- a/rows.go +++ b/rows.go @@ -350,8 +350,10 @@ func (f *File) xmlDecoder(name string) (bool, *xml.Decoder, *os.File, error) { return true, f.xmlNewDecoder(tempFile), tempFile, err } -// SetRowHeight provides a function to set the height of a single row. For -// example, set the height of the first row in Sheet1: +// SetRowHeight provides a function to set the height of a single row. If the +// value of height is 0, will hide the specified row, if the value of height is +// -1, will unset the custom row height. For example, set the height of the +// first row in Sheet1: // // err := f.SetRowHeight("Sheet1", 1, 50) func (f *File) SetRowHeight(sheet string, row int, height float64) error { @@ -361,6 +363,9 @@ func (f *File) SetRowHeight(sheet string, row int, height float64) error { if height > MaxRowHeight { return ErrMaxRowHeight } + if height < -1 { + return ErrParameterInvalid + } ws, err := f.workSheetReader(sheet) if err != nil { return err @@ -369,9 +374,14 @@ func (f *File) SetRowHeight(sheet string, row int, height float64) error { ws.prepareSheetXML(0, row) rowIdx := row - 1 + if height == -1 { + ws.SheetData.Row[rowIdx].Ht = nil + ws.SheetData.Row[rowIdx].CustomHeight = false + return err + } ws.SheetData.Row[rowIdx].Ht = float64Ptr(height) ws.SheetData.Row[rowIdx].CustomHeight = true - return nil + return err } // getRowHeight provides a function to get row height in pixels by given sheet diff --git a/rows_test.go b/rows_test.go index 8fd0bfc5b4..88492a0dd2 100644 --- a/rows_test.go +++ b/rows_test.go @@ -1034,6 +1034,22 @@ func TestSetRowStyle(t *testing.T) { assert.EqualError(t, f.SetRowStyle("Sheet1", 1, 1, cellStyleID), "XML syntax error on line 1: invalid UTF-8") } +func TestSetRowHeight(t *testing.T) { + f := NewFile() + // Test hidden row by set row height to 0 + assert.NoError(t, f.SetRowHeight("Sheet1", 2, 0)) + ht, err := f.GetRowHeight("Sheet1", 2) + assert.NoError(t, err) + assert.Empty(t, ht) + // Test unset custom row height + assert.NoError(t, f.SetRowHeight("Sheet1", 2, -1)) + ht, err = f.GetRowHeight("Sheet1", 2) + assert.NoError(t, err) + assert.Equal(t, defaultRowHeight, ht) + // Test set row height with invalid height value + assert.Equal(t, ErrParameterInvalid, f.SetRowHeight("Sheet1", 2, -2)) +} + func TestNumberFormats(t *testing.T) { f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if !assert.NoError(t, err) { From 77ece87e325a639dc49ce1387f0c7690f6230c26 Mon Sep 17 00:00:00 2001 From: cui fliter Date: Thu, 7 Dec 2023 15:22:26 +0800 Subject: [PATCH 836/957] This fix some function names in comment (#1747) Signed-off-by: cui fliter --- cell.go | 2 +- crypt.go | 2 +- slicer.go | 2 +- styles.go | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cell.go b/cell.go index 80853eec94..0f56a40d2c 100644 --- a/cell.go +++ b/cell.go @@ -547,7 +547,7 @@ func (c *xlsxC) setStr(val string) { c.V, c.XMLSpace = trimCellValue(val, false) } -// getCellDate parse cell value which containing a boolean. +// getCellBool parse cell value which containing a boolean. func (c *xlsxC) getCellBool(f *File, raw bool) (string, error) { if !raw { if c.V == "1" { diff --git a/crypt.go b/crypt.go index 13985336c8..6bfb1330bf 100644 --- a/crypt.go +++ b/crypt.go @@ -676,7 +676,7 @@ func (c *cfb) writeUint64(value int) { c.writeBytes(buf) } -// writeBytes write strings in the stream by a given value with an offset. +// writeStrings 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)) diff --git a/slicer.go b/slicer.go index 6dd855eba6..c1afc2a8dc 100644 --- a/slicer.go +++ b/slicer.go @@ -349,7 +349,7 @@ func (f *File) genSlicerName(name string) string { return slicerName } -// genSlicerNames generates a unique slicer cache name by giving the slicer name. +// genSlicerCacheName generates a unique slicer cache name by giving the slicer name. func (f *File) genSlicerCacheName(name string) string { var ( cnt int diff --git a/styles.go b/styles.go index f58a3ceb3e..f0be38a8e2 100644 --- a/styles.go +++ b/styles.go @@ -3238,7 +3238,7 @@ func drawCondFmtErrors(p int, ct, ref, GUID string, format *ConditionalFormatOpt }, nil } -// drawCondFmtErrors provides a function to create conditional formatting rule +// drawCondFmtNoErrors provides a function to create conditional formatting rule // for cells without errors by given priority, criteria type and format settings. func drawCondFmtNoErrors(p int, ct, ref, GUID string, format *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule) { return &xlsxCfRule{ @@ -3250,7 +3250,7 @@ func drawCondFmtNoErrors(p int, ct, ref, GUID string, format *ConditionalFormatO }, nil } -// drawCondFmtErrors provides a function to create conditional formatting rule +// drawCondFmtBlanks provides a function to create conditional formatting rule // for blank cells by given priority, criteria type and format settings. func drawCondFmtBlanks(p int, ct, ref, GUID string, format *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule) { return &xlsxCfRule{ @@ -3262,7 +3262,7 @@ func drawCondFmtBlanks(p int, ct, ref, GUID string, format *ConditionalFormatOpt }, nil } -// drawCondFmtErrors provides a function to create conditional formatting rule +// drawCondFmtNoBlanks provides a function to create conditional formatting rule // for no blanks cells by given priority, criteria type and format settings. func drawCondFmtNoBlanks(p int, ct, ref, GUID string, format *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule) { return &xlsxCfRule{ From 866f3086cd6714ab9114331fb8a73b1b4a1df0a1 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 8 Dec 2023 00:09:06 +0800 Subject: [PATCH 837/957] This closes #1745, prevent panic on get conditional format without above average rules - Define internal map variable globally instead of inside of functions --- sparkline.go | 677 +++++++++++++++++++++++++-------------------------- styles.go | 334 ++++++++++++++----------- 2 files changed, 530 insertions(+), 481 deletions(-) diff --git a/sparkline.go b/sparkline.go index 810d21365c..5e3ff20228 100644 --- a/sparkline.go +++ b/sparkline.go @@ -18,345 +18,342 @@ import ( "strings" ) -// addSparklineGroupByStyle provides a function to create x14:sparklineGroups -// element by given sparkline style ID. -func (f *File) addSparklineGroupByStyle(ID int) *xlsxX14SparklineGroup { - groups := []*xlsxX14SparklineGroup{ - { - 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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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] +// sparklineGroupPresets defined the list of sparkline group to create +// x14:sparklineGroups element by given sparkline style ID. +var sparklineGroupPresets = []*xlsxX14SparklineGroup{ + { + 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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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 } // AddSparkline provides a function to add sparklines to the worksheet by @@ -415,7 +412,7 @@ func (f *File) AddSparkline(sheet string, opts *SparklineOptions) error { } sparkType = specifiedSparkTypes } - group = f.addSparklineGroupByStyle(opts.Style) + group = sparklineGroupPresets[opts.Style] group.Type = sparkType group.ColorAxis = &xlsxColor{RGB: "FF000000"} group.DisplayEmptyCellsAs = "gap" diff --git a/styles.go b/styles.go index f0be38a8e2..78c0791ef5 100644 --- a/styles.go +++ b/styles.go @@ -23,87 +23,6 @@ import ( "strings" ) -// validType defined the list of valid validation types. -var validType = map[string]string{ - "cell": "cellIs", - "average": "aboveAverage", - "duplicate": "duplicateValues", - "unique": "uniqueValues", - "top": "top10", - "bottom": "top10", - "text": "text", - "time_period": "timePeriod", - "blanks": "containsBlanks", - "no_blanks": "notContainsBlanks", - "errors": "containsErrors", - "no_errors": "notContainsErrors", - "2_color_scale": "2_color_scale", - "3_color_scale": "3_color_scale", - "data_bar": "dataBar", - "formula": "expression", - "icon_set": "iconSet", -} - -// criteriaType defined the list of valid criteria types. -var criteriaType = map[string]string{ - "!=": "notEqual", - "<": "lessThan", - "<=": "lessThanOrEqual", - "<>": "notEqual", - "=": "equal", - "==": "equal", - ">": "greaterThan", - ">=": "greaterThanOrEqual", - "begins with": "beginsWith", - "between": "between", - "containing": "containsText", - "continue month": "nextMonth", - "continue week": "nextWeek", - "ends with": "endsWith", - "equal to": "equal", - "greater than or equal to": "greaterThanOrEqual", - "greater than": "greaterThan", - "last 7 days": "last7Days", - "last month": "lastMonth", - "last week": "lastWeek", - "less than or equal to": "lessThanOrEqual", - "less than": "lessThan", - "not between": "notBetween", - "not containing": "notContains", - "not equal to": "notEqual", - "this month": "thisMonth", - "this week": "thisWeek", - "today": "today", - "tomorrow": "tomorrow", - "yesterday": "yesterday", -} - -// operatorType defined the list of valid operator types. -var operatorType = map[string]string{ - "beginsWith": "begins with", - "between": "between", - "containsText": "containing", - "endsWith": "ends with", - "equal": "equal to", - "greaterThan": "greater than", - "greaterThanOrEqual": "greater than or equal to", - "last7Days": "last 7 days", - "lastMonth": "last month", - "lastWeek": "last week", - "lessThan": "less than", - "lessThanOrEqual": "less than or equal to", - "nextMonth": "continue month", - "nextWeek": "continue week", - "notBetween": "not between", - "notContains": "not containing", - "notEqual": "not equal to", - "thisMonth": "this month", - "thisWeek": "this week", - "today": "today", - "tomorrow": "tomorrow", - "yesterday": "yesterday", -} - // stylesReader provides a function to get the pointer to the structure after // deserialization of xl/styles.xml. func (f *File) stylesReader() (*xlsxStyleSheet, error) { @@ -1262,6 +1181,140 @@ var ( "expression": extractCondFmtExp, "iconSet": extractCondFmtIconSet, } + // validType defined the list of valid validation types. + validType = map[string]string{ + "cell": "cellIs", + "average": "aboveAverage", + "duplicate": "duplicateValues", + "unique": "uniqueValues", + "top": "top10", + "bottom": "top10", + "text": "text", + "time_period": "timePeriod", + "blanks": "containsBlanks", + "no_blanks": "notContainsBlanks", + "errors": "containsErrors", + "no_errors": "notContainsErrors", + "2_color_scale": "2_color_scale", + "3_color_scale": "3_color_scale", + "data_bar": "dataBar", + "formula": "expression", + "icon_set": "iconSet", + } + // criteriaType defined the list of valid criteria types. + criteriaType = map[string]string{ + "!=": "notEqual", + "<": "lessThan", + "<=": "lessThanOrEqual", + "<>": "notEqual", + "=": "equal", + "==": "equal", + ">": "greaterThan", + ">=": "greaterThanOrEqual", + "begins with": "beginsWith", + "between": "between", + "containing": "containsText", + "continue month": "nextMonth", + "continue week": "nextWeek", + "ends with": "endsWith", + "equal to": "equal", + "greater than or equal to": "greaterThanOrEqual", + "greater than": "greaterThan", + "last 7 days": "last7Days", + "last month": "lastMonth", + "last week": "lastWeek", + "less than or equal to": "lessThanOrEqual", + "less than": "lessThan", + "not between": "notBetween", + "not containing": "notContains", + "not equal to": "notEqual", + "this month": "thisMonth", + "this week": "thisWeek", + "today": "today", + "tomorrow": "tomorrow", + "yesterday": "yesterday", + } + // operatorType defined the list of valid operator types. + operatorType = map[string]string{ + "beginsWith": "begins with", + "between": "between", + "containsText": "containing", + "endsWith": "ends with", + "equal": "equal to", + "greaterThan": "greater than", + "greaterThanOrEqual": "greater than or equal to", + "last7Days": "last 7 days", + "lastMonth": "last month", + "lastWeek": "last week", + "lessThan": "less than", + "lessThanOrEqual": "less than or equal to", + "nextMonth": "continue month", + "nextWeek": "continue week", + "notBetween": "not between", + "notContains": "not containing", + "notEqual": "not equal to", + "thisMonth": "this month", + "thisWeek": "this week", + "today": "today", + "tomorrow": "tomorrow", + "yesterday": "yesterday", + } + // cellIsCriteriaType defined the list of valid criteria types used for + // cellIs conditional formats. + cellIsCriteriaType = []string{ + "equal", + "notEqual", + "greaterThan", + "lessThan", + "greaterThanOrEqual", + "lessThanOrEqual", + "containsText", + "notContains", + "beginsWith", + "endsWith", + } + // cfvo3 defined the icon set conditional formatting rules. + cfvo3 = &xlsxCfRule{IconSet: &xlsxIconSet{Cfvo: []*xlsxCfvo{ + {Type: "percent", Val: "0"}, + {Type: "percent", Val: "33"}, + {Type: "percent", Val: "67"}, + }}} + // cfvo4 defined the icon set conditional formatting rules. + cfvo4 = &xlsxCfRule{IconSet: &xlsxIconSet{Cfvo: []*xlsxCfvo{ + {Type: "percent", Val: "0"}, + {Type: "percent", Val: "25"}, + {Type: "percent", Val: "50"}, + {Type: "percent", Val: "75"}, + }}} + // cfvo5 defined the icon set conditional formatting rules. + 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"}, + }}} + // condFmtIconSetPresets defined the list of icon set conditional formatting + // rules. + condFmtIconSetPresets = 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, + } ) // getThemeColor provides a function to convert theme color or index color to @@ -2761,7 +2814,10 @@ func (f *File) appendCfRule(ws *xlsxWorksheet, rule *xlsxX14CfRule) error { // 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, extLst *xlsxExtLst) ConditionalFormatOptions { - format := ConditionalFormatOptions{StopIfTrue: c.StopIfTrue, Type: "cell", Criteria: operatorType[c.Operator], Format: *c.DxfID} + format := ConditionalFormatOptions{StopIfTrue: c.StopIfTrue, Type: "cell", Criteria: operatorType[c.Operator]} + if c.DxfID != nil { + format.Format = *c.DxfID + } if len(c.Formula) == 2 { format.MinValue, format.MaxValue = c.Formula[0], c.Formula[1] return format @@ -2773,13 +2829,21 @@ func extractCondFmtCellIs(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOp // extractCondFmtTimePeriod provides a function to extract conditional format // settings for time period by given conditional formatting rule. func extractCondFmtTimePeriod(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { - return ConditionalFormatOptions{StopIfTrue: c.StopIfTrue, Type: "time_period", Criteria: operatorType[c.Operator], Format: *c.DxfID} + format := ConditionalFormatOptions{StopIfTrue: c.StopIfTrue, Type: "time_period", Criteria: operatorType[c.Operator]} + if c.DxfID != nil { + format.Format = *c.DxfID + } + return format } // extractCondFmtText provides a function to extract conditional format // settings for text cell values by given conditional formatting rule. func extractCondFmtText(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { - return ConditionalFormatOptions{StopIfTrue: c.StopIfTrue, Type: "text", Criteria: operatorType[c.Operator], Format: *c.DxfID, Value: c.Text} + format := ConditionalFormatOptions{StopIfTrue: c.StopIfTrue, Type: "text", Criteria: operatorType[c.Operator], Value: c.Text} + if c.DxfID != nil { + format.Format = *c.DxfID + } + return format } // extractCondFmtTop10 provides a function to extract conditional format @@ -2790,10 +2854,12 @@ func extractCondFmtTop10(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOpt StopIfTrue: c.StopIfTrue, Type: "top", Criteria: "=", - Format: *c.DxfID, Percent: c.Percent, Value: strconv.Itoa(c.Rank), } + if c.DxfID != nil { + format.Format = *c.DxfID + } if c.Bottom { format.Type = "bottom" } @@ -2804,68 +2870,88 @@ func extractCondFmtTop10(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOpt // settings for above average and below average by given conditional formatting // rule. func extractCondFmtAboveAverage(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { - return ConditionalFormatOptions{ - StopIfTrue: c.StopIfTrue, - Type: "average", - Criteria: "=", - Format: *c.DxfID, - AboveAverage: *c.AboveAverage, + format := ConditionalFormatOptions{ + StopIfTrue: c.StopIfTrue, + Type: "average", + Criteria: "=", + } + if c.DxfID != nil { + format.Format = *c.DxfID } + if c.AboveAverage != nil { + format.AboveAverage = *c.AboveAverage + } + return format } // extractCondFmtDuplicateUniqueValues provides a function to extract // conditional format settings for duplicate and unique values by given // conditional formatting rule. func extractCondFmtDuplicateUniqueValues(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { - return ConditionalFormatOptions{ + format := ConditionalFormatOptions{ StopIfTrue: c.StopIfTrue, Type: map[string]string{ "duplicateValues": "duplicate", "uniqueValues": "unique", }[c.Type], Criteria: "=", - Format: *c.DxfID, } + if c.DxfID != nil { + format.Format = *c.DxfID + } + return format } // extractCondFmtBlanks provides a function to extract conditional format // settings for blank cells by given conditional formatting rule. func extractCondFmtBlanks(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { - return ConditionalFormatOptions{ + format := ConditionalFormatOptions{ StopIfTrue: c.StopIfTrue, Type: "blanks", - Format: *c.DxfID, } + if c.DxfID != nil { + format.Format = *c.DxfID + } + return format } // extractCondFmtNoBlanks provides a function to extract conditional format // settings for no blank cells by given conditional formatting rule. func extractCondFmtNoBlanks(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { - return ConditionalFormatOptions{ + format := ConditionalFormatOptions{ StopIfTrue: c.StopIfTrue, Type: "no_blanks", - Format: *c.DxfID, } + if c.DxfID != nil { + format.Format = *c.DxfID + } + return format } // extractCondFmtErrors provides a function to extract conditional format // settings for cells with errors by given conditional formatting rule. func extractCondFmtErrors(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { - return ConditionalFormatOptions{ + format := ConditionalFormatOptions{ StopIfTrue: c.StopIfTrue, Type: "errors", - Format: *c.DxfID, } + if c.DxfID != nil { + format.Format = *c.DxfID + } + return format } // extractCondFmtNoErrors provides a function to extract conditional format // settings for cells without errors by given conditional formatting rule. func extractCondFmtNoErrors(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { - return ConditionalFormatOptions{ + format := ConditionalFormatOptions{ StopIfTrue: c.StopIfTrue, Type: "no_errors", - Format: *c.DxfID, } + if c.DxfID != nil { + format.Format = *c.DxfID + } + return format } // extractCondFmtColorScale provides a function to extract conditional format @@ -2960,7 +3046,10 @@ func extractCondFmtDataBar(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatO // extractCondFmtExp provides a function to extract conditional format settings // for expression by given conditional formatting rule. func extractCondFmtExp(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { - format := ConditionalFormatOptions{StopIfTrue: c.StopIfTrue, Type: "formula", Format: *c.DxfID} + format := ConditionalFormatOptions{StopIfTrue: c.StopIfTrue, Type: "formula"} + if c.DxfID != nil { + format.Format = *c.DxfID + } if len(c.Formula) > 0 { format.Criteria = c.Formula[0] } @@ -3032,7 +3121,7 @@ func drawCondFmtCellIs(p int, ct, ref, GUID string, format *ConditionalFormatOpt if ct == "between" || ct == "notBetween" { c.Formula = append(c.Formula, []string{format.MinValue, format.MaxValue}...) } - if inStrSlice([]string{"equal", "notEqual", "greaterThan", "lessThan", "greaterThanOrEqual", "lessThanOrEqual", "containsText", "notContains", "beginsWith", "endsWith"}, ct, true) != -1 { + if inStrSlice(cellIsCriteriaType, ct, true) != -1 { c.Formula = append(c.Formula, format.Value) } return c, nil @@ -3277,44 +3366,7 @@ func drawCondFmtNoBlanks(p int, ct, ref, GUID string, format *ConditionalFormatO // 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, ref, 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] + cfRule, ok := condFmtIconSetPresets[format.IconStyle] if !ok { return nil, nil } From 7b3dd03947bd5d4702f822b3611a09b506fb257b Mon Sep 17 00:00:00 2001 From: cui fliter Date: Sat, 9 Dec 2023 12:08:29 +0800 Subject: [PATCH 838/957] Remove unused exported struct ShapeColor (#1746) Signed-off-by: cui fliter --- xmlDrawing.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/xmlDrawing.go b/xmlDrawing.go index 26a8e2f149..5770ac8224 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -443,13 +443,6 @@ type Shape struct { Paragraph []RichTextRun } -// ShapeColor directly maps the color settings of the shape. -type ShapeColor struct { - Line string - Fill string - Effect string -} - // ShapeLine directly maps the line settings of the shape. type ShapeLine struct { Color string From 284345e47111ddf389525ba8ce16d852261e9299 Mon Sep 17 00:00:00 2001 From: Xuesong <42825823+CooolNv@users.noreply.github.com> Date: Wed, 13 Dec 2023 09:22:41 +0800 Subject: [PATCH 839/957] This closes #1749, fix incorrect adjust merged cells on remove rows (#1753) --- adjust.go | 6 ++++-- rows_test.go | 9 +++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/adjust.go b/adjust.go index b5a99c5316..74d7a36ad1 100644 --- a/adjust.go +++ b/adjust.go @@ -618,6 +618,7 @@ func (f *File) adjustMergeCells(ws *xlsxWorksheet, sheet string, dir adjustDirec if dir == rows { if y1 == num && y2 == num && offset < 0 { f.deleteMergeCell(ws, i) + i-- continue } @@ -625,6 +626,7 @@ func (f *File) adjustMergeCells(ws *xlsxWorksheet, sheet string, dir adjustDirec } else { if x1 == num && x2 == num && offset < 0 { f.deleteMergeCell(ws, i) + i-- continue } @@ -644,8 +646,8 @@ func (f *File) adjustMergeCells(ws *xlsxWorksheet, sheet string, dir adjustDirec } // adjustMergeCellsHelper provides a function for adjusting merge cells to -// compare and calculate cell reference by the given pivot, operation reference and -// offset. +// 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 { p1, p2 = p2, p1 diff --git a/rows_test.go b/rows_test.go index 88492a0dd2..cd72e745dd 100644 --- a/rows_test.go +++ b/rows_test.go @@ -353,6 +353,15 @@ func TestRemoveRow(t *testing.T) { assert.NoError(t, f.RemoveRow(sheet1, 10)) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestRemoveRow.xlsx"))) + f = NewFile() + assert.NoError(t, f.MergeCell("Sheet1", "A1", "C1")) + assert.NoError(t, f.MergeCell("Sheet1", "A2", "C2")) + assert.NoError(t, f.RemoveRow("Sheet1", 1)) + mergedCells, err := f.GetMergeCells("Sheet1") + assert.NoError(t, err) + assert.Equal(t, "A1", mergedCells[0].GetStartAxis()) + assert.Equal(t, "C1", mergedCells[0].GetEndAxis()) + // Test remove row on not exist worksheet assert.EqualError(t, f.RemoveRow("SheetN", 1), "sheet SheetN does not exist") // Test remove row with invalid sheet name From dfaf418f340f260c5005e8343135cd6af60b8e58 Mon Sep 17 00:00:00 2001 From: yuegu520 <153715491+yuegu520@users.noreply.github.com> Date: Thu, 14 Dec 2023 00:03:53 +0800 Subject: [PATCH 840/957] This closes # 1704, support set the data labels position for the chart (#1755) - Breaking change: remove the Sizes field in the ChartSeries data type - Add new field DataLabelPosition in the ChartSeries data type, support to sets the position of the chart series data label - Add new field BubbleSize in the Chart data type, support set the bubble size in all data series for the bubble chart or 3D bubble chart - Add new exported ChartDataLabelPositionType data type - Update docs and unit test for the AddChart function - Fix a v2.7.1 regression bug, the bubble is hidden in the bubble or 3D bubble chart, commit ID: c2d6707a850bdc7dbb32f68481b4b266b9cf7367 --- chart.go | 21 +++++++++++++++------ chart_test.go | 18 +++++++++--------- drawing.go | 30 +++++++++++++++++++++++++----- templates.go | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ xmlChart.go | 32 +++++++++++++++++--------------- 5 files changed, 114 insertions(+), 35 deletions(-) diff --git a/chart.go b/chart.go index 5b7a5a29ff..ffc04567b2 100644 --- a/chart.go +++ b/chart.go @@ -688,11 +688,11 @@ func (opts *Chart) parseTitle() { // // Name // Categories -// Sizes // Values // Fill // Line // Marker +// DataLabelPosition // // 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 @@ -703,8 +703,6 @@ func (opts *Chart) parseTitle() { // 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. @@ -733,6 +731,8 @@ func (opts *Chart) parseTitle() { // x // auto // +// DataLabelPosition: This sets the position of the chart series data label. +// // Set properties of the chart legend. The options that can be set are: // // Position @@ -776,11 +776,11 @@ func (opts *Chart) parseTitle() { // 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, +// 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: // // SecondPlotValues // ShowBubbleSize @@ -891,6 +891,15 @@ func (opts *Chart) parseTitle() { // Set chart size by 'Dimension' property. The 'Dimension' property is optional. // The default width is 480, and height is 260. // +// Set the bubble size in all data series for the bubble chart or 3D bubble +// chart by 'BubbleSizes' property. The 'BubbleSizes' property is optional. +// The default width is 100, and the value should be great than 0 and less or +// equal than 300. +// +// Set the doughnut hole size in all data series for the doughnut chart by +// 'HoleSize' property. The 'HoleSize' property is optional. The default width +// is 75, and the value should be great than 0 and less or equal than 90. +// // 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: diff --git a/chart_test.go b/chart_test.go index a988d3f6f5..350852eb4e 100644 --- a/chart_test.go +++ b/chart_test.go @@ -176,14 +176,14 @@ func TestAddChart(t *testing.T) { } 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"}, + {Name: "Sheet1!$A$30", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$30:$D$30", DataLabelPosition: ChartDataLabelsPositionAbove}, + {Name: "Sheet1!$A$31", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$31:$D$31", DataLabelPosition: ChartDataLabelsPositionLeft}, + {Name: "Sheet1!$A$32", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$32:$D$32", DataLabelPosition: ChartDataLabelsPositionBestFit}, + {Name: "Sheet1!$A$33", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$33:$D$33", DataLabelPosition: ChartDataLabelsPositionCenter}, + {Name: "Sheet1!$A$34", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$34:$D$34", DataLabelPosition: ChartDataLabelsPositionInsideBase}, + {Name: "Sheet1!$A$35", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$35:$D$35", DataLabelPosition: ChartDataLabelsPositionInsideEnd}, + {Name: "Sheet1!$A$36", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$36:$D$36", DataLabelPosition: ChartDataLabelsPositionOutsideEnd}, + {Name: "Sheet1!$A$37", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$37:$D$37", DataLabelPosition: ChartDataLabelsPositionRight}, } format := GraphicOptions{ ScaleX: defaultDrawingScale, @@ -265,7 +265,7 @@ func TestAddChart(t *testing.T) { {sheetName: "Sheet2", cell: "AV32", opts: &Chart{Type: Contour, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "Contour Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet2", cell: "BD1", opts: &Chart{Type: WireframeContour, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "Wireframe Contour Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, // bubble chart - {sheetName: "Sheet2", cell: "BD16", opts: &Chart{Type: Bubble, Series: series4, Format: format, Legend: legend, Title: []RichTextRun{{Text: "Bubble Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet2", cell: "BD16", opts: &Chart{Type: Bubble, Series: series4, Format: format, Legend: legend, Title: []RichTextRun{{Text: "Bubble Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", BubbleSize: 75}}, {sheetName: "Sheet2", cell: "BD32", opts: &Chart{Type: Bubble3D, Series: series4, Format: format, Legend: legend, Title: []RichTextRun{{Text: "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: []RichTextRun{{Text: "Pie of Pie Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true}, YAxis: ChartAxis{MajorGridLines: true}}}, diff --git a/drawing.go b/drawing.go index a65c88559d..43e33128f0 100644 --- a/drawing.go +++ b/drawing.go @@ -659,6 +659,9 @@ func (f *File) drawBubbleChart(opts *Chart) *cPlotArea { }, ValAx: []*cAxs{f.drawPlotAreaCatAx(opts)[0], f.drawPlotAreaValAx(opts)[0]}, } + if opts.BubbleSize > 0 && opts.BubbleSize <= 300 { + plotArea.BubbleChart.BubbleScale = &attrValFloat{Val: float64Ptr(float64(opts.BubbleSize))} + } return plotArea } @@ -710,7 +713,7 @@ func (f *File) drawChartSeries(opts *Chart) *[]cSer { SpPr: f.drawChartSeriesSpPr(k, opts), Marker: f.drawChartSeriesMarker(k, opts), DPt: f.drawChartSeriesDPt(k, opts), - DLbls: f.drawChartSeriesDLbls(opts), + DLbls: f.drawChartSeriesDLbls(k, opts), InvertIfNegative: &attrValBool{Val: boolPtr(false)}, Cat: f.drawChartSeriesCat(opts.Series[k], opts), Smooth: &attrValBool{Val: boolPtr(opts.Series[k].Line.Smooth)}, @@ -885,12 +888,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[ChartType]bool{Bubble: true, Bubble3D: true}[opts.Type]; !ok || v.Sizes == "" { + if _, ok := map[ChartType]bool{Bubble: true, Bubble3D: true}[opts.Type]; !ok { return nil } return &cVal{ NumRef: &cNumRef{ - F: v.Sizes, + F: v.Values, }, } } @@ -932,16 +935,33 @@ func (f *File) drawChartDLbls(opts *Chart) *cDLbls { } } +// inSupportedChartDataLabelsPositionType provides a method to check if an +// element is present in an array, and return the index of its location, +// otherwise return -1. +func inSupportedChartDataLabelsPositionType(a []ChartDataLabelPositionType, x ChartDataLabelPositionType) int { + for idx, n := range a { + if x == n { + return idx + } + } + return -1 +} + // drawChartSeriesDLbls provides a function to draw the c:dLbls element by // given format sets. -func (f *File) drawChartSeriesDLbls(opts *Chart) *cDLbls { +func (f *File) drawChartSeriesDLbls(i int, opts *Chart) *cDLbls { dLbls := f.drawChartDLbls(opts) chartSeriesDLbls := map[ChartType]*cDLbls{ - Scatter: nil, Surface3D: nil, WireframeSurface3D: nil, Contour: nil, WireframeContour: nil, Bubble: nil, Bubble3D: nil, + Scatter: nil, Surface3D: nil, WireframeSurface3D: nil, Contour: nil, WireframeContour: nil, } if _, ok := chartSeriesDLbls[opts.Type]; ok { return nil } + if types, ok := supportedChartDataLabelsPosition[opts.Type]; ok && opts.Series[i].DataLabelPosition != ChartDataLabelsPositionUnset { + if inSupportedChartDataLabelsPositionType(types, opts.Series[i].DataLabelPosition) != -1 { + dLbls.DLblPos = &attrValString{Val: stringPtr(chartDataLabelsPositionTypes[opts.Series[i].DataLabelPosition])} + } + } return dLbls } diff --git a/templates.go b/templates.go index 41a107c474..3861f6b154 100644 --- a/templates.go +++ b/templates.go @@ -217,6 +217,54 @@ const ( ColorMappingTypeUnset int = -1 ) +// ChartDataLabelPositionType is the type of chart data labels position. +type ChartDataLabelPositionType byte + +// Chart data labels positions types enumeration. +const ( + ChartDataLabelsPositionUnset ChartDataLabelPositionType = iota + ChartDataLabelsPositionBestFit + ChartDataLabelsPositionBelow + ChartDataLabelsPositionCenter + ChartDataLabelsPositionInsideBase + ChartDataLabelsPositionInsideEnd + ChartDataLabelsPositionLeft + ChartDataLabelsPositionOutsideEnd + ChartDataLabelsPositionRight + ChartDataLabelsPositionAbove +) + +// chartDataLabelsPositionTypes defined supported chart data labels position +// types. +var chartDataLabelsPositionTypes = map[ChartDataLabelPositionType]string{ + ChartDataLabelsPositionBestFit: "bestFit", + ChartDataLabelsPositionBelow: "b", + ChartDataLabelsPositionCenter: "ctr", + ChartDataLabelsPositionInsideBase: "inBase", + ChartDataLabelsPositionInsideEnd: "inEnd", + ChartDataLabelsPositionLeft: "l", + ChartDataLabelsPositionOutsideEnd: "outEnd", + ChartDataLabelsPositionRight: "r", + ChartDataLabelsPositionAbove: "t", +} + +// supportedChartDataLabelsPosition defined supported chart data labels position +// types for each type of chart. +var supportedChartDataLabelsPosition = map[ChartType][]ChartDataLabelPositionType{ + Bar: {ChartDataLabelsPositionCenter, ChartDataLabelsPositionInsideBase, ChartDataLabelsPositionInsideEnd, ChartDataLabelsPositionOutsideEnd}, + BarStacked: {ChartDataLabelsPositionCenter, ChartDataLabelsPositionInsideBase, ChartDataLabelsPositionInsideEnd}, + BarPercentStacked: {ChartDataLabelsPositionCenter, ChartDataLabelsPositionInsideBase, ChartDataLabelsPositionInsideEnd}, + Col: {ChartDataLabelsPositionCenter, ChartDataLabelsPositionInsideBase, ChartDataLabelsPositionInsideEnd, ChartDataLabelsPositionOutsideEnd}, + ColStacked: {ChartDataLabelsPositionCenter, ChartDataLabelsPositionInsideBase, ChartDataLabelsPositionInsideEnd}, + ColPercentStacked: {ChartDataLabelsPositionCenter, ChartDataLabelsPositionInsideBase, ChartDataLabelsPositionInsideEnd}, + Line: {ChartDataLabelsPositionBelow, ChartDataLabelsPositionCenter, ChartDataLabelsPositionLeft, ChartDataLabelsPositionRight, ChartDataLabelsPositionAbove}, + Pie: {ChartDataLabelsPositionBestFit, ChartDataLabelsPositionCenter, ChartDataLabelsPositionInsideEnd, ChartDataLabelsPositionOutsideEnd}, + Pie3D: {ChartDataLabelsPositionBestFit, ChartDataLabelsPositionCenter, ChartDataLabelsPositionInsideEnd, ChartDataLabelsPositionOutsideEnd}, + Scatter: {ChartDataLabelsPositionBelow, ChartDataLabelsPositionCenter, ChartDataLabelsPositionLeft, ChartDataLabelsPositionRight, ChartDataLabelsPositionAbove}, + Bubble: {ChartDataLabelsPositionBelow, ChartDataLabelsPositionCenter, ChartDataLabelsPositionLeft, ChartDataLabelsPositionRight, ChartDataLabelsPositionAbove}, + Bubble3D: {ChartDataLabelsPositionBelow, ChartDataLabelsPositionCenter, ChartDataLabelsPositionLeft, ChartDataLabelsPositionRight, ChartDataLabelsPositionAbove}, +} + const ( defaultTempFileSST = "sharedStrings" defaultXMLPathCalcChain = "xl/calcChain.xml" diff --git a/xmlChart.go b/xmlChart.go index 93dd857843..5e209303b6 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -482,14 +482,15 @@ 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"` - ShowSerName *attrValBool `xml:"showSerName"` - ShowPercent *attrValBool `xml:"showPercent"` - ShowBubbleSize *attrValBool `xml:"showBubbleSize"` - ShowLeaderLines *attrValBool `xml:"showLeaderLines"` + NumFmt *cNumFmt `xml:"numFmt"` + DLblPos *attrValString `xml:"dLblPos"` + ShowLegendKey *attrValBool `xml:"showLegendKey"` + ShowVal *attrValBool `xml:"showVal"` + ShowCatName *attrValBool `xml:"showCatName"` + ShowSerName *attrValBool `xml:"showSerName"` + ShowPercent *attrValBool `xml:"showPercent"` + ShowBubbleSize *attrValBool `xml:"showBubbleSize"` + ShowLeaderLines *attrValBool `xml:"showLeaderLines"` } // cLegend (Legend) directly maps the legend element. This element specifies @@ -577,6 +578,7 @@ type Chart struct { PlotArea ChartPlotArea Border ChartLine ShowBlanksAs string + BubbleSize int HoleSize int order int } @@ -602,11 +604,11 @@ type ChartLine struct { // ChartSeries directly maps the format settings of the chart series. type ChartSeries struct { - Name string - Categories string - Sizes string - Values string - Fill Fill - Line ChartLine - Marker ChartMarker + Name string + Categories string + Values string + Fill Fill + Line ChartLine + Marker ChartMarker + DataLabelPosition ChartDataLabelPositionType } From 00d62590f4791013aca4a27f4d2f1ddc8546a09a Mon Sep 17 00:00:00 2001 From: li Date: Fri, 15 Dec 2023 13:09:42 +0800 Subject: [PATCH 841/957] This closes #664, support get embedded cell images (#1759) Co-authored-by: liying05 --- calc.go | 11 ++++++++ calc_test.go | 4 +++ chart.go | 10 +++---- excelize.go | 1 + picture.go | 66 +++++++++++++++++++++++++++++++++++++++++++-- picture_test.go | 40 +++++++++++++++++++++++++++ templates.go | 24 +++++++++-------- xmlDecodeDrawing.go | 14 +++++++++- 8 files changed, 151 insertions(+), 19 deletions(-) diff --git a/calc.go b/calc.go index a5dbd72b38..8fd207a3d3 100644 --- a/calc.go +++ b/calc.go @@ -18640,3 +18640,14 @@ func (fn *formulaFuncs) DVAR(argsList *list.List) formulaArg { func (fn *formulaFuncs) DVARP(argsList *list.List) formulaArg { return fn.database("DVARP", argsList) } + +// DISPIMG function calculates the Kingsoft WPS Office embedded image ID. The +// syntax of the function is: +// +// DISPIMG(picture_name,display_mode) +func (fn *formulaFuncs) DISPIMG(argsList *list.List) formulaArg { + if argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, "DISPIMG requires 2 numeric arguments") + } + return argsList.Front().Value.(formulaArg) +} diff --git a/calc_test.go b/calc_test.go index 71f3396406..a3a6a8313e 100644 --- a/calc_test.go +++ b/calc_test.go @@ -2236,6 +2236,8 @@ func TestCalcCellValue(t *testing.T) { // YIELDMAT "=YIELDMAT(\"01/01/2017\",\"06/30/2018\",\"06/01/2014\",5.5%,101)": "0.0419422478838651", "=YIELDMAT(\"01/01/2017\",\"06/30/2018\",\"06/01/2014\",5.5%,101,0)": "0.0419422478838651", + // DISPIMG + "=_xlfn.DISPIMG(\"ID_********************************\",1)": "ID_********************************", } for formula, expected := range mathCalc { f := prepareCalcData(cellData) @@ -4609,6 +4611,8 @@ func TestCalcCellValue(t *testing.T) { "=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"}, + // DISPIMG + "=_xlfn.DISPIMG()": {"#VALUE!", "DISPIMG requires 2 numeric arguments"}, } for formula, expected := range mathCalcError { f := prepareCalcData(cellData) diff --git a/chart.go b/chart.go index ffc04567b2..8296826eda 100644 --- a/chart.go +++ b/chart.go @@ -892,9 +892,9 @@ func (opts *Chart) parseTitle() { // The default width is 480, and height is 260. // // Set the bubble size in all data series for the bubble chart or 3D bubble -// chart by 'BubbleSizes' property. The 'BubbleSizes' property is optional. -// The default width is 100, and the value should be great than 0 and less or -// equal than 300. +// chart by 'BubbleSizes' property. The 'BubbleSizes' property is optional. The +// default width is 100, and the value should be great than 0 and less or equal +// than 300. // // Set the doughnut hole size in all data series for the doughnut chart by // 'HoleSize' property. The 'HoleSize' property is optional. The default width @@ -932,7 +932,7 @@ func (opts *Chart) parseTitle() { // } // enable, disable := true, false // if err := f.AddChart("Sheet1", "E1", &excelize.Chart{ -// Type: "col", +// Type: excelize.Col, // Series: []excelize.ChartSeries{ // { // Name: "Sheet1!$A$2", @@ -966,7 +966,7 @@ func (opts *Chart) parseTitle() { // ShowVal: true, // }, // }, &excelize.Chart{ -// Type: "line", +// Type: excelize.Line, // Series: []excelize.ChartSeries{ // { // Name: "Sheet1!$A$4", diff --git a/excelize.go b/excelize.go index 0b85760bda..b7dd50869e 100644 --- a/excelize.go +++ b/excelize.go @@ -43,6 +43,7 @@ type File struct { Comments map[string]*xlsxComments ContentTypes *xlsxTypes DecodeVMLDrawing map[string]*decodeVmlDrawing + DecodeCellImages *decodeCellImages Drawings sync.Map Path string Pkg sync.Map diff --git a/picture.go b/picture.go index 9411f07524..1c0c8c7a82 100644 --- a/picture.go +++ b/picture.go @@ -15,6 +15,7 @@ import ( "bytes" "encoding/xml" "image" + "io" "os" "path" "path/filepath" @@ -467,14 +468,22 @@ func (f *File) GetPictures(sheet, cell string) ([]Picture, error) { } f.mu.Unlock() if ws.Drawing == nil { - return nil, err + return f.getCellImages(sheet, cell) } target := f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID) drawingXML := strings.TrimPrefix(strings.ReplaceAll(target, "..", "xl"), "/") drawingRelationships := strings.ReplaceAll( strings.ReplaceAll(target, "../drawings", "xl/drawings/_rels"), ".xml", ".xml.rels") - return f.getPicture(row, col, drawingXML, drawingRelationships) + imgs, err := f.getCellImages(sheet, cell) + if err != nil { + return nil, err + } + pics, err := f.getPicture(row, col, drawingXML, drawingRelationships) + if err != nil { + return nil, err + } + return append(imgs, pics...), err } // GetPictureCells returns all picture cell references in a worksheet by a @@ -741,3 +750,56 @@ func (f *File) getPictureCells(drawingXML, drawingRelationships string) ([]strin } return cells, err } + +// cellImagesReader provides a function to get the pointer to the structure +// after deserialization of xl/cellimages.xml. +func (f *File) cellImagesReader() (*decodeCellImages, error) { + if f.DecodeCellImages == nil { + f.DecodeCellImages = new(decodeCellImages) + if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathCellImages)))). + Decode(f.DecodeCellImages); err != nil && err != io.EOF { + return f.DecodeCellImages, err + } + } + return f.DecodeCellImages, nil +} + +// getCellImages provides a function to get the Kingsoft WPS Office embedded +// cell images by given worksheet name and cell reference. +func (f *File) getCellImages(sheet, cell string) ([]Picture, error) { + formula, err := f.GetCellFormula(sheet, cell) + if err != nil { + return nil, err + } + if !strings.HasPrefix(strings.TrimPrefix(strings.TrimPrefix(formula, "="), "_xlfn."), "DISPIMG") { + return nil, err + } + imgID, err := f.CalcCellValue(sheet, cell) + if err != nil { + return nil, err + } + cellImages, err := f.cellImagesReader() + if err != nil { + return nil, err + } + rels, err := f.relsReader(defaultXMLPathCellImagesRels) + if rels == nil { + return nil, err + } + var pics []Picture + for _, cellImg := range cellImages.CellImage { + if cellImg.Pic.NvPicPr.CNvPr.Name == imgID { + for _, r := range rels.Relationships { + if r.ID == cellImg.Pic.BlipFill.Blip.Embed { + pic := Picture{Extension: filepath.Ext(r.Target), Format: &GraphicOptions{}} + if buffer, _ := f.Pkg.Load("xl/" + r.Target); buffer != nil { + pic.File = buffer.([]byte) + pic.Format.AltText = cellImg.Pic.NvPicPr.CNvPr.Descr + pics = append(pics, pic) + } + } + } + } + } + return pics, err +} diff --git a/picture_test.go b/picture_test.go index b98941f4ad..3573f452f2 100644 --- a/picture_test.go +++ b/picture_test.go @@ -216,6 +216,7 @@ func TestGetPicture(t *testing.T) { cells, err := f.GetPictureCells("Sheet2") assert.NoError(t, err) assert.Equal(t, []string{"K16"}, cells) + assert.NoError(t, f.Close()) // Test get picture from none drawing worksheet f = NewFile() @@ -229,11 +230,41 @@ func TestGetPicture(t *testing.T) { path := "xl/drawings/drawing1.xml" f.Drawings.Delete(path) f.Pkg.Store(path, MacintoshCyrillicCharset) + _, err = f.GetPictures("Sheet1", "F21") + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") _, 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") + assert.NoError(t, f.Close()) + + // Test get embedded cell pictures + f, err = OpenFile(filepath.Join("test", "TestGetPicture.xlsx")) + assert.NoError(t, err) + assert.NoError(t, f.SetCellFormula("Sheet1", "F21", "=_xlfn.DISPIMG(\"ID_********************************\",1)")) + f.Pkg.Store(defaultXMLPathCellImages, []byte(``)) + f.Pkg.Store(defaultXMLPathCellImagesRels, []byte(``)) + pics, err = f.GetPictures("Sheet1", "F21") + assert.NoError(t, err) + assert.Len(t, pics, 2) + assert.Equal(t, "CellImage1", pics[0].Format.AltText) + + // Test get embedded cell pictures with invalid formula + assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "=_xlfn.DISPIMG()")) + _, err = f.GetPictures("Sheet1", "A1") + assert.EqualError(t, err, "DISPIMG requires 2 numeric arguments") + + // Test get embedded cell pictures with unsupported charset + f.Relationships.Delete(defaultXMLPathCellImagesRels) + f.Pkg.Store(defaultXMLPathCellImagesRels, MacintoshCyrillicCharset) + _, err = f.GetPictures("Sheet1", "F21") + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") + f.Pkg.Store(defaultXMLPathCellImages, MacintoshCyrillicCharset) + f.DecodeCellImages = nil + _, err = f.GetPictures("Sheet1", "F21") + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") + assert.NoError(t, f.Close()) } func TestAddDrawingPicture(t *testing.T) { @@ -394,3 +425,12 @@ func TestExtractDecodeCellAnchor(t *testing.T) { cb := func(a *decodeCellAnchor, r *xlsxRelationship) {} f.extractDecodeCellAnchor(&xdrCellAnchor{GraphicFrame: string(MacintoshCyrillicCharset)}, "", cond, cb) } + +func TestGetCellImages(t *testing.T) { + f := NewFile() + f.Sheet.Delete("xl/worksheets/sheet1.xml") + f.Pkg.Store("xl/worksheets/sheet1.xml", MacintoshCyrillicCharset) + _, err := f.getCellImages("Sheet1", "A1") + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") + assert.NoError(t, f.Close()) +} diff --git a/templates.go b/templates.go index 3861f6b154..43d6df4c13 100644 --- a/templates.go +++ b/templates.go @@ -266,17 +266,19 @@ var supportedChartDataLabelsPosition = map[ChartType][]ChartDataLabelPositionTyp } const ( - defaultTempFileSST = "sharedStrings" - defaultXMLPathCalcChain = "xl/calcChain.xml" - defaultXMLPathContentTypes = "[Content_Types].xml" - defaultXMLPathDocPropsApp = "docProps/app.xml" - defaultXMLPathDocPropsCore = "docProps/core.xml" - defaultXMLPathSharedStrings = "xl/sharedStrings.xml" - defaultXMLPathStyles = "xl/styles.xml" - defaultXMLPathTheme = "xl/theme/theme1.xml" - defaultXMLPathVolatileDeps = "xl/volatileDependencies.xml" - defaultXMLPathWorkbook = "xl/workbook.xml" - defaultXMLPathWorkbookRels = "xl/_rels/workbook.xml.rels" + defaultTempFileSST = "sharedStrings" + defaultXMLPathCalcChain = "xl/calcChain.xml" + defaultXMLPathCellImages = "xl/cellimages.xml" + defaultXMLPathCellImagesRels = "xl/_rels/cellimages.xml.rels" + defaultXMLPathContentTypes = "[Content_Types].xml" + defaultXMLPathDocPropsApp = "docProps/app.xml" + defaultXMLPathDocPropsCore = "docProps/core.xml" + defaultXMLPathSharedStrings = "xl/sharedStrings.xml" + defaultXMLPathStyles = "xl/styles.xml" + defaultXMLPathTheme = "xl/theme/theme1.xml" + defaultXMLPathVolatileDeps = "xl/volatileDependencies.xml" + defaultXMLPathWorkbook = "xl/workbook.xml" + defaultXMLPathWorkbookRels = "xl/_rels/workbook.xml.rels" ) // IndexedColorMapping is the table of default mappings from indexed color value diff --git a/xmlDecodeDrawing.go b/xmlDecodeDrawing.go index fb4ea07bd8..1473817af7 100644 --- a/xmlDecodeDrawing.go +++ b/xmlDecodeDrawing.go @@ -83,7 +83,7 @@ type decodeCNvSpPr struct { // changed after serialization and deserialization, two different structures // are defined. decodeWsDr just for deserialization. type decodeWsDr struct { - XMLName xml.Name `xml:"http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing wsDr,omitempty"` + XMLName xml.Name `xml:"http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing wsDr"` A string `xml:"xmlns a,attr"` Xdr string `xml:"xmlns xdr,attr"` R string `xml:"xmlns r,attr"` @@ -242,3 +242,15 @@ type decodeClientData struct { FLocksWithSheet bool `xml:"fLocksWithSheet,attr"` FPrintsWithSheet bool `xml:"fPrintsWithSheet,attr"` } + +// decodeCellImages directly maps the Kingsoft WPS Office embedded cell images. +type decodeCellImages struct { + XMLName xml.Name `xml:"http://www.wps.cn/officeDocument/2017/etCustomData cellImages"` + CellImage []decodeCellImage `xml:"cellImage"` +} + +// decodeCellImage defines the structure used to deserialize the Kingsoft WPS +// Office embedded cell images. +type decodeCellImage struct { + Pic decodePic `xml:"pic"` +} From 37e2d946be6e2dfe9778b0261004f448bcdb0067 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 19 Dec 2023 23:39:45 +0800 Subject: [PATCH 842/957] Breaking changes: Go 1.18 and later required - This made the GetPictureCell function support get embedded cell images - Update dependencies module - Update GitHub workflow - Update documentation for the AddChart function --- .github/workflows/go.yml | 2 +- README.md | 2 +- README_zh.md | 2 +- chart.go | 5 +++ go.mod | 22 +++++++++----- go.sum | 66 ++++++++-------------------------------- picture.go | 38 +++++++++++++++++++++-- picture_test.go | 24 +++++++++++++++ 8 files changed, 93 insertions(+), 68 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 75b6172c3c..e799c2624e 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, 1.20.x, '>=1.21.1'] + go-version: [1.18.x, 1.19.x, 1.20.x, '>=1.21.1'] os: [ubuntu-latest, macos-latest, windows-latest] targetplatform: [x86, x64] diff --git a/README.md b/README.md index 8eea55f0f1..3f9e4e0dc2 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.16 or later. There are some [incompatible changes](https://github.com/golang/go/issues/61881) in the Go 1.21.0, the Excelize library can not working with that version normally, if you are using the Go 1.21.x, please upgrade to the Go 1.21.1 and later version. 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/). +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.18 or later. There are some [incompatible changes](https://github.com/golang/go/issues/61881) in the Go 1.21.0, the Excelize library can not working with that version normally, if you are using the Go 1.21.x, please upgrade to the Go 1.21.1 and later version. 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 5f82bcd6a1..6d1803a6e6 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 等多种文档格式,高度兼容带有样式、图片(表)、透视表、切片器等复杂组件的文档,并提供流式读写函数,用于处理包含大规模数据的工作簿。可应用于各类报表平台、云计算、边缘计算等系统。使用本类库要求使用的 Go 语言为 1.16 或更高版本,请注意,Go 1.21.0 中存在[不兼容的更改](https://github.com/golang/go/issues/61881),导致 Excelize 基础库无法在该版本上正常工作,如果您使用的是 Go 1.21.x,请升级到 Go 1.21.1 及更高版本。完整的使用文档请访问 [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.18 或更高版本,请注意,Go 1.21.0 中存在[不兼容的更改](https://github.com/golang/go/issues/61881),导致 Excelize 基础库无法在该版本上正常工作,如果您使用的是 Go 1.21.x,请升级到 Go 1.21.1 及更高版本。完整的使用文档请访问 [go.dev](https://pkg.go.dev/github.com/xuri/excelize/v2) 或查看 [参考文档](https://xuri.me/excelize/)。 ## 快速上手 diff --git a/chart.go b/chart.go index 8296826eda..d054841adb 100644 --- a/chart.go +++ b/chart.go @@ -789,6 +789,7 @@ func (opts *Chart) parseTitle() { // ShowPercent // ShowSerName // ShowVal +// NumFmt // // SecondPlotValues: Specifies the values in second plot for the 'pieOfPie' and // 'barOfPie' chart. @@ -811,6 +812,10 @@ func (opts *Chart) parseTitle() { // ShowVal: Specifies that the value shall be shown in a data label. // The 'ShowVal' property is optional. The default value is false. // +// NumFmt: Specifies that if linked to source and set custom number format code +// for data labels. The 'NumFmt' property is optional. The default format code +// is 'General'. +// // Set the primary horizontal and vertical axis options by 'XAxis' and 'YAxis'. // The properties of 'XAxis' that can be set are: // diff --git a/go.mod b/go.mod index 32cb416a3d..197af264c9 100644 --- a/go.mod +++ b/go.mod @@ -1,16 +1,22 @@ module github.com/xuri/excelize/v2 -go 1.16 +go 1.18 require ( 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.8.0 - github.com/xuri/efp v0.0.0-20230802181842-ad255f2331ca + github.com/stretchr/testify v1.8.4 + github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53 github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05 - golang.org/x/crypto v0.14.0 - golang.org/x/image v0.11.0 - golang.org/x/net v0.17.0 - golang.org/x/text v0.13.0 + golang.org/x/crypto v0.17.0 + golang.org/x/image v0.14.0 + golang.org/x/net v0.19.0 + golang.org/x/text v0.14.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/richardlehane/msoleps v1.0.3 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 23e1c02f52..4026d7eeb9 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,3 @@ -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= @@ -10,62 +9,21 @@ github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7 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/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-20230802181842-ad255f2331ca h1:uvPMDVyP7PXMMioYdyPH+0O+Ta/UO1WFfNYMO3Wz0eg= -github.com/xuri/efp v0.0.0-20230802181842-ad255f2331ca/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53 h1:Chd9DkqERQQuHpXjR/HSV1jLZA6uaoiwwH3vSuF3IW0= +github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05 h1:qhbILQo1K3mphbwKh1vNm4oGezE1eF9fQWmNiIpSfI4= github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05/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= -golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/image v0.11.0 h1:ds2RoQvBvYTiJkwpSFDwCcDFNX7DqjL2WsUgTNk0Ooo= -golang.org/x/image v0.11.0/go.mod h1:bglhjqbqVuEb9e9+eNR45Jfu7D+T4Qan+NhQk8Ck2P8= -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.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.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.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= -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.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -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= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4= +golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/picture.go b/picture.go index 1c0c8c7a82..86b33b9037 100644 --- a/picture.go +++ b/picture.go @@ -497,14 +497,21 @@ func (f *File) GetPictureCells(sheet string) ([]string, error) { } f.mu.Unlock() if ws.Drawing == nil { - return nil, err + return f.getEmbeddedImageCells(sheet) } target := f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID) drawingXML := strings.TrimPrefix(strings.ReplaceAll(target, "..", "xl"), "/") drawingRelationships := strings.ReplaceAll( strings.ReplaceAll(target, "../drawings", "xl/drawings/_rels"), ".xml", ".xml.rels") - - return f.getPictureCells(drawingXML, drawingRelationships) + embeddedImageCells, err := f.getEmbeddedImageCells(sheet) + if err != nil { + return nil, err + } + imageCells, err := f.getPictureCells(drawingXML, drawingRelationships) + if err != nil { + return nil, err + } + return append(embeddedImageCells, imageCells...), err } // DeletePicture provides a function to delete all pictures in a cell by given @@ -764,6 +771,31 @@ func (f *File) cellImagesReader() (*decodeCellImages, error) { return f.DecodeCellImages, nil } +// getEmbeddedImageCells returns all the Kingsoft WPS Office embedded image +// cells reference by given worksheet name. +func (f *File) getEmbeddedImageCells(sheet string) ([]string, error) { + var ( + err error + cells []string + ) + ws, err := f.workSheetReader(sheet) + if err != nil { + return cells, err + } + for _, row := range ws.SheetData.Row { + for _, c := range row.C { + if c.F != nil && c.F.Content != "" && + strings.HasPrefix(strings.TrimPrefix(strings.TrimPrefix(c.F.Content, "="), "_xlfn."), "DISPIMG") { + if _, err = f.CalcCellValue(sheet, c.R); err != nil { + return cells, err + } + cells = append(cells, c.R) + } + } + } + return cells, err +} + // getCellImages provides a function to get the Kingsoft WPS Office embedded // cell images by given worksheet name and cell reference. func (f *File) getCellImages(sheet, cell string) ([]Picture, error) { diff --git a/picture_test.go b/picture_test.go index 3573f452f2..d9414d3ff0 100644 --- a/picture_test.go +++ b/picture_test.go @@ -417,6 +417,21 @@ func TestGetPictureCells(t *testing.T) { // Test get picture cells on not exists worksheet _, err = f.GetPictureCells("SheetN") assert.EqualError(t, err, "sheet SheetN does not exist") + assert.NoError(t, f.Close()) + + // Test get embedded picture cells + f = NewFile() + assert.NoError(t, f.AddPicture("Sheet1", "A1", filepath.Join("test", "images", "excel.png"), nil)) + assert.NoError(t, f.SetCellFormula("Sheet1", "A2", "=_xlfn.DISPIMG(\"ID_********************************\",1)")) + cells, err = f.GetPictureCells("Sheet1") + assert.NoError(t, err) + assert.Equal(t, []string{"A2", "A1"}, cells) + + // Test get embedded cell pictures with invalid formula + assert.NoError(t, f.SetCellFormula("Sheet1", "A2", "=_xlfn.DISPIMG()")) + _, err = f.GetPictureCells("Sheet1") + assert.EqualError(t, err, "DISPIMG requires 2 numeric arguments") + assert.NoError(t, f.Close()) } func TestExtractDecodeCellAnchor(t *testing.T) { @@ -434,3 +449,12 @@ func TestGetCellImages(t *testing.T) { assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") assert.NoError(t, f.Close()) } + +func TestGetEmbeddedImageCells(t *testing.T) { + f := NewFile() + f.Sheet.Delete("xl/worksheets/sheet1.xml") + f.Pkg.Store("xl/worksheets/sheet1.xml", MacintoshCyrillicCharset) + _, err := f.getEmbeddedImageCells("Sheet1") + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") + assert.NoError(t, f.Close()) +} From e998c374ac4ab3f4141c0be25d474258280a6d23 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 22 Dec 2023 20:49:14 +0800 Subject: [PATCH 843/957] This closes #1767, change struct field tabRatio date type to float64 --- xmlWorkbook.go | 74 +++++++++++++++++++++++++------------------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/xmlWorkbook.go b/xmlWorkbook.go index cc953975ed..a24b2c034d 100644 --- a/xmlWorkbook.go +++ b/xmlWorkbook.go @@ -142,19 +142,19 @@ type xlsxBookViews struct { // http://schemas.openxmlformats.org/spreadsheetml/2006/main This element // specifies a single Workbook view. type xlsxWorkBookView struct { - Visibility string `xml:"visibility,attr,omitempty"` - Minimized bool `xml:"minimized,attr,omitempty"` - ShowHorizontalScroll *bool `xml:"showHorizontalScroll,attr"` - ShowVerticalScroll *bool `xml:"showVerticalScroll,attr"` - ShowSheetTabs *bool `xml:"showSheetTabs,attr"` - XWindow string `xml:"xWindow,attr,omitempty"` - YWindow string `xml:"yWindow,attr,omitempty"` - WindowWidth int `xml:"windowWidth,attr,omitempty"` - WindowHeight int `xml:"windowHeight,attr,omitempty"` - TabRatio int `xml:"tabRatio,attr,omitempty"` - FirstSheet int `xml:"firstSheet,attr,omitempty"` - ActiveTab int `xml:"activeTab,attr,omitempty"` - AutoFilterDateGrouping *bool `xml:"autoFilterDateGrouping,attr"` + Visibility string `xml:"visibility,attr,omitempty"` + Minimized bool `xml:"minimized,attr,omitempty"` + ShowHorizontalScroll *bool `xml:"showHorizontalScroll,attr"` + ShowVerticalScroll *bool `xml:"showVerticalScroll,attr"` + ShowSheetTabs *bool `xml:"showSheetTabs,attr"` + XWindow string `xml:"xWindow,attr,omitempty"` + YWindow string `xml:"yWindow,attr,omitempty"` + WindowWidth int `xml:"windowWidth,attr,omitempty"` + WindowHeight int `xml:"windowHeight,attr,omitempty"` + TabRatio float64 `xml:"tabRatio,attr,omitempty"` + FirstSheet int `xml:"firstSheet,attr,omitempty"` + ActiveTab int `xml:"activeTab,attr,omitempty"` + AutoFilterDateGrouping *bool `xml:"autoFilterDateGrouping,attr"` } // xlsxSheets directly maps the sheets element from the namespace @@ -349,30 +349,30 @@ type xlsxCustomWorkbookViews struct { // to implement configurable display modes, the customWorkbookView element // should be used to persist the settings for those display modes. type xlsxCustomWorkbookView struct { - ActiveSheetID *int `xml:"activeSheetId,attr"` - AutoUpdate *bool `xml:"autoUpdate,attr"` - ChangesSavedWin *bool `xml:"changesSavedWin,attr"` - GUID *string `xml:"guid,attr"` - IncludeHiddenRowCol *bool `xml:"includeHiddenRowCol,attr"` - IncludePrintSettings *bool `xml:"includePrintSettings,attr"` - Maximized *bool `xml:"maximized,attr"` - MergeInterval int `xml:"mergeInterval,attr"` - Minimized *bool `xml:"minimized,attr"` - Name *string `xml:"name,attr"` - OnlySync *bool `xml:"onlySync,attr"` - PersonalView *bool `xml:"personalView,attr"` - ShowComments *string `xml:"showComments,attr"` - ShowFormulaBar *bool `xml:"showFormulaBar,attr"` - ShowHorizontalScroll *bool `xml:"showHorizontalScroll,attr"` - ShowObjects *string `xml:"showObjects,attr"` - ShowSheetTabs *bool `xml:"showSheetTabs,attr"` - ShowStatusbar *bool `xml:"showStatusbar,attr"` - ShowVerticalScroll *bool `xml:"showVerticalScroll,attr"` - TabRatio *int `xml:"tabRatio,attr"` - WindowHeight *int `xml:"windowHeight,attr"` - WindowWidth *int `xml:"windowWidth,attr"` - XWindow *int `xml:"xWindow,attr"` - YWindow *int `xml:"yWindow,attr"` + ActiveSheetID *int `xml:"activeSheetId,attr"` + AutoUpdate *bool `xml:"autoUpdate,attr"` + ChangesSavedWin *bool `xml:"changesSavedWin,attr"` + GUID *string `xml:"guid,attr"` + IncludeHiddenRowCol *bool `xml:"includeHiddenRowCol,attr"` + IncludePrintSettings *bool `xml:"includePrintSettings,attr"` + Maximized *bool `xml:"maximized,attr"` + MergeInterval int `xml:"mergeInterval,attr"` + Minimized *bool `xml:"minimized,attr"` + Name *string `xml:"name,attr"` + OnlySync *bool `xml:"onlySync,attr"` + PersonalView *bool `xml:"personalView,attr"` + ShowComments *string `xml:"showComments,attr"` + ShowFormulaBar *bool `xml:"showFormulaBar,attr"` + ShowHorizontalScroll *bool `xml:"showHorizontalScroll,attr"` + ShowObjects *string `xml:"showObjects,attr"` + ShowSheetTabs *bool `xml:"showSheetTabs,attr"` + ShowStatusbar *bool `xml:"showStatusbar,attr"` + ShowVerticalScroll *bool `xml:"showVerticalScroll,attr"` + TabRatio *float64 `xml:"tabRatio,attr"` + WindowHeight *int `xml:"windowHeight,attr"` + WindowWidth *int `xml:"windowWidth,attr"` + XWindow *int `xml:"xWindow,attr"` + YWindow *int `xml:"yWindow,attr"` } // DefinedName directly maps the name for a cell or cell range on a From 8831afc5585c126ec4edbc21e0a2a67d7183eed7 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 25 Dec 2023 21:51:09 +0800 Subject: [PATCH 844/957] This recover the Sizes field in the ChartSeries data type removed in commit dfaf418f340f260c5005e8343135cd6af60b8e58 - Update unit tests and documentation of the internal uintPtr function --- chart.go | 6 +++++- chart_test.go | 16 ++++++++-------- drawing.go | 6 +++++- lib.go | 4 ++-- xmlChart.go | 1 + 5 files changed, 21 insertions(+), 12 deletions(-) diff --git a/chart.go b/chart.go index d054841adb..3321697c8b 100644 --- a/chart.go +++ b/chart.go @@ -707,7 +707,11 @@ func (opts *Chart) parseTitle() { // 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. +// Sizes: This sets the bubble size in a data series. The 'Sizes' property is +// optional and the default value was same with 'Values'. +// +// Fill: This set the format for the data series fill. The 'Fill' property is +// optional // // 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 diff --git a/chart_test.go b/chart_test.go index 350852eb4e..79501eeb7f 100644 --- a/chart_test.go +++ b/chart_test.go @@ -176,14 +176,14 @@ func TestAddChart(t *testing.T) { } 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", DataLabelPosition: ChartDataLabelsPositionAbove}, - {Name: "Sheet1!$A$31", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$31:$D$31", DataLabelPosition: ChartDataLabelsPositionLeft}, - {Name: "Sheet1!$A$32", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$32:$D$32", DataLabelPosition: ChartDataLabelsPositionBestFit}, - {Name: "Sheet1!$A$33", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$33:$D$33", DataLabelPosition: ChartDataLabelsPositionCenter}, - {Name: "Sheet1!$A$34", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$34:$D$34", DataLabelPosition: ChartDataLabelsPositionInsideBase}, - {Name: "Sheet1!$A$35", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$35:$D$35", DataLabelPosition: ChartDataLabelsPositionInsideEnd}, - {Name: "Sheet1!$A$36", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$36:$D$36", DataLabelPosition: ChartDataLabelsPositionOutsideEnd}, - {Name: "Sheet1!$A$37", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$37:$D$37", DataLabelPosition: ChartDataLabelsPositionRight}, + {Name: "Sheet1!$A$30", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$30:$D$30", Sizes: "Sheet1!$B$30:$D$30", DataLabelPosition: ChartDataLabelsPositionAbove}, + {Name: "Sheet1!$A$31", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$31:$D$31", Sizes: "Sheet1!$B$31:$D$31", DataLabelPosition: ChartDataLabelsPositionLeft}, + {Name: "Sheet1!$A$32", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$32:$D$32", Sizes: "Sheet1!$B$32:$D$32", DataLabelPosition: ChartDataLabelsPositionBestFit}, + {Name: "Sheet1!$A$33", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$33:$D$33", Sizes: "Sheet1!$B$33:$D$33", DataLabelPosition: ChartDataLabelsPositionCenter}, + {Name: "Sheet1!$A$34", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$34:$D$34", Sizes: "Sheet1!$B$34:$D$34", DataLabelPosition: ChartDataLabelsPositionInsideBase}, + {Name: "Sheet1!$A$35", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$35:$D$35", Sizes: "Sheet1!$B$35:$D$35", DataLabelPosition: ChartDataLabelsPositionInsideEnd}, + {Name: "Sheet1!$A$36", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$36:$D$36", Sizes: "Sheet1!$B$36:$D$36", DataLabelPosition: ChartDataLabelsPositionOutsideEnd}, + {Name: "Sheet1!$A$37", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$37:$D$37", Sizes: "Sheet1!$B$37:$D$37", DataLabelPosition: ChartDataLabelsPositionRight}, } format := GraphicOptions{ ScaleX: defaultDrawingScale, diff --git a/drawing.go b/drawing.go index 43e33128f0..6003235254 100644 --- a/drawing.go +++ b/drawing.go @@ -891,9 +891,13 @@ func (f *File) drawCharSeriesBubbleSize(v ChartSeries, opts *Chart) *cVal { if _, ok := map[ChartType]bool{Bubble: true, Bubble3D: true}[opts.Type]; !ok { return nil } + fVal := v.Values + if v.Sizes != "" { + fVal = v.Sizes + } return &cVal{ NumRef: &cNumRef{ - F: v.Values, + F: fVal, }, } } diff --git a/lib.go b/lib.go index bc564225e2..db0d7d7a80 100644 --- a/lib.go +++ b/lib.go @@ -431,8 +431,8 @@ 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 } +// uintPtr returns a pointer to an unsigned integer with the given value. +func uintPtr(u uint) *uint { return &u } // float64Ptr returns a pointer to a float64 with the given value. func float64Ptr(f float64) *float64 { return &f } diff --git a/xmlChart.go b/xmlChart.go index 5e209303b6..175fc866d2 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -607,6 +607,7 @@ type ChartSeries struct { Name string Categories string Values string + Sizes string Fill Fill Line ChartLine Marker ChartMarker From bb8e5dacd24f41a10927fc9ca975c4df2a3a4b24 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 28 Dec 2023 16:38:13 +0800 Subject: [PATCH 845/957] This closes #1769 and closes #1770, support multiple conditional formats rules - Update the unit tests --- styles.go | 160 ++++++++++++++++++++++++++++++------------------ styles_test.go | 16 ++++- xmlWorksheet.go | 10 ++- 3 files changed, 122 insertions(+), 64 deletions(-) diff --git a/styles.go b/styles.go index 78c0791ef5..73bbdef955 100644 --- a/styles.go +++ b/styles.go @@ -1161,25 +1161,61 @@ var ( "iconSet": drawCondFmtIconSet, } // extractContFmtFunc defines functions to get conditional formats. - extractContFmtFunc = map[string]func(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions{ - "cellIs": extractCondFmtCellIs, - "timePeriod": extractCondFmtTimePeriod, - "containsText": extractCondFmtText, - "notContainsText": extractCondFmtText, - "beginsWith": extractCondFmtText, - "endsWith": extractCondFmtText, - "top10": extractCondFmtTop10, - "aboveAverage": extractCondFmtAboveAverage, - "duplicateValues": extractCondFmtDuplicateUniqueValues, - "uniqueValues": extractCondFmtDuplicateUniqueValues, - "containsBlanks": extractCondFmtBlanks, - "notContainsBlanks": extractCondFmtNoBlanks, - "containsErrors": extractCondFmtErrors, - "notContainsErrors": extractCondFmtNoErrors, - "colorScale": extractCondFmtColorScale, - "dataBar": extractCondFmtDataBar, - "expression": extractCondFmtExp, - "iconSet": extractCondFmtIconSet, + extractContFmtFunc = map[string]func(*File, *xlsxCfRule, *xlsxExtLst) ConditionalFormatOptions{ + "cellIs": func(f *File, c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { + return f.extractCondFmtCellIs(c, extLst) + }, + "timePeriod": func(f *File, c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { + return f.extractCondFmtTimePeriod(c, extLst) + }, + "containsText": func(f *File, c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { + return f.extractCondFmtText(c, extLst) + }, + "notContainsText": func(f *File, c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { + return f.extractCondFmtText(c, extLst) + }, + "beginsWith": func(f *File, c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { + return f.extractCondFmtText(c, extLst) + }, + "endsWith": func(f *File, c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { + return f.extractCondFmtText(c, extLst) + }, + "top10": func(f *File, c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { + return f.extractCondFmtTop10(c, extLst) + }, + "aboveAverage": func(f *File, c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { + return f.extractCondFmtAboveAverage(c, extLst) + }, + "duplicateValues": func(f *File, c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { + return f.extractCondFmtDuplicateUniqueValues(c, extLst) + }, + "uniqueValues": func(f *File, c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { + return f.extractCondFmtDuplicateUniqueValues(c, extLst) + }, + "containsBlanks": func(f *File, c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { + return f.extractCondFmtBlanks(c, extLst) + }, + "notContainsBlanks": func(f *File, c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { + return f.extractCondFmtNoBlanks(c, extLst) + }, + "containsErrors": func(f *File, c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { + return f.extractCondFmtErrors(c, extLst) + }, + "notContainsErrors": func(f *File, c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { + return f.extractCondFmtNoErrors(c, extLst) + }, + "colorScale": func(f *File, c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { + return f.extractCondFmtColorScale(c, extLst) + }, + "dataBar": func(f *File, c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { + return f.extractCondFmtDataBar(c, extLst) + }, + "expression": func(f *File, c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { + return f.extractCondFmtExp(c, extLst) + }, + "iconSet": func(f *File, c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { + return f.extractCondFmtIconSet(c, extLst) + }, } // validType defined the list of valid validation types. validType = map[string]string{ @@ -2713,7 +2749,6 @@ func (f *File) SetConditionalFormat(sheet, rangeRef string, opts []ConditionalFo rules += len(cf.CfRule) } var ( - GUID = fmt.Sprintf("{00000000-0000-0000-%04X-%012X}", f.getSheetID(sheet), rules) cfRule []*xlsxCfRule noCriteriaTypes = []string{ "containsBlanks", @@ -2735,7 +2770,8 @@ func (f *File) SetConditionalFormat(sheet, rangeRef string, opts []ConditionalFo if ok || inStrSlice(noCriteriaTypes, vt, true) != -1 { drawFunc, ok := drawContFmtFunc[vt] if ok { - rule, x14rule := drawFunc(p, ct, strings.Split(rangeRef, ":")[0], GUID, &v) + rule, x14rule := drawFunc(p, ct, strings.Split(rangeRef, ":")[0], + fmt.Sprintf("{00000000-0000-0000-%04X-%012X}", f.getSheetID(sheet), rules+p), &v) if rule == nil { return ErrParameterInvalid } @@ -2813,7 +2849,7 @@ func (f *File) appendCfRule(ws *xlsxWorksheet, rule *xlsxX14CfRule) 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, extLst *xlsxExtLst) ConditionalFormatOptions { +func (f *File) extractCondFmtCellIs(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { format := ConditionalFormatOptions{StopIfTrue: c.StopIfTrue, Type: "cell", Criteria: operatorType[c.Operator]} if c.DxfID != nil { format.Format = *c.DxfID @@ -2828,7 +2864,7 @@ func extractCondFmtCellIs(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOp // extractCondFmtTimePeriod provides a function to extract conditional format // settings for time period by given conditional formatting rule. -func extractCondFmtTimePeriod(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { +func (f *File) extractCondFmtTimePeriod(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { format := ConditionalFormatOptions{StopIfTrue: c.StopIfTrue, Type: "time_period", Criteria: operatorType[c.Operator]} if c.DxfID != nil { format.Format = *c.DxfID @@ -2838,7 +2874,7 @@ func extractCondFmtTimePeriod(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalForm // extractCondFmtText provides a function to extract conditional format // settings for text cell values by given conditional formatting rule. -func extractCondFmtText(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { +func (f *File) extractCondFmtText(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { format := ConditionalFormatOptions{StopIfTrue: c.StopIfTrue, Type: "text", Criteria: operatorType[c.Operator], Value: c.Text} if c.DxfID != nil { format.Format = *c.DxfID @@ -2849,7 +2885,7 @@ func extractCondFmtText(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOpti // 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, extLst *xlsxExtLst) ConditionalFormatOptions { +func (f *File) extractCondFmtTop10(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { format := ConditionalFormatOptions{ StopIfTrue: c.StopIfTrue, Type: "top", @@ -2869,7 +2905,7 @@ func extractCondFmtTop10(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOpt // extractCondFmtAboveAverage provides a function to extract conditional format // settings for above average and below average by given conditional formatting // rule. -func extractCondFmtAboveAverage(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { +func (f *File) extractCondFmtAboveAverage(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { format := ConditionalFormatOptions{ StopIfTrue: c.StopIfTrue, Type: "average", @@ -2887,7 +2923,7 @@ func extractCondFmtAboveAverage(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFo // extractCondFmtDuplicateUniqueValues provides a function to extract // conditional format settings for duplicate and unique values by given // conditional formatting rule. -func extractCondFmtDuplicateUniqueValues(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { +func (f *File) extractCondFmtDuplicateUniqueValues(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { format := ConditionalFormatOptions{ StopIfTrue: c.StopIfTrue, Type: map[string]string{ @@ -2904,7 +2940,7 @@ func extractCondFmtDuplicateUniqueValues(c *xlsxCfRule, extLst *xlsxExtLst) Cond // extractCondFmtBlanks provides a function to extract conditional format // settings for blank cells by given conditional formatting rule. -func extractCondFmtBlanks(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { +func (f *File) extractCondFmtBlanks(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { format := ConditionalFormatOptions{ StopIfTrue: c.StopIfTrue, Type: "blanks", @@ -2917,7 +2953,7 @@ func extractCondFmtBlanks(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOp // extractCondFmtNoBlanks provides a function to extract conditional format // settings for no blank cells by given conditional formatting rule. -func extractCondFmtNoBlanks(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { +func (f *File) extractCondFmtNoBlanks(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { format := ConditionalFormatOptions{ StopIfTrue: c.StopIfTrue, Type: "no_blanks", @@ -2930,7 +2966,7 @@ func extractCondFmtNoBlanks(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormat // extractCondFmtErrors provides a function to extract conditional format // settings for cells with errors by given conditional formatting rule. -func extractCondFmtErrors(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { +func (f *File) extractCondFmtErrors(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { format := ConditionalFormatOptions{ StopIfTrue: c.StopIfTrue, Type: "errors", @@ -2943,7 +2979,7 @@ func extractCondFmtErrors(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOp // extractCondFmtNoErrors provides a function to extract conditional format // settings for cells without errors by given conditional formatting rule. -func extractCondFmtNoErrors(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { +func (f *File) extractCondFmtNoErrors(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { format := ConditionalFormatOptions{ StopIfTrue: c.StopIfTrue, Type: "no_errors", @@ -2957,7 +2993,7 @@ func extractCondFmtNoErrors(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormat // 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, extLst *xlsxExtLst) ConditionalFormatOptions { +func (f *File) extractCondFmtColorScale(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { format := ConditionalFormatOptions{StopIfTrue: c.StopIfTrue} format.Type, format.Criteria = "2_color_scale", "=" values := len(c.ColorScale.Cfvo) @@ -2967,12 +3003,12 @@ func extractCondFmtColorScale(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalForm 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.MinColor = "#" + f.getThemeColor(c.ColorScale.Color[0]) 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") + format.MaxColor = "#" + f.getThemeColor(c.ColorScale.Color[1]) } if colors == 3 { format.Type = "3_color_scale" @@ -2980,19 +3016,37 @@ func extractCondFmtColorScale(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalForm 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.MidColor = "#" + f.getThemeColor(c.ColorScale.Color[1]) 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") + format.MaxColor = "#" + f.getThemeColor(c.ColorScale.Color[2]) } return format } +// extractCondFmtDataBarRule provides a function to extract conditional format +// settings for data bar by given conditional formatting rule extension list. +func (f *File) extractCondFmtDataBarRule(ID string, format *ConditionalFormatOptions, condFmts []decodeX14ConditionalFormatting) { + for _, condFmt := range condFmts { + for _, rule := range condFmt.CfRule { + if rule.DataBar != nil && rule.ID == ID { + format.BarDirection = rule.DataBar.Direction + if rule.DataBar.Gradient != nil && !*rule.DataBar.Gradient { + format.BarSolid = true + } + if rule.DataBar.BorderColor != nil { + format.BarBorderColor = "#" + f.getThemeColor(rule.DataBar.BorderColor) + } + } + } + } +} + // extractCondFmtDataBar provides a function to extract conditional format // settings for data bar by given conditional formatting rule. -func extractCondFmtDataBar(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { +func (f *File) extractCondFmtDataBar(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { format := ConditionalFormatOptions{Type: "data_bar", Criteria: "="} if c.DataBar != nil { format.StopIfTrue = c.StopIfTrue @@ -3000,33 +3054,17 @@ func extractCondFmtDataBar(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatO 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") + format.BarColor = "#" + f.getThemeColor(c.DataBar.Color[0]) 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 - format.BarDirection = rule.DataBar.Direction - if rule.DataBar.BorderColor != nil { - format.BarBorderColor = "#" + strings.TrimPrefix(strings.ToUpper(rule.DataBar.BorderColor.RGB), "FF") - } - } - } - } - } - extractExtLst := func(extLst *decodeExtLst) { + extractExtLst := func(ID string, extLst *decodeExtLst) { for _, ext := range extLst.Ext { if ext.URI == ExtURIConditionalFormattings { - decodeCondFmts := new(decodeX14ConditionalFormattings) + decodeCondFmts := new(decodeX14ConditionalFormattingRules) if err := xml.Unmarshal([]byte(ext.Content), &decodeCondFmts); err == nil { - var condFmts []decodeX14ConditionalFormatting - if err = xml.Unmarshal([]byte(decodeCondFmts.Content), &condFmts); err == nil { - extractDataBarRule(condFmts) - } + f.extractCondFmtDataBarRule(ID, &format, decodeCondFmts.CondFmt) } } } @@ -3036,7 +3074,7 @@ func extractCondFmtDataBar(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatO if err := xml.Unmarshal([]byte(c.ExtLst.Ext), &ext); err == nil && extLst != nil { decodeExtLst := new(decodeExtLst) if err = xml.Unmarshal([]byte(""+extLst.Ext+""), decodeExtLst); err == nil { - extractExtLst(decodeExtLst) + extractExtLst(ext.ID, decodeExtLst) } } } @@ -3045,7 +3083,7 @@ func extractCondFmtDataBar(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatO // extractCondFmtExp provides a function to extract conditional format settings // for expression by given conditional formatting rule. -func extractCondFmtExp(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { +func (f *File) extractCondFmtExp(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { format := ConditionalFormatOptions{StopIfTrue: c.StopIfTrue, Type: "formula"} if c.DxfID != nil { format.Format = *c.DxfID @@ -3058,7 +3096,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 { +func (f *File) extractCondFmtIconSet(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { format := ConditionalFormatOptions{Type: "icon_set"} if c.IconSet != nil { if c.IconSet.ShowValue != nil { @@ -3082,7 +3120,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, ws.ExtLst)) + opts = append(opts, extractFunc(f, cr, ws.ExtLst)) } } conditionalFormats[cf.SQRef] = opts diff --git a/styles_test.go b/styles_test.go index 881828f996..89dad301ce 100644 --- a/styles_test.go +++ b/styles_test.go @@ -236,9 +236,21 @@ func TestGetConditionalFormats(t *testing.T) { assert.NoError(t, err) assert.Equal(t, format, opts["A1:A2"]) } - // Test get conditional formats on no exists worksheet + // Test get multiple conditional formats f := NewFile() - _, err := f.GetConditionalFormats("SheetN") + expected := []ConditionalFormatOptions{ + {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: false, StopIfTrue: true}, + } + err := f.SetConditionalFormat("Sheet1", "A1:A2", expected) + assert.NoError(t, err) + opts, err := f.GetConditionalFormats("Sheet1") + assert.NoError(t, err) + assert.Equal(t, expected, opts["A1:A2"]) + + // Test get conditional formats on no exists worksheet + 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") diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 177f1363c2..fa7a89eb91 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -720,6 +720,14 @@ type decodeX14ConditionalFormattings struct { Content string `xml:",innerxml"` } +// decodeX14ConditionalFormattingRules directly maps the conditionalFormattings +// element. +type decodeX14ConditionalFormattingRules struct { + XMLName xml.Name `xml:"conditionalFormattings"` + XMLNSXM string `xml:"xmlns:xm,attr"` + CondFmt []decodeX14ConditionalFormatting `xml:"conditionalFormatting"` +} + // decodeX14ConditionalFormatting directly maps the conditionalFormatting // element. type decodeX14ConditionalFormatting struct { @@ -741,7 +749,7 @@ type decodeX14DataBar struct { MaxLength int `xml:"maxLength,attr"` MinLength int `xml:"minLength,attr"` Border bool `xml:"border,attr,omitempty"` - Gradient bool `xml:"gradient,attr"` + Gradient *bool `xml:"gradient,attr"` ShowValue bool `xml:"showValue,attr,omitempty"` Direction string `xml:"direction,attr,omitempty"` Cfvo []*xlsxCfvo `xml:"cfvo"` From f4e395137d8ffbab4a5132156e22d0c03eddbf31 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 30 Dec 2023 14:41:16 +0800 Subject: [PATCH 846/957] This closes #1770, fix incorrect multiple conditional formats rules priorities - Rename variable name hCell to topLeftCell, and rename vCell to bottomRightCell - Update the unit tests --- merge.go | 20 ++++++++++---------- pivotTable.go | 12 ++++++------ stream.go | 8 ++++---- styles.go | 17 +++++++++-------- styles_test.go | 42 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 71 insertions(+), 28 deletions(-) diff --git a/merge.go b/merge.go index 48783d1e2c..fedd9ec48e 100644 --- a/merge.go +++ b/merge.go @@ -49,16 +49,16 @@ func (mc *xlsxMergeCell) Rect() ([]int, error) { // | | // |A8(x3,y4) C8(x4,y4)| // +------------------------+ -func (f *File) MergeCell(sheet, hCell, vCell string) error { - rect, err := rangeRefToCoordinates(hCell + ":" + vCell) +func (f *File) MergeCell(sheet, topLeftCell, bottomRightCell string) error { + rect, err := rangeRefToCoordinates(topLeftCell + ":" + bottomRightCell) if err != nil { return err } // Correct the range reference, such correct C1:B3 to B1:C3. _ = sortCoordinates(rect) - hCell, _ = CoordinatesToCellName(rect[0], rect[1]) - vCell, _ = CoordinatesToCellName(rect[2], rect[3]) + topLeftCell, _ = CoordinatesToCellName(rect[0], rect[1]) + bottomRightCell, _ = CoordinatesToCellName(rect[2], rect[3]) ws, err := f.workSheetReader(sheet) if err != nil { @@ -66,7 +66,7 @@ func (f *File) MergeCell(sheet, hCell, vCell string) error { } ws.mu.Lock() defer ws.mu.Unlock() - ref := hCell + ":" + vCell + ref := topLeftCell + ":" + bottomRightCell if ws.MergeCells != nil { ws.MergeCells.Cells = append(ws.MergeCells.Cells, &xlsxMergeCell{Ref: ref, rect: rect}) } else { @@ -82,14 +82,14 @@ func (f *File) MergeCell(sheet, hCell, vCell string) error { // err := f.UnmergeCell("Sheet1", "D3", "E9") // // Attention: overlapped range will also be unmerged. -func (f *File) UnmergeCell(sheet, hCell, vCell string) error { +func (f *File) UnmergeCell(sheet, topLeftCell, bottomRightCell string) error { ws, err := f.workSheetReader(sheet) if err != nil { return err } ws.mu.Lock() defer ws.mu.Unlock() - rect1, err := rangeRefToCoordinates(hCell + ":" + vCell) + rect1, err := rangeRefToCoordinates(topLeftCell + ":" + bottomRightCell) if err != nil { return err } @@ -265,9 +265,9 @@ func mergeCell(cell1, cell2 *xlsxMergeCell) *xlsxMergeCell { if rect1[3] < rect2[3] { rect1[3], rect2[3] = rect2[3], rect1[3] } - hCell, _ := CoordinatesToCellName(rect1[0], rect1[1]) - vCell, _ := CoordinatesToCellName(rect1[2], rect1[3]) - return &xlsxMergeCell{rect: rect1, Ref: hCell + ":" + vCell} + topLeftCell, _ := CoordinatesToCellName(rect1[0], rect1[1]) + bottomRightCell, _ := CoordinatesToCellName(rect1[2], rect1[3]) + return &xlsxMergeCell{rect: rect1, Ref: topLeftCell + ":" + bottomRightCell} } // MergeCell define a merged cell data. diff --git a/pivotTable.go b/pivotTable.go index 9512c27a94..04bc56259c 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -274,8 +274,8 @@ func (f *File) addPivotCache(opts *PivotTableOptions) error { } // data range has been checked order, _ := f.getTableFieldsOrder(opts) - hCell, _ := CoordinatesToCellName(coordinates[0], coordinates[1]) - vCell, _ := CoordinatesToCellName(coordinates[2], coordinates[3]) + topLeftCell, _ := CoordinatesToCellName(coordinates[0], coordinates[1]) + bottomRightCell, _ := CoordinatesToCellName(coordinates[2], coordinates[3]) pc := xlsxPivotCacheDefinition{ SaveData: false, RefreshOnLoad: true, @@ -285,7 +285,7 @@ func (f *File) addPivotCache(opts *PivotTableOptions) error { CacheSource: &xlsxCacheSource{ Type: "worksheet", WorksheetSource: &xlsxWorksheetSource{ - Ref: hCell + ":" + vCell, + Ref: topLeftCell + ":" + bottomRightCell, Sheet: dataSheet, }, }, @@ -315,8 +315,8 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, opts *PivotTableOptions) return newPivotTableRangeError(err.Error()) } - hCell, _ := CoordinatesToCellName(coordinates[0], coordinates[1]) - vCell, _ := CoordinatesToCellName(coordinates[2], coordinates[3]) + topLeftCell, _ := CoordinatesToCellName(coordinates[0], coordinates[1]) + bottomRightCell, _ := CoordinatesToCellName(coordinates[2], coordinates[3]) pivotTableStyle := func() string { if opts.PivotTableStyleName == "" { @@ -340,7 +340,7 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, opts *PivotTableOptions) ShowError: &opts.ShowError, DataCaption: "Values", Location: &xlsxLocation{ - Ref: hCell + ":" + vCell, + Ref: topLeftCell + ":" + bottomRightCell, FirstDataCol: 1, FirstDataRow: 1, FirstHeaderRow: 1, diff --git a/stream.go b/stream.go index 2247bcb8cc..05fb4cbde8 100644 --- a/stream.go +++ b/stream.go @@ -484,16 +484,16 @@ func (sw *StreamWriter) SetPanes(panes *Panes) error { // 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 { - _, err := cellRefsToCoordinates(hCell, vCell) +func (sw *StreamWriter) MergeCell(topLeftCell, bottomRightCell string) error { + _, err := cellRefsToCoordinates(topLeftCell, bottomRightCell) if err != nil { return err } sw.mergeCellsCount++ _, _ = sw.mergeCells.WriteString(``) return nil } diff --git a/styles.go b/styles.go index 73bbdef955..2782c4dfef 100644 --- a/styles.go +++ b/styles.go @@ -2262,13 +2262,13 @@ func (f *File) GetCellStyle(sheet, cell string) (int, error) { // 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) +func (f *File) SetCellStyle(sheet, topLeftCell, bottomRightCell string, styleID int) error { + hCol, hRow, err := CellNameToCoordinates(topLeftCell) if err != nil { return err } - vCol, vRow, err := CellNameToCoordinates(vCell) + vCol, vRow, err := CellNameToCoordinates(bottomRightCell) if err != nil { return err } @@ -2759,19 +2759,20 @@ func (f *File) SetConditionalFormat(sheet, rangeRef string, opts []ConditionalFo "iconSet", } ) - for p, v := range opts { + for i, opt := range opts { var vt, ct string var ok bool // "type" is a required parameter, check for valid validation types. - vt, ok = validType[v.Type] + vt, ok = validType[opt.Type] if ok { // Check for valid criteria types. - ct, ok = criteriaType[v.Criteria] + ct, ok = criteriaType[opt.Criteria] if ok || inStrSlice(noCriteriaTypes, vt, true) != -1 { drawFunc, ok := drawContFmtFunc[vt] if ok { - rule, x14rule := drawFunc(p, ct, strings.Split(rangeRef, ":")[0], - fmt.Sprintf("{00000000-0000-0000-%04X-%012X}", f.getSheetID(sheet), rules+p), &v) + priority := rules + i + rule, x14rule := drawFunc(priority, ct, strings.Split(rangeRef, ":")[0], + fmt.Sprintf("{00000000-0000-0000-%04X-%012X}", f.getSheetID(sheet), priority), &opt) if rule == nil { return ErrParameterInvalid } diff --git a/styles_test.go b/styles_test.go index 89dad301ce..6674dbe957 100644 --- a/styles_test.go +++ b/styles_test.go @@ -193,6 +193,48 @@ func TestSetConditionalFormat(t *testing.T) { assert.Equal(t, ErrParameterInvalid, f.SetConditionalFormat("Sheet1", "A1:A2", []ConditionalFormatOptions{{Type: "icon_set", IconStyle: "unknown"}})) // Test unsupported conditional formatting rule types assert.Equal(t, ErrParameterInvalid, f.SetConditionalFormat("Sheet1", "A1", []ConditionalFormatOptions{{Type: "unsupported"}})) + + t.Run("multi_conditional_formatting_rules_priority", func(t *testing.T) { + f := NewFile() + var condFmts []ConditionalFormatOptions + for _, color := range []string{ + "#264B96", // Blue + "#F9A73E", // Yellow + "#006F3C", // Green + } { + condFmts = append(condFmts, ConditionalFormatOptions{ + Type: "data_bar", + Criteria: "=", + MinType: "num", + MaxType: "num", + MinValue: "0", + MaxValue: "5", + BarColor: color, + BarSolid: true, + }) + } + assert.NoError(t, f.SetConditionalFormat("Sheet1", "A1:A5", condFmts)) + assert.NoError(t, f.SetConditionalFormat("Sheet1", "B1:B5", condFmts)) + for r := 1; r <= 20; r++ { + cell, err := CoordinatesToCellName(1, r) + assert.NoError(t, err) + assert.NoError(t, f.SetCellValue("Sheet1", cell, r)) + cell, err = CoordinatesToCellName(2, r) + assert.NoError(t, err) + assert.NoError(t, f.SetCellValue("Sheet1", cell, r)) + } + ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") + assert.True(t, ok) + var priorities []int + expected := []int{1, 2, 3, 4, 5, 6} + for _, condFmt := range ws.(*xlsxWorksheet).ConditionalFormatting { + for _, rule := range condFmt.CfRule { + priorities = append(priorities, rule.Priority) + } + } + assert.Equal(t, expected, priorities) + assert.NoError(t, f.Close()) + }) } func TestGetConditionalFormats(t *testing.T) { From 792656552bf102b9703d45cce62def5abe5afe4a Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 9 Jan 2024 20:56:20 +0800 Subject: [PATCH 847/957] This closes #1777, fix the GetStyle or GetConditionalStyle function to returns incorrect DecimalPlaces field value - Update documentation for the NewStyle function - Update unit tests - Update dependencies Go module - Update GitHub workflow dependencies package version - Update copyright agreement statement --- .github/workflows/codeql-analysis.yml | 6 +-- .github/workflows/go.yml | 2 +- LICENSE | 2 +- adjust.go | 2 +- calc.go | 2 +- calcchain.go | 2 +- cell.go | 2 +- chart.go | 2 +- col.go | 2 +- crypt.go | 2 +- crypt_test.go | 2 +- datavalidation.go | 2 +- datavalidation_test.go | 2 +- date.go | 2 +- docProps.go | 2 +- docProps_test.go | 2 +- drawing.go | 2 +- drawing_test.go | 2 +- errors.go | 2 +- excelize.go | 2 +- file.go | 2 +- go.mod | 4 +- go.sum | 8 ++-- lib.go | 2 +- merge.go | 2 +- numfmt.go | 63 ++++++++++++++++++++++++++- picture.go | 2 +- pivotTable.go | 2 +- rows.go | 2 +- shape.go | 2 +- sheet.go | 2 +- sheetpr.go | 2 +- sheetview.go | 2 +- slicer.go | 2 +- sparkline.go | 2 +- stream.go | 2 +- styles.go | 35 ++++++++++++--- styles_test.go | 55 +++++++++++++++++++++-- table.go | 2 +- templates.go | 2 +- vml.go | 2 +- vmlDrawing.go | 2 +- vml_test.go | 2 +- workbook.go | 2 +- xmlApp.go | 2 +- xmlCalcChain.go | 2 +- xmlChart.go | 2 +- xmlChartSheet.go | 2 +- xmlComments.go | 2 +- xmlContentTypes.go | 2 +- xmlCore.go | 2 +- xmlDecodeDrawing.go | 2 +- xmlDrawing.go | 2 +- xmlPivotCache.go | 2 +- xmlPivotTable.go | 2 +- xmlSharedStrings.go | 2 +- xmlSlicers.go | 2 +- xmlStyles.go | 2 +- xmlTable.go | 2 +- xmlTheme.go | 2 +- xmlWorkbook.go | 2 +- xmlWorksheet.go | 2 +- 62 files changed, 209 insertions(+), 74 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index e457ab7e03..62e26de452 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -24,12 +24,12 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index e799c2624e..ff0c8d07d0 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Install Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} diff --git a/LICENSE b/LICENSE index b9bcc5737f..684d80f085 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 2016-2023 The excelize Authors. +Copyright (c) 2016-2024 The excelize Authors. Copyright (c) 2011-2017 Geoffrey J. Teale All rights reserved. diff --git a/adjust.go b/adjust.go index 74d7a36ad1..35ecdd1262 100644 --- a/adjust.go +++ b/adjust.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // diff --git a/calc.go b/calc.go index 8fd207a3d3..7fc87ba1e6 100644 --- a/calc.go +++ b/calc.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // diff --git a/calcchain.go b/calcchain.go index a7cd259cc7..f85169a7c4 100644 --- a/calcchain.go +++ b/calcchain.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // diff --git a/cell.go b/cell.go index 0f56a40d2c..ad0920848b 100644 --- a/cell.go +++ b/cell.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // diff --git a/chart.go b/chart.go index 3321697c8b..5c7f8d75a5 100644 --- a/chart.go +++ b/chart.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // diff --git a/col.go b/col.go index dd7ffafc4e..d5700bad20 100644 --- a/col.go +++ b/col.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // diff --git a/crypt.go b/crypt.go index 6bfb1330bf..d2d6f46efa 100644 --- a/crypt.go +++ b/crypt.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // diff --git a/crypt_test.go b/crypt_test.go index 7b4cac7243..c8735fc63b 100644 --- a/crypt_test.go +++ b/crypt_test.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // diff --git a/datavalidation.go b/datavalidation.go index 4d2f360b28..4a1f634276 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // diff --git a/datavalidation_test.go b/datavalidation_test.go index c331ebe101..18816fbfd6 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // diff --git a/date.go b/date.go index a59c694705..c7ab5aa943 100644 --- a/date.go +++ b/date.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // diff --git a/docProps.go b/docProps.go index 3d81545497..45957b7ed4 100644 --- a/docProps.go +++ b/docProps.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // diff --git a/docProps_test.go b/docProps_test.go index dfe5536a9e..9ec66cf0cd 100644 --- a/docProps_test.go +++ b/docProps_test.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // diff --git a/drawing.go b/drawing.go index 6003235254..0dbc06e56a 100644 --- a/drawing.go +++ b/drawing.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // diff --git a/drawing_test.go b/drawing_test.go index 90846702cd..c8c95fd8bc 100644 --- a/drawing_test.go +++ b/drawing_test.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // diff --git a/errors.go b/errors.go index b5fe34eac1..0d4aba06bf 100644 --- a/errors.go +++ b/errors.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // diff --git a/excelize.go b/excelize.go index b7dd50869e..05049add98 100644 --- a/excelize.go +++ b/excelize.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. diff --git a/file.go b/file.go index 31a96746f4..dc42e1e21f 100644 --- a/file.go +++ b/file.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // diff --git a/go.mod b/go.mod index 197af264c9..958af04113 100644 --- a/go.mod +++ b/go.mod @@ -8,9 +8,9 @@ require ( github.com/stretchr/testify v1.8.4 github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53 github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05 - golang.org/x/crypto v0.17.0 + golang.org/x/crypto v0.18.0 golang.org/x/image v0.14.0 - golang.org/x/net v0.19.0 + golang.org/x/net v0.20.0 golang.org/x/text v0.14.0 ) diff --git a/go.sum b/go.sum index 4026d7eeb9..99281827b6 100644 --- a/go.sum +++ b/go.sum @@ -15,12 +15,12 @@ github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53 h1:Chd9DkqERQQuHpXjR/HSV1 github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05 h1:qhbILQo1K3mphbwKh1vNm4oGezE1eF9fQWmNiIpSfI4= github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4= golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= -golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= -golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/lib.go b/lib.go index db0d7d7a80..44a6ee5104 100644 --- a/lib.go +++ b/lib.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // diff --git a/merge.go b/merge.go index fedd9ec48e..af34a55fff 100644 --- a/merge.go +++ b/merge.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // diff --git a/numfmt.go b/numfmt.go index a303b640f9..2034318ae7 100644 --- a/numfmt.go +++ b/numfmt.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // @@ -4680,6 +4680,67 @@ func (f *File) checkDateTimePattern() error { return nil } +// extractNumFmtDecimal returns decimal places, if has a decimal point token and +// zero place holder token from a number format code token list. +func extractNumFmtDecimal(tokens []nfp.Token) (int, bool, bool) { + decimal, point, zero := 0, false, false + for _, token := range tokens { + if token.TType == nfp.TokenTypeDecimalPoint { + point = true + } + if token.TType == nfp.TokenTypeZeroPlaceHolder { + if point { + decimal = len(token.TValue) + } + zero = true + } + } + return decimal, point, zero +} + +// extractNumFmtDecimal returns decimal places from a number format code that +// has the same decimal places in positive part negative part or only positive +// part, if the given number format code is not suitable for numeric this +// function will return -1. +func (f *File) extractNumFmtDecimal(fmtCode string) int { + var ( + p = nfp.NumberFormatParser() + pos, neg, posPoint, negPoint, posZero, negZero bool + posDecimal, negDecimal int + ) + for i, section := range p.Parse(fmtCode) { + if i == 0 { + pos = true + posDecimal, posPoint, posZero = extractNumFmtDecimal(section.Items) + } + if i == 1 { + neg = true + negDecimal, negPoint, negZero = extractNumFmtDecimal(section.Items) + } + } + if !pos { + return -1 + } + equalPosNegDecimal := posPoint && negPoint && posDecimal == negDecimal + equalPosNegZero := !posPoint && !negPoint && posZero && negZero + if neg { + if equalPosNegDecimal { + return posDecimal + } + if equalPosNegZero { + return 0 + } + return -1 + } + if posPoint { + return posDecimal + } + if posZero { + return 0 + } + return -1 +} + // langNumFmtFuncZhCN returns number format code by given date and time pattern // for country code zh-cn. func (f *File) langNumFmtFuncZhCN(numFmtID int) string { diff --git a/picture.go b/picture.go index 86b33b9037..a7d6a2e887 100644 --- a/picture.go +++ b/picture.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // diff --git a/pivotTable.go b/pivotTable.go index 04bc56259c..405378c6e7 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // diff --git a/rows.go b/rows.go index 894b1ba77d..3869260890 100644 --- a/rows.go +++ b/rows.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // diff --git a/shape.go b/shape.go index 6a48f794f7..5aa6ba2418 100644 --- a/shape.go +++ b/shape.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // diff --git a/sheet.go b/sheet.go index 83ad73e458..1f3a0e0f6e 100644 --- a/sheet.go +++ b/sheet.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // diff --git a/sheetpr.go b/sheetpr.go index 6b734e688f..665541fff4 100644 --- a/sheetpr.go +++ b/sheetpr.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // diff --git a/sheetview.go b/sheetview.go index 3ca3d8c482..6edb87fe24 100644 --- a/sheetview.go +++ b/sheetview.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // diff --git a/slicer.go b/slicer.go index c1afc2a8dc..c1562ff05f 100644 --- a/slicer.go +++ b/slicer.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // diff --git a/sparkline.go b/sparkline.go index 5e3ff20228..1ecdd69758 100644 --- a/sparkline.go +++ b/sparkline.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // diff --git a/stream.go b/stream.go index 05fb4cbde8..adf9b8ba0c 100644 --- a/stream.go +++ b/stream.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // diff --git a/styles.go b/styles.go index 2782c4dfef..d90502aee9 100644 --- a/styles.go +++ b/styles.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // @@ -278,6 +278,14 @@ func parseFormatStyleSet(style *Style) (*Style, error) { // single // double // +// NumFmt is used to set the built-in all languages formats index, built-in +// language formats index, or built-in currency formats index, it doesn't work +// when you specify the custom number format by CustomNumFmt. When you get +// style definition by the GetStyle or GetConditionalStyle function, the NumFmt +// only works if the number format code is exactly equal with any built-in all +// languages format code, built-in language formats code, or built-in currency +// format code. +// // Excel's built-in all languages formats are shown in the following table: // // Index | Format String @@ -919,8 +927,8 @@ func parseFormatStyleSet(style *Style) (*Style, error) { // 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: +// Excelize support set custom number format for cell by CustomNumFmt field. For +// example, set number as date type in Uruguay (Spanish) format for Sheet1!A6: // // f := excelize.NewFile() // defer func() { @@ -940,7 +948,15 @@ func parseFormatStyleSet(style *Style) (*Style, error) { // } // err = f.SetCellStyle("Sheet1", "A6", "A6", style) // -// Cell Sheet1!A6 in the Excel Application: martes, 04 de Julio de 2017 +// Cell Sheet1!A6 in the spreadsheet application: martes, 04 de Julio de 2017 +// +// DecimalPlaces is used to set the decimal places for built-in currency +// formats, it doesn't work if you have specified the built-in all languages +// formats or built-in language formats by NumFmt field, or specify the custom +// number format by CustomNumFmt. When you get style definition by the GetStyle +// or GetConditionalStyle function, the DecimalPlaces only doesn't nil if a +// number format code has the same decimal places in the positive part negative +// part, or only the positive part. func (f *File) NewStyle(style *Style) (int, error) { var ( fs *Style @@ -1498,12 +1514,21 @@ func (f *File) extractFont(fnt *xlsxFont, s *xlsxStyleSheet, style *Style) { func (f *File) extractNumFmt(n *int, s *xlsxStyleSheet, style *Style) { if n != nil { numFmtID := *n - if _, ok := builtInNumFmt[numFmtID]; ok || isLangNumFmt(numFmtID) { + if builtInFmtCode, ok := builtInNumFmt[numFmtID]; ok || isLangNumFmt(numFmtID) { style.NumFmt = numFmtID + if decimalPlaces := f.extractNumFmtDecimal(builtInFmtCode); decimalPlaces != -1 { + style.DecimalPlaces = &decimalPlaces + } return } if s.NumFmts != nil { for _, numFmt := range s.NumFmts.NumFmt { + if numFmt.NumFmtID != numFmtID { + continue + } + if decimalPlaces := f.extractNumFmtDecimal(numFmt.FormatCode); decimalPlaces != -1 { + style.DecimalPlaces = &decimalPlaces + } style.CustomNumFmt = &numFmt.FormatCode if strings.Contains(numFmt.FormatCode, ";[Red]") { style.NegRed = true diff --git a/styles_test.go b/styles_test.go index 6674dbe957..9680fab704 100644 --- a/styles_test.go +++ b/styles_test.go @@ -443,7 +443,8 @@ func TestConditionalStyle(t *testing.T) { assert.NoError(t, err) style, err = f.GetConditionalStyle(idx) assert.NoError(t, err) - assert.Equal(t, expected, style) + assert.Equal(t, expected.NumFmt, style.NumFmt) + assert.Zero(t, *style.DecimalPlaces) _, err = f.NewConditionalStyle(&Style{NumFmt: 27}) assert.NoError(t, err) numFmt := "general" @@ -641,6 +642,7 @@ func TestGetStyle(t *testing.T) { assert.Equal(t, expected.Alignment, style.Alignment) assert.Equal(t, expected.Protection, style.Protection) assert.Equal(t, expected.NumFmt, style.NumFmt) + assert.Nil(t, style.DecimalPlaces) expected = &Style{ Fill: Fill{Type: "pattern", Pattern: 1, Color: []string{"0000FF"}}, @@ -650,6 +652,15 @@ func TestGetStyle(t *testing.T) { style, err = f.GetStyle(styleID) assert.NoError(t, err) assert.Equal(t, expected.Fill, style.Fill) + assert.Nil(t, style.DecimalPlaces) + + expected = &Style{NumFmt: 2} + styleID, err = f.NewStyle(expected) + assert.NoError(t, err) + style, err = f.GetStyle(styleID) + assert.NoError(t, err) + assert.Equal(t, expected.NumFmt, style.NumFmt) + assert.Equal(t, 2, *style.DecimalPlaces) expected = &Style{NumFmt: 27} styleID, err = f.NewStyle(expected) @@ -657,6 +668,7 @@ func TestGetStyle(t *testing.T) { style, err = f.GetStyle(styleID) assert.NoError(t, err) assert.Equal(t, expected.NumFmt, style.NumFmt) + assert.Nil(t, style.DecimalPlaces) expected = &Style{NumFmt: 165} styleID, err = f.NewStyle(expected) @@ -664,13 +676,50 @@ func TestGetStyle(t *testing.T) { style, err = f.GetStyle(styleID) assert.NoError(t, err) assert.Equal(t, expected.NumFmt, style.NumFmt) + assert.Equal(t, 2, *style.DecimalPlaces) - expected = &Style{NumFmt: 165, NegRed: true} + decimal := 4 + expected = &Style{NumFmt: 165, DecimalPlaces: &decimal, NegRed: true} styleID, err = f.NewStyle(expected) assert.NoError(t, err) style, err = f.GetStyle(styleID) assert.NoError(t, err) - assert.Equal(t, expected.NumFmt, style.NumFmt) + assert.Equal(t, 0, style.NumFmt) + assert.Equal(t, *expected.DecimalPlaces, *style.DecimalPlaces) + assert.Equal(t, "[$$-409]#,##0.0000;[Red][$$-409]#,##0.0000", *style.CustomNumFmt) + + for _, val := range [][]interface{}{ + {"$#,##0", 0}, + {"$#,##0.0", 1}, + {"_($* #,##0_);_($* (#,##0);_($* \"-\"_);_(@_)", 0}, + {"_($* #,##000_);_($* (#,##000);_($* \"-\"_);_(@_)", 0}, + {"_($* #,##0.0000_);_($* (#,##0.0000);_($* \"-\"????_);_(@_)", 4}, + } { + numFmtCode := val[0].(string) + expected = &Style{CustomNumFmt: &numFmtCode} + styleID, err = f.NewStyle(expected) + assert.NoError(t, err) + style, err = f.GetStyle(styleID) + assert.NoError(t, err) + assert.Equal(t, val[1].(int), *style.DecimalPlaces, numFmtCode) + } + + for _, val := range []string{ + ";$#,##0", + ";$#,##0;", + ";$#,##0.0", + ";$#,##0.0;", + "$#,##0;0.0", + "_($* #,##0_);;_($* \"-\"_);_(@_)", + "_($* #,##0.0_);_($* (#,##0.00);_($* \"-\"_);_(@_)", + } { + expected = &Style{CustomNumFmt: &val} + styleID, err = f.NewStyle(expected) + assert.NoError(t, err) + style, err = f.GetStyle(styleID) + assert.NoError(t, err) + assert.Nil(t, style.DecimalPlaces) + } // Test get style with custom color index f.Styles.Colors = &xlsxStyleColors{ diff --git a/table.go b/table.go index 9d5820d974..d5d1fc45d3 100644 --- a/table.go +++ b/table.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // diff --git a/templates.go b/templates.go index 43d6df4c13..7028a5b194 100644 --- a/templates.go +++ b/templates.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // diff --git a/vml.go b/vml.go index 993f2f1c85..3c221b5b13 100644 --- a/vml.go +++ b/vml.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // diff --git a/vmlDrawing.go b/vmlDrawing.go index fa293be5cf..c84c3c9186 100644 --- a/vmlDrawing.go +++ b/vmlDrawing.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // diff --git a/vml_test.go b/vml_test.go index 50e9a04fae..ad584029e5 100644 --- a/vml_test.go +++ b/vml_test.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // diff --git a/workbook.go b/workbook.go index 5dfe9d2555..5dd26f077b 100644 --- a/workbook.go +++ b/workbook.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // diff --git a/xmlApp.go b/xmlApp.go index 6109ec204c..9432886e11 100644 --- a/xmlApp.go +++ b/xmlApp.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // diff --git a/xmlCalcChain.go b/xmlCalcChain.go index 358cd8d174..81ee47a156 100644 --- a/xmlCalcChain.go +++ b/xmlCalcChain.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // diff --git a/xmlChart.go b/xmlChart.go index 175fc866d2..60406b7d91 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // diff --git a/xmlChartSheet.go b/xmlChartSheet.go index a710871d44..47a26e5995 100644 --- a/xmlChartSheet.go +++ b/xmlChartSheet.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // diff --git a/xmlComments.go b/xmlComments.go index a28f5cc31d..8b997c2f3d 100644 --- a/xmlComments.go +++ b/xmlComments.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // diff --git a/xmlContentTypes.go b/xmlContentTypes.go index ee13069dfa..beec2ba96f 100644 --- a/xmlContentTypes.go +++ b/xmlContentTypes.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // diff --git a/xmlCore.go b/xmlCore.go index d28a71f63d..7b9be61d41 100644 --- a/xmlCore.go +++ b/xmlCore.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // diff --git a/xmlDecodeDrawing.go b/xmlDecodeDrawing.go index 1473817af7..8077af8454 100644 --- a/xmlDecodeDrawing.go +++ b/xmlDecodeDrawing.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // diff --git a/xmlDrawing.go b/xmlDrawing.go index 5770ac8224..41c25ee5ef 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // diff --git a/xmlPivotCache.go b/xmlPivotCache.go index 9f5a84165a..14b1db0afa 100644 --- a/xmlPivotCache.go +++ b/xmlPivotCache.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // diff --git a/xmlPivotTable.go b/xmlPivotTable.go index 163a801d6e..8275bb6723 100644 --- a/xmlPivotTable.go +++ b/xmlPivotTable.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // diff --git a/xmlSharedStrings.go b/xmlSharedStrings.go index b2b65d1efa..f17e291f3a 100644 --- a/xmlSharedStrings.go +++ b/xmlSharedStrings.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // diff --git a/xmlSlicers.go b/xmlSlicers.go index 56dde04ef9..036f7a5272 100644 --- a/xmlSlicers.go +++ b/xmlSlicers.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // diff --git a/xmlStyles.go b/xmlStyles.go index 613001c8e4..925573347c 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // diff --git a/xmlTable.go b/xmlTable.go index 4fc00b1cca..79dd1b5709 100644 --- a/xmlTable.go +++ b/xmlTable.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // diff --git a/xmlTheme.go b/xmlTheme.go index 88cc5730e6..5f46da9c99 100644 --- a/xmlTheme.go +++ b/xmlTheme.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // diff --git a/xmlWorkbook.go b/xmlWorkbook.go index a24b2c034d..423c9eb71d 100644 --- a/xmlWorkbook.go +++ b/xmlWorkbook.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // diff --git a/xmlWorksheet.go b/xmlWorksheet.go index fa7a89eb91..a897718a0c 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2024 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. // From 50e23df865354c7ebc6f34424ef3359f4528afe0 Mon Sep 17 00:00:00 2001 From: 3zmx Date: Thu, 18 Jan 2024 15:31:43 +0800 Subject: [PATCH 848/957] ref #65, support _xlfn.ANCHORARRAY formula function (#1784) - Initial formula array calculation support - Update unit test and documentation --- adjust.go | 154 ++++++++++++++++++++++++++++++++++------- adjust_test.go | 26 ++++++- calc.go | 54 +++++++++++++-- calc_test.go | 115 +++++++++++++++++++++++++++++- calcchain.go | 2 +- cell.go | 83 +++++++++++++++++++++- cell_test.go | 65 ++++++++++++----- chart.go | 2 +- col.go | 2 +- crypt.go | 2 +- crypt_test.go | 4 +- datavalidation.go | 2 +- datavalidation_test.go | 2 +- date.go | 2 +- docProps.go | 2 +- docProps_test.go | 2 +- drawing.go | 2 +- drawing_test.go | 2 +- errors.go | 2 +- excelize.go | 3 +- file.go | 2 +- lib.go | 2 +- merge.go | 2 +- numfmt.go | 2 +- picture.go | 2 +- picture_test.go | 8 +-- pivotTable.go | 2 +- rows.go | 2 +- rows_test.go | 2 +- shape.go | 2 +- sheet.go | 2 +- sheet_test.go | 2 +- sheetpr.go | 2 +- sheetview.go | 2 +- slicer.go | 2 +- sparkline.go | 2 +- stream.go | 2 +- styles.go | 2 +- table.go | 2 +- templates.go | 2 +- vml.go | 2 +- vmlDrawing.go | 2 +- vml_test.go | 2 +- workbook.go | 2 +- xmlApp.go | 2 +- xmlCalcChain.go | 2 +- xmlChart.go | 2 +- xmlChartSheet.go | 2 +- xmlComments.go | 2 +- xmlContentTypes.go | 2 +- xmlCore.go | 2 +- xmlDecodeDrawing.go | 2 +- xmlDrawing.go | 2 +- xmlPivotCache.go | 2 +- xmlPivotTable.go | 2 +- xmlSharedStrings.go | 2 +- xmlSlicers.go | 2 +- xmlStyles.go | 2 +- xmlTable.go | 2 +- xmlTheme.go | 2 +- xmlWorkbook.go | 2 +- xmlWorksheet.go | 3 +- 62 files changed, 504 insertions(+), 115 deletions(-) diff --git a/adjust.go b/adjust.go index 35ecdd1262..bae49bc94a 100644 --- a/adjust.go +++ b/adjust.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize @@ -165,7 +165,7 @@ func (f *File) adjustColDimensions(sheet string, ws *xlsxWorksheet, col, offset worksheet.SheetData.Row[rowIdx].C[colIdx].R, _ = CoordinatesToCellName(newCol, cellRow) } } - if err := f.adjustFormula(sheet, sheetN, worksheet.SheetData.Row[rowIdx].C[colIdx].F, columns, col, offset, false); err != nil { + if err := f.adjustFormula(sheet, sheetN, &worksheet.SheetData.Row[rowIdx].C[colIdx], columns, col, offset, false); err != nil { return err } } @@ -228,8 +228,8 @@ func (r *xlsxRow) adjustSingleRowDimensions(offset int) { // adjustSingleRowFormulas provides a function to adjust single row formulas. func (f *File) adjustSingleRowFormulas(sheet, sheetN string, r *xlsxRow, num, offset int, si bool) error { - for _, col := range r.C { - if err := f.adjustFormula(sheet, sheetN, col.F, rows, num, offset, si); err != nil { + for i := 0; i < len(r.C); i++ { + if err := f.adjustFormula(sheet, sheetN, &r.C[i], rows, num, offset, si); err != nil { return err } } @@ -273,37 +273,32 @@ func (f *File) adjustCellRef(ref string, dir adjustDirection, num, offset int) ( // adjustFormula provides a function to adjust formula reference and shared // formula reference. -func (f *File) adjustFormula(sheet, sheetN string, formula *xlsxF, dir adjustDirection, num, offset int, si bool) error { - if formula == nil { +func (f *File) adjustFormula(sheet, sheetN string, cell *xlsxC, dir adjustDirection, num, offset int, si bool) error { + var err error + if cell.f != "" { + if cell.f, err = f.adjustFormulaRef(sheet, sheetN, cell.f, false, dir, num, offset); err != nil { + return err + } + } + if cell.F == nil { return nil } - var err error - if formula.Ref != "" && sheet == sheetN { - if formula.Ref, _, err = f.adjustCellRef(formula.Ref, dir, num, offset); err != nil { + if cell.F.Ref != "" && sheet == sheetN { + if cell.F.Ref, _, err = f.adjustCellRef(cell.F.Ref, dir, num, offset); err != nil { return err } - if si && formula.Si != nil { - formula.Si = intPtr(*formula.Si + 1) + if si && cell.F.Si != nil { + cell.F.Si = intPtr(*cell.F.Si + 1) } } - if formula.Content != "" { - if formula.Content, err = f.adjustFormulaRef(sheet, sheetN, formula.Content, false, dir, num, offset); err != nil { + if cell.F.Content != "" { + if cell.F.Content, err = f.adjustFormulaRef(sheet, sheetN, cell.F.Content, false, dir, num, offset); err != nil { return err } } return nil } -// isFunctionStop provides a function to check if token is a function stop. -func isFunctionStop(token efp.Token) bool { - return token.TType == efp.TokenTypeFunction && token.TSubType == efp.TokenSubTypeStop -} - -// isFunctionStart provides a function to check if token is a function start. -func isFunctionStart(token efp.Token) bool { - return token.TType == efp.TokenTypeFunction && token.TSubType == efp.TokenSubTypeStart -} - // escapeSheetName enclose sheet name in single quotation marks if the giving // worksheet name includes spaces or non-alphabetical characters. func escapeSheetName(name string) string { @@ -442,11 +437,11 @@ func (f *File) adjustFormulaRef(sheet, sheetN, formula string, keepRelative bool val += operand continue } - if isFunctionStart(token) { + if isFunctionStartToken(token) { val += token.TValue + string(efp.ParenOpen) continue } - if isFunctionStop(token) { + if isFunctionStopToken(token) { val += token.TValue + string(efp.ParenClose) continue } @@ -459,6 +454,115 @@ func (f *File) adjustFormulaRef(sheet, sheetN, formula string, keepRelative bool return val, nil } +// arrayFormulaOperandToken defines meta fields for transforming the array +// formula to the normal formula. +type arrayFormulaOperandToken struct { + operandTokenIndex, topLeftCol, topLeftRow, bottomRightCol, bottomRightRow int + sheetName, sourceCellRef, targetCellRef string +} + +// setCoordinates convert each corner cell reference in the array formula cell +// range to the coordinate number. +func (af *arrayFormulaOperandToken) setCoordinates() error { + for i, ref := range strings.Split(af.sourceCellRef, ":") { + cellRef, col, row, err := parseRef(ref) + if err != nil { + return err + } + var c, r int + if col { + if cellRef.Row = TotalRows; i == 1 { + cellRef.Row = 1 + } + } + if row { + if cellRef.Col = MaxColumns; i == 1 { + cellRef.Col = 1 + } + } + if c, r = cellRef.Col, cellRef.Row; cellRef.Sheet != "" { + af.sheetName = cellRef.Sheet + "!" + } + if af.topLeftCol == 0 || c < af.topLeftCol { + af.topLeftCol = c + } + if af.topLeftRow == 0 || r < af.topLeftRow { + af.topLeftRow = r + } + if c > af.bottomRightCol { + af.bottomRightCol = c + } + if r > af.bottomRightRow { + af.bottomRightRow = r + } + } + return nil +} + +// transformArrayFormula transforms an array formula to the normal formula by +// giving a formula tokens list and formula operand tokens list. +func transformArrayFormula(tokens []efp.Token, afs []arrayFormulaOperandToken) string { + var val string + for i, token := range tokens { + var skip bool + for _, af := range afs { + if af.operandTokenIndex == i { + val += af.sheetName + af.targetCellRef + skip = true + break + } + } + if skip { + continue + } + if isFunctionStartToken(token) { + val += token.TValue + string(efp.ParenOpen) + continue + } + if isFunctionStopToken(token) { + val += token.TValue + string(efp.ParenClose) + continue + } + if token.TType == efp.TokenTypeOperand && token.TSubType == efp.TokenSubTypeText { + val += string(efp.QuoteDouble) + strings.ReplaceAll(token.TValue, "\"", "\"\"") + string(efp.QuoteDouble) + continue + } + val += token.TValue + } + return val +} + +// getArrayFormulaTokens returns parsed formula token and operand related token +// list for in array formula. +func getArrayFormulaTokens(sheet, formula string, definedNames []DefinedName) ([]efp.Token, []arrayFormulaOperandToken, error) { + var ( + ps = efp.ExcelParser() + tokens = ps.Parse(formula) + arrayFormulaOperandTokens []arrayFormulaOperandToken + ) + for i, token := range tokens { + if token.TSubType == efp.TokenSubTypeRange && token.TType == efp.TokenTypeOperand { + tokenVal := token.TValue + for _, definedName := range definedNames { + if (definedName.Scope == "Workbook" || definedName.Scope == sheet) && definedName.Name == tokenVal { + tokenVal = definedName.RefersTo + } + } + if len(strings.Split(tokenVal, ":")) > 1 { + arrayFormulaOperandToken := arrayFormulaOperandToken{ + operandTokenIndex: i, + sourceCellRef: tokenVal, + } + if err := arrayFormulaOperandToken.setCoordinates(); err != nil { + return tokens, arrayFormulaOperandTokens, err + } + arrayFormulaOperandTokens = append(arrayFormulaOperandTokens, arrayFormulaOperandToken) + } + } + } + return tokens, arrayFormulaOperandTokens, 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 a8dd2ff379..bfaa61c133 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -557,9 +557,9 @@ func TestAdjustFormula(t *testing.T) { assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAdjustFormula.xlsx"))) assert.NoError(t, f.Close()) - assert.NoError(t, f.adjustFormula("Sheet1", "Sheet1", nil, rows, 0, 0, false)) - assert.Equal(t, newCellNameToCoordinatesError("-", newInvalidCellNameError("-")), f.adjustFormula("Sheet1", "Sheet1", &xlsxF{Ref: "-"}, rows, 0, 0, false)) - assert.Equal(t, ErrColumnNumber, f.adjustFormula("Sheet1", "Sheet1", &xlsxF{Ref: "XFD1:XFD1"}, columns, 0, 1, false)) + assert.NoError(t, f.adjustFormula("Sheet1", "Sheet1", &xlsxC{}, rows, 0, 0, false)) + assert.Equal(t, newCellNameToCoordinatesError("-", newInvalidCellNameError("-")), f.adjustFormula("Sheet1", "Sheet1", &xlsxC{F: &xlsxF{Ref: "-"}}, rows, 0, 0, false)) + assert.Equal(t, ErrColumnNumber, f.adjustFormula("Sheet1", "Sheet1", &xlsxC{F: &xlsxF{Ref: "XFD1:XFD1"}}, columns, 0, 1, false)) _, err := f.adjustFormulaRef("Sheet1", "Sheet1", "XFE1", false, columns, 0, 1) assert.Equal(t, ErrColumnNumber, err) @@ -940,6 +940,26 @@ func TestAdjustFormula(t *testing.T) { assert.NoError(t, f.InsertRows("Sheet1", 2, 1)) assert.NoError(t, f.InsertCols("Sheet1", "A", 1)) }) + t.Run("for_array_formula_cell", func(t *testing.T) { + f := NewFile() + assert.NoError(t, f.SetSheetRow("Sheet1", "A1", &[]int{1, 2})) + assert.NoError(t, f.SetSheetRow("Sheet1", "A2", &[]int{3, 4})) + formulaType, ref := STCellFormulaTypeArray, "C1:C2" + assert.NoError(t, f.SetCellFormula("Sheet1", "C1", "A1:A2*B1:B2", FormulaOpts{Ref: &ref, Type: &formulaType})) + assert.NoError(t, f.InsertRows("Sheet1", 1, 1)) + assert.NoError(t, f.InsertCols("Sheet1", "A", 1)) + result, err := f.CalcCellValue("Sheet1", "D2") + assert.NoError(t, err) + assert.Equal(t, "2", result) + result, err = f.CalcCellValue("Sheet1", "D3") + assert.NoError(t, err) + assert.Equal(t, "12", result) + + // Test adjust array formula with invalid range reference + formulaType, ref = STCellFormulaTypeArray, "E1:E2" + assert.NoError(t, f.SetCellFormula("Sheet1", "E1", "XFD1:XFD1", FormulaOpts{Ref: &ref, Type: &formulaType})) + assert.EqualError(t, f.InsertCols("Sheet1", "A", 1), "the column number must be greater than or equal to 1 and less than or equal to 16384") + }) } func TestAdjustVolatileDeps(t *testing.T) { diff --git a/calc.go b/calc.go index 7fc87ba1e6..2488eda9f7 100644 --- a/calc.go +++ b/calc.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize @@ -838,7 +838,7 @@ func (f *File) CalcCellValue(sheet, cell string, opts ...Options) (result string // 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 { + if formula, err = f.getCellFormula(sheet, cell, true); err != nil { return } ps := efp.ExcelParser() @@ -1467,7 +1467,7 @@ func (f *File) parseToken(ctx *calcContext, sheet string, token efp.Token, opdSt } // parseRef parse reference for a cell, column name or row number. -func (f *File) parseRef(ref string) (cellRef, bool, bool, error) { +func parseRef(ref string) (cellRef, bool, bool, error) { var ( err, colErr, rowErr error cr cellRef @@ -1526,7 +1526,7 @@ func (f *File) parseReference(ctx *calcContext, sheet, reference string) (formul if len(ranges) > 1 { var cr cellRange for i, ref := range ranges { - cellRef, col, row, err := f.parseRef(ref) + cellRef, col, row, err := parseRef(ref) if err != nil { return newErrorFormulaArg(formulaErrorNAME, "invalid reference"), errors.New("invalid reference") } @@ -1550,7 +1550,7 @@ func (f *File) parseReference(ctx *calcContext, sheet, reference string) (formul cellRanges.PushBack(cr) return f.rangeResolver(ctx, cellRefs, cellRanges) } - cellRef, _, _, err := f.parseRef(reference) + cellRef, _, _, err := parseRef(reference) if err != nil { return newErrorFormulaArg(formulaErrorNAME, "invalid reference"), errors.New("invalid reference") } @@ -1601,7 +1601,7 @@ func (f *File) cellResolver(ctx *calcContext, sheet, cell string) (formulaArg, e err error ) ref := fmt.Sprintf("%s!%s", sheet, cell) - if formula, _ := f.GetCellFormula(sheet, cell); len(formula) != 0 { + if formula, _ := f.getCellFormula(sheet, cell, true); len(formula) != 0 { ctx.mu.Lock() if ctx.entry != ref { if ctx.iterations[ref] <= f.options.MaxCalcIterations { @@ -14505,6 +14505,48 @@ func (fn *formulaFuncs) ADDRESS(argsList *list.List) formulaArg { return newStringFormulaArg(fmt.Sprintf("%s%s", sheetText, addr)) } +// ANCHORARRAY function returns the entire spilled range for the dynamic array +// in cell. The syntax of the function is: +// +// ANCHORARRAY(cell) +func (fn *formulaFuncs) ANCHORARRAY(argsList *list.List) formulaArg { + if argsList.Len() != 1 { + return newErrorFormulaArg(formulaErrorVALUE, "ANCHORARRAY requires 1 numeric argument") + } + ws, err := fn.f.workSheetReader(fn.sheet) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + } + ref := argsList.Front().Value.(formulaArg).cellRefs.Front().Value.(cellRef) + cell := ws.SheetData.Row[ref.Row-1].C[ref.Col-1] + if cell.F == nil { + return newEmptyFormulaArg() + } + coordinates, err := rangeRefToCoordinates(cell.F.Ref) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + } + _ = sortCoordinates(coordinates) + var mtx [][]formulaArg + for c := coordinates[0]; c <= coordinates[2]; c++ { + var row []formulaArg + for r := coordinates[1]; r <= coordinates[3]; r++ { + cellName, _ := CoordinatesToCellName(c, r) + result, err := fn.f.CalcCellValue(ref.Sheet, cellName, Options{RawCellValue: true}) + if err != nil { + return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + } + arg := newStringFormulaArg(result) + if num := arg.ToNumber(); num.Type == ArgNumber { + arg = num + } + row = append(row, arg) + } + mtx = append(mtx, row) + } + return newMatrixFormulaArg(mtx) +} + // CHOOSE function returns a value from an array, that corresponds to a // supplied index number (position). The syntax of the function is: // diff --git a/calc_test.go b/calc_test.go index a3a6a8313e..99b1eb26c4 100644 --- a/calc_test.go +++ b/calc_test.go @@ -4689,7 +4689,7 @@ func TestCalcCellValue(t *testing.T) { assert.EqualError(t, err, "sheet SheetN does not exist") // Test get calculated cell value with invalid sheet name _, err = f.CalcCellValue("Sheet:1", "A1") - assert.EqualError(t, err, ErrSheetNameInvalid.Error()) + assert.Equal(t, ErrSheetNameInvalid, err) // Test get calculated cell value with not support formula f = prepareCalcData(cellData) assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "=UNSUPPORT(A1)")) @@ -4823,6 +4823,119 @@ func TestCalcCompareFormulaArgMatrix(t *testing.T) { assert.Equal(t, compareFormulaArgMatrix(lhs, rhs, newNumberFormulaArg(matchModeMaxLess), false), criteriaG) } +func TestCalcANCHORARRAY(t *testing.T) { + f := NewFile() + assert.NoError(t, f.SetCellValue("Sheet1", "A1", 1)) + assert.NoError(t, f.SetCellValue("Sheet1", "A2", 2)) + formulaType, ref := STCellFormulaTypeArray, "B1:B2" + assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "A1:A2", + FormulaOpts{Ref: &ref, Type: &formulaType})) + assert.NoError(t, f.SetCellFormula("Sheet1", "C1", "SUM(_xlfn.ANCHORARRAY($B$1))")) + result, err := f.CalcCellValue("Sheet1", "C1") + assert.NoError(t, err) + assert.Equal(t, "3", result) + + assert.NoError(t, f.SetCellFormula("Sheet1", "C1", "SUM(_xlfn.ANCHORARRAY(\"\",\"\"))")) + result, err = f.CalcCellValue("Sheet1", "C1") + assert.EqualError(t, err, "ANCHORARRAY requires 1 numeric argument") + assert.Equal(t, "#VALUE!", result) + + fn := &formulaFuncs{f: f, sheet: "SheetN"} + argsList := list.New() + argsList.PushBack(newStringFormulaArg("$B$1")) + formulaArg := fn.ANCHORARRAY(argsList) + assert.Equal(t, "sheet SheetN does not exist", formulaArg.Value()) + + fn.sheet = "Sheet1" + argsList = argsList.Init() + arg := newStringFormulaArg("$A$1") + arg.cellRefs = list.New() + arg.cellRefs.PushBack(cellRef{Row: 1, Col: 1}) + argsList.PushBack(arg) + formulaArg = fn.ANCHORARRAY(argsList) + assert.Equal(t, ArgEmpty, formulaArg.Type) + + ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") + assert.True(t, ok) + ws.(*xlsxWorksheet).SheetData.Row[0].C[0].F = &xlsxF{} + formulaArg = fn.ANCHORARRAY(argsList) + assert.Equal(t, ArgError, formulaArg.Type) + assert.Equal(t, ErrParameterInvalid.Error(), formulaArg.Value()) + + argsList = argsList.Init() + arg = newStringFormulaArg("$B$1") + arg.cellRefs = list.New() + arg.cellRefs.PushBack(cellRef{Row: 1, Col: 1, Sheet: "SheetN"}) + argsList.PushBack(arg) + ws.(*xlsxWorksheet).SheetData.Row[0].C[0].F = &xlsxF{Ref: "A1:A1"} + formulaArg = fn.ANCHORARRAY(argsList) + assert.Equal(t, ArgError, formulaArg.Type) + assert.Equal(t, "sheet SheetN does not exist", formulaArg.Value()) +} + +func TestCalcArrayFormula(t *testing.T) { + t.Run("matrix_multiplication", func(t *testing.T) { + f := NewFile() + assert.NoError(t, f.SetSheetRow("Sheet1", "A1", &[]int{1, 2})) + assert.NoError(t, f.SetSheetRow("Sheet1", "A2", &[]int{3, 4})) + formulaType, ref := STCellFormulaTypeArray, "C1:C2" + assert.NoError(t, f.SetCellFormula("Sheet1", "C1", "A1:A2*B1:B2", + FormulaOpts{Ref: &ref, Type: &formulaType})) + result, err := f.CalcCellValue("Sheet1", "C1") + assert.NoError(t, err) + assert.Equal(t, "2", result) + result, err = f.CalcCellValue("Sheet1", "C2") + assert.NoError(t, err) + assert.Equal(t, "12", result) + }) + t.Run("matrix_multiplication_with_defined_name", func(t *testing.T) { + f := NewFile() + assert.NoError(t, f.SetSheetRow("Sheet1", "A1", &[]int{1, 2})) + assert.NoError(t, f.SetSheetRow("Sheet1", "A2", &[]int{3, 4})) + assert.NoError(t, f.SetDefinedName(&DefinedName{ + Name: "matrix", + RefersTo: "Sheet1!$A$1:$A$2", + })) + formulaType, ref := STCellFormulaTypeArray, "C1:C2" + assert.NoError(t, f.SetCellFormula("Sheet1", "C1", "matrix*B1:B2+\"1\"", + FormulaOpts{Ref: &ref, Type: &formulaType})) + result, err := f.CalcCellValue("Sheet1", "C1") + assert.NoError(t, err) + assert.Equal(t, "3", result) + result, err = f.CalcCellValue("Sheet1", "C2") + assert.NoError(t, err) + assert.Equal(t, "13", result) + }) + t.Run("columm_multiplication", func(t *testing.T) { + f := NewFile() + assert.NoError(t, f.SetSheetRow("Sheet1", "A1", &[]int{1, 2})) + assert.NoError(t, f.SetSheetRow("Sheet1", "A2", &[]int{3, 4})) + formulaType, ref := STCellFormulaTypeArray, "C1:C1048576" + assert.NoError(t, f.SetCellFormula("Sheet1", "C1", "A:A*B:B", + FormulaOpts{Ref: &ref, Type: &formulaType})) + result, err := f.CalcCellValue("Sheet1", "C1") + assert.NoError(t, err) + assert.Equal(t, "2", result) + result, err = f.CalcCellValue("Sheet1", "C2") + assert.NoError(t, err) + assert.Equal(t, "12", result) + }) + t.Run("row_multiplication", func(t *testing.T) { + f := NewFile() + assert.NoError(t, f.SetSheetRow("Sheet1", "A1", &[]int{1, 2})) + assert.NoError(t, f.SetSheetRow("Sheet1", "A2", &[]int{3, 4})) + formulaType, ref := STCellFormulaTypeArray, "A3:XFD3" + assert.NoError(t, f.SetCellFormula("Sheet1", "A3", "1:1*2:2", + FormulaOpts{Ref: &ref, Type: &formulaType})) + result, err := f.CalcCellValue("Sheet1", "A3") + assert.NoError(t, err) + assert.Equal(t, "3", result) + result, err = f.CalcCellValue("Sheet1", "B3") + assert.NoError(t, err) + assert.Equal(t, "8", result) + }) +} + func TestCalcTRANSPOSE(t *testing.T) { cellData := [][]interface{}{ {"a", "d"}, diff --git a/calcchain.go b/calcchain.go index f85169a7c4..32c2ef14cf 100644 --- a/calcchain.go +++ b/calcchain.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize diff --git a/cell.go b/cell.go index ad0920848b..721ba7bc01 100644 --- a/cell.go +++ b/cell.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize @@ -660,7 +660,22 @@ func (f *File) SetCellDefault(sheet, cell, value string) error { // 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) { + return f.getCellFormula(sheet, cell, false) +} + +// getCellFormula provides a function to get transformed formula from cell by +// given worksheet name and cell reference in spreadsheet. +func (f *File) getCellFormula(sheet, cell string, transformed bool) (string, error) { return f.getCellStringFunc(sheet, cell, func(x *xlsxWorksheet, c *xlsxC) (string, bool, error) { + if transformed && !f.formulaChecked { + if err := f.setArrayFormulaCells(); err != nil { + return "", false, err + } + f.formulaChecked = true + } + if transformed && c.f != "" { + return c.f, true, nil + } if c.F == nil { return "", false, nil } @@ -785,6 +800,11 @@ func (f *File) SetCellFormula(sheet, cell, formula string, opts ...FormulaOpts) return err } c.F.T = *opt.Type + if c.F.T == STCellFormulaTypeArray && opt.Ref != nil { + if err = ws.setArrayFormula(sheet, &xlsxF{Ref: *opt.Ref, Content: formula}, f.GetDefinedName()); err != nil { + return err + } + } if c.F.T == STCellFormulaTypeShared { if err = ws.setSharedFormula(*opt.Ref); err != nil { return err @@ -799,6 +819,67 @@ func (f *File) SetCellFormula(sheet, cell, formula string, opts ...FormulaOpts) return err } +// setArrayFormula transform the array formula in an array formula range to the +// normal formula and set cells in this range to the formula as the normal +// formula. +func (ws *xlsxWorksheet) setArrayFormula(sheet string, formula *xlsxF, definedNames []DefinedName) error { + if len(strings.Split(formula.Ref, ":")) < 2 { + return nil + } + coordinates, err := rangeRefToCoordinates(formula.Ref) + if err != nil { + return err + } + _ = sortCoordinates(coordinates) + tokens, arrayFormulaOperandTokens, err := getArrayFormulaTokens(sheet, formula.Content, definedNames) + if err != nil { + return err + } + topLeftCol, topLeftRow := coordinates[0], coordinates[1] + for c := coordinates[0]; c <= coordinates[2]; c++ { + for r := coordinates[1]; r <= coordinates[3]; r++ { + colOffset, rowOffset := c-topLeftCol, r-topLeftRow + for i, af := range arrayFormulaOperandTokens { + colNum, rowNum := af.topLeftCol+colOffset, af.topLeftRow+rowOffset + if colNum <= af.bottomRightCol && rowNum <= af.bottomRightRow { + arrayFormulaOperandTokens[i].targetCellRef, _ = CoordinatesToCellName(colNum, rowNum) + } + } + ws.prepareSheetXML(c, r) + if cell := &ws.SheetData.Row[r-1].C[c-1]; cell.f == "" { + cell.f = transformArrayFormula(tokens, arrayFormulaOperandTokens) + } + } + } + return err +} + +// setArrayFormulaCells transform the array formula in all worksheets to the +// normal formula and set cells in the array formula reference range to the +// formula as the normal formula. +func (f *File) setArrayFormulaCells() error { + definedNames := f.GetDefinedName() + for _, sheetN := range f.GetSheetList() { + ws, err := f.workSheetReader(sheetN) + if err != nil { + if err.Error() == newNotWorksheetError(sheetN).Error() { + continue + } + return err + } + for _, row := range ws.SheetData.Row { + for _, cell := range row.C { + if cell.F != nil && cell.F.T == STCellFormulaTypeArray { + if err = ws.setArrayFormula(sheetN, cell.F, definedNames); err != nil { + return err + } + } + } + } + } + return nil +} + // setSharedFormula set shared formula for the cells. func (ws *xlsxWorksheet) setSharedFormula(ref string) error { coordinates, err := rangeRefToCoordinates(ref) diff --git a/cell_test.go b/cell_test.go index c3a622ede8..0ed6e87fbc 100644 --- a/cell_test.go +++ b/cell_test.go @@ -134,11 +134,11 @@ func TestCheckCellInRangeRef(t *testing.T) { } ok, err := f.checkCellInRangeRef("A1", "A:B") - assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) + assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), err) assert.False(t, ok) ok, err = f.checkCellInRangeRef("AA0", "Z0:AB1") - assert.EqualError(t, err, newCellNameToCoordinatesError("AA0", newInvalidCellNameError("AA0")).Error()) + assert.Equal(t, newCellNameToCoordinatesError("AA0", newInvalidCellNameError("AA0")), err) assert.False(t, ok) } @@ -172,9 +172,9 @@ func TestSetCellFloat(t *testing.T) { assert.Equal(t, "123.42", val, "A1 should be 123.42") }) f := NewFile() - assert.EqualError(t, f.SetCellFloat(sheet, "A", 123.42, -1, 64), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) + assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.SetCellFloat(sheet, "A", 123.42, -1, 64)) // 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()) + assert.Equal(t, ErrSheetNameInvalid, f.SetCellFloat("Sheet:1", "A1", 123.42, -1, 64)) } func TestSetCellUint(t *testing.T) { @@ -232,8 +232,8 @@ func TestSetCellValuesMultiByte(t *testing.T) { 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()) + assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.SetCellValue("Sheet1", "A", time.Now().UTC())) + assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.SetCellValue("Sheet1", "A", time.Duration(1e13))) // Test set cell value with column and row style inherit style1, err := f.NewStyle(&Style{NumFmt: 2}) assert.NoError(t, err) @@ -251,7 +251,7 @@ func TestSetCellValue(t *testing.T) { assert.Equal(t, "0.50", B2) // Test set cell value with invalid sheet name - assert.EqualError(t, f.SetCellValue("Sheet:1", "A1", "A1"), ErrSheetNameInvalid.Error()) + assert.Equal(t, ErrSheetNameInvalid, f.SetCellValue("Sheet:1", "A1", "A1")) // Test set cell value with unsupported charset shared strings table f.SharedStrings = nil f.Pkg.Store(defaultXMLPathSharedStrings, MacintoshCyrillicCharset) @@ -299,9 +299,9 @@ 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()) + assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.SetCellBool("Sheet1", "A", true)) // Test set cell boolean data type value with invalid sheet name - assert.EqualError(t, f.SetCellBool("Sheet:1", "A1", true), ErrSheetNameInvalid.Error()) + assert.Equal(t, ErrSheetNameInvalid, f.SetCellBool("Sheet:1", "A1", true)) } func TestSetCellTime(t *testing.T) { @@ -486,7 +486,7 @@ func TestGetCellValue(t *testing.T) { 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()) + assert.Equal(t, ErrSheetNameInvalid, err) } func TestGetCellType(t *testing.T) { @@ -499,10 +499,10 @@ func TestGetCellType(t *testing.T) { assert.NoError(t, err) assert.Equal(t, CellTypeSharedString, cellType) _, err = f.GetCellType("Sheet1", "A") - assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) + assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), err) // Test get cell type with invalid sheet name _, err = f.GetCellType("Sheet:1", "A1") - assert.EqualError(t, err, ErrSheetNameInvalid.Error()) + assert.Equal(t, ErrSheetNameInvalid, err) } func TestGetValueFrom(t *testing.T) { @@ -528,7 +528,7 @@ func TestGetCellFormula(t *testing.T) { // Test get cell formula with invalid sheet name _, err = f.GetCellFormula("Sheet:1", "A1") - assert.EqualError(t, err, ErrSheetNameInvalid.Error()) + assert.Equal(t, ErrSheetNameInvalid, err) // Test get cell formula on no formula cell assert.NoError(t, f.SetCellValue("Sheet1", "A1", true)) @@ -556,6 +556,25 @@ func TestGetCellFormula(t *testing.T) { formula, err := f.GetCellFormula("Sheet1", "B2") assert.NoError(t, err) assert.Equal(t, "", formula) + + // Test get array formula with invalid cell range reference + f = NewFile() + assert.NoError(t, f.AddChartSheet("Chart1", &Chart{Type: Line})) + _, err = f.NewSheet("Sheet2") + assert.NoError(t, err) + formulaType, ref := STCellFormulaTypeArray, "B1:B2" + assert.NoError(t, f.SetCellFormula("Sheet2", "B1", "A1:B2", FormulaOpts{Ref: &ref, Type: &formulaType})) + ws, ok := f.Sheet.Load("xl/worksheets/sheet3.xml") + assert.True(t, ok) + ws.(*xlsxWorksheet).SheetData.Row[0].C[1].F.Ref = ":" + _, err = f.getCellFormula("Sheet2", "A1", true) + assert.Equal(t, newCellNameToCoordinatesError("", newInvalidCellNameError("")), err) + + // Test set formula for the cells in array formula range with unsupported charset + f = NewFile() + f.Sheet.Delete("xl/worksheets/sheet1.xml") + f.Pkg.Store("xl/worksheets/sheet1.xml", MacintoshCyrillicCharset) + assert.EqualError(t, f.setArrayFormulaCells(), "XML syntax error on line 1: invalid UTF-8") } func ExampleFile_SetCellFloat() { @@ -614,10 +633,10 @@ func TestSetCellFormula(t *testing.T) { assert.NoError(t, f.SetCellFormula("Sheet1", "C19", "SUM(Sheet2!D2,Sheet2!D9)")) // Test set cell formula with invalid sheet name - assert.EqualError(t, f.SetCellFormula("Sheet:1", "A1", "SUM(1,2)"), ErrSheetNameInvalid.Error()) + assert.Equal(t, ErrSheetNameInvalid, f.SetCellFormula("Sheet:1", "A1", "SUM(1,2)")) // 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.Equal(t, newCellNameToCoordinatesError("C", newInvalidCellNameError("C")), f.SetCellFormula("Sheet1", "C", "SUM(Sheet2!D2,Sheet2!D9)")) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellFormula1.xlsx"))) assert.NoError(t, f.Close()) @@ -649,7 +668,7 @@ func TestSetCellFormula(t *testing.T) { ref = "D1:D5" assert.NoError(t, f.SetCellFormula("Sheet1", "D1", "=A1+C1", FormulaOpts{Ref: &ref, Type: &formulaType})) ref = "" - assert.EqualError(t, f.SetCellFormula("Sheet1", "D1", "=A1+C1", FormulaOpts{Ref: &ref, Type: &formulaType}), ErrParameterInvalid.Error()) + assert.Equal(t, ErrParameterInvalid, f.SetCellFormula("Sheet1", "D1", "=A1+C1", FormulaOpts{Ref: &ref, Type: &formulaType})) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellFormula5.xlsx"))) // Test set table formula for the cells @@ -661,6 +680,14 @@ func TestSetCellFormula(t *testing.T) { 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"))) + + // Test set array formula with invalid cell range reference + formulaType, ref = STCellFormulaTypeArray, ":" + assert.Equal(t, newCellNameToCoordinatesError("", newInvalidCellNameError("")), f.SetCellFormula("Sheet1", "B1", "A1:A2", FormulaOpts{Ref: &ref, Type: &formulaType})) + + // Test set array formula with invalid cell reference + formulaType, ref = STCellFormulaTypeArray, "A1:A2" + assert.Equal(t, ErrColumnNumber, f.SetCellFormula("Sheet1", "A1", "SUM(XFE1:XFE2)", FormulaOpts{Ref: &ref, Type: &formulaType})) } func TestGetCellRichText(t *testing.T) { @@ -742,7 +769,7 @@ func TestGetCellRichText(t *testing.T) { assert.EqualError(t, err, "sheet SheetN does not exist") // Test set cell rich text with illegal cell reference _, err = f.GetCellRichText("Sheet1", "A") - assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) + assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), err) // 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 @@ -759,7 +786,7 @@ func TestGetCellRichText(t *testing.T) { 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()) + assert.Equal(t, ErrSheetNameInvalid, err) } func TestSetCellRichText(t *testing.T) { @@ -858,7 +885,7 @@ func TestSetCellRichText(t *testing.T) { // 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()) + assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.SetCellRichText("Sheet1", "A", richTextRun)) richTextRun = []RichTextRun{{Text: strings.Repeat("s", TotalCellChars+1)}} // Test set cell rich text with characters over the maximum limit assert.EqualError(t, f.SetCellRichText("Sheet1", "A1", richTextRun), ErrCellCharsLength.Error()) diff --git a/chart.go b/chart.go index 5c7f8d75a5..a1078f7025 100644 --- a/chart.go +++ b/chart.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize diff --git a/col.go b/col.go index d5700bad20..b51a283a43 100644 --- a/col.go +++ b/col.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize diff --git a/crypt.go b/crypt.go index d2d6f46efa..8bbb58bf0d 100644 --- a/crypt.go +++ b/crypt.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize diff --git a/crypt_test.go b/crypt_test.go index c8735fc63b..d7fd0550c1 100644 --- a/crypt_test.go +++ b/crypt_test.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize @@ -39,7 +39,7 @@ func TestEncrypt(t *testing.T) { assert.NoError(t, err) raw[2050] = 3 _, err = Decrypt(raw, &Options{Password: "password"}) - assert.EqualError(t, err, ErrUnsupportedEncryptMechanism.Error()) + assert.Equal(t, ErrUnsupportedEncryptMechanism, err) // Test encrypt spreadsheet with invalid password assert.EqualError(t, f.SaveAs(filepath.Join("test", "Encryption.xlsx"), Options{Password: strings.Repeat("*", MaxFieldLength+1)}), ErrPasswordLengthInvalid.Error()) diff --git a/datavalidation.go b/datavalidation.go index 4a1f634276..8e6e5945a1 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize diff --git a/datavalidation_test.go b/datavalidation_test.go index 18816fbfd6..8816df0664 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize diff --git a/date.go b/date.go index c7ab5aa943..de39b9cfd4 100644 --- a/date.go +++ b/date.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize diff --git a/docProps.go b/docProps.go index 45957b7ed4..f6d1489ebe 100644 --- a/docProps.go +++ b/docProps.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize diff --git a/docProps_test.go b/docProps_test.go index 9ec66cf0cd..4456bfc36b 100644 --- a/docProps_test.go +++ b/docProps_test.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize diff --git a/drawing.go b/drawing.go index 0dbc06e56a..92481bf417 100644 --- a/drawing.go +++ b/drawing.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize diff --git a/drawing_test.go b/drawing_test.go index c8c95fd8bc..3c25057d29 100644 --- a/drawing_test.go +++ b/drawing_test.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize diff --git a/errors.go b/errors.go index 0d4aba06bf..b460dfd2da 100644 --- a/errors.go +++ b/errors.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize diff --git a/excelize.go b/excelize.go index 05049add98..b86957247b 100644 --- a/excelize.go +++ b/excelize.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. // // See https://xuri.me/excelize for more information about this package. package excelize @@ -30,6 +30,7 @@ import ( type File struct { mu sync.Mutex checked sync.Map + formulaChecked bool options *Options sharedStringItem [][]uint sharedStringsMap map[string]int diff --git a/file.go b/file.go index dc42e1e21f..894dd0ab9d 100644 --- a/file.go +++ b/file.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize diff --git a/lib.go b/lib.go index 44a6ee5104..a1e340c880 100644 --- a/lib.go +++ b/lib.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize diff --git a/merge.go b/merge.go index af34a55fff..9acf54d639 100644 --- a/merge.go +++ b/merge.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize diff --git a/numfmt.go b/numfmt.go index 2034318ae7..c4022ba039 100644 --- a/numfmt.go +++ b/numfmt.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize diff --git a/picture.go b/picture.go index a7d6a2e887..8b006f8e3e 100644 --- a/picture.go +++ b/picture.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize diff --git a/picture_test.go b/picture_test.go index d9414d3ff0..376f3997c6 100644 --- a/picture_test.go +++ b/picture_test.go @@ -60,7 +60,7 @@ func TestAddPicture(t *testing.T) { // Test add picture to worksheet from bytes 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", &Picture{Extension: ".png", File: file, Format: &GraphicOptions{AltText: "Excel Logo"}}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) + assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.AddPictureFromBytes("Sheet1", "A", &Picture{Extension: ".png", File: file, Format: &GraphicOptions{AltText: "Excel Logo"}})) for _, preset := range [][]string{{"Q8", "gif"}, {"Q15", "jpg"}, {"Q22", "tif"}, {"Q28", "bmp"}} { assert.NoError(t, f.AddPicture("Sheet1", preset[0], filepath.Join("test", "images", fmt.Sprintf("excel.%s", preset[1])), nil)) @@ -165,7 +165,7 @@ func TestGetPicture(t *testing.T) { // Try to get picture from a worksheet with illegal cell reference _, err = f.GetPictures("Sheet1", "A") - assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) + assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), err) // Try to get picture from a worksheet that doesn't contain any images pics, err = f.GetPictures("Sheet3", "I9") @@ -366,11 +366,11 @@ func TestDrawingResize(t *testing.T) { 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()) + assert.Equal(t, newCellNameToCoordinatesError("", newInvalidCellNameError("")), err) 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"), &GraphicOptions{AutoFit: true}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) + assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.AddPicture("Sheet1", "A1", filepath.Join("test", "images", "excel.jpg"), &GraphicOptions{AutoFit: true})) } func TestSetContentTypePartRelsExtensions(t *testing.T) { diff --git a/pivotTable.go b/pivotTable.go index 405378c6e7..0b6ad3b729 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize diff --git a/rows.go b/rows.go index 3869260890..b87d45dddf 100644 --- a/rows.go +++ b/rows.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize diff --git a/rows_test.go b/rows_test.go index cd72e745dd..ad15715430 100644 --- a/rows_test.go +++ b/rows_test.go @@ -239,7 +239,7 @@ func TestColumns(t *testing.T) { rows.decoder = f.xmlNewDecoder(bytes.NewReader([]byte(`1`))) assert.True(t, rows.Next()) _, err = rows.Columns() - assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) + assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), err) // Test token is nil rows.decoder = f.xmlNewDecoder(bytes.NewReader(nil)) diff --git a/shape.go b/shape.go index 5aa6ba2418..cc05bf3856 100644 --- a/shape.go +++ b/shape.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize diff --git a/sheet.go b/sheet.go index 1f3a0e0f6e..ff07231ae2 100644 --- a/sheet.go +++ b/sheet.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize diff --git a/sheet_test.go b/sheet_test.go index 044af91708..eb9e5712f7 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -181,7 +181,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, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) + assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), err) assert.Equal(t, []string(nil), result) f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`A`)) diff --git a/sheetpr.go b/sheetpr.go index 665541fff4..f9d517252e 100644 --- a/sheetpr.go +++ b/sheetpr.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize diff --git a/sheetview.go b/sheetview.go index 6edb87fe24..fbc2d1d67d 100644 --- a/sheetview.go +++ b/sheetview.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize diff --git a/slicer.go b/slicer.go index c1562ff05f..a7f26edab5 100644 --- a/slicer.go +++ b/slicer.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize diff --git a/sparkline.go b/sparkline.go index 1ecdd69758..5d872c3ef9 100644 --- a/sparkline.go +++ b/sparkline.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize diff --git a/stream.go b/stream.go index adf9b8ba0c..8fbbcfd72f 100644 --- a/stream.go +++ b/stream.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize diff --git a/styles.go b/styles.go index d90502aee9..489ce1de5c 100644 --- a/styles.go +++ b/styles.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize diff --git a/table.go b/table.go index d5d1fc45d3..ea49d3cb33 100644 --- a/table.go +++ b/table.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize diff --git a/templates.go b/templates.go index 7028a5b194..d9fb18c246 100644 --- a/templates.go +++ b/templates.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. // // This file contains default templates for XML files we don't yet populated // based on content. diff --git a/vml.go b/vml.go index 3c221b5b13..9250e17dee 100644 --- a/vml.go +++ b/vml.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize diff --git a/vmlDrawing.go b/vmlDrawing.go index c84c3c9186..2c3a69a92a 100644 --- a/vmlDrawing.go +++ b/vmlDrawing.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize diff --git a/vml_test.go b/vml_test.go index ad584029e5..be18d0930d 100644 --- a/vml_test.go +++ b/vml_test.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize diff --git a/workbook.go b/workbook.go index 5dd26f077b..44db6c9add 100644 --- a/workbook.go +++ b/workbook.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize diff --git a/xmlApp.go b/xmlApp.go index 9432886e11..10a93d4483 100644 --- a/xmlApp.go +++ b/xmlApp.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize diff --git a/xmlCalcChain.go b/xmlCalcChain.go index 81ee47a156..785bcba652 100644 --- a/xmlCalcChain.go +++ b/xmlCalcChain.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize diff --git a/xmlChart.go b/xmlChart.go index 60406b7d91..ee97a69765 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize diff --git a/xmlChartSheet.go b/xmlChartSheet.go index 47a26e5995..cc80b2d496 100644 --- a/xmlChartSheet.go +++ b/xmlChartSheet.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize diff --git a/xmlComments.go b/xmlComments.go index 8b997c2f3d..5eb328a812 100644 --- a/xmlComments.go +++ b/xmlComments.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize diff --git a/xmlContentTypes.go b/xmlContentTypes.go index beec2ba96f..0c00a54695 100644 --- a/xmlContentTypes.go +++ b/xmlContentTypes.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize diff --git a/xmlCore.go b/xmlCore.go index 7b9be61d41..ac8f7458a2 100644 --- a/xmlCore.go +++ b/xmlCore.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize diff --git a/xmlDecodeDrawing.go b/xmlDecodeDrawing.go index 8077af8454..8cd7625fe7 100644 --- a/xmlDecodeDrawing.go +++ b/xmlDecodeDrawing.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize diff --git a/xmlDrawing.go b/xmlDrawing.go index 41c25ee5ef..5cca3cf251 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize diff --git a/xmlPivotCache.go b/xmlPivotCache.go index 14b1db0afa..c04ebbd689 100644 --- a/xmlPivotCache.go +++ b/xmlPivotCache.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize diff --git a/xmlPivotTable.go b/xmlPivotTable.go index 8275bb6723..41405c3e85 100644 --- a/xmlPivotTable.go +++ b/xmlPivotTable.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize diff --git a/xmlSharedStrings.go b/xmlSharedStrings.go index f17e291f3a..6133ad4c06 100644 --- a/xmlSharedStrings.go +++ b/xmlSharedStrings.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize diff --git a/xmlSlicers.go b/xmlSlicers.go index 036f7a5272..6e68897562 100644 --- a/xmlSlicers.go +++ b/xmlSlicers.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize diff --git a/xmlStyles.go b/xmlStyles.go index 925573347c..02f4becc6f 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize diff --git a/xmlTable.go b/xmlTable.go index 79dd1b5709..a2bb2bc662 100644 --- a/xmlTable.go +++ b/xmlTable.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize diff --git a/xmlTheme.go b/xmlTheme.go index 5f46da9c99..ec0c2bda49 100644 --- a/xmlTheme.go +++ b/xmlTheme.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize diff --git a/xmlWorkbook.go b/xmlWorkbook.go index 423c9eb71d..6d489e9e83 100644 --- a/xmlWorkbook.go +++ b/xmlWorkbook.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize diff --git a/xmlWorksheet.go b/xmlWorksheet.go index a897718a0c..83e08054f5 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -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.16 or later. +// data. This library needs Go version 1.18 or later. package excelize @@ -477,6 +477,7 @@ type xlsxC struct { F *xlsxF `xml:"f"` // Formula V string `xml:"v,omitempty"` // Value IS *xlsxSI `xml:"is"` + f string } // xlsxF represents a formula for the cell. The formula expression is From 4eb3486682366a2df3ab1391b995b4285e493854 Mon Sep 17 00:00:00 2001 From: 327674413 <46646499+327674413@users.noreply.github.com> Date: Sun, 21 Jan 2024 00:05:28 +0800 Subject: [PATCH 849/957] This closes #1783, support set conditional formatting with multiple cell ranges (#1787) --- adjust.go | 4 ++-- styles.go | 55 +++++++++++++++++++++++++++++++++++++++++--------- styles_test.go | 8 ++++++-- 3 files changed, 54 insertions(+), 13 deletions(-) diff --git a/adjust.go b/adjust.go index bae49bc94a..9346bd1a6f 100644 --- a/adjust.go +++ b/adjust.go @@ -471,12 +471,12 @@ func (af *arrayFormulaOperandToken) setCoordinates() error { } var c, r int if col { - if cellRef.Row = TotalRows; i == 1 { + if cellRef.Row = TotalRows; i == 0 { cellRef.Row = 1 } } if row { - if cellRef.Col = MaxColumns; i == 1 { + if cellRef.Col = MaxColumns; i == 0 { cellRef.Col = 1 } } diff --git a/styles.go b/styles.go index 489ce1de5c..54635042ac 100644 --- a/styles.go +++ b/styles.go @@ -2760,13 +2760,9 @@ func (f *File) SetConditionalFormat(sheet, rangeRef string, opts []ConditionalFo if err != nil { return err } - if strings.Contains(rangeRef, ":") { - rect, err := rangeRefToCoordinates(rangeRef) - if err != nil { - return err - } - _ = sortCoordinates(rect) - rangeRef, _ = f.coordinatesToRangeRef(rect, strings.Contains(rangeRef, "$")) + SQRef, mastCell, err := prepareConditionalFormatRange(rangeRef) + if err != nil { + return err } // Create a pseudo GUID for each unique rule. var rules int @@ -2796,7 +2792,7 @@ func (f *File) SetConditionalFormat(sheet, rangeRef string, opts []ConditionalFo drawFunc, ok := drawContFmtFunc[vt] if ok { priority := rules + i - rule, x14rule := drawFunc(priority, ct, strings.Split(rangeRef, ":")[0], + rule, x14rule := drawFunc(priority, ct, mastCell, fmt.Sprintf("{00000000-0000-0000-%04X-%012X}", f.getSheetID(sheet), priority), &opt) if rule == nil { return ErrParameterInvalid @@ -2817,12 +2813,53 @@ func (f *File) SetConditionalFormat(sheet, rangeRef string, opts []ConditionalFo } ws.ConditionalFormatting = append(ws.ConditionalFormatting, &xlsxConditionalFormatting{ - SQRef: rangeRef, + SQRef: SQRef, CfRule: cfRule, }) return err } +// prepareConditionalFormatRange returns checked cell range and master cell +// reference by giving conditional formatting range reference. +func prepareConditionalFormatRange(rangeRef string) (string, string, error) { + var SQRef, mastCell string + if rangeRef == "" { + return SQRef, mastCell, ErrParameterRequired + } + rangeRef = strings.ReplaceAll(rangeRef, ",", " ") + for i, cellRange := range strings.Split(rangeRef, " ") { + var cellNames []string + for j, ref := range strings.Split(cellRange, ":") { + if j > 1 { + return SQRef, mastCell, ErrParameterInvalid + } + cellRef, col, row, err := parseRef(ref) + if err != nil { + return SQRef, mastCell, err + } + var c, r int + if col { + if cellRef.Row = TotalRows; j == 0 { + cellRef.Row = 1 + } + } + if row { + if cellRef.Col = MaxColumns; j == 0 { + cellRef.Col = 1 + } + } + c, r = cellRef.Col, cellRef.Row + cellName, _ := CoordinatesToCellName(c, r) + cellNames = append(cellNames, cellName) + if i == 0 && j == 0 { + mastCell = cellName + } + } + SQRef += strings.Join(cellNames, ":") + " " + } + return strings.TrimSuffix(SQRef, " "), mastCell, nil +} + // appendCfRule provides a function to append rules to conditional formatting. func (f *File) appendCfRule(ws *xlsxWorksheet, rule *xlsxX14CfRule) error { var ( diff --git a/styles_test.go b/styles_test.go index 9680fab704..9267da951e 100644 --- a/styles_test.go +++ b/styles_test.go @@ -178,6 +178,10 @@ func TestSetConditionalFormat(t *testing.T) { assert.NoError(t, f.SetConditionalFormat("Sheet1", ref, condFmts)) } f = NewFile() + // Test creating a conditional format without cell reference + assert.Equal(t, ErrParameterRequired, f.SetConditionalFormat("Sheet1", "", nil)) + // Test creating a conditional format with invalid cell reference + assert.Equal(t, ErrParameterInvalid, f.SetConditionalFormat("Sheet1", "A1:A2:A3", nil)) // Test creating a conditional format with existing extension lists ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") assert.True(t, ok) @@ -272,11 +276,11 @@ func TestGetConditionalFormats(t *testing.T) { {{Type: "icon_set", IconStyle: "3Arrows", ReverseIcons: true, IconsOnly: true}}, } { f := NewFile() - err := f.SetConditionalFormat("Sheet1", "A2:A1", format) + err := f.SetConditionalFormat("Sheet1", "A2:A1,B:B,2:2", format) assert.NoError(t, err) opts, err := f.GetConditionalFormats("Sheet1") assert.NoError(t, err) - assert.Equal(t, format, opts["A1:A2"]) + assert.Equal(t, format, opts["A2:A1 B1:B1048576 A2:XFD2"]) } // Test get multiple conditional formats f := NewFile() From 5399572353aa8da7cb37603b6bcdd09ec020ede1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?MELF=E6=99=93=E5=AE=87?= Date: Mon, 22 Jan 2024 09:41:57 +0800 Subject: [PATCH 850/957] This closes #1786, support set fill color of the chart (#1788) - Add a new field Fill in Chart, ChartPlotArea, and ChartMarker struct - Support set solid color or transparent fill for chart area, plot area, and maker --- chart_test.go | 12 +++++++++--- drawing.go | 43 ++++++++++++++++++++++++++----------------- xmlChart.go | 3 +++ 3 files changed, 38 insertions(+), 20 deletions(-) diff --git a/chart_test.go b/chart_test.go index 79501eeb7f..e70f06aa6a 100644 --- a/chart_test.go +++ b/chart_test.go @@ -158,7 +158,12 @@ func TestAddChart(t *testing.T) { {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"}, + { + Name: "Sheet1!$A$37", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$37:$D$37", + Marker: ChartMarker{ + Fill: Fill{Type: "pattern", Color: []string{"FFFF00"}, Pattern: 1}, + }, + }, } series2 := []ChartSeries{ { @@ -203,14 +208,15 @@ func TestAddChart(t *testing.T) { ShowPercent: true, ShowSerName: true, ShowVal: true, + Fill: Fill{Type: "pattern", Pattern: 1}, } for _, c := range []struct { sheetName, cell string opts *Chart }{ {sheetName: "Sheet1", cell: "P1", opts: &Chart{Type: Col, Series: series, Format: format, Legend: ChartLegend{Position: "none", ShowLegendKey: true}, Title: []RichTextRun{{Text: "2D Column Chart"}}, PlotArea: plotArea, Border: ChartLine{Type: ChartLineNone}, 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: []RichTextRun{{Text: "2D Stacked Column Chart"}}, PlotArea: plotArea, Border: ChartLine{Type: ChartLineAutomatic}, ShowBlanksAs: "zero"}}, - {sheetName: "Sheet1", cell: "P16", opts: &Chart{Type: ColPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "100% Stacked Column Chart"}}, PlotArea: plotArea, Border: ChartLine{Type: ChartLineSolid, Width: 2}, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "X1", opts: &Chart{Type: ColStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "2D Stacked Column Chart"}}, PlotArea: plotArea, Fill: Fill{Type: "pattern", Pattern: 1}, Border: ChartLine{Type: ChartLineAutomatic}, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "P16", opts: &Chart{Type: ColPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "100% Stacked Column Chart"}}, PlotArea: plotArea, Fill: Fill{Type: "pattern", Color: []string{"EEEEEE"}, Pattern: 1}, Border: ChartLine{Type: ChartLineSolid, Width: 2}, ShowBlanksAs: "zero"}}, {sheetName: "Sheet1", cell: "X16", opts: &Chart{Type: Col3DClustered, Series: series, Format: format, Legend: ChartLegend{Position: "bottom", ShowLegendKey: false}, Title: []RichTextRun{{Text: "3D Clustered Column Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet1", cell: "P30", opts: &Chart{Type: Col3DStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Stacked Column Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet1", cell: "X30", opts: &Chart{Type: Col3DPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D 100% Stacked Column Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, diff --git a/drawing.go b/drawing.go index 92481bf417..035ca4b072 100644 --- a/drawing.go +++ b/drawing.go @@ -107,6 +107,7 @@ func (f *File) addChart(opts *Chart, comboCharts []*Chart) { }, }, } + xlsxChartSpace.SpPr = f.drawShapeFill(opts.Fill, xlsxChartSpace.SpPr) plotAreaFunc := map[ChartType]func(*Chart) *cPlotArea{ Area: f.drawBaseChart, AreaStacked: f.drawBaseChart, @@ -167,6 +168,7 @@ func (f *File) addChart(opts *Chart, comboCharts []*Chart) { if opts.Legend.Position == "none" { xlsxChartSpace.Chart.Legend = nil } + xlsxChartSpace.Chart.PlotArea.SpPr = f.drawShapeFill(opts.PlotArea.Fill, xlsxChartSpace.Chart.PlotArea.SpPr) addChart := func(c, p *cPlotArea) { immutable, mutable := reflect.ValueOf(c).Elem(), reflect.ValueOf(p).Elem() for i := 0; i < mutable.NumField(); i++ { @@ -727,19 +729,28 @@ func (f *File) drawChartSeries(opts *Chart) *[]cSer { return &ser } +// drawShapeFill provides a function to draw the a:solidFill element by given +// fill format sets. +func (f *File) drawShapeFill(fill Fill, spPr *cSpPr) *cSpPr { + if fill.Type == "pattern" && fill.Pattern == 1 { + if spPr == nil { + spPr = &cSpPr{} + } + if len(fill.Color) == 1 { + spPr.SolidFill = &aSolidFill{SrgbClr: &attrValString{Val: stringPtr(strings.TrimPrefix(fill.Color[0], "#"))}} + return spPr + } + spPr.SolidFill = nil + spPr.NoFill = stringPtr("") + } + return spPr +} + // drawChartSeriesSpPr provides a function to draw the c:spPr element by given // format sets. func (f *File) drawChartSeriesSpPr(i int, opts *Chart) *cSpPr { - var srgbClr *attrValString - var schemeClr *aSchemeClr - - 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}} + spPr := &cSpPr{SolidFill: &aSolidFill{SchemeClr: &aSchemeClr{Val: "accent" + strconv.Itoa((opts.order+i)%6+1)}}} + spPr = f.drawShapeFill(opts.Series[i].Fill, spPr) spPrScatter := &cSpPr{ Ln: &aLn{ W: 25400, @@ -748,12 +759,9 @@ func (f *File) drawChartSeriesSpPr(i int, opts *Chart) *cSpPr { } spPrLine := &cSpPr{ Ln: &aLn{ - W: f.ptToEMUs(opts.Series[i].Line.Width), - Cap: "rnd", // rnd, sq, flat - SolidFill: &aSolidFill{ - SchemeClr: schemeClr, - SrgbClr: srgbClr, - }, + W: f.ptToEMUs(opts.Series[i].Line.Width), + Cap: "rnd", // rnd, sq, flat + SolidFill: spPr.SolidFill, }, } if chartSeriesSpPr, ok := map[ChartType]*cSpPr{ @@ -761,7 +769,7 @@ func (f *File) drawChartSeriesSpPr(i int, opts *Chart) *cSpPr { }[opts.Type]; ok { return chartSeriesSpPr } - if srgbClr != nil { + if spPr.SolidFill.SrgbClr != nil { return spPr } return nil @@ -857,6 +865,7 @@ func (f *File) drawChartSeriesMarker(i int, opts *Chart) *cMarker { }, } } + marker.SpPr = f.drawShapeFill(opts.Series[i].Marker.Fill, marker.SpPr) chartSeriesMarker := map[ChartType]*cMarker{Scatter: marker, Line: marker} return chartSeriesMarker[opts.Type] } diff --git a/xmlChart.go b/xmlChart.go index ee97a69765..c5d601e0eb 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -561,6 +561,7 @@ type ChartPlotArea struct { ShowPercent bool ShowSerName bool ShowVal bool + Fill Fill NumFmt ChartNumFmt } @@ -576,6 +577,7 @@ type Chart struct { XAxis ChartAxis YAxis ChartAxis PlotArea ChartPlotArea + Fill Fill Border ChartLine ShowBlanksAs string BubbleSize int @@ -591,6 +593,7 @@ type ChartLegend struct { // ChartMarker directly maps the format settings of the chart marker. type ChartMarker struct { + Fill Fill Symbol string Size int } From 9b078980df40dc6877f7057b113b3f999f57adee Mon Sep 17 00:00:00 2001 From: L4nn15ter <50442451+L4nn15ter@users.noreply.github.com> Date: Wed, 24 Jan 2024 14:01:56 +0800 Subject: [PATCH 851/957] This closes #1789, delete VML shape on delete comment (#1790) - Improve delete cell comment shape compatibility with KingSoft WPS - Update unit test --- vml.go | 150 ++++++++++++++++++++++++++++------------------------ vml_test.go | 4 ++ 2 files changed, 86 insertions(+), 68 deletions(-) diff --git a/vml.go b/vml.go index 9250e17dee..3f0f470ce2 100644 --- a/vml.go +++ b/vml.go @@ -130,13 +130,14 @@ func (f *File) AddComment(sheet string, opts Comment) error { // // err := f.DeleteComment("Sheet1", "A30") func (f *File) DeleteComment(sheet, cell string) error { - if err := checkSheetName(sheet); err != nil { + ws, err := f.workSheetReader(sheet) + if err != nil { return err } - sheetXMLPath, ok := f.getSheetXMLPath(sheet) - if !ok { - return ErrSheetNotExist{sheet} + if ws.LegacyDrawing == nil { + return err } + sheetXMLPath, _ := f.getSheetXMLPath(sheet) commentsXML := f.getSheetComments(filepath.Base(sheetXMLPath)) if !strings.HasPrefix(commentsXML, "/") { commentsXML = "xl" + strings.TrimPrefix(commentsXML, "..") @@ -164,6 +165,82 @@ func (f *File) DeleteComment(sheet, cell string) error { } f.Comments[commentsXML] = cmts } + sheetRelationshipsDrawingVML := f.getSheetRelationshipsTargetByID(sheet, ws.LegacyDrawing.RID) + return f.deleteFormControl(sheetRelationshipsDrawingVML, cell, true) +} + +// deleteFormControl provides the method to delete shape from +// xl/drawings/vmlDrawing%d.xml by giving path, cell and shape type. +func (f *File) deleteFormControl(sheetRelationshipsDrawingVML, cell string, isComment bool) error { + col, row, err := CellNameToCoordinates(cell) + if err != nil { + return err + } + vmlID, _ := strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(sheetRelationshipsDrawingVML, "../drawings/vmlDrawing"), ".vml")) + drawingVML := strings.ReplaceAll(sheetRelationshipsDrawingVML, "..", "xl") + vml := f.VMLDrawing[drawingVML] + if vml == nil { + vml = &vmlDrawing{ + XMLNSv: "urn:schemas-microsoft-com:vml", + XMLNSo: "urn:schemas-microsoft-com:office:office", + XMLNSx: "urn:schemas-microsoft-com:office:excel", + XMLNSmv: "http://macVmlSchemaUri", + ShapeLayout: &xlsxShapeLayout{ + Ext: "edit", IDmap: &xlsxIDmap{Ext: "edit", Data: vmlID}, + }, + ShapeType: &xlsxShapeType{ + Stroke: &xlsxStroke{JoinStyle: "miter"}, + VPath: &vPath{GradientShapeOK: "t", ConnectType: "rect"}, + }, + } + // Load exist VML shapes from xl/drawings/vmlDrawing%d.vml + d, err := f.decodeVMLDrawingReader(drawingVML) + if err != nil { + return err + } + if d != nil { + vml.ShapeType.ID = d.ShapeType.ID + vml.ShapeType.CoordSize = d.ShapeType.CoordSize + vml.ShapeType.Spt = d.ShapeType.Spt + vml.ShapeType.Path = d.ShapeType.Path + for _, v := range d.Shape { + s := xlsxShape{ + ID: v.ID, + Type: v.Type, + Style: v.Style, + Button: v.Button, + Filled: v.Filled, + FillColor: v.FillColor, + InsetMode: v.InsetMode, + Stroked: v.Stroked, + StrokeColor: v.StrokeColor, + Val: v.Val, + } + vml.Shape = append(vml.Shape, s) + } + } + } + cond := func(objectType string) bool { + if isComment { + return objectType == "Note" + } + return objectType != "Note" + } + for i, sp := range vml.Shape { + var shapeVal decodeShapeVal + if err = xml.Unmarshal([]byte(fmt.Sprintf("%s", sp.Val)), &shapeVal); err == nil && + cond(shapeVal.ClientData.ObjectType) && shapeVal.ClientData.Anchor != "" { + leftCol, topRow, err := extractAnchorCell(shapeVal.ClientData.Anchor) + if err != nil { + return err + } + if leftCol == col-1 && topRow == row-1 { + vml.Shape = append(vml.Shape[:i], vml.Shape[i+1:]...) + break + } + } + } + f.VMLDrawing[drawingVML] = vml return err } @@ -375,74 +452,11 @@ func (f *File) DeleteFormControl(sheet, cell string) error { if err != nil { return err } - col, row, err := CellNameToCoordinates(cell) - if err != nil { - return err - } if ws.LegacyDrawing == nil { return err } sheetRelationshipsDrawingVML := f.getSheetRelationshipsTargetByID(sheet, ws.LegacyDrawing.RID) - vmlID, _ := strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(sheetRelationshipsDrawingVML, "../drawings/vmlDrawing"), ".vml")) - drawingVML := strings.ReplaceAll(sheetRelationshipsDrawingVML, "..", "xl") - vml := f.VMLDrawing[drawingVML] - if vml == nil { - vml = &vmlDrawing{ - XMLNSv: "urn:schemas-microsoft-com:vml", - XMLNSo: "urn:schemas-microsoft-com:office:office", - XMLNSx: "urn:schemas-microsoft-com:office:excel", - XMLNSmv: "http://macVmlSchemaUri", - ShapeLayout: &xlsxShapeLayout{ - Ext: "edit", IDmap: &xlsxIDmap{Ext: "edit", Data: vmlID}, - }, - ShapeType: &xlsxShapeType{ - Stroke: &xlsxStroke{JoinStyle: "miter"}, - VPath: &vPath{GradientShapeOK: "t", ConnectType: "rect"}, - }, - } - // Load exist VML shapes from xl/drawings/vmlDrawing%d.vml - d, err := f.decodeVMLDrawingReader(drawingVML) - if err != nil { - return err - } - if d != nil { - vml.ShapeType.ID = d.ShapeType.ID - vml.ShapeType.CoordSize = d.ShapeType.CoordSize - vml.ShapeType.Spt = d.ShapeType.Spt - vml.ShapeType.Path = d.ShapeType.Path - for _, v := range d.Shape { - s := xlsxShape{ - ID: v.ID, - Type: v.Type, - Style: v.Style, - Button: v.Button, - Filled: v.Filled, - FillColor: v.FillColor, - InsetMode: v.InsetMode, - Stroked: v.Stroked, - StrokeColor: v.StrokeColor, - Val: v.Val, - } - vml.Shape = append(vml.Shape, s) - } - } - } - for i, sp := range vml.Shape { - var shapeVal decodeShapeVal - if err = xml.Unmarshal([]byte(fmt.Sprintf("%s", sp.Val)), &shapeVal); err == nil && - shapeVal.ClientData.ObjectType != "Note" && shapeVal.ClientData.Anchor != "" { - leftCol, topRow, err := extractAnchorCell(shapeVal.ClientData.Anchor) - if err != nil { - return err - } - if leftCol == col-1 && topRow == row-1 { - vml.Shape = append(vml.Shape[:i], vml.Shape[i+1:]...) - break - } - } - } - f.VMLDrawing[drawingVML] = vml - return err + return f.deleteFormControl(sheetRelationshipsDrawingVML, cell, false) } // countVMLDrawing provides a function to get VML drawing files count storage diff --git a/vml_test.go b/vml_test.go index be18d0930d..d05b67f412 100644 --- a/vml_test.go +++ b/vml_test.go @@ -122,6 +122,10 @@ func TestDeleteComment(t *testing.T) { 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") + + f = NewFile() + // Test delete comment on a no comments worksheet + assert.NoError(t, f.DeleteComment("Sheet1", "A1")) } func TestDecodeVMLDrawingReader(t *testing.T) { From e4497c494d0150c9e5c4c58e948743dadbf219fb Mon Sep 17 00:00:00 2001 From: Jerry Date: Thu, 25 Jan 2024 14:39:21 +0800 Subject: [PATCH 852/957] ref #65, new formula function: DBCS (#1791) Co-authored-by: wujierui --- calc.go | 33 +++++++++++++++++++++++++++++++++ calc_test.go | 15 +++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/calc.go b/calc.go index 2488eda9f7..a6086a063c 100644 --- a/calc.go +++ b/calc.go @@ -455,6 +455,7 @@ type formulaFuncs struct { // DAYS // DAYS360 // DB +// DBCS // DCOUNT // DCOUNTA // DDB @@ -13566,6 +13567,38 @@ func (fn *formulaFuncs) concat(name string, argsList *list.List) formulaArg { return newStringFormulaArg(buf.String()) } +// DBCS converts half-width (single-byte) letters within a character string to +// full-width (double-byte) characters. The syntax of the function is: +// +// DBCS(text) +func (fn *formulaFuncs) DBCS(argsList *list.List) formulaArg { + if argsList.Len() != 1 { + return newErrorFormulaArg(formulaErrorVALUE, "DBCS requires 1 argument") + } + arg := argsList.Front().Value.(formulaArg) + if arg.Type == ArgError { + return arg + } + if fn.f.options.CultureInfo == CultureNameZhCN { + var chars []string + for _, r := range arg.Value() { + code := r + if code == 32 { + code = 12288 + } else { + code += 65248 + } + if (code < 32 || code > 126) && r != 165 && code < 65381 { + chars = append(chars, string(code)) + } else { + chars = append(chars, string(r)) + } + } + return newStringFormulaArg(strings.Join(chars, "")) + } + return arg +} + // EXACT function tests if two supplied text strings or values are exactly // equal and if so, returns TRUE; Otherwise, the function returns FALSE. The // function is case-sensitive. The syntax of the function is: diff --git a/calc_test.go b/calc_test.go index 99b1eb26c4..83b578f4cb 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1723,6 +1723,10 @@ func TestCalcCellValue(t *testing.T) { "=CONCATENATE(TRUE(),1,FALSE(),\"0\",INT(2))": "TRUE1FALSE02", "=CONCATENATE(MUNIT(2))": "1001", "=CONCATENATE(A1:B2)": "1425", + // DBCS + "=DBCS(\"\")": "", + "=DBCS(123.456)": "123.456", + "=DBCS(\"123.456\")": "123.456", // EXACT "=EXACT(1,\"1\")": "TRUE", "=EXACT(1,1)": "TRUE", @@ -3836,6 +3840,9 @@ func TestCalcCellValue(t *testing.T) { // CONCATENATE "=CONCATENATE(NA())": {"#N/A", "#N/A"}, "=CONCATENATE(1,1/0)": {"#DIV/0!", "#DIV/0!"}, + // DBCS + "=DBCS(NA())": {"#N/A", "#N/A"}, + "=DBCS()": {"#VALUE!", "DBCS requires 1 argument"}, // EXACT "=EXACT()": {"#VALUE!", "EXACT requires 2 arguments"}, "=EXACT(1,2,3)": {"#VALUE!", "EXACT requires 2 arguments"}, @@ -5194,6 +5201,14 @@ func TestCalcDatabase(t *testing.T) { } } +func TestCalcDBCS(t *testing.T) { + f := NewFile(Options{CultureInfo: CultureNameZhCN}) + assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "=DBCS(\"`~·!@#$¥%…^&*()_-+=[]{}\\|;:'\"\"<,>.?/01234567890 abc ABC \uff65\uff9e\uff9f \uff74\uff78\uff7e\uff99\")")) + result, err := f.CalcCellValue("Sheet1", "A1") + assert.NoError(t, err) + assert.Equal(t, "\uff40\uff5e\u00b7\uff01\uff20\uff03\uff04\u00a5\uff05\u2026\uff3e\uff06\uff0a\uff08\uff09\uff3f\uff0d\uff0b\uff1d\uff3b\uff3d\uff5b\uff5d\uff3c\uff5c\uff1b\uff1a\uff07\uff02\uff1c\uff0c\uff1e\uff0e\uff1f\uff0f\uff10\uff11\uff12\uff13\uff14\uff15\uff16\uff17\uff18\uff19\uff10\u3000\uff41\uff42\uff43\u3000\uff21\uff22\uff23\u3000\uff65\uff9e\uff9f\u3000\uff74\uff78\uff7e\uff99", result) +} + func TestCalcFORMULATEXT(t *testing.T) { f, formulaText := NewFile(), "=SUM(B1:C1)" assert.NoError(t, f.SetCellFormula("Sheet1", "A1", formulaText)) From 9a6855375e81067021ff0808f3b7a0c36d68afca Mon Sep 17 00:00:00 2001 From: cherry Date: Mon, 29 Jan 2024 10:18:21 +0800 Subject: [PATCH 853/957] This closes #1792, support to update defined names reference when rename worksheet (#1797) --- adjust.go | 27 +++++++++++++++++++++++++++ sheet.go | 6 ++++++ sheet_test.go | 19 +++++++++++++++++++ 3 files changed, 52 insertions(+) diff --git a/adjust.go b/adjust.go index 9346bd1a6f..5d6004067c 100644 --- a/adjust.go +++ b/adjust.go @@ -454,6 +454,33 @@ func (f *File) adjustFormulaRef(sheet, sheetN, formula string, keepRelative bool return val, nil } +// adjustRangeSheetName returns replaced range reference by given source and +// target sheet name. +func adjustRangeSheetName(rng, source, target string) string { + cellRefs := strings.Split(rng, ",") + for i, cellRef := range cellRefs { + rangeRefs := strings.Split(cellRef, ":") + for j, rangeRef := range rangeRefs { + parts := strings.Split(rangeRef, "!") + for k, part := range parts { + singleQuote := strings.HasPrefix(part, "'") && strings.HasSuffix(part, "'") + if singleQuote { + part = strings.TrimPrefix(strings.TrimSuffix(part, "'"), "'") + } + if part == source { + if part = target; singleQuote { + part = "'" + part + "'" + } + } + parts[k] = part + } + rangeRefs[j] = strings.Join(parts, "!") + } + cellRefs[i] = strings.Join(rangeRefs, ":") + } + return strings.Join(cellRefs, ",") +} + // arrayFormulaOperandToken defines meta fields for transforming the array // formula to the normal formula. type arrayFormulaOperandToken struct { diff --git a/sheet.go b/sheet.go index ff07231ae2..315507dbfa 100644 --- a/sheet.go +++ b/sheet.go @@ -374,6 +374,12 @@ func (f *File) SetSheetName(source, target string) error { delete(f.sheetMap, source) } } + if wb.DefinedNames == nil { + return err + } + for i, dn := range wb.DefinedNames.DefinedName { + wb.DefinedNames.DefinedName[i].Data = adjustRangeSheetName(dn.Data, source, target) + } return err } diff --git a/sheet_test.go b/sheet_test.go index eb9e5712f7..271262ee1c 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -467,6 +467,25 @@ func TestSetSheetName(t *testing.T) { 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()) + + // Test set worksheet name with existing defined name and auto filter + assert.NoError(t, f.AutoFilter("Sheet1", "A1:A2", nil)) + assert.NoError(t, f.SetDefinedName(&DefinedName{ + Name: "Name1", + RefersTo: "$B$2", + })) + assert.NoError(t, f.SetDefinedName(&DefinedName{ + Name: "Name2", + RefersTo: "$A1$2:A2", + })) + assert.NoError(t, f.SetDefinedName(&DefinedName{ + Name: "Name3", + RefersTo: "Sheet1!$A$1:'Sheet1'!A1:Sheet1!$A$1,Sheet1!A1:Sheet3!A1,Sheet3!A1", + })) + assert.NoError(t, f.SetSheetName("Sheet1", "Sheet2")) + for i, expected := range []string{"'Sheet2'!$A$1:$A$2", "$B$2", "$A1$2:A2", "Sheet2!$A$1:'Sheet2'!A1:Sheet2!$A$1,Sheet2!A1:Sheet3!A1,Sheet3!A1"} { + assert.Equal(t, expected, f.WorkBook.DefinedNames.DefinedName[i].Data) + } } func TestWorksheetWriter(t *testing.T) { From 99e91e19efc562828af58fb9fde33b2e5d01f9a0 Mon Sep 17 00:00:00 2001 From: xxxwang1983 <132745901+xxxwang1983@users.noreply.github.com> Date: Tue, 30 Jan 2024 09:58:24 +0800 Subject: [PATCH 854/957] This closes #1794, add new GetBaseColor function (#1798) Co-authored-by: wangjingwei --- styles.go | 47 +++++++++++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/styles.go b/styles.go index 54635042ac..91e4c5728d 100644 --- a/styles.go +++ b/styles.go @@ -1369,14 +1369,11 @@ var ( } ) -// getThemeColor provides a function to convert theme color or index color to -// RGB color. -func (f *File) getThemeColor(clr *xlsxColor) string { - var RGB string - if clr == nil || f.Theme == nil { - return RGB - } - if clrScheme := f.Theme.ThemeElements.ClrScheme; clr.Theme != nil { +// GetBaseColor returns the preferred hex color code by giving hex color code, +// indexed color, and theme color. +func (f *File) GetBaseColor(hexColor string, indexedColor int, themeColor *int) string { + if f.Theme != nil && themeColor != nil { + clrScheme := f.Theme.ThemeElements.ClrScheme if val, ok := map[int]*string{ 0: &clrScheme.Lt1.SysClr.LastClr, 1: &clrScheme.Dk1.SysClr.LastClr, @@ -1388,21 +1385,35 @@ func (f *File) getThemeColor(clr *xlsxColor) string { 7: clrScheme.Accent4.SrgbClr.Val, 8: clrScheme.Accent5.SrgbClr.Val, 9: clrScheme.Accent6.SrgbClr.Val, - }[*clr.Theme]; ok && val != nil { - return strings.TrimPrefix(ThemeColor(*val, clr.Tint), "FF") + }[*themeColor]; ok && val != nil { + return *val } } - if len(clr.RGB) == 6 { - return clr.RGB + if len(hexColor) == 6 { + return hexColor + } + if len(hexColor) == 8 { + return strings.TrimPrefix(hexColor, "FF") + } + if f.Styles != nil && f.Styles.Colors != nil && f.Styles.Colors.IndexedColors != nil && + indexedColor < len(f.Styles.Colors.IndexedColors.RgbColor) { + return strings.TrimPrefix(f.Styles.Colors.IndexedColors.RgbColor[indexedColor].RGB, "FF") } - if len(clr.RGB) == 8 { - return strings.TrimPrefix(clr.RGB, "FF") + if indexedColor < len(IndexedColorMapping) { + return IndexedColorMapping[indexedColor] } - if f.Styles.Colors != nil && f.Styles.Colors.IndexedColors != nil && clr.Indexed < len(f.Styles.Colors.IndexedColors.RgbColor) { - return strings.TrimPrefix(ThemeColor(strings.TrimPrefix(f.Styles.Colors.IndexedColors.RgbColor[clr.Indexed].RGB, "FF"), clr.Tint), "FF") + return hexColor +} + +// getThemeColor provides a function to convert theme color or index color to +// RGB color. +func (f *File) getThemeColor(clr *xlsxColor) string { + var RGB string + if clr == nil || f.Theme == nil { + return RGB } - if clr.Indexed < len(IndexedColorMapping) { - return strings.TrimPrefix(ThemeColor(IndexedColorMapping[clr.Indexed], clr.Tint), "FF") + if RGB = f.GetBaseColor(clr.RGB, clr.Indexed, clr.Theme); RGB != "" { + RGB = strings.TrimPrefix(ThemeColor(RGB, clr.Tint), "FF") } return RGB } From a258e3d8580c5a4d16454bc4642d2a1704dd0ed9 Mon Sep 17 00:00:00 2001 From: funa12 <21205286+funa12@users.noreply.github.com> Date: Fri, 2 Feb 2024 11:11:16 +0900 Subject: [PATCH 855/957] Fix CalcCellValue does not return raw value when enable RawCellValue (#1803) --- calc.go | 2 +- calc_test.go | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/calc.go b/calc.go index a6086a063c..ef1b0c0136 100644 --- a/calc.go +++ b/calc.go @@ -823,7 +823,7 @@ func (f *File) CalcCellValue(sheet, cell string, opts ...Options) (result string styleIdx, _ = f.GetCellStyle(sheet, cell) } result = token.Value() - if isNum, precision, decimal := isNumeric(result); isNum { + if isNum, precision, decimal := isNumeric(result); isNum && !rawCellValue { if precision > 15 { result, err = f.formattedValue(&xlsxC{S: styleIdx, V: strings.ToUpper(strconv.FormatFloat(decimal, 'G', 15, 64))}, rawCellValue, CellTypeNumber) return diff --git a/calc_test.go b/calc_test.go index 83b578f4cb..f6b37905b4 100644 --- a/calc_test.go +++ b/calc_test.go @@ -6301,6 +6301,28 @@ func TestNestedFunctionsWithOperators(t *testing.T) { } } +func TestFormulaRawCellValueOption(t *testing.T) { + f := NewFile() + rawTest := []struct { + value string + raw bool + expected string + }{ + {"=\"10e3\"", false, "10000"}, + {"=\"10e3\"", true, "10e3"}, + {"=\"10\" & \"e3\"", false, "10000"}, + {"=\"10\" & \"e3\"", true, "10e3"}, + {"=\"1111111111111111\"", false, "1.11111111111111E+15"}, + {"=\"1111111111111111\"", true, "1111111111111111"}, + } + for _, test := range rawTest { + assert.NoError(t, f.SetCellFormula("Sheet1", "A1", test.value)) + val, err := f.CalcCellValue("Sheet1", "A1", Options{RawCellValue: test.raw}) + assert.NoError(t, err) + assert.Equal(t, test.expected, val) + } +} + func TestFormulaArgToToken(t *testing.T) { assert.Equal(t, efp.Token{ From bba155e06da987f631278dba8c80545713b4a3b7 Mon Sep 17 00:00:00 2001 From: coolbit <46238171+coolbit@users.noreply.github.com> Date: Sun, 4 Feb 2024 22:31:03 +0800 Subject: [PATCH 856/957] This closes #1805, support set chart axis font family, size and strike style (#1809) - Update unit test workflow dependencies package version --- .github/workflows/go.yml | 2 +- chart_test.go | 2 +- drawing.go | 48 +++++++++++++++++++++++++--------------- 3 files changed, 32 insertions(+), 20 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index ff0c8d07d0..5eed9fd7f5 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@v3 + uses: codecov/codecov-action@v4 with: file: coverage.txt flags: unittests diff --git a/chart_test.go b/chart_test.go index e70f06aa6a..969b57fc7c 100644 --- a/chart_test.go +++ b/chart_test.go @@ -214,7 +214,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: []RichTextRun{{Text: "2D Column Chart"}}, PlotArea: plotArea, Border: ChartLine{Type: ChartLineNone}, 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: "P1", opts: &Chart{Type: Col, Series: series, Format: format, Legend: ChartLegend{Position: "none", ShowLegendKey: true}, Title: []RichTextRun{{Text: "2D Column Chart"}}, PlotArea: plotArea, Border: ChartLine{Type: ChartLineNone}, ShowBlanksAs: "zero", XAxis: ChartAxis{Font: Font{Bold: true, Italic: true, Underline: "dbl", Family: "Times New Roman", Size: 15, Strike: true, 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: []RichTextRun{{Text: "2D Stacked Column Chart"}}, PlotArea: plotArea, Fill: Fill{Type: "pattern", Pattern: 1}, Border: ChartLine{Type: ChartLineAutomatic}, ShowBlanksAs: "zero"}}, {sheetName: "Sheet1", cell: "P16", opts: &Chart{Type: ColPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "100% Stacked Column Chart"}}, PlotArea: plotArea, Fill: Fill{Type: "pattern", Color: []string{"EEEEEE"}, Pattern: 1}, Border: ChartLine{Type: ChartLineSolid, Width: 2}, ShowBlanksAs: "zero"}}, {sheetName: "Sheet1", cell: "X16", opts: &Chart{Type: Col3DClustered, Series: series, Format: format, Legend: ChartLegend{Position: "bottom", ShowLegendKey: false}, Title: []RichTextRun{{Text: "3D Clustered Column Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, diff --git a/drawing.go b/drawing.go index 035ca4b072..bc7b8a47aa 100644 --- a/drawing.go +++ b/drawing.go @@ -1155,6 +1155,34 @@ func (f *File) drawPlotAreaSerAx(opts *Chart) []*cAxs { } } +// drawChartFont provides a function to draw the a:rPr element. +func drawChartFont(fnt *Font, r *aRPr) { + if fnt == nil { + return + } + r.B = fnt.Bold + r.I = fnt.Italic + if idx := inStrSlice(supportedDrawingUnderlineTypes, fnt.Underline, true); idx != -1 { + r.U = supportedDrawingUnderlineTypes[idx] + } + if fnt.Color != "" { + if r.SolidFill == nil { + r.SolidFill = &aSolidFill{} + } + r.SolidFill.SchemeClr = nil + r.SolidFill.SrgbClr = &attrValString{Val: stringPtr(strings.ReplaceAll(strings.ToUpper(fnt.Color), "#", ""))} + } + if fnt.Family != "" { + r.Latin.Typeface = fnt.Family + } + if fnt.Size > 0 { + r.Sz = fnt.Size * 100 + } + if fnt.Strike { + r.Strike = "sngStrike" + } +} + // drawPlotAreaTitles provides a function to draw the c:title element. func (f *File) drawPlotAreaTitles(runs []RichTextRun, vert string) *cTitle { if len(runs) == 0 { @@ -1163,15 +1191,7 @@ func (f *File) drawPlotAreaTitles(runs []RichTextRun, vert string) *cTitle { 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 - } - } + drawChartFont(run.Font, &r.RPr) title.Tx.Rich.P = append(title.Tx.Rich.P, aP{ PPr: &aPPr{DefRPr: aRPr{}}, R: r, @@ -1241,15 +1261,7 @@ func (f *File) drawPlotAreaTxPr(opts *ChartAxis) *cTxPr { }, } if opts != nil { - 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.Font.Color != "" { - cTxPr.P.PPr.DefRPr.SolidFill.SchemeClr = nil - cTxPr.P.PPr.DefRPr.SolidFill.SrgbClr = &attrValString{Val: stringPtr(strings.ReplaceAll(strings.ToUpper(opts.Font.Color), "#", ""))} - } + drawChartFont(&opts.Font, &cTxPr.P.PPr.DefRPr) } return cTxPr } From 9cbe3b6bd085a5b45ade85ba70ef578ab31a0b0f Mon Sep 17 00:00:00 2001 From: zhukewen Date: Mon, 5 Feb 2024 00:06:38 +0800 Subject: [PATCH 857/957] This closes #1807, calculation engine support date and formula type cell (#1810) Co-authored-by: zhualong <274131322@qq.com> --- .github/workflows/go.yml | 2 ++ calc.go | 14 +++++++++++++- calc_test.go | 29 +++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 5eed9fd7f5..2743d60c3f 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -32,6 +32,8 @@ jobs: - name: Codecov uses: codecov/codecov-action@v4 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} with: file: coverage.txt flags: unittests diff --git a/calc.go b/calc.go index ef1b0c0136..59c88852c1 100644 --- a/calc.go +++ b/calc.go @@ -1632,8 +1632,20 @@ func (f *File) cellResolver(ctx *calcContext, sheet, cell string) (formulaArg, e return arg.ToNumber(), err case CellTypeInlineString, CellTypeSharedString: return arg, err - default: + case CellTypeFormula: + if value != "" { + return arg, err + } return newEmptyFormulaArg(), err + case CellTypeDate: + if value, err = f.GetCellValue(sheet, cell); err == nil { + if num := newStringFormulaArg(value).ToNumber(); num.Type == ArgNumber { + return num, err + } + } + return arg, err + default: + return newErrorFormulaArg(value, value), err } } diff --git a/calc_test.go b/calc_test.go index f6b37905b4..350e516511 100644 --- a/calc_test.go +++ b/calc_test.go @@ -6379,6 +6379,35 @@ func TestCalcCellResolver(t *testing.T) { assert.NoError(t, err) assert.Equal(t, expected, result) } + // Test calculates formula that reference date and error type cells + assert.NoError(t, f.SetCellValue("Sheet1", "C1", "20200208T080910.123")) + assert.NoError(t, f.SetCellValue("Sheet1", "C2", "2020-07-10 15:00:00.000")) + assert.NoError(t, f.SetCellValue("Sheet1", "C3", formulaErrorDIV)) + ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") + assert.True(t, ok) + ws.(*xlsxWorksheet).SheetData.Row[0].C[2].T = "d" + ws.(*xlsxWorksheet).SheetData.Row[0].C[2].V = "20200208T080910.123" + ws.(*xlsxWorksheet).SheetData.Row[1].C[2].T = "d" + ws.(*xlsxWorksheet).SheetData.Row[1].C[2].V = "2020-07-10 15:00:00.000" + ws.(*xlsxWorksheet).SheetData.Row[2].C[2].T = "e" + ws.(*xlsxWorksheet).SheetData.Row[2].C[2].V = formulaErrorDIV + for _, tbl := range [][]string{ + {"D1", "=SUM(C1,1)", "43870.3397004977"}, + {"D2", "=LEN(C2)", "23"}, + {"D3", "=IFERROR(C3,TRUE)", "TRUE"}, + } { + assert.NoError(t, f.SetCellFormula("Sheet1", tbl[0], tbl[1])) + result, err := f.CalcCellValue("Sheet1", tbl[0]) + assert.NoError(t, err) + assert.Equal(t, tbl[2], result) + } + // Test calculates formula that reference invalid cell + assert.NoError(t, f.SetCellValue("Sheet1", "E1", "E1")) + assert.NoError(t, f.SetCellFormula("Sheet1", "F1", "=LEN(E1)")) + f.SharedStrings = nil + f.Pkg.Store(defaultXMLPathSharedStrings, MacintoshCyrillicCharset) + _, err := f.CalcCellValue("Sheet1", "F1") + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") } func TestEvalInfixExp(t *testing.T) { From ee2ef152d939c6a5bdd703167a58ba22a866f06b Mon Sep 17 00:00:00 2001 From: Vivek Kairi Date: Thu, 15 Feb 2024 11:00:07 +0530 Subject: [PATCH 858/957] This closes #1815, cell value reading functions inherit the Options settings of the OpenReader (#1816) Co-authored-by: Vivek Kairi --- calc.go | 5 +++-- cell.go | 2 +- col.go | 2 +- excelize.go | 6 +++--- file.go | 2 +- rows.go | 2 +- 6 files changed, 10 insertions(+), 9 deletions(-) diff --git a/calc.go b/calc.go index 59c88852c1..f03c573612 100644 --- a/calc.go +++ b/calc.go @@ -805,14 +805,15 @@ type formulaFuncs struct { // Z.TEST // ZTEST func (f *File) CalcCellValue(sheet, cell string, opts ...Options) (result string, err error) { + options := f.getOptions(opts...) var ( - rawCellValue = getOptions(opts...).RawCellValue + rawCellValue = options.RawCellValue styleIdx int token formulaArg ) if token, err = f.calcCellValue(&calcContext{ entry: fmt.Sprintf("%s!%s", sheet, cell), - maxCalcIterations: getOptions(opts...).MaxCalcIterations, + maxCalcIterations: options.MaxCalcIterations, iterations: make(map[string]uint), iterationsCache: make(map[string]formulaArg), }, sheet, cell); err != nil { diff --git a/cell.go b/cell.go index 721ba7bc01..5d547e814a 100644 --- a/cell.go +++ b/cell.go @@ -72,7 +72,7 @@ func (f *File) GetCellValue(sheet, cell string, opts ...Options) (string, error) if err != nil { return "", true, err } - val, err := c.getValueFrom(f, sst, getOptions(opts...).RawCellValue) + val, err := c.getValueFrom(f, sst, f.getOptions(opts...).RawCellValue) return val, true, err }) } diff --git a/col.go b/col.go index b51a283a43..c51fdada3a 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 = getOptions(opts...).RawCellValue + cols.rawCellValue = cols.f.getOptions(opts...).RawCellValue if cols.sst, rowIterator.err = cols.f.sharedStringsReader(); rowIterator.err != nil { return rowIterator.cells, rowIterator.err } diff --git a/excelize.go b/excelize.go index b86957247b..152e21222d 100644 --- a/excelize.go +++ b/excelize.go @@ -180,7 +180,7 @@ func OpenReader(r io.Reader, opts ...Options) (*File, error) { return nil, err } f := newFile() - f.options = getOptions(opts...) + f.options = f.getOptions(opts...) if err = f.checkOpenReaderOptions(); err != nil { return nil, err } @@ -219,8 +219,8 @@ func OpenReader(r io.Reader, opts ...Options) (*File, error) { // getOptions provides a function to parse the optional settings for open // and reading spreadsheet. -func getOptions(opts ...Options) *Options { - options := &Options{} +func (f *File) getOptions(opts ...Options) *Options { + options := f.options for _, opt := range opts { options = &opt } diff --git a/file.go b/file.go index 894dd0ab9d..067f9997cc 100644 --- a/file.go +++ b/file.go @@ -50,7 +50,7 @@ func NewFile(opts ...Options) *File { ws, _ := f.workSheetReader("Sheet1") f.Sheet.Store("xl/worksheets/sheet1.xml", ws) f.Theme, _ = f.themeReader() - f.options = getOptions(opts...) + f.options = f.getOptions(opts...) return f } diff --git a/rows.go b/rows.go index b87d45dddf..7541a0b217 100644 --- a/rows.go +++ b/rows.go @@ -151,7 +151,7 @@ func (rows *Rows) Columns(opts ...Options) ([]string, error) { } var rowIterator rowXMLIterator var token xml.Token - rows.rawCellValue = getOptions(opts...).RawCellValue + rows.rawCellValue = rows.f.getOptions(opts...).RawCellValue if rows.sst, rowIterator.err = rows.f.sharedStringsReader(); rowIterator.err != nil { return rowIterator.cells, rowIterator.err } From 02b84a906cb6adc54c0616080cc334a7af05d69d Mon Sep 17 00:00:00 2001 From: Ed <4785890+edwardfward@users.noreply.github.com> Date: Fri, 23 Feb 2024 19:11:31 -0600 Subject: [PATCH 859/957] This closes #1820, converted styleFillVariants from slice to func (#1821) --- styles.go | 47 +++++++++++++++++++++++++++-------------------- styles_test.go | 13 +++++++++++-- 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/styles.go b/styles.go index 91e4c5728d..3b7c28ab2d 100644 --- a/styles.go +++ b/styles.go @@ -1071,25 +1071,28 @@ var ( "gray0625", } // styleFillVariants list all preset variants of the fill style. - styleFillVariants = []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}, + styleFillVariants = func() []xlsxGradientFill { + return []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}, + } } + // getXfIDFuncs provides a function to get xfID by given style. getXfIDFuncs = map[string]func(int, xlsxXf, *Style) bool{ "numFmt": func(numFmtID int, xf xlsxXf, style *Style) bool { @@ -1132,6 +1135,7 @@ var ( return reflect.DeepEqual(xf.Protection, newProtection(style)) && xf.ApplyProtection != nil && *xf.ApplyProtection }, } + // extractStyleCondFuncs provides a function set to returns if shoudle be // extract style definition by given style. extractStyleCondFuncs = map[string]func(xlsxXf, *xlsxStyleSheet) bool{ @@ -1157,6 +1161,7 @@ var ( return xf.ApplyProtection == nil || (xf.ApplyProtection != nil && *xf.ApplyProtection) }, } + // drawContFmtFunc defines functions to create conditional formats. drawContFmtFunc = map[string]func(p int, ct, ref, GUID string, fmtCond *ConditionalFormatOptions) (*xlsxCfRule, *xlsxX14CfRule){ "cellIs": drawCondFmtCellIs, @@ -1176,6 +1181,7 @@ var ( "expression": drawCondFmtExp, "iconSet": drawCondFmtIconSet, } + // extractContFmtFunc defines functions to get conditional formats. extractContFmtFunc = map[string]func(*File, *xlsxCfRule, *xlsxExtLst) ConditionalFormatOptions{ "cellIs": func(f *File, c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { @@ -1233,6 +1239,7 @@ var ( return f.extractCondFmtIconSet(c, extLst) }, } + // validType defined the list of valid validation types. validType = map[string]string{ "cell": "cellIs", @@ -1456,7 +1463,7 @@ func (f *File) extractFills(fl *xlsxFill, s *xlsxStyleSheet, style *Style) { var fill Fill if fl.GradientFill != nil { fill.Type = "gradient" - for shading, variants := range styleFillVariants { + for shading, variants := range styleFillVariants() { if fl.GradientFill.Bottom == variants.Bottom && fl.GradientFill.Degree == variants.Degree && fl.GradientFill.Left == variants.Left && @@ -2024,7 +2031,7 @@ func newFills(style *Style, fg bool) *xlsxFill { if len(style.Fill.Color) != 2 || style.Fill.Shading < 0 || style.Fill.Shading > 16 { break } - gradient := styleFillVariants[style.Fill.Shading] + gradient := styleFillVariants()[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 { diff --git a/styles_test.go b/styles_test.go index 9267da951e..e11740e815 100644 --- a/styles_test.go +++ b/styles_test.go @@ -341,7 +341,16 @@ func TestNewStyle(t *testing.T) { _, err = f.NewStyle(nil) assert.NoError(t, err) + // Test gradient fills + f = NewFile() + styleID1, err := f.NewStyle(&Style{Fill: Fill{Type: "gradient", Color: []string{"FFFFFF", "4E71BE"}, Shading: 1, Pattern: 1}}) + assert.NoError(t, err) + styleID2, err := f.NewStyle(&Style{Fill: Fill{Type: "gradient", Color: []string{"FF0000", "4E71BE"}, Shading: 1, Pattern: 1}}) + assert.NoError(t, err) + assert.NotEqual(t, styleID1, styleID2) + var exp string + f = NewFile() _, err = f.NewStyle(&Style{CustomNumFmt: &exp}) assert.Equal(t, ErrCustomNumFmt, err) _, err = f.NewStyle(&Style{Font: &Font{Family: strings.Repeat("s", MaxFontFamilyLength+1)}}) @@ -356,7 +365,7 @@ func TestNewStyle(t *testing.T) { CustomNumFmt: &numFmt, }) assert.NoError(t, err) - assert.Equal(t, 2, styleID) + assert.Equal(t, 1, styleID) assert.NotNil(t, f.Styles) assert.NotNil(t, f.Styles.CellXfs) @@ -371,7 +380,7 @@ func TestNewStyle(t *testing.T) { NumFmt: 32, // must not be in currencyNumFmt }) assert.NoError(t, err) - assert.Equal(t, 3, styleID) + assert.Equal(t, 2, styleID) assert.NotNil(t, f.Styles) assert.NotNil(t, f.Styles.CellXfs) From 688808b2b4f7bb1f338991c810cd2ee6a7bb1451 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 26 Feb 2024 02:22:51 +0800 Subject: [PATCH 860/957] This closes #1819, formula calculation engine support array formulas - Improve the defined name and table name validation rules - Rename internal variable names to avoid the same with Go 1.21's built-in min and max functions - Simplify data type conversion in internal code - Update GitHub Actions workflow configuration, test on Go 1.22.x, and disable Go module cache - Update dependencies module --- .github/workflows/go.yml | 3 +- calc.go | 220 +++++++++++++++++++++------------------ col.go | 12 +-- date_test.go | 8 +- drawing.go | 44 ++++---- excelize.go | 14 +-- go.mod | 4 +- go.sum | 8 +- hsl.go | 16 +-- numfmt.go | 176 +++++++++++++++---------------- rows.go | 6 +- stream.go | 12 +-- table.go | 21 ++-- templates.go | 159 +++++++++++++++++++++++++++- 14 files changed, 443 insertions(+), 260 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 2743d60c3f..c56feda925 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -5,7 +5,7 @@ jobs: test: strategy: matrix: - go-version: [1.18.x, 1.19.x, 1.20.x, '>=1.21.1'] + go-version: [1.18.x, 1.19.x, 1.20.x, '>=1.21.1', 1.22.x] os: [ubuntu-latest, macos-latest, windows-latest] targetplatform: [x86, x64] @@ -17,6 +17,7 @@ jobs: uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} + cache: false - name: Checkout code uses: actions/checkout@v4 diff --git a/calc.go b/calc.go index f03c573612..496ecd41ee 100644 --- a/calc.go +++ b/calc.go @@ -281,6 +281,10 @@ func (fa formulaArg) Value() (value string) { return fmt.Sprintf("%g", fa.Number) case ArgString: return fa.String + case ArgMatrix: + if args := fa.ToList(); len(args) > 0 { + return args[0].Value() + } case ArgError: return fa.Error } @@ -299,6 +303,10 @@ func (fa formulaArg) ToNumber() formulaArg { } case ArgNumber: n = fa.Number + case ArgMatrix: + if args := fa.ToList(); len(args) > 0 { + return args[0].ToNumber() + } } return newNumberFormulaArg(n) } @@ -920,9 +928,14 @@ func newEmptyFormulaArg() formulaArg { // // 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() - var inArray, inArrayRow bool + var ( + err error + inArray, inArrayRow bool + formulaArray [][]formulaArg + formulaArrayRow []formulaArg + opdStack, optStack, opfStack = NewStack(), NewStack(), NewStack() + opfdStack, opftStack, argsStack = NewStack(), NewStack(), NewStack() + ) for i := 0; i < len(tokens); i++ { token := tokens[i] @@ -936,11 +949,11 @@ func (f *File) evalInfixExp(ctx *calcContext, sheet, cell string, tokens []efp.T // function start if isFunctionStartToken(token) { if token.TValue == "ARRAY" { - inArray = true + inArray, formulaArray = true, [][]formulaArg{} continue } if token.TValue == "ARRAYROW" { - inArrayRow = true + inArrayRow, formulaArrayRow = true, []formulaArg{} continue } opfStack.Push(token) @@ -1021,14 +1034,16 @@ func (f *File) evalInfixExp(ctx *calcContext, sheet, cell string, tokens []efp.T } if inArrayRow && isOperand(token) { + formulaArrayRow = append(formulaArrayRow, opfdStack.Pop().(formulaArg)) continue } if inArrayRow && isFunctionStopToken(token) { + formulaArray = append(formulaArray, formulaArrayRow) inArrayRow = false continue } if inArray && isFunctionStopToken(token) { - argsStack.Peek().(*list.List).PushBack(opfdStack.Pop()) + argsStack.Peek().(*list.List).PushBack(newMatrixFormulaArg(formulaArray)) inArray = false continue } @@ -1825,13 +1840,13 @@ func (fn *formulaFuncs) bassel(argsList *list.List, modfied bool) formulaArg { if n.Type != ArgNumber { return n } - max, x1 := 100, x.Number*0.5 + maxVal, x1 := 100, x.Number*0.5 x2 := x1 * x1 x1 = math.Pow(x1, n.Number) n1, n2, n3, n4, add := fact(n.Number), 1.0, 0.0, n.Number, false result := x1 / n1 t := result * 0.9 - for result != t && max != 0 { + for result != t && maxVal != 0 { x1 *= x2 n3++ n1 *= n3 @@ -1844,7 +1859,7 @@ func (fn *formulaFuncs) bassel(argsList *list.List, modfied bool) formulaArg { } else { result -= r } - max-- + maxVal-- add = !add } return newNumberFormulaArg(result) @@ -2151,8 +2166,8 @@ func (fn *formulaFuncs) bitwise(name string, argsList *list.List) formulaArg { if num1.Type != ArgNumber || num2.Type != ArgNumber { return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) } - max := math.Pow(2, 48) - 1 - if num1.Number < 0 || num1.Number > max || num2.Number < 0 || num2.Number > max { + maxVal := math.Pow(2, 48) - 1 + if num1.Number < 0 || num1.Number > maxVal || num2.Number < 0 || num2.Number > maxVal { return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) } bitwiseFuncMap := map[string]func(a, b int) int{ @@ -6958,9 +6973,9 @@ func (fn *formulaFuncs) BETAdotINV(argsList *list.List) formulaArg { // incompleteGamma is an implementation of the incomplete gamma function. func incompleteGamma(a, x float64) float64 { - max := 32 + maxVal := 32 summer := 0.0 - for n := 0; n <= max; n++ { + for n := 0; n <= maxVal; n++ { divisor := a for i := 1; i <= n; i++ { divisor *= a + float64(i) @@ -7079,7 +7094,7 @@ func (fn *formulaFuncs) BINOMdotDISTdotRANGE(argsList *list.List) formulaArg { // 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 + q, i, sum, maxVal := 1-p, 0.0, 0.0, 0.0 n = math.Floor(n) if q > p { factor := math.Pow(q, n) @@ -7091,8 +7106,8 @@ func binominv(n, p, alpha float64) float64 { return i } factor := math.Pow(p, n) - sum, max = 1-factor, n - for i = 0; i < max && sum >= alpha; i++ { + sum, maxVal = 1-factor, n + for i = 0; i < maxVal && sum >= alpha; i++ { factor *= (n - i) / (i + 1) * q / p sum -= factor } @@ -8078,10 +8093,13 @@ func (fn *formulaFuncs) FORECASTdotLINEAR(argsList *list.List) formulaArg { return fn.pearsonProduct("FORECAST.LINEAR", 3, argsList) } -// maritxToSortedColumnList convert matrix formula arguments to a ascending +// matrixToSortedColumnList convert matrix formula arguments to a ascending // order list by column. -func maritxToSortedColumnList(arg formulaArg) formulaArg { - mtx, cols := []formulaArg{}, len(arg.Matrix[0]) +func matrixToSortedColumnList(arg formulaArg) formulaArg { + var ( + mtx []formulaArg + cols = len(arg.Matrix[0]) + ) for colIdx := 0; colIdx < cols; colIdx++ { for _, row := range arg.Matrix { cell := row[colIdx] @@ -8120,14 +8138,14 @@ func (fn *formulaFuncs) FREQUENCY(argsList *list.List) formulaArg { c [][]formulaArg i, j int ) - if dataMtx = maritxToSortedColumnList(data); dataMtx.Type != ArgList { + if dataMtx = matrixToSortedColumnList(data); dataMtx.Type != ArgList { return dataMtx } - if binsMtx = maritxToSortedColumnList(bins); binsMtx.Type != ArgList { + if binsMtx = matrixToSortedColumnList(bins); binsMtx.Type != ArgList { return binsMtx } for row := 0; row < len(binsMtx.List)+1; row++ { - rows := []formulaArg{} + var rows []formulaArg for col := 0; col < 1; col++ { rows = append(rows, newNumberFormulaArg(0)) } @@ -8349,8 +8367,8 @@ func (fn *formulaFuncs) GEOMEAN(argsList *list.List) formulaArg { return product } count := fn.COUNT(argsList) - min := fn.MIN(argsList) - if product.Number > 0 && min.Number > 0 { + minVal := fn.MIN(argsList) + if product.Number > 0 && minVal.Number > 0 { return newNumberFormulaArg(math.Pow(product.Number, 1/count.Number)) } return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) @@ -9055,7 +9073,7 @@ func (fn *formulaFuncs) HARMEAN(argsList *list.List) formulaArg { if argsList.Len() < 1 { return newErrorFormulaArg(formulaErrorVALUE, "HARMEAN requires at least 1 argument") } - if min := fn.MIN(argsList); min.Number < 0 { + if minVal := fn.MIN(argsList); minVal.Number < 0 { return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) } number, val, cnt := 0.0, 0.0, 0.0 @@ -10002,7 +10020,7 @@ func (fn *formulaFuncs) MAX(argsList *list.List) formulaArg { if argsList.Len() == 0 { return newErrorFormulaArg(formulaErrorVALUE, "MAX requires at least 1 argument") } - return fn.max(false, argsList) + return fn.maxValue(false, argsList) } // MAXA function returns the largest value from a supplied set of numeric @@ -10015,7 +10033,7 @@ func (fn *formulaFuncs) MAXA(argsList *list.List) formulaArg { if argsList.Len() == 0 { return newErrorFormulaArg(formulaErrorVALUE, "MAXA requires at least 1 argument") } - return fn.max(true, argsList) + return fn.maxValue(true, argsList) } // MAXIFS function returns the maximum value from a subset of values that are @@ -10031,36 +10049,36 @@ func (fn *formulaFuncs) MAXIFS(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) } var args []formulaArg - max, maxRange := -math.MaxFloat64, argsList.Front().Value.(formulaArg).Matrix + maxVal, maxRange := -math.MaxFloat64, argsList.Front().Value.(formulaArg).Matrix for arg := argsList.Front().Next(); arg != nil; arg = arg.Next() { args = append(args, arg.Value.(formulaArg)) } for _, ref := range formulaIfsMatch(args) { - if num := maxRange[ref.Row][ref.Col].ToNumber(); num.Type == ArgNumber && max < num.Number { - max = num.Number + if num := maxRange[ref.Row][ref.Col].ToNumber(); num.Type == ArgNumber && maxVal < num.Number { + maxVal = num.Number } } - if max == -math.MaxFloat64 { - max = 0 + if maxVal == -math.MaxFloat64 { + maxVal = 0 } - return newNumberFormulaArg(max) + return newNumberFormulaArg(maxVal) } // calcListMatrixMax is part of the implementation max. -func calcListMatrixMax(maxa bool, max float64, arg formulaArg) float64 { +func calcListMatrixMax(maxa bool, maxVal float64, arg formulaArg) float64 { for _, cell := range arg.ToList() { - if cell.Type == ArgNumber && cell.Number > max { + if cell.Type == ArgNumber && cell.Number > maxVal { if maxa && cell.Boolean || !cell.Boolean { - max = cell.Number + maxVal = cell.Number } } } - return max + return maxVal } -// max is an implementation of the formula functions MAX and MAXA. -func (fn *formulaFuncs) max(maxa bool, argsList *list.List) formulaArg { - max := -math.MaxFloat64 +// maxValue is an implementation of the formula functions MAX and MAXA. +func (fn *formulaFuncs) maxValue(maxa bool, argsList *list.List) formulaArg { + maxVal := -math.MaxFloat64 for token := argsList.Front(); token != nil; token = token.Next() { arg := token.Value.(formulaArg) switch arg.Type { @@ -10069,29 +10087,29 @@ func (fn *formulaFuncs) max(maxa bool, argsList *list.List) formulaArg { continue } else { num := arg.ToBool() - if num.Type == ArgNumber && num.Number > max { - max = num.Number + if num.Type == ArgNumber && num.Number > maxVal { + maxVal = num.Number continue } } num := arg.ToNumber() - if num.Type != ArgError && num.Number > max { - max = num.Number + if num.Type != ArgError && num.Number > maxVal { + maxVal = num.Number } case ArgNumber: - if arg.Number > max { - max = arg.Number + if arg.Number > maxVal { + maxVal = arg.Number } case ArgList, ArgMatrix: - max = calcListMatrixMax(maxa, max, arg) + maxVal = calcListMatrixMax(maxa, maxVal, arg) case ArgError: return arg } } - if max == -math.MaxFloat64 { - max = 0 + if maxVal == -math.MaxFloat64 { + maxVal = 0 } - return newNumberFormulaArg(max) + return newNumberFormulaArg(maxVal) } // MEDIAN function returns the statistical median (the middle value) of a list @@ -10145,7 +10163,7 @@ func (fn *formulaFuncs) MIN(argsList *list.List) formulaArg { if argsList.Len() == 0 { return newErrorFormulaArg(formulaErrorVALUE, "MIN requires at least 1 argument") } - return fn.min(false, argsList) + return fn.minValue(false, argsList) } // MINA function returns the smallest value from a supplied set of numeric @@ -10158,7 +10176,7 @@ func (fn *formulaFuncs) MINA(argsList *list.List) formulaArg { if argsList.Len() == 0 { return newErrorFormulaArg(formulaErrorVALUE, "MINA requires at least 1 argument") } - return fn.min(true, argsList) + return fn.minValue(true, argsList) } // MINIFS function returns the minimum value from a subset of values that are @@ -10174,36 +10192,36 @@ func (fn *formulaFuncs) MINIFS(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) } var args []formulaArg - min, minRange := math.MaxFloat64, argsList.Front().Value.(formulaArg).Matrix + minVal, minRange := math.MaxFloat64, argsList.Front().Value.(formulaArg).Matrix for arg := argsList.Front().Next(); arg != nil; arg = arg.Next() { args = append(args, arg.Value.(formulaArg)) } for _, ref := range formulaIfsMatch(args) { - if num := minRange[ref.Row][ref.Col].ToNumber(); num.Type == ArgNumber && min > num.Number { - min = num.Number + if num := minRange[ref.Row][ref.Col].ToNumber(); num.Type == ArgNumber && minVal > num.Number { + minVal = num.Number } } - if min == math.MaxFloat64 { - min = 0 + if minVal == math.MaxFloat64 { + minVal = 0 } - return newNumberFormulaArg(min) + return newNumberFormulaArg(minVal) } // calcListMatrixMin is part of the implementation min. -func calcListMatrixMin(mina bool, min float64, arg formulaArg) float64 { +func calcListMatrixMin(mina bool, minVal float64, arg formulaArg) float64 { for _, cell := range arg.ToList() { - if cell.Type == ArgNumber && cell.Number < min { + if cell.Type == ArgNumber && cell.Number < minVal { if mina && cell.Boolean || !cell.Boolean { - min = cell.Number + minVal = cell.Number } } } - return min + return minVal } -// min is an implementation of the formula functions MIN and MINA. -func (fn *formulaFuncs) min(mina bool, argsList *list.List) formulaArg { - min := math.MaxFloat64 +// minValue is an implementation of the formula functions MIN and MINA. +func (fn *formulaFuncs) minValue(mina bool, argsList *list.List) formulaArg { + minVal := math.MaxFloat64 for token := argsList.Front(); token != nil; token = token.Next() { arg := token.Value.(formulaArg) switch arg.Type { @@ -10212,29 +10230,29 @@ func (fn *formulaFuncs) min(mina bool, argsList *list.List) formulaArg { continue } else { num := arg.ToBool() - if num.Type == ArgNumber && num.Number < min { - min = num.Number + if num.Type == ArgNumber && num.Number < minVal { + minVal = num.Number continue } } num := arg.ToNumber() - if num.Type != ArgError && num.Number < min { - min = num.Number + if num.Type != ArgError && num.Number < minVal { + minVal = num.Number } case ArgNumber: - if arg.Number < min { - min = arg.Number + if arg.Number < minVal { + minVal = arg.Number } case ArgList, ArgMatrix: - min = calcListMatrixMin(mina, min, arg) + minVal = calcListMatrixMin(mina, minVal, arg) case ArgError: return arg } } - if min == math.MaxFloat64 { - min = 0 + if minVal == math.MaxFloat64 { + minVal = 0 } - return newNumberFormulaArg(min) + return newNumberFormulaArg(minVal) } // pearsonProduct is an implementation of the formula functions FORECAST, @@ -13554,7 +13572,7 @@ func (fn *formulaFuncs) code(name string, argsList *list.List) formulaArg { // // CONCAT(text1,[text2],...) func (fn *formulaFuncs) CONCAT(argsList *list.List) formulaArg { - return fn.concat("CONCAT", argsList) + return fn.concat(argsList) } // CONCATENATE function joins together a series of supplied text strings into @@ -13562,12 +13580,12 @@ func (fn *formulaFuncs) CONCAT(argsList *list.List) formulaArg { // // CONCATENATE(text1,[text2],...) func (fn *formulaFuncs) CONCATENATE(argsList *list.List) formulaArg { - return fn.concat("CONCATENATE", argsList) + return fn.concat(argsList) } // concat is an implementation of the formula functions CONCAT and // CONCATENATE. -func (fn *formulaFuncs) concat(name string, argsList *list.List) formulaArg { +func (fn *formulaFuncs) concat(argsList *list.List) formulaArg { var buf bytes.Buffer for arg := argsList.Front(); arg != nil; arg = arg.Next() { for _, cell := range arg.Value.(formulaArg).ToList() { @@ -13831,16 +13849,16 @@ func (fn *formulaFuncs) LENB(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "LENB requires 1 string argument") } - bytes := 0 + result := 0 for _, r := range argsList.Front().Value.(formulaArg).Value() { b := utf8.RuneLen(r) if b == 1 { - bytes++ + result++ } else if b > 1 { - bytes += 2 + result += 2 } } - return newNumberFormulaArg(float64(bytes)) + return newNumberFormulaArg(float64(result)) } // LOWER converts all characters in a supplied text string to lower case. The @@ -14774,7 +14792,7 @@ func (fn *formulaFuncs) COLUMN(argsList *list.List) formulaArg { // 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) { +func calcColsRowsMinMax(cols bool, argsList *list.List) (minVal, maxVal int) { getVal := func(cols bool, cell cellRef) int { if cols { return cell.Col @@ -14784,22 +14802,22 @@ func calcColsRowsMinMax(cols bool, 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 = getVal(cols, cr.Value.(cellRange).From) + if minVal == 0 { + minVal = getVal(cols, cr.Value.(cellRange).From) } - if max < getVal(cols, cr.Value.(cellRange).To) { - max = getVal(cols, cr.Value.(cellRange).To) + if maxVal < getVal(cols, cr.Value.(cellRange).To) { + maxVal = getVal(cols, cr.Value.(cellRange).To) } } } 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 = getVal(cols, refs.Value.(cellRef)) + if minVal == 0 { + minVal = getVal(cols, refs.Value.(cellRef)) } - if max < getVal(cols, refs.Value.(cellRef)) { - max = getVal(cols, refs.Value.(cellRef)) + if maxVal < getVal(cols, refs.Value.(cellRef)) { + maxVal = getVal(cols, refs.Value.(cellRef)) } } } @@ -14814,13 +14832,13 @@ func (fn *formulaFuncs) COLUMNS(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "COLUMNS requires 1 argument") } - min, max := calcColsRowsMinMax(true, argsList) - if max == MaxColumns { + minVal, maxVal := calcColsRowsMinMax(true, argsList) + if maxVal == MaxColumns { return newNumberFormulaArg(float64(MaxColumns)) } - result := max - min + 1 - if max == min { - if min == 0 { + result := maxVal - minVal + 1 + if maxVal == minVal { + if minVal == 0 { return newErrorFormulaArg(formulaErrorVALUE, "invalid reference") } return newNumberFormulaArg(float64(1)) @@ -15555,13 +15573,13 @@ func (fn *formulaFuncs) ROWS(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "ROWS requires 1 argument") } - min, max := calcColsRowsMinMax(false, argsList) - if max == TotalRows { + minVal, maxVal := calcColsRowsMinMax(false, argsList) + if maxVal == TotalRows { return newNumberFormulaArg(TotalRows) } - result := max - min + 1 - if max == min { - if min == 0 { + result := maxVal - minVal + 1 + if maxVal == minVal { + if minVal == 0 { return newErrorFormulaArg(formulaErrorVALUE, "invalid reference") } return newNumberFormulaArg(float64(1)) diff --git a/col.go b/col.go index c51fdada3a..b1b9c0d38c 100644 --- a/col.go +++ b/col.go @@ -354,20 +354,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) (min, max int, err error) { +func (f *File) parseColRange(columns string) (minVal, maxVal int, err error) { colsTab := strings.Split(columns, ":") - min, err = ColumnNameToNumber(colsTab[0]) + minVal, err = ColumnNameToNumber(colsTab[0]) if err != nil { return } - max = min + maxVal = minVal if len(colsTab) == 2 { - if max, err = ColumnNameToNumber(colsTab[1]); err != nil { + if maxVal, err = ColumnNameToNumber(colsTab[1]); err != nil { return } } - if max < min { - min, max = max, min + if maxVal < minVal { + minVal, maxVal = maxVal, minVal } return } diff --git a/date_test.go b/date_test.go index 4091e378db..11011cf3b5 100644 --- a/date_test.go +++ b/date_test.go @@ -68,22 +68,22 @@ func TestTimeFromExcelTime(t *testing.T) { }) } for hour := 0; hour < 24; hour++ { - for min := 0; min < 60; min++ { + for minVal := 0; minVal < 60; minVal++ { for sec := 0; sec < 60; sec++ { - date := time.Date(2021, time.December, 30, hour, min, sec, 0, time.UTC) + date := time.Date(2021, time.December, 30, hour, minVal, sec, 0, time.UTC) // Test use 1900 date system excel1900Time, err := timeToExcelTime(date, false) assert.NoError(t, err) date1900Out := timeFromExcelTime(excel1900Time, false) assert.EqualValues(t, hour, date1900Out.Hour()) - assert.EqualValues(t, min, date1900Out.Minute()) + assert.EqualValues(t, minVal, 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, minVal, date1904Out.Minute()) assert.EqualValues(t, sec, date1904Out.Second()) } } diff --git a/drawing.go b/drawing.go index bc7b8a47aa..3c2da06728 100644 --- a/drawing.go +++ b/drawing.go @@ -980,21 +980,21 @@ func (f *File) drawChartSeriesDLbls(i int, opts *Chart) *cDLbls { // drawPlotAreaCatAx provides a function to draw the c:catAx element. func (f *File) drawPlotAreaCatAx(opts *Chart) []*cAxs { - max := &attrValFloat{Val: opts.XAxis.Maximum} - min := &attrValFloat{Val: opts.XAxis.Minimum} + maxVal := &attrValFloat{Val: opts.XAxis.Maximum} + minVal := &attrValFloat{Val: opts.XAxis.Minimum} if opts.XAxis.Maximum == nil { - max = nil + maxVal = nil } if opts.XAxis.Minimum == nil { - min = nil + minVal = nil } axs := []*cAxs{ { AxID: &attrValInt{Val: intPtr(100000000)}, Scaling: &cScaling{ Orientation: &attrValString{Val: stringPtr(orientation[opts.XAxis.ReverseOrder])}, - Max: max, - Min: min, + Max: maxVal, + Min: minVal, }, Delete: &attrValBool{Val: boolPtr(opts.XAxis.None)}, AxPos: &attrValString{Val: stringPtr(catAxPos[opts.XAxis.ReverseOrder])}, @@ -1030,8 +1030,8 @@ func (f *File) drawPlotAreaCatAx(opts *Chart) []*cAxs { AxID: &attrValInt{Val: intPtr(opts.XAxis.axID)}, Scaling: &cScaling{ Orientation: &attrValString{Val: stringPtr(orientation[opts.XAxis.ReverseOrder])}, - Max: max, - Min: min, + Max: maxVal, + Min: minVal, }, Delete: &attrValBool{Val: boolPtr(true)}, AxPos: &attrValString{Val: stringPtr("b")}, @@ -1052,13 +1052,13 @@ func (f *File) drawPlotAreaCatAx(opts *Chart) []*cAxs { // drawPlotAreaValAx provides a function to draw the c:valAx element. func (f *File) drawPlotAreaValAx(opts *Chart) []*cAxs { - max := &attrValFloat{Val: opts.YAxis.Maximum} - min := &attrValFloat{Val: opts.YAxis.Minimum} + maxVal := &attrValFloat{Val: opts.YAxis.Maximum} + minVal := &attrValFloat{Val: opts.YAxis.Minimum} if opts.YAxis.Maximum == nil { - max = nil + maxVal = nil } if opts.YAxis.Minimum == nil { - min = nil + minVal = nil } var logBase *attrValFloat if opts.YAxis.LogBase >= 2 && opts.YAxis.LogBase <= 1000 { @@ -1070,8 +1070,8 @@ func (f *File) drawPlotAreaValAx(opts *Chart) []*cAxs { Scaling: &cScaling{ LogBase: logBase, Orientation: &attrValString{Val: stringPtr(orientation[opts.YAxis.ReverseOrder])}, - Max: max, - Min: min, + Max: maxVal, + Min: minVal, }, Delete: &attrValBool{Val: boolPtr(opts.YAxis.None)}, AxPos: &attrValString{Val: stringPtr(valAxPos[opts.YAxis.ReverseOrder])}, @@ -1109,8 +1109,8 @@ func (f *File) drawPlotAreaValAx(opts *Chart) []*cAxs { AxID: &attrValInt{Val: intPtr(opts.YAxis.axID)}, Scaling: &cScaling{ Orientation: &attrValString{Val: stringPtr(orientation[opts.YAxis.ReverseOrder])}, - Max: max, - Min: min, + Max: maxVal, + Min: minVal, }, Delete: &attrValBool{Val: boolPtr(false)}, AxPos: &attrValString{Val: stringPtr("r")}, @@ -1129,21 +1129,21 @@ func (f *File) drawPlotAreaValAx(opts *Chart) []*cAxs { // drawPlotAreaSerAx provides a function to draw the c:serAx element. func (f *File) drawPlotAreaSerAx(opts *Chart) []*cAxs { - max := &attrValFloat{Val: opts.YAxis.Maximum} - min := &attrValFloat{Val: opts.YAxis.Minimum} + maxVal := &attrValFloat{Val: opts.YAxis.Maximum} + minVal := &attrValFloat{Val: opts.YAxis.Minimum} if opts.YAxis.Maximum == nil { - max = nil + maxVal = nil } if opts.YAxis.Minimum == nil { - min = nil + minVal = nil } return []*cAxs{ { AxID: &attrValInt{Val: intPtr(100000005)}, Scaling: &cScaling{ Orientation: &attrValString{Val: stringPtr(orientation[opts.YAxis.ReverseOrder])}, - Max: max, - Min: min, + Max: maxVal, + Min: minVal, }, Delete: &attrValBool{Val: boolPtr(opts.YAxis.None)}, AxPos: &attrValString{Val: stringPtr(catAxPos[opts.XAxis.ReverseOrder])}, diff --git a/excelize.go b/excelize.go index 152e21222d..87ef22dd52 100644 --- a/excelize.go +++ b/excelize.go @@ -334,9 +334,9 @@ func (ws *xlsxWorksheet) checkSheet() { // with r="0" attribute. func (ws *xlsxWorksheet) checkSheetRows() (int, []xlsxRow) { var ( - row, max int - r0 []xlsxRow - maxRowNum = func(num int, c []xlsxC) int { + row, maxVal int + r0 []xlsxRow + maxRowNum = func(num int, c []xlsxC) int { for _, cell := range c { if _, n, err := CellNameToCoordinates(cell.R); err == nil && n > num { num = n @@ -351,8 +351,8 @@ func (ws *xlsxWorksheet) checkSheetRows() (int, []xlsxRow) { continue } if i == 0 && *r.R == 0 { - if num := maxRowNum(row, r.C); num > max { - max = num + if num := maxRowNum(row, r.C); num > maxVal { + maxVal = num } r0 = append(r0, r) continue @@ -361,8 +361,8 @@ func (ws *xlsxWorksheet) checkSheetRows() (int, []xlsxRow) { row = *r.R } } - if max > row { - row = max + if maxVal > row { + row = maxVal } return row, r0 } diff --git a/go.mod b/go.mod index 958af04113..357b84e631 100644 --- a/go.mod +++ b/go.mod @@ -8,9 +8,9 @@ require ( github.com/stretchr/testify v1.8.4 github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53 github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05 - golang.org/x/crypto v0.18.0 + golang.org/x/crypto v0.19.0 golang.org/x/image v0.14.0 - golang.org/x/net v0.20.0 + golang.org/x/net v0.21.0 golang.org/x/text v0.14.0 ) diff --git a/go.sum b/go.sum index 99281827b6..64de49a3e6 100644 --- a/go.sum +++ b/go.sum @@ -15,12 +15,12 @@ github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53 h1:Chd9DkqERQQuHpXjR/HSV1 github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05 h1:qhbILQo1K3mphbwKh1vNm4oGezE1eF9fQWmNiIpSfI4= github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= -golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4= golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= -golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/hsl.go b/hsl.go index 68ddf21704..b8fb77b21a 100644 --- a/hsl.go +++ b/hsl.go @@ -65,21 +65,21 @@ func RGBToHSL(r, g, b uint8) (h, s, l float64) { fR := float64(r) / 255 fG := float64(g) / 255 fB := float64(b) / 255 - max := math.Max(math.Max(fR, fG), fB) - min := math.Min(math.Min(fR, fG), fB) - l = (max + min) / 2 - if max == min { + maxVal := math.Max(math.Max(fR, fG), fB) + minVal := math.Min(math.Min(fR, fG), fB) + l = (maxVal + minVal) / 2 + if maxVal == minVal { // Achromatic. h, s = 0, 0 } else { // Chromatic. - d := max - min + d := maxVal - minVal if l > 0.5 { - s = d / (2.0 - max - min) + s = d / (2.0 - maxVal - minVal) } else { - s = d / (max + min) + s = d / (maxVal + minVal) } - switch max { + switch maxVal { case fR: h = (fG - fB) / d if fG < fB { diff --git a/numfmt.go b/numfmt.go index c4022ba039..d37130b741 100644 --- a/numfmt.go +++ b/numfmt.go @@ -5776,7 +5776,7 @@ func localMonthsNameKiche(t time.Time, abbr int) string { if abbr == 4 || abbr > 6 { return monthNamesKiche[int(t.Month())-1] } - return string([]rune(monthNamesKicheAbbr[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesKicheAbbr[(t.Month() - 1)])[:1]) } // localMonthsNameKinyarwanda returns the Kinyarwanda name of the month. @@ -5787,7 +5787,7 @@ func localMonthsNameKinyarwanda(t time.Time, abbr int) string { if abbr == 4 || abbr > 6 { return monthNamesKinyarwanda[int(t.Month())-1] } - return string([]rune(monthNamesKinyarwanda[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesKinyarwanda[(t.Month() - 1)])[:1]) } // localMonthsNameKiswahili returns the Kiswahili name of the month. @@ -5798,7 +5798,7 @@ func localMonthsNameKiswahili(t time.Time, abbr int) string { if abbr == 4 || abbr > 6 { return monthNamesKiswahili[int(t.Month())-1] } - return string([]rune(monthNamesKiswahili[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesKiswahili[(t.Month() - 1)])[:1]) } // localMonthsNameKonkani returns the Konkani name of the month. @@ -5809,7 +5809,7 @@ func localMonthsNameKonkani(t time.Time, abbr int) string { if abbr == 4 || abbr > 6 { return monthNamesKonkani[int(t.Month())-1] } - return string([]rune(monthNamesKonkani[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesKonkani[(t.Month() - 1)])[:1]) } // localMonthsNameKorean returns the Korean name of the month. @@ -5828,7 +5828,7 @@ func localMonthsNameKyrgyz(t time.Time, abbr int) string { if abbr == 4 || abbr > 6 { return monthNamesKyrgyz[int(t.Month())-1] } - return string([]rune(monthNamesKyrgyz[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesKyrgyz[(t.Month() - 1)])[:1]) } // localMonthsNameLao returns the Lao name of the month. @@ -5839,7 +5839,7 @@ func localMonthsNameLao(t time.Time, abbr int) string { if abbr == 4 || abbr > 6 { return monthNamesLao[int(t.Month())-1] } - return string([]rune(monthNamesLao[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesLao[(t.Month() - 1)])[:1]) } // localMonthsNameLatin returns the Latin name of the month. @@ -5850,7 +5850,7 @@ func localMonthsNameLatin(t time.Time, abbr int) string { if abbr == 4 || abbr > 6 { return monthNamesLatin[int(t.Month())-1] } - return string([]rune(monthNamesLatin[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesLatin[(t.Month() - 1)])[:1]) } // localMonthsNameLatvian returns the Latvian name of the month. @@ -5861,7 +5861,7 @@ func localMonthsNameLatvian(t time.Time, abbr int) string { if abbr == 4 || abbr > 6 { return monthNamesLatvian[int(t.Month())-1] } - return string([]rune(monthNamesLatvian[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesLatvian[(t.Month() - 1)])[:1]) } // localMonthsNameLithuanian returns the Lithuanian name of the month. @@ -5872,7 +5872,7 @@ func localMonthsNameLithuanian(t time.Time, abbr int) string { if abbr == 4 || abbr > 6 { return monthNamesLithuanian[int(t.Month())-1] } - return string([]rune(monthNamesLithuanian[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesLithuanian[(t.Month() - 1)])[:1]) } // localMonthsNameLowerSorbian returns the LowerSorbian name of the month. @@ -5883,7 +5883,7 @@ func localMonthsNameLowerSorbian(t time.Time, abbr int) string { if abbr == 4 || abbr > 6 { return monthNamesLowerSorbian[int(t.Month())-1] } - return string([]rune(monthNamesLowerSorbian[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesLowerSorbian[(t.Month() - 1)])[:1]) } // localMonthsNameLuxembourgish returns the Luxembourgish name of the month. @@ -5894,7 +5894,7 @@ func localMonthsNameLuxembourgish(t time.Time, abbr int) string { if abbr == 4 || abbr > 6 { return monthNamesLuxembourgish[int(t.Month())-1] } - return string([]rune(monthNamesLuxembourgish[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesLuxembourgish[(t.Month() - 1)])[:1]) } // localMonthsNameMacedonian returns the Macedonian name of the month. @@ -5905,7 +5905,7 @@ func localMonthsNameMacedonian(t time.Time, abbr int) string { if abbr == 4 || abbr > 6 { return monthNamesMacedonian[int(t.Month())-1] } - return string([]rune(monthNamesMacedonian[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesMacedonian[(t.Month() - 1)])[:1]) } // localMonthsNameMalay returns the Malay name of the month. @@ -5916,7 +5916,7 @@ func localMonthsNameMalay(t time.Time, abbr int) string { if abbr == 4 || abbr > 6 { return monthNamesMalay[int(t.Month())-1] } - return string([]rune(monthNamesMalay[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesMalay[(t.Month() - 1)])[:1]) } // localMonthsNameMalayalam returns the Malayalam name of the month. @@ -5927,7 +5927,7 @@ func localMonthsNameMalayalam(t time.Time, abbr int) string { if abbr == 4 || abbr > 6 { return monthNamesMalayalam[int(t.Month())-1] } - return string([]rune(monthNamesMalayalam[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesMalayalam[(t.Month() - 1)])[:1]) } // localMonthsNameMaltese returns the Maltese name of the month. @@ -5938,7 +5938,7 @@ func localMonthsNameMaltese(t time.Time, abbr int) string { if abbr == 4 || abbr > 6 { return monthNamesMaltese[int(t.Month())-1] } - return string([]rune(monthNamesMaltese[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesMaltese[(t.Month() - 1)])[:1]) } // localMonthsNameMaori returns the Maori name of the month. @@ -5949,13 +5949,13 @@ func localMonthsNameMaori(t time.Time, abbr int) string { if abbr == 4 || abbr > 6 { return monthNamesMaori[int(t.Month())-1] } - return string([]rune(monthNamesMaori[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesMaori[(t.Month() - 1)])[:1]) } // localMonthsNameMapudungun returns the Mapudungun name of the month. func localMonthsNameMapudungun(t time.Time, abbr int) string { if abbr == 5 { - return string([]rune(monthNamesMapudungun[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesMapudungun[(t.Month() - 1)])[:1]) } return monthNamesMapudungun[int(t.Month())-1] } @@ -5968,7 +5968,7 @@ func localMonthsNameMarathi(t time.Time, abbr int) string { if abbr == 4 || abbr > 6 { return monthNamesMarathi[int(t.Month())-1] } - return string([]rune(monthNamesMarathi[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesMarathi[(t.Month() - 1)])[:1]) } // localMonthsNameMohawk returns the Mohawk name of the month. @@ -5979,7 +5979,7 @@ func localMonthsNameMohawk(t time.Time, abbr int) string { if abbr == 4 || abbr > 6 { return monthNamesMohawk[int(t.Month())-1] } - return string([]rune(monthNamesMohawk[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesMohawk[(t.Month() - 1)])[:1]) } // localMonthsNameMongolian returns the Mongolian name of the month. @@ -5990,7 +5990,7 @@ func localMonthsNameMongolian(t time.Time, abbr int) string { if abbr == 4 || abbr > 6 { return monthNamesMongolian[int(t.Month())-1] } - return string([]rune(monthNamesMongolian[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesMongolian[(t.Month() - 1)])[:1]) } // localMonthsNameMorocco returns the Morocco name of the month. @@ -6012,7 +6012,7 @@ func localMonthsNameNepali(t time.Time, abbr int) string { if abbr == 4 || abbr > 6 { return monthNamesNepali[int(t.Month())-1] } - return string([]rune(monthNamesNepali[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesNepali[(t.Month() - 1)])[:1]) } // localMonthsNameNepaliIN returns the India Nepali name of the month. @@ -6023,7 +6023,7 @@ func localMonthsNameNepaliIN(t time.Time, abbr int) string { if abbr == 4 || abbr > 6 { return monthNamesNepaliIN[int(t.Month())-1] } - return string([]rune(monthNamesNepaliIN[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesNepaliIN[(t.Month() - 1)])[:1]) } // localMonthsNameNigeria returns the Nigeria name of the month. @@ -6045,7 +6045,7 @@ func localMonthsNameNorwegian(t time.Time, abbr int) string { if abbr == 4 || abbr > 6 { return monthNamesNorwegian[int(t.Month())-1] } - return string([]rune(monthNamesNorwegian[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesNorwegian[(t.Month() - 1)])[:1]) } // localMonthsNameOccitan returns the Occitan name of the month. @@ -6056,13 +6056,13 @@ func localMonthsNameOccitan(t time.Time, abbr int) string { if abbr == 4 || abbr > 6 { return monthNamesOccitan[int(t.Month())-1] } - return string([]rune(monthNamesOccitan[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesOccitan[(t.Month() - 1)])[:1]) } // localMonthsNameOdia returns the Odia name of the month. func localMonthsNameOdia(t time.Time, abbr int) string { if abbr == 5 { - return string([]rune(monthNamesOdia[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesOdia[(t.Month() - 1)])[:1]) } return monthNamesOdia[int(t.Month())-1] } @@ -6075,7 +6075,7 @@ func localMonthsNameOromo(t time.Time, abbr int) string { if abbr == 4 || abbr > 6 { return monthNamesOromo[int(t.Month())-1] } - return string([]rune(monthNamesOromo[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesOromo[(t.Month() - 1)])[:1]) } // localMonthsNamePashto returns the Pashto name of the month. @@ -6094,7 +6094,7 @@ func localMonthsNamePashto(t time.Time, abbr int) string { // localMonthsNamePersian returns the Persian name of the month. func localMonthsNamePersian(t time.Time, abbr int) string { if abbr == 5 { - return string([]rune(monthNamesPersian[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesPersian[(t.Month() - 1)])[:1]) } return monthNamesPersian[int(t.Month())-1] } @@ -6102,29 +6102,29 @@ func localMonthsNamePersian(t time.Time, abbr int) string { // localMonthsNamePolish returns the Polish name of the month. func localMonthsNamePolish(t time.Time, abbr int) string { if abbr == 3 { - return string([]rune(monthNamesPolish[int(t.Month()-1)])[:3]) + return string([]rune(monthNamesPolish[(t.Month() - 1)])[:3]) } if abbr == 4 || abbr > 6 { return monthNamesPolish[int(t.Month())-1] } - return string([]rune(monthNamesPolish[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesPolish[(t.Month() - 1)])[:1]) } // localMonthsNamePortuguese returns the Portuguese name of the month. func localMonthsNamePortuguese(t time.Time, abbr int) string { if abbr == 3 { - return string([]rune(monthNamesPortuguese[int(t.Month()-1)])[:3]) + return string([]rune(monthNamesPortuguese[(t.Month() - 1)])[:3]) } if abbr == 4 || abbr > 6 { return monthNamesPortuguese[int(t.Month())-1] } - return string([]rune(monthNamesPortuguese[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesPortuguese[(t.Month() - 1)])[:1]) } // localMonthsNamePunjabi returns the Punjabi name of the month. func localMonthsNamePunjabi(t time.Time, abbr int) string { if abbr == 5 { - return string([]rune(monthNamesPunjabi[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesPunjabi[(t.Month() - 1)])[:1]) } return monthNamesPunjabi[int(t.Month())-1] } @@ -6132,7 +6132,7 @@ func localMonthsNamePunjabi(t time.Time, abbr int) string { // localMonthsNamePunjabiArab returns the Punjabi Arab name of the month. func localMonthsNamePunjabiArab(t time.Time, abbr int) string { if abbr == 5 { - return string([]rune(monthNamesPunjabiArab[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesPunjabiArab[(t.Month() - 1)])[:1]) } return monthNamesPunjabiArab[int(t.Month())-1] } @@ -6140,26 +6140,26 @@ func localMonthsNamePunjabiArab(t time.Time, abbr int) string { // localMonthsNameQuechua returns the Quechua name of the month. func localMonthsNameQuechua(t time.Time, abbr int) string { if abbr == 3 { - return string([]rune(monthNamesQuechua[int(t.Month()-1)])[:3]) + return string([]rune(monthNamesQuechua[(t.Month() - 1)])[:3]) } if abbr == 4 || abbr > 6 { return monthNamesQuechua[int(t.Month())-1] } - return string([]rune(monthNamesQuechua[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesQuechua[(t.Month() - 1)])[:1]) } // localMonthsNameQuechuaEcuador returns the QuechuaEcuador name of the month. func localMonthsNameQuechuaEcuador(t time.Time, abbr int) string { if abbr == 3 { if int(t.Month()) == 1 { - return string([]rune(monthNamesQuechuaEcuador[int(t.Month()-1)])[:4]) + return string([]rune(monthNamesQuechuaEcuador[(t.Month() - 1)])[:4]) } - return string([]rune(monthNamesQuechuaEcuador[int(t.Month()-1)])[:3]) + return string([]rune(monthNamesQuechuaEcuador[(t.Month() - 1)])[:3]) } if abbr == 4 || abbr > 6 { return monthNamesQuechuaEcuador[int(t.Month())-1] } - return string([]rune(monthNamesQuechuaEcuador[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesQuechuaEcuador[(t.Month() - 1)])[:1]) } // localMonthsNameRomanian returns the Romanian name of the month. @@ -6170,7 +6170,7 @@ func localMonthsNameRomanian(t time.Time, abbr int) string { if abbr == 4 || abbr > 6 { return monthNamesRomanian[int(t.Month())-1] } - return string([]rune(monthNamesRomanian[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesRomanian[(t.Month() - 1)])[:1]) } // localMonthsNameRomansh returns the Romansh name of the month. @@ -6181,7 +6181,7 @@ func localMonthsNameRomansh(t time.Time, abbr int) string { if abbr == 4 || abbr > 6 { return monthNamesRomansh[int(t.Month())-1] } - return string([]rune(monthNamesRomansh[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesRomansh[(t.Month() - 1)])[:1]) } // localMonthsNameRussian returns the Russian name of the month. @@ -6207,7 +6207,7 @@ func localMonthsNameSakha(t time.Time, abbr int) string { if abbr == 4 || abbr > 6 { return monthNamesSakha[int(t.Month())-1] } - return string([]rune(monthNamesSakha[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesSakha[(t.Month() - 1)])[:1]) } // localMonthsNameSami returns the Sami name of the month. @@ -6218,7 +6218,7 @@ func localMonthsNameSami(t time.Time, abbr int) string { if abbr == 4 || abbr > 6 { return monthNamesSami[int(t.Month())-1] } - return string([]rune(monthNamesSami[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesSami[(t.Month() - 1)])[:1]) } // localMonthsNameSamiLule returns the Sami (Lule) name of the month. @@ -6229,25 +6229,25 @@ func localMonthsNameSamiLule(t time.Time, abbr int) string { if abbr == 4 || abbr > 6 { return monthNamesSamiLule[int(t.Month())-1] } - return string([]rune(monthNamesSamiLule[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesSamiLule[(t.Month() - 1)])[:1]) } // localMonthsNameSamiNorthern returns the Sami (Northern) name of the month. func localMonthsNameSamiNorthern(t time.Time, abbr int) string { if abbr == 3 { - return monthNamesSamiNorthernAbbr[int(t.Month()-1)] + return monthNamesSamiNorthernAbbr[(t.Month() - 1)] } if abbr == 4 || abbr > 6 { return monthNamesSamiNorthern[int(t.Month())-1] } - return string([]rune(monthNamesSamiNorthern[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesSamiNorthern[(t.Month() - 1)])[:1]) } // localMonthsNameSamiNorthernFI returns the Sami (Northern) Finland name of the // month. func localMonthsNameSamiNorthernFI(t time.Time, abbr int) string { if abbr == 3 { - return monthNamesSamiNorthernAbbr[int(t.Month()-1)] + return monthNamesSamiNorthernAbbr[(t.Month() - 1)] } if abbr == 4 || abbr > 6 { if int(t.Month()) == 1 { @@ -6255,13 +6255,13 @@ func localMonthsNameSamiNorthernFI(t time.Time, abbr int) string { } return monthNamesSamiNorthern[int(t.Month())-1] } - return string([]rune(monthNamesSamiNorthern[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesSamiNorthern[(t.Month() - 1)])[:1]) } // localMonthsNameSamiSkolt returns the Sami (Skolt) name of the month. func localMonthsNameSamiSkolt(t time.Time, abbr int) string { if abbr == 5 { - return string([]rune(monthNamesSamiSkolt[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesSamiSkolt[(t.Month() - 1)])[:1]) } return monthNamesSamiSkolt[int(t.Month())-1] } @@ -6269,18 +6269,18 @@ func localMonthsNameSamiSkolt(t time.Time, abbr int) string { // localMonthsNameSamiSouthern returns the Sami (Southern) name of the month. func localMonthsNameSamiSouthern(t time.Time, abbr int) string { if abbr == 3 { - return monthNamesSamiSouthernAbbr[int(t.Month()-1)] + return monthNamesSamiSouthernAbbr[(t.Month() - 1)] } if abbr == 4 || abbr > 6 { return monthNamesSamiSouthern[int(t.Month())-1] } - return string([]rune(monthNamesSamiSouthern[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesSamiSouthern[(t.Month() - 1)])[:1]) } // localMonthsNameSanskrit returns the Sanskrit name of the month. func localMonthsNameSanskrit(t time.Time, abbr int) string { if abbr == 5 { - return string([]rune(monthNamesSanskrit[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesSanskrit[(t.Month() - 1)])[:1]) } return monthNamesSanskrit[int(t.Month())-1] } @@ -6288,85 +6288,85 @@ func localMonthsNameSanskrit(t time.Time, abbr int) string { // localMonthsNameScottishGaelic returns the Scottish Gaelic name of the month. func localMonthsNameScottishGaelic(t time.Time, abbr int) string { if abbr == 3 { - return monthNamesScottishGaelicAbbr[int(t.Month()-1)] + return monthNamesScottishGaelicAbbr[(t.Month() - 1)] } if abbr == 4 || abbr > 6 { return monthNamesScottishGaelic[int(t.Month())-1] } - return string([]rune(monthNamesScottishGaelic[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesScottishGaelic[(t.Month() - 1)])[:1]) } // localMonthsNameSerbian returns the Serbian (Cyrillic) name of the month. func localMonthsNameSerbian(t time.Time, abbr int) string { if abbr == 3 { - return monthNamesSerbianAbbr[int(t.Month()-1)] + return monthNamesSerbianAbbr[(t.Month() - 1)] } if abbr == 4 || abbr > 6 { return monthNamesSerbian[int(t.Month())-1] } - return string([]rune(monthNamesSerbian[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesSerbian[(t.Month() - 1)])[:1]) } // localMonthsNameSerbianBA returns the Serbian (Cyrillic) Bosnia and // Herzegovina name of the month. func localMonthsNameSerbianBA(t time.Time, abbr int) string { if abbr == 3 { - return monthNamesSerbianBAAbbr[int(t.Month()-1)] + return monthNamesSerbianBAAbbr[(t.Month() - 1)] } if abbr == 4 || abbr > 6 { return monthNamesSerbianBA[int(t.Month())-1] } - return string([]rune(monthNamesSerbianBA[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesSerbianBA[(t.Month() - 1)])[:1]) } // localMonthsNameSerbianLatin returns the Serbian (Latin) name of the month. func localMonthsNameSerbianLatin(t time.Time, abbr int) string { if abbr == 3 { - return string([]rune(monthNamesSerbianLatin[int(t.Month()-1)])[:3]) + return string([]rune(monthNamesSerbianLatin[(t.Month() - 1)])[:3]) } if abbr == 4 || abbr > 6 { return monthNamesSerbianLatin[int(t.Month())-1] } - return string([]rune(monthNamesSerbianLatin[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesSerbianLatin[(t.Month() - 1)])[:1]) } // localMonthsNameSerbianLatinCS returns the Serbian (Latin) name of the month. func localMonthsNameSerbianLatinCS(t time.Time, abbr int) string { if abbr == 3 { - return monthNamesSerbianLatinAbbr[int(t.Month()-1)] + return monthNamesSerbianLatinAbbr[(t.Month() - 1)] } if abbr == 4 || abbr > 6 { return monthNamesSerbianLatin[int(t.Month())-1] } - return string([]rune(monthNamesSerbianLatin[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesSerbianLatin[(t.Month() - 1)])[:1]) } // localMonthsNameSesothoSaLeboa returns the Sesotho sa Leboa name of the month. func localMonthsNameSesothoSaLeboa(t time.Time, abbr int) string { if abbr == 3 { - return monthNamesSesothoSaLeboaAbbr[int(t.Month()-1)] + return monthNamesSesothoSaLeboaAbbr[(t.Month() - 1)] } if abbr == 4 || abbr > 6 { return monthNamesSesothoSaLeboa[int(t.Month())-1] } - return string([]rune(monthNamesSesothoSaLeboa[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesSesothoSaLeboa[(t.Month() - 1)])[:1]) } // localMonthsNameSetswana returns the Setswana name of the month. func localMonthsNameSetswana(t time.Time, abbr int) string { if abbr == 3 { - return monthNamesSetswanaAbbr[int(t.Month()-1)] + return monthNamesSetswanaAbbr[(t.Month() - 1)] } if abbr == 4 || abbr > 6 { return monthNamesSetswana[int(t.Month())-1] } - return string([]rune(monthNamesSetswana[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesSetswana[(t.Month() - 1)])[:1]) } // localMonthsNameSindhi returns the Sindhi name of the month. func localMonthsNameSindhi(t time.Time, abbr int) string { if abbr == 5 { - return string([]rune(monthNamesSindhi[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesSindhi[(t.Month() - 1)])[:1]) } return monthNamesSindhi[int(t.Month())-1] } @@ -6374,12 +6374,12 @@ func localMonthsNameSindhi(t time.Time, abbr int) string { // localMonthsNameSinhala returns the Sinhala name of the month. func localMonthsNameSinhala(t time.Time, abbr int) string { if abbr == 3 { - return monthNamesSinhalaAbbr[int(t.Month()-1)] + return monthNamesSinhalaAbbr[(t.Month() - 1)] } if abbr == 4 || abbr > 6 { return monthNamesSinhala[int(t.Month())-1] } - return string([]rune(monthNamesSinhala[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesSinhala[(t.Month() - 1)])[:1]) } // localMonthsNameSlovak returns the Slovak name of the month. @@ -6390,40 +6390,40 @@ func localMonthsNameSlovak(t time.Time, abbr int) string { if abbr == 4 || abbr > 6 { return monthNamesSlovak[int(t.Month())-1] } - return string([]rune(monthNamesSlovak[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesSlovak[(t.Month() - 1)])[:1]) } // localMonthsNameSlovenian returns the Slovenian name of the month. func localMonthsNameSlovenian(t time.Time, abbr int) string { if abbr == 3 { - return monthNamesSlovenianAbbr[int(t.Month()-1)] + return monthNamesSlovenianAbbr[(t.Month() - 1)] } if abbr == 4 || abbr > 6 { return monthNamesSlovenian[int(t.Month())-1] } - return string([]rune(monthNamesSlovenian[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesSlovenian[(t.Month() - 1)])[:1]) } // localMonthsNameSomali returns the Somali name of the month. func localMonthsNameSomali(t time.Time, abbr int) string { if abbr == 3 { - return monthNamesSomaliAbbr[int(t.Month()-1)] + return monthNamesSomaliAbbr[(t.Month() - 1)] } if abbr == 4 || abbr > 6 { return monthNamesSomali[int(t.Month())-1] } - return string([]rune(monthNamesSomali[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesSomali[(t.Month() - 1)])[:1]) } // localMonthsNameSotho returns the Sotho name of the month. func localMonthsNameSotho(t time.Time, abbr int) string { if abbr == 3 { - return monthNamesSothoAbbr[int(t.Month()-1)] + return monthNamesSothoAbbr[(t.Month() - 1)] } if abbr == 4 || abbr > 6 { return monthNamesSotho[int(t.Month())-1] } - return string([]rune(monthNamesSotho[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesSotho[(t.Month() - 1)])[:1]) } // localMonthsNameSpanish returns the Spanish name of the month. @@ -6478,7 +6478,7 @@ func localMonthsNameSyriac(t time.Time, abbr int) string { if abbr == 4 { return monthNamesSyriac[int(t.Month())-1] } - return string([]rune(monthNamesSyriac[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesSyriac[(t.Month() - 1)])[:1]) } // localMonthsNameTajik returns the Tajik name of the month. @@ -6489,7 +6489,7 @@ func localMonthsNameTajik(t time.Time, abbr int) string { if abbr == 4 { return monthNamesTajik[int(t.Month())-1] } - return string([]rune(monthNamesTajik[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesTajik[(t.Month() - 1)])[:1]) } // localMonthsNameTamazight returns the Tamazight name of the month. @@ -6500,13 +6500,13 @@ func localMonthsNameTamazight(t time.Time, abbr int) string { if abbr == 4 { return monthNamesTamazight[int(t.Month())-1] } - return string([]rune(monthNamesTamazight[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesTamazight[(t.Month() - 1)])[:1]) } // localMonthsNameTamil returns the Tamil name of the month. func localMonthsNameTamil(t time.Time, abbr int) string { if abbr == 5 { - return string([]rune(monthNamesTamil[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesTamil[(t.Month() - 1)])[:1]) } return monthNamesTamil[int(t.Month())-1] } @@ -6519,7 +6519,7 @@ func localMonthsNameTamilLK(t time.Time, abbr int) string { if abbr == 4 { return monthNamesTamil[int(t.Month())-1] } - return string([]rune(monthNamesTamil[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesTamil[(t.Month() - 1)])[:1]) } // localMonthsNameTatar returns the Tatar name of the month. @@ -6530,7 +6530,7 @@ func localMonthsNameTatar(t time.Time, abbr int) string { if abbr == 4 { return monthNamesTatar[int(t.Month())-1] } - return string([]rune(monthNamesTatar[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesTatar[(t.Month() - 1)])[:1]) } // localMonthsNameTelugu returns the Telugu name of the month. @@ -6541,7 +6541,7 @@ func localMonthsNameTelugu(t time.Time, abbr int) string { if abbr == 4 { return monthNamesTelugu[int(t.Month())-1] } - return string([]rune(monthNamesTelugu[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesTelugu[(t.Month() - 1)])[:1]) } // localMonthsNameSyllabics returns the Syllabics name of the month. @@ -6589,7 +6589,7 @@ func localMonthsNameTigrinya(t time.Time, abbr int) string { if abbr == 4 { return monthNamesTigrinya[int(t.Month())-1] } - return string([]rune(monthNamesTigrinya[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesTigrinya[(t.Month() - 1)])[:1]) } // localMonthsNameTsonga returns the Tsonga name of the month. @@ -6600,7 +6600,7 @@ func localMonthsNameTsonga(t time.Time, abbr int) string { if abbr == 4 { return monthNamesTsonga[int(t.Month())-1] } - return string([]rune(monthNamesTsonga[int(t.Month()-1)])[:1]) + return string([]rune(monthNamesTsonga[(t.Month() - 1)])[:1]) } // localMonthsNameTraditionalMongolian returns the Traditional Mongolian name of @@ -6922,10 +6922,10 @@ func (nf *numberFormat) daysHandler(token nfp.Token) { } if strings.Contains(strings.ToUpper(token.TValue), "A") { if l == 3 { - nf.result += weekdayNamesAbbr[int(nf.t.Weekday())] + nf.result += weekdayNamesAbbr[nf.t.Weekday()] } if l > 3 { - nf.result += weekdayNames[int(nf.t.Weekday())] + nf.result += weekdayNames[nf.t.Weekday()] } return } @@ -6936,9 +6936,9 @@ func (nf *numberFormat) daysHandler(token nfp.Token) { case 2: nf.result += fmt.Sprintf("%02d", nf.t.Day()) case 3: - nf.result += weekdayNamesAbbr[int(nf.t.Weekday())] + nf.result += weekdayNamesAbbr[nf.t.Weekday()] default: - nf.result += weekdayNames[int(nf.t.Weekday())] + nf.result += weekdayNames[nf.t.Weekday()] } } } diff --git a/rows.go b/rows.go index 7541a0b217..878c8d857c 100644 --- a/rows.go +++ b/rows.go @@ -63,7 +63,7 @@ func (f *File) GetRows(sheet string, opts ...Options) ([][]string, error) { return nil, err } rows, _ := f.Rows(sheet) - results, cur, max := make([][]string, 0, 64), 0, 0 + results, cur, maxVal := make([][]string, 0, 64), 0, 0 for rows.Next() { cur++ row, err := rows.Columns(opts...) @@ -72,10 +72,10 @@ func (f *File) GetRows(sheet string, opts ...Options) ([][]string, error) { } results = append(results, row) if len(row) > 0 { - max = cur + maxVal = cur } } - return results[:max], rows.Close() + return results[:maxVal], rows.Close() } // Rows defines an iterator to a sheet. diff --git a/stream.go b/stream.go index 8fbbcfd72f..ef7fa1311b 100644 --- a/stream.go +++ b/stream.go @@ -439,24 +439,24 @@ func (sw *StreamWriter) SetRow(cell string, values []interface{}, opts ...RowOpt // the width column B:C as 20: // // err := sw.SetColWidth(2, 3, 20) -func (sw *StreamWriter) SetColWidth(min, max int, width float64) error { +func (sw *StreamWriter) SetColWidth(minVal, maxVal int, width float64) error { if sw.sheetWritten { return ErrStreamSetColWidth } - if min < MinColumns || min > MaxColumns || max < MinColumns || max > MaxColumns { + if minVal < MinColumns || minVal > MaxColumns || maxVal < MinColumns || maxVal > MaxColumns { return ErrColumnNumber } if width > MaxColumnWidth { return ErrColumnWidth } - if min > max { - min, max = max, min + if minVal > maxVal { + minVal, maxVal = maxVal, minVal } sw.cols.WriteString(``) diff --git a/table.go b/table.go index ea49d3cb33..c0c1594483 100644 --- a/table.go +++ b/table.go @@ -19,7 +19,6 @@ import ( "regexp" "strconv" "strings" - "unicode" "unicode/utf8" ) @@ -316,14 +315,22 @@ func checkDefinedName(name string) error { if utf8.RuneCountInString(name) > MaxFieldLength { return ErrNameLength } - for i, c := range name { - if string(c) == "_" { - continue + inCodeRange := func(code int, tbl []int) bool { + for i := 0; i < len(tbl); i += 2 { + if tbl[i] <= code && code <= tbl[i+1] { + return true + } } - if unicode.IsLetter(c) { - continue + return false + } + for i, c := range name { + if i == 0 { + if inCodeRange(int(c), supportedDefinedNameAtStartCharCodeRange) { + continue + } + return newInvalidNameError(name) } - if i > 0 && (unicode.IsDigit(c) || c == '.') { + if inCodeRange(int(c), supportedDefinedNameAfterStartCharCodeRange) { continue } return newInvalidNameError(name) diff --git a/templates.go b/templates.go index d9fb18c246..60c895d856 100644 --- a/templates.go +++ b/templates.go @@ -302,6 +302,163 @@ var IndexedColorMapping = []string{ "000000", "FFFFFF", } +// supportedDefinedNameAtStartCharCodeRange list the valid first character of a +// defined name ASCII letters. +var supportedDefinedNameAtStartCharCodeRange = []int{ + 65, 90, 92, 92, 95, 95, 97, 122, 161, 161, 164, 164, + 167, 168, 170, 170, 173, 173, 175, 186, 188, 696, 699, 705, + 711, 711, 713, 715, 717, 717, 720, 721, 728, 731, 733, 733, + 736, 740, 750, 750, 880, 883, 886, 887, 890, 893, 902, 902, + 904, 906, 908, 908, 910, 929, 931, 1013, 1015, 1153, 1162, 1315, + 1329, 1366, 1369, 1369, 1377, 1415, 1488, 1514, 1520, 1522, 1569, 1610, + 1646, 1647, 1649, 1747, 1749, 1749, 1765, 1766, 1774, 1775, 1786, 1788, + 1791, 1791, 1808, 1808, 1810, 1839, 1869, 1957, 1969, 1969, 1994, 2026, + 2036, 2037, 2042, 2042, 2308, 2361, 2365, 2365, 2384, 2384, 2392, 2401, + 2417, 2418, 2427, 2431, 2437, 2444, 2447, 2448, 2451, 2472, 2474, 2480, + 2482, 2482, 2486, 2489, 2493, 2493, 2510, 2510, 2524, 2525, 2527, 2529, + 2544, 2545, 2565, 2570, 2575, 2576, 2579, 2600, 2602, 2608, 2610, 2611, + 2613, 2614, 2616, 2617, 2649, 2652, 2654, 2654, 2674, 2676, 2693, 2701, + 2703, 2705, 2707, 2728, 2730, 2736, 2738, 2739, 2741, 2745, 2749, 2749, + 2768, 2768, 2784, 2785, 2821, 2828, 2831, 2832, 2835, 2856, 2858, 2864, + 2866, 2867, 2869, 2873, 2877, 2877, 2908, 2909, 2911, 2913, 2929, 2929, + 2947, 2947, 2949, 2954, 2958, 2960, 2962, 2965, 2969, 2970, 2972, 2972, + 2974, 2975, 2979, 2980, 2984, 2986, 2990, 3001, 3024, 3024, 3077, 3084, + 3086, 3088, 3090, 3112, 3114, 3123, 3125, 3129, 3133, 3133, 3160, 3161, + 3168, 3169, 3205, 3212, 3214, 3216, 3218, 3240, 3242, 3251, 3253, 3257, + 3261, 3261, 3294, 3294, 3296, 3297, 3333, 3340, 3342, 3344, 3346, 3368, + 3370, 3385, 3389, 3389, 3424, 3425, 3450, 3455, 3461, 3478, 3482, 3505, + 3507, 3515, 3517, 3517, 3520, 3526, 3585, 3642, 3648, 3662, 3713, 3714, + 3716, 3716, 3719, 3720, 3722, 3722, 3725, 3725, 3732, 3735, 3737, 3743, + 3745, 3747, 3749, 3749, 3751, 3751, 3754, 3755, 3757, 3760, 3762, 3763, + 3773, 3773, 3776, 3780, 3782, 3782, 3804, 3805, 3840, 3840, 3904, 3911, + 3913, 3948, 3976, 3979, 4096, 4138, 4159, 4159, 4176, 4181, 4186, 4189, + 4193, 4193, 4197, 4198, 4206, 4208, 4213, 4225, 4238, 4238, 4256, 4293, + 4304, 4346, 4348, 4348, 4352, 4441, 4447, 4514, 4520, 4601, 4608, 4680, + 4682, 4685, 4688, 4694, 4696, 4696, 4698, 4701, 4704, 4744, 4746, 4749, + 4752, 4784, 4786, 4789, 4792, 4798, 4800, 4800, 4802, 4805, 4808, 4822, + 4824, 4880, 4882, 4885, 4888, 4954, 4992, 5007, 5024, 5108, 5121, 5740, + 5743, 5750, 5761, 5786, 5792, 5866, 5870, 5872, 5888, 5900, 5902, 5905, + 5920, 5937, 5952, 5969, 5984, 5996, 5998, 6000, 6016, 6067, 6103, 6103, + 6108, 6108, 6176, 6263, 6272, 6312, 6314, 6314, 6400, 6428, 6480, 6509, + 6512, 6516, 6528, 6569, 6593, 6599, 6656, 6678, 6917, 6963, 6981, 6987, + 7043, 7072, 7086, 7087, 7168, 7203, 7245, 7247, 7258, 7293, 7424, 7615, + 7680, 7957, 7960, 7965, 7968, 8005, 8008, 8013, 8016, 8023, 8025, 8025, + 8027, 8027, 8029, 8029, 8031, 8061, 8064, 8116, 8118, 8124, 8126, 8126, + 8130, 8132, 8134, 8140, 8144, 8147, 8150, 8155, 8160, 8172, 8178, 8180, + 8182, 8188, 8208, 8208, 8211, 8214, 8216, 8216, 8220, 8221, 8224, 8225, + 8229, 8231, 8240, 8240, 8242, 8243, 8245, 8245, 8251, 8251, 8305, 8305, + 8308, 8308, 8319, 8319, 8321, 8324, 8336, 8340, 8450, 8451, 8453, 8453, + 8455, 8455, 8457, 8467, 8469, 8470, 8473, 8477, 8481, 8482, 8484, 8484, + 8486, 8486, 8488, 8488, 8490, 8493, 8495, 8505, 8508, 8511, 8517, 8521, + 8526, 8526, 8531, 8532, 8539, 8542, 8544, 8584, 8592, 8601, 8658, 8658, + 8660, 8660, 8704, 8704, 8706, 8707, 8711, 8712, 8715, 8715, 8719, 8719, + 8721, 8721, 8725, 8725, 8730, 8730, 8733, 8736, 8739, 8739, 8741, 8741, + 8743, 8748, 8750, 8750, 8756, 8759, 8764, 8765, 8776, 8776, 8780, 8780, + 8786, 8786, 8800, 8801, 8804, 8807, 8810, 8811, 8814, 8815, 8834, 8835, + 8838, 8839, 8853, 8853, 8857, 8857, 8869, 8869, 8895, 8895, 8978, 8978, + 9312, 9397, 9424, 9449, 9472, 9547, 9552, 9588, 9601, 9615, 9618, 9621, + 9632, 9633, 9635, 9641, 9650, 9651, 9654, 9655, 9660, 9661, 9664, 9665, + 9670, 9672, 9675, 9675, 9678, 9681, 9698, 9701, 9711, 9711, 9733, 9734, + 9737, 9737, 9742, 9743, 9756, 9756, 9758, 9758, 9792, 9792, 9794, 9794, + 9824, 9825, 9827, 9829, 9831, 9834, 9836, 9837, 9839, 9839, 11264, 11310, + 11312, 11358, 11360, 11375, 11377, 11389, 11392, 11492, 11520, 11557, 11568, 11621, + 11631, 11631, 11648, 11670, 11680, 11686, 11688, 11694, 11696, 11702, 11704, 11710, + 11712, 11718, 11720, 11726, 11728, 11734, 11736, 11742, 12288, 12291, 12293, 12311, + 12317, 12319, 12321, 12329, 12337, 12341, 12344, 12348, 12353, 12438, 12443, 12447, + 12449, 12543, 12549, 12589, 12593, 12686, 12704, 12727, 12784, 12828, 12832, 12841, + 12849, 12850, 12857, 12857, 12896, 12923, 12927, 12927, 12963, 12968, 13059, 13059, + 13069, 13069, 13076, 13076, 13080, 13080, 13090, 13091, 13094, 13095, 13099, 13099, + 13110, 13110, 13115, 13115, 13129, 13130, 13133, 13133, 13137, 13137, 13143, 13143, + 13179, 13182, 13184, 13188, 13192, 13258, 13261, 13267, 13269, 13270, 13272, 13272, + 13275, 13277, 13312, 19893, 19968, 40899, 40960, 42124, 42240, 42508, 42512, 42527, + 42538, 42539, 42560, 42591, 42594, 42606, 42624, 42647, 42786, 42887, 42891, 42892, + 43003, 43009, 43011, 43013, 43015, 43018, 43020, 43042, 43072, 43123, 43138, 43187, + 43274, 43301, 43312, 43334, 43520, 43560, 43584, 43586, 43588, 43595, 44032, 55203, + 57344, 63560, 63744, 64045, 64048, 64106, 64112, 64217, 64256, 64262, 64275, 64279, + 64285, 64285, 64287, 64296, 64298, 64310, 64312, 64316, 64318, 64318, 64320, 64321, + 64323, 64324, 64326, 64433, 64467, 64829, 64848, 64911, 64914, 64967, 65008, 65019, + 65072, 65073, 65075, 65092, 65097, 65106, 65108, 65111, 65113, 65126, 65128, 65131, + 65136, 65140, 65142, 65276, 65281, 65374, 65377, 65470, 65474, 65479, 65482, 65487, + 65490, 65495, 65498, 65500, 65504, 65510, +} + +// supportedDefinedNameAfterStartCharCodeRange list the valid after first +// character of a defined name ASCII letters. +var supportedDefinedNameAfterStartCharCodeRange = []int{ + 46, 46, 48, 57, 63, 63, 65, 90, 92, 92, 95, 95, + 97, 122, 161, 161, 164, 164, 167, 168, 170, 170, 173, 173, + 175, 186, 188, 887, 890, 893, 900, 902, 904, 906, 908, 908, + 910, 929, 931, 1315, 1329, 1366, 1369, 1369, 1377, 1415, 1425, 1469, + 1471, 1471, 1473, 1474, 1476, 1477, 1479, 1479, 1488, 1514, 1520, 1522, + 1536, 1539, 1542, 1544, 1547, 1547, 1550, 1562, 1567, 1567, 1569, 1630, + 1632, 1641, 1646, 1747, 1749, 1791, 1807, 1866, 1869, 1969, 1984, 2038, + 2042, 2042, 2305, 2361, 2364, 2381, 2384, 2388, 2392, 2403, 2406, 2415, + 2417, 2418, 2427, 2431, 2433, 2435, 2437, 2444, 2447, 2448, 2451, 2472, + 2474, 2480, 2482, 2482, 2486, 2489, 2492, 2500, 2503, 2504, 2507, 2510, + 2519, 2519, 2524, 2525, 2527, 2531, 2534, 2554, 2561, 2563, 2565, 2570, + 2575, 2576, 2579, 2600, 2602, 2608, 2610, 2611, 2613, 2614, 2616, 2617, + 2620, 2620, 2622, 2626, 2631, 2632, 2635, 2637, 2641, 2641, 2649, 2652, + 2654, 2654, 2662, 2677, 2689, 2691, 2693, 2701, 2703, 2705, 2707, 2728, + 2730, 2736, 2738, 2739, 2741, 2745, 2748, 2757, 2759, 2761, 2763, 2765, + 2768, 2768, 2784, 2787, 2790, 2799, 2801, 2801, 2817, 2819, 2821, 2828, + 2831, 2832, 2835, 2856, 2858, 2864, 2866, 2867, 2869, 2873, 2876, 2884, + 2887, 2888, 2891, 2893, 2902, 2903, 2908, 2909, 2911, 2915, 2918, 2929, + 2946, 2947, 2949, 2954, 2958, 2960, 2962, 2965, 2969, 2970, 2972, 2972, + 2974, 2975, 2979, 2980, 2984, 2986, 2990, 3001, 3006, 3010, 3014, 3016, + 3018, 3021, 3024, 3024, 3031, 3031, 3046, 3066, 3073, 3075, 3077, 3084, + 3086, 3088, 3090, 3112, 3114, 3123, 3125, 3129, 3133, 3140, 3142, 3144, + 3146, 3149, 3157, 3158, 3160, 3161, 3168, 3171, 3174, 3183, 3192, 3199, + 3202, 3203, 3205, 3212, 3214, 3216, 3218, 3240, 3242, 3251, 3253, 3257, + 3260, 3268, 3270, 3272, 3274, 3277, 3285, 3286, 3294, 3294, 3296, 3299, + 3302, 3311, 3313, 3314, 3330, 3331, 3333, 3340, 3342, 3344, 3346, 3368, + 3370, 3385, 3389, 3396, 3398, 3400, 3402, 3405, 3415, 3415, 3424, 3427, + 3430, 3445, 3449, 3455, 3458, 3459, 3461, 3478, 3482, 3505, 3507, 3515, + 3517, 3517, 3520, 3526, 3530, 3530, 3535, 3540, 3542, 3542, 3544, 3551, + 3570, 3571, 3585, 3642, 3647, 3662, 3664, 3673, 3713, 3714, 3716, 3716, + 3719, 3720, 3722, 3722, 3725, 3725, 3732, 3735, 3737, 3743, 3745, 3747, + 3749, 3749, 3751, 3751, 3754, 3755, 3757, 3769, 3771, 3773, 3776, 3780, + 3782, 3782, 3784, 3789, 3792, 3801, 3804, 3805, 3840, 3843, 3859, 3897, + 3902, 3911, 3913, 3948, 3953, 3972, 3974, 3979, 3984, 3991, 3993, 4028, + 4030, 4044, 4046, 4047, 4096, 4169, 4176, 4249, 4254, 4293, 4304, 4346, + 4348, 4348, 4352, 4441, 4447, 4514, 4520, 4601, 4608, 4680, 4682, 4685, + 4688, 4694, 4696, 4696, 4698, 4701, 4704, 4744, 4746, 4749, 4752, 4784, + 4786, 4789, 4792, 4798, 4800, 4800, 4802, 4805, 4808, 4822, 4824, 4880, + 4882, 4885, 4888, 4954, 4959, 4960, 4969, 4988, 4992, 5017, 5024, 5108, + 5121, 5740, 5743, 5750, 5760, 5786, 5792, 5866, 5870, 5872, 5888, 5900, + 5902, 5908, 5920, 5940, 5952, 5971, 5984, 5996, 5998, 6000, 6002, 6003, + 6016, 6099, 6103, 6103, 6107, 6109, 6112, 6121, 6128, 6137, 6155, 6158, + 6160, 6169, 6176, 6263, 6272, 6314, 6400, 6428, 6432, 6443, 6448, 6459, + 6464, 6464, 6470, 6509, 6512, 6516, 6528, 6569, 6576, 6601, 6608, 6617, + 6624, 6683, 6912, 6987, 6992, 7001, 7009, 7036, 7040, 7082, 7086, 7097, + 7168, 7223, 7232, 7241, 7245, 7293, 7424, 7654, 7678, 7957, 7960, 7965, + 7968, 8005, 8008, 8013, 8016, 8023, 8025, 8025, 8027, 8027, 8029, 8029, + 8031, 8061, 8064, 8116, 8118, 8132, 8134, 8147, 8150, 8155, 8157, 8175, + 8178, 8180, 8182, 8190, 8192, 8208, 8211, 8214, 8216, 8216, 8220, 8221, + 8224, 8225, 8229, 8240, 8242, 8243, 8245, 8245, 8251, 8251, 8260, 8260, + 8274, 8274, 8287, 8292, 8298, 8305, 8308, 8316, 8319, 8332, 8336, 8340, + 8352, 8373, 8400, 8432, 8448, 8527, 8531, 8584, 8592, 9000, 9003, 9191, + 9216, 9254, 9280, 9290, 9312, 9885, 9888, 9916, 9920, 9923, 9985, 9988, + 9990, 9993, 9996, 10023, 10025, 10059, 10061, 10061, 10063, 10066, 10070, 10070, + 10072, 10078, 10081, 10087, 10102, 10132, 10136, 10159, 10161, 10174, 10176, 10180, + 10183, 10186, 10188, 10188, 10192, 10213, 10224, 10626, 10649, 10711, 10716, 10747, + 10750, 11084, 11088, 11092, 11264, 11310, 11312, 11358, 11360, 11375, 11377, 11389, + 11392, 11498, 11517, 11517, 11520, 11557, 11568, 11621, 11631, 11631, 11648, 11670, + 11680, 11686, 11688, 11694, 11696, 11702, 11704, 11710, 11712, 11718, 11720, 11726, + 11728, 11734, 11736, 11742, 11744, 11775, 11823, 11823, 11904, 11929, 11931, 12019, + 12032, 12245, 12272, 12283, 12288, 12311, 12317, 12335, 12337, 12348, 12350, 12351, + 12353, 12438, 12441, 12447, 12449, 12543, 12549, 12589, 12593, 12686, 12688, 12727, + 12736, 12771, 12784, 12830, 12832, 12867, 12880, 13054, 13056, 19893, 19904, 40899, + 40960, 42124, 42128, 42182, 42240, 42508, 42512, 42539, 42560, 42591, 42594, 42610, + 42620, 42621, 42623, 42647, 42752, 42892, 43003, 43051, 43072, 43123, 43136, 43204, + 43216, 43225, 43264, 43310, 43312, 43347, 43520, 43574, 43584, 43597, 43600, 43609, + 44032, 55203, 55296, 64045, 64048, 64106, 64112, 64217, 64256, 64262, 64275, 64279, + 64285, 64310, 64312, 64316, 64318, 64318, 64320, 64321, 64323, 64324, 64326, 64433, + 64467, 64829, 64848, 64911, 64914, 64967, 65008, 65021, 65024, 65039, 65056, 65062, + 65072, 65073, 65075, 65092, 65097, 65106, 65108, 65111, 65113, 65126, 65128, 65131, + 65136, 65140, 65142, 65276, 65279, 65279, 65281, 65374, 65377, 65470, 65474, 65479, + 65482, 65487, 65490, 65495, 65498, 65500, 65504, 65510, 65512, 65518, 65529, 65533, +} + // supportedImageTypes defined supported image types. var supportedImageTypes = map[string]string{ ".bmp": ".bmp", ".emf": ".emf", ".emz": ".emz", ".gif": ".gif", @@ -332,7 +489,7 @@ var supportedDrawingUnderlineTypes = []string{ var supportedPositioning = []string{"absolute", "oneCell", "twoCell"} // builtInDefinedNames defined built-in defined names are built with a _xlnm prefix. -var builtInDefinedNames = []string{"_xlnm.Print_Area", "_xlnm.Print_Titles", "_xlnm._FilterDatabase"} +var builtInDefinedNames = []string{"_xlnm.Print_Area", "_xlnm.Print_Titles", "_xlnm.Criteria", "_xlnm._FilterDatabase", "_xlnm.Extract", "_xlnm.Consolidate_Area", "_xlnm.Database", "_xlnm.Sheet_Title"} const templateDocpropsApp = `0Go Excelize` From bb603b37d0e6506922361b923617555e4262ea5e Mon Sep 17 00:00:00 2001 From: helloWorld <451761285@qq.com> Date: Tue, 27 Feb 2024 16:43:47 +0800 Subject: [PATCH 861/957] Clear slave cells value when merging cells (#1824) --- merge.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/merge.go b/merge.go index 9acf54d639..2574c6c464 100644 --- a/merge.go +++ b/merge.go @@ -66,6 +66,17 @@ func (f *File) MergeCell(sheet, topLeftCell, bottomRightCell string) error { } ws.mu.Lock() defer ws.mu.Unlock() + for col := rect[0]; col <= rect[2]; col++ { + for row := rect[1]; row <= rect[3]; row++ { + if col == rect[0] && row == rect[1] { + continue + } + ws.prepareSheetXML(col, row) + c := &ws.SheetData.Row[row-1].C[col-1] + c.setCellDefault("") + _ = f.removeFormula(c, ws, sheet) + } + } ref := topLeftCell + ":" + bottomRightCell if ws.MergeCells != nil { ws.MergeCells.Cells = append(ws.MergeCells.Cells, &xlsxMergeCell{Ref: ref, rect: rect}) From 7b4da3906df28b38b4cbe2bab4a5c362c57630fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B2=B3=E6=99=A8=E6=97=AD?= Date: Thu, 29 Feb 2024 09:16:39 +0800 Subject: [PATCH 862/957] This closes #1819, closes #1827, formula function ISNUMBER, OR and FIND support matrix arguments (#1829) - Keep minimum column and row number in formula operand when deleting columns and rows - Update unit tests --- adjust.go | 10 +++++-- calc.go | 82 ++++++++++++++++++++++++++++++++++++---------------- calc_test.go | 10 ++++--- 3 files changed, 70 insertions(+), 32 deletions(-) diff --git a/adjust.go b/adjust.go index 5d6004067c..75faf16ad6 100644 --- a/adjust.go +++ b/adjust.go @@ -320,7 +320,9 @@ func adjustFormulaColumnName(name, operand string, abs, keepRelative bool, dir a return "", operand, false, err } if dir == columns && col >= num { - col += offset + if col += offset; col < 1 { + col = 1 + } colName, err := ColumnNumberToName(col) return "", operand + colName, false, err } @@ -334,8 +336,10 @@ func adjustFormulaRowNumber(name, operand string, abs, keepRelative bool, dir ad } row, _ := strconv.Atoi(name) if dir == rows && row >= num { - row += offset - if row <= 0 || row > TotalRows { + if row += offset; row < 1 { + row = 1 + } + if row > TotalRows { return "", operand + name, false, ErrMaxRows } return "", operand + strconv.Itoa(row), false, nil diff --git a/calc.go b/calc.go index 496ecd41ee..be5f6416aa 100644 --- a/calc.go +++ b/calc.go @@ -11602,7 +11602,22 @@ func (fn *formulaFuncs) ISNUMBER(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "ISNUMBER requires 1 argument") } - if argsList.Front().Value.(formulaArg).Type == ArgNumber { + arg := argsList.Front().Value.(formulaArg) + if arg.Type == ArgMatrix { + var mtx [][]formulaArg + for _, row := range arg.Matrix { + var array []formulaArg + for _, val := range row { + if val.Type == ArgNumber { + array = append(array, newBoolFormulaArg(true)) + } + array = append(array, newBoolFormulaArg(false)) + } + mtx = append(mtx, array) + } + return newMatrixFormulaArg(mtx) + } + if arg.Type == ArgNumber { return newBoolFormulaArg(true) } return newBoolFormulaArg(false) @@ -11951,11 +11966,14 @@ func (fn *formulaFuncs) OR(argsList *list.List) formulaArg { return newStringFormulaArg(strings.ToUpper(strconv.FormatBool(or))) } case ArgMatrix: - // TODO - return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + args := list.New() + for _, arg := range token.ToList() { + args.PushBack(arg) + } + return fn.OR(args) } } - return newStringFormulaArg(strings.ToUpper(strconv.FormatBool(or))) + return newBoolFormulaArg(or) } // SWITCH function compares a number of supplied values to a supplied test @@ -13741,34 +13759,48 @@ func (fn *formulaFuncs) find(name string, argsList *list.List) formulaArg { if args.Type != ArgList { return args } - findText := argsList.Front().Value.(formulaArg).Value() + findTextArg := argsList.Front().Value.(formulaArg) withinText := argsList.Front().Next().Value.(formulaArg).Value() startNum := int(args.List[0].Number) - if findText == "" { - return newNumberFormulaArg(float64(startNum)) - } dbcs, search := name == "FINDB" || name == "SEARCHB", name == "SEARCH" || name == "SEARCHB" - if search { - findText, withinText = strings.ToUpper(findText), strings.ToUpper(withinText) - } - offset, ok := matchPattern(findText, withinText, dbcs, startNum) - if !ok { - return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) - } - result := offset - if dbcs { - var pre int - for idx := range withinText { - if pre > offset { - break + find := func(findText string) formulaArg { + if findText == "" { + return newNumberFormulaArg(float64(startNum)) + } + if search { + findText, withinText = strings.ToUpper(findText), strings.ToUpper(withinText) + } + offset, ok := matchPattern(findText, withinText, dbcs, startNum) + if !ok { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + result := offset + if dbcs { + var pre int + for idx := range withinText { + if pre > offset { + break + } + if idx-pre > 1 { + result++ + } + pre = idx } - if idx-pre > 1 { - result++ + } + return newNumberFormulaArg(float64(result)) + } + if findTextArg.Type == ArgMatrix { + var mtx [][]formulaArg + for _, row := range findTextArg.Matrix { + var array []formulaArg + for _, findText := range row { + array = append(array, find(findText.Value())) } - pre = idx + mtx = append(mtx, array) } + return newMatrixFormulaArg(mtx) } - return newNumberFormulaArg(float64(result)) + return find(findTextArg.Value()) } // LEFT function returns a specified number of characters from the start of a diff --git a/calc_test.go b/calc_test.go index 350e516511..123db378bd 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1451,8 +1451,9 @@ func TestCalcCellValue(t *testing.T) { "=ISNONTEXT(\"Excelize\")": "FALSE", "=ISNONTEXT(NA())": "TRUE", // ISNUMBER - "=ISNUMBER(A1)": "TRUE", - "=ISNUMBER(D1)": "FALSE", + "=ISNUMBER(A1)": "TRUE", + "=ISNUMBER(D1)": "FALSE", + "=ISNUMBER(A1:B1)": "TRUE", // ISODD "=ISODD(A1)": "TRUE", "=ISODD(A2)": "FALSE", @@ -1526,6 +1527,7 @@ func TestCalcCellValue(t *testing.T) { "=OR(1=2,2=3)": "FALSE", "=OR(1=1,2=3)": "TRUE", "=OR(\"TRUE\",\"FALSE\")": "TRUE", + "=OR(A1:B1)": "TRUE", // SWITCH "=SWITCH(1,1,\"A\",2,\"B\",3,\"C\",\"N\")": "A", "=SWITCH(3,1,\"A\",2,\"B\",3,\"C\",\"N\")": "C", @@ -1748,6 +1750,7 @@ func TestCalcCellValue(t *testing.T) { "=FIND(\"\",\"Original Text\")": "1", "=FIND(\"\",\"Original Text\",2)": "2", "=FIND(\"s\",\"Sales\",2)": "5", + "=FIND(D1:E2,\"Month\")": "1", // FINDB "=FINDB(\"T\",\"Original Text\")": "10", "=FINDB(\"t\",\"Original Text\")": "13", @@ -3663,7 +3666,6 @@ func TestCalcCellValue(t *testing.T) { "=NOT(\"\")": {"#VALUE!", "NOT expects 1 boolean or numeric argument"}, // OR "=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"}, @@ -4773,7 +4775,7 @@ func TestCalcOR(t *testing.T) { }) fn := formulaFuncs{} result := fn.OR(argsList) - assert.Equal(t, result.String, "FALSE") + assert.Equal(t, result.Value(), "FALSE") assert.Empty(t, result.Error) } From 9d4c2e60f66b12da7760ab4bfafbe182d83c7583 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E7=8E=8B?= <1416825008@qq.com> Date: Fri, 1 Mar 2024 10:12:17 +0800 Subject: [PATCH 863/957] This closes #1825, made AddDataValidation and DeleteDataValidation functions concurrency safe (#1828) - Remove the receiver of internal coordinatesToRangeRef, squashSqref and flatSqref functions - Update unit tests Co-authored-by: chenwang --- adjust.go | 8 ++++---- cell_test.go | 12 ++++++++++++ datavalidation.go | 24 +++++++++++++++--------- lib.go | 4 ++-- lib_test.go | 9 ++++----- rows.go | 4 ++-- sheet.go | 2 +- stream.go | 2 +- table.go | 4 ++-- 9 files changed, 43 insertions(+), 26 deletions(-) diff --git a/adjust.go b/adjust.go index 75faf16ad6..b4f81f6ddd 100644 --- a/adjust.go +++ b/adjust.go @@ -267,7 +267,7 @@ func (f *File) adjustCellRef(ref string, dir adjustDirection, num, offset int) ( coordinates[3] += offset } } - ref, err = f.coordinatesToRangeRef(coordinates) + ref, err = coordinatesToRangeRef(coordinates) return ref, delete, err } @@ -666,7 +666,7 @@ func (f *File) adjustTable(ws *xlsxWorksheet, sheet string, dir adjustDirection, idx-- continue } - t.Ref, _ = f.coordinatesToRangeRef([]int{x1, y1, x2, y2}) + t.Ref, _ = coordinatesToRangeRef([]int{x1, y1, x2, y2}) if t.AutoFilter != nil { t.AutoFilter.Ref = t.Ref } @@ -706,7 +706,7 @@ func (f *File) adjustAutoFilter(ws *xlsxWorksheet, sheet string, dir adjustDirec coordinates = f.adjustAutoFilterHelper(dir, coordinates, num, offset) x1, y1, x2, y2 = coordinates[0], coordinates[1], coordinates[2], coordinates[3] - ws.AutoFilter.Ref, err = f.coordinatesToRangeRef([]int{x1, y1, x2, y2}) + ws.AutoFilter.Ref, err = coordinatesToRangeRef([]int{x1, y1, x2, y2}) return err } @@ -773,7 +773,7 @@ func (f *File) adjustMergeCells(ws *xlsxWorksheet, sheet string, dir adjustDirec continue } mergedCells.rect = []int{x1, y1, x2, y2} - if mergedCells.Ref, err = f.coordinatesToRangeRef([]int{x1, y1, x2, y2}); err != nil { + if mergedCells.Ref, err = coordinatesToRangeRef([]int{x1, y1, x2, y2}); err != nil { return err } } diff --git a/cell_test.go b/cell_test.go index 0ed6e87fbc..42472cfd72 100644 --- a/cell_test.go +++ b/cell_test.go @@ -88,6 +88,14 @@ func TestConcurrency(t *testing.T) { visible, err := f.GetColVisible("Sheet1", "A") assert.NoError(t, err) assert.Equal(t, true, visible) + // Concurrency add data validation + dv := NewDataValidation(true) + dv.Sqref = fmt.Sprintf("A%d:B%d", val, val) + dv.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorGreaterThan) + dv.SetInput(fmt.Sprintf("title:%d", val), strconv.Itoa(val)) + assert.NoError(t, f.AddDataValidation("Sheet1", dv)) + // Concurrency delete data validation with reference sequence + assert.NoError(t, f.DeleteDataValidation("Sheet1", dv.Sqref)) wg.Done() }(i, t) } @@ -97,6 +105,10 @@ func TestConcurrency(t *testing.T) { t.Error(err) } assert.Equal(t, "1", val) + // Test the length of data validation + dataValidations, err := f.GetDataValidations("Sheet1") + assert.NoError(t, err) + assert.Len(t, dataValidations, 0) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestConcurrency.xlsx"))) assert.NoError(t, f.Close()) } diff --git a/datavalidation.go b/datavalidation.go index 8e6e5945a1..c2ec1f1f8e 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -222,8 +222,9 @@ func (dv *DataValidation) SetSqref(sqref string) { } // AddDataValidation provides set data validation on a range of the worksheet -// by given data validation object and worksheet name. The data validation -// object can be created by NewDataValidation function. +// by given data validation object and worksheet name. This function is +// concurrency safe. The data validation object can be created by +// NewDataValidation function. // // Example 1, set data validation on Sheet1!A1:B2 with validation criteria // settings, show error alert after invalid data is entered with "Stop" style @@ -256,6 +257,8 @@ func (f *File) AddDataValidation(sheet string, dv *DataValidation) error { if err != nil { return err } + ws.mu.Lock() + defer ws.mu.Unlock() if nil == ws.DataValidations { ws.DataValidations = new(xlsxDataValidations) } @@ -323,13 +326,16 @@ func (f *File) GetDataValidations(sheet string) ([]*DataValidation, error) { } // DeleteDataValidation delete data validation by given worksheet name and -// reference sequence. All data validations in the worksheet will be deleted +// reference sequence. This function is concurrency safe. +// 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 } + ws.mu.Lock() + defer ws.mu.Unlock() if ws.DataValidations == nil { return nil } @@ -337,14 +343,14 @@ func (f *File) DeleteDataValidation(sheet string, sqref ...string) error { ws.DataValidations = nil return nil } - delCells, err := f.flatSqref(sqref[0]) + delCells, err := flatSqref(sqref[0]) if err != nil { return err } dv := ws.DataValidations for i := 0; i < len(dv.DataValidation); i++ { var applySqref []string - colCells, err := f.flatSqref(dv.DataValidation[i].Sqref) + colCells, err := flatSqref(dv.DataValidation[i].Sqref) if err != nil { return err } @@ -357,7 +363,7 @@ func (f *File) DeleteDataValidation(sheet string, sqref ...string) error { } } for _, col := range colCells { - applySqref = append(applySqref, f.squashSqref(col)...) + applySqref = append(applySqref, squashSqref(col)...) } dv.DataValidation[i].Sqref = strings.Join(applySqref, " ") if len(applySqref) == 0 { @@ -373,7 +379,7 @@ func (f *File) DeleteDataValidation(sheet string, sqref ...string) error { } // squashSqref generates cell reference sequence by given cells coordinates list. -func (f *File) squashSqref(cells [][]int) []string { +func squashSqref(cells [][]int) []string { if len(cells) == 1 { cell, _ := CoordinatesToCellName(cells[0][0], cells[0][1]) return []string{cell} @@ -384,7 +390,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 { - ref, _ := f.coordinatesToRangeRef(append(cells[l], cells[r]...)) + ref, _ := coordinatesToRangeRef(append(cells[l], cells[r]...)) if l == r { ref, _ = CoordinatesToCellName(cells[l][0], cells[l][1]) } @@ -394,7 +400,7 @@ func (f *File) squashSqref(cells [][]int) []string { r++ } } - ref, _ := f.coordinatesToRangeRef(append(cells[l], cells[r]...)) + ref, _ := coordinatesToRangeRef(append(cells[l], cells[r]...)) if l == r { ref, _ = CoordinatesToCellName(cells[l][0], cells[l][1]) } diff --git a/lib.go b/lib.go index a1e340c880..bfa992e587 100644 --- a/lib.go +++ b/lib.go @@ -323,7 +323,7 @@ func sortCoordinates(coordinates []int) error { // coordinatesToRangeRef provides a function to convert a pair of coordinates // to range reference. -func (f *File) coordinatesToRangeRef(coordinates []int, abs ...bool) (string, error) { +func coordinatesToRangeRef(coordinates []int, abs ...bool) (string, error) { if len(coordinates) != 4 { return "", ErrCoordinates } @@ -360,7 +360,7 @@ func (f *File) getDefinedNameRefTo(definedNameName, currentSheet string) (refTo } // flatSqref convert reference sequence to cell reference list. -func (f *File) flatSqref(sqref string) (cells map[int][][]int, err error) { +func flatSqref(sqref string) (cells map[int][][]int, err error) { var coordinates []int cells = make(map[int][][]int) for _, ref := range strings.Fields(sqref) { diff --git a/lib_test.go b/lib_test.go index 48e730d0f1..7500f4951a 100644 --- a/lib_test.go +++ b/lib_test.go @@ -218,14 +218,13 @@ func TestCoordinatesToCellName_Error(t *testing.T) { } func TestCoordinatesToRangeRef(t *testing.T) { - f := NewFile() - _, err := f.coordinatesToRangeRef([]int{}) + _, err := coordinatesToRangeRef([]int{}) assert.EqualError(t, err, ErrCoordinates.Error()) - _, err = f.coordinatesToRangeRef([]int{1, -1, 1, 1}) + _, err = coordinatesToRangeRef([]int{1, -1, 1, 1}) assert.Equal(t, newCoordinatesToCellNameError(1, -1), err) - _, err = f.coordinatesToRangeRef([]int{1, 1, 1, -1}) + _, err = coordinatesToRangeRef([]int{1, 1, 1, -1}) assert.Equal(t, newCoordinatesToCellNameError(1, -1), err) - ref, err := f.coordinatesToRangeRef([]int{1, 1, 1, 1}) + ref, err := coordinatesToRangeRef([]int{1, 1, 1, 1}) assert.NoError(t, err) assert.EqualValues(t, ref, "A1:A1") } diff --git a/rows.go b/rows.go index 878c8d857c..814ff9c353 100644 --- a/rows.go +++ b/rows.go @@ -705,7 +705,7 @@ func (f *File) duplicateConditionalFormat(ws *xlsxWorksheet, sheet string, row, x1, y1, x2, y2 := coordinates[0], coordinates[1], coordinates[2], coordinates[3] if y1 == y2 && y1 == row { cfCopy := deepcopy.Copy(*cf).(xlsxConditionalFormatting) - if cfCopy.SQRef, err = f.coordinatesToRangeRef([]int{x1, row2, x2, row2}, abs); err != nil { + if cfCopy.SQRef, err = coordinatesToRangeRef([]int{x1, row2, x2, row2}, abs); err != nil { return err } cfs = append(cfs, &cfCopy) @@ -736,7 +736,7 @@ func (f *File) duplicateDataValidations(ws *xlsxWorksheet, sheet string, row, ro x1, y1, x2, y2 := coordinates[0], coordinates[1], coordinates[2], coordinates[3] if y1 == y2 && y1 == row { dvCopy := deepcopy.Copy(*dv).(xlsxDataValidation) - if dvCopy.Sqref, err = f.coordinatesToRangeRef([]int{x1, row2, x2, row2}, abs); err != nil { + if dvCopy.Sqref, err = coordinatesToRangeRef([]int{x1, row2, x2, row2}, abs); err != nil { return err } dvs = append(dvs, &dvCopy) diff --git a/sheet.go b/sheet.go index 315507dbfa..e089c41e3c 100644 --- a/sheet.go +++ b/sheet.go @@ -2014,7 +2014,7 @@ func (f *File) SetSheetDimension(sheet string, rangeRef string) error { return err } _ = sortCoordinates(coordinates) - ref, err := f.coordinatesToRangeRef(coordinates) + ref, err := coordinatesToRangeRef(coordinates) ws.Dimension = &xlsxDimension{Ref: ref} return err } diff --git a/stream.go b/stream.go index ef7fa1311b..e8d9ea649c 100644 --- a/stream.go +++ b/stream.go @@ -184,7 +184,7 @@ func (sw *StreamWriter) AddTable(table *Table) error { } // Correct table reference range, such correct C1:B3 to B1:C3. - ref, err := sw.file.coordinatesToRangeRef(coordinates) + ref, err := coordinatesToRangeRef(coordinates) if err != nil { return err } diff --git a/table.go b/table.go index c0c1594483..611bc560c8 100644 --- a/table.go +++ b/table.go @@ -350,7 +350,7 @@ func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, opts *Tab y1++ } // Correct table range reference, such correct C1:B3 to B1:C3. - ref, err := f.coordinatesToRangeRef([]int{x1, y1, x2, y2}) + ref, err := coordinatesToRangeRef([]int{x1, y1, x2, y2}) if err != nil { return err } @@ -463,7 +463,7 @@ func (f *File) AutoFilter(sheet, rangeRef string, opts []AutoFilterOptions) erro } _ = sortCoordinates(coordinates) // Correct reference range, such correct C1:B3 to B1:C3. - ref, _ := f.coordinatesToRangeRef(coordinates, true) + ref, _ := coordinatesToRangeRef(coordinates, true) wb, err := f.workbookReader() if err != nil { return err From 963a0585358ac3a583655a4db66ed126e7232787 Mon Sep 17 00:00:00 2001 From: Paolo Barbolini Date: Sun, 3 Mar 2024 02:39:50 +0100 Subject: [PATCH 864/957] Optimize getSharedFormula to avoid runtime.duffcopy (#1837) --- cell.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cell.go b/cell.go index 5d547e814a..3f1dd486a2 100644 --- a/cell.go +++ b/cell.go @@ -1655,8 +1655,10 @@ 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 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 { + for row := 0; row < len(ws.SheetData.Row); row++ { + r := &ws.SheetData.Row[row] + for column := 0; column < len(r.C); column++ { + c := &r.C[column] if c.F != nil && c.F.Ref != "" && c.F.T == STCellFormulaTypeShared && c.F.Si != nil && *c.F.Si == si { col, row, _ := CellNameToCoordinates(cell) sharedCol, sharedRow, _ := CellNameToCoordinates(c.R) From f20bbd1f1dcb44eeaf60ed2f5bdc1ac77000a930 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 4 Mar 2024 21:40:27 +0800 Subject: [PATCH 865/957] This closes #1830, closes #1831, and closes #1833 - Fix a v2.8.1 regression bug, auto filter does not work in the LibreOffice - Fix a v2.8.1 regression bug, support to adjust data validation with multiple cell range - Fix incorrect result data type of the DATE formula function - Update the unit tests --- adjust.go | 69 +++++++++++++++++++++++++++----------------------- adjust_test.go | 10 ++++++++ calc.go | 2 +- calc_test.go | 5 ++-- table.go | 4 +-- table_test.go | 2 +- 6 files changed, 54 insertions(+), 38 deletions(-) diff --git a/adjust.go b/adjust.go index b4f81f6ddd..a92944d61a 100644 --- a/adjust.go +++ b/adjust.go @@ -237,38 +237,43 @@ func (f *File) adjustSingleRowFormulas(sheet, sheetN string, r *xlsxRow, num, of } // adjustCellRef provides a function to adjust cell reference. -func (f *File) adjustCellRef(ref string, dir adjustDirection, num, offset int) (string, bool, error) { - if !strings.Contains(ref, ":") { - ref += ":" + ref - } - var delete bool - coordinates, err := rangeRefToCoordinates(ref) - if err != nil { - return ref, delete, err - } - if dir == columns { - if offset < 0 && coordinates[0] == coordinates[2] { - delete = true - } - if coordinates[0] >= num { - coordinates[0] += offset - } - if coordinates[2] >= num { - coordinates[2] += offset +func (f *File) adjustCellRef(cellRef string, dir adjustDirection, num, offset int) (string, error) { + var SQRef []string + for _, ref := range strings.Split(cellRef, " ") { + if !strings.Contains(ref, ":") { + ref += ":" + ref } - } else { - if offset < 0 && coordinates[1] == coordinates[3] { - delete = true + coordinates, err := rangeRefToCoordinates(ref) + if err != nil { + return "", err } - if coordinates[1] >= num { - coordinates[1] += offset + if dir == columns { + if offset < 0 && coordinates[0] == coordinates[2] { + continue + } + if coordinates[0] >= num { + coordinates[0] += offset + } + if coordinates[2] >= num { + coordinates[2] += offset + } + } else { + if offset < 0 && coordinates[1] == coordinates[3] { + continue + } + if coordinates[1] >= num { + coordinates[1] += offset + } + if coordinates[3] >= num { + coordinates[3] += offset + } } - if coordinates[3] >= num { - coordinates[3] += offset + if ref, err = coordinatesToRangeRef(coordinates); err != nil { + return "", err } + SQRef = append(SQRef, ref) } - ref, err = coordinatesToRangeRef(coordinates) - return ref, delete, err + return strings.Join(SQRef, " "), nil } // adjustFormula provides a function to adjust formula reference and shared @@ -284,7 +289,7 @@ func (f *File) adjustFormula(sheet, sheetN string, cell *xlsxC, dir adjustDirect return nil } if cell.F.Ref != "" && sheet == sheetN { - if cell.F.Ref, _, err = f.adjustCellRef(cell.F.Ref, dir, num, offset); err != nil { + if cell.F.Ref, err = f.adjustCellRef(cell.F.Ref, dir, num, offset); err != nil { return err } if si && cell.F.Si != nil { @@ -932,11 +937,11 @@ func (f *File) adjustConditionalFormats(ws *xlsxWorksheet, sheet string, dir adj if cf == nil { continue } - ref, del, err := f.adjustCellRef(cf.SQRef, dir, num, offset) + ref, err := f.adjustCellRef(cf.SQRef, dir, num, offset) if err != nil { return err } - if del { + if ref == "" { ws.ConditionalFormatting = append(ws.ConditionalFormatting[:i], ws.ConditionalFormatting[i+1:]...) i-- @@ -967,11 +972,11 @@ func (f *File) adjustDataValidations(ws *xlsxWorksheet, sheet string, dir adjust continue } if sheet == sheetN { - ref, del, err := f.adjustCellRef(dv.Sqref, dir, num, offset) + ref, err := f.adjustCellRef(dv.Sqref, dir, num, offset) if err != nil { return err } - if del { + if ref == "" { worksheet.DataValidations.DataValidation = append(worksheet.DataValidations.DataValidation[:i], worksheet.DataValidations.DataValidation[i+1:]...) i-- diff --git a/adjust_test.go b/adjust_test.go index bfaa61c133..a6ea323993 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -1059,6 +1059,16 @@ func TestAdjustDataValidations(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "\"A<,B>,C\",D\t,E',F\"", dvs[2].Formula1) + // Test adjust data validation with multiple cell range + dv = NewDataValidation(true) + dv.Sqref = "G1:G3 H1:H3" + assert.NoError(t, dv.SetDropList([]string{"1", "2", "3"})) + assert.NoError(t, f.AddDataValidation("Sheet1", dv)) + assert.NoError(t, f.InsertRows("Sheet1", 2, 1)) + dvs, err = f.GetDataValidations("Sheet1") + assert.NoError(t, err) + assert.Equal(t, "G1:G4 H1:H4", dvs[3].Sqref) + dv = NewDataValidation(true) dv.Sqref = "C5:D6" assert.NoError(t, dv.SetRange("Sheet1!A1048576", "Sheet1!XFD1", DataValidationTypeWhole, DataValidationOperatorBetween)) diff --git a/calc.go b/calc.go index be5f6416aa..433efb80d7 100644 --- a/calc.go +++ b/calc.go @@ -12081,7 +12081,7 @@ func (fn *formulaFuncs) DATE(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorVALUE, "DATE requires 3 number arguments") } d := makeDate(int(year.Number), time.Month(month.Number), int(day.Number)) - return newStringFormulaArg(timeFromExcelTime(daysBetween(excelMinTime1900.Unix(), d)+1, false).String()) + return newNumberFormulaArg(daysBetween(excelMinTime1900.Unix(), d) + 1) } // calcDateDif is an implementation of the formula function DATEDIF, diff --git a/calc_test.go b/calc_test.go index 123db378bd..71d0acd0c7 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1540,8 +1540,9 @@ func TestCalcCellValue(t *testing.T) { "=XOR(1>0,0>1,INT(0),INT(1),A1:A4,2)": "FALSE", // Date and Time Functions // DATE - "=DATE(2020,10,21)": "2020-10-21 00:00:00 +0000 UTC", - "=DATE(1900,1,1)": "1899-12-31 00:00:00 +0000 UTC", + "=DATE(2020,10,21)": "44125", + "=DATE(2020,10,21)+1": "44126", + "=DATE(1900,1,1)": "1", // DATEDIF "=DATEDIF(43101,43101,\"D\")": "0", "=DATEDIF(43101,43891,\"d\")": "790", diff --git a/table.go b/table.go index 611bc560c8..5ca7894e67 100644 --- a/table.go +++ b/table.go @@ -474,7 +474,7 @@ func (f *File) AutoFilter(sheet, rangeRef string, opts []AutoFilterOptions) erro } filterRange := fmt.Sprintf("'%s'!%s", sheet, ref) d := xlsxDefinedName{ - Name: builtInDefinedNames[2], + Name: builtInDefinedNames[3], Hidden: true, LocalSheetID: intPtr(sheetID), Data: filterRange, @@ -490,7 +490,7 @@ func (f *File) AutoFilter(sheet, rangeRef string, opts []AutoFilterOptions) erro if definedName.LocalSheetID != nil { localSheetID = *definedName.LocalSheetID } - if definedName.Name == builtInDefinedNames[2] && localSheetID == sheetID && definedName.Hidden { + if definedName.Name == builtInDefinedNames[3] && localSheetID == sheetID && definedName.Hidden { wb.DefinedNames.DefinedName[idx].Data = filterRange definedNameExists = true } diff --git a/table_test.go b/table_test.go index cda9cb07de..8abfe1fb40 100644 --- a/table_test.go +++ b/table_test.go @@ -174,7 +174,7 @@ func TestAutoFilter(t *testing.T) { assert.EqualError(t, f.AutoFilter("Sheet1", "D4:B1", nil), "XML syntax error on line 1: invalid UTF-8") // Test add auto filter with empty local sheet ID f = NewFile() - f.WorkBook = &xlsxWorkbook{DefinedNames: &xlsxDefinedNames{DefinedName: []xlsxDefinedName{{Name: builtInDefinedNames[2], Hidden: true}}}} + f.WorkBook = &xlsxWorkbook{DefinedNames: &xlsxDefinedNames{DefinedName: []xlsxDefinedName{{Name: builtInDefinedNames[3], Hidden: true}}}} assert.NoError(t, f.AutoFilter("Sheet1", "A1:B1", nil)) } From 4ed493819a82313907217faeecb9f8713bbc66f0 Mon Sep 17 00:00:00 2001 From: Evan lu <55533161+iEvan-lhr@users.noreply.github.com> Date: Wed, 6 Mar 2024 09:26:38 +0800 Subject: [PATCH 866/957] This closes #1835, support get data validations which storage in the extension lists (#1834) --- cell_test.go | 2 +- datavalidation.go | 87 +++++++++++++++++++++++++++++------------- datavalidation_test.go | 26 +++++++++++++ templates.go | 2 +- xmlWorksheet.go | 1 + 5 files changed, 90 insertions(+), 28 deletions(-) diff --git a/cell_test.go b/cell_test.go index 42472cfd72..61178c29a3 100644 --- a/cell_test.go +++ b/cell_test.go @@ -91,7 +91,7 @@ func TestConcurrency(t *testing.T) { // Concurrency add data validation dv := NewDataValidation(true) dv.Sqref = fmt.Sprintf("A%d:B%d", val, val) - dv.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorGreaterThan) + assert.NoError(t, dv.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorGreaterThan)) dv.SetInput(fmt.Sprintf("title:%d", val), strconv.Itoa(val)) assert.NoError(t, f.AddDataValidation("Sheet1", dv)) // Concurrency delete data validation with reference sequence diff --git a/datavalidation.go b/datavalidation.go index c2ec1f1f8e..40ffd1950f 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -13,6 +13,7 @@ package excelize import ( "fmt" + "io" "math" "strings" "unicode/utf16" @@ -293,36 +294,70 @@ func (f *File) GetDataValidations(sheet string) ([]*DataValidation, error) { if err != nil { return nil, err } - if ws.DataValidations == nil || len(ws.DataValidations.DataValidation) == 0 { - return nil, err + var ( + dataValidations []*DataValidation + decodeExtLst = new(decodeExtLst) + decodeDataValidations *xlsxDataValidations + ext *xlsxExt + ) + if ws.DataValidations != nil { + dataValidations = append(dataValidations, getDataValidations(ws.DataValidations)...) } - var dvs []*DataValidation - for _, dv := range ws.DataValidations.DataValidation { - if dv != nil { - dataValidation := &DataValidation{ - AllowBlank: dv.AllowBlank, - Error: dv.Error, - ErrorStyle: dv.ErrorStyle, - ErrorTitle: dv.ErrorTitle, - Operator: dv.Operator, - Prompt: dv.Prompt, - PromptTitle: dv.PromptTitle, - ShowDropDown: dv.ShowDropDown, - ShowErrorMessage: dv.ShowErrorMessage, - ShowInputMessage: dv.ShowInputMessage, - Sqref: dv.Sqref, - Type: dv.Type, - } - if dv.Formula1 != nil { - dataValidation.Formula1 = unescapeDataValidationFormula(dv.Formula1.Content) - } - if dv.Formula2 != nil { - dataValidation.Formula2 = unescapeDataValidationFormula(dv.Formula2.Content) + if ws.ExtLst != nil { + if err = f.xmlNewDecoder(strings.NewReader("" + ws.ExtLst.Ext + "")). + Decode(decodeExtLst); err != nil && err != io.EOF { + return dataValidations, err + } + for _, ext = range decodeExtLst.Ext { + if ext.URI == ExtURIDataValidations { + decodeDataValidations = new(xlsxDataValidations) + _ = f.xmlNewDecoder(strings.NewReader(ext.Content)).Decode(decodeDataValidations) + dataValidations = append(dataValidations, getDataValidations(decodeDataValidations)...) } - dvs = append(dvs, dataValidation) } } - return dvs, err + return dataValidations, err +} + +// getDataValidations returns data validations list by given worksheet data +// validations. +func getDataValidations(dvs *xlsxDataValidations) []*DataValidation { + if dvs == nil { + return nil + } + var dataValidations []*DataValidation + for _, dv := range dvs.DataValidation { + if dv == nil { + continue + } + dataValidation := &DataValidation{ + AllowBlank: dv.AllowBlank, + Error: dv.Error, + ErrorStyle: dv.ErrorStyle, + ErrorTitle: dv.ErrorTitle, + Operator: dv.Operator, + Prompt: dv.Prompt, + PromptTitle: dv.PromptTitle, + ShowDropDown: dv.ShowDropDown, + ShowErrorMessage: dv.ShowErrorMessage, + ShowInputMessage: dv.ShowInputMessage, + Sqref: dv.Sqref, + Type: dv.Type, + } + if dv.Formula1 != nil { + dataValidation.Formula1 = unescapeDataValidationFormula(dv.Formula1.Content) + } + if dv.Formula2 != nil { + dataValidation.Formula2 = unescapeDataValidationFormula(dv.Formula2.Content) + } + if dv.XMSqref != "" { + dataValidation.Sqref = dv.XMSqref + dataValidation.Formula1 = strings.TrimSuffix(strings.TrimPrefix(dataValidation.Formula1, ""), "") + dataValidation.Formula2 = strings.TrimSuffix(strings.TrimPrefix(dataValidation.Formula2, ""), "") + } + dataValidations = append(dataValidations, dataValidation) + } + return dataValidations } // DeleteDataValidation delete data validation by given worksheet name and diff --git a/datavalidation_test.go b/datavalidation_test.go index 8816df0664..7508ba33b6 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -12,6 +12,7 @@ package excelize import ( + "fmt" "math" "path/filepath" "strings" @@ -104,6 +105,31 @@ func TestDataValidation(t *testing.T) { dataValidations, err = f.GetDataValidations("Sheet1") assert.NoError(t, err) assert.Equal(t, []*DataValidation(nil), dataValidations) + + // Test get data validations which storage in the extension lists + f = NewFile() + ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") + assert.True(t, ok) + ws.(*xlsxWorksheet).ExtLst = &xlsxExtLst{Ext: fmt.Sprintf(`Sheet1!$B$1:$B$5A7:B8`, ExtURIDataValidations, NameSpaceSpreadSheetX14.Value)} + dataValidations, err = f.GetDataValidations("Sheet1") + assert.NoError(t, err) + assert.Equal(t, []*DataValidation{ + { + AllowBlank: true, + Type: "list", + Formula1: "Sheet1!$B$1:$B$5", + Sqref: "A7:B8", + }, + }, dataValidations) + + // Test get data validations with invalid extension list characters + ws.(*xlsxWorksheet).ExtLst = &xlsxExtLst{Ext: fmt.Sprintf(``, ExtURIDataValidations, NameSpaceSpreadSheetX14.Value)} + _, err = f.GetDataValidations("Sheet1") + assert.EqualError(t, err, "XML syntax error on line 1: element closed by ") + + // Test get validations without validations + assert.Nil(t, getDataValidations(nil)) + assert.Nil(t, getDataValidations(&xlsxDataValidations{DataValidation: []*xlsxDataValidation{nil}})) } func TestDataValidationError(t *testing.T) { diff --git a/templates.go b/templates.go index 60c895d856..0f21be54bc 100644 --- a/templates.go +++ b/templates.go @@ -103,7 +103,7 @@ const ( ExtURIConditionalFormattingRuleID = "{B025F937-C7B1-47D3-B67F-A62EFF666E3E}" ExtURIConditionalFormattings = "{78C0D931-6437-407d-A8EE-F0AAD7539E65}" ExtURIDataModel = "{FCE2AD5D-F65C-4FA6-A056-5C36A1767C68}" - ExtURIDataValidations = "{CCE6A557-97BC-4B89-ADB6-D9C93CAAB3DF}" + ExtURIDataValidations = "{CCE6A557-97BC-4b89-ADB6-D9C93CAAB3DF}" ExtURIDrawingBlip = "{28A0092B-C50C-407E-A947-70E740481C1C}" ExtURIExternalLinkPr = "{FCE6A71B-6B00-49CD-AB44-F6B1AE7CDE65}" ExtURIIgnoredErrors = "{01252117-D84E-4E92-8308-4BE1C098FCBB}" diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 83e08054f5..c5e06b8293 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -441,6 +441,7 @@ type xlsxDataValidation struct { ShowErrorMessage bool `xml:"showErrorMessage,attr,omitempty"` ShowInputMessage bool `xml:"showInputMessage,attr,omitempty"` Sqref string `xml:"sqref,attr"` + XMSqref string `xml:"sqref,omitempty"` Type string `xml:"type,attr,omitempty"` Formula1 *xlsxInnerXML `xml:"formula1"` Formula2 *xlsxInnerXML `xml:"formula2"` From 585ebff5b7b20924a80c35b4a15fece6ff2473da Mon Sep 17 00:00:00 2001 From: yeahyear <138094847+yetyear@users.noreply.github.com> Date: Wed, 13 Mar 2024 14:39:22 +0800 Subject: [PATCH 867/957] Typo fix for the comment of the extractStyleCondFuncs variable (#1846) Signed-off-by: yetyear --- styles.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/styles.go b/styles.go index 3b7c28ab2d..4192f1180a 100644 --- a/styles.go +++ b/styles.go @@ -1136,7 +1136,7 @@ var ( }, } - // extractStyleCondFuncs provides a function set to returns if shoudle be + // extractStyleCondFuncs provides a function set to returns if should be // extract style definition by given style. extractStyleCondFuncs = map[string]func(xlsxXf, *xlsxStyleSheet) bool{ "fill": func(xf xlsxXf, s *xlsxStyleSheet) bool { From 4eb088cf736120228ba69fd740f940c3b309cc86 Mon Sep 17 00:00:00 2001 From: hu5ky <1650546312@qq.com> Date: Fri, 15 Mar 2024 11:36:34 +0800 Subject: [PATCH 868/957] This fix performance impact introduced in #1692 (#1849) Co-authored-by: chun.zhang2 - This fix speed slowdown and memory usage increase base on the reverts commit 6220a798fd79231bf4b3d7ef587bb2b79d276710 - Fix panic on read workbook with internal row element without r attribute - Update the unit tests --- adjust.go | 10 ++-- adjust_test.go | 2 +- cell.go | 6 +-- cell_test.go | 8 ++-- col.go | 6 +-- col_test.go | 2 +- excelize.go | 123 ++++++++++++++++++++++++------------------------ rows.go | 14 +++--- sheet.go | 2 +- xmlWorksheet.go | 2 +- 10 files changed, 88 insertions(+), 87 deletions(-) diff --git a/adjust.go b/adjust.go index a92944d61a..46b8215e76 100644 --- a/adjust.go +++ b/adjust.go @@ -201,13 +201,13 @@ func (f *File) adjustRowDimensions(sheet string, ws *xlsxWorksheet, row, offset return nil } lastRow := &ws.SheetData.Row[totalRows-1] - if newRow := *lastRow.R + offset; *lastRow.R >= row && newRow > 0 && newRow > TotalRows { + if newRow := lastRow.R + offset; lastRow.R >= row && newRow > 0 && newRow > TotalRows { return ErrMaxRows } numOfRows := len(ws.SheetData.Row) for i := 0; i < numOfRows; i++ { r := &ws.SheetData.Row[i] - if newRow := *r.R + offset; *r.R >= row && newRow > 0 { + if newRow := r.R + offset; r.R >= row && newRow > 0 { r.adjustSingleRowDimensions(offset) } if err := f.adjustSingleRowFormulas(sheet, sheet, r, row, offset, false); err != nil { @@ -219,10 +219,10 @@ func (f *File) adjustRowDimensions(sheet string, ws *xlsxWorksheet, row, offset // adjustSingleRowDimensions provides a function to adjust single row dimensions. func (r *xlsxRow) adjustSingleRowDimensions(offset int) { - r.R = intPtr(*r.R + offset) + r.R += offset for i, col := range r.C { colName, _, _ := SplitCellName(col.R) - r.C[i].R, _ = JoinCellName(colName, *r.R) + r.C[i].R, _ = JoinCellName(colName, r.R) } } @@ -701,7 +701,7 @@ func (f *File) adjustAutoFilter(ws *xlsxWorksheet, sheet string, dir adjustDirec ws.AutoFilter = nil for rowIdx := range ws.SheetData.Row { rowData := &ws.SheetData.Row[rowIdx] - if rowData.R != nil && *rowData.R > y1 && *rowData.R <= y2 { + if rowData.R > y1 && rowData.R <= y2 { rowData.Hidden = false } } diff --git a/adjust_test.go b/adjust_test.go index a6ea323993..00b5112db0 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -289,7 +289,7 @@ func TestAdjustAutoFilter(t *testing.T) { f := NewFile() assert.NoError(t, f.adjustAutoFilter(&xlsxWorksheet{ SheetData: xlsxSheetData{ - Row: []xlsxRow{{Hidden: true, R: intPtr(2)}}, + Row: []xlsxRow{{Hidden: true, R: 2}}, }, AutoFilter: &xlsxAutoFilter{ Ref: "A1:A3", diff --git a/cell.go b/cell.go index 3f1dd486a2..e9b5730ed3 100644 --- a/cell.go +++ b/cell.go @@ -1426,8 +1426,8 @@ func (f *File) getCellStringFunc(sheet, cell string, fn func(x *xlsxWorksheet, c return "", err } lastRowNum := 0 - if l := len(ws.SheetData.Row); l > 0 && ws.SheetData.Row[l-1].R != nil { - lastRowNum = *ws.SheetData.Row[l-1].R + if l := len(ws.SheetData.Row); l > 0 { + lastRowNum = ws.SheetData.Row[l-1].R } // keep in mind: row starts from 1 @@ -1437,7 +1437,7 @@ func (f *File) getCellStringFunc(sheet, cell string, fn func(x *xlsxWorksheet, c for rowIdx := range ws.SheetData.Row { rowData := &ws.SheetData.Row[rowIdx] - if rowData.R != nil && *rowData.R != row { + if rowData.R != row { continue } for colIdx := range rowData.C { diff --git a/cell_test.go b/cell_test.go index 61178c29a3..7517d28f6e 100644 --- a/cell_test.go +++ b/cell_test.go @@ -278,7 +278,7 @@ func TestSetCellValue(t *testing.T) { f.Pkg.Store(defaultXMLPathSharedStrings, []byte(fmt.Sprintf(`aa`, NameSpaceSpreadSheet.Value))) f.Sheet.Store("xl/worksheets/sheet1.xml", &xlsxWorksheet{ SheetData: xlsxSheetData{Row: []xlsxRow{ - {R: intPtr(1), C: []xlsxC{{R: "A1", T: "str", V: "1"}}}, + {R: 1, C: []xlsxC{{R: "A1", T: "str", V: "1"}}}, }}, }) assert.NoError(t, f.SetCellValue("Sheet1", "A1", "b")) @@ -387,6 +387,9 @@ func TestGetCellValue(t *testing.T) { f.Sheet.Delete("xl/worksheets/sheet1.xml") f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `H6r0A6F4A6B6C6100B3`))) f.checked = sync.Map{} + cell, err = f.GetCellValue("Sheet1", "H6") + assert.Equal(t, "H6", cell) + assert.NoError(t, err) rows, err = f.GetRows("Sheet1") assert.Equal(t, [][]string{ {"A6", "B6", "C6"}, @@ -397,9 +400,6 @@ func TestGetCellValue(t *testing.T) { {"", "", "", "", "", "", "", "H6"}, }, rows) assert.NoError(t, err) - cell, err = f.GetCellValue("Sheet1", "H6") - assert.Equal(t, "H6", cell) - assert.NoError(t, err) f.Sheet.Delete("xl/worksheets/sheet1.xml") f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `A1A3`))) diff --git a/col.go b/col.go index b1b9c0d38c..68ffc70cc0 100644 --- a/col.go +++ b/col.go @@ -63,16 +63,16 @@ type Cols struct { // fmt.Println() // } func (f *File) GetCols(sheet string, opts ...Options) ([][]string, error) { - if _, err := f.workSheetReader(sheet); err != nil { + cols, err := f.Cols(sheet) + if err != nil { return nil, err } - cols, err := f.Cols(sheet) results := make([][]string, 0, 64) for cols.Next() { col, _ := cols.Rows(opts...) results = append(results, col) } - return results, err + return results, nil } // Next will return true if the next column is found. diff --git a/col_test.go b/col_test.go index 6174254395..2e7aeb80c7 100644 --- a/col_test.go +++ b/col_test.go @@ -140,7 +140,7 @@ func TestGetColsError(t *testing.T) { f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(`B`, NameSpaceSpreadSheet.Value))) f.checked = sync.Map{} _, err = f.GetCols("Sheet1") - assert.EqualError(t, err, `strconv.ParseInt: parsing "A": invalid syntax`) + assert.EqualError(t, err, `strconv.Atoi: parsing "A": invalid syntax`) f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(`B`, NameSpaceSpreadSheet.Value))) _, err = f.GetCols("Sheet1") diff --git a/excelize.go b/excelize.go index 87ef22dd52..de385de874 100644 --- a/excelize.go +++ b/excelize.go @@ -305,84 +305,85 @@ 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 (ws *xlsxWorksheet) checkSheet() { - row, r0 := ws.checkSheetRows() - sheetData := xlsxSheetData{Row: make([]xlsxRow, row)} - row = 0 - for _, r := range ws.SheetData.Row { - if r.R == nil { - row++ - r.R = intPtr(row) - sheetData.Row[row-1] = r - continue - } - if *r.R == row && row > 0 { - sheetData.Row[*r.R-1].C = append(sheetData.Row[*r.R-1].C, r.C...) - continue - } - if *r.R != 0 { - sheetData.Row[*r.R-1] = r - row = *r.R - } - } - for i := 1; i <= len(sheetData.Row); i++ { - sheetData.Row[i-1].R = intPtr(i) - } - ws.checkSheetR0(&sheetData, r0) -} - -// checkSheetRows returns the last row number of the worksheet and rows element -// with r="0" attribute. -func (ws *xlsxWorksheet) checkSheetRows() (int, []xlsxRow) { var ( - row, maxVal int - r0 []xlsxRow - maxRowNum = func(num int, c []xlsxC) int { - for _, cell := range c { - if _, n, err := CellNameToCoordinates(cell.R); err == nil && n > num { - num = n + row int + r0Rows []xlsxRow + lastRowNum = func(r xlsxRow) int { + var num int + for _, cell := range r.C { + if _, row, err := CellNameToCoordinates(cell.R); err == nil { + if row > num { + num = row + } } } return num } ) - for i, r := range ws.SheetData.Row { - if r.R == nil { - row++ - continue - } - if i == 0 && *r.R == 0 { - if num := maxRowNum(row, r.C); num > maxVal { - maxVal = num + for i := 0; i < len(ws.SheetData.Row); i++ { + r := ws.SheetData.Row[i] + if r.R == 0 || r.R == row { + num := lastRowNum(r) + if num > row { + row = num } - r0 = append(r0, r) + if num == 0 { + row++ + } + r.R = row + r0Rows = append(r0Rows, r) + ws.SheetData.Row = append(ws.SheetData.Row[:i], ws.SheetData.Row[i+1:]...) + i-- continue } - if *r.R != 0 && *r.R > row { - row = *r.R + if r.R != 0 && r.R > row { + row = r.R } } - if maxVal > row { - row = maxVal + sheetData := xlsxSheetData{Row: make([]xlsxRow, row)} + row = 0 + for _, r := range ws.SheetData.Row { + if r.R != 0 { + sheetData.Row[r.R-1] = r + row = r.R + } + } + for _, r0Row := range r0Rows { + sheetData.Row[r0Row.R-1].R = r0Row.R + ws.checkSheetR0(&sheetData, &r0Row, true) + } + for i := 1; i <= row; i++ { + sheetData.Row[i-1].R = i + ws.checkSheetR0(&sheetData, &sheetData.Row[i-1], false) } - return row, 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 (ws *xlsxWorksheet) checkSheetR0(sheetData *xlsxSheetData, r0s []xlsxRow) { - for _, r0 := range r0s { - for _, cell := range r0.C { - if col, row, err := CellNameToCoordinates(cell.R); err == nil { - rowIdx := row - 1 - columns, colIdx := len(sheetData.Row[rowIdx].C), col-1 - for c := columns; c < col; c++ { - sheetData.Row[rowIdx].C = append(sheetData.Row[rowIdx].C, xlsxC{}) - } - if !sheetData.Row[rowIdx].C[colIdx].hasValue() { - sheetData.Row[rowIdx].C[colIdx] = cell - } - } +func (ws *xlsxWorksheet) checkSheetR0(sheetData *xlsxSheetData, rowData *xlsxRow, r0 bool) { + checkRow := func(col, row int, r0 bool, cell xlsxC) { + rowIdx := row - 1 + columns, colIdx := len(sheetData.Row[rowIdx].C), col-1 + for c := columns; c < col; c++ { + sheetData.Row[rowIdx].C = append(sheetData.Row[rowIdx].C, xlsxC{}) + } + if !sheetData.Row[rowIdx].C[colIdx].hasValue() { + sheetData.Row[rowIdx].C[colIdx] = cell + } + if r0 { + sheetData.Row[rowIdx].C[colIdx] = cell + } + } + var err error + for i, cell := range rowData.C { + col, row := i+1, rowData.R + if cell.R == "" { + checkRow(col, row, r0, cell) + continue + } + if col, row, err = CellNameToCoordinates(cell.R); err == nil && r0 { + checkRow(col, row, r0, cell) } } ws.SheetData = *sheetData diff --git a/rows.go b/rows.go index 814ff9c353..7610836f17 100644 --- a/rows.go +++ b/rows.go @@ -59,10 +59,10 @@ var duplicateHelperFunc = [3]func(*File, *xlsxWorksheet, string, int, int) error // fmt.Println() // } func (f *File) GetRows(sheet string, opts ...Options) ([][]string, error) { - if _, err := f.workSheetReader(sheet); err != nil { + rows, err := f.Rows(sheet) + if err != nil { return nil, err } - rows, _ := f.Rows(sheet) results, cur, maxVal := make([][]string, 0, 64), 0, 0 for rows.Next() { cur++ @@ -392,7 +392,7 @@ func (f *File) getRowHeight(sheet string, row int) int { defer ws.mu.Unlock() for i := range ws.SheetData.Row { v := &ws.SheetData.Row[i] - if v.R != nil && *v.R == row && v.Ht != nil { + if v.R == row && v.Ht != nil { return int(convertRowHeightToPixels(*v.Ht)) } } @@ -423,7 +423,7 @@ 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 != nil && *v.R == row && v.Ht != nil { + if v.R == row && v.Ht != nil { return *v.Ht, nil } } @@ -578,7 +578,7 @@ func (f *File) RemoveRow(sheet string, row int) error { keep := 0 for rowIdx := 0; rowIdx < len(ws.SheetData.Row); rowIdx++ { v := &ws.SheetData.Row[rowIdx] - if v.R != nil && *v.R != row { + if v.R != row { ws.SheetData.Row[keep] = *v keep++ } @@ -649,7 +649,7 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) error { var rowCopy xlsxRow for i, r := range ws.SheetData.Row { - if *r.R == row { + if r.R == row { rowCopy = deepcopy.Copy(ws.SheetData.Row[i]).(xlsxRow) ok = true break @@ -666,7 +666,7 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) error { idx2 := -1 for i, r := range ws.SheetData.Row { - if *r.R == row2 { + if r.R == row2 { idx2 = i break } diff --git a/sheet.go b/sheet.go index e089c41e3c..f1267f7c80 100644 --- a/sheet.go +++ b/sheet.go @@ -1957,7 +1957,7 @@ func (ws *xlsxWorksheet) prepareSheetXML(col int, row int) { if rowCount < row { // append missing rows for rowIdx := rowCount; rowIdx < row; rowIdx++ { - ws.SheetData.Row = append(ws.SheetData.Row, xlsxRow{R: intPtr(rowIdx + 1), CustomHeight: customHeight, Ht: ht, C: make([]xlsxC, 0, sizeHint)}) + ws.SheetData.Row = append(ws.SheetData.Row, xlsxRow{R: rowIdx + 1, CustomHeight: customHeight, Ht: ht, C: make([]xlsxC, 0, sizeHint)}) } } rowData := &ws.SheetData.Row[row-1] diff --git a/xmlWorksheet.go b/xmlWorksheet.go index c5e06b8293..3a71d698f4 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -308,7 +308,7 @@ type xlsxSheetData struct { // particular row in the worksheet. type xlsxRow struct { C []xlsxC `xml:"c"` - R *int `xml:"r,attr"` + R int `xml:"r,attr,omitempty"` Spans string `xml:"spans,attr,omitempty"` S int `xml:"s,attr,omitempty"` CustomFormat bool `xml:"customFormat,attr,omitempty"` From 9e884c798be12fc1b2b985ed2db7f09f765f990a Mon Sep 17 00:00:00 2001 From: vic <1018595261@qq.com> Date: Tue, 19 Mar 2024 08:58:52 +0800 Subject: [PATCH 869/957] This closes #1847, support apply number format with alignment (#1852) - Update dependencies module - Update unit tests --- excelize_test.go | 10 +++++----- go.mod | 6 +++--- go.sum | 8 ++++++++ numfmt.go | 29 +++++++++++++++++++++++------ numfmt_test.go | 13 +++++++++---- 5 files changed, 48 insertions(+), 18 deletions(-) diff --git a/excelize_test.go b/excelize_test.go index 9879302648..95f0b41fa3 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -742,11 +742,11 @@ 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 3/4", "37947 3/4", "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 ", "37,948", "$37,948", "37,947.75", "$37,947.75", "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 3/4", "-37947 3/4", "-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)", "(37,948)", "$(37,948)", "(37,947.75)", "$(37,947.75)", "-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 ", "0 ", "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", "$0", "0.01", "$0.01", "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/9", "2 1/10", "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", "$2", "2.10", "$2.10", "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"}, + {"37947.75", "37948", "37947.75", "37,948", "37,947.75", "3794775%", "3794775.00%", "3.79E+04", "37947 3/4", "37947 3/4", "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 ", " 37,948 ", " $37,948 ", " 37,947.75 ", " $37,947.75 ", "00:00", "910746:00:00", "00:00.0", "37947.7500001", "37947.7500001"}, + {"-37947.75", "-37948", "-37947.75", "-37,948", "-37,947.75", "-3794775%", "-3794775.00%", "-3.79E+04", "-37947 3/4", "-37947 3/4", "-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)", " (37,948)", " $(37,948)", " (37,947.75)", " $(37,947.75)", "-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 ", "0 ", "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 ", " $0 ", " 0.01 ", " $0.01 ", "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/9", "2 1/10", "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 ", " $2 ", " 2.10 ", " $2.10 ", "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"}, } for c, v := range value { diff --git a/go.mod b/go.mod index 357b84e631..8c12fa1eb6 100644 --- a/go.mod +++ b/go.mod @@ -7,10 +7,10 @@ require ( github.com/richardlehane/mscfb v1.0.4 github.com/stretchr/testify v1.8.4 github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53 - github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05 - golang.org/x/crypto v0.19.0 + github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 + golang.org/x/crypto v0.21.0 golang.org/x/image v0.14.0 - golang.org/x/net v0.21.0 + golang.org/x/net v0.22.0 golang.org/x/text v0.14.0 ) diff --git a/go.sum b/go.sum index 64de49a3e6..9c04edd934 100644 --- a/go.sum +++ b/go.sum @@ -15,12 +15,20 @@ github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53 h1:Chd9DkqERQQuHpXjR/HSV1 github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05 h1:qhbILQo1K3mphbwKh1vNm4oGezE1eF9fQWmNiIpSfI4= github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= +github.com/xuri/nfp v0.0.0-20240316161844-5bacf1a74267 h1:p0lQ21ogqdVWcdXpqSlD7gu/3whO1YWNiOaPJNBfunU= +github.com/xuri/nfp v0.0.0-20240316161844-5bacf1a74267/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= +github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A= +github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4= golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/numfmt.go b/numfmt.go index d37130b741..47f2d4a56f 100644 --- a/numfmt.go +++ b/numfmt.go @@ -682,6 +682,7 @@ var ( } // supportedTokenTypes list the supported number format token types currently. supportedTokenTypes = []string{ + nfp.TokenTypeAlignment, nfp.TokenSubTypeCurrencyString, nfp.TokenSubTypeLanguageInfo, nfp.TokenTypeColor, @@ -4797,14 +4798,14 @@ func format(value, numFmt string, date1904 bool, cellType CellType, opts *Option if nf.isNumeric { switch section.Type { case nfp.TokenSectionPositive: - return nf.positiveHandler() + return nf.alignmentHandler(nf.positiveHandler()) case nfp.TokenSectionNegative: - return nf.negativeHandler() + return nf.alignmentHandler(nf.negativeHandler()) default: - return nf.zeroHandler() + return nf.alignmentHandler(nf.zeroHandler()) } } - return nf.textHandler() + return nf.alignmentHandler(nf.textHandler()) } return value } @@ -5082,13 +5083,29 @@ func (nf *numberFormat) dateTimeHandler() string { return nf.printSwitchArgument(nf.result) } +// alignmentHandler will be handling alignment token for each number format +// selection for a number format expression. +func (nf *numberFormat) alignmentHandler(result string) string { + tokens := nf.section[nf.sectionIdx].Items + if len(tokens) == 0 { + return result + } + if tokens[0].TType == nfp.TokenTypeAlignment { + result = nfp.Whitespace + result + } + if l := len(tokens); tokens[l-1].TType == nfp.TokenTypeAlignment { + result += nfp.Whitespace + } + return result +} + // 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 token.TType == nfp.TokenTypeGeneral { + return strconv.FormatFloat(nf.number, 'G', 10, 64) } if inStrSlice(supportedNumberTokenTypes, token.TType, true) != -1 { fmtNum = true diff --git a/numfmt_test.go b/numfmt_test.go index b449feb44c..49e4fa03be 100644 --- a/numfmt_test.go +++ b/numfmt_test.go @@ -11,6 +11,9 @@ func TestNumFmt(t *testing.T) { for _, item := range [][]string{ {"123", "general", "123"}, {"-123", ";general", "-123"}, + {"43543.5448726851", "General", "43543.54487"}, + {"-43543.5448726851", "General", "-43543.54487"}, + {"1234567890.12345", "General", "1234567890"}, {"43528", "y", "19"}, {"43528", "Y", "19"}, {"43528", "yy", "19"}, @@ -3488,14 +3491,16 @@ func TestNumFmt(t *testing.T) { {"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"}, - {"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 "}, {"-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)"}, + {"-8.04506", "$#,##0.00_);[Red]($#,##0.00)", "($8.05)"}, + {"43543.5448726851", `_("$"* #,##0.00_);_("$"* \(#,##0.00\);_("$"* "-"??_);_(@_)`, " $43,543.54 "}, {"1234.5678", "0", "1235"}, {"1234.5678", "0.00", "1234.57"}, {"1234.5678", "#,##0", "1,235"}, From 5975d87f7eb9da5a986bfedd82cfbdd696210d06 Mon Sep 17 00:00:00 2001 From: realzuojianxiang <568625626@qq.com> Date: Fri, 22 Mar 2024 16:09:45 +0800 Subject: [PATCH 870/957] This closes #1851, and closes #1856 fix formula calculation result round issue (#1860) - The SetSheetName function now support case sensitivity - Update unit tests --- calc.go | 18 +++++++++--------- calc_test.go | 26 ++++++++++++++++++++++---- sheet.go | 2 +- sheet_test.go | 5 ++++- 4 files changed, 36 insertions(+), 15 deletions(-) diff --git a/calc.go b/calc.go index 433efb80d7..8777bb9413 100644 --- a/calc.go +++ b/calc.go @@ -831,8 +831,8 @@ func (f *File) CalcCellValue(sheet, cell string, opts ...Options) (result string if !rawCellValue { styleIdx, _ = f.GetCellStyle(sheet, cell) } - result = token.Value() - if isNum, precision, decimal := isNumeric(result); isNum && !rawCellValue { + if token.Type == ArgNumber && !token.Boolean { + _, precision, decimal := isNumeric(token.Value()) if precision > 15 { result, err = f.formattedValue(&xlsxC{S: styleIdx, V: strings.ToUpper(strconv.FormatFloat(decimal, 'G', 15, 64))}, rawCellValue, CellTypeNumber) return @@ -840,7 +840,9 @@ func (f *File) CalcCellValue(sheet, cell string, opts ...Options) (result string if !strings.HasPrefix(result, "0") { result, err = f.formattedValue(&xlsxC{S: styleIdx, V: strings.ToUpper(strconv.FormatFloat(decimal, 'f', -1, 64))}, rawCellValue, CellTypeNumber) } + return } + result, err = f.formattedValue(&xlsxC{S: styleIdx, V: token.Value()}, rawCellValue, CellTypeInlineString) return } @@ -4281,7 +4283,7 @@ func (fn *formulaFuncs) EXP(argsList *list.List) formulaArg { if number.Type == ArgError { return number } - return newStringFormulaArg(strings.ToUpper(fmt.Sprintf("%g", math.Exp(number.Number)))) + return newNumberFormulaArg(math.Exp(number.Number)) } // fact returns the factorial of a supplied number. @@ -4359,7 +4361,7 @@ func (fn *formulaFuncs) FLOOR(argsList *list.List) formulaArg { val-- } } - return newStringFormulaArg(strings.ToUpper(fmt.Sprintf("%g", val*significance.Number))) + return newNumberFormulaArg(val * significance.Number) } // FLOORdotMATH function rounds a supplied number down to a supplied multiple @@ -11570,12 +11572,10 @@ func (fn *formulaFuncs) ISNA(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "ISNA requires 1 argument") } - token := argsList.Front().Value.(formulaArg) - result := "FALSE" - if token.Type == ArgError && token.String == formulaErrorNA { - result = "TRUE" + if token := argsList.Front().Value.(formulaArg); token.Type == ArgError && token.String == formulaErrorNA { + return newBoolFormulaArg(true) } - return newStringFormulaArg(result) + return newBoolFormulaArg(false) } // ISNONTEXT function tests if a supplied value is text. If not, the diff --git a/calc_test.go b/calc_test.go index 71d0acd0c7..a842a2365e 100644 --- a/calc_test.go +++ b/calc_test.go @@ -6311,13 +6311,31 @@ func TestFormulaRawCellValueOption(t *testing.T) { raw bool expected string }{ - {"=\"10e3\"", false, "10000"}, + {"=VALUE(\"1.0E-07\")", false, "0.00"}, + {"=VALUE(\"1.0E-07\")", true, "0.0000001"}, + {"=\"text\"", false, "$text"}, + {"=\"text\"", true, "text"}, + {"=\"10e3\"", false, "$10e3"}, {"=\"10e3\"", true, "10e3"}, - {"=\"10\" & \"e3\"", false, "10000"}, + {"=\"10\" & \"e3\"", false, "$10e3"}, {"=\"10\" & \"e3\"", true, "10e3"}, - {"=\"1111111111111111\"", false, "1.11111111111111E+15"}, + {"=10e3", false, "10000.00"}, + {"=10e3", true, "10000"}, + {"=\"1111111111111111\"", false, "$1111111111111111"}, {"=\"1111111111111111\"", true, "1111111111111111"}, - } + {"=1111111111111111", false, "1111111111111110.00"}, + {"=1111111111111111", true, "1.11111111111111E+15"}, + {"=1444.00000000003", false, "1444.00"}, + {"=1444.00000000003", true, "1444.00000000003"}, + {"=1444.000000000003", false, "1444.00"}, + {"=1444.000000000003", true, "1444"}, + {"=ROUND(1444.00000000000003,2)", false, "1444.00"}, + {"=ROUND(1444.00000000000003,2)", true, "1444"}, + } + exp := "0.00;0.00;;$@" + styleID, err := f.NewStyle(&Style{CustomNumFmt: &exp}) + assert.NoError(t, err) + assert.NoError(t, f.SetCellStyle("Sheet1", "A1", "A1", styleID)) for _, test := range rawTest { assert.NoError(t, f.SetCellFormula("Sheet1", "A1", test.value)) val, err := f.CalcCellValue("Sheet1", "A1", Options{RawCellValue: test.raw}) diff --git a/sheet.go b/sheet.go index f1267f7c80..f9f067a5cb 100644 --- a/sheet.go +++ b/sheet.go @@ -363,7 +363,7 @@ func (f *File) SetSheetName(source, target string) error { if err = checkSheetName(target); err != nil { return err } - if strings.EqualFold(target, source) { + if target == source { return err } wb, _ := f.workbookReader() diff --git a/sheet_test.go b/sheet_test.go index 271262ee1c..5c96efc1d2 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -465,8 +465,11 @@ func TestSetSheetName(t *testing.T) { // Test set worksheet with the same name assert.NoError(t, f.SetSheetName("Sheet1", "Sheet1")) assert.Equal(t, "Sheet1", f.GetSheetName(0)) + // Test set worksheet with the different 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()) + assert.Equal(t, f.SetSheetName("Sheet:1", "Sheet1"), ErrSheetNameInvalid) // Test set worksheet name with existing defined name and auto filter assert.NoError(t, f.AutoFilter("Sheet1", "A1:A2", nil)) From 703b73779c06265bc8f23e3d9cbd628d6635fead Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 25 Mar 2024 08:33:29 +0800 Subject: [PATCH 871/957] This closes #1861, fix missing parentheses in the adjusted formula - Allow adjust cell reference with max rows/columns - Fix incorrect data validation escape result - Update out date reference link in the documentation - Update unit tests --- adjust.go | 57 +++++++++++++++++++++++++++----------------------- adjust_test.go | 12 +++++++++-- excelize.go | 6 +++--- 3 files changed, 44 insertions(+), 31 deletions(-) diff --git a/adjust.go b/adjust.go index 46b8215e76..7c8ec9ee21 100644 --- a/adjust.go +++ b/adjust.go @@ -239,6 +239,17 @@ func (f *File) adjustSingleRowFormulas(sheet, sheetN string, r *xlsxRow, num, of // adjustCellRef provides a function to adjust cell reference. func (f *File) adjustCellRef(cellRef string, dir adjustDirection, num, offset int) (string, error) { var SQRef []string + applyOffset := func(coordinates []int, idx1, idx2, maxVal int) []int { + if coordinates[idx1] >= num { + coordinates[idx1] += offset + } + if coordinates[idx2] >= num { + if coordinates[idx2] += offset; coordinates[idx2] > maxVal { + coordinates[idx2] = maxVal + } + } + return coordinates + } for _, ref := range strings.Split(cellRef, " ") { if !strings.Contains(ref, ":") { ref += ":" + ref @@ -251,22 +262,12 @@ func (f *File) adjustCellRef(cellRef string, dir adjustDirection, num, offset in if offset < 0 && coordinates[0] == coordinates[2] { continue } - if coordinates[0] >= num { - coordinates[0] += offset - } - if coordinates[2] >= num { - coordinates[2] += offset - } + coordinates = applyOffset(coordinates, 0, 2, MaxColumns) } else { if offset < 0 && coordinates[1] == coordinates[3] { continue } - if coordinates[1] >= num { - coordinates[1] += offset - } - if coordinates[3] >= num { - coordinates[3] += offset - } + coordinates = applyOffset(coordinates, 1, 3, TotalRows) } if ref, err = coordinatesToRangeRef(coordinates); err != nil { return "", err @@ -446,12 +447,8 @@ func (f *File) adjustFormulaRef(sheet, sheetN, formula string, keepRelative bool val += operand continue } - if isFunctionStartToken(token) { - val += token.TValue + string(efp.ParenOpen) - continue - } - if isFunctionStopToken(token) { - val += token.TValue + string(efp.ParenClose) + if paren := transformParenthesesToken(token); paren != "" { + val += transformParenthesesToken(token) continue } if token.TType == efp.TokenTypeOperand && token.TSubType == efp.TokenSubTypeText { @@ -463,6 +460,18 @@ func (f *File) adjustFormulaRef(sheet, sheetN, formula string, keepRelative bool return val, nil } +// transformParenthesesToken returns formula part with parentheses by given +// token. +func transformParenthesesToken(token efp.Token) string { + if isFunctionStartToken(token) || isBeginParenthesesToken(token) { + return token.TValue + string(efp.ParenOpen) + } + if isFunctionStopToken(token) || isEndParenthesesToken(token) { + return token.TValue + string(efp.ParenClose) + } + return "" +} + // adjustRangeSheetName returns replaced range reference by given source and // target sheet name. func adjustRangeSheetName(rng, source, target string) string { @@ -551,12 +560,8 @@ func transformArrayFormula(tokens []efp.Token, afs []arrayFormulaOperandToken) s if skip { continue } - if isFunctionStartToken(token) { - val += token.TValue + string(efp.ParenOpen) - continue - } - if isFunctionStopToken(token) { - val += token.TValue + string(efp.ParenClose) + if paren := transformParenthesesToken(token); paren != "" { + val += transformParenthesesToken(token) continue } if token.TType == efp.TokenTypeOperand && token.TSubType == efp.TokenSubTypeText { @@ -985,14 +990,14 @@ func (f *File) adjustDataValidations(ws *xlsxWorksheet, sheet string, dir adjust worksheet.DataValidations.DataValidation[i].Sqref = ref } if worksheet.DataValidations.DataValidation[i].Formula1 != nil { - formula := unescapeDataValidationFormula(worksheet.DataValidations.DataValidation[i].Formula1.Content) + formula := formulaUnescaper.Replace(worksheet.DataValidations.DataValidation[i].Formula1.Content) if formula, err = f.adjustFormulaRef(sheet, sheetN, formula, false, dir, num, offset); err != nil { return err } worksheet.DataValidations.DataValidation[i].Formula1 = &xlsxInnerXML{Content: formulaEscaper.Replace(formula)} } if worksheet.DataValidations.DataValidation[i].Formula2 != nil { - formula := unescapeDataValidationFormula(worksheet.DataValidations.DataValidation[i].Formula2.Content) + formula := formulaUnescaper.Replace(worksheet.DataValidations.DataValidation[i].Formula2.Content) if formula, err = f.adjustFormulaRef(sheet, sheetN, formula, false, dir, num, offset); err != nil { return err } diff --git a/adjust_test.go b/adjust_test.go index 00b5112db0..2982562d97 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -463,6 +463,14 @@ func TestAdjustCols(t *testing.T) { assert.NoError(t, f.InsertCols("Sheet1", "A", 2)) assert.Nil(t, ws.(*xlsxWorksheet).Cols) + + f = NewFile() + assert.NoError(t, f.SetCellFormula("Sheet1", "A2", "(1-0.5)/2")) + assert.NoError(t, f.InsertCols("Sheet1", "A", 1)) + formula, err := f.GetCellFormula("Sheet1", "B2") + assert.NoError(t, err) + assert.Equal(t, "(1-0.5)/2", formula) + assert.NoError(t, f.Close()) } func TestAdjustColDimensions(t *testing.T) { @@ -1061,13 +1069,13 @@ func TestAdjustDataValidations(t *testing.T) { // Test adjust data validation with multiple cell range dv = NewDataValidation(true) - dv.Sqref = "G1:G3 H1:H3" + dv.Sqref = "G1:G3 H1:H3 A3:A1048576" assert.NoError(t, dv.SetDropList([]string{"1", "2", "3"})) assert.NoError(t, f.AddDataValidation("Sheet1", dv)) assert.NoError(t, f.InsertRows("Sheet1", 2, 1)) dvs, err = f.GetDataValidations("Sheet1") assert.NoError(t, err) - assert.Equal(t, "G1:G4 H1:H4", dvs[3].Sqref) + assert.Equal(t, "G1:G4 H1:H4 A4:A1048576", dvs[3].Sqref) dv = NewDataValidation(true) dv.Sqref = "C5:D6" diff --git a/excelize.go b/excelize.go index de385de874..2641018747 100644 --- a/excelize.go +++ b/excelize.go @@ -453,14 +453,14 @@ func (f *File) addRels(relPath, relType, target, targetMode string) int { // UpdateLinkedValue fix linked values within a spreadsheet are not updating in // 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 +// https://learn.microsoft.com/en-us/archive/msdn-technet-forums/e16bae1f-6a2c-4325-8013-e989a3479066 // // 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: // -// +// // // SUM(Sheet2!D2,Sheet2!D11) // 100 @@ -469,7 +469,7 @@ func (f *File) addRels(relPath, relType, target, targetMode string) int { // // to // -// +// // // SUM(Sheet2!D2,Sheet2!D11) // From 838232fd27d4ad5bd9677513f98dd23d147ef733 Mon Sep 17 00:00:00 2001 From: Matthew Sackman Date: Tue, 26 Mar 2024 15:19:23 +0000 Subject: [PATCH 872/957] Add support for get the Microsoft 365 cell images (#1857) - Update unit tests --- excelize.go | 38 ++++++++++++++++++++++ picture.go | 73 ++++++++++++++++++++++++++++++++++++++----- picture_test.go | 58 ++++++++++++++++++++++++++++++++-- templates.go | 29 +++++++++-------- xmlMetaData.go | 83 +++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 259 insertions(+), 22 deletions(-) create mode 100644 xmlMetaData.go diff --git a/excelize.go b/excelize.go index 2641018747..1bf0248b3f 100644 --- a/excelize.go +++ b/excelize.go @@ -589,3 +589,41 @@ func (f *File) setContentTypePartProjectExtensions(contentType string) error { } return err } + +// metadataReader provides a function to get the pointer to the structure +// after deserialization of xl/metadata.xml. +func (f *File) metadataReader() (*xlsxMetadata, error) { + var mataData xlsxMetadata + if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLMetadata)))). + Decode(&mataData); err != nil && err != io.EOF { + return &mataData, err + } + return &mataData, nil +} + +// richValueRelReader provides a function to get the pointer to the structure +// after deserialization of xl/richData/richValueRel.xml. +func (f *File) richValueRelReader() (*xlsxRichValueRels, error) { + var richValueRels xlsxRichValueRels + if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLRichDataRichValueRel)))). + Decode(&richValueRels); err != nil && err != io.EOF { + return &richValueRels, err + } + return &richValueRels, nil +} + +// getRichDataRichValueRelRelationships provides a function to get drawing +// relationships from xl/richData/_rels/richValueRel.xml.rels by given +// relationship ID. +func (f *File) getRichDataRichValueRelRelationships(rID string) *xlsxRelationship { + if rels, _ := f.relsReader(defaultXMLRichDataRichValueRelRels); rels != nil { + rels.mu.Lock() + defer rels.mu.Unlock() + for _, v := range rels.Relationships { + if v.ID == rID { + return &v + } + } + } + return nil +} diff --git a/picture.go b/picture.go index 8b006f8e3e..e6410dc982 100644 --- a/picture.go +++ b/picture.go @@ -497,13 +497,13 @@ func (f *File) GetPictureCells(sheet string) ([]string, error) { } f.mu.Unlock() if ws.Drawing == nil { - return f.getEmbeddedImageCells(sheet) + return f.getImageCells(sheet) } target := f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID) drawingXML := strings.TrimPrefix(strings.ReplaceAll(target, "..", "xl"), "/") drawingRelationships := strings.ReplaceAll( strings.ReplaceAll(target, "../drawings", "xl/drawings/_rels"), ".xml", ".xml.rels") - embeddedImageCells, err := f.getEmbeddedImageCells(sheet) + embeddedImageCells, err := f.getImageCells(sheet) if err != nil { return nil, err } @@ -771,9 +771,9 @@ func (f *File) cellImagesReader() (*decodeCellImages, error) { return f.DecodeCellImages, nil } -// getEmbeddedImageCells returns all the Kingsoft WPS Office embedded image -// cells reference by given worksheet name. -func (f *File) getEmbeddedImageCells(sheet string) ([]string, error) { +// getImageCells returns all the Microsoft 365 cell images and the Kingsoft WPS +// Office embedded image cells reference by given worksheet name. +func (f *File) getImageCells(sheet string) ([]string, error) { var ( err error cells []string @@ -791,14 +791,73 @@ func (f *File) getEmbeddedImageCells(sheet string) ([]string, error) { } cells = append(cells, c.R) } + r, err := f.getImageCellRel(&c) + if err != nil { + return cells, err + } + if r != nil { + cells = append(cells, c.R) + } + } } return cells, err } -// getCellImages provides a function to get the Kingsoft WPS Office embedded -// cell images by given worksheet name and cell reference. +// getImageCellRel returns the Microsoft 365 cell image relationship. +func (f *File) getImageCellRel(c *xlsxC) (*xlsxRelationship, error) { + var r *xlsxRelationship + if c.Vm == nil || c.V != formulaErrorVALUE { + return r, nil + } + metaData, err := f.metadataReader() + if err != nil { + return r, err + } + vmd := metaData.ValueMetadata + if vmd == nil || int(*c.Vm) > len(vmd.Bk) || len(vmd.Bk[*c.Vm-1].Rc) == 0 { + return r, err + } + richValueRel, err := f.richValueRelReader() + if err != nil { + return r, err + } + if vmd.Bk[*c.Vm-1].Rc[0].V >= len(richValueRel.Rels) { + return r, err + } + rID := richValueRel.Rels[vmd.Bk[*c.Vm-1].Rc[0].V].ID + if r = f.getRichDataRichValueRelRelationships(rID); r != nil && r.Type != SourceRelationshipImage { + return nil, err + } + return r, err +} + +// getCellImages provides a function to get the Microsoft 365 cell images and +// the Kingsoft WPS Office embedded cell images by given worksheet name and cell +// reference. func (f *File) getCellImages(sheet, cell string) ([]Picture, error) { + pics, err := f.getDispImages(sheet, cell) + if err != nil { + return pics, err + } + _, err = f.getCellStringFunc(sheet, cell, func(x *xlsxWorksheet, c *xlsxC) (string, bool, error) { + r, err := f.getImageCellRel(c) + if err != nil || r == nil { + return "", true, err + } + pic := Picture{Extension: filepath.Ext(r.Target), Format: &GraphicOptions{}} + if buffer, _ := f.Pkg.Load(strings.TrimPrefix(strings.ReplaceAll(r.Target, "..", "xl"), "/")); buffer != nil { + pic.File = buffer.([]byte) + pics = append(pics, pic) + } + return "", true, nil + }) + return pics, err +} + +// getDispImages provides a function to get the Kingsoft WPS Office embedded +// cell images by given worksheet name and cell reference. +func (f *File) getDispImages(sheet, cell string) ([]Picture, error) { formula, err := f.GetCellFormula(sheet, cell) if err != nil { return nil, err diff --git a/picture_test.go b/picture_test.go index 376f3997c6..dfd5f623da 100644 --- a/picture_test.go +++ b/picture_test.go @@ -448,13 +448,67 @@ func TestGetCellImages(t *testing.T) { _, err := f.getCellImages("Sheet1", "A1") assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") assert.NoError(t, f.Close()) + + // Test get the Microsoft 365 cell images + f = NewFile() + assert.NoError(t, f.AddPicture("Sheet1", "A1", filepath.Join("test", "images", "excel.png"), nil)) + f.Pkg.Store(defaultXMLMetadata, []byte(``)) + f.Pkg.Store(defaultXMLRichDataRichValueRel, []byte(``)) + f.Pkg.Store(defaultXMLRichDataRichValueRelRels, []byte(fmt.Sprintf(``, SourceRelationshipImage))) + f.Sheet.Store("xl/worksheets/sheet1.xml", &xlsxWorksheet{ + SheetData: xlsxSheetData{Row: []xlsxRow{ + {R: 1, C: []xlsxC{{R: "A1", T: "e", V: formulaErrorVALUE, Vm: uintPtr(1)}}}, + }}, + }) + pics, err := f.GetPictures("Sheet1", "A1") + assert.NoError(t, err) + assert.Equal(t, 1, len(pics)) + cells, err := f.GetPictureCells("Sheet1") + assert.NoError(t, err) + assert.Equal(t, []string{"A1"}, cells) + + // Test get the Microsoft 365 cell images without image relationships parts + f.Relationships.Delete(defaultXMLRichDataRichValueRelRels) + f.Pkg.Store(defaultXMLRichDataRichValueRelRels, []byte(fmt.Sprintf(``, SourceRelationshipHyperLink))) + pics, err = f.GetPictures("Sheet1", "A1") + assert.NoError(t, err) + assert.Empty(t, pics) + // Test get the Microsoft 365 cell images with unsupported charset rich data rich value relationships + f.Relationships.Delete(defaultXMLRichDataRichValueRelRels) + f.Pkg.Store(defaultXMLRichDataRichValueRelRels, MacintoshCyrillicCharset) + pics, err = f.GetPictures("Sheet1", "A1") + assert.NoError(t, err) + assert.Empty(t, pics) + // Test get the Microsoft 365 cell images with unsupported charset rich data rich value + f.Pkg.Store(defaultXMLRichDataRichValueRel, MacintoshCyrillicCharset) + _, err = f.GetPictures("Sheet1", "A1") + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") + // Test get the Microsoft 365 image cells without block of metadata records + cells, err = f.GetPictureCells("Sheet1") + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") + assert.Empty(t, cells) + // Test get the Microsoft 365 cell images with rich data rich value relationships + f.Pkg.Store(defaultXMLMetadata, []byte(``)) + f.Pkg.Store(defaultXMLRichDataRichValueRel, []byte(``)) + pics, err = f.GetPictures("Sheet1", "A1") + assert.NoError(t, err) + assert.Empty(t, pics) + // Test get the Microsoft 365 cell images with unsupported charset meta data + f.Pkg.Store(defaultXMLMetadata, MacintoshCyrillicCharset) + _, err = f.GetPictures("Sheet1", "A1") + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") + // Test get the Microsoft 365 cell images without block of metadata records + f.Pkg.Store(defaultXMLMetadata, []byte(``)) + pics, err = f.GetPictures("Sheet1", "A1") + assert.NoError(t, err) + assert.Empty(t, pics) } -func TestGetEmbeddedImageCells(t *testing.T) { +func TestGetImageCells(t *testing.T) { f := NewFile() f.Sheet.Delete("xl/worksheets/sheet1.xml") f.Pkg.Store("xl/worksheets/sheet1.xml", MacintoshCyrillicCharset) - _, err := f.getEmbeddedImageCells("Sheet1") + _, err := f.getImageCells("Sheet1") assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") assert.NoError(t, f.Close()) } diff --git a/templates.go b/templates.go index 0f21be54bc..e097e81805 100644 --- a/templates.go +++ b/templates.go @@ -266,19 +266,22 @@ var supportedChartDataLabelsPosition = map[ChartType][]ChartDataLabelPositionTyp } const ( - defaultTempFileSST = "sharedStrings" - defaultXMLPathCalcChain = "xl/calcChain.xml" - defaultXMLPathCellImages = "xl/cellimages.xml" - defaultXMLPathCellImagesRels = "xl/_rels/cellimages.xml.rels" - defaultXMLPathContentTypes = "[Content_Types].xml" - defaultXMLPathDocPropsApp = "docProps/app.xml" - defaultXMLPathDocPropsCore = "docProps/core.xml" - defaultXMLPathSharedStrings = "xl/sharedStrings.xml" - defaultXMLPathStyles = "xl/styles.xml" - defaultXMLPathTheme = "xl/theme/theme1.xml" - defaultXMLPathVolatileDeps = "xl/volatileDependencies.xml" - defaultXMLPathWorkbook = "xl/workbook.xml" - defaultXMLPathWorkbookRels = "xl/_rels/workbook.xml.rels" + defaultTempFileSST = "sharedStrings" + defaultXMLMetadata = "xl/metadata.xml" + defaultXMLPathCalcChain = "xl/calcChain.xml" + defaultXMLPathCellImages = "xl/cellimages.xml" + defaultXMLPathCellImagesRels = "xl/_rels/cellimages.xml.rels" + defaultXMLPathContentTypes = "[Content_Types].xml" + defaultXMLPathDocPropsApp = "docProps/app.xml" + defaultXMLPathDocPropsCore = "docProps/core.xml" + defaultXMLPathSharedStrings = "xl/sharedStrings.xml" + defaultXMLPathStyles = "xl/styles.xml" + defaultXMLPathTheme = "xl/theme/theme1.xml" + defaultXMLPathVolatileDeps = "xl/volatileDependencies.xml" + defaultXMLPathWorkbook = "xl/workbook.xml" + defaultXMLPathWorkbookRels = "xl/_rels/workbook.xml.rels" + defaultXMLRichDataRichValueRel = "xl/richData/richValueRel.xml" + defaultXMLRichDataRichValueRelRels = "xl/richData/_rels/richValueRel.xml.rels" ) // IndexedColorMapping is the table of default mappings from indexed color value diff --git a/xmlMetaData.go b/xmlMetaData.go new file mode 100644 index 0000000000..e8cf7dbb8c --- /dev/null +++ b/xmlMetaData.go @@ -0,0 +1,83 @@ +// Copyright 2016 - 2024 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. +// +// Package excelize 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.18 or later. + +package excelize + +import "encoding/xml" + +// xlsxMetadata directly maps the metadata element. A cell in a spreadsheet +// application can have metadata associated with it. Metadata is just a set of +// additional properties about the particular cell, and this metadata is stored +// in the metadata xml part. There are two types of metadata: cell metadata and +// value metadata. Cell metadata contains information about the cell itself, +// and this metadata can be carried along with the cell as it moves +// (insert, shift, copy/paste, merge, unmerge, etc). Value metadata is +// information about the value of a particular cell. Value metadata properties +// can be propagated along with the value as it is referenced in formulas. +type xlsxMetadata struct { + XMLName xml.Name `xml:"metadata"` + MetadataTypes *xlsxInnerXML `xml:"metadataTypes"` + MetadataStrings *xlsxInnerXML `xml:"metadataStrings"` + MdxMetadata *xlsxInnerXML `xml:"mdxMetadata"` + FutureMetadata []xlsxFutureMetadata `xml:"futureMetadata"` + CellMetadata *xlsxMetadataBlocks `xml:"cellMetadata"` + ValueMetadata *xlsxMetadataBlocks `xml:"valueMetadata"` + ExtLst *xlsxInnerXML `xml:"extLst"` +} + +// xlsxFutureMetadata directly maps the futureMetadata element. This element +// represents future metadata information. +type xlsxFutureMetadata struct { + Bk []xlsxFutureMetadataBlock `xml:"bk"` + ExtLst *xlsxInnerXML `xml:"extLst"` +} + +// xlsxFutureMetadataBlock directly maps the kb element. This element represents +// a block of future metadata information. This is a location for storing +// feature extension information. +type xlsxFutureMetadataBlock struct { + ExtLst *xlsxInnerXML `xml:"extLst"` +} + +// xlsxMetadataBlocks directly maps the metadata element. This element +// represents cell metadata information. Cell metadata is information metadata +// about a specific cell, and it stays tied to that cell position. +type xlsxMetadataBlocks struct { + Count int `xml:"count,attr,omitempty"` + Bk []xlsxMetadataBlock `xml:"bk"` +} + +// xlsxMetadataBlock directly maps the bk element. This element represents a +// block of metadata records. +type xlsxMetadataBlock struct { + Rc []xlsxMetadataRecord `xml:"rc"` +} + +// xlsxMetadataRecord directly maps the rc element. This element represents a +// reference to a specific metadata record. +type xlsxMetadataRecord struct { + T int `xml:"t,attr"` + V int `xml:"v,attr"` +} + +// xlsxRichValueRels directly maps the richValueRels element. This element that +// specifies a list of rich value relationships. +type xlsxRichValueRels struct { + XMLName xml.Name `xml:"richValueRels"` + Rels []xlsxRichValueRelRelationship `xml:"rel"` + ExtLst *xlsxInnerXML `xml:"extLst"` +} + +// xlsxRichValueRelRelationship directly maps the rel element. This element +// specifies a relationship for a rich value property. +type xlsxRichValueRelRelationship struct { + ID string `xml:"id,attr"` +} From 5e500f5e5dcd97e3f68d34c15290d3ffc359f101 Mon Sep 17 00:00:00 2001 From: yangyile-yyle88 <162403837+yyle88@users.noreply.github.com> Date: Wed, 27 Mar 2024 14:50:51 +0700 Subject: [PATCH 873/957] Introduce new exported PictureInsertType enumeration (#1864) --- picture.go | 33 ++++++++++++++++++++++++++------- picture_test.go | 8 ++++++-- xmlDrawing.go | 7 ++++--- 3 files changed, 36 insertions(+), 12 deletions(-) diff --git a/picture.go b/picture.go index e6410dc982..d784079d75 100644 --- a/picture.go +++ b/picture.go @@ -23,6 +23,17 @@ import ( "strings" ) +// PictureInsertType defines the type of the picture has been inserted into the +// worksheet. +type PictureInsertType int + +// Insert picture types. +const ( + PictureInsertTypePlaceOverCells PictureInsertType = iota + PictureInsertTypePlaceInCell + PictureInsertTypeDISPIMG +) + // parseGraphicOptions provides a function to parse the format settings of // the picture with default value. func parseGraphicOptions(opts *GraphicOptions) *GraphicOptions { @@ -52,7 +63,10 @@ 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: BMP, EMF, EMZ, GIF, JPEG, JPG, PNG, -// SVG, TIF, TIFF, WMF, and WMZ. This function is concurrency safe. For example: +// SVG, TIF, TIFF, WMF, and WMZ. This function is concurrency-safe. Note that +// this function only supports adding pictures placed over the cells currently, +// and doesn't support adding pictures placed in cells or creating the Kingsoft +// WPS Office embedded image cells. For example: // // package main // @@ -167,8 +181,10 @@ func (f *File) AddPicture(sheet, cell, name string, opts *GraphicOptions) 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, supported image -// types: EMF, EMZ, GIF, JPEG, JPG, PNG, SVG, TIF, TIFF, WMF, and WMZ. For -// example: +// types: EMF, EMZ, GIF, JPEG, JPG, PNG, SVG, TIF, TIFF, WMF, and WMZ. Note that +// this function only supports adding pictures placed over the cells currently, +// and doesn't support adding pictures placed in cells or creating the Kingsoft +// WPS Office embedded image cells.For example: // // package main // @@ -211,6 +227,9 @@ func (f *File) AddPictureFromBytes(sheet, cell string, pic *Picture) error { if !ok { return ErrImgExt } + if pic.InsertType != PictureInsertTypePlaceOverCells { + return ErrParameterInvalid + } options := parseGraphicOptions(pic.Format) img, _, err := image.DecodeConfig(bytes.NewReader(pic.File)) if err != nil { @@ -577,7 +596,7 @@ func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) cond := func(from *xlsxFrom) bool { return from.Col == col && from.Row == row } cond2 := func(from *decodeFrom) bool { return from.Col == col && from.Row == row } cb := func(a *xdrCellAnchor, r *xlsxRelationship) { - pic := Picture{Extension: filepath.Ext(r.Target), Format: &GraphicOptions{}} + pic := Picture{Extension: filepath.Ext(r.Target), Format: &GraphicOptions{}, InsertType: PictureInsertTypePlaceOverCells} if buffer, _ := f.Pkg.Load(strings.ReplaceAll(r.Target, "..", "xl")); buffer != nil { pic.File = buffer.([]byte) pic.Format.AltText = a.Pic.NvPicPr.CNvPr.Descr @@ -585,7 +604,7 @@ func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) } } cb2 := func(a *decodeCellAnchor, r *xlsxRelationship) { - pic := Picture{Extension: filepath.Ext(r.Target), Format: &GraphicOptions{}} + pic := Picture{Extension: filepath.Ext(r.Target), Format: &GraphicOptions{}, InsertType: PictureInsertTypePlaceOverCells} if buffer, _ := f.Pkg.Load(strings.ReplaceAll(r.Target, "..", "xl")); buffer != nil { pic.File = buffer.([]byte) pic.Format.AltText = a.Pic.NvPicPr.CNvPr.Descr @@ -845,7 +864,7 @@ func (f *File) getCellImages(sheet, cell string) ([]Picture, error) { if err != nil || r == nil { return "", true, err } - pic := Picture{Extension: filepath.Ext(r.Target), Format: &GraphicOptions{}} + pic := Picture{Extension: filepath.Ext(r.Target), Format: &GraphicOptions{}, InsertType: PictureInsertTypePlaceInCell} if buffer, _ := f.Pkg.Load(strings.TrimPrefix(strings.ReplaceAll(r.Target, "..", "xl"), "/")); buffer != nil { pic.File = buffer.([]byte) pics = append(pics, pic) @@ -882,7 +901,7 @@ func (f *File) getDispImages(sheet, cell string) ([]Picture, error) { if cellImg.Pic.NvPicPr.CNvPr.Name == imgID { for _, r := range rels.Relationships { if r.ID == cellImg.Pic.BlipFill.Blip.Embed { - pic := Picture{Extension: filepath.Ext(r.Target), Format: &GraphicOptions{}} + pic := Picture{Extension: filepath.Ext(r.Target), Format: &GraphicOptions{}, InsertType: PictureInsertTypeDISPIMG} if buffer, _ := f.Pkg.Load("xl/" + r.Target); buffer != nil { pic.File = buffer.([]byte) pic.Format.AltText = cellImg.Pic.NvPicPr.CNvPr.Descr diff --git a/picture_test.go b/picture_test.go index dfd5f623da..7d08b8d48a 100644 --- a/picture_test.go +++ b/picture_test.go @@ -12,10 +12,9 @@ import ( "strings" "testing" + "github.com/stretchr/testify/assert" _ "golang.org/x/image/bmp" _ "golang.org/x/image/tiff" - - "github.com/stretchr/testify/assert" ) func BenchmarkAddPictureFromBytes(b *testing.B) { @@ -59,6 +58,8 @@ func TestAddPicture(t *testing.T) { // Test add picture to worksheet from bytes 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 unsupported insert type + assert.Equal(t, ErrParameterInvalid, f.AddPictureFromBytes("Sheet1", "Q1", &Picture{Extension: ".png", File: file, Format: &GraphicOptions{AltText: "Excel Logo"}, InsertType: PictureInsertTypePlaceInCell})) // Test add picture to worksheet from bytes with illegal cell reference assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.AddPictureFromBytes("Sheet1", "A", &Picture{Extension: ".png", File: file, Format: &GraphicOptions{AltText: "Excel Logo"}})) @@ -150,6 +151,7 @@ func TestGetPicture(t *testing.T) { assert.NoError(t, err) assert.Len(t, pics[0].File, 13233) assert.Empty(t, pics[0].Format.AltText) + assert.Equal(t, PictureInsertTypePlaceOverCells, pics[0].InsertType) f, err = prepareTestBook1() if !assert.NoError(t, err) { @@ -249,6 +251,7 @@ func TestGetPicture(t *testing.T) { assert.NoError(t, err) assert.Len(t, pics, 2) assert.Equal(t, "CellImage1", pics[0].Format.AltText) + assert.Equal(t, PictureInsertTypeDISPIMG, pics[0].InsertType) // Test get embedded cell pictures with invalid formula assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "=_xlfn.DISPIMG()")) @@ -463,6 +466,7 @@ func TestGetCellImages(t *testing.T) { pics, err := f.GetPictures("Sheet1", "A1") assert.NoError(t, err) assert.Equal(t, 1, len(pics)) + assert.Equal(t, PictureInsertTypePlaceInCell, pics[0].InsertType) cells, err := f.GetPictureCells("Sheet1") assert.NoError(t, err) assert.Equal(t, []string{"A1"}, cells) diff --git a/xmlDrawing.go b/xmlDrawing.go index 5cca3cf251..7981166a80 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -409,9 +409,10 @@ type xdrTxBody struct { // Picture maps the format settings of the picture. type Picture struct { - Extension string - File []byte - Format *GraphicOptions + Extension string + File []byte + Format *GraphicOptions + InsertType PictureInsertType } // GraphicOptions directly maps the format settings of the picture. From ffad7aecb5b98d16ab97e853ff9129b1af59acaf Mon Sep 17 00:00:00 2001 From: yunkeweb <32658824+yunkeweb@users.noreply.github.com> Date: Thu, 28 Mar 2024 16:37:35 +0800 Subject: [PATCH 874/957] Support get rich data value rels index from rich value part (#1866) --- excelize.go | 11 +++++++++ picture.go | 42 ++++++++++++++++++++++++++------ picture_test.go | 64 +++++++++++++++++++++++++++++++++++-------------- templates.go | 1 + xmlMetaData.go | 17 +++++++++++++ 5 files changed, 110 insertions(+), 25 deletions(-) diff --git a/excelize.go b/excelize.go index 1bf0248b3f..8b1b7634d6 100644 --- a/excelize.go +++ b/excelize.go @@ -601,6 +601,17 @@ func (f *File) metadataReader() (*xlsxMetadata, error) { return &mataData, nil } +// richValueReader provides a function to get the pointer to the structure after +// deserialization of xl/richData/richvalue.xml. +func (f *File) richValueReader() (*xlsxRichValueData, error) { + var richValue xlsxRichValueData + if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLRichDataRichValue)))). + Decode(&richValue); err != nil && err != io.EOF { + return &richValue, err + } + return &richValue, nil +} + // richValueRelReader provides a function to get the pointer to the structure // after deserialization of xl/richData/richValueRel.xml. func (f *File) richValueRelReader() (*xlsxRichValueRels, error) { diff --git a/picture.go b/picture.go index d784079d75..a52bbc39f5 100644 --- a/picture.go +++ b/picture.go @@ -450,7 +450,8 @@ func (f *File) addMedia(file []byte, ext string) string { // 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 image contents as []byte data types. This function is -// concurrency safe. For example: +// concurrency safe. Note that, this function doesn't support getting cell image +// inserted by IMAGE formula function currently. For example: // // f, err := excelize.OpenFile("Book1.xlsx") // if err != nil { @@ -506,7 +507,8 @@ func (f *File) GetPictures(sheet, cell string) ([]Picture, error) { } // GetPictureCells returns all picture cell references in a worksheet by a -// specific worksheet name. +// specific worksheet name. Note that, this function doesn't support getting +// cell image inserted by IMAGE formula function currently. func (f *File) GetPictureCells(sheet string) ([]string, error) { f.mu.Lock() ws, err := f.workSheetReader(sheet) @@ -790,7 +792,7 @@ func (f *File) cellImagesReader() (*decodeCellImages, error) { return f.DecodeCellImages, nil } -// getImageCells returns all the Microsoft 365 cell images and the Kingsoft WPS +// getImageCells returns all the cell images and the Kingsoft WPS // Office embedded image cells reference by given worksheet name. func (f *File) getImageCells(sheet string) ([]string, error) { var ( @@ -823,7 +825,29 @@ func (f *File) getImageCells(sheet string) ([]string, error) { return cells, err } -// getImageCellRel returns the Microsoft 365 cell image relationship. +// getImageCellRichValueIdx returns index of the cell image rich value by given +// cell value meta index and meta blocks. +func (f *File) getImageCellRichValueIdx(vm uint, blocks *xlsxMetadataBlocks) (int, error) { + richValueIdx := blocks.Bk[vm-1].Rc[0].V + richValue, err := f.richValueReader() + if err != nil { + return -1, err + } + if richValueIdx >= len(richValue.Rv) { + return -1, err + } + rv := richValue.Rv[richValueIdx].V + if len(rv) != 2 || rv[1] != "5" { + return -1, err + } + richValueRelIdx, err := strconv.Atoi(rv[0]) + if err != nil { + return -1, err + } + return richValueRelIdx, err +} + +// getImageCellRel returns the cell image relationship. func (f *File) getImageCellRel(c *xlsxC) (*xlsxRelationship, error) { var r *xlsxRelationship if c.Vm == nil || c.V != formulaErrorVALUE { @@ -837,21 +861,25 @@ func (f *File) getImageCellRel(c *xlsxC) (*xlsxRelationship, error) { if vmd == nil || int(*c.Vm) > len(vmd.Bk) || len(vmd.Bk[*c.Vm-1].Rc) == 0 { return r, err } + richValueRelIdx, err := f.getImageCellRichValueIdx(*c.Vm, vmd) + if err != nil || richValueRelIdx == -1 { + return r, err + } richValueRel, err := f.richValueRelReader() if err != nil { return r, err } - if vmd.Bk[*c.Vm-1].Rc[0].V >= len(richValueRel.Rels) { + if richValueRelIdx >= len(richValueRel.Rels) { return r, err } - rID := richValueRel.Rels[vmd.Bk[*c.Vm-1].Rc[0].V].ID + rID := richValueRel.Rels[richValueRelIdx].ID if r = f.getRichDataRichValueRelRelationships(rID); r != nil && r.Type != SourceRelationshipImage { return nil, err } return r, err } -// getCellImages provides a function to get the Microsoft 365 cell images and +// getCellImages provides a function to get the cell images and // the Kingsoft WPS Office embedded cell images by given worksheet name and cell // reference. func (f *File) getCellImages(sheet, cell string) ([]Picture, error) { diff --git a/picture_test.go b/picture_test.go index 7d08b8d48a..bb906345e8 100644 --- a/picture_test.go +++ b/picture_test.go @@ -452,17 +452,22 @@ func TestGetCellImages(t *testing.T) { assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") assert.NoError(t, f.Close()) - // Test get the Microsoft 365 cell images - f = NewFile() - assert.NoError(t, f.AddPicture("Sheet1", "A1", filepath.Join("test", "images", "excel.png"), nil)) - f.Pkg.Store(defaultXMLMetadata, []byte(``)) - f.Pkg.Store(defaultXMLRichDataRichValueRel, []byte(``)) - f.Pkg.Store(defaultXMLRichDataRichValueRelRels, []byte(fmt.Sprintf(``, SourceRelationshipImage))) - f.Sheet.Store("xl/worksheets/sheet1.xml", &xlsxWorksheet{ - SheetData: xlsxSheetData{Row: []xlsxRow{ - {R: 1, C: []xlsxC{{R: "A1", T: "e", V: formulaErrorVALUE, Vm: uintPtr(1)}}}, - }}, - }) + // Test get the cell images + prepareWorkbook := func() *File { + f := NewFile() + assert.NoError(t, f.AddPicture("Sheet1", "A1", filepath.Join("test", "images", "excel.png"), nil)) + f.Pkg.Store(defaultXMLMetadata, []byte(``)) + f.Pkg.Store(defaultXMLRichDataRichValue, []byte(`05`)) + f.Pkg.Store(defaultXMLRichDataRichValueRel, []byte(``)) + f.Pkg.Store(defaultXMLRichDataRichValueRelRels, []byte(fmt.Sprintf(``, SourceRelationshipImage))) + f.Sheet.Store("xl/worksheets/sheet1.xml", &xlsxWorksheet{ + SheetData: xlsxSheetData{Row: []xlsxRow{ + {R: 1, C: []xlsxC{{R: "A1", T: "e", V: formulaErrorVALUE, Vm: uintPtr(1)}}}, + }}, + }) + return f + } + f = prepareWorkbook() pics, err := f.GetPictures("Sheet1", "A1") assert.NoError(t, err) assert.Equal(t, 1, len(pics)) @@ -471,41 +476,64 @@ func TestGetCellImages(t *testing.T) { assert.NoError(t, err) assert.Equal(t, []string{"A1"}, cells) - // Test get the Microsoft 365 cell images without image relationships parts + // Test get the cell images without image relationships parts f.Relationships.Delete(defaultXMLRichDataRichValueRelRels) f.Pkg.Store(defaultXMLRichDataRichValueRelRels, []byte(fmt.Sprintf(``, SourceRelationshipHyperLink))) pics, err = f.GetPictures("Sheet1", "A1") assert.NoError(t, err) assert.Empty(t, pics) - // Test get the Microsoft 365 cell images with unsupported charset rich data rich value relationships + // Test get the cell images with unsupported charset rich data rich value relationships f.Relationships.Delete(defaultXMLRichDataRichValueRelRels) f.Pkg.Store(defaultXMLRichDataRichValueRelRels, MacintoshCyrillicCharset) pics, err = f.GetPictures("Sheet1", "A1") assert.NoError(t, err) assert.Empty(t, pics) - // Test get the Microsoft 365 cell images with unsupported charset rich data rich value + // Test get the cell images with unsupported charset rich data rich value f.Pkg.Store(defaultXMLRichDataRichValueRel, MacintoshCyrillicCharset) _, err = f.GetPictures("Sheet1", "A1") assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") - // Test get the Microsoft 365 image cells without block of metadata records + // Test get the image cells without block of metadata records cells, err = f.GetPictureCells("Sheet1") assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") assert.Empty(t, cells) - // Test get the Microsoft 365 cell images with rich data rich value relationships + // Test get the cell images with rich data rich value relationships f.Pkg.Store(defaultXMLMetadata, []byte(``)) f.Pkg.Store(defaultXMLRichDataRichValueRel, []byte(``)) pics, err = f.GetPictures("Sheet1", "A1") assert.NoError(t, err) assert.Empty(t, pics) - // Test get the Microsoft 365 cell images with unsupported charset meta data + // Test get the cell images with unsupported charset meta data f.Pkg.Store(defaultXMLMetadata, MacintoshCyrillicCharset) _, err = f.GetPictures("Sheet1", "A1") assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") - // Test get the Microsoft 365 cell images without block of metadata records + // Test get the cell images without block of metadata records f.Pkg.Store(defaultXMLMetadata, []byte(``)) pics, err = f.GetPictures("Sheet1", "A1") assert.NoError(t, err) assert.Empty(t, pics) + + f = prepareWorkbook() + // Test get the cell images with empty image cell rich value + f.Pkg.Store(defaultXMLRichDataRichValue, []byte(`5`)) + pics, err = f.GetPictures("Sheet1", "A1") + assert.EqualError(t, err, "strconv.Atoi: parsing \"\": invalid syntax") + assert.Empty(t, pics) + // Test get the cell images without image cell rich value + f.Pkg.Store(defaultXMLRichDataRichValue, []byte(`01`)) + pics, err = f.GetPictures("Sheet1", "A1") + assert.NoError(t, err) + assert.Empty(t, pics) + // Test get the cell images with unsupported charset rich value + f.Pkg.Store(defaultXMLRichDataRichValue, MacintoshCyrillicCharset) + _, err = f.GetPictures("Sheet1", "A1") + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") + + f = prepareWorkbook() + // Test get the cell images with invalid rich value index + f.Pkg.Store(defaultXMLMetadata, []byte(``)) + pics, err = f.GetPictures("Sheet1", "A1") + assert.NoError(t, err) + assert.Empty(t, pics) } func TestGetImageCells(t *testing.T) { diff --git a/templates.go b/templates.go index e097e81805..7d72e3b0b1 100644 --- a/templates.go +++ b/templates.go @@ -280,6 +280,7 @@ const ( defaultXMLPathVolatileDeps = "xl/volatileDependencies.xml" defaultXMLPathWorkbook = "xl/workbook.xml" defaultXMLPathWorkbookRels = "xl/_rels/workbook.xml.rels" + defaultXMLRichDataRichValue = "xl/richData/rdrichvalue.xml" defaultXMLRichDataRichValueRel = "xl/richData/richValueRel.xml" defaultXMLRichDataRichValueRelRels = "xl/richData/_rels/richValueRel.xml.rels" ) diff --git a/xmlMetaData.go b/xmlMetaData.go index e8cf7dbb8c..b3f97b6a3f 100644 --- a/xmlMetaData.go +++ b/xmlMetaData.go @@ -68,6 +68,23 @@ type xlsxMetadataRecord struct { V int `xml:"v,attr"` } +// xlsxRichValueData directly maps the rvData element that specifies rich value +// data. +type xlsxRichValueData struct { + XMLName xml.Name `xml:"rvData"` + Count int `xml:"count,attr,omitempty"` + Rv []xlsxRichValue `xml:"rv"` + ExtLst *xlsxInnerXML `xml:"extLst"` +} + +// xlsxRichValue directly maps the rv element that specifies rich value data +// information for a single rich value +type xlsxRichValue struct { + S int `xml:"s,attr"` + V []string `xml:"v"` + Fb *xlsxInnerXML `xml:"fb"` +} + // xlsxRichValueRels directly maps the richValueRels element. This element that // specifies a list of rich value relationships. type xlsxRichValueRels struct { From 99992214503c3e2135170b98448ac37af00bf2fa Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 1 Apr 2024 08:49:21 +0800 Subject: [PATCH 875/957] This closes #1865, unescape newline character in stream writer - Update dependencies module --- cell.go | 4 +++- go.sum | 8 -------- picture.go | 2 +- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/cell.go b/cell.go index e9b5730ed3..3b712c651f 100644 --- a/cell.go +++ b/cell.go @@ -505,7 +505,9 @@ func trimCellValue(value string, escape bool) (v string, ns xml.Attr) { } if escape { var buf bytes.Buffer - _ = xml.EscapeText(&buf, []byte(value)) + enc := xml.NewEncoder(&buf) + enc.EncodeToken(xml.CharData(value)) + enc.Flush() value = buf.String() } if len(value) > 0 { diff --git a/go.sum b/go.sum index 9c04edd934..22aee209c6 100644 --- a/go.sum +++ b/go.sum @@ -13,20 +13,12 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53 h1:Chd9DkqERQQuHpXjR/HSV1jLZA6uaoiwwH3vSuF3IW0= github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= -github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05 h1:qhbILQo1K3mphbwKh1vNm4oGezE1eF9fQWmNiIpSfI4= -github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= -github.com/xuri/nfp v0.0.0-20240316161844-5bacf1a74267 h1:p0lQ21ogqdVWcdXpqSlD7gu/3whO1YWNiOaPJNBfunU= -github.com/xuri/nfp v0.0.0-20240316161844-5bacf1a74267/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A= github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= -golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4= golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= -golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= diff --git a/picture.go b/picture.go index a52bbc39f5..cc0c2b8c9b 100644 --- a/picture.go +++ b/picture.go @@ -184,7 +184,7 @@ func (f *File) AddPicture(sheet, cell, name string, opts *GraphicOptions) error // types: EMF, EMZ, GIF, JPEG, JPG, PNG, SVG, TIF, TIFF, WMF, and WMZ. Note that // this function only supports adding pictures placed over the cells currently, // and doesn't support adding pictures placed in cells or creating the Kingsoft -// WPS Office embedded image cells.For example: +// WPS Office embedded image cells. For example: // // package main // From 5dc22e874b0e687ff0888d82259692b5852386b7 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 2 Apr 2024 08:47:57 +0800 Subject: [PATCH 876/957] Support get the cell images inserted by IMAGE formula function --- excelize.go | 37 +++++++++++++++++---- picture.go | 87 +++++++++++++++++++++++++++++++------------------ picture_test.go | 64 ++++++++++++++++++++++++++++-------- templates.go | 36 ++++++++++---------- xmlMetaData.go | 17 ++++++++++ 5 files changed, 174 insertions(+), 67 deletions(-) diff --git a/excelize.go b/excelize.go index 8b1b7634d6..e0959aa646 100644 --- a/excelize.go +++ b/excelize.go @@ -605,7 +605,7 @@ func (f *File) metadataReader() (*xlsxMetadata, error) { // deserialization of xl/richData/richvalue.xml. func (f *File) richValueReader() (*xlsxRichValueData, error) { var richValue xlsxRichValueData - if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLRichDataRichValue)))). + if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLRdRichValuePart)))). Decode(&richValue); err != nil && err != io.EOF { return &richValue, err } @@ -616,18 +616,43 @@ func (f *File) richValueReader() (*xlsxRichValueData, error) { // after deserialization of xl/richData/richValueRel.xml. func (f *File) richValueRelReader() (*xlsxRichValueRels, error) { var richValueRels xlsxRichValueRels - if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLRichDataRichValueRel)))). + if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLRdRichValueRel)))). Decode(&richValueRels); err != nil && err != io.EOF { return &richValueRels, err } return &richValueRels, nil } -// getRichDataRichValueRelRelationships provides a function to get drawing -// relationships from xl/richData/_rels/richValueRel.xml.rels by given -// relationship ID. +// richValueWebImageReader provides a function to get the pointer to the +// structure after deserialization of xl/richData/rdRichValueWebImage.xml. +func (f *File) richValueWebImageReader() (*xlsxWebImagesSupportingRichData, error) { + var richValueWebImages xlsxWebImagesSupportingRichData + if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLRdRichValueWebImagePart)))). + Decode(&richValueWebImages); err != nil && err != io.EOF { + return &richValueWebImages, err + } + return &richValueWebImages, nil +} + +// getRichDataRichValueRelRelationships provides a function to get relationships +// from xl/richData/_rels/richValueRel.xml.rels by given relationship ID. func (f *File) getRichDataRichValueRelRelationships(rID string) *xlsxRelationship { - if rels, _ := f.relsReader(defaultXMLRichDataRichValueRelRels); rels != nil { + if rels, _ := f.relsReader(defaultXMLRdRichValueRelRels); rels != nil { + rels.mu.Lock() + defer rels.mu.Unlock() + for _, v := range rels.Relationships { + if v.ID == rID { + return &v + } + } + } + return nil +} + +// getRichValueWebImageRelationships provides a function to get relationships +// from xl/richData/_rels/rdRichValueWebImage.xml.rels by given relationship ID. +func (f *File) getRichValueWebImageRelationships(rID string) *xlsxRelationship { + if rels, _ := f.relsReader(defaultXMLRdRichValueWebImagePartRels); rels != nil { rels.mu.Lock() defer rels.mu.Unlock() for _, v := range rels.Relationships { diff --git a/picture.go b/picture.go index cc0c2b8c9b..27d6b7fe8c 100644 --- a/picture.go +++ b/picture.go @@ -31,6 +31,7 @@ type PictureInsertType int const ( PictureInsertTypePlaceOverCells PictureInsertType = iota PictureInsertTypePlaceInCell + PictureInsertTypeIMAGE PictureInsertTypeDISPIMG ) @@ -450,8 +451,7 @@ func (f *File) addMedia(file []byte, ext string) string { // 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 image contents as []byte data types. This function is -// concurrency safe. Note that, this function doesn't support getting cell image -// inserted by IMAGE formula function currently. For example: +// concurrency safe. For example: // // f, err := excelize.OpenFile("Book1.xlsx") // if err != nil { @@ -507,8 +507,7 @@ func (f *File) GetPictures(sheet, cell string) ([]Picture, error) { } // GetPictureCells returns all picture cell references in a worksheet by a -// specific worksheet name. Note that, this function doesn't support getting -// cell image inserted by IMAGE formula function currently. +// specific worksheet name. func (f *File) GetPictureCells(sheet string) ([]string, error) { f.mu.Lock() ws, err := f.workSheetReader(sheet) @@ -812,7 +811,7 @@ func (f *File) getImageCells(sheet string) ([]string, error) { } cells = append(cells, c.R) } - r, err := f.getImageCellRel(&c) + r, err := f.getImageCellRel(&c, &Picture{}) if err != nil { return cells, err } @@ -825,30 +824,52 @@ func (f *File) getImageCells(sheet string) ([]string, error) { return cells, err } -// getImageCellRichValueIdx returns index of the cell image rich value by given -// cell value meta index and meta blocks. -func (f *File) getImageCellRichValueIdx(vm uint, blocks *xlsxMetadataBlocks) (int, error) { - richValueIdx := blocks.Bk[vm-1].Rc[0].V - richValue, err := f.richValueReader() +// getRichDataRichValueRel returns relationship of the cell image by given meta +// blocks value. +func (f *File) getRichDataRichValueRel(val string) (*xlsxRelationship, error) { + var r *xlsxRelationship + idx, err := strconv.Atoi(val) if err != nil { - return -1, err + return r, err } - if richValueIdx >= len(richValue.Rv) { - return -1, err + richValueRel, err := f.richValueRelReader() + if err != nil { + return r, err } - rv := richValue.Rv[richValueIdx].V - if len(rv) != 2 || rv[1] != "5" { - return -1, err + if idx >= len(richValueRel.Rels) { + return r, err + } + rID := richValueRel.Rels[idx].ID + if r = f.getRichDataRichValueRelRelationships(rID); r != nil && r.Type != SourceRelationshipImage { + return nil, err + } + return r, err +} + +// getRichDataWebImagesRel returns relationship of a web image by given meta +// blocks value. +func (f *File) getRichDataWebImagesRel(val string) (*xlsxRelationship, error) { + var r *xlsxRelationship + idx, err := strconv.Atoi(val) + if err != nil { + return r, err } - richValueRelIdx, err := strconv.Atoi(rv[0]) + richValueWebImages, err := f.richValueWebImageReader() if err != nil { - return -1, err + return r, err + } + if idx >= len(richValueWebImages.WebImageSrd) { + return r, err } - return richValueRelIdx, err + rID := richValueWebImages.WebImageSrd[idx].Blip.RID + if r = f.getRichValueWebImageRelationships(rID); r != nil && r.Type != SourceRelationshipImage { + return nil, err + } + return r, err } // getImageCellRel returns the cell image relationship. -func (f *File) getImageCellRel(c *xlsxC) (*xlsxRelationship, error) { +func (f *File) getImageCellRel(c *xlsxC, pic *Picture) (*xlsxRelationship, error) { var r *xlsxRelationship if c.Vm == nil || c.V != formulaErrorVALUE { return r, nil @@ -861,20 +882,23 @@ func (f *File) getImageCellRel(c *xlsxC) (*xlsxRelationship, error) { if vmd == nil || int(*c.Vm) > len(vmd.Bk) || len(vmd.Bk[*c.Vm-1].Rc) == 0 { return r, err } - richValueRelIdx, err := f.getImageCellRichValueIdx(*c.Vm, vmd) - if err != nil || richValueRelIdx == -1 { - return r, err - } - richValueRel, err := f.richValueRelReader() + richValueIdx := vmd.Bk[*c.Vm-1].Rc[0].V + richValue, err := f.richValueReader() if err != nil { return r, err } - if richValueRelIdx >= len(richValueRel.Rels) { + if richValueIdx >= len(richValue.Rv) { return r, err } - rID := richValueRel.Rels[richValueRelIdx].ID - if r = f.getRichDataRichValueRelRelationships(rID); r != nil && r.Type != SourceRelationshipImage { - return nil, err + rv := richValue.Rv[richValueIdx].V + if len(rv) == 2 && rv[1] == "5" { + pic.InsertType = PictureInsertTypePlaceInCell + return f.getRichDataRichValueRel(rv[0]) + } + // cell image inserted by IMAGE formula function + if len(rv) > 3 && rv[1]+rv[2] == "10" { + pic.InsertType = PictureInsertTypeIMAGE + return f.getRichDataWebImagesRel(rv[0]) } return r, err } @@ -888,11 +912,12 @@ func (f *File) getCellImages(sheet, cell string) ([]Picture, error) { return pics, err } _, err = f.getCellStringFunc(sheet, cell, func(x *xlsxWorksheet, c *xlsxC) (string, bool, error) { - r, err := f.getImageCellRel(c) + pic := Picture{Format: &GraphicOptions{}, InsertType: PictureInsertTypePlaceInCell} + r, err := f.getImageCellRel(c, &pic) if err != nil || r == nil { return "", true, err } - pic := Picture{Extension: filepath.Ext(r.Target), Format: &GraphicOptions{}, InsertType: PictureInsertTypePlaceInCell} + pic.Extension = filepath.Ext(r.Target) if buffer, _ := f.Pkg.Load(strings.TrimPrefix(strings.ReplaceAll(r.Target, "..", "xl"), "/")); buffer != nil { pic.File = buffer.([]byte) pics = append(pics, pic) diff --git a/picture_test.go b/picture_test.go index bb906345e8..23054c1da1 100644 --- a/picture_test.go +++ b/picture_test.go @@ -246,7 +246,7 @@ func TestGetPicture(t *testing.T) { assert.NoError(t, err) assert.NoError(t, f.SetCellFormula("Sheet1", "F21", "=_xlfn.DISPIMG(\"ID_********************************\",1)")) f.Pkg.Store(defaultXMLPathCellImages, []byte(``)) - f.Pkg.Store(defaultXMLPathCellImagesRels, []byte(``)) + f.Pkg.Store(defaultXMLPathCellImagesRels, []byte(fmt.Sprintf(``, SourceRelationshipImage))) pics, err = f.GetPictures("Sheet1", "F21") assert.NoError(t, err) assert.Len(t, pics, 2) @@ -457,9 +457,9 @@ func TestGetCellImages(t *testing.T) { f := NewFile() assert.NoError(t, f.AddPicture("Sheet1", "A1", filepath.Join("test", "images", "excel.png"), nil)) f.Pkg.Store(defaultXMLMetadata, []byte(``)) - f.Pkg.Store(defaultXMLRichDataRichValue, []byte(`05`)) - f.Pkg.Store(defaultXMLRichDataRichValueRel, []byte(``)) - f.Pkg.Store(defaultXMLRichDataRichValueRelRels, []byte(fmt.Sprintf(``, SourceRelationshipImage))) + f.Pkg.Store(defaultXMLRdRichValuePart, []byte(`05`)) + f.Pkg.Store(defaultXMLRdRichValueRel, []byte(``)) + f.Pkg.Store(defaultXMLRdRichValueRelRels, []byte(fmt.Sprintf(``, SourceRelationshipImage))) f.Sheet.Store("xl/worksheets/sheet1.xml", &xlsxWorksheet{ SheetData: xlsxSheetData{Row: []xlsxRow{ {R: 1, C: []xlsxC{{R: "A1", T: "e", V: formulaErrorVALUE, Vm: uintPtr(1)}}}, @@ -477,19 +477,19 @@ func TestGetCellImages(t *testing.T) { assert.Equal(t, []string{"A1"}, cells) // Test get the cell images without image relationships parts - f.Relationships.Delete(defaultXMLRichDataRichValueRelRels) - f.Pkg.Store(defaultXMLRichDataRichValueRelRels, []byte(fmt.Sprintf(``, SourceRelationshipHyperLink))) + f.Relationships.Delete(defaultXMLRdRichValueRelRels) + f.Pkg.Store(defaultXMLRdRichValueRelRels, []byte(fmt.Sprintf(``, SourceRelationshipHyperLink))) pics, err = f.GetPictures("Sheet1", "A1") assert.NoError(t, err) assert.Empty(t, pics) // Test get the cell images with unsupported charset rich data rich value relationships - f.Relationships.Delete(defaultXMLRichDataRichValueRelRels) - f.Pkg.Store(defaultXMLRichDataRichValueRelRels, MacintoshCyrillicCharset) + f.Relationships.Delete(defaultXMLRdRichValueRelRels) + f.Pkg.Store(defaultXMLRdRichValueRelRels, MacintoshCyrillicCharset) pics, err = f.GetPictures("Sheet1", "A1") assert.NoError(t, err) assert.Empty(t, pics) // Test get the cell images with unsupported charset rich data rich value - f.Pkg.Store(defaultXMLRichDataRichValueRel, MacintoshCyrillicCharset) + f.Pkg.Store(defaultXMLRdRichValueRel, MacintoshCyrillicCharset) _, err = f.GetPictures("Sheet1", "A1") assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") // Test get the image cells without block of metadata records @@ -498,7 +498,7 @@ func TestGetCellImages(t *testing.T) { assert.Empty(t, cells) // Test get the cell images with rich data rich value relationships f.Pkg.Store(defaultXMLMetadata, []byte(``)) - f.Pkg.Store(defaultXMLRichDataRichValueRel, []byte(``)) + f.Pkg.Store(defaultXMLRdRichValueRel, []byte(``)) pics, err = f.GetPictures("Sheet1", "A1") assert.NoError(t, err) assert.Empty(t, pics) @@ -514,17 +514,17 @@ func TestGetCellImages(t *testing.T) { f = prepareWorkbook() // Test get the cell images with empty image cell rich value - f.Pkg.Store(defaultXMLRichDataRichValue, []byte(`5`)) + f.Pkg.Store(defaultXMLRdRichValuePart, []byte(`5`)) pics, err = f.GetPictures("Sheet1", "A1") assert.EqualError(t, err, "strconv.Atoi: parsing \"\": invalid syntax") assert.Empty(t, pics) // Test get the cell images without image cell rich value - f.Pkg.Store(defaultXMLRichDataRichValue, []byte(`01`)) + f.Pkg.Store(defaultXMLRdRichValuePart, []byte(`01`)) pics, err = f.GetPictures("Sheet1", "A1") assert.NoError(t, err) assert.Empty(t, pics) // Test get the cell images with unsupported charset rich value - f.Pkg.Store(defaultXMLRichDataRichValue, MacintoshCyrillicCharset) + f.Pkg.Store(defaultXMLRdRichValuePart, MacintoshCyrillicCharset) _, err = f.GetPictures("Sheet1", "A1") assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") @@ -534,6 +534,44 @@ func TestGetCellImages(t *testing.T) { pics, err = f.GetPictures("Sheet1", "A1") assert.NoError(t, err) assert.Empty(t, pics) + + f = prepareWorkbook() + // Test get the cell images inserted by IMAGE formula function + f.Pkg.Store(defaultXMLRdRichValuePart, []byte(`0100`)) + f.Pkg.Store(defaultXMLRdRichValueWebImagePart, []byte(`
+ `)) + f.Pkg.Store(defaultXMLRdRichValueWebImagePartRels, []byte(fmt.Sprintf(``, SourceRelationshipHyperLink, SourceRelationshipImage))) + pics, err = f.GetPictures("Sheet1", "A1") + assert.NoError(t, err) + assert.Equal(t, 1, len(pics)) + assert.Equal(t, PictureInsertTypeIMAGE, pics[0].InsertType) + + // Test get the cell images inserted by IMAGE formula function with unsupported charset web images relationships + f.Relationships.Delete(defaultXMLRdRichValueWebImagePartRels) + f.Pkg.Store(defaultXMLRdRichValueWebImagePartRels, MacintoshCyrillicCharset) + pics, err = f.GetPictures("Sheet1", "A1") + assert.NoError(t, err) + assert.Empty(t, pics) + + // Test get the cell images inserted by IMAGE formula function without image part + f.Relationships.Delete(defaultXMLRdRichValueWebImagePartRels) + f.Pkg.Store(defaultXMLRdRichValueWebImagePartRels, []byte(fmt.Sprintf(``, SourceRelationshipHyperLink, SourceRelationshipHyperLink))) + pics, err = f.GetPictures("Sheet1", "A1") + assert.NoError(t, err) + assert.Empty(t, pics) + // Test get the cell images inserted by IMAGE formula function with unsupported charset web images part + f.Pkg.Store(defaultXMLRdRichValueWebImagePart, MacintoshCyrillicCharset) + _, err = f.GetPictures("Sheet1", "A1") + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") + // Test get the cell images inserted by IMAGE formula function with empty charset web images part + f.Pkg.Store(defaultXMLRdRichValueWebImagePart, []byte(``)) + pics, err = f.GetPictures("Sheet1", "A1") + assert.NoError(t, err) + assert.Empty(t, pics) + // Test get the cell images inserted by IMAGE formula function with invalid rich value index + f.Pkg.Store(defaultXMLRdRichValuePart, []byte(`100`)) + _, err = f.GetPictures("Sheet1", "A1") + assert.EqualError(t, err, "strconv.Atoi: parsing \"\": invalid syntax") } func TestGetImageCells(t *testing.T) { diff --git a/templates.go b/templates.go index 7d72e3b0b1..3bf4c5fe69 100644 --- a/templates.go +++ b/templates.go @@ -266,23 +266,25 @@ var supportedChartDataLabelsPosition = map[ChartType][]ChartDataLabelPositionTyp } const ( - defaultTempFileSST = "sharedStrings" - defaultXMLMetadata = "xl/metadata.xml" - defaultXMLPathCalcChain = "xl/calcChain.xml" - defaultXMLPathCellImages = "xl/cellimages.xml" - defaultXMLPathCellImagesRels = "xl/_rels/cellimages.xml.rels" - defaultXMLPathContentTypes = "[Content_Types].xml" - defaultXMLPathDocPropsApp = "docProps/app.xml" - defaultXMLPathDocPropsCore = "docProps/core.xml" - defaultXMLPathSharedStrings = "xl/sharedStrings.xml" - defaultXMLPathStyles = "xl/styles.xml" - defaultXMLPathTheme = "xl/theme/theme1.xml" - defaultXMLPathVolatileDeps = "xl/volatileDependencies.xml" - defaultXMLPathWorkbook = "xl/workbook.xml" - defaultXMLPathWorkbookRels = "xl/_rels/workbook.xml.rels" - defaultXMLRichDataRichValue = "xl/richData/rdrichvalue.xml" - defaultXMLRichDataRichValueRel = "xl/richData/richValueRel.xml" - defaultXMLRichDataRichValueRelRels = "xl/richData/_rels/richValueRel.xml.rels" + defaultTempFileSST = "sharedStrings" + defaultXMLMetadata = "xl/metadata.xml" + defaultXMLPathCalcChain = "xl/calcChain.xml" + defaultXMLPathCellImages = "xl/cellimages.xml" + defaultXMLPathCellImagesRels = "xl/_rels/cellimages.xml.rels" + defaultXMLPathContentTypes = "[Content_Types].xml" + defaultXMLPathDocPropsApp = "docProps/app.xml" + defaultXMLPathDocPropsCore = "docProps/core.xml" + defaultXMLPathSharedStrings = "xl/sharedStrings.xml" + defaultXMLPathStyles = "xl/styles.xml" + defaultXMLPathTheme = "xl/theme/theme1.xml" + defaultXMLPathVolatileDeps = "xl/volatileDependencies.xml" + defaultXMLPathWorkbook = "xl/workbook.xml" + defaultXMLPathWorkbookRels = "xl/_rels/workbook.xml.rels" + defaultXMLRdRichValuePart = "xl/richData/rdrichvalue.xml" + defaultXMLRdRichValueRel = "xl/richData/richValueRel.xml" + defaultXMLRdRichValueRelRels = "xl/richData/_rels/richValueRel.xml.rels" + defaultXMLRdRichValueWebImagePart = "xl/richData/rdRichValueWebImage.xml" + defaultXMLRdRichValueWebImagePartRels = "xl/richData/_rels/rdRichValueWebImage.xml.rels" ) // IndexedColorMapping is the table of default mappings from indexed color value diff --git a/xmlMetaData.go b/xmlMetaData.go index b3f97b6a3f..016e348674 100644 --- a/xmlMetaData.go +++ b/xmlMetaData.go @@ -98,3 +98,20 @@ type xlsxRichValueRels struct { type xlsxRichValueRelRelationship struct { ID string `xml:"id,attr"` } + +// xlsxWebImagesSupportingRichData directly maps the webImagesSrd element. This +// element specifies a list of sets of properties associated with web image rich +// values. +type xlsxWebImagesSupportingRichData struct { + XMLName xml.Name `xml:"webImagesSrd"` + WebImageSrd []xlsxWebImageSupportingRichData `xml:"webImageSrd"` + ExtLst *xlsxInnerXML `xml:"extLst"` +} + +// xlsxWebImageSupportingRichData directly maps the webImageSrd element. This +// element specifies a set of properties for a web image rich value. +type xlsxWebImageSupportingRichData struct { + Address xlsxExternalReference `xml:"address"` + MoreImagesAddress xlsxExternalReference `xml:"moreImagesAddress"` + Blip xlsxExternalReference `xml:"blip"` +} From 5f8a5b86901d882d19576f0fd9176ad8a9775752 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 3 Apr 2024 08:44:46 +0800 Subject: [PATCH 877/957] This closes #1867, breaking changes: change the data type for the ConditionalFormatOptions structure field Format as a pointer --- adjust_test.go | 2 +- excelize_test.go | 20 ++++----- rows_test.go | 2 +- styles.go | 107 ++++++++++++++++------------------------------- styles_test.go | 56 ++++++++++++------------- xmlWorksheet.go | 2 +- 6 files changed, 78 insertions(+), 111 deletions(-) diff --git a/adjust_test.go b/adjust_test.go index 2982562d97..eb5b47391c 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -999,7 +999,7 @@ func TestAdjustConditionalFormats(t *testing.T) { { Type: "cell", Criteria: "greater than", - Format: formatID, + Format: &formatID, Value: "0", }, } diff --git a/excelize_test.go b/excelize_test.go index 95f0b41fa3..c441b5e169 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -1088,7 +1088,7 @@ func TestConditionalFormat(t *testing.T) { { Type: "cell", Criteria: "between", - Format: format1, + Format: &format1, MinValue: "6", MaxValue: "8", }, @@ -1100,7 +1100,7 @@ func TestConditionalFormat(t *testing.T) { { Type: "cell", Criteria: ">", - Format: format3, + Format: &format3, Value: "6", }, }, @@ -1111,7 +1111,7 @@ func TestConditionalFormat(t *testing.T) { { Type: "top", Criteria: "=", - Format: format3, + Format: &format3, }, }, )) @@ -1121,7 +1121,7 @@ func TestConditionalFormat(t *testing.T) { { Type: "unique", Criteria: "=", - Format: format2, + Format: &format2, }, }, )) @@ -1131,7 +1131,7 @@ func TestConditionalFormat(t *testing.T) { { Type: "duplicate", Criteria: "=", - Format: format2, + Format: &format2, }, }, )) @@ -1141,7 +1141,7 @@ func TestConditionalFormat(t *testing.T) { { Type: "top", Criteria: "=", - Format: format1, + Format: &format1, Value: "6", Percent: true, }, @@ -1153,7 +1153,7 @@ func TestConditionalFormat(t *testing.T) { { Type: "average", Criteria: "=", - Format: format3, + Format: &format3, AboveAverage: true, }, }, @@ -1164,7 +1164,7 @@ func TestConditionalFormat(t *testing.T) { { Type: "average", Criteria: "=", - Format: format1, + Format: &format1, AboveAverage: false, }, }, @@ -1187,7 +1187,7 @@ func TestConditionalFormat(t *testing.T) { { Type: "formula", Criteria: "L2<3", - Format: format1, + Format: &format1, }, }, )) @@ -1197,7 +1197,7 @@ func TestConditionalFormat(t *testing.T) { { Type: "cell", Criteria: ">", - Format: format4, + Format: &format4, Value: "0", }, }, diff --git a/rows_test.go b/rows_test.go index ad15715430..01b20a0fcf 100644 --- a/rows_test.go +++ b/rows_test.go @@ -892,7 +892,7 @@ func TestDuplicateRow(t *testing.T) { assert.NoError(t, err) expected := []ConditionalFormatOptions{ - {Type: "cell", Criteria: "greater than", Format: format, Value: "0"}, + {Type: "cell", Criteria: "greater than", Format: &format, Value: "0"}, } assert.NoError(t, f.SetConditionalFormat("Sheet1", "A1", expected)) diff --git a/styles.go b/styles.go index 4192f1180a..ccd758a6c2 100644 --- a/styles.go +++ b/styles.go @@ -2445,7 +2445,7 @@ func (f *File) SetCellStyle(sheet, topLeftCell, bottomRightCell string, styleID // { // Type: "cell", // Criteria: ">", -// Format: format, +// Format: &format, // Value: "6", // }, // }, @@ -2458,7 +2458,7 @@ func (f *File) SetCellStyle(sheet, topLeftCell, bottomRightCell string, styleID // { // Type: "cell", // Criteria: ">", -// Format: format, +// Format: &format, // Value: "$C$1", // }, // }, @@ -2482,7 +2482,7 @@ func (f *File) SetCellStyle(sheet, topLeftCell, bottomRightCell string, styleID // } // err = f.SetConditionalFormat("Sheet1", "D1:D10", // []excelize.ConditionalFormatOptions{ -// {Type: "cell", Criteria: ">", Format: format, Value: "6"}, +// {Type: "cell", Criteria: ">", Format: &format, Value: "6"}, // }, // ) // @@ -2534,7 +2534,7 @@ func (f *File) SetCellStyle(sheet, topLeftCell, bottomRightCell string, styleID // { // Type: "cell", // Criteria: "between", -// Format: format, +// Format: &format, // MinValue: 6", // MaxValue: 8", // }, @@ -2554,7 +2554,7 @@ func (f *File) SetCellStyle(sheet, topLeftCell, bottomRightCell string, styleID // { // Type: "average", // Criteria: "=", -// Format: format1, +// Format: &format1, // AboveAverage: true, // }, // }, @@ -2566,7 +2566,7 @@ func (f *File) SetCellStyle(sheet, topLeftCell, bottomRightCell string, styleID // { // Type: "average", // Criteria: "=", -// Format: format2, +// Format: &format2, // AboveAverage: false, // }, // }, @@ -2578,7 +2578,7 @@ func (f *File) SetCellStyle(sheet, topLeftCell, bottomRightCell string, styleID // // Highlight cells rules: Duplicate Values... // err := f.SetConditionalFormat("Sheet1", "A1:A10", // []excelize.ConditionalFormatOptions{ -// {Type: "duplicate", Criteria: "=", Format: format}, +// {Type: "duplicate", Criteria: "=", Format: &format}, // }, // ) // @@ -2587,7 +2587,7 @@ func (f *File) SetCellStyle(sheet, topLeftCell, bottomRightCell string, styleID // // Highlight cells rules: Not Equal To... // err := f.SetConditionalFormat("Sheet1", "A1:A10", // []excelize.ConditionalFormatOptions{ -// {Type: "unique", Criteria: "=", Format: format}, +// {Type: "unique", Criteria: "=", Format: &format}, // }, // ) // @@ -2600,7 +2600,7 @@ func (f *File) SetCellStyle(sheet, topLeftCell, bottomRightCell string, styleID // { // Type: "top", // Criteria: "=", -// Format: format, +// Format: &format, // Value: "6", // }, // }, @@ -2613,7 +2613,7 @@ func (f *File) SetCellStyle(sheet, topLeftCell, bottomRightCell string, styleID // { // Type: "top", // Criteria: "=", -// Format: format, +// Format: &format, // Value: "6", // Percent: true, // }, @@ -2931,10 +2931,7 @@ func (f *File) appendCfRule(ws *xlsxWorksheet, rule *xlsxX14CfRule) error { // settings for cell value (include between, not between, equal, not equal, // greater than and less than) by given conditional formatting rule. func (f *File) extractCondFmtCellIs(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { - format := ConditionalFormatOptions{StopIfTrue: c.StopIfTrue, Type: "cell", Criteria: operatorType[c.Operator]} - if c.DxfID != nil { - format.Format = *c.DxfID - } + format := ConditionalFormatOptions{Format: c.DxfID, StopIfTrue: c.StopIfTrue, Type: "cell", Criteria: operatorType[c.Operator]} if len(c.Formula) == 2 { format.MinValue, format.MaxValue = c.Formula[0], c.Formula[1] return format @@ -2946,21 +2943,13 @@ func (f *File) extractCondFmtCellIs(c *xlsxCfRule, extLst *xlsxExtLst) Condition // extractCondFmtTimePeriod provides a function to extract conditional format // settings for time period by given conditional formatting rule. func (f *File) extractCondFmtTimePeriod(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { - format := ConditionalFormatOptions{StopIfTrue: c.StopIfTrue, Type: "time_period", Criteria: operatorType[c.Operator]} - if c.DxfID != nil { - format.Format = *c.DxfID - } - return format + return ConditionalFormatOptions{Format: c.DxfID, StopIfTrue: c.StopIfTrue, Type: "time_period", Criteria: operatorType[c.Operator]} } // extractCondFmtText provides a function to extract conditional format // settings for text cell values by given conditional formatting rule. func (f *File) extractCondFmtText(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { - format := ConditionalFormatOptions{StopIfTrue: c.StopIfTrue, Type: "text", Criteria: operatorType[c.Operator], Value: c.Text} - if c.DxfID != nil { - format.Format = *c.DxfID - } - return format + return ConditionalFormatOptions{Format: c.DxfID, StopIfTrue: c.StopIfTrue, Type: "text", Criteria: operatorType[c.Operator], Value: c.Text} } // extractCondFmtTop10 provides a function to extract conditional format @@ -2968,15 +2957,13 @@ func (f *File) extractCondFmtText(c *xlsxCfRule, extLst *xlsxExtLst) Conditional // rule. func (f *File) extractCondFmtTop10(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { format := ConditionalFormatOptions{ + Format: c.DxfID, StopIfTrue: c.StopIfTrue, Type: "top", Criteria: "=", Percent: c.Percent, Value: strconv.Itoa(c.Rank), } - if c.DxfID != nil { - format.Format = *c.DxfID - } if c.Bottom { format.Type = "bottom" } @@ -2988,13 +2975,11 @@ func (f *File) extractCondFmtTop10(c *xlsxCfRule, extLst *xlsxExtLst) Conditiona // rule. func (f *File) extractCondFmtAboveAverage(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { format := ConditionalFormatOptions{ + Format: c.DxfID, StopIfTrue: c.StopIfTrue, Type: "average", Criteria: "=", } - if c.DxfID != nil { - format.Format = *c.DxfID - } if c.AboveAverage != nil { format.AboveAverage = *c.AboveAverage } @@ -3005,7 +2990,8 @@ func (f *File) extractCondFmtAboveAverage(c *xlsxCfRule, extLst *xlsxExtLst) Con // conditional format settings for duplicate and unique values by given // conditional formatting rule. func (f *File) extractCondFmtDuplicateUniqueValues(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { - format := ConditionalFormatOptions{ + return ConditionalFormatOptions{ + Format: c.DxfID, StopIfTrue: c.StopIfTrue, Type: map[string]string{ "duplicateValues": "duplicate", @@ -3013,62 +2999,46 @@ func (f *File) extractCondFmtDuplicateUniqueValues(c *xlsxCfRule, extLst *xlsxEx }[c.Type], Criteria: "=", } - if c.DxfID != nil { - format.Format = *c.DxfID - } - return format } // extractCondFmtBlanks provides a function to extract conditional format // settings for blank cells by given conditional formatting rule. func (f *File) extractCondFmtBlanks(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { - format := ConditionalFormatOptions{ + return ConditionalFormatOptions{ + Format: c.DxfID, StopIfTrue: c.StopIfTrue, Type: "blanks", } - if c.DxfID != nil { - format.Format = *c.DxfID - } - return format } // extractCondFmtNoBlanks provides a function to extract conditional format // settings for no blank cells by given conditional formatting rule. func (f *File) extractCondFmtNoBlanks(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { - format := ConditionalFormatOptions{ + return ConditionalFormatOptions{ + Format: c.DxfID, StopIfTrue: c.StopIfTrue, Type: "no_blanks", } - if c.DxfID != nil { - format.Format = *c.DxfID - } - return format } // extractCondFmtErrors provides a function to extract conditional format // settings for cells with errors by given conditional formatting rule. func (f *File) extractCondFmtErrors(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { - format := ConditionalFormatOptions{ + return ConditionalFormatOptions{ + Format: c.DxfID, StopIfTrue: c.StopIfTrue, Type: "errors", } - if c.DxfID != nil { - format.Format = *c.DxfID - } - return format } // extractCondFmtNoErrors provides a function to extract conditional format // settings for cells without errors by given conditional formatting rule. func (f *File) extractCondFmtNoErrors(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { - format := ConditionalFormatOptions{ + return ConditionalFormatOptions{ + Format: c.DxfID, StopIfTrue: c.StopIfTrue, Type: "no_errors", } - if c.DxfID != nil { - format.Format = *c.DxfID - } - return format } // extractCondFmtColorScale provides a function to extract conditional format @@ -3165,10 +3135,7 @@ func (f *File) extractCondFmtDataBar(c *xlsxCfRule, extLst *xlsxExtLst) Conditio // extractCondFmtExp provides a function to extract conditional format settings // for expression by given conditional formatting rule. func (f *File) extractCondFmtExp(c *xlsxCfRule, extLst *xlsxExtLst) ConditionalFormatOptions { - format := ConditionalFormatOptions{StopIfTrue: c.StopIfTrue, Type: "formula"} - if c.DxfID != nil { - format.Format = *c.DxfID - } + format := ConditionalFormatOptions{Format: c.DxfID, StopIfTrue: c.StopIfTrue, Type: "formula"} if len(c.Formula) > 0 { format.Criteria = c.Formula[0] } @@ -3234,7 +3201,7 @@ func drawCondFmtCellIs(p int, ct, ref, GUID string, format *ConditionalFormatOpt StopIfTrue: format.StopIfTrue, Type: validType[format.Type], Operator: ct, - DxfID: intPtr(format.Format), + DxfID: format.Format, } // "between" and "not between" criteria require 2 values. if ct == "between" || ct == "notBetween" { @@ -3268,7 +3235,7 @@ func drawCondFmtTimePeriod(p int, ct, ref, GUID string, format *ConditionalForma "continue month": fmt.Sprintf("AND(MONTH(%[1]s)=MONTH(TODAY())+1,OR(YEAR(%[1]s)=YEAR(TODAY()),AND(MONTH(%[1]s)=12,YEAR(%[1]s)=YEAR(TODAY())+1)))", ref), }[ct], }, - DxfID: intPtr(format.Format), + DxfID: format.Format, }, nil } @@ -3298,7 +3265,7 @@ func drawCondFmtText(p int, ct, ref, GUID string, format *ConditionalFormatOptio strings.NewReplacer(`"`, `""`).Replace(format.Value), ref), }[ct], }, - DxfID: intPtr(format.Format), + DxfID: format.Format, }, nil } @@ -3312,7 +3279,7 @@ func drawCondFmtTop10(p int, ct, ref, GUID string, format *ConditionalFormatOpti Bottom: format.Type == "bottom", Type: validType[format.Type], Rank: 10, - DxfID: intPtr(format.Format), + DxfID: format.Format, Percent: format.Percent, } if rank, err := strconv.Atoi(format.Value); err == nil { @@ -3330,7 +3297,7 @@ func drawCondFmtAboveAverage(p int, ct, ref, GUID string, format *ConditionalFor StopIfTrue: format.StopIfTrue, Type: validType[format.Type], AboveAverage: boolPtr(format.AboveAverage), - DxfID: intPtr(format.Format), + DxfID: format.Format, }, nil } @@ -3342,7 +3309,7 @@ func drawCondFmtDuplicateUniqueValues(p int, ct, ref, GUID string, format *Condi Priority: p + 1, StopIfTrue: format.StopIfTrue, Type: validType[format.Type], - DxfID: intPtr(format.Format), + DxfID: format.Format, }, nil } @@ -3430,7 +3397,7 @@ func drawCondFmtExp(p int, ct, ref, GUID string, format *ConditionalFormatOption StopIfTrue: format.StopIfTrue, Type: validType[format.Type], Formula: []string{format.Criteria}, - DxfID: intPtr(format.Format), + DxfID: format.Format, }, nil } @@ -3442,7 +3409,7 @@ func drawCondFmtErrors(p int, ct, ref, GUID string, format *ConditionalFormatOpt StopIfTrue: format.StopIfTrue, Type: validType[format.Type], Formula: []string{fmt.Sprintf("ISERROR(%s)", ref)}, - DxfID: intPtr(format.Format), + DxfID: format.Format, }, nil } @@ -3454,7 +3421,7 @@ func drawCondFmtNoErrors(p int, ct, ref, GUID string, format *ConditionalFormatO StopIfTrue: format.StopIfTrue, Type: validType[format.Type], Formula: []string{fmt.Sprintf("NOT(ISERROR(%s))", ref)}, - DxfID: intPtr(format.Format), + DxfID: format.Format, }, nil } @@ -3466,7 +3433,7 @@ func drawCondFmtBlanks(p int, ct, ref, GUID string, format *ConditionalFormatOpt StopIfTrue: format.StopIfTrue, Type: validType[format.Type], Formula: []string{fmt.Sprintf("LEN(TRIM(%s))=0", ref)}, - DxfID: intPtr(format.Format), + DxfID: format.Format, }, nil } @@ -3478,7 +3445,7 @@ func drawCondFmtNoBlanks(p int, ct, ref, GUID string, format *ConditionalFormatO StopIfTrue: format.StopIfTrue, Type: validType[format.Type], Formula: []string{fmt.Sprintf("LEN(TRIM(%s))>0", ref)}, - DxfID: intPtr(format.Format), + DxfID: format.Format, }, nil } diff --git a/styles_test.go b/styles_test.go index e11740e815..1c309bcfee 100644 --- a/styles_test.go +++ b/styles_test.go @@ -172,7 +172,7 @@ func TestSetConditionalFormat(t *testing.T) { // 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"}, + {Type: "data_bar", BarColor: "#A9D08E", BarSolid: true, Format: intPtr(0), Criteria: "=", MinType: "min", MaxType: "max"}, } for _, ref := range []string{"A1:A2", "B1:B2"} { assert.NoError(t, f.SetConditionalFormat("Sheet1", ref, condFmts)) @@ -243,36 +243,36 @@ func TestSetConditionalFormat(t *testing.T) { func TestGetConditionalFormats(t *testing.T) { for _, format := range [][]ConditionalFormatOptions{ - {{Type: "cell", Format: 1, Criteria: "greater than", Value: "6"}}, - {{Type: "cell", Format: 1, Criteria: "between", MinValue: "6", MaxValue: "8"}}, - {{Type: "time_period", Format: 1, Criteria: "yesterday"}}, - {{Type: "time_period", Format: 1, Criteria: "today"}}, - {{Type: "time_period", Format: 1, Criteria: "tomorrow"}}, - {{Type: "time_period", Format: 1, Criteria: "last 7 days"}}, - {{Type: "time_period", Format: 1, Criteria: "last week"}}, - {{Type: "time_period", Format: 1, Criteria: "this week"}}, - {{Type: "time_period", Format: 1, Criteria: "continue week"}}, - {{Type: "time_period", Format: 1, Criteria: "last month"}}, - {{Type: "time_period", Format: 1, Criteria: "this month"}}, - {{Type: "time_period", Format: 1, Criteria: "continue month"}}, - {{Type: "text", Format: 1, Criteria: "containing", Value: "~!@#$%^&*()_+{}|:<>?\"';"}}, - {{Type: "text", Format: 1, Criteria: "not containing", Value: "text"}}, - {{Type: "text", Format: 1, Criteria: "begins with", Value: "prefix"}}, - {{Type: "text", Format: 1, Criteria: "ends with", Value: "suffix"}}, - {{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: "cell", Format: intPtr(1), Criteria: "greater than", Value: "6"}}, + {{Type: "cell", Format: intPtr(1), Criteria: "between", MinValue: "6", MaxValue: "8"}}, + {{Type: "time_period", Format: intPtr(1), Criteria: "yesterday"}}, + {{Type: "time_period", Format: intPtr(1), Criteria: "today"}}, + {{Type: "time_period", Format: intPtr(1), Criteria: "tomorrow"}}, + {{Type: "time_period", Format: intPtr(1), Criteria: "last 7 days"}}, + {{Type: "time_period", Format: intPtr(1), Criteria: "last week"}}, + {{Type: "time_period", Format: intPtr(1), Criteria: "this week"}}, + {{Type: "time_period", Format: intPtr(1), Criteria: "continue week"}}, + {{Type: "time_period", Format: intPtr(1), Criteria: "last month"}}, + {{Type: "time_period", Format: intPtr(1), Criteria: "this month"}}, + {{Type: "time_period", Format: intPtr(1), Criteria: "continue month"}}, + {{Type: "text", Format: intPtr(1), Criteria: "containing", Value: "~!@#$%^&*()_+{}|:<>?\"';"}}, + {{Type: "text", Format: intPtr(1), Criteria: "not containing", Value: "text"}}, + {{Type: "text", Format: intPtr(1), Criteria: "begins with", Value: "prefix"}}, + {{Type: "text", Format: intPtr(1), Criteria: "ends with", Value: "suffix"}}, + {{Type: "top", Format: intPtr(1), Criteria: "=", Value: "6"}}, + {{Type: "bottom", Format: intPtr(1), Criteria: "=", Value: "6"}}, + {{Type: "average", AboveAverage: true, Format: intPtr(1), Criteria: "="}}, + {{Type: "duplicate", Format: intPtr(1), Criteria: "="}}, + {{Type: "unique", Format: intPtr(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: "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: "blanks", Format: 1}}, - {{Type: "no_blanks", Format: 1}}, - {{Type: "errors", Format: 1}}, - {{Type: "no_errors", Format: 1}}, + {{Type: "formula", Format: intPtr(1), Criteria: "="}}, + {{Type: "blanks", Format: intPtr(1)}}, + {{Type: "no_blanks", Format: intPtr(1)}}, + {{Type: "errors", Format: intPtr(1)}}, + {{Type: "no_errors", Format: intPtr(1)}}, {{Type: "icon_set", IconStyle: "3Arrows", ReverseIcons: true, IconsOnly: true}}, } { f := NewFile() @@ -309,7 +309,7 @@ func TestUnsetConditionalFormat(t *testing.T) { 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}}) assert.NoError(t, err) - assert.NoError(t, f.SetConditionalFormat("Sheet1", "A1:A10", []ConditionalFormatOptions{{Type: "cell", Criteria: ">", Format: format, Value: "6"}})) + 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") diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 3a71d698f4..43359d5d58 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -917,7 +917,7 @@ type ConditionalFormatOptions struct { Type string AboveAverage bool Percent bool - Format int + Format *int Criteria string Value string MinType string From 3e636ae7b20285d4efb3d21bba0e1c165318f9f8 Mon Sep 17 00:00:00 2001 From: Nima <52995288+iraj720@users.noreply.github.com> Date: Thu, 11 Apr 2024 18:42:56 +0330 Subject: [PATCH 878/957] This closes #1874, reduces memory usage for the GetRows function (#1875) - Avoid allocate memory for reading continuously empty rows on the tail of the worksheet --- rows.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rows.go b/rows.go index 7610836f17..bf22d0cf03 100644 --- a/rows.go +++ b/rows.go @@ -70,8 +70,11 @@ func (f *File) GetRows(sheet string, opts ...Options) ([][]string, error) { if err != nil { break } - results = append(results, row) if len(row) > 0 { + if emptyRows := cur - maxVal - 1; emptyRows > 0 { + results = append(results, make([][]string, emptyRows)...) + } + results = append(results, row) maxVal = cur } } From f8487a68a87f3ebc8be5fef9e6ceffa1346167aa Mon Sep 17 00:00:00 2001 From: jianxinhou <51222175+jianxinhou@users.noreply.github.com> Date: Thu, 18 Apr 2024 13:21:46 +0800 Subject: [PATCH 879/957] This closes #1879, compatible with the escaped quote symbol in none formula data validation rules (#1880) - Update dependencies module to fix vulnerabilities - Update unit tests Co-authored-by: houjianxin.rupert --- adjust.go | 4 ++-- adjust_test.go | 20 ++++++++++++++++++++ datavalidation.go | 5 +++++ go.mod | 6 +++--- go.sum | 12 ++++++------ 5 files changed, 36 insertions(+), 11 deletions(-) diff --git a/adjust.go b/adjust.go index 7c8ec9ee21..583e2b979f 100644 --- a/adjust.go +++ b/adjust.go @@ -989,14 +989,14 @@ func (f *File) adjustDataValidations(ws *xlsxWorksheet, sheet string, dir adjust } worksheet.DataValidations.DataValidation[i].Sqref = ref } - if worksheet.DataValidations.DataValidation[i].Formula1 != nil { + if worksheet.DataValidations.DataValidation[i].Formula1.isFormula() { formula := formulaUnescaper.Replace(worksheet.DataValidations.DataValidation[i].Formula1.Content) if formula, err = f.adjustFormulaRef(sheet, sheetN, formula, false, dir, num, offset); err != nil { return err } worksheet.DataValidations.DataValidation[i].Formula1 = &xlsxInnerXML{Content: formulaEscaper.Replace(formula)} } - if worksheet.DataValidations.DataValidation[i].Formula2 != nil { + if worksheet.DataValidations.DataValidation[i].Formula2.isFormula() { formula := formulaUnescaper.Replace(worksheet.DataValidations.DataValidation[i].Formula2.Content) if formula, err = f.adjustFormulaRef(sheet, sheetN, formula, false, dir, num, offset); err != nil { return err diff --git a/adjust_test.go b/adjust_test.go index eb5b47391c..cde12fb287 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -4,6 +4,7 @@ import ( "encoding/xml" "fmt" "path/filepath" + "strings" "testing" _ "image/jpeg" @@ -1099,6 +1100,25 @@ func TestAdjustDataValidations(t *testing.T) { f.Sheet.Delete("xl/worksheets/sheet1.xml") f.Pkg.Store("xl/worksheets/sheet1.xml", MacintoshCyrillicCharset) assert.EqualError(t, f.adjustDataValidations(nil, "Sheet1", columns, 0, 0, 1), "XML syntax error on line 1: invalid UTF-8") + + t.Run("for_escaped_data_validation_rules_formula", func(t *testing.T) { + f := NewFile() + _, err := f.NewSheet("Sheet2") + assert.NoError(t, err) + dv := NewDataValidation(true) + dv.Sqref = "A1" + assert.NoError(t, dv.SetDropList([]string{"option1", strings.Repeat("\"", 4)})) + ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") + assert.True(t, ok) + assert.NoError(t, f.AddDataValidation("Sheet1", dv)) + // The double quote symbol in none formula data validation rules will be escaped in the Kingsoft WPS Office + formula := strings.ReplaceAll(fmt.Sprintf("\"option1, %s", strings.Repeat("\"", 9)), "\"", """) + ws.(*xlsxWorksheet).DataValidations.DataValidation[0].Formula1.Content = formula + f.RemoveCol("Sheet2", "A") + dvs, err := f.GetDataValidations("Sheet1") + assert.NoError(t, err) + assert.Equal(t, formula, dvs[0].Formula1) + }) } func TestAdjustDrawings(t *testing.T) { diff --git a/datavalidation.go b/datavalidation.go index 40ffd1950f..f42c1db903 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -442,6 +442,11 @@ func squashSqref(cells [][]int) []string { return append(refs, ref) } +// isFormulaDataValidation returns whether the data validation rule is a formula. +func (dv *xlsxInnerXML) isFormula() bool { + return dv != nil && !(strings.HasPrefix(dv.Content, """) && strings.HasSuffix(dv.Content, """)) +} + // unescapeDataValidationFormula returns unescaped data validation formula. func unescapeDataValidationFormula(val string) string { if strings.HasPrefix(val, "\"") { // Text detection diff --git a/go.mod b/go.mod index 8c12fa1eb6..d9879927dd 100644 --- a/go.mod +++ b/go.mod @@ -6,11 +6,11 @@ require ( github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 github.com/richardlehane/mscfb v1.0.4 github.com/stretchr/testify v1.8.4 - github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53 + github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 - golang.org/x/crypto v0.21.0 + golang.org/x/crypto v0.22.0 golang.org/x/image v0.14.0 - golang.org/x/net v0.22.0 + golang.org/x/net v0.24.0 golang.org/x/text v0.14.0 ) diff --git a/go.sum b/go.sum index 22aee209c6..a18c395579 100644 --- a/go.sum +++ b/go.sum @@ -11,16 +11,16 @@ github.com/richardlehane/msoleps v1.0.3 h1:aznSZzrwYRl3rLKRT3gUk9am7T/mLNSnJINvN github.com/richardlehane/msoleps v1.0.3/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53 h1:Chd9DkqERQQuHpXjR/HSV1jLZA6uaoiwwH3vSuF3IW0= -github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= +github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7psK/lVsjIS2otl+1WyRyY= +github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A= github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4= golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= -golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= -golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= From 055349d8a62e6b4e66bcf3854c8a9086e912c409 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 26 Apr 2024 00:23:10 +0800 Subject: [PATCH 880/957] Fix a v2.8.1 regression bug, error on duplicate rows, if conditional formatting or data validation has multiple cell range reference - Update unit tests --- adjust_test.go | 2 +- cell.go | 2 +- rows.go | 67 ++++++++++++++++++++++++++++++++------------------ 3 files changed, 45 insertions(+), 26 deletions(-) diff --git a/adjust_test.go b/adjust_test.go index cde12fb287..4f42e5145d 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -1114,7 +1114,7 @@ func TestAdjustDataValidations(t *testing.T) { // The double quote symbol in none formula data validation rules will be escaped in the Kingsoft WPS Office formula := strings.ReplaceAll(fmt.Sprintf("\"option1, %s", strings.Repeat("\"", 9)), "\"", """) ws.(*xlsxWorksheet).DataValidations.DataValidation[0].Formula1.Content = formula - f.RemoveCol("Sheet2", "A") + assert.NoError(t, f.RemoveCol("Sheet2", "A")) dvs, err := f.GetDataValidations("Sheet1") assert.NoError(t, err) assert.Equal(t, formula, dvs[0].Formula1) diff --git a/cell.go b/cell.go index 3b712c651f..a13313a487 100644 --- a/cell.go +++ b/cell.go @@ -506,7 +506,7 @@ func trimCellValue(value string, escape bool) (v string, ns xml.Attr) { if escape { var buf bytes.Buffer enc := xml.NewEncoder(&buf) - enc.EncodeToken(xml.CharData(value)) + _ = enc.EncodeToken(xml.CharData(value)) enc.Flush() value = buf.String() } diff --git a/rows.go b/rows.go index bf22d0cf03..d43a015040 100644 --- a/rows.go +++ b/rows.go @@ -691,26 +691,46 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) error { return err } +// duplicateSQRefHelper provides a function to adjust conditional formatting and +// data validations cell reference when duplicate rows. +func duplicateSQRefHelper(row, row2 int, ref string) (string, error) { + if !strings.Contains(ref, ":") { + ref += ":" + ref + } + abs := strings.Contains(ref, "$") + coordinates, err := rangeRefToCoordinates(ref) + if err != nil { + return "", err + } + x1, y1, x2, y2 := coordinates[0], coordinates[1], coordinates[2], coordinates[3] + if y1 == y2 && y1 == row { + if ref, err = coordinatesToRangeRef([]int{x1, row2, x2, row2}, abs); err != nil { + return "", err + } + return ref, err + } + return "", err +} + // duplicateConditionalFormat create conditional formatting for the destination // row if there are conditional formats in the copied row. func (f *File) duplicateConditionalFormat(ws *xlsxWorksheet, sheet string, row, row2 int) error { var cfs []*xlsxConditionalFormatting for _, cf := range ws.ConditionalFormatting { if cf != nil { - if !strings.Contains(cf.SQRef, ":") { - cf.SQRef += ":" + cf.SQRef - } - abs := strings.Contains(cf.SQRef, "$") - coordinates, err := rangeRefToCoordinates(cf.SQRef) - if err != nil { - return err - } - x1, y1, x2, y2 := coordinates[0], coordinates[1], coordinates[2], coordinates[3] - if y1 == y2 && y1 == row { - cfCopy := deepcopy.Copy(*cf).(xlsxConditionalFormatting) - if cfCopy.SQRef, err = coordinatesToRangeRef([]int{x1, row2, x2, row2}, abs); err != nil { + var SQRef []string + for _, ref := range strings.Split(cf.SQRef, " ") { + coordinates, err := duplicateSQRefHelper(row, row2, ref) + if err != nil { return err } + if coordinates != "" { + SQRef = append(SQRef, coordinates) + } + } + if len(SQRef) > 0 { + cfCopy := deepcopy.Copy(*cf).(xlsxConditionalFormatting) + cfCopy.SQRef = strings.Join(SQRef, " ") cfs = append(cfs, &cfCopy) } } @@ -728,20 +748,19 @@ func (f *File) duplicateDataValidations(ws *xlsxWorksheet, sheet string, row, ro var dvs []*xlsxDataValidation for _, dv := range ws.DataValidations.DataValidation { if dv != nil { - if !strings.Contains(dv.Sqref, ":") { - dv.Sqref += ":" + dv.Sqref - } - abs := strings.Contains(dv.Sqref, "$") - coordinates, err := rangeRefToCoordinates(dv.Sqref) - if err != nil { - return err - } - x1, y1, x2, y2 := coordinates[0], coordinates[1], coordinates[2], coordinates[3] - if y1 == y2 && y1 == row { - dvCopy := deepcopy.Copy(*dv).(xlsxDataValidation) - if dvCopy.Sqref, err = coordinatesToRangeRef([]int{x1, row2, x2, row2}, abs); err != nil { + var SQRef []string + for _, ref := range strings.Split(dv.Sqref, " ") { + coordinates, err := duplicateSQRefHelper(row, row2, ref) + if err != nil { return err } + if coordinates != "" { + SQRef = append(SQRef, coordinates) + } + } + if len(SQRef) > 0 { + dvCopy := deepcopy.Copy(*dv).(xlsxDataValidation) + dvCopy.Sqref = strings.Join(SQRef, " ") dvs = append(dvs, &dvCopy) } } From 7715c1462a917c657d4022a4fe5b57d41d77055a Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 27 Apr 2024 20:13:43 +0800 Subject: [PATCH 881/957] This closes #1886, remove the namespace prefix for the default spreadsheet namespace - Improvement compatibility for the workbook internal part with a spreadsheet namespace prefix - Update GitHub Action configuration, using the macOS 13 in the unit test pipeline to temporarily resolve test failed in macos-14-arm64 --- .github/workflows/go.yml | 2 +- lib.go | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index c56feda925..c7eb187d4d 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -6,7 +6,7 @@ jobs: strategy: matrix: go-version: [1.18.x, 1.19.x, 1.20.x, '>=1.21.1', 1.22.x] - os: [ubuntu-latest, macos-latest, windows-latest] + os: [ubuntu-latest, macos-13, windows-latest] targetplatform: [x86, x64] runs-on: ${{ matrix.os }} diff --git a/lib.go b/lib.go index bfa992e587..420f69a0f5 100644 --- a/lib.go +++ b/lib.go @@ -646,6 +646,11 @@ func getRootElement(d *xml.Decoder) []xml.Attr { case xml.StartElement: tokenIdx++ if tokenIdx == 1 { + for i := 0; i < len(startElement.Attr); i++ { + if startElement.Attr[i].Value == NameSpaceSpreadSheet.Value { + startElement.Attr[i] = NameSpaceSpreadSheet + } + } return startElement.Attr } } From 781c38481dcd30fbd0bd3b43630e29dcdda7a3b3 Mon Sep 17 00:00:00 2001 From: barlevd <32372804+barlevd@users.noreply.github.com> Date: Tue, 30 Apr 2024 19:05:05 +0300 Subject: [PATCH 882/957] This closes #1889, refs #1732 and #1735 (#1890) Saving workbook with reverse sorted internal part path to keep same hash of identical files and fix incorrect MIME type --- file.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/file.go b/file.go index 067f9997cc..03af61eb1a 100644 --- a/file.go +++ b/file.go @@ -212,7 +212,7 @@ func (f *File) writeToZip(zw *zip.Writer) error { files = append(files, path.(string)) return true }) - sort.Strings(files) + sort.Sort(sort.Reverse(sort.StringSlice(files))) for _, path := range files { var fi io.Writer if fi, err = zw.Create(path); err != nil { @@ -228,7 +228,7 @@ func (f *File) writeToZip(zw *zip.Writer) error { tempFiles = append(tempFiles, path.(string)) return true }) - sort.Strings(tempFiles) + sort.Sort(sort.Reverse(sort.StringSlice(tempFiles))) for _, path := range tempFiles { var fi io.Writer if fi, err = zw.Create(path); err != nil { From a64efca31f64b9c04ee7c244c31586f06b1a5f12 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 3 May 2024 10:16:00 +0000 Subject: [PATCH 883/957] This fixes #1888, read internal media files with absolute path --- picture.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/picture.go b/picture.go index 27d6b7fe8c..7b46df80b8 100644 --- a/picture.go +++ b/picture.go @@ -598,7 +598,7 @@ func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) cond2 := func(from *decodeFrom) bool { return from.Col == col && from.Row == row } cb := func(a *xdrCellAnchor, r *xlsxRelationship) { pic := Picture{Extension: filepath.Ext(r.Target), Format: &GraphicOptions{}, InsertType: PictureInsertTypePlaceOverCells} - if buffer, _ := f.Pkg.Load(strings.ReplaceAll(r.Target, "..", "xl")); buffer != nil { + if buffer, _ := f.Pkg.Load(filepath.ToSlash(filepath.Clean("xl/drawings/" + r.Target))); buffer != nil { pic.File = buffer.([]byte) pic.Format.AltText = a.Pic.NvPicPr.CNvPr.Descr pics = append(pics, pic) @@ -606,7 +606,7 @@ func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) } cb2 := func(a *decodeCellAnchor, r *xlsxRelationship) { pic := Picture{Extension: filepath.Ext(r.Target), Format: &GraphicOptions{}, InsertType: PictureInsertTypePlaceOverCells} - if buffer, _ := f.Pkg.Load(strings.ReplaceAll(r.Target, "..", "xl")); buffer != nil { + if buffer, _ := f.Pkg.Load(filepath.ToSlash(filepath.Clean("xl/drawings/" + r.Target))); buffer != nil { pic.File = buffer.([]byte) pic.Format.AltText = a.Pic.NvPicPr.CNvPr.Descr pics = append(pics, pic) @@ -756,14 +756,14 @@ func (f *File) getPictureCells(drawingXML, drawingRelationships string) ([]strin cond := func(from *xlsxFrom) bool { return true } cond2 := func(from *decodeFrom) bool { return true } cb := func(a *xdrCellAnchor, r *xlsxRelationship) { - if _, ok := f.Pkg.Load(strings.ReplaceAll(r.Target, "..", "xl")); ok { + if _, ok := f.Pkg.Load(filepath.ToSlash(filepath.Clean("xl/drawings/" + r.Target))); ok { if cell, err := CoordinatesToCellName(a.From.Col+1, a.From.Row+1); err == nil && inStrSlice(cells, cell, true) == -1 { cells = append(cells, cell) } } } cb2 := func(a *decodeCellAnchor, r *xlsxRelationship) { - if _, ok := f.Pkg.Load(strings.ReplaceAll(r.Target, "..", "xl")); ok { + if _, ok := f.Pkg.Load(filepath.ToSlash(filepath.Clean("xl/drawings/" + r.Target))); ok { if cell, err := CoordinatesToCellName(a.From.Col+1, a.From.Row+1); err == nil && inStrSlice(cells, cell, true) == -1 { cells = append(cells, cell) } From 5f583549f4cbbd33c7b143532fbccee4d3426653 Mon Sep 17 00:00:00 2001 From: nna <56760191+18409615759@users.noreply.github.com> Date: Tue, 14 May 2024 12:06:10 +0800 Subject: [PATCH 884/957] Add unit test for the stream writer to improved line of code coverage (#1898) - Update dependencies modules - Using the workbook instead of XLSX in the function comments --- chart_test.go | 8 +++---- excelize.go | 2 +- excelize_test.go | 14 ++++++------ go.mod | 6 ++--- go.sum | 12 +++++----- sparkline.go | 6 ++--- stream.go | 8 +++---- stream_test.go | 57 ++++++++++++++++++++++++++++++++++++++++++++++-- 8 files changed, 82 insertions(+), 31 deletions(-) diff --git a/chart_test.go b/chart_test.go index 969b57fc7c..0d870add41 100644 --- a/chart_test.go +++ b/chart_test.go @@ -409,7 +409,7 @@ func TestDeleteChart(t *testing.T) { } func TestChartWithLogarithmicBase(t *testing.T) { - // Create test XLSX file with data + // Create test workbook with data f := NewFile() sheet1 := f.GetSheetName(0) categories := map[string]float64{ @@ -454,14 +454,14 @@ func TestChartWithLogarithmicBase(t *testing.T) { assert.NoError(t, f.AddChart(sheet1, c.cell, c.opts)) } - // Export XLSX file for human confirmation + // Export workbook for human confirmation assert.NoError(t, f.SaveAs(filepath.Join("test", "TestChartWithLogarithmicBase10.xlsx"))) - // Write the XLSX file to a buffer + // Write the workbook to a buffer var buffer bytes.Buffer assert.NoError(t, f.Write(&buffer)) - // Read back the XLSX file from the buffer + // Read back the workbook from the buffer newFile, err := OpenReader(&buffer) assert.NoError(t, err) diff --git a/excelize.go b/excelize.go index e0959aa646..c46984f505 100644 --- a/excelize.go +++ b/excelize.go @@ -228,7 +228,7 @@ func (f *File) getOptions(opts ...Options) *Options { } // CharsetTranscoder Set user defined codepage transcoder function for open -// XLSX from non UTF-8 encoding. +// workbook from non UTF-8 encoding. func (f *File) CharsetTranscoder(fn charsetTranscoderFn) *File { f.CharsetReader = fn; return f } // Creates new XML decoder with charset reader. diff --git a/excelize_test.go b/excelize_test.go index c441b5e169..b689602a98 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -365,11 +365,11 @@ func TestNewFile(t *testing.T) { f := NewFile() _, err := f.NewSheet("Sheet1") assert.NoError(t, err) - _, err = f.NewSheet("XLSXSheet2") + _, err = f.NewSheet("Sheet2") assert.NoError(t, err) - _, err = f.NewSheet("XLSXSheet3") + _, err = f.NewSheet("Sheet3") assert.NoError(t, err) - assert.NoError(t, f.SetCellInt("XLSXSheet2", "A23", 56)) + assert.NoError(t, f.SetCellInt("Sheet2", "A23", 56)) assert.NoError(t, f.SetCellStr("Sheet1", "B20", "42")) f.SetActiveSheet(0) @@ -962,7 +962,7 @@ func TestSetDeleteSheet(t *testing.T) { f, err := prepareTestBook3() assert.NoError(t, err) - assert.NoError(t, f.DeleteSheet("XLSXSheet3")) + assert.NoError(t, f.DeleteSheet("Sheet3")) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetDeleteSheet.TestBook3.xlsx"))) }) @@ -1610,13 +1610,13 @@ func prepareTestBook1() (*File, error) { func prepareTestBook3() (*File, error) { f := NewFile() - if _, err := f.NewSheet("XLSXSheet2"); err != nil { + if _, err := f.NewSheet("Sheet2"); err != nil { return nil, err } - if _, err := f.NewSheet("XLSXSheet3"); err != nil { + if _, err := f.NewSheet("Sheet3"); err != nil { return nil, err } - if err := f.SetCellInt("XLSXSheet2", "A23", 56); err != nil { + if err := f.SetCellInt("Sheet2", "A23", 56); err != nil { return nil, err } if err := f.SetCellStr("Sheet1", "B20", "42"); err != nil { diff --git a/go.mod b/go.mod index d9879927dd..ee3d8e5ba0 100644 --- a/go.mod +++ b/go.mod @@ -8,10 +8,10 @@ require ( github.com/stretchr/testify v1.8.4 github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 - golang.org/x/crypto v0.22.0 + golang.org/x/crypto v0.23.0 golang.org/x/image v0.14.0 - golang.org/x/net v0.24.0 - golang.org/x/text v0.14.0 + golang.org/x/net v0.25.0 + golang.org/x/text v0.15.0 ) require ( diff --git a/go.sum b/go.sum index a18c395579..cb6250459d 100644 --- a/go.sum +++ b/go.sum @@ -15,14 +15,14 @@ github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7 github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A= github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= -golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= -golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4= golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= -golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/sparkline.go b/sparkline.go index 5d872c3ef9..ec8baf63d4 100644 --- a/sparkline.go +++ b/sparkline.go @@ -359,9 +359,9 @@ var sparklineGroupPresets = []*xlsxX14SparklineGroup{ // AddSparkline provides a function to add sparklines to the worksheet by // 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 -// sparkline. Changes are applied to all three: +// 2010 and later only. You can write them to workbook that can be read by 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.SparklineOptions{ // Location: []string{"A1", "A2", "A3"}, diff --git a/stream.go b/stream.go index e8d9ea649c..189732f387 100644 --- a/stream.go +++ b/stream.go @@ -290,7 +290,7 @@ func (sw *StreamWriter) getRowValues(hRow, hCol, vCol int) (res []string, err er } } -// Check if the token is an XLSX row with the matching row number. +// Check if the token is an worksheet row with the matching row number. func getRowElement(token xml.Token, hRow int) (startElement xml.StartElement, ok bool) { startElement, ok = token.(xml.StartElement) if !ok { @@ -527,7 +527,7 @@ 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) + setCellIntFunc(c, val) case float32: c.T, c.V = setCellFloat(float64(val), -1, 32) case float64: @@ -554,7 +554,7 @@ func (sw *StreamWriter) setCellValFunc(c *xlsxC, val interface{}) error { } // setCellIntFunc is a wrapper of SetCellInt. -func setCellIntFunc(c *xlsxC, val interface{}) (err error) { +func setCellIntFunc(c *xlsxC, val interface{}) { switch val := val.(type) { case int: c.T, c.V = setCellInt(val) @@ -576,9 +576,7 @@ func setCellIntFunc(c *xlsxC, val interface{}) (err error) { c.T, c.V = setCellUint(uint64(val)) case uint64: c.T, c.V = setCellUint(val) - default: } - return } // writeCell constructs a cell XML and writes it to the buffer. diff --git a/stream_test.go b/stream_test.go index da3fd14125..d7c116c84d 100644 --- a/stream_test.go +++ b/stream_test.go @@ -3,6 +3,7 @@ package excelize import ( "encoding/xml" "fmt" + "io" "math/rand" "os" "path/filepath" @@ -224,6 +225,8 @@ func TestStreamTable(t *testing.T) { assert.Equal(t, newCellNameToCoordinatesError("B", newInvalidCellNameError("B")), streamWriter.AddTable(&Table{Range: "A1:B"})) // Test add table with invalid table name assert.Equal(t, newInvalidNameError("1Table"), streamWriter.AddTable(&Table{Range: "A:B1", Name: "1Table"})) + // Test add table with row number exceeds maximum limit + assert.Equal(t, ErrMaxRows, streamWriter.AddTable(&Table{Range: "A1048576:C1048576"})) // Test add table with unsupported charset content types file.ContentTypes = nil file.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) @@ -332,8 +335,7 @@ func TestStreamSetRowWithStyle(t *testing.T) { Cell{StyleID: blueStyleID, Value: "value3"}, &Cell{StyleID: blueStyleID, Value: "value3"}, }, RowOpts{StyleID: grayStyleID})) - err = streamWriter.Flush() - assert.NoError(t, err) + assert.NoError(t, streamWriter.Flush()) ws, err := file.workSheetReader("Sheet1") assert.NoError(t, err) @@ -398,3 +400,54 @@ func TestStreamWriterOutlineLevel(t *testing.T) { } assert.NoError(t, file.Close()) } + +func TestStreamWriterReader(t *testing.T) { + var ( + err error + sw = StreamWriter{ + rawData: bufferedWriter{}, + } + ) + sw.rawData.tmp, err = os.CreateTemp(os.TempDir(), "excelize-") + assert.NoError(t, err) + assert.NoError(t, sw.rawData.tmp.Close()) + // Test reader stat a closed temp file + _, err = sw.rawData.Reader() + assert.Error(t, err) + _, err = sw.getRowValues(1, 1, 1) + assert.Error(t, err) + os.Remove(sw.rawData.tmp.Name()) + + sw = StreamWriter{ + file: NewFile(), + rawData: bufferedWriter{}, + } + // Test getRowValues without expected row + sw.rawData.buf.WriteString("") + _, err = sw.getRowValues(1, 1, 1) + assert.NoError(t, err) + sw.rawData.buf.Reset() + // Test getRowValues with illegal cell reference + sw.rawData.buf.WriteString("") + _, err = sw.getRowValues(1, 1, 1) + assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), err) + sw.rawData.buf.Reset() + // Test getRowValues with invalid c element characters + sw.rawData.buf.WriteString("") + _, err = sw.getRowValues(1, 1, 1) + assert.EqualError(t, err, "XML syntax error on line 1: element closed by ") + sw.rawData.buf.Reset() +} + +func TestStreamWriterGetRowElement(t *testing.T) { + // Test get row element without r attribute + dec := xml.NewDecoder(strings.NewReader("")) + for { + token, err := dec.Token() + if err == io.EOF { + break + } + _, ok := getRowElement(token, 0) + assert.False(t, ok) + } +} From 42ad4d6959b5a4502873cad3328c11936a36e788 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 24 May 2024 22:05:07 +0800 Subject: [PATCH 885/957] This closes #1906, fix a v2.8.1 regression bug introduced by commit d9a0da7b48bac4175a23193a60f973c64d27835f - Fix incorrect cell value written if save multiple times - Update unit tests --- excelize_test.go | 24 ++++++++++++++++++++++++ sheet.go | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/excelize_test.go b/excelize_test.go index b689602a98..6e2e6b0335 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -208,6 +208,30 @@ func TestSaveFile(t *testing.T) { assert.NoError(t, err) assert.NoError(t, f.Save()) assert.NoError(t, f.Close()) + + t.Run("for_save_multiple_times", func(t *testing.T) { + { + f, err := OpenFile(filepath.Join("test", "TestSaveFile.xlsx")) + assert.NoError(t, err) + assert.NoError(t, f.SetCellValue("Sheet1", "A20", 20)) + assert.NoError(t, f.Save()) + + assert.NoError(t, f.SetCellValue("Sheet1", "A21", 21)) + assert.NoError(t, f.Save()) + assert.NoError(t, f.Close()) + } + { + f, err := OpenFile(filepath.Join("test", "TestSaveFile.xlsx")) + assert.NoError(t, err) + val, err := f.GetCellValue("Sheet1", "A20") + assert.NoError(t, err) + assert.Equal(t, "20", val) + val, err = f.GetCellValue("Sheet1", "A21") + assert.NoError(t, err) + assert.Equal(t, "21", val) + assert.NoError(t, f.Close()) + } + }) } func TestSaveAsWrongPath(t *testing.T) { diff --git a/sheet.go b/sheet.go index f9f067a5cb..82c8f76cf0 100644 --- a/sheet.go +++ b/sheet.go @@ -167,7 +167,7 @@ func (f *File) workSheetWriter() { _, ok := f.checked.Load(p.(string)) if ok { f.Sheet.Delete(p.(string)) - f.checked.Store(p.(string), false) + f.checked.Delete(p.(string)) } buffer.Reset() } From 0c3dfb16054d60dd22ce43429fb5f0ddf64b1a4f Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 25 May 2024 14:54:59 +0800 Subject: [PATCH 886/957] This closes #1903, made GetCellStyle function concurrency safe - Update comment of the function and unit test --- cell_test.go | 3 +++ styles.go | 9 ++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/cell_test.go b/cell_test.go index 7517d28f6e..be974d5a3c 100644 --- a/cell_test.go +++ b/cell_test.go @@ -42,6 +42,9 @@ func TestConcurrency(t *testing.T) { assert.NoError(t, err) // Concurrency set cell style assert.NoError(t, f.SetCellStyle("Sheet1", "A3", "A3", style)) + // Concurrency get cell style + _, err = f.GetCellStyle("Sheet1", "A3") + assert.NoError(t, err) // Concurrency add picture assert.NoError(t, f.AddPicture("Sheet1", "F21", filepath.Join("test", "images", "excel.jpg"), &GraphicOptions{ diff --git a/styles.go b/styles.go index ccd758a6c2..7aee0ff575 100644 --- a/styles.go +++ b/styles.go @@ -2186,19 +2186,22 @@ 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 reference. +// name and cell reference. This function is concurrency safe. func (f *File) GetCellStyle(sheet, cell string) (int, error) { + f.mu.Lock() ws, err := f.workSheetReader(sheet) if err != nil { + f.mu.Unlock() return 0, err } + f.mu.Unlock() + ws.mu.Lock() + defer ws.mu.Unlock() col, row, err := CellNameToCoordinates(cell) if err != nil { return 0, 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 } From 08d25006f9aa8474a9d4fdc24fbbddc67aa43064 Mon Sep 17 00:00:00 2001 From: xiaokui <3050446902@qq.com> Date: Mon, 27 May 2024 20:45:37 +0800 Subject: [PATCH 887/957] This fixed can not found code coverage on Windows (#1908) Co-authored-by: Qi Jinkui --- .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 c7eb187d4d..3f100be0e9 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -29,7 +29,7 @@ jobs: run: go build -v . - name: Test - run: env GO111MODULE=on go test -v -timeout 30m -race ./... -coverprofile=coverage.txt -covermode=atomic + run: env GO111MODULE=on go test -v -timeout 30m -race ./... -coverprofile='coverage.txt' -covermode=atomic - name: Codecov uses: codecov/codecov-action@v4 From c349313850653d3e071c4c7d44963fe42ac638d2 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 29 May 2024 21:05:34 +0800 Subject: [PATCH 888/957] This closes #1910, fix a v2.8.1 regression bug introduced by commit 866f3086cd6714ab9114331fb8a73b1b4a1df0a1 - Fix spark lines duplicate when creating spark lines on multiple sheets --- sparkline.go | 676 ++++++++++++++++++++++++++------------------------- 1 file changed, 339 insertions(+), 337 deletions(-) diff --git a/sparkline.go b/sparkline.go index ec8baf63d4..4013a2ab5f 100644 --- a/sparkline.go +++ b/sparkline.go @@ -18,342 +18,344 @@ import ( "strings" ) -// sparklineGroupPresets defined the list of sparkline group to create -// x14:sparklineGroups element by given sparkline style ID. -var sparklineGroupPresets = []*xlsxX14SparklineGroup{ - { - 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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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 +// getSparklineGroupPresets returns the preset list of sparkline group to create +// x14:sparklineGroups element. +func getSparklineGroupPresets() []*xlsxX14SparklineGroup { + return []*xlsxX14SparklineGroup{ + { + 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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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: &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 + } } // AddSparkline provides a function to add sparklines to the worksheet by @@ -412,7 +414,7 @@ func (f *File) AddSparkline(sheet string, opts *SparklineOptions) error { } sparkType = specifiedSparkTypes } - group = sparklineGroupPresets[opts.Style] + group = getSparklineGroupPresets()[opts.Style] group.Type = sparkType group.ColorAxis = &xlsxColor{RGB: "FF000000"} group.DisplayEmptyCellsAs = "gap" From 1a99dd4a233ce170e91cdccb3fedc11e1925b038 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=81=94=E7=9B=9F=E5=B0=91=E4=BE=A0?= Date: Mon, 17 Jun 2024 21:54:15 +0800 Subject: [PATCH 889/957] This closes #1921, fix set axis format doesn't work in combo chart (#1924) - Fix incorrect primary axis titles position --- drawing.go | 233 +++++++++++++++++++++++------------------------------ 1 file changed, 101 insertions(+), 132 deletions(-) diff --git a/drawing.go b/drawing.go index 3c2da06728..49b7fcbc74 100644 --- a/drawing.go +++ b/drawing.go @@ -108,7 +108,7 @@ func (f *File) addChart(opts *Chart, comboCharts []*Chart) { }, } xlsxChartSpace.SpPr = f.drawShapeFill(opts.Fill, xlsxChartSpace.SpPr) - plotAreaFunc := map[ChartType]func(*Chart) *cPlotArea{ + plotAreaFunc := map[ChartType]func(pa *cPlotArea, opts *Chart) *cPlotArea{ Area: f.drawBaseChart, AreaStacked: f.drawBaseChart, AreaPercentStacked: f.drawBaseChart, @@ -179,11 +179,11 @@ func (f *File) addChart(opts *Chart, comboCharts []*Chart) { immutable.FieldByName(mutable.Type().Field(i).Name).Set(field) } } - addChart(xlsxChartSpace.Chart.PlotArea, plotAreaFunc[opts.Type](opts)) + addChart(xlsxChartSpace.Chart.PlotArea, plotAreaFunc[opts.Type](xlsxChartSpace.Chart.PlotArea, opts)) order := len(opts.Series) for idx := range comboCharts { comboCharts[idx].order = order - addChart(xlsxChartSpace.Chart.PlotArea, plotAreaFunc[comboCharts[idx].Type](comboCharts[idx])) + addChart(xlsxChartSpace.Chart.PlotArea, plotAreaFunc[comboCharts[idx].Type](xlsxChartSpace.Chart.PlotArea, comboCharts[idx])) order += len(comboCharts[idx].Series) } chart, _ := xml.Marshal(xlsxChartSpace) @@ -193,7 +193,7 @@ func (f *File) addChart(opts *Chart, comboCharts []*Chart) { // 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 *Chart) *cPlotArea { +func (f *File) drawBaseChart(pa *cPlotArea, opts *Chart) *cPlotArea { c := cCharts{ BarDir: &attrValString{ Val: stringPtr("col"), @@ -217,8 +217,8 @@ func (f *File) drawBaseChart(opts *Chart) *cPlotArea { if *c.Overlap.Val, ok = plotAreaChartOverlap[opts.Type]; !ok { c.Overlap = nil } - catAx := f.drawPlotAreaCatAx(opts) - valAx := f.drawPlotAreaValAx(opts) + catAx := f.drawPlotAreaCatAx(pa, opts) + valAx := f.drawPlotAreaValAx(pa, opts) charts := map[ChartType]*cPlotArea{ Area: { AreaChart: &c, @@ -436,7 +436,7 @@ func (f *File) drawBaseChart(opts *Chart) *cPlotArea { // drawDoughnutChart provides a function to draw the c:plotArea element for // doughnut chart by given format sets. -func (f *File) drawDoughnutChart(opts *Chart) *cPlotArea { +func (f *File) drawDoughnutChart(pa *cPlotArea, opts *Chart) *cPlotArea { holeSize := 75 if opts.HoleSize > 0 && opts.HoleSize <= 90 { holeSize = opts.HoleSize @@ -455,7 +455,7 @@ func (f *File) drawDoughnutChart(opts *Chart) *cPlotArea { // drawLineChart provides a function to draw the c:plotArea element for line // chart by given format sets. -func (f *File) drawLineChart(opts *Chart) *cPlotArea { +func (f *File) drawLineChart(pa *cPlotArea, opts *Chart) *cPlotArea { return &cPlotArea{ LineChart: &cCharts{ Grouping: &attrValString{ @@ -468,14 +468,14 @@ func (f *File) drawLineChart(opts *Chart) *cPlotArea { DLbls: f.drawChartDLbls(opts), AxID: f.genAxID(opts), }, - CatAx: f.drawPlotAreaCatAx(opts), - ValAx: f.drawPlotAreaValAx(opts), + CatAx: f.drawPlotAreaCatAx(pa, opts), + ValAx: f.drawPlotAreaValAx(pa, opts), } } // drawLine3DChart provides a function to draw the c:plotArea element for line // chart by given format sets. -func (f *File) drawLine3DChart(opts *Chart) *cPlotArea { +func (f *File) drawLine3DChart(pa *cPlotArea, opts *Chart) *cPlotArea { return &cPlotArea{ Line3DChart: &cCharts{ Grouping: &attrValString{ @@ -488,14 +488,14 @@ func (f *File) drawLine3DChart(opts *Chart) *cPlotArea { DLbls: f.drawChartDLbls(opts), AxID: f.genAxID(opts), }, - CatAx: f.drawPlotAreaCatAx(opts), - ValAx: f.drawPlotAreaValAx(opts), + CatAx: f.drawPlotAreaCatAx(pa, opts), + ValAx: f.drawPlotAreaValAx(pa, opts), } } // drawPieChart provides a function to draw the c:plotArea element for pie // chart by given format sets. -func (f *File) drawPieChart(opts *Chart) *cPlotArea { +func (f *File) drawPieChart(pa *cPlotArea, opts *Chart) *cPlotArea { return &cPlotArea{ PieChart: &cCharts{ VaryColors: &attrValBool{ @@ -508,7 +508,7 @@ func (f *File) drawPieChart(opts *Chart) *cPlotArea { // drawPie3DChart provides a function to draw the c:plotArea element for 3D // pie chart by given format sets. -func (f *File) drawPie3DChart(opts *Chart) *cPlotArea { +func (f *File) drawPie3DChart(pa *cPlotArea, opts *Chart) *cPlotArea { return &cPlotArea{ Pie3DChart: &cCharts{ VaryColors: &attrValBool{ @@ -521,7 +521,7 @@ 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 { +func (f *File) drawPieOfPieChart(pa *cPlotArea, opts *Chart) *cPlotArea { var splitPos *attrValInt if opts.PlotArea.SecondPlotValues > 0 { splitPos = &attrValInt{Val: intPtr(opts.PlotArea.SecondPlotValues)} @@ -543,7 +543,7 @@ 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 { +func (f *File) drawBarOfPieChart(pa *cPlotArea, opts *Chart) *cPlotArea { var splitPos *attrValInt if opts.PlotArea.SecondPlotValues > 0 { splitPos = &attrValInt{Val: intPtr(opts.PlotArea.SecondPlotValues)} @@ -565,7 +565,7 @@ func (f *File) drawBarOfPieChart(opts *Chart) *cPlotArea { // drawRadarChart provides a function to draw the c:plotArea element for radar // chart by given format sets. -func (f *File) drawRadarChart(opts *Chart) *cPlotArea { +func (f *File) drawRadarChart(pa *cPlotArea, opts *Chart) *cPlotArea { return &cPlotArea{ RadarChart: &cCharts{ RadarStyle: &attrValString{ @@ -578,14 +578,14 @@ func (f *File) drawRadarChart(opts *Chart) *cPlotArea { DLbls: f.drawChartDLbls(opts), AxID: f.genAxID(opts), }, - CatAx: f.drawPlotAreaCatAx(opts), - ValAx: f.drawPlotAreaValAx(opts), + CatAx: f.drawPlotAreaCatAx(pa, opts), + ValAx: f.drawPlotAreaValAx(pa, opts), } } // drawScatterChart provides a function to draw the c:plotArea element for // scatter chart by given format sets. -func (f *File) drawScatterChart(opts *Chart) *cPlotArea { +func (f *File) drawScatterChart(pa *cPlotArea, opts *Chart) *cPlotArea { return &cPlotArea{ ScatterChart: &cCharts{ ScatterStyle: &attrValString{ @@ -598,14 +598,14 @@ func (f *File) drawScatterChart(opts *Chart) *cPlotArea { DLbls: f.drawChartDLbls(opts), AxID: f.genAxID(opts), }, - CatAx: f.drawPlotAreaCatAx(opts), - ValAx: f.drawPlotAreaValAx(opts), + CatAx: f.drawPlotAreaCatAx(pa, opts), + ValAx: f.drawPlotAreaValAx(pa, opts), } } // drawSurface3DChart provides a function to draw the c:surface3DChart element by // given format sets. -func (f *File) drawSurface3DChart(opts *Chart) *cPlotArea { +func (f *File) drawSurface3DChart(pa *cPlotArea, opts *Chart) *cPlotArea { plotArea := &cPlotArea{ Surface3DChart: &cCharts{ Ser: f.drawChartSeries(opts), @@ -615,8 +615,8 @@ func (f *File) drawSurface3DChart(opts *Chart) *cPlotArea { {Val: intPtr(100000005)}, }, }, - CatAx: f.drawPlotAreaCatAx(opts), - ValAx: f.drawPlotAreaValAx(opts), + CatAx: f.drawPlotAreaCatAx(pa, opts), + ValAx: f.drawPlotAreaValAx(pa, opts), SerAx: f.drawPlotAreaSerAx(opts), } if opts.Type == WireframeSurface3D { @@ -627,7 +627,7 @@ func (f *File) drawSurface3DChart(opts *Chart) *cPlotArea { // drawSurfaceChart provides a function to draw the c:surfaceChart element by // given format sets. -func (f *File) drawSurfaceChart(opts *Chart) *cPlotArea { +func (f *File) drawSurfaceChart(pa *cPlotArea, opts *Chart) *cPlotArea { plotArea := &cPlotArea{ SurfaceChart: &cCharts{ Ser: f.drawChartSeries(opts), @@ -637,8 +637,8 @@ func (f *File) drawSurfaceChart(opts *Chart) *cPlotArea { {Val: intPtr(100000005)}, }, }, - CatAx: f.drawPlotAreaCatAx(opts), - ValAx: f.drawPlotAreaValAx(opts), + CatAx: f.drawPlotAreaCatAx(pa, opts), + ValAx: f.drawPlotAreaValAx(pa, opts), SerAx: f.drawPlotAreaSerAx(opts), } if opts.Type == WireframeContour { @@ -649,7 +649,7 @@ func (f *File) drawSurfaceChart(opts *Chart) *cPlotArea { // drawBubbleChart provides a function to draw the c:bubbleChart element by // given format sets. -func (f *File) drawBubbleChart(opts *Chart) *cPlotArea { +func (f *File) drawBubbleChart(pa *cPlotArea, opts *Chart) *cPlotArea { plotArea := &cPlotArea{ BubbleChart: &cCharts{ VaryColors: &attrValBool{ @@ -659,7 +659,7 @@ func (f *File) drawBubbleChart(opts *Chart) *cPlotArea { DLbls: f.drawChartDLbls(opts), AxID: f.genAxID(opts), }, - ValAx: []*cAxs{f.drawPlotAreaCatAx(opts)[0], f.drawPlotAreaValAx(opts)[0]}, + ValAx: []*cAxs{f.drawPlotAreaCatAx(pa, opts)[0], f.drawPlotAreaValAx(pa, opts)[0]}, } if opts.BubbleSize > 0 && opts.BubbleSize <= 300 { plotArea.BubbleChart.BubbleScale = &attrValFloat{Val: float64Ptr(float64(opts.BubbleSize))} @@ -979,7 +979,7 @@ func (f *File) drawChartSeriesDLbls(i int, opts *Chart) *cDLbls { } // drawPlotAreaCatAx provides a function to draw the c:catAx element. -func (f *File) drawPlotAreaCatAx(opts *Chart) []*cAxs { +func (f *File) drawPlotAreaCatAx(pa *cPlotArea, opts *Chart) []*cAxs { maxVal := &attrValFloat{Val: opts.XAxis.Maximum} minVal := &attrValFloat{Val: opts.XAxis.Minimum} if opts.XAxis.Maximum == nil { @@ -988,70 +988,53 @@ func (f *File) drawPlotAreaCatAx(opts *Chart) []*cAxs { if opts.XAxis.Minimum == nil { minVal = nil } - axs := []*cAxs{ - { - AxID: &attrValInt{Val: intPtr(100000000)}, - Scaling: &cScaling{ - Orientation: &attrValString{Val: stringPtr(orientation[opts.XAxis.ReverseOrder])}, - Max: maxVal, - Min: minVal, - }, - 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")}, - Title: f.drawPlotAreaTitles(opts.XAxis.Title, ""), - TickLblPos: &attrValString{Val: stringPtr("nextTo")}, - SpPr: f.drawPlotAreaSpPr(), - TxPr: f.drawPlotAreaTxPr(&opts.YAxis), - CrossAx: &attrValInt{Val: intPtr(100000001)}, - Crosses: &attrValString{Val: stringPtr("autoZero")}, - Auto: &attrValBool{Val: boolPtr(true)}, - LblAlgn: &attrValString{Val: stringPtr("ctr")}, - LblOffset: &attrValInt{Val: intPtr(100)}, - NoMultiLvlLbl: &attrValBool{Val: boolPtr(false)}, - }, + ax := &cAxs{ + AxID: &attrValInt{Val: intPtr(100000000)}, + Scaling: &cScaling{ + Orientation: &attrValString{Val: stringPtr(orientation[opts.XAxis.ReverseOrder])}, + Max: maxVal, + Min: minVal, + }, + 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")}, + Title: f.drawPlotAreaTitles(opts.XAxis.Title, ""), + TickLblPos: &attrValString{Val: stringPtr("nextTo")}, + SpPr: f.drawPlotAreaSpPr(), + TxPr: f.drawPlotAreaTxPr(&opts.XAxis), + CrossAx: &attrValInt{Val: intPtr(100000001)}, + Crosses: &attrValString{Val: stringPtr("autoZero")}, + Auto: &attrValBool{Val: boolPtr(true)}, + LblAlgn: &attrValString{Val: stringPtr("ctr")}, + LblOffset: &attrValInt{Val: intPtr(100)}, + NoMultiLvlLbl: &attrValBool{Val: boolPtr(false)}, } if numFmt := f.drawChartNumFmt(opts.XAxis.NumFmt); numFmt != nil { - axs[0].NumFmt = numFmt + ax.NumFmt = numFmt } if opts.XAxis.MajorGridLines { - axs[0].MajorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()} + ax.MajorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()} } if opts.XAxis.MinorGridLines { - axs[0].MinorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()} + ax.MinorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()} } if opts.XAxis.TickLabelSkip != 0 { - axs[0].TickLblSkip = &attrValInt{Val: intPtr(opts.XAxis.TickLabelSkip)} + ax.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: maxVal, - Min: minVal, - }, - 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)}, - }) + if opts.order > 0 && opts.YAxis.Secondary && pa.CatAx != nil { + ax.AxID = &attrValInt{Val: intPtr(opts.XAxis.axID)} + ax.Delete = &attrValBool{Val: boolPtr(true)} + ax.Crosses = nil + ax.CrossAx = &attrValInt{Val: intPtr(opts.YAxis.axID)} + return []*cAxs{pa.CatAx[0], ax} } - return axs + return []*cAxs{ax} } // drawPlotAreaValAx provides a function to draw the c:valAx element. -func (f *File) drawPlotAreaValAx(opts *Chart) []*cAxs { +func (f *File) drawPlotAreaValAx(pa *cPlotArea, opts *Chart) []*cAxs { maxVal := &attrValFloat{Val: opts.YAxis.Maximum} minVal := &attrValFloat{Val: opts.YAxis.Minimum} if opts.YAxis.Maximum == nil { @@ -1064,67 +1047,53 @@ func (f *File) drawPlotAreaValAx(opts *Chart) []*cAxs { if opts.YAxis.LogBase >= 2 && opts.YAxis.LogBase <= 1000 { logBase = &attrValFloat{Val: float64Ptr(opts.YAxis.LogBase)} } - axs := []*cAxs{ - { - AxID: &attrValInt{Val: intPtr(100000001)}, - Scaling: &cScaling{ - LogBase: logBase, - Orientation: &attrValString{Val: stringPtr(orientation[opts.YAxis.ReverseOrder])}, - Max: maxVal, - Min: minVal, - }, - 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], - }, - 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(100000000)}, - Crosses: &attrValString{Val: stringPtr("autoZero")}, - CrossBetween: &attrValString{Val: stringPtr(chartValAxCrossBetween[opts.Type])}, - }, + ax := &cAxs{ + AxID: &attrValInt{Val: intPtr(100000001)}, + Scaling: &cScaling{ + LogBase: logBase, + Orientation: &attrValString{Val: stringPtr(orientation[opts.YAxis.ReverseOrder])}, + Max: maxVal, + Min: minVal, + }, + 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], + }, + 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(100000000)}, + Crosses: &attrValString{Val: stringPtr("autoZero")}, + CrossBetween: &attrValString{Val: stringPtr(chartValAxCrossBetween[opts.Type])}, } if numFmt := f.drawChartNumFmt(opts.YAxis.NumFmt); numFmt != nil { - axs[0].NumFmt = numFmt + ax.NumFmt = numFmt } if opts.YAxis.MajorGridLines { - axs[0].MajorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()} + ax.MajorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()} } if opts.YAxis.MinorGridLines { - axs[0].MinorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()} + ax.MinorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()} } if pos, ok := valTickLblPos[opts.Type]; ok { - axs[0].TickLblPos.Val = stringPtr(pos) + ax.TickLblPos.Val = stringPtr(pos) } if opts.YAxis.MajorUnit != 0 { - axs[0].MajorUnit = &attrValFloat{Val: float64Ptr(opts.YAxis.MajorUnit)} + ax.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: maxVal, - Min: minVal, - }, - 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])}, - }) + if opts.order > 0 && opts.YAxis.Secondary && pa.ValAx != nil { + ax.AxID = &attrValInt{Val: intPtr(opts.YAxis.axID)} + ax.AxPos = &attrValString{Val: stringPtr("r")} + ax.Title = nil + ax.Crosses = &attrValString{Val: stringPtr("max")} + ax.CrossAx = &attrValInt{Val: intPtr(opts.XAxis.axID)} + return []*cAxs{pa.ValAx[0], ax} } - return axs + return []*cAxs{ax} } // drawPlotAreaSerAx provides a function to draw the c:serAx element. From f04aa8dd31b5bebd49870505390c8af095c720e0 Mon Sep 17 00:00:00 2001 From: wangsongyan <1104237534@qq.com> Date: Wed, 19 Jun 2024 20:45:25 +0800 Subject: [PATCH 890/957] Add new AutoFitIgnoreAspect field in the GraphicOptions data type (#1923) - Support fill the cell with the image and ignore its aspect ratio - Update the unit tests --- picture.go | 7 +++++++ picture_test.go | 5 +++-- xmlDrawing.go | 25 +++++++++++++------------ 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/picture.go b/picture.go index 7b46df80b8..a9a17bcf00 100644 --- a/picture.go +++ b/picture.go @@ -140,6 +140,10 @@ func parseGraphicOptions(opts *GraphicOptions) *GraphicOptions { // The optional parameter "AutoFit" specifies if you make graph object size // auto-fits the cell, the default value of that is 'false'. // +// The optional parameter "AutoFitIgnoreAspect" specifies if fill the cell with +// the image and ignore its aspect ratio, the default value of that is 'false'. +// This option only works when the "AutoFit" is enabled. +// // The optional parameter "OffsetX" specifies the horizontal offset of the graph // object with the cell, the default value of that is 0. // @@ -735,6 +739,9 @@ func (f *File) drawingResize(sheet, cell string, width, height float64, opts *Gr asp := float64(cellHeight) / height height, width = float64(cellHeight), width*asp } + if opts.AutoFitIgnoreAspect { + width, height = float64(cellWidth), float64(cellHeight) + } width, height = width-float64(opts.OffsetX), height-float64(opts.OffsetY) w, h = int(width*opts.ScaleX), int(height*opts.ScaleY) return diff --git a/picture_test.go b/picture_test.go index 23054c1da1..aec0b5c7e1 100644 --- a/picture_test.go +++ b/picture_test.go @@ -48,6 +48,7 @@ func TestAddPicture(t *testing.T) { // Test add picture to worksheet with autofit 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})) + assert.NoError(t, f.AddPicture("Sheet1", "C30", filepath.Join("test", "images", "excel.jpg"), &GraphicOptions{AutoFit: true, AutoFitIgnoreAspect: true})) _, err = f.NewSheet("AddPicture") assert.NoError(t, err) assert.NoError(t, f.SetRowHeight("AddPicture", 10, 30)) @@ -82,7 +83,7 @@ func TestAddPicture(t *testing.T) { // Test get picture cells cells, err := f.GetPictureCells("Sheet1") assert.NoError(t, err) - assert.Equal(t, []string{"F21", "A30", "B30", "Q1", "Q8", "Q15", "Q22", "Q28"}, cells) + assert.Equal(t, []string{"F21", "A30", "B30", "C30", "Q1", "Q8", "Q15", "Q22", "Q28"}, cells) assert.NoError(t, f.Close()) f, err = OpenFile(filepath.Join("test", "TestAddPicture1.xlsx")) @@ -91,7 +92,7 @@ func TestAddPicture(t *testing.T) { f.Drawings.Delete(path) cells, err = f.GetPictureCells("Sheet1") assert.NoError(t, err) - assert.Equal(t, []string{"F21", "A30", "B30", "Q1", "Q8", "Q15", "Q22", "Q28"}, cells) + assert.Equal(t, []string{"F21", "A30", "B30", "C30", "Q1", "Q8", "Q15", "Q22", "Q28"}, cells) // Test get picture cells with unsupported charset f.Drawings.Delete(path) f.Pkg.Store(path, MacintoshCyrillicCharset) diff --git a/xmlDrawing.go b/xmlDrawing.go index 7981166a80..f3dbf0997f 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -417,18 +417,19 @@ type Picture struct { // GraphicOptions directly maps the format settings of the picture. type GraphicOptions struct { - AltText string - PrintObject *bool - Locked *bool - LockAspectRatio bool - AutoFit bool - OffsetX int - OffsetY int - ScaleX float64 - ScaleY float64 - Hyperlink string - HyperlinkType string - Positioning string + AltText string + PrintObject *bool + Locked *bool + LockAspectRatio bool + AutoFit bool + AutoFitIgnoreAspect bool + OffsetX int + OffsetY int + ScaleX float64 + ScaleY float64 + Hyperlink string + HyperlinkType string + Positioning string } // Shape directly maps the format settings of the shape. From 4e6457accde38876ecdf8bb1c6f1e8bce8fd2139 Mon Sep 17 00:00:00 2001 From: Vovka Morkovka Date: Thu, 20 Jun 2024 20:13:12 -0400 Subject: [PATCH 891/957] This closes #1926, fix secondary vertical axis title is not displayed (#1928) --- drawing.go | 1 - 1 file changed, 1 deletion(-) diff --git a/drawing.go b/drawing.go index 49b7fcbc74..bbd75ecbcc 100644 --- a/drawing.go +++ b/drawing.go @@ -1088,7 +1088,6 @@ func (f *File) drawPlotAreaValAx(pa *cPlotArea, opts *Chart) []*cAxs { if opts.order > 0 && opts.YAxis.Secondary && pa.ValAx != nil { ax.AxID = &attrValInt{Val: intPtr(opts.YAxis.axID)} ax.AxPos = &attrValString{Val: stringPtr("r")} - ax.Title = nil ax.Crosses = &attrValString{Val: stringPtr("max")} ax.CrossAx = &attrValInt{Val: intPtr(opts.XAxis.axID)} return []*cAxs{pa.ValAx[0], ax} From b18b48099be3bf4d738bd82e36e4212c69176e50 Mon Sep 17 00:00:00 2001 From: Aybek <100071536+zhayt@users.noreply.github.com> Date: Fri, 5 Jul 2024 11:34:02 +0500 Subject: [PATCH 892/957] Optimize ColumnNumberToName function performance, reduce about 50% memory usage and 50% time cost (#1935) Co-authored-by: zhayt --- lib.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib.go b/lib.go index 420f69a0f5..e01c305862 100644 --- a/lib.go +++ b/lib.go @@ -232,12 +232,18 @@ func ColumnNumberToName(num int) (string, error) { if num < MinColumns || num > MaxColumns { return "", ErrColumnNumber } - var col string + estimatedLength := 0 + for n := num; n > 0; n = (n - 1) / 26 { + estimatedLength++ + } + + result := make([]byte, estimatedLength) for num > 0 { - col = string(rune((num-1)%26+65)) + col + estimatedLength-- + result[estimatedLength] = byte((num-1)%26 + 'A') num = (num - 1) / 26 } - return col, nil + return string(result), nil } // CellNameToCoordinates converts alphanumeric cell name to [X, Y] coordinates From 7999a492a4b7a302ecf31c990d1e1e9ff3146d74 Mon Sep 17 00:00:00 2001 From: ShowerBandV <59394693+ShowerBandV@users.noreply.github.com> Date: Sat, 6 Jul 2024 09:25:09 +0800 Subject: [PATCH 893/957] This closes #1937, fix GetPivotTables returns incorrect data range (#1941) - Add unit test for get pivot table with across worksheet data range, update dependencies package and updated comments of the GetMergeCells function --- go.mod | 8 ++++---- go.sum | 16 ++++++++-------- merge.go | 4 ++-- pivotTable.go | 2 +- pivotTable_test.go | 7 +++++++ 5 files changed, 22 insertions(+), 15 deletions(-) diff --git a/go.mod b/go.mod index ee3d8e5ba0..6e31bbc9c0 100644 --- a/go.mod +++ b/go.mod @@ -8,10 +8,10 @@ require ( github.com/stretchr/testify v1.8.4 github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 - golang.org/x/crypto v0.23.0 - golang.org/x/image v0.14.0 - golang.org/x/net v0.25.0 - golang.org/x/text v0.15.0 + golang.org/x/crypto v0.25.0 + golang.org/x/image v0.18.0 + golang.org/x/net v0.27.0 + golang.org/x/text v0.16.0 ) require ( diff --git a/go.sum b/go.sum index cb6250459d..904edc85c8 100644 --- a/go.sum +++ b/go.sum @@ -15,14 +15,14 @@ github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7 github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A= github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= -golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= -golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4= -golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= -golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= +golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= 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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/merge.go b/merge.go index 2574c6c464..63427bab97 100644 --- a/merge.go +++ b/merge.go @@ -139,8 +139,8 @@ func (f *File) UnmergeCell(sheet, topLeftCell, bottomRightCell string) error { return nil } -// GetMergeCells provides a function to get all merged cells from a worksheet -// currently. +// GetMergeCells provides a function to get all merged cells from a specific +// worksheet. func (f *File) GetMergeCells(sheet string) ([]MergeCell, error) { var mergeCells []MergeCell ws, err := f.workSheetReader(sheet) diff --git a/pivotTable.go b/pivotTable.go index 0b6ad3b729..c3b9ea648c 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -806,7 +806,7 @@ func (f *File) getPivotTable(sheet, pivotTableXML, pivotCacheRels string) (Pivot pivotTableXML: pivotTableXML, pivotCacheXML: pivotCacheXML, pivotSheetName: sheet, - DataRange: fmt.Sprintf("%s!%s", sheet, pc.CacheSource.WorksheetSource.Ref), + DataRange: fmt.Sprintf("%s!%s", pc.CacheSource.WorksheetSource.Sheet, pc.CacheSource.WorksheetSource.Ref), PivotTableRange: fmt.Sprintf("%s!%s", sheet, pt.Location.Ref), Name: pt.Name, } diff --git a/pivotTable_test.go b/pivotTable_test.go index 49bc7d9caa..f3bb2bfd73 100644 --- a/pivotTable_test.go +++ b/pivotTable_test.go @@ -139,6 +139,13 @@ func TestPivotTable(t *testing.T) { ShowColHeaders: true, ShowLastColumn: true, })) + + // Test get pivot table with across worksheet data range + pivotTables, err = f.GetPivotTables("Sheet2") + assert.NoError(t, err) + assert.Len(t, pivotTables, 1) + assert.Equal(t, "Sheet1!A1:E31", pivotTables[0].DataRange) + assert.NoError(t, f.AddPivotTable(&PivotTableOptions{ DataRange: "Sheet1!A1:E31", PivotTableRange: "Sheet2!A20:AR60", From 53b65150ce68a183e2273f8cc5e93978a8c045cf Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 7 Jul 2024 17:22:13 +0800 Subject: [PATCH 894/957] This closes #1940, SetCellHyperLink function now support remove hyperlink by None linkType - Update unit tests --- cell.go | 36 ++++++++++++++++++++++++++++++------ excelize_test.go | 12 ++++++++++++ 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/cell.go b/cell.go index a13313a487..959b4089eb 100644 --- a/cell.go +++ b/cell.go @@ -957,14 +957,36 @@ type HyperlinkOpts struct { Tooltip *string } +// removeHyperLink remove hyperlink for worksheet and delete relationships for +// the worksheet by given sheet name and cell reference. Note that if the cell +// in a range reference, the whole hyperlinks will be deleted. +func (f *File) removeHyperLink(ws *xlsxWorksheet, sheet, cell string) error { + for idx := 0; idx < len(ws.Hyperlinks.Hyperlink); idx++ { + link := ws.Hyperlinks.Hyperlink[idx] + ok, err := f.checkCellInRangeRef(cell, link.Ref) + if err != nil { + return err + } + if link.Ref == cell || ok { + ws.Hyperlinks.Hyperlink = append(ws.Hyperlinks.Hyperlink[:idx], ws.Hyperlinks.Hyperlink[idx+1:]...) + idx-- + f.deleteSheetRelationships(sheet, link.RID) + } + } + if len(ws.Hyperlinks.Hyperlink) == 0 { + ws.Hyperlinks = nil + } + return nil +} + // SetCellHyperLink provides a function to set cell hyperlink by given -// worksheet name and link URL address. LinkType defines two types of +// worksheet name and link URL address. LinkType defines three 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. 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. +// this workbook or "None" for remove hyperlink. 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. // // display, tooltip := "https://github.com/xuri/excelize", "Excelize on GitHub" // if err := f.SetCellHyperLink("Sheet1", "A3", @@ -1032,6 +1054,8 @@ func (f *File) SetCellHyperLink(sheet, cell, link, linkType string, opts ...Hype Ref: cell, Location: link, } + case "None": + return f.removeHyperLink(ws, sheet, cell) default: return newInvalidLinkTypeError(linkType) } diff --git a/excelize_test.go b/excelize_test.go index 6e2e6b0335..7eb689fd21 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -455,6 +455,18 @@ func TestSetCellHyperLink(t *testing.T) { assert.Equal(t, link, true) assert.Equal(t, "https://github.com/xuri/excelize", target) assert.NoError(t, err) + + // Test remove hyperlink for a cell + f = NewFile() + assert.NoError(t, f.SetCellHyperLink("Sheet1", "A1", "Sheet1!D8", "Location")) + ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml") + assert.True(t, ok) + ws.(*xlsxWorksheet).Hyperlinks.Hyperlink[0].Ref = "A1:D4" + assert.NoError(t, f.SetCellHyperLink("Sheet1", "B2", "", "None")) + // Test remove hyperlink for a cell with invalid cell reference + assert.NoError(t, f.SetCellHyperLink("Sheet1", "A1", "Sheet1!D8", "Location")) + ws.(*xlsxWorksheet).Hyperlinks.Hyperlink[0].Ref = "A:A" + assert.Error(t, f.SetCellHyperLink("Sheet1", "B2", "", "None"), newCellNameToCoordinatesError("A", newInvalidCellNameError("A"))) } func TestGetCellHyperLink(t *testing.T) { From 431c31029e28a2acb27759acaffb2de285d063f8 Mon Sep 17 00:00:00 2001 From: Patrick Wang Date: Thu, 11 Jul 2024 14:39:16 +0800 Subject: [PATCH 895/957] This closes #1944, add new TickLabelPosition field in the ChartAxis data type (#1946) - Introduce new exported ChartTickLabelPositionType enumeration - Update unit tests --- chart.go | 21 ++++++++++++++++++++- chart_test.go | 2 +- drawing.go | 8 ++++---- xmlChart.go | 29 +++++++++++++++-------------- 4 files changed, 40 insertions(+), 20 deletions(-) diff --git a/chart.go b/chart.go index a1078f7025..b6d450476d 100644 --- a/chart.go +++ b/chart.go @@ -90,6 +90,19 @@ const ( ChartLineAutomatic ) +// ChartTickLabelPositionType is the type of supported chart tick label position +// types. +type ChartTickLabelPositionType byte + +// This section defines the supported chart tick label position types +// enumeration. +const ( + ChartTickLabelNextToAxis ChartTickLabelPositionType = iota + ChartTickLabelHigh + ChartTickLabelLow + ChartTickLabelNone +) + // This section defines the default value of chart properties. var ( chartView3DRotX = map[ChartType]int{ @@ -484,7 +497,13 @@ var ( true: "r", false: "l", } - valTickLblPos = map[ChartType]string{ + tickLblPosVal = map[ChartTickLabelPositionType]string{ + ChartTickLabelNextToAxis: "nextTo", + ChartTickLabelHigh: "high", + ChartTickLabelLow: "low", + ChartTickLabelNone: "none", + } + tickLblPosNone = map[ChartType]string{ Contour: "none", WireframeContour: "none", } diff --git a/chart_test.go b/chart_test.go index 0d870add41..c847978581 100644 --- a/chart_test.go +++ b/chart_test.go @@ -237,7 +237,7 @@ func TestAddChart(t *testing.T) { {sheetName: "Sheet2", cell: "P1", opts: &Chart{Type: Line3D, Series: series2, Format: format, Legend: ChartLegend{Position: "top", ShowLegendKey: false}, Title: []RichTextRun{{Text: "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: []RichTextRun{{Text: "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: []RichTextRun{{Text: "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: []RichTextRun{{Text: "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: "X16", opts: &Chart{Type: Line, Series: series2, Format: format, Legend: ChartLegend{Position: "top", ShowLegendKey: false}, Title: []RichTextRun{{Text: "Line Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, TickLabelSkip: 1, TickLabelPosition: ChartTickLabelLow}, 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: []RichTextRun{{Text: "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: []RichTextRun{{Text: "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 diff --git a/drawing.go b/drawing.go index bbd75ecbcc..95d5366fbc 100644 --- a/drawing.go +++ b/drawing.go @@ -1001,7 +1001,7 @@ func (f *File) drawPlotAreaCatAx(pa *cPlotArea, opts *Chart) []*cAxs { MajorTickMark: &attrValString{Val: stringPtr("none")}, MinorTickMark: &attrValString{Val: stringPtr("none")}, Title: f.drawPlotAreaTitles(opts.XAxis.Title, ""), - TickLblPos: &attrValString{Val: stringPtr("nextTo")}, + TickLblPos: &attrValString{Val: stringPtr(tickLblPosVal[opts.XAxis.TickLabelPosition])}, SpPr: f.drawPlotAreaSpPr(), TxPr: f.drawPlotAreaTxPr(&opts.XAxis), CrossAx: &attrValInt{Val: intPtr(100000001)}, @@ -1063,7 +1063,7 @@ func (f *File) drawPlotAreaValAx(pa *cPlotArea, opts *Chart) []*cAxs { }, MajorTickMark: &attrValString{Val: stringPtr("none")}, MinorTickMark: &attrValString{Val: stringPtr("none")}, - TickLblPos: &attrValString{Val: stringPtr("nextTo")}, + TickLblPos: &attrValString{Val: stringPtr(tickLblPosVal[opts.YAxis.TickLabelPosition])}, SpPr: f.drawPlotAreaSpPr(), TxPr: f.drawPlotAreaTxPr(&opts.YAxis), CrossAx: &attrValInt{Val: intPtr(100000000)}, @@ -1079,7 +1079,7 @@ func (f *File) drawPlotAreaValAx(pa *cPlotArea, opts *Chart) []*cAxs { if opts.YAxis.MinorGridLines { ax.MinorGridlines = &cChartLines{SpPr: f.drawPlotAreaSpPr()} } - if pos, ok := valTickLblPos[opts.Type]; ok { + if pos, ok := tickLblPosNone[opts.Type]; ok { ax.TickLblPos.Val = stringPtr(pos) } if opts.YAxis.MajorUnit != 0 { @@ -1115,7 +1115,7 @@ func (f *File) drawPlotAreaSerAx(opts *Chart) []*cAxs { }, Delete: &attrValBool{Val: boolPtr(opts.YAxis.None)}, AxPos: &attrValString{Val: stringPtr(catAxPos[opts.XAxis.ReverseOrder])}, - TickLblPos: &attrValString{Val: stringPtr("nextTo")}, + TickLblPos: &attrValString{Val: stringPtr(tickLblPosVal[opts.YAxis.TickLabelPosition])}, SpPr: f.drawPlotAreaSpPr(), TxPr: f.drawPlotAreaTxPr(nil), CrossAx: &attrValInt{Val: intPtr(100000001)}, diff --git a/xmlChart.go b/xmlChart.go index c5d601e0eb..273edacb0f 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -530,20 +530,21 @@ type ChartNumFmt struct { // 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 - Secondary bool - Maximum *float64 - Minimum *float64 - Font Font - LogBase float64 - NumFmt ChartNumFmt - Title []RichTextRun - axID int + None bool + MajorGridLines bool + MinorGridLines bool + MajorUnit float64 + TickLabelPosition ChartTickLabelPositionType + 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 307e5330619eba7b51c4abdd63e6f458bfb70bd6 Mon Sep 17 00:00:00 2001 From: wxy <2498871854@qq.com> Date: Fri, 12 Jul 2024 08:07:19 +0800 Subject: [PATCH 896/957] This closes #1942, fix percent sign missing in formatted result for zero numeric cell value (#1947) - Updated unit tests --- numfmt.go | 32 +++++++++++--------------------- numfmt_test.go | 5 ++++- 2 files changed, 15 insertions(+), 22 deletions(-) diff --git a/numfmt.go b/numfmt.go index 47f2d4a56f..319229f772 100644 --- a/numfmt.go +++ b/numfmt.go @@ -4799,10 +4799,8 @@ func format(value, numFmt string, date1904 bool, cellType CellType, opts *Option switch section.Type { case nfp.TokenSectionPositive: return nf.alignmentHandler(nf.positiveHandler()) - case nfp.TokenSectionNegative: - return nf.alignmentHandler(nf.negativeHandler()) default: - return nf.alignmentHandler(nf.zeroHandler()) + return nf.alignmentHandler(nf.negativeHandler()) } } return nf.alignmentHandler(nf.textHandler()) @@ -7108,11 +7106,6 @@ func (nf *numberFormat) negativeHandler() (result string) { return nf.numberHandler() } -// zeroHandler will be handling zero selection for a number format expression. -func (nf *numberFormat) zeroHandler() string { - return nf.value -} - // 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 { @@ -7137,21 +7130,18 @@ func (nf *numberFormat) getValueSectionType(value string) (float64, string) { return 0, nfp.TokenSectionText } number, _ := strconv.ParseFloat(value, 64) - if number > 0 { + if number >= 0 { 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 + var hasNeg bool + for _, sec := range nf.section { + if sec.Type == nfp.TokenSectionNegative { + hasNeg = true } - return number, nfp.TokenSectionNegative } - return number, nfp.TokenSectionZero + if !hasNeg { + nf.usePositive = true + return number, nfp.TokenSectionPositive + } + return number, nfp.TokenSectionNegative } diff --git a/numfmt_test.go b/numfmt_test.go index 49e4fa03be..9e59fb256d 100644 --- a/numfmt_test.go +++ b/numfmt_test.go @@ -69,7 +69,10 @@ func TestNumFmt(t *testing.T) { {"0.97952546296296295", "h:m", "23:30"}, {"43528", "mmmm", "March"}, {"43528", "dddd", "Monday"}, - {"0", ";;;", "0"}, + {"0", ";;;", ""}, + {"0", "0%", "0%"}, + {"0", "0.0%", "0.0%"}, + {"0", "0.00%", "0.00%"}, {"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"}, From 9c278365f2137e0528fec6c4ab98ca53c2e8041b Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 13 Jul 2024 10:41:57 +0800 Subject: [PATCH 897/957] This closes #1945, an error will be return if column header cell is empty in pivot table data range - Update unit tests --- pivotTable.go | 9 +++++++-- pivotTable_test.go | 20 ++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/pivotTable.go b/pivotTable.go index c3b9ea648c..7ed79ada03 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -260,6 +260,9 @@ func (f *File) getTableFieldsOrder(opts *PivotTableOptions) ([]string, error) { if err != nil { return order, err } + if name == "" { + return order, ErrParameterInvalid + } order = append(order, name) } return order, nil @@ -272,8 +275,10 @@ func (f *File) addPivotCache(opts *PivotTableOptions) error { if err != nil { return newPivotTableDataRangeError(err.Error()) } - // data range has been checked - order, _ := f.getTableFieldsOrder(opts) + order, err := f.getTableFieldsOrder(opts) + if err != nil { + return newPivotTableDataRangeError(err.Error()) + } topLeftCell, _ := CoordinatesToCellName(coordinates[0], coordinates[1]) bottomRightCell, _ := CoordinatesToCellName(coordinates[2], coordinates[3]) pc := xlsxPivotCacheDefinition{ diff --git a/pivotTable_test.go b/pivotTable_test.go index f3bb2bfd73..133301d09e 100644 --- a/pivotTable_test.go +++ b/pivotTable_test.go @@ -300,6 +300,7 @@ func TestPivotTable(t *testing.T) { assert.EqualError(t, err, `parameter 'DataRange' parsing error: parameter is required`) // Test add pivot table with unsupported charset content types. f = NewFile() + assert.NoError(t, f.SetSheetRow("Sheet1", "A1", &[]string{"Month", "Year", "Type", "Sales", "Region"})) f.ContentTypes = nil f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{ @@ -393,6 +394,25 @@ func TestPivotTableDataRange(t *testing.T) { f.Relationships.Delete("xl/worksheets/_rels/sheet1.xml.rels") f.Pkg.Delete("xl/worksheets/_rels/sheet1.xml.rels") assert.EqualError(t, f.DeletePivotTable("Sheet1", "PivotTable1"), "table PivotTable1 does not exist") + + t.Run("data_range_with_empty_column", func(t *testing.T) { + // Test add pivot table with data range doesn't organized as a list with labeled columns + f := NewFile() + // Create some data in a sheet + month := []string{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"} + types := []string{"Meat", "Dairy", "Beverages", "Produce"} + assert.NoError(t, f.SetSheetRow("Sheet1", "A1", &[]string{"Month", "", "Type"})) + 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("C%d", row), types[rand.Intn(4)])) + } + assert.Equal(t, newPivotTableDataRangeError("parameter is invalid"), f.AddPivotTable(&PivotTableOptions{ + DataRange: "Sheet1!A1:E31", + PivotTableRange: "Sheet1!G2:M34", + Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}}, + Data: []PivotTableField{{Data: "Type"}}, + })) + }) } func TestParseFormatPivotTableSet(t *testing.T) { From 68a1704900e099e01377e29a5037b21d2a9ac036 Mon Sep 17 00:00:00 2001 From: pjh591029530 <52460532+pjh591029530@users.noreply.github.com> Date: Wed, 17 Jul 2024 08:44:16 +0800 Subject: [PATCH 898/957] This fix missing horizontal axis in scatter chart with negative values (#1953) Co-authored-by: Simmons <1815481@qq.com> --- drawing.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/drawing.go b/drawing.go index 95d5366fbc..545a22f96d 100644 --- a/drawing.go +++ b/drawing.go @@ -598,8 +598,7 @@ func (f *File) drawScatterChart(pa *cPlotArea, opts *Chart) *cPlotArea { DLbls: f.drawChartDLbls(opts), AxID: f.genAxID(opts), }, - CatAx: f.drawPlotAreaCatAx(pa, opts), - ValAx: f.drawPlotAreaValAx(pa, opts), + ValAx: append(f.drawPlotAreaCatAx(pa, opts), f.drawPlotAreaValAx(pa, opts)...), } } @@ -659,7 +658,7 @@ func (f *File) drawBubbleChart(pa *cPlotArea, opts *Chart) *cPlotArea { DLbls: f.drawChartDLbls(opts), AxID: f.genAxID(opts), }, - ValAx: []*cAxs{f.drawPlotAreaCatAx(pa, opts)[0], f.drawPlotAreaValAx(pa, opts)[0]}, + ValAx: append(f.drawPlotAreaCatAx(pa, opts), f.drawPlotAreaValAx(pa, opts)...), } if opts.BubbleSize > 0 && opts.BubbleSize <= 300 { plotArea.BubbleChart.BubbleScale = &attrValFloat{Val: float64Ptr(float64(opts.BubbleSize))} From 4dd34477f7e0d726c13660b03a9a1da421f05cd3 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 18 Jul 2024 21:05:36 +0800 Subject: [PATCH 899/957] This closes #1955, refs #119, support to set cell value with an IEEE 754 "not-a-number" value or infinity --- cell.go | 17 ++++++++++++----- cell_test.go | 13 +++++++++++++ stream.go | 4 ++-- stream_test.go | 5 ++++- 4 files changed, 31 insertions(+), 8 deletions(-) diff --git a/cell.go b/cell.go index 959b4089eb..95caea3106 100644 --- a/cell.go +++ b/cell.go @@ -15,6 +15,7 @@ import ( "bytes" "encoding/xml" "fmt" + "math" "os" "reflect" "strconv" @@ -385,6 +386,9 @@ 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 { + if math.IsNaN(value) || math.IsInf(value, 0) { + return f.SetCellStr(sheet, cell, fmt.Sprint(value)) + } f.mu.Lock() ws, err := f.workSheetReader(sheet) if err != nil { @@ -399,16 +403,19 @@ func (f *File) SetCellFloat(sheet, cell string, value float64, precision, bitSiz return err } c.S = ws.prepareCellStyle(col, row, c.S) - c.T, c.V = setCellFloat(value, precision, bitSize) - c.IS = nil + c.setCellFloat(value, precision, bitSize) return f.removeFormula(c, ws, sheet) } // setCellFloat prepares cell type and string type cell value by a given float // value. -func setCellFloat(value float64, precision, bitSize int) (t string, v string) { - v = strconv.FormatFloat(value, 'f', precision, bitSize) - return +func (c *xlsxC) setCellFloat(value float64, precision, bitSize int) { + if math.IsNaN(value) || math.IsInf(value, 0) { + c.setInlineStr(fmt.Sprint(value)) + return + } + c.T, c.V = "", strconv.FormatFloat(value, 'f', precision, bitSize) + c.IS = nil } // SetCellStr provides a function to set string type value of a cell. Total diff --git a/cell_test.go b/cell_test.go index be974d5a3c..e64e46423b 100644 --- a/cell_test.go +++ b/cell_test.go @@ -292,6 +292,19 @@ func TestSetCellValue(t *testing.T) { val, err = f.GetCellValue("Sheet1", "B1") assert.NoError(t, err) assert.Equal(t, "b", val) + + f = NewFile() + // Test set cell value with an IEEE 754 "not-a-number" value or infinity + for num, expected := range map[float64]string{ + math.NaN(): "NaN", + math.Inf(0): "+Inf", + math.Inf(-1): "-Inf", + } { + assert.NoError(t, f.SetCellValue("Sheet1", "A1", num)) + val, err := f.GetCellValue("Sheet1", "A1") + assert.NoError(t, err) + assert.Equal(t, expected, val) + } } func TestSetCellValues(t *testing.T) { diff --git a/stream.go b/stream.go index 189732f387..125fb4b6d0 100644 --- a/stream.go +++ b/stream.go @@ -529,9 +529,9 @@ func (sw *StreamWriter) setCellValFunc(c *xlsxC, val interface{}) error { case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: setCellIntFunc(c, val) case float32: - c.T, c.V = setCellFloat(float64(val), -1, 32) + c.setCellFloat(float64(val), -1, 32) case float64: - c.T, c.V = setCellFloat(val, -1, 64) + c.setCellFloat(val, -1, 64) case string: c.setCellValue(val) case []byte: diff --git a/stream_test.go b/stream_test.go index d7c116c84d..61387fe2e1 100644 --- a/stream_test.go +++ b/stream_test.go @@ -4,6 +4,7 @@ import ( "encoding/xml" "fmt" "io" + "math" "math/rand" "os" "path/filepath" @@ -76,6 +77,8 @@ func TestStreamWriter(t *testing.T) { assert.NoError(t, streamWriter.SetRow("A7", nil, RowOpts{Height: 20, Hidden: true, StyleID: styleID})) assert.Equal(t, ErrMaxRowHeight, streamWriter.SetRow("A8", nil, RowOpts{Height: MaxRowHeight + 1})) + assert.NoError(t, streamWriter.SetRow("A9", []interface{}{math.NaN(), math.Inf(0), math.Inf(-1)})) + for rowID := 10; rowID <= 51200; rowID++ { row := make([]interface{}, 50) for colID := 0; colID < 50; colID++ { @@ -145,7 +148,7 @@ func TestStreamWriter(t *testing.T) { cells += len(row) } assert.NoError(t, rows.Close()) - assert.Equal(t, 2559559, cells) + assert.Equal(t, 2559562, cells) // Save spreadsheet with password. assert.NoError(t, file.SaveAs(filepath.Join("test", "EncryptionTestStreamWriter.xlsx"), Options{Password: "password"})) assert.NoError(t, file.Close()) From d81b4c8661b0b30d8b8e7996fb4d6c5dbc2b07ca Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 19 Jul 2024 22:22:47 +0800 Subject: [PATCH 900/957] This closes #1957, fix missing shape macro missing after adjusted drawing object --- xmlDecodeDrawing.go | 18 ++++++++---------- xmlDrawing.go | 17 ++++++++++++++++- xmlPivotTable.go | 14 +++++++------- 3 files changed, 31 insertions(+), 18 deletions(-) diff --git a/xmlDecodeDrawing.go b/xmlDecodeDrawing.go index 8cd7625fe7..5c900fc373 100644 --- a/xmlDecodeDrawing.go +++ b/xmlDecodeDrawing.go @@ -36,7 +36,7 @@ type decodeCellAnchorPos struct { To *xlsxTo `xml:"to"` Pos *xlsxInnerXML `xml:"pos"` Ext *xlsxInnerXML `xml:"ext"` - Sp *xlsxInnerXML `xml:"sp"` + Sp *xlsxSp `xml:"sp"` GrpSp *xlsxInnerXML `xml:"grpSp"` GraphicFrame *xlsxInnerXML `xml:"graphicFrame"` CxnSp *xlsxInnerXML `xml:"cxnSp"` @@ -46,16 +46,14 @@ type decodeCellAnchorPos struct { ClientData *xlsxInnerXML `xml:"clientData"` } -// xdrSp (Shape) directly maps the sp element. This element specifies the -// existence of a single shape. A shape can either be a preset or a custom -// geometry, defined using the SpreadsheetDrawingML framework. In addition to -// a geometry each shape can have both visual and non-visual properties -// attached. Text and corresponding styling information can also be attached -// to a shape. This shape is specified along with all other shapes within -// either the shape tree or group shape elements. +// decodeSp defines the structure used to deserialize the sp element. type decodeSp struct { - NvSpPr *decodeNvSpPr `xml:"nvSpPr"` - SpPr *decodeSpPr `xml:"spPr"` + Macro string `xml:"macro,attr,omitempty"` + TextLink string `xml:"textlink,attr,omitempty"` + FLocksText bool `xml:"fLocksText,attr,omitempty"` + FPublished *bool `xml:"fPublished,attr"` + NvSpPr *decodeNvSpPr `xml:"nvSpPr"` + SpPr *decodeSpPr `xml:"spPr"` } // decodeSp (Non-Visual Properties for a Shape) directly maps the nvSpPr diff --git a/xmlDrawing.go b/xmlDrawing.go index f3dbf0997f..3d39d35d94 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -238,7 +238,7 @@ type xlsxCellAnchorPos struct { To *xlsxTo `xml:"xdr:to"` Pos *xlsxInnerXML `xml:"xdr:pos"` Ext *xlsxInnerXML `xml:"xdr:ext"` - Sp *xlsxInnerXML `xml:"xdr:sp"` + Sp *xlsxSp `xml:"xdr:sp"` GrpSp *xlsxInnerXML `xml:"xdr:grpSp"` GraphicFrame *xlsxInnerXML `xml:"xdr:graphicFrame"` CxnSp *xlsxInnerXML `xml:"xdr:cxnSp"` @@ -248,6 +248,21 @@ type xlsxCellAnchorPos struct { ClientData *xlsxInnerXML `xml:"xdr:clientData"` } +// xdrSp (Shape) directly maps the sp element. This element specifies the +// existence of a single shape. A shape can either be a preset or a custom +// geometry, defined using the SpreadsheetDrawingML framework. In addition to +// a geometry each shape can have both visual and non-visual properties +// attached. Text and corresponding styling information can also be attached +// to a shape. This shape is specified along with all other shapes within +// either the shape tree or group shape elements. +type xlsxSp struct { + Macro string `xml:"macro,attr,omitempty"` + TextLink string `xml:"textlink,attr,omitempty"` + FLocksText bool `xml:"fLocksText,attr,omitempty"` + FPublished *bool `xml:"fPublished,attr"` + Content string `xml:",innerxml"` +} + // xlsxPoint2D describes the position of a drawing element within a spreadsheet. type xlsxPoint2D struct { XMLName xml.Name `xml:"xdr:pos"` diff --git a/xmlPivotTable.go b/xmlPivotTable.go index 41405c3e85..fd45ca84eb 100644 --- a/xmlPivotTable.go +++ b/xmlPivotTable.go @@ -56,15 +56,15 @@ type xlsxPivotTableDefinition struct { EnableDrill bool `xml:"enableDrill,attr,omitempty"` EnableFieldProperties bool `xml:"enableFieldProperties,attr,omitempty"` PreserveFormatting bool `xml:"preserveFormatting,attr,omitempty"` - UseAutoFormatting *bool `xml:"useAutoFormatting,attr,omitempty"` + UseAutoFormatting *bool `xml:"useAutoFormatting,attr"` PageWrap int `xml:"pageWrap,attr,omitempty"` - PageOverThenDown *bool `xml:"pageOverThenDown,attr,omitempty"` + PageOverThenDown *bool `xml:"pageOverThenDown,attr"` SubtotalHiddenItems bool `xml:"subtotalHiddenItems,attr,omitempty"` - RowGrandTotals *bool `xml:"rowGrandTotals,attr,omitempty"` - ColGrandTotals *bool `xml:"colGrandTotals,attr,omitempty"` + RowGrandTotals *bool `xml:"rowGrandTotals,attr"` + ColGrandTotals *bool `xml:"colGrandTotals,attr"` FieldPrintTitles bool `xml:"fieldPrintTitles,attr,omitempty"` ItemPrintTitles bool `xml:"itemPrintTitles,attr,omitempty"` - MergeItem *bool `xml:"mergeItem,attr,omitempty"` + MergeItem *bool `xml:"mergeItem,attr"` ShowDropZones bool `xml:"showDropZones,attr,omitempty"` CreatedVersion int `xml:"createdVersion,attr,omitempty"` Indent int `xml:"indent,attr,omitempty"` @@ -74,7 +74,7 @@ type xlsxPivotTableDefinition struct { Compact *bool `xml:"compact,attr"` Outline *bool `xml:"outline,attr"` OutlineData bool `xml:"outlineData,attr,omitempty"` - CompactData *bool `xml:"compactData,attr,omitempty"` + CompactData *bool `xml:"compactData,attr"` Published bool `xml:"published,attr,omitempty"` GridDropZones bool `xml:"gridDropZones,attr,omitempty"` Immersive bool `xml:"immersive,attr,omitempty"` @@ -150,7 +150,7 @@ type xlsxPivotField struct { DataSourceSort bool `xml:"dataSourceSort,attr,omitempty"` NonAutoSortDefault bool `xml:"nonAutoSortDefault,attr,omitempty"` RankBy int `xml:"rankBy,attr,omitempty"` - DefaultSubtotal *bool `xml:"defaultSubtotal,attr,omitempty"` + DefaultSubtotal *bool `xml:"defaultSubtotal,attr"` SumSubtotal bool `xml:"sumSubtotal,attr,omitempty"` CountASubtotal bool `xml:"countASubtotal,attr,omitempty"` AvgSubtotal bool `xml:"avgSubtotal,attr,omitempty"` From 30c4cd70e04ad511dd3fd36f083168dffd06feac Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 23 Jul 2024 21:48:17 +0800 Subject: [PATCH 901/957] This close #1963, prevent the GetStyle function panic when theme without sysClr --- styles.go | 31 +++++++++++++++++++++---------- styles_test.go | 2 ++ xmlTheme.go | 2 +- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/styles.go b/styles.go index 7aee0ff575..64c0bc0768 100644 --- a/styles.go +++ b/styles.go @@ -1376,22 +1376,33 @@ var ( } ) +// colorChoice returns a hex color code from the actual color values. +func (clr *decodeCTColor) colorChoice() *string { + if clr.SrgbClr != nil { + return clr.SrgbClr.Val + } + if clr.SysClr != nil { + return &clr.SysClr.LastClr + } + return nil +} + // GetBaseColor returns the preferred hex color code by giving hex color code, // indexed color, and theme color. func (f *File) GetBaseColor(hexColor string, indexedColor int, themeColor *int) string { if f.Theme != nil && themeColor != nil { clrScheme := f.Theme.ThemeElements.ClrScheme if val, ok := map[int]*string{ - 0: &clrScheme.Lt1.SysClr.LastClr, - 1: &clrScheme.Dk1.SysClr.LastClr, - 2: clrScheme.Lt2.SrgbClr.Val, - 3: clrScheme.Dk2.SrgbClr.Val, - 4: clrScheme.Accent1.SrgbClr.Val, - 5: clrScheme.Accent2.SrgbClr.Val, - 6: clrScheme.Accent3.SrgbClr.Val, - 7: clrScheme.Accent4.SrgbClr.Val, - 8: clrScheme.Accent5.SrgbClr.Val, - 9: clrScheme.Accent6.SrgbClr.Val, + 0: clrScheme.Lt1.colorChoice(), + 1: clrScheme.Dk1.colorChoice(), + 2: clrScheme.Lt2.colorChoice(), + 3: clrScheme.Dk2.colorChoice(), + 4: clrScheme.Accent1.colorChoice(), + 5: clrScheme.Accent2.colorChoice(), + 6: clrScheme.Accent3.colorChoice(), + 7: clrScheme.Accent4.colorChoice(), + 8: clrScheme.Accent5.colorChoice(), + 9: clrScheme.Accent6.colorChoice(), }[*themeColor]; ok && val != nil { return *val } diff --git a/styles_test.go b/styles_test.go index 1c309bcfee..1b5d3a254e 100644 --- a/styles_test.go +++ b/styles_test.go @@ -613,6 +613,8 @@ func TestGetThemeColor(t *testing.T) { assert.Equal(t, "FFFFFF", f.getThemeColor(&xlsxColor{RGB: "FFFFFF"})) assert.Equal(t, "FF8080", f.getThemeColor(&xlsxColor{Indexed: 2, Tint: 0.5})) assert.Empty(t, f.getThemeColor(&xlsxColor{Indexed: len(IndexedColorMapping), Tint: 0.5})) + clr := &decodeCTColor{} + assert.Nil(t, clr.colorChoice()) } func TestGetStyle(t *testing.T) { diff --git a/xmlTheme.go b/xmlTheme.go index ec0c2bda49..6bbabf68a7 100644 --- a/xmlTheme.go +++ b/xmlTheme.go @@ -148,7 +148,7 @@ type xlsxEffectStyleLst struct { EffectStyleLst string `xml:",innerxml"` } -// xlsxBgFillStyleLst element defines a list of background fills that are +// xlsxBgFillStyleLst element defines a list of background fills that are // used within a theme. The background fills consist of three fills, arranged // in order from subtle to moderate to intense. type xlsxBgFillStyleLst struct { From d21b598235769617c4bfa3c4b845d764027ed348 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 31 Jul 2024 09:10:05 +0800 Subject: [PATCH 902/957] This closes #1968, closes #1969 - Fix missing conditional formatting after remove column - Fix the SetSheetVisible function panic on none views sheet - Updated unit tests --- adjust.go | 4 ++-- adjust_test.go | 51 ++++++++++++++++++++++++++++++++++++++++++++++++++ sheet.go | 5 +++++ sheet_test.go | 12 ++++++++++++ 4 files changed, 70 insertions(+), 2 deletions(-) diff --git a/adjust.go b/adjust.go index 583e2b979f..ab97c435b3 100644 --- a/adjust.go +++ b/adjust.go @@ -259,12 +259,12 @@ func (f *File) adjustCellRef(cellRef string, dir adjustDirection, num, offset in return "", err } if dir == columns { - if offset < 0 && coordinates[0] == coordinates[2] { + if offset < 0 && coordinates[0] == coordinates[2] && num == coordinates[0] { continue } coordinates = applyOffset(coordinates, 0, 2, MaxColumns) } else { - if offset < 0 && coordinates[1] == coordinates[3] { + if offset < 0 && coordinates[1] == coordinates[3] && num == coordinates[1] { continue } coordinates = applyOffset(coordinates, 1, 3, TotalRows) diff --git a/adjust_test.go b/adjust_test.go index 4f42e5145d..fccfdf22b1 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -1020,6 +1020,57 @@ func TestAdjustConditionalFormats(t *testing.T) { ws.(*xlsxWorksheet).ConditionalFormatting[0] = nil assert.NoError(t, f.RemoveCol("Sheet1", "B")) + + t.Run("for_remove_conditional_formats_column", func(t *testing.T) { + f := NewFile() + format := []ConditionalFormatOptions{{ + Type: "data_bar", + Criteria: "=", + MinType: "min", + MaxType: "max", + BarColor: "#638EC6", + }} + assert.NoError(t, f.SetConditionalFormat("Sheet1", "D2:D3", format)) + assert.NoError(t, f.SetConditionalFormat("Sheet1", "D5", format)) + assert.NoError(t, f.RemoveCol("Sheet1", "D")) + opts, err := f.GetConditionalFormats("Sheet1") + assert.NoError(t, err) + assert.Len(t, opts, 0) + }) + t.Run("for_remove_conditional_formats_row", func(t *testing.T) { + f := NewFile() + format := []ConditionalFormatOptions{{ + Type: "data_bar", + Criteria: "=", + MinType: "min", + MaxType: "max", + BarColor: "#638EC6", + }} + assert.NoError(t, f.SetConditionalFormat("Sheet1", "D2:E2", format)) + assert.NoError(t, f.SetConditionalFormat("Sheet1", "F2", format)) + assert.NoError(t, f.RemoveRow("Sheet1", 2)) + opts, err := f.GetConditionalFormats("Sheet1") + assert.NoError(t, err) + assert.Len(t, opts, 0) + }) + t.Run("for_adjust_conditional_formats_row", func(t *testing.T) { + f := NewFile() + format := []ConditionalFormatOptions{{ + Type: "data_bar", + Criteria: "=", + MinType: "min", + MaxType: "max", + BarColor: "#638EC6", + }} + assert.NoError(t, f.SetConditionalFormat("Sheet1", "D2:D3", format)) + assert.NoError(t, f.SetConditionalFormat("Sheet1", "D5", format)) + assert.NoError(t, f.RemoveRow("Sheet1", 1)) + opts, err := f.GetConditionalFormats("Sheet1") + assert.NoError(t, err) + assert.Len(t, opts, 2) + assert.Equal(t, format, opts["D1:D2"]) + assert.Equal(t, format, opts["D4:D4"]) + }) } func TestAdjustDataValidations(t *testing.T) { diff --git a/sheet.go b/sheet.go index 82c8f76cf0..ee66b90afe 100644 --- a/sheet.go +++ b/sheet.go @@ -773,6 +773,11 @@ func (f *File) SetSheetVisible(sheet string, visible bool, veryHidden ...bool) e return err } tabSelected := false + if ws.SheetViews == nil { + ws.SheetViews = &xlsxSheetViews{ + SheetView: []xlsxSheetView{{WorkbookViewID: 0}}, + } + } if len(ws.SheetViews.SheetView) > 0 { tabSelected = ws.SheetViews.SheetView[0].TabSelected } diff --git a/sheet_test.go b/sheet_test.go index 5c96efc1d2..1c4f55acf2 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -570,6 +570,18 @@ func TestSetSheetVisible(t *testing.T) { f.WorkBook = nil f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset) assert.EqualError(t, f.SetSheetVisible("Sheet1", false), "XML syntax error on line 1: invalid UTF-8") + + // Test set sheet visible with empty sheet views + f = NewFile() + _, err := f.NewSheet("Sheet2") + assert.NoError(t, err) + ws, ok := f.Sheet.Load("xl/worksheets/sheet2.xml") + assert.True(t, ok) + ws.(*xlsxWorksheet).SheetViews = nil + assert.NoError(t, f.SetSheetVisible("Sheet2", false)) + visible, err := f.GetSheetVisible("Sheet2") + assert.NoError(t, err) + assert.False(t, visible) } func TestGetSheetVisible(t *testing.T) { From 9a386575152d038c9a0509000238bbf1f41afccd Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 18 Aug 2024 00:18:02 +0800 Subject: [PATCH 903/957] This related for #720 and closes #1965, add new NumFmt field in the PivotTableField data type - Support set and get built-in number format of the pivot table data filed cells - Update unit tests - Fixed ineffectual assignment issue --- calc.go | 4 ++-- pivotTable.go | 29 +++++++++++++++++++++++++++-- pivotTable_test.go | 8 ++++---- styles.go | 12 +++++------- xmlPivotTable.go | 2 +- 5 files changed, 39 insertions(+), 16 deletions(-) diff --git a/calc.go b/calc.go index 8777bb9413..193e01fa33 100644 --- a/calc.go +++ b/calc.go @@ -14459,7 +14459,7 @@ func (fn *formulaFuncs) VALUE(argsList *list.List) formulaArg { value, _ := decimal.Float64() return newNumberFormulaArg(value * percent) } - dateValue, timeValue, errTime, errDate := 0.0, 0.0, false, false + dateValue, timeValue, errTime := 0.0, 0.0, false if !isDateOnlyFmt(text) { h, m, s, _, _, err := strToTime(text) errTime = err.Type == ArgError @@ -14468,7 +14468,7 @@ func (fn *formulaFuncs) VALUE(argsList *list.List) formulaArg { } } y, m, d, _, err := strToDate(text) - errDate = err.Type == ArgError + errDate := err.Type == ArgError if !errDate { dateValue = daysBetween(excelMinTime1900.Unix(), makeDate(y, time.Month(m), d)) + 1 } diff --git a/pivotTable.go b/pivotTable.go index 7ed79ada03..490b48716e 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -62,6 +62,10 @@ type PivotTableOptions struct { } // PivotTableField directly maps the field settings of the pivot table. +// +// Name specifies the name of the data field. Maximum 255 characters +// are allowed in data field name, excess characters will be truncated. +// // Subtotal specifies the aggregation function that applies to this data // field. The default value is sum. The possible values for this attribute // are: @@ -78,8 +82,9 @@ type PivotTableOptions struct { // Var // Varp // -// Name specifies the name of the data field. Maximum 255 characters -// are allowed in data field name, excess characters will be truncated. +// NumFmt specifies the number format ID of the data field, this filed only +// accepts built-in number format ID and does not support custom number format +// expression currently. type PivotTableField struct { Compact bool Data string @@ -87,6 +92,7 @@ type PivotTableField struct { Outline bool Subtotal string DefaultSubtotal bool + NumFmt int } // AddPivotTable provides the method to add pivot table by given pivot table @@ -452,6 +458,7 @@ func (f *File) addPivotDataFields(pt *xlsxPivotTableDefinition, opts *PivotTable } dataFieldsSubtotals := f.getPivotTableFieldsSubtotal(opts.Data) dataFieldsName := f.getPivotTableFieldsName(opts.Data) + dataFieldsNumFmtID := f.getPivotTableFieldsNumFmtID(opts.Data) for idx, dataField := range dataFieldsIndex { if pt.DataFields == nil { pt.DataFields = &xlsxDataFields{} @@ -460,6 +467,7 @@ func (f *File) addPivotDataFields(pt *xlsxPivotTableDefinition, opts *PivotTable Name: dataFieldsName[idx], Fld: dataField, Subtotal: dataFieldsSubtotals[idx], + NumFmtID: dataFieldsNumFmtID[idx], }) } @@ -687,6 +695,22 @@ func (f *File) getPivotTableFieldName(name string, fields []PivotTableField) str return "" } +// getPivotTableFieldsNumFmtID prepare fields number format ID by given pivot +// table fields. +func (f *File) getPivotTableFieldsNumFmtID(fields []PivotTableField) []int { + field := make([]int, len(fields)) + for idx, fld := range fields { + if _, ok := builtInNumFmt[fld.NumFmt]; ok { + field[idx] = fld.NumFmt + continue + } + if (27 <= fld.NumFmt && fld.NumFmt <= 36) || (50 <= fld.NumFmt && fld.NumFmt <= 81) { + field[idx] = fld.NumFmt + } + } + return field +} + // getPivotTableFieldOptions return options for specific field by given field name. func (f *File) getPivotTableFieldOptions(name string, fields []PivotTableField) (options PivotTableField, ok bool) { for _, field := range fields { @@ -891,6 +915,7 @@ func (f *File) extractPivotTableFields(order []string, pt *xlsxPivotTableDefinit Data: order[field.Fld], Name: field.Name, Subtotal: cases.Title(language.English).String(field.Subtotal), + NumFmt: field.NumFmtID, }) } } diff --git a/pivotTable_test.go b/pivotTable_test.go index 133301d09e..58b1dbe331 100644 --- a/pivotTable_test.go +++ b/pivotTable_test.go @@ -34,7 +34,7 @@ func TestPivotTable(t *testing.T) { Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, Filter: []PivotTableField{{Data: "Region"}}, Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, - Data: []PivotTableField{{Data: "Sales", Subtotal: "Sum", Name: "Summarize by Sum"}}, + Data: []PivotTableField{{Data: "Sales", Subtotal: "Sum", Name: "Summarize by Sum", NumFmt: 38}}, RowGrandTotals: true, ColGrandTotals: true, ShowDrill: true, @@ -131,7 +131,7 @@ func TestPivotTable(t *testing.T) { PivotTableRange: "Sheet2!A1:AN17", Rows: []PivotTableField{{Data: "Month"}}, Columns: []PivotTableField{{Data: "Region", DefaultSubtotal: true}, {Data: "Type", DefaultSubtotal: true}, {Data: "Year"}}, - Data: []PivotTableField{{Data: "Sales", Subtotal: "Min", Name: "Summarize by Min"}}, + Data: []PivotTableField{{Data: "Sales", Subtotal: "Min", Name: "Summarize by Min", NumFmt: 32}}, RowGrandTotals: true, ColGrandTotals: true, ShowDrill: true, @@ -151,7 +151,7 @@ func TestPivotTable(t *testing.T) { PivotTableRange: "Sheet2!A20:AR60", Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Type"}}, Columns: []PivotTableField{{Data: "Region", DefaultSubtotal: true}, {Data: "Year"}}, - Data: []PivotTableField{{Data: "Sales", Subtotal: "Product", Name: "Summarize by Product"}}, + Data: []PivotTableField{{Data: "Sales", Subtotal: "Product", Name: "Summarize by Product", NumFmt: 32}}, RowGrandTotals: true, ColGrandTotals: true, ShowDrill: true, @@ -171,7 +171,7 @@ func TestPivotTable(t *testing.T) { PivotTableRange: "Sheet2!A65:AJ100", Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, Columns: []PivotTableField{{Data: "Region", DefaultSubtotal: true}, {Data: "Type"}}, - Data: []PivotTableField{{Data: "Sales", Subtotal: "Sum", Name: "Sum of Sales"}, {Data: "Sales", Subtotal: "Average", Name: "Average of Sales"}}, + Data: []PivotTableField{{Data: "Sales", Subtotal: "Sum", Name: "Sum of Sales", NumFmt: -1}, {Data: "Sales", Subtotal: "Average", Name: "Average of Sales", NumFmt: 38}}, RowGrandTotals: true, ColGrandTotals: true, ShowDrill: true, diff --git a/styles.go b/styles.go index 64c0bc0768..34016514bb 100644 --- a/styles.go +++ b/styles.go @@ -1902,27 +1902,25 @@ func (f *File) newFont(style *Style) (*xlsxFont, error) { // getNumFmtID provides a function to get number format code ID. // If given number format code does not exist, will return -1. -func getNumFmtID(styleSheet *xlsxStyleSheet, style *Style) (numFmtID int) { - numFmtID = -1 +func getNumFmtID(styleSheet *xlsxStyleSheet, style *Style) int { + numFmtID := -1 if _, ok := builtInNumFmt[style.NumFmt]; ok { return style.NumFmt } if (27 <= style.NumFmt && style.NumFmt <= 36) || (50 <= style.NumFmt && style.NumFmt <= 81) { - numFmtID = style.NumFmt - return + return style.NumFmt } if fmtCode, ok := currencyNumFmt[style.NumFmt]; ok { numFmtID = style.NumFmt if styleSheet.NumFmts != nil { for _, numFmt := range styleSheet.NumFmts.NumFmt { if numFmt.FormatCode == fmtCode { - numFmtID = numFmt.NumFmtID - return + return numFmt.NumFmtID } } } } - return + return numFmtID } // newNumFmt provides a function to check if number format code in the range diff --git a/xmlPivotTable.go b/xmlPivotTable.go index fd45ca84eb..8937503be0 100644 --- a/xmlPivotTable.go +++ b/xmlPivotTable.go @@ -273,7 +273,7 @@ type xlsxDataField struct { ShowDataAs string `xml:"showDataAs,attr,omitempty"` BaseField int `xml:"baseField,attr,omitempty"` BaseItem int64 `xml:"baseItem,attr,omitempty"` - NumFmtID string `xml:"numFmtId,attr,omitempty"` + NumFmtID int `xml:"numFmtId,attr,omitempty"` ExtLst *xlsxExtLst `xml:"extLst"` } From c805be1f6f16ae2f3cc7c1ed67b1e0465940646e Mon Sep 17 00:00:00 2001 From: zhangyimingdatiancai <2654488395@qq.com> Date: Fri, 23 Aug 2024 10:47:47 +0800 Subject: [PATCH 904/957] This related for #810, add new functions DeleteSlicer and GetSlicers (#1943) - Update unit tests --- errors.go | 6 + pivotTable.go | 29 +++- pivotTable_test.go | 2 + slicer.go | 357 +++++++++++++++++++++++++++++++++++++++++--- slicer_test.go | 249 +++++++++++++++++++++++++++++- table.go | 24 ++- xmlDecodeDrawing.go | 26 +++- xmlSlicers.go | 7 +- 8 files changed, 658 insertions(+), 42 deletions(-) diff --git a/errors.go b/errors.go index b460dfd2da..b12b06cba2 100644 --- a/errors.go +++ b/errors.go @@ -264,6 +264,12 @@ func newInvalidStyleID(styleID int) error { return fmt.Errorf("invalid style ID %d", styleID) } +// newNoExistSlicerError defined the error message on receiving the non existing +// slicer name. +func newNoExistSlicerError(name string) error { + return fmt.Errorf("slicer %s does not exist", name) +} + // newNoExistTableError defined the error message on receiving the non existing // table name. func newNoExistTableError(name string) error { diff --git a/pivotTable.go b/pivotTable.go index 490b48716e..6205c54311 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -785,12 +785,11 @@ func (f *File) getPivotTableDataRange(opts *PivotTableOptions) error { opts.pivotDataRange = opts.DataRange return nil } - for _, sheetName := range f.GetSheetList() { - tables, err := f.GetTables(sheetName) - e := ErrSheetNotExist{sheetName} - if err != nil && err.Error() != newNotWorksheetError(sheetName).Error() && err.Error() != e.Error() { - return err - } + tbls, err := f.getTables() + if err != nil { + return err + } + for sheetName, tables := range tbls { for _, table := range tables { if table.Name == opts.DataRange { opts.pivotDataRange, opts.namedDataRange = fmt.Sprintf("%s!%s", sheetName, table.Range), true @@ -1016,8 +1015,8 @@ func (f *File) DeletePivotTable(sheet, name string) error { return err } pivotTableCaches := map[string]int{} - for _, sheetName := range f.GetSheetList() { - sheetPivotTables, _ := f.GetPivotTables(sheetName) + pivotTables, _ := f.getPivotTables() + for _, sheetPivotTables := range pivotTables { for _, sheetPivotTable := range sheetPivotTables { pivotTableCaches[sheetPivotTable.pivotCacheXML]++ } @@ -1038,3 +1037,17 @@ func (f *File) DeletePivotTable(sheet, name string) error { } return newNoExistTableError(name) } + +// getPivotTables provides a function to get all pivot tables in a workbook. +func (f *File) getPivotTables() (map[string][]PivotTableOptions, error) { + pivotTables := map[string][]PivotTableOptions{} + for _, sheetName := range f.GetSheetList() { + pts, err := f.GetPivotTables(sheetName) + e := ErrSheetNotExist{sheetName} + if err != nil && err.Error() != newNotWorksheetError(sheetName).Error() && err.Error() != e.Error() { + return pivotTables, err + } + pivotTables[sheetName] = append(pivotTables[sheetName], pts...) + } + return pivotTables, nil +} diff --git a/pivotTable_test.go b/pivotTable_test.go index 58b1dbe331..50f95bfc43 100644 --- a/pivotTable_test.go +++ b/pivotTable_test.go @@ -343,6 +343,8 @@ func TestPivotTable(t *testing.T) { f.Pkg.Store("xl/pivotTables/pivotTable1.xml", MacintoshCyrillicCharset) _, err = f.GetPivotTables("Sheet1") assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") + _, err = f.getPivotTables() + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") assert.NoError(t, f.Close()) } diff --git a/slicer.go b/slicer.go index a7f26edab5..7b4a2d8967 100644 --- a/slicer.go +++ b/slicer.go @@ -53,17 +53,23 @@ import ( // // Format specifies the format of the slicer, this setting is optional. type SlicerOptions struct { - Name string - Cell string - TableSheet string - TableName string - Caption string - Macro string - Width uint - Height uint - DisplayHeader *bool - ItemDesc bool - Format GraphicOptions + slicerXML string + slicerCacheXML string + slicerCacheName string + slicerSheetName string + slicerSheetRID string + drawingXML string + Name string + Cell string + TableSheet string + TableName string + Caption string + Macro string + Width uint + Height uint + DisplayHeader *bool + ItemDesc bool + Format GraphicOptions } // AddSlicer function inserts a slicer by giving the worksheet name and slicer @@ -99,7 +105,7 @@ func (f *File) AddSlicer(sheet string, opts *SlicerOptions) error { if err != nil { return err } - slicerCacheName, err := f.setSlicerCache(sheet, colIdx, opts, table, pivotTable) + slicerCacheName, err := f.setSlicerCache(colIdx, opts, table, pivotTable) if err != nil { return err } @@ -224,7 +230,6 @@ func (f *File) addSheetSlicer(sheet, extURI string) (int, error) { slicerID = f.countSlicers() + 1 ws, err = f.workSheetReader(sheet) decodeExtLst = new(decodeExtLst) - slicerList = new(decodeSlicerList) ) if err != nil { return slicerID, err @@ -236,6 +241,7 @@ func (f *File) addSheetSlicer(sheet, extURI string) (int, error) { } for _, ext := range decodeExtLst.Ext { if ext.URI == extURI { + slicerList := new(decodeSlicerList) _ = f.xmlNewDecoder(strings.NewReader(ext.Content)).Decode(slicerList) for _, slicer := range slicerList.Slicer { if slicer.RID != "" { @@ -390,14 +396,13 @@ func (f *File) genSlicerCacheName(name string) string { // setSlicerCache check if a slicer cache already exists or add a new slicer // cache by giving the column index, slicer, table options, and returns the // slicer cache name. -func (f *File) setSlicerCache(sheet string, colIdx int, opts *SlicerOptions, table *Table, pivotTable *PivotTableOptions) (string, error) { +func (f *File) setSlicerCache(colIdx int, opts *SlicerOptions, table *Table, pivotTable *PivotTableOptions) (string, error) { var ok bool var slicerCacheName string f.Pkg.Range(func(k, v interface{}) bool { if strings.Contains(k.(string), "xl/slicerCaches/slicerCache") { - slicerCache := &xlsxSlicerCacheDefinition{} - if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(v.([]byte)))). - Decode(slicerCache); err != nil && err != io.EOF { + slicerCache, err := f.slicerCacheReader(k.(string)) + if err != nil { return true } if pivotTable != nil && slicerCache.PivotTables != nil { @@ -449,6 +454,20 @@ func (f *File) slicerReader(slicerXML string) (*xlsxSlicers, error) { return slicer, nil } +// slicerCacheReader provides a function to get the pointer to the structure +// after deserialization of xl/slicerCaches/slicerCache%d.xml. +func (f *File) slicerCacheReader(slicerCacheXML string) (*xlsxSlicerCacheDefinition, error) { + content, ok := f.Pkg.Load(slicerCacheXML) + slicerCache := &xlsxSlicerCacheDefinition{} + if ok && content != nil { + if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(content.([]byte)))). + Decode(slicerCache); err != nil && err != io.EOF { + return nil, err + } + } + return slicerCache, nil +} + // timelineReader provides a function to get the pointer to the structure // after deserialization of xl/timelines/timeline%d.xml. func (f *File) timelineReader(timelineXML string) (*xlsxTimelines, error) { @@ -586,6 +605,7 @@ func (f *File) addDrawingSlicer(sheet, slicerName string, ns xml.Attr, opts *Sli return err } graphicFrame := xlsxGraphicFrame{ + Macro: opts.Macro, NvGraphicFramePr: xlsxNvGraphicFramePr{ CNvPr: &xlsxCNvPr{ ID: cNvPrID, @@ -725,3 +745,306 @@ func (f *File) addWorkbookSlicerCache(slicerCacheID int, URI string) error { wb.ExtLst = &xlsxExtLst{Ext: strings.TrimSuffix(strings.TrimPrefix(string(extLstBytes), ""), "")} return err } + +// GetSlicers provides the method to get all slicers in a worksheet by a given +// worksheet name. Note that, this function does not support getting the height, +// width, and graphic options of the slicer shape currently. +func (f *File) GetSlicers(sheet string) ([]SlicerOptions, error) { + var ( + slicers []SlicerOptions + ws, err = f.workSheetReader(sheet) + decodeExtLst = new(decodeExtLst) + ) + if err != nil { + return slicers, err + } + if ws.ExtLst == nil { + return slicers, err + } + target := f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID) + drawingXML := strings.TrimPrefix(strings.ReplaceAll(target, "..", "xl"), "/") + if err = f.xmlNewDecoder(strings.NewReader("" + ws.ExtLst.Ext + "")). + Decode(decodeExtLst); err != nil && err != io.EOF { + return slicers, err + } + for _, ext := range decodeExtLst.Ext { + if ext.URI == ExtURISlicerListX14 || ext.URI == ExtURISlicerListX15 { + slicerList := new(decodeSlicerList) + _ = f.xmlNewDecoder(strings.NewReader(ext.Content)).Decode(&slicerList) + for _, slicer := range slicerList.Slicer { + if slicer.RID != "" { + opts, err := f.getSlicers(sheet, slicer.RID, drawingXML) + if err != nil { + return slicers, err + } + slicers = append(slicers, opts...) + } + } + } + } + return slicers, err +} + +// getSlicerCache provides a function to get a slicer cache by given slicer +// cache name and slicer options. +func (f *File) getSlicerCache(slicerCacheName string, opt *SlicerOptions) *xlsxSlicerCacheDefinition { + var ( + err error + slicerCache *xlsxSlicerCacheDefinition + ) + f.Pkg.Range(func(k, v interface{}) bool { + if strings.Contains(k.(string), "xl/slicerCaches/slicerCache") { + slicerCache, err = f.slicerCacheReader(k.(string)) + if err != nil { + return true + } + if slicerCache.Name == slicerCacheName { + opt.slicerCacheXML = k.(string) + return false + } + } + return true + }) + return slicerCache +} + +// getSlicers provides a function to get slicers options by given worksheet +// name, slicer part relationship ID and drawing part path. +func (f *File) getSlicers(sheet, rID, drawingXML string) ([]SlicerOptions, error) { + var ( + opts []SlicerOptions + sheetRelationshipsSlicerXML = f.getSheetRelationshipsTargetByID(sheet, rID) + slicerXML = strings.ReplaceAll(sheetRelationshipsSlicerXML, "..", "xl") + slicers, err = f.slicerReader(slicerXML) + ) + if err != nil { + return opts, err + } + for _, slicer := range slicers.Slicer { + opt := SlicerOptions{ + slicerXML: slicerXML, + slicerCacheName: slicer.Cache, + slicerSheetName: sheet, + slicerSheetRID: rID, + drawingXML: drawingXML, + Name: slicer.Name, + Caption: slicer.Caption, + DisplayHeader: slicer.ShowCaption, + } + slicerCache := f.getSlicerCache(slicer.Cache, &opt) + if slicerCache == nil { + return opts, err + } + if err := f.extractTableSlicer(slicerCache, &opt); err != nil { + return opts, err + } + if err := f.extractPivotTableSlicer(slicerCache, &opt); err != nil { + return opts, err + } + if err = f.extractSlicerCellAnchor(drawingXML, &opt); err != nil { + return opts, err + } + opts = append(opts, opt) + } + return opts, err +} + +// extractTableSlicer extract table slicer options from slicer cache. +func (f *File) extractTableSlicer(slicerCache *xlsxSlicerCacheDefinition, opt *SlicerOptions) error { + if slicerCache.ExtLst != nil { + tables, err := f.getTables() + if err != nil { + return err + } + ext := new(xlsxExt) + _ = f.xmlNewDecoder(strings.NewReader(slicerCache.ExtLst.Ext)).Decode(ext) + if ext.URI == ExtURISlicerCacheDefinition { + tableSlicerCache := new(decodeTableSlicerCache) + _ = f.xmlNewDecoder(strings.NewReader(ext.Content)).Decode(tableSlicerCache) + opt.ItemDesc = tableSlicerCache.SortOrder == "descending" + for sheetName, sheetTables := range tables { + for _, table := range sheetTables { + if tableSlicerCache.TableID == table.tID { + opt.TableName = table.Name + opt.TableSheet = sheetName + } + } + } + } + } + return nil +} + +// extractPivotTableSlicer extract pivot table slicer options from slicer cache. +func (f *File) extractPivotTableSlicer(slicerCache *xlsxSlicerCacheDefinition, opt *SlicerOptions) error { + pivotTables, err := f.getPivotTables() + if err != nil { + return err + } + if slicerCache.PivotTables != nil { + for _, pt := range slicerCache.PivotTables.PivotTable { + opt.TableName = pt.Name + for sheetName, sheetPivotTables := range pivotTables { + for _, pivotTable := range sheetPivotTables { + if opt.TableName == pivotTable.Name { + opt.TableSheet = sheetName + } + } + } + } + if slicerCache.Data != nil && slicerCache.Data.Tabular != nil { + opt.ItemDesc = slicerCache.Data.Tabular.SortOrder == "descending" + } + } + return nil +} + +// extractSlicerCellAnchor extract slicer drawing object from two cell anchor by +// giving drawing part path and slicer options. +func (f *File) extractSlicerCellAnchor(drawingXML string, opt *SlicerOptions) error { + var ( + wsDr *xlsxWsDr + deCellAnchor = new(decodeCellAnchor) + deChoice = new(decodeChoice) + err error + ) + if wsDr, _, err = f.drawingParser(drawingXML); err != nil { + return err + } + wsDr.mu.Lock() + defer wsDr.mu.Unlock() + cond := func(ac *xlsxAlternateContent) bool { + if ac != nil { + _ = f.xmlNewDecoder(strings.NewReader(ac.Content)).Decode(&deChoice) + if deChoice.XMLNSSle15 == NameSpaceDrawingMLSlicerX15.Value || deChoice.XMLNSA14 == NameSpaceDrawingMLA14.Value { + if deChoice.GraphicFrame.NvGraphicFramePr.CNvPr.Name == opt.Name { + return true + } + } + } + return false + } + for _, anchor := range wsDr.TwoCellAnchor { + for _, ac := range anchor.AlternateContent { + if cond(ac) { + if anchor.From != nil { + opt.Macro = deChoice.GraphicFrame.Macro + if opt.Cell, err = CoordinatesToCellName(anchor.From.Col+1, anchor.From.Row+1); err != nil { + return err + } + } + return err + } + } + _ = f.xmlNewDecoder(strings.NewReader("" + anchor.GraphicFrame + "")).Decode(&deCellAnchor) + for _, ac := range deCellAnchor.AlternateContent { + if cond(ac) { + if deCellAnchor.From != nil { + opt.Macro = deChoice.GraphicFrame.Macro + if opt.Cell, err = CoordinatesToCellName(deCellAnchor.From.Col+1, deCellAnchor.From.Row+1); err != nil { + return err + } + } + return err + } + } + } + return err +} + +// getAllSlicers provides a function to get all slicers in a workbook. +func (f *File) getAllSlicers() (map[string][]SlicerOptions, error) { + slicers := map[string][]SlicerOptions{} + for _, sheetName := range f.GetSheetList() { + sles, err := f.GetSlicers(sheetName) + e := ErrSheetNotExist{sheetName} + if err != nil && err.Error() != newNotWorksheetError(sheetName).Error() && err.Error() != e.Error() { + return slicers, err + } + slicers[sheetName] = append(slicers[sheetName], sles...) + } + return slicers, nil +} + +// DeleteSlicer provides the method to delete a slicer by a given slicer name. +func (f *File) DeleteSlicer(name string) error { + sles, err := f.getAllSlicers() + if err != nil { + return err + } + for _, slicers := range sles { + for _, slicer := range slicers { + if slicer.Name != name { + continue + } + _ = f.deleteSlicer(slicer) + return f.deleteSlicerCache(sles, slicer) + } + } + return newNoExistSlicerError(name) +} + +// getSlicers provides a function to delete slicer by given slicer options. +func (f *File) deleteSlicer(opts SlicerOptions) error { + slicers, err := f.slicerReader(opts.slicerXML) + if err != nil { + return err + } + for i := 0; i < len(slicers.Slicer); i++ { + if slicers.Slicer[i].Name == opts.Name { + slicers.Slicer = append(slicers.Slicer[:i], slicers.Slicer[i+1:]...) + i-- + } + } + if len(slicers.Slicer) == 0 { + var ( + extLstBytes []byte + ws, err = f.workSheetReader(opts.slicerSheetName) + decodeExtLst = new(decodeExtLst) + ) + if err != nil { + return err + } + if err = f.xmlNewDecoder(strings.NewReader("" + ws.ExtLst.Ext + "")). + Decode(decodeExtLst); err != nil && err != io.EOF { + return err + } + for i, ext := range decodeExtLst.Ext { + if ext.URI == ExtURISlicerListX14 || ext.URI == ExtURISlicerListX15 { + slicerList := new(decodeSlicerList) + _ = f.xmlNewDecoder(strings.NewReader(ext.Content)).Decode(slicerList) + for _, slicer := range slicerList.Slicer { + if slicer.RID == opts.slicerSheetRID { + decodeExtLst.Ext = append(decodeExtLst.Ext[:i], decodeExtLst.Ext[i+1:]...) + extLstBytes, err = xml.Marshal(decodeExtLst) + ws.ExtLst = &xlsxExtLst{Ext: strings.TrimSuffix(strings.TrimPrefix(string(extLstBytes), ""), "")} + f.Pkg.Delete(opts.slicerXML) + _ = f.removeContentTypesPart(ContentTypeSlicer, "/"+opts.slicerXML) + f.deleteSheetRelationships(opts.slicerSheetName, opts.slicerSheetRID) + return err + } + } + } + } + } + output, err := xml.Marshal(slicers) + f.saveFileList(opts.slicerXML, output) + return err +} + +// deleteSlicerCache provides a function to delete the slicer cache by giving +// slicer options if the slicer cache is no longer used. +func (f *File) deleteSlicerCache(sles map[string][]SlicerOptions, opts SlicerOptions) error { + for _, slicers := range sles { + for _, slicer := range slicers { + if slicer.Name != opts.Name && slicer.slicerCacheName == opts.slicerCacheName { + return nil + } + } + } + if err := f.DeleteDefinedName(&DefinedName{Name: opts.slicerCacheName}); err != nil { + return err + } + f.Pkg.Delete(opts.slicerCacheXML) + return f.removeContentTypesPart(ContentTypeSlicerCache, "/"+opts.slicerCacheXML) +} diff --git a/slicer_test.go b/slicer_test.go index da6fa915f1..5a79a80668 100644 --- a/slicer_test.go +++ b/slicer_test.go @@ -5,12 +5,13 @@ import ( "math/rand" "os" "path/filepath" + "strings" "testing" "github.com/stretchr/testify/assert" ) -func TestAddSlicer(t *testing.T) { +func TestSlicer(t *testing.T) { f := NewFile() disable, colName := false, "_!@#$%^&*()-+=|\\/<>" assert.NoError(t, f.SetCellValue("Sheet1", "B1", colName)) @@ -45,8 +46,29 @@ func TestAddSlicer(t *testing.T) { DisplayHeader: &disable, ItemDesc: true, })) + // Test get table slicers + slicers, err := f.GetSlicers("Sheet1") + assert.NoError(t, err) + assert.Equal(t, "Column1", slicers[0].Name) + assert.Equal(t, "E1", slicers[0].Cell) + assert.Equal(t, "Sheet1", slicers[0].TableSheet) + assert.Equal(t, "Table1", slicers[0].TableName) + assert.Equal(t, "Column1", slicers[0].Caption) + assert.Equal(t, "Column1 1", slicers[1].Name) + assert.Equal(t, "I1", slicers[1].Cell) + assert.Equal(t, "Sheet1", slicers[1].TableSheet) + assert.Equal(t, "Table1", slicers[1].TableName) + assert.Equal(t, "Column1", slicers[1].Caption) + assert.Equal(t, colName, slicers[2].Name) + assert.Equal(t, "M1", slicers[2].Cell) + assert.Equal(t, "Sheet1", slicers[2].TableSheet) + assert.Equal(t, "Table1", slicers[2].TableName) + assert.Equal(t, colName, slicers[2].Caption) + assert.Equal(t, "Button1_Click", slicers[2].Macro) + assert.False(t, *slicers[2].DisplayHeader) + assert.True(t, slicers[2].ItemDesc) // Test create two pivot tables in a new worksheet - _, err := f.NewSheet("Sheet2") + _, err = f.NewSheet("Sheet2") assert.NoError(t, err) // Create some data in a sheet month := []string{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"} @@ -116,6 +138,25 @@ func TestAddSlicer(t *testing.T) { Caption: "Region", ItemDesc: true, })) + // Test get pivot table slicers + slicers, err = f.GetSlicers("Sheet2") + assert.NoError(t, err) + assert.Equal(t, "Month", slicers[0].Name) + assert.Equal(t, "G42", slicers[0].Cell) + assert.Equal(t, "Sheet2", slicers[0].TableSheet) + assert.Equal(t, "PivotTable1", slicers[0].TableName) + assert.Equal(t, "Month", slicers[0].Caption) + assert.Equal(t, "Month 1", slicers[1].Name) + assert.Equal(t, "K42", slicers[1].Cell) + assert.Equal(t, "Sheet2", slicers[1].TableSheet) + assert.Equal(t, "PivotTable1", slicers[1].TableName) + assert.Equal(t, "Month", slicers[1].Caption) + assert.Equal(t, "Region", slicers[2].Name) + assert.Equal(t, "O42", slicers[2].Cell) + assert.Equal(t, "Sheet2", slicers[2].TableSheet) + assert.Equal(t, "PivotTable2", slicers[2].TableName) + assert.Equal(t, "Region", slicers[2].Caption) + assert.True(t, slicers[2].ItemDesc) // Test add a table slicer with empty slicer options assert.Equal(t, ErrParameterRequired, f.AddSlicer("Sheet1", nil)) // Test add a table slicer with invalid slicer options @@ -167,6 +208,48 @@ func TestAddSlicer(t *testing.T) { }), "XML syntax error on line 1: invalid UTF-8") assert.NoError(t, f.Close()) + // Test open a workbook and get already exist slicers + f, err = OpenFile(workbookPath) + assert.NoError(t, err) + slicers, err = f.GetSlicers("Sheet1") + assert.NoError(t, err) + assert.Equal(t, "Column1", slicers[0].Name) + assert.Equal(t, "E1", slicers[0].Cell) + assert.Equal(t, "Sheet1", slicers[0].TableSheet) + assert.Equal(t, "Table1", slicers[0].TableName) + assert.Equal(t, "Column1", slicers[0].Caption) + assert.Equal(t, "Column1 1", slicers[1].Name) + assert.Equal(t, "I1", slicers[1].Cell) + assert.Equal(t, "Sheet1", slicers[1].TableSheet) + assert.Equal(t, "Table1", slicers[1].TableName) + assert.Equal(t, "Column1", slicers[1].Caption) + assert.Equal(t, colName, slicers[2].Name) + assert.Equal(t, "M1", slicers[2].Cell) + assert.Equal(t, "Sheet1", slicers[2].TableSheet) + assert.Equal(t, "Table1", slicers[2].TableName) + assert.Equal(t, colName, slicers[2].Caption) + assert.Equal(t, "Button1_Click", slicers[2].Macro) + assert.False(t, *slicers[2].DisplayHeader) + assert.True(t, slicers[2].ItemDesc) + slicers, err = f.GetSlicers("Sheet2") + assert.NoError(t, err) + assert.Equal(t, "Month", slicers[0].Name) + assert.Equal(t, "G42", slicers[0].Cell) + assert.Equal(t, "Sheet2", slicers[0].TableSheet) + assert.Equal(t, "PivotTable1", slicers[0].TableName) + assert.Equal(t, "Month", slicers[0].Caption) + assert.Equal(t, "Month 1", slicers[1].Name) + assert.Equal(t, "K42", slicers[1].Cell) + assert.Equal(t, "Sheet2", slicers[1].TableSheet) + assert.Equal(t, "PivotTable1", slicers[1].TableName) + assert.Equal(t, "Month", slicers[1].Caption) + assert.Equal(t, "Region", slicers[2].Name) + assert.Equal(t, "O42", slicers[2].Cell) + assert.Equal(t, "Sheet2", slicers[2].TableSheet) + assert.Equal(t, "PivotTable2", slicers[2].TableName) + assert.Equal(t, "Region", slicers[2].Caption) + assert.True(t, slicers[2].ItemDesc) + // Test add a pivot table slicer with workbook which contains timeline f, err = OpenFile(workbookPath) assert.NoError(t, err) @@ -274,6 +357,113 @@ func TestAddSlicer(t *testing.T) { Caption: "Column1", }), "XML syntax error on line 1: invalid UTF-8") assert.NoError(t, f.Close()) + + f = NewFile() + // Test get sheet slicers without slicer + slicers, err = f.GetSlicers("Sheet1") + assert.NoError(t, err) + assert.Empty(t, slicers) + // Test get sheet slicers with not exist worksheet name + _, err = f.GetSlicers("SheetN") + assert.EqualError(t, err, "sheet SheetN does not exist") + assert.NoError(t, f.Close()) + + f, err = OpenFile(workbookPath) + assert.NoError(t, err) + // Test get sheet slicers with unsupported charset slicer cache + f.Pkg.Store("xl/slicerCaches/slicerCache1.xml", MacintoshCyrillicCharset) + _, err = f.GetSlicers("Sheet1") + assert.NoError(t, err) + // Test get sheet slicers with unsupported charset slicer + f.Pkg.Store("xl/slicers/slicer1.xml", MacintoshCyrillicCharset) + _, err = f.GetSlicers("Sheet1") + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") + // Test get sheet slicers with invalid worksheet extension list + ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml") + assert.True(t, ok) + ws.(*xlsxWorksheet).ExtLst.Ext = "<>" + _, err = f.GetSlicers("Sheet1") + assert.Error(t, err) + assert.NoError(t, f.Close()) + + f, err = OpenFile(workbookPath) + assert.NoError(t, err) + // Test get sheet slicers without slicer cache + f.Pkg.Range(func(k, v interface{}) bool { + if strings.Contains(k.(string), "xl/slicerCaches/slicerCache") { + f.Pkg.Delete(k.(string)) + } + return true + }) + slicers, err = f.GetSlicers("Sheet1") + assert.NoError(t, err) + assert.Empty(t, slicers) + assert.NoError(t, f.Close()) + // Test open a workbook and get sheet slicer with invalid cell reference in the drawing part + f, err = OpenFile(workbookPath) + assert.NoError(t, err) + f.Pkg.Store("xl/drawings/drawing1.xml", []byte(fmt.Sprintf(`
-1-1`, NameSpaceDrawingMLSpreadSheet.Value, NameSpaceDrawingMLSlicerX15.Value))) + _, err = f.GetSlicers("Sheet1") + assert.Equal(t, newCoordinatesToCellNameError(0, 0), err) + // Test get sheet slicer without slicer shape in the drawing part + f.Drawings.Delete("xl/drawings/drawing1.xml") + f.Pkg.Store("xl/drawings/drawing1.xml", []byte(fmt.Sprintf(``, NameSpaceDrawingMLSpreadSheet.Value))) + _, err = f.GetSlicers("Sheet1") + assert.NoError(t, err) + f.Drawings.Delete("xl/drawings/drawing1.xml") + // Test get sheet slicers with unsupported charset drawing part + f.Pkg.Store("xl/drawings/drawing1.xml", MacintoshCyrillicCharset) + _, err = f.GetSlicers("Sheet1") + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") + // Test get sheet slicers with unsupported charset table + f.Pkg.Store("xl/tables/table1.xml", MacintoshCyrillicCharset) + _, err = f.GetSlicers("Sheet1") + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") + // Test get sheet slicers with unsupported charset pivot table + f.Pkg.Store("xl/pivotTables/pivotTable1.xml", MacintoshCyrillicCharset) + _, err = f.GetSlicers("Sheet2") + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") + assert.NoError(t, f.Close()) + + // Test create a workbook and get sheet slicer with invalid cell reference in the drawing part + f = NewFile() + assert.NoError(t, f.AddTable("Sheet1", &Table{ + Name: "Table1", + Range: "A1:D5", + })) + assert.NoError(t, f.AddSlicer("Sheet1", &SlicerOptions{ + Name: "Column1", + Cell: "E1", + TableSheet: "Sheet1", + TableName: "Table1", + Caption: "Column1", + })) + drawing, ok := f.Drawings.Load("xl/drawings/drawing1.xml") + assert.True(t, ok) + drawing.(*xlsxWsDr).TwoCellAnchor[0].From = &xlsxFrom{Col: -1, Row: -1} + _, err = f.GetSlicers("Sheet1") + assert.Equal(t, newCoordinatesToCellNameError(0, 0), err) + assert.NoError(t, f.Close()) + + // Test open a workbook and delete slicers + f, err = OpenFile(workbookPath) + assert.NoError(t, err) + for _, name := range []string{colName, "Column1 1", "Column1"} { + assert.NoError(t, f.DeleteSlicer(name)) + } + for _, name := range []string{"Month", "Month 1", "Region"} { + assert.NoError(t, f.DeleteSlicer(name)) + } + // Test delete slicer with no exits slicer name + assert.Equal(t, newNoExistSlicerError("x"), f.DeleteSlicer("x")) + assert.NoError(t, f.Close()) + + // Test open a workbook and delete sheet slicer with unsupported charset slicer cache + f, err = OpenFile(workbookPath) + assert.NoError(t, err) + f.Pkg.Store("xl/slicers/slicer1.xml", MacintoshCyrillicCharset) + assert.EqualError(t, f.DeleteSlicer("Column1"), "XML syntax error on line 1: invalid UTF-8") + assert.NoError(t, f.Close()) } func TestAddSheetSlicer(t *testing.T) { @@ -296,36 +486,81 @@ func TestAddSheetTableSlicer(t *testing.T) { func TestSetSlicerCache(t *testing.T) { f := NewFile() f.Pkg.Store("xl/slicerCaches/slicerCache1.xml", MacintoshCyrillicCharset) - _, err := f.setSlicerCache("Sheet1", 1, &SlicerOptions{}, &Table{}, nil) + _, err := f.setSlicerCache(1, &SlicerOptions{}, &Table{}, nil) assert.NoError(t, err) assert.NoError(t, f.Close()) f = NewFile() f.Pkg.Store("xl/slicerCaches/slicerCache2.xml", []byte(fmt.Sprintf(``, NameSpaceSpreadSheetX14.Value, ExtURISlicerCacheDefinition))) - _, err = f.setSlicerCache("Sheet1", 1, &SlicerOptions{}, &Table{}, nil) + _, err = f.setSlicerCache(1, &SlicerOptions{}, &Table{}, nil) assert.NoError(t, err) assert.NoError(t, f.Close()) f = NewFile() f.Pkg.Store("xl/slicerCaches/slicerCache2.xml", []byte(fmt.Sprintf(``, NameSpaceSpreadSheetX14.Value, ExtURISlicerCacheDefinition))) - _, err = f.setSlicerCache("Sheet1", 1, &SlicerOptions{}, &Table{}, nil) + _, err = f.setSlicerCache(1, &SlicerOptions{}, &Table{}, nil) assert.NoError(t, err) assert.NoError(t, f.Close()) f = NewFile() f.Pkg.Store("xl/slicerCaches/slicerCache2.xml", []byte(fmt.Sprintf(``, NameSpaceSpreadSheetX14.Value, ExtURISlicerCacheDefinition))) - _, err = f.setSlicerCache("Sheet1", 1, &SlicerOptions{}, &Table{tID: 1}, nil) + _, err = f.setSlicerCache(1, &SlicerOptions{}, &Table{tID: 1}, nil) assert.NoError(t, err) assert.NoError(t, f.Close()) f = NewFile() f.Pkg.Store("xl/slicerCaches/slicerCache2.xml", []byte(fmt.Sprintf(``, NameSpaceSpreadSheetX14.Value))) - _, err = f.setSlicerCache("Sheet1", 1, &SlicerOptions{}, &Table{tID: 1}, nil) + _, err = f.setSlicerCache(1, &SlicerOptions{}, &Table{tID: 1}, nil) assert.NoError(t, err) assert.NoError(t, f.Close()) } +func TestDeleteSlicer(t *testing.T) { + f, slicerXML := NewFile(), "xl/slicers/slicer1.xml" + assert.NoError(t, f.AddTable("Sheet1", &Table{ + Name: "Table1", + Range: "A1:D5", + })) + assert.NoError(t, f.AddSlicer("Sheet1", &SlicerOptions{ + Name: "Column1", + Cell: "E1", + TableSheet: "Sheet1", + TableName: "Table1", + Caption: "Column1", + })) + // Test delete sheet slicers with invalid worksheet extension list + ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") + assert.True(t, ok) + ws.(*xlsxWorksheet).ExtLst.Ext = "<>" + assert.Error(t, f.deleteSlicer(SlicerOptions{ + slicerXML: slicerXML, + slicerSheetName: "Sheet1", + Name: "Column1", + })) + // Test delete slicer with unsupported charset worksheet + f.Sheet.Delete("xl/worksheets/sheet1.xml") + f.Pkg.Store("xl/worksheets/sheet1.xml", MacintoshCyrillicCharset) + assert.EqualError(t, f.deleteSlicer(SlicerOptions{ + slicerXML: slicerXML, + slicerSheetName: "Sheet1", + Name: "Column1", + }), "XML syntax error on line 1: invalid UTF-8") + // Test delete slicer with unsupported charset slicer + f.Pkg.Store(slicerXML, MacintoshCyrillicCharset) + assert.EqualError(t, f.deleteSlicer(SlicerOptions{slicerXML: slicerXML}), "XML syntax error on line 1: invalid UTF-8") + assert.NoError(t, f.Close()) +} + +func TestDeleteSlicerCache(t *testing.T) { + f := NewFile() + // Test delete slicer cache with unsupported charset workbook + f.WorkBook = nil + f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset) + assert.EqualError(t, f.deleteSlicerCache(nil, SlicerOptions{}), "XML syntax error on line 1: invalid UTF-8") + assert.NoError(t, f.Close()) +} + func TestAddSlicerCache(t *testing.T) { f := NewFile() f.ContentTypes = nil diff --git a/table.go b/table.go index 5ca7894e67..aec7b26038 100644 --- a/table.go +++ b/table.go @@ -173,11 +173,11 @@ func (f *File) DeleteTable(name string) error { if err := checkDefinedName(name); err != nil { return err } - for _, sheet := range f.GetSheetList() { - tables, err := f.GetTables(sheet) - if err != nil { - return err - } + tbls, err := f.getTables() + if err != nil { + return err + } + for sheet, tables := range tbls { for _, table := range tables { if table.Name != name { continue @@ -201,6 +201,20 @@ func (f *File) DeleteTable(name string) error { return newNoExistTableError(name) } +// getTables provides a function to get all tables in a workbook. +func (f *File) getTables() (map[string][]Table, error) { + tables := map[string][]Table{} + for _, sheetName := range f.GetSheetList() { + tbls, err := f.GetTables(sheetName) + e := ErrSheetNotExist{sheetName} + if err != nil && err.Error() != newNotWorksheetError(sheetName).Error() && err.Error() != e.Error() { + return tables, err + } + tables[sheetName] = append(tables[sheetName], tbls...) + } + return tables, nil +} + // countTables provides a function to get table files count storage in the // folder xl/tables. func (f *File) countTables() int { diff --git a/xmlDecodeDrawing.go b/xmlDecodeDrawing.go index 5c900fc373..a59e7c45e4 100644 --- a/xmlDecodeDrawing.go +++ b/xmlDecodeDrawing.go @@ -24,7 +24,7 @@ type decodeCellAnchor struct { Sp *decodeSp `xml:"sp"` Pic *decodePic `xml:"pic"` ClientData *decodeClientData `xml:"clientData"` - AlternateContent []*xlsxAlternateContent `xml:"mc:AlternateContent"` + AlternateContent []*xlsxAlternateContent `xml:"AlternateContent"` Content string `xml:",innerxml"` } @@ -46,6 +46,28 @@ type decodeCellAnchorPos struct { ClientData *xlsxInnerXML `xml:"clientData"` } +// decodeChoice defines the structure used to deserialize the mc:Choice element. +type decodeChoice struct { + XMLName xml.Name `xml:"Choice"` + XMLNSA14 string `xml:"a14,attr"` + XMLNSSle15 string `xml:"sle15,attr"` + Requires string `xml:"Requires,attr"` + GraphicFrame decodeGraphicFrame `xml:"graphicFrame"` +} + +// decodeGraphicFrame defines the structure used to deserialize the +// xdr:graphicFrame element. +type decodeGraphicFrame struct { + Macro string `xml:"macro,attr"` + NvGraphicFramePr decodeNvGraphicFramePr `xml:"nvGraphicFramePr"` +} + +// decodeNvGraphicFramePr defines the structure used to deserialize the +// xdr:nvGraphicFramePr element. +type decodeNvGraphicFramePr struct { + CNvPr decodeCNvPr `xml:"cNvPr"` +} + // decodeSp defines the structure used to deserialize the sp element. type decodeSp struct { Macro string `xml:"macro,attr,omitempty"` @@ -56,7 +78,7 @@ type decodeSp struct { SpPr *decodeSpPr `xml:"spPr"` } -// decodeSp (Non-Visual Properties for a Shape) directly maps the nvSpPr +// decodeNvSpPr (Non-Visual Properties for a Shape) directly maps the nvSpPr // element. This element specifies all non-visual properties for a shape. This // element is a container for the non-visual identification properties, shape // properties and application properties that are to be associated with a diff --git a/xmlSlicers.go b/xmlSlicers.go index 6e68897562..5c20923cdd 100644 --- a/xmlSlicers.go +++ b/xmlSlicers.go @@ -149,9 +149,10 @@ type xlsxX15SlicerCaches struct { // decodeTableSlicerCache defines the structure used to parse the // x15:tableSlicerCache element of the table slicer cache. type decodeTableSlicerCache struct { - XMLName xml.Name `xml:"tableSlicerCache"` - TableID int `xml:"tableId,attr"` - Column int `xml:"column,attr"` + XMLName xml.Name `xml:"tableSlicerCache"` + TableID int `xml:"tableId,attr"` + Column int `xml:"column,attr"` + SortOrder string `xml:"sortOrder,attr"` } // decodeSlicerList defines the structure used to parse the x14:slicerList From 9c460ffe6cd6f67b174b2ed53991b58429342588 Mon Sep 17 00:00:00 2001 From: wanghaochen2024 Date: Tue, 27 Aug 2024 22:41:32 +0800 Subject: [PATCH 905/957] Add support for applying number format expression with language/location tags and ID (#1951) - Update unit tests for specified date and time format code --- numfmt.go | 842 +++++++++++++++++++++++++------------------------ numfmt_test.go | 50 +++ 2 files changed, 486 insertions(+), 406 deletions(-) diff --git a/numfmt.go b/numfmt.go index 319229f772..f34707b84e 100644 --- a/numfmt.go +++ b/numfmt.go @@ -14,6 +14,7 @@ package excelize import ( "fmt" "math" + "math/big" "strconv" "strings" "time" @@ -719,80 +720,80 @@ var ( 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, weekdayNames: weekdayNamesAfrikaans, weekdayNamesAbbr: weekdayNamesAfrikaansAbbr}, - "436": {tags: []string{"af-ZA"}, localMonth: localMonthsNameAfrikaans, apFmt: apFmtAfrikaans, weekdayNames: weekdayNamesAfrikaans, weekdayNamesAbbr: weekdayNamesAfrikaansAbbr}, - "1C": {tags: []string{"sq"}, localMonth: localMonthsNameAlbanian, apFmt: apFmtAlbanian, weekdayNames: weekdayNamesAlbanian, weekdayNamesAbbr: weekdayNamesAlbanianAbbr}, - "41C": {tags: []string{"sq-AL"}, localMonth: localMonthsNameAlbanian, apFmt: apFmtAlbanian, weekdayNames: weekdayNamesAlbanian, weekdayNamesAbbr: weekdayNamesAlbanianAbbr}, - "84": {tags: []string{"gsw"}, localMonth: localMonthsNameAlsatian, apFmt: apFmtAlsatian, weekdayNames: weekdayNamesAlsatian, weekdayNamesAbbr: weekdayNamesAlsatianAbbr}, - "484": {tags: []string{"gsw-FR"}, localMonth: localMonthsNameAlsatianFrance, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesAlsatianFrance, weekdayNamesAbbr: weekdayNamesAlsatianFranceAbbr}, - "5E": {tags: []string{"am"}, localMonth: localMonthsNameAmharic, apFmt: apFmtAmharic, weekdayNames: weekdayNamesAmharic, weekdayNamesAbbr: weekdayNamesAmharicAbbr}, - "45E": {tags: []string{"am-ET"}, localMonth: localMonthsNameAmharic, apFmt: apFmtAmharic, weekdayNames: weekdayNamesAmharic, weekdayNamesAbbr: weekdayNamesAmharicAbbr}, - "1": {tags: []string{"ar"}, localMonth: localMonthsNameArabic, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr}, - "1401": {tags: []string{"ar-DZ"}, localMonth: localMonthsNameArabic, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr}, - "3C01": {tags: []string{"ar-BH"}, localMonth: localMonthsNameArabic, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr}, - "C01": {tags: []string{"ar-EG"}, localMonth: localMonthsNameArabic, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr}, - "801": {tags: []string{"ar-IQ"}, localMonth: localMonthsNameArabicIraq, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr}, - "2C01": {tags: []string{"ar-JO"}, localMonth: localMonthsNameArabicIraq, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr}, - "3401": {tags: []string{"ar-KW"}, localMonth: localMonthsNameArabic, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr}, - "3001": {tags: []string{"ar-LB"}, localMonth: localMonthsNameArabicIraq, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr}, - "1801": {tags: []string{"ar-MA"}, localMonth: localMonthsNameArabic, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr}, - "2001": {tags: []string{"ar-OM"}, localMonth: localMonthsNameArabic, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr}, - "4001": {tags: []string{"ar-QA"}, localMonth: localMonthsNameArabic, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr}, - "401": {tags: []string{"ar-SA"}, localMonth: localMonthsNameArabic, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr}, - "2801": {tags: []string{"ar-SY"}, localMonth: localMonthsNameArabicIraq, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr}, - "1C01": {tags: []string{"ar-TN"}, localMonth: localMonthsNameArabic, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr}, - "3801": {tags: []string{"ar-AE"}, localMonth: localMonthsNameArabic, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr}, - "2401": {tags: []string{"ar-YE"}, localMonth: localMonthsNameArabic, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr}, - "2B": {tags: []string{"hy"}, localMonth: localMonthsNameArmenian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesArmenian, weekdayNamesAbbr: weekdayNamesArmenianAbbr}, - "42B": {tags: []string{"hy-AM"}, localMonth: localMonthsNameArmenian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesArmenian, weekdayNamesAbbr: weekdayNamesArmenianAbbr}, - "4D": {tags: []string{"as"}, localMonth: localMonthsNameAssamese, apFmt: apFmtAssamese, weekdayNames: weekdayNamesAssamese, weekdayNamesAbbr: weekdayNamesAssameseAbbr}, - "44D": {tags: []string{"as-IN"}, localMonth: localMonthsNameAssamese, apFmt: apFmtAssamese, weekdayNames: weekdayNamesAssamese, weekdayNamesAbbr: weekdayNamesAssameseAbbr}, - "742C": {tags: []string{"az-Cyrl"}, localMonth: localMonthsNameAzerbaijaniCyrillic, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesAzerbaijaniCyrillic, weekdayNamesAbbr: weekdayNamesAzerbaijaniCyrillicAbbr}, - "82C": {tags: []string{"az-Cyrl-AZ"}, localMonth: localMonthsNameAzerbaijaniCyrillic, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesAzerbaijaniCyrillic, weekdayNamesAbbr: weekdayNamesAzerbaijaniCyrillicAbbr}, - "2C": {tags: []string{"az"}, localMonth: localMonthsNameAzerbaijani, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesAzerbaijani, weekdayNamesAbbr: weekdayNamesAzerbaijaniAbbr}, - "782C": {tags: []string{"az-Latn"}, localMonth: localMonthsNameAzerbaijani, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesAzerbaijani, weekdayNamesAbbr: weekdayNamesAzerbaijaniAbbr}, - "42C": {tags: []string{"az-Latn-AZ"}, localMonth: localMonthsNameAzerbaijani, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesAzerbaijani, weekdayNamesAbbr: weekdayNamesAzerbaijaniAbbr}, - "45": {tags: []string{"bn"}, localMonth: localMonthsNameBangla, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBangla, weekdayNamesAbbr: weekdayNamesBanglaAbbr}, - "845": {tags: []string{"bn-BD"}, localMonth: localMonthsNameBangla, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBangla, weekdayNamesAbbr: weekdayNamesBanglaAbbr}, - "445": {tags: []string{"bn-IN"}, localMonth: localMonthsNameBangla, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBangla, weekdayNamesAbbr: weekdayNamesBanglaAbbr}, - "6D": {tags: []string{"ba"}, localMonth: localMonthsNameBashkir, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBashkir, weekdayNamesAbbr: weekdayNamesBashkirAbbr}, - "46D": {tags: []string{"ba-RU"}, localMonth: localMonthsNameBashkir, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBashkir, weekdayNamesAbbr: weekdayNamesBashkirAbbr}, - "2D": {tags: []string{"eu"}, localMonth: localMonthsNameBasque, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBasque, weekdayNamesAbbr: weekdayNamesBasqueAbbr}, - "42D": {tags: []string{"eu-ES"}, localMonth: localMonthsNameBasque, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBasque, weekdayNamesAbbr: weekdayNamesBasqueAbbr}, - "23": {tags: []string{"be"}, localMonth: localMonthsNameBelarusian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBelarusian, weekdayNamesAbbr: weekdayNamesBelarusianAbbr}, - "423": {tags: []string{"be-BY"}, localMonth: localMonthsNameBelarusian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBelarusian, weekdayNamesAbbr: weekdayNamesBelarusianAbbr}, - "641A": {tags: []string{"bs-Cyrl"}, localMonth: localMonthsNameBosnianCyrillic, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBosnianCyrillic, weekdayNamesAbbr: weekdayNamesBosnianCyrillicAbbr}, - "201A": {tags: []string{"bs-Cyrl-BA"}, localMonth: localMonthsNameBosnianCyrillic, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBosnianCyrillic, weekdayNamesAbbr: weekdayNamesBosnianCyrillicAbbr}, - "681A": {tags: []string{"bs-Latn"}, localMonth: localMonthsNameBosnian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBosnian, weekdayNamesAbbr: weekdayNamesBosnianAbbr}, - "781A": {tags: []string{"bs"}, localMonth: localMonthsNameBosnian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBosnian, weekdayNamesAbbr: weekdayNamesBosnianAbbr}, - "141A": {tags: []string{"bs-Latn-BA"}, localMonth: localMonthsNameBosnian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBosnian, weekdayNamesAbbr: weekdayNamesBosnianAbbr}, - "7E": {tags: []string{"br"}, localMonth: localMonthsNameBreton, apFmt: apFmtBreton, weekdayNames: weekdayNamesBreton, weekdayNamesAbbr: weekdayNamesBretonAbbr}, - "47E": {tags: []string{"br-FR"}, localMonth: localMonthsNameBreton, apFmt: apFmtBreton, weekdayNames: weekdayNamesBreton, weekdayNamesAbbr: weekdayNamesBretonAbbr}, - "2": {tags: []string{"bg"}, localMonth: localMonthsNameBulgarian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBulgarian, weekdayNamesAbbr: weekdayNamesBulgarianAbbr}, - "402": {tags: []string{"bg-BG"}, localMonth: localMonthsNameBulgarian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBulgarian, weekdayNamesAbbr: weekdayNamesBulgarianAbbr}, - "55": {tags: []string{"my"}, localMonth: localMonthsNameBurmese, apFmt: apFmtBurmese, weekdayNames: weekdayNamesBurmese, weekdayNamesAbbr: weekdayNamesBurmese}, - "455": {tags: []string{"my-MM"}, localMonth: localMonthsNameBurmese, apFmt: apFmtBurmese, weekdayNames: weekdayNamesBurmese, weekdayNamesAbbr: weekdayNamesBurmese}, - "3": {tags: []string{"ca"}, localMonth: localMonthsNameValencian, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesValencian, weekdayNamesAbbr: weekdayNamesValencianAbbr}, - "403": {tags: []string{"ca-ES"}, localMonth: localMonthsNameValencian, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesValencian, weekdayNamesAbbr: weekdayNamesValencianAbbr}, - "45F": {tags: []string{"tzm-Arab-MA"}, localMonth: localMonthsNameArabicIraq, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr}, - "92": {tags: []string{"ku"}, localMonth: localMonthsNameCentralKurdish, apFmt: apFmtCentralKurdish, weekdayNames: weekdayNamesCentralKurdish, weekdayNamesAbbr: weekdayNamesCentralKurdish}, - "7C92": {tags: []string{"ku-Arab"}, localMonth: localMonthsNameCentralKurdish, apFmt: apFmtCentralKurdish, weekdayNames: weekdayNamesCentralKurdish, weekdayNamesAbbr: weekdayNamesCentralKurdish}, - "492": {tags: []string{"ku-Arab-IQ"}, localMonth: localMonthsNameCentralKurdish, apFmt: apFmtCentralKurdish, weekdayNames: weekdayNamesCentralKurdish, weekdayNamesAbbr: weekdayNamesCentralKurdish}, - "5C": {tags: []string{"chr"}, localMonth: localMonthsNameCherokee, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesCherokee, weekdayNamesAbbr: weekdayNamesCherokeeAbbr}, - "7C5C": {tags: []string{"chr-Cher"}, localMonth: localMonthsNameCherokee, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesCherokee, weekdayNamesAbbr: weekdayNamesCherokeeAbbr}, - "45C": {tags: []string{"chr-Cher-US"}, localMonth: localMonthsNameCherokee, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesCherokee, weekdayNamesAbbr: weekdayNamesCherokeeAbbr}, - "4": {tags: []string{"zh-Hans"}, localMonth: localMonthsNameChinese1, apFmt: nfp.AmPm[2], weekdayNames: weekdayNamesChinese, weekdayNamesAbbr: weekdayNamesChineseAbbr2}, - "7804": {tags: []string{"zh"}, localMonth: localMonthsNameChinese1, apFmt: nfp.AmPm[2], weekdayNames: weekdayNamesChinese, weekdayNamesAbbr: weekdayNamesChineseAbbr2}, - "804": {tags: []string{"zh-CN"}, localMonth: localMonthsNameChinese1, apFmt: nfp.AmPm[2], weekdayNames: weekdayNamesChinese, weekdayNamesAbbr: weekdayNamesChineseAbbr}, - "1004": {tags: []string{"zh-SG"}, localMonth: localMonthsNameChinese2, apFmt: nfp.AmPm[2], weekdayNames: weekdayNamesChinese, weekdayNamesAbbr: weekdayNamesChineseAbbr}, - "7C04": {tags: []string{"zh-Hant"}, localMonth: localMonthsNameChinese3, apFmt: nfp.AmPm[2], weekdayNames: weekdayNamesChinese, weekdayNamesAbbr: weekdayNamesChineseAbbr2}, - "C04": {tags: []string{"zh-HK"}, localMonth: localMonthsNameChinese2, apFmt: nfp.AmPm[2], weekdayNames: weekdayNamesChinese, weekdayNamesAbbr: weekdayNamesChineseAbbr2}, - "1404": {tags: []string{"zh-MO"}, localMonth: localMonthsNameChinese3, apFmt: nfp.AmPm[2], weekdayNames: weekdayNamesChinese, weekdayNamesAbbr: weekdayNamesChineseAbbr2}, - "404": {tags: []string{"zh-TW"}, localMonth: localMonthsNameChinese3, apFmt: nfp.AmPm[2], weekdayNames: weekdayNamesChinese, weekdayNamesAbbr: weekdayNamesChineseAbbr2}, - "9": {tags: []string{"en"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, - "1000": {tags: []string{ + // supportedLanguageInfo directly maps the supported language decimal ID and tags. + supportedLanguageInfo = map[int]languageInfo{ + 54: {tags: []string{"af"}, localMonth: localMonthsNameAfrikaans, apFmt: apFmtAfrikaans, weekdayNames: weekdayNamesAfrikaans, weekdayNamesAbbr: weekdayNamesAfrikaansAbbr}, + 1078: {tags: []string{"af-ZA"}, localMonth: localMonthsNameAfrikaans, apFmt: apFmtAfrikaans, weekdayNames: weekdayNamesAfrikaans, weekdayNamesAbbr: weekdayNamesAfrikaansAbbr}, + 28: {tags: []string{"sq"}, localMonth: localMonthsNameAlbanian, apFmt: apFmtAlbanian, weekdayNames: weekdayNamesAlbanian, weekdayNamesAbbr: weekdayNamesAlbanianAbbr}, + 1052: {tags: []string{"sq-AL"}, localMonth: localMonthsNameAlbanian, apFmt: apFmtAlbanian, weekdayNames: weekdayNamesAlbanian, weekdayNamesAbbr: weekdayNamesAlbanianAbbr}, + 132: {tags: []string{"gsw"}, localMonth: localMonthsNameAlsatian, apFmt: apFmtAlsatian, weekdayNames: weekdayNamesAlsatian, weekdayNamesAbbr: weekdayNamesAlsatianAbbr}, + 1156: {tags: []string{"gsw-FR"}, localMonth: localMonthsNameAlsatianFrance, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesAlsatianFrance, weekdayNamesAbbr: weekdayNamesAlsatianFranceAbbr}, + 94: {tags: []string{"am"}, localMonth: localMonthsNameAmharic, apFmt: apFmtAmharic, weekdayNames: weekdayNamesAmharic, weekdayNamesAbbr: weekdayNamesAmharicAbbr}, + 1118: {tags: []string{"am-ET"}, localMonth: localMonthsNameAmharic, apFmt: apFmtAmharic, weekdayNames: weekdayNamesAmharic, weekdayNamesAbbr: weekdayNamesAmharicAbbr}, + 1: {tags: []string{"ar"}, localMonth: localMonthsNameArabic, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr}, + 5121: {tags: []string{"ar-DZ"}, localMonth: localMonthsNameArabic, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr}, + 15361: {tags: []string{"ar-BH"}, localMonth: localMonthsNameArabic, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr}, + 3073: {tags: []string{"ar-EG"}, localMonth: localMonthsNameArabic, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr}, + 2049: {tags: []string{"ar-IQ"}, localMonth: localMonthsNameArabicIraq, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr}, + 11265: {tags: []string{"ar-JO"}, localMonth: localMonthsNameArabicIraq, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr}, + 13313: {tags: []string{"ar-KW"}, localMonth: localMonthsNameArabic, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr}, + 12289: {tags: []string{"ar-LB"}, localMonth: localMonthsNameArabicIraq, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr}, + 6145: {tags: []string{"ar-MA"}, localMonth: localMonthsNameArabic, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr}, + 8193: {tags: []string{"ar-OM"}, localMonth: localMonthsNameArabic, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr}, + 16385: {tags: []string{"ar-QA"}, localMonth: localMonthsNameArabic, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr}, + 1025: {tags: []string{"ar-SA"}, localMonth: localMonthsNameArabic, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr}, + 10241: {tags: []string{"ar-SY"}, localMonth: localMonthsNameArabicIraq, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr}, + 7169: {tags: []string{"ar-TN"}, localMonth: localMonthsNameArabic, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr}, + 14337: {tags: []string{"ar-AE"}, localMonth: localMonthsNameArabic, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr}, + 9217: {tags: []string{"ar-YE"}, localMonth: localMonthsNameArabic, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr}, + 43: {tags: []string{"hy"}, localMonth: localMonthsNameArmenian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesArmenian, weekdayNamesAbbr: weekdayNamesArmenianAbbr}, + 1067: {tags: []string{"hy-AM"}, localMonth: localMonthsNameArmenian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesArmenian, weekdayNamesAbbr: weekdayNamesArmenianAbbr}, + 77: {tags: []string{"as"}, localMonth: localMonthsNameAssamese, apFmt: apFmtAssamese, weekdayNames: weekdayNamesAssamese, weekdayNamesAbbr: weekdayNamesAssameseAbbr}, + 1101: {tags: []string{"as-IN"}, localMonth: localMonthsNameAssamese, apFmt: apFmtAssamese, weekdayNames: weekdayNamesAssamese, weekdayNamesAbbr: weekdayNamesAssameseAbbr}, + 29740: {tags: []string{"az-Cyrl"}, localMonth: localMonthsNameAzerbaijaniCyrillic, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesAzerbaijaniCyrillic, weekdayNamesAbbr: weekdayNamesAzerbaijaniCyrillicAbbr}, + 2092: {tags: []string{"az-Cyrl-AZ"}, localMonth: localMonthsNameAzerbaijaniCyrillic, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesAzerbaijaniCyrillic, weekdayNamesAbbr: weekdayNamesAzerbaijaniCyrillicAbbr}, + 44: {tags: []string{"az"}, localMonth: localMonthsNameAzerbaijani, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesAzerbaijani, weekdayNamesAbbr: weekdayNamesAzerbaijaniAbbr}, + 30764: {tags: []string{"az-Latn"}, localMonth: localMonthsNameAzerbaijani, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesAzerbaijani, weekdayNamesAbbr: weekdayNamesAzerbaijaniAbbr}, + 1068: {tags: []string{"az-Latn-AZ"}, localMonth: localMonthsNameAzerbaijani, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesAzerbaijani, weekdayNamesAbbr: weekdayNamesAzerbaijaniAbbr}, + 69: {tags: []string{"bn"}, localMonth: localMonthsNameBangla, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBangla, weekdayNamesAbbr: weekdayNamesBanglaAbbr}, + 2117: {tags: []string{"bn-BD"}, localMonth: localMonthsNameBangla, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBangla, weekdayNamesAbbr: weekdayNamesBanglaAbbr}, + 1093: {tags: []string{"bn-IN"}, localMonth: localMonthsNameBangla, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBangla, weekdayNamesAbbr: weekdayNamesBanglaAbbr}, + 109: {tags: []string{"ba"}, localMonth: localMonthsNameBashkir, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBashkir, weekdayNamesAbbr: weekdayNamesBashkirAbbr}, + 1133: {tags: []string{"ba-RU"}, localMonth: localMonthsNameBashkir, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBashkir, weekdayNamesAbbr: weekdayNamesBashkirAbbr}, + 45: {tags: []string{"eu"}, localMonth: localMonthsNameBasque, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBasque, weekdayNamesAbbr: weekdayNamesBasqueAbbr}, + 1069: {tags: []string{"eu-ES"}, localMonth: localMonthsNameBasque, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBasque, weekdayNamesAbbr: weekdayNamesBasqueAbbr}, + 35: {tags: []string{"be"}, localMonth: localMonthsNameBelarusian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBelarusian, weekdayNamesAbbr: weekdayNamesBelarusianAbbr}, + 1059: {tags: []string{"be-BY"}, localMonth: localMonthsNameBelarusian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBelarusian, weekdayNamesAbbr: weekdayNamesBelarusianAbbr}, + 25626: {tags: []string{"bs-Cyrl"}, localMonth: localMonthsNameBosnianCyrillic, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBosnianCyrillic, weekdayNamesAbbr: weekdayNamesBosnianCyrillicAbbr}, + 8218: {tags: []string{"bs-Cyrl-BA"}, localMonth: localMonthsNameBosnianCyrillic, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBosnianCyrillic, weekdayNamesAbbr: weekdayNamesBosnianCyrillicAbbr}, + 26650: {tags: []string{"bs-Latn"}, localMonth: localMonthsNameBosnian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBosnian, weekdayNamesAbbr: weekdayNamesBosnianAbbr}, + 30746: {tags: []string{"bs"}, localMonth: localMonthsNameBosnian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBosnian, weekdayNamesAbbr: weekdayNamesBosnianAbbr}, + 5146: {tags: []string{"bs-Latn-BA"}, localMonth: localMonthsNameBosnian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBosnian, weekdayNamesAbbr: weekdayNamesBosnianAbbr}, + 126: {tags: []string{"br"}, localMonth: localMonthsNameBreton, apFmt: apFmtBreton, weekdayNames: weekdayNamesBreton, weekdayNamesAbbr: weekdayNamesBretonAbbr}, + 1150: {tags: []string{"br-FR"}, localMonth: localMonthsNameBreton, apFmt: apFmtBreton, weekdayNames: weekdayNamesBreton, weekdayNamesAbbr: weekdayNamesBretonAbbr}, + 2: {tags: []string{"bg"}, localMonth: localMonthsNameBulgarian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBulgarian, weekdayNamesAbbr: weekdayNamesBulgarianAbbr}, + 1026: {tags: []string{"bg-BG"}, localMonth: localMonthsNameBulgarian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesBulgarian, weekdayNamesAbbr: weekdayNamesBulgarianAbbr}, + 85: {tags: []string{"my"}, localMonth: localMonthsNameBurmese, apFmt: apFmtBurmese, weekdayNames: weekdayNamesBurmese, weekdayNamesAbbr: weekdayNamesBurmese}, + 1109: {tags: []string{"my-MM"}, localMonth: localMonthsNameBurmese, apFmt: apFmtBurmese, weekdayNames: weekdayNamesBurmese, weekdayNamesAbbr: weekdayNamesBurmese}, + 3: {tags: []string{"ca"}, localMonth: localMonthsNameValencian, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesValencian, weekdayNamesAbbr: weekdayNamesValencianAbbr}, + 1027: {tags: []string{"ca-ES"}, localMonth: localMonthsNameValencian, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesValencian, weekdayNamesAbbr: weekdayNamesValencianAbbr}, + 1119: {tags: []string{"tzm-Arab-MA"}, localMonth: localMonthsNameArabicIraq, apFmt: apFmtArabic, weekdayNames: weekdayNamesArabic, weekdayNamesAbbr: weekdayNamesArabicAbbr}, + 146: {tags: []string{"ku"}, localMonth: localMonthsNameCentralKurdish, apFmt: apFmtCentralKurdish, weekdayNames: weekdayNamesCentralKurdish, weekdayNamesAbbr: weekdayNamesCentralKurdish}, + 31890: {tags: []string{"ku-Arab"}, localMonth: localMonthsNameCentralKurdish, apFmt: apFmtCentralKurdish, weekdayNames: weekdayNamesCentralKurdish, weekdayNamesAbbr: weekdayNamesCentralKurdish}, + 1170: {tags: []string{"ku-Arab-IQ"}, localMonth: localMonthsNameCentralKurdish, apFmt: apFmtCentralKurdish, weekdayNames: weekdayNamesCentralKurdish, weekdayNamesAbbr: weekdayNamesCentralKurdish}, + 92: {tags: []string{"chr"}, localMonth: localMonthsNameCherokee, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesCherokee, weekdayNamesAbbr: weekdayNamesCherokeeAbbr}, + 31836: {tags: []string{"chr-Cher"}, localMonth: localMonthsNameCherokee, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesCherokee, weekdayNamesAbbr: weekdayNamesCherokeeAbbr}, + 1116: {tags: []string{"chr-Cher-US"}, localMonth: localMonthsNameCherokee, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesCherokee, weekdayNamesAbbr: weekdayNamesCherokeeAbbr}, + 4: {tags: []string{"zh-Hans"}, localMonth: localMonthsNameChinese1, apFmt: nfp.AmPm[2], weekdayNames: weekdayNamesChinese, weekdayNamesAbbr: weekdayNamesChineseAbbr2}, + 30724: {tags: []string{"zh"}, localMonth: localMonthsNameChinese1, apFmt: nfp.AmPm[2], weekdayNames: weekdayNamesChinese, weekdayNamesAbbr: weekdayNamesChineseAbbr2}, + 2052: {tags: []string{"zh-CN"}, localMonth: localMonthsNameChinese1, apFmt: nfp.AmPm[2], weekdayNames: weekdayNamesChinese, weekdayNamesAbbr: weekdayNamesChineseAbbr}, + 4100: {tags: []string{"zh-SG"}, localMonth: localMonthsNameChinese2, apFmt: nfp.AmPm[2], weekdayNames: weekdayNamesChinese, weekdayNamesAbbr: weekdayNamesChineseAbbr}, + 31748: {tags: []string{"zh-Hant"}, localMonth: localMonthsNameChinese3, apFmt: nfp.AmPm[2], weekdayNames: weekdayNamesChinese, weekdayNamesAbbr: weekdayNamesChineseAbbr2}, + 3076: {tags: []string{"zh-HK"}, localMonth: localMonthsNameChinese2, apFmt: nfp.AmPm[2], weekdayNames: weekdayNamesChinese, weekdayNamesAbbr: weekdayNamesChineseAbbr2}, + 5124: {tags: []string{"zh-MO"}, localMonth: localMonthsNameChinese3, apFmt: nfp.AmPm[2], weekdayNames: weekdayNamesChinese, weekdayNamesAbbr: weekdayNamesChineseAbbr2}, + 1028: {tags: []string{"zh-TW"}, localMonth: localMonthsNameChinese3, apFmt: nfp.AmPm[2], weekdayNames: weekdayNamesChinese, weekdayNamesAbbr: weekdayNamesChineseAbbr2}, + 9: {tags: []string{"en"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + 4096: {tags: []string{ "aa", "aa-DJ", "aa-ER", "aa-ER", "aa-NA", "agq", "agq-CM", "ak", "ak-GH", "sq-ML", "gsw-LI", "gsw-CH", "ar-TD", "ar-KM", "ar-DJ", "ar-ER", "ar-IL", "ar-MR", "ar-PS", "ar-SO", "ar-SS", "ar-SD", "ar-001", "ast", "ast-ES", "asa", "asa-TZ", "ksf", "ksf-CM", @@ -837,333 +838,335 @@ var ( "vai-Vaii-LR", "vai-Latn-LR", "vai-Latn", "vo", "vo-001", "vun", "vun-TZ", "wae", "wae-CH", "wal", "wae-ET", "yav", "yav-CM", "yo-BJ", "dje", "dje-NE", }, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, - "C09": {tags: []string{"en-AU"}, localMonth: localMonthsNameEnglish, apFmt: strings.ToLower(nfp.AmPm[0]), weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, - "2809": {tags: []string{"en-BZ"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, - "1009": {tags: []string{"en-CA"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, - "2409": {tags: []string{"en-029"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, - "3C09": {tags: []string{"en-HK"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, - "4009": {tags: []string{"en-IN"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, - "1809": {tags: []string{"en-IE"}, localMonth: localMonthsNameEnglish, apFmt: strings.ToLower(nfp.AmPm[0]), weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, - "2009": {tags: []string{"en-JM"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, - "4409": {tags: []string{"en-MY"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, - "1409": {tags: []string{"en-NZ"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, - "3409": {tags: []string{"en-PH"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, - "4809": {tags: []string{"en-SG"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, - "1C09": {tags: []string{"en-ZA"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, - "2C09": {tags: []string{"en-TT"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, - "4C09": {tags: []string{"en-AE"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, - "809": {tags: []string{"en-GB"}, localMonth: localMonthsNameEnglish, apFmt: strings.ToLower(nfp.AmPm[0]), weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, - "409": {tags: []string{"en-US"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, - "3009": {tags: []string{"en-ZW"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, - "25": {tags: []string{"et"}, localMonth: localMonthsNameEstonian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEstonian, weekdayNamesAbbr: weekdayNamesEstonianAbbr}, - "425": {tags: []string{"et-EE"}, localMonth: localMonthsNameEstonian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEstonian, weekdayNamesAbbr: weekdayNamesEstonianAbbr}, - "38": {tags: []string{"fo"}, localMonth: localMonthsNameFaroese, apFmt: apFmtFaroese, weekdayNames: weekdayNamesFaroese, weekdayNamesAbbr: weekdayNamesFaroeseAbbr}, - "438": {tags: []string{"fo-FO"}, localMonth: localMonthsNameFaroese, apFmt: apFmtFaroese, weekdayNames: weekdayNamesFaroese, weekdayNamesAbbr: weekdayNamesFaroeseAbbr}, - "64": {tags: []string{"fil"}, localMonth: localMonthsNameFilipino, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFilipino, weekdayNamesAbbr: weekdayNamesFilipinoAbbr}, - "464": {tags: []string{"fil-PH"}, localMonth: localMonthsNameFilipino, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFilipino, weekdayNamesAbbr: weekdayNamesFilipinoAbbr}, - "B": {tags: []string{"fi"}, localMonth: localMonthsNameFinnish, apFmt: apFmtFinnish, weekdayNames: weekdayNamesFinnish, weekdayNamesAbbr: weekdayNamesFinnishAbbr}, - "40B": {tags: []string{"fi-FI"}, localMonth: localMonthsNameFinnish, apFmt: apFmtFinnish, weekdayNames: weekdayNamesFinnish, weekdayNamesAbbr: weekdayNamesFinnishAbbr}, - "C": {tags: []string{"fr"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, - "80C": {tags: []string{"fr-BE"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, - "2C0C": {tags: []string{"fr-CM"}, localMonth: localMonthsNameFrench, apFmt: apFmtCameroon, weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, - "C0C": {tags: []string{"fr-CA"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, - "1C0C": {tags: []string{"fr-029"}, localMonth: localMonthsNameCaribbean, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, - "240C": {tags: []string{"fr-CD"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, - "300C": {tags: []string{"fr-CI"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, - "40C": {tags: []string{"fr-FR"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, - "3C0C": {tags: []string{"fr-HT"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, - "140C": {tags: []string{"fr-LU"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, - "340C": {tags: []string{"fr-ML"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, - "380C": {tags: []string{"fr-MA"}, localMonth: localMonthsNameMorocco, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, - "180C": {tags: []string{"fr-MC"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, - "200C": {tags: []string{"fr-RE"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, - "280C": {tags: []string{"fr-SN"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, - "62": {tags: []string{"fy"}, localMonth: localMonthsNameFrisian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrisian, weekdayNamesAbbr: weekdayNamesFrisianAbbr}, - "462": {tags: []string{"fy-NL"}, localMonth: localMonthsNameFrisian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrisian, weekdayNamesAbbr: weekdayNamesFrisianAbbr}, - "67": {tags: []string{"ff"}, localMonth: localMonthsNameFulah, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFulah, weekdayNamesAbbr: weekdayNamesFulahAbbr}, - "7C67": {tags: []string{"ff-Latn"}, localMonth: localMonthsNameFulah, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFulah, weekdayNamesAbbr: weekdayNamesFulahAbbr}, - "467": {tags: []string{"ff-NG", "ff-Latn-NG"}, localMonth: localMonthsNameNigeria, apFmt: apFmtNigeria, weekdayNames: weekdayNamesNigeria, weekdayNamesAbbr: weekdayNamesNigeriaAbbr}, - "867": {tags: []string{"ff-SN"}, localMonth: localMonthsNameNigeria, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesNigeria, weekdayNamesAbbr: weekdayNamesNigeriaAbbr}, - "56": {tags: []string{"gl"}, localMonth: localMonthsNameGalician, apFmt: apFmtCuba, weekdayNames: weekdayNamesGalician, weekdayNamesAbbr: weekdayNamesGalicianAbbr}, - "456": {tags: []string{"gl-ES"}, localMonth: localMonthsNameGalician, apFmt: apFmtCuba, weekdayNames: weekdayNamesGalician, weekdayNamesAbbr: weekdayNamesGalicianAbbr}, - "37": {tags: []string{"ka"}, localMonth: localMonthsNameGeorgian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGeorgian, weekdayNamesAbbr: weekdayNamesGeorgianAbbr}, - "437": {tags: []string{"ka-GE"}, localMonth: localMonthsNameGeorgian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGeorgian, weekdayNamesAbbr: weekdayNamesGeorgianAbbr}, - "7": {tags: []string{"de"}, localMonth: localMonthsNameGerman, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGerman, weekdayNamesAbbr: weekdayNamesGermanAbbr}, - "C07": {tags: []string{"de-AT"}, localMonth: localMonthsNameAustria, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGerman, weekdayNamesAbbr: weekdayNamesGermanAbbr}, - "407": {tags: []string{"de-DE"}, localMonth: localMonthsNameGerman, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGerman, weekdayNamesAbbr: weekdayNamesGermanAbbr}, - "1407": {tags: []string{"de-LI"}, localMonth: localMonthsNameGerman, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGerman, weekdayNamesAbbr: weekdayNamesGermanAbbr}, - "807": {tags: []string{"de-CH"}, localMonth: localMonthsNameGerman, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGerman, weekdayNamesAbbr: weekdayNamesGermanAbbr}, - "8": {tags: []string{"el"}, localMonth: localMonthsNameGreek, apFmt: apFmtGreek, weekdayNames: weekdayNamesGreek, weekdayNamesAbbr: weekdayNamesGreekAbbr}, - "408": {tags: []string{"el-GR"}, localMonth: localMonthsNameGreek, apFmt: apFmtGreek, weekdayNames: weekdayNamesGreek, weekdayNamesAbbr: weekdayNamesGreekAbbr}, - "6F": {tags: []string{"kl"}, localMonth: localMonthsNameGreenlandic, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGreenlandic, weekdayNamesAbbr: weekdayNamesGreenlandicAbbr}, - "46F": {tags: []string{"kl-GL"}, localMonth: localMonthsNameGreenlandic, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGreenlandic, weekdayNamesAbbr: weekdayNamesGreenlandicAbbr}, - "74": {tags: []string{"gn"}, localMonth: localMonthsNameGuarani, apFmt: apFmtCuba, weekdayNames: weekdayNamesGuarani, weekdayNamesAbbr: weekdayNamesGuaraniAbbr}, - "474": {tags: []string{"gn-PY"}, localMonth: localMonthsNameGuarani, apFmt: apFmtCuba, weekdayNames: weekdayNamesGuarani, weekdayNamesAbbr: weekdayNamesGuaraniAbbr}, - "47": {tags: []string{"gu"}, localMonth: localMonthsNameGujarati, apFmt: apFmtGujarati, weekdayNames: weekdayNamesGujarati, weekdayNamesAbbr: weekdayNamesGujaratiAbbr}, - "447": {tags: []string{"gu-IN"}, localMonth: localMonthsNameGujarati, apFmt: apFmtGujarati, weekdayNames: weekdayNamesGujarati, weekdayNamesAbbr: weekdayNamesGujaratiAbbr}, - "68": {tags: []string{"ha"}, localMonth: localMonthsNameHausa, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesHausa, weekdayNamesAbbr: weekdayNamesHausaAbbr}, - "7C68": {tags: []string{"ha-Latn"}, localMonth: localMonthsNameHausa, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesHausa, weekdayNamesAbbr: weekdayNamesHausaAbbr}, - "468": {tags: []string{"ha-Latn-NG"}, localMonth: localMonthsNameHausa, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesHausa, weekdayNamesAbbr: weekdayNamesHausaAbbr}, - "75": {tags: []string{"haw"}, localMonth: localMonthsNameHawaiian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesHawaiian, weekdayNamesAbbr: weekdayNamesHawaiianAbbr}, - "475": {tags: []string{"haw-US"}, localMonth: localMonthsNameHawaiian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesHawaiian, weekdayNamesAbbr: weekdayNamesHawaiianAbbr}, - "D": {tags: []string{"he"}, localMonth: localMonthsNameHebrew, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesHebrew, weekdayNamesAbbr: weekdayNamesHebrewAbbr}, - "40D": {tags: []string{"he-IL"}, localMonth: localMonthsNameHebrew, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesHebrew, weekdayNamesAbbr: weekdayNamesHebrewAbbr}, - "39": {tags: []string{"hi"}, localMonth: localMonthsNameHindi, apFmt: apFmtHindi, weekdayNames: weekdayNamesHindi, weekdayNamesAbbr: weekdayNamesHindiAbbr}, - "439": {tags: []string{"hi-IN"}, localMonth: localMonthsNameHindi, apFmt: apFmtHindi, weekdayNames: weekdayNamesHindi, weekdayNamesAbbr: weekdayNamesHindiAbbr}, - "E": {tags: []string{"hu"}, localMonth: localMonthsNameHungarian, apFmt: apFmtHungarian, weekdayNames: weekdayNamesHungarian, weekdayNamesAbbr: weekdayNamesHungarianAbbr}, - "40E": {tags: []string{"hu-HU"}, localMonth: localMonthsNameHungarian, apFmt: apFmtHungarian, weekdayNames: weekdayNamesHungarian, weekdayNamesAbbr: weekdayNamesHungarianAbbr}, - "F": {tags: []string{"is"}, localMonth: localMonthsNameIcelandic, apFmt: apFmtIcelandic, weekdayNames: weekdayNamesIcelandic, weekdayNamesAbbr: weekdayNamesIcelandicAbbr}, - "40F": {tags: []string{"is-IS"}, localMonth: localMonthsNameIcelandic, apFmt: apFmtIcelandic, weekdayNames: weekdayNamesIcelandic, weekdayNamesAbbr: weekdayNamesIcelandicAbbr}, - "70": {tags: []string{"ig"}, localMonth: localMonthsNameIgbo, apFmt: apFmtIgbo, weekdayNames: weekdayNamesIgbo, weekdayNamesAbbr: weekdayNamesIgboAbbr}, - "470": {tags: []string{"ig-NG"}, localMonth: localMonthsNameIgbo, apFmt: apFmtIgbo, weekdayNames: weekdayNamesIgbo, weekdayNamesAbbr: weekdayNamesIgboAbbr}, - "21": {tags: []string{"id"}, localMonth: localMonthsNameIndonesian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesIndonesian, weekdayNamesAbbr: weekdayNamesIndonesianAbbr}, - "421": {tags: []string{"id-ID"}, localMonth: localMonthsNameIndonesian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesIndonesian, weekdayNamesAbbr: weekdayNamesIndonesianAbbr}, - "5D": {tags: []string{"iu"}, localMonth: localMonthsNameInuktitut, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesInuktitut, weekdayNamesAbbr: weekdayNamesInuktitutAbbr}, - "7C5D": {tags: []string{"iu-Latn"}, localMonth: localMonthsNameInuktitut, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesInuktitut, weekdayNamesAbbr: weekdayNamesInuktitutAbbr}, - "85D": {tags: []string{"iu-Latn-CA"}, localMonth: localMonthsNameInuktitut, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesInuktitut, weekdayNamesAbbr: weekdayNamesInuktitutAbbr}, - "785D": {tags: []string{"iu-Cans"}, localMonth: localMonthsNameSyllabics, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSyllabics, weekdayNamesAbbr: weekdayNamesSyllabicsAbbr}, - "45D": {tags: []string{"iu-Cans-CA"}, localMonth: localMonthsNameSyllabics, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSyllabics, weekdayNamesAbbr: weekdayNamesSyllabicsAbbr}, - "3C": {tags: []string{"ga"}, localMonth: localMonthsNameIrish, apFmt: apFmtIrish, weekdayNames: weekdayNamesIrish, weekdayNamesAbbr: weekdayNamesIrishAbbr}, - "83C": {tags: []string{"ga-IE"}, localMonth: localMonthsNameIrish, apFmt: apFmtIrish, weekdayNames: weekdayNamesIrish, weekdayNamesAbbr: weekdayNamesIrishAbbr}, - "10": {tags: []string{"it"}, localMonth: localMonthsNameItalian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesItalian, weekdayNamesAbbr: weekdayNamesItalianAbbr}, - "410": {tags: []string{"it-IT"}, localMonth: localMonthsNameItalian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesItalian, weekdayNamesAbbr: weekdayNamesItalianAbbr}, - "810": {tags: []string{"it-CH"}, localMonth: localMonthsNameItalian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesItalian, weekdayNamesAbbr: weekdayNamesItalianAbbr}, - "11": {tags: []string{"ja"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese, weekdayNames: weekdayNamesJapanese, weekdayNamesAbbr: weekdayNamesJapaneseAbbr}, - "411": {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese, weekdayNames: weekdayNamesJapanese, weekdayNamesAbbr: weekdayNamesJapaneseAbbr}, - "800411": {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese, weekdayNames: weekdayNamesJapanese, weekdayNamesAbbr: weekdayNamesJapaneseAbbr}, + 3081: {tags: []string{"en-AU"}, localMonth: localMonthsNameEnglish, apFmt: strings.ToLower(nfp.AmPm[0]), weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + 10249: {tags: []string{"en-BZ"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + 4105: {tags: []string{"en-CA"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + 9225: {tags: []string{"en-029"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + 15369: {tags: []string{"en-HK"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + 16393: {tags: []string{"en-IN"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + 6153: {tags: []string{"en-IE"}, localMonth: localMonthsNameEnglish, apFmt: strings.ToLower(nfp.AmPm[0]), weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + 8201: {tags: []string{"en-JM"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + 17417: {tags: []string{"en-MY"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + 5129: {tags: []string{"en-NZ"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + 13321: {tags: []string{"en-PH"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + 18441: {tags: []string{"en-SG"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + 7177: {tags: []string{"en-ZA"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + 11273: {tags: []string{"en-TT"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + 19465: {tags: []string{"en-AE"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + 2057: {tags: []string{"en-GB"}, localMonth: localMonthsNameEnglish, apFmt: strings.ToLower(nfp.AmPm[0]), weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + 1033: {tags: []string{"en-US"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + 12297: {tags: []string{"en-ZW"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + 37: {tags: []string{"et"}, localMonth: localMonthsNameEstonian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEstonian, weekdayNamesAbbr: weekdayNamesEstonianAbbr}, + 1061: {tags: []string{"et-EE"}, localMonth: localMonthsNameEstonian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEstonian, weekdayNamesAbbr: weekdayNamesEstonianAbbr}, + 56: {tags: []string{"fo"}, localMonth: localMonthsNameFaroese, apFmt: apFmtFaroese, weekdayNames: weekdayNamesFaroese, weekdayNamesAbbr: weekdayNamesFaroeseAbbr}, + 1080: {tags: []string{"fo-FO"}, localMonth: localMonthsNameFaroese, apFmt: apFmtFaroese, weekdayNames: weekdayNamesFaroese, weekdayNamesAbbr: weekdayNamesFaroeseAbbr}, + 100: {tags: []string{"fil"}, localMonth: localMonthsNameFilipino, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFilipino, weekdayNamesAbbr: weekdayNamesFilipinoAbbr}, + 1124: {tags: []string{"fil-PH"}, localMonth: localMonthsNameFilipino, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFilipino, weekdayNamesAbbr: weekdayNamesFilipinoAbbr}, + 11: {tags: []string{"fi"}, localMonth: localMonthsNameFinnish, apFmt: apFmtFinnish, weekdayNames: weekdayNamesFinnish, weekdayNamesAbbr: weekdayNamesFinnishAbbr}, + 1035: {tags: []string{"fi-FI"}, localMonth: localMonthsNameFinnish, apFmt: apFmtFinnish, weekdayNames: weekdayNamesFinnish, weekdayNamesAbbr: weekdayNamesFinnishAbbr}, + 12: {tags: []string{"fr"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, + 2060: {tags: []string{"fr-BE"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, + 11276: {tags: []string{"fr-CM"}, localMonth: localMonthsNameFrench, apFmt: apFmtCameroon, weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, + 3084: {tags: []string{"fr-CA"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, + 7180: {tags: []string{"fr-029"}, localMonth: localMonthsNameCaribbean, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, + 9228: {tags: []string{"fr-CD"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, + 12300: {tags: []string{"fr-CI"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, + 1036: {tags: []string{"fr-FR"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, + 15372: {tags: []string{"fr-HT"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, + 5132: {tags: []string{"fr-LU"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, + 13324: {tags: []string{"fr-ML"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, + 14348: {tags: []string{"fr-MA"}, localMonth: localMonthsNameMorocco, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, + 6156: {tags: []string{"fr-MC"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, + 8204: {tags: []string{"fr-RE"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, + 10252: {tags: []string{"fr-SN"}, localMonth: localMonthsNameFrench, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrench, weekdayNamesAbbr: weekdayNamesFrenchAbbr}, + 98: {tags: []string{"fy"}, localMonth: localMonthsNameFrisian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrisian, weekdayNamesAbbr: weekdayNamesFrisianAbbr}, + 1122: {tags: []string{"fy-NL"}, localMonth: localMonthsNameFrisian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFrisian, weekdayNamesAbbr: weekdayNamesFrisianAbbr}, + 103: {tags: []string{"ff"}, localMonth: localMonthsNameFulah, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFulah, weekdayNamesAbbr: weekdayNamesFulahAbbr}, + 31847: {tags: []string{"ff-Latn"}, localMonth: localMonthsNameFulah, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesFulah, weekdayNamesAbbr: weekdayNamesFulahAbbr}, + 1127: {tags: []string{"ff-NG", "ff-Latn-NG"}, localMonth: localMonthsNameNigeria, apFmt: apFmtNigeria, weekdayNames: weekdayNamesNigeria, weekdayNamesAbbr: weekdayNamesNigeriaAbbr}, + 2151: {tags: []string{"ff-SN"}, localMonth: localMonthsNameNigeria, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesNigeria, weekdayNamesAbbr: weekdayNamesNigeriaAbbr}, + 86: {tags: []string{"gl"}, localMonth: localMonthsNameGalician, apFmt: apFmtCuba, weekdayNames: weekdayNamesGalician, weekdayNamesAbbr: weekdayNamesGalicianAbbr}, + 1110: {tags: []string{"gl-ES"}, localMonth: localMonthsNameGalician, apFmt: apFmtCuba, weekdayNames: weekdayNamesGalician, weekdayNamesAbbr: weekdayNamesGalicianAbbr}, + 55: {tags: []string{"ka"}, localMonth: localMonthsNameGeorgian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGeorgian, weekdayNamesAbbr: weekdayNamesGeorgianAbbr}, + 1079: {tags: []string{"ka-GE"}, localMonth: localMonthsNameGeorgian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGeorgian, weekdayNamesAbbr: weekdayNamesGeorgianAbbr}, + 7: {tags: []string{"de"}, localMonth: localMonthsNameGerman, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGerman, weekdayNamesAbbr: weekdayNamesGermanAbbr}, + 3079: {tags: []string{"de-AT"}, localMonth: localMonthsNameAustria, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGerman, weekdayNamesAbbr: weekdayNamesGermanAbbr}, + 1031: {tags: []string{"de-DE"}, localMonth: localMonthsNameGerman, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGerman, weekdayNamesAbbr: weekdayNamesGermanAbbr}, + 5127: {tags: []string{"de-LI"}, localMonth: localMonthsNameGerman, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGerman, weekdayNamesAbbr: weekdayNamesGermanAbbr}, + 2055: {tags: []string{"de-CH"}, localMonth: localMonthsNameGerman, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGerman, weekdayNamesAbbr: weekdayNamesGermanAbbr}, + 8: {tags: []string{"el"}, localMonth: localMonthsNameGreek, apFmt: apFmtGreek, weekdayNames: weekdayNamesGreek, weekdayNamesAbbr: weekdayNamesGreekAbbr}, + 1032: {tags: []string{"el-GR"}, localMonth: localMonthsNameGreek, apFmt: apFmtGreek, weekdayNames: weekdayNamesGreek, weekdayNamesAbbr: weekdayNamesGreekAbbr}, + 111: {tags: []string{"kl"}, localMonth: localMonthsNameGreenlandic, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGreenlandic, weekdayNamesAbbr: weekdayNamesGreenlandicAbbr}, + 1135: {tags: []string{"kl-GL"}, localMonth: localMonthsNameGreenlandic, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesGreenlandic, weekdayNamesAbbr: weekdayNamesGreenlandicAbbr}, + 116: {tags: []string{"gn"}, localMonth: localMonthsNameGuarani, apFmt: apFmtCuba, weekdayNames: weekdayNamesGuarani, weekdayNamesAbbr: weekdayNamesGuaraniAbbr}, + 1140: {tags: []string{"gn-PY"}, localMonth: localMonthsNameGuarani, apFmt: apFmtCuba, weekdayNames: weekdayNamesGuarani, weekdayNamesAbbr: weekdayNamesGuaraniAbbr}, + 71: {tags: []string{"gu"}, localMonth: localMonthsNameGujarati, apFmt: apFmtGujarati, weekdayNames: weekdayNamesGujarati, weekdayNamesAbbr: weekdayNamesGujaratiAbbr}, + 1095: {tags: []string{"gu-IN"}, localMonth: localMonthsNameGujarati, apFmt: apFmtGujarati, weekdayNames: weekdayNamesGujarati, weekdayNamesAbbr: weekdayNamesGujaratiAbbr}, + 104: {tags: []string{"ha"}, localMonth: localMonthsNameHausa, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesHausa, weekdayNamesAbbr: weekdayNamesHausaAbbr}, + 31848: {tags: []string{"ha-Latn"}, localMonth: localMonthsNameHausa, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesHausa, weekdayNamesAbbr: weekdayNamesHausaAbbr}, + 1128: {tags: []string{"ha-Latn-NG"}, localMonth: localMonthsNameHausa, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesHausa, weekdayNamesAbbr: weekdayNamesHausaAbbr}, + 117: {tags: []string{"haw"}, localMonth: localMonthsNameHawaiian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesHawaiian, weekdayNamesAbbr: weekdayNamesHawaiianAbbr}, + 1141: {tags: []string{"haw-US"}, localMonth: localMonthsNameHawaiian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesHawaiian, weekdayNamesAbbr: weekdayNamesHawaiianAbbr}, + 13: {tags: []string{"he"}, localMonth: localMonthsNameHebrew, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesHebrew, weekdayNamesAbbr: weekdayNamesHebrewAbbr}, + 1037: {tags: []string{"he-IL"}, localMonth: localMonthsNameHebrew, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesHebrew, weekdayNamesAbbr: weekdayNamesHebrewAbbr}, + 57: {tags: []string{"hi"}, localMonth: localMonthsNameHindi, apFmt: apFmtHindi, weekdayNames: weekdayNamesHindi, weekdayNamesAbbr: weekdayNamesHindiAbbr}, + 1081: {tags: []string{"hi-IN"}, localMonth: localMonthsNameHindi, apFmt: apFmtHindi, weekdayNames: weekdayNamesHindi, weekdayNamesAbbr: weekdayNamesHindiAbbr}, + 14: {tags: []string{"hu"}, localMonth: localMonthsNameHungarian, apFmt: apFmtHungarian, weekdayNames: weekdayNamesHungarian, weekdayNamesAbbr: weekdayNamesHungarianAbbr}, + 1038: {tags: []string{"hu-HU"}, localMonth: localMonthsNameHungarian, apFmt: apFmtHungarian, weekdayNames: weekdayNamesHungarian, weekdayNamesAbbr: weekdayNamesHungarianAbbr}, + 15: {tags: []string{"is"}, localMonth: localMonthsNameIcelandic, apFmt: apFmtIcelandic, weekdayNames: weekdayNamesIcelandic, weekdayNamesAbbr: weekdayNamesIcelandicAbbr}, + 1039: {tags: []string{"is-IS"}, localMonth: localMonthsNameIcelandic, apFmt: apFmtIcelandic, weekdayNames: weekdayNamesIcelandic, weekdayNamesAbbr: weekdayNamesIcelandicAbbr}, + 112: {tags: []string{"ig"}, localMonth: localMonthsNameIgbo, apFmt: apFmtIgbo, weekdayNames: weekdayNamesIgbo, weekdayNamesAbbr: weekdayNamesIgboAbbr}, + 1136: {tags: []string{"ig-NG"}, localMonth: localMonthsNameIgbo, apFmt: apFmtIgbo, weekdayNames: weekdayNamesIgbo, weekdayNamesAbbr: weekdayNamesIgboAbbr}, + 33: {tags: []string{"id"}, localMonth: localMonthsNameIndonesian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesIndonesian, weekdayNamesAbbr: weekdayNamesIndonesianAbbr}, + 1057: {tags: []string{"id-ID"}, localMonth: localMonthsNameIndonesian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesIndonesian, weekdayNamesAbbr: weekdayNamesIndonesianAbbr}, + 93: {tags: []string{"iu"}, localMonth: localMonthsNameInuktitut, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesInuktitut, weekdayNamesAbbr: weekdayNamesInuktitutAbbr}, + 31837: {tags: []string{"iu-Latn"}, localMonth: localMonthsNameInuktitut, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesInuktitut, weekdayNamesAbbr: weekdayNamesInuktitutAbbr}, + 2141: {tags: []string{"iu-Latn-CA"}, localMonth: localMonthsNameInuktitut, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesInuktitut, weekdayNamesAbbr: weekdayNamesInuktitutAbbr}, + 30813: {tags: []string{"iu-Cans"}, localMonth: localMonthsNameSyllabics, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSyllabics, weekdayNamesAbbr: weekdayNamesSyllabicsAbbr}, + 1117: {tags: []string{"iu-Cans-CA"}, localMonth: localMonthsNameSyllabics, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSyllabics, weekdayNamesAbbr: weekdayNamesSyllabicsAbbr}, + 60: {tags: []string{"ga"}, localMonth: localMonthsNameIrish, apFmt: apFmtIrish, weekdayNames: weekdayNamesIrish, weekdayNamesAbbr: weekdayNamesIrishAbbr}, + 2108: {tags: []string{"ga-IE"}, localMonth: localMonthsNameIrish, apFmt: apFmtIrish, weekdayNames: weekdayNamesIrish, weekdayNamesAbbr: weekdayNamesIrishAbbr}, + 16: {tags: []string{"it"}, localMonth: localMonthsNameItalian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesItalian, weekdayNamesAbbr: weekdayNamesItalianAbbr}, + 1040: {tags: []string{"it-IT"}, localMonth: localMonthsNameItalian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesItalian, weekdayNamesAbbr: weekdayNamesItalianAbbr}, + 2064: {tags: []string{"it-CH"}, localMonth: localMonthsNameItalian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesItalian, weekdayNamesAbbr: weekdayNamesItalianAbbr}, + 17: {tags: []string{"ja"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese, weekdayNames: weekdayNamesJapanese, weekdayNamesAbbr: weekdayNamesJapaneseAbbr}, + 1041: {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese, weekdayNames: weekdayNamesJapanese, weekdayNamesAbbr: weekdayNamesJapaneseAbbr}, + 75: {tags: []string{"kn"}, localMonth: localMonthsNameKannada, apFmt: apFmtKannada, weekdayNames: weekdayNamesKannada, weekdayNamesAbbr: weekdayNamesKannadaAbbr}, + 1099: {tags: []string{"kn-IN"}, localMonth: localMonthsNameKannada, apFmt: apFmtKannada, weekdayNames: weekdayNamesKannada, weekdayNamesAbbr: weekdayNamesKannadaAbbr}, + 1137: {tags: []string{"kr-Latn-NG"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + 96: {tags: []string{"ks"}, localMonth: localMonthsNameKashmiri, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesKashmiri, weekdayNamesAbbr: weekdayNamesKashmiriAbbr}, + 1120: {tags: []string{"ks-Arab"}, localMonth: localMonthsNameKashmiri, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesKashmiri, weekdayNamesAbbr: weekdayNamesKashmiriAbbr}, + 2144: {tags: []string{"ks-Deva-IN"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + 63: {tags: []string{"kk"}, localMonth: localMonthsNameKazakh, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesKazakh, weekdayNamesAbbr: weekdayNamesKazakhAbbr}, + 1087: {tags: []string{"kk-KZ"}, localMonth: localMonthsNameKazakh, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesKazakh, weekdayNamesAbbr: weekdayNamesKazakhAbbr}, + 83: {tags: []string{"km"}, localMonth: localMonthsNameKhmer, apFmt: apFmtKhmer, weekdayNames: weekdayNamesKhmer, weekdayNamesAbbr: weekdayNamesKhmerAbbr}, + 1107: {tags: []string{"km-KH"}, localMonth: localMonthsNameKhmer, apFmt: apFmtKhmer, weekdayNames: weekdayNamesKhmer, weekdayNamesAbbr: weekdayNamesKhmerAbbr}, + 134: {tags: []string{"quc"}, localMonth: localMonthsNameKiche, apFmt: apFmtCuba, weekdayNames: weekdayNamesKiche, weekdayNamesAbbr: weekdayNamesKicheAbbr}, + 1158: {tags: []string{"quc-Latn-GT"}, localMonth: localMonthsNameKiche, apFmt: apFmtCuba, weekdayNames: weekdayNamesKiche, weekdayNamesAbbr: weekdayNamesKicheAbbr}, + 135: {tags: []string{"rw"}, localMonth: localMonthsNameKinyarwanda, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesKinyarwanda, weekdayNamesAbbr: weekdayNamesKinyarwandaAbbr}, + 1159: {tags: []string{"rw-RW"}, localMonth: localMonthsNameKinyarwanda, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesKinyarwanda, weekdayNamesAbbr: weekdayNamesKinyarwandaAbbr}, + 65: {tags: []string{"sw"}, localMonth: localMonthsNameKiswahili, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesKiswahili, weekdayNamesAbbr: weekdayNamesKiswahiliAbbr}, + 1089: {tags: []string{"sw-KE"}, localMonth: localMonthsNameKiswahili, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesKiswahili, weekdayNamesAbbr: weekdayNamesKiswahiliAbbr}, + 87: {tags: []string{"kok"}, localMonth: localMonthsNameKonkani, apFmt: apFmtKonkani, weekdayNames: weekdayNamesKonkani, weekdayNamesAbbr: weekdayNamesKonkaniAbbr}, + 1111: {tags: []string{"kok-IN"}, localMonth: localMonthsNameKonkani, apFmt: apFmtKonkani, weekdayNames: weekdayNamesKonkani, weekdayNamesAbbr: weekdayNamesKonkaniAbbr}, + 18: {tags: []string{"ko"}, localMonth: localMonthsNameKorean, apFmt: apFmtKorean, weekdayNames: weekdayNamesKorean, weekdayNamesAbbr: weekdayNamesKoreanAbbr}, + 1042: {tags: []string{"ko-KR"}, localMonth: localMonthsNameKorean, apFmt: apFmtKorean, weekdayNames: weekdayNamesKorean, weekdayNamesAbbr: weekdayNamesKoreanAbbr}, + 64: {tags: []string{"ky"}, localMonth: localMonthsNameKyrgyz, apFmt: apFmtKyrgyz, weekdayNames: weekdayNamesKyrgyz, weekdayNamesAbbr: weekdayNamesKyrgyzAbbr}, + 1088: {tags: []string{"ky-KG"}, localMonth: localMonthsNameKyrgyz, apFmt: apFmtKyrgyz, weekdayNames: weekdayNamesKyrgyz, weekdayNamesAbbr: weekdayNamesKyrgyzAbbr}, + 84: {tags: []string{"lo"}, localMonth: localMonthsNameLao, apFmt: apFmtLao, weekdayNames: weekdayNamesLao, weekdayNamesAbbr: weekdayNamesLaoAbbr}, + 1108: {tags: []string{"lo-LA"}, localMonth: localMonthsNameLao, apFmt: apFmtLao, weekdayNames: weekdayNamesLao, weekdayNamesAbbr: weekdayNamesLaoAbbr}, + 1142: {tags: []string{"la-VA"}, localMonth: localMonthsNameLatin, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesLatin, weekdayNamesAbbr: weekdayNamesLatinAbbr}, + 38: {tags: []string{"lv"}, localMonth: localMonthsNameLatvian, apFmt: apFmtLatvian, weekdayNames: weekdayNamesLatvian, weekdayNamesAbbr: weekdayNamesLatvianAbbr}, + 1062: {tags: []string{"lv-LV"}, localMonth: localMonthsNameLatvian, apFmt: apFmtLatvian, weekdayNames: weekdayNamesLatvian, weekdayNamesAbbr: weekdayNamesLatvianAbbr}, + 39: {tags: []string{"lt"}, localMonth: localMonthsNameLithuanian, apFmt: apFmtLithuanian, weekdayNames: weekdayNamesLithuanian, weekdayNamesAbbr: weekdayNamesLithuanianAbbr}, + 1063: {tags: []string{"lt-LT"}, localMonth: localMonthsNameLithuanian, apFmt: apFmtLithuanian, weekdayNames: weekdayNamesLithuanian, weekdayNamesAbbr: weekdayNamesLithuanianAbbr}, + 31790: {tags: []string{"dsb"}, localMonth: localMonthsNameLowerSorbian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesLowerSorbian, weekdayNamesAbbr: weekdayNamesLowerSorbianAbbr}, + 2094: {tags: []string{"dsb-DE"}, localMonth: localMonthsNameLowerSorbian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesLowerSorbian, weekdayNamesAbbr: weekdayNamesLowerSorbianAbbr}, + 110: {tags: []string{"lb"}, localMonth: localMonthsNameLuxembourgish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesLuxembourgish, weekdayNamesAbbr: weekdayNamesLuxembourgishAbbr}, + 1134: {tags: []string{"lb-LU"}, localMonth: localMonthsNameLuxembourgish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesLuxembourgish, weekdayNamesAbbr: weekdayNamesLuxembourgishAbbr}, + 47: {tags: []string{"mk"}, localMonth: localMonthsNameMacedonian, apFmt: apFmtMacedonian, weekdayNames: weekdayNamesMacedonian, weekdayNamesAbbr: weekdayNamesMacedonianAbbr}, + 1071: {tags: []string{"mk-MK"}, localMonth: localMonthsNameMacedonian, apFmt: apFmtMacedonian, weekdayNames: weekdayNamesMacedonian, weekdayNamesAbbr: weekdayNamesMacedonianAbbr}, + 62: {tags: []string{"ms"}, localMonth: localMonthsNameMalay, apFmt: apFmtMalay, weekdayNames: weekdayNamesMalay, weekdayNamesAbbr: weekdayNamesMalayAbbr}, + 2110: {tags: []string{"ms-BN"}, localMonth: localMonthsNameMalay, apFmt: apFmtMalay, weekdayNames: weekdayNamesMalay, weekdayNamesAbbr: weekdayNamesMalayAbbr}, + 1086: {tags: []string{"ms-MY"}, localMonth: localMonthsNameMalay, apFmt: apFmtMalay, weekdayNames: weekdayNamesMalay, weekdayNamesAbbr: weekdayNamesMalayAbbr}, + 76: {tags: []string{"ml"}, localMonth: localMonthsNameMalayalam, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesMalayalam, weekdayNamesAbbr: weekdayNamesMalayalamAbbr}, + 1100: {tags: []string{"ml-IN"}, localMonth: localMonthsNameMalayalam, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesMalayalam, weekdayNamesAbbr: weekdayNamesMalayalamAbbr}, + 58: {tags: []string{"mt"}, localMonth: localMonthsNameMaltese, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesMaltese, weekdayNamesAbbr: weekdayNamesMalteseAbbr}, + 1082: {tags: []string{"mt-MT"}, localMonth: localMonthsNameMaltese, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesMaltese, weekdayNamesAbbr: weekdayNamesMalteseAbbr}, + 129: {tags: []string{"mi"}, localMonth: localMonthsNameMaori, apFmt: apFmtCuba, weekdayNames: weekdayNamesMaori, weekdayNamesAbbr: weekdayNamesMaoriAbbr}, + 1153: {tags: []string{"mi-NZ"}, localMonth: localMonthsNameMaori, apFmt: apFmtCuba, weekdayNames: weekdayNamesMaori, weekdayNamesAbbr: weekdayNamesMaoriAbbr}, + 122: {tags: []string{"arn"}, localMonth: localMonthsNameMapudungun, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesMapudungun, weekdayNamesAbbr: weekdayNamesMapudungunAbbr}, + 1146: {tags: []string{"arn-CL"}, localMonth: localMonthsNameMapudungun, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesMapudungun, weekdayNamesAbbr: weekdayNamesMapudungunAbbr}, + 78: {tags: []string{"mr"}, localMonth: localMonthsNameMarathi, apFmt: apFmtKonkani, weekdayNames: weekdayNamesMarathi, weekdayNamesAbbr: weekdayNamesMarathiAbbr}, + 1102: {tags: []string{"mr-IN"}, localMonth: localMonthsNameMarathi, apFmt: apFmtKonkani, weekdayNames: weekdayNamesMarathi, weekdayNamesAbbr: weekdayNamesMarathiAbbr}, + 124: {tags: []string{"moh"}, localMonth: localMonthsNameMohawk, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesMohawk, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + 1148: {tags: []string{"moh-CA"}, localMonth: localMonthsNameMohawk, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesMohawk, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, + 80: {tags: []string{"mn"}, localMonth: localMonthsNameMongolian, apFmt: apFmtMongolian, weekdayNames: weekdayNamesMongolian, weekdayNamesAbbr: weekdayNamesMongolianAbbr}, + 30800: {tags: []string{"mn-Cyrl"}, localMonth: localMonthsNameMongolian, apFmt: apFmtMongolian, weekdayNames: weekdayNamesMongolian, weekdayNamesAbbr: weekdayNamesMongolianCyrlAbbr}, + 1104: {tags: []string{"mn-MN"}, localMonth: localMonthsNameMongolian, apFmt: apFmtMongolian, weekdayNames: weekdayNamesMongolian, weekdayNamesAbbr: weekdayNamesMongolianCyrlAbbr}, + 31824: {tags: []string{"mn-Mong"}, localMonth: localMonthsNameTraditionalMongolian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTraditionalMongolian, weekdayNamesAbbr: weekdayNamesTraditionalMongolian}, + 2128: {tags: []string{"mn-Mong-CN"}, localMonth: localMonthsNameTraditionalMongolian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTraditionalMongolian, weekdayNamesAbbr: weekdayNamesTraditionalMongolian}, + 3152: {tags: []string{"mn-Mong-MN"}, localMonth: localMonthsNameTraditionalMongolian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTraditionalMongolianMN, weekdayNamesAbbr: weekdayNamesTraditionalMongolianMN}, + 97: {tags: []string{"ne"}, localMonth: localMonthsNameNepali, apFmt: apFmtHindi, weekdayNames: weekdayNamesNepali, weekdayNamesAbbr: weekdayNamesNepaliAbbr}, + 2145: {tags: []string{"ne-IN"}, localMonth: localMonthsNameNepaliIN, apFmt: apFmtHindi, weekdayNames: weekdayNamesNepaliIN, weekdayNamesAbbr: weekdayNamesNepaliINAbbr}, + 1121: {tags: []string{"ne-NP"}, localMonth: localMonthsNameNepali, apFmt: apFmtHindi, weekdayNames: weekdayNamesNepali, weekdayNamesAbbr: weekdayNamesNepaliAbbr}, + 20: {tags: []string{"no"}, localMonth: localMonthsNameNorwegian, apFmt: apFmtCuba, weekdayNames: weekdayNamesNorwegian, weekdayNamesAbbr: weekdayNamesNorwegianAbbr}, + 31764: {tags: []string{"nb"}, localMonth: localMonthsNameNorwegian, apFmt: apFmtCuba, weekdayNames: weekdayNamesNorwegian, weekdayNamesAbbr: weekdayNamesNorwegianNOAbbr}, + 1044: {tags: []string{"nb-NO"}, localMonth: localMonthsNameNorwegian, apFmt: apFmtCuba, weekdayNames: weekdayNamesNorwegian, weekdayNamesAbbr: weekdayNamesNorwegianNOAbbr}, + 30740: {tags: []string{"nn"}, localMonth: localMonthsNameNorwegian, apFmt: apFmtNorwegian, weekdayNames: weekdayNamesNorwegianNynorsk, weekdayNamesAbbr: weekdayNamesNorwegianNynorskAbbr}, + 2068: {tags: []string{"nn-NO"}, localMonth: localMonthsNameNorwegian, apFmt: apFmtNorwegian, weekdayNames: weekdayNamesNorwegianNynorsk, weekdayNamesAbbr: weekdayNamesNorwegianNynorskAbbr}, + 130: {tags: []string{"oc"}, localMonth: localMonthsNameOccitan, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesOccitan, weekdayNamesAbbr: weekdayNamesOccitanAbbr}, + 1154: {tags: []string{"oc-FR"}, localMonth: localMonthsNameOccitan, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesOccitan, weekdayNamesAbbr: weekdayNamesOccitanAbbr}, + 72: {tags: []string{"or"}, localMonth: localMonthsNameOdia, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesOdia, weekdayNamesAbbr: weekdayNamesOdiaAbbr}, + 1096: {tags: []string{"or-IN"}, localMonth: localMonthsNameOdia, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesOdia, weekdayNamesAbbr: weekdayNamesOdiaAbbr}, + 114: {tags: []string{"om"}, localMonth: localMonthsNameOromo, apFmt: apFmtOromo, weekdayNames: weekdayNamesOromo, weekdayNamesAbbr: weekdayNamesOromoAbbr}, + 1138: {tags: []string{"om-ET"}, localMonth: localMonthsNameOromo, apFmt: apFmtOromo, weekdayNames: weekdayNamesOromo, weekdayNamesAbbr: weekdayNamesOromoAbbr}, + 99: {tags: []string{"ps"}, localMonth: localMonthsNamePashto, apFmt: apFmtPashto, weekdayNames: weekdayNamesPashto, weekdayNamesAbbr: weekdayNamesPashto}, + 1123: {tags: []string{"ps-AF"}, localMonth: localMonthsNamePashto, apFmt: apFmtPashto, weekdayNames: weekdayNamesPashto, weekdayNamesAbbr: weekdayNamesPashto}, + 41: {tags: []string{"fa"}, localMonth: localMonthsNamePersian, apFmt: apFmtPersian, weekdayNames: weekdayNamesPersian, weekdayNamesAbbr: weekdayNamesPersian}, + 1065: {tags: []string{"fa-IR"}, localMonth: localMonthsNamePersian, apFmt: apFmtPersian, weekdayNames: weekdayNamesPersian, weekdayNamesAbbr: weekdayNamesPersian}, + 21: {tags: []string{"pl"}, localMonth: localMonthsNamePolish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesPolish, weekdayNamesAbbr: weekdayNamesPolishAbbr}, + 1045: {tags: []string{"pl-PL"}, localMonth: localMonthsNamePolish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesPolish, weekdayNamesAbbr: weekdayNamesPolishAbbr}, + 22: {tags: []string{"pt"}, localMonth: localMonthsNamePortuguese, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesPortuguese, weekdayNamesAbbr: weekdayNamesPortugueseAbbr}, + 1046: {tags: []string{"pt-BR"}, localMonth: localMonthsNamePortuguese, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesPortuguese, weekdayNamesAbbr: weekdayNamesPortugueseAbbr}, + 2070: {tags: []string{"pt-PT"}, localMonth: localMonthsNamePortuguese, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesPortuguese, weekdayNamesAbbr: weekdayNamesPortugueseAbbr}, + 70: {tags: []string{"pa"}, localMonth: localMonthsNamePunjabi, apFmt: apFmtPunjabi, weekdayNames: weekdayNamesPunjabi, weekdayNamesAbbr: weekdayNamesPunjabiAbbr}, + 31814: {tags: []string{"pa-Arab"}, localMonth: localMonthsNamePunjabiArab, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesPunjabiArab, weekdayNamesAbbr: weekdayNamesPunjabiArab}, + 1094: {tags: []string{"pa-IN"}, localMonth: localMonthsNamePunjabi, apFmt: apFmtPunjabi, weekdayNames: weekdayNamesPunjabi, weekdayNamesAbbr: weekdayNamesPunjabiAbbr}, + 2118: {tags: []string{"pa-Arab-PK"}, localMonth: localMonthsNamePunjabiArab, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesPunjabiArab, weekdayNamesAbbr: weekdayNamesPunjabiArab}, + 107: {tags: []string{"quz"}, localMonth: localMonthsNameQuechua, apFmt: apFmtCuba, weekdayNames: weekdayNamesQuechua, weekdayNamesAbbr: weekdayNamesQuechuaAbbr}, + 1131: {tags: []string{"quz-BO"}, localMonth: localMonthsNameQuechua, apFmt: apFmtCuba, weekdayNames: weekdayNamesQuechua, weekdayNamesAbbr: weekdayNamesQuechuaAbbr}, + 2155: {tags: []string{"quz-EC"}, localMonth: localMonthsNameQuechuaEcuador, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesQuechuaEcuador, weekdayNamesAbbr: weekdayNamesQuechuaEcuadorAbbr}, + 3179: {tags: []string{"quz-PE"}, localMonth: localMonthsNameQuechua, apFmt: apFmtCuba, weekdayNames: weekdayNamesQuechuaPeru, weekdayNamesAbbr: weekdayNamesQuechuaPeruAbbr}, + 24: {tags: []string{"ro"}, localMonth: localMonthsNameRomanian, apFmt: apFmtCuba, weekdayNames: weekdayNamesRomanian, weekdayNamesAbbr: weekdayNamesRomanianAbbr}, + 2072: {tags: []string{"ro-MD"}, localMonth: localMonthsNameRomanian, apFmt: apFmtCuba, weekdayNames: weekdayNamesRomanian, weekdayNamesAbbr: weekdayNamesRomanianMoldovaAbbr}, + 1048: {tags: []string{"ro-RO"}, localMonth: localMonthsNameRomanian, apFmt: apFmtCuba, weekdayNames: weekdayNamesRomanian, weekdayNamesAbbr: weekdayNamesRomanianAbbr}, + 23: {tags: []string{"rm"}, localMonth: localMonthsNameRomansh, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesRomansh, weekdayNamesAbbr: weekdayNamesRomanshAbbr}, + 1047: {tags: []string{"rm-CH"}, localMonth: localMonthsNameRomansh, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesRomansh, weekdayNamesAbbr: weekdayNamesRomanshAbbr}, + 25: {tags: []string{"ru"}, localMonth: localMonthsNameRussian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesRussian, weekdayNamesAbbr: weekdayNamesRussianAbbr}, + 2073: {tags: []string{"ru-MD"}, localMonth: localMonthsNameRussian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesRussian, weekdayNamesAbbr: weekdayNamesRussianAbbr}, + 1049: {tags: []string{"ru-RU"}, localMonth: localMonthsNameRussian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesRussian, weekdayNamesAbbr: weekdayNamesRussianAbbr}, + 133: {tags: []string{"sah"}, localMonth: localMonthsNameSakha, apFmt: apFmtSakha, weekdayNames: weekdayNamesSakha, weekdayNamesAbbr: weekdayNamesSakhaAbbr}, + 1157: {tags: []string{"sah-RU"}, localMonth: localMonthsNameSakha, apFmt: apFmtSakha, weekdayNames: weekdayNamesSakha, weekdayNamesAbbr: weekdayNamesSakhaAbbr}, + 28731: {tags: []string{"smn"}, localMonth: localMonthsNameSami, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSami, weekdayNamesAbbr: weekdayNamesSamiAbbr}, + 9275: {tags: []string{"smn-FI"}, localMonth: localMonthsNameSami, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSami, weekdayNamesAbbr: weekdayNamesSamiAbbr}, + 31803: {tags: []string{"smj"}, localMonth: localMonthsNameSamiLule, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiSamiLule, weekdayNamesAbbr: weekdayNamesSamiSwedenAbbr}, + 4155: {tags: []string{"smj-NO"}, localMonth: localMonthsNameSamiLule, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiSamiLule, weekdayNamesAbbr: weekdayNamesSamiSamiLuleAbbr}, + 5179: {tags: []string{"smj-SE"}, localMonth: localMonthsNameSamiLule, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiSweden, weekdayNamesAbbr: weekdayNamesSamiSwedenAbbr}, + 59: {tags: []string{"se"}, localMonth: localMonthsNameSamiNorthern, apFmt: apFmtSamiNorthern, weekdayNames: weekdayNamesSamiNorthern, weekdayNamesAbbr: weekdayNamesSamiNorthernAbbr}, + 3131: {tags: []string{"se-FI"}, localMonth: localMonthsNameSamiNorthernFI, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiNorthernFI, weekdayNamesAbbr: weekdayNamesSamiNorthernFIAbbr}, + 1083: {tags: []string{"se-NO"}, localMonth: localMonthsNameSamiNorthern, apFmt: apFmtSamiNorthern, weekdayNames: weekdayNamesSamiNorthern, weekdayNamesAbbr: weekdayNamesSamiNorthernAbbr}, + 2107: {tags: []string{"se-SE"}, localMonth: localMonthsNameSamiNorthern, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiNorthernSE, weekdayNamesAbbr: weekdayNamesSamiNorthernSEAbbr}, + 29755: {tags: []string{"sms"}, localMonth: localMonthsNameSamiSkolt, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiSkolt, weekdayNamesAbbr: weekdayNamesSamiSkoltAbbr}, + 8251: {tags: []string{"sms-FI"}, localMonth: localMonthsNameSamiSkolt, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiSkolt, weekdayNamesAbbr: weekdayNamesSamiSkoltAbbr}, + 30779: {tags: []string{"sma"}, localMonth: localMonthsNameSamiSouthern, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiSouthern, weekdayNamesAbbr: weekdayNamesSamiSouthernAbbr}, + 6203: {tags: []string{"sma-NO"}, localMonth: localMonthsNameSamiSouthern, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiSouthern, weekdayNamesAbbr: weekdayNamesSamiSouthernAbbr}, + 7227: {tags: []string{"sma-SE"}, localMonth: localMonthsNameSamiSouthern, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiSouthern, weekdayNamesAbbr: weekdayNamesSamiSouthernAbbr}, + 79: {tags: []string{"sa"}, localMonth: localMonthsNameSanskrit, apFmt: apFmtSanskrit, weekdayNames: weekdayNamesSanskrit, weekdayNamesAbbr: weekdayNamesSanskritAbbr}, + 1103: {tags: []string{"sa-IN"}, localMonth: localMonthsNameSanskrit, apFmt: apFmtSanskrit, weekdayNames: weekdayNamesSanskrit, weekdayNamesAbbr: weekdayNamesSanskritAbbr}, + 145: {tags: []string{"gd"}, localMonth: localMonthsNameScottishGaelic, apFmt: apFmtScottishGaelic, weekdayNames: weekdayNamesGaelic, weekdayNamesAbbr: weekdayNamesGaelicAbbr}, + 1169: {tags: []string{"gd-GB"}, localMonth: localMonthsNameScottishGaelic, apFmt: apFmtScottishGaelic, weekdayNames: weekdayNamesGaelic, weekdayNamesAbbr: weekdayNamesGaelicAbbr}, + 27674: {tags: []string{"sr-Cyrl"}, localMonth: localMonthsNameSerbian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSerbian, weekdayNamesAbbr: weekdayNamesSerbianAbbr}, + 7194: {tags: []string{"sr-Cyrl-BA"}, localMonth: localMonthsNameSerbianBA, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSerbianBA, weekdayNamesAbbr: weekdayNamesSerbianBAAbbr}, + 12314: {tags: []string{"sr-Cyrl-ME"}, localMonth: localMonthsNameSerbian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSerbianME, weekdayNamesAbbr: weekdayNamesSerbianBAAbbr}, + 10266: {tags: []string{"sr-Cyrl-RS"}, localMonth: localMonthsNameSerbian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSerbian, weekdayNamesAbbr: weekdayNamesSerbianAbbr}, + 3098: {tags: []string{"sr-Cyrl-CS"}, localMonth: localMonthsNameSerbian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSerbian, weekdayNamesAbbr: weekdayNamesSerbianAbbr}, + 28698: {tags: []string{"sr-Latn"}, localMonth: localMonthsNameSerbianLatin, apFmt: apFmtSerbianLatin, weekdayNames: weekdayNamesSerbianLatin, weekdayNamesAbbr: weekdayNamesSerbianLatinAbbr}, + 31770: {tags: []string{"sr"}, localMonth: localMonthsNameSerbianLatin, apFmt: apFmtSerbianLatin, weekdayNames: weekdayNamesSerbianLatin, weekdayNamesAbbr: weekdayNamesSerbianLatinAbbr}, + 6170: {tags: []string{"sr-Latn-BA"}, localMonth: localMonthsNameSerbianLatin, apFmt: apFmtSerbianLatinBA, weekdayNames: weekdayNamesSerbianLatinBA, weekdayNamesAbbr: weekdayNamesSerbianLatinBAAbbr}, + 11290: {tags: []string{"sr-Latn-ME"}, localMonth: localMonthsNameSerbianLatin, apFmt: apFmtSerbianLatinBA, weekdayNames: weekdayNamesSerbianLatinME, weekdayNamesAbbr: weekdayNamesSerbianLatinAbbr}, + 9242: {tags: []string{"sr-Latn-RS"}, localMonth: localMonthsNameSerbianLatin, apFmt: apFmtSerbianLatin, weekdayNames: weekdayNamesSerbianLatin, weekdayNamesAbbr: weekdayNamesSerbianLatinAbbr}, + 2074: {tags: []string{"sr-Latn-CS"}, localMonth: localMonthsNameSerbianLatinCS, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSerbianLatin, weekdayNamesAbbr: weekdayNamesSerbianLatinCSAbbr}, + 108: {tags: []string{"nso"}, localMonth: localMonthsNameSesothoSaLeboa, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSesothoSaLeboa, weekdayNamesAbbr: weekdayNamesSesothoSaLeboaAbbr}, + 1132: {tags: []string{"nso-ZA"}, localMonth: localMonthsNameSesothoSaLeboa, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSesothoSaLeboa, weekdayNamesAbbr: weekdayNamesSesothoSaLeboaAbbr}, + 50: {tags: []string{"tn"}, localMonth: localMonthsNameSetswana, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSetswana, weekdayNamesAbbr: weekdayNamesSetswanaAbbr}, + 2098: {tags: []string{"tn-BW"}, localMonth: localMonthsNameSetswana, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSetswana, weekdayNamesAbbr: weekdayNamesSetswanaAbbr}, + 1074: {tags: []string{"tn-ZA"}, localMonth: localMonthsNameSetswana, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSetswana, weekdayNamesAbbr: weekdayNamesSetswanaAbbr}, + 89: {tags: []string{"sd"}, localMonth: localMonthsNameSindhi, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSindhi, weekdayNamesAbbr: weekdayNamesSindhiAbbr}, + 31833: {tags: []string{"sd-Arab"}, localMonth: localMonthsNameSindhi, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSindhi, weekdayNamesAbbr: weekdayNamesSindhiAbbr}, + 2137: {tags: []string{"sd-Arab-PK"}, localMonth: localMonthsNameSindhi, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSindhi, weekdayNamesAbbr: weekdayNamesSindhiAbbr}, + 91: {tags: []string{"si"}, localMonth: localMonthsNameSinhala, apFmt: apFmtSinhala, weekdayNames: weekdayNamesSindhi, weekdayNamesAbbr: weekdayNamesSindhiAbbr}, + 1115: {tags: []string{"si-LK"}, localMonth: localMonthsNameSinhala, apFmt: apFmtSinhala, weekdayNames: weekdayNamesSindhi, weekdayNamesAbbr: weekdayNamesSindhiAbbr}, + 27: {tags: []string{"sk"}, localMonth: localMonthsNameSlovak, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSlovak, weekdayNamesAbbr: weekdayNamesSlovakAbbr}, + 1051: {tags: []string{"sk-SK"}, localMonth: localMonthsNameSlovak, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSlovak, weekdayNamesAbbr: weekdayNamesSlovakAbbr}, + 36: {tags: []string{"sl"}, localMonth: localMonthsNameSlovenian, apFmt: apFmtSlovenian, weekdayNames: weekdayNamesSlovenian, weekdayNamesAbbr: weekdayNamesSlovenianAbbr}, + 1060: {tags: []string{"sl-SI"}, localMonth: localMonthsNameSlovenian, apFmt: apFmtSlovenian, weekdayNames: weekdayNamesSlovenian, weekdayNamesAbbr: weekdayNamesSlovenianAbbr}, + 119: {tags: []string{"so"}, localMonth: localMonthsNameSomali, apFmt: apFmtSomali, weekdayNames: weekdayNamesSomali, weekdayNamesAbbr: weekdayNamesSomaliAbbr}, + 1143: {tags: []string{"so-SO"}, localMonth: localMonthsNameSomali, apFmt: apFmtSomali, weekdayNames: weekdayNamesSomali, weekdayNamesAbbr: weekdayNamesSomaliAbbr}, + 48: {tags: []string{"st"}, localMonth: localMonthsNameSotho, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSotho, weekdayNamesAbbr: weekdayNamesSothoAbbr}, + 1072: {tags: []string{"st-ZA"}, localMonth: localMonthsNameSotho, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSotho, weekdayNamesAbbr: weekdayNamesSothoAbbr}, + 10: {tags: []string{"es"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishAbbr}, + 11274: {tags: []string{"es-AR"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + 8202: {tags: []string{"es-VE"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + 16394: {tags: []string{"es-BO"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + 13322: {tags: []string{"es-CL"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + 9226: {tags: []string{"es-CO"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + 5130: {tags: []string{"es-CR"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + 23562: {tags: []string{"es-CU"}, localMonth: localMonthsNameSpanish, apFmt: apFmtCuba, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + 7178: {tags: []string{"es-DO"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + 12298: {tags: []string{"es-EC"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + 17418: {tags: []string{"es-SV"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + 4106: {tags: []string{"es-GT"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + 18442: {tags: []string{"es-HN"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + 22538: {tags: []string{"es-419"}, localMonth: localMonthsNameSpanish, apFmt: apFmtCuba, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + 2058: {tags: []string{"es-MX"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + 19466: {tags: []string{"es-NI"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + 6154: {tags: []string{"es-PA"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + 15370: {tags: []string{"es-PY"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + 10250: {tags: []string{"es-PE"}, localMonth: localMonthsNameSpanishPE, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + 20490: {tags: []string{"es-PR"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + 1034: {tags: []string{"es-ES_tradnl"}, localMonth: localMonthsNameSpanish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishAbbr}, + 3082: {tags: []string{"es-ES"}, localMonth: localMonthsNameSpanish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishAbbr}, + 21514: {tags: []string{"es-US"}, localMonth: localMonthsNameSpanish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishUSAbbr}, + 14346: {tags: []string{"es-UY"}, localMonth: localMonthsNameSpanishPE, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, + 29: {tags: []string{"sv"}, localMonth: localMonthsNameSwedish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSwedish, weekdayNamesAbbr: weekdayNamesSwedishAbbr}, + 2077: {tags: []string{"sv-FI"}, localMonth: localMonthsNameSwedishFI, apFmt: apFmtSwedish, weekdayNames: weekdayNamesSwedish, weekdayNamesAbbr: weekdayNamesSwedishAbbr}, + 1053: {tags: []string{"sv-SE"}, localMonth: localMonthsNameSwedishFI, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSwedish, weekdayNamesAbbr: weekdayNamesSwedishAbbr}, + 90: {tags: []string{"syr"}, localMonth: localMonthsNameSyriac, apFmt: apFmtSyriac, weekdayNames: weekdayNamesSyriac, weekdayNamesAbbr: weekdayNamesSyriacAbbr}, + 1114: {tags: []string{"syr-SY"}, localMonth: localMonthsNameSyriac, apFmt: apFmtSyriac, weekdayNames: weekdayNamesSyriac, weekdayNamesAbbr: weekdayNamesSyriacAbbr}, + 40: {tags: []string{"tg"}, localMonth: localMonthsNameTajik, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTajik, weekdayNamesAbbr: weekdayNamesTajikAbbr}, + 31784: {tags: []string{"tg-Cyrl"}, localMonth: localMonthsNameTajik, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTajik, weekdayNamesAbbr: weekdayNamesTajikAbbr}, + 1064: {tags: []string{"tg-Cyrl-TJ"}, localMonth: localMonthsNameTajik, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTajik, weekdayNamesAbbr: weekdayNamesTajikAbbr}, + 95: {tags: []string{"tzm"}, localMonth: localMonthsNameTamazight, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTamazight, weekdayNamesAbbr: weekdayNamesTamazightAbbr}, + 31839: {tags: []string{"tzm-Latn"}, localMonth: localMonthsNameTamazight, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTamazight, weekdayNamesAbbr: weekdayNamesTamazightAbbr}, + 2143: {tags: []string{"tzm-Latn-DZ"}, localMonth: localMonthsNameTamazight, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTamazight, weekdayNamesAbbr: weekdayNamesTamazightAbbr}, + 73: {tags: []string{"ta"}, localMonth: localMonthsNameTamil, apFmt: apFmtTamil, weekdayNames: weekdayNamesTamil, weekdayNamesAbbr: weekdayNamesTamilAbbr}, + 1097: {tags: []string{"ta-IN"}, localMonth: localMonthsNameTamil, apFmt: apFmtTamil, weekdayNames: weekdayNamesTamil, weekdayNamesAbbr: weekdayNamesTamilAbbr}, + 2121: {tags: []string{"ta-LK"}, localMonth: localMonthsNameTamilLK, apFmt: apFmtTamil, weekdayNames: weekdayNamesTamilLK, weekdayNamesAbbr: weekdayNamesTamilLKAbbr}, + 68: {tags: []string{"tt"}, localMonth: localMonthsNameTatar, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTatar, weekdayNamesAbbr: weekdayNamesTatarAbbr}, + 1092: {tags: []string{"tt-RU"}, localMonth: localMonthsNameTatar, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTatar, weekdayNamesAbbr: weekdayNamesTatarAbbr}, + 74: {tags: []string{"te"}, localMonth: localMonthsNameTelugu, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTelugu, weekdayNamesAbbr: weekdayNamesTeluguAbbr}, + 1098: {tags: []string{"te-IN"}, localMonth: localMonthsNameTelugu, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTelugu, weekdayNamesAbbr: weekdayNamesTeluguAbbr}, + 30: {tags: []string{"th"}, localMonth: localMonthsNameThai, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesThai, weekdayNamesAbbr: weekdayNamesThaiAbbr}, + 1054: {tags: []string{"th-TH"}, localMonth: localMonthsNameThai, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesThai, weekdayNamesAbbr: weekdayNamesThaiAbbr}, + 81: {tags: []string{"bo"}, localMonth: localMonthsNameTibetan, apFmt: apFmtTibetan, weekdayNames: weekdayNamesTibetan, weekdayNamesAbbr: weekdayNamesTibetanAbbr}, + 1105: {tags: []string{"bo-CN"}, localMonth: localMonthsNameTibetan, apFmt: apFmtTibetan, weekdayNames: weekdayNamesTibetan, weekdayNamesAbbr: weekdayNamesTibetanAbbr}, + 115: {tags: []string{"ti"}, localMonth: localMonthsNameTigrinya, apFmt: apFmtTigrinya, weekdayNames: weekdayNamesTigrinya, weekdayNamesAbbr: weekdayNamesTigrinyaAbbr}, + 2163: {tags: []string{"ti-ER"}, localMonth: localMonthsNameTigrinya, apFmt: apFmtTigrinyaER, weekdayNames: weekdayNamesTigrinya, weekdayNamesAbbr: weekdayNamesTigrinyaAbbr}, + 1139: {tags: []string{"ti-ET"}, localMonth: localMonthsNameTigrinya, apFmt: apFmtTigrinya, weekdayNames: weekdayNamesTigrinya, weekdayNamesAbbr: weekdayNamesTigrinyaAbbr}, + 49: {tags: []string{"ts"}, localMonth: localMonthsNameTsonga, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTsonga, weekdayNamesAbbr: weekdayNamesTsongaAbbr}, + 1073: {tags: []string{"ts-ZA"}, localMonth: localMonthsNameTsonga, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTsonga, weekdayNamesAbbr: weekdayNamesTsongaAbbr}, + 31: {tags: []string{"tr"}, localMonth: localMonthsNameTurkish, apFmt: apFmtTurkish, weekdayNames: weekdayNamesTurkish, weekdayNamesAbbr: weekdayNamesTurkishAbbr}, + 1055: {tags: []string{"tr-TR"}, localMonth: localMonthsNameTurkish, apFmt: apFmtTurkish, weekdayNames: weekdayNamesTurkish, weekdayNamesAbbr: weekdayNamesTurkishAbbr}, + 66: {tags: []string{"tk"}, localMonth: localMonthsNameTurkmen, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTurkmen, weekdayNamesAbbr: weekdayNamesTurkmenAbbr}, + 1090: {tags: []string{"tk-TM"}, localMonth: localMonthsNameTurkmen, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTurkmen, weekdayNamesAbbr: weekdayNamesTurkmenAbbr}, + 34: {tags: []string{"uk"}, localMonth: localMonthsNameUkrainian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesUkrainian, weekdayNamesAbbr: weekdayNamesUkrainianAbbr}, + 1058: {tags: []string{"uk-UA"}, localMonth: localMonthsNameUkrainian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesUkrainian, weekdayNamesAbbr: weekdayNamesUkrainianAbbr}, + 46: {tags: []string{"hsb"}, localMonth: localMonthsNameUpperSorbian, apFmt: apFmtUpperSorbian, weekdayNames: weekdayNamesSorbian, weekdayNamesAbbr: weekdayNamesSorbianAbbr}, + 1070: {tags: []string{"hsb-DE"}, localMonth: localMonthsNameUpperSorbian, apFmt: apFmtUpperSorbian, weekdayNames: weekdayNamesSorbian, weekdayNamesAbbr: weekdayNamesSorbianAbbr}, + 32: {tags: []string{"ur"}, localMonth: localMonthsNamePunjabiArab, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesUrdu, weekdayNamesAbbr: weekdayNamesUrdu}, + 2080: {tags: []string{"ur-IN"}, localMonth: localMonthsNamePunjabiArab, apFmt: apFmtUrdu, weekdayNames: weekdayNamesUrduIN, weekdayNamesAbbr: weekdayNamesUrduIN}, + 1056: {tags: []string{"ur-PK"}, localMonth: localMonthsNamePunjabiArab, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesUrdu, weekdayNamesAbbr: weekdayNamesUrdu}, + 128: {tags: []string{"ug"}, localMonth: localMonthsNameUyghur, apFmt: apFmtUyghur, weekdayNames: weekdayNamesUyghur, weekdayNamesAbbr: weekdayNamesUyghurAbbr}, + 1152: {tags: []string{"ug-CN"}, localMonth: localMonthsNameUyghur, apFmt: apFmtUyghur, weekdayNames: weekdayNamesUyghur, weekdayNamesAbbr: weekdayNamesUyghurAbbr}, + 30787: {tags: []string{"uz-Cyrl"}, localMonth: localMonthsNameUzbekCyrillic, apFmt: apFmtUzbekCyrillic, weekdayNames: weekdayNamesUzbekCyrillic, weekdayNamesAbbr: weekdayNamesUzbekCyrillicAbbr}, + 2115: {tags: []string{"uz-Cyrl-UZ"}, localMonth: localMonthsNameUzbekCyrillic, apFmt: apFmtUzbekCyrillic, weekdayNames: weekdayNamesUzbekCyrillic, weekdayNamesAbbr: weekdayNamesUzbekCyrillicAbbr}, + 67: {tags: []string{"uz"}, localMonth: localMonthsNameUzbek, apFmt: apFmtUzbek, weekdayNames: weekdayNamesUzbek, weekdayNamesAbbr: weekdayNamesUzbekAbbr}, + 31811: {tags: []string{"uz-Latn"}, localMonth: localMonthsNameUzbek, apFmt: apFmtUzbek, weekdayNames: weekdayNamesUzbek, weekdayNamesAbbr: weekdayNamesUzbekAbbr}, + 1091: {tags: []string{"uz-Latn-UZ"}, localMonth: localMonthsNameUzbek, apFmt: apFmtUzbek, weekdayNames: weekdayNamesUzbek, weekdayNamesAbbr: weekdayNamesUzbekAbbr}, + 2051: {tags: []string{"ca-ES-valencia"}, localMonth: localMonthsNameValencian, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesValencian, weekdayNamesAbbr: weekdayNamesValencianAbbr}, + 51: {tags: []string{"ve"}, localMonth: localMonthsNameVenda, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesVenda, weekdayNamesAbbr: weekdayNamesVendaAbbr}, + 1075: {tags: []string{"ve-ZA"}, localMonth: localMonthsNameVenda, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesVenda, weekdayNamesAbbr: weekdayNamesVendaAbbr}, + 42: {tags: []string{"vi"}, localMonth: localMonthsNameVietnamese, apFmt: apFmtVietnamese, weekdayNames: weekdayNamesVietnamese, weekdayNamesAbbr: weekdayNamesVietnameseAbbr}, + 1066: {tags: []string{"vi-VN"}, localMonth: localMonthsNameVietnamese, apFmt: apFmtVietnamese, weekdayNames: weekdayNamesVietnamese, weekdayNamesAbbr: weekdayNamesVietnameseAbbr}, + 82: {tags: []string{"cy"}, localMonth: localMonthsNameWelsh, apFmt: apFmtWelsh, weekdayNames: weekdayNamesWelsh, weekdayNamesAbbr: weekdayNamesWelshAbbr}, + 1106: {tags: []string{"cy-GB"}, localMonth: localMonthsNameWelsh, apFmt: apFmtWelsh, weekdayNames: weekdayNamesWelsh, weekdayNamesAbbr: weekdayNamesWelshAbbr}, + 136: {tags: []string{"wo"}, localMonth: localMonthsNameWolof, apFmt: apFmtWolof, weekdayNames: weekdayNamesWolof, weekdayNamesAbbr: weekdayNamesWolofAbbr}, + 1160: {tags: []string{"wo-SN"}, localMonth: localMonthsNameWolof, apFmt: apFmtWolof, weekdayNames: weekdayNamesWolof, weekdayNamesAbbr: weekdayNamesWolofAbbr}, + 52: {tags: []string{"xh"}, localMonth: localMonthsNameXhosa, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesXhosa, weekdayNamesAbbr: weekdayNamesXhosaAbbr}, + 1076: {tags: []string{"xh-ZA"}, localMonth: localMonthsNameXhosa, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesXhosa, weekdayNamesAbbr: weekdayNamesXhosaAbbr}, + 120: {tags: []string{"ii"}, localMonth: localMonthsNameYi, apFmt: apFmtYi, weekdayNames: weekdayNamesYi, weekdayNamesAbbr: weekdayNamesYiAbbr}, + 1144: {tags: []string{"ii-CN"}, localMonth: localMonthsNameYi, apFmt: apFmtYi, weekdayNames: weekdayNamesYi, weekdayNamesAbbr: weekdayNamesYiAbbr}, + 1085: {tags: []string{"yi-001"}, localMonth: localMonthsNameYiddish, apFmt: apFmtYiddish, weekdayNames: weekdayNamesYiddish, weekdayNamesAbbr: weekdayNamesYiddishAbbr}, + 106: {tags: []string{"yo"}, localMonth: localMonthsNameYoruba, apFmt: apFmtYoruba, weekdayNames: weekdayNamesYoruba, weekdayNamesAbbr: weekdayNamesYorubaAbbr}, + 1130: {tags: []string{"yo-NG"}, localMonth: localMonthsNameYoruba, apFmt: apFmtYoruba, weekdayNames: weekdayNamesYoruba, weekdayNamesAbbr: weekdayNamesYorubaAbbr}, + 53: {tags: []string{"zu"}, localMonth: localMonthsNameZulu, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesZulu, weekdayNamesAbbr: weekdayNamesZuluAbbr}, + 1077: {tags: []string{"zu-ZA"}, localMonth: localMonthsNameZulu, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesZulu, weekdayNamesAbbr: weekdayNamesZuluAbbr}, + } + // supportedLanguageCodeInfo directly maps the supported language code and tags. + supportedLanguageCodeInfo = map[string]languageInfo{ "JA-JP-X-GANNEN": {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese, weekdayNames: weekdayNamesJapanese, weekdayNamesAbbr: weekdayNamesJapaneseAbbr}, "JA-JP-X-GANNEN,80": {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese, weekdayNames: weekdayNamesJapanese, weekdayNamesAbbr: weekdayNamesJapaneseAbbr, useGannen: true}, - "4B": {tags: []string{"kn"}, localMonth: localMonthsNameKannada, apFmt: apFmtKannada, weekdayNames: weekdayNamesKannada, weekdayNamesAbbr: weekdayNamesKannadaAbbr}, - "44B": {tags: []string{"kn-IN"}, localMonth: localMonthsNameKannada, apFmt: apFmtKannada, weekdayNames: weekdayNamesKannada, weekdayNamesAbbr: weekdayNamesKannadaAbbr}, - "471": {tags: []string{"kr-Latn-NG"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, - "60": {tags: []string{"ks"}, localMonth: localMonthsNameKashmiri, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesKashmiri, weekdayNamesAbbr: weekdayNamesKashmiriAbbr}, - "460": {tags: []string{"ks-Arab"}, localMonth: localMonthsNameKashmiri, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesKashmiri, weekdayNamesAbbr: weekdayNamesKashmiriAbbr}, - "860": {tags: []string{"ks-Deva-IN"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, - "3F": {tags: []string{"kk"}, localMonth: localMonthsNameKazakh, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesKazakh, weekdayNamesAbbr: weekdayNamesKazakhAbbr}, - "43F": {tags: []string{"kk-KZ"}, localMonth: localMonthsNameKazakh, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesKazakh, weekdayNamesAbbr: weekdayNamesKazakhAbbr}, - "53": {tags: []string{"km"}, localMonth: localMonthsNameKhmer, apFmt: apFmtKhmer, weekdayNames: weekdayNamesKhmer, weekdayNamesAbbr: weekdayNamesKhmerAbbr}, - "453": {tags: []string{"km-KH"}, localMonth: localMonthsNameKhmer, apFmt: apFmtKhmer, weekdayNames: weekdayNamesKhmer, weekdayNamesAbbr: weekdayNamesKhmerAbbr}, - "86": {tags: []string{"quc"}, localMonth: localMonthsNameKiche, apFmt: apFmtCuba, weekdayNames: weekdayNamesKiche, weekdayNamesAbbr: weekdayNamesKicheAbbr}, - "486": {tags: []string{"quc-Latn-GT"}, localMonth: localMonthsNameKiche, apFmt: apFmtCuba, weekdayNames: weekdayNamesKiche, weekdayNamesAbbr: weekdayNamesKicheAbbr}, - "87": {tags: []string{"rw"}, localMonth: localMonthsNameKinyarwanda, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesKinyarwanda, weekdayNamesAbbr: weekdayNamesKinyarwandaAbbr}, - "487": {tags: []string{"rw-RW"}, localMonth: localMonthsNameKinyarwanda, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesKinyarwanda, weekdayNamesAbbr: weekdayNamesKinyarwandaAbbr}, - "41": {tags: []string{"sw"}, localMonth: localMonthsNameKiswahili, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesKiswahili, weekdayNamesAbbr: weekdayNamesKiswahiliAbbr}, - "441": {tags: []string{"sw-KE"}, localMonth: localMonthsNameKiswahili, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesKiswahili, weekdayNamesAbbr: weekdayNamesKiswahiliAbbr}, - "57": {tags: []string{"kok"}, localMonth: localMonthsNameKonkani, apFmt: apFmtKonkani, weekdayNames: weekdayNamesKonkani, weekdayNamesAbbr: weekdayNamesKonkaniAbbr}, - "457": {tags: []string{"kok-IN"}, localMonth: localMonthsNameKonkani, apFmt: apFmtKonkani, weekdayNames: weekdayNamesKonkani, weekdayNamesAbbr: weekdayNamesKonkaniAbbr}, - "12": {tags: []string{"ko"}, localMonth: localMonthsNameKorean, apFmt: apFmtKorean, weekdayNames: weekdayNamesKorean, weekdayNamesAbbr: weekdayNamesKoreanAbbr}, - "412": {tags: []string{"ko-KR"}, localMonth: localMonthsNameKorean, apFmt: apFmtKorean, weekdayNames: weekdayNamesKorean, weekdayNamesAbbr: weekdayNamesKoreanAbbr}, - "40": {tags: []string{"ky"}, localMonth: localMonthsNameKyrgyz, apFmt: apFmtKyrgyz, weekdayNames: weekdayNamesKyrgyz, weekdayNamesAbbr: weekdayNamesKyrgyzAbbr}, - "440": {tags: []string{"ky-KG"}, localMonth: localMonthsNameKyrgyz, apFmt: apFmtKyrgyz, weekdayNames: weekdayNamesKyrgyz, weekdayNamesAbbr: weekdayNamesKyrgyzAbbr}, - "54": {tags: []string{"lo"}, localMonth: localMonthsNameLao, apFmt: apFmtLao, weekdayNames: weekdayNamesLao, weekdayNamesAbbr: weekdayNamesLaoAbbr}, - "454": {tags: []string{"lo-LA"}, localMonth: localMonthsNameLao, apFmt: apFmtLao, weekdayNames: weekdayNamesLao, weekdayNamesAbbr: weekdayNamesLaoAbbr}, - "476": {tags: []string{"la-VA"}, localMonth: localMonthsNameLatin, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesLatin, weekdayNamesAbbr: weekdayNamesLatinAbbr}, - "26": {tags: []string{"lv"}, localMonth: localMonthsNameLatvian, apFmt: apFmtLatvian, weekdayNames: weekdayNamesLatvian, weekdayNamesAbbr: weekdayNamesLatvianAbbr}, - "426": {tags: []string{"lv-LV"}, localMonth: localMonthsNameLatvian, apFmt: apFmtLatvian, weekdayNames: weekdayNamesLatvian, weekdayNamesAbbr: weekdayNamesLatvianAbbr}, - "27": {tags: []string{"lt"}, localMonth: localMonthsNameLithuanian, apFmt: apFmtLithuanian, weekdayNames: weekdayNamesLithuanian, weekdayNamesAbbr: weekdayNamesLithuanianAbbr}, - "427": {tags: []string{"lt-LT"}, localMonth: localMonthsNameLithuanian, apFmt: apFmtLithuanian, weekdayNames: weekdayNamesLithuanian, weekdayNamesAbbr: weekdayNamesLithuanianAbbr}, - "7C2E": {tags: []string{"dsb"}, localMonth: localMonthsNameLowerSorbian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesLowerSorbian, weekdayNamesAbbr: weekdayNamesLowerSorbianAbbr}, - "82E": {tags: []string{"dsb-DE"}, localMonth: localMonthsNameLowerSorbian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesLowerSorbian, weekdayNamesAbbr: weekdayNamesLowerSorbianAbbr}, - "6E": {tags: []string{"lb"}, localMonth: localMonthsNameLuxembourgish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesLuxembourgish, weekdayNamesAbbr: weekdayNamesLuxembourgishAbbr}, - "46E": {tags: []string{"lb-LU"}, localMonth: localMonthsNameLuxembourgish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesLuxembourgish, weekdayNamesAbbr: weekdayNamesLuxembourgishAbbr}, - "2F": {tags: []string{"mk"}, localMonth: localMonthsNameMacedonian, apFmt: apFmtMacedonian, weekdayNames: weekdayNamesMacedonian, weekdayNamesAbbr: weekdayNamesMacedonianAbbr}, - "42F": {tags: []string{"mk-MK"}, localMonth: localMonthsNameMacedonian, apFmt: apFmtMacedonian, weekdayNames: weekdayNamesMacedonian, weekdayNamesAbbr: weekdayNamesMacedonianAbbr}, - "3E": {tags: []string{"ms"}, localMonth: localMonthsNameMalay, apFmt: apFmtMalay, weekdayNames: weekdayNamesMalay, weekdayNamesAbbr: weekdayNamesMalayAbbr}, - "83E": {tags: []string{"ms-BN"}, localMonth: localMonthsNameMalay, apFmt: apFmtMalay, weekdayNames: weekdayNamesMalay, weekdayNamesAbbr: weekdayNamesMalayAbbr}, - "43E": {tags: []string{"ms-MY"}, localMonth: localMonthsNameMalay, apFmt: apFmtMalay, weekdayNames: weekdayNamesMalay, weekdayNamesAbbr: weekdayNamesMalayAbbr}, - "4C": {tags: []string{"ml"}, localMonth: localMonthsNameMalayalam, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesMalayalam, weekdayNamesAbbr: weekdayNamesMalayalamAbbr}, - "44C": {tags: []string{"ml-IN"}, localMonth: localMonthsNameMalayalam, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesMalayalam, weekdayNamesAbbr: weekdayNamesMalayalamAbbr}, - "3A": {tags: []string{"mt"}, localMonth: localMonthsNameMaltese, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesMaltese, weekdayNamesAbbr: weekdayNamesMalteseAbbr}, - "43A": {tags: []string{"mt-MT"}, localMonth: localMonthsNameMaltese, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesMaltese, weekdayNamesAbbr: weekdayNamesMalteseAbbr}, - "81": {tags: []string{"mi"}, localMonth: localMonthsNameMaori, apFmt: apFmtCuba, weekdayNames: weekdayNamesMaori, weekdayNamesAbbr: weekdayNamesMaoriAbbr}, - "481": {tags: []string{"mi-NZ"}, localMonth: localMonthsNameMaori, apFmt: apFmtCuba, weekdayNames: weekdayNamesMaori, weekdayNamesAbbr: weekdayNamesMaoriAbbr}, - "7A": {tags: []string{"arn"}, localMonth: localMonthsNameMapudungun, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesMapudungun, weekdayNamesAbbr: weekdayNamesMapudungunAbbr}, - "47A": {tags: []string{"arn-CL"}, localMonth: localMonthsNameMapudungun, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesMapudungun, weekdayNamesAbbr: weekdayNamesMapudungunAbbr}, - "4E": {tags: []string{"mr"}, localMonth: localMonthsNameMarathi, apFmt: apFmtKonkani, weekdayNames: weekdayNamesMarathi, weekdayNamesAbbr: weekdayNamesMarathiAbbr}, - "44E": {tags: []string{"mr-IN"}, localMonth: localMonthsNameMarathi, apFmt: apFmtKonkani, weekdayNames: weekdayNamesMarathi, weekdayNamesAbbr: weekdayNamesMarathiAbbr}, - "7C": {tags: []string{"moh"}, localMonth: localMonthsNameMohawk, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesMohawk, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, - "47C": {tags: []string{"moh-CA"}, localMonth: localMonthsNameMohawk, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesMohawk, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, - "50": {tags: []string{"mn"}, localMonth: localMonthsNameMongolian, apFmt: apFmtMongolian, weekdayNames: weekdayNamesMongolian, weekdayNamesAbbr: weekdayNamesMongolianAbbr}, - "7850": {tags: []string{"mn-Cyrl"}, localMonth: localMonthsNameMongolian, apFmt: apFmtMongolian, weekdayNames: weekdayNamesMongolian, weekdayNamesAbbr: weekdayNamesMongolianCyrlAbbr}, - "450": {tags: []string{"mn-MN"}, localMonth: localMonthsNameMongolian, apFmt: apFmtMongolian, weekdayNames: weekdayNamesMongolian, weekdayNamesAbbr: weekdayNamesMongolianCyrlAbbr}, - "7C50": {tags: []string{"mn-Mong"}, localMonth: localMonthsNameTraditionalMongolian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTraditionalMongolian, weekdayNamesAbbr: weekdayNamesTraditionalMongolian}, - "850": {tags: []string{"mn-Mong-CN"}, localMonth: localMonthsNameTraditionalMongolian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTraditionalMongolian, weekdayNamesAbbr: weekdayNamesTraditionalMongolian}, - "C50": {tags: []string{"mn-Mong-MN"}, localMonth: localMonthsNameTraditionalMongolian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTraditionalMongolianMN, weekdayNamesAbbr: weekdayNamesTraditionalMongolianMN}, - "61": {tags: []string{"ne"}, localMonth: localMonthsNameNepali, apFmt: apFmtHindi, weekdayNames: weekdayNamesNepali, weekdayNamesAbbr: weekdayNamesNepaliAbbr}, - "861": {tags: []string{"ne-IN"}, localMonth: localMonthsNameNepaliIN, apFmt: apFmtHindi, weekdayNames: weekdayNamesNepaliIN, weekdayNamesAbbr: weekdayNamesNepaliINAbbr}, - "461": {tags: []string{"ne-NP"}, localMonth: localMonthsNameNepali, apFmt: apFmtHindi, weekdayNames: weekdayNamesNepali, weekdayNamesAbbr: weekdayNamesNepaliAbbr}, - "14": {tags: []string{"no"}, localMonth: localMonthsNameNorwegian, apFmt: apFmtCuba, weekdayNames: weekdayNamesNorwegian, weekdayNamesAbbr: weekdayNamesNorwegianAbbr}, - "7C14": {tags: []string{"nb"}, localMonth: localMonthsNameNorwegian, apFmt: apFmtCuba, weekdayNames: weekdayNamesNorwegian, weekdayNamesAbbr: weekdayNamesNorwegianNOAbbr}, - "414": {tags: []string{"nb-NO"}, localMonth: localMonthsNameNorwegian, apFmt: apFmtCuba, weekdayNames: weekdayNamesNorwegian, weekdayNamesAbbr: weekdayNamesNorwegianNOAbbr}, - "7814": {tags: []string{"nn"}, localMonth: localMonthsNameNorwegian, apFmt: apFmtNorwegian, weekdayNames: weekdayNamesNorwegianNynorsk, weekdayNamesAbbr: weekdayNamesNorwegianNynorskAbbr}, - "814": {tags: []string{"nn-NO"}, localMonth: localMonthsNameNorwegian, apFmt: apFmtNorwegian, weekdayNames: weekdayNamesNorwegianNynorsk, weekdayNamesAbbr: weekdayNamesNorwegianNynorskAbbr}, - "82": {tags: []string{"oc"}, localMonth: localMonthsNameOccitan, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesOccitan, weekdayNamesAbbr: weekdayNamesOccitanAbbr}, - "482": {tags: []string{"oc-FR"}, localMonth: localMonthsNameOccitan, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesOccitan, weekdayNamesAbbr: weekdayNamesOccitanAbbr}, - "48": {tags: []string{"or"}, localMonth: localMonthsNameOdia, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesOdia, weekdayNamesAbbr: weekdayNamesOdiaAbbr}, - "448": {tags: []string{"or-IN"}, localMonth: localMonthsNameOdia, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesOdia, weekdayNamesAbbr: weekdayNamesOdiaAbbr}, - "72": {tags: []string{"om"}, localMonth: localMonthsNameOromo, apFmt: apFmtOromo, weekdayNames: weekdayNamesOromo, weekdayNamesAbbr: weekdayNamesOromoAbbr}, - "472": {tags: []string{"om-ET"}, localMonth: localMonthsNameOromo, apFmt: apFmtOromo, weekdayNames: weekdayNamesOromo, weekdayNamesAbbr: weekdayNamesOromoAbbr}, - "63": {tags: []string{"ps"}, localMonth: localMonthsNamePashto, apFmt: apFmtPashto, weekdayNames: weekdayNamesPashto, weekdayNamesAbbr: weekdayNamesPashto}, - "463": {tags: []string{"ps-AF"}, localMonth: localMonthsNamePashto, apFmt: apFmtPashto, weekdayNames: weekdayNamesPashto, weekdayNamesAbbr: weekdayNamesPashto}, - "29": {tags: []string{"fa"}, localMonth: localMonthsNamePersian, apFmt: apFmtPersian, weekdayNames: weekdayNamesPersian, weekdayNamesAbbr: weekdayNamesPersian}, - "429": {tags: []string{"fa-IR"}, localMonth: localMonthsNamePersian, apFmt: apFmtPersian, weekdayNames: weekdayNamesPersian, weekdayNamesAbbr: weekdayNamesPersian}, - "15": {tags: []string{"pl"}, localMonth: localMonthsNamePolish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesPolish, weekdayNamesAbbr: weekdayNamesPolishAbbr}, - "415": {tags: []string{"pl-PL"}, localMonth: localMonthsNamePolish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesPolish, weekdayNamesAbbr: weekdayNamesPolishAbbr}, - "16": {tags: []string{"pt"}, localMonth: localMonthsNamePortuguese, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesPortuguese, weekdayNamesAbbr: weekdayNamesPortugueseAbbr}, - "416": {tags: []string{"pt-BR"}, localMonth: localMonthsNamePortuguese, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesPortuguese, weekdayNamesAbbr: weekdayNamesPortugueseAbbr}, - "816": {tags: []string{"pt-PT"}, localMonth: localMonthsNamePortuguese, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesPortuguese, weekdayNamesAbbr: weekdayNamesPortugueseAbbr}, - "46": {tags: []string{"pa"}, localMonth: localMonthsNamePunjabi, apFmt: apFmtPunjabi, weekdayNames: weekdayNamesPunjabi, weekdayNamesAbbr: weekdayNamesPunjabiAbbr}, - "7C46": {tags: []string{"pa-Arab"}, localMonth: localMonthsNamePunjabiArab, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesPunjabiArab, weekdayNamesAbbr: weekdayNamesPunjabiArab}, - "446": {tags: []string{"pa-IN"}, localMonth: localMonthsNamePunjabi, apFmt: apFmtPunjabi, weekdayNames: weekdayNamesPunjabi, weekdayNamesAbbr: weekdayNamesPunjabiAbbr}, - "846": {tags: []string{"pa-Arab-PK"}, localMonth: localMonthsNamePunjabiArab, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesPunjabiArab, weekdayNamesAbbr: weekdayNamesPunjabiArab}, - "6B": {tags: []string{"quz"}, localMonth: localMonthsNameQuechua, apFmt: apFmtCuba, weekdayNames: weekdayNamesQuechua, weekdayNamesAbbr: weekdayNamesQuechuaAbbr}, - "46B": {tags: []string{"quz-BO"}, localMonth: localMonthsNameQuechua, apFmt: apFmtCuba, weekdayNames: weekdayNamesQuechua, weekdayNamesAbbr: weekdayNamesQuechuaAbbr}, - "86B": {tags: []string{"quz-EC"}, localMonth: localMonthsNameQuechuaEcuador, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesQuechuaEcuador, weekdayNamesAbbr: weekdayNamesQuechuaEcuadorAbbr}, - "C6B": {tags: []string{"quz-PE"}, localMonth: localMonthsNameQuechua, apFmt: apFmtCuba, weekdayNames: weekdayNamesQuechuaPeru, weekdayNamesAbbr: weekdayNamesQuechuaPeruAbbr}, - "18": {tags: []string{"ro"}, localMonth: localMonthsNameRomanian, apFmt: apFmtCuba, weekdayNames: weekdayNamesRomanian, weekdayNamesAbbr: weekdayNamesRomanianAbbr}, - "818": {tags: []string{"ro-MD"}, localMonth: localMonthsNameRomanian, apFmt: apFmtCuba, weekdayNames: weekdayNamesRomanian, weekdayNamesAbbr: weekdayNamesRomanianMoldovaAbbr}, - "418": {tags: []string{"ro-RO"}, localMonth: localMonthsNameRomanian, apFmt: apFmtCuba, weekdayNames: weekdayNamesRomanian, weekdayNamesAbbr: weekdayNamesRomanianAbbr}, - "17": {tags: []string{"rm"}, localMonth: localMonthsNameRomansh, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesRomansh, weekdayNamesAbbr: weekdayNamesRomanshAbbr}, - "417": {tags: []string{"rm-CH"}, localMonth: localMonthsNameRomansh, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesRomansh, weekdayNamesAbbr: weekdayNamesRomanshAbbr}, - "19": {tags: []string{"ru"}, localMonth: localMonthsNameRussian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesRussian, weekdayNamesAbbr: weekdayNamesRussianAbbr}, - "819": {tags: []string{"ru-MD"}, localMonth: localMonthsNameRussian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesRussian, weekdayNamesAbbr: weekdayNamesRussianAbbr}, - "419": {tags: []string{"ru-RU"}, localMonth: localMonthsNameRussian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesRussian, weekdayNamesAbbr: weekdayNamesRussianAbbr}, - "85": {tags: []string{"sah"}, localMonth: localMonthsNameSakha, apFmt: apFmtSakha, weekdayNames: weekdayNamesSakha, weekdayNamesAbbr: weekdayNamesSakhaAbbr}, - "485": {tags: []string{"sah-RU"}, localMonth: localMonthsNameSakha, apFmt: apFmtSakha, weekdayNames: weekdayNamesSakha, weekdayNamesAbbr: weekdayNamesSakhaAbbr}, - "703B": {tags: []string{"smn"}, localMonth: localMonthsNameSami, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSami, weekdayNamesAbbr: weekdayNamesSamiAbbr}, - "243B": {tags: []string{"smn-FI"}, localMonth: localMonthsNameSami, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSami, weekdayNamesAbbr: weekdayNamesSamiAbbr}, - "7C3B": {tags: []string{"smj"}, localMonth: localMonthsNameSamiLule, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiSamiLule, weekdayNamesAbbr: weekdayNamesSamiSwedenAbbr}, - "103B": {tags: []string{"smj-NO"}, localMonth: localMonthsNameSamiLule, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiSamiLule, weekdayNamesAbbr: weekdayNamesSamiSamiLuleAbbr}, - "143B": {tags: []string{"smj-SE"}, localMonth: localMonthsNameSamiLule, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiSweden, weekdayNamesAbbr: weekdayNamesSamiSwedenAbbr}, - "3B": {tags: []string{"se"}, localMonth: localMonthsNameSamiNorthern, apFmt: apFmtSamiNorthern, weekdayNames: weekdayNamesSamiNorthern, weekdayNamesAbbr: weekdayNamesSamiNorthernAbbr}, - "C3B": {tags: []string{"se-FI"}, localMonth: localMonthsNameSamiNorthernFI, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiNorthernFI, weekdayNamesAbbr: weekdayNamesSamiNorthernFIAbbr}, - "43B": {tags: []string{"se-NO"}, localMonth: localMonthsNameSamiNorthern, apFmt: apFmtSamiNorthern, weekdayNames: weekdayNamesSamiNorthern, weekdayNamesAbbr: weekdayNamesSamiNorthernAbbr}, - "83B": {tags: []string{"se-SE"}, localMonth: localMonthsNameSamiNorthern, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiNorthernSE, weekdayNamesAbbr: weekdayNamesSamiNorthernSEAbbr}, - "743B": {tags: []string{"sms"}, localMonth: localMonthsNameSamiSkolt, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiSkolt, weekdayNamesAbbr: weekdayNamesSamiSkoltAbbr}, - "203B": {tags: []string{"sms-FI"}, localMonth: localMonthsNameSamiSkolt, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiSkolt, weekdayNamesAbbr: weekdayNamesSamiSkoltAbbr}, - "783B": {tags: []string{"sma"}, localMonth: localMonthsNameSamiSouthern, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiSouthern, weekdayNamesAbbr: weekdayNamesSamiSouthernAbbr}, - "183B": {tags: []string{"sma-NO"}, localMonth: localMonthsNameSamiSouthern, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiSouthern, weekdayNamesAbbr: weekdayNamesSamiSouthernAbbr}, - "1C3B": {tags: []string{"sma-SE"}, localMonth: localMonthsNameSamiSouthern, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSamiSouthern, weekdayNamesAbbr: weekdayNamesSamiSouthernAbbr}, - "4F": {tags: []string{"sa"}, localMonth: localMonthsNameSanskrit, apFmt: apFmtSanskrit, weekdayNames: weekdayNamesSanskrit, weekdayNamesAbbr: weekdayNamesSanskritAbbr}, - "44F": {tags: []string{"sa-IN"}, localMonth: localMonthsNameSanskrit, apFmt: apFmtSanskrit, weekdayNames: weekdayNamesSanskrit, weekdayNamesAbbr: weekdayNamesSanskritAbbr}, - "91": {tags: []string{"gd"}, localMonth: localMonthsNameScottishGaelic, apFmt: apFmtScottishGaelic, weekdayNames: weekdayNamesGaelic, weekdayNamesAbbr: weekdayNamesGaelicAbbr}, - "491": {tags: []string{"gd-GB"}, localMonth: localMonthsNameScottishGaelic, apFmt: apFmtScottishGaelic, weekdayNames: weekdayNamesGaelic, weekdayNamesAbbr: weekdayNamesGaelicAbbr}, - "6C1A": {tags: []string{"sr-Cyrl"}, localMonth: localMonthsNameSerbian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSerbian, weekdayNamesAbbr: weekdayNamesSerbianAbbr}, - "1C1A": {tags: []string{"sr-Cyrl-BA"}, localMonth: localMonthsNameSerbianBA, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSerbianBA, weekdayNamesAbbr: weekdayNamesSerbianBAAbbr}, - "301A": {tags: []string{"sr-Cyrl-ME"}, localMonth: localMonthsNameSerbian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSerbianME, weekdayNamesAbbr: weekdayNamesSerbianBAAbbr}, - "281A": {tags: []string{"sr-Cyrl-RS"}, localMonth: localMonthsNameSerbian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSerbian, weekdayNamesAbbr: weekdayNamesSerbianAbbr}, - "C1A": {tags: []string{"sr-Cyrl-CS"}, localMonth: localMonthsNameSerbian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSerbian, weekdayNamesAbbr: weekdayNamesSerbianAbbr}, - "701A": {tags: []string{"sr-Latn"}, localMonth: localMonthsNameSerbianLatin, apFmt: apFmtSerbianLatin, weekdayNames: weekdayNamesSerbianLatin, weekdayNamesAbbr: weekdayNamesSerbianLatinAbbr}, - "7C1A": {tags: []string{"sr"}, localMonth: localMonthsNameSerbianLatin, apFmt: apFmtSerbianLatin, weekdayNames: weekdayNamesSerbianLatin, weekdayNamesAbbr: weekdayNamesSerbianLatinAbbr}, - "181A": {tags: []string{"sr-Latn-BA"}, localMonth: localMonthsNameSerbianLatin, apFmt: apFmtSerbianLatinBA, weekdayNames: weekdayNamesSerbianLatinBA, weekdayNamesAbbr: weekdayNamesSerbianLatinBAAbbr}, - "2C1A": {tags: []string{"sr-Latn-ME"}, localMonth: localMonthsNameSerbianLatin, apFmt: apFmtSerbianLatinBA, weekdayNames: weekdayNamesSerbianLatinME, weekdayNamesAbbr: weekdayNamesSerbianLatinAbbr}, - "241A": {tags: []string{"sr-Latn-RS"}, localMonth: localMonthsNameSerbianLatin, apFmt: apFmtSerbianLatin, weekdayNames: weekdayNamesSerbianLatin, weekdayNamesAbbr: weekdayNamesSerbianLatinAbbr}, - "81A": {tags: []string{"sr-Latn-CS"}, localMonth: localMonthsNameSerbianLatinCS, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSerbianLatin, weekdayNamesAbbr: weekdayNamesSerbianLatinCSAbbr}, - "6C": {tags: []string{"nso"}, localMonth: localMonthsNameSesothoSaLeboa, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSesothoSaLeboa, weekdayNamesAbbr: weekdayNamesSesothoSaLeboaAbbr}, - "46C": {tags: []string{"nso-ZA"}, localMonth: localMonthsNameSesothoSaLeboa, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSesothoSaLeboa, weekdayNamesAbbr: weekdayNamesSesothoSaLeboaAbbr}, - "32": {tags: []string{"tn"}, localMonth: localMonthsNameSetswana, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSetswana, weekdayNamesAbbr: weekdayNamesSetswanaAbbr}, - "832": {tags: []string{"tn-BW"}, localMonth: localMonthsNameSetswana, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSetswana, weekdayNamesAbbr: weekdayNamesSetswanaAbbr}, - "432": {tags: []string{"tn-ZA"}, localMonth: localMonthsNameSetswana, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSetswana, weekdayNamesAbbr: weekdayNamesSetswanaAbbr}, - "59": {tags: []string{"sd"}, localMonth: localMonthsNameSindhi, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSindhi, weekdayNamesAbbr: weekdayNamesSindhiAbbr}, - "7C59": {tags: []string{"sd-Arab"}, localMonth: localMonthsNameSindhi, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSindhi, weekdayNamesAbbr: weekdayNamesSindhiAbbr}, - "859": {tags: []string{"sd-Arab-PK"}, localMonth: localMonthsNameSindhi, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSindhi, weekdayNamesAbbr: weekdayNamesSindhiAbbr}, - "5B": {tags: []string{"si"}, localMonth: localMonthsNameSinhala, apFmt: apFmtSinhala, weekdayNames: weekdayNamesSindhi, weekdayNamesAbbr: weekdayNamesSindhiAbbr}, - "45B": {tags: []string{"si-LK"}, localMonth: localMonthsNameSinhala, apFmt: apFmtSinhala, weekdayNames: weekdayNamesSindhi, weekdayNamesAbbr: weekdayNamesSindhiAbbr}, - "1B": {tags: []string{"sk"}, localMonth: localMonthsNameSlovak, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSlovak, weekdayNamesAbbr: weekdayNamesSlovakAbbr}, - "41B": {tags: []string{"sk-SK"}, localMonth: localMonthsNameSlovak, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSlovak, weekdayNamesAbbr: weekdayNamesSlovakAbbr}, - "24": {tags: []string{"sl"}, localMonth: localMonthsNameSlovenian, apFmt: apFmtSlovenian, weekdayNames: weekdayNamesSlovenian, weekdayNamesAbbr: weekdayNamesSlovenianAbbr}, - "424": {tags: []string{"sl-SI"}, localMonth: localMonthsNameSlovenian, apFmt: apFmtSlovenian, weekdayNames: weekdayNamesSlovenian, weekdayNamesAbbr: weekdayNamesSlovenianAbbr}, - "77": {tags: []string{"so"}, localMonth: localMonthsNameSomali, apFmt: apFmtSomali, weekdayNames: weekdayNamesSomali, weekdayNamesAbbr: weekdayNamesSomaliAbbr}, - "477": {tags: []string{"so-SO"}, localMonth: localMonthsNameSomali, apFmt: apFmtSomali, weekdayNames: weekdayNamesSomali, weekdayNamesAbbr: weekdayNamesSomaliAbbr}, - "30": {tags: []string{"st"}, localMonth: localMonthsNameSotho, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSotho, weekdayNamesAbbr: weekdayNamesSothoAbbr}, - "430": {tags: []string{"st-ZA"}, localMonth: localMonthsNameSotho, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSotho, weekdayNamesAbbr: weekdayNamesSothoAbbr}, - "A": {tags: []string{"es"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishAbbr}, - "2C0A": {tags: []string{"es-AR"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, - "200A": {tags: []string{"es-VE"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, - "400A": {tags: []string{"es-BO"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, - "340A": {tags: []string{"es-CL"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, - "240A": {tags: []string{"es-CO"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, - "140A": {tags: []string{"es-CR"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, - "5C0A": {tags: []string{"es-CU"}, localMonth: localMonthsNameSpanish, apFmt: apFmtCuba, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, - "1C0A": {tags: []string{"es-DO"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, - "300A": {tags: []string{"es-EC"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, - "440A": {tags: []string{"es-SV"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, - "100A": {tags: []string{"es-GT"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, - "480A": {tags: []string{"es-HN"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, - "580A": {tags: []string{"es-419"}, localMonth: localMonthsNameSpanish, apFmt: apFmtCuba, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, - "80A": {tags: []string{"es-MX"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanish, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, - "4C0A": {tags: []string{"es-NI"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, - "180A": {tags: []string{"es-PA"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, - "3C0A": {tags: []string{"es-PY"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, - "280A": {tags: []string{"es-PE"}, localMonth: localMonthsNameSpanishPE, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, - "500A": {tags: []string{"es-PR"}, localMonth: localMonthsNameSpanish, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, - "40A": {tags: []string{"es-ES_tradnl"}, localMonth: localMonthsNameSpanish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishAbbr}, - "C0A": {tags: []string{"es-ES"}, localMonth: localMonthsNameSpanish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishAbbr}, - "540A": {tags: []string{"es-US"}, localMonth: localMonthsNameSpanish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishUSAbbr}, - "380A": {tags: []string{"es-UY"}, localMonth: localMonthsNameSpanishPE, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesSpanish, weekdayNamesAbbr: weekdayNamesSpanishARAbbr}, - "1D": {tags: []string{"sv"}, localMonth: localMonthsNameSwedish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSwedish, weekdayNamesAbbr: weekdayNamesSwedishAbbr}, - "81D": {tags: []string{"sv-FI"}, localMonth: localMonthsNameSwedishFI, apFmt: apFmtSwedish, weekdayNames: weekdayNamesSwedish, weekdayNamesAbbr: weekdayNamesSwedishAbbr}, - "41D": {tags: []string{"sv-SE"}, localMonth: localMonthsNameSwedishFI, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesSwedish, weekdayNamesAbbr: weekdayNamesSwedishAbbr}, - "5A": {tags: []string{"syr"}, localMonth: localMonthsNameSyriac, apFmt: apFmtSyriac, weekdayNames: weekdayNamesSyriac, weekdayNamesAbbr: weekdayNamesSyriacAbbr}, - "45A": {tags: []string{"syr-SY"}, localMonth: localMonthsNameSyriac, apFmt: apFmtSyriac, weekdayNames: weekdayNamesSyriac, weekdayNamesAbbr: weekdayNamesSyriacAbbr}, - "28": {tags: []string{"tg"}, localMonth: localMonthsNameTajik, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTajik, weekdayNamesAbbr: weekdayNamesTajikAbbr}, - "7C28": {tags: []string{"tg-Cyrl"}, localMonth: localMonthsNameTajik, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTajik, weekdayNamesAbbr: weekdayNamesTajikAbbr}, - "428": {tags: []string{"tg-Cyrl-TJ"}, localMonth: localMonthsNameTajik, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTajik, weekdayNamesAbbr: weekdayNamesTajikAbbr}, - "5F": {tags: []string{"tzm"}, localMonth: localMonthsNameTamazight, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTamazight, weekdayNamesAbbr: weekdayNamesTamazightAbbr}, - "7C5F": {tags: []string{"tzm-Latn"}, localMonth: localMonthsNameTamazight, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTamazight, weekdayNamesAbbr: weekdayNamesTamazightAbbr}, - "85F": {tags: []string{"tzm-Latn-DZ"}, localMonth: localMonthsNameTamazight, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTamazight, weekdayNamesAbbr: weekdayNamesTamazightAbbr}, - "49": {tags: []string{"ta"}, localMonth: localMonthsNameTamil, apFmt: apFmtTamil, weekdayNames: weekdayNamesTamil, weekdayNamesAbbr: weekdayNamesTamilAbbr}, - "449": {tags: []string{"ta-IN"}, localMonth: localMonthsNameTamil, apFmt: apFmtTamil, weekdayNames: weekdayNamesTamil, weekdayNamesAbbr: weekdayNamesTamilAbbr}, - "849": {tags: []string{"ta-LK"}, localMonth: localMonthsNameTamilLK, apFmt: apFmtTamil, weekdayNames: weekdayNamesTamilLK, weekdayNamesAbbr: weekdayNamesTamilLKAbbr}, - "44": {tags: []string{"tt"}, localMonth: localMonthsNameTatar, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTatar, weekdayNamesAbbr: weekdayNamesTatarAbbr}, - "444": {tags: []string{"tt-RU"}, localMonth: localMonthsNameTatar, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTatar, weekdayNamesAbbr: weekdayNamesTatarAbbr}, - "4A": {tags: []string{"te"}, localMonth: localMonthsNameTelugu, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTelugu, weekdayNamesAbbr: weekdayNamesTeluguAbbr}, - "44A": {tags: []string{"te-IN"}, localMonth: localMonthsNameTelugu, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTelugu, weekdayNamesAbbr: weekdayNamesTeluguAbbr}, - "1E": {tags: []string{"th"}, localMonth: localMonthsNameThai, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesThai, weekdayNamesAbbr: weekdayNamesThaiAbbr}, - "41E": {tags: []string{"th-TH"}, localMonth: localMonthsNameThai, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesThai, weekdayNamesAbbr: weekdayNamesThaiAbbr}, - "51": {tags: []string{"bo"}, localMonth: localMonthsNameTibetan, apFmt: apFmtTibetan, weekdayNames: weekdayNamesTibetan, weekdayNamesAbbr: weekdayNamesTibetanAbbr}, - "451": {tags: []string{"bo-CN"}, localMonth: localMonthsNameTibetan, apFmt: apFmtTibetan, weekdayNames: weekdayNamesTibetan, weekdayNamesAbbr: weekdayNamesTibetanAbbr}, - "73": {tags: []string{"ti"}, localMonth: localMonthsNameTigrinya, apFmt: apFmtTigrinya, weekdayNames: weekdayNamesTigrinya, weekdayNamesAbbr: weekdayNamesTigrinyaAbbr}, - "873": {tags: []string{"ti-ER"}, localMonth: localMonthsNameTigrinya, apFmt: apFmtTigrinyaER, weekdayNames: weekdayNamesTigrinya, weekdayNamesAbbr: weekdayNamesTigrinyaAbbr}, - "473": {tags: []string{"ti-ET"}, localMonth: localMonthsNameTigrinya, apFmt: apFmtTigrinya, weekdayNames: weekdayNamesTigrinya, weekdayNamesAbbr: weekdayNamesTigrinyaAbbr}, - "31": {tags: []string{"ts"}, localMonth: localMonthsNameTsonga, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTsonga, weekdayNamesAbbr: weekdayNamesTsongaAbbr}, - "431": {tags: []string{"ts-ZA"}, localMonth: localMonthsNameTsonga, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTsonga, weekdayNamesAbbr: weekdayNamesTsongaAbbr}, - "1F": {tags: []string{"tr"}, localMonth: localMonthsNameTurkish, apFmt: apFmtTurkish, weekdayNames: weekdayNamesTurkish, weekdayNamesAbbr: weekdayNamesTurkishAbbr}, - "41F": {tags: []string{"tr-TR"}, localMonth: localMonthsNameTurkish, apFmt: apFmtTurkish, weekdayNames: weekdayNamesTurkish, weekdayNamesAbbr: weekdayNamesTurkishAbbr}, - "42": {tags: []string{"tk"}, localMonth: localMonthsNameTurkmen, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTurkmen, weekdayNamesAbbr: weekdayNamesTurkmenAbbr}, - "442": {tags: []string{"tk-TM"}, localMonth: localMonthsNameTurkmen, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesTurkmen, weekdayNamesAbbr: weekdayNamesTurkmenAbbr}, - "22": {tags: []string{"uk"}, localMonth: localMonthsNameUkrainian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesUkrainian, weekdayNamesAbbr: weekdayNamesUkrainianAbbr}, - "422": {tags: []string{"uk-UA"}, localMonth: localMonthsNameUkrainian, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesUkrainian, weekdayNamesAbbr: weekdayNamesUkrainianAbbr}, - "2E": {tags: []string{"hsb"}, localMonth: localMonthsNameUpperSorbian, apFmt: apFmtUpperSorbian, weekdayNames: weekdayNamesSorbian, weekdayNamesAbbr: weekdayNamesSorbianAbbr}, - "42E": {tags: []string{"hsb-DE"}, localMonth: localMonthsNameUpperSorbian, apFmt: apFmtUpperSorbian, weekdayNames: weekdayNamesSorbian, weekdayNamesAbbr: weekdayNamesSorbianAbbr}, - "20": {tags: []string{"ur"}, localMonth: localMonthsNamePunjabiArab, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesUrdu, weekdayNamesAbbr: weekdayNamesUrdu}, - "820": {tags: []string{"ur-IN"}, localMonth: localMonthsNamePunjabiArab, apFmt: apFmtUrdu, weekdayNames: weekdayNamesUrduIN, weekdayNamesAbbr: weekdayNamesUrduIN}, - "420": {tags: []string{"ur-PK"}, localMonth: localMonthsNamePunjabiArab, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesUrdu, weekdayNamesAbbr: weekdayNamesUrdu}, - "80": {tags: []string{"ug"}, localMonth: localMonthsNameUyghur, apFmt: apFmtUyghur, weekdayNames: weekdayNamesUyghur, weekdayNamesAbbr: weekdayNamesUyghurAbbr}, - "480": {tags: []string{"ug-CN"}, localMonth: localMonthsNameUyghur, apFmt: apFmtUyghur, weekdayNames: weekdayNamesUyghur, weekdayNamesAbbr: weekdayNamesUyghurAbbr}, - "7843": {tags: []string{"uz-Cyrl"}, localMonth: localMonthsNameUzbekCyrillic, apFmt: apFmtUzbekCyrillic, weekdayNames: weekdayNamesUzbekCyrillic, weekdayNamesAbbr: weekdayNamesUzbekCyrillicAbbr}, - "843": {tags: []string{"uz-Cyrl-UZ"}, localMonth: localMonthsNameUzbekCyrillic, apFmt: apFmtUzbekCyrillic, weekdayNames: weekdayNamesUzbekCyrillic, weekdayNamesAbbr: weekdayNamesUzbekCyrillicAbbr}, - "43": {tags: []string{"uz"}, localMonth: localMonthsNameUzbek, apFmt: apFmtUzbek, weekdayNames: weekdayNamesUzbek, weekdayNamesAbbr: weekdayNamesUzbekAbbr}, - "7C43": {tags: []string{"uz-Latn"}, localMonth: localMonthsNameUzbek, apFmt: apFmtUzbek, weekdayNames: weekdayNamesUzbek, weekdayNamesAbbr: weekdayNamesUzbekAbbr}, - "443": {tags: []string{"uz-Latn-UZ"}, localMonth: localMonthsNameUzbek, apFmt: apFmtUzbek, weekdayNames: weekdayNamesUzbek, weekdayNamesAbbr: weekdayNamesUzbekAbbr}, - "803": {tags: []string{"ca-ES-valencia"}, localMonth: localMonthsNameValencian, apFmt: apFmtSpanishAR, weekdayNames: weekdayNamesValencian, weekdayNamesAbbr: weekdayNamesValencianAbbr}, - "33": {tags: []string{"ve"}, localMonth: localMonthsNameVenda, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesVenda, weekdayNamesAbbr: weekdayNamesVendaAbbr}, - "433": {tags: []string{"ve-ZA"}, localMonth: localMonthsNameVenda, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesVenda, weekdayNamesAbbr: weekdayNamesVendaAbbr}, - "2A": {tags: []string{"vi"}, localMonth: localMonthsNameVietnamese, apFmt: apFmtVietnamese, weekdayNames: weekdayNamesVietnamese, weekdayNamesAbbr: weekdayNamesVietnameseAbbr}, - "42A": {tags: []string{"vi-VN"}, localMonth: localMonthsNameVietnamese, apFmt: apFmtVietnamese, weekdayNames: weekdayNamesVietnamese, weekdayNamesAbbr: weekdayNamesVietnameseAbbr}, - "52": {tags: []string{"cy"}, localMonth: localMonthsNameWelsh, apFmt: apFmtWelsh, weekdayNames: weekdayNamesWelsh, weekdayNamesAbbr: weekdayNamesWelshAbbr}, - "452": {tags: []string{"cy-GB"}, localMonth: localMonthsNameWelsh, apFmt: apFmtWelsh, weekdayNames: weekdayNamesWelsh, weekdayNamesAbbr: weekdayNamesWelshAbbr}, - "88": {tags: []string{"wo"}, localMonth: localMonthsNameWolof, apFmt: apFmtWolof, weekdayNames: weekdayNamesWolof, weekdayNamesAbbr: weekdayNamesWolofAbbr}, - "488": {tags: []string{"wo-SN"}, localMonth: localMonthsNameWolof, apFmt: apFmtWolof, weekdayNames: weekdayNamesWolof, weekdayNamesAbbr: weekdayNamesWolofAbbr}, - "34": {tags: []string{"xh"}, localMonth: localMonthsNameXhosa, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesXhosa, weekdayNamesAbbr: weekdayNamesXhosaAbbr}, - "434": {tags: []string{"xh-ZA"}, localMonth: localMonthsNameXhosa, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesXhosa, weekdayNamesAbbr: weekdayNamesXhosaAbbr}, - "78": {tags: []string{"ii"}, localMonth: localMonthsNameYi, apFmt: apFmtYi, weekdayNames: weekdayNamesYi, weekdayNamesAbbr: weekdayNamesYiAbbr}, - "478": {tags: []string{"ii-CN"}, localMonth: localMonthsNameYi, apFmt: apFmtYi, weekdayNames: weekdayNamesYi, weekdayNamesAbbr: weekdayNamesYiAbbr}, - "43D": {tags: []string{"yi-001"}, localMonth: localMonthsNameYiddish, apFmt: apFmtYiddish, weekdayNames: weekdayNamesYiddish, weekdayNamesAbbr: weekdayNamesYiddishAbbr}, - "6A": {tags: []string{"yo"}, localMonth: localMonthsNameYoruba, apFmt: apFmtYoruba, weekdayNames: weekdayNamesYoruba, weekdayNamesAbbr: weekdayNamesYorubaAbbr}, - "46A": {tags: []string{"yo-NG"}, localMonth: localMonthsNameYoruba, apFmt: apFmtYoruba, weekdayNames: weekdayNamesYoruba, weekdayNamesAbbr: weekdayNamesYorubaAbbr}, - "35": {tags: []string{"zu"}, localMonth: localMonthsNameZulu, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesZulu, weekdayNamesAbbr: weekdayNamesZuluAbbr}, - "435": {tags: []string{"zu-ZA"}, localMonth: localMonthsNameZulu, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesZulu, weekdayNamesAbbr: weekdayNamesZuluAbbr}, } // japaneseEraYears list the Japanese era name periods. japaneseEraYears = []time.Time{ @@ -4633,6 +4636,31 @@ var ( } ) +// getSupportedLanguageInfo returns language infomation by giving language code. +// This function does not support different calendar type of the language +// currently. For example: the hexadecimal language code 3010429 (fa-IR,301) +// will be convert to 0429 (fa-IR). +func getSupportedLanguageInfo(lang string) (languageInfo, bool) { + hex := lang + if len(hex) > 4 { + hex = hex[len(hex)-4:] + } + n := new(big.Int) + n.SetString(hex, 16) + if info, ok := supportedLanguageInfo[int(n.Int64())]; ok { + return info, ok + } + if info, ok := supportedLanguageCodeInfo[lang]; ok { + return info, ok + } + for _, info := range supportedLanguageInfo { + if inStrSlice(info.tags, lang, false) != -1 { + return info, true + } + } + return languageInfo{}, false +} + // 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 { @@ -5142,21 +5170,21 @@ func (nf *numberFormat) currencyLanguageHandler(token nfp.Token) (bool, error) { return false, ErrUnsupportedNumberFormat } if part.Token.TType == nfp.TokenSubTypeLanguageInfo { - if strings.EqualFold(part.Token.TValue, "F800") { // [$-x-sysdate] + if inStrSlice([]string{"F800", "x-sysdate", "1010000"}, part.Token.TValue, false) != -1 { if nf.opts != nil && nf.opts.LongDatePattern != "" { nf.value = format(nf.value, nf.opts.LongDatePattern, nf.date1904, nf.cellType, nf.opts) return true, nil } part.Token.TValue = "409" } - if strings.EqualFold(part.Token.TValue, "F400") { // [$-x-systime] + if inStrSlice([]string{"F400", "x-systime"}, part.Token.TValue, false) != -1 { if nf.opts != nil && nf.opts.LongTimePattern != "" { nf.value = format(nf.value, nf.opts.LongTimePattern, nf.date1904, nf.cellType, nf.opts) return true, nil } part.Token.TValue = "409" } - if _, ok := supportedLanguageInfo[strings.ToUpper(part.Token.TValue)]; !ok { + if _, ok := getSupportedLanguageInfo(strings.ToUpper(part.Token.TValue)); !ok { return false, ErrUnsupportedNumberFormat } nf.localCode = strings.ToUpper(part.Token.TValue) @@ -5170,7 +5198,7 @@ func (nf *numberFormat) currencyLanguageHandler(token nfp.Token) (bool, error) { // localAmPm return AM/PM name by supported language ID. func (nf *numberFormat) localAmPm(ap string) string { - if languageInfo, ok := supportedLanguageInfo[nf.localCode]; ok { + if languageInfo, ok := getSupportedLanguageInfo(nf.localCode); ok { return languageInfo.apFmt } return ap @@ -6813,7 +6841,7 @@ func localMonthsNameZulu(t time.Time, abbr int) string { // localMonthsName return months name by supported language ID. func (nf *numberFormat) localMonthsName(abbr int) string { - if languageInfo, ok := supportedLanguageInfo[nf.localCode]; ok { + if languageInfo, ok := getSupportedLanguageInfo(nf.localCode); ok { return languageInfo.localMonth(nf.t, abbr) } return localMonthsNameEnglish(nf.t, abbr) @@ -6892,7 +6920,8 @@ func (nf *numberFormat) yearsHandler(token nfp.Token) { if year == -1 { return } - nf.useGannen = supportedLanguageInfo[nf.localCode].useGannen + langInfo, _ := getSupportedLanguageInfo(nf.localCode) + nf.useGannen = langInfo.useGannen switch len(token.TValue) { case 1: nf.useGannen = false @@ -6927,7 +6956,8 @@ func (nf *numberFormat) yearsHandler(token nfp.Token) { // daysHandler will be handling days in the date and times types tokens for a // number format expression. func (nf *numberFormat) daysHandler(token nfp.Token) { - info, l := supportedLanguageInfo[nf.localCode], len(token.TValue) + info, _ := getSupportedLanguageInfo(nf.localCode) + l := len(token.TValue) weekdayNames, weekdayNamesAbbr := info.weekdayNames, info.weekdayNamesAbbr if len(weekdayNames) != 7 { weekdayNames = weekdayNamesEnglish diff --git a/numfmt_test.go b/numfmt_test.go index 9e59fb256d..21a07d26fe 100644 --- a/numfmt_test.go +++ b/numfmt_test.go @@ -82,22 +82,62 @@ func TestNumFmt(t *testing.T) { {"text", "AM/PM h h:mm", "text"}, {"43466.189571759256", "[$-404]aaa;@", "週二"}, {"43466.189571759256", "[$-404]aaaa;@", "星期二"}, + {"43466.189571759256", "[$-zh-TW]aaa;@", "週二"}, + {"43466.189571759256", "[$-zh-TW]aaaa;@", "星期二"}, {"43466.189571759256", "[$-804]aaa;@", "周二"}, {"43466.189571759256", "[$-804]aaaa;@", "星期二"}, + {"43466.189571759256", "[$-0804]aaa;@", "周二"}, + {"43466.189571759256", "[$-0804]aaaa;@", "星期二"}, + {"43466.189571759256", "[$-zh-CN]aaa;@", "周二"}, + {"43466.189571759256", "[$-zh-CN]aaaa;@", "星期二"}, {"43466.189571759256", "[$-435]aaa;@", "Bi."}, {"43466.189571759256", "[$-435]aaaa;@", "ULwesibili"}, + {"43466.189571759256", "[$-0435]aaa;@", "Bi."}, + {"43466.189571759256", "[$-0435]aaaa;@", "ULwesibili"}, + {"43466.189571759256", "[$-zu-ZA]aaa;@", "Bi."}, + {"43466.189571759256", "[$-zu-ZA]aaaa;@", "ULwesibili"}, {"43466.189571759256", "[$-404]ddd;@", "週二"}, {"43466.189571759256", "[$-404]dddd;@", "星期二"}, + {"43466.189571759256", "[$-0404]ddd;@", "週二"}, + {"43466.189571759256", "[$-0404]dddd;@", "星期二"}, + {"43466.189571759256", "[$-zh-TW]ddd;@", "週二"}, + {"43466.189571759256", "[$-zh-TW]dddd;@", "星期二"}, {"43466.189571759256", "[$-804]ddd;@", "周二"}, {"43466.189571759256", "[$-804]dddd;@", "星期二"}, + {"43466.189571759256", "[$-0804]ddd;@", "周二"}, + {"43466.189571759256", "[$-0804]dddd;@", "星期二"}, + {"43466.189571759256", "[$-zh-CN]ddd;@", "周二"}, + {"43466.189571759256", "[$-zh-CN]dddd;@", "星期二"}, {"43466.189571759256", "[$-435]ddd;@", "Bi."}, {"43466.189571759256", "[$-435]dddd;@", "ULwesibili"}, + {"43466.189571759256", "[$-0435]ddd;@", "Bi."}, + {"43466.189571759256", "[$-0435]dddd;@", "ULwesibili"}, + {"43466.189571759256", "[$-zu-ZA]ddd;@", "Bi."}, + {"43466.189571759256", "[$-zu-ZA]dddd;@", "ULwesibili"}, {"44562.189571759256", "[$-36]mmm dd yyyy h:mm AM/PM d", "Jan. 01 2022 4:32 vm. 1"}, {"44562.189571759256", "[$-36]mmmm dd yyyy h:mm AM/PM dd", "Januarie 01 2022 4:32 vm. 01"}, {"44562.189571759256", "[$-36]mmmmm dd yyyy h:mm AM/PM ddd", "J 01 2022 4:32 vm. Sa."}, {"44682.18957170139", "[$-36]mmm dd yyyy h:mm AM/PM dddd", "Mei 01 2022 4:32 vm. Sondag"}, {"44682.18957170139", "[$-36]mmmm dd yyyy h:mm AM/PM aaa", "Mei 01 2022 4:32 vm. So."}, {"44682.18957170139", "[$-36]mmmmm dd yyyy h:mm AM/PM aaaa", "M 01 2022 4:32 vm. Sondag"}, + {"44562.189571759256", "[$-036]mmm dd yyyy h:mm AM/PM d", "Jan. 01 2022 4:32 vm. 1"}, + {"44562.189571759256", "[$-036]mmmm dd yyyy h:mm AM/PM dd", "Januarie 01 2022 4:32 vm. 01"}, + {"44562.189571759256", "[$-036]mmmmm dd yyyy h:mm AM/PM ddd", "J 01 2022 4:32 vm. Sa."}, + {"44682.18957170139", "[$-036]mmm dd yyyy h:mm AM/PM dddd", "Mei 01 2022 4:32 vm. Sondag"}, + {"44682.18957170139", "[$-036]mmmm dd yyyy h:mm AM/PM aaa", "Mei 01 2022 4:32 vm. So."}, + {"44682.18957170139", "[$-036]mmmmm dd yyyy h:mm AM/PM aaaa", "M 01 2022 4:32 vm. Sondag"}, + {"44562.189571759256", "[$-0036]mmm dd yyyy h:mm AM/PM d", "Jan. 01 2022 4:32 vm. 1"}, + {"44562.189571759256", "[$-0036]mmmm dd yyyy h:mm AM/PM dd", "Januarie 01 2022 4:32 vm. 01"}, + {"44562.189571759256", "[$-0036]mmmmm dd yyyy h:mm AM/PM ddd", "J 01 2022 4:32 vm. Sa."}, + {"44682.18957170139", "[$-0036]mmm dd yyyy h:mm AM/PM dddd", "Mei 01 2022 4:32 vm. Sondag"}, + {"44682.18957170139", "[$-0036]mmmm dd yyyy h:mm AM/PM aaa", "Mei 01 2022 4:32 vm. So."}, + {"44682.18957170139", "[$-0036]mmmmm dd yyyy h:mm AM/PM aaaa", "M 01 2022 4:32 vm. Sondag"}, + {"44562.189571759256", "[$-af]mmm dd yyyy h:mm AM/PM d", "Jan. 01 2022 4:32 vm. 1"}, + {"44562.189571759256", "[$-af]mmmm dd yyyy h:mm AM/PM dd", "Januarie 01 2022 4:32 vm. 01"}, + {"44562.189571759256", "[$-af]mmmmm dd yyyy h:mm AM/PM ddd", "J 01 2022 4:32 vm. Sa."}, + {"44682.18957170139", "[$-af]mmm dd yyyy h:mm AM/PM dddd", "Mei 01 2022 4:32 vm. Sondag"}, + {"44682.18957170139", "[$-af]mmmm dd yyyy h:mm AM/PM aaa", "Mei 01 2022 4:32 vm. So."}, + {"44682.18957170139", "[$-af]mmmmm dd yyyy h:mm AM/PM aaaa", "M 01 2022 4:32 vm. Sondag"}, {"44562.189571759256", "[$-436]mmm dd yyyy h:mm AM/PM d", "Jan. 01 2022 4:32 vm. 1"}, {"44562.189571759256", "[$-436]mmmm dd yyyy h:mm AM/PM dd", "Januarie 01 2022 4:32 vm. 01"}, {"44562.189571759256", "[$-436]mmmmm dd yyyy h:mm AM/PM ddd", "J 01 2022 4:32 vm. Sa."}, @@ -248,6 +288,14 @@ func TestNumFmt(t *testing.T) { {"43543.503206018519", "[$-401]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, {"43543.503206018519", "[$-401]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, {"43543.503206018519", "[$-401]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"44562.189571759256", "[$-1010401]mmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-1010401]mmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-1010401]mmmmm dd yyyy h:mm AM/PM", "\u064A 01 2022 4:32 \u0635"}, + {"44562.189571759256", "[$-1010401]mmmmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"}, + {"43543.503206018519", "[$-1010401]mmm dd yyyy h:mm AM/PM", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645"}, + {"43543.503206018519", "[$-1010401]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"43543.503206018519", "[$-1010401]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"43543.503206018519", "[$-1010401]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, {"44562.189571759256", "[$-2801]mmm dd yyyy h:mm AM/PM", "\u0643\u0627\u0646\u0648\u0646%A0\u0627\u0644\u062B\u0627\u0646\u064A 01 2022 4:32 \u0635"}, {"44562.189571759256", "[$-2801]mmmm dd yyyy h:mm AM/PM", "\u0643\u0627\u0646\u0648\u0646%A0\u0627\u0644\u062B\u0627\u0646\u064A 01 2022 4:32 \u0635"}, {"44562.189571759256", "[$-2801]mmmmm dd yyyy h:mm AM/PM", "\u0643 01 2022 4:32 \u0635"}, @@ -3588,7 +3636,9 @@ func TestNumFmt(t *testing.T) { // 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", "[$-x-sysdate]dddd, mmmm dd, yyyy", "2019年3月19日"}, {"43543.503206018519", "[$-F400]h:mm:ss AM/PM", "12:04:37"}, + {"43543.503206018519", "[$-x-systime]h:mm:ss AM/PM", "12:04:37"}, } { result := format(item[0], item[1], false, CellTypeNumber, &Options{ ShortDatePattern: "yyyy/m/d", From 8f871316083c527f32f8e21e324d07d7cf6ec80e Mon Sep 17 00:00:00 2001 From: centurion-hub Date: Sat, 31 Aug 2024 10:57:26 +0800 Subject: [PATCH 906/957] This closes #1979, fix decimal value round issue (#1984) - Updated unit tests --- numfmt.go | 47 ++++++++++++++++++++++++++--------------------- numfmt_test.go | 1 + 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/numfmt.go b/numfmt.go index f34707b84e..cae88d64ab 100644 --- a/numfmt.go +++ b/numfmt.go @@ -4838,12 +4838,26 @@ func format(value, numFmt string, date1904 bool, cellType CellType, opts *Option // 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), ".") +func (nf *numberFormat) getNumberPartLen() (int, int) { + var intPart, fracPart, intLen, fracLen int + parts := strings.Split(strconv.FormatFloat(math.Abs(nf.number), 'f', -1, 64), ".") + intPart = len(parts[0]) if len(parts) == 2 { - return len(parts[0]), len(parts[1]) + fracPart = len(parts[1]) } - return len(parts[0]), 0 + if nf.intHolder > intPart { + nf.intHolder = intPart + } + if intLen = intPart; nf.intPadding+nf.intHolder > intPart { + intLen = nf.intPadding + nf.intHolder + } + if fracLen = fracPart; fracPart > nf.fracHolder+nf.fracPadding { + fracLen = nf.fracHolder + nf.fracPadding + } + if nf.fracPadding > fracPart { + fracLen = nf.fracPadding + } + return intLen, fracLen } // getNumberFmtConf generate the number format padding and placeholder @@ -5021,25 +5035,12 @@ func (nf *numberFormat) printBigNumber(decimal float64, fracLen int) string { // numberHandler handling number format expression for positive and negative // numeric. func (nf *numberFormat) numberHandler() string { + nf.getNumberFmtConf() var ( - num = nf.number - intPart, fracPart = getNumberPartLen(nf.number) - intLen, fracLen int - result string + num = nf.number + intLen, fracLen = nf.getNumberPartLen() + result string ) - nf.getNumberFmtConf() - if nf.intHolder > intPart { - nf.intHolder = intPart - } - if intLen = intPart; nf.intPadding+nf.intHolder > intPart { - intLen = nf.intPadding + nf.intHolder - } - 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 && !nf.useScientificNotation { return nf.printNumberLiteral(nf.printBigNumber(decimal, fracLen)) @@ -5062,6 +5063,10 @@ func (nf *numberFormat) numberHandler() string { if nf.useFraction { num = math.Floor(math.Abs(num)) } + if !nf.useScientificNotation { + ratio := math.Pow(10, float64(fracLen)) + num = math.Round(num*ratio) / ratio + } if result = fmt.Sprintf(fmtCode, math.Abs(num)); nf.useCommaSep { result = printCommaSep(result) } diff --git a/numfmt_test.go b/numfmt_test.go index 21a07d26fe..61bf41bb48 100644 --- a/numfmt_test.go +++ b/numfmt_test.go @@ -3553,6 +3553,7 @@ func TestNumFmt(t *testing.T) { {"-8.04506", "$#,##0.00_);[Red]($#,##0.00)", "($8.05)"}, {"43543.5448726851", `_("$"* #,##0.00_);_("$"* \(#,##0.00\);_("$"* "-"??_);_(@_)`, " $43,543.54 "}, {"1234.5678", "0", "1235"}, + {"1234.125", "0.00", "1234.13"}, {"1234.5678", "0.00", "1234.57"}, {"1234.5678", "#,##0", "1,235"}, {"1234.5678", "#,##0.00", "1,234.57"}, From 29366fd126fe5c6df103c2392a59c3a16ebd5a69 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 1 Sep 2024 12:10:01 +0800 Subject: [PATCH 907/957] Add new fields for pivot table options and pivot field options - Add new fields FieldPrintTitles and ItemPrintTitles in the PivotTableOptions data type - Add new fields ShowAll and InsertBlankRow in the PivotTableField data type - Export 4 constants ExtURIDataField, ExtURIPivotField, ExtURIPivotFilter and ExtURIPivotHierarchy - Update unit tests - Update dependencies modules --- go.mod | 6 +++--- go.sum | 12 ++++++------ pivotTable.go | 28 +++++++++++++++++++++------- pivotTable_test.go | 6 ++++-- templates.go | 4 ++++ 5 files changed, 38 insertions(+), 18 deletions(-) diff --git a/go.mod b/go.mod index 6e31bbc9c0..c9192e52d9 100644 --- a/go.mod +++ b/go.mod @@ -8,10 +8,10 @@ require ( github.com/stretchr/testify v1.8.4 github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 - golang.org/x/crypto v0.25.0 + golang.org/x/crypto v0.26.0 golang.org/x/image v0.18.0 - golang.org/x/net v0.27.0 - golang.org/x/text v0.16.0 + golang.org/x/net v0.28.0 + golang.org/x/text v0.17.0 ) require ( diff --git a/go.sum b/go.sum index 904edc85c8..346cd7758d 100644 --- a/go.sum +++ b/go.sum @@ -15,14 +15,14 @@ github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7 github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A= github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= -golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= -golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= -golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= -golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/pivotTable.go b/pivotTable.go index 6205c54311..9caf037872 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -58,6 +58,8 @@ type PivotTableOptions struct { ShowRowStripes bool ShowColStripes bool ShowLastColumn bool + FieldPrintTitles bool + ItemPrintTitles bool PivotTableStyleName string } @@ -90,6 +92,8 @@ type PivotTableField struct { Data string Name string Outline bool + ShowAll bool + InsertBlankRow bool Subtotal string DefaultSubtotal bool NumFmt int @@ -349,6 +353,8 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, opts *PivotTableOptions) CreatedVersion: pivotTableVersion, CompactData: &opts.CompactData, ShowError: &opts.ShowError, + FieldPrintTitles: opts.FieldPrintTitles, + ItemPrintTitles: opts.ItemPrintTitles, DataCaption: "Values", Location: &xlsxLocation{ Ref: topLeftCell + ":" + bottomRightCell, @@ -555,6 +561,8 @@ func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opts *PivotTableOpti DataField: inPivotTableField(opts.Data, name) != -1, Compact: &rowOptions.Compact, Outline: &rowOptions.Outline, + ShowAll: rowOptions.ShowAll, + InsertBlankRow: rowOptions.InsertBlankRow, DefaultSubtotal: &rowOptions.DefaultSubtotal, Items: &xlsxItems{ Count: len(items), @@ -591,6 +599,8 @@ func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opts *PivotTableOpti DataField: inPivotTableField(opts.Data, name) != -1, Compact: &columnOptions.Compact, Outline: &columnOptions.Outline, + ShowAll: columnOptions.ShowAll, + InsertBlankRow: columnOptions.InsertBlankRow, DefaultSubtotal: &columnOptions.DefaultSubtotal, Items: &xlsxItems{ Count: len(items), @@ -831,12 +841,14 @@ func (f *File) getPivotTable(sheet, pivotTableXML, pivotCacheRels string) (Pivot return opts, err } opts = PivotTableOptions{ - pivotTableXML: pivotTableXML, - pivotCacheXML: pivotCacheXML, - pivotSheetName: sheet, - DataRange: fmt.Sprintf("%s!%s", pc.CacheSource.WorksheetSource.Sheet, pc.CacheSource.WorksheetSource.Ref), - PivotTableRange: fmt.Sprintf("%s!%s", sheet, pt.Location.Ref), - Name: pt.Name, + pivotTableXML: pivotTableXML, + pivotCacheXML: pivotCacheXML, + pivotSheetName: sheet, + DataRange: fmt.Sprintf("%s!%s", pc.CacheSource.WorksheetSource.Sheet, pc.CacheSource.WorksheetSource.Ref), + PivotTableRange: fmt.Sprintf("%s!%s", sheet, pt.Location.Ref), + Name: pt.Name, + FieldPrintTitles: pt.FieldPrintTitles, + ItemPrintTitles: pt.ItemPrintTitles, } if pc.CacheSource.WorksheetSource.Name != "" { opts.DataRange = pc.CacheSource.WorksheetSource.Name @@ -924,7 +936,9 @@ func (f *File) extractPivotTableFields(order []string, pt *xlsxPivotTableDefinit // settings by given pivot table fields. func extractPivotTableField(data string, fld *xlsxPivotField) PivotTableField { pivotTableField := PivotTableField{ - Data: data, + Data: data, + ShowAll: fld.ShowAll, + InsertBlankRow: fld.InsertBlankRow, } fields := []string{"Compact", "Name", "Outline", "Subtotal", "DefaultSubtotal"} immutable, mutable := reflect.ValueOf(*fld), reflect.ValueOf(&pivotTableField).Elem() diff --git a/pivotTable_test.go b/pivotTable_test.go index 50f95bfc43..74ce61bec0 100644 --- a/pivotTable_test.go +++ b/pivotTable_test.go @@ -31,9 +31,9 @@ func TestPivotTable(t *testing.T) { DataRange: "Sheet1!A1:E31", PivotTableRange: "Sheet1!G2:M34", Name: "PivotTable1", - Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, + Rows: []PivotTableField{{Data: "Month", ShowAll: true, DefaultSubtotal: true}, {Data: "Year"}}, Filter: []PivotTableField{{Data: "Region"}}, - Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, + Columns: []PivotTableField{{Data: "Type", ShowAll: true, InsertBlankRow: true, DefaultSubtotal: true}}, Data: []PivotTableField{{Data: "Sales", Subtotal: "Sum", Name: "Summarize by Sum", NumFmt: 38}}, RowGrandTotals: true, ColGrandTotals: true, @@ -42,6 +42,8 @@ func TestPivotTable(t *testing.T) { ShowColHeaders: true, ShowLastColumn: true, ShowError: true, + ItemPrintTitles: true, + FieldPrintTitles: true, PivotTableStyleName: "PivotStyleLight16", } assert.NoError(t, f.AddPivotTable(expected)) diff --git a/templates.go b/templates.go index 3bf4c5fe69..5aafc43e8a 100644 --- a/templates.go +++ b/templates.go @@ -102,6 +102,7 @@ const ( ExtURICalcFeatures = "{B58B0392-4F1F-4190-BB64-5DF3571DCE5F}" ExtURIConditionalFormattingRuleID = "{B025F937-C7B1-47D3-B67F-A62EFF666E3E}" ExtURIConditionalFormattings = "{78C0D931-6437-407d-A8EE-F0AAD7539E65}" + ExtURIDataField = "{E15A36E0-9728-4E99-A89B-3F7291B0FE68}" ExtURIDataModel = "{FCE2AD5D-F65C-4FA6-A056-5C36A1767C68}" ExtURIDataValidations = "{CCE6A557-97BC-4b89-ADB6-D9C93CAAB3DF}" ExtURIDrawingBlip = "{28A0092B-C50C-407E-A947-70E740481C1C}" @@ -112,6 +113,9 @@ const ( ExtURIPivotCacheDefinition = "{725AE2AE-9491-48be-B2B4-4EB974FC3084}" ExtURIPivotCachesX14 = "{876F7934-8845-4945-9796-88D515C7AA90}" ExtURIPivotCachesX15 = "{841E416B-1EF1-43b6-AB56-02D37102CBD5}" + ExtURIPivotField = "{2946ED86-A175-432a-8AC1-64E0C546D7DE}" + ExtURIPivotFilter = "{0605FD5F-26C8-4aeb-8148-2DB25E43C511}" + ExtURIPivotHierarchy = "{F1805F06-0CD304483-9156-8803C3D141DF}" ExtURIPivotTableReferences = "{983426D0-5260-488c-9760-48F4B6AC55F4}" ExtURIProtectedRanges = "{FC87AEE6-9EDD-4A0A-B7FB-166176984837}" ExtURISlicerCacheDefinition = "{2F2917AC-EB37-4324-AD4E-5DD8C200BD13}" From 0447cb22c8e9a0fff4e76d4a499df65555a2de29 Mon Sep 17 00:00:00 2001 From: Zhang Zhipeng <414326615@qq.com> Date: Mon, 2 Sep 2024 09:19:50 +0800 Subject: [PATCH 908/957] This close #1985, fix incorrect result of cell value after apply general number format (#1986) - Update unit tests --- numfmt.go | 5 ++++- numfmt_test.go | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/numfmt.go b/numfmt.go index cae88d64ab..265fe28379 100644 --- a/numfmt.go +++ b/numfmt.go @@ -5136,7 +5136,10 @@ func (nf *numberFormat) positiveHandler() string { var fmtNum bool for _, token := range nf.section[nf.sectionIdx].Items { if token.TType == nfp.TokenTypeGeneral { - return strconv.FormatFloat(nf.number, 'G', 10, 64) + if isNum, precision, _ := isNumeric(nf.value); isNum && precision > 11 { + return strconv.FormatFloat(nf.number, 'G', 10, 64) + } + return nf.value } if inStrSlice(supportedNumberTokenTypes, token.TType, true) != -1 { fmtNum = true diff --git a/numfmt_test.go b/numfmt_test.go index 61bf41bb48..c3e1a9928b 100644 --- a/numfmt_test.go +++ b/numfmt_test.go @@ -11,6 +11,7 @@ func TestNumFmt(t *testing.T) { for _, item := range [][]string{ {"123", "general", "123"}, {"-123", ";general", "-123"}, + {"12345678901", "General", "12345678901"}, {"43543.5448726851", "General", "43543.54487"}, {"-43543.5448726851", "General", "-43543.54487"}, {"1234567890.12345", "General", "1234567890"}, From aca04ecf57cd17efbb7d720e8508fa494c6b27aa Mon Sep 17 00:00:00 2001 From: Ben Smith Date: Wed, 4 Sep 2024 14:47:02 +0300 Subject: [PATCH 909/957] This closes #1987, support absolute paths for pictures (#1988) --- picture.go | 26 ++++++++++++++++++++------ picture_test.go | 13 +++++++++++++ 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/picture.go b/picture.go index a9a17bcf00..42be183f74 100644 --- a/picture.go +++ b/picture.go @@ -497,8 +497,7 @@ func (f *File) GetPictures(sheet, cell string) ([]Picture, error) { target := f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID) drawingXML := strings.TrimPrefix(strings.ReplaceAll(target, "..", "xl"), "/") drawingRelationships := strings.ReplaceAll( - strings.ReplaceAll(target, "../drawings", "xl/drawings/_rels"), ".xml", ".xml.rels") - + strings.ReplaceAll(drawingXML, "xl/drawings", "xl/drawings/_rels"), ".xml", ".xml.rels") imgs, err := f.getCellImages(sheet, cell) if err != nil { return nil, err @@ -526,7 +525,8 @@ func (f *File) GetPictureCells(sheet string) ([]string, error) { target := f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID) drawingXML := strings.TrimPrefix(strings.ReplaceAll(target, "..", "xl"), "/") drawingRelationships := strings.ReplaceAll( - strings.ReplaceAll(target, "../drawings", "xl/drawings/_rels"), ".xml", ".xml.rels") + strings.ReplaceAll(drawingXML, "xl/drawings", "xl/drawings/_rels"), ".xml", ".xml.rels") + embeddedImageCells, err := f.getImageCells(sheet) if err != nil { return nil, err @@ -609,8 +609,15 @@ func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) } } cb2 := func(a *decodeCellAnchor, r *xlsxRelationship) { - pic := Picture{Extension: filepath.Ext(r.Target), Format: &GraphicOptions{}, InsertType: PictureInsertTypePlaceOverCells} - if buffer, _ := f.Pkg.Load(filepath.ToSlash(filepath.Clean("xl/drawings/" + r.Target))); buffer != nil { + var target string + if strings.HasPrefix(r.Target, "/") { + target = strings.TrimPrefix(r.Target, "/") + } else { + target = filepath.ToSlash(filepath.Clean("xl/drawings/" + r.Target)) + } + + pic := Picture{Extension: filepath.Ext(target), Format: &GraphicOptions{}, InsertType: PictureInsertTypePlaceOverCells} + if buffer, _ := f.Pkg.Load(target); buffer != nil { pic.File = buffer.([]byte) pic.Format.AltText = a.Pic.NvPicPr.CNvPr.Descr pics = append(pics, pic) @@ -770,7 +777,14 @@ func (f *File) getPictureCells(drawingXML, drawingRelationships string) ([]strin } } cb2 := func(a *decodeCellAnchor, r *xlsxRelationship) { - if _, ok := f.Pkg.Load(filepath.ToSlash(filepath.Clean("xl/drawings/" + r.Target))); ok { + var target string + if strings.HasPrefix(r.Target, "/") { + target = strings.TrimPrefix(r.Target, "/") + } else { + target = filepath.ToSlash(filepath.Clean("xl/drawings/" + r.Target)) + } + + if _, ok := f.Pkg.Load(target); ok { if cell, err := CoordinatesToCellName(a.From.Col+1, a.From.Row+1); err == nil && inStrSlice(cells, cell, true) == -1 { cells = append(cells, cell) } diff --git a/picture_test.go b/picture_test.go index aec0b5c7e1..9da63f99ba 100644 --- a/picture_test.go +++ b/picture_test.go @@ -219,6 +219,19 @@ func TestGetPicture(t *testing.T) { cells, err := f.GetPictureCells("Sheet2") assert.NoError(t, err) assert.Equal(t, []string{"K16"}, cells) + + // Try to get picture cells with absolute target path in the drawing relationship + rels, err := f.relsReader("xl/drawings/_rels/drawing2.xml.rels") + assert.NoError(t, err) + rels.Relationships[0].Target = "/xl/media/image2.jpeg" + cells, err = f.GetPictureCells("Sheet2") + assert.NoError(t, err) + assert.Equal(t, []string{"K16"}, cells) + // Try to get pictures with absolute target path in the drawing relationship + pics, err = f.GetPictures("Sheet2", "K16") + assert.NoError(t, err) + assert.Len(t, pics, 1) + assert.NoError(t, f.Close()) // Test get picture from none drawing worksheet From 8c0ef7f90dddf319770fef818caca445dfb642be Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 5 Sep 2024 21:38:19 +0800 Subject: [PATCH 910/957] This closes #1983, support create combo chart with same types - Add new exported ChartLineUnset enumeration - Fix set line type of line chart does not work - Support set line type of scatter chart --- chart.go | 3 +- drawing.go | 345 ++++++++++++++++++++++++++++------------------------ xmlChart.go | 40 +++--- 3 files changed, 208 insertions(+), 180 deletions(-) diff --git a/chart.go b/chart.go index b6d450476d..5b4b39faf9 100644 --- a/chart.go +++ b/chart.go @@ -85,7 +85,8 @@ type ChartLineType byte // This section defines the currently supported chart line types enumeration. const ( - ChartLineSolid ChartLineType = iota + ChartLineUnset ChartLineType = iota + ChartLineSolid ChartLineNone ChartLineAutomatic ) diff --git a/drawing.go b/drawing.go index 545a22f96d..d0b66affaf 100644 --- a/drawing.go +++ b/drawing.go @@ -176,7 +176,12 @@ func (f *File) addChart(opts *Chart, comboCharts []*Chart) { if field.IsNil() { continue } - immutable.FieldByName(mutable.Type().Field(i).Name).Set(field) + fld := immutable.FieldByName(mutable.Type().Field(i).Name) + if field.Kind() == reflect.Slice && i < 16 { // All []*cCharts type fields + fld.Set(reflect.Append(fld, field.Index(0))) + continue + } + fld.Set(field) } } addChart(xlsxChartSpace.Chart.PlotArea, plotAreaFunc[opts.Type](xlsxChartSpace.Chart.PlotArea, opts)) @@ -194,239 +199,241 @@ func (f *File) addChart(opts *Chart, comboCharts []*Chart) { // drawBaseChart provides a function to draw the c:plotArea element for bar, // and column series charts by given format sets. func (f *File) drawBaseChart(pa *cPlotArea, opts *Chart) *cPlotArea { - c := cCharts{ - BarDir: &attrValString{ - Val: stringPtr("col"), - }, - Grouping: &attrValString{ - Val: stringPtr(plotAreaChartGrouping[opts.Type]), - }, - VaryColors: &attrValBool{ - Val: opts.VaryColors, + c := []*cCharts{ + { + BarDir: &attrValString{ + Val: stringPtr("col"), + }, + Grouping: &attrValString{ + Val: stringPtr(plotAreaChartGrouping[opts.Type]), + }, + VaryColors: &attrValBool{ + Val: opts.VaryColors, + }, + Ser: f.drawChartSeries(opts), + Shape: f.drawChartShape(opts), + DLbls: f.drawChartDLbls(opts), + AxID: f.genAxID(opts), + Overlap: &attrValInt{Val: intPtr(100)}, }, - Ser: f.drawChartSeries(opts), - Shape: f.drawChartShape(opts), - DLbls: f.drawChartDLbls(opts), - AxID: f.genAxID(opts), - Overlap: &attrValInt{Val: intPtr(100)}, } var ok bool - if *c.BarDir.Val, ok = plotAreaChartBarDir[opts.Type]; !ok { - c.BarDir = nil + if *c[0].BarDir.Val, ok = plotAreaChartBarDir[opts.Type]; !ok { + c[0].BarDir = nil } - if *c.Overlap.Val, ok = plotAreaChartOverlap[opts.Type]; !ok { - c.Overlap = nil + if *c[0].Overlap.Val, ok = plotAreaChartOverlap[opts.Type]; !ok { + c[0].Overlap = nil } catAx := f.drawPlotAreaCatAx(pa, opts) valAx := f.drawPlotAreaValAx(pa, opts) charts := map[ChartType]*cPlotArea{ Area: { - AreaChart: &c, + AreaChart: c, CatAx: catAx, ValAx: valAx, }, AreaStacked: { - AreaChart: &c, + AreaChart: c, CatAx: catAx, ValAx: valAx, }, AreaPercentStacked: { - AreaChart: &c, + AreaChart: c, CatAx: catAx, ValAx: valAx, }, Area3D: { - Area3DChart: &c, + Area3DChart: c, CatAx: catAx, ValAx: valAx, }, Area3DStacked: { - Area3DChart: &c, + Area3DChart: c, CatAx: catAx, ValAx: valAx, }, Area3DPercentStacked: { - Area3DChart: &c, + Area3DChart: c, CatAx: catAx, ValAx: valAx, }, Bar: { - BarChart: &c, + BarChart: c, CatAx: catAx, ValAx: valAx, }, BarStacked: { - BarChart: &c, + BarChart: c, CatAx: catAx, ValAx: valAx, }, BarPercentStacked: { - BarChart: &c, + BarChart: c, CatAx: catAx, ValAx: valAx, }, Bar3DClustered: { - Bar3DChart: &c, + Bar3DChart: c, CatAx: catAx, ValAx: valAx, }, Bar3DStacked: { - Bar3DChart: &c, + Bar3DChart: c, CatAx: catAx, ValAx: valAx, }, Bar3DPercentStacked: { - Bar3DChart: &c, + Bar3DChart: c, CatAx: catAx, ValAx: valAx, }, Bar3DConeClustered: { - Bar3DChart: &c, + Bar3DChart: c, CatAx: catAx, ValAx: valAx, }, Bar3DConeStacked: { - Bar3DChart: &c, + Bar3DChart: c, CatAx: catAx, ValAx: valAx, }, Bar3DConePercentStacked: { - Bar3DChart: &c, + Bar3DChart: c, CatAx: catAx, ValAx: valAx, }, Bar3DPyramidClustered: { - Bar3DChart: &c, + Bar3DChart: c, CatAx: catAx, ValAx: valAx, }, Bar3DPyramidStacked: { - Bar3DChart: &c, + Bar3DChart: c, CatAx: catAx, ValAx: valAx, }, Bar3DPyramidPercentStacked: { - Bar3DChart: &c, + Bar3DChart: c, CatAx: catAx, ValAx: valAx, }, Bar3DCylinderClustered: { - Bar3DChart: &c, + Bar3DChart: c, CatAx: catAx, ValAx: valAx, }, Bar3DCylinderStacked: { - Bar3DChart: &c, + Bar3DChart: c, CatAx: catAx, ValAx: valAx, }, Bar3DCylinderPercentStacked: { - Bar3DChart: &c, + Bar3DChart: c, CatAx: catAx, ValAx: valAx, }, Col: { - BarChart: &c, + BarChart: c, CatAx: catAx, ValAx: valAx, }, ColStacked: { - BarChart: &c, + BarChart: c, CatAx: catAx, ValAx: valAx, }, ColPercentStacked: { - BarChart: &c, + BarChart: c, CatAx: catAx, ValAx: valAx, }, Col3D: { - Bar3DChart: &c, + Bar3DChart: c, CatAx: catAx, ValAx: valAx, }, Col3DClustered: { - Bar3DChart: &c, + Bar3DChart: c, CatAx: catAx, ValAx: valAx, }, Col3DStacked: { - Bar3DChart: &c, + Bar3DChart: c, CatAx: catAx, ValAx: valAx, }, Col3DPercentStacked: { - Bar3DChart: &c, + Bar3DChart: c, CatAx: catAx, ValAx: valAx, }, Col3DCone: { - Bar3DChart: &c, + Bar3DChart: c, CatAx: catAx, ValAx: valAx, }, Col3DConeClustered: { - Bar3DChart: &c, + Bar3DChart: c, CatAx: catAx, ValAx: valAx, }, Col3DConeStacked: { - Bar3DChart: &c, + Bar3DChart: c, CatAx: catAx, ValAx: valAx, }, Col3DConePercentStacked: { - Bar3DChart: &c, + Bar3DChart: c, CatAx: catAx, ValAx: valAx, }, Col3DPyramid: { - Bar3DChart: &c, + Bar3DChart: c, CatAx: catAx, ValAx: valAx, }, Col3DPyramidClustered: { - Bar3DChart: &c, + Bar3DChart: c, CatAx: catAx, ValAx: valAx, }, Col3DPyramidStacked: { - Bar3DChart: &c, + Bar3DChart: c, CatAx: catAx, ValAx: valAx, }, Col3DPyramidPercentStacked: { - Bar3DChart: &c, + Bar3DChart: c, CatAx: catAx, ValAx: valAx, }, Col3DCylinder: { - Bar3DChart: &c, + Bar3DChart: c, CatAx: catAx, ValAx: valAx, }, Col3DCylinderClustered: { - Bar3DChart: &c, + Bar3DChart: c, CatAx: catAx, ValAx: valAx, }, Col3DCylinderStacked: { - Bar3DChart: &c, + Bar3DChart: c, CatAx: catAx, ValAx: valAx, }, Col3DCylinderPercentStacked: { - Bar3DChart: &c, + Bar3DChart: c, CatAx: catAx, ValAx: valAx, }, Bubble: { - BubbleChart: &c, + BubbleChart: c, CatAx: catAx, ValAx: valAx, }, Bubble3D: { - BubbleChart: &c, + BubbleChart: c, CatAx: catAx, ValAx: valAx, }, @@ -443,12 +450,14 @@ func (f *File) drawDoughnutChart(pa *cPlotArea, opts *Chart) *cPlotArea { } return &cPlotArea{ - DoughnutChart: &cCharts{ - VaryColors: &attrValBool{ - Val: opts.VaryColors, + DoughnutChart: []*cCharts{ + { + VaryColors: &attrValBool{ + Val: opts.VaryColors, + }, + Ser: f.drawChartSeries(opts), + HoleSize: &attrValInt{Val: intPtr(holeSize)}, }, - Ser: f.drawChartSeries(opts), - HoleSize: &attrValInt{Val: intPtr(holeSize)}, }, } } @@ -457,16 +466,18 @@ func (f *File) drawDoughnutChart(pa *cPlotArea, opts *Chart) *cPlotArea { // chart by given format sets. func (f *File) drawLineChart(pa *cPlotArea, opts *Chart) *cPlotArea { return &cPlotArea{ - LineChart: &cCharts{ - Grouping: &attrValString{ - Val: stringPtr(plotAreaChartGrouping[opts.Type]), - }, - VaryColors: &attrValBool{ - Val: boolPtr(false), + LineChart: []*cCharts{ + { + Grouping: &attrValString{ + Val: stringPtr(plotAreaChartGrouping[opts.Type]), + }, + VaryColors: &attrValBool{ + Val: boolPtr(false), + }, + Ser: f.drawChartSeries(opts), + DLbls: f.drawChartDLbls(opts), + AxID: f.genAxID(opts), }, - Ser: f.drawChartSeries(opts), - DLbls: f.drawChartDLbls(opts), - AxID: f.genAxID(opts), }, CatAx: f.drawPlotAreaCatAx(pa, opts), ValAx: f.drawPlotAreaValAx(pa, opts), @@ -477,16 +488,18 @@ func (f *File) drawLineChart(pa *cPlotArea, opts *Chart) *cPlotArea { // chart by given format sets. func (f *File) drawLine3DChart(pa *cPlotArea, opts *Chart) *cPlotArea { return &cPlotArea{ - Line3DChart: &cCharts{ - Grouping: &attrValString{ - Val: stringPtr(plotAreaChartGrouping[opts.Type]), - }, - VaryColors: &attrValBool{ - Val: boolPtr(false), + Line3DChart: []*cCharts{ + { + Grouping: &attrValString{ + Val: stringPtr(plotAreaChartGrouping[opts.Type]), + }, + VaryColors: &attrValBool{ + Val: boolPtr(false), + }, + Ser: f.drawChartSeries(opts), + DLbls: f.drawChartDLbls(opts), + AxID: f.genAxID(opts), }, - Ser: f.drawChartSeries(opts), - DLbls: f.drawChartDLbls(opts), - AxID: f.genAxID(opts), }, CatAx: f.drawPlotAreaCatAx(pa, opts), ValAx: f.drawPlotAreaValAx(pa, opts), @@ -497,11 +510,13 @@ func (f *File) drawLine3DChart(pa *cPlotArea, opts *Chart) *cPlotArea { // chart by given format sets. func (f *File) drawPieChart(pa *cPlotArea, opts *Chart) *cPlotArea { return &cPlotArea{ - PieChart: &cCharts{ - VaryColors: &attrValBool{ - Val: opts.VaryColors, + PieChart: []*cCharts{ + { + VaryColors: &attrValBool{ + Val: opts.VaryColors, + }, + Ser: f.drawChartSeries(opts), }, - Ser: f.drawChartSeries(opts), }, } } @@ -510,11 +525,13 @@ func (f *File) drawPieChart(pa *cPlotArea, opts *Chart) *cPlotArea { // pie chart by given format sets. func (f *File) drawPie3DChart(pa *cPlotArea, opts *Chart) *cPlotArea { return &cPlotArea{ - Pie3DChart: &cCharts{ - VaryColors: &attrValBool{ - Val: opts.VaryColors, + Pie3DChart: []*cCharts{ + { + VaryColors: &attrValBool{ + Val: opts.VaryColors, + }, + Ser: f.drawChartSeries(opts), }, - Ser: f.drawChartSeries(opts), }, } } @@ -527,16 +544,18 @@ func (f *File) drawPieOfPieChart(pa *cPlotArea, opts *Chart) *cPlotArea { splitPos = &attrValInt{Val: intPtr(opts.PlotArea.SecondPlotValues)} } return &cPlotArea{ - OfPieChart: &cCharts{ - OfPieType: &attrValString{ - Val: stringPtr("pie"), - }, - VaryColors: &attrValBool{ - Val: opts.VaryColors, + OfPieChart: []*cCharts{ + { + OfPieType: &attrValString{ + Val: stringPtr("pie"), + }, + VaryColors: &attrValBool{ + Val: opts.VaryColors, + }, + Ser: f.drawChartSeries(opts), + SplitPos: splitPos, + SerLines: &attrValString{}, }, - Ser: f.drawChartSeries(opts), - SplitPos: splitPos, - SerLines: &attrValString{}, }, } } @@ -549,16 +568,18 @@ func (f *File) drawBarOfPieChart(pa *cPlotArea, opts *Chart) *cPlotArea { splitPos = &attrValInt{Val: intPtr(opts.PlotArea.SecondPlotValues)} } return &cPlotArea{ - OfPieChart: &cCharts{ - OfPieType: &attrValString{ - Val: stringPtr("bar"), - }, - VaryColors: &attrValBool{ - Val: opts.VaryColors, + OfPieChart: []*cCharts{ + { + OfPieType: &attrValString{ + Val: stringPtr("bar"), + }, + VaryColors: &attrValBool{ + Val: opts.VaryColors, + }, + SplitPos: splitPos, + Ser: f.drawChartSeries(opts), + SerLines: &attrValString{}, }, - SplitPos: splitPos, - Ser: f.drawChartSeries(opts), - SerLines: &attrValString{}, }, } } @@ -567,16 +588,18 @@ func (f *File) drawBarOfPieChart(pa *cPlotArea, opts *Chart) *cPlotArea { // chart by given format sets. func (f *File) drawRadarChart(pa *cPlotArea, opts *Chart) *cPlotArea { return &cPlotArea{ - RadarChart: &cCharts{ - RadarStyle: &attrValString{ - Val: stringPtr("marker"), - }, - VaryColors: &attrValBool{ - Val: boolPtr(false), + RadarChart: []*cCharts{ + { + RadarStyle: &attrValString{ + Val: stringPtr("marker"), + }, + VaryColors: &attrValBool{ + Val: boolPtr(false), + }, + Ser: f.drawChartSeries(opts), + DLbls: f.drawChartDLbls(opts), + AxID: f.genAxID(opts), }, - Ser: f.drawChartSeries(opts), - DLbls: f.drawChartDLbls(opts), - AxID: f.genAxID(opts), }, CatAx: f.drawPlotAreaCatAx(pa, opts), ValAx: f.drawPlotAreaValAx(pa, opts), @@ -587,16 +610,18 @@ func (f *File) drawRadarChart(pa *cPlotArea, opts *Chart) *cPlotArea { // scatter chart by given format sets. func (f *File) drawScatterChart(pa *cPlotArea, opts *Chart) *cPlotArea { return &cPlotArea{ - ScatterChart: &cCharts{ - ScatterStyle: &attrValString{ - Val: stringPtr("smoothMarker"), // line,lineMarker,marker,none,smooth,smoothMarker - }, - VaryColors: &attrValBool{ - Val: boolPtr(false), + ScatterChart: []*cCharts{ + { + ScatterStyle: &attrValString{ + Val: stringPtr("smoothMarker"), // line,lineMarker,marker,none,smooth,smoothMarker + }, + VaryColors: &attrValBool{ + Val: boolPtr(false), + }, + Ser: f.drawChartSeries(opts), + DLbls: f.drawChartDLbls(opts), + AxID: f.genAxID(opts), }, - Ser: f.drawChartSeries(opts), - DLbls: f.drawChartDLbls(opts), - AxID: f.genAxID(opts), }, ValAx: append(f.drawPlotAreaCatAx(pa, opts), f.drawPlotAreaValAx(pa, opts)...), } @@ -606,12 +631,14 @@ func (f *File) drawScatterChart(pa *cPlotArea, opts *Chart) *cPlotArea { // given format sets. func (f *File) drawSurface3DChart(pa *cPlotArea, opts *Chart) *cPlotArea { plotArea := &cPlotArea{ - Surface3DChart: &cCharts{ - Ser: f.drawChartSeries(opts), - AxID: []*attrValInt{ - {Val: intPtr(100000000)}, - {Val: intPtr(100000001)}, - {Val: intPtr(100000005)}, + Surface3DChart: []*cCharts{ + { + Ser: f.drawChartSeries(opts), + AxID: []*attrValInt{ + {Val: intPtr(100000000)}, + {Val: intPtr(100000001)}, + {Val: intPtr(100000005)}, + }, }, }, CatAx: f.drawPlotAreaCatAx(pa, opts), @@ -619,7 +646,7 @@ func (f *File) drawSurface3DChart(pa *cPlotArea, opts *Chart) *cPlotArea { SerAx: f.drawPlotAreaSerAx(opts), } if opts.Type == WireframeSurface3D { - plotArea.Surface3DChart.Wireframe = &attrValBool{Val: boolPtr(true)} + plotArea.Surface3DChart[0].Wireframe = &attrValBool{Val: boolPtr(true)} } return plotArea } @@ -628,12 +655,14 @@ func (f *File) drawSurface3DChart(pa *cPlotArea, opts *Chart) *cPlotArea { // given format sets. func (f *File) drawSurfaceChart(pa *cPlotArea, opts *Chart) *cPlotArea { plotArea := &cPlotArea{ - SurfaceChart: &cCharts{ - Ser: f.drawChartSeries(opts), - AxID: []*attrValInt{ - {Val: intPtr(100000000)}, - {Val: intPtr(100000001)}, - {Val: intPtr(100000005)}, + SurfaceChart: []*cCharts{ + { + Ser: f.drawChartSeries(opts), + AxID: []*attrValInt{ + {Val: intPtr(100000000)}, + {Val: intPtr(100000001)}, + {Val: intPtr(100000005)}, + }, }, }, CatAx: f.drawPlotAreaCatAx(pa, opts), @@ -641,7 +670,7 @@ func (f *File) drawSurfaceChart(pa *cPlotArea, opts *Chart) *cPlotArea { SerAx: f.drawPlotAreaSerAx(opts), } if opts.Type == WireframeContour { - plotArea.SurfaceChart.Wireframe = &attrValBool{Val: boolPtr(true)} + plotArea.SurfaceChart[0].Wireframe = &attrValBool{Val: boolPtr(true)} } return plotArea } @@ -650,18 +679,20 @@ func (f *File) drawSurfaceChart(pa *cPlotArea, opts *Chart) *cPlotArea { // given format sets. func (f *File) drawBubbleChart(pa *cPlotArea, opts *Chart) *cPlotArea { plotArea := &cPlotArea{ - BubbleChart: &cCharts{ - VaryColors: &attrValBool{ - Val: opts.VaryColors, + BubbleChart: []*cCharts{ + { + VaryColors: &attrValBool{ + Val: opts.VaryColors, + }, + Ser: f.drawChartSeries(opts), + DLbls: f.drawChartDLbls(opts), + AxID: f.genAxID(opts), }, - Ser: f.drawChartSeries(opts), - DLbls: f.drawChartDLbls(opts), - AxID: f.genAxID(opts), }, ValAx: append(f.drawPlotAreaCatAx(pa, opts), f.drawPlotAreaValAx(pa, opts)...), } if opts.BubbleSize > 0 && opts.BubbleSize <= 300 { - plotArea.BubbleChart.BubbleScale = &attrValFloat{Val: float64Ptr(float64(opts.BubbleSize))} + plotArea.BubbleChart[0].BubbleScale = &attrValFloat{Val: float64Ptr(float64(opts.BubbleSize))} } return plotArea } @@ -750,23 +781,19 @@ func (f *File) drawShapeFill(fill Fill, spPr *cSpPr) *cSpPr { func (f *File) drawChartSeriesSpPr(i int, opts *Chart) *cSpPr { spPr := &cSpPr{SolidFill: &aSolidFill{SchemeClr: &aSchemeClr{Val: "accent" + strconv.Itoa((opts.order+i)%6+1)}}} spPr = f.drawShapeFill(opts.Series[i].Fill, spPr) - spPrScatter := &cSpPr{ - Ln: &aLn{ - W: 25400, - NoFill: &attrValString{}, - }, - } - spPrLine := &cSpPr{ + solid := &cSpPr{ Ln: &aLn{ W: f.ptToEMUs(opts.Series[i].Line.Width), Cap: "rnd", // rnd, sq, flat SolidFill: spPr.SolidFill, }, } - if chartSeriesSpPr, ok := map[ChartType]*cSpPr{ - Line: spPrLine, Scatter: spPrScatter, + noLn := &cSpPr{Ln: &aLn{NoFill: &attrValString{}}} + if chartSeriesSpPr, ok := map[ChartType]map[ChartLineType]*cSpPr{ + Line: {ChartLineUnset: solid, ChartLineSolid: solid, ChartLineNone: noLn, ChartLineAutomatic: solid}, + Scatter: {ChartLineUnset: noLn, ChartLineSolid: solid, ChartLineNone: noLn, ChartLineAutomatic: noLn}, }[opts.Type]; ok { - return chartSeriesSpPr + return chartSeriesSpPr[opts.Series[i].Line.Type] } if spPr.SolidFill.SrgbClr != nil { return spPr diff --git a/xmlChart.go b/xmlChart.go index 273edacb0f..90c29fcf46 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -300,26 +300,26 @@ type cView3D struct { // cPlotArea directly maps the plotArea element. This element specifies the // plot area of the chart. type cPlotArea struct { - Layout *string `xml:"layout"` - AreaChart *cCharts `xml:"areaChart"` - Area3DChart *cCharts `xml:"area3DChart"` - BarChart *cCharts `xml:"barChart"` - Bar3DChart *cCharts `xml:"bar3DChart"` - 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"` - RadarChart *cCharts `xml:"radarChart"` - ScatterChart *cCharts `xml:"scatterChart"` - Surface3DChart *cCharts `xml:"surface3DChart"` - SurfaceChart *cCharts `xml:"surfaceChart"` - CatAx []*cAxs `xml:"catAx"` - ValAx []*cAxs `xml:"valAx"` - SerAx []*cAxs `xml:"serAx"` - SpPr *cSpPr `xml:"spPr"` + Layout *string `xml:"layout"` + AreaChart []*cCharts `xml:"areaChart"` + Area3DChart []*cCharts `xml:"area3DChart"` + BarChart []*cCharts `xml:"barChart"` + Bar3DChart []*cCharts `xml:"bar3DChart"` + 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"` + RadarChart []*cCharts `xml:"radarChart"` + ScatterChart []*cCharts `xml:"scatterChart"` + Surface3DChart []*cCharts `xml:"surface3DChart"` + SurfaceChart []*cCharts `xml:"surfaceChart"` + CatAx []*cAxs `xml:"catAx"` + ValAx []*cAxs `xml:"valAx"` + SerAx []*cAxs `xml:"serAx"` + SpPr *cSpPr `xml:"spPr"` } // cCharts specifies the common element of the chart. From ad8541790df7d423803aca9851132639179ad8f2 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 8 Sep 2024 12:19:58 +0800 Subject: [PATCH 911/957] This closes #1989, fix incorrect result of formula functions XIRR and XNPV - Require number data type instead of string - Fix incorrect formula error type --- calc.go | 26 ++++++++++++-------------- calc_test.go | 40 ++++++++++++++++++++-------------------- 2 files changed, 32 insertions(+), 34 deletions(-) diff --git a/calc.go b/calc.go index 193e01fa33..e867d2fd23 100644 --- a/calc.go +++ b/calc.go @@ -18197,28 +18197,26 @@ func (fn *formulaFuncs) prepareXArgs(values, dates formulaArg) (valuesArg, dates valuesArg = append(valuesArg, numArg.Number) continue } - err = newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + err = newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) return } if len(valuesArg) < 2 { err = newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) return } - args, date := list.New(), 0.0 + date := 0.0 for _, arg := range dates.ToList() { - args.Init() - args.PushBack(arg) - dateValue := fn.DATEVALUE(args) - if dateValue.Type != ArgNumber { - err = newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) - return - } - if dateValue.Number < date { - err = newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) - return + if arg.Type == ArgNumber { + datesArg = append(datesArg, arg.Number) + if arg.Number < date { + err = newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + return + } + date = arg.Number + continue } - datesArg = append(datesArg, dateValue.Number) - date = dateValue.Number + err = newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + return } if len(valuesArg) != len(datesArg) { err = newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) diff --git a/calc_test.go b/calc_test.go index a842a2365e..868533c088 100644 --- a/calc_test.go +++ b/calc_test.go @@ -5562,13 +5562,13 @@ func TestCalcSUMIFSAndAVERAGEIFS(t *testing.T) { func TestCalcXIRR(t *testing.T) { cellData := [][]interface{}{ - {-100.00, "01/01/2016"}, - {20.00, "04/01/2016"}, - {40.00, "10/01/2016"}, - {25.00, "02/01/2017"}, - {8.00, "03/01/2017"}, - {15.00, "06/01/2017"}, - {-1e-10, "09/01/2017"}, + {-100.00, 42370}, + {20.00, 42461}, + {40.00, 42644}, + {25.00, 42767}, + {8.00, 42795}, + {15.00, 42887}, + {-1e-10, 42979}, } f := prepareCalcData(cellData) formulaList := map[string]string{ @@ -5584,8 +5584,8 @@ func TestCalcXIRR(t *testing.T) { 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(\"\",B1:B4)": {"#VALUE!", "#VALUE!"}, + "=XIRR(A1:A4,\"\")": {"#VALUE!", "#VALUE!"}, "=XIRR(A1:A4,B1:B4,\"\")": {"#NUM!", "#NUM!"}, "=XIRR(A2:A6,B2:B6)": {"#NUM!", "#NUM!"}, "=XIRR(A2:A7,B2:B7)": {"#NUM!", "#NUM!"}, @@ -5708,15 +5708,15 @@ func TestCalcXLOOKUP(t *testing.T) { func TestCalcXNPV(t *testing.T) { cellData := [][]interface{}{ {nil, 0.05}, - {"01/01/2016", -10000, nil}, - {"02/01/2016", 2000}, - {"05/01/2016", 2400}, - {"07/01/2016", 2900}, - {"11/01/2016", 3500}, - {"01/01/2017", 4100}, + {42370, -10000, nil}, + {42401, 2000}, + {42491, 2400}, + {42552, 2900}, + {42675, 3500}, + {42736, 4100}, {}, - {"02/01/2016"}, - {"01/01/2016"}, + {42401}, + {42370}, } f := prepareCalcData(cellData) formulaList := map[string]string{ @@ -5732,9 +5732,9 @@ func TestCalcXNPV(t *testing.T) { "=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,\"\",A2:A7)": {"#VALUE!", "#VALUE!"}, + "=XNPV(B1,B2:B7,\"\")": {"#VALUE!", "#VALUE!"}, + "=XNPV(B1,B2:B7,C2:C7)": {"#VALUE!", "#VALUE!"}, "=XNPV(B1,B2,A2)": {"#NUM!", "#NUM!"}, "=XNPV(B1,B2:B3,A2:A5)": {"#NUM!", "#NUM!"}, "=XNPV(B1,B2:B3,A9:A10)": {"#VALUE!", "#VALUE!"}, From 02189fb016fbc81328166b20ccca9e49922f0cef Mon Sep 17 00:00:00 2001 From: ArcholSevier <166369836+ArcholSevier@users.noreply.github.com> Date: Thu, 12 Sep 2024 22:07:18 +0800 Subject: [PATCH 912/957] Ref #65, new formula function DOLLAR (#1992) - Update unit tests --- calc.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 16 ++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/calc.go b/calc.go index e867d2fd23..9784ea6059 100644 --- a/calc.go +++ b/calc.go @@ -478,6 +478,7 @@ type formulaFuncs struct { // DISC // DMAX // DMIN +// DOLLAR // DOLLARDE // DOLLARFR // DPRODUCT @@ -16341,6 +16342,49 @@ func (fn *formulaFuncs) DISC(argsList *list.List) formulaArg { return fn.discIntrate("DISC", argsList) } +// DOLLAR function rounds a supplied number to a specified number of decimal +// places and then converts this into a text string with a currency format. The +// syntax of the function is: +// +// DOLLAR(number,[decimals]) +func (fn *formulaFuncs) DOLLAR(argsList *list.List) formulaArg { + if argsList.Len() == 0 { + return newErrorFormulaArg(formulaErrorVALUE, "DOLLAR requires at least 1 argument") + } + if argsList.Len() > 2 { + return newErrorFormulaArg(formulaErrorVALUE, "DOLLAR requires 1 or 2 arguments") + } + numArg := argsList.Front().Value.(formulaArg) + n := numArg.ToNumber() + if n.Type != ArgNumber { + return n + } + decimals, dot, value := 2, ".", numArg.Value() + if argsList.Len() == 2 { + d := argsList.Back().Value.(formulaArg).ToNumber() + if d.Type != ArgNumber { + return d + } + if d.Number < 0 { + value = strconv.FormatFloat(fn.round(n.Number, d.Number, down), 'f', -1, 64) + } + if d.Number >= 128 { + return newErrorFormulaArg(formulaErrorVALUE, "decimal value should be less than 128") + } + if decimals = int(d.Number); decimals < 0 { + decimals, dot = 0, "" + } + } + symbol := map[CultureName]string{ + CultureNameUnknown: "$", + CultureNameEnUS: "$", + CultureNameZhCN: "¥", + }[fn.f.options.CultureInfo] + numFmtCode := fmt.Sprintf("%s#,##0%s%s;(%s#,##0%s%s)", + symbol, dot, strings.Repeat("0", decimals), symbol, dot, strings.Repeat("0", decimals)) + return newStringFormulaArg(format(value, numFmtCode, false, CellTypeNumber, nil)) +} + // DOLLARDE function converts a dollar value in fractional notation, into a // dollar value expressed as a decimal. The syntax of the function is: // diff --git a/calc_test.go b/calc_test.go index 868533c088..c2ae332f64 100644 --- a/calc_test.go +++ b/calc_test.go @@ -2117,6 +2117,16 @@ func TestCalcCellValue(t *testing.T) { "=DDB(10000,1000,5,5)": "296", // DISC "=DISC(\"04/01/2016\",\"03/31/2021\",95,100)": "0.01", + // DOLLAR + "=DOLLAR(1234.56)": "$1,234.56", + "=DOLLAR(1234.56,0)": "$1,235", + "=DOLLAR(1234.56,1)": "$1,234.6", + "=DOLLAR(1234.56,2)": "$1,234.56", + "=DOLLAR(1234.56,3)": "$1,234.560", + "=DOLLAR(1234.56,-2)": "$1,200", + "=DOLLAR(1234.56,-3)": "$1,000", + "=DOLLAR(-1234.56,3)": "($1,234.560)", + "=DOLLAR(-1234.56,-3)": "($1,000)", // DOLLARDE "=DOLLARDE(1.01,16)": "1.0625", // DOLLARFR @@ -4250,6 +4260,12 @@ func TestCalcCellValue(t *testing.T) { "=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"}, + // DOLLAR + "DOLLAR()": {"#VALUE!", "DOLLAR requires at least 1 argument"}, + "DOLLAR(0,0,0)": {"#VALUE!", "DOLLAR requires 1 or 2 arguments"}, + "DOLLAR(\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "DOLLAR(0,\"\")": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, + "DOLLAR(1,200)": {"#VALUE!", "decimal value should be less than 128"}, // DOLLARDE "=DOLLARDE()": {"#VALUE!", "DOLLARDE requires 2 arguments"}, "=DOLLARDE(\"\",0)": {"#VALUE!", "strconv.ParseFloat: parsing \"\": invalid syntax"}, From 41c7dd30ce7efe4771624994918afa9cde6cbfd4 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 21 Sep 2024 15:39:36 +0800 Subject: [PATCH 913/957] This closes #1993, support to set and get pivot table classic layout - Add new field `ClassicLayout` in the `PivotTableOptions` - Add a new exported error variable `ErrPivotTableClassicLayout` - Update unit tests - Add documentation for the SetDefinedName function, ref #1015 --- calc_test.go | 2 +- errors.go | 2 ++ pivotTable.go | 49 ++++++++++++++++++++++++++++++++++++---------- pivotTable_test.go | 18 ++++++++++++----- sheet.go | 18 +++++++++++++++++ 5 files changed, 73 insertions(+), 16 deletions(-) diff --git a/calc_test.go b/calc_test.go index c2ae332f64..fc28a05955 100644 --- a/calc_test.go +++ b/calc_test.go @@ -2126,7 +2126,7 @@ func TestCalcCellValue(t *testing.T) { "=DOLLAR(1234.56,-2)": "$1,200", "=DOLLAR(1234.56,-3)": "$1,000", "=DOLLAR(-1234.56,3)": "($1,234.560)", - "=DOLLAR(-1234.56,-3)": "($1,000)", + "=DOLLAR(-1234.56,-3)": "($1,000)", // DOLLARDE "=DOLLARDE(1.01,16)": "1.0625", // DOLLARFR diff --git a/errors.go b/errors.go index b12b06cba2..dc58b42395 100644 --- a/errors.go +++ b/errors.go @@ -97,6 +97,8 @@ var ( // ErrPasswordLengthInvalid defined the error message on invalid password // length. ErrPasswordLengthInvalid = errors.New("password length invalid") + // ErrPivotTableClassicLayout + ErrPivotTableClassicLayout = errors.New("cannot enable ClassicLayout and CompactData in the same time") // ErrSave defined the error message for saving file. ErrSave = errors.New("no path defined for file, consider File.WriteTo or File.Write") // ErrSheetIdx defined the error message on receive the invalid worksheet diff --git a/pivotTable.go b/pivotTable.go index 9caf037872..03475c0931 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -51,6 +51,7 @@ type PivotTableOptions struct { UseAutoFormatting bool PageOverThenDown bool MergeItem bool + ClassicLayout bool CompactData bool ShowError bool ShowRowHeaders bool @@ -220,6 +221,9 @@ func (f *File) parseFormatPivotTableSet(opts *PivotTableOptions) (*xlsxWorksheet if !ok { return dataSheet, pivotTableSheetPath, ErrSheetNotExist{pivotTableSheetName} } + if opts.CompactData && opts.ClassicLayout { + return nil, "", ErrPivotTableClassicLayout + } return dataSheet, pivotTableSheetPath, err } @@ -352,6 +356,7 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, opts *PivotTableOptions) MergeItem: &opts.MergeItem, CreatedVersion: pivotTableVersion, CompactData: &opts.CompactData, + GridDropZones: opts.ClassicLayout, ShowError: &opts.ShowError, FieldPrintTitles: opts.FieldPrintTitles, ItemPrintTitles: opts.ItemPrintTitles, @@ -387,6 +392,12 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, opts *PivotTableOptions) if pt.Name == "" { pt.Name = fmt.Sprintf("PivotTable%d", pivotTableID) } + + // set classic layout + if opts.ClassicLayout { + pt.Compact, pt.CompactData = boolPtr(false), boolPtr(false) + } + // pivot fields _ = f.addPivotFields(&pt, opts) @@ -537,6 +548,14 @@ func (f *File) addPivotColFields(pt *xlsxPivotTableDefinition, opts *PivotTableO return err } +// setClassicLayout provides a method to set classic layout for pivot table by +// setting Compact and Outline to false. +func (fld *xlsxPivotField) setClassicLayout(classicLayout bool) { + if classicLayout { + fld.Compact, fld.Outline = boolPtr(false), boolPtr(false) + } +} + // 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 *PivotTableOptions) error { @@ -554,8 +573,7 @@ func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opts *PivotTableOpti } else { items = append(items, &xlsxItem{T: "default"}) } - - pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, &xlsxPivotField{ + fld := &xlsxPivotField{ Name: f.getPivotTableFieldName(name, opts.Rows), Axis: "axisRow", DataField: inPivotTableField(opts.Data, name) != -1, @@ -568,11 +586,13 @@ func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opts *PivotTableOpti Count: len(items), Item: items, }, - }) + } + fld.setClassicLayout(opts.ClassicLayout) + pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, fld) continue } if inPivotTableField(opts.Filter, name) != -1 { - pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, &xlsxPivotField{ + fld := &xlsxPivotField{ Axis: "axisPage", DataField: inPivotTableField(opts.Data, name) != -1, Name: f.getPivotTableFieldName(name, opts.Columns), @@ -582,7 +602,9 @@ func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opts *PivotTableOpti {T: "default"}, }, }, - }) + } + fld.setClassicLayout(opts.ClassicLayout) + pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, fld) continue } if inPivotTableField(opts.Columns, name) != -1 { @@ -593,7 +615,7 @@ func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opts *PivotTableOpti } else { items = append(items, &xlsxItem{T: "default"}) } - pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, &xlsxPivotField{ + fld := &xlsxPivotField{ Name: f.getPivotTableFieldName(name, opts.Columns), Axis: "axisCol", DataField: inPivotTableField(opts.Data, name) != -1, @@ -606,16 +628,22 @@ func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opts *PivotTableOpti Count: len(items), Item: items, }, - }) + } + fld.setClassicLayout(opts.ClassicLayout) + pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, fld) continue } if inPivotTableField(opts.Data, name) != -1 { - pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, &xlsxPivotField{ + fld := &xlsxPivotField{ DataField: true, - }) + } + fld.setClassicLayout(opts.ClassicLayout) + pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, fld) continue } - pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, &xlsxPivotField{}) + fld := &xlsxPivotField{} + fld.setClassicLayout(opts.ClassicLayout) + pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, fld) } return err } @@ -847,6 +875,7 @@ func (f *File) getPivotTable(sheet, pivotTableXML, pivotCacheRels string) (Pivot DataRange: fmt.Sprintf("%s!%s", pc.CacheSource.WorksheetSource.Sheet, pc.CacheSource.WorksheetSource.Ref), PivotTableRange: fmt.Sprintf("%s!%s", sheet, pt.Location.Ref), Name: pt.Name, + ClassicLayout: pt.GridDropZones, FieldPrintTitles: pt.FieldPrintTitles, ItemPrintTitles: pt.ItemPrintTitles, } diff --git a/pivotTable_test.go b/pivotTable_test.go index 74ce61bec0..20528cc5a5 100644 --- a/pivotTable_test.go +++ b/pivotTable_test.go @@ -38,12 +38,13 @@ func TestPivotTable(t *testing.T) { RowGrandTotals: true, ColGrandTotals: true, ShowDrill: true, + ClassicLayout: true, + ShowError: true, ShowRowHeaders: true, ShowColHeaders: true, ShowLastColumn: true, - ShowError: true, - ItemPrintTitles: true, FieldPrintTitles: true, + ItemPrintTitles: true, PivotTableStyleName: "PivotStyleLight16", } assert.NoError(t, f.AddPivotTable(expected)) @@ -265,18 +266,25 @@ func TestPivotTable(t *testing.T) { assert.NoError(t, err) // Test add pivot table with invalid sheet name - assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{ + assert.Error(t, f.AddPivotTable(&PivotTableOptions{ DataRange: "Sheet:1!A1:E31", PivotTableRange: "Sheet:1!G2:M34", Rows: []PivotTableField{{Data: "Year"}}, - }), ErrSheetNameInvalid.Error()) + }), ErrSheetNameInvalid) + // Test add pivot table with enable ClassicLayout and CompactData in the same time + assert.Error(t, f.AddPivotTable(&PivotTableOptions{ + DataRange: "Sheet1!A1:E31", + PivotTableRange: "Sheet1!G2:M34", + CompactData: true, + ClassicLayout: true, + }), ErrPivotTableClassicLayout) // Test delete pivot table with not exists worksheet assert.EqualError(t, f.DeletePivotTable("SheetN", "PivotTable1"), "sheet SheetN does not exist") // Test delete pivot table with not exists pivot table name assert.EqualError(t, f.DeletePivotTable("Sheet1", "PivotTableN"), "table PivotTableN does not exist") // Test adjust range with invalid range _, _, err = f.adjustRange("") - assert.EqualError(t, err, ErrParameterRequired.Error()) + assert.Error(t, err, ErrParameterRequired) // Test adjust range with incorrect range _, _, err = f.adjustRange("sheet1!") assert.EqualError(t, err, "parameter is invalid") diff --git a/sheet.go b/sheet.go index ee66b90afe..7cb13e24c0 100644 --- a/sheet.go +++ b/sheet.go @@ -1657,6 +1657,24 @@ func (f *File) GetPageLayout(sheet string) (PageLayoutOptions, error) { // Comment: "defined name comment", // Scope: "Sheet2", // }) +// +// If you fill the RefersTo property with only one columns range without a +// comma, it will work as "Columns to repeat at left" only. For example: +// +// err := f.SetDefinedName(&excelize.DefinedName{ +// Name: "_xlnm.Print_Titles", +// RefersTo: "Sheet1!$A:$A", +// Scope: "Sheet1", +// }) +// +// If you fill the RefersTo property with only one rows range without a comma, +// it will work as "Rows to repeat at top" only. For example: +// +// err := f.SetDefinedName(&excelize.DefinedName{ +// Name: "_xlnm.Print_Titles", +// RefersTo: "Sheet1!$1:$1", +// Scope: "Sheet1", +// }) func (f *File) SetDefinedName(definedName *DefinedName) error { if definedName.Name == "" || definedName.RefersTo == "" { return ErrParameterInvalid From bebb8020699ee65bf39fa44380142f3a3d39bc5b Mon Sep 17 00:00:00 2001 From: liuwangchao <59493205+liuwangchao@users.noreply.github.com> Date: Fri, 27 Sep 2024 14:58:18 +0800 Subject: [PATCH 914/957] This closes #1999, fix error on GetCellRichText function when read cell without SST index (#2000) - Add unit test for get cell rich text when string item index is invalid - Add comments for the variable ErrPasswordLengthInvalid - Update dependencies modules - Update GitHub Actions workflow configuration, test on Go 1.23.x --- .github/workflows/go.yml | 2 +- cell.go | 4 ++-- cell_test.go | 7 +++++++ errors.go | 3 ++- go.mod | 8 ++++---- go.sum | 16 ++++++++-------- 6 files changed, 24 insertions(+), 16 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 3f100be0e9..6e3db35a37 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -5,7 +5,7 @@ jobs: test: strategy: matrix: - go-version: [1.18.x, 1.19.x, 1.20.x, '>=1.21.1', 1.22.x] + go-version: [1.18.x, 1.19.x, 1.20.x, '>=1.21.1', 1.22.x, 1.23.x] os: [ubuntu-latest, macos-13, windows-latest] targetplatform: [x86, x64] diff --git a/cell.go b/cell.go index 95caea3106..302ea9179b 100644 --- a/cell.go +++ b/cell.go @@ -1115,11 +1115,11 @@ func (f *File) GetCellRichText(sheet, cell string) (runs []RichTextRun, err erro runs = getCellRichText(c.IS) return } - if c.T == "" { + if c.T != "s" || c.V == "" { return } siIdx, err := strconv.Atoi(c.V) - if err != nil || c.T != "s" { + if err != nil { return } sst, err := f.sharedStringsReader() diff --git a/cell_test.go b/cell_test.go index e64e46423b..fa671732e2 100644 --- a/cell_test.go +++ b/cell_test.go @@ -785,6 +785,13 @@ func TestGetCellRichText(t *testing.T) { 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 invalid + ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml") + assert.True(t, ok) + ws.(*xlsxWorksheet).SheetData.Row[0].C[0] = xlsxC{T: "s", V: "A", IS: &xlsxSI{}} + runs, err = f.GetCellRichText("Sheet1", "A1") + assert.EqualError(t, err, "strconv.Atoi: parsing \"A\": invalid syntax") + assert.Equal(t, 0, len(runs)) // Test get cell rich text on invalid string item index ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml") assert.True(t, ok) diff --git a/errors.go b/errors.go index dc58b42395..55a3c3c3e6 100644 --- a/errors.go +++ b/errors.go @@ -97,7 +97,8 @@ var ( // ErrPasswordLengthInvalid defined the error message on invalid password // length. ErrPasswordLengthInvalid = errors.New("password length invalid") - // ErrPivotTableClassicLayout + // ErrPivotTableClassicLayout defined the error message on enable + // ClassicLayout and CompactData in the same time. ErrPivotTableClassicLayout = errors.New("cannot enable ClassicLayout and CompactData in the same time") // ErrSave defined the error message for saving file. ErrSave = errors.New("no path defined for file, consider File.WriteTo or File.Write") diff --git a/go.mod b/go.mod index c9192e52d9..7edba1b46f 100644 --- a/go.mod +++ b/go.mod @@ -8,15 +8,15 @@ require ( github.com/stretchr/testify v1.8.4 github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 - golang.org/x/crypto v0.26.0 + golang.org/x/crypto v0.27.0 golang.org/x/image v0.18.0 - golang.org/x/net v0.28.0 - golang.org/x/text v0.17.0 + golang.org/x/net v0.29.0 + golang.org/x/text v0.18.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/richardlehane/msoleps v1.0.3 // indirect + github.com/richardlehane/msoleps v1.0.4 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 346cd7758d..de22a1d4f0 100644 --- a/go.sum +++ b/go.sum @@ -7,22 +7,22 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN 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/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/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00= +github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7psK/lVsjIS2otl+1WyRyY= github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A= github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= -golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= -golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= -golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= -golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= -golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= -golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= From b23e5a26df2a70102503fd73bf2f7ede38e189b8 Mon Sep 17 00:00:00 2001 From: "Jian Yu, Chen" <77830479+Zncl2222@users.noreply.github.com> Date: Mon, 30 Sep 2024 21:00:59 +0800 Subject: [PATCH 915/957] This closes #1076, add new function MoveSheet to support changing sheet order in the workbook (#1996) - Add unit tests --- sheet.go | 43 +++++++++++++++++++++++++++++++++++++++++++ sheet_test.go | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/sheet.go b/sheet.go index 7cb13e24c0..48cf88411e 100644 --- a/sheet.go +++ b/sheet.go @@ -600,6 +600,49 @@ func (f *File) DeleteSheet(sheet string) error { return err } +// MoveSheet moves a sheet to a specified position in the workbook. The function +// moves the source sheet before the target sheet. After moving, other sheets +// will be shifted to the left or right. If the sheet is already at the target +// position, the function will not perform any action. Not that this function +// will be ungroup all sheets after moving. For example, move Sheet2 before +// Sheet1: +// +// err := f.MoveSheet("Sheet2", "Sheet1") +func (f *File) MoveSheet(source, target string) error { + if strings.EqualFold(source, target) { + return nil + } + wb, err := f.workbookReader() + if err != nil { + return err + } + sourceIdx, err := f.GetSheetIndex(source) + if err != nil { + return err + } + targetIdx, err := f.GetSheetIndex(target) + if err != nil { + return err + } + if sourceIdx < 0 { + return ErrSheetNotExist{source} + } + if targetIdx < 0 { + return ErrSheetNotExist{target} + } + _ = f.UngroupSheets() + activeSheetName := f.GetSheetName(f.GetActiveSheetIndex()) + sourceSheet := wb.Sheets.Sheet[sourceIdx] + wb.Sheets.Sheet = append(wb.Sheets.Sheet[:sourceIdx], wb.Sheets.Sheet[sourceIdx+1:]...) + if targetIdx > sourceIdx { + targetIdx-- + } + wb.Sheets.Sheet = append(wb.Sheets.Sheet[:targetIdx], append([]xlsxSheet{sourceSheet}, wb.Sheets.Sheet[targetIdx:]...)...) + activeSheetIdx, _ := f.GetSheetIndex(activeSheetName) + f.SetActiveSheet(activeSheetIdx) + return err +} + // deleteAndAdjustDefinedNames delete and adjust defined name in the workbook // by given worksheet ID. func deleteAndAdjustDefinedNames(wb *xlsxWorkbook, deleteLocalSheetID int) { diff --git a/sheet_test.go b/sheet_test.go index 1c4f55acf2..47fcd97e46 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -547,6 +547,43 @@ func TestDeleteSheet(t *testing.T) { assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDeleteSheet2.xlsx"))) } +func TestMoveSheet(t *testing.T) { + f := NewFile() + defer f.Close() + for i := 2; i < 6; i++ { + _, err := f.NewSheet("Sheet" + strconv.Itoa(i)) + assert.NoError(t, err) + } + assert.Equal(t, []string{"Sheet1", "Sheet2", "Sheet3", "Sheet4", "Sheet5"}, f.GetSheetList()) + + // Move target to first position + assert.NoError(t, f.MoveSheet("Sheet2", "Sheet1")) + assert.Equal(t, []string{"Sheet2", "Sheet1", "Sheet3", "Sheet4", "Sheet5"}, f.GetSheetList()) + assert.Equal(t, "Sheet1", f.GetSheetName(f.GetActiveSheetIndex())) + + // Move target to last position + assert.NoError(t, f.MoveSheet("Sheet2", "Sheet5")) + assert.NoError(t, f.MoveSheet("Sheet5", "Sheet2")) + assert.Equal(t, []string{"Sheet1", "Sheet3", "Sheet4", "Sheet5", "Sheet2"}, f.GetSheetList()) + + // Move target to same position + assert.NoError(t, f.MoveSheet("Sheet1", "Sheet1")) + assert.Equal(t, []string{"Sheet1", "Sheet3", "Sheet4", "Sheet5", "Sheet2"}, f.GetSheetList()) + + // Test move sheet with invalid sheet name + assert.Equal(t, ErrSheetNameBlank, f.MoveSheet("", "Sheet2")) + assert.Equal(t, ErrSheetNameBlank, f.MoveSheet("Sheet1", "")) + + // Test move sheet on not exists worksheet + assert.Equal(t, ErrSheetNotExist{"SheetN"}, f.MoveSheet("SheetN", "Sheet2")) + assert.Equal(t, ErrSheetNotExist{"SheetN"}, f.MoveSheet("Sheet1", "SheetN")) + + // Test move sheet with unsupported workbook charset + f.WorkBook = nil + f.Pkg.Store("xl/workbook.xml", MacintoshCyrillicCharset) + assert.EqualError(t, f.MoveSheet("Sheet2", "Sheet1"), "XML syntax error on line 1: invalid UTF-8") +} + func TestDeleteAndAdjustDefinedNames(t *testing.T) { deleteAndAdjustDefinedNames(nil, 0) deleteAndAdjustDefinedNames(&xlsxWorkbook{}, 0) From f1d1a5dc2b7f1e6a10a9275b2a6e392638457ee7 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 10 Oct 2024 22:44:38 +0800 Subject: [PATCH 916/957] This closes #2004, support apply number format for time and duration cell value - Add unit tests - Update dependencies modules --- cell.go | 4 ++-- cell_test.go | 25 ++++++++++++++++++++++++- date.go | 28 ++++++++++++++++++++++++++++ excelize.go | 13 ++++++++----- go.mod | 6 +++--- go.sum | 12 ++++++------ 6 files changed, 71 insertions(+), 17 deletions(-) diff --git a/cell.go b/cell.go index 302ea9179b..7f601d3711 100644 --- a/cell.go +++ b/cell.go @@ -144,7 +144,7 @@ func (f *File) SetCellValue(sheet, cell string, value interface{}) error { if err != nil { return err } - err = f.setDefaultTimeStyle(sheet, cell, 21) + err = f.setDefaultTimeStyle(sheet, cell, getDurationNumFmt(v)) case time.Time: err = f.setCellTimeFunc(sheet, cell, v) case bool: @@ -256,7 +256,7 @@ func (f *File) setCellTimeFunc(sheet, cell string, value time.Time) error { return err } if isNum { - _ = f.setDefaultTimeStyle(sheet, cell, 22) + _ = f.setDefaultTimeStyle(sheet, cell, getTimeNumFmt(value)) } return err } diff --git a/cell_test.go b/cell_test.go index fa671732e2..5590a3682a 100644 --- a/cell_test.go +++ b/cell_test.go @@ -305,6 +305,29 @@ func TestSetCellValue(t *testing.T) { assert.NoError(t, err) assert.Equal(t, expected, val) } + // Test set cell value with time duration + for val, expected := range map[time.Duration]string{ + time.Hour*21 + time.Minute*51 + time.Second*44: "21:51:44", + time.Hour*21 + time.Minute*50: "21:50", + time.Hour*24 + time.Minute*51 + time.Second*44: "24:51:44", + time.Hour*24 + time.Minute*50: "24:50:00", + } { + assert.NoError(t, f.SetCellValue("Sheet1", "A1", val)) + val, err := f.GetCellValue("Sheet1", "A1") + assert.NoError(t, err) + assert.Equal(t, expected, val) + } + // Test set cell value with time + for val, expected := range map[time.Time]string{ + time.Date(2024, time.October, 1, 0, 0, 0, 0, time.UTC): "Oct-24", + time.Date(2024, time.October, 10, 0, 0, 0, 0, time.UTC): "10-10-24", + time.Date(2024, time.October, 10, 12, 0, 0, 0, time.UTC): "10/10/24 12:00", + } { + assert.NoError(t, f.SetCellValue("Sheet1", "A1", val)) + val, err := f.GetCellValue("Sheet1", "A1") + assert.NoError(t, err) + assert.Equal(t, expected, val) + } } func TestSetCellValues(t *testing.T) { @@ -314,7 +337,7 @@ func TestSetCellValues(t *testing.T) { v, err := f.GetCellValue("Sheet1", "A1") assert.NoError(t, err) - assert.Equal(t, v, "12/31/10 00:00") + assert.Equal(t, v, "12-31-10") // 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)) diff --git a/date.go b/date.go index de39b9cfd4..c26dd49db4 100644 --- a/date.go +++ b/date.go @@ -214,3 +214,31 @@ func formatYear(y int) int { } return y } + +// getDurationNumFmt returns most simplify numbers format code for time +// duration type cell value by given worksheet name, cell reference and number. +func getDurationNumFmt(d time.Duration) int { + if d >= time.Hour*24 { + return 46 + } + // Whole minutes + if d.Minutes() == float64(int(d.Minutes())) { + return 20 + } + return 21 +} + +// getTimeNumFmt returns most simplify numbers format code for time type cell +// value by given worksheet name, cell reference and number. +func getTimeNumFmt(t time.Time) int { + nextMonth := t.AddDate(0, 1, 0) + // Whole months + if t.Day() == 1 && nextMonth.Day() == 1 { + return 17 + } + // Whole days + if t.Hour() == 0 && t.Minute() == 0 && t.Second() == 0 && t.Nanosecond() == 0 { + return 14 + } + return 22 +} diff --git a/excelize.go b/excelize.go index c46984f505..b53a171466 100644 --- a/excelize.go +++ b/excelize.go @@ -242,15 +242,18 @@ func (f *File) xmlNewDecoder(rdr io.Reader) (ret *xml.Decoder) { // time.Time type cell value by given worksheet name, cell reference and // number format code. func (f *File) setDefaultTimeStyle(sheet, cell string, format int) error { - s, err := f.GetCellStyle(sheet, cell) + styleIdx, err := f.GetCellStyle(sheet, cell) if err != nil { return err } - if s == 0 { - style, _ := f.NewStyle(&Style{NumFmt: format}) - err = f.SetCellStyle(sheet, cell, cell, style) + if styleIdx == 0 { + styleIdx, _ = f.NewStyle(&Style{NumFmt: format}) + } else { + style, _ := f.GetStyle(styleIdx) + style.NumFmt = format + styleIdx, _ = f.NewStyle(style) } - return err + return f.SetCellStyle(sheet, cell, cell, styleIdx) } // workSheetReader provides a function to get the pointer to the structure diff --git a/go.mod b/go.mod index 7edba1b46f..22ba8e1dfe 100644 --- a/go.mod +++ b/go.mod @@ -8,10 +8,10 @@ require ( github.com/stretchr/testify v1.8.4 github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 - golang.org/x/crypto v0.27.0 + golang.org/x/crypto v0.28.0 golang.org/x/image v0.18.0 - golang.org/x/net v0.29.0 - golang.org/x/text v0.18.0 + golang.org/x/net v0.30.0 + golang.org/x/text v0.19.0 ) require ( diff --git a/go.sum b/go.sum index de22a1d4f0..33f90a052f 100644 --- a/go.sum +++ b/go.sum @@ -15,14 +15,14 @@ github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7 github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A= github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= -golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= -golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= -golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= -golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= From d1937a0cde23ff87549e59ef261a19d65b735c7b Mon Sep 17 00:00:00 2001 From: wushiling50 <120616893+wushiling50@users.noreply.github.com> Date: Mon, 21 Oct 2024 09:36:04 +0800 Subject: [PATCH 917/957] This closes #1885, add new CultureNameJaJP, CultureNameKoKR and CultureNameZhTW enumeration values (#1895) - Support apply number format for the Japanese calendar years, the Korean Danki calendar and the Republic of China year - Update unit tests Signed-off-by: wushiling50 <2531010934@qq.com> --- calc.go | 7 +- excelize_test.go | 11 ++- numfmt.go | 172 ++++++++++++++++++++++++++++++++++++++--------- numfmt_test.go | 17 +++++ 4 files changed, 174 insertions(+), 33 deletions(-) diff --git a/calc.go b/calc.go index 9784ea6059..976ee53dd2 100644 --- a/calc.go +++ b/calc.go @@ -13629,7 +13629,9 @@ func (fn *formulaFuncs) DBCS(argsList *list.List) formulaArg { if arg.Type == ArgError { return arg } - if fn.f.options.CultureInfo == CultureNameZhCN { + if fn.f.options.CultureInfo == CultureNameJaJP || + fn.f.options.CultureInfo == CultureNameZhCN || + fn.f.options.CultureInfo == CultureNameZhTW { var chars []string for _, r := range arg.Value() { code := r @@ -16378,7 +16380,10 @@ func (fn *formulaFuncs) DOLLAR(argsList *list.List) formulaArg { symbol := map[CultureName]string{ CultureNameUnknown: "$", CultureNameEnUS: "$", + CultureNameJaJP: "¥", + CultureNameKoKR: "\u20a9", CultureNameZhCN: "¥", + CultureNameZhTW: "NT$", }[fn.f.options.CultureInfo] numFmtCode := fmt.Sprintf("%s#,##0%s%s;(%s#,##0%s%s)", symbol, dot, strings.Repeat("0", decimals), symbol, dot, strings.Repeat("0", decimals)) diff --git a/excelize_test.go b/excelize_test.go index 7eb689fd21..94cf18441f 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -870,11 +870,17 @@ 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"}} + rawCellValues := make([][]string, 42) + for i := 0; i < 42; i++ { + rawCellValues[i] = []string{"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"}}, + CultureNameJaJP: {{"R5.8.24"}, {"令和5年8月24日"}, {"令和5年8月24日"}, {"8/24/23"}, {"2023年8月24日"}, {"0時00分"}, {"0時00分00秒"}, {"2023年8月"}, {"8月24日"}, {"R5.8.24"}, {"R5.8.24"}, {"令和5年8月24日"}, {"2023年8月"}, {"8月24日"}, {"令和5年8月24日"}, {"2023年8月"}, {"8月24日"}, {"R5.8.24"}, {"令和5年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"}}, + CultureNameKoKR: [][]string{[]string{"4356年 08月 24日"}, []string{"08-24"}, []string{"08-24"}, []string{"08-24-56"}, []string{"4356년 08월 24일"}, []string{"0시 00분"}, []string{"0시 00분 00초"}, []string{"4356-08-24"}, []string{"4356-08-24"}, []string{"4356年 08月 24日"}, []string{"4356年 08月 24日"}, []string{"08-24"}, []string{"4356-08-24"}, []string{"4356-08-24"}, []string{"08-24"}, []string{"4356-08-24"}, []string{"4356-08-24"}, []string{"4356年 08月 24日"}, []string{"08-24"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"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"}}, + CultureNameZhTW: {{"112/8/24"}, {"112年8月24日"}, {"112年8月24日"}, {"8/24/23"}, {"2023年8月24日"}, {"00時00分"}, {"00時00分00秒"}, {"上午12時00分"}, {"上午12時00分00秒"}, {"112/8/24"}, {"112/8/24"}, {"112年8月24日"}, {"上午12時00分"}, {"上午12時00分00秒"}, {"112年8月24日"}, {"上午12時00分"}, {"上午12時00分00秒"}, {"112/8/24"}, {"112年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) @@ -886,7 +892,10 @@ func TestSetCellStyleLangNumberFormat(t *testing.T) { // 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"}}, + CultureNameJaJP: {{"R5.8.24"}, {"令和5年8月24日"}, {"令和5年8月24日"}, {"2023-8-24"}, {"2023年8月24日"}, {"00:00:00"}, {"00:00:00"}, {"2023年8月"}, {"8月24日"}, {"R5.8.24"}, {"R5.8.24"}, {"令和5年8月24日"}, {"2023年8月"}, {"8月24日"}, {"令和5年8月24日"}, {"2023年8月"}, {"8月24日"}, {"R5.8.24"}, {"令和5年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"}}, + CultureNameKoKR: [][]string{[]string{"4356年 08月 24日"}, []string{"08-24"}, []string{"08-24"}, []string{"4356-8-24"}, []string{"4356년 08월 24일"}, []string{"00:00:00"}, []string{"00:00:00"}, []string{"4356-08-24"}, []string{"4356-08-24"}, []string{"4356年 08月 24日"}, []string{"4356年 08月 24日"}, []string{"08-24"}, []string{"4356-08-24"}, []string{"4356-08-24"}, []string{"08-24"}, []string{"4356-08-24"}, []string{"4356-08-24"}, []string{"4356年 08月 24日"}, []string{"08-24"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"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"}}, + CultureNameZhTW: {{"112/8/24"}, {"112年8月24日"}, {"112年8月24日"}, {"2023-8-24"}, {"2023年8月24日"}, {"00:00:00"}, {"00:00:00"}, {"上午12時00分"}, {"上午12時00分00秒"}, {"112/8/24"}, {"112/8/24"}, {"112年8月24日"}, {"上午12時00分"}, {"上午12時00分00秒"}, {"112年8月24日"}, {"上午12時00分"}, {"上午12時00分00秒"}, {"112/8/24"}, {"112年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) diff --git a/numfmt.go b/numfmt.go index 265fe28379..9a963979fe 100644 --- a/numfmt.go +++ b/numfmt.go @@ -57,7 +57,10 @@ type CultureName byte const ( CultureNameUnknown CultureName = iota CultureNameEnUS + CultureNameJaJP + CultureNameKoKR CultureNameZhCN + CultureNameZhTW ) var ( @@ -791,7 +794,7 @@ var ( 31748: {tags: []string{"zh-Hant"}, localMonth: localMonthsNameChinese3, apFmt: nfp.AmPm[2], weekdayNames: weekdayNamesChinese, weekdayNamesAbbr: weekdayNamesChineseAbbr2}, 3076: {tags: []string{"zh-HK"}, localMonth: localMonthsNameChinese2, apFmt: nfp.AmPm[2], weekdayNames: weekdayNamesChinese, weekdayNamesAbbr: weekdayNamesChineseAbbr2}, 5124: {tags: []string{"zh-MO"}, localMonth: localMonthsNameChinese3, apFmt: nfp.AmPm[2], weekdayNames: weekdayNamesChinese, weekdayNamesAbbr: weekdayNamesChineseAbbr2}, - 1028: {tags: []string{"zh-TW"}, localMonth: localMonthsNameChinese3, apFmt: nfp.AmPm[2], weekdayNames: weekdayNamesChinese, weekdayNamesAbbr: weekdayNamesChineseAbbr2}, + 1028: {tags: []string{"zh-TW"}, localMonth: localMonthsNameChinese3, apFmt: nfp.AmPm[2], weekdayNames: weekdayNamesChinese, weekdayNamesAbbr: weekdayNamesChineseAbbr2, useGannen: true}, 9: {tags: []string{"en"}, localMonth: localMonthsNameEnglish, apFmt: nfp.AmPm[0], weekdayNames: weekdayNamesEnglish, weekdayNamesAbbr: weekdayNamesEnglishAbbr}, 4096: {tags: []string{ "aa", "aa-DJ", "aa-ER", "aa-ER", "aa-NA", "agq", "agq-CM", "ak", "ak-GH", "sq-ML", @@ -1168,6 +1171,10 @@ var ( "JA-JP-X-GANNEN": {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese, weekdayNames: weekdayNamesJapanese, weekdayNamesAbbr: weekdayNamesJapaneseAbbr}, "JA-JP-X-GANNEN,80": {tags: []string{"ja-JP"}, localMonth: localMonthsNameChinese3, apFmt: apFmtJapanese, weekdayNames: weekdayNamesJapanese, weekdayNamesAbbr: weekdayNamesJapaneseAbbr, useGannen: true}, } + // republicOfChinaYear defined start time of the Republic of China + republicOfChinaYear = time.Date(1912, time.January, 1, 0, 0, 0, 0, time.UTC) + // republicOfChinaEraName defined the Republic of China era name for the Republic of China calendar. + republicOfChinaEraName = []string{"\u4e2d\u83ef\u6c11\u570b", "\u6c11\u570b", "\u524d"} // japaneseEraYears list the Japanese era name periods. japaneseEraYears = []time.Time{ time.Date(1868, time.August, 8, 0, 0, 0, 0, time.UTC), @@ -4634,6 +4641,24 @@ var ( return r.Replace(s) }, } + // langNumFmtFunc defines functions to apply language number format code. + langNumFmtFunc = map[CultureName]func(f *File, numFmtID int) string{ + CultureNameEnUS: func(f *File, numFmtID int) string { + return f.langNumFmtFuncEnUS(numFmtID) + }, + CultureNameJaJP: func(f *File, numFmtID int) string { + return f.langNumFmtFuncJaJP(numFmtID) + }, + CultureNameKoKR: func(f *File, numFmtID int) string { + return f.langNumFmtFuncKoKR(numFmtID) + }, + CultureNameZhCN: func(f *File, numFmtID int) string { + return f.langNumFmtFuncZhCN(numFmtID) + }, + CultureNameZhTW: func(f *File, numFmtID int) string { + return f.langNumFmtFuncZhTW(numFmtID) + }, + } ) // getSupportedLanguageInfo returns language infomation by giving language code. @@ -4694,6 +4719,54 @@ func (f *File) langNumFmtFuncEnUS(numFmtID int) string { return "" } +// langNumFmtFuncJaJP returns number format code by given date and time pattern +// for country code ja-jp. +func (f *File) langNumFmtFuncJaJP(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["ja-jp"][numFmtID] +} + +// langNumFmtFuncKoKR returns number format code by given date and time pattern +// for country code ko-kr. +func (f *File) langNumFmtFuncKoKR(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["ko-kr"][numFmtID] +} + +// 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] +} + +// langNumFmtFuncZhTW returns number format code by given date and time pattern +// for country code zh-tw. +func (f *File) langNumFmtFuncZhTW(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-tw"][numFmtID] +} + // 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} { @@ -4770,18 +4843,6 @@ func (f *File) extractNumFmtDecimal(fmtCode string) int { return -1 } -// 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) { @@ -4789,11 +4850,8 @@ func (f *File) getBuiltInNumFmtCode(numFmtID int) (string, bool) { return fmtCode, true } if isLangNumFmt(numFmtID) { - if f.options.CultureInfo == CultureNameEnUS { - return f.langNumFmtFuncEnUS(numFmtID), true - } - if f.options.CultureInfo == CultureNameZhCN { - return f.langNumFmtFuncZhCN(numFmtID), true + if fn, ok := langNumFmtFunc[f.options.CultureInfo]; ok { + return fn(f, numFmtID), true } } return "", false @@ -6912,23 +6970,13 @@ func eraYear(t time.Time) (int, int) { return i, year } -// yearsHandler will be handling years in the date and times types tokens for a -// number format expression. -func (nf *numberFormat) yearsHandler(token nfp.Token) { - if strings.Contains(strings.ToUpper(token.TValue), "Y") { - if len(token.TValue) <= 2 { - nf.result += strconv.Itoa(nf.t.Year())[2:] - return - } - nf.result += strconv.Itoa(nf.t.Year()) - return - } +// japaneseYearHandler handling the Japanease calendar years. +func (nf *numberFormat) japaneseYearHandler(token nfp.Token, langInfo languageInfo) { if strings.Contains(strings.ToUpper(token.TValue), "G") { i, year := eraYear(nf.t) if year == -1 { return } - langInfo, _ := getSupportedLanguageInfo(nf.localCode) nf.useGannen = langInfo.useGannen switch len(token.TValue) { case 1: @@ -6939,7 +6987,6 @@ func (nf *numberFormat) yearsHandler(token nfp.Token) { default: nf.result += japaneseEraNames[i] } - return } if strings.Contains(strings.ToUpper(token.TValue), "E") { _, year := eraYear(nf.t) @@ -6961,6 +7008,69 @@ func (nf *numberFormat) yearsHandler(token nfp.Token) { } } +// republicOfChinaYearHandler handling the Republic of China calendar years. +func (nf *numberFormat) republicOfChinaYearHandler(token nfp.Token, langInfo languageInfo) { + if strings.Contains(strings.ToUpper(token.TValue), "G") { + year := nf.t.Year() - republicOfChinaYear.Year() + 1 + if year == 1 { + nf.useGannen = langInfo.useGannen + } + var name string + if name = republicOfChinaEraName[0]; len(token.TValue) < 3 { + name = republicOfChinaEraName[1] + } + if year < 0 { + name += republicOfChinaEraName[2] + } + nf.result += name + } + if strings.Contains(strings.ToUpper(token.TValue), "E") { + year := nf.t.Year() - republicOfChinaYear.Year() + 1 + if year < 0 { + year = republicOfChinaYear.Year() - nf.t.Year() + } + if year == 1 && nf.useGannen { + nf.result += "\u5143" + return + } + if len(token.TValue) == 1 && !nf.useGannen { + nf.result += strconv.Itoa(year) + } + } +} + +// yearsHandler will be handling years in the date and times types tokens for a +// number format expression. +func (nf *numberFormat) yearsHandler(token nfp.Token) { + langInfo, _ := getSupportedLanguageInfo(nf.localCode) + if strings.Contains(strings.ToUpper(token.TValue), "Y") { + year := nf.t.Year() + if nf.opts != nil && nf.opts.CultureInfo == CultureNameKoKR { + year += 2333 + } + if len(token.TValue) <= 2 { + nf.result += strconv.Itoa(year)[2:] + return + } + nf.result += strconv.Itoa(year) + return + } + if inStrSlice(langInfo.tags, "zh-TW", false) != -1 || + nf.opts != nil && nf.opts.CultureInfo == CultureNameZhTW { + nf.republicOfChinaYearHandler(token, langInfo) + return + } + if inStrSlice(langInfo.tags, "ja-JP", false) != -1 || + nf.opts != nil && nf.opts.CultureInfo == CultureNameJaJP { + nf.japaneseYearHandler(token, langInfo) + return + } + if strings.Contains(strings.ToUpper(token.TValue), "E") { + nf.result += strconv.Itoa(nf.t.Year()) + return + } +} + // daysHandler will be handling days in the date and times types tokens for a // number format expression. func (nf *numberFormat) daysHandler(token nfp.Token) { diff --git a/numfmt_test.go b/numfmt_test.go index c3e1a9928b..158104755e 100644 --- a/numfmt_test.go +++ b/numfmt_test.go @@ -289,6 +289,20 @@ func TestNumFmt(t *testing.T) { {"43543.503206018519", "[$-401]mmmm dd yyyy h:mm AM/PM aaa", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, {"43543.503206018519", "[$-401]mmmmm dd yyyy h:mm AM/PM ddd", "\u0645 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, {"43543.503206018519", "[$-401]mmmmmm dd yyyy h:mm AM/PM dddd", "\u0645\u0627\u0631\u0633 19 2019 12:04 \u0645 \u0627\u0644\u062B\u0644\u0627\u062B\u0627\u0621"}, + {"43466.189571759256", "[$-404]g\"年\"m\"月\"d\"日\";@", "\u6c11\u570b\u5e74\u0031\u6708\u0031\u65e5"}, + {"43466.189571759256", "[$-404]e\"年\"m\"月\"d\"日\";@", "\u0031\u0030\u0038\u5e74\u0031\u6708\u0031\u65e5"}, + {"43466.189571759256", "[$-404]ge\"年\"m\"月\"d\"日\";@", "\u6c11\u570b\u0031\u0030\u0038\u5e74\u0031\u6708\u0031\u65e5"}, + {"43466.189571759256", "[$-404]gge\"年\"m\"月\"d\"日\";@", "\u6c11\u570b\u0031\u0030\u0038\u5e74\u0031\u6708\u0031\u65e5"}, + {"43466.189571759256", "[$-404]ggge\"年\"m\"月\"d\"日\";@", "\u4e2d\u83ef\u6c11\u570b\u0031\u0030\u0038\u5e74\u0031\u6708\u0031\u65e5"}, + {"43466.189571759256", "[$-404]gggge\"年\"m\"月\"d\"日\";@", "\u4e2d\u83ef\u6c11\u570b\u0031\u0030\u0038\u5e74\u0031\u6708\u0031\u65e5"}, + {"4385.5083333333332", "[$-404]ge\"年\"m\"月\"d\"日\";@", "\u6c11\u570b\u5143\u5e74\u0031\u6708\u0032\u65e5"}, + {"4385.5083333333332", "[$-404]gge\"年\"m\"月\"d\"日\";@", "\u6c11\u570b\u5143\u5e74\u0031\u6708\u0032\u65e5"}, + {"4385.5083333333332", "[$-404]ggge\"年\"m\"月\"d\"日\";@", "\u4e2d\u83ef\u6c11\u570b\u5143\u5e74\u0031\u6708\u0032\u65e5"}, + {"4385.5083333333332", "[$-404]gggge\"年\"m\"月\"d\"日\";@", "\u4e2d\u83ef\u6c11\u570b\u5143\u5e74\u0031\u6708\u0032\u65e5"}, + {"123", "[$-404]ge\"年\"m\"月\"d\"日\";@", "\u6c11\u570b\u524d\u0031\u0032\u5e74\u0035\u6708\u0032\u65e5"}, + {"123", "[$-404]gge\"年\"m\"月\"d\"日\";@", "\u6c11\u570b\u524d\u0031\u0032\u5e74\u0035\u6708\u0032\u65e5"}, + {"123", "[$-404]ggge\"年\"m\"月\"d\"日\";@", "\u4e2d\u83ef\u6c11\u570b\u524d\u0031\u0032\u5e74\u0035\u6708\u0032\u65e5"}, + {"123", "[$-404]gggge\"年\"m\"月\"d\"日\";@", "\u4e2d\u83ef\u6c11\u570b\u524d\u0031\u0032\u5e74\u0035\u6708\u0032\u65e5"}, {"44562.189571759256", "[$-1010401]mmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"}, {"44562.189571759256", "[$-1010401]mmmm dd yyyy h:mm AM/PM", "\u064A\u0646\u0627\u064A\u0631 01 2022 4:32 \u0635"}, {"44562.189571759256", "[$-1010401]mmmmm dd yyyy h:mm AM/PM", "\u064A 01 2022 4:32 \u0635"}, @@ -2722,6 +2736,9 @@ func TestNumFmt(t *testing.T) { {"44835.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM aaa", "\u0e15 01 2022 4:32 AM \u0E2A."}, {"44866.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM ddd", "\u0e1e 01 2022 4:32 AM \u0E2D."}, {"44896.18957170139", "[$-41E]mmmmm dd yyyy h:mm AM/PM dddd", "\u0e18 01 2022 4:32 AM \u0E1E\u0E24\u0E2B\u0E31\u0E2A\u0E1A\u0E14\u0E35"}, + {"100", "g\"年\"m\"月\"d\"日\";@", "年4月9日"}, + {"100", "e\"年\"m\"月\"d\"日\";@", "1900年4月9日"}, + {"100", "ge\"年\"m\"月\"d\"日\";@", "1900年4月9日"}, {"100", "[$-411]ge\"年\"m\"月\"d\"日\";@", "1900年4月9日"}, {"43709", "[$-411]ge\"年\"m\"月\"d\"日\";@", "R1年9月1日"}, {"43709", "[$-411]gge\"年\"m\"月\"d\"日\";@", "\u4EE41年9月1日"}, From af190c7fdc15409655b7409954984afd7d1640d7 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 23 Oct 2024 21:52:32 +0800 Subject: [PATCH 918/957] This closes #2014, fix redundant none type pattern fill generated - Simplify unit tests --- excelize_test.go | 4 ++-- styles.go | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/excelize_test.go b/excelize_test.go index 94cf18441f..7416409a49 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -878,7 +878,7 @@ func TestSetCellStyleLangNumberFormat(t *testing.T) { 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"}}, CultureNameJaJP: {{"R5.8.24"}, {"令和5年8月24日"}, {"令和5年8月24日"}, {"8/24/23"}, {"2023年8月24日"}, {"0時00分"}, {"0時00分00秒"}, {"2023年8月"}, {"8月24日"}, {"R5.8.24"}, {"R5.8.24"}, {"令和5年8月24日"}, {"2023年8月"}, {"8月24日"}, {"令和5年8月24日"}, {"2023年8月"}, {"8月24日"}, {"R5.8.24"}, {"令和5年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"}}, - CultureNameKoKR: [][]string{[]string{"4356年 08月 24日"}, []string{"08-24"}, []string{"08-24"}, []string{"08-24-56"}, []string{"4356년 08월 24일"}, []string{"0시 00분"}, []string{"0시 00분 00초"}, []string{"4356-08-24"}, []string{"4356-08-24"}, []string{"4356年 08月 24日"}, []string{"4356年 08月 24日"}, []string{"08-24"}, []string{"4356-08-24"}, []string{"4356-08-24"}, []string{"08-24"}, []string{"4356-08-24"}, []string{"4356-08-24"}, []string{"4356年 08月 24日"}, []string{"08-24"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}}, + CultureNameKoKR: {{"4356年 08月 24日"}, {"08-24"}, {"08-24"}, {"08-24-56"}, {"4356년 08월 24일"}, {"0시 00분"}, {"0시 00분 00초"}, {"4356-08-24"}, {"4356-08-24"}, {"4356年 08月 24日"}, {"4356年 08月 24日"}, {"08-24"}, {"4356-08-24"}, {"4356-08-24"}, {"08-24"}, {"4356-08-24"}, {"4356-08-24"}, {"4356年 08月 24日"}, {"08-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日"}, {"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"}}, CultureNameZhTW: {{"112/8/24"}, {"112年8月24日"}, {"112年8月24日"}, {"8/24/23"}, {"2023年8月24日"}, {"00時00分"}, {"00時00分00秒"}, {"上午12時00分"}, {"上午12時00分00秒"}, {"112/8/24"}, {"112/8/24"}, {"112年8月24日"}, {"上午12時00分"}, {"上午12時00分00秒"}, {"112年8月24日"}, {"上午12時00分"}, {"上午12時00分00秒"}, {"112/8/24"}, {"112年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"}}, } { @@ -893,7 +893,7 @@ func TestSetCellStyleLangNumberFormat(t *testing.T) { 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"}}, CultureNameJaJP: {{"R5.8.24"}, {"令和5年8月24日"}, {"令和5年8月24日"}, {"2023-8-24"}, {"2023年8月24日"}, {"00:00:00"}, {"00:00:00"}, {"2023年8月"}, {"8月24日"}, {"R5.8.24"}, {"R5.8.24"}, {"令和5年8月24日"}, {"2023年8月"}, {"8月24日"}, {"令和5年8月24日"}, {"2023年8月"}, {"8月24日"}, {"R5.8.24"}, {"令和5年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"}}, - CultureNameKoKR: [][]string{[]string{"4356年 08月 24日"}, []string{"08-24"}, []string{"08-24"}, []string{"4356-8-24"}, []string{"4356년 08월 24일"}, []string{"00:00:00"}, []string{"00:00:00"}, []string{"4356-08-24"}, []string{"4356-08-24"}, []string{"4356年 08月 24日"}, []string{"4356年 08月 24日"}, []string{"08-24"}, []string{"4356-08-24"}, []string{"4356-08-24"}, []string{"08-24"}, []string{"4356-08-24"}, []string{"4356-08-24"}, []string{"4356年 08月 24日"}, []string{"08-24"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}, []string{"45162"}}, + CultureNameKoKR: {{"4356年 08月 24日"}, {"08-24"}, {"08-24"}, {"4356-8-24"}, {"4356년 08월 24일"}, {"00:00:00"}, {"00:00:00"}, {"4356-08-24"}, {"4356-08-24"}, {"4356年 08月 24日"}, {"4356年 08月 24日"}, {"08-24"}, {"4356-08-24"}, {"4356-08-24"}, {"08-24"}, {"4356-08-24"}, {"4356-08-24"}, {"4356年 08月 24日"}, {"08-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"}}, CultureNameZhTW: {{"112/8/24"}, {"112年8月24日"}, {"112年8月24日"}, {"2023-8-24"}, {"2023年8月24日"}, {"00:00:00"}, {"00:00:00"}, {"上午12時00分"}, {"上午12時00分00秒"}, {"112/8/24"}, {"112/8/24"}, {"112年8月24日"}, {"上午12時00分"}, {"上午12時00分00秒"}, {"112年8月24日"}, {"上午12時00分"}, {"上午12時00分00秒"}, {"112/8/24"}, {"112年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"}}, } { diff --git a/styles.go b/styles.go index 34016514bb..c534dd4a2c 100644 --- a/styles.go +++ b/styles.go @@ -2051,11 +2051,12 @@ func newFills(style *Style, fg bool) *xlsxFill { if style.Fill.Pattern > 18 || style.Fill.Pattern < 0 { break } + var pattern xlsxPatternFill + pattern.PatternType = styleFillPatterns[style.Fill.Pattern] if len(style.Fill.Color) < 1 { + fill.PatternFill = &pattern break } - var pattern xlsxPatternFill - pattern.PatternType = styleFillPatterns[style.Fill.Pattern] if fg { if pattern.FgColor == nil { pattern.FgColor = new(xlsxColor) From 0d5d1c53b2bd33c68784f19fb8a4324ed68f372e Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 25 Oct 2024 08:52:59 +0800 Subject: [PATCH 919/957] This closes #2015, fix a v2.9.0 regression bug introduced by commit 7715c1462a917c657d4022a4fe5b57d41d77055a - Fix corrupted workbook generated by open the workbook generated by stream writer - Update unit tests --- lib.go | 9 +++++++-- lib_test.go | 4 ++++ pivotTable_test.go | 2 +- sheet_test.go | 2 +- slicer_test.go | 2 +- 5 files changed, 14 insertions(+), 5 deletions(-) diff --git a/lib.go b/lib.go index e01c305862..08ce248fe9 100644 --- a/lib.go +++ b/lib.go @@ -652,11 +652,16 @@ func getRootElement(d *xml.Decoder) []xml.Attr { case xml.StartElement: tokenIdx++ if tokenIdx == 1 { + var ns bool for i := 0; i < len(startElement.Attr); i++ { - if startElement.Attr[i].Value == NameSpaceSpreadSheet.Value { - startElement.Attr[i] = NameSpaceSpreadSheet + if startElement.Attr[i].Value == NameSpaceSpreadSheet.Value && + startElement.Attr[i].Name == NameSpaceSpreadSheet.Name { + ns = true } } + if !ns { + startElement.Attr = append(startElement.Attr, NameSpaceSpreadSheet) + } return startElement.Attr } } diff --git a/lib_test.go b/lib_test.go index 7500f4951a..489552834d 100644 --- a/lib_test.go +++ b/lib_test.go @@ -289,6 +289,10 @@ func TestBytesReplace(t *testing.T) { func TestGetRootElement(t *testing.T) { assert.Len(t, getRootElement(xml.NewDecoder(strings.NewReader(""))), 0) + // Test get workbook root element which all workbook XML namespace has prefix + f := NewFile() + d := f.xmlNewDecoder(bytes.NewReader([]byte(``))) + assert.Len(t, getRootElement(d), 3) } func TestSetIgnorableNameSpace(t *testing.T) { diff --git a/pivotTable_test.go b/pivotTable_test.go index 20528cc5a5..21c2a1d4d0 100644 --- a/pivotTable_test.go +++ b/pivotTable_test.go @@ -516,7 +516,7 @@ func TestDeleteWorkbookPivotCache(t *testing.T) { f := NewFile() // Test delete workbook pivot table cache with unsupported workbook charset f.WorkBook = nil - f.Pkg.Store("xl/workbook.xml", MacintoshCyrillicCharset) + f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset) assert.EqualError(t, f.deleteWorkbookPivotCache(PivotTableOptions{pivotCacheXML: "pivotCache/pivotCacheDefinition1.xml"}), "XML syntax error on line 1: invalid UTF-8") // Test delete workbook pivot table cache with unsupported workbook relationships charset diff --git a/sheet_test.go b/sheet_test.go index 47fcd97e46..89aa507278 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -580,7 +580,7 @@ func TestMoveSheet(t *testing.T) { // Test move sheet with unsupported workbook charset f.WorkBook = nil - f.Pkg.Store("xl/workbook.xml", MacintoshCyrillicCharset) + f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset) assert.EqualError(t, f.MoveSheet("Sheet2", "Sheet1"), "XML syntax error on line 1: invalid UTF-8") } diff --git a/slicer_test.go b/slicer_test.go index 5a79a80668..df9e6678a9 100644 --- a/slicer_test.go +++ b/slicer_test.go @@ -597,7 +597,7 @@ func TestAddWorkbookSlicerCache(t *testing.T) { // Test add a workbook slicer cache with unsupported charset workbook f := NewFile() f.WorkBook = nil - f.Pkg.Store("xl/workbook.xml", MacintoshCyrillicCharset) + f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset) assert.EqualError(t, f.addWorkbookSlicerCache(1, ExtURISlicerCachesX15), "XML syntax error on line 1: invalid UTF-8") assert.NoError(t, f.Close()) } From b7375bc6d402fc70a408b138688e964087389462 Mon Sep 17 00:00:00 2001 From: Ilia Mirkin Date: Sun, 3 Nov 2024 21:39:55 -0500 Subject: [PATCH 920/957] This closes #1395, add new function SetLegacyDrawingHF support to set graphics in a header/footer (#2018) --- picture.go | 10 +++++++ sheet.go | 2 +- vml.go | 79 +++++++++++++++++++++++++++++++++++++++++++++++++++ vmlDrawing.go | 56 +++++++++++++++++++++++++++++++----- vml_test.go | 32 +++++++++++++++++++++ 5 files changed, 171 insertions(+), 8 deletions(-) diff --git a/picture.go b/picture.go index 42be183f74..96a913344c 100644 --- a/picture.go +++ b/picture.go @@ -295,6 +295,16 @@ func (f *File) addSheetLegacyDrawing(sheet string, rID int) { } } +// addSheetLegacyDrawingHF provides a function to add legacy drawing +// header/footer element to xl/worksheets/sheet%d.xml by given +// worksheet name and relationship index. +func (f *File) addSheetLegacyDrawingHF(sheet string, rID int) { + ws, _ := f.workSheetReader(sheet) + ws.LegacyDrawingHF = &xlsxLegacyDrawingHF{ + RID: "rId" + strconv.Itoa(rID), + } +} + // addSheetDrawing provides a function to add drawing element to // xl/worksheets/sheet%d.xml by given worksheet name and relationship index. func (f *File) addSheetDrawing(sheet string, rID int) { diff --git a/sheet.go b/sheet.go index 48cf88411e..c52fb94c43 100644 --- a/sheet.go +++ b/sheet.go @@ -1239,7 +1239,7 @@ func attrValToBool(name string, attrs []xml.Attr) (val bool, err error) { // | // &F | Current workbook's file name // | -// &G | Drawing object as background (Not support currently) +// &G | Drawing object as background (Use SetLegacyDrawingHF) // | // &H | Shadow text format // | diff --git a/vml.go b/vml.go index 3f0f470ce2..53ef815884 100644 --- a/vml.go +++ b/vml.go @@ -1070,3 +1070,82 @@ func extractVMLFont(font []decodeVMLFont) []RichTextRun { } return runs } + +// SetLegacyDrawingHF provides a mechanism to set the graphics that +// can be referenced in the Header/Footer defitions via &G. +// +// The extension should be provided with a "." in front, e.g. ".png". +// The width/height should have units in them, e.g. "100pt". +func (f *File) SetLegacyDrawingHF(sheet string, g *HeaderFooterGraphics) error { + vmlID := f.countVMLDrawing() + 1 + + vml := &vmlDrawing{ + XMLNSv: "urn:schemas-microsoft-com:vml", + XMLNSo: "urn:schemas-microsoft-com:office:office", + XMLNSx: "urn:schemas-microsoft-com:office:excel", + ShapeLayout: &xlsxShapeLayout{ + Ext: "edit", IDmap: &xlsxIDmap{Ext: "edit", Data: vmlID}, + }, + ShapeType: &xlsxShapeType{ + ID: "_x0000_t75", + CoordSize: "21600,21600", + Spt: 75, + PreferRelative: "t", + Path: "m@4@5l@4@11@9@11@9@5xe", + Filled: "f", + Stroked: "f", + Stroke: &xlsxStroke{JoinStyle: "miter"}, + VFormulas: &vFormulas{ + Formulas: []vFormula{ + {Equation: "if lineDrawn pixelLineWidth 0"}, + {Equation: "sum @0 1 0"}, + {Equation: "sum 0 0 @1"}, + {Equation: "prod @2 1 2"}, + {Equation: "prod @3 21600 pixelWidth"}, + {Equation: "prod @3 21600 pixelHeight"}, + {Equation: "sum @0 0 1"}, + {Equation: "prod @6 1 2"}, + {Equation: "prod @7 21600 pixelWidth"}, + {Equation: "sum @8 21600 0"}, + {Equation: "prod @7 21600 pixelHeight"}, + {Equation: "sum @10 21600 0"}, + }, + }, + VPath: &vPath{ExtrusionOK: "f", GradientShapeOK: "t", ConnectType: "rect"}, + Lock: &oLock{Ext: "edit", AspectRatio: "t"}, + }, + } + + style := fmt.Sprintf("position:absolute;margin-left:0;margin-top:0;width:%s;height:%s;z-index:1", g.Width, g.Height) + drawingVML := "xl/drawings/vmlDrawing" + strconv.Itoa(vmlID) + ".vml" + drawingVMLRels := "xl/drawings/_rels/vmlDrawing" + strconv.Itoa(vmlID) + ".vml.rels" + + mediaStr := ".." + strings.TrimPrefix(f.addMedia(g.File, g.Extension), "xl") + imageID := f.addRels(drawingVMLRels, SourceRelationshipImage, mediaStr, "") + + shape := xlsxShape{ + ID: "RH", + Spid: "_x0000_s1025", + Type: "#_x0000_t75", + Style: style, + } + s, _ := xml.Marshal(encodeShape{ + ImageData: &vImageData{RelID: "rId" + strconv.Itoa(imageID)}, + Lock: &oLock{Ext: "edit", Rotation: "t"}, + }) + shape.Val = string(s[13 : len(s)-14]) + vml.Shape = append(vml.Shape, shape) + f.VMLDrawing[drawingVML] = vml + + sheetRelationshipsDrawingVML := "../drawings/vmlDrawing" + strconv.Itoa(vmlID) + ".vml" + sheetXMLPath, _ := f.getSheetXMLPath(sheet) + sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetXMLPath, "xl/worksheets/") + ".rels" + + drawingID := f.addRels(sheetRels, SourceRelationshipDrawingVML, sheetRelationshipsDrawingVML, "") + f.addSheetNameSpace(sheet, SourceRelationship) + f.addSheetLegacyDrawingHF(sheet, drawingID) + if err := f.setContentTypePartImageExtensions(); err != nil { + return err + } + return f.setContentTypePartVMLExtensions() +} diff --git a/vmlDrawing.go b/vmlDrawing.go index 2c3a69a92a..c021018ea1 100644 --- a/vmlDrawing.go +++ b/vmlDrawing.go @@ -20,7 +20,7 @@ type vmlDrawing struct { XMLNSv string `xml:"xmlns:v,attr"` XMLNSo string `xml:"xmlns:o,attr"` XMLNSx string `xml:"xmlns:x,attr"` - XMLNSmv string `xml:"xmlns:mv,attr"` + XMLNSmv string `xml:"xmlns:mv,attr,omitempty"` ShapeLayout *xlsxShapeLayout `xml:"o:shapelayout"` ShapeType *xlsxShapeType `xml:"v:shapetype"` Shape []xlsxShape `xml:"v:shape"` @@ -44,6 +44,7 @@ type xlsxIDmap struct { type xlsxShape struct { XMLName xml.Name `xml:"v:shape"` ID string `xml:"id,attr"` + Spid string `xml:"o:spid,attr,omitempty"` Type string `xml:"type,attr"` Style string `xml:"style,attr"` Button string `xml:"o:button,attr,omitempty"` @@ -57,12 +58,17 @@ type xlsxShape struct { // xlsxShapeType directly maps the shapetype element. type xlsxShapeType struct { - ID string `xml:"id,attr"` - CoordSize string `xml:"coordsize,attr"` - Spt int `xml:"o:spt,attr"` - Path string `xml:"path,attr"` - Stroke *xlsxStroke `xml:"v:stroke"` - VPath *vPath `xml:"v:path"` + ID string `xml:"id,attr"` + CoordSize string `xml:"coordsize,attr"` + Spt int `xml:"o:spt,attr"` + PreferRelative string `xml:"o:preferrelative,attr,omitempty"` + Path string `xml:"path,attr"` + Filled string `xml:"filled,attr,omitempty"` + Stroked string `xml:"stroked,attr,omitempty"` + Stroke *xlsxStroke `xml:"v:stroke"` + VFormulas *vFormulas `xml:"v:formulas"` + VPath *vPath `xml:"v:path"` + Lock *oLock `xml:"o:lock"` } // xlsxStroke directly maps the stroke element. @@ -72,10 +78,28 @@ type xlsxStroke struct { // vPath directly maps the v:path element. type vPath struct { + ExtrusionOK string `xml:"o:extrusionok,attr,omitempty"` GradientShapeOK string `xml:"gradientshapeok,attr,omitempty"` ConnectType string `xml:"o:connecttype,attr"` } +// oLock directly maps the o:lock element. +type oLock struct { + Ext string `xml:"v:ext,attr"` + Rotation string `xml:"rotation,attr,omitempty"` + AspectRatio string `xml:"aspectratio,attr,omitempty"` +} + +// vFormulas directly maps to the v:formulas element +type vFormulas struct { + Formulas []vFormula `xml:"v:f"` +} + +// vFormula directly maps to the v:f element +type vFormula struct { + Equation string `xml:"eqn,attr"` +} + // vFill directly maps the v:fill element. This element must be defined within a // Shape element. type vFill struct { @@ -106,6 +130,13 @@ type vTextBox struct { Div *xlsxDiv `xml:"div"` } +// vImageData directly maps the v:imagedata element. This element must be +// defined within a Shape element. +type vImageData struct { + RelID string `xml:"o:relid,attr"` + Title string `xml:"o:title,attr,omitempty"` +} + // xlsxDiv directly maps the div element. type xlsxDiv struct { Style string `xml:"style,attr"` @@ -254,7 +285,9 @@ type encodeShape struct { Shadow *vShadow `xml:"v:shadow"` Path *vPath `xml:"v:path"` TextBox *vTextBox `xml:"v:textbox"` + ImageData *vImageData `xml:"v:imagedata"` ClientData *xClientData `xml:"x:ClientData"` + Lock *oLock `xml:"o:lock"` } // formCtrlPreset defines the structure used to form control presets. @@ -301,3 +334,12 @@ type FormControl struct { Type FormControlType Format GraphicOptions } + +// HeaderFooterGraphics defines the settings for an image to be +// accessible from the header/footer options. +type HeaderFooterGraphics struct { + File []byte + Extension string + Width string + Height string +} diff --git a/vml_test.go b/vml_test.go index d05b67f412..b79e63dafd 100644 --- a/vml_test.go +++ b/vml_test.go @@ -412,3 +412,35 @@ func TestExtractFormControl(t *testing.T) { _, err := extractFormControl(string(MacintoshCyrillicCharset)) assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") } + +func TestSetLegacyDrawingHF(t *testing.T) { + f := NewFile() + sheet := "Sheet1" + headerFooterOptions := HeaderFooterOptions{ + OddHeader: "&LExcelize&R&G", + } + assert.NoError(t, f.SetHeaderFooter(sheet, &headerFooterOptions)) + file, err := os.ReadFile(filepath.Join("test", "images", "excel.png")) + assert.NoError(t, err) + assert.NoError(t, f.SetLegacyDrawingHF(sheet, &HeaderFooterGraphics{ + Extension: ".png", + File: file, + Width: "50pt", + Height: "32pt", + })) + assert.NoError(t, f.SetCellValue(sheet, "A1", "Example")) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetLegacyDrawingHF.xlsx"))) + assert.NoError(t, f.Close()) + + // Test set legacy drawing header/footer with unsupported charset content types + f = NewFile() + f.ContentTypes = nil + f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) + assert.EqualError(t, f.SetLegacyDrawingHF(sheet, &HeaderFooterGraphics{ + Extension: ".png", + File: file, + Width: "50pt", + Height: "32pt", + }), "XML syntax error on line 1: invalid UTF-8") + assert.NoError(t, f.Close()) +} From d2be5cdf8e4c3e0e3155de4743603f082e51ac19 Mon Sep 17 00:00:00 2001 From: Ilia Mirkin Date: Fri, 8 Nov 2024 03:59:07 -0500 Subject: [PATCH 921/957] The SetPageLayout function support set page order of page layout (#2022) - Add new fields PageOrder for PageLayoutOptions - Add a new exported error variable ErrPageSetupAdjustTo - An error will be return if the option value of the SetPageLayout function is invalid - Updated unit tests --- errors.go | 9 +++++++++ sheet.go | 28 ++++++++++++++++++++++------ sheet_test.go | 11 +++++++++++ templates.go | 6 ++++++ xmlWorksheet.go | 3 +++ 5 files changed, 51 insertions(+), 6 deletions(-) diff --git a/errors.go b/errors.go index 55a3c3c3e6..76708b41fa 100644 --- a/errors.go +++ b/errors.go @@ -88,6 +88,9 @@ var ( // ErrOutlineLevel defined the error message on receive an invalid outline // level number. ErrOutlineLevel = errors.New("invalid outline level") + // ErrPageSetupAdjustTo defined the error message for receiving a page setup + // adjust to value exceeds limit. + ErrPageSetupAdjustTo = errors.New("adjust to value must be between 10 and 400") // ErrParameterInvalid defined the error message on receive the invalid // parameter. ErrParameterInvalid = errors.New("parameter is invalid") @@ -249,6 +252,12 @@ 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) } +// newInvalidPageLayoutValueError defined the error message on receiving the invalid +// page layout options value. +func newInvalidPageLayoutValueError(name, value, msg string) error { + return fmt.Errorf("invalid %s value %q, acceptable value should be one of %s", name, value, msg) +} + // newInvalidRowNumberError defined the error message on receiving the invalid // row number. func newInvalidRowNumberError(row int) error { diff --git a/sheet.go b/sheet.go index c52fb94c43..5bff9b75d3 100644 --- a/sheet.go +++ b/sheet.go @@ -1609,8 +1609,7 @@ func (f *File) SetPageLayout(sheet string, opts *PageLayoutOptions) error { if opts == nil { return err } - ws.setPageSetUp(opts) - return err + return ws.setPageSetUp(opts) } // newPageSetUp initialize page setup settings for the worksheet if which not @@ -1622,12 +1621,15 @@ func (ws *xlsxWorksheet) newPageSetUp() { } // setPageSetUp set page setup settings for the worksheet by given options. -func (ws *xlsxWorksheet) setPageSetUp(opts *PageLayoutOptions) { +func (ws *xlsxWorksheet) setPageSetUp(opts *PageLayoutOptions) error { if opts.Size != nil { ws.newPageSetUp() ws.PageSetUp.PaperSize = opts.Size } - if opts.Orientation != nil && (*opts.Orientation == "portrait" || *opts.Orientation == "landscape") { + if opts.Orientation != nil { + if inStrSlice(supportedPageOrientation, *opts.Orientation, true) == -1 { + return newInvalidPageLayoutValueError("Orientation", *opts.Orientation, strings.Join(supportedPageOrientation, ", ")) + } ws.newPageSetUp() ws.PageSetUp.Orientation = *opts.Orientation } @@ -1636,7 +1638,10 @@ func (ws *xlsxWorksheet) setPageSetUp(opts *PageLayoutOptions) { ws.PageSetUp.FirstPageNumber = strconv.Itoa(int(*opts.FirstPageNumber)) ws.PageSetUp.UseFirstPageNumber = true } - if opts.AdjustTo != nil && 10 <= *opts.AdjustTo && *opts.AdjustTo <= 400 { + if opts.AdjustTo != nil { + if *opts.AdjustTo < 10 || 400 < *opts.AdjustTo { + return ErrPageSetupAdjustTo + } ws.newPageSetUp() ws.PageSetUp.Scale = int(*opts.AdjustTo) } @@ -1652,13 +1657,21 @@ func (ws *xlsxWorksheet) setPageSetUp(opts *PageLayoutOptions) { ws.newPageSetUp() ws.PageSetUp.BlackAndWhite = *opts.BlackAndWhite } + if opts.PageOrder != nil { + if inStrSlice(supportedPageOrder, *opts.PageOrder, true) == -1 { + return newInvalidPageLayoutValueError("PageOrder", *opts.PageOrder, strings.Join(supportedPageOrder, ", ")) + } + ws.newPageSetUp() + ws.PageSetUp.PageOrder = *opts.PageOrder + } + return nil } // GetPageLayout provides a function to gets worksheet page layout. func (f *File) GetPageLayout(sheet string) (PageLayoutOptions, error) { opts := PageLayoutOptions{ Size: intPtr(0), - Orientation: stringPtr("portrait"), + Orientation: stringPtr(supportedPageOrientation[0]), FirstPageNumber: uintPtr(1), AdjustTo: uintPtr(100), } @@ -1686,6 +1699,9 @@ func (f *File) GetPageLayout(sheet string) (PageLayoutOptions, error) { opts.FitToWidth = ws.PageSetUp.FitToWidth } opts.BlackAndWhite = boolPtr(ws.PageSetUp.BlackAndWhite) + if ws.PageSetUp.PageOrder != "" { + opts.PageOrder = stringPtr(ws.PageSetUp.PageOrder) + } } return opts, err } diff --git a/sheet_test.go b/sheet_test.go index 89aa507278..03f85eab85 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -210,6 +210,7 @@ func TestSetPageLayout(t *testing.T) { FitToHeight: intPtr(2), FitToWidth: intPtr(2), BlackAndWhite: boolPtr(true), + PageOrder: stringPtr("overThenDown"), } assert.NoError(t, f.SetPageLayout("Sheet1", &expected)) opts, err := f.GetPageLayout("Sheet1") @@ -219,6 +220,16 @@ func TestSetPageLayout(t *testing.T) { 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()) + // Test set page layout with invalid parameters + assert.EqualError(t, f.SetPageLayout("Sheet1", &PageLayoutOptions{ + AdjustTo: uintPtr(5), + }), "adjust to value must be between 10 and 400") + assert.EqualError(t, f.SetPageLayout("Sheet1", &PageLayoutOptions{ + Orientation: stringPtr("x"), + }), "invalid Orientation value \"x\", acceptable value should be one of portrait, landscape") + assert.EqualError(t, f.SetPageLayout("Sheet1", &PageLayoutOptions{ + PageOrder: stringPtr("x"), + }), "invalid PageOrder value \"x\", acceptable value should be one of overThenDown, downThenOver") } func TestGetPageLayout(t *testing.T) { diff --git a/templates.go b/templates.go index 5aafc43e8a..68d8e1f69d 100644 --- a/templates.go +++ b/templates.go @@ -498,6 +498,12 @@ var supportedDrawingUnderlineTypes = []string{ // supportedPositioning defined supported positioning types. var supportedPositioning = []string{"absolute", "oneCell", "twoCell"} +// supportedPageOrientation defined supported page setup page orientation. +var supportedPageOrientation = []string{"portrait", "landscape"} + +// supportedPageOrder defined supported page setup page order. +var supportedPageOrder = []string{"overThenDown", "downThenOver"} + // builtInDefinedNames defined built-in defined names are built with a _xlnm prefix. var builtInDefinedNames = []string{"_xlnm.Print_Area", "_xlnm.Print_Titles", "_xlnm.Criteria", "_xlnm._FilterDatabase", "_xlnm.Extract", "_xlnm.Consolidate_Area", "_xlnm.Database", "_xlnm.Sheet_Title"} diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 43359d5d58..667bdf584c 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -1006,6 +1006,9 @@ type PageLayoutOptions struct { FitToWidth *int // BlackAndWhite specified print black and white. BlackAndWhite *bool + // PageOrder specifies the ordering of multiple pages. Values + // accepted: overThenDown, downThenOver + PageOrder *string } // ViewOptions directly maps the settings of sheet view. From 30d3561d0e9bceb3a669ee864e5dcc672eff5341 Mon Sep 17 00:00:00 2001 From: Ilia Mirkin Date: Sat, 9 Nov 2024 05:36:42 -0500 Subject: [PATCH 922/957] Rename SetLegacyDrawingHF to AddHeaderFooterImage (#2023) - Add new exported HeaderFooterImagePositionType enumeration - An error will be return if the image format is unsupported - Rename exported data type HeaderFooterGraphics to HeaderFooterImageOptions - Support add and update exist header and footer images - Changes the VML data ID to sheet ID - Update unit tests - Update dependencies modules --- go.mod | 6 +- go.sum | 12 ++-- sheet.go | 2 +- vml.go | 178 +++++++++++++++++++++++++++++++++++--------------- vmlDrawing.go | 23 ++++--- vml_test.go | 89 +++++++++++++++++++++---- 6 files changed, 226 insertions(+), 84 deletions(-) diff --git a/go.mod b/go.mod index 22ba8e1dfe..48848c6dff 100644 --- a/go.mod +++ b/go.mod @@ -8,10 +8,10 @@ require ( github.com/stretchr/testify v1.8.4 github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 - golang.org/x/crypto v0.28.0 + golang.org/x/crypto v0.29.0 golang.org/x/image v0.18.0 - golang.org/x/net v0.30.0 - golang.org/x/text v0.19.0 + golang.org/x/net v0.31.0 + golang.org/x/text v0.20.0 ) require ( diff --git a/go.sum b/go.sum index 33f90a052f..2c4284e202 100644 --- a/go.sum +++ b/go.sum @@ -15,14 +15,14 @@ github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7 github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A= github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= -golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= -golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= +golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= -golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= -golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= -golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= -golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= +golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= 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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/sheet.go b/sheet.go index 5bff9b75d3..f57797b4ee 100644 --- a/sheet.go +++ b/sheet.go @@ -1239,7 +1239,7 @@ func attrValToBool(name string, attrs []xml.Attr) (val bool, err error) { // | // &F | Current workbook's file name // | -// &G | Drawing object as background (Use SetLegacyDrawingHF) +// &G | Drawing object as background (Use AddHeaderFooterImage) // | // &H | Shadow text format // | diff --git a/vml.go b/vml.go index 53ef815884..d8fcbb0fdb 100644 --- a/vml.go +++ b/vml.go @@ -36,6 +36,16 @@ const ( FormControlScrollBar ) +// HeaderFooterImagePositionType is the type of header and footer image position. +type HeaderFooterImagePositionType byte + +// Worksheet header and footer image position types enumeration. +const ( + HeaderFooterImagePositionLeft HeaderFooterImagePositionType = iota + HeaderFooterImagePositionCenter + HeaderFooterImagePositionRight +) + // GetComments retrieves all comments in a worksheet by given worksheet name. func (f *File) GetComments(sheet string) ([]Comment, error) { var comments []Comment @@ -519,6 +529,7 @@ func (f *File) addVMLObject(opts vmlOptions) error { } vmlID = f.countVMLDrawing() + 1 } + sheetID := f.getSheetID(opts.sheet) drawingVML := "xl/drawings/vmlDrawing" + strconv.Itoa(vmlID) + ".vml" sheetRelationshipsDrawingVML := "../drawings/vmlDrawing" + strconv.Itoa(vmlID) + ".vml" sheetXMLPath, _ := f.getSheetXMLPath(opts.sheet) @@ -534,7 +545,7 @@ func (f *File) addVMLObject(opts vmlOptions) error { f.addSheetNameSpace(opts.sheet, SourceRelationship) f.addSheetLegacyDrawing(opts.sheet, rID) } - if err = f.addDrawingVML(vmlID, drawingVML, prepareFormCtrlOptions(&opts)); err != nil { + if err = f.addDrawingVML(sheetID, drawingVML, prepareFormCtrlOptions(&opts)); err != nil { return err } if !opts.formCtrl { @@ -823,7 +834,7 @@ func (f *File) addFormCtrlShape(preset formCtrlPreset, col, row int, anchor stri // anchor value is a comma-separated list of data written out as: LeftColumn, // LeftOffset, TopRow, TopOffset, RightColumn, RightOffset, BottomRow, // BottomOffset. -func (f *File) addDrawingVML(dataID int, drawingVML string, opts *vmlOptions) error { +func (f *File) addDrawingVML(sheetID int, drawingVML string, opts *vmlOptions) error { col, row, err := CellNameToCoordinates(opts.FormControl.Cell) if err != nil { return err @@ -843,7 +854,7 @@ func (f *File) addDrawingVML(dataID int, drawingVML string, opts *vmlOptions) er XMLNSx: "urn:schemas-microsoft-com:office:excel", XMLNSmv: "http://macVmlSchemaUri", ShapeLayout: &xlsxShapeLayout{ - Ext: "edit", IDmap: &xlsxIDmap{Ext: "edit", Data: dataID}, + Ext: "edit", IDmap: &xlsxIDmap{Ext: "edit", Data: sheetID}, }, ShapeType: &xlsxShapeType{ ID: fmt.Sprintf("_x0000_t%d", vmlID), @@ -1071,79 +1082,138 @@ func extractVMLFont(font []decodeVMLFont) []RichTextRun { return runs } -// SetLegacyDrawingHF provides a mechanism to set the graphics that -// can be referenced in the Header/Footer defitions via &G. +// AddHeaderFooterImage provides a mechanism to set the graphics that can be +// referenced in the header and footer definitions via &G, file base name, +// extension name and file bytes, supported image types: EMF, EMZ, GIF, JPEG, +// JPG, PNG, SVG, TIF, TIFF, WMF, and WMZ. // // The extension should be provided with a "." in front, e.g. ".png". -// The width/height should have units in them, e.g. "100pt". -func (f *File) SetLegacyDrawingHF(sheet string, g *HeaderFooterGraphics) error { +// The width and height should have units in them, e.g. "100pt". +func (f *File) AddHeaderFooterImage(sheet string, opts *HeaderFooterImageOptions) error { + ws, err := f.workSheetReader(sheet) + if err != nil { + return err + } + ext, ok := supportedImageTypes[strings.ToLower(opts.Extension)] + if !ok { + return ErrImgExt + } + sheetID := f.getSheetID(sheet) vmlID := f.countVMLDrawing() + 1 + drawingVML := "xl/drawings/vmlDrawing" + strconv.Itoa(vmlID) + ".vml" + sheetRelationshipsDrawingVML := "../drawings/vmlDrawing" + strconv.Itoa(vmlID) + ".vml" + sheetXMLPath, _ := f.getSheetXMLPath(sheet) + sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetXMLPath, "xl/worksheets/") + ".rels" + if ws.LegacyDrawingHF != nil { + // The worksheet already has a VML relationships, use the relationships drawing ../drawings/vmlDrawing%d.vml. + sheetRelationshipsDrawingVML = f.getSheetRelationshipsTargetByID(sheet, ws.LegacyDrawingHF.RID) + vmlID, _ = strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(sheetRelationshipsDrawingVML, "../drawings/vmlDrawing"), ".vml")) + drawingVML = strings.ReplaceAll(sheetRelationshipsDrawingVML, "..", "xl") + } else { + // Add first VML drawing for given sheet. + rID := f.addRels(sheetRels, SourceRelationshipDrawingVML, sheetRelationshipsDrawingVML, "") + f.addSheetNameSpace(sheet, SourceRelationship) + f.addSheetLegacyDrawingHF(sheet, rID) + } - vml := &vmlDrawing{ - XMLNSv: "urn:schemas-microsoft-com:vml", - XMLNSo: "urn:schemas-microsoft-com:office:office", - XMLNSx: "urn:schemas-microsoft-com:office:excel", - ShapeLayout: &xlsxShapeLayout{ - Ext: "edit", IDmap: &xlsxIDmap{Ext: "edit", Data: vmlID}, - }, - ShapeType: &xlsxShapeType{ - ID: "_x0000_t75", - CoordSize: "21600,21600", - Spt: 75, - PreferRelative: "t", - Path: "m@4@5l@4@11@9@11@9@5xe", - Filled: "f", - Stroked: "f", - Stroke: &xlsxStroke{JoinStyle: "miter"}, - VFormulas: &vFormulas{ - Formulas: []vFormula{ - {Equation: "if lineDrawn pixelLineWidth 0"}, - {Equation: "sum @0 1 0"}, - {Equation: "sum 0 0 @1"}, - {Equation: "prod @2 1 2"}, - {Equation: "prod @3 21600 pixelWidth"}, - {Equation: "prod @3 21600 pixelHeight"}, - {Equation: "sum @0 0 1"}, - {Equation: "prod @6 1 2"}, - {Equation: "prod @7 21600 pixelWidth"}, - {Equation: "sum @8 21600 0"}, - {Equation: "prod @7 21600 pixelHeight"}, - {Equation: "sum @10 21600 0"}, + shapeID := map[HeaderFooterImagePositionType]string{ + HeaderFooterImagePositionLeft: "L", + HeaderFooterImagePositionCenter: "C", + HeaderFooterImagePositionRight: "R", + }[opts.Position] + + map[bool]string{false: "H", true: "F"}[opts.IsFooter] + + map[bool]string{false: "", true: "FIRST"}[opts.FirstPage] + vml := f.VMLDrawing[drawingVML] + if vml == nil { + vml = &vmlDrawing{ + XMLNSv: "urn:schemas-microsoft-com:vml", + XMLNSo: "urn:schemas-microsoft-com:office:office", + XMLNSx: "urn:schemas-microsoft-com:office:excel", + ShapeLayout: &xlsxShapeLayout{ + Ext: "edit", IDmap: &xlsxIDmap{Ext: "edit", Data: sheetID}, + }, + ShapeType: &xlsxShapeType{ + ID: "_x0000_t75", + CoordSize: "21600,21600", + Spt: 75, + PreferRelative: "t", + Path: "m@4@5l@4@11@9@11@9@5xe", + Filled: "f", + Stroked: "f", + Stroke: &xlsxStroke{JoinStyle: "miter"}, + VFormulas: &vFormulas{ + Formulas: []vFormula{ + {Equation: "if lineDrawn pixelLineWidth 0"}, + {Equation: "sum @0 1 0"}, + {Equation: "sum 0 0 @1"}, + {Equation: "prod @2 1 2"}, + {Equation: "prod @3 21600 pixelWidth"}, + {Equation: "prod @3 21600 pixelHeight"}, + {Equation: "sum @0 0 1"}, + {Equation: "prod @6 1 2"}, + {Equation: "prod @7 21600 pixelWidth"}, + {Equation: "sum @8 21600 0"}, + {Equation: "prod @7 21600 pixelHeight"}, + {Equation: "sum @10 21600 0"}, + }, }, + VPath: &vPath{ExtrusionOK: "f", GradientShapeOK: "t", ConnectType: "rect"}, + Lock: &oLock{Ext: "edit", AspectRatio: "t"}, }, - VPath: &vPath{ExtrusionOK: "f", GradientShapeOK: "t", ConnectType: "rect"}, - Lock: &oLock{Ext: "edit", AspectRatio: "t"}, - }, + } + // Load exist VML shapes from xl/drawings/vmlDrawing%d.vml + d, err := f.decodeVMLDrawingReader(drawingVML) + if err != nil { + return err + } + if d != nil { + vml.ShapeType.ID = d.ShapeType.ID + vml.ShapeType.CoordSize = d.ShapeType.CoordSize + vml.ShapeType.Spt = d.ShapeType.Spt + vml.ShapeType.PreferRelative = d.ShapeType.PreferRelative + vml.ShapeType.Path = d.ShapeType.Path + vml.ShapeType.Filled = d.ShapeType.Filled + vml.ShapeType.Stroked = d.ShapeType.Stroked + for _, v := range d.Shape { + s := xlsxShape{ + ID: v.ID, + SpID: v.SpID, + Type: v.Type, + Style: v.Style, + Val: v.Val, + } + vml.Shape = append(vml.Shape, s) + } + } } - style := fmt.Sprintf("position:absolute;margin-left:0;margin-top:0;width:%s;height:%s;z-index:1", g.Width, g.Height) - drawingVML := "xl/drawings/vmlDrawing" + strconv.Itoa(vmlID) + ".vml" + for idx, shape := range vml.Shape { + if shape.ID == shapeID { + vml.Shape = append(vml.Shape[:idx], vml.Shape[idx+1:]...) + } + } + + style := fmt.Sprintf("position:absolute;margin-left:0;margin-top:0;width:%s;height:%s;z-index:1", opts.Width, opts.Height) drawingVMLRels := "xl/drawings/_rels/vmlDrawing" + strconv.Itoa(vmlID) + ".vml.rels" - mediaStr := ".." + strings.TrimPrefix(f.addMedia(g.File, g.Extension), "xl") + mediaStr := ".." + strings.TrimPrefix(f.addMedia(opts.File, ext), "xl") imageID := f.addRels(drawingVMLRels, SourceRelationshipImage, mediaStr, "") shape := xlsxShape{ - ID: "RH", - Spid: "_x0000_s1025", + ID: shapeID, + SpID: "_x0000_s1025", Type: "#_x0000_t75", Style: style, } - s, _ := xml.Marshal(encodeShape{ + sp, _ := xml.Marshal(encodeShape{ ImageData: &vImageData{RelID: "rId" + strconv.Itoa(imageID)}, Lock: &oLock{Ext: "edit", Rotation: "t"}, }) - shape.Val = string(s[13 : len(s)-14]) + + shape.Val = string(sp[13 : len(sp)-14]) vml.Shape = append(vml.Shape, shape) f.VMLDrawing[drawingVML] = vml - sheetRelationshipsDrawingVML := "../drawings/vmlDrawing" + strconv.Itoa(vmlID) + ".vml" - sheetXMLPath, _ := f.getSheetXMLPath(sheet) - sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetXMLPath, "xl/worksheets/") + ".rels" - - drawingID := f.addRels(sheetRels, SourceRelationshipDrawingVML, sheetRelationshipsDrawingVML, "") - f.addSheetNameSpace(sheet, SourceRelationship) - f.addSheetLegacyDrawingHF(sheet, drawingID) if err := f.setContentTypePartImageExtensions(); err != nil { return err } diff --git a/vmlDrawing.go b/vmlDrawing.go index c021018ea1..182b53194f 100644 --- a/vmlDrawing.go +++ b/vmlDrawing.go @@ -44,7 +44,7 @@ type xlsxIDmap struct { type xlsxShape struct { XMLName xml.Name `xml:"v:shape"` ID string `xml:"id,attr"` - Spid string `xml:"o:spid,attr,omitempty"` + SpID string `xml:"o:spid,attr,omitempty"` Type string `xml:"type,attr"` Style string `xml:"style,attr"` Button string `xml:"o:button,attr,omitempty"` @@ -193,15 +193,19 @@ type decodeVmlDrawing struct { // decodeShapeType defines the structure used to parse the shapetype element in // the file xl/drawings/vmlDrawing%d.vml. type decodeShapeType struct { - ID string `xml:"id,attr"` - CoordSize string `xml:"coordsize,attr"` - Spt int `xml:"spt,attr"` - Path string `xml:"path,attr"` + ID string `xml:"id,attr"` + CoordSize string `xml:"coordsize,attr"` + Spt int `xml:"spt,attr"` + PreferRelative string `xml:"preferrelative,attr,omitempty"` + Path string `xml:"path,attr"` + Filled string `xml:"filled,attr,omitempty"` + Stroked string `xml:"stroked,attr,omitempty"` } // decodeShape defines the structure used to parse the particular shape element. type decodeShape struct { ID string `xml:"id,attr"` + SpID string `xml:"spid,attr,omitempty"` Type string `xml:"type,attr"` Style string `xml:"style,attr"` Button string `xml:"button,attr,omitempty"` @@ -335,10 +339,13 @@ type FormControl struct { Format GraphicOptions } -// HeaderFooterGraphics defines the settings for an image to be -// accessible from the header/footer options. -type HeaderFooterGraphics struct { +// HeaderFooterImageOptions defines the settings for an image to be accessible +// from the worksheet header and footer options. +type HeaderFooterImageOptions struct { + Position HeaderFooterImagePositionType File []byte + IsFooter bool + FirstPage bool Extension string Width string Height string diff --git a/vml_test.go b/vml_test.go index b79e63dafd..50571a1401 100644 --- a/vml_test.go +++ b/vml_test.go @@ -413,32 +413,97 @@ func TestExtractFormControl(t *testing.T) { assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") } -func TestSetLegacyDrawingHF(t *testing.T) { - f := NewFile() - sheet := "Sheet1" +func TestAddHeaderFooterImage(t *testing.T) { + f, sheet, wb := NewFile(), "Sheet1", filepath.Join("test", "TestAddHeaderFooterImage.xlsx") headerFooterOptions := HeaderFooterOptions{ - OddHeader: "&LExcelize&R&G", + DifferentFirst: true, + OddHeader: "&L&GExcelize&C&G&R&G", + OddFooter: "&L&GExcelize&C&G&R&G", + FirstHeader: "&L&GExcelize&C&G&R&G", + FirstFooter: "&L&GExcelize&C&G&R&G", } assert.NoError(t, f.SetHeaderFooter(sheet, &headerFooterOptions)) - file, err := os.ReadFile(filepath.Join("test", "images", "excel.png")) + assert.NoError(t, f.SetSheetView(sheet, -1, &ViewOptions{View: stringPtr("pageLayout")})) + images := map[string][]byte{ + ".wmf": nil, ".tif": nil, ".png": nil, + ".jpg": nil, ".gif": nil, ".emz": nil, ".emf": nil, + } + for ext := range images { + img, err := os.ReadFile(filepath.Join("test", "images", "excel"+ext)) + assert.NoError(t, err) + images[ext] = img + } + for _, opt := range []struct { + position HeaderFooterImagePositionType + file []byte + isFooter bool + firstPage bool + ext string + }{ + {position: HeaderFooterImagePositionLeft, file: images[".tif"], firstPage: true, ext: ".tif"}, + {position: HeaderFooterImagePositionCenter, file: images[".gif"], firstPage: true, ext: ".gif"}, + {position: HeaderFooterImagePositionRight, file: images[".png"], firstPage: true, ext: ".png"}, + {position: HeaderFooterImagePositionLeft, file: images[".emf"], isFooter: true, firstPage: true, ext: ".emf"}, + {position: HeaderFooterImagePositionCenter, file: images[".wmf"], isFooter: true, firstPage: true, ext: ".wmf"}, + {position: HeaderFooterImagePositionRight, file: images[".emz"], isFooter: true, firstPage: true, ext: ".emz"}, + {position: HeaderFooterImagePositionLeft, file: images[".png"], ext: ".png"}, + {position: HeaderFooterImagePositionCenter, file: images[".png"], ext: ".png"}, + {position: HeaderFooterImagePositionRight, file: images[".png"], ext: ".png"}, + {position: HeaderFooterImagePositionLeft, file: images[".tif"], isFooter: true, ext: ".tif"}, + {position: HeaderFooterImagePositionCenter, file: images[".tif"], isFooter: true, ext: ".tif"}, + {position: HeaderFooterImagePositionRight, file: images[".tif"], isFooter: true, ext: ".tif"}, + } { + assert.NoError(t, f.AddHeaderFooterImage(sheet, &HeaderFooterImageOptions{ + Position: opt.position, + File: opt.file, + IsFooter: opt.isFooter, + FirstPage: opt.firstPage, + Extension: opt.ext, + Width: "50pt", + Height: "32pt", + })) + } + assert.NoError(t, f.SetCellValue(sheet, "A1", "Example")) + + // Test add header footer image with not exist sheet + assert.EqualError(t, f.AddHeaderFooterImage("SheetN", nil), "sheet SheetN does not exist") + // Test add header footer image with unsupported file type + assert.Equal(t, f.AddHeaderFooterImage(sheet, &HeaderFooterImageOptions{ + Extension: "jpg", + }), ErrImgExt) + assert.NoError(t, f.SaveAs(wb)) + assert.NoError(t, f.Close()) + // Test change already exist header image with the different image + f, err := OpenFile(wb) assert.NoError(t, err) - assert.NoError(t, f.SetLegacyDrawingHF(sheet, &HeaderFooterGraphics{ - Extension: ".png", - File: file, + assert.NoError(t, f.AddHeaderFooterImage(sheet, &HeaderFooterImageOptions{ + File: images[".jpg"], + FirstPage: true, + Extension: ".jpg", Width: "50pt", Height: "32pt", })) - assert.NoError(t, f.SetCellValue(sheet, "A1", "Example")) - assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetLegacyDrawingHF.xlsx"))) + assert.NoError(t, f.Save()) assert.NoError(t, f.Close()) + // Test add header image with unsupported charset VML drawing + f, err = OpenFile(wb) + assert.NoError(t, err) + f.Pkg.Store("xl/drawings/vmlDrawing1.vml", MacintoshCyrillicCharset) + assert.EqualError(t, f.AddHeaderFooterImage(sheet, &HeaderFooterImageOptions{ + File: images[".jpg"], + Extension: ".jpg", + Width: "50pt", + Height: "32pt", + }), "XML syntax error on line 1: invalid UTF-8") + assert.NoError(t, f.Close()) // Test set legacy drawing header/footer with unsupported charset content types f = NewFile() f.ContentTypes = nil f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) - assert.EqualError(t, f.SetLegacyDrawingHF(sheet, &HeaderFooterGraphics{ + assert.EqualError(t, f.AddHeaderFooterImage(sheet, &HeaderFooterImageOptions{ Extension: ".png", - File: file, + File: images[".png"], Width: "50pt", Height: "32pt", }), "XML syntax error on line 1: invalid UTF-8") From 5f446f25f0a09de04d6b8b93b4dd151bdac75bd6 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 15 Nov 2024 22:03:10 +0800 Subject: [PATCH 923/957] This closes #2025, support set chart axis text direction and rotation - Add new field Alignment in the ChartAxis data type - Update unit tests - Update doc of the AddHeaderFooterImage function --- chart.go | 20 ++++++++++++++++++++ chart_test.go | 6 +++--- drawing.go | 6 ++++++ templates.go | 4 ++++ vml.go | 5 ++--- xmlChart.go | 1 + 6 files changed, 36 insertions(+), 6 deletions(-) diff --git a/chart.go b/chart.go index 5b4b39faf9..e325dc82b4 100644 --- a/chart.go +++ b/chart.go @@ -850,6 +850,7 @@ func (opts *Chart) parseTitle() { // ReverseOrder // Maximum // Minimum +// Alignment // Font // NumFmt // Title @@ -864,6 +865,7 @@ func (opts *Chart) parseTitle() { // ReverseOrder // Maximum // Minimum +// Alignment // Font // LogBase // NumFmt @@ -896,6 +898,24 @@ func (opts *Chart) parseTitle() { // Minimum: Specifies that the fixed minimum, 0 is auto. The 'Minimum' property // is optional. The default value is auto. // +// Alignment: Specifies that the alignment of the horizontal and vertical axis. +// The properties of font that can be set are: +// +// TextRotation +// Vertical +// +// The value of 'TextRotation' that can be set from -90 to 90: +// +// The value of 'Vertical' that can be set are: +// +// horz +// vert +// vert270 +// wordArtVert +// eaVert +// mongolianVert +// wordArtVertRtl +// // Font: Specifies that the font of the horizontal and vertical axis. The // properties of font that can be set are: // diff --git a/chart_test.go b/chart_test.go index c847978581..13573317ab 100644 --- a/chart_test.go +++ b/chart_test.go @@ -216,9 +216,9 @@ func TestAddChart(t *testing.T) { }{ {sheetName: "Sheet1", cell: "P1", opts: &Chart{Type: Col, Series: series, Format: format, Legend: ChartLegend{Position: "none", ShowLegendKey: true}, Title: []RichTextRun{{Text: "2D Column Chart"}}, PlotArea: plotArea, Border: ChartLine{Type: ChartLineNone}, ShowBlanksAs: "zero", XAxis: ChartAxis{Font: Font{Bold: true, Italic: true, Underline: "dbl", Family: "Times New Roman", Size: 15, Strike: true, 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: []RichTextRun{{Text: "2D Stacked Column Chart"}}, PlotArea: plotArea, Fill: Fill{Type: "pattern", Pattern: 1}, Border: ChartLine{Type: ChartLineAutomatic}, ShowBlanksAs: "zero"}}, - {sheetName: "Sheet1", cell: "P16", opts: &Chart{Type: ColPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "100% Stacked Column Chart"}}, PlotArea: plotArea, Fill: Fill{Type: "pattern", Color: []string{"EEEEEE"}, Pattern: 1}, Border: ChartLine{Type: ChartLineSolid, Width: 2}, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "P16", opts: &Chart{Type: ColPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "100% Stacked Column Chart"}}, PlotArea: plotArea, Fill: Fill{Type: "pattern", Color: []string{"EEEEEE"}, Pattern: 1}, Border: ChartLine{Type: ChartLineSolid, Width: 2}, ShowBlanksAs: "zero", XAxis: ChartAxis{Alignment: Alignment{Vertical: "wordArtVertRtl", TextRotation: 0}}}}, {sheetName: "Sheet1", cell: "X16", opts: &Chart{Type: Col3DClustered, Series: series, Format: format, Legend: ChartLegend{Position: "bottom", ShowLegendKey: false}, Title: []RichTextRun{{Text: "3D Clustered Column Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, - {sheetName: "Sheet1", cell: "P30", opts: &Chart{Type: Col3DStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Stacked Column Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "P30", opts: &Chart{Type: Col3DStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Stacked Column Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{Alignment: Alignment{Vertical: "vert", TextRotation: 0}}}}, {sheetName: "Sheet1", cell: "X30", opts: &Chart{Type: Col3DPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "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: []RichTextRun{{Text: "Radar Chart"}}, PlotArea: plotArea, ShowBlanksAs: "span"}}, {sheetName: "Sheet1", cell: "AF1", opts: &Chart{Type: Col3DConeStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Column Cone Stacked Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, @@ -233,7 +233,7 @@ func TestAddChart(t *testing.T) { {sheetName: "Sheet1", cell: "AV16", opts: &Chart{Type: Col3DCylinderClustered, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Column Cylinder Clustered Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet1", cell: "AV30", opts: &Chart{Type: Col3DCylinderPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "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: []RichTextRun{{Text: "3D Column Cylinder Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, - {sheetName: "Sheet1", cell: "P45", opts: &Chart{Type: Col3D, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Column Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "P45", opts: &Chart{Type: Col3D, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Column Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{Alignment: Alignment{Vertical: "vert270", TextRotation: 0}}}}, {sheetName: "Sheet2", cell: "P1", opts: &Chart{Type: Line3D, Series: series2, Format: format, Legend: ChartLegend{Position: "top", ShowLegendKey: false}, Title: []RichTextRun{{Text: "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: []RichTextRun{{Text: "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: []RichTextRun{{Text: "Doughnut Chart"}}, PlotArea: ChartPlotArea{ShowBubbleSize: false, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: false, ShowVal: false}, ShowBlanksAs: "zero", HoleSize: 30}}, diff --git a/drawing.go b/drawing.go index d0b66affaf..b02096d8a8 100644 --- a/drawing.go +++ b/drawing.go @@ -1256,6 +1256,12 @@ func (f *File) drawPlotAreaTxPr(opts *ChartAxis) *cTxPr { } if opts != nil { drawChartFont(&opts.Font, &cTxPr.P.PPr.DefRPr) + if -90 <= opts.Alignment.TextRotation && opts.Alignment.TextRotation <= 90 { + cTxPr.BodyPr.Rot = opts.Alignment.TextRotation * 60000 + } + if idx := inStrSlice(supportedDrawingTextVerticalType, opts.Alignment.Vertical, true); idx != -1 { + cTxPr.BodyPr.Vert = supportedDrawingTextVerticalType[idx] + } } return cTxPr } diff --git a/templates.go b/templates.go index 68d8e1f69d..58ae72f5d8 100644 --- a/templates.go +++ b/templates.go @@ -495,6 +495,10 @@ var supportedDrawingUnderlineTypes = []string{ "wavyDbl", } +// supportedDrawingTextVerticalType defined supported text vertical types in +// drawing markup language. +var supportedDrawingTextVerticalType = []string{"horz", "vert", "vert270", "wordArtVert", "eaVert", "mongolianVert", "wordArtVertRtl"} + // supportedPositioning defined supported positioning types. var supportedPositioning = []string{"absolute", "oneCell", "twoCell"} diff --git a/vml.go b/vml.go index d8fcbb0fdb..30faf638a1 100644 --- a/vml.go +++ b/vml.go @@ -1083,9 +1083,8 @@ func extractVMLFont(font []decodeVMLFont) []RichTextRun { } // AddHeaderFooterImage provides a mechanism to set the graphics that can be -// referenced in the header and footer definitions via &G, file base name, -// extension name and file bytes, supported image types: EMF, EMZ, GIF, JPEG, -// JPG, PNG, SVG, TIF, TIFF, WMF, and WMZ. +// referenced in the header and footer definitions via &G, supported image +// types: EMF, EMZ, GIF, JPEG, JPG, PNG, SVG, TIF, TIFF, WMF, and WMZ. // // The extension should be provided with a "." in front, e.g. ".png". // The width and height should have units in them, e.g. "100pt". diff --git a/xmlChart.go b/xmlChart.go index 90c29fcf46..92601b6a77 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -540,6 +540,7 @@ type ChartAxis struct { Secondary bool Maximum *float64 Minimum *float64 + Alignment Alignment Font Font LogBase float64 NumFmt ChartNumFmt From c93618856aaf68b0c4dc1c60f8757da0b95effc4 Mon Sep 17 00:00:00 2001 From: Eng Zer Jun Date: Fri, 22 Nov 2024 21:56:38 +0800 Subject: [PATCH 924/957] This closes #2029, use a faster deepcopy library (#2030) Signed-off-by: Eng Zer Jun --- col.go | 8 +++++--- go.mod | 4 ++-- go.sum | 8 ++++---- rows.go | 10 ++++++---- sheet.go | 8 +++++--- 5 files changed, 22 insertions(+), 16 deletions(-) diff --git a/col.go b/col.go index 68ffc70cc0..a61187781e 100644 --- a/col.go +++ b/col.go @@ -18,7 +18,7 @@ import ( "strconv" "strings" - "github.com/mohae/deepcopy" + "github.com/tiendc/go-deepcopy" ) // Define the default cell size and EMU unit of measurement. @@ -533,7 +533,8 @@ func (f *File) SetColWidth(sheet, startCol, endCol string, width float64) error func flatCols(col xlsxCol, cols []xlsxCol, replacer func(fc, c xlsxCol) xlsxCol) []xlsxCol { var fc []xlsxCol for i := col.Min; i <= col.Max; i++ { - c := deepcopy.Copy(col).(xlsxCol) + var c xlsxCol + deepcopy.Copy(&c, col) c.Min, c.Max = i, i fc = append(fc, c) } @@ -551,7 +552,8 @@ func flatCols(col xlsxCol, cols []xlsxCol, replacer func(fc, c xlsxCol) xlsxCol) fc[idx] = replacer(fc[idx], column) continue } - c := deepcopy.Copy(column).(xlsxCol) + var c xlsxCol + deepcopy.Copy(&c, column) c.Min, c.Max = i, i fc = append(fc, c) } diff --git a/go.mod b/go.mod index 48848c6dff..2a5d355f11 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,9 @@ module github.com/xuri/excelize/v2 go 1.18 require ( - github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 github.com/richardlehane/mscfb v1.0.4 - github.com/stretchr/testify v1.8.4 + github.com/stretchr/testify v1.9.0 + github.com/tiendc/go-deepcopy v1.1.0 github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 golang.org/x/crypto v0.29.0 diff --git a/go.sum b/go.sum index 2c4284e202..07876d5fdd 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,5 @@ 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= @@ -9,8 +7,10 @@ github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7 github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00= github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tiendc/go-deepcopy v1.1.0 h1:rBHhm5vg7WYnGLwktbQouodWjBXDoStOL4S7v/K8S4A= +github.com/tiendc/go-deepcopy v1.1.0/go.mod h1:toXoeQoUqXOOS/X4sKuiAoSk6elIdqc0pN7MTgOOo2I= github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7psK/lVsjIS2otl+1WyRyY= github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A= diff --git a/rows.go b/rows.go index d43a015040..1eb9106e79 100644 --- a/rows.go +++ b/rows.go @@ -20,7 +20,7 @@ import ( "strconv" "strings" - "github.com/mohae/deepcopy" + "github.com/tiendc/go-deepcopy" ) // duplicateHelperFunc defines functions to duplicate helper. @@ -653,7 +653,7 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) error { for i, r := range ws.SheetData.Row { if r.R == row { - rowCopy = deepcopy.Copy(ws.SheetData.Row[i]).(xlsxRow) + deepcopy.Copy(&rowCopy, ws.SheetData.Row[i]) ok = true break } @@ -729,7 +729,8 @@ func (f *File) duplicateConditionalFormat(ws *xlsxWorksheet, sheet string, row, } } if len(SQRef) > 0 { - cfCopy := deepcopy.Copy(*cf).(xlsxConditionalFormatting) + var cfCopy xlsxConditionalFormatting + deepcopy.Copy(&cfCopy, *cf) cfCopy.SQRef = strings.Join(SQRef, " ") cfs = append(cfs, &cfCopy) } @@ -759,7 +760,8 @@ func (f *File) duplicateDataValidations(ws *xlsxWorksheet, sheet string, row, ro } } if len(SQRef) > 0 { - dvCopy := deepcopy.Copy(*dv).(xlsxDataValidation) + var dvCopy xlsxDataValidation + deepcopy.Copy(&dvCopy, *dv) dvCopy.Sqref = strings.Join(SQRef, " ") dvs = append(dvs, &dvCopy) } diff --git a/sheet.go b/sheet.go index f57797b4ee..4e454606f7 100644 --- a/sheet.go +++ b/sheet.go @@ -27,7 +27,7 @@ import ( "unicode/utf16" "unicode/utf8" - "github.com/mohae/deepcopy" + "github.com/tiendc/go-deepcopy" ) // NewSheet provides the function to create a new sheet by given a worksheet @@ -124,7 +124,8 @@ func (f *File) mergeExpandedCols(ws *xlsxWorksheet) { Width: ws.Cols.Col[i-1].Width, }, ws.Cols.Col[i]); i++ { } - column := deepcopy.Copy(ws.Cols.Col[left]).(xlsxCol) + var column xlsxCol + deepcopy.Copy(&column, ws.Cols.Col[left]) if left < i-1 { column.Max = ws.Cols.Col[i-1].Min } @@ -750,7 +751,8 @@ func (f *File) copySheet(from, to int) error { if err != nil { return err } - worksheet := deepcopy.Copy(sheet).(*xlsxWorksheet) + worksheet := &xlsxWorksheet{} + deepcopy.Copy(worksheet, sheet) toSheetID := strconv.Itoa(f.getSheetID(f.GetSheetName(to))) sheetXMLPath := "xl/worksheets/sheet" + toSheetID + ".xml" if len(worksheet.SheetViews.SheetView) > 0 { From 3ca60f8d23824bc284400e04d2f13a63ae258958 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 8 Dec 2024 11:39:54 +0800 Subject: [PATCH 925/957] This closes #2033, support set gap width and overlap for column and bar chart - Add new fields GapWidth and Overlap in the Chart data type - Update unit tests - Update dependencies modules --- chart.go | 46 +++++++++++++++++++++++++++++++++++++++++++++- chart_test.go | 2 +- drawing.go | 43 +++++++++++++++++++++++++++++++++++-------- go.mod | 8 ++++---- go.sum | 16 ++++++++-------- xmlChart.go | 3 +++ xmlWorksheet.go | 2 +- 7 files changed, 97 insertions(+), 23 deletions(-) diff --git a/chart.go b/chart.go index e325dc82b4..b488f7568e 100644 --- a/chart.go +++ b/chart.go @@ -486,6 +486,42 @@ var ( Line: "standard", Line3D: "standard", } + barColChartTypes = []ChartType{ + Bar, + BarStacked, + BarPercentStacked, + Bar3DClustered, + Bar3DStacked, + Bar3DPercentStacked, + Bar3DConeClustered, + Bar3DConeStacked, + Bar3DConePercentStacked, + Bar3DPyramidClustered, + Bar3DPyramidStacked, + Bar3DPyramidPercentStacked, + Bar3DCylinderClustered, + Bar3DCylinderStacked, + Bar3DCylinderPercentStacked, + Col, + ColStacked, + ColPercentStacked, + Col3D, + Col3DClustered, + Col3DStacked, + Col3DPercentStacked, + Col3DCone, + Col3DConeStacked, + Col3DConeClustered, + Col3DConePercentStacked, + Col3DPyramid, + Col3DPyramidClustered, + Col3DPyramidStacked, + Col3DPyramidPercentStacked, + Col3DCylinder, + Col3DCylinderClustered, + Col3DCylinderStacked, + Col3DCylinderPercentStacked, + } orientation = map[bool]string{ true: "maxMin", false: "minMax", @@ -904,7 +940,7 @@ func (opts *Chart) parseTitle() { // TextRotation // Vertical // -// The value of 'TextRotation' that can be set from -90 to 90: +// The value of 'TextRotation' that can be set from -90 to 90. // // The value of 'Vertical' that can be set are: // @@ -949,6 +985,14 @@ func (opts *Chart) parseTitle() { // 'HoleSize' property. The 'HoleSize' property is optional. The default width // is 75, and the value should be great than 0 and less or equal than 90. // +// Set the gap with of the column and bar series chart by 'GapWidth' property. +// The 'GapWidth' property is optional. The default width is 150, and the value +// should be great or equal than 0 and less or equal than 500. +// +// Set series overlap of the column and bar series chart by 'Overlap' property. +// The 'Overlap' property is optional. The default width is 0, and the value +// should be great or equal than -100 and less or equal than 100. +// // 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: diff --git a/chart_test.go b/chart_test.go index 13573317ab..dc2df7ae95 100644 --- a/chart_test.go +++ b/chart_test.go @@ -215,7 +215,7 @@ func TestAddChart(t *testing.T) { opts *Chart }{ {sheetName: "Sheet1", cell: "P1", opts: &Chart{Type: Col, Series: series, Format: format, Legend: ChartLegend{Position: "none", ShowLegendKey: true}, Title: []RichTextRun{{Text: "2D Column Chart"}}, PlotArea: plotArea, Border: ChartLine{Type: ChartLineNone}, ShowBlanksAs: "zero", XAxis: ChartAxis{Font: Font{Bold: true, Italic: true, Underline: "dbl", Family: "Times New Roman", Size: 15, Strike: true, 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: []RichTextRun{{Text: "2D Stacked Column Chart"}}, PlotArea: plotArea, Fill: Fill{Type: "pattern", Pattern: 1}, Border: ChartLine{Type: ChartLineAutomatic}, ShowBlanksAs: "zero"}}, + {sheetName: "Sheet1", cell: "X1", opts: &Chart{Type: ColStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "2D Stacked Column Chart"}}, PlotArea: plotArea, Fill: Fill{Type: "pattern", Pattern: 1}, Border: ChartLine{Type: ChartLineAutomatic}, ShowBlanksAs: "zero", GapWidth: uintPtr(10), Overlap: intPtr(100)}}, {sheetName: "Sheet1", cell: "P16", opts: &Chart{Type: ColPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "100% Stacked Column Chart"}}, PlotArea: plotArea, Fill: Fill{Type: "pattern", Color: []string{"EEEEEE"}, Pattern: 1}, Border: ChartLine{Type: ChartLineSolid, Width: 2}, ShowBlanksAs: "zero", XAxis: ChartAxis{Alignment: Alignment{Vertical: "wordArtVertRtl", TextRotation: 0}}}}, {sheetName: "Sheet1", cell: "X16", opts: &Chart{Type: Col3DClustered, Series: series, Format: format, Legend: ChartLegend{Position: "bottom", ShowLegendKey: false}, Title: []RichTextRun{{Text: "3D Clustered Column Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet1", cell: "P30", opts: &Chart{Type: Col3DStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Stacked Column Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{Alignment: Alignment{Vertical: "vert", TextRotation: 0}}}}, diff --git a/drawing.go b/drawing.go index b02096d8a8..876e6639cf 100644 --- a/drawing.go +++ b/drawing.go @@ -210,20 +210,18 @@ func (f *File) drawBaseChart(pa *cPlotArea, opts *Chart) *cPlotArea { VaryColors: &attrValBool{ Val: opts.VaryColors, }, - Ser: f.drawChartSeries(opts), - Shape: f.drawChartShape(opts), - DLbls: f.drawChartDLbls(opts), - AxID: f.genAxID(opts), - Overlap: &attrValInt{Val: intPtr(100)}, + Ser: f.drawChartSeries(opts), + Shape: f.drawChartShape(opts), + DLbls: f.drawChartDLbls(opts), + GapWidth: f.drawChartGapWidth(opts), + AxID: f.genAxID(opts), + Overlap: f.drawChartOverlap(opts), }, } var ok bool if *c[0].BarDir.Val, ok = plotAreaChartBarDir[opts.Type]; !ok { c[0].BarDir = nil } - if *c[0].Overlap.Val, ok = plotAreaChartOverlap[opts.Type]; !ok { - c[0].Overlap = nil - } catAx := f.drawPlotAreaCatAx(pa, opts) valAx := f.drawPlotAreaValAx(pa, opts) charts := map[ChartType]*cPlotArea{ @@ -697,6 +695,35 @@ func (f *File) drawBubbleChart(pa *cPlotArea, opts *Chart) *cPlotArea { return plotArea } +// drawChartGapWidth provides a function to draw the c:gapWidth element by given +// format sets. +func (f *File) drawChartGapWidth(opts *Chart) *attrValInt { + for _, t := range barColChartTypes { + if t == opts.Type && opts.GapWidth != nil && *opts.GapWidth != 150 && *opts.GapWidth <= 500 { + return &attrValInt{intPtr(int(*opts.GapWidth))} + } + } + return nil +} + +// drawChartOverlap provides a function to draw the c:overlap element by given +// format sets. +func (f *File) drawChartOverlap(opts *Chart) *attrValInt { + var val *attrValInt + if _, ok := plotAreaChartOverlap[opts.Type]; ok { + val = &attrValInt{intPtr(100)} + } + if opts.Overlap != nil && -100 <= *opts.Overlap && *opts.Overlap <= 100 { + val = &attrValInt{intPtr(*opts.Overlap)} + } + for _, t := range barColChartTypes { + if t == opts.Type { + return val + } + } + return nil +} + // drawChartShape provides a function to draw the c:shape element by given // format sets. func (f *File) drawChartShape(opts *Chart) *attrValString { diff --git a/go.mod b/go.mod index 2a5d355f11..250fadc69e 100644 --- a/go.mod +++ b/go.mod @@ -5,13 +5,13 @@ go 1.18 require ( github.com/richardlehane/mscfb v1.0.4 github.com/stretchr/testify v1.9.0 - github.com/tiendc/go-deepcopy v1.1.0 + github.com/tiendc/go-deepcopy v1.2.0 github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 - golang.org/x/crypto v0.29.0 + golang.org/x/crypto v0.30.0 golang.org/x/image v0.18.0 - golang.org/x/net v0.31.0 - golang.org/x/text v0.20.0 + golang.org/x/net v0.32.0 + golang.org/x/text v0.21.0 ) require ( diff --git a/go.sum b/go.sum index 07876d5fdd..3790e505e4 100644 --- a/go.sum +++ b/go.sum @@ -9,20 +9,20 @@ github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/tiendc/go-deepcopy v1.1.0 h1:rBHhm5vg7WYnGLwktbQouodWjBXDoStOL4S7v/K8S4A= -github.com/tiendc/go-deepcopy v1.1.0/go.mod h1:toXoeQoUqXOOS/X4sKuiAoSk6elIdqc0pN7MTgOOo2I= +github.com/tiendc/go-deepcopy v1.2.0 h1:6vCCs+qdLQHzFqY1fcPirsAWOmrLbuccilfp8UzD1Qo= +github.com/tiendc/go-deepcopy v1.2.0/go.mod h1:toXoeQoUqXOOS/X4sKuiAoSk6elIdqc0pN7MTgOOo2I= github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7psK/lVsjIS2otl+1WyRyY= github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A= github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= -golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= -golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= +golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY= +golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= -golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= -golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= -golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= -golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= +golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/xmlChart.go b/xmlChart.go index 92601b6a77..561639960b 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -336,6 +336,7 @@ type cCharts struct { SplitPos *attrValInt `xml:"splitPos"` SerLines *attrValString `xml:"serLines"` DLbls *cDLbls `xml:"dLbls"` + GapWidth *attrValInt `xml:"gapWidth"` Shape *attrValString `xml:"shape"` HoleSize *attrValInt `xml:"holeSize"` Smooth *attrValBool `xml:"smooth"` @@ -584,6 +585,8 @@ type Chart struct { ShowBlanksAs string BubbleSize int HoleSize int + GapWidth *uint + Overlap *int order int } diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 667bdf584c..e9b3dc5aa9 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -1007,7 +1007,7 @@ type PageLayoutOptions struct { // BlackAndWhite specified print black and white. BlackAndWhite *bool // PageOrder specifies the ordering of multiple pages. Values - // accepted: overThenDown, downThenOver + // accepted: overThenDown and downThenOver PageOrder *string } From b53bad35417e743652d2dc8aaa62c308586b4430 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 12 Dec 2024 16:43:48 +0800 Subject: [PATCH 926/957] Breaking changes: Go 1.20 and later required - Update dependencies module --- CODE_OF_CONDUCT.md => .github/CODE_OF_CONDUCT.md | 0 CONTRIBUTING.md => .github/CONTRIBUTING.md | 0 .../PULL_REQUEST_TEMPLATE.md | 0 .github/workflows/go.yml | 8 ++++---- LICENSE | 4 ++-- README.md | 2 +- README_zh.md | 2 +- adjust.go | 4 ++-- calc.go | 4 ++-- calcchain.go | 4 ++-- cell.go | 4 ++-- chart.go | 4 ++-- col.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 | 4 ++-- drawing_test.go | 4 ++-- errors.go | 4 ++-- excelize.go | 4 ++-- file.go | 4 ++-- go.mod | 6 +++--- go.sum | 8 ++++---- lib.go | 4 ++-- merge.go | 4 ++-- numfmt.go | 4 ++-- picture.go | 4 ++-- pivotTable.go | 4 ++-- rows.go | 4 ++-- shape.go | 4 ++-- sheet.go | 4 ++-- sheetpr.go | 4 ++-- sheetview.go | 4 ++-- slicer.go | 4 ++-- sparkline.go | 4 ++-- stream.go | 4 ++-- styles.go | 4 ++-- table.go | 4 ++-- templates.go | 4 ++-- vml.go | 4 ++-- vmlDrawing.go | 4 ++-- vml_test.go | 4 ++-- workbook.go | 4 ++-- xmlApp.go | 4 ++-- xmlCalcChain.go | 4 ++-- xmlChart.go | 4 ++-- xmlChartSheet.go | 4 ++-- xmlComments.go | 4 ++-- xmlContentTypes.go | 4 ++-- xmlCore.go | 4 ++-- xmlDecodeDrawing.go | 4 ++-- xmlDrawing.go | 4 ++-- xmlMetaData.go | 4 ++-- xmlPivotCache.go | 4 ++-- xmlPivotTable.go | 4 ++-- xmlSharedStrings.go | 4 ++-- xmlSlicers.go | 4 ++-- xmlStyles.go | 4 ++-- xmlTable.go | 4 ++-- xmlTheme.go | 4 ++-- xmlWorkbook.go | 4 ++-- xmlWorksheet.go | 4 ++-- 66 files changed, 129 insertions(+), 129 deletions(-) rename CODE_OF_CONDUCT.md => .github/CODE_OF_CONDUCT.md (100%) rename CONTRIBUTING.md => .github/CONTRIBUTING.md (100%) rename PULL_REQUEST_TEMPLATE.md => .github/PULL_REQUEST_TEMPLATE.md (100%) diff --git a/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md similarity index 100% rename from CODE_OF_CONDUCT.md rename to .github/CODE_OF_CONDUCT.md diff --git a/CONTRIBUTING.md b/.github/CONTRIBUTING.md similarity index 100% rename from CONTRIBUTING.md rename to .github/CONTRIBUTING.md diff --git a/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md similarity index 100% rename from PULL_REQUEST_TEMPLATE.md rename to .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 6e3db35a37..ae467b2f36 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -5,8 +5,8 @@ jobs: test: strategy: matrix: - go-version: [1.18.x, 1.19.x, 1.20.x, '>=1.21.1', 1.22.x, 1.23.x] - os: [ubuntu-latest, macos-13, windows-latest] + go-version: [1.20.x, '>=1.21.1', 1.22.x, 1.23.x] + os: [ubuntu-24.04, macos-13, windows-latest] targetplatform: [x86, x64] runs-on: ${{ matrix.os }} @@ -32,10 +32,10 @@ jobs: run: env GO111MODULE=on go test -v -timeout 30m -race ./... -coverprofile='coverage.txt' -covermode=atomic - name: Codecov - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} with: - file: coverage.txt + files: coverage.txt flags: unittests name: codecov-umbrella diff --git a/LICENSE b/LICENSE index 684d80f085..a22e6975ff 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 2016-2024 The excelize Authors. +Copyright (c) 2016-2025 The excelize Authors. Copyright (c) 2011-2017 Geoffrey J. Teale All rights reserved. @@ -27,4 +27,4 @@ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index 3f9e4e0dc2..e862d6a3df 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.18 or later. There are some [incompatible changes](https://github.com/golang/go/issues/61881) in the Go 1.21.0, the Excelize library can not working with that version normally, if you are using the Go 1.21.x, please upgrade to the Go 1.21.1 and later version. 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/). +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.20 or later. There are some [incompatible changes](https://github.com/golang/go/issues/61881) in the Go 1.21.0, the Excelize library can not working with that version normally, if you are using the Go 1.21.x, please upgrade to the Go 1.21.1 and later version. 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 6d1803a6e6..a2bc62123d 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 等多种文档格式,高度兼容带有样式、图片(表)、透视表、切片器等复杂组件的文档,并提供流式读写函数,用于处理包含大规模数据的工作簿。可应用于各类报表平台、云计算、边缘计算等系统。使用本类库要求使用的 Go 语言为 1.18 或更高版本,请注意,Go 1.21.0 中存在[不兼容的更改](https://github.com/golang/go/issues/61881),导致 Excelize 基础库无法在该版本上正常工作,如果您使用的是 Go 1.21.x,请升级到 Go 1.21.1 及更高版本。完整的使用文档请访问 [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.20 或更高版本,请注意,Go 1.21.0 中存在[不兼容的更改](https://github.com/golang/go/issues/61881),导致 Excelize 基础库无法在该版本上正常工作,如果您使用的是 Go 1.21.x,请升级到 Go 1.21.1 及更高版本。完整的使用文档请访问 [go.dev](https://pkg.go.dev/github.com/xuri/excelize/v2) 或查看 [参考文档](https://xuri.me/excelize/)。 ## 快速上手 diff --git a/adjust.go b/adjust.go index ab97c435b3..6696056919 100644 --- a/adjust.go +++ b/adjust.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/calc.go b/calc.go index 976ee53dd2..71d4bd3b1c 100644 --- a/calc.go +++ b/calc.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/calcchain.go b/calcchain.go index 32c2ef14cf..9e68aba00d 100644 --- a/calcchain.go +++ b/calcchain.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/cell.go b/cell.go index 7f601d3711..0f9db87022 100644 --- a/cell.go +++ b/cell.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/chart.go b/chart.go index b488f7568e..873a8c4ce6 100644 --- a/chart.go +++ b/chart.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/col.go b/col.go index a61187781e..914ec2dd5f 100644 --- a/col.go +++ b/col.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/crypt.go b/crypt.go index 8bbb58bf0d..bce4835def 100644 --- a/crypt.go +++ b/crypt.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/crypt_test.go b/crypt_test.go index d7fd0550c1..1dbb34c9aa 100644 --- a/crypt_test.go +++ b/crypt_test.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/datavalidation.go b/datavalidation.go index f42c1db903..dab7c2f6ec 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/datavalidation_test.go b/datavalidation_test.go index 7508ba33b6..2e10936f78 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/date.go b/date.go index c26dd49db4..103daa5bdb 100644 --- a/date.go +++ b/date.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/docProps.go b/docProps.go index f6d1489ebe..4f0eef94c9 100644 --- a/docProps.go +++ b/docProps.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/docProps_test.go b/docProps_test.go index 4456bfc36b..22a325b24e 100644 --- a/docProps_test.go +++ b/docProps_test.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/drawing.go b/drawing.go index 876e6639cf..75e2b6e27c 100644 --- a/drawing.go +++ b/drawing.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/drawing_test.go b/drawing_test.go index 3c25057d29..41b3050d92 100644 --- a/drawing_test.go +++ b/drawing_test.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/errors.go b/errors.go index 76708b41fa..4f08b8435f 100644 --- a/errors.go +++ b/errors.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/excelize.go b/excelize.go index b53a171466..4de1ac1104 100644 --- a/excelize.go +++ b/excelize.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. // // See https://xuri.me/excelize for more information about this package. package excelize diff --git a/file.go b/file.go index 03af61eb1a..2f0c5cc2c7 100644 --- a/file.go +++ b/file.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/go.mod b/go.mod index 250fadc69e..ba041bb706 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,14 @@ module github.com/xuri/excelize/v2 -go 1.18 +go 1.20 require ( github.com/richardlehane/mscfb v1.0.4 github.com/stretchr/testify v1.9.0 github.com/tiendc/go-deepcopy v1.2.0 - github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d + github.com/xuri/efp v0.0.0-20241211021726-c4e992084aa6 github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 - golang.org/x/crypto v0.30.0 + golang.org/x/crypto v0.31.0 golang.org/x/image v0.18.0 golang.org/x/net v0.32.0 golang.org/x/text v0.21.0 diff --git a/go.sum b/go.sum index 3790e505e4..559cbcc0a4 100644 --- a/go.sum +++ b/go.sum @@ -11,12 +11,12 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tiendc/go-deepcopy v1.2.0 h1:6vCCs+qdLQHzFqY1fcPirsAWOmrLbuccilfp8UzD1Qo= github.com/tiendc/go-deepcopy v1.2.0/go.mod h1:toXoeQoUqXOOS/X4sKuiAoSk6elIdqc0pN7MTgOOo2I= -github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7psK/lVsjIS2otl+1WyRyY= -github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= +github.com/xuri/efp v0.0.0-20241211021726-c4e992084aa6 h1:8m6DWBG+dlFNbx5ynvrE7NgI+Y7OlZVMVTpayoW+rCc= +github.com/xuri/efp v0.0.0-20241211021726-c4e992084aa6/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A= github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= -golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY= -golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= diff --git a/lib.go b/lib.go index 08ce248fe9..bfb2ae2f93 100644 --- a/lib.go +++ b/lib.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/merge.go b/merge.go index 63427bab97..0381a25cfc 100644 --- a/merge.go +++ b/merge.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/numfmt.go b/numfmt.go index 9a963979fe..97419a49c5 100644 --- a/numfmt.go +++ b/numfmt.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/picture.go b/picture.go index 96a913344c..c749455912 100644 --- a/picture.go +++ b/picture.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/pivotTable.go b/pivotTable.go index 03475c0931..eece6d03dd 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/rows.go b/rows.go index 1eb9106e79..68e7d0202d 100644 --- a/rows.go +++ b/rows.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/shape.go b/shape.go index cc05bf3856..4c339f69a3 100644 --- a/shape.go +++ b/shape.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/sheet.go b/sheet.go index 4e454606f7..93e76c6397 100644 --- a/sheet.go +++ b/sheet.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/sheetpr.go b/sheetpr.go index f9d517252e..8f2d939331 100644 --- a/sheetpr.go +++ b/sheetpr.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/sheetview.go b/sheetview.go index fbc2d1d67d..79d5cb6a53 100644 --- a/sheetview.go +++ b/sheetview.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/slicer.go b/slicer.go index 7b4a2d8967..f5902cd149 100644 --- a/slicer.go +++ b/slicer.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/sparkline.go b/sparkline.go index 4013a2ab5f..78404bf65d 100644 --- a/sparkline.go +++ b/sparkline.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/stream.go b/stream.go index 125fb4b6d0..a745f0f582 100644 --- a/stream.go +++ b/stream.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/styles.go b/styles.go index c534dd4a2c..3cab1e8a59 100644 --- a/styles.go +++ b/styles.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/table.go b/table.go index aec7b26038..0fb8a7119f 100644 --- a/table.go +++ b/table.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/templates.go b/templates.go index 58ae72f5d8..7f64e154f1 100644 --- a/templates.go +++ b/templates.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. // // This file contains default templates for XML files we don't yet populated // based on content. diff --git a/vml.go b/vml.go index 30faf638a1..437275a1c3 100644 --- a/vml.go +++ b/vml.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/vmlDrawing.go b/vmlDrawing.go index 182b53194f..1ccea63baa 100644 --- a/vmlDrawing.go +++ b/vmlDrawing.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/vml_test.go b/vml_test.go index 50571a1401..7ec29e94ea 100644 --- a/vml_test.go +++ b/vml_test.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/workbook.go b/workbook.go index 44db6c9add..c40e9578b3 100644 --- a/workbook.go +++ b/workbook.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/xmlApp.go b/xmlApp.go index 10a93d4483..ac7ebdaf8d 100644 --- a/xmlApp.go +++ b/xmlApp.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/xmlCalcChain.go b/xmlCalcChain.go index 785bcba652..1cd749d297 100644 --- a/xmlCalcChain.go +++ b/xmlCalcChain.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/xmlChart.go b/xmlChart.go index 561639960b..607c4e380f 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/xmlChartSheet.go b/xmlChartSheet.go index cc80b2d496..ec7b920309 100644 --- a/xmlChartSheet.go +++ b/xmlChartSheet.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/xmlComments.go b/xmlComments.go index 5eb328a812..3bd16688cf 100644 --- a/xmlComments.go +++ b/xmlComments.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/xmlContentTypes.go b/xmlContentTypes.go index 0c00a54695..b38963599b 100644 --- a/xmlContentTypes.go +++ b/xmlContentTypes.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/xmlCore.go b/xmlCore.go index ac8f7458a2..941ee8af0b 100644 --- a/xmlCore.go +++ b/xmlCore.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/xmlDecodeDrawing.go b/xmlDecodeDrawing.go index a59e7c45e4..d30e5113c7 100644 --- a/xmlDecodeDrawing.go +++ b/xmlDecodeDrawing.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/xmlDrawing.go b/xmlDrawing.go index 3d39d35d94..3a3bcf3f28 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/xmlMetaData.go b/xmlMetaData.go index 016e348674..ccfd70052c 100644 --- a/xmlMetaData.go +++ b/xmlMetaData.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/xmlPivotCache.go b/xmlPivotCache.go index c04ebbd689..1e4f737b5c 100644 --- a/xmlPivotCache.go +++ b/xmlPivotCache.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/xmlPivotTable.go b/xmlPivotTable.go index 8937503be0..934ff9ebb5 100644 --- a/xmlPivotTable.go +++ b/xmlPivotTable.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/xmlSharedStrings.go b/xmlSharedStrings.go index 6133ad4c06..c7555deb7f 100644 --- a/xmlSharedStrings.go +++ b/xmlSharedStrings.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/xmlSlicers.go b/xmlSlicers.go index 5c20923cdd..0bd245a7d7 100644 --- a/xmlSlicers.go +++ b/xmlSlicers.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/xmlStyles.go b/xmlStyles.go index 02f4becc6f..9e9deb5761 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/xmlTable.go b/xmlTable.go index a2bb2bc662..77e948ed80 100644 --- a/xmlTable.go +++ b/xmlTable.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/xmlTheme.go b/xmlTheme.go index 6bbabf68a7..503545c400 100644 --- a/xmlTheme.go +++ b/xmlTheme.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/xmlWorkbook.go b/xmlWorkbook.go index 6d489e9e83..c36485a614 100644 --- a/xmlWorkbook.go +++ b/xmlWorkbook.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize diff --git a/xmlWorksheet.go b/xmlWorksheet.go index e9b3dc5aa9..666b13ba84 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -1,4 +1,4 @@ -// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of +// Copyright 2016 - 2025 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.18 or later. +// data. This library needs Go version 1.20 or later. package excelize From 8e0490927eb2636cc4f72fa4cc64c36b00828502 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 14 Dec 2024 09:45:36 +0800 Subject: [PATCH 927/957] Update unit test for internal function unzipToTemp - Move security markdown into .github directory - Fix incorrect docs of the AddChart function - Update the CodeQL config --- SECURITY.md => .github/SECURITY.md | 0 .github/workflows/codeql-analysis.yml | 2 +- chart.go | 2 +- lib_test.go | 6 +----- 4 files changed, 3 insertions(+), 7 deletions(-) rename SECURITY.md => .github/SECURITY.md (100%) diff --git a/SECURITY.md b/.github/SECURITY.md similarity index 100% rename from SECURITY.md rename to .github/SECURITY.md diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 62e26de452..7dc52a200f 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -11,7 +11,7 @@ on: jobs: analyze: name: Analyze - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 strategy: fail-fast: false diff --git a/chart.go b/chart.go index 873a8c4ce6..74c2320cd4 100644 --- a/chart.go +++ b/chart.go @@ -935,7 +935,7 @@ func (opts *Chart) parseTitle() { // is optional. The default value is auto. // // Alignment: Specifies that the alignment of the horizontal and vertical axis. -// The properties of font that can be set are: +// The properties of alignment that can be set are: // // TextRotation // Vertical diff --git a/lib_test.go b/lib_test.go index 489552834d..f6bd94fe64 100644 --- a/lib_test.go +++ b/lib_test.go @@ -7,7 +7,6 @@ import ( "fmt" "io" "os" - "runtime" "strconv" "strings" "sync" @@ -362,9 +361,6 @@ func TestReadBytes(t *testing.T) { } func TestUnzipToTemp(t *testing.T) { - if ver := runtime.Version(); strings.HasPrefix(ver, "go1.19") || strings.HasPrefix(ver, "go1.2") { - t.Skip() - } os.Setenv("TMPDIR", "test") defer os.Unsetenv("TMPDIR") assert.NoError(t, os.Chmod(os.TempDir(), 0o444)) @@ -382,7 +378,7 @@ func TestUnzipToTemp(t *testing.T) { "\x00\x00\x00\x00\x0000000000\x00\x00\x00\x00000" + "00000000PK\x01\x0200000000" + "0000000000000000\v\x00\x00\x00" + - "\x00\x0000PK\x05\x06000000\x05\x000000" + + "\x00\x0000PK\x05\x06000000\x05\x00\xfd\x00\x00\x00" + "\v\x00\x00\x00\x00\x00") z, err := zip.NewReader(bytes.NewReader(data), int64(len(data))) assert.NoError(t, err) From 5ef4a360c1955fdad7e11f1841b4ae8ea150e9f7 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 20 Dec 2024 09:43:44 +0800 Subject: [PATCH 928/957] This closes #2048, fix missing vertical and horizontal border styles - Update dependencies module --- go.mod | 2 +- go.sum | 4 ++-- xmlStyles.go | 18 ++++++++++-------- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/go.mod b/go.mod index ba041bb706..a732a05547 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 golang.org/x/crypto v0.31.0 golang.org/x/image v0.18.0 - golang.org/x/net v0.32.0 + golang.org/x/net v0.33.0 golang.org/x/text v0.21.0 ) diff --git a/go.sum b/go.sum index 559cbcc0a4..40e347c144 100644 --- a/go.sum +++ b/go.sum @@ -19,8 +19,8 @@ golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= -golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= -golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/xmlStyles.go b/xmlStyles.go index 9e9deb5761..c23388abfe 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -159,14 +159,16 @@ type xlsxBorders struct { // border formats (left, right, top, bottom, diagonal). Color is optional. When // missing, 'automatic' is implied. type xlsxBorder struct { - DiagonalDown bool `xml:"diagonalDown,attr,omitempty"` - DiagonalUp bool `xml:"diagonalUp,attr,omitempty"` - Outline bool `xml:"outline,attr,omitempty"` - Left xlsxLine `xml:"left,omitempty"` - Right xlsxLine `xml:"right,omitempty"` - Top xlsxLine `xml:"top,omitempty"` - Bottom xlsxLine `xml:"bottom,omitempty"` - Diagonal xlsxLine `xml:"diagonal,omitempty"` + DiagonalDown bool `xml:"diagonalDown,attr,omitempty"` + DiagonalUp bool `xml:"diagonalUp,attr,omitempty"` + Outline bool `xml:"outline,attr,omitempty"` + Left xlsxLine `xml:"left"` + Right xlsxLine `xml:"right"` + Top xlsxLine `xml:"top"` + Bottom xlsxLine `xml:"bottom"` + Diagonal xlsxLine `xml:"diagonal"` + Vertical *xlsxLine `xml:"vertical"` + Horizontal *xlsxLine `xml:"horizontal"` } // xlsxCellStyles directly maps the cellStyles element. This element contains From 9934bf5c86343ecc32b96454aa8b0b63f99c77bb Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 21 Dec 2024 15:11:17 +0800 Subject: [PATCH 929/957] This closes #2046, add new function AddIgnoredErrors support to ignored error for a range of cells - Add new exported IgnoredErrorsType enumeration - Change the type of DataValidationType, DataValidationErrorStyle, DataValidationOperator, PictureInsertType from int to byte --- datavalidation.go | 6 +++--- picture.go | 2 +- sheet.go | 52 +++++++++++++++++++++++++++++++++++++++++++++-- sheet_test.go | 20 ++++++++++++++++++ xmlWorksheet.go | 24 +++++++++++++++++++++- 5 files changed, 97 insertions(+), 7 deletions(-) diff --git a/datavalidation.go b/datavalidation.go index dab7c2f6ec..37bdbaf04c 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -20,7 +20,7 @@ import ( ) // DataValidationType defined the type of data validation. -type DataValidationType int +type DataValidationType byte // Data validation types. const ( @@ -36,7 +36,7 @@ const ( ) // DataValidationErrorStyle defined the style of data validation error alert. -type DataValidationErrorStyle int +type DataValidationErrorStyle byte // Data validation error styles. const ( @@ -54,7 +54,7 @@ const ( ) // DataValidationOperator operator enum. -type DataValidationOperator int +type DataValidationOperator byte // Data validation operators. const ( diff --git a/picture.go b/picture.go index c749455912..48041683fb 100644 --- a/picture.go +++ b/picture.go @@ -25,7 +25,7 @@ import ( // PictureInsertType defines the type of the picture has been inserted into the // worksheet. -type PictureInsertType int +type PictureInsertType byte // Insert picture types. const ( diff --git a/sheet.go b/sheet.go index 93e76c6397..b5ba9a2515 100644 --- a/sheet.go +++ b/sheet.go @@ -30,6 +30,22 @@ import ( "github.com/tiendc/go-deepcopy" ) +// IgnoredErrorsType is the type of ignored errors. +type IgnoredErrorsType byte + +// Ignored errors types enumeration. +const ( + IgnoredErrorsEvalError = iota + IgnoredErrorsTwoDigitTextYear + IgnoredErrorsNumberStoredAsText + IgnoredErrorsFormula + IgnoredErrorsFormulaRange + IgnoredErrorsUnlockedFormula + IgnoredErrorsEmptyCellReference + IgnoredErrorsListDataValidation + IgnoredErrorsCalculatedColumn +) + // NewSheet provides the function to create a new sheet by given a worksheet // 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 @@ -2026,7 +2042,7 @@ 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 (ws *xlsxWorksheet) prepareSheetXML(col int, row int) { +func (ws *xlsxWorksheet) prepareSheetXML(col, row int) { rowCount := len(ws.SheetData.Row) sizeHint := 0 var ht *float64 @@ -2072,7 +2088,7 @@ func (ws *xlsxWorksheet) makeContiguousColumns(fromRow, toRow, colCount int) { // 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 { +func (f *File) SetSheetDimension(sheet, rangeRef string) error { ws, err := f.workSheetReader(sheet) if err != nil { return err @@ -2115,3 +2131,35 @@ func (f *File) GetSheetDimension(sheet string) (string, error) { } return ref, err } + +// AddIgnoredErrors provides the method to ignored error for a range of cells. +func (f *File) AddIgnoredErrors(sheet, rangeRef string, ignoredErrorsType IgnoredErrorsType) error { + ws, err := f.workSheetReader(sheet) + if err != nil { + return err + } + if rangeRef == "" { + return ErrParameterInvalid + } + if ws.IgnoredErrors == nil { + ws.IgnoredErrors = &xlsxIgnoredErrors{} + } + ie := map[IgnoredErrorsType]xlsxIgnoredError{ + IgnoredErrorsEvalError: {Sqref: rangeRef, EvalError: true}, + IgnoredErrorsTwoDigitTextYear: {Sqref: rangeRef, TwoDigitTextYear: true}, + IgnoredErrorsNumberStoredAsText: {Sqref: rangeRef, NumberStoredAsText: true}, + IgnoredErrorsFormula: {Sqref: rangeRef, Formula: true}, + IgnoredErrorsFormulaRange: {Sqref: rangeRef, FormulaRange: true}, + IgnoredErrorsUnlockedFormula: {Sqref: rangeRef, UnlockedFormula: true}, + IgnoredErrorsEmptyCellReference: {Sqref: rangeRef, EmptyCellReference: true}, + IgnoredErrorsListDataValidation: {Sqref: rangeRef, ListDataValidation: true}, + IgnoredErrorsCalculatedColumn: {Sqref: rangeRef, CalculatedColumn: true}, + }[ignoredErrorsType] + for _, val := range ws.IgnoredErrors.IgnoredError { + if reflect.DeepEqual(val, ie) { + return err + } + } + ws.IgnoredErrors.IgnoredError = append(ws.IgnoredErrors.IgnoredError, ie) + return err +} diff --git a/sheet_test.go b/sheet_test.go index 03f85eab85..9bfed1a9c2 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -821,3 +821,23 @@ func TestSheetDimension(t *testing.T) { assert.Empty(t, dimension) assert.EqualError(t, err, "sheet SheetN does not exist") } + +func TestAddIgnoredErrors(t *testing.T) { + f := NewFile() + assert.NoError(t, f.AddIgnoredErrors("Sheet1", "A1", IgnoredErrorsEvalError)) + assert.NoError(t, f.AddIgnoredErrors("Sheet1", "A1", IgnoredErrorsEvalError)) + assert.NoError(t, f.AddIgnoredErrors("Sheet1", "A1", IgnoredErrorsTwoDigitTextYear)) + assert.NoError(t, f.AddIgnoredErrors("Sheet1", "A1", IgnoredErrorsNumberStoredAsText)) + assert.NoError(t, f.AddIgnoredErrors("Sheet1", "A1", IgnoredErrorsFormula)) + assert.NoError(t, f.AddIgnoredErrors("Sheet1", "A1", IgnoredErrorsFormulaRange)) + assert.NoError(t, f.AddIgnoredErrors("Sheet1", "A1", IgnoredErrorsUnlockedFormula)) + assert.NoError(t, f.AddIgnoredErrors("Sheet1", "A1", IgnoredErrorsEmptyCellReference)) + assert.NoError(t, f.AddIgnoredErrors("Sheet1", "A1", IgnoredErrorsListDataValidation)) + assert.NoError(t, f.AddIgnoredErrors("Sheet1", "A1", IgnoredErrorsCalculatedColumn)) + + assert.Equal(t, ErrSheetNotExist{"SheetN"}, f.AddIgnoredErrors("SheetN", "A1", IgnoredErrorsEvalError)) + assert.Equal(t, ErrParameterInvalid, f.AddIgnoredErrors("Sheet1", "", IgnoredErrorsEvalError)) + + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddIgnoredErrors.xlsx"))) + assert.NoError(t, f.Close()) +} diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 666b13ba84..510deb6cea 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -48,7 +48,7 @@ type xlsxWorksheet struct { ColBreaks *xlsxColBreaks `xml:"colBreaks"` CustomProperties *xlsxInnerXML `xml:"customProperties"` CellWatches *xlsxInnerXML `xml:"cellWatches"` - IgnoredErrors *xlsxInnerXML `xml:"ignoredErrors"` + IgnoredErrors *xlsxIgnoredErrors `xml:"ignoredErrors"` SmartTags *xlsxInnerXML `xml:"smartTags"` Drawing *xlsxDrawing `xml:"drawing"` LegacyDrawing *xlsxLegacyDrawing `xml:"legacyDrawing"` @@ -679,6 +679,28 @@ type xlsxPicture struct { RID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr,omitempty"` } +// xlsxIgnoredError specifies a single ignored error for a range of cells. +type xlsxIgnoredError struct { + XMLName xml.Name `xml:"ignoredError"` + Sqref string `xml:"sqref,attr"` + EvalError bool `xml:"evalError,attr,omitempty"` + TwoDigitTextYear bool `xml:"twoDigitTextYear,attr,omitempty"` + NumberStoredAsText bool `xml:"numberStoredAsText,attr,omitempty"` + Formula bool `xml:"formula,attr,omitempty"` + FormulaRange bool `xml:"formulaRange,attr,omitempty"` + UnlockedFormula bool `xml:"unlockedFormula,attr,omitempty"` + EmptyCellReference bool `xml:"emptyCellReference,attr,omitempty"` + ListDataValidation bool `xml:"listDataValidation,attr,omitempty"` + CalculatedColumn bool `xml:"calculatedColumn,attr,omitempty"` +} + +// xlsxIgnoredErrors specifies a collection of ignored errors, by cell range. +type xlsxIgnoredErrors struct { + XMLName xml.Name `xml:"ignoredErrors"` + IgnoredError []xlsxIgnoredError `xml:"ignoredError"` + ExtLst *xlsxExtLst `xml:"extLst"` +} + // xlsxLegacyDrawing directly maps the legacyDrawing element in the namespace // http://schemas.openxmlformats.org/spreadsheetml/2006/main - A comment is a // rich text note that is attached to, and associated with, a cell, separate From 3f6ecffcca8968aa4bffe0ca0f3769159745bf3a Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 29 Dec 2024 12:30:28 +0800 Subject: [PATCH 930/957] This closes #2052, support to sets the format of the chart series data label - Add new field DataLabel in the ChartSeries data type --- cell.go | 4 ++-- chart.go | 3 +++ drawing.go | 4 ++++ xmlChart.go | 10 ++++++++++ 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/cell.go b/cell.go index 0f9db87022..d642d82897 100644 --- a/cell.go +++ b/cell.go @@ -1214,8 +1214,8 @@ func setRichText(runs []RichTextRun) ([]xlsxR, error) { } // 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: +// worksheet name, cell reference and rich text runs. For example, set rich text +// on the A1 cell of the worksheet named Sheet1: // // package main // diff --git a/chart.go b/chart.go index 74c2320cd4..1448c71ed2 100644 --- a/chart.go +++ b/chart.go @@ -748,6 +748,7 @@ func (opts *Chart) parseTitle() { // Fill // Line // Marker +// DataLabel // DataLabelPosition // // Name: Set the name for the series. The name is displayed in the chart legend @@ -791,6 +792,8 @@ func (opts *Chart) parseTitle() { // x // auto // +// DataLabel: This sets the format of the chart series data label. +// // DataLabelPosition: This sets the position of the chart series data label. // // Set properties of the chart legend. The options that can be set are: diff --git a/drawing.go b/drawing.go index 75e2b6e27c..09f5800868 100644 --- a/drawing.go +++ b/drawing.go @@ -1028,6 +1028,10 @@ func (f *File) drawChartSeriesDLbls(i int, opts *Chart) *cDLbls { dLbls.DLblPos = &attrValString{Val: stringPtr(chartDataLabelsPositionTypes[opts.Series[i].DataLabelPosition])} } } + dLbl := opts.Series[i].DataLabel + dLbls.SpPr = f.drawShapeFill(dLbl.Fill, dLbls.SpPr) + dLbls.TxPr = &cTxPr{BodyPr: aBodyPr{}, P: aP{PPr: &aPPr{DefRPr: aRPr{}}}} + drawChartFont(&dLbl.Font, &dLbls.TxPr.P.PPr.DefRPr) return dLbls } diff --git a/xmlChart.go b/xmlChart.go index 607c4e380f..4c40f1065d 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -484,6 +484,8 @@ type cNumCache struct { // the specific formatting and positioning settings. type cDLbls struct { NumFmt *cNumFmt `xml:"numFmt"` + SpPr *cSpPr `xml:"spPr"` + TxPr *cTxPr `xml:"txPr"` DLblPos *attrValString `xml:"dLblPos"` ShowLegendKey *attrValBool `xml:"showLegendKey"` ShowVal *attrValBool `xml:"showVal"` @@ -610,6 +612,13 @@ type ChartLine struct { Width float64 } +// ChartDataLabel directly maps the format settings of the chart labels. +type ChartDataLabel struct { + Alignment Alignment + Font Font + Fill Fill +} + // ChartSeries directly maps the format settings of the chart series. type ChartSeries struct { Name string @@ -619,5 +628,6 @@ type ChartSeries struct { Fill Fill Line ChartLine Marker ChartMarker + DataLabel ChartDataLabel DataLabelPosition ChartDataLabelPositionType } From caf22e4974afa377baeac21803de3923fd78e627 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 2 Jan 2025 07:32:49 +0800 Subject: [PATCH 931/957] This closes #2059, support delete one cell anchor image - Fix delete wrong images in some case which caused by image reference detection issue - Update unit tests --- chart_test.go | 5 +++ drawing.go | 99 +++++++++++++++++++++++++++++-------------------- picture.go | 51 +++++++++++++------------ picture_test.go | 12 +++++- 4 files changed, 101 insertions(+), 66 deletions(-) diff --git a/chart_test.go b/chart_test.go index dc2df7ae95..1527bc71e6 100644 --- a/chart_test.go +++ b/chart_test.go @@ -124,6 +124,11 @@ func TestDeleteDrawing(t *testing.T) { assert.EqualError(t, err, "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{OneCellAnchor: []*xdrCellAnchor{{ + GraphicFrame: string(MacintoshCyrillicCharset), + }}}) + _, err = f.deleteDrawing(0, 0, path, "Chart") + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") f.Drawings.Store(path, &xlsxWsDr{TwoCellAnchor: []*xdrCellAnchor{{ GraphicFrame: string(MacintoshCyrillicCharset), }}}) diff --git a/drawing.go b/drawing.go index 09f5800868..7241dac1f8 100644 --- a/drawing.go +++ b/drawing.go @@ -1484,13 +1484,14 @@ func (f *File) addSheetDrawingChart(drawingXML string, rID int, opts *GraphicOpt // deleteDrawing provides a function to delete the chart graphic frame and // returns deleted embed relationships ID (for unique picture cell anchor) by // given coordinates and graphic type. -func (f *File) deleteDrawing(col, row int, drawingXML, drawingType string) (string, error) { +func (f *File) deleteDrawing(col, row int, drawingXML, drawingType string) ([]string, error) { var ( - err error - rID string - rIDs []string - wsDr *xlsxWsDr - deTwoCellAnchor *decodeCellAnchor + err error + rID string + delRID, refRID []string + rIDMaps = map[string]int{} + wsDr *xlsxWsDr + deCellAnchor *decodeCellAnchor ) xdrCellAnchorFuncs := map[string]func(anchor *xdrCellAnchor) bool{ "Chart": func(anchor *xdrCellAnchor) bool { return anchor.Pic == nil }, @@ -1502,54 +1503,70 @@ func (f *File) deleteDrawing(col, row int, drawingXML, drawingType string) (stri } onAnchorCell := func(c, r int) bool { return c == col && r == row } if wsDr, _, err = f.drawingParser(drawingXML); err != nil { - return rID, err - } - for idx := 0; idx < len(wsDr.TwoCellAnchor); idx++ { - if err = nil; wsDr.TwoCellAnchor[idx].From != nil && xdrCellAnchorFuncs[drawingType](wsDr.TwoCellAnchor[idx]) { - if onAnchorCell(wsDr.TwoCellAnchor[idx].From.Col, wsDr.TwoCellAnchor[idx].From.Row) { - rID, _ = extractEmbedRID(wsDr.TwoCellAnchor[idx].Pic, nil, rIDs) - wsDr.TwoCellAnchor = append(wsDr.TwoCellAnchor[:idx], wsDr.TwoCellAnchor[idx+1:]...) - idx-- + return delRID, err + } + deleteCellAnchor := func(ca []*xdrCellAnchor) ([]*xdrCellAnchor, error) { + for idx := 0; idx < len(ca); idx++ { + if err = nil; ca[idx].From != nil && xdrCellAnchorFuncs[drawingType](ca[idx]) { + rID = extractEmbedRID(ca[idx].Pic, nil) + rIDMaps[rID]++ + if onAnchorCell(ca[idx].From.Col, ca[idx].From.Row) { + refRID = append(refRID, rID) + ca = append(ca[:idx], ca[idx+1:]...) + idx-- + rIDMaps[rID]-- + } continue } - _, rIDs = extractEmbedRID(wsDr.TwoCellAnchor[idx].Pic, nil, rIDs) - } - } - for idx := 0; idx < len(wsDr.TwoCellAnchor); idx++ { - deTwoCellAnchor = new(decodeCellAnchor) - if err = f.xmlNewDecoder(strings.NewReader("" + wsDr.TwoCellAnchor[idx].GraphicFrame + "")). - Decode(deTwoCellAnchor); err != nil && err != io.EOF { - return rID, err - } - if err = nil; deTwoCellAnchor.From != nil && decodeCellAnchorFuncs[drawingType](deTwoCellAnchor) { - if onAnchorCell(deTwoCellAnchor.From.Col, deTwoCellAnchor.From.Row) { - rID, _ = extractEmbedRID(nil, deTwoCellAnchor.Pic, rIDs) - wsDr.TwoCellAnchor = append(wsDr.TwoCellAnchor[:idx], wsDr.TwoCellAnchor[idx+1:]...) - idx-- - continue + deCellAnchor = new(decodeCellAnchor) + if err = f.xmlNewDecoder(strings.NewReader("" + ca[idx].GraphicFrame + "")). + Decode(deCellAnchor); err != nil && err != io.EOF { + return ca, err + } + if err = nil; deCellAnchor.From != nil && decodeCellAnchorFuncs[drawingType](deCellAnchor) { + rID = extractEmbedRID(nil, deCellAnchor.Pic) + rIDMaps[rID]++ + if onAnchorCell(deCellAnchor.From.Col, deCellAnchor.From.Row) { + refRID = append(refRID, rID) + ca = append(ca[:idx], ca[idx+1:]...) + idx-- + rIDMaps[rID]-- + } } - _, rIDs = extractEmbedRID(nil, deTwoCellAnchor.Pic, rIDs) } + return ca, err } - if inStrSlice(rIDs, rID, true) != -1 { - rID = "" + if wsDr.OneCellAnchor, err = deleteCellAnchor(wsDr.OneCellAnchor); err != nil { + return delRID, err + } + if wsDr.TwoCellAnchor, err = deleteCellAnchor(wsDr.TwoCellAnchor); err != nil { + return delRID, err } f.Drawings.Store(drawingXML, wsDr) - return rID, err + return getUnusedCellAnchorRID(delRID, refRID, rIDMaps), err } -// extractEmbedRID returns embed relationship ID and all relationship ID lists -// for giving cell anchor. -func extractEmbedRID(pic *xlsxPic, decodePic *decodePic, rIDs []string) (string, []string) { +// extractEmbedRID returns embed relationship ID by giving cell anchor. +func extractEmbedRID(pic *xlsxPic, decodePic *decodePic) string { + var rID string if pic != nil { - rIDs = append(rIDs, pic.BlipFill.Blip.Embed) - return pic.BlipFill.Blip.Embed, rIDs + rID = pic.BlipFill.Blip.Embed } if decodePic != nil { - rIDs = append(rIDs, decodePic.BlipFill.Blip.Embed) - return decodePic.BlipFill.Blip.Embed, rIDs + rID = decodePic.BlipFill.Blip.Embed + } + return rID +} + +// getUnusedCellAnchorRID returns relationship ID lists in the cell anchor which +// for remove. +func getUnusedCellAnchorRID(delRID, refRID []string, rIDMaps map[string]int) []string { + for _, rID := range refRID { + if rIDMaps[rID] == 0 && inStrSlice(delRID, rID, false) == -1 { + delRID = append(delRID, rID) + } } - return "", rIDs + return delRID } // deleteDrawingRels provides a function to delete relationships in diff --git a/picture.go b/picture.go index 48041683fb..3980df32cf 100644 --- a/picture.go +++ b/picture.go @@ -566,36 +566,41 @@ func (f *File) DeletePicture(sheet, cell string) error { } drawingXML := strings.ReplaceAll(f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID), "..", "xl") drawingRels := "xl/drawings/_rels/" + filepath.Base(drawingXML) + ".rels" - rID, err := f.deleteDrawing(col, row, drawingXML, "Pic") + rIDs, err := f.deleteDrawing(col, row, drawingXML, "Pic") if err != nil { return err } - rels := f.getDrawingRelationships(drawingRels, rID) - if rels == nil { - return err - } - var used bool - checkPicRef := func(k, v interface{}) bool { - if strings.Contains(k.(string), "xl/drawings/_rels/drawing") { - r, err := f.relsReader(k.(string)) - if err != nil { - return true - } - for _, rel := range r.Relationships { - if rel.ID != rels.ID && rel.Type == SourceRelationshipImage && - filepath.Base(rel.Target) == filepath.Base(rels.Target) { - used = true + for _, rID := range rIDs { + rels := f.getDrawingRelationships(drawingRels, rID) + if rels == nil { + return err + } + var used bool + checkPicRef := func(k, v interface{}) bool { + if strings.Contains(k.(string), "xl/drawings/_rels/drawing") { + if k.(string) == drawingRels { + return true + } + r, err := f.relsReader(k.(string)) + if err != nil { + return true + } + for _, rel := range r.Relationships { + if rel.Type == SourceRelationshipImage && + filepath.Base(rel.Target) == filepath.Base(rels.Target) { + used = true + } } } + return true } - return true - } - f.Relationships.Range(checkPicRef) - f.Pkg.Range(checkPicRef) - if !used { - f.Pkg.Delete(strings.Replace(rels.Target, "../", "xl/", -1)) + f.Relationships.Range(checkPicRef) + f.Pkg.Range(checkPicRef) + if !used { + f.Pkg.Delete(strings.Replace(rels.Target, "../", "xl/", -1)) + } + f.deleteDrawingRels(drawingRels, rID) } - f.deleteDrawingRels(drawingRels, rID) return err } diff --git a/picture_test.go b/picture_test.go index 9da63f99ba..2f5d4773c6 100644 --- a/picture_test.go +++ b/picture_test.go @@ -334,9 +334,9 @@ func TestDeletePicture(t *testing.T) { f, err = OpenFile(filepath.Join("test", "TestDeletePicture.xlsx")) assert.NoError(t, err) // Test delete same picture on different worksheet, the images should be removed - assert.NoError(t, f.DeletePicture("Sheet1", "F10")) - assert.NoError(t, f.DeletePicture("Sheet2", "F1")) + assert.NoError(t, f.DeletePicture("Sheet1", "F20")) assert.NoError(t, f.DeletePicture("Sheet1", "I20")) + assert.NoError(t, f.DeletePicture("Sheet2", "F1")) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDeletePicture2.xlsx"))) // Test delete picture on not exists worksheet @@ -364,6 +364,14 @@ func TestDeletePicture(t *testing.T) { assert.NoError(t, f.DeletePicture("Sheet2", "F1")) assert.NoError(t, f.Close()) + f, err = OpenFile(filepath.Join("test", "TestDeletePicture.xlsx")) + assert.NoError(t, err) + // Test delete picture without drawing relationships + f.Relationships.Delete("xl/drawings/_rels/drawing1.xml.rels") + f.Pkg.Delete("xl/drawings/_rels/drawing1.xml.rels") + assert.NoError(t, f.DeletePicture("Sheet1", "I20")) + assert.NoError(t, f.Close()) + f = NewFile() assert.NoError(t, err) assert.NoError(t, f.AddPicture("Sheet1", "A1", filepath.Join("test", "images", "excel.jpg"), nil)) From 4b4d4df76b84974d16a5e39216dbc8fe9ccad832 Mon Sep 17 00:00:00 2001 From: Arpelicy <165371648+Arpelicy@users.noreply.github.com> Date: Sat, 4 Jan 2025 11:17:56 +0800 Subject: [PATCH 932/957] This closes #2061, fix border styles missing after saved workbook (#2064) - Using form template for GitHub issues - Typo fix for comments of the getSupportedLanguageInfo function --- .github/ISSUE_TEMPLATE/bug_report.md | 44 ------------ .github/ISSUE_TEMPLATE/bug_report.yml | 81 ++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 44 ------------ .github/ISSUE_TEMPLATE/feature_request.yml | 30 ++++++++ numfmt.go | 2 +- styles.go | 29 +++----- xmlStyles.go | 10 +-- 7 files changed, 127 insertions(+), 113 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml delete mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index a8c31a04b3..0000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve - ---- - - - -**Description** - - - -**Steps to reproduce the issue:** -1. -2. -3. - -**Describe the results you received:** - -**Describe the results you expected:** - -**Output of `go version`:** - -```text -(paste your output here) -``` - -**Excelize version or commit ID:** - -```text -(paste here) -``` - -**Environment details (OS, Microsoft Excel™ version, physical, etc.):** diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000000..7005cd6c35 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,81 @@ +name: Bug report +description: Create a report to help us improve +body: + - type: markdown + attributes: + value: | + If you are reporting a new issue, make sure that we do not have any duplicates already open. You can ensure this by searching the issue list for this repository. If there is a duplicate, please close your issue and add a comment to the existing issue instead. + + - type: textarea + id: description + attributes: + label: Description + description: Briefly describe the problem you are having in a few paragraphs. + validations: + required: true + + - type: textarea + id: reproduction-steps + attributes: + label: Steps to reproduce the issue + description: Explain how to cause the issue in the provided reproduction. + placeholder: | + 1. + 2. + 3. + validations: + required: true + + - type: textarea + id: received + attributes: + label: Describe the results you received + validations: + required: true + + - type: textarea + id: expected + attributes: + label: Describe the results you expected + validations: + required: true + + - type: input + id: go-version + attributes: + label: Go version + description: | + Output of `go version`: + placeholder: e.g. 1.23.4 + validations: + required: true + + - type: input + id: excelize-version + attributes: + label: Excelize version or commit ID + description: | + Which version of Excelize are you using? + placeholder: e.g. 2.9.0 + validations: + required: true + + - type: textarea + id: env + attributes: + label: Environment + description: Environment details (OS, Microsoft Excel™ version, physical, etc.) + render: shell + validations: + required: true + + - type: checkboxes + id: checkboxes + attributes: + label: Validations + description: Before submitting the issue, please make sure you do the following + options: + - label: Check that there isn't already an issue that reports the same bug to avoid creating a duplicate. + required: true + - label: The provided reproduction is a minimal reproducible example of the bug. + required: true diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 1737cd4421..0000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project - ---- - - - -**Description** - - - -**Steps to reproduce the issue:** -1. -2. -3. - -**Describe the results you received:** - -**Describe the results you expected:** - -**Output of `go version`:** - -```text -(paste your output here) -``` - -**Excelize version or commit ID:** - -```text -(paste here) -``` - -**Environment details (OS, Microsoft Excel™ version, physical, etc.):** diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000000..af626b8e9a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,30 @@ +name: Feature request +description: Suggest an idea for this project +body: + - type: markdown + attributes: + value: | + If you are reporting a new issue, make sure that we do not have any duplicates already open. You can ensure this by searching the issue list for this repository. If there is a duplicate, please close your issue and add a comment to the existing issue instead. + + - type: textarea + id: description + attributes: + label: Description + description: Describe the feature that you would like added + validations: + required: true + + - type: textarea + id: additional-context + attributes: + label: Additional context + description: Any other context or screenshots about the feature request here? + + - type: checkboxes + id: checkboxes + attributes: + label: Validations + description: Before submitting the issue, please make sure you do the following + options: + - label: Check that there isn't already an issue that requests the same feature to avoid creating a duplicate. + required: true diff --git a/numfmt.go b/numfmt.go index 97419a49c5..d8a9937626 100644 --- a/numfmt.go +++ b/numfmt.go @@ -4661,7 +4661,7 @@ var ( } ) -// getSupportedLanguageInfo returns language infomation by giving language code. +// getSupportedLanguageInfo returns language information by giving language code. // This function does not support different calendar type of the language // currently. For example: the hexadecimal language code 3010429 (fa-IR,301) // will be convert to 0429 (fa-IR). diff --git a/styles.go b/styles.go index 3cab1e8a59..d8b25886ae 100644 --- a/styles.go +++ b/styles.go @@ -1441,8 +1441,8 @@ func (f *File) getThemeColor(clr *xlsxColor) string { func (f *File) extractBorders(bdr *xlsxBorder, s *xlsxStyleSheet, style *Style) { if bdr != nil { var borders []Border - extractBorder := func(lineType string, line xlsxLine) { - if line.Style != "" { + extractBorder := func(lineType string, line *xlsxLine) { + if line != nil && line.Style != "" { borders = append(borders, Border{ Type: lineType, Color: f.getThemeColor(line.Color), @@ -1450,7 +1450,7 @@ func (f *File) extractBorders(bdr *xlsxBorder, s *xlsxStyleSheet, style *Style) }) } } - for i, line := range []xlsxLine{ + for i, line := range []*xlsxLine{ bdr.Left, bdr.Right, bdr.Top, bdr.Bottom, bdr.Diagonal, bdr.Diagonal, } { if i < 4 { @@ -2128,29 +2128,20 @@ func newBorders(style *Style) *xlsxBorder { var border xlsxBorder for _, v := range style.Border { if 0 <= v.Style && v.Style < 14 { - var color xlsxColor - color.RGB = getPaletteColor(v.Color) + line := &xlsxLine{Style: styleBorders[v.Style], Color: &xlsxColor{RGB: getPaletteColor(v.Color)}} switch v.Type { case "left": - border.Left.Style = styleBorders[v.Style] - border.Left.Color = &color + border.Left = line case "right": - border.Right.Style = styleBorders[v.Style] - border.Right.Color = &color + border.Right = line case "top": - border.Top.Style = styleBorders[v.Style] - border.Top.Color = &color + border.Top = line case "bottom": - border.Bottom.Style = styleBorders[v.Style] - border.Bottom.Color = &color + border.Bottom = line case "diagonalUp": - border.Diagonal.Style = styleBorders[v.Style] - border.Diagonal.Color = &color - border.DiagonalUp = true + border.Diagonal, border.DiagonalUp = line, true case "diagonalDown": - border.Diagonal.Style = styleBorders[v.Style] - border.Diagonal.Color = &color - border.DiagonalDown = true + border.Diagonal, border.DiagonalDown = line, true } } } diff --git a/xmlStyles.go b/xmlStyles.go index c23388abfe..edbc996f1f 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -162,11 +162,11 @@ type xlsxBorder struct { DiagonalDown bool `xml:"diagonalDown,attr,omitempty"` DiagonalUp bool `xml:"diagonalUp,attr,omitempty"` Outline bool `xml:"outline,attr,omitempty"` - Left xlsxLine `xml:"left"` - Right xlsxLine `xml:"right"` - Top xlsxLine `xml:"top"` - Bottom xlsxLine `xml:"bottom"` - Diagonal xlsxLine `xml:"diagonal"` + Left *xlsxLine `xml:"left"` + Right *xlsxLine `xml:"right"` + Top *xlsxLine `xml:"top"` + Bottom *xlsxLine `xml:"bottom"` + Diagonal *xlsxLine `xml:"diagonal"` Vertical *xlsxLine `xml:"vertical"` Horizontal *xlsxLine `xml:"horizontal"` } From af422e17009b031012c01c90836bbf1dc18b7a61 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 5 Jan 2025 09:37:31 +0800 Subject: [PATCH 933/957] This closes #1954 and closes #2051, fix get pivot tables panic in some case --- pivotTable.go | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/pivotTable.go b/pivotTable.go index eece6d03dd..bfd4d40a74 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -899,14 +899,27 @@ func (f *File) getPivotTable(sheet, pivotTableXML, pivotCacheRels string) (Pivot opts.ShowLastColumn = si.ShowLastColumn opts.PivotTableStyleName = si.Name } - order, err := f.getTableFieldsOrder(&opts) - if err != nil { + if err = f.getPivotTableDataRange(&opts); err != nil { return opts, err } - f.extractPivotTableFields(order, pt, &opts) + f.extractPivotTableFields(pc.getPivotCacheFieldsName(), pt, &opts) return opts, err } +// getPivotCacheFieldsName returns pivot table fields name list by order from +// pivot cache fields. +func (pc *xlsxPivotCacheDefinition) getPivotCacheFieldsName() []string { + var order []string + if pc.CacheFields != nil { + for _, cf := range pc.CacheFields.CacheField { + if cf != nil { + order = append(order, cf.Name) + } + } + } + return order +} + // pivotTableReader provides a function to get the pointer to the structure // after deserialization of xl/pivotTables/pivotTable%d.xml. func (f *File) pivotTableReader(path string) (*xlsxPivotTableDefinition, error) { From e9efc47316198d7deecc519f1161d859d1d8787b Mon Sep 17 00:00:00 2001 From: ZhuHaiCheng Date: Tue, 14 Jan 2025 19:19:16 +0800 Subject: [PATCH 934/957] Correct comments of the internal function setSheetOutlineProps (#2066) --- sheetpr.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sheetpr.go b/sheetpr.go index 8f2d939331..619f6d8af8 100644 --- a/sheetpr.go +++ b/sheetpr.go @@ -87,7 +87,7 @@ func (ws *xlsxWorksheet) prepareSheetPr() { } } -// setSheetOutlinePr set worksheet outline properties by given options. +// setSheetOutlineProps set worksheet outline properties by given options. func (ws *xlsxWorksheet) setSheetOutlineProps(opts *SheetPropsOptions) { prepareOutlinePr := func(ws *xlsxWorksheet) { ws.prepareSheetPr() From b936343814c1ad075fd90e435cb469c15758cecb Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 21 Jan 2025 09:13:23 +0800 Subject: [PATCH 935/957] This closes #2068, breaking changes: SetCellInt function required int64 data type parameter - Update unit tests --- cell.go | 16 ++++++++-------- excelize_test.go | 6 +++--- sheet_test.go | 4 ++-- stream.go | 10 +++++----- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/cell.go b/cell.go index d642d82897..00892f428e 100644 --- a/cell.go +++ b/cell.go @@ -207,15 +207,15 @@ func (f *File) setCellIntFunc(sheet, cell string, value interface{}) error { var err error switch v := value.(type) { case int: - err = f.SetCellInt(sheet, cell, v) + err = f.SetCellInt(sheet, cell, int64(v)) case int8: - err = f.SetCellInt(sheet, cell, int(v)) + err = f.SetCellInt(sheet, cell, int64(v)) case int16: - err = f.SetCellInt(sheet, cell, int(v)) + err = f.SetCellInt(sheet, cell, int64(v)) case int32: - err = f.SetCellInt(sheet, cell, int(v)) + err = f.SetCellInt(sheet, cell, int64(v)) case int64: - err = f.SetCellInt(sheet, cell, int(v)) + err = f.SetCellInt(sheet, cell, v) case uint: err = f.SetCellUint(sheet, cell, uint64(v)) case uint8: @@ -288,7 +288,7 @@ 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 { +func (f *File) SetCellInt(sheet, cell string, value int64) error { f.mu.Lock() ws, err := f.workSheetReader(sheet) if err != nil { @@ -309,8 +309,8 @@ func (f *File) SetCellInt(sheet, cell string, value int) error { } // setCellInt prepares cell type and string type cell value by a given integer. -func setCellInt(value int) (t string, v string) { - v = strconv.Itoa(value) +func setCellInt(value int64) (t string, v string) { + v = strconv.FormatInt(value, 10) return } diff --git a/excelize_test.go b/excelize_test.go index 7416409a49..9684db2cd6 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -628,7 +628,7 @@ func TestWriteArrayFormula(t *testing.T) { valCell := cell(1, i+firstResLine) assocCell := cell(2, i+firstResLine) - assert.NoError(t, f.SetCellInt("Sheet1", valCell, values[i])) + assert.NoError(t, f.SetCellInt("Sheet1", valCell, int64(values[i]))) assert.NoError(t, f.SetCellStr("Sheet1", assocCell, sample[assoc[i]])) } @@ -642,8 +642,8 @@ func TestWriteArrayFormula(t *testing.T) { stdevCell := cell(i+2, 4) calcStdevCell := cell(i+2, 5) - assert.NoError(t, f.SetCellInt("Sheet1", calcAvgCell, average(i))) - assert.NoError(t, f.SetCellInt("Sheet1", calcStdevCell, stdev(i))) + assert.NoError(t, f.SetCellInt("Sheet1", calcAvgCell, int64(average(i)))) + assert.NoError(t, f.SetCellInt("Sheet1", calcStdevCell, int64(stdev(i)))) // Average can be done with AVERAGEIF assert.NoError(t, f.SetCellFormula("Sheet1", avgCell, fmt.Sprintf("ROUND(AVERAGEIF(%s,%s,%s),0)", assocRange, nameCell, valRange))) diff --git a/sheet_test.go b/sheet_test.go index 9bfed1a9c2..48bb423447 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -675,7 +675,7 @@ func BenchmarkNewSheet(b *testing.B) { func newSheetWithSet() { file := NewFile() for i := 0; i < 1000; i++ { - _ = file.SetCellInt("Sheet1", "A"+strconv.Itoa(i+1), i) + _ = file.SetCellInt("Sheet1", "A"+strconv.Itoa(i+1), int64(i)) } file = nil } @@ -691,7 +691,7 @@ func BenchmarkFile_SaveAs(b *testing.B) { func newSheetWithSave() { file := NewFile() for i := 0; i < 1000; i++ { - _ = file.SetCellInt("Sheet1", "A"+strconv.Itoa(i+1), i) + _ = file.SetCellInt("Sheet1", "A"+strconv.Itoa(i+1), int64(i)) } _ = file.Save() } diff --git a/stream.go b/stream.go index a745f0f582..130c8734f2 100644 --- a/stream.go +++ b/stream.go @@ -557,15 +557,15 @@ func (sw *StreamWriter) setCellValFunc(c *xlsxC, val interface{}) error { func setCellIntFunc(c *xlsxC, val interface{}) { switch val := val.(type) { case int: - c.T, c.V = setCellInt(val) + c.T, c.V = setCellInt(int64(val)) case int8: - c.T, c.V = setCellInt(int(val)) + c.T, c.V = setCellInt(int64(val)) case int16: - c.T, c.V = setCellInt(int(val)) + c.T, c.V = setCellInt(int64(val)) case int32: - c.T, c.V = setCellInt(int(val)) + c.T, c.V = setCellInt(int64(val)) case int64: - c.T, c.V = setCellInt(int(val)) + c.T, c.V = setCellInt(val) case uint: c.T, c.V = setCellUint(uint64(val)) case uint8: From a45d47d57ca7d4872e272330614fc7ae2eaad880 Mon Sep 17 00:00:00 2001 From: xxf0512 Date: Tue, 26 Nov 2024 22:58:19 +0800 Subject: [PATCH 936/957] Fix: int overflow of function --- stream_test.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/stream_test.go b/stream_test.go index 61387fe2e1..d72f188dfe 100644 --- a/stream_test.go +++ b/stream_test.go @@ -380,6 +380,29 @@ func TestStreamSetCellValFunc(t *testing.T) { } } +func TestSetCellIntFunc(t *testing.T) { + cases := []struct{ + val interface{} + target string + }{ + {val: 128, target: "128"}, + {val: int8(-128), target: "-128"}, + {val: int16(-32768), target: "-32768"}, + {val: int32(-2147483648), target: "-2147483648"}, + {val: int64(-9223372036854775808), target: "-9223372036854775808"}, + {val: uint(128), target: "128"}, + {val: uint8(255), target: "255"}, + {val: uint16(65535), target: "65535"}, + {val: uint32(4294967295), target: "4294967295"}, + {val: uint64(18446744073709551615), target: "18446744073709551615"}, + } + for _, c := range cases { + cell := &xlsxC{} + setCellIntFunc(cell, c.val) + assert.Equal(t, c.target, cell.V) + } +} + func TestStreamWriterOutlineLevel(t *testing.T) { file := NewFile() streamWriter, err := file.NewStreamWriter("Sheet1") From 7e614c510433a9b7411fd4a339dc015a4885be2c Mon Sep 17 00:00:00 2001 From: gypsy1234 Date: Wed, 22 Jan 2025 19:54:04 +0900 Subject: [PATCH 937/957] This closes #2072, support adjust data validations cross multiple worksheets (#2073) --- adjust.go | 2 +- adjust_test.go | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/adjust.go b/adjust.go index 6696056919..c0d4cdb9aa 100644 --- a/adjust.go +++ b/adjust.go @@ -969,7 +969,7 @@ func (f *File) adjustDataValidations(ws *xlsxWorksheet, sheet string, dir adjust return err } if worksheet.DataValidations == nil { - return nil + continue } for i := 0; i < len(worksheet.DataValidations.DataValidation); i++ { dv := worksheet.DataValidations.DataValidation[i] diff --git a/adjust_test.go b/adjust_test.go index fccfdf22b1..0acc8bf2eb 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -1170,6 +1170,25 @@ func TestAdjustDataValidations(t *testing.T) { assert.NoError(t, err) assert.Equal(t, formula, dvs[0].Formula1) }) + + t.Run("no_data_validations_on_first_sheet", func(t *testing.T) { + f := NewFile() + + // Add Sheet2 and set a data validation + _, err = f.NewSheet("Sheet2") + assert.NoError(t, err) + dv := NewDataValidation(true) + dv.Sqref = "C5:D6" + assert.NoError(t, f.AddDataValidation("Sheet2", dv)) + + // Adjust Sheet2 by removing a column + assert.NoError(t, f.RemoveCol("Sheet2", "A")) + + // Verify that data validations on Sheet2 are adjusted correctly + dvs, err = f.GetDataValidations("Sheet2") + assert.NoError(t, err) + assert.Equal(t, "B5:C6", dvs[0].Sqref) // Adjusted range + }) } func TestAdjustDrawings(t *testing.T) { From 0e0e2dadcf1a37c45b98b5adc8d3035c6db8b3c7 Mon Sep 17 00:00:00 2001 From: MengZhongYuan <33193572+mengpromax@users.noreply.github.com> Date: Fri, 24 Jan 2025 17:45:25 +0800 Subject: [PATCH 938/957] This close #2075, add new function SetColStyle for streaming writer to support set columns style (#2076) - Add new exported error variable ErrStreamSetColStyle - Fix cell default style doesn't override by none-zero row style when set row by stream writer - Update unit tests Signed-off-by: mengzhongyuan --- col.go | 34 +++++++++++++++-------- errors.go | 3 ++ stream.go | 74 ++++++++++++++++++++++++++++++++++++++------------ stream_test.go | 41 +++++++++++++++++++++++++--- 4 files changed, 119 insertions(+), 33 deletions(-) diff --git a/col.go b/col.go index 914ec2dd5f..365a68aeb5 100644 --- a/col.go +++ b/col.go @@ -450,6 +450,21 @@ func (f *File) SetColStyle(sheet, columns string, styleID int) error { } s.mu.Unlock() ws.mu.Lock() + ws.setColStyle(minVal, maxVal, styleID) + ws.mu.Unlock() + if rows := len(ws.SheetData.Row); rows > 0 { + for col := minVal; col <= maxVal; col++ { + from, _ := CoordinatesToCellName(col, 1) + to, _ := CoordinatesToCellName(col, rows) + err = f.SetCellStyle(sheet, from, to, styleID) + } + } + return err +} + +// setColStyle provides a function to set the style of a single column or +// multiple columns. +func (ws *xlsxWorksheet) setColStyle(minVal, maxVal, styleID int) { if ws.Cols == nil { ws.Cols = &xlsxCols{} } @@ -472,15 +487,6 @@ func (f *File) SetColStyle(sheet, columns string, styleID int) error { fc.Width = c.Width return fc }) - ws.mu.Unlock() - if rows := len(ws.SheetData.Row); rows > 0 { - for col := minVal; col <= maxVal; col++ { - from, _ := CoordinatesToCellName(col, 1) - to, _ := CoordinatesToCellName(col, rows) - err = f.SetCellStyle(sheet, from, to, styleID) - } - } - return err } // SetColWidth provides a function to set the width of a single column or @@ -504,6 +510,13 @@ func (f *File) SetColWidth(sheet, startCol, endCol string, width float64) error f.mu.Unlock() ws.mu.Lock() defer ws.mu.Unlock() + ws.setColWidth(minVal, maxVal, width) + return err +} + +// setColWidth provides a function to set the width of a single column or +// multiple columns. +func (ws *xlsxWorksheet) setColWidth(minVal, maxVal int, width float64) { col := xlsxCol{ Min: minVal, Max: maxVal, @@ -514,7 +527,7 @@ func (f *File) SetColWidth(sheet, startCol, endCol string, width float64) error cols := xlsxCols{} cols.Col = append(cols.Col, col) ws.Cols = &cols - return err + return } ws.Cols.Col = flatCols(col, ws.Cols.Col, func(fc, c xlsxCol) xlsxCol { fc.BestFit = c.BestFit @@ -525,7 +538,6 @@ func (f *File) SetColWidth(sheet, startCol, endCol string, width float64) error fc.Style = c.Style return fc }) - return err } // flatCols provides a method for the column's operation functions to flatten diff --git a/errors.go b/errors.go index 4f08b8435f..5cf55db4d5 100644 --- a/errors.go +++ b/errors.go @@ -135,6 +135,9 @@ var ( // ErrSparklineType defined the error message on receive the invalid // sparkline Type parameters. ErrSparklineType = errors.New("parameter 'Type' must be 'line', 'column' or 'win_loss'") + // ErrStreamSetColStyle defined the error message on set column style in + // stream writing mode. + ErrStreamSetColStyle = errors.New("must call the SetColStyle function before the SetRow function") // 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") diff --git a/stream.go b/stream.go index 130c8734f2..245a7bc3e1 100644 --- a/stream.go +++ b/stream.go @@ -29,7 +29,6 @@ type StreamWriter struct { Sheet string SheetID int sheetWritten bool - cols strings.Builder worksheet *xlsxWorksheet rawData bufferedWriter rows int @@ -413,16 +412,18 @@ func (sw *StreamWriter) SetRow(cell string, values []interface{}, opts ...RowOpt if err != nil { return err } - c := xlsxC{R: ref, S: options.StyleID} + c := xlsxC{R: ref, S: sw.worksheet.prepareCellStyle(col, row, options.StyleID)} + var s int if v, ok := val.(Cell); ok { - c.S = v.StyleID - val = v.Value + s, val = v.StyleID, v.Value setCellFormula(&c, v.Formula) } else if v, ok := val.(*Cell); ok && v != nil { - c.S = v.StyleID - val = v.Value + s, val = v.StyleID, v.Value setCellFormula(&c, v.Formula) } + if s > 0 { + c.S = s + } if err = sw.setCellValFunc(&c, val); err != nil { _, _ = sw.rawData.WriteString(``) return err @@ -433,6 +434,33 @@ func (sw *StreamWriter) SetRow(cell string, values []interface{}, opts ...RowOpt return sw.rawData.Sync() } +// SetColStyle provides a function to set the style of a single column or +// multiple columns for the StreamWriter. Note that you must call +// the 'SetColStyle' function before the 'SetRow' function. For example set +// style of column H on Sheet1: +// +// err := sw.SetColStyle(8, 8, style) +func (sw *StreamWriter) SetColStyle(minVal, maxVal, styleID int) error { + if sw.sheetWritten { + return ErrStreamSetColStyle + } + if minVal < MinColumns || minVal > MaxColumns || maxVal < MinColumns || maxVal > MaxColumns { + return ErrColumnNumber + } + if maxVal < minVal { + minVal, maxVal = maxVal, minVal + } + s, err := sw.file.stylesReader() + if err != nil { + return err + } + if styleID < 0 || s.CellXfs == nil || len(s.CellXfs.Xf) <= styleID { + return newInvalidStyleID(styleID) + } + sw.worksheet.setColStyle(minVal, maxVal, styleID) + return nil +} + // 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 @@ -452,14 +480,7 @@ func (sw *StreamWriter) SetColWidth(minVal, maxVal int, width float64) error { if minVal > maxVal { minVal, maxVal = maxVal, minVal } - - sw.cols.WriteString(``) + sw.worksheet.setColWidth(minVal, maxVal, width) return nil } @@ -642,10 +663,27 @@ func writeCell(buf *bufferedWriter, c xlsxC) { func (sw *StreamWriter) writeSheetData() { if !sw.sheetWritten { bulkAppendFields(&sw.rawData, sw.worksheet, 4, 5) - if sw.cols.Len() > 0 { - _, _ = sw.rawData.WriteString("") - _, _ = sw.rawData.WriteString(sw.cols.String()) - _, _ = sw.rawData.WriteString("") + if sw.worksheet.Cols != nil { + for _, col := range sw.worksheet.Cols.Col { + _, _ = sw.rawData.WriteString("") + sw.rawData.WriteString(``) + _, _ = sw.rawData.WriteString("") + } } _, _ = sw.rawData.WriteString(``) sw.sheetWritten = true diff --git a/stream_test.go b/stream_test.go index d72f188dfe..9d64b59144 100644 --- a/stream_test.go +++ b/stream_test.go @@ -154,19 +154,53 @@ func TestStreamWriter(t *testing.T) { assert.NoError(t, file.Close()) } +func TestStreamSetColStyle(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.SetColStyle(3, 2, 0)) + assert.Equal(t, ErrColumnNumber, streamWriter.SetColStyle(0, 3, 20)) + assert.Equal(t, ErrColumnNumber, streamWriter.SetColStyle(MaxColumns+1, 3, 20)) + assert.Equal(t, newInvalidStyleID(2), streamWriter.SetColStyle(1, 3, 2)) + assert.NoError(t, streamWriter.SetRow("A1", []interface{}{"A", "B", "C"})) + assert.Equal(t, ErrStreamSetColStyle, streamWriter.SetColStyle(2, 3, 0)) + + file = NewFile() + defer func() { + assert.NoError(t, file.Close()) + }() + // Test set column style with unsupported charset style sheet + file.Styles = nil + file.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset) + streamWriter, err = file.NewStreamWriter("Sheet1") + assert.NoError(t, err) + assert.EqualError(t, streamWriter.SetColStyle(3, 2, 0), "XML syntax error on line 1: invalid UTF-8") +} + func TestStreamSetColWidth(t *testing.T) { file := NewFile() defer func() { assert.NoError(t, file.Close()) }() + styleID, err := file.NewStyle(&Style{ + Fill: Fill{Type: "pattern", Color: []string{"E0EBF5"}, Pattern: 1}, + }) + if err != nil { + fmt.Println(err) + } streamWriter, err := file.NewStreamWriter("Sheet1") assert.NoError(t, err) assert.NoError(t, streamWriter.SetColWidth(3, 2, 20)) + assert.NoError(t, streamWriter.SetColStyle(3, 2, styleID)) assert.Equal(t, ErrColumnNumber, streamWriter.SetColWidth(0, 3, 20)) assert.Equal(t, ErrColumnNumber, streamWriter.SetColWidth(MaxColumns+1, 3, 20)) assert.Equal(t, ErrColumnWidth, streamWriter.SetColWidth(1, 3, MaxColumnWidth+1)) assert.NoError(t, streamWriter.SetRow("A1", []interface{}{"A", "B", "C"})) assert.Equal(t, ErrStreamSetColWidth, streamWriter.SetColWidth(2, 3, 20)) + assert.NoError(t, streamWriter.Flush()) } func TestStreamSetPanes(t *testing.T) { @@ -323,7 +357,6 @@ func TestStreamSetRowWithStyle(t *testing.T) { defer func() { assert.NoError(t, file.Close()) }() - zeroStyleID := 0 grayStyleID, err := file.NewStyle(&Style{Font: &Font{Color: "777777"}}) assert.NoError(t, err) blueStyleID, err := file.NewStyle(&Style{Font: &Font{Color: "0000FF"}}) @@ -342,7 +375,7 @@ func TestStreamSetRowWithStyle(t *testing.T) { ws, err := file.workSheetReader("Sheet1") assert.NoError(t, err) - for colIdx, expected := range []int{grayStyleID, zeroStyleID, zeroStyleID, blueStyleID, blueStyleID} { + for colIdx, expected := range []int{grayStyleID, grayStyleID, grayStyleID, blueStyleID, blueStyleID} { assert.Equal(t, expected, ws.SheetData.Row[0].C[colIdx].S) } } @@ -381,8 +414,8 @@ func TestStreamSetCellValFunc(t *testing.T) { } func TestSetCellIntFunc(t *testing.T) { - cases := []struct{ - val interface{} + cases := []struct { + val interface{} target string }{ {val: 128, target: "128"}, From b19e5940b84eb10f6b16642aea91c618f702fea2 Mon Sep 17 00:00:00 2001 From: hly-717 <2546626370@qq.com> Date: Sat, 25 Jan 2025 10:01:27 +0800 Subject: [PATCH 939/957] Fix GetStyle function can not get VertAlign format (#2079) - Fix redundant cols element generated by stream writer - Update dependencies module - Update docs for the GetCellRichText function - Move TestSetCellIntFunc function to cell_test.go --- cell.go | 23 +++++++++++++---------- cell_test.go | 46 +++++++++++++++++++++++++++++++++++++--------- go.mod | 6 +++--- go.sum | 12 ++++++------ stream.go | 4 ++-- stream_test.go | 23 ----------------------- 6 files changed, 61 insertions(+), 53 deletions(-) diff --git a/cell.go b/cell.go index 00892f428e..fe204d66cb 100644 --- a/cell.go +++ b/cell.go @@ -1101,7 +1101,7 @@ func getCellRichText(si *xlsxSI) (runs []RichTextRun) { } // GetCellRichText provides a function to get rich text of cell by given -// worksheet. +// worksheet and cell reference. func (f *File) GetCellRichText(sheet, cell string) (runs []RichTextRun, err error) { ws, err := f.workSheetReader(sheet) if err != nil { @@ -1164,7 +1164,7 @@ func newRpr(fnt *Font) *xlsxRPr { // newFont create font format by given run properties for the rich text. func newFont(rPr *xlsxRPr) *Font { - font := Font{Underline: "none"} + var font Font font.Bold = rPr.B != nil font.Italic = rPr.I != nil if rPr.U != nil { @@ -1179,6 +1179,9 @@ func newFont(rPr *xlsxRPr) *Font { if rPr.Sz != nil && rPr.Sz.Val != nil { font.Size = *rPr.Sz.Val } + if rPr.VertAlign != nil && rPr.VertAlign.Val != nil { + font.VertAlign = *rPr.VertAlign.Val + } font.Strike = rPr.Strike != nil if rPr.Color != nil { font.Color = strings.TrimPrefix(rPr.Color.RGB, "FF") @@ -1245,7 +1248,7 @@ func setRichText(runs []RichTextRun) ([]xlsxR, error) { // Text: "bold", // Font: &excelize.Font{ // Bold: true, -// Color: "2354e8", +// Color: "2354E8", // Family: "Times New Roman", // }, // }, @@ -1259,7 +1262,7 @@ func setRichText(runs []RichTextRun) ([]xlsxR, error) { // Text: "italic ", // Font: &excelize.Font{ // Bold: true, -// Color: "e83723", +// Color: "E83723", // Italic: true, // Family: "Times New Roman", // }, @@ -1268,7 +1271,7 @@ func setRichText(runs []RichTextRun) ([]xlsxR, error) { // Text: "text with color and font-family,", // Font: &excelize.Font{ // Bold: true, -// Color: "2354e8", +// Color: "2354E8", // Family: "Times New Roman", // }, // }, @@ -1276,20 +1279,20 @@ func setRichText(runs []RichTextRun) ([]xlsxR, error) { // Text: "\r\nlarge text with ", // Font: &excelize.Font{ // Size: 14, -// Color: "ad23e8", +// Color: "AD23E8", // }, // }, // { // Text: "strike", // Font: &excelize.Font{ -// Color: "e89923", +// Color: "E89923", // Strike: true, // }, // }, // { // Text: " superscript", // Font: &excelize.Font{ -// Color: "dbc21f", +// Color: "DBC21F", // VertAlign: "superscript", // }, // }, @@ -1297,14 +1300,14 @@ func setRichText(runs []RichTextRun) ([]xlsxR, error) { // Text: " and ", // Font: &excelize.Font{ // Size: 14, -// Color: "ad23e8", +// Color: "AD23E8", // VertAlign: "baseline", // }, // }, // { // Text: "underline", // Font: &excelize.Font{ -// Color: "23e833", +// Color: "23E833", // Underline: "single", // }, // }, diff --git a/cell_test.go b/cell_test.go index 5590a3682a..ba051a46b9 100644 --- a/cell_test.go +++ b/cell_test.go @@ -856,7 +856,7 @@ func TestSetCellRichText(t *testing.T) { Text: "bold", Font: &Font{ Bold: true, - Color: "2354e8", + Color: "2354E8", ColorIndexed: 0, Family: "Times New Roman", }, @@ -871,7 +871,7 @@ func TestSetCellRichText(t *testing.T) { Text: "italic ", Font: &Font{ Bold: true, - Color: "e83723", + Color: "E83723", Italic: true, Family: "Times New Roman", }, @@ -880,7 +880,7 @@ func TestSetCellRichText(t *testing.T) { Text: "text with color and font-family, ", Font: &Font{ Bold: true, - Color: "2354e8", + Color: "2354E8", Family: "Times New Roman", }, }, @@ -888,20 +888,20 @@ func TestSetCellRichText(t *testing.T) { Text: "\r\nlarge text with ", Font: &Font{ Size: 14, - Color: "ad23e8", + Color: "AD23E8", }, }, { Text: "strike", Font: &Font{ - Color: "e89923", + Color: "E89923", Strike: true, }, }, { Text: " superscript", Font: &Font{ - Color: "dbc21f", + Color: "DBC21F", VertAlign: "superscript", }, }, @@ -909,14 +909,14 @@ func TestSetCellRichText(t *testing.T) { Text: " and ", Font: &Font{ Size: 14, - Color: "ad23e8", - VertAlign: "BASELINE", + Color: "AD23E8", + VertAlign: "baseline", }, }, { Text: "underline", Font: &Font{ - Color: "23e833", + Color: "23E833", Underline: "single", }, }, @@ -937,6 +937,11 @@ func TestSetCellRichText(t *testing.T) { }) assert.NoError(t, err) assert.NoError(t, f.SetCellStyle("Sheet1", "A1", "A1", style)) + + runs, err := f.GetCellRichText("Sheet1", "A1") + assert.NoError(t, err) + assert.Equal(t, richTextRun, runs) + 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") @@ -1153,6 +1158,29 @@ func TestSharedStringsError(t *testing.T) { }) } +func TestSetCellIntFunc(t *testing.T) { + cases := []struct { + val interface{} + target string + }{ + {val: 128, target: "128"}, + {val: int8(-128), target: "-128"}, + {val: int16(-32768), target: "-32768"}, + {val: int32(-2147483648), target: "-2147483648"}, + {val: int64(-9223372036854775808), target: "-9223372036854775808"}, + {val: uint(128), target: "128"}, + {val: uint8(255), target: "255"}, + {val: uint16(65535), target: "65535"}, + {val: uint32(4294967295), target: "4294967295"}, + {val: uint64(18446744073709551615), target: "18446744073709551615"}, + } + for _, c := range cases { + cell := &xlsxC{} + setCellIntFunc(cell, c.val) + assert.Equal(t, c.target, cell.V) + } +} + func TestSIString(t *testing.T) { assert.Empty(t, xlsxSI{}.String()) } diff --git a/go.mod b/go.mod index a732a05547..3c36460069 100644 --- a/go.mod +++ b/go.mod @@ -7,10 +7,10 @@ require ( github.com/stretchr/testify v1.9.0 github.com/tiendc/go-deepcopy v1.2.0 github.com/xuri/efp v0.0.0-20241211021726-c4e992084aa6 - github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 - golang.org/x/crypto v0.31.0 + github.com/xuri/nfp v0.0.0-20250111060730-82a408b9aa71 + golang.org/x/crypto v0.32.0 golang.org/x/image v0.18.0 - golang.org/x/net v0.33.0 + golang.org/x/net v0.34.0 golang.org/x/text v0.21.0 ) diff --git a/go.sum b/go.sum index 40e347c144..222a04df31 100644 --- a/go.sum +++ b/go.sum @@ -13,14 +13,14 @@ github.com/tiendc/go-deepcopy v1.2.0 h1:6vCCs+qdLQHzFqY1fcPirsAWOmrLbuccilfp8UzD github.com/tiendc/go-deepcopy v1.2.0/go.mod h1:toXoeQoUqXOOS/X4sKuiAoSk6elIdqc0pN7MTgOOo2I= github.com/xuri/efp v0.0.0-20241211021726-c4e992084aa6 h1:8m6DWBG+dlFNbx5ynvrE7NgI+Y7OlZVMVTpayoW+rCc= github.com/xuri/efp v0.0.0-20241211021726-c4e992084aa6/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= -github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A= -github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +github.com/xuri/nfp v0.0.0-20250111060730-82a408b9aa71 h1:hOh7aVDrvGJRxzXrQbDY8E+02oaI//5cHL+97oYpEPw= +github.com/xuri/nfp v0.0.0-20250111060730-82a408b9aa71/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= -golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= -golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/stream.go b/stream.go index 245a7bc3e1..d5e5f0544e 100644 --- a/stream.go +++ b/stream.go @@ -664,8 +664,8 @@ func (sw *StreamWriter) writeSheetData() { if !sw.sheetWritten { bulkAppendFields(&sw.rawData, sw.worksheet, 4, 5) if sw.worksheet.Cols != nil { + _, _ = sw.rawData.WriteString("") for _, col := range sw.worksheet.Cols.Col { - _, _ = sw.rawData.WriteString("") sw.rawData.WriteString(``) - _, _ = sw.rawData.WriteString("") } + _, _ = sw.rawData.WriteString("") } _, _ = sw.rawData.WriteString(``) sw.sheetWritten = true diff --git a/stream_test.go b/stream_test.go index 9d64b59144..a2eea18391 100644 --- a/stream_test.go +++ b/stream_test.go @@ -413,29 +413,6 @@ func TestStreamSetCellValFunc(t *testing.T) { } } -func TestSetCellIntFunc(t *testing.T) { - cases := []struct { - val interface{} - target string - }{ - {val: 128, target: "128"}, - {val: int8(-128), target: "-128"}, - {val: int16(-32768), target: "-32768"}, - {val: int32(-2147483648), target: "-2147483648"}, - {val: int64(-9223372036854775808), target: "-9223372036854775808"}, - {val: uint(128), target: "128"}, - {val: uint8(255), target: "255"}, - {val: uint16(65535), target: "65535"}, - {val: uint32(4294967295), target: "4294967295"}, - {val: uint64(18446744073709551615), target: "18446744073709551615"}, - } - for _, c := range cases { - cell := &xlsxC{} - setCellIntFunc(cell, c.val) - assert.Equal(t, c.target, cell.V) - } -} - func TestStreamWriterOutlineLevel(t *testing.T) { file := NewFile() streamWriter, err := file.NewStreamWriter("Sheet1") From 432462292a40ea72005193735b2f82b287654571 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 26 Jan 2025 19:53:30 +0800 Subject: [PATCH 940/957] This closes #2058, support apply number format with hash and zero place holder - Update unit tests - Disable blank issue creation --- .github/ISSUE_TEMPLATE/config.yml | 1 + numfmt.go | 50 +++++++++++++++++++++---------- numfmt_test.go | 7 +++-- 3 files changed, 39 insertions(+), 19 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/config.yml diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..3ba13e0cec --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false diff --git a/numfmt.go b/numfmt.go index d8a9937626..878ec99974 100644 --- a/numfmt.go +++ b/numfmt.go @@ -4962,37 +4962,54 @@ func (nf *numberFormat) getNumberFmtConf() { } } +// handleDigitsLiteral apply hash and zero place holder tokens for the number +// literal. +func handleDigitsLiteral(text string, tokenValueLen, intPartLen, hashZeroPartLen int) (int, string) { + var result string + l := tokenValueLen + if intPartLen == 0 && len(text) > hashZeroPartLen { + l = len(text) + tokenValueLen - hashZeroPartLen + } + if len(text) < hashZeroPartLen { + intPartLen += len(text) - hashZeroPartLen + } + for i := 0; i < l; i++ { + j := i + intPartLen + if 0 <= j && j < len([]rune(text)) { + result += string([]rune(text)[j]) + } + } + return l, result +} + // printNumberLiteral apply literal tokens for the pre-formatted text. func (nf *numberFormat) printNumberLiteral(text string) string { var ( - result string - frac float64 - useFraction, useLiteral, usePlaceHolder bool + result string + frac float64 + useFraction bool + intPartLen, hashZeroPartLen int ) if nf.usePositive { result += "-" } + for _, token := range nf.section[nf.sectionIdx].Items { + if token.TType == nfp.TokenTypeHashPlaceHolder || token.TType == nfp.TokenTypeZeroPlaceHolder { + hashZeroPartLen += len(token.TValue) + } + } for _, token := range nf.section[nf.sectionIdx].Items { if token.TType == nfp.TokenTypeCurrencyLanguage { - if changeNumFmtCode, err := nf.currencyLanguageHandler(token); err != nil || changeNumFmtCode { - return nf.value - } + _, _ = nf.currencyLanguageHandler(token) result += nf.currencyString } if token.TType == nfp.TokenTypeLiteral { - if usePlaceHolder { - useLiteral = true - } result += token.TValue } if token.TType == nfp.TokenTypeHashPlaceHolder || token.TType == nfp.TokenTypeZeroPlaceHolder { - if useLiteral && usePlaceHolder { - return nf.value - } - if !usePlaceHolder { - usePlaceHolder = true - result += text - } + digits, str := handleDigitsLiteral(text, len(token.TValue), intPartLen, hashZeroPartLen) + intPartLen += digits + result += str } if token.TType == nfp.TokenTypeFraction { _, frac = math.Modf(nf.number) @@ -5257,6 +5274,7 @@ func (nf *numberFormat) currencyLanguageHandler(token nfp.Token) (bool, error) { } if part.Token.TType == nfp.TokenSubTypeCurrencyString { nf.currencyString = part.Token.TValue + return false, nil } } return false, nil diff --git a/numfmt_test.go b/numfmt_test.go index 158104755e..05971d3dc5 100644 --- a/numfmt_test.go +++ b/numfmt_test.go @@ -3631,10 +3631,12 @@ func TestNumFmt(t *testing.T) { {"-123.4567", "#\\ ?/100", "-123 46/100"}, {"123.4567", "#\\ ?/1000", "123 457/1000"}, {"1234.5678", "[$$-409]#,##0.00", "$1,234.57"}, + {"123", "[$x.-unknown]#,##0.00", "x.123.00"}, + {"123", "[$x.-unknown]MM/DD/YYYY", "x.05/02/1900"}, + {"1234.5678", "0.0xxx00", "1234.5xxx68"}, + {"80145.899999999994", "[$¥-8004]\" \"#\" \"####\"\"", "¥ 8 0146"}, // Unsupported number format {"37947.7500001", "0.00000000E+000", "37947.7500001"}, - {"123", "[$x.-unknown]#,##0.00", "123"}, - {"123", "[$x.-unknown]MM/DD/YYYY", "123"}, {"123", "[DBNum4][$-804]yyyy\"年\"m\"月\";@", "123"}, // Invalid number format {"123", "x0.00s", "123"}, @@ -3646,7 +3648,6 @@ func TestNumFmt(t *testing.T) { {"-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, nil) From 05ed940040c1d5d1961f19a03dcc9c2c616d6f10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kurt=20Inge=20Sm=C3=A5dal?= Date: Fri, 7 Feb 2025 02:01:27 +0100 Subject: [PATCH 941/957] Handle the format '?' in number formatting (#2080) --- numfmt.go | 2 +- numfmt_test.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/numfmt.go b/numfmt.go index 878ec99974..36fc331689 100644 --- a/numfmt.go +++ b/numfmt.go @@ -5006,7 +5006,7 @@ func (nf *numberFormat) printNumberLiteral(text string) string { if token.TType == nfp.TokenTypeLiteral { result += token.TValue } - if token.TType == nfp.TokenTypeHashPlaceHolder || token.TType == nfp.TokenTypeZeroPlaceHolder { + if token.TType == nfp.TokenTypeHashPlaceHolder || token.TType == nfp.TokenTypeZeroPlaceHolder || token.TType == nfp.TokenTypeDigitalPlaceHolder { digits, str := handleDigitsLiteral(text, len(token.TValue), intPartLen, hashZeroPartLen) intPartLen += digits result += str diff --git a/numfmt_test.go b/numfmt_test.go index 05971d3dc5..5380c1bc89 100644 --- a/numfmt_test.go +++ b/numfmt_test.go @@ -3635,6 +3635,7 @@ func TestNumFmt(t *testing.T) { {"123", "[$x.-unknown]MM/DD/YYYY", "x.05/02/1900"}, {"1234.5678", "0.0xxx00", "1234.5xxx68"}, {"80145.899999999994", "[$¥-8004]\" \"#\" \"####\"\"", "¥ 8 0146"}, + {"1", "?", "1"}, // Unsupported number format {"37947.7500001", "0.00000000E+000", "37947.7500001"}, {"123", "[DBNum4][$-804]yyyy\"年\"m\"月\";@", "123"}, From 718417e15ed6ede2ab7a732bbb032c9c3304d957 Mon Sep 17 00:00:00 2001 From: Ivan Hristov <35896427+IvanHristov98@users.noreply.github.com> Date: Sat, 8 Feb 2025 03:40:36 +0200 Subject: [PATCH 942/957] This closes #2083, fix subexpression calculation (#2084) Co-authored-by: Ivan Hristov --- calc.go | 9 +-------- calc_test.go | 3 +++ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/calc.go b/calc.go index 71d4bd3b1c..937c9677b8 100644 --- a/calc.go +++ b/calc.go @@ -1008,13 +1008,6 @@ func (f *File) evalInfixExp(ctx *calcContext, sheet, cell string, tokens []efp.T } } - if isEndParenthesesToken(token) && isBeginParenthesesToken(opftStack.Peek().(efp.Token)) { - if arg := argsStack.Peek().(*list.List).Back(); arg != nil { - opfdStack.Push(arg.Value.(formulaArg)) - argsStack.Peek().(*list.List).Remove(arg) - } - } - // check current token is opft if err = f.parseToken(ctx, sheet, token, opfdStack, opftStack); err != nil { return newEmptyFormulaArg(), err @@ -1085,7 +1078,7 @@ func (f *File) evalInfixExpFunc(ctx *calcContext, sheet, cell string, token, nex opftStack.Pop() // remove current function separator opfStack.Pop() if opfStack.Len() > 0 { // still in function stack - if nextToken.TType == efp.TokenTypeOperatorInfix || (opftStack.Len() > 1 && opfdStack.Len() > 0) { + if nextToken.TType == efp.TokenTypeOperatorInfix || opftStack.Len() > 1 { // mathematics calculate in formula function opfdStack.Push(arg) return newEmptyFormulaArg() diff --git a/calc_test.go b/calc_test.go index fc28a05955..7581203d96 100644 --- a/calc_test.go +++ b/calc_test.go @@ -895,6 +895,9 @@ 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", + "=IF(2<0, 1, (4))": "4", + "=IF(2>0, (1), 4)": "1", + "=IF(2>0, (A1)*2.5, 4)": "2.5", "=SUM({1,2,3,4,\"\"})": "10", // SUMIF "=SUMIF(F1:F5, \"\")": "0", From 52642854413f0ff528792db55b145bea88dbeec0 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 19 Feb 2025 14:12:59 +0800 Subject: [PATCH 943/957] This closes #2091, update GitHub Actions workflow configuration, test on Go 1.24.x - Update dependencies modules --- .github/workflows/go.yml | 2 +- go.mod | 8 ++++---- go.sum | 16 ++++++++-------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index ae467b2f36..67e47968cd 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -5,7 +5,7 @@ jobs: test: strategy: matrix: - go-version: [1.20.x, '>=1.21.1', 1.22.x, 1.23.x] + go-version: [1.20.x, '>=1.21.1', 1.22.x, 1.23.x, 1.24.x] os: [ubuntu-24.04, macos-13, windows-latest] targetplatform: [x86, x64] diff --git a/go.mod b/go.mod index 3c36460069..1947685606 100644 --- a/go.mod +++ b/go.mod @@ -5,13 +5,13 @@ go 1.20 require ( github.com/richardlehane/mscfb v1.0.4 github.com/stretchr/testify v1.9.0 - github.com/tiendc/go-deepcopy v1.2.0 + github.com/tiendc/go-deepcopy v1.5.0 github.com/xuri/efp v0.0.0-20241211021726-c4e992084aa6 github.com/xuri/nfp v0.0.0-20250111060730-82a408b9aa71 - golang.org/x/crypto v0.32.0 + golang.org/x/crypto v0.33.0 golang.org/x/image v0.18.0 - golang.org/x/net v0.34.0 - golang.org/x/text v0.21.0 + golang.org/x/net v0.35.0 + golang.org/x/text v0.22.0 ) require ( diff --git a/go.sum b/go.sum index 222a04df31..777b03ccc6 100644 --- a/go.sum +++ b/go.sum @@ -9,20 +9,20 @@ github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/tiendc/go-deepcopy v1.2.0 h1:6vCCs+qdLQHzFqY1fcPirsAWOmrLbuccilfp8UzD1Qo= -github.com/tiendc/go-deepcopy v1.2.0/go.mod h1:toXoeQoUqXOOS/X4sKuiAoSk6elIdqc0pN7MTgOOo2I= +github.com/tiendc/go-deepcopy v1.5.0 h1:TbtS9hclrKZcF1AHby8evDm4mIQU36i6tSYuvx/TstY= +github.com/tiendc/go-deepcopy v1.5.0/go.mod h1:toXoeQoUqXOOS/X4sKuiAoSk6elIdqc0pN7MTgOOo2I= github.com/xuri/efp v0.0.0-20241211021726-c4e992084aa6 h1:8m6DWBG+dlFNbx5ynvrE7NgI+Y7OlZVMVTpayoW+rCc= github.com/xuri/efp v0.0.0-20241211021726-c4e992084aa6/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= github.com/xuri/nfp v0.0.0-20250111060730-82a408b9aa71 h1:hOh7aVDrvGJRxzXrQbDY8E+02oaI//5cHL+97oYpEPw= github.com/xuri/nfp v0.0.0-20250111060730-82a408b9aa71/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= -golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= -golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= -golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= -golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= 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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= From c6d161fc76e6165cc95693a1c50696dac7faa74b Mon Sep 17 00:00:00 2001 From: Artur Chopikian Date: Sun, 2 Mar 2025 05:24:49 +0200 Subject: [PATCH 944/957] Reduce trim cell value memory allocation for blank cells (#2096) --- cell.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cell.go b/cell.go index fe204d66cb..35f64854c8 100644 --- a/cell.go +++ b/cell.go @@ -510,14 +510,14 @@ func trimCellValue(value string, escape bool) (v string, ns xml.Attr) { if utf8.RuneCountInString(value) > TotalCellChars { value = string([]rune(value)[:TotalCellChars]) } - if escape { + if escape && value != "" { var buf bytes.Buffer enc := xml.NewEncoder(&buf) _ = enc.EncodeToken(xml.CharData(value)) - enc.Flush() + _ = enc.Flush() value = buf.String() } - if len(value) > 0 { + if value != "" { prefix, suffix := value[0], value[len(value)-1] for _, ascii := range []byte{9, 10, 13, 32} { if prefix == ascii || suffix == ascii { From aef20e226cba5cab0fc9331c63f34f563cd24ebc Mon Sep 17 00:00:00 2001 From: Roman Shevelev Date: Tue, 4 Mar 2025 18:20:24 +0500 Subject: [PATCH 945/957] Introduce 2 new functions SetCalcProps and GetCalcProps (#2098) - Add new CalcPropsOptions data type - Support assign exported data structure fields value to internal data structure fields dynamically by specified fields name list - Simplify code for function SetAppProps, SetDocProps, SetWorkbookProps, GetWorkbookProps and getPivotTable - Update unit tests --- docProps.go | 57 +++++++----------------------------- lib.go | 49 +++++++++++++++++++++++++++++++ pivotTable.go | 25 ++++------------ workbook.go | 76 ++++++++++++++++++++++++++++++++++++++---------- workbook_test.go | 36 +++++++++++++++++++++++ xmlWorkbook.go | 20 ++++++++++++- 6 files changed, 182 insertions(+), 81 deletions(-) diff --git a/docProps.go b/docProps.go index 4f0eef94c9..b4f3cfa4c4 100644 --- a/docProps.go +++ b/docProps.go @@ -65,34 +65,15 @@ import ( // AppVersion: "16.0000", // }) func (f *File) SetAppProps(appProperties *AppProperties) error { - var ( - app *xlsxProperties - err error - field string - fields []string - immutable, mutable reflect.Value - output []byte - ) - app = new(xlsxProperties) - if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathDocPropsApp)))). + app := new(xlsxProperties) + if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathDocPropsApp)))). Decode(app); err != nil && err != io.EOF { return err } - fields = []string{"Application", "ScaleCrop", "DocSecurity", "Company", "LinksUpToDate", "HyperlinksChanged", "AppVersion"} - immutable, mutable = reflect.ValueOf(*appProperties), reflect.ValueOf(app).Elem() - for _, field = range fields { - immutableField := immutable.FieldByName(field) - switch immutableField.Kind() { - case reflect.Bool: - mutable.FieldByName(field).SetBool(immutableField.Bool()) - case reflect.Int: - mutable.FieldByName(field).SetInt(immutableField.Int()) - default: - mutable.FieldByName(field).SetString(immutableField.String()) - } - } + setNoPtrFieldsVal([]string{"Application", "ScaleCrop", "DocSecurity", "Company", "LinksUpToDate", "HyperlinksChanged", "AppVersion"}, + reflect.ValueOf(*appProperties), reflect.ValueOf(app).Elem()) app.Vt = NameSpaceDocumentPropertiesVariantTypes.Value - output, err = xml.Marshal(app) + output, err := xml.Marshal(app) f.saveFileList(defaultXMLPathDocPropsApp, output) return err } @@ -180,22 +161,12 @@ func (f *File) GetAppProps() (ret *AppProperties, err error) { // Version: "1.0.0", // }) func (f *File) SetDocProps(docProperties *DocProperties) error { - var ( - core *decodeCoreProperties - err error - field, val string - fields []string - immutable, mutable reflect.Value - newProps *xlsxCoreProperties - output []byte - ) - - core = new(decodeCoreProperties) - if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathDocPropsCore)))). + core := new(decodeCoreProperties) + if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathDocPropsCore)))). Decode(core); err != nil && err != io.EOF { return err } - newProps = &xlsxCoreProperties{ + newProps := &xlsxCoreProperties{ Dc: NameSpaceDublinCore, Dcterms: NameSpaceDublinCoreTerms, Dcmitype: NameSpaceDublinCoreMetadataInitiative, @@ -219,23 +190,17 @@ func (f *File) SetDocProps(docProperties *DocProperties) error { if core.Modified != nil { newProps.Modified = &xlsxDcTerms{Type: core.Modified.Type, Text: core.Modified.Text} } - fields = []string{ + setNoPtrFieldsVal([]string{ "Category", "ContentStatus", "Creator", "Description", "Identifier", "Keywords", "LastModifiedBy", "Revision", "Subject", "Title", "Language", "Version", - } - immutable, mutable = reflect.ValueOf(*docProperties), reflect.ValueOf(newProps).Elem() - for _, field = range fields { - if val = immutable.FieldByName(field).String(); val != "" { - mutable.FieldByName(field).SetString(val) - } - } + }, reflect.ValueOf(*docProperties), reflect.ValueOf(newProps).Elem()) if docProperties.Created != "" { newProps.Created = &xlsxDcTerms{Type: "dcterms:W3CDTF", Text: docProperties.Created} } if docProperties.Modified != "" { newProps.Modified = &xlsxDcTerms{Type: "dcterms:W3CDTF", Text: docProperties.Modified} } - output, err = xml.Marshal(newProps) + output, err := xml.Marshal(newProps) f.saveFileList(defaultXMLPathDocPropsCore, output) return err diff --git a/lib.go b/lib.go index bfb2ae2f93..207cd839bd 100644 --- a/lib.go +++ b/lib.go @@ -21,6 +21,7 @@ import ( "math" "math/big" "os" + "reflect" "regexp" "strconv" "strings" @@ -878,6 +879,54 @@ func continuedFraction(n float64, i int64, limit int64, prec float64) *big.Rat { return res } +// assignFieldValue assigns the value from an immutable reflect.Value to a +// mutable reflect.Value based on the type of the immutable value. +func assignFieldValue(field string, immutable, mutable reflect.Value) { + switch immutable.Kind() { + case reflect.Bool: + mutable.FieldByName(field).SetBool(immutable.Bool()) + case reflect.Int: + mutable.FieldByName(field).SetInt(immutable.Int()) + default: + mutable.FieldByName(field).SetString(immutable.String()) + } +} + +// setNoPtrFieldsVal assigns values from the pointer or no-pointer structs +// fields (immutable) value to no-pointer struct field. +func setNoPtrFieldsVal(fields []string, immutable, mutable reflect.Value) { + for _, field := range fields { + immutableField := immutable.FieldByName(field) + if immutableField.Kind() == reflect.Ptr { + if immutableField.IsValid() && !immutableField.IsNil() { + assignFieldValue(field, immutableField.Elem(), mutable) + } + continue + } + assignFieldValue(field, immutableField, mutable) + } +} + +// setPtrFieldsVal assigns values from the pointer or no-pointer structs +// fields (immutable) value to pointer struct field. +func setPtrFieldsVal(fields []string, immutable, mutable reflect.Value) { + for _, field := range fields { + immutableField := immutable.FieldByName(field) + if immutableField.Kind() == reflect.Ptr { + if immutableField.IsValid() && !immutableField.IsNil() { + mutable.FieldByName(field).Set(immutableField.Elem()) + } + continue + } + if immutableField.IsZero() { + continue + } + ptr := reflect.New(immutableField.Type()) + ptr.Elem().Set(immutableField) + mutable.FieldByName(field).Set(ptr) + } +} + // Stack defined an abstract data type that serves as a collection of elements. type Stack struct { list *list.List diff --git a/pivotTable.go b/pivotTable.go index bfd4d40a74..c71ca9ffcd 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -883,14 +883,10 @@ func (f *File) getPivotTable(sheet, pivotTableXML, pivotCacheRels string) (Pivot opts.DataRange = pc.CacheSource.WorksheetSource.Name _ = f.getPivotTableDataRange(&opts) } - fields := []string{"RowGrandTotals", "ColGrandTotals", "ShowDrill", "UseAutoFormatting", "PageOverThenDown", "MergeItem", "CompactData", "ShowError"} - immutable, mutable := reflect.ValueOf(*pt), reflect.ValueOf(&opts).Elem() - for _, field := range fields { - immutableField := immutable.FieldByName(field) - if immutableField.Kind() == reflect.Ptr && !immutableField.IsNil() && immutableField.Elem().Kind() == reflect.Bool { - mutable.FieldByName(field).SetBool(immutableField.Elem().Bool()) - } - } + setPtrFieldsVal([]string{ + "RowGrandTotals", "ColGrandTotals", "ShowDrill", + "UseAutoFormatting", "PageOverThenDown", "MergeItem", "CompactData", "ShowError", + }, reflect.ValueOf(*pt), reflect.ValueOf(&opts).Elem()) if si := pt.PivotTableStyleInfo; si != nil { opts.ShowRowHeaders = si.ShowRowHeaders opts.ShowColHeaders = si.ShowColHeaders @@ -982,17 +978,8 @@ func extractPivotTableField(data string, fld *xlsxPivotField) PivotTableField { ShowAll: fld.ShowAll, InsertBlankRow: fld.InsertBlankRow, } - fields := []string{"Compact", "Name", "Outline", "Subtotal", "DefaultSubtotal"} - immutable, mutable := reflect.ValueOf(*fld), reflect.ValueOf(&pivotTableField).Elem() - for _, field := range fields { - immutableField := immutable.FieldByName(field) - if immutableField.Kind() == reflect.String { - mutable.FieldByName(field).SetString(immutableField.String()) - } - if immutableField.Kind() == reflect.Ptr && !immutableField.IsNil() && immutableField.Elem().Kind() == reflect.Bool { - mutable.FieldByName(field).SetBool(immutableField.Elem().Bool()) - } - } + setPtrFieldsVal([]string{"Compact", "Name", "Outline", "DefaultSubtotal"}, + reflect.ValueOf(*fld), reflect.ValueOf(&pivotTableField).Elem()) return pivotTableField } diff --git a/workbook.go b/workbook.go index c40e9578b3..c908241583 100644 --- a/workbook.go +++ b/workbook.go @@ -16,12 +16,16 @@ import ( "encoding/xml" "io" "path/filepath" + "reflect" "strconv" "strings" ) // SetWorkbookProps provides a function to sets workbook properties. func (f *File) SetWorkbookProps(opts *WorkbookPropsOptions) error { + if opts == nil { + return nil + } wb, err := f.workbookReader() if err != nil { return err @@ -29,33 +33,75 @@ func (f *File) SetWorkbookProps(opts *WorkbookPropsOptions) error { if wb.WorkbookPr == nil { wb.WorkbookPr = new(xlsxWorkbookPr) } + setNoPtrFieldsVal([]string{"Date1904", "FilterPrivacy", "CodeName"}, + reflect.ValueOf(*opts), reflect.ValueOf(wb.WorkbookPr).Elem()) + return err +} + +// GetWorkbookProps provides a function to gets workbook properties. +func (f *File) GetWorkbookProps() (WorkbookPropsOptions, error) { + var opts WorkbookPropsOptions + wb, err := f.workbookReader() + if err != nil { + return opts, err + } + if wb.WorkbookPr == nil { + return opts, err + } + setPtrFieldsVal([]string{"Date1904", "FilterPrivacy", "CodeName"}, + reflect.ValueOf(*wb.WorkbookPr), reflect.ValueOf(&opts).Elem()) + return opts, err +} + +// SetCalcProps provides a function to sets calculation properties. +func (f *File) SetCalcProps(opts *CalcPropsOptions) error { if opts == nil { return nil } - if opts.Date1904 != nil { - wb.WorkbookPr.Date1904 = *opts.Date1904 + wb, err := f.workbookReader() + if err != nil { + return err + } + if wb.CalcPr == nil { + wb.CalcPr = new(xlsxCalcPr) } - if opts.FilterPrivacy != nil { - wb.WorkbookPr.FilterPrivacy = *opts.FilterPrivacy + setNoPtrFieldsVal([]string{ + "CalcCompleted", "CalcOnSave", "ForceFullCalc", "FullCalcOnLoad", "FullPrecision", "Iterate", + "IterateDelta", + "CalcMode", "RefMode", + }, reflect.ValueOf(*opts), reflect.ValueOf(wb.CalcPr).Elem()) + if opts.CalcID != nil { + wb.CalcPr.CalcID = int(*opts.CalcID) } - if opts.CodeName != nil { - wb.WorkbookPr.CodeName = *opts.CodeName + if opts.ConcurrentManualCount != nil { + wb.CalcPr.ConcurrentManualCount = int(*opts.ConcurrentManualCount) } - return nil + if opts.IterateCount != nil { + wb.CalcPr.IterateCount = int(*opts.IterateCount) + } + wb.CalcPr.ConcurrentCalc = opts.ConcurrentCalc + return err } -// GetWorkbookProps provides a function to gets workbook properties. -func (f *File) GetWorkbookProps() (WorkbookPropsOptions, error) { - var opts WorkbookPropsOptions +// GetCalcProps provides a function to gets calculation properties. +func (f *File) GetCalcProps() (CalcPropsOptions, error) { + var opts CalcPropsOptions 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) + if wb.CalcPr == nil { + return opts, err } + setPtrFieldsVal([]string{ + "CalcCompleted", "CalcOnSave", "ForceFullCalc", "FullCalcOnLoad", "FullPrecision", "Iterate", + "IterateDelta", + "CalcMode", "RefMode", + }, reflect.ValueOf(*wb.CalcPr), reflect.ValueOf(&opts).Elem()) + opts.CalcID = uintPtr(uint(wb.CalcPr.CalcID)) + opts.ConcurrentManualCount = uintPtr(uint(wb.CalcPr.ConcurrentManualCount)) + opts.IterateCount = uintPtr(uint(wb.CalcPr.IterateCount)) + opts.ConcurrentCalc = wb.CalcPr.ConcurrentCalc return opts, err } @@ -99,7 +145,7 @@ func (f *File) ProtectWorkbook(opts *WorkbookProtectionOptions) error { wb.WorkbookProtection.WorkbookHashValue = hashValue wb.WorkbookProtection.WorkbookSpinCount = int(workbookProtectionSpinCount) } - return nil + return err } // UnprotectWorkbook provides a function to remove protection for workbook, diff --git a/workbook_test.go b/workbook_test.go index 73dda6cef5..2e3baac8c6 100644 --- a/workbook_test.go +++ b/workbook_test.go @@ -21,6 +21,10 @@ func TestWorkbookProps(t *testing.T) { opts, err := f.GetWorkbookProps() assert.NoError(t, err) assert.Equal(t, expected, opts) + wb.WorkbookPr = nil + opts, err = f.GetWorkbookProps() + assert.NoError(t, err) + assert.Equal(t, WorkbookPropsOptions{}, opts) // Test set workbook properties with unsupported charset workbook f.WorkBook = nil f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset) @@ -32,6 +36,38 @@ func TestWorkbookProps(t *testing.T) { assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") } +func TestCalcProps(t *testing.T) { + f := NewFile() + assert.NoError(t, f.SetCalcProps(nil)) + wb, err := f.workbookReader() + assert.NoError(t, err) + wb.CalcPr = nil + expected := CalcPropsOptions{ + FullCalcOnLoad: boolPtr(true), + CalcID: uintPtr(122211), + ConcurrentManualCount: uintPtr(5), + IterateCount: uintPtr(10), + ConcurrentCalc: boolPtr(true), + } + assert.NoError(t, f.SetCalcProps(&expected)) + opts, err := f.GetCalcProps() + assert.NoError(t, err) + assert.Equal(t, expected, opts) + wb.CalcPr = nil + opts, err = f.GetCalcProps() + assert.NoError(t, err) + assert.Equal(t, CalcPropsOptions{}, opts) + // Test set workbook properties with unsupported charset workbook + f.WorkBook = nil + f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset) + assert.EqualError(t, f.SetCalcProps(&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.GetCalcProps() + assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") +} + func TestDeleteWorkbookRels(t *testing.T) { f := NewFile() // Test delete pivot table without worksheet relationships diff --git a/xmlWorkbook.go b/xmlWorkbook.go index c36485a614..6a6b5017eb 100644 --- a/xmlWorkbook.go +++ b/xmlWorkbook.go @@ -315,7 +315,7 @@ type xlsxDefinedName struct { // displaying the results as values in the cells that contain the formulas. type xlsxCalcPr struct { CalcCompleted bool `xml:"calcCompleted,attr,omitempty"` - CalcID string `xml:"calcId,attr,omitempty"` + CalcID int `xml:"calcId,attr,omitempty"` CalcMode string `xml:"calcMode,attr,omitempty"` CalcOnSave bool `xml:"calcOnSave,attr,omitempty"` ConcurrentCalc *bool `xml:"concurrentCalc,attr"` @@ -384,6 +384,24 @@ type DefinedName struct { Scope string } +// CalcPropsOptions defines the collection of properties the application uses to +// record calculation status and details. +type CalcPropsOptions struct { + CalcID *uint `xml:"calcId,attr"` + CalcMode *string `xml:"calcMode,attr"` + FullCalcOnLoad *bool `xml:"fullCalcOnLoad,attr"` + RefMode *string `xml:"refMode,attr"` + Iterate *bool `xml:"iterate,attr"` + IterateCount *uint `xml:"iterateCount,attr"` + IterateDelta *float64 `xml:"iterateDelta,attr"` + FullPrecision *bool `xml:"fullPrecision,attr"` + CalcCompleted *bool `xml:"calcCompleted,attr"` + CalcOnSave *bool `xml:"calcOnSave,attr"` + ConcurrentCalc *bool `xml:"concurrentCalc,attr"` + ConcurrentManualCount *uint `xml:"concurrentManualCount,attr"` + ForceFullCalc *bool `xml:"forceFullCalc,attr"` +} + // WorkbookPropsOptions directly maps the settings of workbook proprieties. type WorkbookPropsOptions struct { Date1904 *bool From d6931a8f3e8dc950af5cd3b00ed5a3039e0eb608 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 6 Mar 2025 19:47:10 +0800 Subject: [PATCH 946/957] Add optional value checking for CalcMode and RefMode calculation properties - Add valid optional value prompt in error message for the functions: `AddFormControl`, `AddPicture` and `AddPictureFromBytes` - Rename the internal error message function `newInvalidPageLayoutValueError` to `newInvalidOptionalValue` - Update unit tests --- errors.go | 9 +++++---- picture.go | 2 +- picture_test.go | 3 ++- sheet.go | 4 ++-- templates.go | 6 ++++++ vml.go | 2 +- vml_test.go | 9 +++++---- workbook.go | 10 +++++++++- workbook_test.go | 7 +++++-- 9 files changed, 36 insertions(+), 16 deletions(-) diff --git a/errors.go b/errors.go index 5cf55db4d5..80338b1994 100644 --- a/errors.go +++ b/errors.go @@ -14,6 +14,7 @@ package excelize import ( "errors" "fmt" + "strings" ) var ( @@ -255,10 +256,10 @@ 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) } -// newInvalidPageLayoutValueError defined the error message on receiving the invalid -// page layout options value. -func newInvalidPageLayoutValueError(name, value, msg string) error { - return fmt.Errorf("invalid %s value %q, acceptable value should be one of %s", name, value, msg) +// newInvalidOptionalValue defined the error message on receiving the invalid +// optional value. +func newInvalidOptionalValue(name, value string, values []string) error { + return fmt.Errorf("invalid %s value %q, acceptable value should be one of %s", name, value, strings.Join(values, ", ")) } // newInvalidRowNumberError defined the error message on receiving the invalid diff --git a/picture.go b/picture.go index 3980df32cf..704021fe8f 100644 --- a/picture.go +++ b/picture.go @@ -355,7 +355,7 @@ func (f *File) addDrawingPicture(sheet, drawingXML, cell, ext string, rID, hyper return err } if opts.Positioning != "" && inStrSlice(supportedPositioning, opts.Positioning, true) == -1 { - return ErrParameterInvalid + return newInvalidOptionalValue("Positioning", opts.Positioning, supportedPositioning) } width, height := img.Width, img.Height if opts.AutoFit { diff --git a/picture_test.go b/picture_test.go index 2f5d4773c6..c0c9075583 100644 --- a/picture_test.go +++ b/picture_test.go @@ -290,7 +290,8 @@ func TestAddDrawingPicture(t *testing.T) { 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()) // Test addDrawingPicture with invalid positioning types - assert.Equal(t, f.addDrawingPicture("sheet1", "", "A1", "", 0, 0, image.Config{}, &GraphicOptions{Positioning: "x"}), ErrParameterInvalid) + assert.Equal(t, newInvalidOptionalValue("Positioning", "x", supportedPositioning), + f.addDrawingPicture("sheet1", "", "A1", "", 0, 0, image.Config{}, &GraphicOptions{Positioning: "x"})) path := "xl/drawings/drawing1.xml" f.Pkg.Store(path, MacintoshCyrillicCharset) diff --git a/sheet.go b/sheet.go index b5ba9a2515..abd7f52a02 100644 --- a/sheet.go +++ b/sheet.go @@ -1646,7 +1646,7 @@ func (ws *xlsxWorksheet) setPageSetUp(opts *PageLayoutOptions) error { } if opts.Orientation != nil { if inStrSlice(supportedPageOrientation, *opts.Orientation, true) == -1 { - return newInvalidPageLayoutValueError("Orientation", *opts.Orientation, strings.Join(supportedPageOrientation, ", ")) + return newInvalidOptionalValue("Orientation", *opts.Orientation, supportedPageOrientation) } ws.newPageSetUp() ws.PageSetUp.Orientation = *opts.Orientation @@ -1677,7 +1677,7 @@ func (ws *xlsxWorksheet) setPageSetUp(opts *PageLayoutOptions) error { } if opts.PageOrder != nil { if inStrSlice(supportedPageOrder, *opts.PageOrder, true) == -1 { - return newInvalidPageLayoutValueError("PageOrder", *opts.PageOrder, strings.Join(supportedPageOrder, ", ")) + return newInvalidOptionalValue("PageOrder", *opts.PageOrder, supportedPageOrder) } ws.newPageSetUp() ws.PageSetUp.PageOrder = *opts.PageOrder diff --git a/templates.go b/templates.go index 7f64e154f1..f27b8defb6 100644 --- a/templates.go +++ b/templates.go @@ -476,6 +476,12 @@ var supportedImageTypes = map[string]string{ ".tif": ".tiff", ".tiff": ".tiff", ".wmf": ".wmf", ".wmz": ".wmz", } +// supportedCalcMode defined supported formula calculate mode. +var supportedCalcMode = []string{"manual", "auto", "autoNoTable"} + +// supportedRefMode defined supported formula calculate mode. +var supportedRefMode = []string{"A1", "R1C1"} + // supportedContentTypes defined supported file format types. var supportedContentTypes = map[string]string{ ".xlam": ContentTypeAddinMacro, diff --git a/vml.go b/vml.go index 437275a1c3..5923bee28e 100644 --- a/vml.go +++ b/vml.go @@ -809,7 +809,7 @@ func (f *File) addFormCtrlShape(preset formCtrlPreset, col, row int, anchor stri if opts.Format.Positioning != "" { idx := inStrSlice(supportedPositioning, opts.Format.Positioning, true) if idx == -1 { - return &sp, ErrParameterInvalid + return &sp, newInvalidOptionalValue("Positioning", opts.Format.Positioning, supportedPositioning) } sp.ClientData.MoveWithCells = []*string{stringPtr(""), nil, nil}[idx] sp.ClientData.SizeWithCells = []*string{stringPtr(""), stringPtr(""), nil}[idx] diff --git a/vml_test.go b/vml_test.go index 7ec29e94ea..d6fed05f7d 100644 --- a/vml_test.go +++ b/vml_test.go @@ -271,10 +271,11 @@ func TestFormControl(t *testing.T) { Cell: "A1", Type: FormControlButton, Macro: "Button1_Click", })) // Test add form control with invalid positioning types - assert.Equal(t, f.AddFormControl("Sheet1", FormControl{ - Cell: "A1", Type: FormControlButton, - Format: GraphicOptions{Positioning: "x"}, - }), ErrParameterInvalid) + assert.Equal(t, newInvalidOptionalValue("Positioning", "x", supportedPositioning), + f.AddFormControl("Sheet1", FormControl{ + Cell: "A1", Type: FormControlButton, + Format: GraphicOptions{Positioning: "x"}, + })) // Test add spin form control with illegal cell link reference assert.Equal(t, f.AddFormControl("Sheet1", FormControl{ Cell: "C5", Type: FormControlSpinButton, CellLink: "*", diff --git a/workbook.go b/workbook.go index c908241583..7df97febf6 100644 --- a/workbook.go +++ b/workbook.go @@ -53,7 +53,9 @@ func (f *File) GetWorkbookProps() (WorkbookPropsOptions, error) { return opts, err } -// SetCalcProps provides a function to sets calculation properties. +// SetCalcProps provides a function to sets calculation properties. Optional +// value of "CalcMode" property is: "manual", "auto" or "autoNoTable". Optional +// value of "RefMode" property is: "A1" or "R1C1". func (f *File) SetCalcProps(opts *CalcPropsOptions) error { if opts == nil { return nil @@ -65,6 +67,12 @@ func (f *File) SetCalcProps(opts *CalcPropsOptions) error { if wb.CalcPr == nil { wb.CalcPr = new(xlsxCalcPr) } + if opts.CalcMode != nil && inStrSlice(supportedCalcMode, *opts.CalcMode, true) == -1 { + return newInvalidOptionalValue("CalcMode", *opts.CalcMode, supportedCalcMode) + } + if opts.RefMode != nil && inStrSlice(supportedRefMode, *opts.RefMode, true) == -1 { + return newInvalidOptionalValue("RefMode", *opts.RefMode, supportedRefMode) + } setNoPtrFieldsVal([]string{ "CalcCompleted", "CalcOnSave", "ForceFullCalc", "FullCalcOnLoad", "FullPrecision", "Iterate", "IterateDelta", diff --git a/workbook_test.go b/workbook_test.go index 2e3baac8c6..75f137fc2d 100644 --- a/workbook_test.go +++ b/workbook_test.go @@ -57,11 +57,14 @@ func TestCalcProps(t *testing.T) { opts, err = f.GetCalcProps() assert.NoError(t, err) assert.Equal(t, CalcPropsOptions{}, opts) - // Test set workbook properties with unsupported charset workbook + // Test set calculation properties with unsupported optional value + assert.Equal(t, newInvalidOptionalValue("CalcMode", "AUTO", supportedCalcMode), f.SetCalcProps(&CalcPropsOptions{CalcMode: stringPtr("AUTO")})) + assert.Equal(t, newInvalidOptionalValue("RefMode", "a1", supportedRefMode), f.SetCalcProps(&CalcPropsOptions{RefMode: stringPtr("a1")})) + // Test set calculation properties with unsupported charset workbook f.WorkBook = nil f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset) assert.EqualError(t, f.SetCalcProps(&expected), "XML syntax error on line 1: invalid UTF-8") - // Test get workbook properties with unsupported charset workbook + // Test get calculation properties with unsupported charset workbook f.WorkBook = nil f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset) _, err = f.GetCalcProps() From 2bb89f4bd971421cb608b779c84c6e8908f7873a Mon Sep 17 00:00:00 2001 From: LZCZ <74762395+LZCZ@users.noreply.github.com> Date: Sun, 16 Mar 2025 14:00:30 +0000 Subject: [PATCH 947/957] This closes #2102, fix panic on set chart title font (#2103) - Update unit tests --- chart_test.go | 2 +- drawing.go | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/chart_test.go b/chart_test.go index 1527bc71e6..42e2a65764 100644 --- a/chart_test.go +++ b/chart_test.go @@ -219,7 +219,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: []RichTextRun{{Text: "2D Column Chart"}}, PlotArea: plotArea, Border: ChartLine{Type: ChartLineNone}, ShowBlanksAs: "zero", XAxis: ChartAxis{Font: Font{Bold: true, Italic: true, Underline: "dbl", Family: "Times New Roman", Size: 15, Strike: true, 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: "P1", opts: &Chart{Type: Col, Series: series, Format: format, Legend: ChartLegend{Position: "none", ShowLegendKey: true}, Title: []RichTextRun{{Text: "2D Column Chart", Font: &Font{Size: 11, Family: "Calibri"}}}, PlotArea: plotArea, Border: ChartLine{Type: ChartLineNone}, ShowBlanksAs: "zero", XAxis: ChartAxis{Font: Font{Bold: true, Italic: true, Underline: "dbl", Family: "Times New Roman", Size: 15, Strike: true, 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: []RichTextRun{{Text: "2D Stacked Column Chart"}}, PlotArea: plotArea, Fill: Fill{Type: "pattern", Pattern: 1}, Border: ChartLine{Type: ChartLineAutomatic}, ShowBlanksAs: "zero", GapWidth: uintPtr(10), Overlap: intPtr(100)}}, {sheetName: "Sheet1", cell: "P16", opts: &Chart{Type: ColPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "100% Stacked Column Chart"}}, PlotArea: plotArea, Fill: Fill{Type: "pattern", Color: []string{"EEEEEE"}, Pattern: 1}, Border: ChartLine{Type: ChartLineSolid, Width: 2}, ShowBlanksAs: "zero", XAxis: ChartAxis{Alignment: Alignment{Vertical: "wordArtVertRtl", TextRotation: 0}}}}, {sheetName: "Sheet1", cell: "X16", opts: &Chart{Type: Col3DClustered, Series: series, Format: format, Legend: ChartLegend{Position: "bottom", ShowLegendKey: false}, Title: []RichTextRun{{Text: "3D Clustered Column Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, diff --git a/drawing.go b/drawing.go index 7241dac1f8..8c336f2907 100644 --- a/drawing.go +++ b/drawing.go @@ -1198,6 +1198,9 @@ func drawChartFont(fnt *Font, r *aRPr) { r.SolidFill.SrgbClr = &attrValString{Val: stringPtr(strings.ReplaceAll(strings.ToUpper(fnt.Color), "#", ""))} } if fnt.Family != "" { + if r.Latin == nil { + r.Latin = &xlsxCTTextFont{} + } r.Latin.Typeface = fnt.Family } if fnt.Size > 0 { From d399e7bc3ed839b09208ebcc0c69ded51b19a00a Mon Sep 17 00:00:00 2001 From: hm3248 Date: Sun, 16 Mar 2025 12:04:03 -0400 Subject: [PATCH 948/957] Breaking changes: Go 1.23 and later required for upgrade of dependency package `golang.org/x/crypto` (#2104) - Fix panic on delete calc chain - Remove XML tags for the CalcPropsOptions data structure - Update dependencies modules - Update the godoc --- .github/workflows/go.yml | 2 +- README.md | 2 +- README_zh.md | 2 +- adjust.go | 4 ++-- calc.go | 2 +- calcchain.go | 2 +- cell.go | 2 +- chart.go | 2 +- col.go | 2 +- crypt.go | 2 +- crypt_test.go | 2 +- datavalidation.go | 2 +- datavalidation_test.go | 2 +- date.go | 2 +- docProps.go | 2 +- docProps_test.go | 2 +- drawing.go | 2 +- drawing_test.go | 2 +- errors.go | 2 +- excelize.go | 2 +- file.go | 2 +- go.mod | 16 ++++++++-------- go.sum | 28 ++++++++++++++-------------- lib.go | 2 +- merge.go | 2 +- numfmt.go | 2 +- picture.go | 2 +- pivotTable.go | 2 +- rows.go | 2 +- shape.go | 2 +- sheet.go | 2 +- sheetpr.go | 2 +- sheetview.go | 2 +- slicer.go | 2 +- sparkline.go | 2 +- stream.go | 2 +- styles.go | 2 +- table.go | 2 +- templates.go | 2 +- vml.go | 2 +- vmlDrawing.go | 2 +- vml_test.go | 2 +- workbook.go | 2 +- xmlApp.go | 2 +- xmlCalcChain.go | 2 +- xmlChart.go | 2 +- xmlChartSheet.go | 2 +- xmlComments.go | 2 +- xmlContentTypes.go | 2 +- xmlCore.go | 2 +- xmlDecodeDrawing.go | 2 +- xmlDrawing.go | 2 +- xmlMetaData.go | 2 +- xmlPivotCache.go | 2 +- xmlPivotTable.go | 2 +- xmlSharedStrings.go | 2 +- xmlSlicers.go | 2 +- xmlStyles.go | 2 +- xmlTable.go | 2 +- xmlTheme.go | 2 +- xmlWorkbook.go | 28 ++++++++++++++-------------- xmlWorksheet.go | 2 +- 62 files changed, 96 insertions(+), 96 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 67e47968cd..5e16cfccae 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -5,7 +5,7 @@ jobs: test: strategy: matrix: - go-version: [1.20.x, '>=1.21.1', 1.22.x, 1.23.x, 1.24.x] + go-version: [1.23.x, 1.24.x] os: [ubuntu-24.04, macos-13, windows-latest] targetplatform: [x86, x64] diff --git a/README.md b/README.md index e862d6a3df..9f57747ba7 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.20 or later. There are some [incompatible changes](https://github.com/golang/go/issues/61881) in the Go 1.21.0, the Excelize library can not working with that version normally, if you are using the Go 1.21.x, please upgrade to the Go 1.21.1 and later version. 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/). +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.23 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 a2bc62123d..fab0bdd7bc 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 等多种文档格式,高度兼容带有样式、图片(表)、透视表、切片器等复杂组件的文档,并提供流式读写函数,用于处理包含大规模数据的工作簿。可应用于各类报表平台、云计算、边缘计算等系统。使用本类库要求使用的 Go 语言为 1.20 或更高版本,请注意,Go 1.21.0 中存在[不兼容的更改](https://github.com/golang/go/issues/61881),导致 Excelize 基础库无法在该版本上正常工作,如果您使用的是 Go 1.21.x,请升级到 Go 1.21.1 及更高版本。完整的使用文档请访问 [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.23 或更高版本。完整的使用文档请访问 [go.dev](https://pkg.go.dev/github.com/xuri/excelize/v2) 或查看 [参考文档](https://xuri.me/excelize/)。 ## 快速上手 diff --git a/adjust.go b/adjust.go index c0d4cdb9aa..28e54e5809 100644 --- a/adjust.go +++ b/adjust.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize @@ -847,7 +847,7 @@ func (f *File) adjustCalcChain(ws *xlsxWorksheet, sheet string, dir adjustDirect // If sheet ID is omitted, it is assumed to be the same as the i value of // the previous cell. var prevSheetID int - for i := 0; i < len(f.CalcChain.C); i++ { + for i := 0; f.CalcChain != nil && i < len(f.CalcChain.C); i++ { c := f.CalcChain.C[i] if c.I == 0 { c.I = prevSheetID diff --git a/calc.go b/calc.go index 937c9677b8..ba51590b3f 100644 --- a/calc.go +++ b/calc.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize diff --git a/calcchain.go b/calcchain.go index 9e68aba00d..edd917d371 100644 --- a/calcchain.go +++ b/calcchain.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize diff --git a/cell.go b/cell.go index 35f64854c8..b710cc7138 100644 --- a/cell.go +++ b/cell.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize diff --git a/chart.go b/chart.go index 1448c71ed2..88df5c2f21 100644 --- a/chart.go +++ b/chart.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize diff --git a/col.go b/col.go index 365a68aeb5..6608048a83 100644 --- a/col.go +++ b/col.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize diff --git a/crypt.go b/crypt.go index bce4835def..27f5c8502d 100644 --- a/crypt.go +++ b/crypt.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize diff --git a/crypt_test.go b/crypt_test.go index 1dbb34c9aa..1e57f608ed 100644 --- a/crypt_test.go +++ b/crypt_test.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize diff --git a/datavalidation.go b/datavalidation.go index 37bdbaf04c..ab61931625 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize diff --git a/datavalidation_test.go b/datavalidation_test.go index 2e10936f78..a5d2becaf3 100644 --- a/datavalidation_test.go +++ b/datavalidation_test.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize diff --git a/date.go b/date.go index 103daa5bdb..7d8757f1ee 100644 --- a/date.go +++ b/date.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize diff --git a/docProps.go b/docProps.go index b4f3cfa4c4..975f51b0a2 100644 --- a/docProps.go +++ b/docProps.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize diff --git a/docProps_test.go b/docProps_test.go index 22a325b24e..dc26e2b285 100644 --- a/docProps_test.go +++ b/docProps_test.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize diff --git a/drawing.go b/drawing.go index 8c336f2907..c029fdf7d3 100644 --- a/drawing.go +++ b/drawing.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize diff --git a/drawing_test.go b/drawing_test.go index 41b3050d92..9160824062 100644 --- a/drawing_test.go +++ b/drawing_test.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize diff --git a/errors.go b/errors.go index 80338b1994..409f22d563 100644 --- a/errors.go +++ b/errors.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize diff --git a/excelize.go b/excelize.go index 4de1ac1104..8448999ab2 100644 --- a/excelize.go +++ b/excelize.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. // // See https://xuri.me/excelize for more information about this package. package excelize diff --git a/file.go b/file.go index 2f0c5cc2c7..aa0816c9c2 100644 --- a/file.go +++ b/file.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize diff --git a/go.mod b/go.mod index 1947685606..296407c2ae 100644 --- a/go.mod +++ b/go.mod @@ -1,17 +1,17 @@ module github.com/xuri/excelize/v2 -go 1.20 +go 1.23.0 require ( github.com/richardlehane/mscfb v1.0.4 - github.com/stretchr/testify v1.9.0 + github.com/stretchr/testify v1.10.0 github.com/tiendc/go-deepcopy v1.5.0 - github.com/xuri/efp v0.0.0-20241211021726-c4e992084aa6 - github.com/xuri/nfp v0.0.0-20250111060730-82a408b9aa71 - golang.org/x/crypto v0.33.0 - golang.org/x/image v0.18.0 - golang.org/x/net v0.35.0 - golang.org/x/text v0.22.0 + github.com/xuri/efp v0.0.0-20250227110027-3491fafc2b79 + github.com/xuri/nfp v0.0.0-20250226145837-86d5fc24b2ba + golang.org/x/crypto v0.36.0 + golang.org/x/image v0.25.0 + golang.org/x/net v0.37.0 + golang.org/x/text v0.23.0 ) require ( diff --git a/go.sum b/go.sum index 777b03ccc6..82f3630523 100644 --- a/go.sum +++ b/go.sum @@ -7,22 +7,22 @@ github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7 github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00= github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tiendc/go-deepcopy v1.5.0 h1:TbtS9hclrKZcF1AHby8evDm4mIQU36i6tSYuvx/TstY= github.com/tiendc/go-deepcopy v1.5.0/go.mod h1:toXoeQoUqXOOS/X4sKuiAoSk6elIdqc0pN7MTgOOo2I= -github.com/xuri/efp v0.0.0-20241211021726-c4e992084aa6 h1:8m6DWBG+dlFNbx5ynvrE7NgI+Y7OlZVMVTpayoW+rCc= -github.com/xuri/efp v0.0.0-20241211021726-c4e992084aa6/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= -github.com/xuri/nfp v0.0.0-20250111060730-82a408b9aa71 h1:hOh7aVDrvGJRxzXrQbDY8E+02oaI//5cHL+97oYpEPw= -github.com/xuri/nfp v0.0.0-20250111060730-82a408b9aa71/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= -golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= -golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= -golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= -golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= -golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= -golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= -golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= -golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +github.com/xuri/efp v0.0.0-20250227110027-3491fafc2b79 h1:78nKszZqigiBRBVcoe/AuPzyLTWW5B+ltBaUX1rlIXA= +github.com/xuri/efp v0.0.0-20250227110027-3491fafc2b79/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= +github.com/xuri/nfp v0.0.0-20250226145837-86d5fc24b2ba h1:DhIu6n3qU0joqG9f4IO6a/Gkerd+flXrmlJ+0yX2W8U= +github.com/xuri/nfp v0.0.0-20250226145837-86d5fc24b2ba/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ= +golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs= +golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= +golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/lib.go b/lib.go index 207cd839bd..e06e7f5105 100644 --- a/lib.go +++ b/lib.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize diff --git a/merge.go b/merge.go index 0381a25cfc..9f2d25b04d 100644 --- a/merge.go +++ b/merge.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize diff --git a/numfmt.go b/numfmt.go index 36fc331689..c5f6094c84 100644 --- a/numfmt.go +++ b/numfmt.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize diff --git a/picture.go b/picture.go index 704021fe8f..de0d555870 100644 --- a/picture.go +++ b/picture.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize diff --git a/pivotTable.go b/pivotTable.go index c71ca9ffcd..4825bef05a 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize diff --git a/rows.go b/rows.go index 68e7d0202d..436a5d6abf 100644 --- a/rows.go +++ b/rows.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize diff --git a/shape.go b/shape.go index 4c339f69a3..1bbf6964d6 100644 --- a/shape.go +++ b/shape.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize diff --git a/sheet.go b/sheet.go index abd7f52a02..65f2a4b008 100644 --- a/sheet.go +++ b/sheet.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize diff --git a/sheetpr.go b/sheetpr.go index 619f6d8af8..523e7b9375 100644 --- a/sheetpr.go +++ b/sheetpr.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize diff --git a/sheetview.go b/sheetview.go index 79d5cb6a53..fdc645b17a 100644 --- a/sheetview.go +++ b/sheetview.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize diff --git a/slicer.go b/slicer.go index f5902cd149..8073cf72ff 100644 --- a/slicer.go +++ b/slicer.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize diff --git a/sparkline.go b/sparkline.go index 78404bf65d..4597317096 100644 --- a/sparkline.go +++ b/sparkline.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize diff --git a/stream.go b/stream.go index d5e5f0544e..89081b8dde 100644 --- a/stream.go +++ b/stream.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize diff --git a/styles.go b/styles.go index d8b25886ae..5992cc39e0 100644 --- a/styles.go +++ b/styles.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize diff --git a/table.go b/table.go index 0fb8a7119f..1d47e21e85 100644 --- a/table.go +++ b/table.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize diff --git a/templates.go b/templates.go index f27b8defb6..b8cf159131 100644 --- a/templates.go +++ b/templates.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. // // This file contains default templates for XML files we don't yet populated // based on content. diff --git a/vml.go b/vml.go index 5923bee28e..7ea3e22a68 100644 --- a/vml.go +++ b/vml.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize diff --git a/vmlDrawing.go b/vmlDrawing.go index 1ccea63baa..44b5ea5f42 100644 --- a/vmlDrawing.go +++ b/vmlDrawing.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize diff --git a/vml_test.go b/vml_test.go index d6fed05f7d..dabd374747 100644 --- a/vml_test.go +++ b/vml_test.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize diff --git a/workbook.go b/workbook.go index 7df97febf6..d1cb1da45b 100644 --- a/workbook.go +++ b/workbook.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize diff --git a/xmlApp.go b/xmlApp.go index ac7ebdaf8d..e82218df17 100644 --- a/xmlApp.go +++ b/xmlApp.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize diff --git a/xmlCalcChain.go b/xmlCalcChain.go index 1cd749d297..472c87faa1 100644 --- a/xmlCalcChain.go +++ b/xmlCalcChain.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize diff --git a/xmlChart.go b/xmlChart.go index 4c40f1065d..abb0e4adbe 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize diff --git a/xmlChartSheet.go b/xmlChartSheet.go index ec7b920309..401642cfa2 100644 --- a/xmlChartSheet.go +++ b/xmlChartSheet.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize diff --git a/xmlComments.go b/xmlComments.go index 3bd16688cf..81072ff694 100644 --- a/xmlComments.go +++ b/xmlComments.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize diff --git a/xmlContentTypes.go b/xmlContentTypes.go index b38963599b..79708df11f 100644 --- a/xmlContentTypes.go +++ b/xmlContentTypes.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize diff --git a/xmlCore.go b/xmlCore.go index 941ee8af0b..f726baf47a 100644 --- a/xmlCore.go +++ b/xmlCore.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize diff --git a/xmlDecodeDrawing.go b/xmlDecodeDrawing.go index d30e5113c7..8a20c5d5c6 100644 --- a/xmlDecodeDrawing.go +++ b/xmlDecodeDrawing.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize diff --git a/xmlDrawing.go b/xmlDrawing.go index 3a3bcf3f28..f363849014 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize diff --git a/xmlMetaData.go b/xmlMetaData.go index ccfd70052c..90542fbd39 100644 --- a/xmlMetaData.go +++ b/xmlMetaData.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize diff --git a/xmlPivotCache.go b/xmlPivotCache.go index 1e4f737b5c..f634e61a0e 100644 --- a/xmlPivotCache.go +++ b/xmlPivotCache.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize diff --git a/xmlPivotTable.go b/xmlPivotTable.go index 934ff9ebb5..766c7e1a6c 100644 --- a/xmlPivotTable.go +++ b/xmlPivotTable.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize diff --git a/xmlSharedStrings.go b/xmlSharedStrings.go index c7555deb7f..37ea197d85 100644 --- a/xmlSharedStrings.go +++ b/xmlSharedStrings.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize diff --git a/xmlSlicers.go b/xmlSlicers.go index 0bd245a7d7..4b48bf7f39 100644 --- a/xmlSlicers.go +++ b/xmlSlicers.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize diff --git a/xmlStyles.go b/xmlStyles.go index edbc996f1f..93ad33cce3 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize diff --git a/xmlTable.go b/xmlTable.go index 77e948ed80..60bc307ed4 100644 --- a/xmlTable.go +++ b/xmlTable.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize diff --git a/xmlTheme.go b/xmlTheme.go index 503545c400..bb48590970 100644 --- a/xmlTheme.go +++ b/xmlTheme.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize diff --git a/xmlWorkbook.go b/xmlWorkbook.go index 6a6b5017eb..0b0a4aa4ce 100644 --- a/xmlWorkbook.go +++ b/xmlWorkbook.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize @@ -387,19 +387,19 @@ type DefinedName struct { // CalcPropsOptions defines the collection of properties the application uses to // record calculation status and details. type CalcPropsOptions struct { - CalcID *uint `xml:"calcId,attr"` - CalcMode *string `xml:"calcMode,attr"` - FullCalcOnLoad *bool `xml:"fullCalcOnLoad,attr"` - RefMode *string `xml:"refMode,attr"` - Iterate *bool `xml:"iterate,attr"` - IterateCount *uint `xml:"iterateCount,attr"` - IterateDelta *float64 `xml:"iterateDelta,attr"` - FullPrecision *bool `xml:"fullPrecision,attr"` - CalcCompleted *bool `xml:"calcCompleted,attr"` - CalcOnSave *bool `xml:"calcOnSave,attr"` - ConcurrentCalc *bool `xml:"concurrentCalc,attr"` - ConcurrentManualCount *uint `xml:"concurrentManualCount,attr"` - ForceFullCalc *bool `xml:"forceFullCalc,attr"` + CalcID *uint + CalcMode *string + FullCalcOnLoad *bool + RefMode *string + Iterate *bool + IterateCount *uint + IterateDelta *float64 + FullPrecision *bool + CalcCompleted *bool + CalcOnSave *bool + ConcurrentCalc *bool + ConcurrentManualCount *uint + ForceFullCalc *bool } // WorkbookPropsOptions directly maps the settings of workbook proprieties. diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 510deb6cea..45840d043d 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -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.20 or later. +// data. This library needs Go version 1.23 or later. package excelize From e9d27c7fab285d87d26035cc325c1658004ecab5 Mon Sep 17 00:00:00 2001 From: "Moises P. Sena" Date: Sun, 23 Mar 2025 08:58:06 -0300 Subject: [PATCH 949/957] This closes #2108, Add docs to SheetPropsOptions function for how to set 4 kinds of scaling options (#2109) --- xmlWorksheet.go | 53 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 45840d043d..4f17ffd862 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -1071,7 +1071,58 @@ type ViewOptions struct { ZoomScale *float64 } -// SheetPropsOptions directly maps the settings of sheet view. +// SetSheetProps provides a function to set worksheet properties. There 4 kinds +// of presets "Custom Scaling Options" in the spreadsheet applications, if you +// need to set those kind of scaling options, please using the "SetSheetProps" +// and "SetPageLayout" functions to approach these 4 scaling options: +// +// 1. No Scaling (Print sheets at their actual size): +// +// disable := false +// if err := f.SetSheetProps("Sheet1", &excelize.SheetPropsOptions{ +// FitToPage: &disable, +// }); err != nil { +// fmt.Println(err) +// } +// +// 2. Fit Sheet on One Page (Shrink the printout so that it fits on one page): +// +// enable := true +// if err := f.SetSheetProps("Sheet1", &excelize.SheetPropsOptions{ +// FitToPage: &enable, +// }); err != nil { +// fmt.Println(err) +// } +// +// 3. Fit All Columns on One Page (Shrink the printout so that it is one page +// wide): +// +// enable, zero := true, 0 +// if err := f.SetSheetProps("Sheet1", &excelize.SheetPropsOptions{ +// FitToPage: &enable, +// }); err != nil { +// fmt.Println(err) +// } +// if err := f.SetPageLayout("Sheet1", &excelize.PageLayoutOptions{ +// FitToHeight: &zero, +// }); err != nil { +// fmt.Println(err) +// } +// +// 4. Fit All Rows on One Page (Shrink the printout so that it is one page +// high): +// +// enable, zero := true, 0 +// if err := f.SetSheetProps("Sheet1", &excelize.SheetPropsOptions{ +// FitToPage: &enable, +// }); err != nil { +// fmt.Println(err) +// } +// if err := f.SetPageLayout("Sheet1", &excelize.PageLayoutOptions{ +// FitToWidth: &zero, +// }); err != nil { +// fmt.Println(err) +// } 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 From 91d36ccf7cfdf52b9827e2786c83cfc27cbb2f38 Mon Sep 17 00:00:00 2001 From: Artur Chopikian Date: Tue, 25 Mar 2025 06:41:15 +0200 Subject: [PATCH 950/957] Fix performance regression in v2.9.0, reduce trim cell value memory allocation for blank cells (#2100) --- cell.go | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/cell.go b/cell.go index b710cc7138..43c40b13ba 100644 --- a/cell.go +++ b/cell.go @@ -12,7 +12,6 @@ package excelize import ( - "bytes" "encoding/xml" "fmt" "math" @@ -510,13 +509,6 @@ func trimCellValue(value string, escape bool) (v string, ns xml.Attr) { if utf8.RuneCountInString(value) > TotalCellChars { value = string([]rune(value)[:TotalCellChars]) } - if escape && value != "" { - var buf bytes.Buffer - enc := xml.NewEncoder(&buf) - _ = enc.EncodeToken(xml.CharData(value)) - _ = enc.Flush() - value = buf.String() - } if value != "" { prefix, suffix := value[0], value[len(value)-1] for _, ascii := range []byte{9, 10, 13, 32} { @@ -528,6 +520,12 @@ func trimCellValue(value string, escape bool) (v string, ns xml.Attr) { break } } + + if escape { + var buf strings.Builder + _ = xml.EscapeText(&buf, []byte(value)) + value = strings.ReplaceAll(buf.String(), " ", "\n") + } } v = bstrMarshal(value) return From 3650e5c58d62b92e3c3ea4bee08f9217309e2283 Mon Sep 17 00:00:00 2001 From: Paolo Barbolini Date: Sat, 29 Mar 2025 15:51:05 +0100 Subject: [PATCH 951/957] Avoid looking up absent cells in `rangeResolver` (#2111) --- calc.go | 29 ++++++++++++++++++++++------- calc_test.go | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 7 deletions(-) diff --git a/calc.go b/calc.go index ba51590b3f..44dc4e44af 100644 --- a/calc.go +++ b/calc.go @@ -1690,16 +1690,31 @@ func (f *File) rangeResolver(ctx *calcContext, cellRefs, cellRanges *list.List) // extract value from ranges if cellRanges.Len() > 0 { arg.Type = ArgMatrix + + var ws *xlsxWorksheet + ws, err = f.workSheetReader(sheet) + if err != nil { + return + } + for row := valueRange[0]; row <= valueRange[1]; row++ { + colMax := 0 + if row <= len(ws.SheetData.Row) { + rowData := &ws.SheetData.Row[row-1] + colMax = min(valueRange[3], len(rowData.C)) + } + var matrixRow []formulaArg for col := valueRange[2]; col <= valueRange[3]; col++ { - 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 + value := newEmptyFormulaArg() + if col <= colMax { + var cell string + if cell, err = CoordinatesToCellName(col, row); err != nil { + return + } + if value, err = f.cellResolver(ctx, sheet, cell); err != nil { + return + } } matrixRow = append(matrixRow, value) } diff --git a/calc_test.go b/calc_test.go index 7581203d96..5f24cd3544 100644 --- a/calc_test.go +++ b/calc_test.go @@ -6303,6 +6303,43 @@ func TestCalcBetainvProbIterator(t *testing.T) { assert.Equal(t, 1.0, betainvProbIterator(1, 1, 1, 1, 1, 1, 1, 1, 1)) } +func TestCalcRangeResolver(t *testing.T) { + f := NewFile() + assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "=SUM(Sheet1!B:B)")) + cellRefs := list.New() + cellRanges := list.New() + // Test extract value from ranges on invalid ranges + cellRanges.PushBack(cellRange{ + From: cellRef{Col: 1, Row: 1, Sheet: "SheetN"}, + To: cellRef{Col: 1, Row: TotalRows, Sheet: "SheetN"}, + }) + _, err := f.rangeResolver(&calcContext{}, cellRefs, cellRanges) + assert.EqualError(t, err, "sheet SheetN does not exist") + + ws, err := f.workSheetReader("Sheet1") + ws.SheetData.Row = make([]xlsxRow, TotalRows+1) + ws.SheetData.Row[TotalRows].C = make([]xlsxC, 3) + assert.NoError(t, err) + cellRanges.Init() + cellRanges.PushBack(cellRange{ + From: cellRef{Col: 3, Row: TotalRows, Sheet: "Sheet1"}, + To: cellRef{Col: 3, Row: TotalRows + 1, Sheet: "Sheet1"}, + }) + _, err = f.rangeResolver(&calcContext{}, cellRefs, cellRanges) + assert.Equal(t, ErrMaxRows, err) + + // Test extract value from references with invalid references + cellRanges.Init() + cellRefs.PushBack(cellRef{Col: 1, Row: 1, Sheet: "SheetN"}) + _, err = f.rangeResolver(&calcContext{}, cellRefs, cellRanges) + assert.EqualError(t, err, "sheet SheetN does not exist") + + cellRefs.Init() + cellRefs.PushBack(cellRef{Col: 1, Row: TotalRows + 1, Sheet: "SheetN"}) + _, err = f.rangeResolver(&calcContext{}, cellRefs, cellRanges) + assert.Equal(t, ErrMaxRows, err) +} + func TestNestedFunctionsWithOperators(t *testing.T) { f := NewFile() formulaList := map[string]string{ From e12da49e38c43ebe47997688c16905dc91e042d9 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 4 Apr 2025 17:50:18 +0800 Subject: [PATCH 952/957] This fixes #2056, fix incorrect formula calculation result caused by shared formula parsed error - Also reference #844 - Update unit test - Update documentation of the SheetPropsOptions data structure - Update dependencies modules --- cell.go | 103 +++++++++++++++++++++--------------------------- cell_test.go | 8 ++-- go.mod | 4 +- go.sum | 8 ++-- xmlWorksheet.go | 9 +++-- 5 files changed, 61 insertions(+), 71 deletions(-) diff --git a/cell.go b/cell.go index 43c40b13ba..f2df30478c 100644 --- a/cell.go +++ b/cell.go @@ -21,6 +21,8 @@ import ( "strings" "time" "unicode/utf8" + + "github.com/xuri/efp" ) // CellType is the type of cell value type. @@ -1640,44 +1642,16 @@ func isOverlap(rect1, rect2 []int) bool { // parseSharedFormula generate dynamic part of shared formula for target cell // by given column and rows distance and origin shared formula. -func parseSharedFormula(dCol, dRow int, orig []byte) (res string, start int) { - var ( - end int - stringLiteral bool - ) - for end = 0; end < len(orig); end++ { - c := orig[end] - if c == '"' { - stringLiteral = !stringLiteral - } - if stringLiteral { - continue // Skip characters in quotes - } - if c >= 'A' && c <= 'Z' || c == '$' { - res += string(orig[start:end]) - start = end - end++ - foundNum := false - for ; end < len(orig); end++ { - idc := orig[end] - if idc >= '0' && idc <= '9' || idc == '$' { - foundNum = true - } else if idc >= 'A' && idc <= 'Z' { - if foundNum { - break - } - } else { - break - } - } - if foundNum { - cellID := string(orig[start:end]) - res += shiftCell(cellID, dCol, dRow) - start = end - } +func parseSharedFormula(dCol, dRow int, orig string) string { + ps := efp.ExcelParser() + tokens := ps.Parse(string(orig)) + for i := 0; i < len(tokens); i++ { + token := tokens[i] + if token.TType == efp.TokenTypeOperand && token.TSubType == efp.TokenSubTypeRange { + tokens[i].TValue = shiftCell(token.TValue, dCol, dRow) } } - return + return ps.Render() } // getSharedFormula find a cell contains the same formula as another cell, @@ -1698,12 +1672,7 @@ func getSharedFormula(ws *xlsxWorksheet, si int, cell string) string { sharedCol, sharedRow, _ := CellNameToCoordinates(c.R) dCol := col - sharedCol dRow := row - sharedRow - orig := []byte(c.F.Content) - res, start := parseSharedFormula(dCol, dRow, orig) - if start < len(orig) { - res += string(orig[start:]) - } - return res + return parseSharedFormula(dCol, dRow, c.F.Content) } } } @@ -1712,21 +1681,39 @@ func getSharedFormula(ws *xlsxWorksheet, si int, cell string) string { // shiftCell returns the cell shifted according to dCol and dRow taking into // consideration absolute references with dollar sign ($) -func shiftCell(cellID string, dCol, dRow int) string { - fCol, fRow, _ := CellNameToCoordinates(cellID) - signCol, signRow := "", "" - if strings.Index(cellID, "$") == 0 { - signCol = "$" - } else { - // Shift column - fCol += dCol - } - if strings.LastIndex(cellID, "$") > 0 { - signRow = "$" - } else { - // Shift row - fRow += dRow +func shiftCell(val string, dCol, dRow int) string { + parts := strings.Split(val, ":") + for j := 0; j < len(parts); j++ { + cell := parts[j] + trimmedCellName := strings.ReplaceAll(cell, "$", "") + c, r, err := CellNameToCoordinates(trimmedCellName) + if err == nil { + absCol := strings.Index(cell, "$") == 0 + absRow := strings.LastIndex(cell, "$") > 0 + if !absCol && !absRow { + parts[j], _ = CoordinatesToCellName(c+dCol, r+dRow) + } + if !absCol && absRow { + colName, _ := ColumnNumberToName(c + dCol) + parts[j] = colName + "$" + strconv.Itoa(r) + } + if absCol && !absRow { + colName, _ := ColumnNumberToName(c) + parts[j] = "$" + colName + strconv.Itoa(r+dRow) + } + continue + } + // Cell reference is a column name + c, err = ColumnNameToNumber(trimmedCellName) + if err == nil && !strings.HasPrefix(cell, "$") { + parts[j], _ = ColumnNumberToName(c + dCol) + continue + } + // Cell reference is a row number + r, err = strconv.Atoi(trimmedCellName) + if err == nil && !strings.HasPrefix(cell, "$") { + parts[j] = strconv.Itoa(r + dRow) + } } - colName, _ := ColumnNumberToName(fCol) - return signCol + colName + signRow + strconv.Itoa(fRow) + return strings.Join(parts, ":") } diff --git a/cell_test.go b/cell_test.go index ba051a46b9..b9069a354e 100644 --- a/cell_test.go +++ b/cell_test.go @@ -591,9 +591,11 @@ func TestGetCellFormula(t *testing.T) { sheetData := `12*A12%s34567` for sharedFormula, expected := range map[string]string{ - `2*A2`: `2*A3`, - `2*A1A`: `2*A2A`, - `2*$A$2+LEN("")`: `2*$A$2+LEN("")`, + `2*A2`: `2*A3`, + `2*A1A`: `2*A1A`, + `2*$A$2+LEN("")`: `2*$A$2+LEN("")`, + `SUMIF(A:A,$B11, 5:5)`: `SUMIF(A:A,$B12,6:6)`, + `SUMIF(A:A,B$11, 5:5)`: `SUMIF(A:A,B$11,6:6)`, } { f.Sheet.Delete("xl/worksheets/sheet1.xml") f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, sharedFormula))) diff --git a/go.mod b/go.mod index 296407c2ae..53b05c8e78 100644 --- a/go.mod +++ b/go.mod @@ -5,12 +5,12 @@ go 1.23.0 require ( github.com/richardlehane/mscfb v1.0.4 github.com/stretchr/testify v1.10.0 - github.com/tiendc/go-deepcopy v1.5.0 + github.com/tiendc/go-deepcopy v1.5.1 github.com/xuri/efp v0.0.0-20250227110027-3491fafc2b79 github.com/xuri/nfp v0.0.0-20250226145837-86d5fc24b2ba golang.org/x/crypto v0.36.0 golang.org/x/image v0.25.0 - golang.org/x/net v0.37.0 + golang.org/x/net v0.38.0 golang.org/x/text v0.23.0 ) diff --git a/go.sum b/go.sum index 82f3630523..e66a0ba3e4 100644 --- a/go.sum +++ b/go.sum @@ -9,8 +9,8 @@ github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/tiendc/go-deepcopy v1.5.0 h1:TbtS9hclrKZcF1AHby8evDm4mIQU36i6tSYuvx/TstY= -github.com/tiendc/go-deepcopy v1.5.0/go.mod h1:toXoeQoUqXOOS/X4sKuiAoSk6elIdqc0pN7MTgOOo2I= +github.com/tiendc/go-deepcopy v1.5.1 h1:5ymXIB8ReIywehne6oy3HgywC8LicXYucPBNnj5QQxE= +github.com/tiendc/go-deepcopy v1.5.1/go.mod h1:toXoeQoUqXOOS/X4sKuiAoSk6elIdqc0pN7MTgOOo2I= github.com/xuri/efp v0.0.0-20250227110027-3491fafc2b79 h1:78nKszZqigiBRBVcoe/AuPzyLTWW5B+ltBaUX1rlIXA= github.com/xuri/efp v0.0.0-20250227110027-3491fafc2b79/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= github.com/xuri/nfp v0.0.0-20250226145837-86d5fc24b2ba h1:DhIu6n3qU0joqG9f4IO6a/Gkerd+flXrmlJ+0yX2W8U= @@ -19,8 +19,8 @@ golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ= golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs= -golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= -golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 4f17ffd862..dab4caf321 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -1071,10 +1071,11 @@ type ViewOptions struct { ZoomScale *float64 } -// SetSheetProps provides a function to set worksheet properties. There 4 kinds -// of presets "Custom Scaling Options" in the spreadsheet applications, if you -// need to set those kind of scaling options, please using the "SetSheetProps" -// and "SetPageLayout" functions to approach these 4 scaling options: +// SheetPropsOptions provides a function to set worksheet properties. There 4 +// kinds of presets "Custom Scaling Options" in the spreadsheet applications, if +// you need to set those kind of scaling options, please using the +// "SetSheetProps" and "SetPageLayout" functions to approach these 4 scaling +// options: // // 1. No Scaling (Print sheets at their actual size): // From 6007d25231b134935ee3aedad4cfb5f13cfb4a62 Mon Sep 17 00:00:00 2001 From: timesince Date: Wed, 9 Apr 2025 16:02:53 +0800 Subject: [PATCH 953/957] Simplifying code with directly swap the values of two variables (#2114) --- calc.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/calc.go b/calc.go index 44dc4e44af..c36e500942 100644 --- a/calc.go +++ b/calc.go @@ -12940,9 +12940,7 @@ func (fn *formulaFuncs) NETWORKDAYSdotINTL(argsList *list.List) formulaArg { sign := 1 if startDate.Number > endDate.Number { sign = -1 - temp := startDate.Number - startDate.Number = endDate.Number - endDate.Number = temp + startDate.Number, endDate.Number = endDate.Number, startDate.Number } offset := endDate.Number - startDate.Number count := int(math.Floor(offset/7) * float64(workdaysPerWeek)) From f85dae6cfa7aabfa67a957a1dc1b926e07a58147 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 11 Apr 2025 14:45:48 +0800 Subject: [PATCH 954/957] This closes #2113, AddFormControl function support set cell link for check box --- vml.go | 9 ++++++++- vml_test.go | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/vml.go b/vml.go index 7ea3e22a68..08d54985d2 100644 --- a/vml.go +++ b/vml.go @@ -372,7 +372,11 @@ func (f *File) commentsWriter() { // by given worksheet name and form control options. Supported form control // type: button, check box, group box, label, option button, scroll bar and // spinner. If set macro for the form control, the workbook extension should be -// XLSM or XLTM. Scroll value must be between 0 and 30000. +// XLSM or XLTM. Scroll value must be between 0 and 30000. Please note that if a +// cell link is set for a checkbox form control, Excelize will not assign a +// value to the linked cell when the checkbox is checked. To reflect the +// checkbox state, please use the 'SetCellValue' function to manually set the +// linked cell's value to true. // // Example 1, add button form control with macro, rich-text, custom button size, // print property on Sheet1!A2, and let the button do not move or size with @@ -826,6 +830,9 @@ func (f *File) addFormCtrlShape(preset formCtrlPreset, col, row int, anchor stri if (opts.Type == FormControlCheckBox || opts.Type == FormControlOptionButton) && opts.Checked { sp.ClientData.Checked = 1 } + if opts.FormControl.Type == FormControlCheckBox { + sp.ClientData.FmlaLink = opts.CellLink + } return &sp, sp.addFormCtrl(opts) } diff --git a/vml_test.go b/vml_test.go index dabd374747..22b14f9575 100644 --- a/vml_test.go +++ b/vml_test.go @@ -192,7 +192,7 @@ func TestFormControl(t *testing.T) { }, { Cell: "A6", Type: FormControlCheckBox, Text: "Check Box 2", - Format: GraphicOptions{Positioning: "twoCell"}, + CellLink: "C5", Format: GraphicOptions{Positioning: "twoCell"}, }, { Cell: "A7", Type: FormControlOptionButton, Text: "Option Button 1", Checked: true, From ce9061fe468b7eb3a74ccf24e06bfc00a6d3552e Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 16 Apr 2025 19:54:32 +0800 Subject: [PATCH 955/957] This fix corrupted workbook generated when any inner ZIP64 file's size exceeds 4GB - Update unit tests - Support set version required to 4.5 in local file header - Note that this fix not work on Office 2010 --- excelize.go | 1 + file.go | 90 +++++++++++++++++++++++++++++++++++----------------- file_test.go | 69 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 131 insertions(+), 29 deletions(-) diff --git a/excelize.go b/excelize.go index 8448999ab2..61bb6d3489 100644 --- a/excelize.go +++ b/excelize.go @@ -31,6 +31,7 @@ type File struct { mu sync.Mutex checked sync.Map formulaChecked bool + zip64Entries []string options *Options sharedStringItem [][]uint sharedStringsMap map[string]int diff --git a/file.go b/file.go index aa0816c9c2..c5ccff53db 100644 --- a/file.go +++ b/file.go @@ -14,8 +14,10 @@ package excelize import ( "archive/zip" "bytes" + "encoding/binary" "encoding/xml" "io" + "math" "os" "path/filepath" "sort" @@ -123,17 +125,11 @@ func (f *File) WriteTo(w io.Writer, opts ...Options) (int64, error) { return 0, err } } - if f.options != nil && f.options.Password != "" { - buf, err := f.WriteToBuffer() - if err != nil { - return 0, err - } - return buf.WriteTo(w) - } - if err := f.writeDirectToWriter(w); err != nil { + buf, err := f.WriteToBuffer() + if err != nil { return 0, err } - return 0, nil + return buf.WriteTo(w) } // WriteToBuffer provides a function to get bytes.Buffer from the saved file, @@ -143,32 +139,22 @@ func (f *File) WriteToBuffer() (*bytes.Buffer, error) { zw := zip.NewWriter(buf) if err := f.writeToZip(zw); err != nil { - return buf, zw.Close() + _ = zw.Close() + return buf, err } - + if err := zw.Close(); err != nil { + return buf, err + } + f.writeZip64LFH(buf) if f.options != nil && f.options.Password != "" { - if err := zw.Close(); err != nil { - return buf, err - } b, err := Encrypt(buf.Bytes(), f.options) if err != nil { return buf, err } buf.Reset() buf.Write(b) - return buf, nil - } - return buf, zw.Close() -} - -// writeDirectToWriter provides a function to write to io.Writer. -func (f *File) writeDirectToWriter(w io.Writer) error { - zw := zip.NewWriter(w) - if err := f.writeToZip(zw); err != nil { - _ = zw.Close() - return err } - return zw.Close() + return buf, nil } // writeToZip provides a function to write to zip.Writer @@ -197,11 +183,16 @@ func (f *File) writeToZip(zw *zip.Writer) error { _ = stream.rawData.Close() return err } - if _, err = io.Copy(fi, from); err != nil { + written, err := io.Copy(fi, from) + if err != nil { return err } + if written > math.MaxUint32 { + f.zip64Entries = append(f.zip64Entries, path) + } } var ( + n int err error files, tempFiles []string ) @@ -219,7 +210,9 @@ func (f *File) writeToZip(zw *zip.Writer) error { break } content, _ := f.Pkg.Load(path) - _, err = fi.Write(content.([]byte)) + if n, err = fi.Write(content.([]byte)); n > math.MaxUint32 { + f.zip64Entries = append(f.zip64Entries, path) + } } f.tempFiles.Range(func(path, content interface{}) bool { if _, ok := f.Pkg.Load(path); ok { @@ -234,7 +227,46 @@ func (f *File) writeToZip(zw *zip.Writer) error { if fi, err = zw.Create(path); err != nil { break } - _, err = fi.Write(f.readBytes(path)) + if n, err = fi.Write(f.readBytes(path)); n > math.MaxUint32 { + f.zip64Entries = append(f.zip64Entries, path) + } } return err } + +// writeZip64LFH function sets the ZIP version to 0x2D (45) in the Local File +// Header (LFH). Excel strictly enforces ZIP64 format validation rules. When any +// file within the workbook (OCP) exceeds 4GB in size, the ZIP64 format must be +// used according to the PKZIP specification. However, ZIP files generated using +// Go's standard archive/zip library always set the version in the local file +// header to 20 (ZIP version 2.0) by default, as defined in the internal +// 'writeHeader' function during ZIP creation. The archive/zip package only sets +// the 'ReaderVersion' to 45 (ZIP64 version 4.5) in the central directory for +// entries larger than 4GB. This results in a version mismatch between the +// central directory and the local file header. As a result, opening the +// generated workbook with spreadsheet application will prompt file corruption. +func (f *File) writeZip64LFH(buf *bytes.Buffer) error { + if len(f.zip64Entries) == 0 { + return nil + } + data, offset := buf.Bytes(), 0 + for offset < len(data) { + idx := bytes.Index(data[offset:], []byte{0x50, 0x4b, 0x03, 0x04}) + if idx == -1 { + break + } + idx += offset + if idx+30 > len(data) { + break + } + filenameLen := int(binary.LittleEndian.Uint16(data[idx+26 : idx+28])) + if idx+30+filenameLen > len(data) { + break + } + if inStrSlice(f.zip64Entries, string(data[idx+30:idx+30+filenameLen]), true) != -1 { + binary.LittleEndian.PutUint16(data[idx+4:idx+6], 45) + } + offset = idx + 1 + } + return nil +} diff --git a/file_test.go b/file_test.go index 4272a7b4f1..4a9b5eaa22 100644 --- a/file_test.go +++ b/file_test.go @@ -3,6 +3,8 @@ package excelize import ( "bufio" "bytes" + "encoding/binary" + "math" "os" "path/filepath" "strings" @@ -95,3 +97,70 @@ func TestClose(t *testing.T) { f.tempFiles.Store("/d/", "/d/") require.Error(t, f.Close()) } + +func TestZip64(t *testing.T) { + f := NewFile() + _, err := f.NewSheet("Sheet2") + assert.NoError(t, err) + sw, err := f.NewStreamWriter("Sheet1") + assert.NoError(t, err) + for r := range 131 { + rowData := make([]interface{}, 1000) + for c := range 1000 { + rowData[c] = strings.Repeat("c", TotalCellChars) + } + cell, err := CoordinatesToCellName(1, r+1) + assert.NoError(t, err) + assert.NoError(t, sw.SetRow(cell, rowData)) + } + assert.NoError(t, sw.Flush()) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestZip64.xlsx"))) + assert.NoError(t, f.Close()) + + // Test with filename length overflow + f = NewFile() + f.zip64Entries = append(f.zip64Entries, defaultXMLPathSharedStrings) + buf := new(bytes.Buffer) + buf.Write([]byte{0x50, 0x4b, 0x03, 0x04}) + buf.Write(make([]byte, 20)) + assert.NoError(t, f.writeZip64LFH(buf)) + + // Test with file header less than the required 30 for the fixed header part + f = NewFile() + f.zip64Entries = append(f.zip64Entries, defaultXMLPathSharedStrings) + buf.Reset() + buf.Write([]byte{0x50, 0x4b, 0x03, 0x04}) + buf.Write(make([]byte, 22)) + binary.Write(buf, binary.LittleEndian, uint16(10)) + buf.Write(make([]byte, 2)) + buf.WriteString("test") + assert.NoError(t, f.writeZip64LFH(buf)) + + t.Run("for_save_zip64_with_in_memory_file_over_4GB", func(t *testing.T) { + // Test save workbook in ZIP64 format with in memory file with size over 4GB. + f := NewFile() + f.Sheet.Delete("xl/worksheets/sheet1.xml") + f.Pkg.Store("xl/worksheets/sheet1.xml", make([]byte, math.MaxUint32+1)) + _, err := f.WriteToBuffer() + assert.NoError(t, err) + assert.NoError(t, f.Close()) + }) + + t.Run("for_save_zip64_with_in_temporary_file_over_4GB", func(t *testing.T) { + // Test save workbook in ZIP64 format with temporary file with size over 4GB. + if os.Getenv("GITHUB_ACTIONS") == "true" { + t.Skip() + } + f := NewFile() + f.Pkg.Delete("xl/worksheets/sheet1.xml") + f.Sheet.Delete("xl/worksheets/sheet1.xml") + tmp, err := os.CreateTemp(os.TempDir(), "excelize-") + assert.NoError(t, err) + assert.NoError(t, tmp.Truncate(math.MaxUint32+1)) + f.tempFiles.Store("xl/worksheets/sheet1.xml", tmp.Name()) + assert.NoError(t, tmp.Close()) + _, err = f.WriteToBuffer() + assert.NoError(t, err) + assert.NoError(t, f.Close()) + }) +} From 55cf0d42a76bc4f8d8810d4d8dcc5017debe1082 Mon Sep 17 00:00:00 2001 From: shcabin <5463832+shcabin@users.noreply.github.com> Date: Fri, 18 Apr 2025 19:52:59 +0800 Subject: [PATCH 956/957] Improve cell read performance by optimizing XML parsing (#2116) - Rows iterator speedup about 20%, memory allocation reduce about 10% - Update unit test - Extends time out to 50 minutes in GitHub Action for made TestZip64 stable --- .github/workflows/go.yml | 2 +- rows.go | 59 +++++++++++++++++++++++++++++++++++++- rows_test.go | 61 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 120 insertions(+), 2 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 5e16cfccae..d29ee78f49 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -29,7 +29,7 @@ jobs: run: go build -v . - name: Test - run: env GO111MODULE=on go test -v -timeout 30m -race ./... -coverprofile='coverage.txt' -covermode=atomic + run: env GO111MODULE=on go test -v -timeout 50m -race ./... -coverprofile='coverage.txt' -covermode=atomic - name: Codecov uses: codecov/codecov-action@v5 diff --git a/rows.go b/rows.go index 436a5d6abf..1852b9c67d 100644 --- a/rows.go +++ b/rows.go @@ -231,7 +231,7 @@ func (rows *Rows) rowXMLHandler(rowIterator *rowXMLIterator, xmlElement *xml.Sta if rowIterator.inElement == "c" { rowIterator.cellCol++ colCell := xlsxC{} - _ = rows.decoder.DecodeElement(&colCell, xmlElement) + colCell.cellXMLHandler(rows.decoder, xmlElement) if colCell.R != "" { if rowIterator.cellCol, _, rowIterator.err = CellNameToCoordinates(colCell.R); rowIterator.err != nil { return @@ -244,6 +244,63 @@ func (rows *Rows) rowXMLHandler(rowIterator *rowXMLIterator, xmlElement *xml.Sta } } +// cellXMLAttrHandler parse the cell XML element attributes of the worksheet. +func (cell *xlsxC) cellXMLAttrHandler(start *xml.StartElement) error { + for _, attr := range start.Attr { + switch attr.Name.Local { + case "r": + cell.R = attr.Value + case "s": + val, err := strconv.ParseInt(attr.Value, 10, 64) + if err != nil { + return err + } + if math.MinInt <= val && val <= math.MaxInt { + cell.S = int(val) + } + case "t": + cell.T = attr.Value + default: + } + } + return nil +} + +// cellXMLHandler parse the cell XML element of the worksheet. +func (cell *xlsxC) cellXMLHandler(decoder *xml.Decoder, start *xml.StartElement) error { + cell.XMLName = start.Name + err := cell.cellXMLAttrHandler(start) + if err != nil { + return err + } + for { + tok, err := decoder.Token() + if err != nil { + return err + } + var se xml.StartElement + switch el := tok.(type) { + case xml.StartElement: + se = el + switch se.Name.Local { + case "v": + err = decoder.DecodeElement(&cell.V, &se) + case "f": + err = decoder.DecodeElement(&cell.F, &se) + case "is": + err = decoder.DecodeElement(&cell.IS, &se) + } + if err != nil { + return err + } + case xml.EndElement: + if el == start.End() { + return nil + } + } + } +} + // Rows returns a rows iterator, used for streaming reading data for a // worksheet with a large data. This function is concurrency safe. For // example: diff --git a/rows_test.go b/rows_test.go index 01b20a0fcf..5f2c1fd66a 100644 --- a/rows_test.go +++ b/rows_test.go @@ -5,6 +5,7 @@ import ( "encoding/xml" "fmt" "path/filepath" + "strconv" "testing" "github.com/stretchr/testify/assert" @@ -1157,6 +1158,66 @@ func TestNumberFormats(t *testing.T) { assert.Equal(t, "2019/3/19", result, "A1") } +func TestCellXMLHandler(t *testing.T) { + var ( + content = []byte(fmt.Sprintf(`10String2*A10A32422.30000000000022022-10-22T15:05:29Z`, NameSpaceSpreadSheet.Value)) + expected, ws xlsxWorksheet + row *xlsxRow + ) + assert.NoError(t, xml.Unmarshal(content, &expected)) + decoder := xml.NewDecoder(bytes.NewReader(content)) + rows := Rows{decoder: decoder} + for { + token, _ := decoder.Token() + if token == nil { + break + } + switch element := token.(type) { + case xml.StartElement: + if element.Name.Local == "row" { + r, err := strconv.Atoi(element.Attr[0].Value) + assert.NoError(t, err) + ws.SheetData.Row = append(ws.SheetData.Row, xlsxRow{R: r}) + row = &ws.SheetData.Row[len(ws.SheetData.Row)-1] + } + if element.Name.Local == "c" { + colCell := xlsxC{} + assert.NoError(t, colCell.cellXMLHandler(rows.decoder, &element)) + row.C = append(row.C, colCell) + } + } + } + assert.Equal(t, expected.SheetData.Row, ws.SheetData.Row) + + for _, rowXML := range []string{ + `10`, // s need number + `10 `, // missing + ``, // incorrect data + } { + ws := xlsxWorksheet{} + content := []byte(fmt.Sprintf(`%s`, NameSpaceSpreadSheet.Value, rowXML)) + expected := xml.Unmarshal(content, &ws) + assert.Error(t, expected) + decoder := xml.NewDecoder(bytes.NewReader(content)) + rows := Rows{decoder: decoder} + for { + token, _ := decoder.Token() + if token == nil { + break + } + switch element := token.(type) { + case xml.StartElement: + if element.Name.Local == "c" { + colCell := xlsxC{} + err := colCell.cellXMLHandler(rows.decoder, &element) + assert.Error(t, err) + assert.Equal(t, expected, err) + } + } + } + } +} + func BenchmarkRows(b *testing.B) { f, _ := OpenFile(filepath.Join("test", "Book1.xlsx")) for i := 0; i < b.N; i++ { From 0f19d7fcd7c7c6ab3f25a363ced93896a4d323e8 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 22 Apr 2025 07:59:52 +0800 Subject: [PATCH 957/957] This closes #2117, support add data table for chart - Add ShowDataTable and ShowDataTableKeys fields in the ChartPlotArea data type - Update unit tests --- chart.go | 8 ++++++++ chart_test.go | 2 +- drawing.go | 14 ++++++++++++++ xmlChart.go | 32 +++++++++++++++++++++++--------- 4 files changed, 46 insertions(+), 10 deletions(-) diff --git a/chart.go b/chart.go index 88df5c2f21..011fbd1c3c 100644 --- a/chart.go +++ b/chart.go @@ -863,6 +863,14 @@ func (opts *Chart) parseTitle() { // ShowCatName: Specifies that the category name shall be shown in the data // label. The 'ShowCatName' property is optional. The default value is true. // +// ShowDataTable: Used for add data table under chart, depending on the chart +// type, only available for area, bar, column and line series type charts. The +// 'ShowDataTable' property is optional. The default value is false. +// +// ShowDataTableKeys: Used for add legend key in data table, only works on +// 'ShowDataTable' is enabled. The 'ShowDataTableKeys' 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. // diff --git a/chart_test.go b/chart_test.go index 42e2a65764..f9fdb28ce8 100644 --- a/chart_test.go +++ b/chart_test.go @@ -293,7 +293,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: []RichTextRun{{Text: 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}})) + assert.NoError(t, f.AddChart("Combo Charts", props[0].(string), &Chart{Type: Col, Series: series[:4], Format: format, Legend: legend, Title: []RichTextRun{{Text: props[2].(string)}}, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowDataTable: true, ShowDataTableKeys: true, 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 c029fdf7d3..640804761f 100644 --- a/drawing.go +++ b/drawing.go @@ -169,6 +169,7 @@ func (f *File) addChart(opts *Chart, comboCharts []*Chart) { xlsxChartSpace.Chart.Legend = nil } xlsxChartSpace.Chart.PlotArea.SpPr = f.drawShapeFill(opts.PlotArea.Fill, xlsxChartSpace.Chart.PlotArea.SpPr) + xlsxChartSpace.Chart.PlotArea.DTable = f.drawPlotAreaDTable(opts) addChart := func(c, p *cPlotArea) { immutable, mutable := reflect.ValueOf(c).Elem(), reflect.ValueOf(p).Elem() for i := 0; i < mutable.NumField(); i++ { @@ -1232,6 +1233,19 @@ func (f *File) drawPlotAreaTitles(runs []RichTextRun, vert string) *cTitle { return title } +// drawPlotAreaDTable provides a function to draw the c:dTable element. +func (f *File) drawPlotAreaDTable(opts *Chart) *cDTable { + if _, ok := plotAreaChartGrouping[opts.Type]; ok && opts.PlotArea.ShowDataTable { + return &cDTable{ + ShowHorzBorder: &attrValBool{Val: boolPtr(true)}, + ShowVertBorder: &attrValBool{Val: boolPtr(true)}, + ShowOutline: &attrValBool{Val: boolPtr(true)}, + ShowKeys: &attrValBool{Val: boolPtr(opts.PlotArea.ShowDataTableKeys)}, + } + } + return nil +} + // drawPlotAreaSpPr provides a function to draw the c:spPr element. func (f *File) drawPlotAreaSpPr() *cSpPr { return &cSpPr{ diff --git a/xmlChart.go b/xmlChart.go index abb0e4adbe..629aa33d1a 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -215,6 +215,17 @@ type aRPr struct { Cs *aCs `xml:"a:cs"` } +// cDTable (Data Table) directly maps the dTable element. +type cDTable struct { + ShowHorzBorder *attrValBool `xml:"showHorzBorder"` + ShowVertBorder *attrValBool `xml:"showVertBorder"` + ShowOutline *attrValBool `xml:"showOutline"` + ShowKeys *attrValBool `xml:"showKeys"` + SpPr *cSpPr `xml:"spPr"` + TxPr *cTxPr `xml:"txPr"` + ExtLst *xlsxExtLst `xml:"extLst"` +} + // cSpPr (Shape Properties) directly maps the spPr element. This element // specifies the visual shape properties that can be applied to a shape. These // properties include the shape fill, outline, geometry, effects, and 3D @@ -319,6 +330,7 @@ type cPlotArea struct { CatAx []*cAxs `xml:"catAx"` ValAx []*cAxs `xml:"valAx"` SerAx []*cAxs `xml:"serAx"` + DTable *cDTable `xml:"dTable"` SpPr *cSpPr `xml:"spPr"` } @@ -559,15 +571,17 @@ type ChartDimension struct { // ChartPlotArea directly maps the format settings of the plot area. type ChartPlotArea struct { - SecondPlotValues int - ShowBubbleSize bool - ShowCatName bool - ShowLeaderLines bool - ShowPercent bool - ShowSerName bool - ShowVal bool - Fill Fill - NumFmt ChartNumFmt + SecondPlotValues int + ShowBubbleSize bool + ShowCatName bool + ShowDataTable bool + ShowDataTableKeys bool + ShowLeaderLines bool + ShowPercent bool + ShowSerName bool + ShowVal bool + Fill Fill + NumFmt ChartNumFmt } // Chart directly maps the format settings of the chart.

QY1t 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 literal 190484 zcmdRVhg%cfwl@NTC<;m_ovP-4kRR6Q7NgErY18t-i)=&w2zZs ze@7QYUt}(jPx)@q^%c)MLshLym;KaPyK3@olMcOzdvQnhhI0Q)i;tWyqpc~Q7#a?A zy}X*@AEuce=-z%hHRqob3~xA4!tRDl$vwxB7+$dC(FtcH(PDZU)q?{t*LWhR&q-d8 zP=9_wp*O=C70G<-7P(;-9>1V8uTp5#>Y$XcvqT`6oBTZDVI|2kOs4-tXCOZ`FoB5W zrgOaT!Za_mz4&1Pm7Chbhp{&zkM^hxa#!=I46;|NlOz&WY)BrDS6yO|w#=p`(I7jP zs+AjbVjlt|i@df9qg{^*T9AcEGp(!L8Pf)hr7s8HVd2N~f{vEF_=db{X9gv6w;jG- zS|p9S^EL+wbsHcwqV8^?ZoS#eJM6=+Y?@zNs0%J<4wJmWM-ib{_dKpSU1lHiN^L`8ug!?_(-_C8l#=^{=MZ+? zRA3qhv(%TFmz&GZsYUn6M|%SX&+VL>KZNFvk9j|}20uL5N`#|?9uNL{ir~gMwAVRq zZ%}1O$??|I&Nc(wVHxRn9=6>XHfc<~BVu}EOc$AS#yfMp_S3`7u6mHD)U+|q8LrY#Jk$t;({p^dB(i+Z*YWjlgX)ZUTBj`+=UQC$tLSP>C zw--0Y$@)84E63QINoeYaNN7oKtFT&7%f6ub8g^6oqEP1}Lz3uDvyb%2VLBgazP;>O zzT-qu+9~HjWkt4-#{?h`c}&|z&b>@_Go14hjS<(aA_`|Uig(OUuF9#py`y0uf34xvaM4|>S84vN$6kVD-7h6|UN^SR|CGnN){N>}Q#SQ5jS+Um9BAuy^X&!?fi`fO&g#VzCWsQrI>XutR_w;OLnD?6J zn`gJ;uDUuM&eHjI6%lZ+jd_ug>YIJgKhOwG+;e2UZp>g>eQ<`Uh zdoe8;k2DoQA*whY&7`___Jf}5X2U7N_XZb|gGE5eGVdIR#8Ssn#1epfP&IrCsZp0< zmu^_)N1c4Dm7;Fz*G3wd1u1rsb}g%Xec{pO3=zqPI&XB8bgYUY#UVNyDHKkZ^y7bN ztc_1ioJ@*-rAYA{x<4em&ai%ay=TaJh+9xcuu*VAaM*O{fk6do;(7~Ljs~dEJ)fg^ zDIc0qEEgjeBlCRi!<&aPjv=NwmD~A4CL*S;rh5-?1^tFdBi#pcB~u?xEZ7R&-m{l< zn1)niG>*;dQXE@HevOD`hxnY3UA5>j<+if_)UWi`;H{Cqh(1t1;g{PNWM*~!<|Fn; z0*@5xN1d)dpR8Y~zc{N*x{h*5573Qa=u`_7JXY21(OcvPg_@|evB;YJ#HNM;CRh1ZjyB+ zdt1)`i@+~Eqzf_#8G5BffQW#h8+7`U5v@NU^~ z#berNM&g@i(wYPRhosWUXQd_vu$;Y|y6kv)!j|2B;D<&@yD|D)j`r+!v!I4R?#0l> z*NfIlI!Z=L4oV=1Z%9(ePKf6D?s+P<;mjUKhHE-oa&WGd{UAT3^+K(4=*5+= z+Rjpy5|!1wsk6iL7iV)Dz{}E?ZcxZXyr4DveDZD}|9*Z(KK#u}w`$K&Pws2W*VUH} zBJ(taqhxO)xV}nx>Hh4wvv$YXG`Jpu>fUVGjF(uH=#vPk@wPYbw}~-{r|)g(U0W+$ zqv?$oJ$(4lyK7ms_4UOo2f~t%>Rl#M`nF>xZ@_GB!(X-kv>=t{;#+y#ZSHreHB&M3 zv(5)C)|CKHKTjJk#b$-(9)P)x#6e)9-OeCCTUBNSaDF&;n@z1x)EmE{i>Jau6Qgyq*ztSv@3UG($m3vo~y&j83ao>uX|h zY;=J^d5uqdJ#bE>6@Z=mx>-DsDj-TPx-%m2WhNa7n3g%+SWjQiUf&b)G5cUW0Z;r? z`;4(((DMQIS^M|13d(78Wx2KPqpX&bpj-zV&qb`S#Lgq+J>>LHnh&RzU@)gSr)YC) zmxJ0mO*#iT(8slYra5@+RMlS!iStSfjtnR z+b!lFN5Sq2{1xzFrMU|cO}3HlEeTwAE33hgS!_xn@A7! z0ZK}Zj*xSf_xA6wB=rsk&SFAXg%~M zA-Tc(`|pCbG0!0h$px6RsfCY)zMj0jyQ`?}6L&iY(EwKu;@c!7fB<>orK^LFEoXqM zi<`H6fa2}H-jF9=|Gq7DoAa+%e4Z=bw$L}^RCD)o;Jhb#S5*AA(q&FgPJq{wr}DLgG`BM(%;M>p2&!YJO4Nn zugT~$1vu~Jk&w;_XsfH32F&i}P&QjV_vF5k)1 zYJj|WEOuuz=8{D~ig~~TT0<4#>O6~gH?f(x-K809a_}$siOn$IO=^npuE*m4=QI~Pvq}1Y?)>92ckhCibWWa+22T<;wmyg1q#WLS=f*M+jTMwf0)tlkyUfRcAz(Y8vVwn zKd_P7;KgT?&+REnShuL-jsV(=t1D_Q*1d<+G!lPD`A5ugD``ZHjjbkW0D4S*LmZ zzW_+N)Z6rWjf7%=H#3&H!dJ5EAHPba2RWIxosRf}*#AQn zz!#QYQd}jHIQL%pm^JiNMu>(*eV1=trs)g7zl5B-BS1Wzj(+Yxb>?4s}pMW5oLVvI{J%lH}YwnJZaCDT|-FpyPgxOPMIU_+~IAnABfLe7UwF0@pFtcifMal zt3NLNX=>*!NBPi(8yxv>mT&&}#YWo89`MzOc0o9|`5{@)RTe|re$V*TnvQ_`o^o%? zzMt^`Qja(&9Gcclvf5rfpTF)RWXVKu?BVcn_S@~7g+1N7^dr=daqmBs>K`H$dd`Cz zcq(t!eq1_Q0{3O$pcv?sw!PMhKJ&@E+h^}N_@w7T>vitxN0~>6P8Y=CD!bhNp!W|v zTbI88Y$7}O)|pzLU9Roc@!Q6?Xq)Rwr^3Oz7VVek{tMnIM?&A>kqCu4yt&BI?N`h% z?enh4lL;dXqdO?yAeZYXsh9z2kRgVpTm`lw&H9Ism_YJ%{ry_X#}XX_?DBjaJxboy zCBv!38rZ*CiGNH;>QIEMO4F`I49!+dxR>d!QuoF_uL4y$0C1LWWd=m^Z+w%yS(@dmgDYZOHIyZ4h=3E!AmD^HMpWgVYmA-hG9y zt_~@1_iOl#O9!{c(AEMlI)F#mq56=3x+Ke|*giAo--G`%NF3+eXQ?WL=Z$y^8XWyK zBJoFGxjIWn(QjDdz|CPR)f$N2wzg}!0TgUK`bJb>cDVa;uxUCMKtMwL6kjZ$q_4#; z7DY)F_FuTHzdk&6JjKCM4kg-ZCuXTY{O9E_+|m3TT+#fy_w>)WR%Vdp9)chNr6`u+ zf`D4Oh(Nv&!R*W@tHBIoLbNzv^#$!c_q$640eVuSUx!Ve7*NxHbU3%=YcEt{d}K_>6rxiE=Ag2^(!$)Y1wX}%{s~~z3x>*$gFW^x>3&Fy~d@A z0^A?P!QUn{5zlknq*bbGVvA;hzeY8Ap|z!$9%&GMy2xH4q7bdt$!H@uwwyvuE%QOd z?b+6Pq6C2~T$Y_-#j;y>=q&DVNc+Nq#JpiBDUcWKEI7jMwyGKXnPs^Bg(#zen9gd` zgD(Gv*u7i-O|`xs>fb8hy{q|N1^T=QKM>zgh#E7-RFIwBiVM3hPf?^fa>GVIX(2M+ zYs2ITx}Gemj!TcD>F4KK1brjGXjalV;g3AdITK}XeBo6relYuLA0{bRw z=IL?TFb%a#g0o;UYBi7w(xlh%fC3yuUw#SLvSN}9va&O=J#t8kI(xEdsR^KUzy=bK z$c2eNn+Nziuo!NjS{3^2J6@&%P8WI)!ySbeDS=AsLWyCGp&9J+?%cfhpnJOUF?!Xu zN6MK#c$3`DQfaTR@*T%bv~)1-XT3#06y*l?!4XdKY~EgrAzr0_Ew=cFXYs3^0(QuL z!*1lszkJ-k!?ARBcCBLAb}9Te#$2-jyzTz>D~GnMAY?_{sRSQAL*_xvlgaC*Woh-1 zFFp-*_>dv~z(MEpuwpcEPkU4mx(m{&g>MV2kr7{wlK$X9^OUwWN}O*xDdu((J*X%~&Y^ zI>Gb8` zD~|O;*Pg(Jt1@?uRu}|FPp?8O=Y1gWO=?c2W5C)3V)uK6VP=_{+OmGDFm`cn# zaa#1wSAt}IrHGr`T<2+-xPqJ!AjzNMJ--ORwuCY_YRvsfpfF$PptEjGi-;gAWcThToY_T;eerqFPJE*W>YFZf9rIWK z=b!aO@nfYt1#3^AvktlNnpNQHn^mSf_vViwg0c4NCZWmgv6bsAZg)pPF_bz!VWrWB zY=}<5{tcPR2Nb}^$0@_{acQtCX^C}>(^=p#{2zlAj&oyTw4(cq*@>JecgId}ZcM!m z&i%Uzl-EyheH-pU?V{!?{d#95S2DM(`v%j;LET?Q0E1L9QORjeKcve%d!JOAZm18W z>Tu{fGTO6)Kdwh&-ldjiUh?cl1u#l1hq|h3#?TczyplGDe~PfyG4SZdEt`RX5`Jl$cMZm*y<6tbU_f^-INQRK7BxsK7xW8WiF6(Atmr{C*QW5&2(6 zn6fSQWeQ4kO7By~=D<5=8?Ns)vE|gblrdJ$=&@U6ri9nTfUN2Jb*4IgfR5y3gU0L0 z$LQC*N#^ORZWnj$ykAWZ6`~`(=$Edu1NH|KA?#AaQdaYBj1ZG<#zn_sbV?)4dtE6J zw0hwIO{g)pVh{R9nQCQ+MZgmlj(J7J>z1ht^G0 zvF(y>08JG1-d?FZ3i!v|ba^_7BV(A{wF>^wch%(OI~!;0+491(S+EdHfm;W$Q%-QB zr;6HhEenwxuGibQy&}dAzf?33>pmc+G`y^#8{-;zd9Cuyjtzb(@V-ZAQjtKeW9N+l zGl8Y+GG_N61>HaU*zHp$LrvZs>YRm=67R~z$MDY`L@Igrs?FMm2=_={{CAuBFK+oW z6T=A8O1!xqLFXA%1TuqvhLSOND*t{c^qm0ktO{u67(Km}NANqlaBTf4tm=^3)BT;c zvP@w5lMf=g^Pl4mT|8+XN_1pX*k8`KUq#}qqT}~L=AWBBXVaF@bqJx)FIYKalhCOF zvdCsfZAeqxxiOC8_1kCFi1R<&pG>002{_Oetw){)H2|Z)`q)4liqdX;wFb1pl;A@S z)JI$r`0^FHa&uF?U-5yc<$glo69rJ7ifOvZ3y{jnV&RKb?Kfwk6XAjnULDk3NeG_h zA@2Zc@#N+~tdEd@>ftwGuOi|&bU#5%>}Zns6JDK8eG9F=A6oop|HpA|HVH3K z?*%*o74K~n?hCDGs)iKf#F8$g&4p@iHWA0uTJ&Zy#gY1l>bGAql>VIP-Iw*Bvqx0p z-k-j18kY)y73|Gvgm_)11u@w19|o>oQ#u|H3m;Q-E)g)){%x_ZG4e?X^fi%#-`Kw| z!4;#86c*?pF^VG)ghvha-Ujn7Sx>k)Espw6<$=)YRrB|RlnN;0T3R8YfdvIx#Ot7B zS}AZfUr&&SyD6JcK3vUpjF|tmPKj}SBT=;TgN-QGNThYJwwwwhHNh-4AcKx8l%&yJmSSd6+qH4mU|mz)Dr!A%L41%$!n zL46|#AH@L_1VEvCB8Oc5th5ctm@W6WQ?Mat=sk!DcKAD7@cU8#A#E~rkq~+j2jZOt z$AS%r_o0E-2IfAOeeMms5CPbvuDlxlu+9U3NP7!@b{6r%Dn}xW^%^fu(+@m}ib)Kn zQ#>)`jE-NGK?q>q>8)zgT-(AC{nBLByyWb`E%dzZ26JjkkJwZw-s3G^yV(V12id}T zXH0(=O9H>gmEgrr|ExJ+4x%deXl8ojVs;)Eqe_E_LJm(nF)xH8PSPd`fN5np9EdR& zS~dNk08YpF%baL@^}ljBR6QWVJuc9xsC@oJo&k1y^`;x$(y~fzZ^zNk;VlW9>m9>r zVnUFBcq$RKEwE>y#Ta`)vbuGZ0Ph#veEbqC;CL-FPc-;s^y*wf5oPdP}& zF86BbMHuzV_Nb-NZP7k|q2(;{V4Y>kHzx|FvTr{ahw|bMBA*vCEZrF81X(#IkB7!z zF7^9kSVjy#`%q1JmvLfI;3~YsGidR-*Drq9SoeyWP&Xq8xo7+NVaUA; zInzG4_1k=E6OPKo=L6-4*A>|(x}oJK3Jme13Ex7i;3?Zs_y~SOigQ=;j1M<0&{Zml zg-ttC;U>lMfjtuAVZTfOwy*Wn?P_Aa&@Yjx0ky;uQ1f!TQm2B5-8JW~c)H?4S+GHO z;9>2eH1pDsSeLuKtM2Nsg^da+P{i%4@!l!(k~9ihb7Hy5cXAOvj|bH_^kJ6r1B9E6 zA?$R6bTsB!fszhbhbgr5#Gmt?rWJTx72R&VU$<~>X8 zis3ygdD5HfL_gN=ma>e@N}z^3j1XXs9B6}7bun{RGp4N;9+h8$H6`);2$bRy-FXk! zKg1+=^)MllX2I9sKhB6c8HoR5fx}&~!-Nu7ZrlMvgpEi}L5;Ec=XQG=fh0rUmlDws zKQ$@T*fW&*V(4%l!7CzW3sa%~d%RJ0Gm^tckL`j#hw^17Vox`t$W584&f8 zQaxk4lx3p2UteO}6N?z*xE6M3)v7T&#t|78(ERdWe*fQm?^4aV7YgomTBW0X5{HcR3`^jd6UfCU9j0SLjv7Irwr+pt6z^7I+7X3Z7k z1cZMIP;^Vc^LO#DT)%q^k0L@IwOaE-t3Int{6M`Nxn#3?Zt0LqcvcPkV^qg747w8W z>cMNgDHZIMX#ZL9totf|1CaJnW;re!tVf`ZMgf`eLnrioj+fI%;m(92q`SE%o^2x4 z>65uSLD>8^6R3U}t|Zg!*C5KoA+g+TG9_11`A;~bH@H8(avZwOM0fhEq+A>lr;8|; z4QqCSkP(9k>V+GO22BYv4a5lJCZH;DtS*utVZE|*aA>aiiDJ)MolO5A ziCwymBZjA}kwh_WWq?)vjKn3*y314iaYhQcup*)(2c`^*l@N>)PMFqO9X6q1eqJ8Ly?PCY?jjId+t#xa9yF~&43~LZCVr;nQ>WeWy!npy$FqypSC<64nsg2w-XLBmMkZo_E#=0h z={I}7g_;nMTL3<#Y6aTqc(q?Y#kMY^+i-oO(3%~ZKV{43Q!Dh0ao4KVp~U33@60p< z!@_$wJh7y=N7IY3!(^2ctUY&$(I5FzWun;e*t0RXbr{|<4o^LxOqN5MD%;iA<-Nv) zxRkJB7u^X2?yB>kGc8w_z3GdL zRnwmrkaFG}JjnkGc_3c{+zKb64gEle$v?mFJ!1 z5P!pu8sotwDA*>VmL8!t3<{h4`36YhxinTwl4MjLH$N^a8)#F<7!hUUbmb#!- zAo$(}ve`UAMz6o=sPC%N>dsevt2|=$%|>(%W=CxaP$ug^d_M6$$SKE#Ei*lb_XI#% zObnkA{OM&rrgd%*#ElCG4%lHuf=b2)P|FDpF2Eh#YNTG=c{X?O;t%@mPa0Z@(N#pj z9?gQI!5?tin4~E8XTSp}Jvi=J+TO+nJRR?Nx!Oh#8Z!!qah6!}qGYN^kOzi<3qIvY z4xZmp*_a3Qw;4TlX6B}0y2XOcSLp5?elY{^{42mzI)B^q^P8*_@eoY(&$(UFdk9xD z*uZY>RY=<9q*OEmY^wWA8u@NeK=Pbxgrg)k-;HiAp{tyWK2w$#0|VKvLf|j)VtxMU z5vF`e#Q68>qG2vj<&9V(q&0d+%ypCCH4F2>{mC;oNy5!=#n&+)-aWCtrlE1d(8}=1 zF#c3%obXix1?Ry{7Qs`$5~3TH#5{3wj3_AYG`POn+z~fJTmw)VxZ@sRa*kHEdOV-? zghkXM*Qr}F0H(+F{O8@oP7hlo`K?@!OYc@nZl8FFBTT3u8RM!`&w1-%bhBdaL0|wU z>%?he*RT^6jlD#usSNy~-k$$pHoYo)@Tc=4yi5)><``NG(FM@zGjBLxLp<63G=p$3 z3!D`?aRbn+W1xO0|yrY&43VLK|Li^ z4Rr2GzBJiAEencnu9y zQ@sj@a0xJD7S@}#mxf6z$9Zvz9}swOLRz#qk?@j{v`I19=qY>J3zvet=Kd8b28DT2csN$G+)TAxtMi7iN2Mj`Qyl$DDpEeV!cnu z-HyL2tGC@rGMRQGm;pAr#?0>2xY8dvyKlVWLNdHe;&CT8t7}kcq}imar2B_wN4`M} z8&t3>>v_y`<)h;cR|eP#x3^UCu>aM#a2<5XiRPk>QaIV4?cD~lt6LyCqO9f>vVgTi zfFXdFm?CIzLK8u%JxNjipKfTG-sFcJ>@a7u8ABUhp9~#?WK^K{wqSrW@oAuIjUrWt zHnd@A94fa|Rfl(Js7=aCVIw;y{i)i7I$?aCCFUlrIq%VNn2BIf?QfGrRK8l{6U+|k zX%D={*gt&KOQzC)UQ35AL7N$z<;R9Qb-kky#6VfFKS*t`8cqxxi z@@yP-_Smsion`sv9Z#3ALz0YI;P7>PT$lGi&K2jzTz4Tw)U{ z6l=>HP@^YN_2g#$;tZuS^ue3*+Css#2h3SG1$nH0tRVe^8G%+e)f4Y}8z}q02}_p9 zTCD>BJCdePmyMWdDRlLFa!$32GvK2*Ay^8E(o(;P}yJt_T~1e`Qu z|0zgn!fLxZPK9vybz-=QH&9`tIt!s-e6R__c{)>gvI}Y0iiqDp?wL8Zh zEy9iFqW+HYn$y9cfVTJ6|Lndn;kkXV(xTzNP&xM@En?52XnU~! z>K}D7Ze8XPw52o>U(8GxJd(sbYP7NL_ANe^Xf;`v8~Ep4GzaI%1GsO`gS6aGL=y-W^101A<(I6iLQ-e)++Di4V*GZrtL|2{*-QK| z8x4COl1@FySpYX(1fMd2p1exxvLA5ikuaPU?y09`j~}GtPy0dTN3E*JVSQj|>-Svl z^{ZegL{Kllj>;2!Btiz9?N7GOqMpog&UC%y3Y^MLBC+`xCCgk7?XqY*NnNhHmL{+QxTv6*8`9Ue>B_~l?m zo;cr`7Sx7b`)P#(&95E9r5@ky>cwk+f{S?1-5Sqfy3o{d@}xS#tmB@iGZ=fQ;#~qk zE=vIe5zIjJXsJp0x6dy1iODMFaYq+^P~h%q6R4>%71rudUwRbSj;vH@0vb+^ON+s+ zZ;n}oH`2|zH)Y;%Z^|p|PUZ;*wZEIZ3z5iq1@+HpitJ6M%}`e5tMToX@I)*XcJFRJ zk!4Q{tobZ{+}G<*4VfDcT{KtZ`;sexM*<1>G|Xr&>NsLbG#3GkU${Rl+!BL7`pWHt z-2qJ~x+`j)b{qpP|Gh=m2pis7-K$n)eil6q@RY%gaW_qLA0LcvDg>9hL-wJf^8R1l zTJ=XpStC(H$tnb%aQya_%A|;&HN)f3^*bR+_~$9(P~Ii!O?V@ho{R$CMLPvS`^{rf@Nr-ZH5R;_y#UzgzxUsB0%0QP( zp2ejwF%IC|P z|K$6@ zfWs$A9;6;Prut-(YLA#=6+!*W z8ivBspOT?hdy#7meS@5WsLKfH`;+=#8DRZ(I*)}fLgOV0=y&)I^=k@P$zZ(~osQmk zidpn}GK1@assRuS&MRN6?ZAjq4R5irRg z2uJUb$>VrqjDN%yl5Oi|ZQL79Y|#zx0$+Qsje)tH^K5 z(l8aS76&&`OUj#YK3vKM|D6(Gk_z-1^=ORpp$oA%c^I>`G?#7PSr|ZS3l6Z44D^6V zI(q-g*{+e#`B2&~olQ$;!#-JmT{F9=$mOdWRfoNYouO*ZMBi>yOj>Xc=}M=TZ3oH! z+OY2v(k@(&YT}%>o)w&${l&_=g`195p(opp;vc-zIXu|D^C3AmbWkABCd|m%3aT$v z`K^dR*LN|XoDlR9^iu5glrg57+63gVJ!VYUr`_2!RPB|Ijz2%Wqg1KJw{w*`XIRm& zgq+?v8g#KnJ!C>-_a-`SrJMRk-_4IPy3J#F4tN-sl zYO}h|6-V{*29^iPFMXcg<@r7MOnXd51*{#z`~lY25wGOzJFJ!1hccIYsYKVqT?70I zE`&D6wF#CP5>hY~-(q97;M%`U?KC*)*fB$9N~RvxRGN-o@Ol2)Q1yKE9Xo@P&?g?_?em*V&6E`MDNCx?%LBWIK;=WWD z|F;#N)OsmSO!+|UfWHl#4l*Y6#`6sl;@*}}0qU?^yV(+5#0)vH`Z>)gU#U&h0kM>E zus*Sz-gE<(p+GMyL@VzmfD6v@rbbh-b7hou)8{bWG_d&DzQWI~kGN?fS=jI)rdNZ} zTG+XV`~6MeqbVB-m<86M_R8qSC}~0W{H1?l_VMW>UtsoQaf^z~Yt|F{`5eTZ;~yRf z?=?NK$j^0SVY}-mAL4ybxYbfPS}+GZW(zuujp-=>?l=VHEZ%o-70DJ5Wc|dkm7lT= zv-+@cjZ|qHyba%Dh(~}-t)OpXpSP|b#cYj1Sd#EAt66l6GT-HOEPmxCZWi(JGVn!% zzAx951+WUy%^k(Ob>A_vm9!u9CyqAP&R4_49?igzvD{tU%s*IoewQ)km@LY^mK^wg z&VlJc@97~o{Xfj`hryF6uYx9XQyWxh%jR_t;`kd}Ce2V6h`e8!$#&w#x@c}3AdN7m zgG>1_H1bh#87=(!Z1T|*C%gKtaV$4*55*>TP|`t`?eU9Ex{vNB#j0Y}G5qoEW5>o- z<(Y|Rd|TDUs3iu*ZFp5!Uk|G%?qS@vf4Lz+q!_4)MS7?yJ*a}pEXb3x=1@mm?!nWp zJ5n8eO4ZL=&l>)q?>|Sv?fz2)49{2SvMYU*BJO3tP1#~9 zh$>cfF}VA>VL5(>XR|uxw~>;g1yt+cT6GmD(3}3Zy(Mk7iV;_dTM{MDUzI$x7PTCZ zdgNV7x)B_E5E+y3Dqzgslow&*5-g_{1`1i3ll@?0&Z-b!s1dv8*Sz6(HmfH*dTc34 z<0AIBT&0%Av)#bGu*tQgiD_VZ%eAX~sSy8Y*{#;OwVMiWjSbe^Ex(L^i2GPW z)>taX&Hrj3Z}@|)y~I$l>Kt^$RNlY3r=rD842WXz;iR6T)-{)zn>MTVnngf*2Em10 zuI-Ye#2-*h-F}ntgQz`k9KGl7qu8~k*+Qgq+IHH0xSutpi4`x+BSDz+@4%unpltlf z`ALTD1ngLnIJ6#UAynsk{?4{>9i=2y!6*E)b(`QT$74JewF-Jayb zbba)$e!Y(LOUXQ{M`w)_H6BWE^cgitC_>%D>g(!#kG_jsYYzh1R`u=K;%Kj~D%2j3Y0y;2k@4Zi*_Pfz z9oExqrep~Hj}a&-jEfJdm{fX7KOx-UB6d&r@jhk7KMh35!+~j4Ua!lnAw9fLa-cpR zM%sVPT@87B?A{rPP2TJ6AmGcf+h+Trds_t`N=6uNiog05`$JopKi13HkJSssaam_A zCJpLO7y#mJg}oMBn-=b0VG=F!Ukr?R3d+=hW51a@Yc(wn$$ccn5ha(d_=J#;z3zTE~?h9?HiGe%B#&+mZ)u?ytBN+~pUgy;PZspTyhema~mgD&< zrO(LNXekW0QH}Z>E#v%p(qIqQ@>&BrHH?yPf4gXQK%Y^ylUYarTlh0!76k8Efi9h| zZ^MXL`PLQv>FaZ*?+Hf`Qlg-*Z;u^+g|}wgE4N?*jxH`5g<0%J(cf+t2E0j%?c=yK zrYq24zuaWr)%2<~avSn7=Y2(D)b_lAY<(g}YfpybUpX+NFp=py(q1rBT9OYwaNHo^VS^)RGm%qRI zK4Be;>zB!$aNiX=^qJC>Z}T~CU* z?z#k4%nmNoqY)fRT6^+;I?Zja6M#Iu`Q1&*fo zT{Vs@EGU13();IYj>4l)V6x$6AHZr$=fmIc;$BXcVI65M$NY~-RW4HgcG#893&Ihi zIRt@Qd303IEn7XwLQb}aP1R!~1U9F+xzA6XCJ|xZhI82ZX2#P0*|*^zn2q~7Px^wT z2|wR`FBh5g%hgqWgQHr)+^NEPiI`g!LA_kfrv{GABlh@@%?Hyk-n{3}jyHm~mh@|$ z%2NasTvR%~y$u0b@%+x0?`llS0vd%e!}dUJ_e5S%i_n_li^QF|jmy}V#8u#8`WHxI zp^SEC{nKT;J@=<*P7(ErF>q*8PL=gSQh)41g40L3`JSHwrjYHl#rv+m)o@$B+qY#j z=vANXfdBqAmMsi3yMB5e`Egjg6pbJ_o66>Vw@2@vZ{G5S+%cBsVGahy2s7{F!sU0N_`v2pFXlj^F47X6@p zm2YNy=bgt<#%t}?+8K9!cyPzN2PUw6#VxUz{Xzs77#4aMzce8g12#$fHT7|GEGWa~ z6dJ$8;`SRFvmv!|&{nR_#HNZ`k7SuYJXN-&D+IG1H(2AE&Cifrb{9){5d=ezRB@D0 zg=@Y-f3a*DmzxF~fU1LJ)@Yq3$>b`Jb|#5R{*9>Q)@fBH)9sVXD_la5h^_OLdx4j7 zUaXSkm{4J!zsrMr;be#<0J41eEH4V2d+A3J{Kh1-Vgk045A~NDAOWE|6_WhR1`B)X zDUf5=kl%jTSqk(c)#>*rIvi^r(C8W`<&z0vJhXkmMV`=RjmdC{;PQ+sHF1k0SxW> z$IYq!syDj`bOEh`bbICOZv!yy`yGpRYR(nNGdO;n;&T5reWU_;A}iB~O5R_x^ky1? zmXDFbSZcoJt;v#czjNSIwm0N=t&{Gm)Wu8``kvLal;2IYD+cgOIb`9!c_Nl|iR>YJ za&T z-Qhw+`!cc(z=7D1pw0PVsr9yiz{7diIM@IuZ5@hobb@)BNEIUIEV*4k#Pw-M0%Edf zE%P44%0Xd=BR?xST%(;M=hHNot%5K100T{1?tXO-F;J;7oqOWDJzdDO(6$Lg)xf~Y z&!u_cG26ZaHi5RA%H!D0kjh{9llYmB_;4p^3NfyEh#lAFkWV-Gf$Z@?$I?vLDCl3w zVX~gN|K9&cc*)+4RJpa%diABztlH=M=@h41@hsF7JDe;N&pC5G?#$1ha^P_HgZz%^ ziX9vrFO`L{sRf-B3tdwPuL7(})nh**5SPDU7di?{UhRbX{n)zu(Xx6(*|fd-z1no~ zPOM&Ci$^*^nveY zi_iZf>%4=Sin@NSq97=sQl%>@NJr^ax^$7=L8SNI6QxLz-g|G-dxxO(8hYrF-U)%w z5|Z5bzTbD}&fJ+W`6DEA&e?0P{o89jYwhed4F`S(sBALh-#;3(!!!#lWNfV7$+epa zDTWMN;IbuMNdDop=ltPyS<5}um89o4H5<`8*=#pJrU>S+n`S?#%VkfAN_G9x?Yq?MgE>d;=xw8FXCPqNualz>a}h|hxuCxTHQA}&UCo_19{ zd%$f{y=Lh^1Mj!6sbS1f&25D%Am$zjPJvBYbDi|zG~)CQdC6VxVT!2|eyY_*mkTxf z*6Ns;H|jYmVSy)wd{>F*wreaT`4(W22(At!=vOAv8vs{Odu`CtRv<&W=G?)f<5+@- zPClOfxR4zdT_RI@B(Dp+HA3db2If@iJbJ73%XaVwQ7m7Pu4Ma32=`R#4j`%d(eiSb zJS#5=zI_Tkk0s?Xua^?_%e@(hQGv;fa_qwNa+ce8abv*hfs3iJLIdY^tH&Of|MdF% zs{%sNsG?&O@h1aq!GqN<0%F=)dR2PfA2HC)J4>Dj*{rQMmyUJMZK|xAGsYz>X4Uwt zSI9HOt)8igRUW31WQyB!wRy>_8z2ULA|Shr!y)(Gva_5Tu1+HwcViN5uHwn|CgBYm z>onT*=Y!yfkmIJ!ugJ;p2w&sD@erzM&x7_}xyXzC^z-^9eU`?t!k>q^xP-1!EMj)3 zm0`3OCEMiM)v7ud;MVSd@#kWAl=RC>Ud+i!6Cfz;t}VaYQy z_@kOh=J}|%3WtK$(hs{<)e3)<8Qtq@-(?7I)7(9uVWqQ@=$I%B)S_?aGG?)!bqm=2 zbt<_Tb~P;4=sv`R8H$hjxnL0EBn*gvx;OmfI$6{XmhU$raN;_C?{zy;Gj-DvZ%JQ-kJ*OcPARK1$0- zqWL_>qX#!jZR3>YOHKRQhxo5I3Qts7X>*)@ZK7SynUkt_Golxyn>OF!h2aL6~;$T7%+ld)p0lgIpuMA+f}Q zMXb0D@^{A!BG{{Rm@!8&*gB?YTO2Zz!EbM)!hHc+Jue4bLeWbzZD&1(+><}5GMW3| z-kz^VH0+HDpDTw+u4GJ*quUa^Rk=l(rC=2VTuFAyzp(m#*hVVS%rXf^A#?myzk$%k z!97KIYd7&NG%V=lsB6P}W~G|ebF7VH-KUg-Jx6X3T7A9SoL*Jp1kEy@_Y@DGDtaZK zY|$3(7wN{~wHT*yH~C@M^3y;=tc9KIehD2@M{KrAM--iUgI87_hJVTDO`b=@^o-a3 z-{A)L8gkvWDn!_c(+)be>+oojU9!U32l^fU>Bm7sj>eNkFTa61rZorutdLpM=B#>l z#!a-rs`tVucO$sCVotIcqeKp7R3=0^e+V58qStWl^A5#ia%lp&1_bxPa|cl430||2 zR)?SX=dPr&7ft>**ZV)TkhY(Ifo^*FQdJpg>azxxeS_MdBVmz1s$>wYUPF7hRhh93$N z^mlkz`VIb?{kL$QWChM2wx1Zp#v;q!$r2KwQEx;aMAHAq(xyExf306PYaG`6p^~MX zpkv*k`$n;V+vL-}`(qbQvnZlDhmIfam}$%x^@`|j|1eJe7qUFuu%m69+N~3dS?zz2 zAIg<>6+z;>OJBfZn*4CJL-2xOG8IYu z{3ANdNdZqQy#Kc-NwKllV(36LeWqd`t(L?`oBkh7h>(GJ7co3bvWhHZ9S_+-myy z?PshC!!j84IFC0t zY&)0MZs$)Rx45F9n2#uWKfc3uy#hR?QzPC2xN=PtJ7%<9l__d>VVk5u8b-3a z!KKW^i9catDo-GizFc=sKK}LNs`D!ZHJ8PLP5v&!g|M=&Xu+kJLy!@$N*1`Y43WV) zwJy@TES_QkV(}rJKG%CbsT&1th>^4e(I*|@)A#_7b=iQ_vzVX{Ssw>`NLh2nGp>G( zeaWUY0=$sFC8w}zCy_SxM$KE6% z;+A3m{B8eK>;zj3>@VP$&%wp{GFbfg+Ye`#DlQ};x-k^ED8_d%}! z8{x2!O^D~di0A4YgDFcUuUX(Gvw4KD4iS>QO*UUoCvbNl?ZYq7VJ%1Ue9Ljlz@@6v zICYxHS1qC7Jcb4w?0uq6MnKHo9u08Yb3`;>*oCQ)1k5Ihe@UI~au(Ax!Hy8vdJtFe zsP3ZC2MbH+K{aCQHy#1?1Ch{|uH5+^5g~sgxH}KRpb#%fag$3Snc_zWYS1N^y9Z_k zJmXCw#IB1^OhcY>kTblq>*g<_N?M+dXWF_=)pk7KB&a2B-M=@X`dkXwk<(@C%>QV= zQJ7#x-=9$`Ol?gyG=?_gd0R~j?>NIp>-n|=MG?i<2cmAf;z>S0@<3d=q!CpXVV+>V zl;A0p7dc~BQoXoUu>of<{NSrCHoZHljdo}eKjg9Q zJ{)2?<~UP=?-VlZD<>|3=qF)|TAW?SsSUWMZK}p*Z~U1b>dWn#qqx@ewubq2y7(Mm zMj_@=;_-n7;=!`lp_B#w@skvZgKVW(B}g6WqFW?HbtgRzYU2D}hw$7F&9LdEGbkV+ zCVyTxl;tBEsC|TuI9Nb*BU=-$96vz^fUQ3Q#f&a~oiF^|Axvfet)6!2gaY%kN{%5 zfm^G5mj0scJv8J*Ro^x8GH8reGzu;!L>=hnq~_3f&`s9m0kv3(bEoz8q&Y^m;(tTOX? zi#2d9pYplI3i+#=hkbSOEMPK|vT)eD?y(H|PhvISegVQLW*QTUE)`HEG&vS|;_m8! zHraMMFca7B?Ma$UQ^Gy>owcp?1TD1P1N<*EB$@oZ-YXmsJtq0)HoxDlm2JMt&%j8m zRa#|D`rze#N(hqd6<#A zxVu)spnmVb)Yc1TvX=D9J`P+Ut=YsY>Dl1cQ4+Qr>&y@FU)}VkFdEEVyZA=Z^~MTw zw34-1z49Ngn|-mT2+2B?Yu~wccP|{|%J3Ew2TrK(_bE0!64({SFA&3vA!6~k(F7Ie zO1R5Z+Js5PGKe4eclzCl$RMo!+W(IK>F-ex<`hE0p8?f?f>*KB?bt_>gv(cRq>KvI zFd(@xV9a{oG#4d--YKeK8{EHhek9xEFdg?4(efUo4AkDPFPz8&!~E}OOJeUMk6A!U z*qcAG``78H?cQg}^+yh)urbMqYEk$oqJnIqkUbV777caMb1GWh@emC}#nA3$83GmU zTO9g42@jtvCsa!e>SiXg*>_+A&WZdGL8j$XduXEh{eKCS5~IV#d2y_avG7Ax-={f- zxA@{`OOFuOwA`VVRcXIkjbYs$V@K${aDBG)lMq`z&#i?|Ws)8PIUYw@zHBMo!jQo= zveKEsE0MU%Y_VXg=$sETvjG1nsQ&Ba)$ABe*Q>D0SQhAt?cPIVtx{jb=uutb-ljhT z^}q=PWw+iA1WTkdA`a4PGl}dAA&f$vUabIRnd`wq$&bP7czyp0$Dbf~SZRzRtOk1! zc;UJqcv06DGt<~Tj+X!AIHUNJR5P&sFfD@59ihgjyIm**0OQ#xY=7j{Z`v(RKsPqX zb3@R5s#i0&#yhMmCy&Lb&9Wi0qBCm2VY}TDb=OTQX&-|f?Vt&$PS-=qNc-LtBzAJb zq(SVx`drsuw(dDtY`mHW+~YZzqbw=D@fdj~p}^vFJ{K5E*wS`sVQTYfaC?b?*akO) zs{50~GJDA7LP;Ou^zwFaOT`?p7&CIyynZc&V`xo(VN{RYWt#juetV8Oj0jNtn1e5x zKBFJAkdULIku)DzUhHXE3?0HXvmgwRaO~^D)f2wW;Ko5hsZFLPvhNo0)n}#1b9FQ; z(HBievoQLWav2Br{)-GWE!k|Md_dw&x2ES0&-Xg~gM#H@$V|6KH9?;X{Eu3R#VZaT zA6CP!%Q{lk7dV{#dxw0^!+mAY?Gq;^7lV*tj_cphKcB1ig^n$_L|0iqAGk=5Hy`_7 z)fgI&d%Ww!{Qm5Bot+weh&Y2vdYblc3-BMD7jMJZ&}^8EqHMgT9~49ix*BivVW-{8 z6qS%sui**mh~_F>P%MEMdbaW>i08xHk~-tHp$6q+eET@ZCF)3x(i8Xl^t`UR2l+V~ zyW^rc2`m^j;l9~lLcU+t}4j)t7Q)2ne>m4O4oFz~CY=b|yCZ+jKOR1z<=)gXA@ zIaM+JM?6z%_l#SHNwV4vS+iKSPv%I%WV{v&^RnBnKD-W4R}l&;Bp@ZGCm!E3bV8Ns ze|D@ktnJLqB%;4BzO-?n<8M;FN{WA&P_pJMU$)2{-_?c;bbqMWv=@3*$?XW{_HFpP zx+~X==`VIp?o+&PnK~xfcCsXsU#88o zVVbE7PrEh3Zf$hTO14pEii(Chr`2sYU#mnpPUi~e!f3a}n96f8MiH1}@^wKKkaLkm z4;4Krg^9+|HH~;So2X?#o`hZL-3GmXD?*g^9DG0DDaLq1U%Oufw|aO-4=OkU zO0Yp9l0eUFN5DU0mL_}+NF6m4^L5s;V&gSnSw5};PHO89-r;{~j0;%U?X>v@Zu=un zm$@l8atlj79BlRBl$713h(VETZih+WyDY#sM75Il1$?zeA{o^Q+j~3PQl)Uxfg9}f z<|<+M>9-ekqx&^r?#kt+vTUQ3OXscGPvqoD3urG4w1znJg8Rt(F&Y9;&{V&8jGYHn zS6vSpO#x3S7!Biw&GY)OWqZ$+6K7R^xofFl7=LNpPALyjkgm}9tKaN3cGl zfM*iOJ{a($f(h`cvCCj|BVcsL#n+UgWRsQ=<734nO^#IUiUAmoKYrM&%IdEkiV%j) zSM1l?gOpjY@G~Alx1G{VrCAXR=pU^1Ex!n@#NKVNT3(`+b=Ez@Ic%^y=JMw$P^J+q zY2uS)VhA^p7H`$j$e?0_$3sjXa<4L5pBVEV*6#cNKuMe`!Jh<&-CnaJRyv#~C)oOw z_vk)Nd^E=8xKwIpmXk&UoiCI;mr-rYmMd}Qc3+WF9%auayfv7&Wp-ua(H9?)tVduS z{+E}(Q*-k{+9zj;pTIS6xj3wi07;L}W$Q z&(mbm_iOHoqb3g50X5u)&khTJF%Jk7J8$#upUNP8Jg)PbXa%=Y4tJ|ae61N+F$*$%!R3S)^zn90#fwx{q{kPBGUaWx+w|cpgXJ%(Z`;6#N5sFY-OQ& z+wg7~pF2u@XX{DxLYQ~80$fjJ>QckX9w7EEJ=DYM6`cNevM}CO)c0^)&yiPRQt!gP zDTfXVzhcB+wi-hspPKJ0l_Vb9sbg5Y7F){S0smCi#>CVXrQ?avsZ%Gm6B#zyc0K!+ zQ{W|#p}k92)-2&TDp;AXKnY4fNjTqnY+Cmc17iWm>Ts zKJ;PgF{A&=T^4UD_>1woyY^n5<_?{-cSZU=LD1RPbu7mPGyIUP<{L+UilNR$jThdW z%8%XdiwYy(>rvg1{6I{u+hfG%pyiUwDi2H*WO@}Wk-~`0GkNX#FnuT;AJW=s4>%1FJFBr&3vnct#-Dw}G?SXIxsbd57dHuA7 zW2xA7;k!=V2~L2Me4~Y8Zblb}`g6^3^2wMm^%JiT)c%5-WkL>DQqR zBx3T>J}~V|fZy^t$;22RGFwT-5J4~hx;EQ?mPLOkQZzzSkBm0Du$mEs8d)eV~Jo2_izJ#sNlvCo|HemhClzq#)o zm=@a|I}gvo6XUMhQb2M+fShczz(R0>GY&#Xls>@m8|kD5HDQ?5C-z^rOP-$GzIsBT zmLKjJtEsoA&YfUjpaM+^=TjuHvcUy0=SoJTGrPbAE4X_Rr`w~&%OCemRmBt zSGm1Nut2cxxHAU=Xzvm@W#QHmU!6A^%5`CW{D=Ru-*9YtkJeS!jY$?~F+4tqCB?6p zqS>aKyl|skt7(2aR`WLg;K6G*+r#`n4~|O{^be+)X+=KbY+-f=`6xcmh}Tr~!GMt$ z%{Y2n&~~i2H1tKd!762zFYklY6$6baRF7HqT($mn&FSF$2mVmhl7f@+irNnKWbwjT z1~5(kE7t+NR2U*%&|Kb%%~wFT=OKlXYOKV3v%@M|`z2g78hj%udH&%9^>`;LM>xRf znnWezLt>GSp9iP3&mYr%-e6=Aavft+I80T*p9R%Isy^i~R@imho` zXYwg2=xUN8mq`H&w5nGxGBeTo-$ZLOAFymx+ zcXEmQ)1pOZXZj!;IKfREvv`MR#Z9MH2jk=UWXIl}@f&-I;dOh}Z#vfE_Rp ztuIXS$5XuikSyxGP{%XWg@yNI@fAk+R;&{Cq5WbD79s~EfeO!wbgA}o4D}Q5w%hUz z=?^ep_0h6Pd$gc4u{rkkmE1+AEjBFXb1t~V0Axc&l0N?4Mz)_*40 zXu))rG1OQ+W%}HHgp!I3I|{*dY@;_Y`4kUfWmha4%(zhF>Vf}w z_fGt>`!*zYO$w+T)1RgJnZCg5@roNR+HBl1_TF<4bMv(E3Y33el=E+1;6B(oP(4CSoU8?Mh(Tbxafx1q4&Y5mZkT6MuR(R+d0T3h zOf2XI*KK0p=el)Q<&p)^kPr&=-D2Q>hE{vMBv~l`oqJg1^h`3@GYW-f0gpvFk;Z~_ z9nf(9u|l#-q)X+V(cLLz_tB~150)%?1jg`3iF;OYD||9NcstmR`~pd zwf5+2&QIsNUg~GHkQ~QVw`C5^{D3WiOfA+%-8Fv}4rSe{sIWJ~CoMXneB1tMM4O3< zT*kg>^>)aVu+T@#9o@BE%Dq{XQ*}=|s>?;Wo3hkHTx9mkE#V`UH&^uZlquqc9m`zD z?Y#?nLwTK@4nEw&iUaAE?|;OQ9R6LWyYbf>sy=H4VZeub^HdCkhr$78PUcf3xE%>G z=|vJma)z9=NwWeo-0!-ahsBpen7+f|aNVjqLmW*9;4JX6)ij7RxKOGYWX}9OlR|0e zSq}A}i6M;n-*$lu}{=n-C75{TvKXX zt$Q;;5O~uVJ*JluEmv`` z_87K*QTCU})?^!-U{G^B-kIWMO1aOfVo5)GNFKpBCb8T5Y415f@(H6qr0zWUrALq| ze-srJZRNIVn4j<&2*e;l7`x~x>32~tIVl(;^fpR4_w$BxW_!xTKh2EVJ3=S_U16B` zd(&SEk`6|t#2jwYQ;pHZk|a+%mIf2kV_YPx!`7Ey;?Oj9EKMudha7&_+aP;{RsE{ z6SMG=;IuEoa3PKQwCh`0-=q6y3%bSEl$#v|5>S`*v?az`v@fLt!y|S(8ISngW$2#9 zkYlrt9;k0d?J2H9IRSuX%MrI{Q1;~ol|4=qliFA}(hw9XA~Hvb3AVFle~lTql5ZM+ z%_yAx<}pLHnXQp~ZAy#3b^vk}o1^-pK@t0(yL&L7!K-L8`AXw9WWANaKPehY)_KddNrwy(o!$|lrrGvoQ$JE02D zG2HP!qOS51MooUHZ5rFjuCxAs`3G~A&j(c4)qiR(AGK^wc&rYBWLJ~lpSNRg*@a@CLlyo z!w4H40?e5aC>I@GeBua}p?|#(60393%2f_UHYni!{&ln&r>71`k>2D&{X*>E#P;h>Yh#x9UD7fAW%-v|9EFiPpC?&p4TK}T5VJLUqsTQ^|99oD` z)D$zJ*i~3=ZM%rhQ7hie|F!iN{7XNi#qKV!pXS9E+AO@fVIKU_53jublRR`9f2gm` z06nL5Z1*faD49E4ti&ZBKU99o*JNt9(>BIYgx*zy3Wr+yC2GqDO>ljpPRU4m)3LX4 z#??5?qPk-JTEw08MY%gG#liI?hk7T9t?L>i+Gw}g-skuqShSe}tWT}8S4EXqs0By;6vrWDdiyXN7OVo4&LE{td zE5|FFuL5dS%`Uk|?JYXHiRH0J;ymYg1}C6mae?>iRYu{Bx8|S<pna zpZAQa&#W+J(c?05e|#rT-L|SxWlwksLM_i~rv@B#D!j)plvraZEx)W1G`@Mfo2b9T z=6bZ63}dfDbp@R@KkM<-ry{xY@Lf#o5rDDMR2lebV@W#M{qJl3W$CcR;Siq9X<||h znH%c@p@|RC<)*aV>R&0NY2S64Wxj~)!X~AgFM5vk)gM;dWQrghCFs+!@Q}OyIf!074E=PaBUWlj>1+$E>eo>M6pn?x-72*_}zGEO40^u zqirX<*&2jcWQMP@}1#F@1YS-)f>##J!@cfT36ANv6*1e_wz#!ILF|j7vo?TT86PW@m zq68{~)^0?5A|A#y^Xu1UoEATIYdHLuwP_(O4x;ZO*kGhTRbCoZtI~3 z>8g^F;NaNJ+I$-|g#%*!$IZ=QDk+N$Rg7Xyql zu_tZ9Ek~U|V+G9`q15Vtv+fviA@ruo&%I8YeABQk4a+5xLibE38)yR6O#?TxyEf+g zZ*k<;X5EQ9{Pau>pz_^oeRp>l`^xjv`Gf5?@AOUYRmleDp^1~4wNk5Jzpgh4NFTLK ztuLz(MmD*h_g_)X>|)Q{4Ql9{BMY+$Q-KKgucheQaB=F&P*ue_wij?_kDaE}sn!}g z1;^48^)9ej+#4Xe&n4Z2>ML+Lj4-s#{V%O<2w?=RqrKn`mH;N=^^EwKiZsHU{kspc z3~o2;4_>L?UgR-HrWJ0&etp@52{q4JRUfzj@A)Inb~VM4#Nzd_+ae7zu4vtZnm64Z9OMTz!$@vtU}NAjTo;ne z$H%aw{1BnwTn=GA-8T+#@D#C;(Nc}Uv-nHOLypl&eX-vzBHzKr2?0tECE;1P3K4}I z4&_Qgc-t$-vmXyON?v+dw6(Qo^76B0DxyQ3D6?xqZz(FyB0MZ6?pAzSXkL69**u3T z$JuE6gpvFeO)RW9n#B0p%9}_+uQXVC2Yjb)Q>k5~P4mO?KRk0^io8J!_5Job9_?N>cW*@~ zfLY5A2y)8Yh@-07CopUpD@}vhREAz)d$1Nc6!iQ!W#YTdVrE6k`n|B*Jy>7p#T0L}=H58X;6_ zX)acWq5UXEH)p6zhF;KJa>;!xi9@sj>m`nwzByMPa(%zdlIisZc$^EG2v;kg=o(Zl z=B9UX7?4^g5CYY5S<VBxND7b`FecAZ+eTk4uYDxZG?2Suf6-$oO57I1mF_mZcCKBPBQen2VgQ~0 zGMAHTcQw=87FZG4p6R_Xp-qw|V|`S7W*gNTO?=o{azTkp9xz<%p0vHQpS>^Qr{p!? zk~w93q63q%#v&CEfcO=a_^MAgW)mG4w|4X#%e#!|%!sqR!xhiLh(%q&*lOTu^O#5l znF#*7GrL!}aDBXY46oGx+E1G{r6Wwki0BF2bUu8=6;<~6rSr5VVUEH#V`Fp=z_=Dv zh}|zpK}fD6yhZa3V+_g4dykhezJk`4kijnr&(m)dH&_;*Qa!wSS*#!Ebq`8~B6xkR z6N;mQ{|2Pr?|pXMLL4AEk5MYk)HOs}nGgXg@FfC@JN(-G)gwG;1qXYfeQFIZ54RGw zok*_cJZ}YPWb+TJXW*-+-O8o`N}i)Awo6P()tA}8txmJh%4?gzrr5QA>}UBEd`h%@ zBJ`j&r6RRwJo9{lZ28HLw^w@>YE-zoHeCo@jXt*K{f}qw_VCab53|aBOR%oQcIq40 z*>BCdLDwu+M^x;VVG?!-xM04@=l2hGqB5xMe@r~6)r(a+4mrL#IToZ2McY+tcY%Yz zMxWz+t~SFh2_*f=m3&1Gy9yOFBAkZA2ZD zGJ|r~Y9QwV!FCc}d^-uaA#6YU4cuB|R)-!w5%CytdM!=ld4dgOU3Sv%_M#Z?(i0}2 zR4@-sa?kU|{(zh4m`CU6a!8O=rP6Rp08s0ER;MK9(knJ?4BLK?hS(o3xC+pW{%GIV zh?;T7hCt|DJMvp0nu>(`sDpX>(`Sm$%~BiNl>*4XHXPO>y%tQ|LhPLnWX2 zOo*<#iQS?)6}<`Tk~)~^dA`+G9G-js#6D@lnC|D=;_2Ph>)d6x8Vb`a6mN!z%~vP! zi;q(l%BF>dz%%}K#LnT~dTK7NSmgF7H+f_qH8cvIwiGl~S3qA^;AnVkZr9cxcf@};!0z5 zarP`Xe_hmb;&zC`PM||+Yg>}`yyi72*3h;`(q2N~=pMH6f`Axtc+O5M(^nQn&6-9A znUCwCzI#t*KI}*3m8Xf+^jr}^bZD8GhxQngA#+IDT@KpOI3EO662Q2wJqO^Q)uR$L zlx6+5PEWfWsW6fkPIuWb9+x#Db$-8oe+)LFsV+vJILmw2^sV*Qpl*?9b3{wO!~)ge zM?ep6ArItS6cI{T4>yMQE*93jh~IAPXG;?4Lrn&2cXD3-?iP}7nNVkl6^A>8N1zo< z(spiQyA_{5{2@zwzm~We_d{3k?*U-vsK0TH7>*Yvh7T?muOC6J2rln5s7oe}2_6n6 zYLQ7sxu0^QA`H|EmvaKY&Nh=4`ADy@Gky7cavW>(*6q%~x5H+MJgeMC46Xbxps1&c zNj*~?L((Y6CEcgQ_opZmhB~hSqWs;A>M_IdEu~DQv_@PSzg;sF{P}Ujixa_$t90}% zs=KT7X5BiB5eO(w3J`sRq{?*fF2@^GaOHw^Rf~vgvobt5ChgcaCAU|fM34_@}U@&QlClsZfSZqM>R zZD#N0_>yD%&O_Z+$*y#OQ0HjQEzMk`vR@KR;ZyiUiHa`?%;?gbU+7fGvFEdz3T>ucoXbDeYGEk|`BUNmZ6t2ZL1JL}#0cLF%L*5TZbAH?Sz z#xE9~80+qs@242L0{2ky+6#e9RqWt+Vy{79dP%d({&ci3PTIE<|TBpGakKYV& zsHi`p2+DrA^ob`AA3~WjG|jyurnGalewupr(BNL(oe6>Qb9A3#er2~0D=_yqveVDh z=)-oX5cH@6jyD;`xIXNd@%MfAPhhXW0wu}}dS6H<=Yjj&P4?N@ef`bv&$rW1YpJ`8 ze!N`Mw4m^A#!}mK5h2d;C--yV#<#T1znd2bP+q9pt#luQ6&hgo`(_TwC;lm&Gzw;& zj8}8BX$XY{8r=WV1OD^-V(%xj&uKlM;x&=xWCoa1_}bUF7rFf{<|M?;zqL%GYLi(Q zKULeLLr57z}v>8<|uuZFAC2WN?w+N}{OAr}PxaJLNe#L)s65aZ*Wo7}|kiBVE}d&l(k$-)hd}lqFxIpe7yxMo~Tn z*%|wrs9t9sgZB-jCXCZ40M(-r%>l*rb%we5EK1?ps@vN>yzOvy@Ua^`SV=`*oW*)j z?i;xK!mjP;#Bl3J*hz{#45W$i93*xdAx<4mcI`eCT=O67kbG$8vyW?i_}b0jXy4Dg z6D-_f!i;S_dD+G%F+a+4CoRyW^5;oA@8S+3v-;!KEG>89^2W=)*v`{NI{%9Y5jcoq ztvwKNCD{MUqf6k0@!$jRTSE+BHPr0{SgpF`YPUj?wb`XENeMzGe_jqg^&0HH%`xm6 z6gy9;C|}OAace3e^GU#f_cd_{kdM=x=2G16*!1<_A6{37y=$Jzs5P7K=4t~^FYqEKxt_l>q@uZm z*HH9=bRJ{beqiUC%o?ttwZxc~+51>AvrBt<;No5l-(=0Qq#ZN9v<1_{sATz9#$|$^ zi07B>+2fkiZ0t1@?Fva(E`xV`UdKMD*qW*d9P?Z^^PYGRi|IKcD|_@RZpkGn$QD%P zMW2RCSsm)?%P*Bt8>(AQs}R0GPa?iMbi1%t1h6G#7qo2MLQLcQbNrNY0ud3ir_Z75 zf)2rsb?VYqo77}P#^Ru`(vbQKY)L9?RWSI-1(KH)2orTf1=kF3l6ECLl-Lx#{oz>4 zdHMX;x$*(ttzftL3LR9)DblnN78T8YCB{g5Hz()KbrkIM-^_)1-rOR|?Kv)zd8cCR z89G=l3vxL6_{M}a(PHi4kp+e`Gxy#e)(ytf7Ea`rw%Zf*Sl4&@1JHLg{7Ek zlAxA;xJ?KO($<~h4s`*gye3)rgF9C0tKrSkQ&9Yc8d@{!-QJi%Y+z_d4p0T)cGg`# zX4)J-7XuaNeQAbu9*mb9T=nvm_OZOnP>3=Kv?@3p4RdCxplhh8^gF5^#$@JPq~Z>l zfEyTTVP*(09KzsLF3DdzSHpFwaPQdVs-+Z8urP-vHg@yRPIcFPUW4x$cnYTOzr*)? zU!yNkiQ8>yuYgJ;q7HhJA`LoBulDR*!VZvN!P z1i%=Uia(n(-F6~25hTVFx&1@=V{+f!BkbqW8kL6GIYxJE%eBu>f4Z&0q0Y2Ik}pwE z8TfUngj0TeuLh^QiUgASC`%`Oo2Q#-{BW}2AR~5Mh_fZIb77S#5lt{bHe&D-zy8Sj zrT*Ur^Oyg%EJaHBqu{`fbj&~&6c480HgryAxf*5_$Gys*$ljYgor0S9W{=+} zK&HECZ?_X_2zN{cz}YU**H+n^kLtV+~R|ZxG{!UP_;_*WYZu9qSk|^WGD> z8F-m{;q(Zv7pJwx5MuYISo8uf=DO}R>RRNre4~>5j3-V7@lq5!HS9Bli=QmEm?m3q zd<-Jee;LH|Fo;P+sz#_L0`F`{lzVtb0mdBLb?tCb3I@9I-k|(MhuS33UAJzbj6+l# zNv(++(z~HNO8>j;d9~e8Rh-0Htp6L#C_g9b3=J9gluO`v2l851a6|&5Tf~d4U?%WD zGBILlB65LE$l?d+6fDK?4$u>IxPR3IvftZz+-Oeo(l*xH+7ceBx;MPV!PadzvGlXM zC?*2^Gl4`CzF)(a4{dS$qyXYfdYW-}-kiN1T@6xtmqn@AV%mvh-(CxStg4$P{~63c zw(!31(b5N@Sy~z6>NJ^bZp6*;pq#y|#f@4NBd&9eL_r+S_7o`Rhlb4*EH)s3FGUakUO{&wyb zEx4CGxys4p_P1W)B<}r&7(A}Ij;LG_|BzN*S#PnWY2rH2~OI|_N4pJYpw2Qj12}KQ<=i!fp4sn z9!Wis#rDkE9Z|$Jy-*|O^ZXG+2SEGQPyFr}deCA*1zAX2LcEZOoN+)ONS~FuHgCK!XtQ{Dmb7RejJ+!0O!?e5_hlBGKaE}A1 z%kzvMACad!hV^o3w3JmRqE~wA=MiFerhk}ph`8%{6eL_)ONjog0P23|;S(_tru_-6 z!81to+IZzpws%^-B=vVVGj4XZ-}k{X`t{z_J7kUe=LZVLFcV%X4FNgBIb1-!Og%{Q z`%?;fW(h-I>ZleFuWUn!B6@MPT+sMHE@6G0Re!_9 zizMc)PQ&VWf?VI1Z(l`)-}#>vQfJYgb`iCaxV0F&mcD~XCx6A@u9dS5?i31cAvX!@GCatsL1suZRK zUC4^M7Wg*Fq*zhTAj~a2Pzv|WF3QS&y+qWDY_~r&YCjp`(n}NH@`8ZdWZt8I)U1Re z{ChXpZDRq&|8Yo~C65vU6aMsZDVZG=`_pIzl7LQ9WiB)g+fjK`Xx_*!_2mA4Ub5C7h`5X3AuQI5B|>j- z79fv5!TLnaFcI~(>t&W;+|on*pyw=+WAD0#D(l7sdK=s0{;TSi%?yvo@GZPrUyTDr zf(*|=iZEjIoUIoh_E*;dJ@(g)?qPI|IEoW%m@;u=T>+xNEbcoe1Lp@Tw}Bm6)=Wo# z;2sKl1s(009$c*Pe0NO}JdW>VTPe)C%6eMa+PqZy0Z^Dd4C;xW{ShdH4&D*W1`i)_ z`{TkE5aeh=wR{!v-GnKC;l6?TNuG!!D>+dp&V&7Xo9+&Q+b<4oikQA13{iWc#$K*h zg!^D5{NcP&WtU!X9i@fp$)Nv(GNCNP7bUxI$V(`m%?H6w4TWT}(c7pUzFyVExD_&T z?DOUxpaG`E%xBB*wnCzN2k5wN!icrMfh0^piLi9@>%b(R zCDHhQp7g&5p7b<2=c63a9>=@yMVff}7+pwlwAigP>wW+>1ItNWL7MZ(fO0-aptnViNma@e<(}WG#1!mrdL-SYdij6H{eOo4-PyqRgd6$9WeLpt zk-w&gaF7N*mX<~x?RV=u`3-mp2T47;buYd3{&g2i=W~4(WKGDp3L^Wcq0 z7_=i#bIFWjfAYUZ_&*mT{al0a^Gf{&o94bt0A2l#G6o}h>kx*MZk4jRBt=&}Zk!Jh_9>SsK!I!D3S|PDO*e~-Q z1FjKIj+YzzDp%QWHk!0w^Sz*J*ZJ1Ws;%`fVy@g_}$MMjFB6p`n!t`PgZGR!*e}o*)*ZlWY&l@e*bX!{Uv34hfpwCo!ZqD=p9jM|) zeLUC>qnk0wtpuvtUE;*F#RF{fOlhedC~0zE6mf1zaioq#EFdHVs6dBW;y&@S209+r z}Gg{RX$#Met7iy{~ND; z8oc;*(?Ysyu+=rRj09VLW120&iZkDX)BnTOS4Ty)zTpzmE!`jpD&5V{ArdAaEhr$R zbPo+mmmt!hG(+dWfV6Z=4&BYr%*@^TJLlZH?pnz5Kfb;9o6q|^?*|*ZgHHbrW7}@R zE;|~=IrX7IFS>Ln*zht3PW(7{s3onE%W13YDMyYoX((--q#>Og3;7)csR*57eT0mk zP$RiSq%dsNXQFZ{KQy}UME)!=D-gV!2Y($EcBcQ!IQ0MXEUI6$ai{mT zsdj+2;y5c!G&XH5t2WrX`qruX*2TPTI|)1$8m*OSsXyTEXH7?L*mbj0 zY!BhUC43iT_;aBEgxI=x=^OKUgq0rov>QV-52U9Ss__0)ef#`!mh~liQ(EPJ@$+BP z&CyePQCwrww<_nvfm5UG(dT)Bjs5JK3n&s@ixa}fF2H;Q1A3DlbeO}*a85Xygkg*9 zx?PYw71xL;tTyUS_v2^r%S>AF+w)ZfwGXr$D3;zKXrobWPmBv-BOxfD8fh{kVnHV4 zfP@o`jJ}tK7AAzhjrhkV{xOc+`%f(IGY{_w=if=#FwJ0ZQ^lzHiGD3`t9I+8iOlMP zky>rh_GN+3x1D| zgT$Dc-kfPsUU%g5<)7iv?~}>ydr-;>Q~vb$nml-X)~H)O^8bf9s$Uwtf=={r<2Do8 zVV{>DyFJPn>43dQN$g<*9t;oVLIB}Ye?my*eV*XXFZ=sV(0U*)!Br4@;(ae!J_Q_pi`VcVEu89 zDtV~acW8{YCL0_y#`@U7zc@Fa$UNkk5gkVf@{Q6*XM6_s|6KR)e{AT}(34$(ezzsV z*sZ2C^c1iZKN|GzY8D@Q7ka^XJe&r0_yV4~j&wuRcYq@=uP$QZQY;jVz`qiP;@22T z7!4C#mzR5=a6M(_P9ROhA8fQ+Y2n<4K5@Ls&3Ad$SUe)VrZRC@wpDMz^a^Z+z+*w; z6p8)V`b5cqoIK2w+w;BJ>I6Uk*M0cZkf!tKnhIUT`JBFdlYKmZ+VS!ZyE=PvikA8@BQp)F| z$z)lsq|fv;-Hd9k}J0dS(O$;{k=0isp+bf4lZkWZun#lP7Fu zH_sbO8tDGD;o11d#*#7ry^xnwgpVR0bD&#Y}=m6kvR_Py613>D;URMgOfd|EV__l)EvF)L+Vk*rCRylz~5c&mZ-& z0N%G^=68L%1ST4jMW+{_60YS%{cO%WLSzlzW>lNye_5Ie!w@2IhBYJIsBKJ=+L3OJ zm$lT7fVSZ4=?7-onrt|UipaH_d@)cr&iTi?Hau=0MHDGnORm0PB6trmEKZ~Fq_&a!|arEaFDYP*dGJU zDs>Uxz@8oWOvw^o@*lJKZbA?g@ZVQ41P(*Qf)a@YPuhO0x}2^GN85*jNup8jfJsE; ztqaesht(`A15zHrV+t^!)hg8cQtYor{I^p7_u>DPku20~ zq*UmyUFa|p;Nh3S*s<6iG-w9TOS8VOKzJu}UdwX3Q9iAwM>Sf9VQQmpa`7K$szuN* zRk2^X;k+}6HzZ5on#m(Z=A;o{Gx3;=v8J|9T;~BO2`;A36Qk(n)M?Ita!WU)fcEb< zNU9WgzL>=;07C@Ho=#AntgWK+qCU1uT0(^m7Sf(8i{bMADfjifc2Y>DzjndiCm7S} zM-!(MUu`-b!|r#%Xa*`zCj{vC1+*OM82Z5%6M}l;Ih5t?ILgNQPYl8Ur49w|tnf3T zj7{ekQpn(LrRTjzi}jqo=D0aV*TCaYo=~K<5)F3UeYYi07#a7^`CenmE&L5;^m9S7 zM{E&h{@bb#buu1_42dTC+K=@-v@{kRqH~Ip>1I96W^~aSQw`}lN{L?v=MkgCcJ84c z(chMoX=O)aKTSE4Sjob+)QH$QI+T+j4oMj5I@Ey{mOjia<(;a^m08r!H?aCG(`sKT zLH)hi{M-dwOivx$muE(%G-Kdrf8PeKf**w<=#F^9XOvTbT(O1G5k2*qZDtC_{a*@* zI9DKh{I|gDu7d)L1}mp8Z-g&oPk-IbGJQ;A@WJA$(>E*s-*-v}9tehR((GiC9%r?a zql;bOMnGqPo`@O0-xV$$WO@pdN7WWynrRi@2sa~$(0k*M=Yrc?fsdL07=7U9s@D+{ z8_JK04;_H}s(sk>(7XG!)ZBu1nuoC8p2=YOk1!2~Wk;zYscQxP2|6_wY`VJt9IzA$ zx@s}t>Nj>7=5QI1qGk2%W*0mF&V)S>&ObpHA$$q1Y=gzr_%q_@iFaeN{weMP_MxAv z=6@p;sjFLbGV+Ft-o#icYhs=H*dS7pDej%5>t!Pm_L>< zoqy6~B(PA`h>MtbIL_|Tr%B^7V>b8ZKZeQLi;x??kJ{rvSw zc~A|h$iua4(9zaKEu5r@U+1Y_qQWe&>072#N&25xL-oct&?R$P;(uTW>AZ8-QP$uQ zBsU5-euh61O+o_nj(N(aw-E7XDsGpXyeJ6);L~LIFP;no9tGr+vW8L)Y*(EJjA|5L zyLZB9w5?ytGH;Yh%vOm!*k-`X06>^Cbrz)fVWKXKRFhyJzi7^1e(BGL_`g3w33Mx* zjK`3)wkbD*+SBR{uD6S z!NEuPoZ#K~WkDkC%w2+9WijK&EVSJ@e2-s)6~~gZwcbAy9b@(4%F>0&yj%Oab%qG% ztAU@DO=dkQhjs+~HBdUZIg9&7I=83a-xBJ=D`*%iI^W=lZxJ5nz9wY+wB{FhV-WV9 zdoLRs)%YSD%(4{#uA7HwYagz{`2My)GX`2%MSQ#zsX7D$<~yekiqs-^5D8K-bS1w6 zuFGu@;mWWSP7{*Z9gj`<)Eu)YRD~w;DbaO5{q;h~gRS!p>7H-Q|BJ}bknLC9DIbPT zuYdeW5zl)mR`I-sAc29H$46O|Nl)8OvYNu)bD(NRH~~IOJlf$ExcT-pgzFNnc3Z!Z z&UpMM&TZk^MyhUI0VvGpcY;c9k(C+j38do@NfFYSyM;i#()ytUi3uJ563EMvVjT0W+#Ly&cf_ z1p$9GJH~%o56pAxGLes}z(g~8BaGzyEa>nDDu2Bnag%0$8k`@E7hjcR!=sgdK7B^` z>({e;zp$R`g8K~2({U7s2=(-W zP{(BM_=-_j#%_}@t7hMq^j+^l1kg8d^lx@1fgrXBzjrK-Do+58}K$-kUB(WosfW!u%KBA*FgpSl`ybZ20j*%+STx#4b-dj04k~hFQOwxEF0jOT4_s+v1zo6aRG{(VR z4@MFkBYP2x37fNm|Wd9tc#}rL4`M-Mp@!zqvQoACYG@ zuXf@kWykU)O|+w|rCW#@PRAeRAIkdaU-Jd&qm5#WbEhyBbvO=}Y52$mn$vkTym6-y{pa-3edx^GC$C ztcl<`Wb_`_n+Y9cK0iq}Q%=u64-?RsRc>WQiP;m3k{i(PgP!Q~e1rvE6;1gz_<`)7=lNpWH6U%G3}MuvuMhC!fpk+W2}>Vut+ek|oR! zNpv^Om(=w%IU7$`nZSikfv?B2xYU-R1lajPAMJj+49lr&DST7N=es8tBYV=DnMK5e z?SB-^%IWg1pRayx$rY25TPMlMZ`Y`3w~(GAvBRVFlv%y)0~9}qE;;ozQ&O$Ytoi}? zYTG!*V~d2g_vek%Bcm*jklXdJ)Ab-cC+)Q*#-YT)VW}51SK}MbE6lZa4wc4_Ok+?z`HM$3~zmONwBA!pO#xv z+|{*iIwVXW4J_1fXvfbe(ezmke?OO?4&z4%N)jUQ26uYyi6`V(FZ|9N1G9(O-~4vk zy{pB`K5gP*@-j|otD=K6aTts`opd;8+U3961flzm3!>)yoecn|TVIJ3DCa3DZ+f&z z04br^*H^)7xbi?%j*y37*fssAxGpF|JBEF71PB_h zk*s1klBRbQqukp%z_pYpwqe+~p>zv}Q=>s0Y|FswG`R2U9Q1b4sLGb<`-f{V#InbO zKzTS}@FWZK3jPIQgm=pOx4=x0qF^-50j0bwcr9>K)sxEh-sfWOj^+J_pJCoLQ;<`g zP&HUhk(n?;J@cH*6QQ4G10E?vKBbDDjlfiqVLST?9F?aY?(dTzQoVSt@f@-7&50{0 z&dxU+cCp}*HSapEtvO2x_t%xbvbtLrnX*a-x~(V@ha%7CoaY?s?}t2;UG8r7DA@Jz ztZO_xy82?|sCmtDrTc9((74G1?&qa2dEKYHj*?eNt-gyFuYeV=*CAM~(-p}er$lga%d zsm=5OOSzl9>OQxiW9Y@APolij2GfWAj^Hym8dVRyJ?USp^f_#~P}^g0aEso}7Z~B6 z?)KgtZ9Z%@S|H7YWt3;Vc6yahUyjo}=-HyPH=pX*v~9KMwOPhP$@Qdg4sCE#-nfxw z#mC6RwC72(d+t}cAT|EW4TrgKJ;1;H>W#_Kpa&0d_gsY7o#{4?SlNV`sY+i+rd@es z@u^cF4o#es?10So6Kz4#`+1}-HQS8+ zw>(jU|Gc}m!=L-gi{X~hzkH7)r|Ig$mcH`x?6cLm^mM#l558f(aAiGenR{1?F>K&6 zBxQYu_I`@JWr=6Jnsqf`{nr8siMv^@tQUNOOMP~--o&B?t-?%zBzj(^ni9VW_m>kQ zg^s`TN9%LL_~wwisXJ&9?Nl9cBY$csm*9-`C94 zLzHQsK6u=8?L6c9#%eaFjO!KncldQMF%suHY;QE{Nz7|c2Pm5G(W9(oIp7o zaFiR)>C1QD9KMi~&)HXNk6mm{4@$sgPv24taBlS{>zhi=cx+~Pe91!YS@(Iag2~^> z2iOPbFHE6R&6z8~h(R;V=QAugSEo`^EnQJZs=)C%v zIwg1gj+yUxck>8-Aw9CH-X(@U|7gN4A$(1?+SteSjnfY`2S6- zC9jlcb4!}T4U;kD&cMESRBDoXYEUi6eFT~k_4K*vO?fw6;RvUl%McmFz*VFg?Uci` z6&w+eG%F=ys{tw}V}bT4yVWR>A&ojk&>IrPn-NgNZ^_%EjuHpv;na`a^l!8L&f|yu z&#Na72UrI<5M?GE6+=t2;p`L39o({41f%u=iHzU!Nbq(;IU$rDyIAQE~+9;zf z_z^U}{rDb!uGz=qj{5$)c=iv`w2U}IG11t%Emu{(f|pOvE>6W4wZCLqvp1r@7bMgu zJ=f~`;x>=k%lmZPzFc$Wkw7^$V>)HBv3mlpOuq!f;z6ZVsG~Hx-G=2Nyd9QPWCKe( zb?oAOaPw8}+9Bg1%*f-W{W5hL^wBJ#cq@As{Z-}u{P`Y7-)S)!Sx;!pq2~ekQv=HL z_KbEtq@lZA)5A1_Nq4oQz32oEFlNB>AHUz8{UBOT>p9RQzK6t>u(8jsV&_@3T%qHJQ`!{m_?NNXB7MXx3iM1;sQ6Ma1m`w~nh-*E6zpmFW4 z)J|dEUC3+g)OH^_vi74dGC<|L2BZ|NeLRR-$V7+F5DROlzGBFw>9gghq2zw#w1ejz z2AVh7ee|B(ff0c{fRyWw!a*MT=w9wcPb9yoN)=)Aih+h-0CL}W#ypthu7Em|NbT>= z%@E|#3YPP|UEhzy=>k71&Mw@Wd7a&%KQ2bLNj{jgoc!XMgeyD<7zwe4kR2_mmK%BU z%&xA?Di7K1=io1`Q*qLCH!WVd?f#PyMCq5x34Dy>WQOKQ@35Ng$e4ikZ=JX@dOcfp zPBg-UuLutv=+yW=m;?%%SZBB;z~>A*n*7Y{J%&VgWCos-cRcvH!x2MHA9xx{F}gDc zY>d7e%ush+hN;nmwc^D&#v2nRMGzc0hj)r)tL1B#>J-x?M(kps&iS@wPr9L(8h`3$ z!2{m%eKoe8fiZ$@#Kiy|b`|B538|2(>v|2_ubX?7=k~?$9sO6!7^&iE`6yBO3;ku?Xi}TRgU1to#O0>||Lu-#_;Zr%*x1 zY|47*Xz|r15fc+feZ9)j^e=6pvuq~Gea{NqxSC|qrbg-veL(dXM9muZBZL2{`Sys_ zhpRh4RbD=p={A^eeK{j0`QeQjTQatQw$;@3yErWgxYowE{Z;4v)eFwW+p^5{4#54($3pV&NurElY)Khxfpg`L`p*AZn|fE1u$V8&u?sIZ7G03} z6({Ol?L*yO_s8ZMp?ZPfY8JvomP~r zLxByoLYT`7dJA|<%!g1W!s0wv$NS9hmrEZ7Ucu`CZil-j_S$62j8H*tL#2uQ$dF86P}|#BM0rP2ft9+1ar%xuq2%}g?2Fx<$O1x z#oas#YOiP~JP&l!L?2#7ZDEpAlQ!rmTrYVCt8-}BV*vfosCq#R!969~5oHZoVx^;3 zAr~Cwx^ZcSTlID|3#|du5@V85N;GO1L=Gy!5)Y@UWT%H;we9B11q5{Yx{Znhwc&>q z=IE=+Q8mWR#O?wP?c+6gwl3yrPgWIr_hvN(s4#@Y4jObmn0)~5iy4bI zZs^&y`?{)1t|Cye`LbXd={I*8n}iJk=@VLT-azV#Zk-BN--ahoCFu?dWj!&|uwKh_ zn$rCA(R^y#I%)ik?v06nOyzf%uO44Ci_jjQJ|R7{i?aznKFU!B*CaEYtvXO%%_?Ob zv+Ncc2>{II%`y&hjC?693-ASzZvOPc8RODgnwj2u~kIZU>T9NR;f&&BVMQcjt z`2h?rX8kKxcX8TC1~Z?HkOy*?k>I+<##!+=OA#AY%eMUjF?a;axxCsmzjFz%nt0dy zDc|STrE81Ub^ul%3Vlz!l{nf(Yy3MxV>UO3k>V3PZDmR6p>cT9iyrE<38xav`eNAp z4^)sk44`x2H5Hw&-Z#(Z9XNWe_$$ymC<&t|RExQ1{fkFi|MDSF@OEOqTX37!)*#3? z`VgR>J!DvlnM-F{jUS5B7?E;sZFW+DkOA5jbVqR!AvSF+qB^_f5aGTb`k z=Zf#r$Aae)0TVV{cqazOj@8}VQ6krr0zc^Lf#@uLhJ|sWb_g+}ic`Z0X?qjN_XP^g zg{J_k**O;itHqoB${SWbTd~F!9zk)w*8L>p;k>4hKQxUy3Mz5oo;&S276Qf9VHL$H zq*=(&(T?CE?;{dlj2zJI54s^v^ynjO0%-;Dw9sAd%?pcBYX#CRCHOX!CaOco*S zpfc8T3)E|`kR&&j#EPc(-0-0R87V$fS8k^a4nK5DYvyRjbwMq@09FJ+;l|E*FQ#jJ zO}Mz8d1BUp?%8#FbA!#Yb4ExOtT1{L*yeIdy~aBwsG^VqGhYj#uz#>8PH$A|QC4oT z&?-Eh`I_xgfo$DkSWEr^qK{`8UqaX$ve@(`p!t8&1ouiz zf!s~&(Ul>sLPb9A#437^wcpC&+oon&daG$ z$>b&;AlX7cePIO>$^xbLi%pT)!66|@sY{o&R zJ|6*&o?QdKO*$T?b|V(L?vTC)hO~tqdsUSQTaSNvkMJZ6*4M zGZlhI!vU3)$}XgJj1)m#_FCTaobs8v+Kpde=HQd#ict=q^mCPS9WSpM*`2}rV$V;9ri_JQkNv@^^ zYgc}6ky`$W2Cx;Oi?c)^ZMTGCtF#20QX&|_G-R8@ zwi{PVHpd4Tyv8)S?~g^V#&Wd=nX;UVZeDjMp2U6G2EJp~7JY>g z+-!6hucM)kFpqZG@2Sm;KK%o5dlS&%@hX%adL9$>rMW+TVh(A#tRjg$y_Io#!Xtm% zepA)H345qsdKLL)2)iiQ|MaZ(89Xoww3-R-|FX%#pCxZ&XXpvjPOOoca)F2Q)X(e3 z7~M>3W(3iqKco$ajoVMcS)Qqd6;)a+g=e9h-zWqa*L5vcx!B*CIq8QdR{LMeT&-}^ zLL7aDiFDuXM9TTDMtqW6-*6!NlDOiHzv%#N9w4XHTf8Y2DOSU#<2kwk%t30K!#*z- z4M6h;1w|HD`V=7a4*s#)ZAm$ZZDp8-QbQd5?K?n>^=R9=V0S_`(R4%{!@THmYwDZ% z)5tX(Njl+h>8*-$0l#gnmq7y-efLuk@T4$VB23RI zW7kpDPZH78(WUpAEt%=e;*x3pKy+s!JLEQOCZX74@&Qz;5hA8KPwneQjobSH+jbQi zDD?_*nasKB6x6dK<;jqFOu(rB9 z#ExqnduJSBAjBh?PYI%ji0Yj^>j@aMZ`FH~QaQ!0DbC)F)p!dVhhr z2=gkp@LHr*CuF;DYFz)q`3B3hq>v~3M#)Sj>t=qln3G{Fqx}+BSu9F8R`w(!s&3*l z2hN1#=q`keE%#{;7R8f7BnFvf{*STM5zRFvnI2ZHc;=%TwYiLdQV%z)`PTu3BFuw1 zhh7J*Z&b0Tqvy;Rvp(v6gs*xlQ^gn=Rfvm;{J2I1TV4*Ss>^S=v@@(lJx|4cpjUD_ zX{ocbNt|EMe%SECmpj8l*IxJ(SO*f!d>Gh|u8U^u;{L=~wWjVi{uC6) z!=&au+E?*eyvWJ5W*I!9>M?P7EXL;`?bM6m~;UQ%olBb7uz2WEp?wTrin<2!trGGPV zbb90BL?$ylz%;Ec>7TKG7aPm!lhmdb^RA2*aenHkwYJEN zwg6unwl3wajlIaqMzZ_K?`fp~pZvW>mcDe=16u)C2u__|VMac^8kL`EZ#{vNT9!?$ z_p|-rc9|)NJMB%TeiqPwFK_LvCvR>gddfsg>z?kg!g3F;-GQUac7p5WZFi?Z+VK_ z;ST20wQLT2*BZ6VUG7ooiZFJ$=U?m_mcthPArh}tfyISjd4e?*b2$kk$3{&PB^*(ghvp`iX-{QgTvG_S#4J#^XLbs~e)&cxv?d z9H+3w80%F0Po0wfMg6SN2`?~;GIGinF9>-&9S;Ad+kE3q|9c(p;mpBL%kZosKwl%` z)G~W2C5W&uztZoCd@pa1NLY3Pnf*B1gMN)%SOfbEg8uLYhcaIRJc#&-R|a)nC)k<= zr$s}4n&Em8vk>0etM6Fjje|QLjp=EVtiYhnMiD$>b>r6lX=JC~$`l5plbSf)8;6PA zj3v;<6`RMa(%3#QymMusQecY9*1a_PNzaom1(CY)n@uUcM7HFJ*HoS%I-E=9x&~ht zj=6RRI#Sc7%>;a0v1Snu$M!r!~_*6?*YKO=`;N{MfEt>rZ6|3XeO;TU-c$IZfO&u zXWUII5U+*PIC0|8Rm^;Rm$mXTvvJbbeq7kDJ`{3+HcvOnS2V0HroUT^U+wh>j}l`S zX(fi);dZk`edU;aDk#tM7>G{fDxjsc=5o!0Q0Z^K zKA3IO5#i%?eZl=}0z2$<-|Xg6P8e>_n)S-pvx#0>K0NSuMa67Fe?NR`R_$PB+3945 zbvSNywMKzxNZ#6wF>Z@{!L8N<{grzvQ(4XuCyJP-%Rvs&4ery{1tqC)W$RV&^Uvc` z64jE*6;=!07k^U|B=G|}{l_rUt*>#f-ysx6p2Q?_+QE#&T%W+NMKV$74&%07b$RY8U^(o#`yC zXGR&b00~(EY;#>`h@AY85|2MQVWr9)dW;}?2z_34KE2rWz;XIlqv%cZqFNTMsll-P z&0ZMRibZJ{oj9SKeZk@J8|)KS97)9>7XQm-&Bf>Hbe4=%KSKZ_10~g*y(mkBg3X!l z+=72;l!z&=Mbs!(tjSu2>NPjK^#NN{5;?}RsWwjxr;C08nC!8<^5+L;W;+oS#&CYo z1;Ufvixmb3>pj4XxCIj$-Pvogue;QU)*Aiavn`}JnPoUzLCv@`21w*yX@q{M%{U+H zl+XB^Cr>j)4rKBL$0>vXqyj)iz;SsRLcz7hiRSVdyYr0Ry{9f~DiokJRL|T}OlPkXl^>@r3Y+65jEPAUd#^+k1tJi)zMFqtwB2jB+ z_ej)b^gwEq#~SP*wS#A5@5>v4_Wsm$*A)7)-ufrBwt=QmySTo#DBZrRZ+rX#d;Xu9 zD!YdhT_uSI#0!W+I&Qa0$A?qta5lcVJ<}hu9gC{&-T279++Y8jn_BMZh$ln+xf!{- zIuk9egnKHtcz``i0nYU9GHq&66E(%hrjVsVJ z-}>8vSTG6W5p4ei0$9fxF~EK82c<9^!3?k6tQ1n)|7E~3wVR%cmfA~CWq7tL$D~9z zoQ}WB$gN7)f)lzSb{%}agWfBFzcV9`gqeySo$tSBnTGGuGOtmd?-RvR$(rw^gD zJ_A1!XB$QkG4yUbUf6;jzk*-Upma9+o`?)4QT?9t+RRuFBI4JG!i}NwyglwobUI$L|M_dn;^TG=&b@^ z{#DKWC!>uyn%RtYl01{O>A-+Wof?DOCc+@-xN@a}?{tULn%pkfr<1A}*^sz{?t13# zQHfXk|5}oFjC7lw@6CRFimuqSkPWxw<#(WP1Ot z`Q{|$%wDN7bdC*;zUx9PQ#dGS@up`Vd6C#5_L_YEXiKN(z8GE|a)&GttDQCL<5UcA z={g?iI5k&qnifP)Uym$jnogLd9@tKhwgd`(+Kb-!4zr5FNtXW0K@hioF=O!bR~Q{x z^kjbNdHu_5t?o8GJsQSuT90=DdUAwp#zHBoHxHJc2-NJXHs6uJD1aP_v7pQ;YqdOS zXU6;Iv+1M5^_KunKU>HJz&&{H_B2@k8_BN!NgsD&O2hgWBYp~fsDo^FBJBwLK|*G0 zs#4(cW7I8prKHY4ZVE?*`Z7T5=Ypp-et-wPqF7k=@3?g{T|3AOubjBavuSDF%*hN! zF`I5UGAl_x)E~#(jf~c?qr!QU)t8RfNP~P*2aN9~Z%Oab2PjgA^%-R81KnGksL#vk zSbRR0(`?+R`jt_Eya+HU9&naqU1bJ!xAbpV+TscSuA58>;u9K;lm_N43TCy$`aPy0yLVW8qM z|FMTc>np~anU?H!k?!vj4M4Ykw~E9oT5-?Z$aA!9&QA9GfXxQi$_+E}4i?58^3OT! zeB`n+PfLlx@nE_l@$`7}uW!E;{V-Hy*Yp6esT4Zs5*thu70#Xi4ZT}ht<%4`+j+M_ zlA8)kMHKsaUjEy@`{ArFmX;d;iUBt=L@5`T@4wTgux$}|MkRHqnxFF9?_%NY_vBZP zdq2kI)-QV7X%8L6H>okR_#vLoSS4!jeD-NS$QMW-kU50*rF%4GNEF1@Y`Y>=ydU6s zp1koPcVQ!c+CUxl$(t-{bzgcL;oa`**7*=$*0VYf0^p67tYZ4}#})`Ig5L)WQ)M@A z3S7D`T3Q`Hn6mJ zVqed|j%taBaDnCNRkvJ3moA z@3z3B^Ec;wFQ)Y6Z`=83ht$@a)Yy+m%3^=t>IawX^u^60S5wg zVOOzoeVq)>FZ88mMbxtL=GSf?ukvFhk zC7xiiUgAxMB`^bbFY%V^9p1znrcgt_nAC z&K>m*+`HWP_vo~k;+*kW^4Z8oQ?|f#@2iG9peEcIrUDu`RRD93GbDCb03o)4_YA+0 zHdK|?M0AEf*44%fPAiik(z?3hnN|wSDe)5J+iQdU728jp0KUhf~Y}9td@2Vb*_`|5${ZfkuuMTsu zj+7#9go&~F@0$ZAFL6^j9i~d>I?sUVd=|kGQ;5Rtz$BKw&cqAG$V^7akzKq%lpJ0j zU8^m-lH^y|6>Qu+0G)p&9`+E_*|x446rbZAwTq2D!@5E+j~u*BF%hpNfewlR+XtAK z$rhn8z0++T`%}Fuo=+bn#YfO#%%J8VkV(XOv#-nk+F_=C#$x5cA#Y{+9x@~P8N5Y) z-9(BzhDj26u)98a0oO#j8v|>d`j}3RooeRm6VnJt$+{>AxVz&CJY+$G7_g%r>3+|^ z+`;gkT6*JSOFF!mXgh{I@WPDqq@{;`{S6{LD+vS!pSDKWFnb>eJQRik3yOIB+Fe;bhJ zYgO;}^czSx-OC}2{<+CT8~SL*4jis8*{evdFRo24t9>*^mOzx*=x(+twQkC`yM4~* ze(eDrn1wI9kuI=JxJE#UI`AF_(%O6WMc0Po$PKyAyFLypmCC-Ea>PEgreaqSs!qds zut;P2j4iD+oEyqG3jgNVWGhxsge$wY#7@YeT(W70BQPr{Xd^dxAoIGXG2=Wi9q%}= z&lY3)4wUu}drBYj94;0{t4-AyEFsgYm_MNCe*}bU2`T=Q*{SBR=8w@k^0v4K*IP9L z>1`Z(1@mo0(o>r7x{T5+48p3P9w}VA3t+0exdK!FRRIVQv6O-IqREWVPqDJo_6M%0Cw)9szA}4>;}jmCunS(1!`C zu+D7V0}dE<*V4+5%Dpe@OA&aI)9*6- z%s-s8e&{|`0^$9vo+KVFd4}QbV-EWM7f(m3q z6g@r0O#Q3#17~~K<+^*j$Rap<$fYq?<<3a_%}yi;>HP=tw&MG30INWR1MDgba6O`AuV+s=$y7ClAG5!HB0R z&)|n0c|!kMFHZ01AM&YV>(z$UpfcD(KATL)%)-Ym%n$g4`T8&k49tDcIf#2UyeJtJ@- z+U}|-|H%P+S{AAU;6)MSm;s0YI^_Y%HLYQnXeOf7JI}<34DjaN0h*h&Gjh{wSb0|1 z!`C=+Hq|&YN$XDFVi}~Zqu*(A{o0K`+?aFSVBl&Q#!P&~+C*yjfm(wg(o{q|>4 zM`J4PS5wkrF!WlrSqTcYVY)r*7}<>zB(U&Cz{?4Kx>lJHo08%VdYL(Y##*)T*sgcO z>{iDOhQ57dLUq-+pi_jZYZZE7;On>;n&{#ShMlgX;7kuCy2AnrESVvPUO#TV&IaEC zy*^?v^%35^9x!g$@Pw*|%sLoYkWzel=n;vYymet$21{IRR^{)WUtb=^;3IXGKQkx2 zuvSUn&t*#(il$1#4cSOn8aE8I2I=>-ZL_|(xhO#u;vlJGQN24(lXJE3fD~WPjCPl@ zOxk%3Y_f6u_V9YrpEc?}pbH`e)SJ@yRhm#fqle@KSVXIpzu!bo`c)3DGJW)o@mlr& zDYkN|a*C-!t?{@wC!FQA567o*0``do#R9?Y3%i=h){%go%zhksz5n|S=lR#7YHU8TRAsWQ2Z)X%y8P0Bv(*xTXS%6*+>%A8#kwi^?-=Nn- z)zjWqJAk)lAV{GB3zi1Tsb$LR->PRVI((ePf#tzU$F$E!LW4~@I5amcs;-%vg@Fx8 zqMs48!U#Zjg^-&v_1Wj#55XSuXHz90B;=68`b;jvCWqzxDE4Lc(xb5_vcL#z@^D0A z7Xp*#Cj3vSCG0m66I@RNX5J< zJxXWoS0Lo8p)CrCOT+PV9#O>8thej_PbyY}ox%ztWA7#nySJ$R89fk;j-H)>lc0c{ z4N!pp%Id9tpI8vx|E(60U&eb-pKx7LBCxwU`*%ZGVGKP95)yggu8dYly!P7{*c|oG zzZ>*d5`Ar=5Z1xNqI#ysOQnWE^-D$QF_pC(aJ;*@zNHt~x!iiCl(r!wcw3ZoM3U+N z!%ZOMA4T7+V$q8Pu0Q)SrVFuA^-010!1A=3b`ap5G5^z@Cdqw?`z&P)Ma)Cx?y1rYt=KoDP_yot~WAg5g2=L|hFz4@w+tC@gZXVZPv8{y_Te~%kbi+XA*6zD9s(v2$M|}bea?Bl@9+P6*Lq#7!&>4Q_Py`DuYK+76IcA$ z1)(?gX=9>XzMF+_D=3&pr`N0NfyRUoZMJnW>s#Jk(0C*4!?*us1rSRUQPb@kdcjeY z=}xc{7p87l9aTOKUyA?3xXczd^)F8g0{YOHcC2G{NjCsC9u#e-g?8dn?g zC>FXPGYmI5>-$U4-eDu4W%<&;+{C{MNid0VsU7P9R9&eIxz9bHuIAW2uE=QFQBH;;;VE5_* z!eR_wQ3Rj3Ox3dtB%8SvwK7a>qBh8h_|XnV%V^c)Ds}z6Y_|?~-t~u7MPweU2~*$L z7mMMINZ_64jIPJ|FO?)c%~jphi)FWM)v0>^($G@lXF}hY3|J* zf{J%C1`=HCN78pn0e>#6O@h1kyv8V9rog(2!1AKNA4emYv$V>NXSwJPOMQD)g!9Mh zb>Z|r%XuKaRFk^P7_U@N!*@lIDi{M~@PT~~z7BC@>`bymv|tZPsiP}PL3Mq$IZ&zYFj|Jz)qM~j8T@Zn(da+w!3JEFlt&dxUmhBT_rj8z%j zhnq7w9bc1m8+hgseZLA>^d>=D5>Ukj3fsQ~>Hnh_N|jJq zv5J*Vqnq}YJ|4vihB7^r6EMI&<>)TrTWRR-T0&urUMko)MXU!7K#M3Se;fEVBd~Et zEYqR8=SvqN?Z+aV+&%`{x`Sw69r|NILK0u1%%Dv2?FkWgsb#mi!RT0e zUnvdOS4s~Fru{P}Q3Waj(KE%1SO4t!2#yn!GS*OGgj;#AcM-8a0^eJmpeFsRjA$pw zD9vuz^B@MOmffgVw;DQ%^QFH>8Qc*&h+fc#fs;;DOVN|&e}7U*^1|C5XgMG2d&RuE z^SzJvfB3x2CO3a|sqn~1Bp=@Iw5$OD2AJ8{-d-qT|J%Z+v&i9h zUe0f1GCm}od3bLR$%G1mGZNvA#ncx2ayNIbvualH*A4awCl4|i(<8j2hO>k657N1F z(tA8W5QVLVV|=W)5QilUizO?0STnf42maT$RO+M&>reypIXDYooTi_`^Ds69rz^Ur zJAX;rG+YG)i7#)cIcPlGeWLHYp>F0%ex0DyH*1XjxiPxrQryM=inRoyK8#v9QrAx= zu7kt!6_RVwmeGqoHg*MZPvUjPpjY8%-UH}8zVg^e!M7+EX8}!{11{c_y76b4=%Ln= zWZBbxUs^4ApL&N6r*_DFv;P_@Z7&}{l$>0cgi&q`htYlqKe~zjGF!NTlPY&XAS9a^R29M#uwAfy)LWBTdy*9G>o<;MA{r5l>pOJt~+DQmaaiI&&gg{&%31lS7 z?oeLQ95qR;{burN+$5!TYSzT|y5>}yexhxhj@gCeF}T>uNEOB>;c&tYQ9GbtaPf5h zB0{6!Am^W418i6Oi(n6-2cg&l7;yfYWxn_)+xrYWu5{B1h?=6_M-i5`16qQ9r@yMN z_24JNxzOk(Fg~=^QNcD zVYqfD?=R<-07C;mC5PvghYUgaNU=|6|HWFG&BtuNYqA)NE?h zn$j0j#&#;MKK46Mm;1IODDy)1;D%k;>eX#P-DqCP4f_6Hul|WDFRFZ5u8SY~@jBFl z6&$AEjsN<5wrKS{gbiZ@O+3D=>A|Pk!P*sNNU(f7yc&>FUh#1w0R2cwqB4&fU#0Q) zd^CG65G-H6m+c*eE#_`g{rhNFDx$*}G3>KdqdaBKRBQh>+xq`^DXs(({Y-=gOp)=N z!+&3b8lK$gCv73x*m)4$JovyQA-VW7Kj&CMq4eul2MpXfxn1-R`JStyl}|sAaGJ^j z^{UV33zB!Bu6x1iR7IpB1eOoyBSHJPmR>a$4{q4`w#PPmc>Hm5S_{2D_N7Y<#R}j$0vN@%h z39*e4xMD`aOsxY|{eO5!0$Hr@;^7259aIU;uyXG1xw-tG;3u^Jzv^?VZtZ-|`Oi3x zL~~Cjtk*6+3mrhNh&{|vun>0daLloI&ABG7{GK|=rQ*Kx$ODs(ueO7aii89nCs%ok zTs^Ei5lND~jzswcu9c*_e(VZ3iRUj99$pa5kZY$FF;`E1)7dc}-;Os#FIp{;NAy1zXp(&}_bG|LCK0w$xgl95h zwf$mF;Bnve%N_sxy8Zs`m9cmp2O#LR`}Yu77~i~>#BP~YVF9!>VG<)C<$y5u-X>y$ z#ADhkl36Rcs?%~eoA@15>O4xT>MI-m<<~>uqpUIEAg&5lAZjjY zl_)%*kF?)g>;d3(Lia)fY{rD2n{hTbc&}%X5pbernldkTCqO@ahvMJR8omyFd`kV6JWbNzMuv{3&*ClDcBL8)h^stFZVFeSHe!#G?I?+I^ z0(Qz^r%2c7oUWQW#rGY*q6eZx6Shgk8`P_ZaEPja}-AN9Tr~tvzw?yvIr&74?=faeW8|1t(`gK#{6> zW>c@iI}J3|Jr4^mh~Tu{nhUE}V)6XoZ5TAyGK;=0{OF(k>`bFfjlYQaF@Ysc0&@{e zE7zhU)?UQ2=`p>>ZcQity-4rVRJ^f=jmRp~IYSzkJ{|Grhcqhc4*_#y35dbn9t9P< zw#%a_AGTbaes!tdj&)cKmpF0Y*7DshP?ybMBw$TUV2_Fz2?K~HGLzpKFy!VtbpAO9 ztlIRy;GE7Tje*J#n4#!En>cdysy1734c-s*j^=I5_6c8GPNSJ564PFdux{TugY zd^s;>gDrav?*lbxgRmBGJ42*kS%#6AIk4^g8Q$S@skAWWOP;NVCIiPJl+TFTelJkq zlM8`%nfL#B_EkI(WcHA!tHMsJ)f(@K!m++Tm!oh!=DlbND|B_E)d^hLr*R#}+P811 zYMMIJGrj9qNv`T;%|vT0sKR1ho)g?}UZS6LE{b;maVE8HP`Z36@>l&=UG3N~AW|>2 zlscyZ_8<+czWt)0@yS|%U`-pI82aFA8QGwc!pUQWBPL=h+u!{+hPqR)8Mz(-++@uG zh{jm!_UAxc(2PAg6JaJhw38L}r`#Wc!Tt|}&shuTA`v@qD+&V!sw9CWm-fu$Ra`5t z6*Auui5Jp(zwXn{3e(S7{lurWE^{m?)^+*fSS(TncZ1yBYMC%T>bEGAhx`4S36D#X&L(cd#XX1ZNKN0Y=(3o^B zuQb23X>^g_@5bru(IK$8{L4||2R7IUF}&xhP*vf!DsC^}{(Ob;{SEhaqMY&`Go9=v z5ADn7_rss(?v7UPmj#e(2;nKQU%R6a3kv307vhE@5@mkHa5;dDn~Jp(SkADgtoo#) z7bX?gjZ2%`pl}C=0=MaTXPp7g!~tN*ZK6E@Q8z6iWYaz*aORcoTYwaH6S24_qB=4c zo-v6KxwmY+s|6p;o1_Z^;_zg<-hYlO(HWew>8CWjH(TXE`=~<^+P@C?)d?aZUSOz) z&2IT#(!8CgJF6p4h~dr&xRz|*7rT8cC&(pYYRK==WId24@}cMNj>0afECEzCmC6HH zw@f%j(wS&{Z2A3(d}I6Zs|Y0Kuv*Mzlv@gv%^ZMMev$w70R@ovZGOIG(>iZUqF{c7 zk?<-63xeYctNWo5{>OyT1SFT8N7(5t$%S-xZFm%vyp9bmV~rv#M({c+Bk9px9VN6b z-Z~sqf_KB{d$tFm5YL(E>}F3VKY#V3^MMtoqk*3I0Z7m_I3PTW9zpv3&f25a%00~jH_g)ZWISWtY@}+$7xh09bpzCTj$-G^3H^c zif6>WHCO4SVOa;19N`Y<92gtF^H&nvnE{=VK^Tl}2CS@g0K-?EO@h_(5V|T@jB>b}rF= zVO2UP?xG88LmaG*ydY>aFcGa8GqE_WV)On&-8t$4j)ye1fS)6q!A+aBeZfPYgB!l; z%DmhlX7vj5R{q=K2^-$b+5ir340_Qmngwk&Y=j6dAxDOV35O@>NwLqDm{5ZV)QzTk zd5J{PP49s`Tb#lQgWskB-Dal2!z%~-RvJj5u(n;0DBb*Y=QamOTdG8P&vv`mm%s^4 z-s%Yy_lZE<{eAP@)tKToeqyH#+o(3SJ?Rp~Yc^I_+N|`nTas#6tNB!qg-A%wj z6JBn`YSBZ3(~GbugOlXvl`Fy~eDY%Q;Y-}Nnj-i*dv!yny=7nivVT2y=e`-Ll-{Hq z^r)>;MP?YEDawQtJ!zo73$)qK6bAT74kRKpe#BA9^cAp@;GaqP7x7MqB(lgdUH6(n zKq0Rsgzp|}DxOj~JOo*prd3Pm;h3pYG$O+e=Xw&yNpw~nK<(cp@AUBIiIdS=K7PqP z#kMk{M{o7OfRgb);f3=?TjO4)TNTqIwuzrRYa+MAB5(J>?MsFIhCRs)Iv9r|N7@<4 zekEjY5{lUBh+2BYM#g`17bq^~uPW0$Ypxje)PFodRg-d3)v_&ylmwKV-oOoA65OYs z@zO4-B;&#!j&? z9>4fQ)2tu4Ec~JfTS^%qj|&Gm&p5$Vd0=t1bOPk9^m=7^p^v8HuKec#A~eXU+&K6` zac+{`ycDfK$UgW1CE;{-%70*?^f4aYA73i?D`C<7SC z7`7j{63{rDK%oai*9RX>-gC0Uwm=bBp?>nPlj)v#gu3|!Di{1G?Npo_(@Xb14W2s% z9|bU*cx|Pzx2(W`BSh%wJl-bIyi1r6ZOc&=F9f>WA;YQ3_1fT}5U6UhQ`HO$^#Y{J ztd5JR#2G(sPC0X770S2n{*$uy+h)3yCe442K9<1>r1!r9w=u!xGMu@3u<6g+HDsbtbUGPg%T4z$wIbjXn;&H}#Ngt6srX z()|NLv3V?R|2>g}`qQ_+nIv3kiAnwgx{T0;#aX#OueSyElUE?M!>RlfJQ`nyAp1?ijnMwMIfO zNgoJ(oHaWYpBU-K`=sp)iT-UWO=5yJuL2r}YdGz;iJVjQJyD6c{E(j zHDbEyvQLU`tZMu4X7$5?UTvg_J) zsPk{Fef<7({bYe1;tSetzNbujT#UP#_5F~&m^Q-@;M#z-ln$~=0TJ|3 zBT^!erkwhYJ|7Qvf&fz1GQflX8qfi!Rdad^s;mI$sieT(5^;l?qa z(3faQ|H*EW9OurP%qBjCutc>eVVG&qAKuenMR-=Ci&mAzK=-U${ef?MES=>1re}84 zY_C6sw-0j}iQjY_)3}aKHhSJa>e;0m(liwaikD_(;Z_)`)s`?r4);dcC%yWN3Q}`;Y54k-+)Oph86NK@m8vZ;%mxG{ z1vw3N3iu{?UL{Z5ym$#Gw6Lt@*St1G@i*W6j0@ZZzILfr0n^5v@hn|W!~e7VIFyFL zW&s;qbR^&fu_89jh_q8USF0slH+W{==|lOMaE)D&=`>cixgx5BJM_ZS=U?21epG(I zcZe;$`t_hvCXdfH5(PYmtc_!DF{>e8Px0@dMv4EDAqyC)NuxN4W-u>G5k$hBJV#9X81mnGm_>0TS1bVe2Ht z9}h;gvo^e>p4=t;9DOmp^4(Ny3f_O>mEx29p&}*IZn%A6UchSHK{~SW$tydG?|80A zb)K@eaR((@d!F4alcuCJU;b*!CjDfL6a7t3Dzu@=nxxw{4bkSSkfj%<{hmUy?c=qi55E}=1U799uLw8I;SCm(;+~CmCvlZ$CXS`>YGxoXr@R+G z$rp&G;>8KpYFJ%=kk6`M%}2b64r}3(d#xbVU`(8f2Qong8;Zz!WdS03c<^rMgr3wA zZ8NZaSpqJBA}0FGt29OG327ju8T@j&2_)5y-(DGHQ~f8vkra}hIVf9QLBu}|m3s0q z4cRyzMSbu%o4U30Qt zUVS^8#uC5UU*1R5p_|HAj-qZzzU6hbVDsI7)o&=TeBGPKSY2VspM6lIddNC~YN!7| zo7m;5P!BKua8}7BvJun&J~{TyC9=Uf3A7ypIJ?1CeN6hEbVA{9n+%D)r-5X<@-89H z?~x10nTAVv5tn-K>@E4 zk`*eF5U{VDRR2|bA`bKPEkHF+AM8_xm||uw6(&AdGTyt>URkO_ti-2@7>FH@O&yc4 z@>bo=GQGR?w9NVV*70N%vwmdgwXK66)W8-v&C1`bPI(4TX50HY$1l7Hl8vwttYaeZ zozjy(nZpOP&J?{t2Nc0SL0p6L?54hdBuYZ<%rP36>3&@PArcX4LtZh!dr%j*{=r{} zJ66#`F_4t9)TV)J>RBO2DD3w2kaCHacOpZtY#prKeC*5so?B+Ylg$N`N$y2by~Pv7 z(FIZ!8UdV#s|C@|g03{tDcRV5(f=nhkrWnT??2BD8hki3u+K^+UqSBqn9q5i_}G)J zy_o~8b4_+ds_L5RU~_K3s)hEGTIqDnvwB93DZ(#I-@zs7ww89A73Yt;z{GV4q`pbm z+s`%<3T+0KnjmNP*L2rk0_WWi?tdP&o9{gc={G`>yiS9(W3)s$#8mg^iL|_4Qti5# zZ8juJudnuhe{Oz_HCT>3ENG7=Z*11vXpVLC|0ey&A zPgNqa^Y&~5=P)+H8vnL6rTHaA_>N-3_x&e#NApT`O_UQIp>aVW|La$)$FF+H$bReh zbA}M9@Jx{&2p@_5Ge8a!rJ|wMKy17#t$smXg=*zd;n}T=9A&0rb12{ly%7yYcDv?< zzDZ#3uZh@I1@il+LTE#r5Q{ZArqX{E6xW#kiR#!3Bb#!wyX+iDh$M)>-h;PrT#63Wj^XbNqs*`oP;!%W&yPwzDH09B zT~f%^6zf1HS3VHH6@E1-o^|ja_-*21 z-?f_uqb;H)a)cg#J8&SCveQPOf&=rApd_sJk*>VKP#xXa*T{o^1qJLfoq8@Ghue$D zD$A=@gt04tX9gz^Exy!>+4xUI-pXCC7sMvy_ULO%wEIV)Y}IY=ua0JBDM^lEvv^04 z@-4Wla9>)@+RXdcVxgK;%Im;cLgtk0^v+tieI_{zXR&PJz>o~&^1oW4wLW*OrJI*p z;gzB*+zBWWZTXllrZ>AUJE-#S&md5IKoKIQ*^4MDfKwFgJcSX33t)41KBD@l&;BvB z{Mk1Xx?cx9AWStT{INpY!CqkI5bIRRfqB&*xbo4kaTs|Nm9-cvP@Rl*VetXi>fr z=pvo|&$8Z&fdCT#=cTnRTIAyX|M9C>_M$`5#LUAweasKBkn?AjBq5cwB+1wnkw`TV zP2g;#BKmDX#DW&_Z7COL>2also0@^)%I4cSS(&c~(bwNL3DzBqial7>Hv@N&;GSIH z1n`^xFJA+T;etL-RUth-$m3;b0a3`77T;~m<#M~WG0W6)XO#*yRn|&1&6hSYMaCnz zEcMl;5u@K0jc35FU|Y2{Oj{NH_`R_9z)%^8X3`z({-1wb-Nb6UX9jwwIT7*UA5BCS z%e!aoO({~dBbt`!&H^-DlZ zK1YvdI^<%0>^x6uoC#H*2|2c6y5aOp^TZ5s1?TFGed)=d zq<6sY6;9UcRvphMT9~ZPz9o^h*&G<%&!xQl(&F8et3b*-AZCKsu59`(KjlsEs52;1 z>!Fm{)Ym-n3}Th?DL#!a8Z1f~WKUJkYaDtz>a$b|BDDSFSdB!Rg z80=RYT6kX=EqI=tfA+j(+QOxpn(N(;srr4`Xb9hG2Eoja5$pGq(?K!=Eg^cweKdYS zYtrAulgGEWT&rt*$8sM_c{+H;7J(0`el=URHTYE{=%qiiRAJH1SO1lsUuWvoT%+}v zr(Am38|?dXV~yAKzMhq(?0n^}Kx`fxO^w66>rKIrhMB1MapIi`06Lez*%^m*vY)xxM4N_7sb3mC-lr_@vrtEw7SYnWFWuGccqxG^Cp_ z;ze7UtTB)o;pwmVNTtXQ-RO~hwG*>14Obml3}i#*X^>{$tlx6|@%>#u`Br(}NdNxN zy=HXn_@P=}9yg}Xu^My;(>~wuw}bR%G6A~2I7rIjHzK$*_WXo$|n?yq`89Z$K(-V@T=ZJyqQo{67uS^P=`+t)= z_}eo(SC2bPHag6-7pB+uyJ4r*9WbYjM`bGSV5hv$*lAvcoBcU&4nijd4Qoc3YHvb4 z^Rt6N;M?+S4tCT%2wE_6NHmJLk?kBYdSbc)vXe|4Y&lqkkLZ6DVuG|%IM?65ily1j z(p=1`U7O05Q_~dWuSDul$%kj$n52F^O8Ko)#aZR^v@6D3_?N%i zmy`}fjX?GGJdAA%Hu+`L9{ZO4XTZ0c5To`s5h3<{3xoNZpCz|isl&cTnH?}!S%{YB zHjth)gz*DYt8{viN&);Z0r9NtV^E7KhPeeLse-xOVwcZ><`P`Nt7jqfqvaXnm+B9C zWB2o4B^blk-#k}BTT>J>CJnYaT=tJ?u8okU_WjZfkX?lF7(I8LTpocUU9()Dw z&sm%mn0PnXBL=y+AOf}>u%SO>|MEGa_SDls?$4U@7X+irhQ!Vh%qN~#omkEhE2oVc zB;Yaj*^9r~LVAqQr5r-aa|DZ!N?#12LE8N!;3F9bb-f?+V()}O z@6Ah5FilZVPkaX^&mGgs?+7N568fbHi^IjvwC4o%CPcb2n-V2y@)ionMFg<5WAgMd zXzkekKKKs}7(6m`vVff%kzgfiLx;13)7eXD|87oBcZrtu?;;A_i!>GZ#%m?`$^dJK zP-3;-ca}}^x4BQRY+@eq(sRo31zGL-Ya;k!xMSoL!QP2dBGTidKvsq&UJVNDqZe%32V1Fbj<~! zhaSRCA&ZdAIf4~+z5?Qq!`|%kM>bGGE~5J92aXgpG6i)mxd}#^0{M^Qt|0kNJ*~n& zV`f7!KbjYE_u=(>F-q+*FaA_>LQDgxxc|xm(4OI2Bap$4e~6>n9ytG2OiJDMYdcU*wD5zr*0jTulyjOYZVyB8zAcE zv{O?E4tIi!$A5gq9I`tmw&eEhTeb1mHr>6QaplI{ytE_X^w#&1SKmTi-AG=EE^rW_ zP%N`;zpxt*(Rle!086c+{to@A}5r@_{L zjo*hPL6^~6qQ?%j2!&21Hv9dym58m;jBo+ynt z>ozy-Z)cCJA8hX*$SEFAXvL>VuCoy==Aq&a8k_9SL2w%K*87afyNxM-0cKhXrRN<0 zPOAr)&$QBiLU8UqZ18i%?l?=#W7-Mumy57|rBpP`3?(DhgT+k>^E|^s-D7cuPz=Tz z-u0}~31eYk=f%7t>k;UbE3vGhGr03Y-z&FrjiK%~Oz?WxIki=_*T-@6ST1!jZ9aLy zgCeH*z;fYFCK8+X16e}6Tds|uCfOUFSA_e+CuiLhy-I% zo5w(?;}|x7Ssk>R=2~JS+QZd~sx4yj-l_AZ5JYP}k17>djebes=3zgqo5Il^l1SCp z3UeQQ<8)15=&~aH?~@kcp6O~iu4=?tsMEvR%vrwR@rJ3RN)i(ogz`NFz>NSY!XnwJ)d=h%W!(3&H5a9Is+6tNs6!zQ5<3DDI zQ)QCzuW)Ise+HSO8u!qtcQCupm1JzQOB~TgaR`~*(KGbD6}kGpdwC(_J7=J8hjUfP zZU@8bQw({E>XV-Q(VNg%<_>6|XDj<6)~QwN)H7&ph3%{gCFkFvV9ldP_tRDD0mlJFehFS$PhYW1Eo2E5r`5v&h}6_?G03d{^o^`>OrFk$=0 z_yB{Ated2tCg2EV`td69uvXbz$LJa01NM7^SxU!>&Hr=Te?t$WuPxI@eh8A!iZRUH`%0Omhwbq&7*dFwh6VsPv^1Gn4d#S1#3gz<}gxh7|of z|1$Wu1DQ@lbf-egN!KqKy&8f%rsT=wC^Q+#V1 zd3?*Q;?>Y-4aTKu)=w0A?ShcVt?k4W=)ibdb@NSdAqt03efmgl+V%Kd? z=-729OJ@Pqc)s0|b@QtWD2M2NIq~H0RES!J1?|kWKOV@4P|cF#1e_W7*Ex{fiY4<3 zEOqHkd6n4x94CO{$NF^v5W(?F4E6F3V6YhjCknrppyR+PCgCMi9VJmJ#d`hUq zX%HtZKBtj5`~7@^bW)kY%Crt^f!9UPKUU;zsQGSFBSd17U7r=cLG_PLeAHmid8|r51MNP|SUfTly*G z3a;POttwX}&NwRry0Th7_KWpp5Yphl5Dlxmxe&Abh})RyPZSJHb0^G6PcMPhs|{Do zd6>h3w?$+~W~|JOQ|@r3BS@@k zPHg~?r}6WoIvr7O#6JO}tQ2`<>ua0243pU@@kW2u*@sKCn~jenG~v*KVsv5!xl6bbdQCoi1l|_^w;;DwX%uR6IX^KUbGm7W9JRC!FOby4(bIwYY37 zeqR&CG1EDk-kTiGFAxM?1as2PAhg7OK&P}J8ul7npa7seyZK!K^y-jaU{*a>O$b6s z)<3QwP1keDg0M5T#IBwH=iHkxoGJFl7}4Ky$$@eMo%SBxkCzxvlv>W<@>@V&$FweU zQ4~i){vqiPG7&TvI2||g?q@*<(wsUQd4dU0KFVLVBnLtM#o%;-9diEY`Sx-&odx~W z?8m&?QRuMMj2Uto%t8hHDOSVZaAMOsjyL+|_=S%JX}@?b4|8F{NZ(!R<=2}W`QMf5 z9bfdq*zY7(2UY#*jeu`hH4TXp43JQ@Zl^7fs>61)91nhNi4&fRv&@NK0j&u5I{#Be|ZL#o|v-7 zx-dfV!F*0G7y;r6iLW=I$?rE~f>Pe?!iAwnZ1W|-808Pq4DetB>xc7%aA32Q9z3M6 zgks2dUO=Va#`NW;_a5r~q>ss2gnL%(8OlOTJ@#+;!2j^}Y0ov?kfj};7B{BB29FD^ip6FbVdK_K)aebLJbI}6QQ|a&(PjhABd-O8^FLJ_ zskxb4eG2PTyt+o*j3K@BJ6cEo@I9rHJRiY&1FiE4We)dT?gZpgfxpZmo+T*hK<#_K zoCo3Z{Njh>%wW_2!^`=GY59Hg!?&j8!BmEy=mxyI*mxN%y~3Y?RI7=wjk4{Lb-(8F ztnYfk%e{^sSn5r9=xNF6g^hUsdW6`CaHCMC0`Fy@TZv^iqq;g6xWrRAf{fa@qI} z@c43{%Zh9x^3rU6-wZkp`CYJUbR%Qqs|bYaWgk7jeqV{Th@;&J>~Fc95;wF_7J|Cx z+gY`BXl*@4``1|+f9uEH)bSmd6}@}SBf!@g*a2ya?wk&%&I0nX>YYbOs8$rS5j!b* zYX1Kj#;_HmcgVcs7XHhX{IT`PS*UYCmI2v`&Qa~-EO@3nok;D3d7E8jWRMV1>B~Xq z+dB_WpWfLwJ?b@v{Z650tua3>1;i&TLC&$VVO-1vk4nM*XLiWZc<9jtm5U`r(3)2>1Ca5S*`XPkNUFh3+FdCg&dZ<^vf#j}2 zFq^3t2Vx!{w)pu40AqX!OJY!9$c!7RA4J@@aG{aQv!Phlh@>hFC*bREz}(*5ife z0v(HyOuA+B)kUjNsDJ|`O!X>jgiS?Q;$JWw+B_oiIRB#^Mv#gyJtLi)RH?t(Qg;Ci9Ad7?KZT{w-p$SkfA zb&-8W7|Vlr$_5>Rj|;8TujVl@LFUzdz-Z5+CQjwQU&oYzd~Xsk7E8*?)PW%!9Y8dL zYQJKHzO-rbLtwHU2RBkQ0eah@a}Z~|y73*KbN(;3rFJU+jY@c2!H`?&0}&z%Tf*Hg zEEj>aR2>T*XjD8E|to2 zaHsP34F>48-Kp0q0kjO4VnPFB?(##ayD!>x81m`xHCZ6zwHN4*8+ErJ9uJgc3Ou3Y z{vu3htJ*5B_ywYg3U96~cS{emun6l7OxT*0$5@<(YVkt@p}yQN5dk{6M;I8k{TkMA zsmCW)X8TpJQAsFD8^Pe}k_#19R2T2UTxzjU+rZbMiTu2a`B=bB=}v5T_P(Ioc7DqE z!=_&R^2oQCRG-+uAmnpX8!H-@0i6PtHPij`vgwniWeZ!}7>Rnsm_S%k#a3 z}6<>Y?_Cv*!>jNK8 z7M5qX-vxssDZa2Y?RDTc@~!8;cQkpr&A=i{MG?0OY}R4G9hfD6MZ^UhCy_KaFf8-4 zA#f)@jP@ut9pWg^HeW%C$3tp;3!|D`PSrrw9{( zhmNik-Sp53e;Thyal6Hun}gF%&ObJ&gw4eXTxH z{X7Cxxg>DAeNg+Ue!_#nQiaD4#ajubK%J)-3v>K2pL-LRkE(fzbVtu2Kp?obKPXe( z18!q0-JmPSa?5i@{JTa^NXL!Z5wp^qTj0Kw(L0J->2&yoJ>=aQtjw2T8}eJAQL2dO z9h{1>x>^kBBUp(y#y1a3k?sLP2ltAH)u4CB8=si^-|VH%+6$-&GBh`~%$EwOsUaDa zW@!)Z3|3a*b#aPn;@WUWHtlB1sXQy@93n$$+PaiJM;(pu9;1-AB%`kw zyET2=z0D;e%MX9~rkvaP!)Rfc6MGudIiTMF4QF81lyV39feZ1H&$O?Qh+0g~Qwczh zLy~Ea_e{(4`1~K%_rij2B`{0$o^*fd>LTUM<^a##NVl+7Jb4wj?F=4T!ILqT9GkKP2^0kMZSsVxDz^R|HgxZR0Yl%HLi!!kZV*IPIS z+l>|I_wLUafKo|rGw5J==N&?d1&vJG6Ip$f*@+||@diOA0qXskZilf&2Z9#B8Hcwn zEz7$XMzmyB)FT~=p_YB-RLO_;=6A3Hk8Qv>e~^G^c&@Q^oQL1d^IUqJ5B}-lIp`8> zBm>#9g&$s8LJECt9O{l7p3n;CI1Sp2#U&jH)(E?)N~zyA4?o7dwZ~&$|L*sEdRc?Y)y|vz80R6W@cY%mB)4Ii9%xZE{?zOK zF=d3)3EY3uaS4=NZYV9Sm1YfYDzSqq*eRo#-BX@tV#E0V(RqU5Fc~ED4oK?JL} z0fa`iG>}N%8ANPLbH?w(^AtWkD-zLm4w7@($WuEB2!MPr+Lu9&o%HT7p_O4P;;uo> zT#krsgjqWHn88kF@e3Py?gtC*rO zz8+c{3h#P;X`9DKV|Vm3Ge7iNkmXn7%iZq16cSKd?c`*ly+3iiL?P{WqX9BKff8vR z+OmApSYig6*^y;JoEZBISHIKZ$x8_^62B)p{OYwuOYq!0J_cBCrS%CTKP1oX5p28q zPy3nrw&v~ua8RaCWhq7EI)?hmNfe*`P|M<7~% zlo+95u9)fF2cS2^ORG90ddo8&uwq9vnW2H-o-3``Q(_sG1L_-%plB5TO>cAs##bgf z#1^=i^z<_LH`QH+Q!B9Geot@(?=)No)eo^){kysaTQxm6*LM?n8$h*T@Gt=#e^-^k z`e6Z+x*}=_aQm0fKsJ=ueW)jgGL5^?bt)`{+5p$)N`xgLa|L;lO)I&itG00K1J`lb zp)4{JPgH21eOc}q`{u2HYER(x?=&cL{L?pNUwwTU85Up!&NK6kJ#bHunBJdtcBI7PJ;@R};$Y=o{XL|A9ZSu<95CKv~OSrwMYbN1*s zPG*<*RBp4_Fn_8m*j&_j2Q#_-v^yK4cfdV_a3>;fBaU!{v4fUcsX;zSnyR*H= zM#(3{HWDJB6{oR(={*;;eV5VH1dy`kV{%zI!pRQWA!t3r0#M$2us|$=l?6S*qB^@@ zm1UOprKkb@s1R^Ws)a6nTHF!3!tRJLQnwgRHo9{Ei{t;+_Drk5x}y=TGW0gr!oI!k zNzfq9_ZkiNU_B9F?`kGARhAC5}g?6*&@#y*3y4XX@wTC*2s3a5$3_Nd3NX@`rP zsrZNn&Kzex@|IXHy55a`u|ZT$o%g8ntlRCdV_Fll^PSq7GJplKa1c=K_%C{!UZ>Ks{FSatO#r zMu2?uX?2dAa93UT2rq;02_RLu00FX+)|+3C){FD{_(M+68%Vx_5*%usAA~%|9ZMhi zn04ZhQbOgQS=^4_OA*KPSq&o9F+B(s)Q^=~bjkpN7K@v*CV z&9_v79O6j=ZkZ}56Ov0_@;j(O!4S!?yM{u9&Ju}YC1Dy?%qwaV-{OgoE(faX^2jto zog(IQ?p@fVdg(+179RDR1ix+oS(b{w2>}RCawvhfVOGx_I{5>PBKGt!7AtVl?(Z1V zWmvcBm2(8%Y5KLDKj`R=&eMqziM@#@BMG-3vOi@t5OoaUbg3iWy7>B4)y4-}Z+0VEXs8>BC zeGg!t6MLdq2=;GWmc1BcGi6V~24(l&yzh?zG zl%kIK&3*ncL@T*x^ z`I!HuH$1`Wppql)c#W#mnESo*>)}BL_S0hn`}49WL;^Ywl>)^q%MoZho&(AO7Ko*> za6N+V{LcG{y(&R0*=X1rN*$I9klD`&^YxmRAIM+H4gZ$;@DY)X!an)vp1Dc>|2TUK zpenbve|T@YLsA+Fy2xb$iZp z&hx(Cd^6wocg~!dGiPRd$GX?Lu3uevYaey3y8G0E3luqtk}QP0wQi%v)tX=)RiH`3er+Pun}|S6u`XN6#?^~ugpt;(u1iMg zt7X49=o+x~tH3gfzvA#Fueho$ZT$1V7I#cH&%vTghg3sfF2*at!;H@kBEz?+@Kgei zZ2CUZApr7r4bLHkC#wW{*2FhHNe) z3m4i&*DpTgqk8^mg-n1TM(XOYIeX#?^ljij;8%W-NQURCdM)(Nga6h^HVy6!sCaJ8 zBNbEDuudIlf3Fk{@`@jXEpC@{Vilp~(6#7In)gyRc7A#jnx8u)OIwxwK5zK-s)HnG z&>Pe983ma!37<%c)~)2%Bc|jZJ(!SV#h+@d(9HIPYI?UZANb71d=M2S~{_d!@QzP z-?>nben-yjiU=?2T2&%q3?Cq)oz4QB$1wH>fxBApQ!$J+EPG3 zr)sQyorB#Q+JN#gyd$l31!_TILCIsk;S(zDe$eU@ZX&-E5guKgoYXdCLYqJkBPk>? zV|+{uE_Km#rez2$sb7EJ5F#U){5;6*Q#F(e+e&zP~9?9UdMSFUh|8gP9)1 zKEWT!r>=gvME4wjSUS}0i#q3P@BYK@QhgesFa^5wm<) z6PLyNj2^KlY2OQ7cQ`np=s>3uiISGOlvIl9`lCN6hBp6p#Dg$sp|A4;Nl@w)$ZS=v z{oBq0d{U6kR2yE6%zv#l-*|!&{>{-Z8EyVF%VvKq82{#P7i;roVp1GT^S2Kj2d3zu6DOK9=8JCYM_x*9|Uz3}4o~g;t96)OV-9wy(d=gZ$V#$A8Xz z`#+oQ&Yxp#aSz!IzFbI+unA;<4=)eJLZLg$kCnP`ZS(*p3bwN)k~4$LEb17zE@z&d zo_l283Hr^b1L7PF4T9tFs<%L}*cC_vaVtok;Ijypn;- zg%5Gnc*gX-B?~YLL{~DVS;sJ^Uwk?g-1e6ic;VUl_1{0+(NoAT?!BU^&}9ST1LXaH zB*j?b9G|*j?UHdva_5TO;slH6Nd#6(0zc<$eo^b4CDIY9>JFAJum30S@NW+f3s@_X z?=Nb>-ubt4188CYzdJX6L*Z%vWdev&W62g_>4#he_YoU|1K2a;{%vXiy$1B8|5I8I>CP$9AvtPd z^ZmMurfT7rIgGa4&xK1}`&d0piml4WRlv|~-u0FGp*PRIG7{vyqsjkxTe|Y#{KEHN zo68?9y9dVqt!)A&wMi%I!QFg$=HLs=zDpo=!we*<|9i(CM1SWGV4}iny6CI-EXZjUC6UI%lbg<=Sl1^*pbI(eaKu8sh7<~MJ{ z(0TOIXF1(w(MI|_oeSUz$%4SubzQ=zj1G)6*#odp#p{38Wz^SPb+$=xc3=#7FM*05 zuOpaNqE&0fEn2ov^wzECRY7b{1_`=dS+xZ;M}g}fbK59#>8GGSJ8fPE=eEMjkj}XP<+z{&;c&84t+A;T0a{HFFs#wQ-N>~Svu_uG zCzkJ7FmizWy8OO85F%Q@wl7Ri}*GLYaxmPn3( zk?A(u82E9cwFkVNPs8GT9i?2jvx(QCiu&QvY8(ybGkQcacSW{!fYA;eXu7vL>D*tEJ`4hv>;^;$Jpfl=2SFTf zCcC!shAygi0B6+&@hUeIU%mpiVIv@zL3l(==Ypx(8d9f=Wa$9#S%eY=O2P%rlwAQ& znt_ZG-vN0)X~-z5r$DlmiC})kAY`Ak0iioAU(q%L0dLY3C%(l2c|mW)OFRN;E858Y zL-)Zrz$(-L>Hcul3eYcI(_40FK2_`&-cfc+;YW#Kt(caCgW@76x# zj!oSL`m5JkYN85i61(yc^tKpIruGWN-6*Sx{btn%*#t|`oF%rmf`l#r@-Gs2$b1q$ zoe$Wfo-sogZV+8m?hmw9>CV|e%`;912sYhx>kK=zeaK1758UfCAr+qJhx1{?G$H$> zN_5OnRz^7!xI?Rqow}9c74fLEQIM{u%TTOp=D$Z(=pcLIyNlY&1LMDB!1GvCJWFpJgv;NM!rqj~_Q4a91at#>x!Sd=$Vb zSeRP|L%&`DQ*758DC5kkdQ?YXa}VT6rJv7JA@qQ7lC=$`@z5ynbV+f7AVheEuJyJ& zdNevm3fT6rPr=8q1jLDgc$lz;3E9%jDbI_O-Q-!6#w($zQd*}EWk+oP?uxf_E<|X~ zI!Mm4PAo~lf%C^4}fx zupl6j&u`(etoszA;R|MT39K-AV36z_0U~7Gd%%CcPnUG(-rbk8zkpmUfidtTH#;-U zdaofk0`3a7?QZK#CpO5{4~2>0bP7UKM7zN_euR@GzV-e)!S8xWTw<8$C|3M_fI{MOs#SDrN(*Rrqg4~1|^jf<#D z9wv!q0aAn?fMxlE6W+*xzAr#6S$D0IrEn83MAGP1i0+7Zkt+!!no5W%YE75$cwKS& zR#DTxb~+ZEKXx_4e3$G5{M^!HG+;y@o4S+NO!DrOwpq;o~i5pqHmr4-gL5Jsw4^<7J)-9 zO6ydWFu5ClLhnj}ksFYIAu^?AzWHAfU$QN=?bKMyo5jmLYkjcvk6Z&zK5NO4Yt4I8v4M=ocAMGhExX*yGXaR;lJkJW+>aN zO*hiEQ90loXCOnT5U3}h*ShjY5vMc##9Jp*Hf?AZ{U)9T_C)zZu*VjI2UjFaZU$*`1_cbWRs5-Q@;7?&G$EV?j_cKFx6*(pW zoWh6)n7>Q^25{eRRVJHw`MCW)<~Td$8tU9q4_#$7BQ%Rh-0>uWzgGqN#5bZ>@j(~u zb@HDu>l)!@TJ;PgnUJ2R^y2aydF6~YTaVl7Rk;z3FSQb4FZ+bF5Y7`246tcL$;uX} z8{8Hms4~P9UCq8G)dB5?K~~HLN&`nmhDZUrriO)8OtPR=uCO~ z8y!=<5>;=!`Cn;9GT9bTFbdz`_S4l!D9$2@65YAAYWs%6Sq=pTNKNQ%&CpwA!W*+m z4DkBbxfjSS!i+&GR3rq$NOg7p$@UdK*!qHdzW-~)A*V~kMDgNyOPt3>5|(TDt4UR1 zVL6?0P_L!m2))~0aRUd(Am;6ot7#<|Bsb#E7K&vkqDeB(2&RkiTYSYK^YLp5hWnWh zk~E*Kaee{pPH%}9PG_zwjig_d#KGXbZ+AaU>u zzIis<@Ci?L4EQ0KXryyW8=}U%s>C>F7DjkBWG{$_0V7JL=^*tt?LzH0p#3$*?a$P{ zfKoiR%yZ2C)IcE7Eqjl0%~Qx7A~wKCFo#PikW%&pg3IE#1yo zv!7pIu#L5Xnu#0af+1eIpg>C4$cD2ww|ae(F;MCK^{zay=j(~!8>XP})=~U=0)<5( zuP$qq$F8NIn4Y!NQ#u%oWSfM6>iY>{cP#G`qWA|d#^Atc4zfbyE*=2oLc=(tEQ&T3 z$LTv0Z%L)#sV${+xY(Ol-xgijHaLjUf6o}fyXnp5&(UM*lf(6F0nVmu=k~O=Zj)~R zIGT!{l?-teC%|Ct<=IYS1`e83z(#T0iPt8W4_g6#|3hPO&<=rPm69(M#a%Pe#+6Y! zJFcHm?jP-iMaOKO^Fzg<%yj{@7 zL8eL+F8{^*-my-CiUK(M<$6lH!#gjRHTdD1D}oo!l4P z_n?Rw#f!y~#Oa}w;$Re$zjOPyWN==H2yR`UDS^(Ag3I}}1SXuk2e0f-9eNMm6P>9t zf2KySzaw@cCwq~Q*aTxMy7jzJwY+R7)hQzBamV3^vUIBUS%h2<{NqxPG>XY3V7I(+ zzk{7k-~W78e*c)()W7X_?47bPv>Qrs1byhKunmPC<4HLJwPLA?-lW@olg7; zDxX#~jhCW@m!u?V8%KV#t zd~-qz<`x1{#_pV_pqlRv!Dis7?B$P{0W(Px{mTNlQZj+shf2mv$Ir_(_(uw_~S7qa4~ryvP2V+F?B zbnu96nxZ6Rur{~l6V>4kqk=d9Ed=pg3awZM9n)*jPD$)`!{fL?5J9+eGNM53 zl6K5TQ|4T%LMowZGVCbw@#Aq%lVbSL@#<=y>`_F_s@q z1c!uJ>cs3-qVOx>$U7!)sWMXu%z!+PCN5 zmP67Su+zJc?`FC*`&t=ehuA#Q;G$GD%uy}N4Y}`PSU^wm&T=1P?4wsvTi*43su|NQ za6Tmq-Tvk~GIpQN5JUDI20#K-VF3wde_}wcz0!K9M|j_a&*&N=o|QIvlK;v3$5tJW z^+Ur1{+HF3Jt}3l2g$9e%M#@UdwPSZ=(!b6I7gH@Tvrrwbnv(Z9#v+i$D4qLoh2Z6 z@7lhfMrdfIR&aw*-S3e1I!Uw+IUbS0l;YK-WVn0*^UW^6_;Jv&1Id?G1+i$wPj^Q>-+;b2V&KK$s5oy2P9IhLrnk z6DhlJIgdIkjGT+GLt+ba^UF5tOsF6);5O?DIdO0eQ3jD9s;h=to&fU1-9!FWuGK-- z#}xNr(*{iqCH(Plxsd>?-;DnO2*mu(Mj}HYE4Dy{Df5!~WV88b%As#d2Zr`&BTK2dY<9nnk>_A$ zw#=t?z8P{}DeZ%!=>sno%yxlp7}Oe%D`(2;L+=1V7+f|Na|T@qZzg`WdkjU}jd9aY zQifj7TR`Kk%&lur=!Lc)`OM&R)(4loweH-qz7zjcf~%>sP~2gijSNn*5Cvite!Ug^U4AjP`k4 zK1PyboGrrQYD?;Q=Vi!(wm|wcNTb;=RI|2{dnmhnt{#LqsiqI6m~RzLjS|m1jq{VA zmiv8}inw^6U5)OwkRLGbbuCkY#PLg(S9O&uZ&s`noXRE^28@*;yLs8aVPIj1XU9f!Mf%=7F%P;G5PXWsqa9~oNEyn>g_%8!9)M2*R zb2J7cipRdX-)?7flFGdrqe#=Gc$<1@O=Ixjwfky2d*bNWlh;zS(jboyZPk1MH*wl) zk-Y-$uVD;S+20tYqyoV{J3or^Ig;K0g5PUGM6UomDAEnX87MPk*z#r~2zOwB$=yGh zR+3tuY6njyN*N;}^cy;{gxOYmi9M|ntW6WccD%V}*Z^+lPL{2l+I&${W0tnjnPToK$U^pkOGzV)-zDP z-`<3as7C$H32|dOqL|r`iNas_eKXa_!eTY&O^E{sPTsEn!7UtRQW;7)n~#7<35RQE`HH>qlVZ=zL!e^`$FVKluI>ttAPJve(y?l zP_t%l&O_S=Nw_QX&E;e$nRs^&!a*k9>|nP~I=(qv?cO9l z3X&|+XY7Ibh&=AxH+EH~B4Rb*u&0sv!*u@R^LD{KjS`cQy$F2m0mJM|lY` zWXuRdYM;@Ex;5?3VouRHm7d3)zS^QFxW}}OvXQ8 zWpVE3i}8kbv`4>JxDm4V(qUm0jE!!AzRkN4V-uhxwnuS&f&kKSHv zBanv~Y=NA}fb&>o5J0WjjIz#q_~sY%d143I7-F-f8%NpH`4S zmtVk?K(&Yv-iDdEikd}3+1s={7jOXQU6~Q6RBwfp2H@0{ty4FcPQy>EB3o$COk+SH z`{T_$xmQDeA+T>GIu=UO7|(E|2mojjV|pQ{qi>a5(DS0Y`Ow%T-U8O}W>f7HvIreX z!9waNa`OAjAJA>NmgUn@Vt^y(fdXIf+a6o=7*KcB4Kv&&mKBG2-x}+~56_tB2m-!4 zn%Zy>b|_61e?{9+UG4D6Z^+(IF3On#pM=M<6X<)qmh;s}Z?qqAaz5O~ zDEq}29R=6}4W8x`3oRum0CP#qYaRXUZ0^~%@t2Zf4x^mQg`fETp-LRoIYyQ4_(hRx zA(Rt_R$7&|?_huID{l5{`RW>uFjVRI^ zXE#sQiP}MD%mZvAx>i5%Lxo_ ze$$S}G`i_|jpWbl?qq&h>Jo1BnRSh-iS;SfRQw%02Vyd!G?(&4}yrc}Z&(Jw2cMl&zQ2-cu!?r0A!5Dv?c>7;QjwhY-0 zt%G;OIjNtlDrp#uEBWP@ZAEHOkp%zr6_br}_Lhmn^o?K=v%So|V=iYkF6}khFravL zIuImvdau;`MiRxag*T#!n3BbL_TH`GC<>KAC*0N=dYk#tE#dLtwzk`x8tNt6mtJUi za#R!CrknMi+Z5!iQ&6Ve)`y4kre%~=qg#Z9&(^&@U6*bCv*J~2K(ULw)OjtUahDd+Ds2q z8Q(?bjaBxOc4v23sCC`ipB5@mDhn=iNACe&&TN*R_uZ#xXuBXNwEN<@4B1w$86TR6 z1PD2FTub*75%KD1w}4TiIfn-MF+JR&85=lIdy1h#LPt z0I*VupLFo;-bmwy-sgSu0nYd>DeXxWMdLD{pY1D;2L|^I2Sn}h4 z<06n0!{gV!Ixr)|)7ndZKl23cl6f^eVdn5x?%31_4L&x%nT~e3X1sP^n zK-E#}3Ipgznl>;?ha-*Jff-llM{ND}P)WSKB%|CP&mA(Lr01RW5O^RF_l0!%r>^BD z!lN=}IcfW8s^y0H=%@JN+PVb*KO_p0a~A>jA_tFc+NhFsyKXee)+XvE zgnJt#RMZ!6mT&~_;}uHol5aAsNKI>?1C7S}x6OqZt`R%3;upruO_XO1p;rdS4iCc0 z&cg!9xcWUT9yCP7B#ZO=>Sm5nfXu}fmi71!C~g;L@`e6A<*L&Vy| z5`DNeIaXiI`(;4+&f!q3PE_Pw93Bj*y=-TNT~=3zksd&esn1K}vc*$aXAD%@Wy zgWYg+I5fWzbX7cx1Up==OV-AJg){Wknq#0^DD^VcS8b?9eK!s6lZVxW?p>pM*lMp- zbzK1zxoJjcUKCQF@KADCtGe^`XCO^!n8|iLC~a_#;$qr;DW3J;2=IC%Lq#a)M~-Pp5Szf27z9xVjb^y z`hLjAA_|3Dd=n!|#L`Z z5XuzV>@5RiVZZW9lhOa=G=mgTV;@?1*-YuWpmm&n%-=353!(#0Rd2cVA2!}Y8n7gd zg4ljyPx?iNcQ6s@vn-|uVfLK{$B~lkvBAU*EI}rRhOK(e$6rVlP*;bgLCIl6V&5^;s>Qp?InMxER=Nmfu7B_+ zIK<=G+G1W1^p5h1f;m^M?3TU!k+*KH;erP=B2*Faj6xev(^p0@rN#CGEipyj&0 zOTR;)C#ZL|H4k@+6-EH`f%8~)wH{P@kM}^9@$~}6kZ}OB4&uZ)$t)@kIvQFWBdWWD zV`v6D`@Ij;7iH%FfjDBM)QHjJZuINU3z@fioOm3Vags-RU$*Mk7Xg*Z?@26T>U!u7 za+Yb{!;E0nX!NW!0a)&&X97m|-+Bxy=cX{m9&8%?Y21f=73!5PY1scH&H~~^R(jBY zz{zAGLwU8)pvR&#c|=pmbZo#gU-URinp$32NKsQe(E7LKvFfV^MsF(RYWnk|tkUMQ zZZ6j{v#%`jnN(Q%_;tq(-S7bQ8)Ik_w?c@q{mIQ!wP$2g04>g<@4J-TCs&)bk1<5B zQyxO^LUPCKn4<4lB5rT&2?+vF0I?b{#wm1ma@vfhgd@RcFBr zfmX%zP^sr)GfuU$G|{|+aZl6a31|er*ELm?OXlpf`q7u))HvQbC(d%30|-&+O+UbT z1-);Yug3w)V+O+N&$P%t3jIL}Nux&tU`?n;o5O?(9as0pnf=b_Sgy&(}%-4p;-vCXsP7GR9wIYbt}uRH{$%QzTVF;4C|f=Z3`-< zYOhP|Tda=hpY=KG7zyW8hRh z`jX`O{;L=TN+1dotrH5(q{G>EoF7WKp>#GwKZ?)6T9yUuX!(8hXuu;{M&<_)Qo)9t zY9YDmdi#P)<|my-j=#9gbcMzSzN5>Yfe|F8PhBkFbE$^TqDWDkH=eW$={O_M64Y6h zov@pw%tb-|up5rW{uf8XNI^tUDbFcBMyZa3mgK#~WXP z4e)LGo3cjXpS;bJHD7&`YycKrguyp9KFFJU08JL5rAgeRjuewVDfXOYANiPXym@NB z$$;@v>E*oxZM~Y+I%ZfB)5m?#c2`~_ZkJUqTLW{#U_+80nR#KcY3@q@Tw*@oIiO5N#ecm3tySg zL{Bv0as(0K`Be=*=F zEcjtrHGo*8m+xWS6c~-iRZP=n$}MXofzdsYGwJHdSwm zMmQ50t4=au&&+Qd!ZU*$u{U)nhMQ=5`4`H3k6X!mYnEI#D2IR8K-}h*gzGzhiAh`f zN_f^o>jrgarG^d3PHh8sY@r%Qo&FdPx^Q~jjYxtR38P}oJppi8%c7j{)oV{*R z`oVYz`zi0(Tr8Rj`GIzkz~J~G;}CDFz%l_xj)%1KdkNQsh?=jJL(OFmVTiJ|;GdW) zyN<;V0n?p=W&M8i3XcgOOR#SG|fN1{VIT zr>f|PFDKtSOYYEakZeBb`~GJMXklo_)-{KaP&VkSSKlc^6RdkS)bM-LJ=BYQ+)oO_ zX##~WYj?w0o6B_o%ttW0D9v|Nep!y0E7ToDD7)3aqBxzGaE8rf)S-xjQ_DP9YSSqJ zy|&PI27!YLWyTV;g08028YVbOw04XKR{jF5-RLPeB@y>XG)(Fuo>JV)VeHu2@SEcQ zDAkI-hM*+gT*a==n(RI_0Mb9a^~_kxBP4kcjsBf3%Ls8MLN?AS0{4RbeBUGGft zXFP^+H%mYrl22Y6HgDgJ=A9e7kMkOHUz2-2(yqjqiK))H`n+(+dp`oshJXyor%tHb z24=gT`wW?PTAj%;Oy!u^Hr!g&xao5U*}5li!}ce%(WQ%JeNczvv2xIyvm`J^D6>&R zo<#lwOuE7KUJlj8#H_V+$vo%VD8`A_=f+_)6UO$_v4Q%N|LQzu!=;V`{(tFPv1~a0 zSC)G7EWHKh-r=|Fu9jGJ$L*=P+F}#*BC zD$7E*>ySQz^W=1fK zE0Q@Qu%EL9>rpfHMsBY{IeT@{!TQ_bxMUCKmG!R4XzqY<2^liqe29$5>>4ljdKArJ zFP^~gGC^TV>n*tRDdrR{xKN-ji)rG& z-cx8ZVMo%Hu)4HqSfEt!UyJV+mRo;i(U&kagI^1`iHj(|ooRL7THZv7nS^+%z7rCO zaDG?neRfXsF4|KD@y&fJl7v1~*$ko%^$AAmrF%y;s?WKPX57^+G4GhQz;3)#YDBLi zg-9y&{dUHmPz8-9>je35SEOO)x3^l6h?5T9hmC0S9aN@88*Nx5ch>+9b*Hs%-iB=8 z-VP5e#I(2rhyboZApr_6B4AGDaQqkj=sNdzI-<=&aM*A~9B3WOLUt|x`-Rox2DnLe z2Z=xUla&v~8#%i!gr3|GeD*mfOUq|7wM!@bf5%5~HhNKD$TsY^8(q~M-}`t8De~qO zNFHKg3YB;v9+alU+=vAOYuipxk&j?_XkY1Nt_OlQdfd|hsL!-{3rbfw2GhwXn9XD& z8lQ#2}NV-5i+U4Wj$y>?O%o~Pe3IHWVA$_tg?Dg zSU-@^2)GZs^>Ewt1EqEVP|+8riH4g&?EhcB0*~t)SFz6n`NEsoFR3y9D28*X2Wg0= znY%x`PD!wQ*czhH(4lb5{u9`ca4RD{^dz5u!!plG(Z z2nR)g$v1U8^=eQTzohu;_0OP|Rhkx59Z*8w{;v>!X!6Wq1XEw+{UE1kj^}=K)-gZh>t}vR zNPoCJl%bh4BKnN$?m)jF>JF6@ist_5DRFm<`+(H>)ucGtUFGQ8R|v8CmF8_Y)*{Ih z42sf2N(G}NV+E@wPvco_S%r&dEO;CY8{KGV3`Lwp_0jA(OgukB3-egS@CaD)-TDHM zy5qasR9ZtQ{@WGxA2>5Vmz)JkW9Yw4PT&<_X|O+Q^4a!2mziWRE)N2Y6JaCO59YE` z%cW??GK;&DXv{QBb)b~BS{wP~tc@@+O=;YXbxG>sX?Vz{n!yTGOIiM#4yK9Q{wv}< zmazE*#h_zBLzy7cs@_pRJ`Chki4ORURW8zR`$Rq~&Rc1bfWx3a9{ASg+?U9NXH7Tibw4hHne_E(H_6>5!vg!=~k*^{_j-nPwJjnyk*8!es z4PlY47sM9@1$TdY-dqa0TnVZ_uDS?KF^W1={XrI$d}psyB&=lbv8MTP&Qj%%?zu1S zAAmSlXscQ*x~kyZ!Svj~_q4}!ONBb(82e5JG?raDC;^8#XG4A3+(5eo#v%bE=pPyf#3~Vr+rzPUc*~H$9PC&Q|}=qplJcGBwpbF*P(pP*14uBSaKZuA?tR zOin3D9kycO;6379p4Z|kiV7{ATpTdQ#VRKOHC={BV*szzMpjZ3nN&@iw8yXmE`9c8 z{*vG0)$v-;*Nu-J>x$bgg*py0soF4Ek5>eu{7!*3adYj05sa!;lhC8To6$7?Ruc5O z^45$d?koM!z_6(%U|~pHn%xepB`cDE)CPEW(Vz591HdiBYy`;rxIq|+W1i&UCv}D6 zypGissP9j*?A2xY${4|@&vGlo(ide9GllywJ%fGG{=A-5O8aEVohxfg1YNm{ zz_sCv?Y=(q=L2EI?u)!tbOiT5);U(=Z0uUpCgdpmk0Z->PdD-QjKAksFtg zdceYkyATv-hjDVe(e#;22b~lMz7R%0iRcFU9Z~acs>XTDtqfGT-O=z7`UdgQ=@;NC z$Rjj}38XXvGoUMcb}d90p&_kHK#JuFr$=p^0hRDA&>P^Ocl_r=A#85^GJnP@Sf><83&0~9F=3Oig^4(C+H8t&lX{R&E{?JUJCta^^R`?y- z>sMOGHm*o+E8kTg^+j0qP^Na74N>Gx*El6Ym+QF_1jd{ipE40j_y`KfR#OP>O`Vd6RpFK8zpOcR$$+n#b|J|z<#ZI>ZwIBY!abMeY1qZM{^b+2)pAG;liZ057n#J%2jaNv1CfX5rfT1moM zAj7@te&?A;x6WVN3HWy~pR(?$>GbzN>%>&dnxll$7f*gL zI7+^I?Tch^xWQBtuk8}|iB}i>T#}z!|F_S1Z1*uM8^<8k{T?oy^T*u)J{hco=&MyxNA|oh!I!>v9;IF zt(3>C_BvC=CNAe$<=^a#B}gYxw%$LVrhHxC`=4Kh!7LLqN=pqdN;l`PVo#BoEHN!I zvTGD6?vEH}Ins*RALf$$C}lYe#s6aF8tEX7{B+c_6fj#1bG=~`w3%^bD%4?QEUJjpk8Gpmr~`WRUL;dwNtlS- zerahQd$Ko_p;pbCtkNMwcr8ig=I{0Ob>z{P&AFy$Rs&g*sA3!+C=;*(vX3A2Og}l`0aLU>?uAkR+ttJi=a+b>zMxSbdDDR zr4ud9k~P0JR`yTj>+J5>i|j}bVP2f^>t+^T3ZYP$_7>F$!zEKytP-p&9xcW1^bQ{? zkNAh0>_bhqeExYatK|00ZP0rB9yr&Sx*X&Z4Ngy~pXTeVlN>4?Ms*k>i^yZtJ&NTD zR&)Y|>n|rB_p(MD^|X3=6b1B=56;V!ASm!K!Hmpzay;%b4%l&V8_m{a}`R-HI3kS8r(VZ6H*j;dJ9vUB8N?wzP4!^!h@Tm(fe==Ob z{c|@t`t=Lj?1%TfVy>zcj@svCd#~p9T7rvX7kNM0I*%arcsij?KKnp%St)I zep5=xStO^S0!_U?b;{d|O*La-%_xl6pHC;F%I}+GD9TI(nYqPU%S?&>I5X^nm&*S0 z)L@ph+%`PMKZBeSQ2u77??A#3xxO9234WDle0MA6c@i$4h)&8AdxeixZv;9GS|ch( zKIorVS|0U){j?JO4e5=`25G$yK^ed1w$#OgiV%{=mnKyI{CpLA_1?1a0`%)D(E;m0 zF3v;(K16QI7Wag#UD`j0C$Zj2VtT}zqj5)M1AAba17mvT&L)-EXmcO6ZMb*;2hrcC z=Bneh1Dw$D@=IYo*_wZTS;mIsqD=DW4krfW~ma}xtUj-J}7oh{tmX?K$~Ct9A%aY=*H7xIUKWu^@4N9{~9tKsp*JH&4f#(p#Y zz_lGgTGI#)UyquM2ymQh)Q1}0$ue{Fuz$7|Cv|Si9H>UCGR56kxq(uYio7m?eHrR> z7B1=CwNd*%;_FNfV@~7O4sw5wuLYXqtoo6+s27%{Pr$DOnc-%v}ii%UWB#9BEtL?qr_U{a!JUl$)<8XQz z7|*R=ArDd)k>3kcGpR}bJj39ZA5Tx~JFcc~p9|k5fyy?WpY2CKo1#KhgK&as@cJ=7 zslWe-wSud4%dPeNw554K{$pCeTYKqh4-JdgG$ZGx)%MBmDnaw;jIRcOf_5*0*I%|@ z`pW|*Zh)`i-w&7X<936hgd{`HFl{>J+NY9vhu?gbyI(*t!92jdETe?buRasU&DP5q=;^Zg=fSkJf5; zJGpP{Gw<~NbNhY;xa|o=WS~SV2CxzP(%Vb?;CT$-v;0Hg>x1jKv3D16 zHVE&y7hm4|`(9)=U5fsy4$>6NpaVGh#FZNN6BgBm%fwbC`ZD$pCCTpB4+}Zc;1mM) zZ(1U3u{hawG}h$LP2m0?-6UgmHh4`M_4@DrA~*%vdtAH$zaK0yUM?`s3`D1x=#^V^ zyyCpFgyROO##T_!?1C5cNZ5JiJ3F@rW~u+upP8ultF{BSpYaD?_+Wh?^!X-`#e8*| zeRT_R*)R2&URL;@Pa{)G!F%6oTIxyd0ZT#v>r!2auFGLwgD ziH;*DSLOqR4ZpAc8+P_CoG1!?j{E$5xg5%^7}7Wt?hh4SYm#zSk(eNAP2(nxL5if= zIqq;b>YWv(ZNNEj83+$u4c;TBziYkH|CaEan4PIyn)71xMgGmlkCToq8P6xm&SwFU z3CUEuq)|Vlzq}@`fAlzvh8avsE;S}u1d_94C<9<_=ebB7MGFW!Q=dHt+V&j(vmeb)jQ%SGi!|(l_U@!SD z!9$r0s5vn!%3BKepI-)~mc^^7sKU}_gFx`J-IduWN8iIj)4so#=}V&7#UQ6-l#v)(i<#8IC&B)MK89a{xU%LAXxkDvARzi*`nmOZ@dv8 z8}~YR1BZ$!N6(PJd38+I30X;bXcxj5FLLnZ{K3T~L;6#*j?y4g`ZGT@&*h+Bv`E+S zgRksTL!y};h_R9}l)a%Y**cy}3>HzG)S0d5dQs&fr|D_5|GFX4tyAi9Hy+~JpO+E^ z^Yxb)H(-Si+YDs&1y|u3j44ZJA2uukQ{zpozgD*y`A1aoD8=Qta0TPpUnL+%mWI%| z+cH+b;yXv@?_)L}kP`)jjDn8VlqU@>|3vA%o$*CZZ2Rf!SX|Dc+=jD{_Ju|RBf zd2y=dE!!zCfIBkA8@P+_3%-KwlKp%}lx-ejtadCoB!x~BuD_Xo9qaw0E?jYRJ&%aW zH9T8UUQheyaBf@a0|9x5+mta1wfyu`VSsQ%2Bf|PeEkc@yMH~C?{OL5Wd3TZEjFgF z$zPjri7pbvDl!XY#G#$tkt4KH{>-O)V?4L>pvx@aru3@Gn_M%Km6s@n%2M))f$_QR zyXW;R!%ZJW7b6P=NbIpb?w^6rHycoUqI<|I6td)TKlnuSR&r_s+KNK-a+pT0M>R1J zWuNxi%p8d$Pvb5?F2F;{Ef5#rX-G)4y5?{E{;JnT>W8RKHS6mX)-%xXJsx)fT@KI3 zo%pMh_`Nvq#&#>X9woQIVkm3rv)Of{SF!_}QoCnwgOL}@BC`YPL1#aVy+SKM7Z@_y z9*sp82%egF!lSw_9$W-hmq;v@=h%|yAAq~&N%m~BG{I7d(t^Mx(9PhD@XFb~weps$!?!K<`{MA{1PH%#hge3fmbJsN-Nw8Sm z-B+kBoOPLc5+<@%vXE{`@#06;#j$GJ$sRCL;TF`_H*ojU4J{D`SYEih*NVg4aQ{I@ zzS+SKM)W7|#{viUtGP0ecYwa>J$ZQ|xf<9R@u9Q~q9L1GpMH+!@iU#>gsgk^Z5$Bn zHAwbElCd1~ITf`^ao>5X>`~S)clYyBVASW|UP>Y^2hAI= z=J;cejL?2=UbfxuYnzXX9Vnsx*{}A4QHQ%*2TBDQ#x6LP@6TLy(;rNZm;=cY!}X`V zZ+Sc(Gf|#D!X}9*8Pl$cSMFNwj)QON-F%#y=gw)N$di=iHC0 ztjd1-afT{bCQGuP(9_}|#Q*(e7vTMNZoer1$)puuVhNpb8kzsu;3bST|(j^s9zD zj`5{USql#h`K+Xd8L6$bTcfY>o14Vs?OJPj38+^`{)btNT!Y>V2XnTDZFB3X<*11T zl^bv9G9QoBg5;8&qcF+SC&n#y0e~aerGJe=>di3Udw-zBq4mZlW2K^(+ht$@NEDQt zVb!H|Oc5pL@oB>I#J8V1G}w%@<7Gt--8Oy4m62|%$Gru{F17m#}h$c4R z!8NswD_jmOiG8E#)mvB2uE_ixYSC`%t=g9MjRI-%vo$dw(&;z;2UG5WT>0gPjAB@n z{H>_FHx$_gB8cTI0f1pKfRF5%H(j(P*7>!FY)ix;WPmUH+_9hfv3WH}0sSqJj*hV3 zMCGCh7ye$1zl>O&%eET5qL>&g41Ebe+qZROlD*t*U#FmHKYmt^mlW zt~bD1AQ7tvfHT^)<7%2iSMz@y107Hk(LI}ECd$iUuy1@(p09iTMsrJj50cH)b00pF z+=SzuU5!APmK6DEOaBaftt)4$Ryr^V-Bc&ix32s0zQnzatXrlI`80EOim`>g9ZRPo z{4XUUKiI*J?XTVOke^lN_@*u_ojc(F{9`zYB(VRzI>QGY5HU96UCSOYwRyG+T6bk6sTC>AdUUtiyyYRahb zeP%E!Sn9Y(yZJh3&>w9fKoRr(P&^(0f_21d^cm@45DbcFLV;cHo6WSWcc#mS>w8bs z`$<Y!&h+`^O9i#; zHuc0d3Su#uOq1Z^@#UXf(%sE#)f zhF5YLnYgXWr@__f1e~M9hkt?3Rx5kuGObi@BTtKWA)~M}@X|CS9B*KW{>+X`CGP>k z>6=l$TV{;IWS!uN*4wH+mN+(FdnpRSo#(3s*x~2rL+~Q2md8$y4^}|so_V=li_g?> zyb+*tZ-0JhNg490%1$QRFa%Va^HzE1%^6VbXdKF(J<$x?{NvO5&*+(R2%**`eb7-u z-B(Xr$cvwpuxjNfHie2EHR%mz4)-Oy?Hr@gK{Br!pYn$MhNlKSVttKE7fPncHcw}f znf!X%qb=V^93hyjC!CVQKqL(+NgOmkhOgLgSyE2eWnVU_K5t5g_l4`m@`J74#rH#^ z(LW?nW>UPrLF;tox#O?z9VNL{goq7sAMh%_g^JQ=16m}`m_ZgU$9xgfPY;{XBDoir z7Aq~@j(O&rJqNz%USLo9em-7vOqc(P>)q3GSwk{c03nQKyiNQ9<^`lI1Nmtz=tqXd zcXz!e8(9cT}5980m^<>MpCXrSqWB0JsW<;mTlI`*ejx#AVtE%?T>ER$C_% zP^k_`*8;E{6IpaPs$41-CAxNW7m!MRV~=ML_3Y09%Bsp03<>HI$o8v0CO{q_TwMX3 zXJAila4`azNX}rI*C#X0_~&;fao$Iw7K-(22gGIdR?Sy>5_UvJO=1&=gJ4S_#^<5W zK*%Gv`%ZUjSqq6W)61bQkCyoClX#u~lo;U3U;O~9V#$&Xu)UNo#Oe6@YhA|HA~eyU z#STOEgOuIF>ukxhZ5ejoz9RW)oe-w*wG1D8PdzRlg-jt`?21eJ`Y{_A0YT_@@hqTz2~0MczH{(^ehrDWD>^qSI`O+ihhj;`)PdiX^-^}9yC(_{+M z#0JljU@iay(w6$#gBwzG0D8|T-17iyzOXtY@ieHNVEgPv1Hb7?0U-{k@ zU`j%(NhG(~fMg5aWn!N5V_TSgF)k@X%9x3(l4*<45~uwH^kLFp|GEQJI2g*pb@;wB zkfppSfW@Vs-UC)?(Q`OMkN+v4Jl-rir)ia8zzuiob(o!;WCbO*J$z@L*&DX?b8#mD zR2$tph7IQ0{_O zz*4WutIrqn#@XpWlb<(pNrszd?%T|uN1-nX*1cezalOl2KPq>t9LD{&s}U>34Daag zxN+f_EOCze?po&t5T`mA!vdB+zS4v1$w>>|ir&6V-bMH4#s_F+bc5fj|EhK6T3>>Q z%j&kR%UbuGCz1KWX-G-Gu~Pu2PX4ykBAnN&)sG?A~8Xq-x8_6-;7p)Rf$Jvb~tBS`gM9Ht1 ziF9^iV6ZjL>WoZ*G{=lC>@Yq??nnF`{AA&BR5c! zRMTX^_%9Z;3T1p$nn1f^9 zy{Rev{AK65v&e-?d>K;?M6s+SSE~s>j_KON&E4&gX~Mt0%d8F^9)|2d>;U(?T<;lh z>!_v&@nQEjFwoN1fEFozc>WcGGUa4)7c&dRT&)*My&^6jDGdOu`YuFL$g*Xw0H$fy zN?RbZ0Qb;rb~_P{6C~f1oKyQ#AYftXO8sOCkjERpo51}GVi9h1G#|AH3qz2ph)a1U zU1jaJQ3j=03MKLaY8AF5dRXw9Jw=ecLlTVWz0IN}vB2kv<0yvN{5h3(ZvuT8HXVa0 z&Ho~!TCJs_A5UzZoE*lS`feEVV$9&WZr20fli-#;=Vcm3>~go5{nctslbY~8_h=BN{Vx$`DEC76EiwBflM#wq?854;JgR#Da1#!uq|8r}2>3(Nmsz;&Zsi>>%46VOqpb`(JXy!7aJH8$w#+NR^j-Lq@nm=$)IZnUh#Qd-qb;^uomyLA zZm4ZkW);3`k?}(^@x$n%?pj|2>~d)%ud6u#A;#WRJmCgXrfzU9DO(Soq_SAXMd%amI9n~0O|b) z_yuVJc=%s0#ghh=KQ0cxfLa``PAA(6L&0;quJc9<7u#^p$t!&%m(c|^nGm}g-|loE zhvMy~&+^KJU)}lE*H{bl0+IVVwTyV!GORve7V>&_?PGuY4Fyawn*O_;bs3U0d_f6~ zX2c9g^*Lck1b=B=U0b zsHyAGz>_eBoAL~98a84O3=HI?R%J5*Q2G~?7U|ieo^~CBHs0s)is96&QZNEsaBrHp zHw_c!I=}m);r=U7&<4aG7fg(f%Kl`cIT*W*2bH2Sh=f)@i4n(Tm!vkAC7!8V7@uVCPRtVpH534U!Ytg~K zDeQEL@O{!E_n|$qf&@#-fOiJ~xX+44pgOy^Qxf$IU?dXEJl@m2<{%k^SS4auWa!xQ z8YJ4AalZ5meC&k9kSqqeLhIRI9Gn7?9^fdIIQMKtuz zi4q*~_koM-t2Qm~T9%eSMJd98n8IQa58o7eeo8)5Mvc^}Wk1Qj4QjR3F&!09j4$1N z!hWCoxg_HhJeh#9vQ&LLFOgK~p<1vPTF812G-mTnetl+FDlpD+9ZnYI2efqIO5fd){ z;4yO<1L$tK1-03-3``s@(s=guJ!1eBT>E753Xe{pNJiVe1!5&Y3T;P)?Id?IiAnqK z5ku<>uf;W(PTKGp<3aGrr&i{wtK$W#kc}JaH0EvldyYR-H!m4&TDh~zmYvlhSD!#1 zrYS<`6TmP4w#_*Z7iWIHJ)Dz*C3#gPJ{y3UFTzD~uXpFaq|_rQU7Y2S-$F?9xR)W% zKV9*ov!H@j#R|^@OA*xXAMX{+o|niW!!Ad6jp$~6mKg*b#4-@})O<9k8xdI*NXjho zklc@n8e{OEcbn`^6>(Vx@;uiM@$3o()&+@zwi9=xm73tjr{AgfU$EY7Ysz~pA>INl zrJ{VR077eUf!Z9AWKs}r17BEqYdkQC^%`TdviI0s;DX4H6&yGi_;EEk0l{H60Nhq`0H@;zuPxsBrZ9Zh8qaMH@{oFP zz^FNOmd?>Or*@{J4q4Y&%JZ4>Mfa*ThQtC7^xcl&eV+)(&jdf?%8Tex-4y-vg9dgliH?(l`FOs!~+T~wC zl>7GEV9BwIH0)B*-|(Y~ENa}h^=iNCF-Ypk>M<~jxo;_4Ddi|N`;gH*aR@Zd@lNcw z+>c6)!Nd^oY0+5w`XbJEx+UCfRrr;Yuh9+@ZaI$Zy`r*BG+wTvAuukHHFWh9(SmN6 zO>^$K^@@|jS{yP)Aq>^3wo>;2k|+%Y%);LfE3SLg;$c(tn~qxjik2mPwitKft1yAN zZmic-f-QRg4*Ks$`@F4jfEFQcX?&|Jy|iiZ8O*J^&|B4|i%i?9wM8or^!AHfQC?$N z%esSJT88K{-zf)$|j*!uMh;Hv4-%`pT4KHIT7NQ?WujjPV^H{}yDmql}#@|v;mG}y> z)7H~A7m+(2#2WOOZLN?_yGi25Ykrz&AsIK50SSa1^|GcoEoIK+>rXhOcUmnjHLl<1 zDePsZ@15G!NAF@cEN+qu23sG9=uNh19>FrlYW#a6P_a6==eb91#*g;GTUQxpHaPFtxZ5Ypxx7zE{R z8vXuEU0=Z+KC@@t1P5$WP6-m|6Z_kb&&|KG(PrD6ELHZq1D90a=+h7g#+nIxz+Kml zixVZbb4R-ZZzlLrijc^Y=l73HPUrF+!oq!Qbw!Yk6IjxD8H;9K#^K>yn^qwJ=&$;y z;^^q8zBZW=z@vG#!I%_qfp0?08F73K$PmuHjFtXA+_n9mq&3qor9h})sA{D#;Re#sJlS9rm^P#Fn%g7 zQJhz;%KWq7q`?oPB3a{2`4#|cyIDkgd3lA}%QGlw8~axG3jGFq?RtC2c;+GK=-VyG ze!KkavG0GV;#$-eTqX_#ci(2Qwv6JqhK#ID zwE6AEX#K!sH1FXiRPmPN zw-=X}LCwWr4I!(x>@pwoUGDo&P`-V-_IkP&7RvstO44qz**Bx6rL@1q2M|*p1JU0; z6fquo0S0K*MwRhd`$cDDiwvZem&NNFV@ongoNTgagvXDiPK;};uc##w>rP0?hefTo2D$4ebzel@Gn~l7`X!fqI<-MY0IubjP?7hg&H7c9}#f_Dk`z1 zF_FKuvhf(LEZ%)n(rj&@N#MqjWZf0o=!-H(0S*p3@0hD#E(ZWV@yC-Cw) zv=bGB-6J$`mk+ZR1bi$=82^f%P4)GEV-W63Pw^6iF1}6+(OH(wmUwr4H8i}fKar)N z-hg-@oj-Vel&!}yeUU22#iI&YbxFeQviZho=Npn0biK^M}rs=E#gky zQkf#XdW8qlry#21v4G+9ZY8tN1*Y-)Lz?5jbCA-AM(%oWl#VC1xR+jA>OH4hH5Wvv zzJYvt;hK0}BL46Q(}u=z8<`&y6jvn%0|Es%w*t**9s7;&Z&1X#L*p;GHIor6oK}To zKC@5`<0DW(_FE9-TZSffi=OU%F`VWkxhL(t2Q@p~vI@BUg1lJUVJMV!tIp9As5^w$ zp#W7eg8YbRITQJku1Lz2@L=2~N#&&^yn z;(-8*4k2asDeu)Rc33Go{!1CJUn3r&Z+* zI&g7}g4K7uRhu=Zi$afzwFlq~jxx(GR*RpkT zYeQq-Ix$ETSMZAo&{Q~s6v9upNggC}KBFbV>fZz0$OP^u*N{@epv}shHeTZqqM2Y2 zeSW;de}C;Mpyi1b=s%GDir}vA>W{;FcGw_;({>`3gAn6FeDz;Dx#S(=pMit9H?Ii= z)6Zn>1X{SoNz<@CN=-?8pyB)R#U7`vZ135;U;j-zxx&bHvu zg|W8Mb)^w;Zy52`WNT2#gw~iY{f8V`0fmg9FJ2A-oWphrK-ZFJRTMc~m-#{kk(G|n z`|zN$x2j!*)#8HAsYhBO#mH-+@mroiAB}$-*C*qQFl4f|@wWoVUNtT(A`mHn_IB|X zv-$lGO zC%BgxabCdI1P~0|Q^k^-e)ODY5LZ|1jEuo)%k&he?`L1XfX>e)>Dz(H=9Ob2E_|Fz z?A-cX=e!$}?1zb+lH^n$m8b7gAu@l6Yq58U?u;s35WGZ5T8V3F+Kw1)VHncPhTi*S zW7LXqs!mqojgf>zc9a*ejf*@A1eoI9Eix=D`+lG!O{IF{_m%-L@D4@bWU#Q-eV|l* z5wHP#H4p=WmAOSTEG%C4Un~8%Xh$j-(M3evwJqVMb7B##QjfoXCp2~VctN&b?6XAjrCPwX}H)Kidwk?Bpq%}pbdJPMy`TNd^JuywLY zy1PToGAAZbNPF92`{{}Tf5D!ws2azHr=xh&pPn_@&6ODm zeKckTB~!xD=0umMy{&dHgt_;>(?rxD?T-p6ON#aC<^%`-RU=)+uP3^G2D%sP-ANI$ zch<)WaL9;2`E5Xe`^;dUXS$|q*JAwtzyAGZ#mFz1OP#MejSW7FQG{__OX)G((DR!A zRx@>Q52dEdgX-GNlGdh%K?jG72LnwC6fHK<_Gfn$D>Bm+$SJFCL7WT@;`%datEQys zN>16k7~NR+t%Ajt#Z%MLU$(hMtz%B({`c?1$4fIVU;T7r-I7m#<$xzQ;H!x}sl`cU zX#Zp$c6fMLYjg|d*cnN`i;+tMfC*_2^5yP4HBJ%G1$yc4Gw}tZ=KJvlx2yfd+-45_ zzJ30=b96|{k3aJ|xf1varf<7|9ahl$jjhY-fQS9UDei5LGzaP)AA2HFr&8*xPD4@$ z{|c>3HQ?d0F`NgV!+K~-y3I;EP}0oc!A4JaxyXnqvW=dIP``4Z=+k(aMT>s)uzsU%@&`a5I4ma}TY&~xsvQBIT9zC(ul9Ul3$u_(X8YuZ; z$WEbTA_*zjedNfb0QwjEF(9ak&;MJ8i4$S$>mi+CJbkMIjajN7csR+{teMzAv)e@H zl_U2Sx0%jEg3!@L)5?ortTUExSC0S6n z=ql9HoKU(ycMS{r`PM0Ex5qgTAjW|aj|>9_UgtHjWj-cie@A~LoJi#GiRp;2$IIZ> zCvL_kw;tU7TedEb$1KQT-=a1xyj(*Oo$~r+mSpjv@<(**G9A$1x#S+zzQc)Bx^kw* zbQRMRdI{YzUM;qSxzDgO)xSg;kJ>J{X+U&#iaS#;zp@nM+w&rcp5wvo>gyVM1IcE^ zO8`zD-RP`|;wIQ!>c3Xs3#>{8+&N~rWTz3}5VU=R=AIQVED{$#=m z;J&$ccl01gmfSzYG$qS{wWM=kL*UJ>~!FJ)(#-RNmgnN)nek|8^m_Jxd-gKf)L zH%yilsZw-c_V@$@l01+&%p~iEh5$_h{Ox1lN#c{b3|<0qKenmc_x`L1VEv{AkaYYT zcuUQYUk60_MfpHF{l+QjY2wg@GD=l)#R<6{eBo%al3R?#THGUpn>5c6zPbLBQ&Gid z!ie$&M1#k!`z!#uyZJh=#92f7O>>}-B0=bL1tx`J=e+p)aH3&)l$TFivTOkyOa}GD z_f41_^~Mc8MaAn}dr>hauVtr^ZMsm2qaADR6%v)E5xH$TG^nhhjDuJycaS7L_u}#I zDMGXWD-yJF<|3d&zqAd61JtEv(OTSHcqAjPcy!Z$b`d}~l*~3b6(A;lNRuW!7U)JV9e z7R%srD}D7FCf0hmlsWba)^zT-%8D0V@zn12w%aTTy-*i~ z^|XLKWd%g($AX-81E>g}1~1fKBJVHVkWnmPb-K~B60yS|>~JOUY%QR%|4$Ixk=sO2 zuoN}cZ$Ncy24Sb4F0(?oWWj?AdJxA2{IM9FHbx)Ny8dCYnvO;VaKdc?nCH-HJ^J6_ z(2ZRc+Sf)e!h36K)HGu)Lbo1K1f_rpYlL`3bhNyTP318O5{fogT(gl$+*0pWrDps{ zumF{!51hlxS~Q@=)rLe*B0v!}#|I1-R$ovx9jO$s8E`Zc*Qt89qESjV3c}kKJrsE7 zF=rqbdA2v%oBu5(yqjqQTLBDGm*VtXbn&3MI>&zQF;YVF3mh)^o-p=d`W`GIRwIbp zJuZ<9zHdH`34#%f7jS3*{)LN2Q4G*#nDqs?FXr4q4SjwYbGuD63Z4=} zDhON{$Jb^qX%^u8gJdFvH-86~4U_PUsU&98xd=Klcs)!fp3vS8-szwvTy-0m+T}wi z1tNX6k#8zXi_XnQyp#3&B#M)1DfJOu@3uMTK9DMc)A>Rdr(8-ppP$^D1RQ)C3`}@~ z{V3z-h6O0p1F~YDR)P#=YWG#@)8axrrP&^iR&@4N zX%Xk*@ufR_YX>-*TU20LGo#DcEb?1<8ib|;lAz=s8j`YtVAmxhMliPAO&S}1{+-9%6Ai44GT2pKjN*O1 zl*QYerZzCwxa)XC==1bCecdhBEZ>F^Nxo|&6*!#r^?Q)(AQgfqA$)r02(*u!jKF+i zP;5F*|GfKwQS8hj*nG!SvQXfdyw0$#X=X8x9oN$8*Jm43t2NlZzoC82L3@oMO3A~C zs96ez3993!AxiRRc!iidt~=I3{oFObtOxFIr?FX7A<+(eWNeJ69?q<0`8P5G{YK)IAy2y4cSw4u>FyzKt z8%&lyd`e+aIp|2#Rwp*&r*|Q}{=#iHlv(En&jAo$E`xrsghQ6x@_PFf6h@Yb*_2z? z_wwb%l^|;mbss)Wg!KeSXPUcxb2gNOK1mAPnRO+l8^#WdXvfPiBH5|hoEN}i&3qHI zVKjARI2Yya-HW|Wl2$>SYckbFfJ<*ze3Mb|S3jfvo2gnEGT-;+pwNL&{Do8?ov!@G z?8+v!Fv!nHBQEgCSicyDcXcSE_3viOM)J5@c^R@?gAbJN75M*#62^S|pcOdKm)>NL z3lxkyj@`s*!Ol+Co_K}$ZjUk(#(r$jh(75GXSk#9T`+lg5Nvc|2h?){pa?cMuEy%) zF{=V5e93s2)Vt?6`f9ns)DBPvF_WS6fbr%8XkOMVg6XN1n_A4VowpUjz7ItfBXR z7sTEWuL5k=H=!55K22}pHb!5}46~P$0fz))+&#}Sw>PIDiNV+xDBl)S1&SN>)mY=* zd6<=-3pAS%$e~|XIq=OH;J}hk_lhuV`WzqMcO+)x#+rY{!*B?+n6C>qCpzxXkq@;( zN5?u8q(7*;Axd^GwwV`E>W)m50#Yp7rM{aYZtalPxU;z;^~_PfJLc@FPc!E5C6 zGoze}A_MC8u(o&%f5X+~p&s4;4ho)#3&gJ-I=@1KSQ6rjVp7<$@2R)nd)#9w)nmC7 z{-r8?Qv+dSH&oI!*?}0ZFH}eFri`wgu4vh!i*{@W6qe5iH`$)+e1)psISC%r+Y^J< zhK$YO3h8)Ks1#bibG9oVx_G^62 zC@Br}TAxZErLqAtIwaSE*WSAA*)tX>l+*I)z0iF1Q>6XLA?XrkkkX*~% ze&QqW;0~O5aNi3XuMQMf1JM@hG~_mL;FfUdfH_NdmzXB+0yHqU`k7f;5-Z)oSnI6ak1=bL;;UQcdv}LnHewG7>j~`@Fxjv6rQeY;V*An^=z?Fwx?)%Zp z<)LpK9oXYy65iA@7Hu=mQg0CbHVYl0FYC-Xa?EBN>LLX{hULl)w1wy-PKM>+b7%L7 z<|O@b5E>p+#&ZdX5AJIP2By|sV8rfDq33>3As4G{0TW)f<F1a@B6_*bsfz398YR+=qFbZ$7asO^tBglnwuSf zuqhZV{Kp)PTcE8opy=zGWm1#I8zS?tC47vCdpW3$p#3a|pe-Zh2eLk!ET}80qo1_@naDZJ`Ott;?1q?SHk%QdYMU zAK=^1eTu<63@n85|3QP?bB-jh<;EDJg0GYsra_jn+5=I$ntpr1j9p+y_^Pq_^Ct!) z!D=Od45t8AxQ_5VFv6^K1jN5zh84$|XR%Sbe7iIk?Y{^o(@g4(CFZ5D;5Aj}ivxHo z{^W2u%q>KnIj_n0{c5A*a+R_^GJQg__|L|U#ap8b`oJ>@4JferG2sHt87fXCLuXwk z&0UBe0LcRXYWm)Mma|mv^CE|{o8XnEDGcE|pL3P=^_XjwZ%ci5;yLHIi_Z>$N2DAn zI8E7NxUu@?pdF(wz`Tca zIH+^>^I6VLZ4wqTeUS+_lJzdKbFKB2YdK|d9zCT(O(h|n^JaLc;Nm!cxEtUtSnLKP z8E!ys-W6b$O91nv4xAyB6T5EjcXZMZ|4%ug;FwJG1fMalYAdt!BX|`?pJ=RD?udjk zO7I4y2Mp};zi!$8b`6VV3AuD{1DAquH59qzNgra8LZ6G2$(yOA#gP>uJncbFm4mT$ zVbFk0C~XBY;VwhPd#cw)0pze%1Vp4pSZni}1dfU}!Yh>E;90>XW5afmlzxVwytf6x zDZGNCXO-<3fsZf4Hsn-4^_yb6cnr9i#<4uf-$Xp}Z+=}Q*QdVaXrzy8S=42RUto@x zNuD}8!{b(g98#1V+c-^%^8*_ejxKiT^A3(>z!8+D`s{b_1AU1K$P5+moZmDO9uj_} z%%t_XixFxbvw{<`e4u<0;KSvX;ssg)*4%oaT?yL9JQZG>|F|LNV{HRUiJL%S90N1~ z8ek@DcDwhNpN<%BLi{DLO4Be1j4I=t`|&l>W${M@6;~`6)_M;rd3f;7d@#`>)x;M6 zd7nO{=no?5>bLoURWJXsR2EpLGa*SHH@MW-SC9kqps(>3cU)+^$JF|90rNDlx>9Er zuRyg;CWM7C{uJI9>HEH`M*CXtC2_9ka{r@Ov;od0Ev9&EJkR)W85zo-QULnn3E?)P zF}`V5{T&7ouFee%O2rUaNVf#Id;~qb$D;fq4iP9hX(o= zEV%@|uTl`q@YI-P9=$LJ1r$uNg+3}J7-iWi7wY-G$dOX4XNr~FB+Y&aZXlPYP4*{l#Y#0BXDAzN5n3v#x_4FTE9>gc}yK>~E^fVL%? zYvPxr?}@AKC#TH5_hOSs9A8l5JdQp)ArlJpRiJdRjo1#;JwE|?YhICs#YEdUwV^_{ zCB#hpczwsRIJ>LDgJ@Owf;Dc#A}q^H&ynRkuG+OF00OeS;c#SHny*C}q%>jG$zQu7 z>DSn}Wx*2%DYuMWjUCvw@0JI=XvvksD1v!E`?rsyNHP|u>By7rdR7pTTdP<70q5 zASu}YkXe$eFKXYMRw4Dgq>jO$s?_Jo{J6l&zHS7fT*RvX7^I=kLO>q43K4#q#n`3P z*^Be2>hMdLg%k@Y4KRULAUx+UsljuZ@vYuu`ooyGin?q1>D{?n%a0BazJ;N?ySAc~ zEXfkzwyAFvoHyJ7hWqD3gjm9aR4+)q=377l6I`z&wEAkf~upa>kY@iQ$k z$UO*>>cZ=%NxE$j?%f2;f|CFix;9;D73J+XQ}vYi66SUQqB1fN+R0i0npk_|B}>`j z++w`$UHn*zhkH0rvgnQSGdUFnS8bL>50fNlKb9`z&qwf6^lhe=FPgy;&LNIx5DZo= zDpSH+3)dtK-eaTnYB5&a?hMe6_I66Z_>O^rmkcB2Jf5g+UU?(360dH6{H`-0&8b4Z zLELTxZj97HUow9u?%q*@2O#(!YfH6MLL!o1XgSPTVs*WnS)~8Z;DoE=G?2x01tQI9 z&tX_TaH4USZ{G!TG*?6hJs%sYw2BAbq|b~Ql^hcW#|_v*@%go4<9C6mJ#HxTcGf0w z5y)?8&UjH8K^x77dyp^Bj{tG}@+>9<7tgt^&Zj=uUg-NpJTf1OuaoLM$tiGTu~8t; zBXedD+*swvk9+BIY%Bx$`$iOUL*owQZjXJy9Rp;N^4wtL;PEo6R^Di!H_1PoH35ei$~Y^n2`4bio0mSdD63Chz5I zt9wyLwQYoF)MW*bwKS>ij3!FYHGvSubvg zG*&}(C-mt{xvPkR)fo#*?@|96RDV-V)nel<{$XBJNV!=amX?+Jdffvo(py3`M%{88 zD_fqUcYc0D9Btwuu!ygI&2d}Q5%&?jG{pywaIzfnA%$R*d{ntqtHJ{e-6AD^)~W=! zXRK~+f<>Naiy8;<6vCfwn=v)=>La@HB&M*Wr8hdR_=Ja0s_qGYa#DHx(!mQ z%x5Q{#+Yil#(vR#GqJxSr^SAuQB}y1&hEd(RJAx1f7EZ#oA{`E!&a*_zPfsQLvu#! zQ;l=SmlDrJZbPO2Z?;m#`#)9vR0d>l`X63DkWm%Q6!ZkNb$w_El=fbf$KZ9y@RrSPUps*1&KSFv>(eg2=HpJcWV$aYIfsF zl3BEjwZRNG9%rgJS%kRivr**K)@lPy+k8!}N9t>ONu#gINlc(Aj=yjjRt*%r-Pzr7 z79-i+4q_G?0?BtR3LpM-9L*h!E*Q;i*gixFnSJF`!9`rY1N0 zfmF}ij;M3VVvc_P`b-CFJn_dwq|vxUWUf$9)ln@DzJm|2tX2s*^j$up)mWDM$ z0`X#?AQG?N+s%6nh@%md6JTD6h~tAJ%D)rgDzdX`GYhLKyvlv~3J6~V;`>`E!=6e` z7Qh6_$uFNLt;QNT#I{Q>^M6>xeLY6%lzFjVK&J>|@ zw3>9{ELgS(tVvKS@A9b==82C~NydGG=A~zv@V|q7;Wnl=4h=nPNqIxpF*Z5sR|wFs z6}3tY%)N*TSnekQZ8~#z(QO3%esu!-YA*XFYAay8N#los^ts;UC?2;mWSip4JKu_# zCGGtyk@iHdrlA`ea7W$$m+}7V;Jm0hAxAZ`IH-3Ai?EL`MEBsM9sf9&3Dru=QsYk6 z<=X7TpAd#5`B4^78+73Yix~j1Pqi`k?&gm6b{Of&&@`jyx-3Hg(rs_o5!1Uv_XCS& z7W^W#_1#B|-`s{G2(TG5^X2?ts&O{FG|kh87Z8;Ut&Oznpapt5#iqzzRw|h6Q@`e9THoHYnx9wjJo1&ZSvYOQL$N%ofSgRZ;J+jt^dsj$nFXJEztQMzrGp(@<3U1 zv2nf4P@f~(mMBL|IE|v)Ba^bu|hRIyw2%@Ls!c#B8mc-$*j8N~;~7k#;>BlOmN6F0ToSAHU1nyCLF~3jjvq5# zzCkL%qP7jz+14(|zk_%~3gN$h@6?32YlPcS)w>ne77P*sZdDKvPt7Qxg2GqjkdZ4v z^xO(fK^0fE2}^qAb$Vi7N9)|MY+0N%+=yvYcqBhtssSUZ?L9?%uec&Of5ixNSF>Dxd%&AT6a zU&xfJz+K~K!#oAhs=6ve+KcOm1PTNw{xR*ya$iTNP2WPOvIm=@8k=;skyNIM!xULs zttvrH-IEy}*FxVEz99nrHA#0n9R~$%dMMFn=jZ}!D)~6ypRnDagov}}4E}yEy|lS$ z4c+TCIAl$>I^wog43ViAeK#eP5H%fT16;!wiW#_S zTr(bwh{41^plmE^UQ(mUf3pe=EKplD&OV2lUMwP_gLDFzuoqf6PgNObzEX}bxnq~*T!1_a36s z&zat*0+g`}o01xD8<|YU74;Z%jdnt=7n8f&Z<%4p3~I3{zo05-Nm&c3)5APTHx4pz z2Jv}!b9O#eL3ymv(4HR61z}94Ury$m+%~m9UQ6DCHBH|Pkn(X`3j8wwU1nV4{z>ml za;_bUXZg|d$;`9D&lYbuRyIUzK6rk(-v>Lp(GKd3pqy@D)CGMgH)`TV3=wZ zoYGfrk90JMa={PS$zF}y`WN$xMOs}8O$s0RPOY#-2hC{!B};+4${Pb)!8?!pj!&g1 z0mtjs4oT5K`i-wKEkJRAK2Tr_GO3R!Y(l^%^v8^di{UVZ|8e{y_-SGr*1G9`(DRNk ziDfwK)7{w~)k5bTDRY3hC&{y;U{y(OqIT)DMXllWGpJ z7a2CXXhSN%e#Vi4<7`JJlGekC)x|crhAH@jHHaN*SQN)sOr21vwD;2G zRD}?yHRP!cS=)IDdj%;>>dA#{Xtef6e9dlPTyN7P;Dg)U4_$<*VK5 zZMMI!1Tu@L)%A@Rjiwv4XRp@IH`%`3T4;IzHdsLPOkERmw|o01;$q%?bAqd)cmm{K zbLtB@#`YuoK_GGbBP59Ta4WF~Qd=nHVsoM-vD#J(?l{@?M`(+j)!aGS(Bv>5?^&rR z^lt!7AC7RX>rXX3dg0Yqo*(#>=S7vFSD`FQ()bw_;Hl&kginzg`d8-uG|HhqIB4%) z9GsOUna_uWh4CrQ)o&GRxU5QPS|W2C+Ts6rd`H~QOE`ee#f7b z3N{HyE!B#Pi>q-`VYmEZ1*`E+63)$Dt4kU@#8mI6*fZ9L6lqTv{u6~*XePEo3OLhO-IwX+aVuCGNrSzY- zN;|g(M&LQP|IhXhTv2^;F?{YZ2sI$a$ZzRzKzCaXQZH+1F92*H5g(7`rOA|+!itF$ z06~lHn?%)KjQc>l#2fkHovGv^7wmEo5@FoAag6Yd+&~InqO3t8o68nae??3Lz`keh z8gcvIL30tG$08=;dTNA&c0Bkad0%_?{tF=H(dkXXWUgnt&ki*;EukTzT7q`e(u?_z){nkz9692O!+Mdz`&1ND7^Cm0}?A_9V-5OI#bMn2-utr=Ndz<7d z_{C2xAczS3k@DBAU%OUk*YP=!gJ_dam@K13Lk2x#b*xH#EPk>7Tktn4b*gSl=LzW| z&GxUVBQ-*+v0WP9abrGG2c3T-zu;G}qD)<4RZoe?gk1}kU2v1ZyI{ZAum9&tz!C6b zm3|tp#hiY2FuFK3B~N#`yIy?ixdMsH!bCL{`RrFqM^X!82haU5^PUPdujBd8aC3*~ zx5i9^m937u-Z(F22HTkeJ25AVIpKo*7bv%@UDOvg7rj5QeUa)Xmf>QGp>?`GO{4fO zK^H%}+4vEEe(sAn{(DA?wsL;gr$0T^Yf&{lRu{gO@uD{T^r!R1zM3X32~QDd)5*{w zWe{Znl8d(tV(wj_?npYTE0g$V>YEGKjn{%v!rA`2udWm0l)GB$lqlx8F$?K;cz`JP zl3VXDP}?jl4`}#lXdNrw9-h02wUfIAr#uuQYXx1<3Yh8f7P4AHmQ#f{S42isOZ5>B zPlER#Y9vW)ZqW(uG3(1?EkHM7CzK53Ux5D%9ajk)SH-VL{#ab~#SUUL* z-9x7HtXo!MficPJ3ZnA@9}}`yyFK`l$Mn>dj#UMo5ouoY&r}R`^Ir6YcZ1l=$H$O= z9!h8S*^f()hteA$3sTqA)Rej5qAim-+ui~33mAA-8Fxsp+-YUOoIz_x?xmASk$6V8 zn8m~8u)|&Pi@Mxg2rt+s}`5*Y=!5 ze^|I$tK?nOhz6cYHvi+d{>ddmKCArcx1bQsWeXnfWMVS1UL<^x2g=tHdwq^n5-qiZ z^L+)Xw*tQ3dG`I@iLejwO+o`7Zl3LRtEn|qMG_D%VxD-F@u^YJjiRCuC+s3&{YN_jtUQd2a`!mRr|8tDu+kin}mT=;T1FJ$_U0M%SCKX6r0Il?#$8$ z^3DSGD$JR{Bk`}Dok>}FOqHw~zY2p+KfM#@d)j!zrqD^A>N1wt>gcJ5eO?AXxJkO> zb8^S_1w+e7Zp1IVZrwO-9Fhd9voah^*4%g~Mxrh-FDScT-1t)l0#;kM?)dz6s#h=p z#C*Y@SHHERbwwyHeBBtqBDudD;kp6d{`@OAGt%^MwqK$jxi6dIB`Qo*e;%wd;CHkC z9CA-|p0mn9!B0)=_~@ja-y{xgL{NgF)HaCOK_5t>56nM*8_iSh1%~f_S2U20^^TEU zR689Mz))odch&~e-)6~2<%XUDu;unO%;h@`Tl27$yg*ee@280D7o(bLy>(h$RQTT? zRq_=PyLnhE2&o1sSe*-*BGzG5wrF3d5t2QArn{ma73eT^2kkdc@9;?)Y}n~^xDHK6 zTE{b!%{L(z;$7g}bOyt3yE%-0g_~T(X<#JOHjkuy4khGJ^k&M2jcf5M9u}A`Qx}3__Vz*atJ!UVKt}E^ z;d8&yY}UTuG}zM0%gbH8SNmvz^4L)iaDH#Ex?Bq#bHdyhEENH|AOrRc|Lv^P+?ez{ z|8%4{=aR^fIsU6;Vque%d;gEL_l%2T*}8`b3L;4b1w;e{0m*_yL6V3_76Hj1S#p*P zDj+D5b5;QZS#lZ_BZ-lhUVAMW zR(kpyXrgmoe&KcPpHENNTqTT`vYgPl=WqR^fpHOjwYQV%HzdS}2AZ0AnF{yr4Xgp7 zVMOH7QY4}|jB2n#heetr6hHeV7%$waJ4NVIYbH-qQ`53oqN@pz?od9du62>Daj-1~ zENc5B+IP26(C)z`kYgcUm~bKW40E(^*jD|_Qj)ym?NMR@<1NpCpnFIB?D!s6;z81tR`fUz`Y&=vI%c! zE5}}kUnZ8_-_79Q#1ya9v1t^qQyy8ReiKVj)d^4VWPr!NTS$+T!2Am9yVcskDJ9|- zRm5@T2I&q8p6N~4?982L=%Jf?1aQ8A140Tb={8MKH&U8g>2uI1;btI_wdu(MeWN$& zaKrLjl}QqPeiyKo=Ns*N=;OFs9Jtu1cQfB77{z9~qQ~_*L#b=T(zs%D`>WXK^;EKJ zH$~o?vcbyGLd}tm=YH!fE`qFmuSqec4H9lu#sZrC{kIIa6Z}W&2(xYXNTLe597@5w zXFZ;Ro5r3Z!<&6qy0~=1Yi03BvCyZNhPd#cc~a^d`O60|koM3#xZNs!eG3E-D4+9> zp`9zT>~I709wmb*E| z-SwlAu<3JE*});32P2$MsCf1cbufz34XWPt&1gYHFt&Y@d#@XQpyCDi(bq^XaYq@} zdgNpfLaG;1we#zB6(r@EVKM}fV3NRka4Fq3K31Bk+76&Jfa#kpny-%e+X(fzQU&c2 zV;dOJctLYK%Ug!MZ6mU)$vSkhd8R zmsw1=v-$(f$eD8wzw68U&p6YB?ztEFq(i4&H1IxR*}l97sh zkG}ZTw`pm_Crf0^dMQ?IdbcTP#b>ii(I!ozOXbuIBVM5Hz{s|i?6G(x0t>Cz@BFju zYSzhSLW0ueX{?sg!gpfb3tlPr&;R5C(Q;xbMeCCfiLGv$Vzld>e!ox#R;z?5mJY;tvgW^A8 zj0y`pgz18sKUgPbjcm$&VZ|flDnn8Ch3Q?n`hX6Ha_&#A(u6-1?d-u`oW>$L1Rl+0Z+Z?HHWZ1bA?Cv~g?Hf(dQR~`vd-qE#+VSEBvwXee z`e^y0)x^w``tvn64mT%o(QM>B}i?dJaE1J1V&j1w}r*_LOezUU^s63z&JMcR=4l zB@_>UhyVuN`)N3MJ-lVxzDogog}%neGgR`lL-f7@6Q+zX{Kked${&i}^lxyUR+b`fgF>kJ{W9miyWkXTg?Wr@hc- zoA^k+L9?dToDukBTHhLosahLLdQ8nE<$`X0CMKq#nkLH2{+2OzzBRI=5$-^Vqd2U#y%Rxg3wJ9dZrZcOk1iK z{iWnJSeF8P5{gDYBHJqO1w|dzqeAf8OMLG)rd+d>c6Tf$Z?`loBz{9F9`@^Abdp(3 z1ONu~{Dr-oZs)5lgp>`;uS?fFfeAY+PbwP>Gh~is?v15ztQjg6jXyxO(TIT#^DhWXK=`GCI97@N1vg^?)m* zgKuW#ob92HAw7$H@ut zTzoc(TBT2ie{GanzRUNpC;+5V)saGdKHHH(Mz}+i`-({0J~<1JhqyP+U#fUd*hpMg z*rCSG#R=9>touv<76xYmG|S&OX>YpmSi_ zIy}UDWnUr0K)X4Iy`eF5$V3_MA{LiHhKaQs@zGta|8c+oe(wR#&N-bRGe>>zEAvUfuA+lDE_#JVm;pCZ^3@F zsS%(H6l61S;pgn8$@~=?1u93`gEyVRG(iP?mii)8tF;&IcvpKtBh%2urpjE76hB>d)|*s1nb`+=y2x7drfesv z2A8bK@WUvX#v(@ar8DIlO=K$U^x$VLdzY3zLZLEx>3w7_&c$P?kNIdFSMzNAkp*QX zosl(ds4!~^Mlz|?()+@(Un%qot_i=5sVKjhcsKU~-z)R_bF5Ah=p7{<=u9634VvYd ziv9%0``J6wbr1>GBmv&odfc);Q?C#Du2{Cc4MRdgKJ;6+0e5r+u)I<}$&R~(;s;GVda*daYGgZO%#F?m%ScaB_&gV7?j|=a zJ$-CCU~~Yd8%aRnNKKRUsc52S8uDi&BGKjr&qCb`s%?;=Mm$$xm8`Ji3^yc*7kOI^ z_ndfvVo-AG#bnHy50dhCWQ0P3XUYTbS~iB6o3YwxTQWqoXg^Jn5xSn1#*E%K5=2wA zw^$79M_lLx%{^d+X(2ps1H+U6!m~X_fJp&zG^pDpxYW_yWCiqk*$O#Q{_thhXV{yhU*dKY|s~o{57ZvdW`jTW*Xehfh$9VFnoJHTX=y*P*`D40K#x8|cXrTfQ+%cHEPS zn-M${-dFRFF6w~{C=-Egc>&EyI6v9g*!_v@8oG}Z`3vNt2|Z*Tg+@&)iF%JOwCmls zW3!YjbWRL0?|7*B6w|%X95<;hyWD*yzlXA5Rp<0`%>zb>XZ(W>Ku{(Y%j@7g^AE^@ zG)gow2xRQ4{v#51Pi{bd-X;ElJlKS6D+=4 zT6obv7n$v+A*qiB#c1?7XvmY9UYzGE($32Kws@D9AZ z0dnvu)sE~OvuyZratN+1kF?e6RGO&swawj8I&}Qb*^|vS@ViZ@K*4_w9;oFN>)53Q z=#SPnLw-z9BXX(hqAJdp3zD%!K*mJ}zy>NU--`Skb8rBML9~$eEDv~ib#7qO6A#vE zJ4l<%LCN?J=R+NDsQzlRj@zU`p4d3J9S=5Yc1-{2w#Fbme8?5RIJWRs8 z56yS%i7Zp2$5kITDa)=JPIfrP={6L7$aHY`zpoeoK9vq&;gE9q3=UJ;9yR8}IZXhj zKk9C4YXotR(`+ayIs)%V>Z~4goQ1dEIG%=E(FiOJebN*V7f-or+~AA(Ai{)uUyTNR zJG4%5M#mEhAO@FGJHCU3P?oHAiVK2t4voTL&+iKhKSYo-q4%l1Vzo%i1|^6%1G>tJ z-W>FW`$_e{Br_7(XCZzkP5qKQx)XNLX#?d)F@<;CfX<;tNN8v+4o$%DpP&h?UYkJJ zTCuQU|0&c#4f!;nhw|=d2?G=79g_T_0uaO-ijR ziFsO6Ld;d7w;t3~TQFBH08Raou$)UC5MaDDQDE(St^PWzsx zH~utFw#VysF2L5O^~NF`q;j2k>sV9cjs`RKY7#t}vnhsi8>88J-!KlF<^^g$5wklPgA)7V|CmQt5*LDTAnK4~Z)obI{8( zxl?N8n{%WuwX62YFud_}2GbT>lyOGIH9sHiDo1SL@G$Y&F$9%Oxecn|yy+?& zra47sp1p_sq?LN>A!%5!4SNUyujoG!i7dG2`^MDDqgY>TiJvl&w7gUH3kQkABDAKN z8b1V4e1)Kyi-)#S$kzw}rq@EVG7@e&&E+dX0#0$=ErTZ&!r0RK!p!b!-f{DDVbyfx zf|#6m)K9PA@24?eHfsyUJU3qLu1&H7OX5moE&QrKMajpTe= zdGqkAIy&U`MK{x*wAZM8Jg6}jlwr4slj?b?9*b9%S_;?c!+PQIKKd4rjZ5SAgGm{- zGWY^g!|*VeC+iii&ON?&FmZwx{=5QzNP`E&=;4 zHqDKAw*etBIlCR(dq+`XBfk*}YnwT-jIC6Ig=aCz3v|C-i4f(xlY2&l%yKd($u|vs zH#?Nv{{nv?XfLNAHXoFM-XcV5+A6Dw@1nt4Ie&3a!rCgoH85a`qJzz>kL30B)JP_h zt#afrm8)d>M{g~JzP#^!4aZ2V`x+*>qH+&IjBc}qia`9M)=Nxp2=IoX)qscRs&+e^ z1eYP^Ml+s1+0i=q)i(=s)+s6;zYcYjQ0IEnqWL-bo2^f3k+d9@Zl{WY7Djb$-RR!p z_d)AhtaN-4ElF=8BHA&Vi>;N5$f@zKHexp4#Ch`6B0HA*!(7k^5;6B*AHZ2D zG3_}?)f(BpVWxoG+uK>{Z*o~(igxos{LU@|Uz9pXz&S!G7@`QVyY1fyu`QK8gCGIF zIxb?gvp&v)-C0kNtj$z@jRqv1hYQYgcbqdhrt&_|uyfE<`eWcE45!p@o`KlLn?alh z{!jTX+&lJhMuLsHf&QPN3w?a9`<=J%EU3J)M_HNen90N&`QOga3E3hjXs-|RGwg@^ z*XcRr36;1X4-XtquyKz{*Poflv=ta`uq+yGc)J8U{mDwY)~k~um|qr5q|?UU^f3yC zWMrg||MBG>(dWBzq*Y#cUlBcX0kOxP{bnwhj$lHqODzZ51bBf#rw<)(qUpuZ`Z3*r z({~cFl{+mF_TNhXx_(}0utimRP`TJBk9nM|`u~?>3k>u+G6A9_0nhU7XZ=g6EM6`PT)2aH%b6mdZ)!W z`eLc?kB+i@1Fx`rT0Sm5-S`BpFQaYOzi8)84%?2rbsX)Oa9^=lo7i6Vn64o&^>2D* zM>{TRB;4NCqBOYHqLTPFv6GE4g-5?a{hy|v5-w#F8*D2~VGy>`G$j$J>#`OTVo>p9 zt}*^1>B^J@USNzyFkDTQGVG|*3gaE2_tw{z`phyM>p&ZIr8D$Y{*5O@k9LVBqwGeI z-i>=%u^%0%!^CnY$7(%3A#JCQmM>r@M31v;`~{vI8*oqtO;eVSrV%0jZnDck$(!ay zzXki6$fyPYy$cSAFSt{(L)|jrxUTmv6SY4H?jOdr2Zxby>ie05gUfrF3OAUb{tkatcYt*F8rlL zWDuzhqrj=hMNn+Uk1B)l3h{2D=6y1owDzs5Tu_2 zE4rUlSigO%&6aTjV6uTTP64ivLUx{7rs{D^4Q-~Gnb{}PS>RakXXZ->j;N)8*U_1$ z5c-iXK{XNp`)%+%%P$NnbtD^typsl}DjJgPrzri% z1%wDU1@WwafB@8$9S^x#0$bU4TYI3;m3+drOcK0RSDkgtBkO-9$~g6&e=k?@YksnS zi(s%2Owlp*fp_C*Q-Zw4KF>DOlL4AkqTNdO!CaM6;5VWeiC>BGQ#`?*OH0W`65^t)xiRs=5o(7!-6 zXysfrd=>-=;6nunsG-;bHBXFW;qDpbuwuY1Du?*Q|u4#*LB-eGw*p@2mL;IDiKSPA*XFdX5}UdrU$B18~6jlL250mUTKY zTK+qP{ep8x@P#_*eRHC;`@;8PEGPIfgc4$rincRgD}Zy99DGVW zr~ZBNoE$G%AEcTTaZk<&)Y#S7IJ#_)AOv@B*OnB1m34 zg_g@4nk{c*^tDTQsh>;;D@BQuKvCVH zYA)oFmr4BDih9ATkYR&Lub5n*$~9c7Z}0PaR>tdKqqVl9auoV1e>w+58lGWRxhEkZ z(e(khcpB6GD3OGs^(8xS{EVmB)~D}$8UBD&3AqGfd#Op1H(hrX29|S83s^y2=XLvs zk(7rWk?+FvKc2-nnEFkf{SLbOw}D1`I0dpEJ`+soGD83`U6&shb(BY(7l8wNno+0o zY-7;d0O|t?!fdD#yu`4(yAQU5W%yVGy-2RI7D?nmCzCOrhmx7}Ek6FY_ z2>@+KmHtMdeA{T3jXfbhxP{HW*g4_yn*D0}7td(Nl?2rW&Xqy>exnk&^NWerf`L2K z4s8{lB;gD_uV(8A^RKl28vZi42H+4EWC#N6yV5rSoUaPFBm$Vq9k3gso2k^4FVW~h z{Ckjq@LI26piMFMpe?O)5L7)tz*qikOjL0seBc%Xjia=;(Ii$kiK_iF?Jog~5}TA% zM5kJvf1Gv$>2>OdHKE$S7H^>B)(r0xw1SfR^U)#x)2hoRlUMD$;ADmjM0xG{fP8nX zKBz{2XT_!LjcK#!*nfgFK%Vdxe=RG*C+-=8b|T45LM7G}y*Vqr!sHs^IoWIJ4wv*q zSfy@!Iq>%TNmtw+VD+M?$}z|9 z!Mmaz0dO+iM3C_!WjQ}HLf7JBt(;tG*oC{|R2(tighpw|T;3JM8BV~Exj!)O?IpbLmL+;<0-kFprl z2;7GVp3Gm9w}e}oRL~`_u?lsuHqVq79NbGrV!xpC^wTr*KI~rH1APK2o1|6hvcQ$K z&0Jr{z@-Tw9yuk=z%Z+$ONyw$6F=i`kA#POw{~-uqyDHo5)BTMj3d7ts46WT%fMnyb_G)s0m09zT5dKCunAal@FkZeGZjc9raDG8ih9F+exF=0gA>1Q(uBtI*z3pTRB zd1J`>1GL3NLB;3TYQig!54dJg5a5n>LQb#aKs#W{d!+z!rT;)e9xzAa&pdC_$I}}K zgJS-Z>xJT^84fz{aP&sMm#!U$gLh20R_vZxJnE1%FI*Otg#YW5`~}tAtda98_s%N` zQ^fKa7qFQbB3iFUNreIL#~X{&H?7V*gXsLPM|{ApHRvR>lVrBJ(2e$i?fSSZ=OpIN zy-1U3L|&@;_4x||oEmus9^)3@rU;Tln*>kJdFXB$O$J9cwLsm54>KZqZUCxTwujm; z3WYs)5BM!LN!C;1R`al|Ze_2KS_tKfXxCADnJcEPjj7IlAs}LNu-Otpj|2?=I-m?M znd|~!lsu$sCf4AxEZ9-*1wtOuP+qL;;8?fOGW6KQqUY8pTw5J(Cdh>CBZDB4#0_Is zP}Q7-2rR|gWGhBZqEN;zsIB)=(63Kb29RQi<(efn1`^qlS`fAk2LYCFZ^{Qj>3d!YPkLZ>He75 zE^?e?u>H{&g=@N%;bY1llN6uQV%!CFvrkb*2y~2+%?<{yBs=`@|LE5?8|9fEB_lf- z8uLe!JNWfRX-x=YMe5H#FACs8EL?O>PcaAZUAru|u1IK&Imkp=Z>gl`nuCH!|8sai zJc!>`K8z}HOUZ#u+U#&roLNpbgILDsZ^`<}EpuGd8i4I6`FF>H9UyqB&Zzj`Vg-0T zIA~8bqyC=>I__8blm3A2SuhPJfbd>BmEOYRZZdTq_At7rh+cC;u4|fGRW(noeIk~r zl}7wzk$@vYB7Wojfiv67lv9npvdRccf0XLmn+-aU!HkRk zhY?uYt-;g#RL4}6f$+7FiGtSAtw=|`$EnZ=JwbVo~sQT(gpmN)jnAg6|LDf5Rj{w^N2WK zL@H>o77n$%{yH?gvRwu-9{ZlrT{VhIfI_|R@~_!CfBsMc*5Nrex;1X&%^u?dx)r{d zXS&RtiA8Z2xwJdb7pW$dvt0YfN|FlmLUhSGwcrd?p^$ zUXloNMSW((c;smA3X{_sw02L4QmI<`I4)`bXRC|Cr}<}9(3a08m5ENYOQ z-1BEq@K-7oI|F!55IM8>5``aAO0MgO1}^N0iQu{&$DKXHe@Nsse+ z;V+*fq!)AV+7rMihLGuyV77ltFQ-E4KE z_Zj6a_pN;rAf6LcI7K-;Z05Bnjz0ek@Rmm&2gQJsD(hKKrW7A&IgcSErnvvUzfUyt zWC^&2V-Nd@4pFPf*1@*Q?3d^*)Yn{4ZF8(Hrkc~gd$&~aa@^fq1O8R>Jra7me75ow z>;4+RQ&QRD*JgtXIG_G8iw20XNo>5FavwEx0TMk6mjYi0X*#+2P!N~+VkhAXEd(e^6lIs|)K}q; zsCLhWO$G-OMeDzml21nAGhg?$4u%zdsC;m&o)PG$H)P{iq^n$+TAd-NroHJl&UWIW1@mDj_P2i<54!q>~u3c z2v-6Wj+N-Bya3eN)k}L4W1B@+H&?@Xmxa5TJ%^aBXEcx2O5>Uj$WPm z2;i$F1KpUYW4c4{924XPBJRNb`{?&kql6XsJ_$EeMc)T*Zn;(Y?H+d15ocYoe&z}| zKQ9kNnP9>Z2N^mhG^Vs46-BF`Cj|-B@9%uGX@B%4XDoFp*W8e2=<~zPJ8JAO4Qe6y+(-^FRvPuql0rR5F|yHNq@Rg4q@RIL})8{=oqoG zLkB-DO^($MlKIFL;;5o=3tOss{|Q^+9XU*SX-DL|DD8Tyae7e)7P6&_0u8Q;>#*aa zPwYY_=lb>wXFy=J^MFoqL((_E*q> zbYOeq^XtZ}EO9+3)Fgmvxw}0K5@QCb_h6rl|6$+uAp1nwsBOqe?BK>i4W{*x5zu4rn}Lvon4a>JY0qR0ASX_at#)UxcJinlMJelQ zKj4Gu?`3q6U)JfXMZ6p}4yd03>dENR#CqklN|`hEFt_D__I)Te72NjgeeOU+QUIk= z{8TtZ7LWh&0SIZYxv2|%`hWB_TweVD&-%gpk2S``8!H-C%Z7Wy4)?cLkbsY9s0SAj z9$T6C8JoBX#>Mkq1lcR7gszuy@OnmJeL;tB&=o3BmH-zK>F)H}j1TI%oB9v%Ed%mu zI^{n6kD7J!K(c8yTlz@EnB7G<&;+c_bz?L5=oNcg6QiMvWN*Lgnu;gNXBBw38aq0Z z(Q7-Z%ad_t1se6|1SeD%ei?w4?*x^6SAKI$iKq~Y%^E?VZQ2xHZhn=4A)L%xi z(~p>Wt!+RJiyR4%bTr5tsknJrXR|=nfcBAgf!+^RJU}`%aMLa_n%=zwF#iUaR>lKd zv&E3S|6nX56m&o8BRr=htQw<|WkQ_GP~;O!r8FXI7Arg^suZy!ywfvW+g9nW#BLmq zS&lfv@oUZbOBrP_eM(_{0LNZI^!;ur7iR{I46KofrnFyWe=Kc77*$otL9+&>{Q+S#$LsZWzVBG%p{x}s!7WT8_u zx2?p(9cIqMDopkD>S&dcoPY(`6b>w1pLE!BWNlfVqTZj@-odUN&U@`CzVcIhnfy`| zeAt0c=Rzr5q&U-^@k|;vU8Qzi(YIlnx0O!;rfH*mK!88k#odkk0$AM6-MYN|eFwaS zzuei-6XFn#P3G8=TeG^1QzH2MB80+a)|=wR^AHE1x@Yvw9C`td)(@GtSYBE$O8x~N z!XHfx)9BNPbkg8crK4xdD_hRuV9n1FjN)fTU1_P$_z^T>3Dt*pTm7Nu+Iv>h@Nfls ztQtJb*K3?hy01PA9?IO#IQ8q}>E;&s@UDjLMms@)NyVp0im&Qz^)V3woxAZ{)7p!* z0ewR<_d#Qz++bzkXAAJ5dC3nKqd}TqT0@r{KyX))2{f;30CIC|>2ax#^ z65x{k*#^yzD;RKeD}aSh`?uQ)?UUhWu#d!7SO$ng9LNtgK_r7KhNrAD-He?hvWw+- zWp2hIKZAFzNG)1Kw_N&mw^H@l0>{ZuAgZ_P>OWk$lYxODF_E3`R03=d3Ok1hOYAicRaey-=QSb&oig}9!tOMzI zIHN4_H)U&un=;3@E72@!4MiudR?# zcD;O%UI=`#0!wH=mcXKyl_Ado7}o)vb&y`&U;&RO+|P6Wy`Q!9=URGUyZe!j(=|fV z6MmKTfbBK7FI??plz|@rg?qG$<)G8^=jRFlkL6#3WXC*iy&=$4uk{+EWHgM0s(}E%{Zaq+$`*(x%O3vVj@|7dMe2O)i{o)ir?)wS@$Zm^t)4j_y%xcwd0)HLT~gZ7 zOAbW?hCdG%y)L^=s*c`4iQ|Uz2bz63(3jX4Ob%$UU<4mym2Yxoe>`m^MujLkDYc(J)>sb|-4eG6 zirZiDOaqT-G6Mg@s7mRN5%mf7FDk*f(imqc4iGucv@^!{=;N83ixc?UxU0~(eWd?g zv0u6JiPX>?bEmdi)i$Q=wOTJOFlyuMy#J%mD;Qsl24>;7#Dl4`CKF?Pp&mg_s8jfC zWD~IrMA#bRAzAa66rBB7O`naT%_D>6pW4(^;vXeYy-W?a3+;|F>!iL0_sOWA!;umE z2jq`uJevZORCP&Tsg#~m8tKmF9e+t8PNJfjx2!8~_wF?8m{W?RZB(%+@$pgFJv=Fx zbcR(Bd}f#E%->oN0$rZS5qIY?#>3E3B4p;e*yFl~h#xR9CyQxUV_wJsmZ(qy6fPF7 z?>vUtz1j`kwYnFq&Vuv~AljIAMs`?9X6{;Df{4Ct{OTTuwlKtK%@UN${dc+Z( zXACPHoNd{|&QAGq#62jhPlMRuzCK1Et7oz5O^X(sw}9y3cO+91faQJ1r&xBjig{dG zk8a*@^I09!5cAl2b%harRnq(M+pEdAvNTI5WiGPs8==eZ#-oi=J|0j!hzI$7TSL8% zIV!`j)M~fTZ6*AFRHv)>4aS)0#a}VTgBORlNkcDw9|k)wZtd{#J1TGI$t-j*M?(~Q zF!h-d*%ohyyYPlxVv_L3_mqWtCoU)o-_x9aZ1Gg!;^2*heC*xM$JQqu5b0hG{_Z_LXzH3_68oVL*|v)(WVGCyJ5Mx}Q_i8>`Ar!%1)Fk%NweKFx>`H!8$ z@*j(7XunO=`l8)=Jux3DR4W5N;L-Mp@)h=un(b1D!&mw6;+s75_dt32o(``{XRcBn zHPOEk$X~S0HHBIL$|OW5?f}AS7Jwl4iFoeEo`#}?9(d?LOe8o&t56P*_jz)hSngR^ zX4=(Kur2-BvaysKfT}fZq-fLQe%JVaH*CyfZLi`nrZP5;;953*E;6>U6iI>+wpzZu zAl7UOB&A=vL|CPD8=y!r?3unUsz>V=A}@je!Nlpe9Pu2IM1H{N&C4Ut9rSw}gaZBv zWF|m1RA>9C_65(#&pCR^p|kHd;+|Umo|swS>g&5Z;zFgP(V$u7sW~xuD1r17$*GsT z6T9P|o?Hf%lg9ndOL?q5u7Lp!S5T*O)k|Z~?&LwZobCP*`9Kz@Ue*P|B>L)f7x?Abjm;UkMXatknZ5v7 zg6o-m_!ESP3!;)W^+hAp%L1x8wsU>N?`*`ardgimMx+PkCY#i&%9YZk$menkmY9DP zP%x_fz=@vRsAIc`qN(A8a~}F^Tn3ww%|~W1M3qxZOUnSnY!z$;p33|i^6&aN?!Hyx zUv=W@p5heTO_t482X-RC0#cniK)bTNPsTwe6iY$OPa~>ku?3Ut9JL*CLFm7i27x2^ zJQ0uYu$rix7tE>=84EH0$~#KfLY#ir{r&x}o6{{bJ;TzV$HigUURlE_&mS z@d+P7+&0X3=Amu_cT?KnQN0G#+KA96lpDew5z;^1n>~w)v0)`4V^vDdjipc6ds>Vx z@8q+R7dcQz8H=Fb3$%NF*x38IyqCU^iL8LoOe+cysnwZF5Ku!y0JMl^PUn z^0y{PRbm>-P?rUB^dokWPCX8klenBh@K0ugx?`4lmw{sT+T}l}S0_XW&n9VvuDG4R z%aoF5wjL!WVqFGPo5$!w%TwZv8bZ#uX?_pY-pdqse}Ll?0%;=Oy(UIg!|t*I zp(_x{_s-EqWGAkZ93&I*Y=t;8`UHK4=+>(~+-B5g%6H>0&_d~KxCj|k7j!+Cs&-x0 zruhSprUqa%9<+Gb@i8MT%z&i<>I%S}!TD|3`!Eq^{}zkX*Nr1IUkg%6cBVP>#L(|Q zx9wg;Wjjf0I|m#e?sfv@;|8~t(G5&!z#g#|h&u<=33`2IHgU$!dcAaHP^CXN{FOsB z$$*S^2(){1gn*PqK*Av3Q}jY|BR7gNE@VH#T4M)fUN^A zb=61vE1L4hz{ZB3?l}|ndh2k&9H}t}-?ML|4@a5mGd3^x^_^09du7==HZh9U5u9^@ zKgY{#G@s%*wcyck_SsLveLPQ!QzmZ#HUUr|KrUyd^;AYB1XTI(fmV#fGy^q9m}%dm|E z7^SQG!0BgUM=P)azeRbcZlO^G8v{i-wDpVa%ZeO%dOdqaD+e`rfR-iYzq2i7{1q;3 zog3<68X~xmjL#ku5&c0o*q;18gyG@OiK2dQ+TvT1jEYapW4uK#8KjWZG-EVW2-IPX8gMV*8j8Kq`(uU?-5u@e1t7 zR_xhtlgqM8pr2G_p+?Z=9|}Ke`lC7|hbvb)9Je_D`;wpRWAM9+d->Y22_kfQ`Pi3- zcI^BDR1AuFsUhVtLlPKXwpcye2RGos6!$YjDrEiHO#KcH9oEWcAXzH-{z&CO(hmsG zeCUWTHC}x5{@hc>n3K;3dKf|_P#JgSkot8Ew%kq$+ABbQix0ZD+1uIFiwOtcj@Gw_ zUAN6HQKYv^%em)6@3Z*J5HVSk6YD5=y<=V6F45G zu46I8E{x~+U{K_Ba*^@cOX4VIF^tKsUJxthJAc+CN-&n5MoRAdPxAY{wmwp%uGe%4 zao53gkfJ0{LfwTtHY@SogU1Xtj6ix>^&-v`Z&X zGCG^m?;{JQ#3DC4O!HP_%KZROm6lj#;sJNea>oz@MHhvdeG13BWY0a?@*c=P%PGQRf zj*qrbr|}wp>Oj9&ht|O(*f|U4Rdi zL>(x+UhhN{#G>%F4Z!__@bju0vt;_MZyA&>CS6{q#w%%vg$VrlsM*RDYrv~(uvO~x>E~eMhWafZE{uJvH zy{hSwzfRS~SD+1ZO%(NZ8Fg06sAh3o62@CyVR#DpGpBYQNH5%n3+f!tetQuc9$>%t zw%T=F(&=8{bgicvr{pviac2dK{8i)FSkBUZ>aJtGr{d2_fnvxf0NwJO5} zj}K1;x`5uNkKoZW(ei-N5BOHsjvSUySAbZ_p2LNT!t&Ck>SjCp9Sa{rsZ>|P(srf` zu7Z~>(MnwK!D+@Y zIntuAM&-`)_o{z}a}79AQsmh=hbN2=X-@r88x2KvCU$CqZoabV@QYjnt}mkk{oi%? z=e?xl;dVKDW_RX;R7%}NjOtB5*VW0*4mE)Eh;w^)Kmjo0(r(>t-}zC%Hy`jQEEJ5Z z_LN%@l?z&l)qGZCTjjkzL9tjr`>V#YWm^kbwX$@(t;0GACF=Lwr)9b$;BePi(tQdZ z<As-6(EJ5C2<^rYhC(}RXf%r%Y4oo_G_8xwY2nfj_oD(Z_XQA_hw3o9R1i? z-D6xDj0H%~&?7oyT@N%O%vtHkSxn4GwXAbvUZE96c7=|QYVy4TW#n;^1gioCV@y-% z)UO~ym!;XG4K4h6WaFK2m}BqW=b(D3$&FG+)Oy+K^j9vP4xpoSKe;s++wAwwDf6>; zCf>s3%BKU_MEKJ+7vmc)^xs%1z68#ZwR_a16S7|<^B7sma$h6h5K^u(>v76Dbayd0bCEvg z^)AW6?HBXLxS(I}!7f$S*+)yenh=REA=3@l3D1ilN z4}2~mah&E5EupcRIGHk%K9=mQr{B^xe22m1p|g&)O>M%6qs5@J=Y}v?Uw9wSI&9|@ zzHA9fyg`|>X(IkM^%(WVsJ_okWyjK1@J1D@>{17*Ay z(@e*2?4?KB0UkHQTuqJS9}kQ7Kt6t}=rG236QO_T!z@xnlfV7Fx600ptDKeIvW?=} z4v{VK#Ba7Q7nAtQXC8NUB$>NjiF??9MSqqzYpyB45=z~=#l5`&$t?~3Y^W3083(yk zWD%RbfJOp0!|}CZZl2DB$Rc6=3SZyg#(FXY$w@-ebTDDvdsoM_x)lZc__!g`^Z@ngx9+vzqSZhn$k{1 z769N*fV?000}gLeiS(3Z{EYidVWsZ0W+2K!Gtw6P!s&|?{(gIkknO~&B7dVZSi~6>>@&khE%b27-9VIilzq2Q6otRt<^c;ZU<9p( zi4AIzQb*79h)trB*D<`mBnbn*{cqi18VCWRwKvgC8Y*;F0DlH8$K^qhicY=pR+RXR81N66!Y7!7PJNm(pnEF zUG;bIL+)mbC~3cHb9%w47B-;B>loW{C?xw^k-kcOT(lsl9}t0ULBel zF&hpfhZN4JWTlQZr?bAhXM1Y!*a?ZYjRU^a+eIRL+&s1qse1Dasp-;O^!(&mPnm42JSd+2N+@N)a86{ zK##~dAIGj^?C1X3oTLP_f^5}kzM3%O?dn_TPDOtHQW&GeSAUCk-Kxf0HThSey_r<4 zSz)35;?A%uepV*;<6M^{dsGGQQT3$w=>VhK zyKk$^o_-v>!t|8dNuNgbe!!xr#V5h8YT7kk?xfn>*Y)d#b8qpxbf#QaGc>I@)?3_q zckRF$=Ag8fWZZ|p<~nIwJQl~c|G!|@6tff9)hjzdc5O{*eep(;&EExffGmoWOL(*c z>GdfuW3QigY7OuQe;lK8vfTa$2X2F}?BDBL+qW2EJFf5J zJ5}NLo2b`hIN;-*;Z@=X{su%UfvJUr0GA=j-rG?qlRUMzvqGZKKBH_(f2JKf3YlqOAvU_e* zOjL`;D>cKspm`_u-ur%`sVIHAP2dWs4*Ek-Ibk6~O5cpQhGs5v{f5wZyd*l_TT!I% zf669OYfyspLjUyI%uSk(HhV*VLh{6)J>aGv(dDVxy?~(lc^d?fEda^7uI5YA#!7>+ zu-`n_u8p&jKe(pJOY0$U@RD3uJ62Dn%=+(;EX}Au!4>s`P^U$MfvkUR@sT;*p!9J& zI@0&<%;CCcCty|CRIe1)B3>{aWYqzGa7Z)QlNNtf+oB|f9Ng_J0n8w>sKMn+oYjRp z)@<8&St14;EW3r(Q^YUj!L24`6>NUNm2nD!WRw7mJ&Ae?=>5$ruTVy&yTg*84SXu+ zu{aNx$4|S}9MeHn_WI?B_QVqicl24R$BA;^agS#Mi^#NF>=Xbjlf|r^)=+Mayqa_2i6HKp7o9E~A5{6x5d3|C(eDIk3w)k<3{gE0J4o zz^)GsFAf*^yMIPhx*tg3OKng|F*^oeCy|ilU>!QXC~mZFL1@`3^mR!!VE??qG3<`+ zoZJQYOU7o;C;p%7>&R^!Y>z>H!2j6U^JdcPzRfX^TURKoU9Wlb6aHg#s0Osut*4|i zF?_(u^PP+v`0ZX(D(OD_8saa??_55v1N}*VqKu&FW%JeYv<*V?{i~hg%wfq@`(((`4o-N%ex(t? z=fGW^MNeE-VSTu{*qdki!eedXI()B11Oc(@y^pK{cC3&dA0{524~fCQ`u= zP_S=>Qif!z=(47^>k=1epQl$)oSaBfx~3UMbGOv!%mvY_Uf7%jDZ{FaWl&3>s5J}N z6bWE~9b;j9l~XRS4}KUQ1iyLv7K)n?T1DPkzxVxpQj<;`_Mp)o$mMJmZQl1c6)8Gn zx5^R`WQusCHMN~)@?*M z##PoKdC*6@FsbdUkBd!$aQa3_z5uTQdZu136Sk1Q6OC!F{3Su&o=!RwDJ-JAa*;vJ zXJQZ`&*!aC0|ROQTM2?eTug&|TY=wl3s%5(;7)<8^p1l=qHaU7{bzl(zbJO5lJSiA zefO`RqD>Ba`(aR}Nl)=zK%6KOhV4Cxz?axn$89)J`kGhj*sTnn$ne4cgu)L9RciH#Z&u@ZBhTf#OkBEAk; zJDQ;k>Rcm>yZ8(^sAB3=Gedeb zCl~+NUsk^fzHs)&v>eyAVd-onppJkv5#k;J^Hjz4s(A`bGy5H8z&4_KVJ(cLhwp{> z&Jr?4LCqyl2?;b>25>{W;T2Ht0Fr2|MWFAIy(p9BGe<8JS^aSlQPC*gbY-8UUlr7nb>=cq^$}Y3<_THe~CG(IQr5(mhtPARV!eSm_gefwPw~!DvlEI z|DR!4LBJZGw9U-$*;wYrVURc#^-WN_sDB(R;F~?^K4D+L6}Urx&>Jenk%?Kenzx7e z7<%)mD*b{`vi!$-87~dqlX^bODBr>p?eZ4m;C(BMUFUVVDJJ?jwFK7wH!@dm;}5rK z(|xKc?$Y!((mfnD&tkaU4Siq2Oq^~AY;Nsn<+xEL-J#*>5_3sEBvWa=!Qe-z_xDrbhb%YP2r(!50&&+v{>60jGk()29t+ z%x3z24zoGg4vboV*Urf2mEUg}(t7gW#4rDpenYu6WUBUt2eJ$^0%MH+OijEi5>95%oyqC2+S&_C2%_rdwr` z<_|nlKGKxlJ8=D-xc{n$N4yQ5&_X z@?fX?UWPAH-a5rRH(}G{?&9ZJl{k)m>s#Yb?*s0B?P89V7I3N`6YKPkvz7OB^&+@(cDTfw0ab2FNFKO8MTBlQp|g^N&u znn48^J61@Z@XuvO4yb)Evn!X(54R|<9X!9(^S>_|vm(*toziJ#*&i!h?$)@?V)`9s z{H)@5B!zpKl-TI;2RHFXr#!#f8g!NkvG=Lv9PV~13jeN&Ez(=EqW8O#oeIv+uK%V) z9V;I(FoytJbsGTdxY~by2Y%zg9KvVSYTkj;Cv`Yq2qWh$Z&*z^`1pjNMxy*g|-1OZoJMQV*Hv0lr8)(>v68{ zq6@$|D%GKJD&e{`z+-#nlQIqPUlAR=mU=^$EBo2XEy=<#welbPrt^S47h6VBbo5+$ zxxcwc+bcFO&+rksJ?_!L(yNB^96Y^9{`!BaRr6}`v4^O(G+mDQ=eU2addezF#GO<= zfvT5(QDPk(uccoIV0_qjVC!v(uS|6H#ib}m_Rz_f&hN9b7%%=LeA&wK9iK{8`VvI_ z{5(YrLjvzgXlwr26|VOIFWaw%$yDF14muZiiPb}?)a9s3Y{#+?;o8&Py=&=i=`YP^ zSy_uH^oJqtyBC@~d~;^(=>F|;wi`dAYBfnmrsrMTRc`ryUG7mXr*O@tM52WWeDUic zYx|_T8=hJ|$Db`H{vL&lk9G-YShY7ovm`)|xqUv?6P3zVOpjmrI4G59b;>cA{vt(j zv(d{M^7=eI)DCZrvd4G2(gnZfrA@(>?F?*|Z^P`atylbLXTPeSpChB4l%H$qH zseUKBWcG%u?=c#Yv|c%(p5c#r#tDY1OF#b?=O~n6goPwaI;?MV0yG`uYmsSQ?Fbfs z^of}q;q{~L27jhjGs$#C74o8y4clr>oBLb;+5zcp85?;*9mzU+MfAI=W99XDEz+Ui zw@2=@vPZWPN*~DT`yq#(4!(bI6Eq}0T(gqqh1KtktJH>GQ+?fzk{vjwt3w3gL(Fv4y^8oVqy zo{7Vc$0bIX224Fqzaz=P_uw*$;$EYhA_m(I(+f|XDIaV{?R!%`nLrOEV~jMJV2GhB zTTJJiLj0jt$FJywz8YUU^fj>&y<)^rzX2qk(REv2iNV7D8J1DKSGgclOm#}QYOfu@vP%bA3gg_xLZ!zKzH_gz<_f4jeMz<#4 zc0c>lGRRokD_qx_W_iEU;+jFC=+-#(X9akn;Nys6vTs0%HNR2_dp)2bZQL!sDe*?s z#Zs-QUK|U`aJ0H5cJ7Pf+V(=Ri_i}x&u$H!Y>6^eTN$e?g{pA}h+_=G(dvMb^;^dS!XN{ z_eu#ApV7JX>dXRblC$DlGA!a44c!K+U9sWT z!q~4?nSPbNKPfX~qJ8Jsw?V%9FvDx%O=WuR+h{2<-=QvbPaYWW^C)iRMn z^+mS>|8)C9@FFRE)^VmGrs(mJuTn^{^g>_=mgnpXI5;y?`l)P}buF5DpqiO+5d{UP z^Kdv%!#Nxe_JtMKSP?@Wm)c!Qd znDZX9*E?T85hvm7G-rH2Sc~y?fG4}2pYCpO@E06p;jJJiH*B~Mx+TgVr(b`LsdB=M zo!OnxxvRXlAEMkO_``>PP5yxSa;$Z1Nc*O7KTD7qSChc#V;5mjSU9DJ{Z&Oc-)`@O zGAiObb$-zqsW#;-=%por_o&7p)Ax!|jAJl6JbY}V$n=$r2~hmb+v3X=;ODj&e%f6` ziM%TwmIlQhW`7d8pca7xdv4qc*{Iv;HVZTm*2#2grYr4{LceI%E!NH=3(P#4vRV3;p!S6;L6BApCwQ; z7uOr;+%3BJf+wv3Sgz_B!}{oHjT~h?OL%KI@hyU^d1(nPTov?}sQ_^-DyCKBlQ1WLm5j%i8 z39NV2`|Aa+7a@98J_eDa4zsa&gm;d%n&v?P#%ocRg%>%^`dvLx&_1JLesURc&8B(; zo(`bNmod49mrf9KPAx^SnDd=Q16tMzh@gg`!_k%|godsCjf7 zFRYG2jPUk^olwoub_Y8ds1Uir_|(u;Z`9N0$;dh#Cs zZlf4csg`kPWALLb9a^Srv7K@SL z=qtS+^`_QyMwSE|oI;Kb)-gUOL2E!Wov=*;&Lm-G6KV4e=UMUfy@~LLy}dnMV_2%s zEz*dF583)2CA>D84;R0H&YW|=K_X$j3L$l0a2IUSA&Wyq;$GWxj(W9dKuP#jR01bX|x%T2$!4^hJhb^t$ z!)N88adU&#v;?&cyO2kbUp!Y05%h}w@1M)`QQaMP2Uwt)PTi9MqUAEJ&{uhM{gv%t zSJkcA12Ui=s^{^tWZQ)aJ|5@2gO^K|12$5cnqW6 zNfDx43w@A4u}e6WgAl1}$;B@XLBDgARaQg;Q0kc2_LrRHU@Emf`p^YfbW?YQnEUSG za$zy$Wszg`q`cO?1ksg2C-KP%3>}z`>!lt1vG99t1F75KBy4cL&rU?;mUZWrmf{1# z6tRbC)&_jvVXGiqL(b-qoJT|GQDuTCA^atXH=#RyRN$*`w0gh~H^-yi_|CQEo=*u# z&MNLcP2$CeZJyHP@1zk0D5P9+f*AL9jLjTc0hYtRBQOQJw@{B`|WoCApcx zychkS8R*YvVeCGAr!gZX3@se{*nYk6MJ%oQt#-xZ<|zP7AF`dnQwoi$gjYU-4`#!! z_J0t33~8n@ZB;3EcIL6X#81_AYoV&vF@U&fE4yU!TLB%hQ>;00vM4xD{94ZL&~JAl!X_OkW>$+ClO zvD^)ph0Yr=G|s#?>WvW72myoLrM#U7PIkZnYG!q(N7{6#Ik)tr)9>CY3%7BP zjQAdAFIoy^om}+*x;#{uge)i5EAQJx6;T(TW>iR*U*Bo<<{ytt&80Ay4iF5D_X=Gx zx=6#fwBlaIMLlJIuRP?-KKvN7g-Q3A))y_|vAI74k z{{CIj&%`EugW@Z!OXpIIO;Z$AkDDibHtXlX`nVW*WR>pr8W)1bt{N4G+QRCG^XlHC zCV(Y7eO}=jtVs!KLoPsSx51u7*%l%>>Z|1CGHqfj$yym%Jk6-*S2MS~8SCGp(y|Im zY-l;o<>D`8C6ipdc>CFdLU3kAnDz1c4V^V=mm`;?!WPwvtvD%dsh-#b%IsLn8VFO_ zUZ4=0_pw6z&d)IR>$Bz7q-Pau36>DD zR%uQlYaqhk1_nHG?$cgYqH71@T=r#`v?!@iInna%8}G@%vEL!7`fZ0WN- zI584lVIvC^szEDU7kyB@-=uUVT_n=igVFzBrgTWxu+7+G>YC5mr%+s=f-~cK@jXj8 zcM!z$8Vx$&TfYV0x=9AEREImNok=V@S5NlL{P?PXF1~RRCTO87w&Cx- z2;ZxK1&fY?GM4M6hwXYeMua@F`tx&R7@gfQ>2afX_tf*aJCpSX`|qG{tySnT4EGSV zQwuh?fXE)oh%J7*IP@TEC#%_7O?>XyK7J6|@09*Fuwe*?Q)JU@Tt&lzbZOs+aqSk^+ZEWLDd#&7^?pw8`pSLHM9@-uJgdGdsHj z8hYae+A>TOk*-TLCMFoSes47;X1LSOBV&#UuqdUUY%8Qs7F0lAA-%~nz3u+-6^mPz>Kv(j&WH2 z(fNb&0rkSV57lhnfK`J1L79o3DtsMRZV5i0+$!{$>n51uGHLOrdx z236m%@ljP2!si5xs&Cw4LtCEF37tvp`Pyjch7 z1mdM&jIAk>apaZ)=qw$^I05_-n!L1`$hx4g^IpQmG1DDjS#LVF)b8PEMzZ($LrY?A zP%eW9_!ybh!eQ_J1eF!mfv=rnrYo&|sIxX}Bi!U_bnl)yF{RM2fQ*)G;@L%ZzYH-` zkP+W|xN;TfLNZH>VPNjcy@wCJ!{#Z0U|)hNH#swXC)0#O^nF zE8ooUpVIwNC(Kk}>Jj;(FxxH$mN$ zdJj`PrJf7IQ9&`k|7kAR67*`OPqDpG4Wjm++*xB4=S-a_B+ zA0QhvFvTzCDz5ki=-2`+N5i*7NEBUPxoV3q36FBdYL!u(!%^GQIHdZhd|K;O*B06$3{f zP(nhU&gQ)aEVrz#=sV2>)9SLB0wMkq_r&4nuh?-mOrU6z#c96YJ!`hZp)a7)H(Bk8 z@4RU21Hb4Zn?n|ow(Dc}a^dPBig?=3wa8!X8kLo6^h5#dY68GD?2D|`wYs|QJoy$@ zP41PO3CtYT_(A5Q^xnBRr&+o04+IsNrBE%L_xAuvE;pB!gXwB(c zlQ`^cS%7n?YHxob!|dWJtGUB4ta;6~+A*|Fp7>gr4y|Tu%9+|W7qQZ5+dOs5nEDWe zoxsLy=cvVaHR@V=M4|a(L`y|g_r)BXk?83qhTk(h(uezHasm+31SJgLWgVI!-)JTl z9Jxat))hgOykB{?tQhqF7sd7AkasNB9{zE$4b1ornf$FDXAev--X_KA zDNZj}0{*UQ*Fa1~Fe*Y38i%R>WtIF@%<((fMa!G6Un;|blx~S={UBxC+ThvZIfIZnsN^`-BO*ANbA%R#fJkgK7N!Bk&! zSg4oR6Q?y_cWgiHu+Yy0X2#_j(+MI7BQ^-XKAS3yK-_n~99RSmA3(QhT7%B-Je~PU z4I^IYi9hta37KZN32}e}2RPg9=2ckRDy-|me1#~iJx$N5ZEZ~G>0XiE+|c%F(VOc6 zq1){p0>M8$e};3#nEfTel|lK{Wo3Br)b_FWuc@tlZP6!LU}^IWn(por3a%ThT;ZmJ z1@Al>ZP;0;R^#Ka3~&fK8!nLdqmKRdD>mLfiTe%yi^9vr+0qjmD75y$uE&Se9o58p z@jL>xaXy0eup{8>(*`L+5O=O}Jg ztyFaf-a-3lzkBFAI$g(`^SFuSDAWG@Vv$a$RzjWsk><-+9#UJowMs8?M2BNhL*n_* zyi3Q5=A3J`gj2iP!Y;GF%eRt05PS0NdTINI!uAjB_u9e!VKLWAebZ*pny!A_=BD?q z&CF`@P)oawxo(o6-%a;Or&83Y@YfMxl*MHAuhQmf;ip}{Y}-<7OJaW6s>M`r7BRKI zxIp>zr`T%yIOWK4yxY@sx8ZoVVPn%_V~7^_YB5|k^Zo}C=T!Sj*^p0LX`bptJq3oD zpjiR9WXfEd5E}Dgfh8Y7ybk3(W}`;IDhX{>_hZ*TQ4aj`!j592G1NwiLuQ_ne1 zImtoQC9DwctzegZ{urB%z5B{vWVwcHnB{6W(I&|*z5>m`I>tJndJlPrBkUiA!xH!B zHthIy44nqN0{6oyjot!BFFS5mX({JC14j9!(~s9>cBU;7pKx}1L5i$$^>T#C#I&Gs zm*LxvudYfjdzP&i>+nTg!PNttUcC(o_I}1@P$or`A8rQCT-l1?O6dIbiY{v>jjA)i z&YzRP;PMrx7UTCJwwumkPp{ES&Fn667+m<=8;(Lh$}X?7MTqpGoBa_onbs=p!*$L1zfXpm4DezO6DQx* z{v^@gkuu-iPd4|`fNDx1oKR(B)B_tN+t2xK+L^|$kc{s856Yyy&C>mcOSgu!-);jF zg)A29ONy!73s5QrUo?nnyiuX+NPKwrY5}IEyCY6 zEE;#TVkHI9V5!?QEuN=?1S%ZcNlk9JL zwusMx-mm2IFDTWCOo2T1(em?eT40K10|}v*?9Rgq9E4OKT>6p2mj!)SKpgKAvNY% zKc>Q1Ow16puSC8r7`Hcj{sv*L78(avpl!z#?B`KO|x z^?dwwAngg>1U;-xhd2vZ1^fOP5t9NAW-h{|B0ND?jjs3$KaI7%W+h_1Nd3tx!N@Sx zMfn|k3xs$VWuHV5PNSoSr8Pfi1OoW0LgRzR4Pbn_g#jn~2UIlZnP%|!&VV-_S5gWm zW5*Oo*4_G86K$lE6mloRo_eRJFr*kC(Pxs9}e?sMFaJ;){sQ%D5UlIqjr}Q%}csKP-X#~gUC!X zeYe|gc>nm-!jfdf&*igTk%aS}BqOFkyPYF&{zn%A2>wbW$f4YbkVz?n2r95grG@5D z?Ue1LB-FJzK561w1HJI@h57<@LZ3YnP;MT8iM~mz%40_oHom;KM6zAck17J?p=MNH zKnb#;R|T$n!6R>P@9XQ*zrr35UF|C8w2oW^_I28~$F14v#NF+NY7D$*cmIjuz<_s7lSX_O zey}Z{47sIgLAlfr-Hj=guz4Qo^qQc$1#9XB< z-R5c9VVzlymM-u!I@c)kFQJQHtEHT_HEyN6LwGRbP8sk;1G*yT9Js9rRtR4IB1lVI zx!CS)JY6fWXvU*7DEEX{uy&SzQ!9Gc$41Nm`l%YY!=2=$R{%kL{H+2eVWcg@TF~?E zUuk&U{OOgN+nH(J!-cv^&>zdR1(g6EFc2n~Bpt82cYZU(xb;PHAT@F>UsA=%Y5|e+zpEuHDQp4%PY)v5_0MPRLvSw*_(*SY|kmdu)*zgIZ zZN_BL*@YTPPA8+?h)J@}J{P-k_M*PfV-YX)d&53mg9XwP1t|M(GJJL!$JMsBxeO{F z{(ohj#?ViXh-^vU$Fm|P7(%w+@9yy@&38O}9Sl=N_-eoA_tOSTq3-TU4_TZliTG*X z*Yh*0xCG8jf1xLnYiF@5(aVpbq1#VxVGm?(Js0L9M@-`Z4DoQGPE6G#U9xb~t+F!% z`LZ)ZvTW1M^1y$%GwJC9uaJc$VwG0_uA|QiJYCP-d=>nk&KRPe3p1j>(k^9C#_guH zU&c2!#~}eZeVG6oDKP&+EzX_X)_Z1AVl%ialFAdG=t&xhLXXhTDi=4BG^|OF+yFI7 zZ>?9P_K014uY*)|ehNLTmOJva6a6yh{=B80Ys>{xbbUbX*71zneC-MZ0j+27o@c!L z_jJZ%k`;}(u!qPVV)6VFixa&{=!_KC29*RKTL>qDAgFW2{3%k3uD1COSYVM`Zj&jqdac#JZu%ErC|x+HusAxnFoUk(yq|Ev zwy=oZcE9bL+b}W6<3?bu-m4JSRgLkFH!n}vm686R-{%xSRuZu}`O;y}MzmUF+C2Hv zTE6b3i?rC=fZlVilvGN-g69{RKABPJhcB3IBZ1c&gcLJI)@v&3vDh-dkA^+e z8LjjH{cE59i|fO>>R9&(-`>#ZKU{KP@sSo-f-^I3}QyrlWJG zB)WP}8S`iH;DJ3g>dF!g+Ljj?!2!fXfF~@%U;X}ZhT1>*Mg6e1{$x#%SH)10J=|Y- zF-<-~ma4O(!p#e}oqjDLe)cfz6WuM7R{e6dClLT9m z({vHTebyF-IlUxDXT-#LG&y|POHRKb@}_(BL5%eyh2i_a4!)fMBPyitbe?%T{XOC# z-fseD7Jw{_aj0+?rRu$k99#?A(<_T_8RT9ONW;Y-2vlQ!Cbv&RF^Bn*p~Y3+z8j%w z^2D6g%G@coFDZ~ONAD72G2$2yw>tE@FcnGP)%u+2%|?8*=YLBEfLV+b|KSXtvcP@I zSNVu>gtnJw*W-Fx|L;#z4GTE>Zppb{(#Q2{^MaJB2vRQ8i42$Z^Y+mbG8IU-g_(NY zX^xTB!NH~v+yTKl*Ryl6NE4q~R ze9oK&oxBDJikkqnP5Do!YWI^MoFu0a5B2=YsSFJeOmz)L)z7I{e{H1`b`#HT^E~bR zVeDfF`;yC9nvrTJvQj0EVJGLX%*O&YlST2O^-31t*>r#yM_-i1s@$~HBK&@agsAT^ zu=dsgPRp>iGlYz+=(VzFM!<48f4h87ry@FnHLUiZ6x&*(-Cb%8&Z`IittRt#nkka! zy_M7%`MNH(%InRfd_vu9{q&2`J%3?KX6cPEz zOjgDEa$&*FQ@$BAp4YrZxrS;}Ex%i)1h?Yk?fcLfI8xHfhe~%s&>X8h0bP}5_>V!K zVqNKPSDG+DQ9|9&uxh;dfl+lux}=`Z_kQ``(@)0g(z)yllxT9DJc`9F zar55%yUM1jBxsK*(iTi>!VPp{?vGp#fHi6OF7?Q?81HZG8SeH=B!ec82ypmC8U9jL z!Hc-HP11Of$nWEd)Fj#1`q)ULf_6i39pH&?j~c0U+y4A!Y#7!DA(q^AkHbE3`d$!7 za-67P6BH}e9RLS({1&pA0g@oTQ7s4hK63U*?i)S&@`#uM0T`Cpy}hAjHm$#homIv? zadetv4mbYb075%l=yq{B@?eH~ubwy>&?1{!x*sHQWoH|?m-Et7cXLEIO`+XOw9QZB853lXLDy3Uf=A&xVyXi0*33bX)zlqLKZ;{`@HhO8LYStAeoVWOKk!& z?{9kz{AvFj^XnQl&3JnV*In8tyAxqAz};PXZhS{R-eIg^;Pa&Xsb^GKL9~W^V&Y;M zgt_rjD+rrqxQhJ z_>E&u56Eg)Y47!6LkLsD$de{e4_(X%2Qa}?U_WCf->Ar7PxZ_kU3xw|{8T)7GJ&~q z6X*#>mK39QnPlecRv}M>Jg;c2tT-&J>+Rh5c$z93CYV+}=K&Fgi^SHnM>#gIAGzcu zypRcUKk7az+4i}%`1v4W{uo>XHDE2dt&-|dziMpOo&`L(+HlR;fm+MWr;St3YR$m2@l?qM>q%tT zTEXL>I}rAM-2YD5%s5bSobq3WNB0|~zKhA_M2aZTN1a*B=Kg-PR>ye_ax-mo>jJtg z{mYm8uB=05^uwxgzjkfaITwqUl`yg&e_j@Qe)KqkWMZUx&!0h>{%UuhX5l~hx9d3^ zo^Tg8jTAJXIOZ`Zfks9*$1Be64CvPGF)IpjE~P;Bj^>cw?$K&ENi88r{&w(&gLdGaR3)wa9B>KTB9`k=>a z@Nu-hIh=;CQX$gsjhLIwyxWlhCvIF!9_x?r!ks!b=3f>RokbFa@ZIn`Jn_3#+|JKG8RMn! zY;o>qahQ?#n{vIPp_ae;4_5W)S;A_DdjzI}8ilU`SE|4vh93lfIP@+^UiTDPyUY2F zq=>bz_aeOCx!K5;n4jvMSm#$`hJ1K$0cu3wiV3HFi6x^B(EV!V=$NU!a8~xxxzoR8 zng^*CRk#Eko}2|jO~Z{Vt;wm#Lcs`m;=ksu8F0(K`y6n0m}FEfV;~l3BILHx;|{1; zILq?=GuGF92?+N^pYs3tG1(aY3VpY79L4-xb_w#v{0sf@Y!1;y98Ib;yb}=iCguxC zokbe41KYMGMX3ow7upby+);R8j)&KR7j@?MGl3`5e-qbbnQM4L`Q1XruWj;aT(gkl znhbEs*#Ekqc(LxxIYIT1oJG1{6E$K4>QLBX_VPtcjgfi~tBjZqp23C8#zsH&H^hJc z;yXk6vgF6dOwO3Vd_)zzYVuzR(Mz-XpG;zNfEyM6X7GPb`!-O@*W>~5bQxcsB{Chk%bW=>Q1 zPRj0ZZ>GHmW+i*JgQx`$jF=JsZ$O%;H13#r1IOclTtWt{5&*j%FQ{^c#*t>Wde+3B zrQG}Rzn~fmP={<^1@QvR!5jR8Z4(6*t%U^weNCm-|IF&~ytzTVpAs3)9agxI7H=&W z7UXrsOX!fP5T4e9>wkWFJxm@=v!2r5-81shP4(%&KUuNQHmuR*$< z5g$Ys)74`k$9tB2qndcgaot!w9}JZ%4Vxyn=o3BGd zn|}U&pnxd3)eM!SQPWT*XhVp+ zUL)blJ!(27hPz2B*BZjJa{pS2AWN2*hb3kr${KZV&UWavncp*I9cb_juRB6W$3@%y zifTVx-dSu_)0m!!brH1flkjrga}{09NPSwBnMM&s-9HQHuP~zDPgEXqqLzqUB`4@* z`L?wG8}*(=&VOYpdK4YHDdA{xB}s$FoM3Z&!uveGa4d4tqV|EbFhSPP(QIyk7B^B{ z5ZM{=(kFM z&mQvJtb%Ph)mu+J_dzvh&3cx3c>Q!k9{d=tpW7*&tw6-YqiwEr8c0Nc|1;~El*c&3 zF&^_PCr=5I2?74JMC;&%xSGB=kDc2B@u^{P9(5zBmnNox?qxA(#S=5{Ytr-@S2zOf z13C8*PzvW|FDDKC|zZtk+6GKCYRfZ$$o=)Wi3Jm`1&f!q1kS*V*TrqZtK8 z{FWlJ8TW=guI-7q^cjsEeulIWB>6p%@*Ef|9g0?uxgdR%cI3T+2_HEgf%&tVcONIL z4O%527HJX0#4M1hk&T6dUFl;bul*kN`2F3sQR1kCDbCElv@Q2x zLnBFlC3NbCs%)e+v8_u((a}w_@%*A83co)S-V_loq+g`mP2wfgojd#*sM3 z+Mzq-p2d9z@iPiSjU}0NAcKwA^|36aC~jx;T)#M#1HChH1;Y^fFHcG^FZp&4?iPsFTE?Z zoqMZ7p#R5P5mVO1DZTW?{ z&-a2&F%Gu)=AyQd&S^saEcLU~vF7`oo(5QS)auiS!l)!teo4isqxGlGO|#IXr-#!iev;cO zu5W5xKc!(eqT9*$#Mu5av8&lNt|ZfqRy~8O^Tq;`wOL?2};(Y5+Pj> z>^wmY;vK*DN(G(sSEhJ5t^nsbcb|3`Irf9mQJ_Z01g8T9v*GB-K9@uNqXhwDw!(b{ z@uQiK@pF~YP;qC07WnM*nO_WKsLl3ILVHJ^^0y!P@zMNQ!>)`rdc6N-l@q}|S(F^E zciMRdyAsYtVW*!ZeBYL;f|eS8snK=@Z9L)=G3j;iY`blJFNPg8S1I)zBbBl*z@tRJqc&i|d{Yw(kUX{yk|hBeN2k2(4E^0D{xS0E1~&rIpVL=VNitT5nJ z;)K5|VM<}y--hu#eY;h+M`$y{>NgI(%Gjkh?>Noo?T%B)9sACY_3A%Xcze-*Fo|aA z0~B#}L=u(MK(3)Uyu$mJgwyV*N-g1&@n^+n?cZ*rt&Y>*L!CBnGXJqaa=n&!;Mg+p zFX{l6|C2hvp%1R5ng0b;*NI_O9ROmp4KzGE_NLFt;ZGOa(RT~DSvEQ*pxdU91Rs-> z>pflR*Y@EOY7NLmhPmAaF}x_!=2x2&_0k1A^;{dyGV_>75G!_nUbK8Otta#7%`k1` z)-OYU?v{N8p)|+Ex!yCHSVGl?aXrQ{r)FvYp~YJVWu5D)f;wxp`_P7?SH4?$WlguQ zyP*2XA7UF<(WbWtjuQ_H3-&wwg!0-3M@%gLs9a_A9sqJj7j1iQ6guEc^51}NHeaRl zp|(8E%aaT){3v#hg=~0h&MyXcB(>}BPqS@e$DWlQCLZ(EV&UD*WY;l>MPw-xYt%9Evlcf?qbR9-IZSjg&Y%>Y?fJSU!D@0 zeKv;4&u>LT{W&98ukpi=wp04okiTbO%yk^l@m~rj$n*c#!U@O)(5=26l$Phs!lwYD zPZvdPJQbFF8G2sQ z&pK0_M`rSag~=Fe8f-VEe%5Y>S2Ywm7gaqXNr%rWk$VOEIp$)ykk0%Dbj&xe z!{!o}lF8agsQ^!JA0JzlvXS4IvW!?lS@`2)dv$1mGLTn-xZ8_cMNk^>r){YuT(^C{ zVu9)X4N4{@x8x9HoP`7?CBDteHl!AtvkZ5@$o8g8b$ov!c#8mzb1VP~vGUo5Ng%h9 z8?z}amX8o$eUvSwxc4fiXX9g%(JlyvidoPhN64_vRt>46Yohf%Vc6_01gBF0Y!6|_ zb@+8c>e9=G{Rd4A;>T~8rfCa3I)1IT3HqrAcA(Sn;Zk$9+1RE^FTJHZ@4s)vSCZIL z8$o}3avp&CDRfVuiN2PE<%!VGhGcfN!dx#bqF9(?k@oimkrHSQ z$6x2+X7}T?3Ldw*lsGfVMJmZFFhgT0p=B74_q&7{{;@VVKIEbKm#M5SADu0S(ct@G zbdO|nvZ?)Z=U>$f(-?7snH1cmF~2HMWPu4z%!S^`-E?vz1ad9aG3&RH;|mdXz(az3 zfN9d8Id&B-_uKVx2_(Ah8jQxWr2`cWS>O)5h0yR8DR19n}Wrw*(m+L z+rS??wK4KuVbb3e*3QS;<*%?fh;F4n@B{GhHn970do;q-EASBr2JhaU_MD|AkMTZ> z*ObyB0GD7p-8t}j>FXjZnCx*Xj>nf!3~Wlz_|9oDVShN(U2=)!17%R;oj!(vW4acV z@d+aFF-TM;YDX>$KKKNa_^Yb)z_D0>fI2C|3e=|)M{a!&mME_LtAcT06Y^!XWcwO8bYJ zE@xhPq4|&9iSsw8P!XVJQ`~>&Dx>Zlb98GgyWHXzT)pzx8-&Vjz!7YjOk?+|I+pt0 zI$gvozYGxF%YTxtVa@J3LB@4B-7`~k8G`>$J;evc|7Jaf86GB>W)&uk-5PaNht9sF z5N|>|@+7RK@eH1-@Nr|jBJmWRGA=z-eOqU*$8sI^on{r{esqc~o>qm3*fnw4A-r5B z^KjPmBC>(sXtf=uFEc0t+kk(t#dSPP6Ei~g2IEFB_*8$c$Ok~f1;FEnKrMed*{NA7 zMK)h@k8TkjTNyTr@(yKFU4yYmC$PK-?sSYOUFx)*_s`(ujV50}L`9~pdt3HnRF{4x#%V8uV18o1ugO?_neC~3*nsg}3n zO~pa48!JqC-7;=l5!!uU*C;U5Hwdc*1n>Ry$BukbB+J|Fyf7uHOe?m>N`1n4wttSc zEV7jf_~*>{wajaD2B6dsTUR83-jSJwIqF?8;I~e?_kC z{gIRsQDYxXmQ=e^+>eX~HK&hq0lF(`xYD%Od! zPPr-`RwVLBytht7KR5Y;%QVBP{&J#q;Tt<^g~#8RJhJIa>N}Pb60Hwj3}nVzUt7A%wUV!CZS}SwM=_e2 z?<7WH5K#x%2|KV1NW^t#0@B(%R;8w&!?7hi#)hApU5l4f*wyHMe_xIV-PV2oSB^+D zTSZ=(f+!zA!S~t?EoCIWGv!F}u~9;r*R9%>f2{?@GbPu79f zb5d#2JNHaylD=O!*H(2TxkR{Xx{$N}X$rxC@I)Rk#6vN2K7i zdHLPtPqwYa0#(yW+9$oG!CjRTaBPFU5%fj?mR0ct87^nUr-KR5Zan_(E#bQuxbwBz zSA|Z6x!?JJabc>exh4V(&3k@wm7Uw?_cvd7pt@b^&ZMtaWtMp{<+Wza zEtqt_`(wH;Yn_>J{X(q|>$Q8MweZOmE!&f(?sjvZ%c;kUo6y#Z_ zTp=riX|mlr!Q$#mUEW5T14s(64Q9O26D5{XLkPO$`!9xKGv_TY_%FI)Xyv|6=ml}e zzc>9o$?~Mt!dOSn?1=I?)*{Mh?N0K{0w#VKMeEjC_v>bCkYhYZz&L_zTz3!GR)7=f z_tnfV*8A`>v(k@c+O|a6RGfQBMaY6R11u|ednYOt@J=zB1yK}**Z;VTI41{{wSl6u zmNSQkAa4a1F=yUlWWl17G|x85Jr62aa#D~_y~CUQ;^!E>7!#JB6qa6Lm(5Z$Co2o( z0+&iyh5xCIZ&FC{jSmIwvc>?SUg##C z+8j0SHm_Eg9MS_17k z%u+k`aGmb3QebS8uo<~M#N@T3FxYiL$SNPO5eCV1KWO4^K-b{obhEEp)_!zD-L4_? zcXOO6e+^ID`10>R-t1ogP@1W6hkL`unfm}|Rfb4@xZ!JpZBu5eLhSDX75lrS&np!? zmr82=c>Q$vi3=e9K6ao5F!%N(UcSH!?-0?@XJbyCo^`4M`Uu%Um%Q%kPhcGN!9M~E*9e84!c_h8rPHAG!RoMz?6e&~Bk zqc^$~#FJdm$doxHGfWrKUFdwtTLK}jy38&v%J}NPpHfBw{C+(L{CKism*yVP+BKb* zMk`MRuX%vPZ24_zs)zPILgQAl>@5kK1vXt7pg8S;alfuz14UR?Ap!) ze^sByUS2R0w63&M3PFM_7>!T!(rF;Z`Ha`$i@^l`h%hkqtGBQ|IF%rC?J7tNEjs5B zrG5)PswJsUEcOfcFFtwe%h?T&09H*ksi=a3;Jh#G8Y9i1gDv{BcEBm*DA?sEpJ$r*670aUDjV_A9rA7v+=v%*{cfBi&a3Jd z43JBQO-A;B=#|pW_8-?gxdL3K0!Flr8MKI28ne(HLH2Zm;PJ6hoBzKM?o(gkN8tNt zN*`2w3kL)q)|!5h;7lqOg$>C4#m)6<{!>$b@*&g6$dd{|CqL2_?+upHtA0e^sQ;{0 zIr&Q=W6wzH+L)K$b$2hes=9A&Hi=hMY0XHen z50A#BH$6TKQZE&8dL*i{jmCyf_k7|GPu6H`$Ed#%j^GSf&}0jsk!HxNlZ!;3h*YVA z?a`Zw3U?kIW+VprLMs!!dq5XlKB7;H;B-I*kfbg& zgqBHs4$=dfyi~d^IbyPYL&ha1Fh2{yn$0aHp^yewqjYvF)iNfRbspIG^AYlSh=8Qh zxxzA}4G;Xo0{T2_jCpziA5Ts!;SLMVfHt}b%*kV%E}ZtS7aSD5x+~@{una+e8{w|= zo#DU0GWUt^fNOj#DG6kL7d7R=Up2Hj)jil6x%UB^KK;5%8UuMEok8{U1!RZQ97C<; zKDv+6;vJ#uVq`E44@kfU?)rB(na$T@&kutGN3T|bA((T2(Je=CL?wLe#!>T(!=Q(e z0Rob5ErE+|EOTQFlyTW4NCUG=M+)qHwG`*5gP3uU#Ny(6e+>B7zd&}(!>89a*ieLH zAL6rht-(pBbDhbeI8azOz^+-pDZn>+JM(J&-uSGeKVGy&b8dLz+lP^l4h1VL;KRHA z>FaR3A$0hg6mhJA_WWmC`PG>G`}9dwL3cQ*0ugIWh^{t9a71}-ko{Pf_PK73-+LuNXYQT%$0|P?m|6B#N$pp=ciKdcWM`_mTpqj^ zt626%B2J($GV*g9mhuf$a&}H~0GyD17neT6R4ufrgchFs5 zjeme`##i>O{sIZk6i$!2-!-(G`v@Z7e^}Xi?gMP!UELz24w?*uW>qA>Zq~+qEGL`* z>$l%FSVArO95wA!G6g&que&#VUBhdwe`wFzeNHR9y!5e6A-fF8@#(SgM zf&%%|@**I??#i5icN^R%oS9B^!>3Oc> zxP<}pF8K=B0Z&kMxbP;xz7@3Y{!P^759jAfxcA;5*4@#k?{O9TJBcUOOVA)55~9$u zfhhgJ_(a)B6$=e66><9Sgw$f=rDy8Jf0I@O)1SWt>Nm0Y1?S2g)k6!)UqfpOo;3d> zC>a;b;G}q2eVu~=IQtS(REE`?J>7H`ywN$;{^d=mA-)o$J~x|q-FQ(=(`~e`dMN(2 zXKCWSrHPGP4}oA`t6(XrP7prlXbI|?dkH=JI09ss+%X9L{?jGGz-@9s$a!Lc7p`_` zVcg!z#-t6Td)-P|eRz0o=YucgVP8$1dkpY42Z z92DPgPyuppHmpTS`0E^B1n%+Y)P}{YJ>#$lvfp zRZVC8{Q)Xq=FF@D<@w?sQSI7F^}({vFeLXf5+sn#T*;c%CC%awlT*bx>3dfObPUj8$$;wVm$s3|6Hnhr>Obqvp0mdAjkq4T`?0HQ zV(_`1513nMEOzFWRxL@Da(Tl$q0e8ykY2sOV!<>>6O=D!-$OjmO{rN{P#S4|{AcXo zE$DI<(Ef$Dt;53Kw3;J6>-d^6I$_^&36E0kxy!=-09udg6 z%Y7jAR-yd}0dNx}NyH4>WOD|FQS*Ju8~T2K{0K^_50))hx_}26uPc$7Aln*(y0z*M zL9$}ZN6N&JrZ&|1^n0}KQnj8^d&0#jr@Y-ozcGqc?)-veFtCE&Ss}TDN;CEwXdUnV zCEY6GLab&Z-`+0M7E%Ih^wrrBkSba3z76K$;^x`h$YApVFpgR1UGFz2g=O)~rZl-6 zn58YKT}P3uCIna+B#h`%HD!}`^48K}3>d3Td{|p>0Yp04HdTH5>j<`+{o z7I%LAZ1H7Ir-Jmzqo)oFRa=3)GN55|hcO?3A-?_nHo&$O(2-ummsx0vUB;Lj9VV2v zb4HTub&g$lP`6`#w@-?bJa*>JFG}7O1X%;~j20a|YkGLD?{g%h`nq-eQZfR4!G^WS z2#x~?Msww{2hSr3WH?2Q-S9gjHr58V_Z$aF(Qks!(uSnFkE1*Jt5qrWLvi&9s}(zW z?nf$9Ii&W)1>?lR+)Ho!10NBW@b>Z58A4~^H9{s|%_Vs!!GI|4`R(I*y6{yA4rtoX z*T6tQKI&xjjqG9w;XgwS>~}H<@;GLI|7aY`r{G0G?pdm?BIK6t+zWa3{dMvdAJ3`I z=a)ckLVSZU&Q#jx?6mF@$o}>cWP3Hrzm&d5G#g|{teVd9>nMC<>_%5V*>``s54g|& zt7!bXf< z9*VkmD)bI+%9PZc``DQ<#| z6CHh)z3U*+b>^i^4f8c35;^1$E?Dn0w}Sz4vdWr7ZgkT7g8a8F2I~ulF1|dOywR@Y ziFw(&o2S3bt>@i2M;O2hnaQi-m*kYr2SxsWxrpFj3Khl)}i4X^0*PE{m>Cr zR(3&=(}&DMe}&nL3JS5UN>!JND<+>tlRtm$5LQ2Ry(K>ESFIg3@U>c`t(st01^xnb#M2* z;ocE#hf&>e*v4mK^W7i;;T^MKXXQ5w>+N@J*Di80L-z32 zbDM=nZTD9=2wV>}6i|C^eI&EEG)}l{d8&M|wmc-Trx+ivvgUqmRZNso~|Jes!TND5novD$HFLF5j3ej+k3@EN@??i5N_XD8)tBIRIZR*1yL3ZBX0R zDsw-Ijl1ms_%*4lg;k($%%T)BiZH|q)$iB{-Aph`?wuQKo09YXxJsELuE}9V zh!&zETp?Jx9hgG{bCdL|ZS>(+t%8MNkw~$~y~Y*%bKvx9F^CVY&S8fjZ=HkdXiC`| zjO+*GWN;qv^kmVtB#_EM0)lx1nW{`f@^N%;FtGXRyKW+DujxjpJ{F3qd;Ci3Nk`Px z{k$k#$C9T_?$E-07hZe}HV8*K5+z63g(;c_TgWp0zJ~KgBe`|5fQww>M5$#BQf@@J zdT#r|4erQ?g+4B`Z!HhTjl%%Tvgno#<;f!li-*tK%xdimP$K<(Nj9nXlvr#*tF zjiA^qs1WM91$V{5;^{77-I>Tcb>W}5K5}#)u_{5gMNM!}nQ{)m4O3=pS^qA~{@xNi zMF#*17bqM6m?$o@{*r4pt@!IjbKj4EHXorIS-?JPHkU0w~dD6?PX_?2gx9r1tfuJ0l+ko|13*w>#t?zC%;0FFoC8lU9QAY58r zfRF%DqE3`EzFjA{0uN)I(@LgPvgLOwt(-i&cSF*Nxr&bGZ-m z+cU!WL&@$aZmAdstsRLMB1#~vEiJ1!p1aJVjO)($jMV*<_I<PCY#C&=3CXsR4AA!omLx9m07M}tw1(jHA|BAixR07uO_2NQu&Wnfad zIHAEBxoSOP`l#1*2+CcBj=;EUFleqAy{Qa`gk z3%*rjL~cUiN$@4;K<9^gjtWJqVeyJOK)Ai)0-~i_lnSP zC%S6%7vG(v_FMpJ>v6Al>!zK~t+#l=h&5O6h42t@qt5@?IybjcZlD}MblkOG7Ja6{ z<%KoWe9^0P0`X`@4Rv3RT_@)ux4c=-0W^gpi*y8qzCcHE>5DL62B!3@P}qVcgj-%; z9$qFar%#kLz&FG+X~r8qz^?0+fY*=R2q)DL*J~DEfK$}4UBm5~Z?>|EDJ-S+GT6Gt z`cpK659}YuEdImxHEK(eTJasncVYRjyD9F`tI$XxpgGb zZ2bmw^~Tx}=&vIwP7ggjtOMoBno%FK^C(dTXQI}^A#-pH|2JjHE zoJ?gJ;8A{YB&iO4zC-Z)_Cb&Q=BA3*;uF?WuYohx!yUBWoRUBQ^B7!_AN#LMXMlCnK%`;@aYA z!pF6j%nuYaX5sUkPI-M^F1z}i4^li%if5aJcTGkq{JIzpOPJ69_gZWII)OaP1!pjh zANL!V_B7kA@@8*Eti?^lj{?LG%PkD;k^LcDoCPy+E;G1W(~XW63|rnGAau?u%5LF| z(&_>IA4xD7&|pl|OsF-0t8*;?4a)Gfs!K3~{N}ptuk|?tJr~<^`i=BCB>}Zm`FTuT zLJw+S*>jtm+KfjZCa-Umc!a|w2k<%VPlMV`6e43iPB=^jx8!TT)R`Og==mr*WwqDe ztABa@1-s%qz7f>U>w@fS6HcFFRR?Y$-(>(D>lyNw|FzWZ9J!?Mn-IMT78nK&BR0H= zNqN}@cXUwS2c-ljA~#u%natmGTa7nzp3`W-KD!QT#0$ZM0RwyvH;jau7Vpr1<1T&^ z{x-2!(=Bl>Nc)Y0ME4d!3nSrr?sD(1^;*ihy>tVm!M!1PLp`|>-WL`sme%dv;0-2% z(YY^-cCh}8)GD636F9;V^IKG&#Y_?1SBa%;r&ZFI@6bG?*yIQwaCPyzI`8rw6>H#_ zYi4`wg}9JQ*oLLVq0wDt8Xs$`BNMaBBNLe@8@5c1i^@(LcJ%?^u7w!d5ZFe!B0a5m z!m#L{r?_^kbfPY8B4%M?S@xMWeZ9N}#_I^9JHY$hG2_zWw(g?uV7?dokdzS}Q^O9)wBQw(Gs`~i z=M@1Diz=I&QFSM%iAp04niys~_aQaRmpuEIRRyK-;aN8UfjjeQvQEb?X|eUDRdf6t zvTO(SO)l;OCMC%pjp;xEAR) zA}nA56jQ-t`grBZd|uN3N{&DXyG;gwMf>OpeCy~tax zkQYF#jbGU}z^(^r@fMPozX6N`E8(P=_8k(E9f=0eEsP^uI7LJ9M&HGbZ7~VAi)6?1ek Kt& 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 Q_~%E*ZJm!*TKnGmUpnDTZ2$lO From cea3d806ecbec5027a8f0d7f10b700707131a7be Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 16 Mar 2020 00:13:01 +0800 Subject: [PATCH 208/957] Resolve #200, ignore empty conditional format style --- excelize_test.go | 17 ++++++++++++++--- styles.go | 10 +++++++--- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/excelize_test.go b/excelize_test.go index 1ce4fe95a1..815a08d86b 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -978,7 +978,7 @@ func TestConditionalFormat(t *testing.T) { fillCells(f, sheet1, 10, 15) - var format1, format2, format3 int + 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}}`) @@ -998,6 +998,12 @@ func TestConditionalFormat(t *testing.T) { t.FailNow() } + // 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() + } + // 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. @@ -1022,8 +1028,13 @@ func TestConditionalFormat(t *testing.T) { 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. assert.NoError(t, f.SetConditionalFormat(sheet1, "L1:L10", fmt.Sprintf(`[{"type":"formula", "criteria":"L2<3", "format":%d}]`, format1))) - // Test set invalid format set in conditional format + // 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") + // Set conditional format on not exists worksheet. + assert.EqualError(t, f.SetConditionalFormat("SheetN", "L1:L10", "[]"), "sheet SheetN is not exist") err = f.SaveAs(filepath.Join("test", "TestConditionalFormat.xlsx")) if !assert.NoError(t, err) { @@ -1053,7 +1064,7 @@ func TestConditionalFormatError(t *testing.T) { fillCells(f, sheet1, 10, 15) - // Set conditional format with illegal JSON string should return error + // 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() diff --git a/styles.go b/styles.go index 175a17c42b..f2171bbc15 100644 --- a/styles.go +++ b/styles.go @@ -1943,9 +1943,13 @@ func (f *File) NewConditionalStyle(style string) (int, error) { return 0, err } dxf := dxf{ - Fill: setFills(fs, false), - Alignment: setAlignment(fs), - Border: setBorders(fs), + Fill: setFills(fs, false), + } + if fs.Alignment != nil { + dxf.Alignment = setAlignment(fs) + } + if len(fs.Border) > 0 { + dxf.Border = setBorders(fs) } if fs.Font != nil { dxf.Font = f.setFont(fs) From a75c6f63bea6c8e438482cb79e1725f23d7f7f9c Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 25 Mar 2020 00:13:29 +0800 Subject: [PATCH 209/957] #451, init struct for chart sheet --- xmlChartSheet.go | 88 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 xmlChartSheet.go diff --git a/xmlChartSheet.go b/xmlChartSheet.go new file mode 100644 index 0000000000..3417eac0dd --- /dev/null +++ b/xmlChartSheet.go @@ -0,0 +1,88 @@ +// Copyright 2016 - 2020 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. +// +// struct code generated by github.com/xuri/xgen +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.10 or later. + +package excelize + +import "encoding/xml" + +// xlsxChartsheet directly maps the chartsheet element of Chartsheet Parts in +// a SpreadsheetML document. +type xlsxChartsheet struct { + XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main chartsheet"` + SheetPr []*xlsxChartsheetPr `xml:"sheetPr"` + SheetViews []*xlsxChartsheetViews `xml:"sheetViews"` + SheetProtection []*xlsxChartsheetProtection `xml:"sheetProtection"` + CustomSheetViews []*xlsxCustomChartsheetViews `xml:"customSheetViews"` + PageMargins *xlsxPageMargins `xml:"pageMargins"` + PageSetup []*xlsxPageSetUp `xml:"pageSetup"` + HeaderFooter *xlsxHeaderFooter `xml:"headerFooter"` + Drawing []*xlsxDrawing `xml:"drawing"` + DrawingHF []*xlsxDrawingHF `xml:"drawingHF"` + Picture []*xlsxPicture `xml:"picture"` + WebPublishItems []*xlsxInnerXML `xml:"webPublishItems"` + ExtLst []*xlsxExtLst `xml:"extLst"` +} + +// 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"` +} + +// xlsxChartsheetViews specifies chart sheet views. +type xlsxChartsheetViews struct { + XMLName xml.Name `xml:"sheetViews"` + SheetView []*xlsxChartsheetView `xml:"sheetView"` + ExtLst []*xlsxExtLst `xml:"extLst"` +} + +// xlsxChartsheetView defines custom view properties for chart sheets. +type xlsxChartsheetView struct { + XMLName xml.Name `xml:"sheetView"` + TabSelectedAttr bool `xml:"tabSelected,attr,omitempty"` + ZoomScaleAttr uint32 `xml:"zoomScale,attr,omitempty"` + WorkbookViewIdAttr uint32 `xml:"workbookViewId,attr"` + ZoomToFitAttr bool `xml:"zoomToFit,attr,omitempty"` + ExtLst []*xlsxExtLst `xml:"extLst"` +} + +// xlsxChartsheetProtection collection expresses the chart sheet protection +// options to enforce when the chart sheet is protected. +type xlsxChartsheetProtection struct { + XMLName xml.Name `xml:"sheetProtection"` + AlgorithmNameAttr string `xml:"algorithmName,attr,omitempty"` + HashValueAttr []byte `xml:"hashValue,attr,omitempty"` + SaltValueAttr []byte `xml:"saltValue,attr,omitempty"` + SpinCountAttr uint32 `xml:"spinCount,attr,omitempty"` + ContentAttr bool `xml:"content,attr,omitempty"` + ObjectsAttr bool `xml:"objects,attr,omitempty"` +} + +// xlsxCustomChartsheetViews collection of custom Chart Sheet View +// information. +type xlsxCustomChartsheetViews struct { + XMLName xml.Name `xml:"customChartsheetViews"` + CustomSheetView []*xlsxCustomChartsheetView `xml:"customSheetView"` +} + +// xlsxCustomChartsheetView defines custom view properties for chart sheets. +type xlsxCustomChartsheetView struct { + XMLName xml.Name `xml:"customChartsheetView"` + GuidAttr string `xml:"guid,attr"` + ScaleAttr uint32 `xml:"scale,attr,omitempty"` + StateAttr string `xml:"state,attr,omitempty"` + ZoomToFitAttr bool `xml:"zoomToFit,attr,omitempty"` + PageMargins []*xlsxPageMargins `xml:"pageMargins"` + PageSetup []*xlsxPageSetUp `xml:"pageSetup"` + HeaderFooter []*xlsxHeaderFooter `xml:"headerFooter"` +} From 6afc468a025984aa1b265b0228f032c5ed881a3b Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 28 Mar 2020 23:47:26 +0800 Subject: [PATCH 210/957] Resolve #451, support create chart sheet --- LICENSE | 1 - chart.go | 68 +++++++++++++++++++++++++++++++ chart_test.go | 23 ++++++++++- drawing.go | 63 +++++++++++++++++++++++++++++ excelize.go | 23 +++-------- picture.go | 16 ++++---- sheet.go | 16 ++++---- styles.go | 2 +- xmlChartSheet.go | 2 +- xmlDrawing.go | 101 ++++++++++++++++++++++++++++------------------- xmlWorksheet.go | 3 +- 11 files changed, 239 insertions(+), 79 deletions(-) diff --git a/LICENSE b/LICENSE index 51ec1fbebb..fe738b9be1 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,6 @@ BSD 3-Clause License Copyright (c) 2016-2020, 360 Enterprise Security Group, Endpoint Security, Inc. -Copyright (c) 2011-2017, Geoffrey J. Teale All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/chart.go b/chart.go index 69c2c950bb..df196e9a8e 100644 --- a/chart.go +++ b/chart.go @@ -11,7 +11,9 @@ package excelize import ( "encoding/json" + "encoding/xml" "errors" + "fmt" "strconv" "strings" ) @@ -768,6 +770,72 @@ func (f *File) AddChart(sheet, cell, format string, combo ...string) error { return err } +// AddChartSheet provides the method to create a chartsheet by given chart +// 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 { + // Check if the worksheet already exists + if f.GetSheetIndex(sheet) != 0 { + return errors.New("already existing name worksheet") + } + formatSet, err := parseFormatChartSet(format) + if err != nil { + return err + } + comboCharts := []*formatChart{} + for _, comboFormat := range combo { + comboChart, err := parseFormatChartSet(comboFormat) + if err != nil { + return err + } + if _, ok := chartValAxNumFmtFormatCode[comboChart.Type]; !ok { + return errors.New("unsupported chart type " + comboChart.Type) + } + comboCharts = append(comboCharts, comboChart) + } + if _, ok := chartValAxNumFmtFormatCode[formatSet.Type]; !ok { + return errors.New("unsupported chart type " + formatSet.Type) + } + cs := xlsxChartsheet{ + SheetViews: []*xlsxChartsheetViews{{ + SheetView: []*xlsxChartsheetView{{ZoomScaleAttr: 100, ZoomToFitAttr: true}}}, + }, + } + wb := f.workbookReader() + sheetID := 0 + for _, v := range wb.Sheets.Sheet { + if v.SheetID > sheetID { + sheetID = v.SheetID + } + } + sheetID++ + path := "xl/chartsheets/sheet" + strconv.Itoa(sheetID) + ".xml" + f.sheetMap[trimSheetName(sheet)] = path + f.Sheet[path] = nil + drawingID := f.countDrawings() + 1 + chartID := f.countCharts() + 1 + drawingXML := "xl/drawings/drawing" + strconv.Itoa(drawingID) + ".xml" + drawingID, drawingXML = f.prepareChartSheetDrawing(&cs, 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.addSheetDrawingChart(sheet, drawingXML, formatSet.Dimension.Width, formatSet.Dimension.Height, drawingRID, &formatSet.Format) + if err != nil { + return err + } + f.addChart(formatSet, comboCharts) + f.addContentTypePart(chartID, "chart") + f.addContentTypePart(sheetID, "chartsheet") + f.addContentTypePart(drawingID, "drawings") + // Update xl/_rels/workbook.xml.rels + rID := f.addRels("xl/_rels/workbook.xml.rels", SourceRelationshipChartsheet, fmt.Sprintf("chartsheets/sheet%d.xml", sheetID), "") + // Update xl/workbook.xml + f.setWorkbook(sheet, sheetID, rID) + v, _ := xml.Marshal(cs) + f.saveFileList(path, replaceRelationshipsBytes(replaceWorkSheetsRelationshipsNameSpaceBytes(v))) + return err +} + // DeleteChart provides a function to delete chart in XLSX by given worksheet // and cell name. func (f *File) DeleteChart(sheet, cell string) (err error) { diff --git a/chart_test.go b/chart_test.go index 98f3555ad7..351e6638a5 100644 --- a/chart_test.go +++ b/chart_test.go @@ -200,12 +200,31 @@ func TestAddChart(t *testing.T) { assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddChart.xlsx"))) // 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") - // Test add combo chart with invalid format set. + // 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") - // Test add combo chart with unsupported chart type. + // 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") } +func TestAddChartSheet(t *testing.T) { + 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 := NewFile() + for k, v := range categories { + assert.NoError(t, f.SetCellValue("Sheet1", k, v)) + } + 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"}}`)) + + 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"}}`), "already existing name worksheet") + // 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.SaveAs(filepath.Join("test", "TestAddChartSheet.xlsx"))) +} + func TestDeleteChart(t *testing.T) { f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) assert.NoError(t, err) diff --git a/drawing.go b/drawing.go index e51b6afcc9..8ca1f49e5b 100644 --- a/drawing.go +++ b/drawing.go @@ -38,6 +38,26 @@ func (f *File) prepareDrawing(xlsx *xlsxWorksheet, drawingID int, sheet, drawing return drawingID, drawingXML } +// prepareChartSheetDrawing provides a function to prepare drawing ID and XML +// by given drawingID, worksheet name and default drawingXML. +func (f *File) prepareChartSheetDrawing(xlsx *xlsxChartsheet, drawingID int, sheet, drawingXML string) (int, string) { + sheetRelationshipsDrawingXML := "../drawings/drawing" + strconv.Itoa(drawingID) + ".xml" + if xlsx.Drawing != nil { + // The worksheet already has a picture or chart relationships, use the relationships drawing ../drawings/drawing%d.xml. + sheetRelationshipsDrawingXML = f.getSheetRelationshipsTargetByID(sheet, xlsx.Drawing.RID) + drawingID, _ = strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(sheetRelationshipsDrawingXML, "../drawings/drawing"), ".xml")) + drawingXML = strings.Replace(sheetRelationshipsDrawingXML, "..", "xl", -1) + } else { + // Add first picture for given sheet. + sheetRels := "xl/chartsheets/_rels/" + strings.TrimPrefix(f.sheetMap[trimSheetName(sheet)], "xl/chartsheets/") + ".rels" + rID := f.addRels(sheetRels, SourceRelationshipDrawingML, sheetRelationshipsDrawingXML, "") + xlsx.Drawing = &xlsxDrawing{ + RID: "rId" + strconv.Itoa(rID), + } + } + return drawingID, drawingXML +} + // 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) { @@ -1209,6 +1229,49 @@ func (f *File) addDrawingChart(sheet, drawingXML, cell string, width, height, rI return err } +// 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(sheet, drawingXML string, width, height, rID int, formatSet *formatPicture) (err error) { + width = int(float64(width) * formatSet.XScale) + height = int(float64(height) * formatSet.YScale) + + content, cNvPrID := f.drawingParser(drawingXML) + absoluteAnchor := xdrCellAnchor{ + EditAs: formatSet.Positioning, + Pos: &xlsxPoint2D{}, + Ext: &xlsxExt{}, + } + + graphicFrame := xlsxGraphicFrame{ + NvGraphicFramePr: xlsxNvGraphicFramePr{ + CNvPr: &xlsxCNvPr{ + ID: cNvPrID, + Name: "Chart " + strconv.Itoa(cNvPrID), + }, + }, + Graphic: &xlsxGraphic{ + GraphicData: &xlsxGraphicData{ + URI: NameSpaceDrawingMLChart, + Chart: &xlsxChart{ + C: NameSpaceDrawingMLChart, + R: SourceRelationship, + RID: "rId" + strconv.Itoa(rID), + }, + }, + }, + } + graphic, _ := xml.Marshal(graphicFrame) + absoluteAnchor.GraphicFrame = string(graphic) + absoluteAnchor.ClientData = &xdrClientData{ + FLocksWithSheet: formatSet.FLocksWithSheet, + FPrintsWithSheet: formatSet.FPrintsWithSheet, + } + content.AbsoluteAnchor = append(content.AbsoluteAnchor, &absoluteAnchor) + f.Drawings[drawingXML] = content + return err +} + // 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) { diff --git a/excelize.go b/excelize.go index 795120d8ba..3dd43119ab 100644 --- a/excelize.go +++ b/excelize.go @@ -228,21 +228,10 @@ func (f *File) addRels(relPath, relType, target, targetMode string) int { } // replaceWorkSheetsRelationshipsNameSpaceBytes provides a function to replace -// xl/worksheets/sheet%d.xml XML tags to self-closing for compatible Microsoft -// Office Excel 2007. -func replaceWorkSheetsRelationshipsNameSpaceBytes(workbookMarshal []byte) []byte { - var oldXmlns = []byte(``) - var newXmlns = []byte(``) - var newXmlns = []byte(``) + var newXmlns = []byte(templateNamespaceIDMap) contentMarshal = bytes.Replace(contentMarshal, oldXmlns, newXmlns, -1) return contentMarshal } @@ -354,13 +343,13 @@ func (f *File) setContentTypePartVBAProjectExtensions() { } for idx, o := range content.Overrides { if o.PartName == "/xl/workbook.xml" { - content.Overrides[idx].ContentType = "application/vnd.ms-excel.sheet.macroEnabled.main+xml" + content.Overrides[idx].ContentType = ContentTypeMacro } } if !ok { content.Defaults = append(content.Defaults, xlsxDefault{ Extension: "bin", - ContentType: "application/vnd.ms-office.vbaProject", + ContentType: ContentTypeVBA, }) } } diff --git a/picture.go b/picture.go index 3e24ce3a8a..ddc048065b 100644 --- a/picture.go +++ b/picture.go @@ -354,7 +354,7 @@ func (f *File) setContentTypePartVMLExtensions() { if !vml { content.Defaults = append(content.Defaults, xlsxDefault{ Extension: "vml", - ContentType: "application/vnd.openxmlformats-officedocument.vmlDrawing", + ContentType: ContentTypeVML, }) } } @@ -368,6 +368,7 @@ func (f *File) addContentTypePart(index int, contentType string) { } partNames := map[string]string{ "chart": "/xl/charts/chart" + strconv.Itoa(index) + ".xml", + "chartsheet": "/xl/chartsheets/sheet" + strconv.Itoa(index) + ".xml", "comments": "/xl/comments" + strconv.Itoa(index) + ".xml", "drawings": "/xl/drawings/drawing" + strconv.Itoa(index) + ".xml", "table": "/xl/tables/table" + strconv.Itoa(index) + ".xml", @@ -375,12 +376,13 @@ func (f *File) addContentTypePart(index int, contentType string) { "pivotCache": "/xl/pivotCache/pivotCacheDefinition" + strconv.Itoa(index) + ".xml", } contentTypes := map[string]string{ - "chart": "application/vnd.openxmlformats-officedocument.drawingml.chart+xml", - "comments": "application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml", - "drawings": "application/vnd.openxmlformats-officedocument.drawing+xml", - "table": "application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml", - "pivotTable": "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml", - "pivotCache": "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml", + "chart": ContentTypeDrawingML, + "chartsheet": ContentTypeSpreadSheetMLChartsheet, + "comments": ContentTypeSpreadSheetMLComments, + "drawings": ContentTypeDrawing, + "table": ContentTypeSpreadSheetMLTable, + "pivotTable": ContentTypeSpreadSheetMLPivotTable, + "pivotCache": ContentTypeSpreadSheetMLPivotCacheDefinition, } s, ok := setContentType[contentType] if ok { diff --git a/sheet.go b/sheet.go index 08b0e96f3d..11f56d9103 100644 --- a/sheet.go +++ b/sheet.go @@ -50,7 +50,7 @@ func (f *File) NewSheet(name string) int { // Update docProps/app.xml f.setAppXML() // Update [Content_Types].xml - f.setContentTypes(sheetID) + f.setContentTypes("/xl/worksheets/sheet"+strconv.Itoa(sheetID)+".xml", ContentTypeSpreadSheetMLWorksheet) // Create new sheet /xl/worksheets/sheet%d.xml f.setSheet(sheetID, name) // Update xl/_rels/workbook.xml.rels @@ -151,11 +151,11 @@ func trimCell(column []xlsxC) []xlsxC { // setContentTypes provides a function to read and update property of contents // type of XLSX. -func (f *File) setContentTypes(index int) { +func (f *File) setContentTypes(partName, contentType string) { content := f.contentTypesReader() content.Overrides = append(content.Overrides, xlsxOverride{ - PartName: "/xl/worksheets/sheet" + strconv.Itoa(index) + ".xml", - ContentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml", + PartName: partName, + ContentType: contentType, }) } @@ -336,8 +336,8 @@ func (f *File) GetSheetIndex(name string) int { return 0 } -// GetSheetMap provides a function to get worksheet name and index map of XLSX. -// For example: +// GetSheetMap provides a function to get worksheet and chartsheet name and +// index map of XLSX. For example: // // f, err := excelize.OpenFile("Book1.xlsx") // if err != nil { @@ -358,8 +358,8 @@ func (f *File) GetSheetMap() map[int]string { return sheetMap } -// getSheetMap provides a function to get worksheet name and XML file path map -// of XLSX. +// getSheetMap provides a function to get worksheet and chartsheet name and +// XML file path map of XLSX. func (f *File) getSheetMap() map[string]string { content := f.workbookReader() rels := f.relsReader("xl/_rels/workbook.xml.rels") diff --git a/styles.go b/styles.go index f2171bbc15..8d8b464293 100644 --- a/styles.go +++ b/styles.go @@ -1018,7 +1018,7 @@ func (f *File) stylesReader() *xlsxStyleSheet { func (f *File) styleSheetWriter() { if f.Styles != nil { output, _ := xml.Marshal(f.Styles) - f.saveFileList("xl/styles.xml", replaceStyleRelationshipsNameSpaceBytes(output)) + f.saveFileList("xl/styles.xml", replaceWorkSheetsRelationshipsNameSpaceBytes(output)) } } diff --git a/xmlChartSheet.go b/xmlChartSheet.go index 3417eac0dd..fae5a16ea8 100644 --- a/xmlChartSheet.go +++ b/xmlChartSheet.go @@ -24,7 +24,7 @@ type xlsxChartsheet struct { PageMargins *xlsxPageMargins `xml:"pageMargins"` PageSetup []*xlsxPageSetUp `xml:"pageSetup"` HeaderFooter *xlsxHeaderFooter `xml:"headerFooter"` - Drawing []*xlsxDrawing `xml:"drawing"` + Drawing *xlsxDrawing `xml:"drawing"` DrawingHF []*xlsxDrawingHF `xml:"drawingHF"` Picture []*xlsxPicture `xml:"picture"` WebPublishItems []*xlsxInnerXML `xml:"webPublishItems"` diff --git a/xmlDrawing.go b/xmlDrawing.go index 2bad16a0bf..142121d213 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -13,40 +13,52 @@ import "encoding/xml" // Source relationship and namespace. const ( - SourceRelationship = "http://schemas.openxmlformats.org/officeDocument/2006/relationships" - 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" - SourceRelationshipPivotTable = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable" - SourceRelationshipPivotCache = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition" - SourceRelationshipVBAProject = "http://schemas.microsoft.com/office/2006/relationships/vbaProject" - SourceRelationshipChart201506 = "http://schemas.microsoft.com/office/drawing/2015/06/chart" - SourceRelationshipChart20070802 = "http://schemas.microsoft.com/office/drawing/2007/8/2/chart" - SourceRelationshipChart2014 = "http://schemas.microsoft.com/office/drawing/2014/chart" - SourceRelationshipCompatibility = "http://schemas.openxmlformats.org/markup-compatibility/2006" - NameSpaceDrawingML = "http://schemas.openxmlformats.org/drawingml/2006/main" - NameSpaceDrawingMLChart = "http://schemas.openxmlformats.org/drawingml/2006/chart" - NameSpaceDrawingMLSpreadSheet = "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" - NameSpaceSpreadSheet = "http://schemas.openxmlformats.org/spreadsheetml/2006/main" - NameSpaceSpreadSheetX14 = "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main" - NameSpaceSpreadSheetX15 = "http://schemas.microsoft.com/office/spreadsheetml/2010/11/main" - NameSpaceSpreadSheetExcel2006Main = "http://schemas.microsoft.com/office/excel/2006/main" - NameSpaceMacExcel2008Main = "http://schemas.microsoft.com/office/mac/excel/2008/main" - NameSpaceXML = "http://www.w3.org/XML/1998/namespace" - NameSpaceXMLSchemaInstance = "http://www.w3.org/2001/XMLSchema-instance" - 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" - StrictNameSpaceSpreadSheet = "http://purl.oclc.org/ooxml/spreadsheetml/main" - NameSpaceDublinCore = "http://purl.org/dc/elements/1.1/" - NameSpaceDublinCoreTerms = "http://purl.org/dc/terms/" - NameSpaceDublinCoreMetadataIntiative = "http://purl.org/dc/dcmitype/" + SourceRelationship = "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + 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" + SourceRelationshipPivotTable = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable" + SourceRelationshipPivotCache = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition" + SourceRelationshipVBAProject = "http://schemas.microsoft.com/office/2006/relationships/vbaProject" + SourceRelationshipChart201506 = "http://schemas.microsoft.com/office/drawing/2015/06/chart" + SourceRelationshipChart20070802 = "http://schemas.microsoft.com/office/drawing/2007/8/2/chart" + SourceRelationshipChart2014 = "http://schemas.microsoft.com/office/drawing/2014/chart" + SourceRelationshipCompatibility = "http://schemas.openxmlformats.org/markup-compatibility/2006" + NameSpaceDrawingML = "http://schemas.openxmlformats.org/drawingml/2006/main" + NameSpaceDrawingMLChart = "http://schemas.openxmlformats.org/drawingml/2006/chart" + NameSpaceDrawingMLSpreadSheet = "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" + NameSpaceSpreadSheet = "http://schemas.openxmlformats.org/spreadsheetml/2006/main" + NameSpaceSpreadSheetX14 = "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main" + NameSpaceSpreadSheetX15 = "http://schemas.microsoft.com/office/spreadsheetml/2010/11/main" + NameSpaceSpreadSheetExcel2006Main = "http://schemas.microsoft.com/office/excel/2006/main" + NameSpaceMacExcel2008Main = "http://schemas.microsoft.com/office/mac/excel/2008/main" + NameSpaceXML = "http://www.w3.org/XML/1998/namespace" + NameSpaceXMLSchemaInstance = "http://www.w3.org/2001/XMLSchema-instance" + 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" + StrictNameSpaceSpreadSheet = "http://purl.oclc.org/ooxml/spreadsheetml/main" + NameSpaceDublinCore = "http://purl.org/dc/elements/1.1/" + NameSpaceDublinCoreTerms = "http://purl.org/dc/terms/" + NameSpaceDublinCoreMetadataIntiative = "http://purl.org/dc/dcmitype/" + ContentTypeDrawing = "application/vnd.openxmlformats-officedocument.drawing+xml" + ContentTypeDrawingML = "application/vnd.openxmlformats-officedocument.drawingml.chart+xml" + ContentTypeMacro = "application/vnd.ms-excel.sheet.macroEnabled.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" + ContentTypeSpreadSheetMLTable = "application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml" + ContentTypeSpreadSheetMLWorksheet = "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml" + ContentTypeVBA = "application/vnd.ms-office.vbaProject" + ContentTypeVML = "application/vnd.openxmlformats-officedocument.vmlDrawing" // 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 @@ -240,6 +252,7 @@ type xdrClientData struct { // with cells and its extents are in EMU units. type xdrCellAnchor struct { EditAs string `xml:"editAs,attr,omitempty"` + Pos *xlsxPoint2D `xml:"xdr:pos"` From *xlsxFrom `xml:"xdr:from"` To *xlsxTo `xml:"xdr:to"` Ext *xlsxExt `xml:"xdr:ext"` @@ -249,15 +262,23 @@ type xdrCellAnchor struct { ClientData *xdrClientData `xml:"xdr:clientData"` } +// xlsxPoint2D describes the position of a drawing element within a spreadsheet. +type xlsxPoint2D struct { + XMLName xml.Name `xml:"xdr:pos"` + X int `xml:"x,attr"` + Y int `xml:"y,attr"` +} + // xlsxWsDr directly maps the root element for a part of this content type shall // wsDr. type xlsxWsDr struct { - XMLName xml.Name `xml:"xdr:wsDr"` - OneCellAnchor []*xdrCellAnchor `xml:"xdr:oneCellAnchor"` - TwoCellAnchor []*xdrCellAnchor `xml:"xdr:twoCellAnchor"` - A string `xml:"xmlns:a,attr,omitempty"` - Xdr string `xml:"xmlns:xdr,attr,omitempty"` - R string `xml:"xmlns:r,attr,omitempty"` + XMLName xml.Name `xml:"xdr:wsDr"` + AbsoluteAnchor []*xdrCellAnchor `xml:"xdr:absoluteAnchor"` + OneCellAnchor []*xdrCellAnchor `xml:"xdr:oneCellAnchor"` + TwoCellAnchor []*xdrCellAnchor `xml:"xdr:twoCellAnchor"` + A string `xml:"xmlns:a,attr,omitempty"` + Xdr string `xml:"xmlns:xdr,attr,omitempty"` + R string `xml:"xmlns:r,attr,omitempty"` } // xlsxGraphicFrame (Graphic Frame) directly maps the xdr:graphicFrame element. diff --git a/xmlWorksheet.go b/xmlWorksheet.go index aa33819c1f..316ffd783f 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -12,8 +12,7 @@ package excelize import "encoding/xml" // xlsxWorksheet directly maps the worksheet element in the namespace -// http://schemas.openxmlformats.org/spreadsheetml/2006/main - currently I have -// not checked it for completeness - it does as much as I need. +// http://schemas.openxmlformats.org/spreadsheetml/2006/main. type xlsxWorksheet struct { XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main worksheet"` SheetPr *xlsxSheetPr `xml:"sheetPr"` From 3f89c6e9799c9c82af1305f080416c53d19e64c1 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 29 Mar 2020 18:44:24 +0800 Subject: [PATCH 211/957] remove ineffectual variable assignments and simplify code --- LICENSE | 2 +- chart.go | 72 +++++++++++++++++++++++---------------------------- chart_test.go | 18 +++++++++++-- drawing.go | 7 ++--- excelize.go | 8 ++++-- rows.go | 2 +- sheet.go | 35 +++++++++++-------------- styles.go | 2 +- 8 files changed, 75 insertions(+), 71 deletions(-) diff --git a/LICENSE b/LICENSE index fe738b9be1..e0f34bbc6a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 2016-2020, 360 Enterprise Security Group, Endpoint Security, Inc. +Copyright (c) 2016-2020 The excelize Authors. All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/chart.go b/chart.go index df196e9a8e..cae833d24a 100644 --- a/chart.go +++ b/chart.go @@ -730,28 +730,14 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // } // func (f *File) AddChart(sheet, cell, format string, combo ...string) error { - formatSet, err := parseFormatChartSet(format) - if err != nil { - return err - } - comboCharts := []*formatChart{} - for _, comboFormat := range combo { - comboChart, err := parseFormatChartSet(comboFormat) - if err != nil { - return err - } - if _, ok := chartValAxNumFmtFormatCode[comboChart.Type]; !ok { - return errors.New("unsupported chart type " + comboChart.Type) - } - comboCharts = append(comboCharts, comboChart) - } // Read sheet data. xlsx, err := f.workSheetReader(sheet) if err != nil { return err } - if _, ok := chartValAxNumFmtFormatCode[formatSet.Type]; !ok { - return errors.New("unsupported chart type " + formatSet.Type) + formatSet, comboCharts, err := f.getFormatChart(format, combo) + if err != nil { + return err } // Add first picture for given sheet, create xl/drawings/ and xl/drawings/_rels/ folder. drawingID := f.countDrawings() + 1 @@ -777,31 +763,18 @@ func (f *File) AddChart(sheet, cell, format string, combo ...string) error { func (f *File) AddChartSheet(sheet, format string, combo ...string) error { // Check if the worksheet already exists if f.GetSheetIndex(sheet) != 0 { - return errors.New("already existing name worksheet") + return errors.New("the same name worksheet already exists") } - formatSet, err := parseFormatChartSet(format) + formatSet, comboCharts, err := f.getFormatChart(format, combo) if err != nil { return err } - comboCharts := []*formatChart{} - for _, comboFormat := range combo { - comboChart, err := parseFormatChartSet(comboFormat) - if err != nil { - return err - } - if _, ok := chartValAxNumFmtFormatCode[comboChart.Type]; !ok { - return errors.New("unsupported chart type " + comboChart.Type) - } - comboCharts = append(comboCharts, comboChart) - } - if _, ok := chartValAxNumFmtFormatCode[formatSet.Type]; !ok { - return errors.New("unsupported chart type " + formatSet.Type) - } cs := xlsxChartsheet{ SheetViews: []*xlsxChartsheetViews{{ SheetView: []*xlsxChartsheetView{{ZoomScaleAttr: 100, ZoomToFitAttr: true}}}, }, } + f.SheetCount++ wb := f.workbookReader() sheetID := 0 for _, v := range wb.Sheets.Sheet { @@ -819,10 +792,7 @@ func (f *File) AddChartSheet(sheet, format string, combo ...string) error { drawingID, drawingXML = f.prepareChartSheetDrawing(&cs, 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.addSheetDrawingChart(sheet, drawingXML, formatSet.Dimension.Width, formatSet.Dimension.Height, drawingRID, &formatSet.Format) - if err != nil { - return err - } + f.addSheetDrawingChart(drawingXML, drawingRID, &formatSet.Format) f.addChart(formatSet, comboCharts) f.addContentTypePart(chartID, "chart") f.addContentTypePart(sheetID, "chartsheet") @@ -831,11 +801,35 @@ func (f *File) AddChartSheet(sheet, format string, combo ...string) error { rID := f.addRels("xl/_rels/workbook.xml.rels", SourceRelationshipChartsheet, fmt.Sprintf("chartsheets/sheet%d.xml", sheetID), "") // Update xl/workbook.xml f.setWorkbook(sheet, sheetID, rID) - v, _ := xml.Marshal(cs) - f.saveFileList(path, replaceRelationshipsBytes(replaceWorkSheetsRelationshipsNameSpaceBytes(v))) + chartsheet, _ := xml.Marshal(cs) + f.saveFileList(path, replaceRelationshipsBytes(replaceRelationshipsNameSpaceBytes(chartsheet))) return err } +// 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{} + formatSet, err := parseFormatChartSet(format) + if err != nil { + return formatSet, comboCharts, err + } + for _, comboFormat := range combo { + comboChart, err := parseFormatChartSet(comboFormat) + if err != nil { + return formatSet, comboCharts, err + } + if _, ok := chartValAxNumFmtFormatCode[comboChart.Type]; !ok { + return formatSet, comboCharts, errors.New("unsupported chart type " + comboChart.Type) + } + comboCharts = append(comboCharts, comboChart) + } + if _, ok := chartValAxNumFmtFormatCode[formatSet.Type]; !ok { + return formatSet, comboCharts, errors.New("unsupported chart type " + formatSet.Type) + } + return formatSet, comboCharts, err +} + // DeleteChart provides a function to delete chart in XLSX by given worksheet // and cell name. func (f *File) DeleteChart(sheet, cell string) (err error) { diff --git a/chart_test.go b/chart_test.go index 351e6638a5..3b419f0059 100644 --- a/chart_test.go +++ b/chart_test.go @@ -198,6 +198,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 illegal cell coordinates + 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"}`), `cannot convert cell "A" to coordinates: invalid cell name "A"`) // 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") // Test add combo chart with invalid format set @@ -217,8 +219,20 @@ func TestAddChartSheet(t *testing.T) { 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"}}`)) - - 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"}}`), "already existing name worksheet") + // Test set the chartsheet as active sheet + var sheetID int + for idx, sheetName := range f.GetSheetMap() { + if sheetName != "Chart1" { + continue + } + sheetID = idx + } + f.SetActiveSheet(sheetID) + + // Test cell value on chartsheet + assert.EqualError(t, f.SetCellValue("Chart1", "A1", true), "sheet Chart1 is chart sheet") + // 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"}}`), "the same name worksheet already exists") // 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") diff --git a/drawing.go b/drawing.go index 8ca1f49e5b..b291d98673 100644 --- a/drawing.go +++ b/drawing.go @@ -1232,10 +1232,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(sheet, drawingXML string, width, height, rID int, formatSet *formatPicture) (err error) { - width = int(float64(width) * formatSet.XScale) - height = int(float64(height) * formatSet.YScale) - +func (f *File) addSheetDrawingChart(drawingXML string, rID int, formatSet *formatPicture) { content, cNvPrID := f.drawingParser(drawingXML) absoluteAnchor := xdrCellAnchor{ EditAs: formatSet.Positioning, @@ -1269,7 +1266,7 @@ func (f *File) addSheetDrawingChart(sheet, drawingXML string, width, height, rID } content.AbsoluteAnchor = append(content.AbsoluteAnchor, &absoluteAnchor) f.Drawings[drawingXML] = content - return err + return } // deleteDrawing provides a function to delete chart graphic frame by given by diff --git a/excelize.go b/excelize.go index 3dd43119ab..520cbb7146 100644 --- a/excelize.go +++ b/excelize.go @@ -156,6 +156,10 @@ func (f *File) workSheetReader(sheet string) (xlsx *xlsxWorksheet, err error) { return } if xlsx = f.Sheet[name]; f.Sheet[name] == nil { + if strings.HasPrefix(name, "xl/chartsheets") { + err = fmt.Errorf("sheet %s is chart sheet", sheet) + return + } xlsx = new(xlsxWorksheet) if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(name)))). Decode(xlsx); err != nil && err != io.EOF { @@ -227,9 +231,9 @@ func (f *File) addRels(relPath, relType, target, targetMode string) int { return rID } -// replaceWorkSheetsRelationshipsNameSpaceBytes provides a function to replace +// replaceRelationshipsNameSpaceBytes provides a function to replace // XML tags to self-closing for compatible Microsoft Office Excel 2007. -func replaceWorkSheetsRelationshipsNameSpaceBytes(contentMarshal []byte) []byte { +func replaceRelationshipsNameSpaceBytes(contentMarshal []byte) []byte { var oldXmlns = []byte(` xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">`) var newXmlns = []byte(templateNamespaceIDMap) contentMarshal = bytes.Replace(contentMarshal, oldXmlns, newXmlns, -1) diff --git a/rows.go b/rows.go index e00a627ed2..76dd0f0982 100644 --- a/rows.go +++ b/rows.go @@ -174,7 +174,7 @@ func (f *File) Rows(sheet string) (*Rows, error) { if f.Sheet[name] != nil { // flush data output, _ := xml.Marshal(f.Sheet[name]) - f.saveFileList(name, replaceWorkSheetsRelationshipsNameSpaceBytes(output)) + f.saveFileList(name, replaceRelationshipsNameSpaceBytes(output)) } var ( err error diff --git a/sheet.go b/sheet.go index 11f56d9103..6ddd6295e0 100644 --- a/sheet.go +++ b/sheet.go @@ -119,7 +119,7 @@ func (f *File) workSheetWriter() { f.Sheet[p].SheetData.Row[k].C = trimCell(v.C) } output, _ := xml.Marshal(sheet) - f.saveFileList(p, replaceRelationshipsBytes(replaceWorkSheetsRelationshipsNameSpaceBytes(output))) + f.saveFileList(p, replaceRelationshipsBytes(replaceRelationshipsNameSpaceBytes(output))) ok := f.checked[p] if ok { delete(f.Sheet, p) @@ -190,7 +190,7 @@ func (f *File) relsWriter() { if rel != nil { output, _ := xml.Marshal(rel) if strings.HasPrefix(path, "xl/worksheets/sheet/rels/sheet") { - output = replaceWorkSheetsRelationshipsNameSpaceBytes(output) + output = replaceRelationshipsNameSpaceBytes(output) } f.saveFileList(path, replaceRelationshipsBytes(output)) } @@ -211,19 +211,6 @@ func replaceRelationshipsBytes(content []byte) []byte { return bytes.Replace(content, oldXmlns, newXmlns, -1) } -// replaceRelationshipsNameSpaceBytes; Some tools that read XLSX files have -// very strict requirements about the structure of the input XML. In -// particular both Numbers on the Mac and SAS dislike inline XML namespace -// declarations, or namespace prefixes that don't match the ones that Excel -// itself uses. This is a problem because the Go XML library doesn't multiple -// namespace declarations in a single element of a document. This function is -// a horrible hack to fix that after the XML marshalling is completed. -func replaceRelationshipsNameSpaceBytes(workbookMarshal []byte) []byte { - oldXmlns := []byte(``) - newXmlns := []byte(` 0 { - maps[v.Name] = fmt.Sprintf("xl/worksheets/%s", pathInfo[pathInfoLen-1]) + if pathInfoLen > 1 { + maps[v.Name] = fmt.Sprintf("xl/%s", strings.Join(pathInfo[pathInfoLen-2:], "/")) } } } @@ -420,7 +411,10 @@ func (f *File) DeleteSheet(name string) { for _, rel := range wbRels.Relationships { if rel.ID == sheet.ID { sheetXML = fmt.Sprintf("xl/%s", rel.Target) - rels = strings.Replace(fmt.Sprintf("xl/%s.rels", rel.Target), "xl/worksheets/", "xl/worksheets/_rels/", -1) + pathInfo := strings.Split(rel.Target, "/") + if len(pathInfo) == 2 { + rels = fmt.Sprintf("xl/%s/_rels/%s.rels", pathInfo[0], pathInfo[1]) + } } } } @@ -430,6 +424,7 @@ func (f *File) DeleteSheet(name string) { delete(f.sheetMap, sheetName) delete(f.XLSX, sheetXML) delete(f.XLSX, rels) + delete(f.Relationships, rels) delete(f.Sheet, sheetXML) f.SheetCount-- } @@ -729,7 +724,7 @@ func (f *File) SearchSheet(sheet, value string, reg ...bool) ([]string, error) { if f.Sheet[name] != nil { // flush data output, _ := xml.Marshal(f.Sheet[name]) - f.saveFileList(name, replaceWorkSheetsRelationshipsNameSpaceBytes(output)) + f.saveFileList(name, replaceRelationshipsNameSpaceBytes(output)) } return f.searchSheet(name, value, regSearch) } diff --git a/styles.go b/styles.go index 8d8b464293..9cf974e88f 100644 --- a/styles.go +++ b/styles.go @@ -1018,7 +1018,7 @@ func (f *File) stylesReader() *xlsxStyleSheet { func (f *File) styleSheetWriter() { if f.Styles != nil { output, _ := xml.Marshal(f.Styles) - f.saveFileList("xl/styles.xml", replaceWorkSheetsRelationshipsNameSpaceBytes(output)) + f.saveFileList("xl/styles.xml", replaceRelationshipsNameSpaceBytes(output)) } } From 3ce4b91be96589847823b6c1b6c123ba7880310f Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 31 Mar 2020 00:02:00 +0800 Subject: [PATCH 212/957] Resolve #345, fix missing comments by GetComments --- chart.go | 2 +- codelingo.yaml | 3 --- comment.go | 13 +++++++------ comment_test.go | 9 ++++++++- drawing.go | 21 +++++++-------------- drawing_test.go | 27 +++++++++++++++++++++++++++ excelize_test.go | 2 +- 7 files changed, 51 insertions(+), 26 deletions(-) delete mode 100644 codelingo.yaml create mode 100644 drawing_test.go diff --git a/chart.go b/chart.go index cae833d24a..2c802ee892 100644 --- a/chart.go +++ b/chart.go @@ -789,7 +789,7 @@ func (f *File) AddChartSheet(sheet, format string, combo ...string) error { drawingID := f.countDrawings() + 1 chartID := f.countCharts() + 1 drawingXML := "xl/drawings/drawing" + strconv.Itoa(drawingID) + ".xml" - drawingID, drawingXML = f.prepareChartSheetDrawing(&cs, drawingID, sheet, drawingXML) + 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) diff --git a/codelingo.yaml b/codelingo.yaml deleted file mode 100644 index dfe344b471..0000000000 --- a/codelingo.yaml +++ /dev/null @@ -1,3 +0,0 @@ -tenets: - - import: codelingo/effective-go - - import: codelingo/code-review-comments diff --git a/comment.go b/comment.go index a5b6085fb1..610eae8690 100644 --- a/comment.go +++ b/comment.go @@ -16,6 +16,7 @@ import ( "fmt" "io" "log" + "path/filepath" "strconv" "strings" ) @@ -35,8 +36,8 @@ func parseFormatCommentsSet(formatSet string) (*formatComment, error) { // the worksheet comments. func (f *File) GetComments() (comments map[string][]Comment) { comments = map[string][]Comment{} - for n := range f.sheetMap { - if d := f.commentsReader("xl" + strings.TrimPrefix(f.getSheetComments(f.GetSheetIndex(n)), "..")); d != nil { + for n, path := range f.sheetMap { + if d := f.commentsReader("xl" + strings.TrimPrefix(f.getSheetComments(filepath.Base(path)), "..")); d != nil { sheetComments := []Comment{} for _, comment := range d.CommentList.Comment { sheetComment := Comment{} @@ -60,9 +61,9 @@ func (f *File) GetComments() (comments map[string][]Comment) { } // getSheetComments provides the method to get the target comment reference by -// given worksheet index. -func (f *File) getSheetComments(sheetID int) string { - var rels = "xl/worksheets/_rels/sheet" + strconv.Itoa(sheetID) + ".xml.rels" +// given worksheet file path. +func (f *File) getSheetComments(sheetFile string) string { + var rels = "xl/worksheets/_rels/" + sheetFile + ".rels" if sheetRels := f.relsReader(rels); sheetRels != nil { for _, v := range sheetRels.Relationships { if v.Type == SourceRelationshipComments { @@ -107,7 +108,6 @@ func (f *File) AddComment(sheet, cell, format string) error { f.addSheetLegacyDrawing(sheet, rID) } commentsXML := "xl/comments" + strconv.Itoa(commentID) + ".xml" - f.addComment(commentsXML, cell, formatSet) var colCount int for i, l := range strings.Split(formatSet.Text, "\n") { if ll := len(l); ll > colCount { @@ -121,6 +121,7 @@ func (f *File) AddComment(sheet, cell, format string) error { if err != nil { return err } + f.addComment(commentsXML, cell, formatSet) f.addContentTypePart(commentID, "comments") return err } diff --git a/comment_test.go b/comment_test.go index 5b83162c78..955d4e87c4 100644 --- a/comment_test.go +++ b/comment_test.go @@ -29,10 +29,17 @@ 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 is not exist") - + // Test add comment on with illegal cell coordinates + assert.EqualError(t, f.AddComment("Sheet1", "A", `{"author":"Excelize: ","text":"This is a comment."}`), `cannot convert cell "A" to coordinates: invalid cell name "A"`) if assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddComments.xlsx"))) { assert.Len(t, f.GetComments(), 2) } + + f.Comments["xl/comments2.xml"] = nil + f.XLSX["xl/comments2.xml"] = []byte(`Excelize: Excelize: `) + comments := f.GetComments() + assert.EqualValues(t, 2, len(comments["Sheet1"])) + assert.EqualValues(t, 1, len(comments["Sheet2"])) } func TestDecodeVMLDrawingReader(t *testing.T) { diff --git a/drawing.go b/drawing.go index b291d98673..13bdab45fe 100644 --- a/drawing.go +++ b/drawing.go @@ -40,22 +40,15 @@ func (f *File) prepareDrawing(xlsx *xlsxWorksheet, drawingID int, sheet, drawing // prepareChartSheetDrawing provides a function to prepare drawing ID and XML // by given drawingID, worksheet name and default drawingXML. -func (f *File) prepareChartSheetDrawing(xlsx *xlsxChartsheet, drawingID int, sheet, drawingXML string) (int, string) { +func (f *File) prepareChartSheetDrawing(xlsx *xlsxChartsheet, drawingID int, sheet string) { sheetRelationshipsDrawingXML := "../drawings/drawing" + strconv.Itoa(drawingID) + ".xml" - if xlsx.Drawing != nil { - // The worksheet already has a picture or chart relationships, use the relationships drawing ../drawings/drawing%d.xml. - sheetRelationshipsDrawingXML = f.getSheetRelationshipsTargetByID(sheet, xlsx.Drawing.RID) - drawingID, _ = strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(sheetRelationshipsDrawingXML, "../drawings/drawing"), ".xml")) - drawingXML = strings.Replace(sheetRelationshipsDrawingXML, "..", "xl", -1) - } else { - // Add first picture for given sheet. - sheetRels := "xl/chartsheets/_rels/" + strings.TrimPrefix(f.sheetMap[trimSheetName(sheet)], "xl/chartsheets/") + ".rels" - rID := f.addRels(sheetRels, SourceRelationshipDrawingML, sheetRelationshipsDrawingXML, "") - xlsx.Drawing = &xlsxDrawing{ - RID: "rId" + strconv.Itoa(rID), - } + // Only allow one chart in a chartsheet. + sheetRels := "xl/chartsheets/_rels/" + strings.TrimPrefix(f.sheetMap[trimSheetName(sheet)], "xl/chartsheets/") + ".rels" + rID := f.addRels(sheetRels, SourceRelationshipDrawingML, sheetRelationshipsDrawingXML, "") + xlsx.Drawing = &xlsxDrawing{ + RID: "rId" + strconv.Itoa(rID), } - return drawingID, drawingXML + return } // addChart provides a function to create chart as xl/charts/chart%d.xml by diff --git a/drawing_test.go b/drawing_test.go new file mode 100644 index 0000000000..0a380eda39 --- /dev/null +++ b/drawing_test.go @@ -0,0 +1,27 @@ +// Copyright 2016 - 2020 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. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.10 or later. + +package excelize + +import ( + "testing" +) + +func TestDrawingParser(t *testing.T) { + f := File{ + Drawings: make(map[string]*xlsxWsDr), + XLSX: map[string][]byte{ + "charset": MacintoshCyrillicCharset, + "wsDr": []byte(``)}, + } + // Test with one cell anchor + f.drawingParser("wsDr") + // Test with unsupport charset + f.drawingParser("charset") +} diff --git a/excelize_test.go b/excelize_test.go index 815a08d86b..e8d3a307bd 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -934,7 +934,7 @@ func TestCopySheetError(t *testing.T) { func TestGetSheetComments(t *testing.T) { f := NewFile() - assert.Equal(t, "", f.getSheetComments(0)) + assert.Equal(t, "", f.getSheetComments("sheet0")) } func TestSetActiveSheet(t *testing.T) { From 736362694adff47424726a4e4e2569f8247e7fcf Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 1 Apr 2020 00:38:12 +0800 Subject: [PATCH 213/957] Add unit test case --- .travis.yml | 1 - file_test.go | 20 ++++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index d94d5d81f0..92852cf487 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,6 @@ install: - go get -d -t -v ./... && go build -v ./... go: - - 1.10.x - 1.11.x - 1.12.x - 1.13.x diff --git a/file_test.go b/file_test.go index 8c5050cc12..e27b754af1 100644 --- a/file_test.go +++ b/file_test.go @@ -1,7 +1,12 @@ package excelize import ( + "bufio" + "bytes" + "strings" "testing" + + "github.com/stretchr/testify/assert" ) func BenchmarkWrite(b *testing.B) { @@ -26,3 +31,18 @@ func BenchmarkWrite(b *testing.B) { } } } + +func TestWriteTo(t *testing.T) { + f := File{} + buf := bytes.Buffer{} + f.XLSX = make(map[string][]byte, 0) + f.XLSX["/d/"] = []byte("s") + _, err := f.WriteTo(bufio.NewWriter(&buf)) + assert.EqualError(t, err, "zip: write to directory") + delete(f.XLSX, "/d/") + // Test file path overflow + const maxUint16 = 1<<16 - 1 + f.XLSX[strings.Repeat("s", maxUint16+1)] = nil + _, err = f.WriteTo(bufio.NewWriter(&buf)) + assert.EqualError(t, err, "zip: FileHeader.Name too long") +} From 59f6af21a378fdde21422a92b79a7b03bba313d4 Mon Sep 17 00:00:00 2001 From: foxmeder Date: Wed, 1 Apr 2020 15:38:37 +0800 Subject: [PATCH 214/957] fix reading wrong string from xml such as below 0 --- rows.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/rows.go b/rows.go index 76dd0f0982..d56c81c240 100644 --- a/rows.go +++ b/rows.go @@ -301,10 +301,12 @@ func (f *File) sharedStringsReader() *xlsxSST { func (xlsx *xlsxC) getValueFrom(f *File, d *xlsxSST) (string, error) { switch xlsx.T { case "s": - xlsxSI := 0 - xlsxSI, _ = strconv.Atoi(xlsx.V) - if len(d.SI) > xlsxSI { - return f.formattedValue(xlsx.S, d.SI[xlsxSI].String()), nil + if xlsx.V != "" { + xlsxSI := 0 + xlsxSI, _ = strconv.Atoi(xlsx.V) + if len(d.SI) > xlsxSI { + return f.formattedValue(xlsx.S, d.SI[xlsxSI].String()), nil + } } return f.formattedValue(xlsx.S, xlsx.V), nil case "str": From 0f2a9053246c3ae45e6c7ba911a1fb135664abdf Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 2 Apr 2020 00:41:14 +0800 Subject: [PATCH 215/957] Performance improvements --- adjust.go | 5 +++-- drawing.go | 2 +- excelize.go | 5 ++--- lib.go | 42 ++++++++++++++++++++++++++++++++++++++++-- lib_test.go | 5 +++++ picture.go | 2 +- rows.go | 14 ++++++++------ sheet.go | 6 +++--- sparkline.go | 2 +- stream.go | 2 +- 10 files changed, 65 insertions(+), 20 deletions(-) diff --git a/adjust.go b/adjust.go index bedeec0ebd..5056839da3 100644 --- a/adjust.go +++ b/adjust.go @@ -80,9 +80,10 @@ func (f *File) adjustColDimensions(xlsx *xlsxWorksheet, col, offset int) { // adjustRowDimensions provides a function to update row dimensions when // inserting or deleting rows or columns. func (f *File) adjustRowDimensions(xlsx *xlsxWorksheet, row, offset int) { - for i, r := range xlsx.SheetData.Row { + for i := range xlsx.SheetData.Row { + r := &xlsx.SheetData.Row[i] if newRow := r.R + offset; r.R >= row && newRow > 0 { - f.ajustSingleRowDimensions(&xlsx.SheetData.Row[i], newRow) + f.ajustSingleRowDimensions(r, newRow) } } } diff --git a/drawing.go b/drawing.go index 13bdab45fe..e410599396 100644 --- a/drawing.go +++ b/drawing.go @@ -1288,7 +1288,7 @@ func (f *File) deleteDrawing(col, row int, drawingXML, drawingType string) (err } for idx := 0; idx < len(wsDr.TwoCellAnchor); idx++ { deTwoCellAnchor = new(decodeTwoCellAnchor) - if err = f.xmlNewDecoder(bytes.NewReader([]byte("" + wsDr.TwoCellAnchor[idx].GraphicFrame + ""))). + if err = f.xmlNewDecoder(bytes.NewReader(stringToBytes("" + wsDr.TwoCellAnchor[idx].GraphicFrame + ""))). Decode(deTwoCellAnchor); err != nil && err != io.EOF { err = fmt.Errorf("xml decode error: %s", err) return diff --git a/excelize.go b/excelize.go index 520cbb7146..ae011d98c1 100644 --- a/excelize.go +++ b/excelize.go @@ -234,10 +234,9 @@ func (f *File) addRels(relPath, relType, target, targetMode string) int { // replaceRelationshipsNameSpaceBytes provides a function to replace // XML tags to self-closing for compatible Microsoft Office Excel 2007. func replaceRelationshipsNameSpaceBytes(contentMarshal []byte) []byte { - var oldXmlns = []byte(` xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">`) + var oldXmlns = stringToBytes(` xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">`) var newXmlns = []byte(templateNamespaceIDMap) - contentMarshal = bytes.Replace(contentMarshal, oldXmlns, newXmlns, -1) - return contentMarshal + return bytesReplace(contentMarshal, oldXmlns, newXmlns, -1) } // UpdateLinkedValue fix linked values within a spreadsheet are not updating in diff --git a/lib.go b/lib.go index 2d606faee4..83cdb4a28f 100644 --- a/lib.go +++ b/lib.go @@ -17,6 +17,7 @@ import ( "log" "strconv" "strings" + "unsafe" ) // ReadZipReader can be used to read an XLSX in memory without touching the @@ -103,7 +104,7 @@ func JoinCellName(col string, row int) (string, error) { if row < 1 { return "", newInvalidRowNumberError(row) } - return fmt.Sprintf("%s%d", normCol, row), nil + return normCol + strconv.Itoa(row), nil } // ColumnNameToNumber provides a function to convert Excel sheet column name @@ -190,6 +191,7 @@ func CoordinatesToCellName(col, row int) (string, error) { } colname, err := ColumnNumberToName(col) if err != nil { + // Error should never happens here. return "", fmt.Errorf("invalid cell coordinates [%d, %d]: %v", col, row, err) } return fmt.Sprintf("%s%d", colname, row), nil @@ -235,11 +237,47 @@ func namespaceStrictToTransitional(content []byte) []byte { StrictNameSpaceSpreadSheet: NameSpaceSpreadSheet, } for s, n := range namespaceTranslationDic { - content = bytes.Replace(content, []byte(s), []byte(n), -1) + content = bytesReplace(content, stringToBytes(s), stringToBytes(n), -1) } return content } +// stringToBytes cast a string to bytes pointer and assign the value of this +// pointer. +func stringToBytes(s string) []byte { + return *(*[]byte)(unsafe.Pointer(&s)) +} + +// bytesReplace replace old bytes with given new. +func bytesReplace(s, old, new []byte, n int) []byte { + if n == 0 { + return s + } + + if len(old) < len(new) { + return bytes.Replace(s, old, new, n) + } + + if n < 0 { + n = len(s) + } + + var wid, i, j, w int + for i, j = 0, 0; i < len(s) && j < n; j++ { + wid = bytes.Index(s[i:], old) + 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:], s[i:]) + return s[0:w] +} + // genSheetPasswd provides a method to generate password for worksheet // protection by given plaintext. When an Excel sheet is being protected with // a password, a 16-bit (two byte) long hash is generated. To verify a diff --git a/lib_test.go b/lib_test.go index 1c30c0e1cd..4605e707a8 100644 --- a/lib_test.go +++ b/lib_test.go @@ -203,3 +203,8 @@ func TestCoordinatesToCellName_Error(t *testing.T) { } } } + +func TestBytesReplace(t *testing.T) { + s := []byte{0x01} + assert.EqualValues(t, s, bytesReplace(s, []byte{}, []byte{}, 0)) +} diff --git a/picture.go b/picture.go index ddc048065b..fcdaa079cb 100644 --- a/picture.go +++ b/picture.go @@ -510,7 +510,7 @@ func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) err = nil for _, anchor := range deWsDr.TwoCellAnchor { deTwoCellAnchor = new(decodeTwoCellAnchor) - if err = f.xmlNewDecoder(bytes.NewReader([]byte("" + anchor.Content + ""))). + if err = f.xmlNewDecoder(bytes.NewReader(stringToBytes("" + anchor.Content + ""))). Decode(deTwoCellAnchor); err != nil && err != io.EOF { err = fmt.Errorf("xml decode error: %s", err) return diff --git a/rows.go b/rows.go index d56c81c240..8f000b344e 100644 --- a/rows.go +++ b/rows.go @@ -424,14 +424,16 @@ func (f *File) RemoveRow(sheet string, row int) error { if row > len(xlsx.SheetData.Row) { return f.adjustHelper(sheet, rows, row, -1) } - for rowIdx := range xlsx.SheetData.Row { - if xlsx.SheetData.Row[rowIdx].R == row { - xlsx.SheetData.Row = append(xlsx.SheetData.Row[:rowIdx], - xlsx.SheetData.Row[rowIdx+1:]...)[:len(xlsx.SheetData.Row)-1] - return f.adjustHelper(sheet, rows, row, -1) + keep := 0 + for rowIdx := 0; rowIdx < len(xlsx.SheetData.Row); rowIdx++ { + v := &xlsx.SheetData.Row[rowIdx] + if v.R != row { + xlsx.SheetData.Row[keep] = *v + keep++ } } - return nil + xlsx.SheetData.Row = xlsx.SheetData.Row[:keep] + return f.adjustHelper(sheet, rows, row, -1) } // InsertRow provides a function to insert a new row after given Excel row diff --git a/sheet.go b/sheet.go index 6ddd6295e0..a3276c2b81 100644 --- a/sheet.go +++ b/sheet.go @@ -206,9 +206,9 @@ func (f *File) setAppXML() { // 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 bytes.Replace(content, oldXmlns, newXmlns, -1) + oldXmlns := stringToBytes(`xmlns:relationships="http://schemas.openxmlformats.org/officeDocument/2006/relationships" relationships`) + newXmlns := stringToBytes("r") + return bytesReplace(content, oldXmlns, newXmlns, -1) } // SetActiveSheet provides function to set default active worksheet of XLSX by diff --git a/sparkline.go b/sparkline.go index ef99da6771..f1e1f40bd5 100644 --- a/sparkline.go +++ b/sparkline.go @@ -516,7 +516,7 @@ func (f *File) appendSparkline(ws *xlsxWorksheet, group *xlsxX14SparklineGroup, for idx, ext = range decodeExtLst.Ext { if ext.URI == ExtURISparklineGroups { decodeSparklineGroups = new(decodeX14SparklineGroups) - if err = f.xmlNewDecoder(bytes.NewReader([]byte(ext.Content))). + if err = f.xmlNewDecoder(bytes.NewReader(stringToBytes(ext.Content))). Decode(decodeSparklineGroups); err != nil && err != io.EOF { return } diff --git a/stream.go b/stream.go index 98cf82892e..1af0b9f06e 100644 --- a/stream.go +++ b/stream.go @@ -365,7 +365,7 @@ func writeCell(buf *bufferedWriter, c xlsxC) { buf.WriteString(`>`) if c.V != "" { buf.WriteString(``) - xml.EscapeText(buf, []byte(c.V)) + xml.EscapeText(buf, stringToBytes(c.V)) buf.WriteString(``) } buf.WriteString(``) From 66d0272f6af59b5f0c97a304379a795420a43e8b Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 6 Apr 2020 00:23:27 +0800 Subject: [PATCH 216/957] Resolve #172, init rich text support --- cell.go | 166 ++++++++++++++++++++++++++++++++++++++++++++ cell_test.go | 81 +++++++++++++++++++++ comment.go | 8 ++- file.go | 1 + picture.go | 30 ++++---- styles.go | 14 +++- styles_test.go | 2 + xmlDrawing.go | 2 + xmlSharedStrings.go | 59 ++++++++++++---- 9 files changed, 331 insertions(+), 32 deletions(-) diff --git a/cell.go b/cell.go index a65968032f..95cfbbfc04 100644 --- a/cell.go +++ b/cell.go @@ -457,6 +457,172 @@ func (f *File) SetCellHyperLink(sheet, axis, link, linkType string) error { return nil } +// SetCellRichText provides a function to set cell with rich text by given +// worksheet. For example: +// +// package main +// +// import ( +// "fmt" +// +// "github.com/360EntSecGroup-Skylar/excelize" +// ) +// +// 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: "blod", +// 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: " and ", +// Font: &excelize.Font{ +// Size: 14, +// Color: "ad23e8", +// }, +// }, +// { +// Text: "underline.", +// Font: &excelize.Font{ +// Color: "23e833", +// Underline: "single", +// }, +// }, +// }); 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 { + return err + } + cellData, col, _, err := f.prepareCell(ws, sheet, cell) + if err != nil { + return err + } + cellData.S = f.prepareCellStyle(ws, col, cellData.S) + si := xlsxSI{} + sst := f.sharedStringsReader() + textRuns := []xlsxR{} + for _, textRun := range runs { + run := xlsxR{T: &xlsxT{Val: textRun.Text}} + if strings.ContainsAny(textRun.Text, "\r\n ") { + run.T.Space = "preserve" + } + fnt := textRun.Font + if fnt != nil { + rpr := xlsxRPr{} + if fnt.Bold { + rpr.B = " " + } + if fnt.Italic { + rpr.I = " " + } + if fnt.Strike { + rpr.Strike = " " + } + if fnt.Underline != "" { + rpr.U = &attrValString{Val: &fnt.Underline} + } + if fnt.Family != "" { + rpr.RFont = &attrValString{Val: &fnt.Family} + } + if fnt.Size > 0.0 { + rpr.Sz = &attrValFloat{Val: &fnt.Size} + } + if fnt.Color != "" { + rpr.Color = &xlsxColor{RGB: getPaletteColor(fnt.Color)} + } + run.RPr = &rpr + } + textRuns = append(textRuns, run) + } + si.R = textRuns + sst.SI = append(sst.SI, si) + sst.Count++ + sst.UniqueCount++ + cellData.T, cellData.V = "s", strconv.Itoa(len(sst.SI)-1) + f.addContentTypePart(0, "sharedStrings") + rels := f.relsReader("xl/_rels/workbook.xml.rels") + for _, rel := range rels.Relationships { + if rel.Target == "sharedStrings.xml" { + return err + } + } + // Update xl/_rels/workbook.xml.rels + f.addRels("xl/_rels/workbook.xml.rels", SourceRelationshipSharedStrings, "sharedStrings.xml", "") + return err +} + // 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: diff --git a/cell_test.go b/cell_test.go index 1efbc5a871..f46b4b9268 100644 --- a/cell_test.go +++ b/cell_test.go @@ -141,3 +141,84 @@ func TestOverflowNumericCell(t *testing.T) { // GOARCH=amd64 - all ok; GOARCH=386 - actual: "-2147483648" assert.Equal(t, "8595602512225", val, "A1 should be 8595602512225") } + +func TestSetCellRichText(t *testing.T) { + f := NewFile() + assert.NoError(t, f.SetRowHeight("Sheet1", 1, 35)) + assert.NoError(t, f.SetColWidth("Sheet1", "A", "A", 44)) + richTextRun := []RichTextRun{ + { + Text: "blod", + Font: &Font{ + Bold: true, + Color: "2354e8", + Family: "Times New Roman", + }, + }, + { + Text: " and ", + Font: &Font{ + Family: "Times New Roman", + }, + }, + { + Text: "italic ", + Font: &Font{ + Bold: true, + Color: "e83723", + Italic: true, + Family: "Times New Roman", + }, + }, + { + Text: "text with color and font-family,", + Font: &Font{ + Bold: true, + Color: "2354e8", + Family: "Times New Roman", + }, + }, + { + Text: "\r\nlarge text with ", + Font: &Font{ + Size: 14, + Color: "ad23e8", + }, + }, + { + Text: "strike", + Font: &Font{ + Color: "e89923", + Strike: true, + }, + }, + { + Text: " and ", + Font: &Font{ + Size: 14, + Color: "ad23e8", + }, + }, + { + Text: "underline.", + Font: &Font{ + Color: "23e833", + Underline: "single", + }, + }, + } + assert.NoError(t, f.SetCellRichText("Sheet1", "A1", richTextRun)) + assert.NoError(t, f.SetCellRichText("Sheet1", "A2", richTextRun)) + style, err := f.NewStyle(&Style{ + Alignment: &Alignment{ + WrapText: true, + }, + }) + 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 + assert.EqualError(t, f.SetCellRichText("SheetN", "A1", richTextRun), "sheet SheetN is not exist") + // Test set cell rich text with illegal cell coordinates + assert.EqualError(t, f.SetCellRichText("Sheet1", "A", richTextRun), `cannot convert cell "A" to coordinates: invalid cell name "A"`) +} diff --git a/comment.go b/comment.go index 610eae8690..e224502389 100644 --- a/comment.go +++ b/comment.go @@ -50,7 +50,9 @@ func (f *File) GetComments() (comments map[string][]Comment) { sheetComment.Text += *comment.Text.T } for _, text := range comment.Text.R { - sheetComment.Text += text.T + if text.T != nil { + sheetComment.Text += text.T.Val + } } sheetComments = append(sheetComments, sheetComment) } @@ -263,7 +265,7 @@ func (f *File) addComment(commentsXML, cell string, formatSet *formatComment) { RFont: &attrValString{Val: stringPtr(defaultFont)}, Family: &attrValInt{Val: intPtr(2)}, }, - T: a, + T: &xlsxT{Val: a}, }, { RPr: &xlsxRPr{ @@ -274,7 +276,7 @@ func (f *File) addComment(commentsXML, cell string, formatSet *formatComment) { RFont: &attrValString{Val: stringPtr(defaultFont)}, Family: &attrValInt{Val: intPtr(2)}, }, - T: t, + T: &xlsxT{Val: t}, }, }, }, diff --git a/file.go b/file.go index 6213bb169b..8fe4115d9e 100644 --- a/file.go +++ b/file.go @@ -97,6 +97,7 @@ func (f *File) WriteToBuffer() (*bytes.Buffer, error) { f.workBookWriter() f.workSheetWriter() f.relsWriter() + f.sharedStringsWriter() f.styleSheetWriter() for path, content := range f.XLSX { diff --git a/picture.go b/picture.go index fcdaa079cb..a6c0f47a33 100644 --- a/picture.go +++ b/picture.go @@ -367,22 +367,24 @@ func (f *File) addContentTypePart(index int, contentType string) { "drawings": f.setContentTypePartImageExtensions, } partNames := map[string]string{ - "chart": "/xl/charts/chart" + strconv.Itoa(index) + ".xml", - "chartsheet": "/xl/chartsheets/sheet" + strconv.Itoa(index) + ".xml", - "comments": "/xl/comments" + strconv.Itoa(index) + ".xml", - "drawings": "/xl/drawings/drawing" + strconv.Itoa(index) + ".xml", - "table": "/xl/tables/table" + strconv.Itoa(index) + ".xml", - "pivotTable": "/xl/pivotTables/pivotTable" + strconv.Itoa(index) + ".xml", - "pivotCache": "/xl/pivotCache/pivotCacheDefinition" + strconv.Itoa(index) + ".xml", + "chart": "/xl/charts/chart" + strconv.Itoa(index) + ".xml", + "chartsheet": "/xl/chartsheets/sheet" + strconv.Itoa(index) + ".xml", + "comments": "/xl/comments" + strconv.Itoa(index) + ".xml", + "drawings": "/xl/drawings/drawing" + strconv.Itoa(index) + ".xml", + "table": "/xl/tables/table" + strconv.Itoa(index) + ".xml", + "pivotTable": "/xl/pivotTables/pivotTable" + strconv.Itoa(index) + ".xml", + "pivotCache": "/xl/pivotCache/pivotCacheDefinition" + strconv.Itoa(index) + ".xml", + "sharedStrings": "/xl/sharedStrings.xml", } contentTypes := map[string]string{ - "chart": ContentTypeDrawingML, - "chartsheet": ContentTypeSpreadSheetMLChartsheet, - "comments": ContentTypeSpreadSheetMLComments, - "drawings": ContentTypeDrawing, - "table": ContentTypeSpreadSheetMLTable, - "pivotTable": ContentTypeSpreadSheetMLPivotTable, - "pivotCache": ContentTypeSpreadSheetMLPivotCacheDefinition, + "chart": ContentTypeDrawingML, + "chartsheet": ContentTypeSpreadSheetMLChartsheet, + "comments": ContentTypeSpreadSheetMLComments, + "drawings": ContentTypeDrawing, + "table": ContentTypeSpreadSheetMLTable, + "pivotTable": ContentTypeSpreadSheetMLPivotTable, + "pivotCache": ContentTypeSpreadSheetMLPivotCacheDefinition, + "sharedStrings": ContentTypeSpreadSheetMLSharedStrings, } s, ok := setContentType[contentType] if ok { diff --git a/styles.go b/styles.go index 9cf974e88f..fe2bed51a3 100644 --- a/styles.go +++ b/styles.go @@ -13,6 +13,7 @@ import ( "bytes" "encoding/json" "encoding/xml" + "errors" "fmt" "io" "log" @@ -1022,6 +1023,15 @@ func (f *File) styleSheetWriter() { } } +// sharedStringsWriter provides a function to save xl/sharedStrings.xml after +// serialize structure. +func (f *File) sharedStringsWriter() { + if f.SharedStrings != nil { + output, _ := xml.Marshal(f.SharedStrings) + f.saveFileList("xl/sharedStrings.xml", replaceRelationshipsNameSpaceBytes(output)) + } +} + // parseFormatStyleSet provides a function to parse the format settings of the // cells and conditional formats. func parseFormatStyleSet(style string) (*Style, error) { @@ -1033,7 +1043,7 @@ func parseFormatStyleSet(style string) (*Style, error) { } // NewStyle provides a function to create the style for cells by given JSON or -// structure. Note that the color field uses RGB color code. +// structure pointer. Note that the color field uses RGB color code. // // The following shows the border styles sorted by excelize index number: // @@ -1906,6 +1916,8 @@ func (f *File) NewStyle(style interface{}) (int, error) { } case *Style: fs = v + default: + return cellXfsID, errors.New("invalid parameter type") } s := f.stylesReader() numFmtID := setNumFmt(s, fs) diff --git a/styles_test.go b/styles_test.go index 5a9a77155f..5681c95235 100644 --- a/styles_test.go +++ b/styles_test.go @@ -193,6 +193,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, "invalid parameter type") } func TestGetDefaultFont(t *testing.T) { diff --git a/xmlDrawing.go b/xmlDrawing.go index 142121d213..191631b03f 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -25,6 +25,7 @@ const ( SourceRelationshipChartsheet = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartsheet" 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" SourceRelationshipChart201506 = "http://schemas.microsoft.com/office/drawing/2015/06/chart" SourceRelationshipChart20070802 = "http://schemas.microsoft.com/office/drawing/2007/8/2/chart" @@ -55,6 +56,7 @@ const ( 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" ContentTypeVBA = "application/vnd.ms-office.vbaProject" diff --git a/xmlSharedStrings.go b/xmlSharedStrings.go index 61e5727b8a..a6525df177 100644 --- a/xmlSharedStrings.go +++ b/xmlSharedStrings.go @@ -28,31 +28,46 @@ type xlsxSST struct { SI []xlsxSI `xml:"si"` } -// xlsxSI directly maps the si element from the namespace -// http://schemas.openxmlformats.org/spreadsheetml/2006/main - currently I have -// not checked this for completeness - it does as much as I need. +// xlsxSI (String Item) is the representation of an individual string in the +// Shared String table. If the string is just a simple string with formatting +// applied at the cell level, then the String Item (si) should contain a +// single text element used to express the string. However, if the string in +// the cell is more complex - i.e., has formatting applied at the character +// level - then the string item shall consist of multiple rich text runs which +// collectively are used to express the string. type xlsxSI struct { - T string `xml:"t"` + T string `xml:"t,omitempty"` R []xlsxR `xml:"r"` } +// 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 { - rows.WriteString(s.T) + if s.T != nil { + rows.WriteString(s.T.Val) + } } return rows.String() } return x.T } -// xlsxR directly maps the r element from the namespace -// http://schemas.openxmlformats.org/spreadsheetml/2006/main - currently I have -// not checked this for completeness - it does as much as I need. +// xlsxR represents a run of rich text. A rich text run is a region of text +// that share a common set of properties, such as formatting properties. The +// 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 string `xml:"t"` + T *xlsxT `xml:"t"` +} + +// xlsxT directly maps the t element in the run properties. +type xlsxT struct { + XMLName xml.Name `xml:"t"` + Space string `xml:"xml:space,attr,omitempty"` + Val string `xml:",innerxml"` } // xlsxRPr (Run Properties) specifies a set of run properties which shall be @@ -61,9 +76,25 @@ type xlsxR struct { // they are directly applied to the run and supersede any formatting from // styles. type xlsxRPr struct { - B string `xml:"b,omitempty"` - Sz *attrValFloat `xml:"sz"` - Color *xlsxColor `xml:"color"` - RFont *attrValString `xml:"rFont"` - Family *attrValInt `xml:"family"` + RFont *attrValString `xml:"rFont"` + Charset *attrValInt `xml:"charset"` + Family *attrValInt `xml:"family"` + B string `xml:"b,omitempty"` + I string `xml:"i,omitempty"` + Strike string `xml:"strike,omitempty"` + Outline string `xml:"outline,omitempty"` + Shadow string `xml:"shadow,omitempty"` + Condense string `xml:"condense,omitempty"` + Extend string `xml:"extend,omitempty"` + Color *xlsxColor `xml:"color"` + Sz *attrValFloat `xml:"sz"` + U *attrValString `xml:"u"` + VertAlign *attrValString `xml:"vertAlign"` + Scheme *attrValString `xml:"scheme"` +} + +// RichTextRun directly maps the settings of the rich text run. +type RichTextRun struct { + Font *Font + Text string } From 6e90fa6b1d00b2c4ce85e79ee4054ee847fbbc87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Mengu=C3=A9?= Date: Wed, 8 Apr 2020 18:49:13 +0200 Subject: [PATCH 217/957] Replace bytes.NewReader(stringToBytes(s)) with strings.NewReader(s) (#610) --- drawing.go | 2 +- picture.go | 2 +- sparkline.go | 5 ++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/drawing.go b/drawing.go index e410599396..7c09d4db1d 100644 --- a/drawing.go +++ b/drawing.go @@ -1288,7 +1288,7 @@ func (f *File) deleteDrawing(col, row int, drawingXML, drawingType string) (err } for idx := 0; idx < len(wsDr.TwoCellAnchor); idx++ { deTwoCellAnchor = new(decodeTwoCellAnchor) - if err = f.xmlNewDecoder(bytes.NewReader(stringToBytes("" + wsDr.TwoCellAnchor[idx].GraphicFrame + ""))). + 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) return diff --git a/picture.go b/picture.go index a6c0f47a33..306a582230 100644 --- a/picture.go +++ b/picture.go @@ -512,7 +512,7 @@ func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) err = nil for _, anchor := range deWsDr.TwoCellAnchor { deTwoCellAnchor = new(decodeTwoCellAnchor) - if err = f.xmlNewDecoder(bytes.NewReader(stringToBytes("" + anchor.Content + ""))). + if err = f.xmlNewDecoder(strings.NewReader("" + anchor.Content + "")). Decode(deTwoCellAnchor); err != nil && err != io.EOF { err = fmt.Errorf("xml decode error: %s", err) return diff --git a/sparkline.go b/sparkline.go index f1e1f40bd5..ce5be4c32c 100644 --- a/sparkline.go +++ b/sparkline.go @@ -10,7 +10,6 @@ package excelize import ( - "bytes" "encoding/xml" "errors" "io" @@ -509,14 +508,14 @@ func (f *File) appendSparkline(ws *xlsxWorksheet, group *xlsxX14SparklineGroup, sparklineGroupsBytes, sparklineGroupBytes, extLstBytes []byte ) decodeExtLst = new(decodeWorksheetExt) - if err = f.xmlNewDecoder(bytes.NewReader([]byte("" + ws.ExtLst.Ext + ""))). + if err = f.xmlNewDecoder(strings.NewReader("" + ws.ExtLst.Ext + "")). Decode(decodeExtLst); err != nil && err != io.EOF { return } for idx, ext = range decodeExtLst.Ext { if ext.URI == ExtURISparklineGroups { decodeSparklineGroups = new(decodeX14SparklineGroups) - if err = f.xmlNewDecoder(bytes.NewReader(stringToBytes(ext.Content))). + if err = f.xmlNewDecoder(strings.NewReader(ext.Content)). Decode(decodeSparklineGroups); err != nil && err != io.EOF { return } From a2e1da8d9d90ed71a33523cfe2c5231cd0b5fad9 Mon Sep 17 00:00:00 2001 From: echarlus Date: Wed, 8 Apr 2020 18:50:20 +0200 Subject: [PATCH 218/957] Fix for issue #608 (#609) --- styles.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/styles.go b/styles.go index fe2bed51a3..61b8e53446 100644 --- a/styles.go +++ b/styles.go @@ -2732,7 +2732,7 @@ func drawCondFmtCellIs(p int, ct string, format *formatConditional) *xlsxCfRule c.Formula = append(c.Formula, format.Minimum) c.Formula = append(c.Formula, format.Maximum) } - _, ok = map[string]bool{"equal": true, "notEqual": true, "greaterThan": true, "lessThan": true}[ct] + _, 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 { c.Formula = append(c.Formula, format.Value) } From e36650f4ffd3e305d2c3834620f97ec382cf6faf Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 9 Apr 2020 01:00:14 +0800 Subject: [PATCH 219/957] Resolve #598, filter support for AddPivotTable --- cell.go | 3 ++- pivotTable.go | 29 +++++++++++++++++++++++++++++ pivotTable_test.go | 1 + xmlPivotTable.go | 6 +++--- 4 files changed, 35 insertions(+), 4 deletions(-) diff --git a/cell.go b/cell.go index 95cfbbfc04..8e7ede1ccb 100644 --- a/cell.go +++ b/cell.go @@ -458,7 +458,8 @@ func (f *File) SetCellHyperLink(sheet, axis, link, linkType string) error { } // SetCellRichText provides a function to set cell with rich text by given -// worksheet. For example: +// worksheet. For example, set rich text on the A1 cell of the worksheet named +// Sheet1: // // package main // diff --git a/pivotTable.go b/pivotTable.go index b7dc859999..7061bfea38 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -25,6 +25,7 @@ type PivotTableOption struct { Rows []PivotTableField Columns []PivotTableField Data []PivotTableField + Filter []PivotTableField } // PivotTableField directly maps the field settings of the pivot table. @@ -86,6 +87,7 @@ type PivotTableField struct { // DataRange: "Sheet1!$A$1:$E$31", // PivotTableRange: "Sheet1!$G$2:$M$34", // Rows: []excelize.PivotTableField{{Data: "Month"}, {Data: "Year"}}, +// Filter: []excelize.PivotTableField{{Data: "Region"}}, // Columns: []excelize.PivotTableField{{Data: "Type"}}, // Data: []excelize.PivotTableField{{Data: "Sales", Name: "Summarize", Subtotal: "Sum"}}, // }); err != nil { @@ -283,6 +285,7 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op Count: 1, I: []*xlsxI{{}}, }, + PageFields: &xlsxPageFields{}, DataFields: &xlsxDataFields{}, PivotTableStyleInfo: &xlsxPivotTableStyleInfo{ Name: "PivotStyleLight16", @@ -320,6 +323,19 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op return err } + // page fields + pageFieldsIndex, err := f.getPivotFieldsIndex(opt.Filter, opt) + if err != nil { + return err + } + pageFieldsName := f.getPivotTableFieldsName(opt.Filter) + for idx, pageField := range pageFieldsIndex { + pt.PageFields.PageField = append(pt.PageFields.PageField, &xlsxPageField{ + Name: pageFieldsName[idx], + Fld: pageField, + }) + } + // data fields dataFieldsIndex, err := f.getPivotFieldsIndex(opt.Data, opt) if err != nil { @@ -412,6 +428,19 @@ func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opt *PivotTableOptio }) continue } + if inPivotTableField(opt.Filter, name) != -1 { + pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, &xlsxPivotField{ + Axis: "axisPage", + Name: f.getPivotTableFieldName(name, opt.Columns), + Items: &xlsxItems{ + Count: 1, + Item: []*xlsxItem{ + {T: "default"}, + }, + }, + }) + continue + } if inPivotTableField(opt.Columns, name) != -1 { pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, &xlsxPivotField{ Axis: "axisCol", diff --git a/pivotTable_test.go b/pivotTable_test.go index 4379538523..767206b4e0 100644 --- a/pivotTable_test.go +++ b/pivotTable_test.go @@ -29,6 +29,7 @@ func TestAddPivotTable(t *testing.T) { DataRange: "Sheet1!$A$1:$E$31", PivotTableRange: "Sheet1!$G$2:$M$34", Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}}, + Filter: []PivotTableField{{Data: "Region"}}, Columns: []PivotTableField{{Data: "Type"}}, Data: []PivotTableField{{Data: "Sales", Subtotal: "Sum", Name: "Summarize by Sum"}}, })) diff --git a/xmlPivotTable.go b/xmlPivotTable.go index 82bbf27ff6..2eff026514 100644 --- a/xmlPivotTable.go +++ b/xmlPivotTable.go @@ -251,9 +251,9 @@ type xlsxPageFields struct { type xlsxPageField struct { Fld int `xml:"fld,attr"` Item int `xml:"item,attr,omitempty"` - Hier int `xml:"hier,attr"` - Name string `xml:"name,attr"` - Cap string `xml:"cap,attr"` + Hier int `xml:"hier,attr,omitempty"` + Name string `xml:"name,attr,omitempty"` + Cap string `xml:"cap,attr,omitempty"` ExtLst *xlsxExtLst `xml:"extLst"` } From 10115b5d889bb229d8707803b9413b20fe798588 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 10 Apr 2020 00:04:23 +0800 Subject: [PATCH 220/957] - Resolve #611, fix failure BenchmarkSetCellValue - Allow empty filter, data, and rows in the pivot table - Add more test case for pivot table --- cell_test.go | 5 ++-- pivotTable.go | 68 +++++++++++++++++++++++++++++++--------------- pivotTable_test.go | 40 +++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 24 deletions(-) diff --git a/cell_test.go b/cell_test.go index f46b4b9268..45e2f2488d 100644 --- a/cell_test.go +++ b/cell_test.go @@ -3,6 +3,7 @@ package excelize import ( "fmt" "path/filepath" + "strconv" "testing" "time" @@ -122,9 +123,9 @@ func BenchmarkSetCellValue(b *testing.B) { cols := []string{"A", "B", "C", "D", "E", "F"} f := NewFile() b.ResetTimer() - for i := 0; i < b.N; i++ { + for i := 1; i <= b.N; i++ { for j := 0; j < len(values); j++ { - if err := f.SetCellValue("Sheet1", fmt.Sprint(cols[j], i), values[j]); err != nil { + if err := f.SetCellValue("Sheet1", cols[j]+strconv.Itoa(i), values[j]); err != nil { b.Error(err) } } diff --git a/pivotTable.go b/pivotTable.go index 7061bfea38..cf043817c7 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -21,7 +21,6 @@ import ( type PivotTableOption struct { DataRange string PivotTableRange string - Page []PivotTableField Rows []PivotTableField Columns []PivotTableField Data []PivotTableField @@ -194,7 +193,6 @@ func (f *File) adjustRange(rangeStr string) (string, []int, error) { // fields. func (f *File) getPivotFieldsOrder(dataRange string) ([]string, error) { order := []string{} - // data range has been checked dataSheet, coordinates, err := f.adjustRange(dataRange) if err != nil { return order, fmt.Errorf("parameter 'DataRange' parsing error: %s", err.Error()) @@ -217,10 +215,8 @@ func (f *File) addPivotCache(pivotCacheID int, pivotCacheXML string, opt *PivotT if err != nil { return fmt.Errorf("parameter 'DataRange' parsing error: %s", err.Error()) } - order, err := f.getPivotFieldsOrder(opt.DataRange) - if err != nil { - return err - } + // data range has been checked + order, _ := f.getPivotFieldsOrder(opt.DataRange) hcell, _ := CoordinatesToCellName(coordinates[0], coordinates[1]) vcell, _ := CoordinatesToCellName(coordinates[2], coordinates[3]) pc := xlsxPivotCacheDefinition{ @@ -272,7 +268,6 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op FirstHeaderRow: 1, }, PivotFields: &xlsxPivotFields{}, - RowFields: &xlsxRowFields{}, RowItems: &xlsxRowItems{ Count: 1, I: []*xlsxI{ @@ -285,8 +280,6 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op Count: 1, I: []*xlsxI{{}}, }, - PageFields: &xlsxPageFields{}, - DataFields: &xlsxDataFields{}, PivotTableStyleInfo: &xlsxPivotTableStyleInfo{ Name: "PivotStyleLight16", ShowRowHeaders: true, @@ -296,33 +289,49 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op } // pivot fields - err = f.addPivotFields(&pt, opt) - if err != nil { - return err - } + _ = f.addPivotFields(&pt, opt) // 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) + + pivotTable, err := xml.Marshal(pt) + f.saveFileList(pivotTableXML, pivotTable) + return err +} + +// 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 { // row fields rowFieldsIndex, err := f.getPivotFieldsIndex(opt.Rows, opt) if err != nil { return err } for _, fieldIdx := range rowFieldsIndex { + if pt.RowFields == nil { + pt.RowFields = &xlsxRowFields{} + } pt.RowFields.Field = append(pt.RowFields.Field, &xlsxField{ X: fieldIdx, }) } // count row fields - pt.RowFields.Count = len(pt.RowFields.Field) - - err = f.addPivotColFields(&pt, opt) - if err != nil { - return err + if pt.RowFields != nil { + pt.RowFields.Count = len(pt.RowFields.Field) } + return err +} +// 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 { // page fields pageFieldsIndex, err := f.getPivotFieldsIndex(opt.Filter, opt) if err != nil { @@ -330,12 +339,25 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op } pageFieldsName := f.getPivotTableFieldsName(opt.Filter) for idx, pageField := range pageFieldsIndex { + if pt.PageFields == nil { + pt.PageFields = &xlsxPageFields{} + } pt.PageFields.PageField = append(pt.PageFields.PageField, &xlsxPageField{ Name: pageFieldsName[idx], Fld: pageField, }) } + // count page fields + if pt.PageFields != nil { + pt.PageFields.Count = len(pt.PageFields.PageField) + } + return err +} + +// 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 { // data fields dataFieldsIndex, err := f.getPivotFieldsIndex(opt.Data, opt) if err != nil { @@ -344,6 +366,9 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op dataFieldsSubtotals := f.getPivotTableFieldsSubtotal(opt.Data) dataFieldsName := f.getPivotTableFieldsName(opt.Data) for idx, dataField := range dataFieldsIndex { + if pt.DataFields == nil { + pt.DataFields = &xlsxDataFields{} + } pt.DataFields.DataField = append(pt.DataFields.DataField, &xlsxDataField{ Name: dataFieldsName[idx], Fld: dataField, @@ -352,10 +377,9 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op } // count data fields - pt.DataFields.Count = len(pt.DataFields.DataField) - - pivotTable, err := xml.Marshal(pt) - f.saveFileList(pivotTableXML, pivotTable) + if pt.DataFields != nil { + pt.DataFields.Count = len(pt.DataFields.DataField) + } return err } diff --git a/pivotTable_test.go b/pivotTable_test.go index 767206b4e0..cc80835b18 100644 --- a/pivotTable_test.go +++ b/pivotTable_test.go @@ -179,6 +179,46 @@ func TestAddPivotTable(t *testing.T) { 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{ + DataRange: "Sheet1!$A$1:$A$1", + }), `parameter 'DataRange' parsing error: parameter is invalid`) +} + +func TestAddPivotPageFields(t *testing.T) { + f := NewFile() + // Test invalid data range + assert.EqualError(t, f.addPivotPageFields(&xlsxPivotTableDefinition{}, &PivotTableOption{ + DataRange: "Sheet1!$A$1:$A$1", + }), `parameter 'DataRange' parsing error: parameter is invalid`) +} + +func TestAddPivotDataFields(t *testing.T) { + f := NewFile() + // Test invalid data range + assert.EqualError(t, f.addPivotDataFields(&xlsxPivotTableDefinition{}, &PivotTableOption{ + DataRange: "Sheet1!$A$1:$A$1", + }), `parameter 'DataRange' parsing error: parameter is invalid`) +} + +func TestAddPivotColFields(t *testing.T) { + f := NewFile() + // Test invalid data range + assert.EqualError(t, f.addPivotColFields(&xlsxPivotTableDefinition{}, &PivotTableOption{ + DataRange: "Sheet1!$A$1:$A$1", + Columns: []PivotTableField{{Data: "Type"}}, + }), `parameter 'DataRange' parsing error: parameter is invalid`) +} + +func TestGetPivotFieldsOrder(t *testing.T) { + f := NewFile() + // Test get pivot fields order with not exist worksheet + _, err := f.getPivotFieldsOrder("SheetN!$A$1:$E$31") + assert.EqualError(t, err, "sheet SheetN is not exist") +} + func TestInStrSlice(t *testing.T) { assert.EqualValues(t, -1, inStrSlice([]string{}, "")) } From 1fe660df648422a53eef0c735657cb2f5237ef7b Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 23 Apr 2020 02:01:14 +0800 Subject: [PATCH 221/957] - Resolve #485 use sheet index instead of ID - added 3 internal function: getSheetID, getActiveSheetID, getSheetNameByID --- README.md | 9 +++- README_zh.md | 8 ++- cell.go | 2 +- chart.go | 2 +- chart_test.go | 10 ++-- col_test.go | 4 +- excelize.go | 2 +- excelize_test.go | 14 ++--- merge_test.go | 4 +- rows_test.go | 8 +-- sheet.go | 137 ++++++++++++++++++++++++++++++++--------------- sheet_test.go | 8 +-- stream.go | 2 +- 13 files changed, 136 insertions(+), 74 deletions(-) diff --git a/README.md b/README.md index 821bbd7686..b3106df013 100644 --- a/README.md +++ b/README.md @@ -13,8 +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 XLSX files. Supports reading and writing XLSX file generated by Microsoft Excel™ 2007 and later. -Supports saving a file without losing original charts of XLSX. This library needs Go version 1.10 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/360EntSecGroup-Skylar/excelize/v2?tab=doc) 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 XLSX / XLSM / XLTM 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.10 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/360EntSecGroup-Skylar/excelize/v2?tab=doc) and [docs reference](https://xuri.me/excelize/). ## Basic Usage @@ -24,6 +23,12 @@ Supports saving a file without losing original charts of XLSX. This library need go get github.com/360EntSecGroup-Skylar/excelize ``` +- If your package management with [Go Modules](https://blog.golang.org/using-go-modules), please install with following command. + +```bash +go get github.com/360EntSecGroup-Skylar/excelize/v2 +``` + ### Create XLSX file Here is a minimal example usage that will create XLSX file. diff --git a/README_zh.md b/README_zh.md index 18db28ffa7..deba22ac68 100644 --- a/README_zh.md +++ b/README_zh.md @@ -13,7 +13,7 @@ ## 简介 -Excelize 是 Go 语言编写的用于操作 Office Excel 文档类库,基于 ECMA-376 Office OpenXML 标准。可以使用它来读取、写入由 Microsoft Excel™ 2007 及以上版本创建的 XLSX 文档。相比较其他的开源类库,Excelize 支持写入原本带有图片(表)、透视表和切片器等复杂样式的文档,还支持向 Excel 文档中插入图片与图表,并且在保存后不会丢失文档原有样式,可以应用于各类报表系统中。使用本类库要求使用的 Go 语言为 1.10 或更高版本,完整的 API 使用文档请访问 [go.dev](https://pkg.go.dev/github.com/360EntSecGroup-Skylar/excelize/v2?tab=doc) 或查看 [参考文档](https://xuri.me/excelize/)。 +Excelize 是 Go 语言编写的用于操作 Office Excel 文档基础库,基于 ECMA-376,ISO/IEC 29500 国际标准。可以使用它来读取、写入由 Microsoft Excel™ 2007 及以上版本创建的电子表格文档。支持 XLSX / XLSM / XLTM 等多种文档格式,高度兼容带有样式、图片(表)、透视表、切片器等复杂组件的文档,并提供流式读写 API,用于处理包含大规模数据的工作簿。可应用于各类报表平台、云计算、边缘计算等系统。使用本类库要求使用的 Go 语言为 1.10 或更高版本,完整的 API 使用文档请访问 [go.dev](https://pkg.go.dev/github.com/360EntSecGroup-Skylar/excelize/v2?tab=doc) 或查看 [参考文档](https://xuri.me/excelize/)。 ## 快速上手 @@ -23,6 +23,12 @@ Excelize 是 Go 语言编写的用于操作 Office Excel 文档类库,基于 E go get github.com/360EntSecGroup-Skylar/excelize ``` +- 如果您使用 [Go Modules](https://blog.golang.org/using-go-modules) 管理软件包,请使用下面的命令来安装最新版本。 + +```bash +go get github.com/360EntSecGroup-Skylar/excelize/v2 +``` + ### 创建 Excel 文档 下面是一个创建 Excel 文档的简单例子: diff --git a/cell.go b/cell.go index 8e7ede1ccb..a69f4d9b2b 100644 --- a/cell.go +++ b/cell.go @@ -337,7 +337,7 @@ func (f *File) SetCellFormula(sheet, axis, formula string, opts ...FormulaOpts) } if formula == "" { cellData.F = nil - f.deleteCalcChain(f.GetSheetIndex(sheet), axis) + f.deleteCalcChain(f.getSheetID(sheet), axis) return err } diff --git a/chart.go b/chart.go index 2c802ee892..8fa0b5de61 100644 --- a/chart.go +++ b/chart.go @@ -762,7 +762,7 @@ func (f *File) AddChart(sheet, cell, format string, combo ...string) error { // a chart. func (f *File) AddChartSheet(sheet, format string, combo ...string) error { // Check if the worksheet already exists - if f.GetSheetIndex(sheet) != 0 { + if f.GetSheetIndex(sheet) != -1 { return errors.New("the same name worksheet already exists") } formatSet, comboCharts, err := f.getFormatChart(format, combo) diff --git a/chart_test.go b/chart_test.go index 3b419f0059..b35cb98cdd 100644 --- a/chart_test.go +++ b/chart_test.go @@ -12,7 +12,7 @@ import ( func TestChartSize(t *testing.T) { xlsx := NewFile() - sheet1 := xlsx.GetSheetName(1) + sheet1 := xlsx.GetSheetName(0) categories := map[string]string{ "A2": "Small", @@ -220,14 +220,14 @@ func TestAddChartSheet(t *testing.T) { } 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"}}`)) // Test set the chartsheet as active sheet - var sheetID int - for idx, sheetName := range f.GetSheetMap() { + var sheetIdx int + for idx, sheetName := range f.GetSheetList() { if sheetName != "Chart1" { continue } - sheetID = idx + sheetIdx = idx } - f.SetActiveSheet(sheetID) + f.SetActiveSheet(sheetIdx) // Test cell value on chartsheet assert.EqualError(t, f.SetCellValue("Chart1", "A1", true), "sheet Chart1 is chart sheet") diff --git a/col_test.go b/col_test.go index 050c998e6c..fcb1619ec9 100644 --- a/col_test.go +++ b/col_test.go @@ -170,7 +170,7 @@ func TestColWidth(t *testing.T) { func TestInsertCol(t *testing.T) { f := NewFile() - sheet1 := f.GetSheetName(1) + sheet1 := f.GetSheetName(0) fillCells(f, sheet1, 10, 10) @@ -188,7 +188,7 @@ func TestInsertCol(t *testing.T) { func TestRemoveCol(t *testing.T) { f := NewFile() - sheet1 := f.GetSheetName(1) + sheet1 := f.GetSheetName(0) fillCells(f, sheet1, 10, 15) diff --git a/excelize.go b/excelize.go index ae011d98c1..73bc1b5348 100644 --- a/excelize.go +++ b/excelize.go @@ -268,7 +268,7 @@ func (f *File) UpdateLinkedValue() error { wb := f.workbookReader() // recalculate formulas wb.CalcPr = nil - for _, name := range f.GetSheetMap() { + for _, name := range f.GetSheetList() { xlsx, err := f.workSheetReader(name) if err != nil { return err diff --git a/excelize_test.go b/excelize_test.go index e8d3a307bd..8ee8051d1f 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -239,7 +239,7 @@ func TestBrokenFile(t *testing.T) { // 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(2) + f3.SetActiveSheet(1) assert.NoError(t, err) }) @@ -908,7 +908,7 @@ func TestCopySheet(t *testing.T) { } idx := f.NewSheet("CopySheet") - assert.NoError(t, f.CopySheet(1, idx)) + assert.NoError(t, f.CopySheet(0, idx)) assert.NoError(t, f.SetCellValue("CopySheet", "F1", "Hello")) val, err := f.GetCellValue("Sheet1", "F1") @@ -924,8 +924,8 @@ func TestCopySheetError(t *testing.T) { t.FailNow() } - assert.EqualError(t, f.copySheet(0, -1), "sheet is not exist") - if !assert.EqualError(t, f.CopySheet(0, -1), "invalid worksheet index") { + assert.EqualError(t, f.copySheet(-1, -2), "sheet is not exist") + if !assert.EqualError(t, f.CopySheet(-1, -2), "invalid worksheet index") { t.FailNow() } @@ -957,7 +957,7 @@ func TestSetSheetVisible(t *testing.T) { func TestGetActiveSheetIndex(t *testing.T) { f := NewFile() f.WorkBook.BookViews = nil - assert.Equal(t, 1, f.GetActiveSheetIndex()) + assert.Equal(t, 0, f.GetActiveSheetIndex()) } func TestRelsWriter(t *testing.T) { @@ -974,7 +974,7 @@ func TestGetSheetView(t *testing.T) { func TestConditionalFormat(t *testing.T) { f := NewFile() - sheet1 := f.GetSheetName(1) + sheet1 := f.GetSheetName(0) fillCells(f, sheet1, 10, 15) @@ -1060,7 +1060,7 @@ func TestConditionalFormat(t *testing.T) { func TestConditionalFormatError(t *testing.T) { f := NewFile() - sheet1 := f.GetSheetName(1) + sheet1 := f.GetSheetName(0) fillCells(f, sheet1, 10, 15) diff --git a/merge_test.go b/merge_test.go index e880d056c2..afe75aac01 100644 --- a/merge_test.go +++ b/merge_test.go @@ -108,7 +108,7 @@ func TestGetMergeCells(t *testing.T) { if !assert.NoError(t, err) { t.FailNow() } - sheet1 := f.GetSheetName(1) + sheet1 := f.GetSheetName(0) mergeCells, err := f.GetMergeCells(sheet1) if !assert.Len(t, mergeCells, len(wants)) { @@ -132,7 +132,7 @@ func TestUnmergeCell(t *testing.T) { if !assert.NoError(t, err) { t.FailNow() } - sheet1 := f.GetSheetName(1) + sheet1 := f.GetSheetName(0) xlsx, err := f.workSheetReader(sheet1) assert.NoError(t, err) diff --git a/rows_test.go b/rows_test.go index a53b0a9350..e5f0524d07 100644 --- a/rows_test.go +++ b/rows_test.go @@ -92,7 +92,7 @@ func TestRowsError(t *testing.T) { func TestRowHeight(t *testing.T) { xlsx := NewFile() - sheet1 := xlsx.GetSheetName(1) + sheet1 := xlsx.GetSheetName(0) assert.EqualError(t, xlsx.SetRowHeight(sheet1, 0, defaultRowHeightPixels+1.0), "invalid row number 0") @@ -199,7 +199,7 @@ func TestRowVisibility(t *testing.T) { func TestRemoveRow(t *testing.T) { f := NewFile() - sheet1 := f.GetSheetName(1) + sheet1 := f.GetSheetName(0) r, err := f.workSheetReader(sheet1) assert.NoError(t, err) const ( @@ -260,7 +260,7 @@ func TestRemoveRow(t *testing.T) { func TestInsertRow(t *testing.T) { xlsx := NewFile() - sheet1 := xlsx.GetSheetName(1) + sheet1 := xlsx.GetSheetName(0) r, err := xlsx.workSheetReader(sheet1) assert.NoError(t, err) const ( @@ -292,7 +292,7 @@ func TestInsertRow(t *testing.T) { // It is important for insert workflow to be constant to avoid side effect with functions related to internal structure. func TestInsertRowInEmptyFile(t *testing.T) { xlsx := NewFile() - sheet1 := xlsx.GetSheetName(1) + sheet1 := xlsx.GetSheetName(0) r, err := xlsx.workSheetReader(sheet1) assert.NoError(t, err) assert.NoError(t, xlsx.InsertRow(sheet1, 1)) diff --git a/sheet.go b/sheet.go index a3276c2b81..50081e8814 100644 --- a/sheet.go +++ b/sheet.go @@ -34,7 +34,7 @@ import ( // the number of sheets in the workbook (file) after appending the new sheet. func (f *File) NewSheet(name string) int { // Check if the worksheet already exists - if f.GetSheetIndex(name) != 0 { + if f.GetSheetIndex(name) != -1 { return f.SheetCount } f.DeleteSheet(name) @@ -57,7 +57,7 @@ func (f *File) NewSheet(name string) int { rID := f.addRels("xl/_rels/workbook.xml.rels", SourceRelationshipWorkSheet, fmt.Sprintf("worksheets/sheet%d.xml", sheetID), "") // Update xl/workbook.xml f.setWorkbook(name, sheetID, rID) - return sheetID + return f.GetSheetIndex(name) } // contentTypesReader provides a function to get the pointer to the @@ -213,15 +213,15 @@ func replaceRelationshipsBytes(content []byte) []byte { // SetActiveSheet provides function to set default active worksheet of XLSX by // given index. Note that active index is different from the index returned by -// function GetSheetMap(). It should be greater than 0 and less than total -// worksheet numbers. +// function GetSheetMap(). It should be greater or equal to 0 and less than +// total worksheet numbers. func (f *File) SetActiveSheet(index int) { - if index < 1 { - index = 1 + if index < 0 { + index = 0 } wb := f.workbookReader() - for activeTab, sheet := range wb.Sheets.Sheet { - if sheet.SheetID == index { + for activeTab := range wb.Sheets.Sheet { + if activeTab == index { if wb.BookViews == nil { wb.BookViews = &xlsxBookViews{} } @@ -234,7 +234,7 @@ func (f *File) SetActiveSheet(index int) { } } } - for idx, name := range f.GetSheetMap() { + for idx, name := range f.GetSheetList() { xlsx, err := f.workSheetReader(name) if err != nil { // Chartsheet @@ -262,7 +262,22 @@ func (f *File) SetActiveSheet(index int) { // GetActiveSheetIndex provides a function to get active sheet index of the // XLSX. If not found the active sheet will be return integer 0. -func (f *File) GetActiveSheetIndex() int { +func (f *File) GetActiveSheetIndex() (index int) { + var sheetID = f.getActiveSheetID() + wb := f.workbookReader() + if wb != nil { + for idx, sheet := range wb.Sheets.Sheet { + if sheet.SheetID == sheetID { + index = idx + } + } + } + return +} + +// getActiveSheetID provides a function to get active sheet index of the +// XLSX. If not found the active sheet will be return integer 0. +func (f *File) getActiveSheetID() int { wb := f.workbookReader() if wb != nil { if wb.BookViews != nil && len(wb.BookViews.WorkBookView) > 0 { @@ -296,39 +311,62 @@ func (f *File) SetSheetName(oldName, newName string) { } } -// GetSheetName provides a function to get worksheet name of XLSX by given -// worksheet index. If given sheet index is invalid, will return an empty +// getSheetNameByID provides a function to get worksheet name of XLSX by given +// worksheet ID. If given sheet ID is invalid, will return an empty // string. -func (f *File) GetSheetName(index int) string { +func (f *File) getSheetNameByID(ID int) string { wb := f.workbookReader() - if wb == nil || index < 1 { + if wb == nil || ID < 1 { return "" } for _, sheet := range wb.Sheets.Sheet { - if index == sheet.SheetID { + if ID == sheet.SheetID { return sheet.Name } } return "" } +// GetSheetName provides a function to get worksheet name of XLSX by given +// worksheet index. If given sheet index is invalid, will return an empty +// string. +func (f *File) GetSheetName(index int) (name string) { + for idx, sheet := range f.GetSheetList() { + if idx == index { + name = sheet + } + } + return +} + +// getSheetID provides a function to get worksheet ID of XLSX by given +// sheet name. If given worksheet name is invalid, will return an integer type +// value -1. +func (f *File) getSheetID(name string) int { + var ID = -1 + for sheetID, sheet := range f.GetSheetMap() { + if sheet == trimSheetName(name) { + ID = sheetID + } + } + return ID +} + // GetSheetIndex provides a function to get worksheet index of XLSX by given // sheet name. If given worksheet name is invalid, will return an integer type -// value 0. +// value -1. func (f *File) GetSheetIndex(name string) int { - wb := f.workbookReader() - if wb != nil { - for _, sheet := range wb.Sheets.Sheet { - if sheet.Name == trimSheetName(name) { - return sheet.SheetID - } + var idx = -1 + for index, sheet := range f.GetSheetList() { + if sheet == trimSheetName(name) { + idx = index } } - return 0 + return idx } // GetSheetMap provides a function to get worksheet and chartsheet name and -// index map of XLSX. For example: +// ID map of XLSX. For example: // // f, err := excelize.OpenFile("Book1.xlsx") // if err != nil { @@ -349,8 +387,20 @@ func (f *File) GetSheetMap() map[int]string { return sheetMap } -// getSheetMap provides a function to get worksheet and chartsheet name and -// XML file path map of XLSX. +// GetSheetList provides a function to get worksheet and chartsheet name list +// of workbook. +func (f *File) GetSheetList() (list []string) { + wb := f.workbookReader() + if wb != nil { + for _, sheet := range wb.Sheets.Sheet { + list = append(list, sheet.Name) + } + } + return +} + +// getSheetMap provides a function to get worksheet name and XML file path map +// of XLSX. func (f *File) getSheetMap() map[string]string { content := f.workbookReader() rels := f.relsReader("xl/_rels/workbook.xml.rels") @@ -397,7 +447,7 @@ func (f *File) SetSheetBackground(sheet, picture string) error { // 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. func (f *File) DeleteSheet(name string) { - if f.SheetCount == 1 || f.GetSheetIndex(name) == 0 { + if f.SheetCount == 1 || f.GetSheetIndex(name) == -1 { return } sheetName := trimSheetName(name) @@ -474,7 +524,7 @@ func (f *File) deleteSheetFromContentTypes(target string) { // return err // func (f *File) CopySheet(from, to int) error { - if from < 1 || to < 1 || from == to || f.GetSheetName(from) == "" || f.GetSheetName(to) == "" { + if from < 0 || to < 0 || from == to || f.GetSheetName(from) == "" || f.GetSheetName(to) == "" { return errors.New("invalid worksheet index") } return f.copySheet(from, to) @@ -483,12 +533,14 @@ func (f *File) CopySheet(from, to int) error { // copySheet provides a function to duplicate a worksheet by gave source and // target worksheet name. func (f *File) copySheet(from, to int) error { - sheet, err := f.workSheetReader(f.GetSheetName(from)) + fromSheet := f.GetSheetName(from) + sheet, err := f.workSheetReader(fromSheet) if err != nil { return err } worksheet := deepcopy.Copy(sheet).(*xlsxWorksheet) - path := "xl/worksheets/sheet" + strconv.Itoa(to) + ".xml" + toSheetID := strconv.Itoa(f.getSheetID(f.GetSheetName(to))) + path := "xl/worksheets/sheet" + toSheetID + ".xml" if len(worksheet.SheetViews.SheetView) > 0 { worksheet.SheetViews.SheetView[0].TabSelected = false } @@ -496,8 +548,8 @@ func (f *File) copySheet(from, to int) error { worksheet.TableParts = nil worksheet.PageSetUp = nil f.Sheet[path] = worksheet - toRels := "xl/worksheets/_rels/sheet" + strconv.Itoa(to) + ".xml.rels" - fromRels := "xl/worksheets/_rels/sheet" + strconv.Itoa(from) + ".xml.rels" + toRels := "xl/worksheets/_rels/sheet" + toSheetID + ".xml.rels" + fromRels := "xl/worksheets/_rels/sheet" + strconv.Itoa(f.getSheetID(fromSheet)) + ".xml.rels" _, ok := f.XLSX[fromRels] if ok { f.XLSX[toRels] = f.XLSX[fromRels] @@ -1303,7 +1355,7 @@ func (f *File) SetDefinedName(definedName *DefinedName) error { Data: definedName.RefersTo, } if definedName.Scope != "" { - if sheetID := f.GetSheetIndex(definedName.Scope); sheetID != 0 { + if sheetID := f.getSheetID(definedName.Scope); sheetID != 0 { sheetID-- d.LocalSheetID = &sheetID } @@ -1312,7 +1364,7 @@ func (f *File) SetDefinedName(definedName *DefinedName) error { for _, dn := range wb.DefinedNames.DefinedName { var scope string if dn.LocalSheetID != nil { - scope = f.GetSheetName(*dn.LocalSheetID + 1) + scope = f.getSheetNameByID(*dn.LocalSheetID + 1) } if scope == definedName.Scope && dn.Name == definedName.Name { return errors.New("the same name already exists on the scope") @@ -1342,7 +1394,7 @@ func (f *File) DeleteDefinedName(definedName *DefinedName) error { for idx, dn := range wb.DefinedNames.DefinedName { var scope string if dn.LocalSheetID != nil { - scope = f.GetSheetName(*dn.LocalSheetID + 1) + scope = f.getSheetNameByID(*dn.LocalSheetID + 1) } if scope == definedName.Scope && dn.Name == definedName.Name { wb.DefinedNames.DefinedName = append(wb.DefinedNames.DefinedName[:idx], wb.DefinedNames.DefinedName[idx+1:]...) @@ -1367,7 +1419,7 @@ func (f *File) GetDefinedName() []DefinedName { Scope: "Workbook", } if dn.LocalSheetID != nil { - definedName.Scope = f.GetSheetName(*dn.LocalSheetID + 1) + definedName.Scope = f.getSheetNameByID(*dn.LocalSheetID + 1) } definedNames = append(definedNames, definedName) } @@ -1381,7 +1433,7 @@ func (f *File) GroupSheets(sheets []string) error { // check an active worksheet in group worksheets var inActiveSheet bool activeSheet := f.GetActiveSheetIndex() - sheetMap := f.GetSheetMap() + sheetMap := f.GetSheetList() for idx, sheetName := range sheetMap { for _, s := range sheets { if s == sheetName && idx == activeSheet { @@ -1416,16 +1468,15 @@ func (f *File) GroupSheets(sheets []string) error { // UngroupSheets provides a function to ungroup worksheets. func (f *File) UngroupSheets() error { activeSheet := f.GetActiveSheetIndex() - sheetMap := f.GetSheetMap() - for sheetID, sheet := range sheetMap { - if activeSheet == sheetID { + for index, sheet := range f.GetSheetList() { + if activeSheet == index { continue } - xlsx, _ := f.workSheetReader(sheet) - sheetViews := xlsx.SheetViews.SheetView + ws, _ := f.workSheetReader(sheet) + sheetViews := ws.SheetViews.SheetView if len(sheetViews) > 0 { for idx := range sheetViews { - xlsx.SheetViews.SheetView[idx].TabSelected = false + ws.SheetViews.SheetView[idx].TabSelected = false } } } diff --git a/sheet_test.go b/sheet_test.go index 38d86e64e2..0014220a26 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -303,10 +303,10 @@ func TestRemovePageBreak(t *testing.T) { func TestGetSheetName(t *testing.T) { f, _ := excelize.OpenFile(filepath.Join("test", "Book1.xlsx")) - assert.Equal(t, "Sheet1", f.GetSheetName(1)) - assert.Equal(t, "Sheet2", f.GetSheetName(2)) - assert.Equal(t, "", f.GetSheetName(0)) - assert.Equal(t, "", f.GetSheetName(3)) + assert.Equal(t, "Sheet1", f.GetSheetName(0)) + assert.Equal(t, "Sheet2", f.GetSheetName(1)) + assert.Equal(t, "", f.GetSheetName(-1)) + assert.Equal(t, "", f.GetSheetName(2)) } func TestGetSheetMap(t *testing.T) { diff --git a/stream.go b/stream.go index 1af0b9f06e..838751d32c 100644 --- a/stream.go +++ b/stream.go @@ -69,7 +69,7 @@ type StreamWriter struct { // } // func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) { - sheetID := f.GetSheetIndex(sheet) + sheetID := f.getSheetID(sheet) if sheetID == 0 { return nil, fmt.Errorf("sheet %s is not exist", sheet) } From 2285d4dc718fb8b96c3b2291c63b39c57468b0b9 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 24 Apr 2020 08:26:16 +0800 Subject: [PATCH 222/957] handle the cell without r attribute in a row element --- rows.go | 16 ++++++++++++++++ rows_test.go | 11 +++++++++++ sheet.go | 10 +++++----- xmlDrawing.go | 1 + 4 files changed, 33 insertions(+), 5 deletions(-) diff --git a/rows.go b/rows.go index 8f000b344e..6a676727ed 100644 --- a/rows.go +++ b/rows.go @@ -593,6 +593,22 @@ func checkRow(xlsx *xlsxWorksheet) error { if colCount == 0 { continue } + // check and fill the cell without r attribute in a row element + rCount := 0 + for idx, cell := range rowData.C { + rCount++ + if cell.R != "" { + lastR, _, err := CellNameToCoordinates(cell.R) + if err != nil { + return err + } + if lastR > rCount { + rCount = lastR + } + continue + } + rowData.C[idx].R, _ = CoordinatesToCellName(rCount, rowIdx+1) + } lastCol, _, err := CellNameToCoordinates(rowData.C[colCount-1].R) if err != nil { return err diff --git a/rows_test.go b/rows_test.go index e5f0524d07..fd7196d05c 100644 --- a/rows_test.go +++ b/rows_test.go @@ -831,6 +831,17 @@ func TestErrSheetNotExistError(t *testing.T) { assert.EqualValues(t, err.Error(), "sheet Sheet1 is not exist") } +func TestCheckRow(t *testing.T) { + f := NewFile() + f.XLSX["xl/worksheets/sheet1.xml"] = []byte(`12345`) + _, err := f.GetRows("Sheet1") + assert.NoError(t, err) + assert.NoError(t, f.SetCellValue("Sheet1", "A1", false)) + f = NewFile() + f.XLSX["xl/worksheets/sheet1.xml"] = []byte(`12345`) + assert.EqualError(t, f.SetCellValue("Sheet1", "A1", false), `cannot convert cell "-" to coordinates: invalid cell name "-"`) +} + 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 50081e8814..8c7f754677 100644 --- a/sheet.go +++ b/sheet.go @@ -237,7 +237,7 @@ func (f *File) SetActiveSheet(index int) { for idx, name := range f.GetSheetList() { xlsx, err := f.workSheetReader(name) if err != nil { - // Chartsheet + // Chartsheet or dialogsheet return } if xlsx.SheetViews == nil { @@ -365,8 +365,8 @@ func (f *File) GetSheetIndex(name string) int { return idx } -// GetSheetMap provides a function to get worksheet and chartsheet name and -// ID map of XLSX. For example: +// GetSheetMap provides a function to get worksheet, chartsheet and +// dialogsheet ID and name map of XLSX. For example: // // f, err := excelize.OpenFile("Book1.xlsx") // if err != nil { @@ -387,8 +387,8 @@ func (f *File) GetSheetMap() map[int]string { return sheetMap } -// GetSheetList provides a function to get worksheet and chartsheet name list -// of workbook. +// GetSheetList provides a function to get worksheet, chartsheet and +// dialogsheet name list of workbook. func (f *File) GetSheetList() (list []string) { wb := f.workbookReader() if wb != nil { diff --git a/xmlDrawing.go b/xmlDrawing.go index 191631b03f..a5e43a1cbc 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -23,6 +23,7 @@ const ( 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" From 48fc4c08a2a80f7826d20bf3fd5a018f8e6f3185 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 3 May 2020 18:44:43 +0800 Subject: [PATCH 223/957] init formula calculation engine, ref #65 and #599 --- calc.go | 605 +++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 116 +++++++++ excelize.go | 8 +- lib.go | 46 ++++ sheet.go | 28 +-- xmlChartSheet.go | 4 +- 6 files changed, 788 insertions(+), 19 deletions(-) create mode 100644 calc.go create mode 100644 calc_test.go diff --git a/calc.go b/calc.go new file mode 100644 index 0000000000..d962fd4b8c --- /dev/null +++ b/calc.go @@ -0,0 +1,605 @@ +// Copyright 2016 - 2020 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. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX / XLSM / XLTM files. Supports reading and writing +// spreadsheet documents generated by Microsoft Exce™ 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.10 or later. + +package excelize + +import ( + "container/list" + "errors" + "fmt" + "math" + "reflect" + "strconv" + "strings" + + "github.com/xuri/efp" +) + +// Excel formula errors +const ( + formulaErrorDIV = "#DIV/0!" + formulaErrorNAME = "#NAME?" + formulaErrorNA = "#N/A" + formulaErrorNUM = "#NUM!" + formulaErrorVALUE = "#VALUE!" + formulaErrorREF = "#REF!" + formulaErrorNULL = "#NULL" + formulaErrorSPILL = "#SPILL!" + formulaErrorCALC = "#CALC!" + formulaErrorGETTINGDATA = "#GETTING_DATA" +) + +// cellRef defines the structure of a cell reference +type cellRef struct { + Col int + Row int + Sheet string +} + +// cellRef defines the structure of a cell range +type cellRange struct { + From cellRef + To cellRef +} + +type formulaFuncs struct{} + +// CalcCellValue provides a function to get calculated cell value. This +// feature is currently in beta. Array formula, table formula and some other +// formulas are not supported currently. +func (f *File) CalcCellValue(sheet, cell string) (result string, err error) { + var ( + formula string + token efp.Token + ) + 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(sheet, tokens); err != nil { + return + } + result = token.TValue + return +} + +// getPriority calculate arithmetic operator priority. +func getPriority(token efp.Token) (pri int) { + var priority = map[string]int{ + "*": 2, + "/": 2, + "+": 1, + "-": 1, + } + pri, _ = priority[token.TValue] + if token.TValue == "-" && token.TType == efp.TokenTypeOperatorPrefix { + pri = 3 + } + if token.TSubType == efp.TokenSubTypeStart && token.TType == efp.TokenTypeSubexpression { // ( + pri = 0 + } + return +} + +// evalInfixExp evaluate syntax analysis by given infix expression after +// 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 of the operation formula +// +func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error) { + var err error + opdStack, optStack, opfStack, opfdStack, opftStack, argsStack := NewStack(), NewStack(), NewStack(), NewStack(), NewStack(), NewStack() + for i := 0; i < len(tokens); i++ { + token := tokens[i] + + // out of function stack + if opfStack.Len() == 0 { + if err = f.parseToken(sheet, token, opdStack, optStack); err != nil { + return efp.Token{}, err + } + } + + // function start + if token.TType == efp.TokenTypeFunction && token.TSubType == efp.TokenSubTypeStart { + opfStack.Push(token) + continue + } + + // in function stack, walk 2 token at once + if opfStack.Len() > 0 { + var nextToken efp.Token + if i+1 < len(tokens) { + nextToken = tokens[i+1] + } + + // current token is args or range, skip next token, order required: parse reference first + if token.TSubType == efp.TokenSubTypeRange { + if !opftStack.Empty() { + // parse reference: must reference at here + result, err := f.parseReference(sheet, token.TValue) + if err != nil { + return efp.Token{TValue: formulaErrorNAME}, err + } + if len(result) != 1 { + return efp.Token{}, errors.New(formulaErrorVALUE) + } + opfdStack.Push(efp.Token{ + TType: efp.TokenTypeOperand, + TSubType: efp.TokenSubTypeNumber, + TValue: result[0], + }) + continue + } + if nextToken.TType == efp.TokenTypeArgument || nextToken.TType == efp.TokenTypeFunction { + // parse reference: reference or range at here + result, err := f.parseReference(sheet, token.TValue) + if err != nil { + return efp.Token{TValue: formulaErrorNAME}, err + } + for _, val := range result { + argsStack.Push(efp.Token{ + TType: efp.TokenTypeOperand, + TSubType: efp.TokenSubTypeNumber, + TValue: val, + }) + } + if len(result) == 0 { + return efp.Token{}, errors.New(formulaErrorVALUE) + } + continue + } + } + + // check current token is opft + if err = f.parseToken(sheet, token, opfdStack, opftStack); err != nil { + return efp.Token{}, err + } + + // current token is arg + if token.TType == efp.TokenTypeArgument { + for !opftStack.Empty() { + // calculate trigger + topOpt := opftStack.Peek().(efp.Token) + if err := calculate(opfdStack, topOpt); err != nil { + return efp.Token{}, err + } + opftStack.Pop() + } + if !opfdStack.Empty() { + argsStack.Push(opfdStack.Pop()) + } + continue + } + + // current token is function stop + if token.TType == efp.TokenTypeFunction && token.TSubType == efp.TokenSubTypeStop { + for !opftStack.Empty() { + // calculate trigger + topOpt := opftStack.Peek().(efp.Token) + if err := calculate(opfdStack, topOpt); err != nil { + return efp.Token{}, err + } + opftStack.Pop() + } + + // push opfd to args + if opfdStack.Len() > 0 { + argsStack.Push(opfdStack.Pop()) + } + // call formula function to evaluate + result, err := callFuncByName(&formulaFuncs{}, opfStack.Peek().(efp.Token).TValue, []reflect.Value{reflect.ValueOf(argsStack)}) + if err != nil { + return efp.Token{}, err + } + opfStack.Pop() + if opfStack.Len() > 0 { // still in function stack + opfdStack.Push(efp.Token{TValue: result, TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + } else { + opdStack.Push(efp.Token{TValue: result, TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + } + } + } + } + for optStack.Len() != 0 { + topOpt := optStack.Peek().(efp.Token) + if err = calculate(opdStack, topOpt); err != nil { + return efp.Token{}, err + } + optStack.Pop() + } + return opdStack.Peek().(efp.Token), err +} + +// calculate evaluate basic arithmetic operations. +func calculate(opdStack *Stack, opt efp.Token) error { + if opt.TValue == "-" && opt.TType == efp.TokenTypeOperatorPrefix { + 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}) + } + if opt.TValue == "+" { + rOpd := opdStack.Pop().(efp.Token) + lOpd := opdStack.Pop().(efp.Token) + lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64) + if err != nil { + return err + } + rOpdVal, err := strconv.ParseFloat(rOpd.TValue, 64) + if err != nil { + return err + } + result := lOpdVal + rOpdVal + opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + } + if opt.TValue == "-" && opt.TType == efp.TokenTypeOperatorInfix { + rOpd := opdStack.Pop().(efp.Token) + lOpd := opdStack.Pop().(efp.Token) + lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64) + if err != nil { + return err + } + rOpdVal, err := strconv.ParseFloat(rOpd.TValue, 64) + if err != nil { + return err + } + result := lOpdVal - rOpdVal + opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + } + if opt.TValue == "*" { + rOpd := opdStack.Pop().(efp.Token) + lOpd := opdStack.Pop().(efp.Token) + lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64) + if err != nil { + return err + } + rOpdVal, err := strconv.ParseFloat(rOpd.TValue, 64) + if err != nil { + return err + } + result := lOpdVal * rOpdVal + opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + } + if opt.TValue == "/" { + rOpd := opdStack.Pop().(efp.Token) + lOpd := opdStack.Pop().(efp.Token) + lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64) + if err != nil { + return err + } + rOpdVal, err := strconv.ParseFloat(rOpd.TValue, 64) + if err != nil { + return err + } + result := lOpdVal / rOpdVal + if rOpdVal == 0 { + return errors.New(formulaErrorDIV) + } + opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + } + return nil +} + +// 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 { + // parse reference: must reference at here + if token.TSubType == efp.TokenSubTypeRange { + result, err := f.parseReference(sheet, token.TValue) + if err != nil { + return errors.New(formulaErrorNAME) + } + if len(result) != 1 { + return errors.New(formulaErrorVALUE) + } + token.TValue = result[0] + token.TType = efp.TokenTypeOperand + token.TSubType = efp.TokenSubTypeNumber + } + if (token.TValue == "-" && token.TType == efp.TokenTypeOperatorPrefix) || token.TValue == "+" || token.TValue == "-" || token.TValue == "*" || token.TValue == "/" { + 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 err + } + if optStack.Len() > 0 { + topOpt = optStack.Peek().(efp.Token) + topOptPriority = getPriority(topOpt) + continue + } + break + } + optStack.Push(token) + } + } + } + if token.TType == efp.TokenTypeSubexpression && token.TSubType == efp.TokenSubTypeStart { // ( + optStack.Push(token) + } + if token.TType == efp.TokenTypeSubexpression && token.TSubType == efp.TokenSubTypeStop { // ) + for optStack.Peek().(efp.Token).TSubType != efp.TokenSubTypeStart && optStack.Peek().(efp.Token).TType != efp.TokenTypeSubexpression { // != ( + topOpt := optStack.Peek().(efp.Token) + if err := calculate(opdStack, topOpt); err != nil { + return err + } + optStack.Pop() + } + optStack.Pop() + } + // opd + if token.TType == efp.TokenTypeOperand && token.TSubType == efp.TokenSubTypeNumber { + opdStack.Push(token) + } + return nil +} + +// parseReference parse reference and extract values by given reference +// characters and default sheet name. +func (f *File) parseReference(sheet, reference string) (result []string, err error) { + reference = strings.Replace(reference, "$", "", -1) + 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] + if cr.Col, cr.Row, err = CellNameToCoordinates(tokens[1]); err != nil { + return + } + if refs.Len() > 0 { + e := refs.Back() + cellRefs.PushBack(e.Value.(cellRef)) + refs.Remove(e) + } + refs.PushBack(cr) + continue + } + if cr.Col, cr.Row, err = CellNameToCoordinates(tokens[0]); err != nil { + 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) + } + if refs.Len() > 0 { + e := refs.Back() + cellRefs.PushBack(e.Value.(cellRef)) + refs.Remove(e) + } + + result, err = f.rangeResolver(cellRefs, cellRanges) + return +} + +// rangeResolver extract value as string from given reference and range list. +// This function will not ignore the empty cell. Note that the result of 3D +// range references may be different from Excel in some cases, for example, +// A1:A2:A2:B3 in Excel will include B2, but we wont. +func (f *File) rangeResolver(cellRefs, cellRanges *list.List) (result []string, err error) { + filter := map[string]string{} + // extract value from ranges + 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) + for col := rng[0]; col <= rng[2]; col++ { + for row := rng[1]; row <= rng[3]; row++ { + var cell string + if cell, err = CoordinatesToCellName(col, row); err != nil { + return + } + if filter[cell], err = f.GetCellValue(cr.From.Sheet, cell); err != nil { + return + } + } + } + } + // extract value from references + for temp := cellRefs.Front(); temp != nil; temp = temp.Next() { + cr := temp.Value.(cellRef) + var cell string + if cell, err = CoordinatesToCellName(cr.Col, cr.Row); err != nil { + return + } + if filter[cell], err = f.GetCellValue(cr.Sheet, cell); err != nil { + return + } + } + + for _, val := range filter { + result = append(result, val) + } + return +} + +// callFuncByName calls the no error or only error return function with +// reflect by given receiver, name and parameters. +func callFuncByName(receiver interface{}, name string, params []reflect.Value) (result string, err error) { + function := reflect.ValueOf(receiver).MethodByName(name) + if function.IsValid() { + rt := function.Call(params) + if len(rt) == 0 { + return + } + if !rt[1].IsNil() { + err = rt[1].Interface().(error) + return + } + result = rt[0].Interface().(string) + return + } + err = fmt.Errorf("not support %s function", name) + return +} + +// Math and Trigonometric functions + +// 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],...) +// +func (fn *formulaFuncs) SUM(argsStack *Stack) (result string, err error) { + var val float64 + var sum float64 + for !argsStack.Empty() { + token := argsStack.Pop().(efp.Token) + if token.TValue == "" { + continue + } + val, err = strconv.ParseFloat(token.TValue, 64) + if err != nil { + return + } + sum += val + } + result = fmt.Sprintf("%g", sum) + return +} + +// PRODUCT function returns the product (multiplication) of a supplied set of numerical values. +// The syntax of the function is: +// +// PRODUCT(number1,[number2],...) +// +func (fn *formulaFuncs) PRODUCT(argsStack *Stack) (result string, err error) { + var ( + val float64 + product float64 = 1 + ) + for !argsStack.Empty() { + token := argsStack.Pop().(efp.Token) + if token.TValue == "" { + continue + } + val, err = strconv.ParseFloat(token.TValue, 64) + if err != nil { + return + } + product = product * val + } + result = fmt.Sprintf("%g", product) + return +} + +// PRODUCT function calculates a given number, raised to a supplied power. +// The syntax of the function is: +// +// POWER(number,power) +// +func (fn *formulaFuncs) POWER(argsStack *Stack) (result string, err error) { + if argsStack.Len() != 2 { + err = errors.New("POWER requires 2 numeric arguments") + return + } + var x, y float64 + y, err = strconv.ParseFloat(argsStack.Pop().(efp.Token).TValue, 64) + if err != nil { + return + } + x, err = strconv.ParseFloat(argsStack.Pop().(efp.Token).TValue, 64) + if err != nil { + return + } + if x == 0 && y == 0 { + err = errors.New(formulaErrorNUM) + return + } + if x == 0 && y < 0 { + err = errors.New(formulaErrorDIV) + return + } + result = fmt.Sprintf("%g", math.Pow(x, y)) + return +} + +// SQRT function calculates the positive square root of a supplied number. +// The syntax of the function is: +// +// SQRT(number) +// +func (fn *formulaFuncs) SQRT(argsStack *Stack) (result string, err error) { + if argsStack.Len() != 1 { + err = errors.New("SQRT requires 1 numeric arguments") + return + } + var val float64 + val, err = strconv.ParseFloat(argsStack.Pop().(efp.Token).TValue, 64) + if err != nil { + return + } + if val < 0 { + err = errors.New(formulaErrorNUM) + return + } + result = fmt.Sprintf("%g", math.Sqrt(val)) + return +} + +// QUOTIENT function returns the integer portion of a division between two supplied numbers. +// The syntax of the function is: +// +// QUOTIENT(numerator,denominator) +// +func (fn *formulaFuncs) QUOTIENT(argsStack *Stack) (result string, err error) { + if argsStack.Len() != 2 { + err = errors.New("QUOTIENT requires 2 numeric arguments") + return + } + var x, y float64 + y, err = strconv.ParseFloat(argsStack.Pop().(efp.Token).TValue, 64) + if err != nil { + return + } + x, err = strconv.ParseFloat(argsStack.Pop().(efp.Token).TValue, 64) + if err != nil { + return + } + if y == 0 { + err = errors.New(formulaErrorDIV) + return + } + result = fmt.Sprintf("%g", math.Trunc(x/y)) + return +} diff --git a/calc_test.go b/calc_test.go new file mode 100644 index 0000000000..0ebe37e30a --- /dev/null +++ b/calc_test.go @@ -0,0 +1,116 @@ +package excelize + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCalcCellValue(t *testing.T) { + prepareData := func() *File { + f := NewFile() + f.SetCellValue("Sheet1", "A1", 1) + f.SetCellValue("Sheet1", "A2", 2) + f.SetCellValue("Sheet1", "A3", 3) + f.SetCellValue("Sheet1", "A4", 0) + f.SetCellValue("Sheet1", "B1", 4) + return f + } + + mathCalc := map[string]string{ + "=SUM(1,2)": "3", + "=SUM(1,2+3)": "6", + "=SUM(SUM(1,2),2)": "5", + "=(-2-SUM(-4+7))*5": "-25", + "SUM(1,2,3,4,5,6,7)": "28", + "=SUM(1,2)+SUM(1,2)": "6", + "=1+SUM(SUM(1,2*3),4)": "12", + "=1+SUM(SUM(1,-2*3),4)": "0", + "=(-2-SUM(-4*(7+7)))*5": "270", + "=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", + // POWER + "=POWER(4,2)": "16", + // SQRT + "=SQRT(4)": "2", + // QUOTIENT + "=QUOTIENT(5, 2)": "2", + "=QUOTIENT(4.5, 3.1)": "1", + "=QUOTIENT(-10, 3)": "-3", + } + for formula, expected := range mathCalc { + f := prepareData() + assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) + result, err := f.CalcCellValue("Sheet1", "C1") + assert.NoError(t, err) + assert.Equal(t, expected, result) + } + mathCalcError := map[string]string{ + // POWER + "=POWER(0,0)": "#NUM!", + "=POWER(0,-1)": "#DIV/0!", + "=POWER(1)": "POWER requires 2 numeric arguments", + // SQRT + "=SQRT(-1)": "#NUM!", + "=SQRT(1,2)": "SQRT requires 1 numeric arguments", + // QUOTIENT + "=QUOTIENT(1,0)": "#DIV/0!", + "=QUOTIENT(1)": "QUOTIENT requires 2 numeric arguments", + } + for formula, expected := range mathCalcError { + f := prepareData() + assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) + result, err := f.CalcCellValue("Sheet1", "C1") + assert.EqualError(t, err, expected) + assert.Equal(t, "", result) + } + + referenceCalc := map[string]string{ + "=A1/A3": "0.3333333333333333", + "=SUM(A1:A2)": "3", + "=SUM(Sheet1!A1,A2)": "3", + "=(-2-SUM(-4+A2))*5": "0", + "=SUM(Sheet1!A1:Sheet1!A1:A2,A2)": "5", + "=SUM(A1,A2,A3)*SUM(2,3)": "30", + "=1+SUM(SUM(A1+A2/A3)*(2-3),2)": "1.3333333333333335", + "=A1/A2/SUM(A1:A2:B1)": "0.07142857142857142", + "=A1/A2/SUM(A1:A2:B1)*A3": "0.21428571428571427", + // PRODUCT + "=PRODUCT(Sheet1!A1:Sheet1!A1:A2,A2)": "4", + } + for formula, expected := range referenceCalc { + f := prepareData() + assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) + result, err := f.CalcCellValue("Sheet1", "C1") + assert.NoError(t, err) + assert.Equal(t, expected, result) + } + + referenceCalcError := map[string]string{ + "=1+SUM(SUM(A1+A2/A4)*(2-3),2)": "#DIV/0!", + } + for formula, expected := range referenceCalcError { + f := prepareData() + assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) + result, err := f.CalcCellValue("Sheet1", "C1") + assert.EqualError(t, err, expected) + assert.Equal(t, "", result) + } + + // Test get calculated cell value on not formula cell. + f := prepareData() + result, err := f.CalcCellValue("Sheet1", "A1") + assert.NoError(t, err) + assert.Equal(t, "", result) + // Test get calculated cell value on not exists worksheet. + f = prepareData() + _, err = f.CalcCellValue("SheetN", "A1") + assert.EqualError(t, err, "sheet SheetN is not exist") + // Test get calculated cell value with not support formula. + f = prepareData() + assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "=UNSUPPORT(A1)")) + _, err = f.CalcCellValue("Sheet1", "A1") + assert.EqualError(t, err, "not support UNSUPPORT function") +} diff --git a/excelize.go b/excelize.go index 73bc1b5348..04e2e85a36 100644 --- a/excelize.go +++ b/excelize.go @@ -3,9 +3,11 @@ // the LICENSE file. // Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.10 or later. +// and read from XLSX / XLSM / XLTM files. Supports reading and writing +// spreadsheet documents generated by Microsoft Exce™ 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.10 or later. // // See https://xuri.me/excelize for more information about this package. package excelize diff --git a/lib.go b/lib.go index 83cdb4a28f..79c7cd42cf 100644 --- a/lib.go +++ b/lib.go @@ -12,6 +12,7 @@ package excelize import ( "archive/zip" "bytes" + "container/list" "fmt" "io" "log" @@ -305,3 +306,48 @@ func genSheetPasswd(plaintext string) string { password ^= 0xCE4B return strings.ToUpper(strconv.FormatInt(password, 16)) } + +// Stack defined an abstract data type that serves as a collection of elements. +type Stack struct { + list *list.List +} + +// NewStack create a new stack. +func NewStack() *Stack { + list := list.New() + return &Stack{list} +} + +// Push a value onto the top of the stack. +func (stack *Stack) Push(value interface{}) { + stack.list.PushBack(value) +} + +// Pop the top item of the stack and return it. +func (stack *Stack) Pop() interface{} { + e := stack.list.Back() + if e != nil { + stack.list.Remove(e) + return e.Value + } + return nil +} + +// Peek view the top item on the stack. +func (stack *Stack) Peek() interface{} { + e := stack.list.Back() + if e != nil { + return e.Value + } + return nil +} + +// Len return the number of items in the stack. +func (stack *Stack) Len() int { + return stack.list.Len() +} + +// Empty the stack. +func (stack *Stack) Empty() bool { + return stack.list.Len() == 0 +} diff --git a/sheet.go b/sheet.go index 8c7f754677..fa858af22f 100644 --- a/sheet.go +++ b/sheet.go @@ -211,10 +211,10 @@ func replaceRelationshipsBytes(content []byte) []byte { return bytesReplace(content, oldXmlns, newXmlns, -1) } -// SetActiveSheet provides function to set default active worksheet of XLSX by -// given index. Note that active index is different from the index returned by -// function GetSheetMap(). It should be greater or equal to 0 and less than -// total worksheet numbers. +// 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 +// and less than the total worksheet numbers. func (f *File) SetActiveSheet(index int) { if index < 0 { index = 0 @@ -327,9 +327,9 @@ func (f *File) getSheetNameByID(ID int) string { return "" } -// GetSheetName provides a function to get worksheet name of XLSX by given -// worksheet index. If given sheet index is invalid, will return an empty -// string. +// GetSheetName provides a function to get the sheet name of the workbook by +// the given sheet index. If the given sheet index is invalid, it will return +// an empty string. func (f *File) GetSheetName(index int) (name string) { for idx, sheet := range f.GetSheetList() { if idx == index { @@ -352,9 +352,9 @@ func (f *File) getSheetID(name string) int { return ID } -// GetSheetIndex provides a function to get worksheet index of XLSX by given -// sheet name. If given worksheet name is invalid, will return an integer type -// value -1. +// GetSheetIndex provides a function to get a sheet index of the workbook by +// the given sheet name. If the given sheet name is invalid, it will return an +// integer type value -1. func (f *File) GetSheetIndex(name string) int { var idx = -1 for index, sheet := range f.GetSheetList() { @@ -365,8 +365,8 @@ func (f *File) GetSheetIndex(name string) int { return idx } -// GetSheetMap provides a function to get worksheet, chartsheet and -// dialogsheet ID and name map of XLSX. For example: +// 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 { @@ -387,8 +387,8 @@ func (f *File) GetSheetMap() map[int]string { return sheetMap } -// GetSheetList provides a function to get worksheet, chartsheet and -// dialogsheet name list of workbook. +// 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() if wb != nil { diff --git a/xmlChartSheet.go b/xmlChartSheet.go index fae5a16ea8..30a06931fa 100644 --- a/xmlChartSheet.go +++ b/xmlChartSheet.go @@ -51,7 +51,7 @@ type xlsxChartsheetView struct { XMLName xml.Name `xml:"sheetView"` TabSelectedAttr bool `xml:"tabSelected,attr,omitempty"` ZoomScaleAttr uint32 `xml:"zoomScale,attr,omitempty"` - WorkbookViewIdAttr uint32 `xml:"workbookViewId,attr"` + WorkbookViewIDAttr uint32 `xml:"workbookViewId,attr"` ZoomToFitAttr bool `xml:"zoomToFit,attr,omitempty"` ExtLst []*xlsxExtLst `xml:"extLst"` } @@ -78,7 +78,7 @@ type xlsxCustomChartsheetViews struct { // xlsxCustomChartsheetView defines custom view properties for chart sheets. type xlsxCustomChartsheetView struct { XMLName xml.Name `xml:"customChartsheetView"` - GuidAttr string `xml:"guid,attr"` + GUIDAttr string `xml:"guid,attr"` ScaleAttr uint32 `xml:"scale,attr,omitempty"` StateAttr string `xml:"state,attr,omitempty"` ZoomToFitAttr bool `xml:"zoomToFit,attr,omitempty"` From bdf05386408d439fda7cd495105f59cb2eaf8099 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 4 May 2020 13:40:04 +0800 Subject: [PATCH 224/957] fn: ABS, GCD, LCM, POWER, PRODUCT, SIGN, SQRT, SUM, QUOTIENT --- calc.go | 209 +++++++++++++++++++++++++++++++++++++++++++++------ calc_test.go | 51 +++++++++++-- 2 files changed, 233 insertions(+), 27 deletions(-) diff --git a/calc.go b/calc.go index d962fd4b8c..5ebdcf7cf5 100644 --- a/calc.go +++ b/calc.go @@ -412,7 +412,7 @@ func (f *File) parseReference(sheet, reference string) (result []string, err err // rangeResolver extract value as string from given reference and range list. // This function will not ignore the empty cell. Note that the result of 3D // range references may be different from Excel in some cases, for example, -// A1:A2:A2:B3 in Excel will include B2, but we wont. +// A1:A2:A2:B3 in Excel will include B1, but we wont. func (f *File) rangeResolver(cellRefs, cellRanges *list.List) (result []string, err error) { filter := map[string]string{} // extract value from ranges @@ -475,14 +475,58 @@ func callFuncByName(receiver interface{}, name string, params []reflect.Value) ( // Math and Trigonometric functions -// SUM function adds together a supplied set of numbers and returns the sum of -// these values. The syntax of the function is: +// ABS function returns the absolute value of any supplied number. The syntax +// of the function is: // -// SUM(number1,[number2],...) +// ABS(number) // -func (fn *formulaFuncs) SUM(argsStack *Stack) (result string, err error) { +func (fn *formulaFuncs) ABS(argsStack *Stack) (result string, err error) { + if argsStack.Len() != 1 { + err = errors.New("ABS requires 1 numeric arguments") + return + } var val float64 - var sum float64 + val, err = strconv.ParseFloat(argsStack.Pop().(efp.Token).TValue, 64) + if err != nil { + return + } + result = fmt.Sprintf("%g", math.Abs(val)) + return +} + +// gcd returns the greatest common divisor of two supplied integers. +func gcd(x, y float64) float64 { + x, y = math.Trunc(x), math.Trunc(y) + if x == 0 { + return y + } + if y == 0 { + return x + } + for x != y { + if x > y { + x = x - y + } else { + y = y - x + } + } + return x +} + +// GCD function returns the greatest common divisor of two or more supplied +// integers.The syntax of the function is: +// +// GCD(number1,[number2],...) +// +func (fn *formulaFuncs) GCD(argsStack *Stack) (result string, err error) { + if argsStack.Len() == 0 { + err = errors.New("GCD requires at least 1 argument") + return + } + var ( + val float64 + nums = []float64{} + ) for !argsStack.Empty() { token := argsStack.Pop().(efp.Token) if token.TValue == "" { @@ -492,21 +536,51 @@ func (fn *formulaFuncs) SUM(argsStack *Stack) (result string, err error) { if err != nil { return } - sum += val + nums = append(nums, val) } - result = fmt.Sprintf("%g", sum) + if nums[0] < 0 { + err = errors.New("GCD only accepts positive arguments") + return + } + if len(nums) == 1 { + result = fmt.Sprintf("%g", nums[0]) + return + } + cd := nums[0] + for i := 1; i < len(nums); i++ { + if nums[i] < 0 { + err = errors.New("GCD only accepts positive arguments") + return + } + cd = gcd(cd, nums[i]) + } + result = fmt.Sprintf("%g", cd) return } -// PRODUCT function returns the product (multiplication) of a supplied set of numerical values. -// The syntax of the function is: +// lcm returns the least common multiple of two supplied integers. +func lcm(a, b float64) float64 { + a = math.Trunc(a) + b = math.Trunc(b) + if a == 0 && b == 0 { + return 0 + } + return a * b / gcd(a, b) +} + +// LCM function returns the least common multiple of two or more supplied +// integers. The syntax of the function is: // -// PRODUCT(number1,[number2],...) +// LCM(number1,[number2],...) // -func (fn *formulaFuncs) PRODUCT(argsStack *Stack) (result string, err error) { +func (fn *formulaFuncs) LCM(argsStack *Stack) (result string, err error) { + if argsStack.Len() == 0 { + err = errors.New("LCM requires at least 1 argument") + return + } var ( - val float64 - product float64 = 1 + val float64 + nums = []float64{} ) for !argsStack.Empty() { token := argsStack.Pop().(efp.Token) @@ -517,13 +591,29 @@ func (fn *formulaFuncs) PRODUCT(argsStack *Stack) (result string, err error) { if err != nil { return } - product = product * val + nums = append(nums, val) } - result = fmt.Sprintf("%g", product) + if nums[0] < 0 { + err = errors.New("LCM only accepts positive arguments") + return + } + if len(nums) == 1 { + result = fmt.Sprintf("%g", nums[0]) + return + } + cm := nums[0] + for i := 1; i < len(nums); i++ { + if nums[i] < 0 { + err = errors.New("LCM only accepts positive arguments") + return + } + cm = lcm(cm, nums[i]) + } + result = fmt.Sprintf("%g", cm) return } -// PRODUCT function calculates a given number, raised to a supplied power. +// POWER function calculates a given number, raised to a supplied power. // The syntax of the function is: // // POWER(number,power) @@ -554,8 +644,62 @@ func (fn *formulaFuncs) POWER(argsStack *Stack) (result string, err error) { return } -// SQRT function calculates the positive square root of a supplied number. -// The syntax of the function is: +// PRODUCT function returns the product (multiplication) of a supplied set of +// numerical values. The syntax of the function is: +// +// PRODUCT(number1,[number2],...) +// +func (fn *formulaFuncs) PRODUCT(argsStack *Stack) (result string, err error) { + var ( + val float64 + product float64 = 1 + ) + for !argsStack.Empty() { + token := argsStack.Pop().(efp.Token) + if token.TValue == "" { + continue + } + val, err = strconv.ParseFloat(token.TValue, 64) + if err != nil { + return + } + product = product * val + } + result = fmt.Sprintf("%g", product) + return +} + +// 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 +// (zero), the function returns 0. The syntax of the function is: +// +// SIGN(number) +// +func (fn *formulaFuncs) SIGN(argsStack *Stack) (result string, err error) { + if argsStack.Len() != 1 { + err = errors.New("SIGN requires 1 numeric arguments") + return + } + var val float64 + val, err = strconv.ParseFloat(argsStack.Pop().(efp.Token).TValue, 64) + if err != nil { + return + } + if val < 0 { + result = "-1" + return + } + if val > 0 { + result = "1" + return + } + result = "0" + return +} + +// SQRT function calculates the positive square root of a supplied number. The +// syntax of the function is: // // SQRT(number) // @@ -577,8 +721,31 @@ func (fn *formulaFuncs) SQRT(argsStack *Stack) (result string, err error) { return } -// QUOTIENT function returns the integer portion of a division between two supplied numbers. -// The syntax of the function is: +// 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],...) +// +func (fn *formulaFuncs) SUM(argsStack *Stack) (result string, err error) { + var val float64 + var sum float64 + for !argsStack.Empty() { + token := argsStack.Pop().(efp.Token) + if token.TValue == "" { + continue + } + val, err = strconv.ParseFloat(token.TValue, 64) + if err != nil { + return + } + sum += val + } + result = fmt.Sprintf("%g", sum) + return +} + +// QUOTIENT function returns the integer portion of a division between two +// supplied numbers. The syntax of the function is: // // QUOTIENT(numerator,denominator) // diff --git a/calc_test.go b/calc_test.go index 0ebe37e30a..84fa955d86 100644 --- a/calc_test.go +++ b/calc_test.go @@ -18,6 +18,35 @@ func TestCalcCellValue(t *testing.T) { } mathCalc := map[string]string{ + // ABS + "=ABS(-1)": "1", + "=ABS(-6.5)": "6.5", + "=ABS(6.5)": "6.5", + "=ABS(0)": "0", + "=ABS(2-4.5)": "2.5", + // GCD + "=GCD(1,5)": "1", + "=GCD(15,10,25)": "5", + "=GCD(0,8,12)": "4", + "=GCD(7,2)": "1", + // LCM + "=LCM(1,5)": "5", + "=LCM(15,10,25)": "150", + "=LCM(1,8,12)": "24", + "=LCM(7,2)": "14", + // POWER + "=POWER(4,2)": "16", + // PRODUCT + "=PRODUCT(3,6)": "18", + // SIGN + "=SIGN(9.5)": "1", + "=SIGN(-9.5)": "-1", + "=SIGN(0)": "0", + "=SIGN(0.00000001)": "1", + "=SIGN(6-7)": "-1", + // SQRT + "=SQRT(4)": "2", + // SUM "=SUM(1,2)": "3", "=SUM(1,2+3)": "6", "=SUM(SUM(1,2),2)": "5", @@ -31,10 +60,6 @@ func TestCalcCellValue(t *testing.T) { "=((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", - // POWER - "=POWER(4,2)": "16", - // SQRT - "=SQRT(4)": "2", // QUOTIENT "=QUOTIENT(5, 2)": "2", "=QUOTIENT(4.5, 3.1)": "1", @@ -48,10 +73,23 @@ func TestCalcCellValue(t *testing.T) { assert.Equal(t, expected, result) } mathCalcError := map[string]string{ + // ABS + "=ABS(1,2)": "ABS requires 1 numeric arguments", + "=ABS(~)": `cannot convert cell "~" to coordinates: invalid cell name "~"`, + // GCD + "=GCD()": "GCD requires at least 1 argument", + "=GCD(-1)": "GCD only accepts positive arguments", + "=GCD(1,-1)": "GCD only accepts positive arguments", + // LCM + "=LCM()": "LCM requires at least 1 argument", + "=LCM(-1)": "LCM only accepts positive arguments", + "=LCM(1,-1)": "LCM only accepts positive arguments", // POWER "=POWER(0,0)": "#NUM!", "=POWER(0,-1)": "#DIV/0!", "=POWER(1)": "POWER requires 2 numeric arguments", + // SIGN + "=SIGN()": "SIGN requires 1 numeric arguments", // SQRT "=SQRT(-1)": "#NUM!", "=SQRT(1,2)": "SQRT requires 1 numeric arguments", @@ -68,6 +106,9 @@ func TestCalcCellValue(t *testing.T) { } referenceCalc := map[string]string{ + // PRODUCT + "=PRODUCT(Sheet1!A1:Sheet1!A1:A2,A2)": "4", + // SUM "=A1/A3": "0.3333333333333333", "=SUM(A1:A2)": "3", "=SUM(Sheet1!A1,A2)": "3", @@ -77,8 +118,6 @@ func TestCalcCellValue(t *testing.T) { "=1+SUM(SUM(A1+A2/A3)*(2-3),2)": "1.3333333333333335", "=A1/A2/SUM(A1:A2:B1)": "0.07142857142857142", "=A1/A2/SUM(A1:A2:B1)*A3": "0.21428571428571427", - // PRODUCT - "=PRODUCT(Sheet1!A1:Sheet1!A1:A2,A2)": "4", } for formula, expected := range referenceCalc { f := prepareData() From 789adf9202b4bc04e74ea8fe72138f1942b2cc0c Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 4 May 2020 18:18:05 +0800 Subject: [PATCH 225/957] fn: ACOS, ACOSH, ACOT, ACOTH, ARABIC, ASIN, ASINH, ATANH, ATAN2, BASE --- calc.go | 362 +++++++++++++++++++++++++++++++++++++++++++++------ calc_test.go | 71 +++++++++- 2 files changed, 394 insertions(+), 39 deletions(-) diff --git a/calc.go b/calc.go index 5ebdcf7cf5..7c912eb6a6 100644 --- a/calc.go +++ b/calc.go @@ -102,11 +102,17 @@ func getPriority(token efp.Token) (pri int) { // opf - Operation formula // opfd - Operand of the operation formula // opft - Operator of the operation formula +// +// Evaluate arguments of the operation formula by list: +// // args - Arguments of the operation formula // +// TODO: handle subtypes: Nothing, Text, Logical, Error, Concatenation, Intersection, Union +// func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error) { var err error - opdStack, optStack, opfStack, opfdStack, opftStack, argsStack := NewStack(), NewStack(), NewStack(), NewStack(), NewStack(), NewStack() + opdStack, optStack, opfStack, opfdStack, opftStack := NewStack(), NewStack(), NewStack(), NewStack(), NewStack() + argsList := list.New() for i := 0; i < len(tokens); i++ { token := tokens[i] @@ -155,7 +161,7 @@ func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error) return efp.Token{TValue: formulaErrorNAME}, err } for _, val := range result { - argsStack.Push(efp.Token{ + argsList.PushBack(efp.Token{ TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber, TValue: val, @@ -184,11 +190,20 @@ func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error) opftStack.Pop() } if !opfdStack.Empty() { - argsStack.Push(opfdStack.Pop()) + argsList.PushBack(opfdStack.Pop()) } continue } + // current token is logical + if token.TType == efp.OperatorsInfix && token.TSubType == efp.TokenSubTypeLogical { + } + + // current token is text + if token.TType == efp.TokenTypeOperand && token.TSubType == efp.TokenSubTypeText { + argsList.PushBack(token) + } + // current token is function stop if token.TType == efp.TokenTypeFunction && token.TSubType == efp.TokenSubTypeStop { for !opftStack.Empty() { @@ -202,13 +217,14 @@ func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error) // push opfd to args if opfdStack.Len() > 0 { - argsStack.Push(opfdStack.Pop()) + argsList.PushBack(opfdStack.Pop()) } // call formula function to evaluate - result, err := callFuncByName(&formulaFuncs{}, opfStack.Peek().(efp.Token).TValue, []reflect.Value{reflect.ValueOf(argsStack)}) + result, err := callFuncByName(&formulaFuncs{}, strings.ReplaceAll(opfStack.Peek().(efp.Token).TValue, "_xlfn.", ""), []reflect.Value{reflect.ValueOf(argsList)}) if err != nil { return efp.Token{}, err } + argsList.Init() opfStack.Pop() if opfStack.Len() > 0 { // still in function stack opfdStack.Push(efp.Token{TValue: result, TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) @@ -480,13 +496,13 @@ func callFuncByName(receiver interface{}, name string, params []reflect.Value) ( // // ABS(number) // -func (fn *formulaFuncs) ABS(argsStack *Stack) (result string, err error) { - if argsStack.Len() != 1 { +func (fn *formulaFuncs) ABS(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { err = errors.New("ABS requires 1 numeric arguments") return } var val float64 - val, err = strconv.ParseFloat(argsStack.Pop().(efp.Token).TValue, 64) + val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) if err != nil { return } @@ -494,6 +510,236 @@ func (fn *formulaFuncs) ABS(argsStack *Stack) (result string, err error) { return } +// ACOS function calculates the arccosine (i.e. the inverse cosine) of a given +// number, and returns an angle, in radians, between 0 and π. The syntax of +// the function is: +// +// ACOS(number) +// +func (fn *formulaFuncs) ACOS(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("ACOS requires 1 numeric arguments") + return + } + var val float64 + val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) + if err != nil { + return + } + result = fmt.Sprintf("%g", math.Acos(val)) + return +} + +// ACOSH function calculates the inverse hyperbolic cosine of a supplied number. +// of the function is: +// +// ACOSH(number) +// +func (fn *formulaFuncs) ACOSH(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("ACOSH requires 1 numeric arguments") + return + } + var val float64 + val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) + if err != nil { + return + } + result = fmt.Sprintf("%g", math.Acosh(val)) + return +} + +// ACOT function calculates the arccotangent (i.e. the inverse cotangent) of a +// given number, and returns an angle, in radians, between 0 and π. The syntax +// of the function is: +// +// ACOT(number) +// +func (fn *formulaFuncs) ACOT(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("ACOT requires 1 numeric arguments") + return + } + var val float64 + val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) + if err != nil { + return + } + result = fmt.Sprintf("%g", math.Pi/2-math.Atan(val)) + return +} + +// ACOTH function calculates the hyperbolic arccotangent (coth) of a supplied +// value. The syntax of the function is: +// +// ACOTH(number) +// +func (fn *formulaFuncs) ACOTH(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("ACOTH requires 1 numeric arguments") + return + } + var val float64 + val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) + if err != nil { + return + } + result = fmt.Sprintf("%g", math.Atanh(1/val)) + return +} + +// ARABIC function converts a Roman numeral into an Arabic numeral. The syntax +// of the function is: +// +// ARABIC(text) +// +func (fn *formulaFuncs) ARABIC(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("ARABIC requires 1 numeric arguments") + return + } + val, last, prefix := 0.0, 0.0, 1.0 + for _, char := range argsList.Front().Value.(efp.Token).TValue { + digit := 0.0 + switch char { + case '-': + prefix = -1 + continue + case 'I': + digit = 1 + case 'V': + digit = 5 + case 'X': + digit = 10 + case 'L': + digit = 50 + case 'C': + digit = 100 + case 'D': + digit = 500 + case 'M': + digit = 1000 + } + val += digit + switch { + case last == digit && (last == 5 || last == 50 || last == 500): + result = formulaErrorVALUE + return + case 2*last == digit: + result = formulaErrorVALUE + return + } + if last < digit { + val -= 2 * last + } + last = digit + } + result = fmt.Sprintf("%g", prefix*val) + return +} + +// ASIN function calculates the arcsine (i.e. the inverse sine) of a given +// number, and returns an angle, in radians, between -π/2 and π/2. The syntax +// of the function is: +// +// ASIN(number) +// +func (fn *formulaFuncs) ASIN(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("ASIN requires 1 numeric arguments") + return + } + var val float64 + val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) + if err != nil { + return + } + result = fmt.Sprintf("%g", math.Asin(val)) + return +} + +// ASINH function calculates the inverse hyperbolic sine of a supplied number. +// The syntax of the function is: +// +// ASINH(number) +// +func (fn *formulaFuncs) ASINH(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("ASINH requires 1 numeric arguments") + return + } + var val float64 + val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) + if err != nil { + return + } + result = fmt.Sprintf("%g", math.Asinh(val)) + return +} + +// ATAN function calculates the arctangent (i.e. the inverse tangent) of a +// given number, and returns an angle, in radians, between -π/2 and +π/2. The +// syntax of the function is: +// +// ATAN(number) +// +func (fn *formulaFuncs) ATAN(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("ATAN requires 1 numeric arguments") + return + } + var val float64 + val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) + if err != nil { + return + } + result = fmt.Sprintf("%g", math.Atan(val)) + return +} + +// ATANH function calculates the inverse hyperbolic tangent of a supplied +// number. The syntax of the function is: +// +// ATANH(number) +// +func (fn *formulaFuncs) ATANH(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("ATANH requires 1 numeric arguments") + return + } + var val float64 + val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) + if err != nil { + return + } + result = fmt.Sprintf("%g", math.Atanh(val)) + return +} + +// ATAN2 function calculates the arctangent (i.e. the inverse tangent) of a +// 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) +// +func (fn *formulaFuncs) ATAN2(argsList *list.List) (result string, err error) { + if argsList.Len() != 2 { + err = errors.New("ATAN2 requires 2 numeric arguments") + return + } + var x, y float64 + x, err = strconv.ParseFloat(argsList.Back().Value.(efp.Token).TValue, 64) + if err != nil { + return + } + y, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) + if err != nil { + return + } + result = fmt.Sprintf("%g", math.Atan2(x, y)) + return +} + // gcd returns the greatest common divisor of two supplied integers. func gcd(x, y float64) float64 { x, y = math.Trunc(x), math.Trunc(y) @@ -513,13 +759,55 @@ func gcd(x, y float64) float64 { return x } +// 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]) +// +func (fn *formulaFuncs) BASE(argsList *list.List) (result string, err error) { + if argsList.Len() < 2 { + err = errors.New("BASE requires at least 2 arguments") + return + } + if argsList.Len() > 3 { + err = errors.New("BASE allows at most 3 arguments") + return + } + var number float64 + var radix, minLength int + number, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) + if err != nil { + return + } + radix, err = strconv.Atoi(argsList.Front().Next().Value.(efp.Token).TValue) + if err != nil { + return + } + if radix < 2 || radix > 36 { + err = errors.New("radix must be an integer ≥ 2 and ≤ 36") + return + } + if argsList.Len() > 2 { + minLength, err = strconv.Atoi(argsList.Back().Value.(efp.Token).TValue) + if err != nil { + return + } + } + result = strconv.FormatInt(int64(number), radix) + if len(result) < minLength { + result = strings.Repeat("0", minLength-len(result)) + result + } + result = strings.ToUpper(result) + return +} + // GCD function returns the greatest common divisor of two or more supplied -// integers.The syntax of the function is: +// integers. The syntax of the function is: // // GCD(number1,[number2],...) // -func (fn *formulaFuncs) GCD(argsStack *Stack) (result string, err error) { - if argsStack.Len() == 0 { +func (fn *formulaFuncs) GCD(argsList *list.List) (result string, err error) { + if argsList.Len() == 0 { err = errors.New("GCD requires at least 1 argument") return } @@ -527,8 +815,8 @@ func (fn *formulaFuncs) GCD(argsStack *Stack) (result string, err error) { val float64 nums = []float64{} ) - for !argsStack.Empty() { - token := argsStack.Pop().(efp.Token) + for arg := argsList.Front(); arg != nil; arg = arg.Next() { + token := arg.Value.(efp.Token) if token.TValue == "" { continue } @@ -573,8 +861,8 @@ func lcm(a, b float64) float64 { // // LCM(number1,[number2],...) // -func (fn *formulaFuncs) LCM(argsStack *Stack) (result string, err error) { - if argsStack.Len() == 0 { +func (fn *formulaFuncs) LCM(argsList *list.List) (result string, err error) { + if argsList.Len() == 0 { err = errors.New("LCM requires at least 1 argument") return } @@ -582,8 +870,8 @@ func (fn *formulaFuncs) LCM(argsStack *Stack) (result string, err error) { val float64 nums = []float64{} ) - for !argsStack.Empty() { - token := argsStack.Pop().(efp.Token) + for arg := argsList.Front(); arg != nil; arg = arg.Next() { + token := arg.Value.(efp.Token) if token.TValue == "" { continue } @@ -618,17 +906,17 @@ func (fn *formulaFuncs) LCM(argsStack *Stack) (result string, err error) { // // POWER(number,power) // -func (fn *formulaFuncs) POWER(argsStack *Stack) (result string, err error) { - if argsStack.Len() != 2 { +func (fn *formulaFuncs) POWER(argsList *list.List) (result string, err error) { + if argsList.Len() != 2 { err = errors.New("POWER requires 2 numeric arguments") return } var x, y float64 - y, err = strconv.ParseFloat(argsStack.Pop().(efp.Token).TValue, 64) + x, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) if err != nil { return } - x, err = strconv.ParseFloat(argsStack.Pop().(efp.Token).TValue, 64) + y, err = strconv.ParseFloat(argsList.Back().Value.(efp.Token).TValue, 64) if err != nil { return } @@ -649,13 +937,13 @@ func (fn *formulaFuncs) POWER(argsStack *Stack) (result string, err error) { // // PRODUCT(number1,[number2],...) // -func (fn *formulaFuncs) PRODUCT(argsStack *Stack) (result string, err error) { +func (fn *formulaFuncs) PRODUCT(argsList *list.List) (result string, err error) { var ( val float64 product float64 = 1 ) - for !argsStack.Empty() { - token := argsStack.Pop().(efp.Token) + for arg := argsList.Front(); arg != nil; arg = arg.Next() { + token := arg.Value.(efp.Token) if token.TValue == "" { continue } @@ -676,13 +964,13 @@ func (fn *formulaFuncs) PRODUCT(argsStack *Stack) (result string, err error) { // // SIGN(number) // -func (fn *formulaFuncs) SIGN(argsStack *Stack) (result string, err error) { - if argsStack.Len() != 1 { +func (fn *formulaFuncs) SIGN(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { err = errors.New("SIGN requires 1 numeric arguments") return } var val float64 - val, err = strconv.ParseFloat(argsStack.Pop().(efp.Token).TValue, 64) + val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) if err != nil { return } @@ -703,13 +991,13 @@ func (fn *formulaFuncs) SIGN(argsStack *Stack) (result string, err error) { // // SQRT(number) // -func (fn *formulaFuncs) SQRT(argsStack *Stack) (result string, err error) { - if argsStack.Len() != 1 { +func (fn *formulaFuncs) SQRT(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { err = errors.New("SQRT requires 1 numeric arguments") return } var val float64 - val, err = strconv.ParseFloat(argsStack.Pop().(efp.Token).TValue, 64) + val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) if err != nil { return } @@ -726,11 +1014,11 @@ func (fn *formulaFuncs) SQRT(argsStack *Stack) (result string, err error) { // // SUM(number1,[number2],...) // -func (fn *formulaFuncs) SUM(argsStack *Stack) (result string, err error) { +func (fn *formulaFuncs) SUM(argsList *list.List) (result string, err error) { var val float64 var sum float64 - for !argsStack.Empty() { - token := argsStack.Pop().(efp.Token) + for arg := argsList.Front(); arg != nil; arg = arg.Next() { + token := arg.Value.(efp.Token) if token.TValue == "" { continue } @@ -749,17 +1037,17 @@ func (fn *formulaFuncs) SUM(argsStack *Stack) (result string, err error) { // // QUOTIENT(numerator,denominator) // -func (fn *formulaFuncs) QUOTIENT(argsStack *Stack) (result string, err error) { - if argsStack.Len() != 2 { +func (fn *formulaFuncs) QUOTIENT(argsList *list.List) (result string, err error) { + if argsList.Len() != 2 { err = errors.New("QUOTIENT requires 2 numeric arguments") return } var x, y float64 - y, err = strconv.ParseFloat(argsStack.Pop().(efp.Token).TValue, 64) + x, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) if err != nil { return } - x, err = strconv.ParseFloat(argsStack.Pop().(efp.Token).TValue, 64) + y, err = strconv.ParseFloat(argsList.Back().Value.(efp.Token).TValue, 64) if err != nil { return } diff --git a/calc_test.go b/calc_test.go index 84fa955d86..bb8ae8ae34 100644 --- a/calc_test.go +++ b/calc_test.go @@ -24,6 +24,49 @@ func TestCalcCellValue(t *testing.T) { "=ABS(6.5)": "6.5", "=ABS(0)": "0", "=ABS(2-4.5)": "2.5", + // ACOS + "=ACOS(-1)": "3.141592653589793", + "=ACOS(0)": "1.5707963267948966", + // ACOSH + "=ACOSH(1)": "0", + "=ACOSH(2.5)": "1.566799236972411", + "=ACOSH(5)": "2.2924316695611777", + // ACOT + "=_xlfn.ACOT(1)": "0.7853981633974483", + "=_xlfn.ACOT(-2)": "2.677945044588987", + "=_xlfn.ACOT(0)": "1.5707963267948966", + // ACOTH + "=_xlfn.ACOTH(-5)": "-0.2027325540540822", + "=_xlfn.ACOTH(1.1)": "1.5222612188617113", + "=_xlfn.ACOTH(2)": "0.5493061443340548", + // ARABIC + `=_xlfn.ARABIC("IV")`: "4", + `=_xlfn.ARABIC("-IV")`: "-4", + `=_xlfn.ARABIC("MCXX")`: "1120", + `=_xlfn.ARABIC("")`: "0", + // ASIN + "=ASIN(-1)": "-1.5707963267948966", + "=ASIN(0)": "0", + // ASINH + "=ASINH(0)": "0", + "=ASINH(-0.5)": "-0.48121182505960347", + "=ASINH(2)": "1.4436354751788103", + // ATAN + "=ATAN(-1)": "-0.7853981633974483", + "=ATAN(0)": "0", + "=ATAN(1)": "0.7853981633974483", + // ATANH + "=ATANH(-0.8)": "-1.0986122886681098", + "=ATANH(0)": "0", + "=ATANH(0.5)": "0.5493061443340548", + // ATAN2 + "=ATAN2(1,1)": "0.7853981633974483", + "=ATAN2(1,-1)": "-0.7853981633974483", + "=ATAN2(4,0)": "0", + // BASE + "=BASE(12,2)": "1100", + "=BASE(12,2,8)": "00001100", + "=BASE(100000,16)": "186A0", // GCD "=GCD(1,5)": "1", "=GCD(15,10,25)": "5", @@ -74,8 +117,32 @@ func TestCalcCellValue(t *testing.T) { } mathCalcError := map[string]string{ // ABS - "=ABS(1,2)": "ABS requires 1 numeric arguments", - "=ABS(~)": `cannot convert cell "~" to coordinates: invalid cell name "~"`, + "=ABS()": "ABS requires 1 numeric arguments", + "=ABS(~)": `cannot convert cell "~" to coordinates: invalid cell name "~"`, + // ACOS + "=ACOS()": "ACOS requires 1 numeric arguments", + // ACOSH + "=ACOSH()": "ACOSH requires 1 numeric arguments", + // ACOT + "=_xlfn.ACOT()": "ACOT requires 1 numeric arguments", + // ACOTH + "=_xlfn.ACOTH()": "ACOTH requires 1 numeric arguments", + // ARABIC + "_xlfn.ARABIC()": "ARABIC requires 1 numeric arguments", + // ASIN + "=ASIN()": "ASIN requires 1 numeric arguments", + // ASINH + "=ASINH()": "ASINH requires 1 numeric arguments", + // ATAN + "=ATAN()": "ATAN requires 1 numeric arguments", + // ATANH + "=ATANH()": "ATANH requires 1 numeric arguments", + // ATAN2 + "=ATAN2()": "ATAN2 requires 2 numeric arguments", + // 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", // GCD "=GCD()": "GCD requires at least 1 argument", "=GCD(-1)": "GCD only accepts positive arguments", From 6f796b88e68e927c71e51e22278f4d43b935e00a Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 4 May 2020 21:22:11 +0800 Subject: [PATCH 226/957] fn: CEILING, CEILING.MATH --- calc.go | 101 ++++++++++++++++++++++++++++++++++++++++++++++++++- calc_test.go | 29 +++++++++++++-- 2 files changed, 126 insertions(+), 4 deletions(-) diff --git a/calc.go b/calc.go index 7c912eb6a6..568f0445cd 100644 --- a/calc.go +++ b/calc.go @@ -220,7 +220,9 @@ func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error) argsList.PushBack(opfdStack.Pop()) } // call formula function to evaluate - result, err := callFuncByName(&formulaFuncs{}, strings.ReplaceAll(opfStack.Peek().(efp.Token).TValue, "_xlfn.", ""), []reflect.Value{reflect.ValueOf(argsList)}) + result, err := callFuncByName(&formulaFuncs{}, strings.NewReplacer( + "_xlfn", "", ".", "").Replace(opfStack.Peek().(efp.Token).TValue), + []reflect.Value{reflect.ValueOf(argsList)}) if err != nil { return efp.Token{}, err } @@ -801,6 +803,103 @@ func (fn *formulaFuncs) BASE(argsList *list.List) (result string, err error) { return } +// 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) +// +func (fn *formulaFuncs) CEILING(argsList *list.List) (result string, err error) { + if argsList.Len() == 0 { + err = errors.New("CEILING requires at least 1 argument") + return + } + if argsList.Len() > 2 { + err = errors.New("CEILING allows at most 2 arguments") + return + } + var number, significance float64 + number, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) + if err != nil { + return + } + significance = 1 + if number < 0 { + significance = -1 + } + if argsList.Len() > 1 { + significance, err = strconv.ParseFloat(argsList.Back().Value.(efp.Token).TValue, 64) + if err != nil { + return + } + } + if significance < 0 && number > 0 { + err = errors.New("negative sig to CEILING invalid") + return + } + if argsList.Len() == 1 { + result = fmt.Sprintf("%g", math.Ceil(number)) + return + } + number, res := math.Modf(number / significance) + if res > 0 { + number++ + } + result = fmt.Sprintf("%g", number*significance) + return +} + +// CEILINGMATH function rounds a supplied number up to a supplied multiple of +// significance. The syntax of the function is: +// +// CEILING.MATH(number,[significance],[mode]) +// +func (fn *formulaFuncs) CEILINGMATH(argsList *list.List) (result string, err error) { + if argsList.Len() == 0 { + err = errors.New("CEILING.MATH requires at least 1 argument") + return + } + if argsList.Len() > 3 { + err = errors.New("CEILING.MATH allows at most 3 arguments") + return + } + var number, significance, mode float64 = 0, 1, 1 + number, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) + if err != nil { + return + } + if number < 0 { + significance = -1 + } + if argsList.Len() > 1 { + significance, err = strconv.ParseFloat(argsList.Front().Next().Value.(efp.Token).TValue, 64) + if err != nil { + return + } + } + if argsList.Len() == 1 { + result = fmt.Sprintf("%g", math.Ceil(number)) + return + } + if argsList.Len() > 2 { + mode, err = strconv.ParseFloat(argsList.Back().Value.(efp.Token).TValue, 64) + if err != nil { + return + } + } + val, res := math.Modf(number / significance) + _, _ = res, mode + if res != 0 { + if number > 0 { + val++ + } else if mode < 0 { + val-- + } + } + + result = fmt.Sprintf("%g", val*significance) + return +} + // GCD function returns the greatest common divisor of two or more supplied // integers. The syntax of the function is: // diff --git a/calc_test.go b/calc_test.go index bb8ae8ae34..a14cc0c83b 100644 --- a/calc_test.go +++ b/calc_test.go @@ -67,6 +67,22 @@ func TestCalcCellValue(t *testing.T) { "=BASE(12,2)": "1100", "=BASE(12,2,8)": "00001100", "=BASE(100000,16)": "186A0", + // CEILING + "=CEILING(22.25,0.1)": "22.3", + "=CEILING(22.25,0.5)": "22.5", + "=CEILING(22.25,1)": "23", + "=CEILING(22.25,10)": "30", + "=CEILING(22.25,20)": "40", + "=CEILING(-22.25,-0.1)": "-22.3", + "=CEILING(-22.25,-1)": "-23", + "=CEILING(-22.25,-5)": "-25", + // _xlfn.CEILING.MATH + "=_xlfn.CEILING.MATH(15.25,1)": "16", + "=_xlfn.CEILING.MATH(15.25,0.1)": "15.3", + "=_xlfn.CEILING.MATH(15.25,5)": "20", + "=_xlfn.CEILING.MATH(-15.25,1)": "-15", + "=_xlfn.CEILING.MATH(-15.25,1,1)": "-15", // should be 16 + "=_xlfn.CEILING.MATH(-15.25,10)": "-10", // GCD "=GCD(1,5)": "1", "=GCD(15,10,25)": "5", @@ -123,11 +139,11 @@ func TestCalcCellValue(t *testing.T) { "=ACOS()": "ACOS requires 1 numeric arguments", // ACOSH "=ACOSH()": "ACOSH requires 1 numeric arguments", - // ACOT + // _xlfn.ACOT "=_xlfn.ACOT()": "ACOT requires 1 numeric arguments", - // ACOTH + // _xlfn.ACOTH "=_xlfn.ACOTH()": "ACOTH requires 1 numeric arguments", - // ARABIC + // _xlfn.ARABIC "_xlfn.ARABIC()": "ARABIC requires 1 numeric arguments", // ASIN "=ASIN()": "ASIN requires 1 numeric arguments", @@ -143,6 +159,13 @@ func TestCalcCellValue(t *testing.T) { "=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", + // 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", + // _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", // GCD "=GCD()": "GCD requires at least 1 argument", "=GCD(-1)": "GCD only accepts positive arguments", From 5c82f2269dfa82c5be3afd7ac140aacbf2221829 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 5 May 2020 17:27:19 +0800 Subject: [PATCH 227/957] #65 fn: CEILING.PRECISE, COMBIN, COMBINA, COS, COSH, COT, COTH, CSC --- calc.go | 319 ++++++++++++++++++++++++++++++++++++++++----------- calc_test.go | 61 +++++++++- 2 files changed, 312 insertions(+), 68 deletions(-) diff --git a/calc.go b/calc.go index 568f0445cd..ed25a58a8b 100644 --- a/calc.go +++ b/calc.go @@ -504,8 +504,7 @@ func (fn *formulaFuncs) ABS(argsList *list.List) (result string, err error) { return } var val float64 - val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) - if err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { return } result = fmt.Sprintf("%g", math.Abs(val)) @@ -524,8 +523,7 @@ func (fn *formulaFuncs) ACOS(argsList *list.List) (result string, err error) { return } var val float64 - val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) - if err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { return } result = fmt.Sprintf("%g", math.Acos(val)) @@ -543,8 +541,7 @@ func (fn *formulaFuncs) ACOSH(argsList *list.List) (result string, err error) { return } var val float64 - val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) - if err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { return } result = fmt.Sprintf("%g", math.Acosh(val)) @@ -563,8 +560,7 @@ func (fn *formulaFuncs) ACOT(argsList *list.List) (result string, err error) { return } var val float64 - val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) - if err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { return } result = fmt.Sprintf("%g", math.Pi/2-math.Atan(val)) @@ -582,8 +578,7 @@ func (fn *formulaFuncs) ACOTH(argsList *list.List) (result string, err error) { return } var val float64 - val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) - if err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { return } result = fmt.Sprintf("%g", math.Atanh(1/val)) @@ -652,8 +647,7 @@ func (fn *formulaFuncs) ASIN(argsList *list.List) (result string, err error) { return } var val float64 - val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) - if err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { return } result = fmt.Sprintf("%g", math.Asin(val)) @@ -671,8 +665,7 @@ func (fn *formulaFuncs) ASINH(argsList *list.List) (result string, err error) { return } var val float64 - val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) - if err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { return } result = fmt.Sprintf("%g", math.Asinh(val)) @@ -691,8 +684,7 @@ func (fn *formulaFuncs) ATAN(argsList *list.List) (result string, err error) { return } var val float64 - val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) - if err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { return } result = fmt.Sprintf("%g", math.Atan(val)) @@ -710,8 +702,7 @@ func (fn *formulaFuncs) ATANH(argsList *list.List) (result string, err error) { return } var val float64 - val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) - if err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { return } result = fmt.Sprintf("%g", math.Atanh(val)) @@ -730,12 +721,10 @@ func (fn *formulaFuncs) ATAN2(argsList *list.List) (result string, err error) { return } var x, y float64 - x, err = strconv.ParseFloat(argsList.Back().Value.(efp.Token).TValue, 64) - if err != nil { + if x, err = strconv.ParseFloat(argsList.Back().Value.(efp.Token).TValue, 64); err != nil { return } - y, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) - if err != nil { + if y, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { return } result = fmt.Sprintf("%g", math.Atan2(x, y)) @@ -777,12 +766,10 @@ func (fn *formulaFuncs) BASE(argsList *list.List) (result string, err error) { } var number float64 var radix, minLength int - number, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) - if err != nil { + if number, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { return } - radix, err = strconv.Atoi(argsList.Front().Next().Value.(efp.Token).TValue) - if err != nil { + if radix, err = strconv.Atoi(argsList.Front().Next().Value.(efp.Token).TValue); err != nil { return } if radix < 2 || radix > 36 { @@ -790,8 +777,7 @@ func (fn *formulaFuncs) BASE(argsList *list.List) (result string, err error) { return } if argsList.Len() > 2 { - minLength, err = strconv.Atoi(argsList.Back().Value.(efp.Token).TValue) - if err != nil { + if minLength, err = strconv.Atoi(argsList.Back().Value.(efp.Token).TValue); err != nil { return } } @@ -817,18 +803,15 @@ func (fn *formulaFuncs) CEILING(argsList *list.List) (result string, err error) err = errors.New("CEILING allows at most 2 arguments") return } - var number, significance float64 - number, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) - if err != nil { + var number, significance float64 = 0, 1 + if number, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { return } - significance = 1 if number < 0 { significance = -1 } if argsList.Len() > 1 { - significance, err = strconv.ParseFloat(argsList.Back().Value.(efp.Token).TValue, 64) - if err != nil { + if significance, err = strconv.ParseFloat(argsList.Back().Value.(efp.Token).TValue, 64); err != nil { return } } @@ -863,16 +846,14 @@ func (fn *formulaFuncs) CEILINGMATH(argsList *list.List) (result string, err err return } var number, significance, mode float64 = 0, 1, 1 - number, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) - if err != nil { + if number, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { return } if number < 0 { significance = -1 } if argsList.Len() > 1 { - significance, err = strconv.ParseFloat(argsList.Front().Next().Value.(efp.Token).TValue, 64) - if err != nil { + if significance, err = strconv.ParseFloat(argsList.Front().Next().Value.(efp.Token).TValue, 64); err != nil { return } } @@ -881,13 +862,11 @@ func (fn *formulaFuncs) CEILINGMATH(argsList *list.List) (result string, err err return } if argsList.Len() > 2 { - mode, err = strconv.ParseFloat(argsList.Back().Value.(efp.Token).TValue, 64) - if err != nil { + if mode, err = strconv.ParseFloat(argsList.Back().Value.(efp.Token).TValue, 64); err != nil { return } } val, res := math.Modf(number / significance) - _, _ = res, mode if res != 0 { if number > 0 { val++ @@ -895,11 +874,231 @@ func (fn *formulaFuncs) CEILINGMATH(argsList *list.List) (result string, err err val-- } } + result = fmt.Sprintf("%g", val*significance) + return +} +// CEILINGPRECISE function rounds a supplied number up (regardless of the +// number's sign), to the nearest multiple of a given number. The syntax of +// the function is: +// +// CEILING.PRECISE(number,[significance]) +// +func (fn *formulaFuncs) CEILINGPRECISE(argsList *list.List) (result string, err error) { + if argsList.Len() == 0 { + err = errors.New("CEILING.PRECISE requires at least 1 argument") + return + } + if argsList.Len() > 2 { + err = errors.New("CEILING.PRECISE allows at most 2 arguments") + return + } + var number, significance float64 = 0, 1 + if number, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + return + } + if number < 0 { + significance = -1 + } + if argsList.Len() == 1 { + result = fmt.Sprintf("%g", math.Ceil(number)) + return + } + if argsList.Len() > 1 { + if significance, err = strconv.ParseFloat(argsList.Back().Value.(efp.Token).TValue, 64); err != nil { + return + } + significance = math.Abs(significance) + if significance == 0 { + result = "0" + return + } + } + val, res := math.Modf(number / significance) + if res != 0 { + if number > 0 { + val++ + } + } result = fmt.Sprintf("%g", val*significance) return } +// 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) +// +func (fn *formulaFuncs) COMBIN(argsList *list.List) (result string, err error) { + if argsList.Len() != 2 { + err = errors.New("COMBIN requires 2 argument") + return + } + var number, chosen, val float64 = 0, 0, 1 + if number, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + return + } + if chosen, err = strconv.ParseFloat(argsList.Back().Value.(efp.Token).TValue, 64); err != nil { + return + } + number, chosen = math.Trunc(number), math.Trunc(chosen) + if chosen > number { + err = errors.New("COMBIN requires number >= number_chosen") + return + } + if chosen == number || chosen == 0 { + result = "1" + return + } + for c := float64(1); c <= chosen; c++ { + val *= (number + 1 - c) / c + } + result = fmt.Sprintf("%g", math.Ceil(val)) + return +} + +// 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) +// +func (fn *formulaFuncs) COMBINA(argsList *list.List) (result string, err error) { + if argsList.Len() != 2 { + err = errors.New("COMBINA requires 2 argument") + return + } + var number, chosen float64 + if number, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + return + } + if chosen, err = strconv.ParseFloat(argsList.Back().Value.(efp.Token).TValue, 64); err != nil { + return + } + number, chosen = math.Trunc(number), math.Trunc(chosen) + if number < chosen { + err = errors.New("COMBINA requires number > number_chosen") + return + } + if number == 0 { + result = "0" + return + } + args := list.New() + args.PushBack(efp.Token{ + TValue: fmt.Sprintf("%g", number+chosen-1), + TType: efp.TokenTypeOperand, + TSubType: efp.TokenSubTypeNumber, + }) + args.PushBack(efp.Token{ + TValue: fmt.Sprintf("%g", number-1), + TType: efp.TokenTypeOperand, + TSubType: efp.TokenSubTypeNumber, + }) + return fn.COMBIN(args) +} + +// COS function calculates the cosine of a given angle. The syntax of the +// function is: +// +// COS(number) +// +func (fn *formulaFuncs) COS(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("COS requires 1 numeric arguments") + return + } + var val float64 + if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + return + } + result = fmt.Sprintf("%g", math.Cos(val)) + return +} + +// COSH function calculates the hyperbolic cosine (cosh) of a supplied number. +// The syntax of the function is: +// +// COSH(number) +// +func (fn *formulaFuncs) COSH(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("COSH requires 1 numeric arguments") + return + } + var val float64 + if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + return + } + result = fmt.Sprintf("%g", math.Cosh(val)) + return +} + +// COT function calculates the cotangent of a given angle. The syntax of the +// function is: +// +// COT(number) +// +func (fn *formulaFuncs) COT(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("COT requires 1 numeric arguments") + return + } + var val float64 + if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + return + } + if val == 0 { + err = errors.New(formulaErrorNAME) + return + } + result = fmt.Sprintf("%g", math.Tan(val)) + return +} + +// COTH function calculates the hyperbolic cotangent (coth) of a supplied +// angle. The syntax of the function is: +// +// COTH(number) +// +func (fn *formulaFuncs) COTH(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("COTH requires 1 numeric arguments") + return + } + var val float64 + if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + return + } + if val == 0 { + err = errors.New(formulaErrorNAME) + return + } + result = fmt.Sprintf("%g", math.Tanh(val)) + return +} + +// CSC function calculates the cosecant of a given angle. The syntax of the +// function is: +// +// CSC(number) +// +func (fn *formulaFuncs) CSC(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("CSC requires 1 numeric arguments") + return + } + var val float64 + if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + return + } + if val == 0 { + err = errors.New(formulaErrorNAME) + return + } + result = fmt.Sprintf("%g", 1/math.Sin(val)) + return +} + // GCD function returns the greatest common divisor of two or more supplied // integers. The syntax of the function is: // @@ -919,8 +1118,7 @@ func (fn *formulaFuncs) GCD(argsList *list.List) (result string, err error) { if token.TValue == "" { continue } - val, err = strconv.ParseFloat(token.TValue, 64) - if err != nil { + if val, err = strconv.ParseFloat(token.TValue, 64); err != nil { return } nums = append(nums, val) @@ -974,8 +1172,7 @@ func (fn *formulaFuncs) LCM(argsList *list.List) (result string, err error) { if token.TValue == "" { continue } - val, err = strconv.ParseFloat(token.TValue, 64) - if err != nil { + if val, err = strconv.ParseFloat(token.TValue, 64); err != nil { return } nums = append(nums, val) @@ -1011,12 +1208,10 @@ func (fn *formulaFuncs) POWER(argsList *list.List) (result string, err error) { return } var x, y float64 - x, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) - if err != nil { + if x, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { return } - y, err = strconv.ParseFloat(argsList.Back().Value.(efp.Token).TValue, 64) - if err != nil { + if y, err = strconv.ParseFloat(argsList.Back().Value.(efp.Token).TValue, 64); err != nil { return } if x == 0 && y == 0 { @@ -1037,17 +1232,13 @@ func (fn *formulaFuncs) POWER(argsList *list.List) (result string, err error) { // PRODUCT(number1,[number2],...) // func (fn *formulaFuncs) PRODUCT(argsList *list.List) (result string, err error) { - var ( - val float64 - product float64 = 1 - ) + var val, product float64 = 0, 1 for arg := argsList.Front(); arg != nil; arg = arg.Next() { token := arg.Value.(efp.Token) if token.TValue == "" { continue } - val, err = strconv.ParseFloat(token.TValue, 64) - if err != nil { + if val, err = strconv.ParseFloat(token.TValue, 64); err != nil { return } product = product * val @@ -1069,8 +1260,7 @@ func (fn *formulaFuncs) SIGN(argsList *list.List) (result string, err error) { return } var val float64 - val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) - if err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { return } if val < 0 { @@ -1096,8 +1286,7 @@ func (fn *formulaFuncs) SQRT(argsList *list.List) (result string, err error) { return } var val float64 - val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) - if err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { return } if val < 0 { @@ -1114,15 +1303,13 @@ func (fn *formulaFuncs) SQRT(argsList *list.List) (result string, err error) { // SUM(number1,[number2],...) // func (fn *formulaFuncs) SUM(argsList *list.List) (result string, err error) { - var val float64 - var sum float64 + var val, sum float64 for arg := argsList.Front(); arg != nil; arg = arg.Next() { token := arg.Value.(efp.Token) if token.TValue == "" { continue } - val, err = strconv.ParseFloat(token.TValue, 64) - if err != nil { + if val, err = strconv.ParseFloat(token.TValue, 64); err != nil { return } sum += val @@ -1142,12 +1329,10 @@ func (fn *formulaFuncs) QUOTIENT(argsList *list.List) (result string, err error) return } var x, y float64 - x, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64) - if err != nil { + if x, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { return } - y, err = strconv.ParseFloat(argsList.Back().Value.(efp.Token).TValue, 64) - if err != nil { + if y, err = strconv.ParseFloat(argsList.Back().Value.(efp.Token).TValue, 64); err != nil { return } if y == 0 { diff --git a/calc_test.go b/calc_test.go index a14cc0c83b..5d441ff6c1 100644 --- a/calc_test.go +++ b/calc_test.go @@ -83,6 +83,44 @@ func TestCalcCellValue(t *testing.T) { "=_xlfn.CEILING.MATH(-15.25,1)": "-15", "=_xlfn.CEILING.MATH(-15.25,1,1)": "-15", // should be 16 "=_xlfn.CEILING.MATH(-15.25,10)": "-10", + // _xlfn.CEILING.PRECISE + "=_xlfn.CEILING.PRECISE(22.25,0.1)": "22.3", + "=_xlfn.CEILING.PRECISE(22.25,0.5)": "22.5", + "=_xlfn.CEILING.PRECISE(22.25,1)": "23", + "=_xlfn.CEILING.PRECISE(22.25)": "23", + "=_xlfn.CEILING.PRECISE(22.25,10)": "30", + "=_xlfn.CEILING.PRECISE(22.25,0)": "0", + "=_xlfn.CEILING.PRECISE(-22.25,1)": "-22", + "=_xlfn.CEILING.PRECISE(-22.25,-1)": "-22", + "=_xlfn.CEILING.PRECISE(-22.25,5)": "-20", + // COMBIN + "=COMBIN(6,1)": "6", + "=COMBIN(6,2)": "15", + "=COMBIN(6,3)": "20", + "=COMBIN(6,4)": "15", + "=COMBIN(6,5)": "6", + "=COMBIN(6,6)": "1", + // _xlfn.COMBINA + "=_xlfn.COMBINA(6,1)": "6", + "=_xlfn.COMBINA(6,2)": "21", + "=_xlfn.COMBINA(6,3)": "56", + "=_xlfn.COMBINA(6,4)": "126", + "=_xlfn.COMBINA(6,5)": "252", + "=_xlfn.COMBINA(6,6)": "462", + // COS + "=COS(0.785398163)": "0.707106781467586", + "=COS(0)": "1", + // COSH + "=COSH(0)": "1", + "=COSH(0.5)": "1.1276259652063807", + "=COSH(-2)": "3.7621956910836314", + // _xlfn.COT + "_xlfn.COT(0.785398163397448)": "0.9999999999999992", + // _xlfn.COTH + "_xlfn.COTH(-3.14159265358979)": "-0.9962720762207499", + // _xlfn.CSC + "_xlfn.CSC(-6)": "3.5788995472544056", + "_xlfn.CSC(1.5707963267949)": "1", // GCD "=GCD(1,5)": "1", "=GCD(15,10,25)": "5", @@ -144,7 +182,7 @@ func TestCalcCellValue(t *testing.T) { // _xlfn.ACOTH "=_xlfn.ACOTH()": "ACOTH requires 1 numeric arguments", // _xlfn.ARABIC - "_xlfn.ARABIC()": "ARABIC requires 1 numeric arguments", + "=_xlfn.ARABIC()": "ARABIC requires 1 numeric arguments", // ASIN "=ASIN()": "ASIN requires 1 numeric arguments", // ASINH @@ -166,6 +204,27 @@ func TestCalcCellValue(t *testing.T) { // _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.PRECISE + "=_xlfn.CEILING.PRECISE()": "CEILING.PRECISE requires at least 1 argument", + "=_xlfn.CEILING.PRECISE(1,2,3)": "CEILING.PRECISE allows at most 2 arguments", + // COMBIN + "=COMBIN()": "COMBIN requires 2 argument", + "=COMBIN(-1,1)": "COMBIN requires number >= number_chosen", + // _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", + // COS + "=COS()": "COS requires 1 numeric arguments", + // COSH + "=COSH()": "COSH requires 1 numeric arguments", + // _xlfn.COT + "=COT()": "COT requires 1 numeric arguments", + // _xlfn.COTH + "=COTH()": "COTH requires 1 numeric arguments", + // _xlfn.CSC + "_xlfn.CSC()": "CSC requires 1 numeric arguments", + "_xlfn.CSC(0)": "#NAME?", // GCD "=GCD()": "GCD requires at least 1 argument", "=GCD(-1)": "GCD only accepts positive arguments", From 97e3f4ce6822bea6d65961c0399f7563450b69b4 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 6 May 2020 00:01:31 +0800 Subject: [PATCH 228/957] #65 fn: CSCH, DECIMAL, DEGREES, EVEN, EXP, FACT, FACTDOUBLE, FLOOR, FLOOR.MATH, FLOOR.PRECISE, INT, ISO.CEILING, LN, LOG, LOG10, MDETERM --- calc.go | 719 ++++++++++++++++++++++++++++++++++++++++++++------- calc_test.go | 178 +++++++++++-- 2 files changed, 777 insertions(+), 120 deletions(-) diff --git a/calc.go b/calc.go index ed25a58a8b..2ab3d61ba2 100644 --- a/calc.go +++ b/calc.go @@ -37,19 +37,26 @@ const ( formulaErrorGETTINGDATA = "#GETTING_DATA" ) -// cellRef defines the structure of a cell reference +// cellRef defines the structure of a cell reference. type cellRef struct { Col int Row int Sheet string } -// cellRef defines the structure of a cell range +// cellRef defines the structure of a cell range. type cellRange struct { From cellRef To cellRef } +// formulaArg is the argument of a formula or function. +type formulaArg struct { + Value string + Matrix []string +} + +// formulaFuncs is the type of the formula functions. type formulaFuncs struct{} // CalcCellValue provides a function to get calculated cell value. This @@ -140,7 +147,7 @@ func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error) if token.TSubType == efp.TokenSubTypeRange { if !opftStack.Empty() { // parse reference: must reference at here - result, err := f.parseReference(sheet, token.TValue) + result, _, err := f.parseReference(sheet, token.TValue) if err != nil { return efp.Token{TValue: formulaErrorNAME}, err } @@ -156,16 +163,16 @@ func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error) } if nextToken.TType == efp.TokenTypeArgument || nextToken.TType == efp.TokenTypeFunction { // parse reference: reference or range at here - result, err := f.parseReference(sheet, token.TValue) + result, matrix, err := f.parseReference(sheet, token.TValue) if err != nil { return efp.Token{TValue: formulaErrorNAME}, err } - for _, val := range result { - argsList.PushBack(efp.Token{ - TType: efp.TokenTypeOperand, - TSubType: efp.TokenSubTypeNumber, - TValue: val, - }) + for idx, val := range result { + arg := formulaArg{Value: val} + if idx < len(matrix) { + arg.Matrix = matrix[idx] + } + argsList.PushBack(arg) } if len(result) == 0 { return efp.Token{}, errors.New(formulaErrorVALUE) @@ -190,7 +197,9 @@ func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error) opftStack.Pop() } if !opfdStack.Empty() { - argsList.PushBack(opfdStack.Pop()) + argsList.PushBack(formulaArg{ + Value: opfdStack.Pop().(efp.Token).TValue, + }) } continue } @@ -201,7 +210,9 @@ func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error) // current token is text if token.TType == efp.TokenTypeOperand && token.TSubType == efp.TokenSubTypeText { - argsList.PushBack(token) + argsList.PushBack(formulaArg{ + Value: token.TValue, + }) } // current token is function stop @@ -217,7 +228,9 @@ func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error) // push opfd to args if opfdStack.Len() > 0 { - argsList.PushBack(opfdStack.Pop()) + argsList.PushBack(formulaArg{ + Value: opfdStack.Pop().(efp.Token).TValue, + }) } // call formula function to evaluate result, err := callFuncByName(&formulaFuncs{}, strings.NewReplacer( @@ -324,7 +337,7 @@ func calculate(opdStack *Stack, opt efp.Token) error { func (f *File) parseToken(sheet string, token efp.Token, opdStack, optStack *Stack) error { // parse reference: must reference at here if token.TSubType == efp.TokenSubTypeRange { - result, err := f.parseReference(sheet, token.TValue) + result, _, err := f.parseReference(sheet, token.TValue) if err != nil { return errors.New(formulaErrorNAME) } @@ -383,7 +396,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) (result []string, err error) { +func (f *File) parseReference(sheet, reference string) (result []string, matrix [][]string, err error) { reference = strings.Replace(reference, "$", "", -1) refs, cellRanges, cellRefs := list.New(), list.New(), list.New() for _, ref := range strings.Split(reference, ":") { @@ -423,7 +436,7 @@ func (f *File) parseReference(sheet, reference string) (result []string, err err refs.Remove(e) } - result, err = f.rangeResolver(cellRefs, cellRanges) + result, matrix, err = f.rangeResolver(cellRefs, cellRanges) return } @@ -431,7 +444,7 @@ func (f *File) parseReference(sheet, reference string) (result []string, err err // This function will not ignore the empty cell. Note that the result of 3D // range references may be different from Excel in some cases, for example, // A1:A2:A2:B3 in Excel will include B1, but we wont. -func (f *File) rangeResolver(cellRefs, cellRanges *list.List) (result []string, err error) { +func (f *File) rangeResolver(cellRefs, cellRanges *list.List) (result []string, matrix [][]string, err error) { filter := map[string]string{} // extract value from ranges for temp := cellRanges.Front(); temp != nil; temp = temp.Next() { @@ -441,16 +454,21 @@ func (f *File) rangeResolver(cellRefs, cellRanges *list.List) (result []string, } rng := []int{cr.From.Col, cr.From.Row, cr.To.Col, cr.To.Row} sortCoordinates(rng) - for col := rng[0]; col <= rng[2]; col++ { - for row := rng[1]; row <= rng[3]; row++ { - var cell string + matrix = [][]string{} + for row := rng[1]; row <= rng[3]; row++ { + var matrixRow = []string{} + for col := rng[0]; col <= rng[2]; col++ { + var cell, value string if cell, err = CoordinatesToCellName(col, row); err != nil { return } - if filter[cell], err = f.GetCellValue(cr.From.Sheet, cell); err != nil { + if value, err = f.GetCellValue(cr.From.Sheet, cell); err != nil { return } + filter[cell] = value + matrixRow = append(matrixRow, value) } + matrix = append(matrix, matrixRow) } } // extract value from references @@ -500,11 +518,11 @@ func callFuncByName(receiver interface{}, name string, params []reflect.Value) ( // func (fn *formulaFuncs) ABS(argsList *list.List) (result string, err error) { if argsList.Len() != 1 { - err = errors.New("ABS requires 1 numeric arguments") + err = errors.New("ABS requires 1 numeric argument") return } var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } result = fmt.Sprintf("%g", math.Abs(val)) @@ -519,11 +537,11 @@ func (fn *formulaFuncs) ABS(argsList *list.List) (result string, err error) { // func (fn *formulaFuncs) ACOS(argsList *list.List) (result string, err error) { if argsList.Len() != 1 { - err = errors.New("ACOS requires 1 numeric arguments") + err = errors.New("ACOS requires 1 numeric argument") return } var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } result = fmt.Sprintf("%g", math.Acos(val)) @@ -537,11 +555,11 @@ func (fn *formulaFuncs) ACOS(argsList *list.List) (result string, err error) { // func (fn *formulaFuncs) ACOSH(argsList *list.List) (result string, err error) { if argsList.Len() != 1 { - err = errors.New("ACOSH requires 1 numeric arguments") + err = errors.New("ACOSH requires 1 numeric argument") return } var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } result = fmt.Sprintf("%g", math.Acosh(val)) @@ -556,11 +574,11 @@ func (fn *formulaFuncs) ACOSH(argsList *list.List) (result string, err error) { // func (fn *formulaFuncs) ACOT(argsList *list.List) (result string, err error) { if argsList.Len() != 1 { - err = errors.New("ACOT requires 1 numeric arguments") + err = errors.New("ACOT requires 1 numeric argument") return } var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } result = fmt.Sprintf("%g", math.Pi/2-math.Atan(val)) @@ -574,11 +592,11 @@ func (fn *formulaFuncs) ACOT(argsList *list.List) (result string, err error) { // func (fn *formulaFuncs) ACOTH(argsList *list.List) (result string, err error) { if argsList.Len() != 1 { - err = errors.New("ACOTH requires 1 numeric arguments") + err = errors.New("ACOTH requires 1 numeric argument") return } var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } result = fmt.Sprintf("%g", math.Atanh(1/val)) @@ -592,11 +610,11 @@ func (fn *formulaFuncs) ACOTH(argsList *list.List) (result string, err error) { // func (fn *formulaFuncs) ARABIC(argsList *list.List) (result string, err error) { if argsList.Len() != 1 { - err = errors.New("ARABIC requires 1 numeric arguments") + err = errors.New("ARABIC requires 1 numeric argument") return } val, last, prefix := 0.0, 0.0, 1.0 - for _, char := range argsList.Front().Value.(efp.Token).TValue { + for _, char := range argsList.Front().Value.(formulaArg).Value { digit := 0.0 switch char { case '-': @@ -643,11 +661,11 @@ func (fn *formulaFuncs) ARABIC(argsList *list.List) (result string, err error) { // func (fn *formulaFuncs) ASIN(argsList *list.List) (result string, err error) { if argsList.Len() != 1 { - err = errors.New("ASIN requires 1 numeric arguments") + err = errors.New("ASIN requires 1 numeric argument") return } var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } result = fmt.Sprintf("%g", math.Asin(val)) @@ -661,11 +679,11 @@ func (fn *formulaFuncs) ASIN(argsList *list.List) (result string, err error) { // func (fn *formulaFuncs) ASINH(argsList *list.List) (result string, err error) { if argsList.Len() != 1 { - err = errors.New("ASINH requires 1 numeric arguments") + err = errors.New("ASINH requires 1 numeric argument") return } var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } result = fmt.Sprintf("%g", math.Asinh(val)) @@ -680,11 +698,11 @@ func (fn *formulaFuncs) ASINH(argsList *list.List) (result string, err error) { // func (fn *formulaFuncs) ATAN(argsList *list.List) (result string, err error) { if argsList.Len() != 1 { - err = errors.New("ATAN requires 1 numeric arguments") + err = errors.New("ATAN requires 1 numeric argument") return } var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } result = fmt.Sprintf("%g", math.Atan(val)) @@ -698,11 +716,11 @@ func (fn *formulaFuncs) ATAN(argsList *list.List) (result string, err error) { // func (fn *formulaFuncs) ATANH(argsList *list.List) (result string, err error) { if argsList.Len() != 1 { - err = errors.New("ATANH requires 1 numeric arguments") + err = errors.New("ATANH requires 1 numeric argument") return } var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } result = fmt.Sprintf("%g", math.Atanh(val)) @@ -721,10 +739,10 @@ func (fn *formulaFuncs) ATAN2(argsList *list.List) (result string, err error) { return } var x, y float64 - if x, err = strconv.ParseFloat(argsList.Back().Value.(efp.Token).TValue, 64); err != nil { + if x, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { return } - if y, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + if y, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } result = fmt.Sprintf("%g", math.Atan2(x, y)) @@ -766,10 +784,10 @@ func (fn *formulaFuncs) BASE(argsList *list.List) (result string, err error) { } var number float64 var radix, minLength int - if number, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } - if radix, err = strconv.Atoi(argsList.Front().Next().Value.(efp.Token).TValue); err != nil { + if radix, err = strconv.Atoi(argsList.Front().Next().Value.(formulaArg).Value); err != nil { return } if radix < 2 || radix > 36 { @@ -777,7 +795,7 @@ func (fn *formulaFuncs) BASE(argsList *list.List) (result string, err error) { return } if argsList.Len() > 2 { - if minLength, err = strconv.Atoi(argsList.Back().Value.(efp.Token).TValue); err != nil { + if minLength, err = strconv.Atoi(argsList.Back().Value.(formulaArg).Value); err != nil { return } } @@ -804,14 +822,14 @@ func (fn *formulaFuncs) CEILING(argsList *list.List) (result string, err error) return } var number, significance float64 = 0, 1 - if number, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } if number < 0 { significance = -1 } if argsList.Len() > 1 { - if significance, err = strconv.ParseFloat(argsList.Back().Value.(efp.Token).TValue, 64); err != nil { + if significance, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { return } } @@ -846,14 +864,14 @@ func (fn *formulaFuncs) CEILINGMATH(argsList *list.List) (result string, err err return } var number, significance, mode float64 = 0, 1, 1 - if number, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } if number < 0 { significance = -1 } if argsList.Len() > 1 { - if significance, err = strconv.ParseFloat(argsList.Front().Next().Value.(efp.Token).TValue, 64); err != nil { + if significance, err = strconv.ParseFloat(argsList.Front().Next().Value.(formulaArg).Value, 64); err != nil { return } } @@ -862,7 +880,7 @@ func (fn *formulaFuncs) CEILINGMATH(argsList *list.List) (result string, err err return } if argsList.Len() > 2 { - if mode, err = strconv.ParseFloat(argsList.Back().Value.(efp.Token).TValue, 64); err != nil { + if mode, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { return } } @@ -894,7 +912,7 @@ func (fn *formulaFuncs) CEILINGPRECISE(argsList *list.List) (result string, err return } var number, significance float64 = 0, 1 - if number, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } if number < 0 { @@ -905,7 +923,7 @@ func (fn *formulaFuncs) CEILINGPRECISE(argsList *list.List) (result string, err return } if argsList.Len() > 1 { - if significance, err = strconv.ParseFloat(argsList.Back().Value.(efp.Token).TValue, 64); err != nil { + if significance, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { return } significance = math.Abs(significance) @@ -935,10 +953,10 @@ func (fn *formulaFuncs) COMBIN(argsList *list.List) (result string, err error) { return } var number, chosen, val float64 = 0, 0, 1 - if number, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } - if chosen, err = strconv.ParseFloat(argsList.Back().Value.(efp.Token).TValue, 64); err != nil { + if chosen, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { return } number, chosen = math.Trunc(number), math.Trunc(chosen) @@ -968,10 +986,10 @@ func (fn *formulaFuncs) COMBINA(argsList *list.List) (result string, err error) return } var number, chosen float64 - if number, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } - if chosen, err = strconv.ParseFloat(argsList.Back().Value.(efp.Token).TValue, 64); err != nil { + if chosen, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { return } number, chosen = math.Trunc(number), math.Trunc(chosen) @@ -984,15 +1002,11 @@ func (fn *formulaFuncs) COMBINA(argsList *list.List) (result string, err error) return } args := list.New() - args.PushBack(efp.Token{ - TValue: fmt.Sprintf("%g", number+chosen-1), - TType: efp.TokenTypeOperand, - TSubType: efp.TokenSubTypeNumber, + args.PushBack(formulaArg{ + Value: fmt.Sprintf("%g", number+chosen-1), }) - args.PushBack(efp.Token{ - TValue: fmt.Sprintf("%g", number-1), - TType: efp.TokenTypeOperand, - TSubType: efp.TokenSubTypeNumber, + args.PushBack(formulaArg{ + Value: fmt.Sprintf("%g", number-1), }) return fn.COMBIN(args) } @@ -1004,11 +1018,11 @@ func (fn *formulaFuncs) COMBINA(argsList *list.List) (result string, err error) // func (fn *formulaFuncs) COS(argsList *list.List) (result string, err error) { if argsList.Len() != 1 { - err = errors.New("COS requires 1 numeric arguments") + err = errors.New("COS requires 1 numeric argument") return } var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } result = fmt.Sprintf("%g", math.Cos(val)) @@ -1022,11 +1036,11 @@ func (fn *formulaFuncs) COS(argsList *list.List) (result string, err error) { // func (fn *formulaFuncs) COSH(argsList *list.List) (result string, err error) { if argsList.Len() != 1 { - err = errors.New("COSH requires 1 numeric arguments") + err = errors.New("COSH requires 1 numeric argument") return } var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } result = fmt.Sprintf("%g", math.Cosh(val)) @@ -1040,11 +1054,11 @@ func (fn *formulaFuncs) COSH(argsList *list.List) (result string, err error) { // func (fn *formulaFuncs) COT(argsList *list.List) (result string, err error) { if argsList.Len() != 1 { - err = errors.New("COT requires 1 numeric arguments") + err = errors.New("COT requires 1 numeric argument") return } var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } if val == 0 { @@ -1062,11 +1076,11 @@ func (fn *formulaFuncs) COT(argsList *list.List) (result string, err error) { // func (fn *formulaFuncs) COTH(argsList *list.List) (result string, err error) { if argsList.Len() != 1 { - err = errors.New("COTH requires 1 numeric arguments") + err = errors.New("COTH requires 1 numeric argument") return } var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } if val == 0 { @@ -1084,11 +1098,11 @@ func (fn *formulaFuncs) COTH(argsList *list.List) (result string, err error) { // func (fn *formulaFuncs) CSC(argsList *list.List) (result string, err error) { if argsList.Len() != 1 { - err = errors.New("CSC requires 1 numeric arguments") + err = errors.New("CSC requires 1 numeric argument") return } var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } if val == 0 { @@ -1099,6 +1113,297 @@ func (fn *formulaFuncs) CSC(argsList *list.List) (result string, err error) { return } +// CSCH function calculates the hyperbolic cosecant (csch) of a supplied +// angle. The syntax of the function is: +// +// CSCH(number) +// +func (fn *formulaFuncs) CSCH(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("CSCH requires 1 numeric argument") + return + } + var val float64 + if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + if val == 0 { + err = errors.New(formulaErrorNAME) + return + } + result = fmt.Sprintf("%g", 1/math.Sinh(val)) + return +} + +// 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) +// +func (fn *formulaFuncs) DECIMAL(argsList *list.List) (result string, err error) { + if argsList.Len() != 2 { + err = errors.New("DECIMAL requires 2 numeric arguments") + return + } + var text = argsList.Front().Value.(formulaArg).Value + var radix int + if radix, err = strconv.Atoi(argsList.Back().Value.(formulaArg).Value); err != nil { + return + } + if len(text) > 2 && (strings.HasPrefix(text, "0x") || strings.HasPrefix(text, "0X")) { + text = text[2:] + } + val, err := strconv.ParseInt(text, radix, 64) + if err != nil { + err = errors.New(formulaErrorNUM) + return + } + result = fmt.Sprintf("%g", float64(val)) + return +} + +// DEGREES function converts radians into degrees. The syntax of the function +// is: +// +// DEGREES(angle) +// +func (fn *formulaFuncs) DEGREES(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("DEGREES requires 1 numeric argument") + return + } + var val float64 + if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + if val == 0 { + err = errors.New(formulaErrorNAME) + return + } + result = fmt.Sprintf("%g", 180.0/math.Pi*val) + return +} + +// EVEN function rounds a supplied number away from zero (i.e. rounds a +// positive number up and a negative number down), to the next even number. +// The syntax of the function is: +// +// EVEN(number) +// +func (fn *formulaFuncs) EVEN(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("EVEN requires 1 numeric argument") + return + } + var number float64 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + sign := math.Signbit(number) + m, frac := math.Modf(number / 2) + val := m * 2 + if frac != 0 { + if !sign { + val += 2 + } else { + val -= 2 + } + } + result = fmt.Sprintf("%g", val) + return +} + +// 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) +// +func (fn *formulaFuncs) EXP(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("EXP requires 1 numeric argument") + return + } + var number float64 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + result = strings.ToUpper(fmt.Sprintf("%g", math.Exp(number))) + return +} + +// fact returns the factorial of a supplied number. +func fact(number float64) float64 { + val := float64(1) + for i := float64(2); i <= number; i++ { + val *= i + } + return val +} + +// FACT function returns the factorial of a supplied number. The syntax of the +// function is: +// +// FACT(number) +// +func (fn *formulaFuncs) FACT(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("FACT requires 1 numeric argument") + return + } + var number float64 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + if number < 0 { + err = errors.New(formulaErrorNUM) + } + result = strings.ToUpper(fmt.Sprintf("%g", fact(number))) + return +} + +// FACTDOUBLE function returns the double factorial of a supplied number. The +// syntax of the function is: +// +// FACTDOUBLE(number) +// +func (fn *formulaFuncs) FACTDOUBLE(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("FACTDOUBLE requires 1 numeric argument") + return + } + var number, val float64 = 0, 1 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + if number < 0 { + err = errors.New(formulaErrorNUM) + } + for i := math.Trunc(number); i > 1; i -= 2 { + val *= i + } + result = strings.ToUpper(fmt.Sprintf("%g", val)) + return +} + +// 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) +// +func (fn *formulaFuncs) FLOOR(argsList *list.List) (result string, err error) { + if argsList.Len() != 2 { + err = errors.New("FLOOR requires 2 numeric arguments") + return + } + var number, significance float64 = 0, 1 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + if significance, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + return + } + if significance < 0 && number >= 0 { + err = errors.New(formulaErrorNUM) + } + val := number + val, res := math.Modf(val / significance) + if res != 0 { + if number < 0 && res < 0 { + val-- + } + } + result = strings.ToUpper(fmt.Sprintf("%g", val*significance)) + return +} + +// FLOORMATH function rounds a supplied number down to a supplied multiple of +// significance. The syntax of the function is: +// +// FLOOR.MATH(number,[significance],[mode]) +// +func (fn *formulaFuncs) FLOORMATH(argsList *list.List) (result string, err error) { + if argsList.Len() == 0 { + err = errors.New("FLOOR.MATH requires at least 1 argument") + return + } + if argsList.Len() > 3 { + err = errors.New("FLOOR.MATH allows at most 3 arguments") + return + } + var number, significance, mode float64 = 0, 1, 1 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + if number < 0 { + significance = -1 + } + if argsList.Len() > 1 { + if significance, err = strconv.ParseFloat(argsList.Front().Next().Value.(formulaArg).Value, 64); err != nil { + return + } + } + if argsList.Len() == 1 { + result = fmt.Sprintf("%g", math.Floor(number)) + return + } + if argsList.Len() > 2 { + if mode, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + return + } + } + val, res := math.Modf(number / significance) + if res != 0 && number < 0 && mode > 0 { + val-- + } + result = fmt.Sprintf("%g", val*significance) + return +} + +// FLOORPRECISE function rounds a supplied number down to a supplied multiple +// of significance. The syntax of the function is: +// +// FLOOR.PRECISE(number,[significance]) +// +func (fn *formulaFuncs) FLOORPRECISE(argsList *list.List) (result string, err error) { + if argsList.Len() == 0 { + err = errors.New("FLOOR.PRECISE requires at least 1 argument") + return + } + if argsList.Len() > 2 { + err = errors.New("FLOOR.PRECISE allows at most 2 arguments") + return + } + var number, significance float64 = 0, 1 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + if number < 0 { + significance = -1 + } + if argsList.Len() == 1 { + result = fmt.Sprintf("%g", math.Floor(number)) + return + } + if argsList.Len() > 1 { + if significance, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + return + } + significance = math.Abs(significance) + if significance == 0 { + result = "0" + return + } + } + val, res := math.Modf(number / significance) + if res != 0 { + if number < 0 { + val-- + } + } + result = fmt.Sprintf("%g", val*significance) + return +} + // GCD function returns the greatest common divisor of two or more supplied // integers. The syntax of the function is: // @@ -1114,11 +1419,11 @@ func (fn *formulaFuncs) GCD(argsList *list.List) (result string, err error) { nums = []float64{} ) for arg := argsList.Front(); arg != nil; arg = arg.Next() { - token := arg.Value.(efp.Token) - if token.TValue == "" { + token := arg.Value.(formulaArg).Value + if token == "" { continue } - if val, err = strconv.ParseFloat(token.TValue, 64); err != nil { + if val, err = strconv.ParseFloat(token, 64); err != nil { return } nums = append(nums, val) @@ -1143,6 +1448,74 @@ func (fn *formulaFuncs) GCD(argsList *list.List) (result string, err error) { return } +// INT function truncates a supplied number down to the closest integer. The +// syntax of the function is: +// +// INT(number) +// +func (fn *formulaFuncs) INT(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("INT requires 1 numeric argument") + return + } + var number float64 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + val, frac := math.Modf(number) + if frac < 0 { + val-- + } + result = fmt.Sprintf("%g", val) + return +} + +// ISOCEILING function rounds a supplied number up (regardless of the number's +// sign), to the nearest multiple of a supplied significance. The syntax of +// the function is: +// +// ISO.CEILING(number,[significance]) +// +func (fn *formulaFuncs) ISOCEILING(argsList *list.List) (result string, err error) { + if argsList.Len() == 0 { + err = errors.New("ISO.CEILING requires at least 1 argument") + return + } + if argsList.Len() > 2 { + err = errors.New("ISO.CEILING allows at most 2 arguments") + return + } + var number, significance float64 = 0, 1 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + if number < 0 { + significance = -1 + } + if argsList.Len() == 1 { + result = fmt.Sprintf("%g", math.Ceil(number)) + return + } + if argsList.Len() > 1 { + if significance, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + return + } + significance = math.Abs(significance) + if significance == 0 { + result = "0" + return + } + } + val, res := math.Modf(number / significance) + if res != 0 { + if number > 0 { + val++ + } + } + result = fmt.Sprintf("%g", val*significance) + return +} + // lcm returns the least common multiple of two supplied integers. func lcm(a, b float64) float64 { a = math.Trunc(a) @@ -1168,11 +1541,11 @@ func (fn *formulaFuncs) LCM(argsList *list.List) (result string, err error) { nums = []float64{} ) for arg := argsList.Front(); arg != nil; arg = arg.Next() { - token := arg.Value.(efp.Token) - if token.TValue == "" { + token := arg.Value.(formulaArg).Value + if token == "" { continue } - if val, err = strconv.ParseFloat(token.TValue, 64); err != nil { + if val, err = strconv.ParseFloat(token, 64); err != nil { return } nums = append(nums, val) @@ -1197,6 +1570,151 @@ func (fn *formulaFuncs) LCM(argsList *list.List) (result string, err error) { return } +// LN function calculates the natural logarithm of a given number. The syntax +// of the function is: +// +// LN(number) +// +func (fn *formulaFuncs) LN(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("LN requires 1 numeric argument") + return + } + var number float64 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + result = fmt.Sprintf("%g", math.Log(number)) + return +} + +// LOG function calculates the logarithm of a given number, to a supplied +// base. The syntax of the function is: +// +// LOG(number,[base]) +// +func (fn *formulaFuncs) LOG(argsList *list.List) (result string, err error) { + if argsList.Len() == 0 { + err = errors.New("LOG requires at least 1 argument") + return + } + if argsList.Len() > 2 { + err = errors.New("LOG allows at most 2 arguments") + return + } + var number, base float64 = 0, 10 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + if argsList.Len() > 1 { + if base, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + return + } + } + if number == 0 { + err = errors.New(formulaErrorNUM) + return + } + if base == 0 { + err = errors.New(formulaErrorNUM) + return + } + if base == 1 { + err = errors.New(formulaErrorDIV) + return + } + result = fmt.Sprintf("%g", math.Log(number)/math.Log(base)) + return +} + +// LOG10 function calculates the base 10 logarithm of a given number. The +// syntax of the function is: +// +// LOG10(number) +// +func (fn *formulaFuncs) LOG10(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("LOG10 requires 1 numeric argument") + return + } + var number float64 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + result = fmt.Sprintf("%g", math.Log10(number)) + return +} + +func minor(sqMtx [][]float64, idx int) [][]float64 { + ret := [][]float64{} + for i := range sqMtx { + if i == 0 { + continue + } + row := []float64{} + for j := range sqMtx { + if j == idx { + continue + } + row = append(row, sqMtx[i][j]) + } + ret = append(ret, row) + } + return ret +} + +// det determinant of the 2x2 matrix. +func det(sqMtx [][]float64) float64 { + if len(sqMtx) == 2 { + m00 := sqMtx[0][0] + m01 := sqMtx[0][1] + m10 := sqMtx[1][0] + m11 := sqMtx[1][1] + return m00*m11 - m10*m01 + } + var res, sgn float64 = 0, 1 + for j := range sqMtx { + res += sgn * sqMtx[0][j] * det(minor(sqMtx, j)) + sgn *= -1 + } + return res +} + +// MDETERM calculates the determinant of a square matrix. The +// syntax of the function is: +// +// MDETERM(array) +// +func (fn *formulaFuncs) MDETERM(argsList *list.List) (result string, err error) { + var num float64 + var rows int + var numMtx = [][]float64{} + var strMtx = [][]string{} + for arg := argsList.Front(); arg != nil; arg = arg.Next() { + if len(arg.Value.(formulaArg).Matrix) == 0 { + break + } + strMtx = append(strMtx, arg.Value.(formulaArg).Matrix) + rows++ + } + for _, row := range strMtx { + if len(row) != rows { + err = errors.New(formulaErrorVALUE) + return + } + numRow := []float64{} + for _, ele := range row { + if num, err = strconv.ParseFloat(ele, 64); err != nil { + return + } + numRow = append(numRow, num) + } + numMtx = append(numMtx, numRow) + } + result = fmt.Sprintf("%g", det(numMtx)) + return +} + // POWER function calculates a given number, raised to a supplied power. // The syntax of the function is: // @@ -1208,10 +1726,10 @@ func (fn *formulaFuncs) POWER(argsList *list.List) (result string, err error) { return } var x, y float64 - if x, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + if x, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } - if y, err = strconv.ParseFloat(argsList.Back().Value.(efp.Token).TValue, 64); err != nil { + if y, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { return } if x == 0 && y == 0 { @@ -1234,11 +1752,11 @@ func (fn *formulaFuncs) POWER(argsList *list.List) (result string, err error) { func (fn *formulaFuncs) PRODUCT(argsList *list.List) (result string, err error) { var val, product float64 = 0, 1 for arg := argsList.Front(); arg != nil; arg = arg.Next() { - token := arg.Value.(efp.Token) - if token.TValue == "" { + token := arg.Value.(formulaArg) + if token.Value == "" { continue } - if val, err = strconv.ParseFloat(token.TValue, 64); err != nil { + if val, err = strconv.ParseFloat(token.Value, 64); err != nil { return } product = product * val @@ -1256,11 +1774,11 @@ func (fn *formulaFuncs) PRODUCT(argsList *list.List) (result string, err error) // func (fn *formulaFuncs) SIGN(argsList *list.List) (result string, err error) { if argsList.Len() != 1 { - err = errors.New("SIGN requires 1 numeric arguments") + err = errors.New("SIGN requires 1 numeric argument") return } var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } if val < 0 { @@ -1282,18 +1800,23 @@ func (fn *formulaFuncs) SIGN(argsList *list.List) (result string, err error) { // func (fn *formulaFuncs) SQRT(argsList *list.List) (result string, err error) { if argsList.Len() != 1 { - err = errors.New("SQRT requires 1 numeric arguments") + err = errors.New("SQRT requires 1 numeric argument") return } - var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + var res float64 + var value = argsList.Front().Value.(formulaArg).Value + if value == "" { + result = "0" return } - if val < 0 { + if res, err = strconv.ParseFloat(value, 64); err != nil { + return + } + if res < 0 { err = errors.New(formulaErrorNUM) return } - result = fmt.Sprintf("%g", math.Sqrt(val)) + result = fmt.Sprintf("%g", math.Sqrt(res)) return } @@ -1305,11 +1828,11 @@ func (fn *formulaFuncs) SQRT(argsList *list.List) (result string, err error) { func (fn *formulaFuncs) SUM(argsList *list.List) (result string, err error) { var val, sum float64 for arg := argsList.Front(); arg != nil; arg = arg.Next() { - token := arg.Value.(efp.Token) - if token.TValue == "" { + token := arg.Value.(formulaArg) + if token.Value == "" { continue } - if val, err = strconv.ParseFloat(token.TValue, 64); err != nil { + if val, err = strconv.ParseFloat(token.Value, 64); err != nil { return } sum += val @@ -1329,10 +1852,10 @@ func (fn *formulaFuncs) QUOTIENT(argsList *list.List) (result string, err error) return } var x, y float64 - if x, err = strconv.ParseFloat(argsList.Front().Value.(efp.Token).TValue, 64); err != nil { + if x, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } - if y, err = strconv.ParseFloat(argsList.Back().Value.(efp.Token).TValue, 64); err != nil { + if y, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { return } if y == 0 { diff --git a/calc_test.go b/calc_test.go index 5d441ff6c1..c66de8cc85 100644 --- a/calc_test.go +++ b/calc_test.go @@ -14,6 +14,7 @@ func TestCalcCellValue(t *testing.T) { f.SetCellValue("Sheet1", "A3", 3) f.SetCellValue("Sheet1", "A4", 0) f.SetCellValue("Sheet1", "B1", 4) + f.SetCellValue("Sheet1", "B2", 5) return f } @@ -115,22 +116,108 @@ func TestCalcCellValue(t *testing.T) { "=COSH(0.5)": "1.1276259652063807", "=COSH(-2)": "3.7621956910836314", // _xlfn.COT - "_xlfn.COT(0.785398163397448)": "0.9999999999999992", + "=_xlfn.COT(0.785398163397448)": "0.9999999999999992", // _xlfn.COTH - "_xlfn.COTH(-3.14159265358979)": "-0.9962720762207499", + "=_xlfn.COTH(-3.14159265358979)": "-0.9962720762207499", // _xlfn.CSC - "_xlfn.CSC(-6)": "3.5788995472544056", - "_xlfn.CSC(1.5707963267949)": "1", + "=_xlfn.CSC(-6)": "3.5788995472544056", + "=_xlfn.CSC(1.5707963267949)": "1", + // _xlfn.CSCH + "=_xlfn.CSCH(-3.14159265358979)": "-0.08658953753004724", + // _xlfn.DECIMAL + `=_xlfn.DECIMAL("1100",2)`: "12", + `=_xlfn.DECIMAL("186A0",16)`: "100000", + `=_xlfn.DECIMAL("31L0",32)`: "100000", + `=_xlfn.DECIMAL("70122",8)`: "28754", + // DEGREES + "=DEGREES(1)": "57.29577951308232", + "=DEGREES(2.5)": "143.2394487827058", + // EVEN + "=EVEN(23)": "24", + "=EVEN(2.22)": "4", + "=EVEN(0)": "0", + "=EVEN(-0.3)": "-2", + "=EVEN(-11)": "-12", + "=EVEN(-4)": "-4", + // EXP + "=EXP(100)": "2.6881171418161356E+43", + "=EXP(0.1)": "1.1051709180756477", + "=EXP(0)": "1", + "=EXP(-5)": "0.006737946999085467", + // FACT + "=FACT(3)": "6", + "=FACT(6)": "720", + "=FACT(10)": "3.6288E+06", + // FACTDOUBLE + "=FACTDOUBLE(5)": "15", + "=FACTDOUBLE(8)": "384", + "=FACTDOUBLE(13)": "135135", + // FLOOR + "=FLOOR(26.75,0.1)": "26.700000000000003", + "=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,-1)": "-26", + "=FLOOR(-26.75,-5)": "-25", + // _xlfn.FLOOR.MATH + "=_xlfn.FLOOR.MATH(58.55)": "58", + "=_xlfn.FLOOR.MATH(58.55,0.1)": "58.5", + "=_xlfn.FLOOR.MATH(58.55,5)": "55", + "=_xlfn.FLOOR.MATH(58.55,1,1)": "58", + "=_xlfn.FLOOR.MATH(-58.55,1)": "-59", + "=_xlfn.FLOOR.MATH(-58.55,1,-1)": "-58", + "=_xlfn.FLOOR.MATH(-58.55,1,1)": "-59", // should be -58 + "=_xlfn.FLOOR.MATH(-58.55,10)": "-60", + // _xlfn.FLOOR.PRECISE + "=_xlfn.FLOOR.PRECISE(26.75,0.1)": "26.700000000000003", + "=_xlfn.FLOOR.PRECISE(26.75,0.5)": "26.5", + "=_xlfn.FLOOR.PRECISE(26.75,1)": "26", + "=_xlfn.FLOOR.PRECISE(26.75)": "26", + "=_xlfn.FLOOR.PRECISE(26.75,10)": "20", + "=_xlfn.FLOOR.PRECISE(26.75,0)": "0", + "=_xlfn.FLOOR.PRECISE(-26.75,1)": "-27", + "=_xlfn.FLOOR.PRECISE(-26.75,-1)": "-27", + "=_xlfn.FLOOR.PRECISE(-26.75,-5)": "-30", // GCD "=GCD(1,5)": "1", "=GCD(15,10,25)": "5", "=GCD(0,8,12)": "4", "=GCD(7,2)": "1", + // INT + "=INT(100.9)": "100", + "=INT(5.22)": "5", + "=INT(5.99)": "5", + "=INT(-6.1)": "-7", + "=INT(-100.9)": "-101", + // ISO.CEILING + "=ISO.CEILING(22.25)": "23", + "=ISO.CEILING(22.25,1)": "23", + "=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,5)": "-20", // LCM "=LCM(1,5)": "5", "=LCM(15,10,25)": "150", "=LCM(1,8,12)": "24", "=LCM(7,2)": "14", + // LN + "=LN(1)": "0", + "=LN(100)": "4.605170185988092", + "=LN(0.5)": "-0.6931471805599453", + // LOG + "=LOG(64,2)": "6", + "=LOG(100)": "2", + "=LOG(4,0.5)": "-2", + "=LOG(500)": "2.6989700043360183", + // LOG10 + "=LOG10(100)": "2", + "=LOG10(1000)": "3", + "=LOG10(0.001)": "-3", + "=LOG10(25)": "1.3979400086720375", // POWER "=POWER(4,2)": "16", // PRODUCT @@ -171,26 +258,26 @@ func TestCalcCellValue(t *testing.T) { } mathCalcError := map[string]string{ // ABS - "=ABS()": "ABS requires 1 numeric arguments", + "=ABS()": "ABS requires 1 numeric argument", "=ABS(~)": `cannot convert cell "~" to coordinates: invalid cell name "~"`, // ACOS - "=ACOS()": "ACOS requires 1 numeric arguments", + "=ACOS()": "ACOS requires 1 numeric argument", // ACOSH - "=ACOSH()": "ACOSH requires 1 numeric arguments", + "=ACOSH()": "ACOSH requires 1 numeric argument", // _xlfn.ACOT - "=_xlfn.ACOT()": "ACOT requires 1 numeric arguments", + "=_xlfn.ACOT()": "ACOT requires 1 numeric argument", // _xlfn.ACOTH - "=_xlfn.ACOTH()": "ACOTH requires 1 numeric arguments", + "=_xlfn.ACOTH()": "ACOTH requires 1 numeric argument", // _xlfn.ARABIC - "=_xlfn.ARABIC()": "ARABIC requires 1 numeric arguments", + "=_xlfn.ARABIC()": "ARABIC requires 1 numeric argument", // ASIN - "=ASIN()": "ASIN requires 1 numeric arguments", + "=ASIN()": "ASIN requires 1 numeric argument", // ASINH - "=ASINH()": "ASINH requires 1 numeric arguments", + "=ASINH()": "ASINH requires 1 numeric argument", // ATAN - "=ATAN()": "ATAN requires 1 numeric arguments", + "=ATAN()": "ATAN requires 1 numeric argument", // ATANH - "=ATANH()": "ATANH requires 1 numeric arguments", + "=ATANH()": "ATANH requires 1 numeric argument", // ATAN2 "=ATAN2()": "ATAN2 requires 2 numeric arguments", // BASE @@ -215,33 +302,75 @@ func TestCalcCellValue(t *testing.T) { "=_xlfn.COMBINA(-1,1)": "COMBINA requires number > number_chosen", "=_xlfn.COMBINA(-1,-1)": "COMBIN requires number >= number_chosen", // COS - "=COS()": "COS requires 1 numeric arguments", + "=COS()": "COS requires 1 numeric argument", // COSH - "=COSH()": "COSH requires 1 numeric arguments", + "=COSH()": "COSH requires 1 numeric argument", // _xlfn.COT - "=COT()": "COT requires 1 numeric arguments", + "=COT()": "COT requires 1 numeric argument", // _xlfn.COTH - "=COTH()": "COTH requires 1 numeric arguments", + "=COTH()": "COTH requires 1 numeric argument", // _xlfn.CSC - "_xlfn.CSC()": "CSC requires 1 numeric arguments", - "_xlfn.CSC(0)": "#NAME?", + "=_xlfn.CSC()": "CSC requires 1 numeric argument", + "=_xlfn.CSC(0)": "#NAME?", + // _xlfn.CSCH + "=_xlfn.CSCH()": "CSCH requires 1 numeric argument", + "=_xlfn.CSCH(0)": "#NAME?", + // _xlfn.DECIMAL + "=_xlfn.DECIMAL()": "DECIMAL requires 2 numeric arguments", + `=_xlfn.DECIMAL("2000", 2)`: "#NUM!", + // DEGREES + "=DEGREES()": "DEGREES requires 1 numeric argument", + // EVEN + "=EVEN()": "EVEN requires 1 numeric argument", + // EXP + "=EXP()": "EXP requires 1 numeric argument", + // FACT + "=FACT()": "FACT requires 1 numeric argument", + "=FACT(-1)": "#NUM!", + // FACTDOUBLE + "=FACTDOUBLE()": "FACTDOUBLE requires 1 numeric argument", + "=FACTDOUBLE(-1)": "#NUM!", + // FLOOR + "=FLOOR()": "FLOOR requires 2 numeric arguments", + "=FLOOR(1,-1)": "#NUM!", + // _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.PRECISE + "=_xlfn.FLOOR.PRECISE()": "FLOOR.PRECISE requires at least 1 argument", + "=_xlfn.FLOOR.PRECISE(1,2,3)": "FLOOR.PRECISE allows at most 2 arguments", // GCD "=GCD()": "GCD requires at least 1 argument", "=GCD(-1)": "GCD only accepts positive arguments", "=GCD(1,-1)": "GCD only accepts positive arguments", + // INT + "=INT()": "INT requires 1 numeric argument", + // ISO.CEILING + "=ISO.CEILING()": "ISO.CEILING requires at least 1 argument", + "=ISO.CEILING(1,2,3)": "ISO.CEILING allows at most 2 arguments", // LCM "=LCM()": "LCM requires at least 1 argument", "=LCM(-1)": "LCM only accepts positive arguments", "=LCM(1,-1)": "LCM only accepts positive arguments", + // LN + "=LN()": "LN requires 1 numeric argument", + // LOG + "=LOG()": "LOG requires at least 1 argument", + "=LOG(1,2,3)": "LOG allows at most 2 arguments", + "=LOG(0,0)": "#NUM!", + "=LOG(1,0)": "#NUM!", + "=LOG(1,1)": "#DIV/0!", + // LOG10 + "=LOG10()": "LOG10 requires 1 numeric argument", // POWER "=POWER(0,0)": "#NUM!", "=POWER(0,-1)": "#DIV/0!", "=POWER(1)": "POWER requires 2 numeric arguments", // SIGN - "=SIGN()": "SIGN requires 1 numeric arguments", + "=SIGN()": "SIGN requires 1 numeric argument", // SQRT "=SQRT(-1)": "#NUM!", - "=SQRT(1,2)": "SQRT requires 1 numeric arguments", + "=SQRT(1,2)": "SQRT requires 1 numeric argument", // QUOTIENT "=QUOTIENT(1,0)": "#DIV/0!", "=QUOTIENT(1)": "QUOTIENT requires 2 numeric arguments", @@ -255,6 +384,8 @@ func TestCalcCellValue(t *testing.T) { } referenceCalc := map[string]string{ + // MDETERM + "=MDETERM(A1:B2)": "-3", // PRODUCT "=PRODUCT(Sheet1!A1:Sheet1!A1:A2,A2)": "4", // SUM @@ -277,6 +408,9 @@ func TestCalcCellValue(t *testing.T) { } referenceCalcError := map[string]string{ + // MDETERM + "=MDETERM(A1:B3)": "#VALUE!", + // SUM "=1+SUM(SUM(A1+A2/A4)*(2-3),2)": "#DIV/0!", } for formula, expected := range referenceCalcError { From 1f73a19e0f3bff9869e333675957dd5c027d0ab9 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 6 May 2020 00:32:53 +0800 Subject: [PATCH 229/957] Resolve #628, omit number format empty --- xmlStyles.go | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/xmlStyles.go b/xmlStyles.go index d6aa4f9f61..b5ec41db95 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -186,12 +186,12 @@ type xlsxCellStyles struct { // workbook. type xlsxCellStyle struct { XMLName xml.Name `xml:"cellStyle"` - BuiltInID *int `xml:"builtinId,attr,omitempty"` - CustomBuiltIn *bool `xml:"customBuiltin,attr,omitempty"` - Hidden *bool `xml:"hidden,attr,omitempty"` - ILevel *bool `xml:"iLevel,attr,omitempty"` Name string `xml:"name,attr"` XfID int `xml:"xfId,attr"` + BuiltInID *int `xml:"builtinId,attr,omitempty"` + ILevel *int `xml:"iLevel,attr,omitempty"` + Hidden *bool `xml:"hidden,attr,omitempty"` + CustomBuiltIn *bool `xml:"customBuiltin,attr,omitempty"` } // xlsxCellStyleXfs directly maps the cellStyleXfs element. This element @@ -209,19 +209,19 @@ type xlsxCellStyleXfs struct { // xlsxXf directly maps the xf element. A single xf element describes all of the // formatting for a cell. type xlsxXf struct { - ApplyAlignment bool `xml:"applyAlignment,attr"` - ApplyBorder bool `xml:"applyBorder,attr"` - ApplyFill bool `xml:"applyFill,attr"` - ApplyFont bool `xml:"applyFont,attr"` - ApplyNumberFormat bool `xml:"applyNumberFormat,attr"` - ApplyProtection bool `xml:"applyProtection,attr"` - BorderID int `xml:"borderId,attr"` - FillID int `xml:"fillId,attr"` - FontID int `xml:"fontId,attr"` - NumFmtID int `xml:"numFmtId,attr"` - PivotButton bool `xml:"pivotButton,attr,omitempty"` + NumFmtID int `xml:"numFmtId,attr,omitempty"` + FontID int `xml:"fontId,attr,omitempty"` + FillID int `xml:"fillId,attr,omitempty"` + BorderID int `xml:"borderId,attr,omitempty"` + XfID *int `xml:"xfId,attr,omitempty"` QuotePrefix bool `xml:"quotePrefix,attr,omitempty"` - XfID *int `xml:"xfId,attr"` + PivotButton bool `xml:"pivotButton,attr,omitempty"` + ApplyNumberFormat bool `xml:"applyNumberFormat,attr,omitempty"` + ApplyFont bool `xml:"applyFont,attr,omitempty"` + ApplyFill bool `xml:"applyFill,attr,omitempty"` + ApplyBorder bool `xml:"applyBorder,attr,omitempty"` + ApplyAlignment bool `xml:"applyAlignment,attr,omitempty"` + ApplyProtection bool `xml:"applyProtection,attr,omitempty"` Alignment *xlsxAlignment `xml:"alignment"` Protection *xlsxProtection `xml:"protection"` } From de34ecaacee83633977298d0424e8eb5eaacb876 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 7 May 2020 00:15:54 +0800 Subject: [PATCH 230/957] #65 fn: MOD, MROUND, MULTINOMIAL, MUNIT, ODD, PI, RADIANS, RAND, RANDBETWEEN, ROMAN --- calc.go | 338 +++++++++++++++++++++++++++++++++++++++++++++++---- calc_test.go | 76 ++++++++++-- 2 files changed, 382 insertions(+), 32 deletions(-) diff --git a/calc.go b/calc.go index 2ab3d61ba2..fb94e27f6a 100644 --- a/calc.go +++ b/calc.go @@ -12,13 +12,16 @@ package excelize import ( + "bytes" "container/list" "errors" "fmt" "math" + "math/rand" "reflect" "strconv" "strings" + "time" "github.com/xuri/efp" ) @@ -1715,6 +1718,168 @@ func (fn *formulaFuncs) MDETERM(argsList *list.List) (result string, err error) return } +// MOD function returns the remainder of a division between two supplied +// numbers. The syntax of the function is: +// +// MOD(number,divisor) +// +func (fn *formulaFuncs) MOD(argsList *list.List) (result string, err error) { + if argsList.Len() != 2 { + err = errors.New("MOD requires 2 numeric arguments") + return + } + var number, divisor float64 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + if divisor, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + return + } + if divisor == 0 { + err = errors.New(formulaErrorDIV) + return + } + trunc, rem := math.Modf(number / divisor) + if rem < 0 { + trunc-- + } + result = fmt.Sprintf("%g", number-divisor*trunc) + return +} + +// MROUND function rounds a supplied number up or down to the nearest multiple +// of a given number. The syntax of the function is: +// +// MOD(number,multiple) +// +func (fn *formulaFuncs) MROUND(argsList *list.List) (result string, err error) { + if argsList.Len() != 2 { + err = errors.New("MROUND requires 2 numeric arguments") + return + } + var number, multiple float64 = 0, 1 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + if multiple, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + return + } + if multiple == 0 { + err = errors.New(formulaErrorNUM) + return + } + if multiple < 0 && number > 0 || + multiple > 0 && number < 0 { + err = errors.New(formulaErrorNUM) + return + } + number, res := math.Modf(number / multiple) + if math.Trunc(res+0.5) > 0 { + number++ + } + result = fmt.Sprintf("%g", number*multiple) + return +} + +// MULTINOMIAL function calculates the ratio of the factorial of a sum of +// supplied values to the product of factorials of those values. The syntax of +// the function is: +// +// MULTINOMIAL(number1,[number2],...) +// +func (fn *formulaFuncs) MULTINOMIAL(argsList *list.List) (result string, err error) { + var val, num, denom float64 = 0, 0, 1 + for arg := argsList.Front(); arg != nil; arg = arg.Next() { + token := arg.Value.(formulaArg) + if token.Value == "" { + continue + } + if val, err = strconv.ParseFloat(token.Value, 64); err != nil { + return + } + num += val + denom *= fact(val) + } + result = fmt.Sprintf("%g", fact(num)/denom) + return +} + +// MUNIT function returns the unit matrix for a specified dimension. The +// syntax of the function is: +// +// MUNIT(dimension) +// +func (fn *formulaFuncs) MUNIT(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("MUNIT requires 1 numeric argument") + return + } + var dimension int + if dimension, err = strconv.Atoi(argsList.Front().Value.(formulaArg).Value); err != nil { + return + } + matrix := make([][]float64, 0, dimension) + for i := 0; i < dimension; i++ { + row := make([]float64, dimension) + for j := 0; j < dimension; j++ { + if i == j { + row[j] = float64(1.0) + } else { + row[j] = float64(0.0) + } + } + matrix = append(matrix, row) + } + return +} + +// ODD function ounds a supplied number away from zero (i.e. rounds a positive +// number up and a negative number down), to the next odd number. The syntax +// of the function is: +// +// ODD(number) +// +func (fn *formulaFuncs) ODD(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("ODD requires 1 numeric argument") + return + } + var number float64 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + if number == 0 { + result = "1" + return + } + sign := math.Signbit(number) + m, frac := math.Modf((number - 1) / 2) + val := m*2 + 1 + if frac != 0 { + if !sign { + val += 2 + } else { + val -= 2 + } + } + result = fmt.Sprintf("%g", val) + return +} + +// PI function returns the value of the mathematical constant π (pi), accurate +// to 15 digits (14 decimal places). The syntax of the function is: +// +// PI() +// +func (fn *formulaFuncs) PI(argsList *list.List) (result string, err error) { + if argsList.Len() != 0 { + err = errors.New("PI accepts no arguments") + return + } + result = fmt.Sprintf("%g", math.Pi) + return +} + // POWER function calculates a given number, raised to a supplied power. // The syntax of the function is: // @@ -1765,6 +1930,154 @@ func (fn *formulaFuncs) PRODUCT(argsList *list.List) (result string, err error) return } +// QUOTIENT function returns the integer portion of a division between two +// supplied numbers. The syntax of the function is: +// +// QUOTIENT(numerator,denominator) +// +func (fn *formulaFuncs) QUOTIENT(argsList *list.List) (result string, err error) { + if argsList.Len() != 2 { + err = errors.New("QUOTIENT requires 2 numeric arguments") + return + } + var x, y float64 + if x, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + if y, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + return + } + if y == 0 { + err = errors.New(formulaErrorDIV) + return + } + result = fmt.Sprintf("%g", math.Trunc(x/y)) + return +} + +// RADIANS function converts radians into degrees. The syntax of the function is: +// +// RADIANS(angle) +// +func (fn *formulaFuncs) RADIANS(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("RADIANS requires 1 numeric argument") + return + } + var angle float64 + if angle, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + result = fmt.Sprintf("%g", math.Pi/180.0*angle) + return +} + +// RAND function generates a random real number between 0 and 1. The syntax of +// the function is: +// +// RAND() +// +func (fn *formulaFuncs) RAND(argsList *list.List) (result string, err error) { + if argsList.Len() != 0 { + err = errors.New("RAND accepts no arguments") + return + } + result = fmt.Sprintf("%g", rand.New(rand.NewSource(time.Now().UnixNano())).Float64()) + return +} + +// RANDBETWEEN function generates a random integer between two supplied +// integers. The syntax of the function is: +// +// RANDBETWEEN(bottom,top) +// +func (fn *formulaFuncs) RANDBETWEEN(argsList *list.List) (result string, err error) { + if argsList.Len() != 2 { + err = errors.New("RANDBETWEEN requires 2 numeric arguments") + return + } + var bottom, top int64 + if bottom, err = strconv.ParseInt(argsList.Front().Value.(formulaArg).Value, 10, 64); err != nil { + return + } + if top, err = strconv.ParseInt(argsList.Back().Value.(formulaArg).Value, 10, 64); err != nil { + return + } + if top < bottom { + err = errors.New(formulaErrorNUM) + return + } + result = fmt.Sprintf("%g", float64(rand.New(rand.NewSource(time.Now().UnixNano())).Int63n(top-bottom+1)+bottom)) + return +} + +// romanNumerals defined a numeral system that originated in ancient Rome and +// remained the usual way of writing numbers throughout Europe well into the +// Late Middle Ages. +type romanNumerals struct { + n float64 + s string +} + +var romanTable = [][]romanNumerals{{{1000, "M"}, {900, "CM"}, {500, "D"}, {400, "CD"}, {100, "C"}, {90, "XC"}, {50, "L"}, {40, "XL"}, {10, "X"}, {9, "IX"}, {5, "V"}, {4, "IV"}, {1, "I"}}, + {{1000, "M"}, {950, "LM"}, {900, "CM"}, {500, "D"}, {450, "LD"}, {400, "CD"}, {100, "C"}, {95, "VC"}, {90, "XC"}, {50, "L"}, {45, "VL"}, {40, "XL"}, {10, "X"}, {9, "IX"}, {5, "V"}, {4, "IV"}, {1, "I"}}, + {{1000, "M"}, {990, "XM"}, {950, "LM"}, {900, "CM"}, {500, "D"}, {490, "XD"}, {450, "LD"}, {400, "CD"}, {100, "C"}, {99, "IC"}, {90, "XC"}, {50, "L"}, {45, "VL"}, {40, "XL"}, {10, "X"}, {9, "IX"}, {5, "V"}, {4, "IV"}, {1, "I"}}, + {{1000, "M"}, {995, "VM"}, {990, "XM"}, {950, "LM"}, {900, "CM"}, {500, "D"}, {495, "VD"}, {490, "XD"}, {450, "LD"}, {400, "CD"}, {100, "C"}, {99, "IC"}, {90, "XC"}, {50, "L"}, {45, "VL"}, {40, "XL"}, {10, "X"}, {9, "IX"}, {5, "V"}, {4, "IV"}, {1, "I"}}, + {{1000, "M"}, {999, "IM"}, {995, "VM"}, {990, "XM"}, {950, "LM"}, {900, "CM"}, {500, "D"}, {499, "ID"}, {495, "VD"}, {490, "XD"}, {450, "LD"}, {400, "CD"}, {100, "C"}, {99, "IC"}, {90, "XC"}, {50, "L"}, {45, "VL"}, {40, "XL"}, {10, "X"}, {9, "IX"}, {5, "V"}, {4, "IV"}, {1, "I"}}} + +// ROMAN function converts an arabic number to Roman. I.e. for a supplied +// integer, the function returns a text string depicting the roman numeral +// form of the number. The syntax of the function is: +// +// ROMAN(number,[form]) +// +func (fn *formulaFuncs) ROMAN(argsList *list.List) (result string, err error) { + if argsList.Len() == 0 { + err = errors.New("ROMAN requires at least 1 argument") + return + } + if argsList.Len() > 2 { + err = errors.New("ROMAN allows at most 2 arguments") + return + } + var number float64 + var form int + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + if argsList.Len() > 1 { + if form, err = strconv.Atoi(argsList.Back().Value.(formulaArg).Value); err != nil { + return + } + if form < 0 { + form = 0 + } else if form > 4 { + form = 4 + } + } + decimalTable := romanTable[0] + switch form { + case 1: + decimalTable = romanTable[1] + case 2: + decimalTable = romanTable[2] + case 3: + decimalTable = romanTable[3] + case 4: + decimalTable = romanTable[4] + } + val := math.Trunc(number) + buf := bytes.Buffer{} + for _, r := range decimalTable { + for val >= r.n { + buf.WriteString(r.s) + val -= r.n + } + } + result = buf.String() + return +} + // 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 @@ -1840,28 +2153,3 @@ func (fn *formulaFuncs) SUM(argsList *list.List) (result string, err error) { result = fmt.Sprintf("%g", sum) return } - -// QUOTIENT function returns the integer portion of a division between two -// supplied numbers. The syntax of the function is: -// -// QUOTIENT(numerator,denominator) -// -func (fn *formulaFuncs) QUOTIENT(argsList *list.List) (result string, err error) { - if argsList.Len() != 2 { - err = errors.New("QUOTIENT requires 2 numeric arguments") - return - } - var x, y float64 - if x, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { - return - } - if y, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { - return - } - if y == 0 { - err = errors.New(formulaErrorDIV) - return - } - result = fmt.Sprintf("%g", math.Trunc(x/y)) - return -} diff --git a/calc_test.go b/calc_test.go index c66de8cc85..2b35e486c6 100644 --- a/calc_test.go +++ b/calc_test.go @@ -218,10 +218,54 @@ func TestCalcCellValue(t *testing.T) { "=LOG10(1000)": "3", "=LOG10(0.001)": "-3", "=LOG10(25)": "1.3979400086720375", + // MOD + "=MOD(6,4)": "2", + "=MOD(6,3)": "0", + "=MOD(6,2.5)": "1", + "=MOD(6,1.333)": "0.6680000000000001", + // MROUND + "=MROUND(333.7,0.5)": "333.5", + "=MROUND(333.8,1)": "334", + "=MROUND(333.3,2)": "334", + "=MROUND(555.3,400)": "400", + "=MROUND(555,1000)": "1000", + "=MROUND(-555.7,-1)": "-556", + "=MROUND(-555.4,-1)": "-555", + "=MROUND(-1555,-1000)": "-2000", + // MULTINOMIAL + "=MULTINOMIAL(3,1,2,5)": "27720", + // _xlfn.MUNIT + "=_xlfn.MUNIT(4)": "", // not support currently + // ODD + "=ODD(22)": "23", + "=ODD(1.22)": "3", + "=ODD(1.22+4)": "7", + "=ODD(0)": "1", + "=ODD(-1.3)": "-3", + "=ODD(-10)": "-11", + "=ODD(-3)": "-3", + // PI + "=PI()": "3.141592653589793", // POWER "=POWER(4,2)": "16", // PRODUCT "=PRODUCT(3,6)": "18", + // QUOTIENT + "=QUOTIENT(5,2)": "2", + "=QUOTIENT(4.5,3.1)": "1", + "=QUOTIENT(-10,3)": "-3", + // RADIANS + "=RADIANS(50)": "0.8726646259971648", + "=RADIANS(-180)": "-3.141592653589793", + "=RADIANS(180)": "3.141592653589793", + "=RADIANS(360)": "6.283185307179586", + // ROMAN + "=ROMAN(499,0)": "CDXCIX", + "=ROMAN(1999,0)": "MCMXCIX", + "=ROMAN(1999,1)": "MLMVLIV", + "=ROMAN(1999,2)": "MXMIX", + "=ROMAN(1999,3)": "MVMIV", + "=ROMAN(1999,4)": "MIM", // SIGN "=SIGN(9.5)": "1", "=SIGN(-9.5)": "-1", @@ -244,10 +288,6 @@ func TestCalcCellValue(t *testing.T) { "=((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", - // QUOTIENT - "=QUOTIENT(5, 2)": "2", - "=QUOTIENT(4.5, 3.1)": "1", - "=QUOTIENT(-10, 3)": "-3", } for formula, expected := range mathCalc { f := prepareData() @@ -362,18 +402,40 @@ func TestCalcCellValue(t *testing.T) { "=LOG(1,1)": "#DIV/0!", // LOG10 "=LOG10()": "LOG10 requires 1 numeric argument", + // MOD + "=MOD()": "MOD requires 2 numeric arguments", + "=MOD(6,0)": "#DIV/0!", + // MROUND + "=MROUND()": "MROUND requires 2 numeric arguments", + "=MROUND(1,0)": "#NUM!", + // _xlfn.MUNIT + "=_xlfn.MUNIT()": "MUNIT requires 1 numeric argument", // not support currently + // ODD + "=ODD()": "ODD requires 1 numeric argument", + // PI + "=PI(1)": "PI accepts no arguments", // POWER "=POWER(0,0)": "#NUM!", "=POWER(0,-1)": "#DIV/0!", "=POWER(1)": "POWER requires 2 numeric arguments", + // QUOTIENT + "=QUOTIENT(1,0)": "#DIV/0!", + "=QUOTIENT(1)": "QUOTIENT requires 2 numeric arguments", + // RADIANS + "=RADIANS()": "RADIANS requires 1 numeric argument", + // RAND + "=RAND(1)": "RAND accepts no arguments", + // RANDBETWEEN + "=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", // SIGN "=SIGN()": "SIGN requires 1 numeric argument", // SQRT "=SQRT(-1)": "#NUM!", "=SQRT(1,2)": "SQRT requires 1 numeric argument", - // QUOTIENT - "=QUOTIENT(1,0)": "#DIV/0!", - "=QUOTIENT(1)": "QUOTIENT requires 2 numeric arguments", } for formula, expected := range mathCalcError { f := prepareData() From 08185c398a0bf352662efa64ebb17c8746556969 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 8 May 2020 00:31:17 +0800 Subject: [PATCH 231/957] #65, fn: ROUND, ROUNDDOWN, ROUNDUP, SEC, SECH, SIN, SINH, SQRTPI, TAN, TANH, TRUNC --- calc.go | 260 +++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 81 +++++++++++++++- 2 files changed, 339 insertions(+), 2 deletions(-) diff --git a/calc.go b/calc.go index fb94e27f6a..cc39ba50e5 100644 --- a/calc.go +++ b/calc.go @@ -2078,6 +2078,141 @@ func (fn *formulaFuncs) ROMAN(argsList *list.List) (result string, err error) { return } +type roundMode byte + +const ( + closest roundMode = iota + down + up +) + +// round rounds a supplied number up or down. +func (fn *formulaFuncs) round(number, digits float64, mode roundMode) float64 { + significance := 1.0 + if digits > 0 { + significance = math.Pow(1/10.0, digits) + } else { + significance = math.Pow(10.0, -digits) + } + val, res := math.Modf(number / significance) + switch mode { + case closest: + const eps = 0.499999999 + if res >= eps { + val++ + } else if res <= -eps { + val-- + } + case down: + case up: + if res > 0 { + val++ + } else if res < 0 { + val-- + } + } + return val * significance +} + +// 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) +// +func (fn *formulaFuncs) ROUND(argsList *list.List) (result string, err error) { + if argsList.Len() != 2 { + err = errors.New("ROUND requires 2 numeric arguments") + return + } + var number, digits float64 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + if digits, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + return + } + result = fmt.Sprintf("%g", fn.round(number, digits, closest)) + return +} + +// 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) +// +func (fn *formulaFuncs) ROUNDDOWN(argsList *list.List) (result string, err error) { + if argsList.Len() != 2 { + err = errors.New("ROUNDDOWN requires 2 numeric arguments") + return + } + var number, digits float64 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + if digits, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + return + } + result = fmt.Sprintf("%g", fn.round(number, digits, down)) + return +} + +// 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) +// +func (fn *formulaFuncs) ROUNDUP(argsList *list.List) (result string, err error) { + if argsList.Len() != 2 { + err = errors.New("ROUNDUP requires 2 numeric arguments") + return + } + var number, digits float64 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + if digits, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + return + } + result = fmt.Sprintf("%g", fn.round(number, digits, up)) + return +} + +// SEC function calculates the secant of a given angle. The syntax of the +// function is: +// +// SEC(number) +// +func (fn *formulaFuncs) SEC(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("SEC requires 1 numeric argument") + return + } + var number float64 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + result = fmt.Sprintf("%g", math.Cos(number)) + return +} + +// SECH function calculates the hyperbolic secant (sech) of a supplied angle. +// The syntax of the function is: +// +// SECH(number) +// +func (fn *formulaFuncs) SECH(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("SECH requires 1 numeric argument") + return + } + var number float64 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + result = fmt.Sprintf("%g", 1/math.Cosh(number)) + return +} + // 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 @@ -2106,6 +2241,42 @@ func (fn *formulaFuncs) SIGN(argsList *list.List) (result string, err error) { return } +// SIN function calculates the sine of a given angle. The syntax of the +// function is: +// +// SIN(number) +// +func (fn *formulaFuncs) SIN(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("SIN requires 1 numeric argument") + return + } + var number float64 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + result = fmt.Sprintf("%g", math.Sin(number)) + return +} + +// SINH function calculates the hyperbolic sine (sinh) of a supplied number. +// The syntax of the function is: +// +// SINH(number) +// +func (fn *formulaFuncs) SINH(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("SINH requires 1 numeric argument") + return + } + var number float64 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + result = fmt.Sprintf("%g", math.Sinh(number)) + return +} + // SQRT function calculates the positive square root of a supplied number. The // syntax of the function is: // @@ -2133,6 +2304,24 @@ func (fn *formulaFuncs) SQRT(argsList *list.List) (result string, err error) { return } +// SQRTPI function returns the square root of a supplied number multiplied by +// the mathematical constant, π. The syntax of the function is: +// +// SQRTPI(number) +// +func (fn *formulaFuncs) SQRTPI(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("SQRTPI requires 1 numeric argument") + return + } + var number float64 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + result = fmt.Sprintf("%g", math.Sqrt(number*math.Pi)) + return +} + // SUM function adds together a supplied set of numbers and returns the sum of // these values. The syntax of the function is: // @@ -2153,3 +2342,74 @@ func (fn *formulaFuncs) SUM(argsList *list.List) (result string, err error) { result = fmt.Sprintf("%g", sum) return } + +// TAN function calculates the tangent of a given angle. The syntax of the +// function is: +// +// TAN(number) +// +func (fn *formulaFuncs) TAN(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("TAN requires 1 numeric argument") + return + } + var number float64 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + result = fmt.Sprintf("%g", math.Tan(number)) + return +} + +// TANH function calculates the hyperbolic tangent (tanh) of a supplied +// number. The syntax of the function is: +// +// TANH(number) +// +func (fn *formulaFuncs) TANH(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("TANH requires 1 numeric argument") + return + } + var number float64 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + result = fmt.Sprintf("%g", math.Tanh(number)) + return +} + +// TRUNC function truncates a supplied number to a specified number of decimal +// places. The syntax of the function is: +// +// TRUNC(number,[number_digits]) +// +func (fn *formulaFuncs) TRUNC(argsList *list.List) (result string, err error) { + if argsList.Len() == 0 { + err = errors.New("TRUNC requires at least 1 argument") + return + } + var number, digits, adjust, rtrim float64 + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + return + } + if argsList.Len() > 1 { + if digits, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + return + } + digits = math.Floor(digits) + } + adjust = math.Pow(10, digits) + x := int((math.Abs(number) - math.Abs(float64(int(number)))) * adjust) + if x != 0 { + if rtrim, err = strconv.ParseFloat(strings.TrimRight(strconv.Itoa(x), "0"), 64); err != nil { + return + } + } + if (digits > 0) && (rtrim < adjust/10) { + result = fmt.Sprintf("%g", number) + return + } + result = fmt.Sprintf("%g", float64(int(number*adjust))/adjust) + return +} diff --git a/calc_test.go b/calc_test.go index 2b35e486c6..6225d50ae5 100644 --- a/calc_test.go +++ b/calc_test.go @@ -266,14 +266,55 @@ func TestCalcCellValue(t *testing.T) { "=ROMAN(1999,2)": "MXMIX", "=ROMAN(1999,3)": "MVMIV", "=ROMAN(1999,4)": "MIM", + // ROUND + "=ROUND(100.319,1)": "100.30000000000001", + "=ROUND(5.28,1)": "5.300000000000001", + "=ROUND(5.9999,3)": "6.000000000000002", + "=ROUND(99.5,0)": "100", + "=ROUND(-6.3,0)": "-6", + "=ROUND(-100.5,0)": "-101", + "=ROUND(-22.45,1)": "-22.5", + "=ROUND(999,-1)": "1000", + "=ROUND(991,-1)": "990", + // ROUNDDOWN + "=ROUNDDOWN(99.999,1)": "99.9", + "=ROUNDDOWN(99.999,2)": "99.99000000000002", + "=ROUNDDOWN(99.999,0)": "99", + "=ROUNDDOWN(99.999,-1)": "90", + "=ROUNDDOWN(-99.999,2)": "-99.99000000000002", + "=ROUNDDOWN(-99.999,-1)": "-90", + // ROUNDUP + "=ROUNDUP(11.111,1)": "11.200000000000001", + "=ROUNDUP(11.111,2)": "11.120000000000003", + "=ROUNDUP(11.111,0)": "12", + "=ROUNDUP(11.111,-1)": "20", + "=ROUNDUP(-11.111,2)": "-11.120000000000003", + "=ROUNDUP(-11.111,-1)": "-20", + // SEC + "=_xlfn.SEC(-3.14159265358979)": "-1", + "=_xlfn.SEC(0)": "1", + // SECH + "=_xlfn.SECH(-3.14159265358979)": "0.0862667383340547", + "=_xlfn.SECH(0)": "1", // SIGN "=SIGN(9.5)": "1", "=SIGN(-9.5)": "-1", "=SIGN(0)": "0", "=SIGN(0.00000001)": "1", "=SIGN(6-7)": "-1", + // SIN + "=SIN(0.785398163)": "0.7071067809055092", + // SINH + "=SINH(0)": "0", + "=SINH(0.5)": "0.5210953054937474", + "=SINH(-2)": "-3.626860407847019", // SQRT "=SQRT(4)": "2", + // SQRTPI + "=SQRTPI(5)": "3.963327297606011", + "=SQRTPI(0.2)": "0.7926654595212022", + "=SQRTPI(100)": "17.72453850905516", + "=SQRTPI(0)": "0", // SUM "=SUM(1,2)": "3", "=SUM(1,2+3)": "6", @@ -288,6 +329,20 @@ func TestCalcCellValue(t *testing.T) { "=((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", + // TAN + "=TAN(1.047197551)": "1.732050806782486", + "=TAN(0)": "0", + // TANH + "=TANH(0)": "0", + "=TANH(0.5)": "0.46211715726000974", + "=TANH(-2)": "-0.9640275800758169", + // TRUNC + "=TRUNC(99.999,1)": "99.9", + "=TRUNC(99.999,2)": "99.99", + "=TRUNC(99.999)": "99", + "=TRUNC(99.999,-1)": "90", + "=TRUNC(-99.999,2)": "-99.99", + "=TRUNC(-99.999,-1)": "-90", } for formula, expected := range mathCalc { f := prepareData() @@ -431,11 +486,33 @@ func TestCalcCellValue(t *testing.T) { // ROMAN "=ROMAN()": "ROMAN requires at least 1 argument", "=ROMAN(1,2,3)": "ROMAN allows at most 2 arguments", + // ROUND + "=ROUND()": "ROUND requires 2 numeric arguments", + // ROUNDDOWN + "=ROUNDDOWN()": "ROUNDDOWN requires 2 numeric arguments", + // ROUNDUP + "=ROUNDUP()": "ROUNDUP requires 2 numeric arguments", + // SEC + "=_xlfn.SEC()": "SEC requires 1 numeric argument", + // _xlfn.SECH + "=_xlfn.SECH()": "SECH requires 1 numeric argument", // SIGN "=SIGN()": "SIGN requires 1 numeric argument", + // SIN + "=SIN()": "SIN requires 1 numeric argument", + // SINH + "=SINH()": "SINH requires 1 numeric argument", // SQRT - "=SQRT(-1)": "#NUM!", - "=SQRT(1,2)": "SQRT requires 1 numeric argument", + "=SQRT()": "SQRT requires 1 numeric argument", + "=SQRT(-1)": "#NUM!", + // SQRTPI + "=SQRTPI()": "SQRTPI requires 1 numeric argument", + // TAN + "=TAN()": "TAN requires 1 numeric argument", + // TANH + "=TANH()": "TANH requires 1 numeric argument", + // TRUNC + "=TRUNC()": "TRUNC requires at least 1 argument", } for formula, expected := range mathCalcError { f := prepareData() From 4188dc7a4a650200c697fd47ad28ba346bf786a6 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 9 May 2020 00:32:36 +0800 Subject: [PATCH 232/957] - fn: SUMSQ - resolve ineffectual assignment - handle exception with invalid formula - update range resolver --- calc.go | 123 +++++++++++++++++++++++++++++++++++++++++---------- calc_test.go | 22 ++++++--- 2 files changed, 116 insertions(+), 29 deletions(-) diff --git a/calc.go b/calc.go index cc39ba50e5..f85d16ad6a 100644 --- a/calc.go +++ b/calc.go @@ -259,12 +259,18 @@ func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error) } optStack.Pop() } + if opdStack.Len() == 0 { + return efp.Token{}, errors.New("formula not valid") + } return opdStack.Peek().(efp.Token), err } // calculate evaluate basic arithmetic operations. func calculate(opdStack *Stack, opt efp.Token) error { if opt.TValue == "-" && opt.TType == efp.TokenTypeOperatorPrefix { + if opdStack.Len() < 1 { + return errors.New("formula not valid") + } opd := opdStack.Pop().(efp.Token) opdVal, err := strconv.ParseFloat(opd.TValue, 64) if err != nil { @@ -274,6 +280,9 @@ func calculate(opdStack *Stack, opt efp.Token) error { opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) } if opt.TValue == "+" { + if opdStack.Len() < 2 { + return errors.New("formula not valid") + } rOpd := opdStack.Pop().(efp.Token) lOpd := opdStack.Pop().(efp.Token) lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64) @@ -288,6 +297,9 @@ func calculate(opdStack *Stack, opt efp.Token) error { opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) } if opt.TValue == "-" && opt.TType == efp.TokenTypeOperatorInfix { + if opdStack.Len() < 2 { + return errors.New("formula not valid") + } rOpd := opdStack.Pop().(efp.Token) lOpd := opdStack.Pop().(efp.Token) lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64) @@ -302,6 +314,9 @@ func calculate(opdStack *Stack, opt efp.Token) error { opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) } if opt.TValue == "*" { + if opdStack.Len() < 2 { + return errors.New("formula not valid") + } rOpd := opdStack.Pop().(efp.Token) lOpd := opdStack.Pop().(efp.Token) lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64) @@ -316,6 +331,9 @@ func calculate(opdStack *Stack, opt efp.Token) error { opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) } if opt.TValue == "/" { + if opdStack.Len() < 2 { + return errors.New("formula not valid") + } rOpd := opdStack.Pop().(efp.Token) lOpd := opdStack.Pop().(efp.Token) lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64) @@ -444,12 +462,13 @@ func (f *File) parseReference(sheet, reference string) (result []string, matrix } // rangeResolver extract value as string from given reference and range list. -// This function will not ignore the empty cell. Note that the result of 3D -// range references may be different from Excel in some cases, for example, -// A1:A2:A2:B3 in Excel will include B1, but we wont. +// 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) (result []string, matrix [][]string, err error) { + var fromRow, toRow, fromCol, toCol int = 1, 1, 1, 1 + var sheet string filter := map[string]string{} - // extract value from ranges + // prepare value range for temp := cellRanges.Front(); temp != nil; temp = temp.Next() { cr := temp.Value.(cellRange) if cr.From.Sheet != cr.To.Sheet { @@ -457,22 +476,59 @@ func (f *File) rangeResolver(cellRefs, cellRanges *list.List) (result []string, } rng := []int{cr.From.Col, cr.From.Row, cr.To.Col, cr.To.Row} sortCoordinates(rng) - matrix = [][]string{} - for row := rng[1]; row <= rng[3]; row++ { + if cr.From.Row < fromRow { + fromRow = cr.From.Row + } + if cr.From.Col < fromCol { + fromCol = cr.From.Col + } + if cr.To.Row > fromRow { + toRow = cr.To.Row + } + if cr.To.Col > toCol { + toCol = cr.To.Col + } + if cr.From.Sheet != "" { + sheet = cr.From.Sheet + } + } + for temp := cellRefs.Front(); temp != nil; temp = temp.Next() { + cr := temp.Value.(cellRef) + if cr.Sheet != "" { + sheet = cr.Sheet + } + if cr.Row < fromRow { + fromRow = cr.Row + } + if cr.Col < fromCol { + fromCol = cr.Col + } + if cr.Row > fromRow { + toRow = cr.Row + } + if cr.Col > toCol { + toCol = cr.Col + } + } + // extract value from ranges + if cellRanges.Len() > 0 { + for row := fromRow; row <= toRow; row++ { var matrixRow = []string{} - for col := rng[0]; col <= rng[2]; col++ { + for col := fromCol; col <= toCol; col++ { var cell, value string if cell, err = CoordinatesToCellName(col, row); err != nil { return } - if value, err = f.GetCellValue(cr.From.Sheet, cell); err != nil { + if value, err = f.GetCellValue(sheet, cell); err != nil { return } filter[cell] = value matrixRow = append(matrixRow, value) + result = append(result, value) } matrix = append(matrix, matrixRow) } + return } // extract value from references for temp := cellRefs.Front(); temp != nil; temp = temp.Next() { @@ -824,7 +880,7 @@ func (fn *formulaFuncs) CEILING(argsList *list.List) (result string, err error) err = errors.New("CEILING allows at most 2 arguments") return } - var number, significance float64 = 0, 1 + number, significance, res := 0.0, 1.0, 0.0 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } @@ -844,7 +900,7 @@ func (fn *formulaFuncs) CEILING(argsList *list.List) (result string, err error) result = fmt.Sprintf("%g", math.Ceil(number)) return } - number, res := math.Modf(number / significance) + number, res = math.Modf(number / significance) if res > 0 { number++ } @@ -866,7 +922,7 @@ func (fn *formulaFuncs) CEILINGMATH(argsList *list.List) (result string, err err err = errors.New("CEILING.MATH allows at most 3 arguments") return } - var number, significance, mode float64 = 0, 1, 1 + number, significance, mode := 0.0, 1.0, 1.0 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } @@ -914,7 +970,7 @@ func (fn *formulaFuncs) CEILINGPRECISE(argsList *list.List) (result string, err err = errors.New("CEILING.PRECISE allows at most 2 arguments") return } - var number, significance float64 = 0, 1 + number, significance := 0.0, 1.0 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } @@ -955,7 +1011,7 @@ func (fn *formulaFuncs) COMBIN(argsList *list.List) (result string, err error) { err = errors.New("COMBIN requires 2 argument") return } - var number, chosen, val float64 = 0, 0, 1 + number, chosen, val := 0.0, 0.0, 1.0 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } @@ -1274,7 +1330,7 @@ func (fn *formulaFuncs) FACTDOUBLE(argsList *list.List) (result string, err erro err = errors.New("FACTDOUBLE requires 1 numeric argument") return } - var number, val float64 = 0, 1 + number, val := 0.0, 1.0 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } @@ -1298,7 +1354,7 @@ func (fn *formulaFuncs) FLOOR(argsList *list.List) (result string, err error) { err = errors.New("FLOOR requires 2 numeric arguments") return } - var number, significance float64 = 0, 1 + var number, significance float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } @@ -1333,7 +1389,7 @@ func (fn *formulaFuncs) FLOORMATH(argsList *list.List) (result string, err error err = errors.New("FLOOR.MATH allows at most 3 arguments") return } - var number, significance, mode float64 = 0, 1, 1 + number, significance, mode := 0.0, 1.0, 1.0 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } @@ -1376,7 +1432,7 @@ func (fn *formulaFuncs) FLOORPRECISE(argsList *list.List) (result string, err er err = errors.New("FLOOR.PRECISE allows at most 2 arguments") return } - var number, significance float64 = 0, 1 + var number, significance float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } @@ -1488,7 +1544,7 @@ func (fn *formulaFuncs) ISOCEILING(argsList *list.List) (result string, err erro err = errors.New("ISO.CEILING allows at most 2 arguments") return } - var number, significance float64 = 0, 1 + var number, significance float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } @@ -1605,7 +1661,7 @@ func (fn *formulaFuncs) LOG(argsList *list.List) (result string, err error) { err = errors.New("LOG allows at most 2 arguments") return } - var number, base float64 = 0, 10 + number, base := 0.0, 10.0 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } @@ -1757,7 +1813,7 @@ func (fn *formulaFuncs) MROUND(argsList *list.List) (result string, err error) { err = errors.New("MROUND requires 2 numeric arguments") return } - var number, multiple float64 = 0, 1 + var number, multiple float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { return } @@ -1788,7 +1844,7 @@ func (fn *formulaFuncs) MROUND(argsList *list.List) (result string, err error) { // MULTINOMIAL(number1,[number2],...) // func (fn *formulaFuncs) MULTINOMIAL(argsList *list.List) (result string, err error) { - var val, num, denom float64 = 0, 0, 1 + val, num, denom := 0.0, 0.0, 1.0 for arg := argsList.Front(); arg != nil; arg = arg.Next() { token := arg.Value.(formulaArg) if token.Value == "" { @@ -1915,7 +1971,7 @@ func (fn *formulaFuncs) POWER(argsList *list.List) (result string, err error) { // PRODUCT(number1,[number2],...) // func (fn *formulaFuncs) PRODUCT(argsList *list.List) (result string, err error) { - var val, product float64 = 0, 1 + val, product := 0.0, 1.0 for arg := argsList.Front(); arg != nil; arg = arg.Next() { token := arg.Value.(formulaArg) if token.Value == "" { @@ -2088,7 +2144,7 @@ const ( // round rounds a supplied number up or down. func (fn *formulaFuncs) round(number, digits float64, mode roundMode) float64 { - significance := 1.0 + var significance float64 if digits > 0 { significance = math.Pow(1/10.0, digits) } else { @@ -2343,6 +2399,27 @@ func (fn *formulaFuncs) SUM(argsList *list.List) (result string, err error) { return } +// SUMSQ function returns the sum of squares of a supplied set of values. The +// syntax of the function is: +// +// SUMSQ(number1,[number2],...) +// +func (fn *formulaFuncs) SUMSQ(argsList *list.List) (result string, err error) { + var val, sq float64 + for arg := argsList.Front(); arg != nil; arg = arg.Next() { + token := arg.Value.(formulaArg) + if token.Value == "" { + continue + } + if val, err = strconv.ParseFloat(token.Value, 64); err != nil { + return + } + sq += val * val + } + result = fmt.Sprintf("%g", sq) + return +} + // TAN function calculates the tangent of a given angle. The syntax of the // function is: // diff --git a/calc_test.go b/calc_test.go index 6225d50ae5..6d0f8535e3 100644 --- a/calc_test.go +++ b/calc_test.go @@ -329,6 +329,9 @@ func TestCalcCellValue(t *testing.T) { "=((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", + // SUMSQ + "=SUMSQ(A1:A4)": "14", + "=SUMSQ(A1,B1,A2,B2,6)": "82", // TAN "=TAN(1.047197551)": "1.732050806782486", "=TAN(0)": "0", @@ -349,7 +352,7 @@ func TestCalcCellValue(t *testing.T) { assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) result, err := f.CalcCellValue("Sheet1", "C1") assert.NoError(t, err) - assert.Equal(t, expected, result) + assert.Equal(t, expected, result, formula) } mathCalcError := map[string]string{ // ABS @@ -507,6 +510,13 @@ func TestCalcCellValue(t *testing.T) { "=SQRT(-1)": "#NUM!", // SQRTPI "=SQRTPI()": "SQRTPI requires 1 numeric argument", + // SUM + "=SUM((": "formula not valid", + "=SUM(-)": "formula not valid", + "=SUM(1+)": "formula not valid", + "=SUM(1-)": "formula not valid", + "=SUM(1*)": "formula not valid", + "=SUM(1/)": "formula not valid", // TAN "=TAN()": "TAN requires 1 numeric argument", // TANH @@ -519,7 +529,7 @@ func TestCalcCellValue(t *testing.T) { assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) result, err := f.CalcCellValue("Sheet1", "C1") assert.EqualError(t, err, expected) - assert.Equal(t, "", result) + assert.Equal(t, "", result, formula) } referenceCalc := map[string]string{ @@ -535,15 +545,15 @@ func TestCalcCellValue(t *testing.T) { "=SUM(Sheet1!A1:Sheet1!A1:A2,A2)": "5", "=SUM(A1,A2,A3)*SUM(2,3)": "30", "=1+SUM(SUM(A1+A2/A3)*(2-3),2)": "1.3333333333333335", - "=A1/A2/SUM(A1:A2:B1)": "0.07142857142857142", - "=A1/A2/SUM(A1:A2:B1)*A3": "0.21428571428571427", + "=A1/A2/SUM(A1:A2:B1)": "0.041666666666666664", + "=A1/A2/SUM(A1:A2:B1)*A3": "0.125", } for formula, expected := range referenceCalc { f := prepareData() assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) result, err := f.CalcCellValue("Sheet1", "C1") assert.NoError(t, err) - assert.Equal(t, expected, result) + assert.Equal(t, expected, result, formula) } referenceCalcError := map[string]string{ @@ -557,7 +567,7 @@ func TestCalcCellValue(t *testing.T) { assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) result, err := f.CalcCellValue("Sheet1", "C1") assert.EqualError(t, err, expected) - assert.Equal(t, "", result) + assert.Equal(t, "", result, formula) } // Test get calculated cell value on not formula cell. From 882abb80988b7c50286dd2e6c6589fab10662db6 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 10 May 2020 16:56:08 +0800 Subject: [PATCH 233/957] - formula engine: reduce cyclomatic complexity - styles: allow empty and default cell formats, #628 --- calc.go | 303 ++++++++++++++++++++++++++++--------------------- calc_test.go | 2 +- cell.go | 4 +- go.mod | 1 + go.sum | 2 + styles.go | 16 +-- styles_test.go | 6 +- xmlStyles.go | 26 ++--- 8 files changed, 206 insertions(+), 154 deletions(-) diff --git a/calc.go b/calc.go index f85d16ad6a..3d4a8be169 100644 --- a/calc.go +++ b/calc.go @@ -63,8 +63,8 @@ type formulaArg struct { type formulaFuncs struct{} // CalcCellValue provides a function to get calculated cell value. This -// feature is currently in beta. Array formula, table formula and some other -// formulas are not supported currently. +// feature is currently in working processing. Array formula, table formula +// and some other formulas are not supported currently. func (f *File) CalcCellValue(sheet, cell string) (result string, err error) { var ( formula string @@ -265,6 +265,89 @@ func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error) return opdStack.Peek().(efp.Token), err } +// calcAdd evaluate addition arithmetic operations. +func calcAdd(opdStack *Stack) error { + if opdStack.Len() < 2 { + return errors.New("formula not valid") + } + rOpd := opdStack.Pop().(efp.Token) + lOpd := opdStack.Pop().(efp.Token) + lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64) + if err != nil { + return err + } + rOpdVal, err := strconv.ParseFloat(rOpd.TValue, 64) + if err != nil { + return err + } + result := lOpdVal + rOpdVal + opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + return nil +} + +// calcAdd evaluate subtraction arithmetic operations. +func calcSubtract(opdStack *Stack) error { + if opdStack.Len() < 2 { + return errors.New("formula not valid") + } + rOpd := opdStack.Pop().(efp.Token) + lOpd := opdStack.Pop().(efp.Token) + lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64) + if err != nil { + return err + } + rOpdVal, err := strconv.ParseFloat(rOpd.TValue, 64) + if err != nil { + return err + } + result := lOpdVal - rOpdVal + opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + return nil +} + +// calcAdd evaluate multiplication arithmetic operations. +func calcMultiply(opdStack *Stack) error { + if opdStack.Len() < 2 { + return errors.New("formula not valid") + } + rOpd := opdStack.Pop().(efp.Token) + lOpd := opdStack.Pop().(efp.Token) + lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64) + if err != nil { + return err + } + rOpdVal, err := strconv.ParseFloat(rOpd.TValue, 64) + if err != nil { + return err + } + result := lOpdVal * rOpdVal + opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + return nil +} + +// calcAdd evaluate division arithmetic operations. +func calcDivide(opdStack *Stack) error { + if opdStack.Len() < 2 { + return errors.New("formula not valid") + } + rOpd := opdStack.Pop().(efp.Token) + lOpd := opdStack.Pop().(efp.Token) + lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64) + if err != nil { + return err + } + rOpdVal, err := strconv.ParseFloat(rOpd.TValue, 64) + if err != nil { + return err + } + result := lOpdVal / rOpdVal + if rOpdVal == 0 { + return errors.New(formulaErrorDIV) + } + opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + return nil +} + // calculate evaluate basic arithmetic operations. func calculate(opdStack *Stack, opt efp.Token) error { if opt.TValue == "-" && opt.TType == efp.TokenTypeOperatorPrefix { @@ -279,80 +362,69 @@ func calculate(opdStack *Stack, opt efp.Token) error { result := 0 - opdVal opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) } + if opt.TValue == "+" { - if opdStack.Len() < 2 { - return errors.New("formula not valid") - } - rOpd := opdStack.Pop().(efp.Token) - lOpd := opdStack.Pop().(efp.Token) - lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64) - if err != nil { + if err := calcAdd(opdStack); err != nil { return err } - rOpdVal, err := strconv.ParseFloat(rOpd.TValue, 64) - if err != nil { - return err - } - result := lOpdVal + rOpdVal - opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) } if opt.TValue == "-" && opt.TType == efp.TokenTypeOperatorInfix { - if opdStack.Len() < 2 { - return errors.New("formula not valid") - } - rOpd := opdStack.Pop().(efp.Token) - lOpd := opdStack.Pop().(efp.Token) - lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64) - if err != nil { + if err := calcSubtract(opdStack); err != nil { return err } - rOpdVal, err := strconv.ParseFloat(rOpd.TValue, 64) - if err != nil { - return err - } - result := lOpdVal - rOpdVal - opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) } if opt.TValue == "*" { - if opdStack.Len() < 2 { - return errors.New("formula not valid") - } - rOpd := opdStack.Pop().(efp.Token) - lOpd := opdStack.Pop().(efp.Token) - lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64) - if err != nil { + if err := calcMultiply(opdStack); err != nil { return err } - rOpdVal, err := strconv.ParseFloat(rOpd.TValue, 64) - if err != nil { - return err - } - result := lOpdVal * rOpdVal - opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) } if opt.TValue == "/" { - if opdStack.Len() < 2 { - return errors.New("formula not valid") - } - rOpd := opdStack.Pop().(efp.Token) - lOpd := opdStack.Pop().(efp.Token) - lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64) - if err != nil { + if err := calcDivide(opdStack); err != nil { return err } - rOpdVal, err := strconv.ParseFloat(rOpd.TValue, 64) - if err != nil { - return err - } - result := lOpdVal / rOpdVal - if rOpdVal == 0 { - return errors.New(formulaErrorDIV) - } - opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) } return nil } +// parseOperatorPrefixToken parse operator prefix token. +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 +} + +// isOperatorPrefixToken determine if the token is parse operator prefix +// token. +func isOperatorPrefixToken(token efp.Token) bool { + if (token.TValue == "-" && token.TType == efp.TokenTypeOperatorPrefix) || + token.TValue == "+" || token.TValue == "-" || token.TValue == "*" || token.TValue == "/" { + return true + } + return false +} + // 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 { @@ -369,30 +441,9 @@ func (f *File) parseToken(sheet string, token efp.Token, opdStack, optStack *Sta token.TType = efp.TokenTypeOperand token.TSubType = efp.TokenSubTypeNumber } - if (token.TValue == "-" && token.TType == efp.TokenTypeOperatorPrefix) || token.TValue == "+" || token.TValue == "-" || token.TValue == "*" || token.TValue == "/" { - 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 err - } - if optStack.Len() > 0 { - topOpt = optStack.Peek().(efp.Token) - topOptPriority = getPriority(topOpt) - continue - } - break - } - optStack.Push(token) - } + if isOperatorPrefixToken(token) { + if err := f.parseOperatorPrefixToken(optStack, opdStack, token); err != nil { + return err } } if token.TType == efp.TokenTypeSubexpression && token.TSubType == efp.TokenSubTypeStart { // ( @@ -461,11 +512,44 @@ func (f *File) parseReference(sheet, reference string) (result []string, matrix return } +// prepareValueRange prepare value range. +func prepareValueRange(cr cellRange, valueRange []int) { + if cr.From.Row < valueRange[0] { + valueRange[0] = cr.From.Row + } + if cr.From.Col < valueRange[2] { + valueRange[2] = cr.From.Col + } + if cr.To.Row > valueRange[0] { + valueRange[1] = cr.To.Row + } + if cr.To.Col > valueRange[3] { + valueRange[3] = cr.To.Col + } +} + +// prepareValueRef prepare value reference. +func prepareValueRef(cr cellRef, valueRange []int) { + if cr.Row < valueRange[0] { + valueRange[0] = cr.Row + } + if cr.Col < valueRange[2] { + valueRange[2] = cr.Col + } + if cr.Row > valueRange[0] { + valueRange[1] = cr.Row + } + if cr.Col > valueRange[3] { + valueRange[3] = cr.Col + } +} + // 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. +// 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) (result []string, matrix [][]string, err error) { - var fromRow, toRow, fromCol, toCol int = 1, 1, 1, 1 + // value range order: from row, to row, from column, to column + valueRange := []int{1, 1, 1, 1} var sheet string filter := map[string]string{} // prepare value range @@ -476,18 +560,7 @@ func (f *File) rangeResolver(cellRefs, cellRanges *list.List) (result []string, } rng := []int{cr.From.Col, cr.From.Row, cr.To.Col, cr.To.Row} sortCoordinates(rng) - if cr.From.Row < fromRow { - fromRow = cr.From.Row - } - if cr.From.Col < fromCol { - fromCol = cr.From.Col - } - if cr.To.Row > fromRow { - toRow = cr.To.Row - } - if cr.To.Col > toCol { - toCol = cr.To.Col - } + prepareValueRange(cr, valueRange) if cr.From.Sheet != "" { sheet = cr.From.Sheet } @@ -497,24 +570,13 @@ func (f *File) rangeResolver(cellRefs, cellRanges *list.List) (result []string, if cr.Sheet != "" { sheet = cr.Sheet } - if cr.Row < fromRow { - fromRow = cr.Row - } - if cr.Col < fromCol { - fromCol = cr.Col - } - if cr.Row > fromRow { - toRow = cr.Row - } - if cr.Col > toCol { - toCol = cr.Col - } + prepareValueRef(cr, valueRange) } // extract value from ranges if cellRanges.Len() > 0 { - for row := fromRow; row <= toRow; row++ { + for row := valueRange[0]; row <= valueRange[1]; row++ { var matrixRow = []string{} - for col := fromCol; col <= toCol; col++ { + for col := valueRange[2]; col <= valueRange[3]; col++ { var cell, value string if cell, err = CoordinatesToCellName(col, row); err != nil { return @@ -672,28 +734,15 @@ func (fn *formulaFuncs) ARABIC(argsList *list.List) (result string, err error) { err = errors.New("ARABIC requires 1 numeric argument") return } + charMap := map[rune]float64{'I': 1, 'V': 5, 'X': 10, 'L': 50, 'C': 100, 'D': 500, 'M': 1000} val, last, prefix := 0.0, 0.0, 1.0 for _, char := range argsList.Front().Value.(formulaArg).Value { digit := 0.0 - switch char { - case '-': + if char == '-' { prefix = -1 continue - case 'I': - digit = 1 - case 'V': - digit = 5 - case 'X': - digit = 10 - case 'L': - digit = 50 - case 'C': - digit = 100 - case 'D': - digit = 500 - case 'M': - digit = 1000 } + digit, _ = charMap[char] val += digit switch { case last == digit && (last == 5 || last == 50 || last == 500): @@ -850,7 +899,7 @@ func (fn *formulaFuncs) BASE(argsList *list.List) (result string, err error) { return } if radix < 2 || radix > 36 { - err = errors.New("radix must be an integer ≥ 2 and ≤ 36") + err = errors.New("radix must be an integer >= 2 and <= 36") return } if argsList.Len() > 2 { diff --git a/calc_test.go b/calc_test.go index 6d0f8535e3..fc107cb422 100644 --- a/calc_test.go +++ b/calc_test.go @@ -381,7 +381,7 @@ func TestCalcCellValue(t *testing.T) { // 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(1,1)": "radix must be an integer >= 2 and <= 36", // CEILING "=CEILING()": "CEILING requires at least 1 argument", "=CEILING(1,2,3)": "CEILING allows at most 2 arguments", diff --git a/cell.go b/cell.go index a69f4d9b2b..63db194019 100644 --- a/cell.go +++ b/cell.go @@ -730,9 +730,9 @@ func (f *File) formattedValue(s int, v string) string { return v } styleSheet := f.stylesReader() - ok := builtInNumFmtFunc[styleSheet.CellXfs.Xf[s].NumFmtID] + ok := builtInNumFmtFunc[*styleSheet.CellXfs.Xf[s].NumFmtID] if ok != nil { - return ok(styleSheet.CellXfs.Xf[s].NumFmtID, v) + return ok(*styleSheet.CellXfs.Xf[s].NumFmtID, v) } return v } diff --git a/go.mod b/go.mod index 420c64eb64..e411f19543 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 github.com/stretchr/testify v1.3.0 + github.com/xuri/efp v0.0.0-20191019043341-b7dc4fe9aa91 golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 golang.org/x/text v0.3.2 // indirect diff --git a/go.sum b/go.sum index 54492ac2ed..ff969f082b 100644 --- a/go.sum +++ b/go.sum @@ -9,6 +9,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/xuri/efp v0.0.0-20191019043341-b7dc4fe9aa91 h1:gp02YctZuIPTk0t7qI+wvg3VQwTPyNmSGG6ZqOsjSL8= +github.com/xuri/efp v0.0.0-20191019043341-b7dc4fe9aa91/go.mod h1:uBiSUepVYMhGTfDeBKKasV4GpgBlzJ46gXUBAqV8qLk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a h1:gHevYm0pO4QUbwy8Dmdr01R5r1BuKtfYqRqF0h/Cbh0= golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= diff --git a/styles.go b/styles.go index 61b8e53446..72b20718bf 100644 --- a/styles.go +++ b/styles.go @@ -2299,21 +2299,21 @@ func setBorders(style *Style) *xlsxBorder { // cell. func setCellXfs(style *xlsxStyleSheet, fontID, numFmtID, fillID, borderID int, applyAlignment, applyProtection bool, alignment *xlsxAlignment, protection *xlsxProtection) int { var xf xlsxXf - xf.FontID = fontID + xf.FontID = intPtr(fontID) if fontID != 0 { - xf.ApplyFont = true + xf.ApplyFont = boolPtr(true) } - xf.NumFmtID = numFmtID + xf.NumFmtID = intPtr(numFmtID) if numFmtID != 0 { - xf.ApplyNumberFormat = true + xf.ApplyNumberFormat = boolPtr(true) } - xf.FillID = fillID - xf.BorderID = borderID + xf.FillID = intPtr(fillID) + xf.BorderID = intPtr(borderID) style.CellXfs.Count++ xf.Alignment = alignment - xf.ApplyAlignment = applyAlignment + xf.ApplyAlignment = boolPtr(applyAlignment) if applyProtection { - xf.ApplyProtection = applyProtection + xf.ApplyProtection = boolPtr(applyProtection) xf.Protection = protection } xfID := 0 diff --git a/styles_test.go b/styles_test.go index 5681c95235..1ff0e4e39e 100644 --- a/styles_test.go +++ b/styles_test.go @@ -33,9 +33,9 @@ func TestStyleFill(t *testing.T) { styles := xl.stylesReader() style := styles.CellXfs.Xf[styleID] if testCase.expectFill { - assert.NotEqual(t, style.FillID, 0, testCase.label) + assert.NotEqual(t, *style.FillID, 0, testCase.label) } else { - assert.Equal(t, style.FillID, 0, testCase.label) + assert.Equal(t, *style.FillID, 0, testCase.label) } } } @@ -188,7 +188,7 @@ func TestNewStyle(t *testing.T) { assert.NoError(t, err) styles := f.stylesReader() fontID := styles.CellXfs.Xf[styleID].FontID - font := styles.Fonts.Font[fontID] + font := styles.Fonts.Font[*fontID] assert.Contains(t, *font.Name.Val, "Times New Roman", "Stored font should contain font name") assert.Equal(t, 2, styles.CellXfs.Count, "Should have 2 styles") _, err = f.NewStyle(&Style{}) diff --git a/xmlStyles.go b/xmlStyles.go index b5ec41db95..42d535b5ac 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -209,19 +209,19 @@ type xlsxCellStyleXfs struct { // xlsxXf directly maps the xf element. A single xf element describes all of the // formatting for a cell. type xlsxXf struct { - NumFmtID int `xml:"numFmtId,attr,omitempty"` - FontID int `xml:"fontId,attr,omitempty"` - FillID int `xml:"fillId,attr,omitempty"` - BorderID int `xml:"borderId,attr,omitempty"` - XfID *int `xml:"xfId,attr,omitempty"` - QuotePrefix bool `xml:"quotePrefix,attr,omitempty"` - PivotButton bool `xml:"pivotButton,attr,omitempty"` - ApplyNumberFormat bool `xml:"applyNumberFormat,attr,omitempty"` - ApplyFont bool `xml:"applyFont,attr,omitempty"` - ApplyFill bool `xml:"applyFill,attr,omitempty"` - ApplyBorder bool `xml:"applyBorder,attr,omitempty"` - ApplyAlignment bool `xml:"applyAlignment,attr,omitempty"` - ApplyProtection bool `xml:"applyProtection,attr,omitempty"` + NumFmtID *int `xml:"numFmtId,attr"` + FontID *int `xml:"fontId,attr"` + FillID *int `xml:"fillId,attr"` + BorderID *int `xml:"borderId,attr"` + XfID *int `xml:"xfId,attr"` + QuotePrefix *bool `xml:"quotePrefix,attr"` + PivotButton *bool `xml:"pivotButton,attr"` + ApplyNumberFormat *bool `xml:"applyNumberFormat,attr"` + ApplyFont *bool `xml:"applyFont,attr"` + ApplyFill *bool `xml:"applyFill,attr"` + ApplyBorder *bool `xml:"applyBorder,attr"` + ApplyAlignment *bool `xml:"applyAlignment,attr"` + ApplyProtection *bool `xml:"applyProtection,attr"` Alignment *xlsxAlignment `xml:"alignment"` Protection *xlsxProtection `xml:"protection"` } From 0feb819d4c08ab52e806214b23d673001bd9fe3e Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 11 May 2020 00:01:22 +0800 Subject: [PATCH 234/957] updated test and go.mod --- calc.go | 141 +++++++++++++++++++---- calc_test.go | 317 ++++++++++++++++++++++++++++++++++++--------------- go.mod | 10 +- go.sum | 31 +++-- 4 files changed, 368 insertions(+), 131 deletions(-) diff --git a/calc.go b/calc.go index 3d4a8be169..61b6dac773 100644 --- a/calc.go +++ b/calc.go @@ -644,6 +644,7 @@ func (fn *formulaFuncs) ABS(argsList *list.List) (result string, err error) { } var val float64 if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } result = fmt.Sprintf("%g", math.Abs(val)) @@ -663,6 +664,7 @@ func (fn *formulaFuncs) ACOS(argsList *list.List) (result string, err error) { } var val float64 if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } result = fmt.Sprintf("%g", math.Acos(val)) @@ -681,6 +683,7 @@ func (fn *formulaFuncs) ACOSH(argsList *list.List) (result string, err error) { } var val float64 if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } result = fmt.Sprintf("%g", math.Acosh(val)) @@ -700,6 +703,7 @@ func (fn *formulaFuncs) ACOT(argsList *list.List) (result string, err error) { } var val float64 if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } result = fmt.Sprintf("%g", math.Pi/2-math.Atan(val)) @@ -718,6 +722,7 @@ func (fn *formulaFuncs) ACOTH(argsList *list.List) (result string, err error) { } var val float64 if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } result = fmt.Sprintf("%g", math.Atanh(1/val)) @@ -774,6 +779,7 @@ func (fn *formulaFuncs) ASIN(argsList *list.List) (result string, err error) { } var val float64 if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } result = fmt.Sprintf("%g", math.Asin(val)) @@ -792,6 +798,7 @@ func (fn *formulaFuncs) ASINH(argsList *list.List) (result string, err error) { } var val float64 if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } result = fmt.Sprintf("%g", math.Asinh(val)) @@ -811,6 +818,7 @@ func (fn *formulaFuncs) ATAN(argsList *list.List) (result string, err error) { } var val float64 if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } result = fmt.Sprintf("%g", math.Atan(val)) @@ -829,6 +837,7 @@ func (fn *formulaFuncs) ATANH(argsList *list.List) (result string, err error) { } var val float64 if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } result = fmt.Sprintf("%g", math.Atanh(val)) @@ -848,34 +857,17 @@ func (fn *formulaFuncs) ATAN2(argsList *list.List) (result string, err error) { } var x, y float64 if x, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if y, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } result = fmt.Sprintf("%g", math.Atan2(x, y)) return } -// gcd returns the greatest common divisor of two supplied integers. -func gcd(x, y float64) float64 { - x, y = math.Trunc(x), math.Trunc(y) - if x == 0 { - return y - } - if y == 0 { - return x - } - for x != y { - if x > y { - x = x - y - } else { - y = y - x - } - } - return x -} - // 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: // @@ -893,9 +885,11 @@ func (fn *formulaFuncs) BASE(argsList *list.List) (result string, err error) { var number float64 var radix, minLength int if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if radix, err = strconv.Atoi(argsList.Front().Next().Value.(formulaArg).Value); err != nil { + err = errors.New(formulaErrorVALUE) return } if radix < 2 || radix > 36 { @@ -904,6 +898,7 @@ func (fn *formulaFuncs) BASE(argsList *list.List) (result string, err error) { } if argsList.Len() > 2 { if minLength, err = strconv.Atoi(argsList.Back().Value.(formulaArg).Value); err != nil { + err = errors.New(formulaErrorVALUE) return } } @@ -931,6 +926,7 @@ func (fn *formulaFuncs) CEILING(argsList *list.List) (result string, err error) } number, significance, res := 0.0, 1.0, 0.0 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if number < 0 { @@ -938,6 +934,7 @@ func (fn *formulaFuncs) CEILING(argsList *list.List) (result string, err error) } if argsList.Len() > 1 { if significance, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } } @@ -973,6 +970,7 @@ func (fn *formulaFuncs) CEILINGMATH(argsList *list.List) (result string, err err } number, significance, mode := 0.0, 1.0, 1.0 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if number < 0 { @@ -980,6 +978,7 @@ func (fn *formulaFuncs) CEILINGMATH(argsList *list.List) (result string, err err } if argsList.Len() > 1 { if significance, err = strconv.ParseFloat(argsList.Front().Next().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } } @@ -989,6 +988,7 @@ func (fn *formulaFuncs) CEILINGMATH(argsList *list.List) (result string, err err } if argsList.Len() > 2 { if mode, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } } @@ -1021,6 +1021,7 @@ func (fn *formulaFuncs) CEILINGPRECISE(argsList *list.List) (result string, err } number, significance := 0.0, 1.0 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if number < 0 { @@ -1032,6 +1033,7 @@ func (fn *formulaFuncs) CEILINGPRECISE(argsList *list.List) (result string, err } if argsList.Len() > 1 { if significance, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } significance = math.Abs(significance) @@ -1062,9 +1064,11 @@ func (fn *formulaFuncs) COMBIN(argsList *list.List) (result string, err error) { } number, chosen, val := 0.0, 0.0, 1.0 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if chosen, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } number, chosen = math.Trunc(number), math.Trunc(chosen) @@ -1095,9 +1099,11 @@ func (fn *formulaFuncs) COMBINA(argsList *list.List) (result string, err error) } var number, chosen float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if chosen, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } number, chosen = math.Trunc(number), math.Trunc(chosen) @@ -1131,6 +1137,7 @@ func (fn *formulaFuncs) COS(argsList *list.List) (result string, err error) { } var val float64 if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } result = fmt.Sprintf("%g", math.Cos(val)) @@ -1149,6 +1156,7 @@ func (fn *formulaFuncs) COSH(argsList *list.List) (result string, err error) { } var val float64 if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } result = fmt.Sprintf("%g", math.Cosh(val)) @@ -1167,10 +1175,11 @@ func (fn *formulaFuncs) COT(argsList *list.List) (result string, err error) { } var val float64 if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if val == 0 { - err = errors.New(formulaErrorNAME) + err = errors.New(formulaErrorDIV) return } result = fmt.Sprintf("%g", math.Tan(val)) @@ -1189,10 +1198,11 @@ func (fn *formulaFuncs) COTH(argsList *list.List) (result string, err error) { } var val float64 if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if val == 0 { - err = errors.New(formulaErrorNAME) + err = errors.New(formulaErrorDIV) return } result = fmt.Sprintf("%g", math.Tanh(val)) @@ -1211,10 +1221,11 @@ func (fn *formulaFuncs) CSC(argsList *list.List) (result string, err error) { } var val float64 if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if val == 0 { - err = errors.New(formulaErrorNAME) + err = errors.New(formulaErrorDIV) return } result = fmt.Sprintf("%g", 1/math.Sin(val)) @@ -1233,10 +1244,11 @@ func (fn *formulaFuncs) CSCH(argsList *list.List) (result string, err error) { } var val float64 if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if val == 0 { - err = errors.New(formulaErrorNAME) + err = errors.New(formulaErrorDIV) return } result = fmt.Sprintf("%g", 1/math.Sinh(val)) @@ -1256,6 +1268,7 @@ func (fn *formulaFuncs) DECIMAL(argsList *list.List) (result string, err error) var text = argsList.Front().Value.(formulaArg).Value var radix int if radix, err = strconv.Atoi(argsList.Back().Value.(formulaArg).Value); err != nil { + err = errors.New(formulaErrorVALUE) return } if len(text) > 2 && (strings.HasPrefix(text, "0x") || strings.HasPrefix(text, "0X")) { @@ -1263,7 +1276,7 @@ func (fn *formulaFuncs) DECIMAL(argsList *list.List) (result string, err error) } val, err := strconv.ParseInt(text, radix, 64) if err != nil { - err = errors.New(formulaErrorNUM) + err = errors.New(formulaErrorVALUE) return } result = fmt.Sprintf("%g", float64(val)) @@ -1282,10 +1295,11 @@ func (fn *formulaFuncs) DEGREES(argsList *list.List) (result string, err error) } var val float64 if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if val == 0 { - err = errors.New(formulaErrorNAME) + err = errors.New(formulaErrorDIV) return } result = fmt.Sprintf("%g", 180.0/math.Pi*val) @@ -1305,6 +1319,7 @@ func (fn *formulaFuncs) EVEN(argsList *list.List) (result string, err error) { } var number float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } sign := math.Signbit(number) @@ -1333,6 +1348,7 @@ func (fn *formulaFuncs) EXP(argsList *list.List) (result string, err error) { } var number float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } result = strings.ToUpper(fmt.Sprintf("%g", math.Exp(number))) @@ -1360,6 +1376,7 @@ func (fn *formulaFuncs) FACT(argsList *list.List) (result string, err error) { } var number float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if number < 0 { @@ -1381,10 +1398,12 @@ func (fn *formulaFuncs) FACTDOUBLE(argsList *list.List) (result string, err erro } number, val := 0.0, 1.0 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if number < 0 { err = errors.New(formulaErrorNUM) + return } for i := math.Trunc(number); i > 1; i -= 2 { val *= i @@ -1405,13 +1424,16 @@ func (fn *formulaFuncs) FLOOR(argsList *list.List) (result string, err error) { } var number, significance float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if significance, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if significance < 0 && number >= 0 { err = errors.New(formulaErrorNUM) + return } val := number val, res := math.Modf(val / significance) @@ -1440,6 +1462,7 @@ func (fn *formulaFuncs) FLOORMATH(argsList *list.List) (result string, err error } number, significance, mode := 0.0, 1.0, 1.0 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if number < 0 { @@ -1447,6 +1470,7 @@ func (fn *formulaFuncs) FLOORMATH(argsList *list.List) (result string, err error } if argsList.Len() > 1 { if significance, err = strconv.ParseFloat(argsList.Front().Next().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } } @@ -1456,6 +1480,7 @@ func (fn *formulaFuncs) FLOORMATH(argsList *list.List) (result string, err error } if argsList.Len() > 2 { if mode, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } } @@ -1483,6 +1508,7 @@ func (fn *formulaFuncs) FLOORPRECISE(argsList *list.List) (result string, err er } var number, significance float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if number < 0 { @@ -1494,6 +1520,7 @@ func (fn *formulaFuncs) FLOORPRECISE(argsList *list.List) (result string, err er } if argsList.Len() > 1 { if significance, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } significance = math.Abs(significance) @@ -1512,6 +1539,25 @@ func (fn *formulaFuncs) FLOORPRECISE(argsList *list.List) (result string, err er return } +// gcd returns the greatest common divisor of two supplied integers. +func gcd(x, y float64) float64 { + x, y = math.Trunc(x), math.Trunc(y) + if x == 0 { + return y + } + if y == 0 { + return x + } + for x != y { + if x > y { + x = x - y + } else { + y = y - x + } + } + return x +} + // GCD function returns the greatest common divisor of two or more supplied // integers. The syntax of the function is: // @@ -1532,6 +1578,7 @@ func (fn *formulaFuncs) GCD(argsList *list.List) (result string, err error) { continue } if val, err = strconv.ParseFloat(token, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } nums = append(nums, val) @@ -1568,6 +1615,7 @@ func (fn *formulaFuncs) INT(argsList *list.List) (result string, err error) { } var number float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } val, frac := math.Modf(number) @@ -1595,6 +1643,7 @@ func (fn *formulaFuncs) ISOCEILING(argsList *list.List) (result string, err erro } var number, significance float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if number < 0 { @@ -1606,6 +1655,7 @@ func (fn *formulaFuncs) ISOCEILING(argsList *list.List) (result string, err erro } if argsList.Len() > 1 { if significance, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } significance = math.Abs(significance) @@ -1654,6 +1704,7 @@ func (fn *formulaFuncs) LCM(argsList *list.List) (result string, err error) { continue } if val, err = strconv.ParseFloat(token, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } nums = append(nums, val) @@ -1690,6 +1741,7 @@ func (fn *formulaFuncs) LN(argsList *list.List) (result string, err error) { } var number float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } result = fmt.Sprintf("%g", math.Log(number)) @@ -1712,10 +1764,12 @@ func (fn *formulaFuncs) LOG(argsList *list.List) (result string, err error) { } number, base := 0.0, 10.0 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if argsList.Len() > 1 { if base, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } } @@ -1747,6 +1801,7 @@ func (fn *formulaFuncs) LOG10(argsList *list.List) (result string, err error) { } var number float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } result = fmt.Sprintf("%g", math.Log10(number)) @@ -1835,9 +1890,11 @@ func (fn *formulaFuncs) MOD(argsList *list.List) (result string, err error) { } var number, divisor float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if divisor, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if divisor == 0 { @@ -1864,9 +1921,11 @@ func (fn *formulaFuncs) MROUND(argsList *list.List) (result string, err error) { } var number, multiple float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if multiple, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if multiple == 0 { @@ -1900,6 +1959,7 @@ func (fn *formulaFuncs) MULTINOMIAL(argsList *list.List) (result string, err err continue } if val, err = strconv.ParseFloat(token.Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } num += val @@ -1921,6 +1981,7 @@ func (fn *formulaFuncs) MUNIT(argsList *list.List) (result string, err error) { } var dimension int if dimension, err = strconv.Atoi(argsList.Front().Value.(formulaArg).Value); err != nil { + err = errors.New(formulaErrorVALUE) return } matrix := make([][]float64, 0, dimension) @@ -1951,6 +2012,7 @@ func (fn *formulaFuncs) ODD(argsList *list.List) (result string, err error) { } var number float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if number == 0 { @@ -1997,9 +2059,11 @@ func (fn *formulaFuncs) POWER(argsList *list.List) (result string, err error) { } var x, y float64 if x, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if y, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if x == 0 && y == 0 { @@ -2027,6 +2091,7 @@ func (fn *formulaFuncs) PRODUCT(argsList *list.List) (result string, err error) continue } if val, err = strconv.ParseFloat(token.Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } product = product * val @@ -2047,9 +2112,11 @@ func (fn *formulaFuncs) QUOTIENT(argsList *list.List) (result string, err error) } var x, y float64 if x, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if y, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if y == 0 { @@ -2071,6 +2138,7 @@ func (fn *formulaFuncs) RADIANS(argsList *list.List) (result string, err error) } var angle float64 if angle, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } result = fmt.Sprintf("%g", math.Pi/180.0*angle) @@ -2103,9 +2171,11 @@ func (fn *formulaFuncs) RANDBETWEEN(argsList *list.List) (result string, err err } var bottom, top int64 if bottom, err = strconv.ParseInt(argsList.Front().Value.(formulaArg).Value, 10, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if top, err = strconv.ParseInt(argsList.Back().Value.(formulaArg).Value, 10, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if top < bottom { @@ -2148,10 +2218,12 @@ func (fn *formulaFuncs) ROMAN(argsList *list.List) (result string, err error) { var number float64 var form int if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if argsList.Len() > 1 { if form, err = strconv.Atoi(argsList.Back().Value.(formulaArg).Value); err != nil { + err = errors.New(formulaErrorVALUE) return } if form < 0 { @@ -2231,9 +2303,11 @@ func (fn *formulaFuncs) ROUND(argsList *list.List) (result string, err error) { } var number, digits float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if digits, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } result = fmt.Sprintf("%g", fn.round(number, digits, closest)) @@ -2252,9 +2326,11 @@ func (fn *formulaFuncs) ROUNDDOWN(argsList *list.List) (result string, err error } var number, digits float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if digits, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } result = fmt.Sprintf("%g", fn.round(number, digits, down)) @@ -2273,9 +2349,11 @@ func (fn *formulaFuncs) ROUNDUP(argsList *list.List) (result string, err error) } var number, digits float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if digits, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } result = fmt.Sprintf("%g", fn.round(number, digits, up)) @@ -2294,6 +2372,7 @@ func (fn *formulaFuncs) SEC(argsList *list.List) (result string, err error) { } var number float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } result = fmt.Sprintf("%g", math.Cos(number)) @@ -2312,6 +2391,7 @@ func (fn *formulaFuncs) SECH(argsList *list.List) (result string, err error) { } var number float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } result = fmt.Sprintf("%g", 1/math.Cosh(number)) @@ -2332,6 +2412,7 @@ func (fn *formulaFuncs) SIGN(argsList *list.List) (result string, err error) { } var val float64 if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if val < 0 { @@ -2358,6 +2439,7 @@ func (fn *formulaFuncs) SIN(argsList *list.List) (result string, err error) { } var number float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } result = fmt.Sprintf("%g", math.Sin(number)) @@ -2376,6 +2458,7 @@ func (fn *formulaFuncs) SINH(argsList *list.List) (result string, err error) { } var number float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } result = fmt.Sprintf("%g", math.Sinh(number)) @@ -2399,6 +2482,7 @@ func (fn *formulaFuncs) SQRT(argsList *list.List) (result string, err error) { return } if res, err = strconv.ParseFloat(value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if res < 0 { @@ -2421,6 +2505,7 @@ func (fn *formulaFuncs) SQRTPI(argsList *list.List) (result string, err error) { } var number float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } result = fmt.Sprintf("%g", math.Sqrt(number*math.Pi)) @@ -2440,6 +2525,7 @@ func (fn *formulaFuncs) SUM(argsList *list.List) (result string, err error) { continue } if val, err = strconv.ParseFloat(token.Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } sum += val @@ -2461,6 +2547,7 @@ func (fn *formulaFuncs) SUMSQ(argsList *list.List) (result string, err error) { continue } if val, err = strconv.ParseFloat(token.Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } sq += val * val @@ -2481,6 +2568,7 @@ func (fn *formulaFuncs) TAN(argsList *list.List) (result string, err error) { } var number float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } result = fmt.Sprintf("%g", math.Tan(number)) @@ -2499,6 +2587,7 @@ func (fn *formulaFuncs) TANH(argsList *list.List) (result string, err error) { } var number float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } result = fmt.Sprintf("%g", math.Tanh(number)) @@ -2517,10 +2606,12 @@ func (fn *formulaFuncs) TRUNC(argsList *list.List) (result string, err error) { } var number, digits, adjust, rtrim float64 if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } if argsList.Len() > 1 { if digits, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + err = errors.New(formulaErrorVALUE) return } digits = math.Floor(digits) diff --git a/calc_test.go b/calc_test.go index fc107cb422..7592078c74 100644 --- a/calc_test.go +++ b/calc_test.go @@ -77,13 +77,16 @@ func TestCalcCellValue(t *testing.T) { "=CEILING(-22.25,-0.1)": "-22.3", "=CEILING(-22.25,-1)": "-23", "=CEILING(-22.25,-5)": "-25", + "=CEILING(22.25)": "23", // _xlfn.CEILING.MATH - "=_xlfn.CEILING.MATH(15.25,1)": "16", - "=_xlfn.CEILING.MATH(15.25,0.1)": "15.3", - "=_xlfn.CEILING.MATH(15.25,5)": "20", - "=_xlfn.CEILING.MATH(-15.25,1)": "-15", - "=_xlfn.CEILING.MATH(-15.25,1,1)": "-15", // should be 16 - "=_xlfn.CEILING.MATH(-15.25,10)": "-10", + "=_xlfn.CEILING.MATH(15.25,1)": "16", + "=_xlfn.CEILING.MATH(15.25,0.1)": "15.3", + "=_xlfn.CEILING.MATH(15.25,5)": "20", + "=_xlfn.CEILING.MATH(-15.25,1)": "-15", + "=_xlfn.CEILING.MATH(-15.25,1,1)": "-15", // should be 16 + "=_xlfn.CEILING.MATH(-15.25,10)": "-10", + "=_xlfn.CEILING.MATH(-15.25)": "-15", + "=_xlfn.CEILING.MATH(-15.25,-5,-1)": "-10", // _xlfn.CEILING.PRECISE "=_xlfn.CEILING.PRECISE(22.25,0.1)": "22.3", "=_xlfn.CEILING.PRECISE(22.25,0.5)": "22.5", @@ -101,6 +104,7 @@ func TestCalcCellValue(t *testing.T) { "=COMBIN(6,4)": "15", "=COMBIN(6,5)": "6", "=COMBIN(6,6)": "1", + "=COMBIN(0,0)": "1", // _xlfn.COMBINA "=_xlfn.COMBINA(6,1)": "6", "=_xlfn.COMBINA(6,2)": "21", @@ -108,6 +112,7 @@ func TestCalcCellValue(t *testing.T) { "=_xlfn.COMBINA(6,4)": "126", "=_xlfn.COMBINA(6,5)": "252", "=_xlfn.COMBINA(6,6)": "462", + "=_xlfn.COMBINA(0,0)": "0", // COS "=COS(0.785398163)": "0.707106781467586", "=COS(0)": "1", @@ -125,10 +130,11 @@ func TestCalcCellValue(t *testing.T) { // _xlfn.CSCH "=_xlfn.CSCH(-3.14159265358979)": "-0.08658953753004724", // _xlfn.DECIMAL - `=_xlfn.DECIMAL("1100",2)`: "12", - `=_xlfn.DECIMAL("186A0",16)`: "100000", - `=_xlfn.DECIMAL("31L0",32)`: "100000", - `=_xlfn.DECIMAL("70122",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.29577951308232", "=DEGREES(2.5)": "143.2394487827058", @@ -181,6 +187,9 @@ func TestCalcCellValue(t *testing.T) { "=_xlfn.FLOOR.PRECISE(-26.75,-1)": "-27", "=_xlfn.FLOOR.PRECISE(-26.75,-5)": "-30", // GCD + "=GCD(0)": "0", + `=GCD("",1)`: "1", + "=GCD(1,0)": "1", "=GCD(1,5)": "1", "=GCD(15,10,25)": "5", "=GCD(0,8,12)": "4", @@ -199,11 +208,15 @@ func TestCalcCellValue(t *testing.T) { "=ISO.CEILING(-22.25,1)": "-22", "=ISO.CEILING(-22.25,0.1)": "-22.200000000000003", "=ISO.CEILING(-22.25,5)": "-20", + "=ISO.CEILING(-22.25,0)": "0", // LCM "=LCM(1,5)": "5", "=LCM(15,10,25)": "150", "=LCM(1,8,12)": "24", "=LCM(7,2)": "14", + "=LCM(7)": "7", + `=LCM("",1)`: "1", + `=LCM(0,0)`: "0", // LN "=LN(1)": "0", "=LN(100)": "4.605170185988092", @@ -219,10 +232,11 @@ func TestCalcCellValue(t *testing.T) { "=LOG10(0.001)": "-3", "=LOG10(25)": "1.3979400086720375", // MOD - "=MOD(6,4)": "2", - "=MOD(6,3)": "0", - "=MOD(6,2.5)": "1", - "=MOD(6,1.333)": "0.6680000000000001", + "=MOD(6,4)": "2", + "=MOD(6,3)": "0", + "=MOD(6,2.5)": "1", + "=MOD(6,1.333)": "0.6680000000000001", + "=MOD(-10.23,1)": "0.7699999999999996", // MROUND "=MROUND(333.7,0.5)": "333.5", "=MROUND(333.8,1)": "334", @@ -233,7 +247,8 @@ func TestCalcCellValue(t *testing.T) { "=MROUND(-555.4,-1)": "-555", "=MROUND(-1555,-1000)": "-2000", // MULTINOMIAL - "=MULTINOMIAL(3,1,2,5)": "27720", + "=MULTINOMIAL(3,1,2,5)": "27720", + `=MULTINOMIAL("",3,1,2,5)`: "27720", // _xlfn.MUNIT "=_xlfn.MUNIT(4)": "", // not support currently // ODD @@ -249,7 +264,8 @@ func TestCalcCellValue(t *testing.T) { // POWER "=POWER(4,2)": "16", // PRODUCT - "=PRODUCT(3,6)": "18", + "=PRODUCT(3,6)": "18", + `=PRODUCT("",3,6)`: "18", // QUOTIENT "=QUOTIENT(5,2)": "2", "=QUOTIENT(4.5,3.1)": "1", @@ -260,12 +276,14 @@ func TestCalcCellValue(t *testing.T) { "=RADIANS(180)": "3.141592653589793", "=RADIANS(360)": "6.283185307179586", // ROMAN - "=ROMAN(499,0)": "CDXCIX", - "=ROMAN(1999,0)": "MCMXCIX", - "=ROMAN(1999,1)": "MLMVLIV", - "=ROMAN(1999,2)": "MXMIX", - "=ROMAN(1999,3)": "MVMIV", - "=ROMAN(1999,4)": "MIM", + "=ROMAN(499,0)": "CDXCIX", + "=ROMAN(1999,0)": "MCMXCIX", + "=ROMAN(1999,1)": "MLMVLIV", + "=ROMAN(1999,2)": "MXMIX", + "=ROMAN(1999,3)": "MVMIV", + "=ROMAN(1999,4)": "MIM", + "=ROMAN(1999,-1)": "MCMXCIX", + "=ROMAN(1999,5)": "MIM", // ROUND "=ROUND(100.319,1)": "100.30000000000001", "=ROUND(5.28,1)": "5.300000000000001", @@ -317,6 +335,7 @@ func TestCalcCellValue(t *testing.T) { "=SQRTPI(0)": "0", // SUM "=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", @@ -330,8 +349,9 @@ func TestCalcCellValue(t *testing.T) { "=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", // SUMSQ - "=SUMSQ(A1:A4)": "14", - "=SUMSQ(A1,B1,A2,B2,6)": "82", + "=SUMSQ(A1:A4)": "14", + "=SUMSQ(A1,B1,A2,B2,6)": "82", + `=SUMSQ("",A1,B1,A2,B2,6)`: "82", // TAN "=TAN(1.047197551)": "1.732050806782486", "=TAN(0)": "0", @@ -356,173 +376,269 @@ func TestCalcCellValue(t *testing.T) { } mathCalcError := map[string]string{ // ABS - "=ABS()": "ABS requires 1 numeric argument", - "=ABS(~)": `cannot convert cell "~" to coordinates: invalid cell name "~"`, + "=ABS()": "ABS requires 1 numeric argument", + `=ABS("X")`: "#VALUE!", + "=ABS(~)": `cannot convert cell "~" to coordinates: invalid cell name "~"`, // ACOS - "=ACOS()": "ACOS requires 1 numeric argument", + "=ACOS()": "ACOS requires 1 numeric argument", + `=ACOS("X")`: "#VALUE!", // ACOSH - "=ACOSH()": "ACOSH requires 1 numeric argument", + "=ACOSH()": "ACOSH requires 1 numeric argument", + `=ACOSH("X")`: "#VALUE!", // _xlfn.ACOT - "=_xlfn.ACOT()": "ACOT requires 1 numeric argument", + "=_xlfn.ACOT()": "ACOT requires 1 numeric argument", + `=_xlfn.ACOT("X")`: "#VALUE!", // _xlfn.ACOTH - "=_xlfn.ACOTH()": "ACOTH requires 1 numeric argument", + "=_xlfn.ACOTH()": "ACOTH requires 1 numeric argument", + `=_xlfn.ACOTH("X")`: "#VALUE!", // _xlfn.ARABIC "=_xlfn.ARABIC()": "ARABIC requires 1 numeric argument", // ASIN - "=ASIN()": "ASIN requires 1 numeric argument", + "=ASIN()": "ASIN requires 1 numeric argument", + `=ASIN("X")`: "#VALUE!", // ASINH - "=ASINH()": "ASINH requires 1 numeric argument", + "=ASINH()": "ASINH requires 1 numeric argument", + `=ASINH("X")`: "#VALUE!", // ATAN - "=ATAN()": "ATAN requires 1 numeric argument", + "=ATAN()": "ATAN requires 1 numeric argument", + `=ATAN("X")`: "#VALUE!", // ATANH - "=ATANH()": "ATANH requires 1 numeric argument", + "=ATANH()": "ATANH requires 1 numeric argument", + `=ATANH("X")`: "#VALUE!", // ATAN2 - "=ATAN2()": "ATAN2 requires 2 numeric arguments", + "=ATAN2()": "ATAN2 requires 2 numeric arguments", + `=ATAN2("X",0)`: "#VALUE!", + `=ATAN2(0,"X")`: "#VALUE!", // 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)`: "#VALUE!", + `=BASE(1,"X")`: "#VALUE!", + `=BASE(1,2,"X")`: "#VALUE!", // 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)`: "#VALUE!", + `=CEILING(0,"X")`: "#VALUE!", // _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")`: "#VALUE!", + `=_xlfn.CEILING.MATH(1,"X")`: "#VALUE!", + `=_xlfn.CEILING.MATH(1,2,"X")`: "#VALUE!", // _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)`: "#VALUE!", + `=_xlfn.CEILING.PRECISE(1,"X")`: "#VALUE!", // COMBIN - "=COMBIN()": "COMBIN requires 2 argument", - "=COMBIN(-1,1)": "COMBIN requires number >= number_chosen", + "=COMBIN()": "COMBIN requires 2 argument", + "=COMBIN(-1,1)": "COMBIN requires number >= number_chosen", + `=COMBIN("X",1)`: "#VALUE!", + `=COMBIN(-1,"X")`: "#VALUE!", // _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()": "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)`: "#VALUE!", + `=_xlfn.COMBINA(-1,"X")`: "#VALUE!", // COS - "=COS()": "COS requires 1 numeric argument", + "=COS()": "COS requires 1 numeric argument", + `=COS("X")`: "#VALUE!", // COSH - "=COSH()": "COSH requires 1 numeric argument", + "=COSH()": "COSH requires 1 numeric argument", + `=COSH("X")`: "#VALUE!", // _xlfn.COT - "=COT()": "COT requires 1 numeric argument", + "=COT()": "COT requires 1 numeric argument", + `=COT("X")`: "#VALUE!", + "=COT(0)": "#DIV/0!", // _xlfn.COTH - "=COTH()": "COTH requires 1 numeric argument", + "=COTH()": "COTH requires 1 numeric argument", + `=COTH("X")`: "#VALUE!", + "=COTH(0)": "#DIV/0!", // _xlfn.CSC - "=_xlfn.CSC()": "CSC requires 1 numeric argument", - "=_xlfn.CSC(0)": "#NAME?", + "=_xlfn.CSC()": "CSC requires 1 numeric argument", + `=_xlfn.CSC("X")`: "#VALUE!", + "=_xlfn.CSC(0)": "#DIV/0!", // _xlfn.CSCH - "=_xlfn.CSCH()": "CSCH requires 1 numeric argument", - "=_xlfn.CSCH(0)": "#NAME?", + "=_xlfn.CSCH()": "CSCH requires 1 numeric argument", + `=_xlfn.CSCH("X")`: "#VALUE!", + "=_xlfn.CSCH(0)": "#DIV/0!", // _xlfn.DECIMAL "=_xlfn.DECIMAL()": "DECIMAL requires 2 numeric arguments", - `=_xlfn.DECIMAL("2000", 2)`: "#NUM!", + `=_xlfn.DECIMAL("X", 2)`: "#VALUE!", + `=_xlfn.DECIMAL(2000, "X")`: "#VALUE!", // DEGREES - "=DEGREES()": "DEGREES requires 1 numeric argument", + "=DEGREES()": "DEGREES requires 1 numeric argument", + `=DEGREES("X")`: "#VALUE!", + "=DEGREES(0)": "#DIV/0!", // EVEN - "=EVEN()": "EVEN requires 1 numeric argument", + "=EVEN()": "EVEN requires 1 numeric argument", + `=EVEN("X")`: "#VALUE!", // EXP - "=EXP()": "EXP requires 1 numeric argument", + "=EXP()": "EXP requires 1 numeric argument", + `=EXP("X")`: "#VALUE!", // FACT - "=FACT()": "FACT requires 1 numeric argument", - "=FACT(-1)": "#NUM!", + "=FACT()": "FACT requires 1 numeric argument", + `=FACT("X")`: "#VALUE!", + "=FACT(-1)": "#NUM!", // FACTDOUBLE - "=FACTDOUBLE()": "FACTDOUBLE requires 1 numeric argument", - "=FACTDOUBLE(-1)": "#NUM!", + "=FACTDOUBLE()": "FACTDOUBLE requires 1 numeric argument", + `=FACTDOUBLE("X")`: "#VALUE!", + "=FACTDOUBLE(-1)": "#NUM!", // FLOOR - "=FLOOR()": "FLOOR requires 2 numeric arguments", - "=FLOOR(1,-1)": "#NUM!", + "=FLOOR()": "FLOOR requires 2 numeric arguments", + `=FLOOR("X",-1)`: "#VALUE!", + `=FLOOR(1,"X")`: "#VALUE!", + "=FLOOR(1,-1)": "#NUM!", // _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)`: "#VALUE!", + `=_xlfn.FLOOR.MATH(1,"X",3)`: "#VALUE!", + `=_xlfn.FLOOR.MATH(1,2,"X")`: "#VALUE!", // _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)`: "#VALUE!", + `=_xlfn.FLOOR.PRECISE(1,"X")`: "#VALUE!", // GCD "=GCD()": "GCD requires at least 1 argument", "=GCD(-1)": "GCD only accepts positive arguments", "=GCD(1,-1)": "GCD only accepts positive arguments", + `=GCD("X")`: "#VALUE!", // INT - "=INT()": "INT requires 1 numeric argument", + "=INT()": "INT requires 1 numeric argument", + `=INT("X")`: "#VALUE!", // 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)`: "#VALUE!", + `=ISO.CEILING(1,"X")`: "#VALUE!", // 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")`: "#VALUE!", // LN - "=LN()": "LN requires 1 numeric argument", + "=LN()": "LN requires 1 numeric argument", + `=LN("X")`: "#VALUE!", // LOG "=LOG()": "LOG requires at least 1 argument", "=LOG(1,2,3)": "LOG allows at most 2 arguments", + `=LOG("X",1)`: "#VALUE!", + `=LOG(1,"X")`: "#VALUE!", "=LOG(0,0)": "#NUM!", "=LOG(1,0)": "#NUM!", "=LOG(1,1)": "#DIV/0!", // LOG10 - "=LOG10()": "LOG10 requires 1 numeric argument", + "=LOG10()": "LOG10 requires 1 numeric argument", + `=LOG10("X")`: "#VALUE!", // MOD - "=MOD()": "MOD requires 2 numeric arguments", - "=MOD(6,0)": "#DIV/0!", + "=MOD()": "MOD requires 2 numeric arguments", + "=MOD(6,0)": "#DIV/0!", + `=MOD("X",0)`: "#VALUE!", + `=MOD(6,"X")`: "#VALUE!", // MROUND - "=MROUND()": "MROUND requires 2 numeric arguments", - "=MROUND(1,0)": "#NUM!", + "=MROUND()": "MROUND requires 2 numeric arguments", + "=MROUND(1,0)": "#NUM!", + "=MROUND(1,-1)": "#NUM!", + `=MROUND("X",0)`: "#VALUE!", + `=MROUND(1,"X")`: "#VALUE!", + // MULTINOMIAL + `=MULTINOMIAL("X")`: "#VALUE!", // _xlfn.MUNIT - "=_xlfn.MUNIT()": "MUNIT requires 1 numeric argument", // not support currently + "=_xlfn.MUNIT()": "MUNIT requires 1 numeric argument", // not support currently + `=_xlfn.MUNIT("X")`: "#VALUE!", // not support currently // ODD - "=ODD()": "ODD requires 1 numeric argument", + "=ODD()": "ODD requires 1 numeric argument", + `=ODD("X")`: "#VALUE!", // PI "=PI(1)": "PI accepts no arguments", // POWER - "=POWER(0,0)": "#NUM!", - "=POWER(0,-1)": "#DIV/0!", - "=POWER(1)": "POWER requires 2 numeric arguments", + `=POWER("X",1)`: "#VALUE!", + `=POWER(1,"X")`: "#VALUE!", + "=POWER(0,0)": "#NUM!", + "=POWER(0,-1)": "#DIV/0!", + "=POWER(1)": "POWER requires 2 numeric arguments", + // PRODUCT + `=PRODUCT("X")`: "#VALUE!", // QUOTIENT - "=QUOTIENT(1,0)": "#DIV/0!", - "=QUOTIENT(1)": "QUOTIENT requires 2 numeric arguments", + `=QUOTIENT("X",1)`: "#VALUE!", + `=QUOTIENT(1,"X")`: "#VALUE!", + "=QUOTIENT(1,0)": "#DIV/0!", + "=QUOTIENT(1)": "QUOTIENT requires 2 numeric arguments", // RADIANS - "=RADIANS()": "RADIANS requires 1 numeric argument", + `=RADIANS("X")`: "#VALUE!", + "=RADIANS()": "RADIANS requires 1 numeric argument", // RAND "=RAND(1)": "RAND accepts no arguments", // RANDBETWEEN - "=RANDBETWEEN()": "RANDBETWEEN requires 2 numeric arguments", - "=RANDBETWEEN(2,1)": "#NUM!", + `=RANDBETWEEN("X",1)`: "#VALUE!", + `=RANDBETWEEN(1,"X")`: "#VALUE!", + "=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")`: "#VALUE!", + `=ROMAN("X",1)`: "#VALUE!", // ROUND - "=ROUND()": "ROUND requires 2 numeric arguments", + "=ROUND()": "ROUND requires 2 numeric arguments", + `=ROUND("X",1)`: "#VALUE!", + `=ROUND(1,"X")`: "#VALUE!", // ROUNDDOWN - "=ROUNDDOWN()": "ROUNDDOWN requires 2 numeric arguments", + "=ROUNDDOWN()": "ROUNDDOWN requires 2 numeric arguments", + `=ROUNDDOWN("X",1)`: "#VALUE!", + `=ROUNDDOWN(1,"X")`: "#VALUE!", // ROUNDUP - "=ROUNDUP()": "ROUNDUP requires 2 numeric arguments", + "=ROUNDUP()": "ROUNDUP requires 2 numeric arguments", + `=ROUNDUP("X",1)`: "#VALUE!", + `=ROUNDUP(1,"X")`: "#VALUE!", // SEC - "=_xlfn.SEC()": "SEC requires 1 numeric argument", + "=_xlfn.SEC()": "SEC requires 1 numeric argument", + `=_xlfn.SEC("X")`: "#VALUE!", // _xlfn.SECH - "=_xlfn.SECH()": "SECH requires 1 numeric argument", + "=_xlfn.SECH()": "SECH requires 1 numeric argument", + `=_xlfn.SECH("X")`: "#VALUE!", // SIGN - "=SIGN()": "SIGN requires 1 numeric argument", + "=SIGN()": "SIGN requires 1 numeric argument", + `=SIGN("X")`: "#VALUE!", // SIN - "=SIN()": "SIN requires 1 numeric argument", + "=SIN()": "SIN requires 1 numeric argument", + `=SIN("X")`: "#VALUE!", // SINH - "=SINH()": "SINH requires 1 numeric argument", + "=SINH()": "SINH requires 1 numeric argument", + `=SINH("X")`: "#VALUE!", // SQRT - "=SQRT()": "SQRT requires 1 numeric argument", - "=SQRT(-1)": "#NUM!", + "=SQRT()": "SQRT requires 1 numeric argument", + `=SQRT("X")`: "#VALUE!", + "=SQRT(-1)": "#NUM!", // SQRTPI - "=SQRTPI()": "SQRTPI requires 1 numeric argument", + "=SQRTPI()": "SQRTPI requires 1 numeric argument", + `=SQRTPI("X")`: "#VALUE!", // SUM - "=SUM((": "formula not valid", - "=SUM(-)": "formula not valid", - "=SUM(1+)": "formula not valid", - "=SUM(1-)": "formula not valid", - "=SUM(1*)": "formula not valid", - "=SUM(1/)": "formula not valid", + "=SUM((": "formula not valid", + "=SUM(-)": "formula not valid", + "=SUM(1+)": "formula not valid", + "=SUM(1-)": "formula not valid", + "=SUM(1*)": "formula not valid", + "=SUM(1/)": "formula not valid", + `=SUM("X")`: "#VALUE!", + // SUMSQ + `=SUMSQ("X")`: "#VALUE!", // TAN - "=TAN()": "TAN requires 1 numeric argument", + "=TAN()": "TAN requires 1 numeric argument", + `=TAN("X")`: "#VALUE!", // TANH - "=TANH()": "TANH requires 1 numeric argument", + "=TANH()": "TANH requires 1 numeric argument", + `=TANH("X")`: "#VALUE!", // TRUNC - "=TRUNC()": "TRUNC requires at least 1 argument", + "=TRUNC()": "TRUNC requires at least 1 argument", + `=TRUNC("X")`: "#VALUE!", + `=TRUNC(1,"X")`: "#VALUE!", } for formula, expected := range mathCalcError { f := prepareData() @@ -570,6 +686,17 @@ func TestCalcCellValue(t *testing.T) { assert.Equal(t, "", result, formula) } + volatileFuncs := []string{ + "=RAND()", + "=RANDBETWEEN(1,2)", + } + for _, formula := range volatileFuncs { + f := prepareData() + assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) + _, err := f.CalcCellValue("Sheet1", "C1") + assert.NoError(t, err) + } + // Test get calculated cell value on not formula cell. f := prepareData() result, err := f.CalcCellValue("Sheet1", "A1") diff --git a/go.mod b/go.mod index e411f19543..f94f33beb2 100644 --- a/go.mod +++ b/go.mod @@ -4,10 +4,14 @@ go 1.12 require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/kr/text v0.2.0 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 - github.com/stretchr/testify v1.3.0 + github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect + github.com/stretchr/testify v1.5.1 github.com/xuri/efp v0.0.0-20191019043341-b7dc4fe9aa91 - golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a - golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 + golang.org/x/image v0.0.0-20200430140353-33d19683fad8 + golang.org/x/net v0.0.0-20200506145744-7e3656a0809f golang.org/x/text v0.3.2 // indirect + gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect + gopkg.in/yaml.v2 v2.2.8 // indirect ) diff --git a/go.sum b/go.sum index ff969f082b..7fa49fe501 100644 --- a/go.sum +++ b/go.sum @@ -1,24 +1,39 @@ -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 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/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/xuri/efp v0.0.0-20191019043341-b7dc4fe9aa91 h1:gp02YctZuIPTk0t7qI+wvg3VQwTPyNmSGG6ZqOsjSL8= github.com/xuri/efp v0.0.0-20191019043341-b7dc4fe9aa91/go.mod h1:uBiSUepVYMhGTfDeBKKasV4GpgBlzJ46gXUBAqV8qLk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a h1:gHevYm0pO4QUbwy8Dmdr01R5r1BuKtfYqRqF0h/Cbh0= -golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/image v0.0.0-20200430140353-33d19683fad8 h1:6WW6V3x1P/jokJBpRQYUJnMHRP6isStQwCozxnU7XQw= +golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f h1:QBjCr1Fz5kw158VqdE9JfI9cJnl/ymnJWAdMuinqL7Y= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 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/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= From dfea8f96edc326717822ec9c4b92f462d0fe1255 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 12 May 2020 23:26:26 +0800 Subject: [PATCH 235/957] - New API: SetSheetFormatPr and GetSheetFormatPr - typo fix, resolve #635 --- sheet.go | 2 +- sheetpr.go | 197 ++++++++++++++++++++++++++++++++++++++++++++++- sheetpr_test.go | 160 ++++++++++++++++++++++++++++++++++++++ xmlPivotCache.go | 11 +++ 4 files changed, 367 insertions(+), 3 deletions(-) diff --git a/sheet.go b/sheet.go index fa858af22f..6a935b1fdc 100644 --- a/sheet.go +++ b/sheet.go @@ -354,7 +354,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. If the given sheet name is invalid, it will return an -// integer type value -1. +// integer type value 0. func (f *File) GetSheetIndex(name string) int { var idx = -1 for index, sheet := range f.GetSheetList() { diff --git a/sheetpr.go b/sheetpr.go index 350e189a34..dbfb7341b3 100644 --- a/sheetpr.go +++ b/sheetpr.go @@ -313,7 +313,7 @@ type PageMarginsOptionsPtr interface { // SetPageMargins provides a function to set worksheet page margins. // // Available options: -// PageMarginBotom(float64) +// PageMarginBottom(float64) // PageMarginFooter(float64) // PageMarginHeader(float64) // PageMarginLeft(float64) @@ -339,7 +339,7 @@ func (f *File) SetPageMargins(sheet string, opts ...PageMarginsOptions) error { // GetPageMargins provides a function to get worksheet page margins. // // Available options: -// PageMarginBotom(float64) +// PageMarginBottom(float64) // PageMarginFooter(float64) // PageMarginHeader(float64) // PageMarginLeft(float64) @@ -357,3 +357,196 @@ func (f *File) GetPageMargins(sheet string, opts ...PageMarginsOptionsPtr) error } 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 + } + *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 + } + *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) + if err != nil { + return err + } + fp := s.SheetFormatPr + if fp == nil { + fp = new(xlsxSheetFormatPr) + s.SheetFormatPr = fp + } + for _, opt := range opts { + opt.setSheetFormatPr(fp) + } + 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) + if err != nil { + return err + } + fp := s.SheetFormatPr + for _, opt := range opts { + opt.getSheetFormatPr(fp) + } + return err +} diff --git a/sheetpr_test.go b/sheetpr_test.go index 25b67d753e..6e031518e1 100644 --- a/sheetpr_test.go +++ b/sheetpr_test.go @@ -307,3 +307,163 @@ func TestGetPageMargins(t *testing.T) { // Test get page margins on not exists worksheet. assert.EqualError(t, f.GetPageMargins("SheetN"), "sheet SheetN is not exist") } + +func ExampleFile_SetSheetFormatPr() { + f := excelize.NewFile() + const sheet = "Sheet1" + + if err := f.SetSheetFormatPr(sheet, + excelize.BaseColWidth(1.0), + excelize.DefaultColWidth(1.0), + excelize.DefaultRowHeight(1.0), + excelize.CustomHeight(true), + excelize.ZeroHeight(true), + excelize.ThickTop(true), + excelize.ThickBottom(true), + ); err != nil { + fmt.Println(err) + } + // Output: +} + +func ExampleFile_GetSheetFormatPr() { + f := excelize.NewFile() + const sheet = "Sheet1" + + var ( + baseColWidth excelize.BaseColWidth + defaultColWidth excelize.DefaultColWidth + defaultRowHeight excelize.DefaultRowHeight + customHeight excelize.CustomHeight + zeroHeight excelize.ZeroHeight + thickTop excelize.ThickTop + thickBottom excelize.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 excelize.SheetFormatPrOptionsPtr + nonDefault excelize.SheetFormatPrOptions + }{ + {new(excelize.BaseColWidth), excelize.BaseColWidth(1.0)}, + {new(excelize.DefaultColWidth), excelize.DefaultColWidth(1.0)}, + {new(excelize.DefaultRowHeight), excelize.DefaultRowHeight(1.0)}, + {new(excelize.CustomHeight), excelize.CustomHeight(true)}, + {new(excelize.ZeroHeight), excelize.ZeroHeight(true)}, + {new(excelize.ThickTop), excelize.ThickTop(true)}, + {new(excelize.ThickBottom), excelize.ThickBottom(true)}, + } + + for i, test := range testData { + t.Run(fmt.Sprintf("TestData%d", i), func(t *testing.T) { + + opt := test.nonDefault + t.Logf("option %T", opt) + + def := deepcopy.Copy(test.container).(excelize.SheetFormatPrOptionsPtr) + val1 := deepcopy.Copy(def).(excelize.SheetFormatPrOptionsPtr) + val2 := deepcopy.Copy(def).(excelize.SheetFormatPrOptionsPtr) + + f := excelize.NewFile() + // Get the default value + assert.NoError(t, f.GetSheetFormatPr(sheet, def), opt) + // Get again and check + assert.NoError(t, f.GetSheetFormatPr(sheet, val1), opt) + if !assert.Equal(t, val1, def, opt) { + t.FailNow() + } + // Set the same value + assert.NoError(t, f.SetSheetFormatPr(sheet, val1), opt) + // 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) { + t.FailNow() + } + // Set a different value + assert.NoError(t, f.SetSheetFormatPr(sheet, test.nonDefault), opt) + assert.NoError(t, f.GetSheetFormatPr(sheet, val1), opt) + // 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) { + 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) { + t.FailNow() + } + // Restore the default value + assert.NoError(t, f.SetSheetFormatPr(sheet, def), opt) + assert.NoError(t, f.GetSheetFormatPr(sheet, val1), opt) + if !assert.Equal(t, def, val1) { + t.FailNow() + } + }) + } +} + +func TestSetSheetFormatPr(t *testing.T) { + f := excelize.NewFile() + assert.NoError(t, f.GetSheetFormatPr("Sheet1")) + f.Sheet["xl/worksheets/sheet1.xml"].SheetFormatPr = nil + assert.NoError(t, f.SetSheetFormatPr("Sheet1", excelize.BaseColWidth(1.0))) + // Test set formatting properties on not exists worksheet. + assert.EqualError(t, f.SetSheetFormatPr("SheetN"), "sheet SheetN is not exist") +} + +func TestGetSheetFormatPr(t *testing.T) { + f := excelize.NewFile() + assert.NoError(t, f.GetSheetFormatPr("Sheet1")) + f.Sheet["xl/worksheets/sheet1.xml"].SheetFormatPr = nil + var ( + baseColWidth excelize.BaseColWidth + defaultColWidth excelize.DefaultColWidth + defaultRowHeight excelize.DefaultRowHeight + customHeight excelize.CustomHeight + zeroHeight excelize.ZeroHeight + thickTop excelize.ThickTop + thickBottom excelize.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 is not exist") +} diff --git a/xmlPivotCache.go b/xmlPivotCache.go index 45b48de99c..feaec54f03 100644 --- a/xmlPivotCache.go +++ b/xmlPivotCache.go @@ -1,3 +1,14 @@ +// Copyright 2016 - 2020 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. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX / XLSM / XLTM files. Supports reading and writing +// spreadsheet documents generated by Microsoft Exce™ 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.10 or later. + package excelize import "encoding/xml" From 9baa1bbc9865bee1b3c8981ab98eb8c9049c40e4 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 14 May 2020 22:36:00 +0800 Subject: [PATCH 236/957] Fix #637, improve the compatibility of the auto filter with Office 2007 - 2013 --- table.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/table.go b/table.go index 55901cd93a..ae47471f90 100644 --- a/table.go +++ b/table.go @@ -290,6 +290,20 @@ func (f *File) AutoFilter(sheet, hcell, vcell, format string) error { return err } ref := cellStart + ":" + cellEnd + wb := f.workbookReader() + d := xlsxDefinedName{ + Name: "_xlnm._FilterDatabase", + Hidden: true, + LocalSheetID: intPtr(f.GetSheetIndex(sheet)), + Data: fmt.Sprintf("%s!%s", sheet, ref), + } + if wb.DefinedNames != nil { + wb.DefinedNames.DefinedName = append(wb.DefinedNames.DefinedName, d) + } else { + wb.DefinedNames = &xlsxDefinedNames{ + DefinedName: []xlsxDefinedName{d}, + } + } refRange := vcol - hcol return f.autoFilter(sheet, ref, refRange, hcol, formatSet) } From c815e4b84b9b777c30d127f384b38105afa2640d Mon Sep 17 00:00:00 2001 From: yuemanxilou Date: Fri, 15 May 2020 14:03:02 +0800 Subject: [PATCH 237/957] avoid duplicate filter database in workbook defined name --- table.go | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/table.go b/table.go index ae47471f90..5a0e46f88e 100644 --- a/table.go +++ b/table.go @@ -281,28 +281,38 @@ func (f *File) AutoFilter(sheet, hcell, vcell, format string) error { formatSet, _ := parseAutoFilterSet(format) var cellStart, cellEnd string - cellStart, err = CoordinatesToCellName(hcol, hrow) - if err != nil { + if cellStart, err = CoordinatesToCellName(hcol, hrow); err != nil { return err } - cellEnd, err = CoordinatesToCellName(vcol, vrow) - if err != nil { + if cellEnd, err = CoordinatesToCellName(vcol, vrow); err != nil { return err } - ref := cellStart + ":" + cellEnd + ref, filterDB := cellStart+":"+cellEnd, "_xlnm._FilterDatabase" wb := f.workbookReader() + sheetID := f.GetSheetIndex(sheet) + filterRange := fmt.Sprintf("%s!%s", sheet, ref) d := xlsxDefinedName{ - Name: "_xlnm._FilterDatabase", + Name: filterDB, Hidden: true, - LocalSheetID: intPtr(f.GetSheetIndex(sheet)), - Data: fmt.Sprintf("%s!%s", sheet, ref), + LocalSheetID: intPtr(sheetID), + Data: filterRange, } - if wb.DefinedNames != nil { - wb.DefinedNames.DefinedName = append(wb.DefinedNames.DefinedName, d) - } else { + if wb.DefinedNames == nil { wb.DefinedNames = &xlsxDefinedNames{ DefinedName: []xlsxDefinedName{d}, } + } else { + var definedNameExists bool + for idx := range wb.DefinedNames.DefinedName { + definedName := wb.DefinedNames.DefinedName[idx] + if definedName.Name == filterDB && *definedName.LocalSheetID == sheetID && definedName.Hidden { + wb.DefinedNames.DefinedName[idx].Data = filterRange + definedNameExists = true + } + } + if !definedNameExists { + wb.DefinedNames.DefinedName = append(wb.DefinedNames.DefinedName, d) + } } refRange := vcol - hcol return f.autoFilter(sheet, ref, refRange, hcol, formatSet) From 98221a332ff9c37c9b20c44e9efdbe4c22a5cf5c Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 17 May 2020 17:36:53 +0800 Subject: [PATCH 238/957] Merge pull request #410 --- picture.go | 53 +++++++++++++++++++++++++++++++++++++++++++++++-- picture_test.go | 21 ++++++++++++++++++++ rows.go | 3 ++- xmlDrawing.go | 1 + 4 files changed, 75 insertions(+), 3 deletions(-) diff --git a/picture.go b/picture.go index 306a582230..71c3b8e9ec 100644 --- a/picture.go +++ b/picture.go @@ -32,6 +32,7 @@ func parseFormatPictureSet(formatSet string) (*formatPicture, error) { FPrintsWithSheet: true, FLocksWithSheet: false, NoChangeAspect: false, + Autofit: false, OffsetX: 0, OffsetY: 0, XScale: 1.0, @@ -244,8 +245,12 @@ func (f *File) addDrawingPicture(sheet, drawingXML, cell, file string, width, he if err != nil { return err } - width = int(float64(width) * formatSet.XScale) - height = int(float64(height) * formatSet.YScale) + if formatSet.Autofit { + width, height, col, row, err = f.drawingResize(sheet, cell, float64(width), float64(height), formatSet) + if err != nil { + return err + } + } col-- row-- colStart, rowStart, _, _, colEnd, rowEnd, x2, y2 := @@ -578,3 +583,47 @@ func (f *File) drawingsWriter() { } } } + +// drawingResize calculate the height and width after resizing. +func (f *File) drawingResize(sheet string, cell string, width, height float64, formatSet *formatPicture) (w, h, c, r int, err error) { + var mergeCells []MergeCell + mergeCells, err = f.GetMergeCells(sheet) + if err != nil { + return + } + var rng []int + var inMergeCell bool + if c, r, err = CellNameToCoordinates(cell); err != nil { + return + } + cellWidth, cellHeight := f.getColWidth(sheet, c), f.getRowHeight(sheet, r) + for _, mergeCell := range mergeCells { + if inMergeCell, err = f.checkCellInArea(cell, mergeCell[0]); err != nil { + return + } + if inMergeCell { + rng, _ = areaRangeToCoordinates(mergeCell.GetStartAxis(), mergeCell.GetEndAxis()) + sortCoordinates(rng) + } + } + if inMergeCell { + c, r = rng[0], rng[1] + for col := rng[0] - 1; col < rng[2]; col++ { + cellWidth += f.getColWidth(sheet, col) + } + for row := rng[1] - 1; row < rng[3]; row++ { + cellHeight += f.getRowHeight(sheet, row) + } + } + if float64(cellWidth) < width { + asp := float64(cellWidth) / width + width, height = float64(cellWidth), height*asp + } + if float64(cellHeight) < height { + 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) + return +} diff --git a/picture_test.go b/picture_test.go index fdc6f0db02..015d8540b7 100644 --- a/picture_test.go +++ b/picture_test.go @@ -47,6 +47,15 @@ func TestAddPicture(t *testing.T) { file, err := ioutil.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.SetRowHeight("AddPicture", 10, 30)) + assert.NoError(t, f.MergeCell("AddPicture", "B3", "D9")) + 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. assert.NoError(t, f.AddPictureFromBytes("Sheet1", "Q1", "", "Excel Logo", ".png", file)) // Test add picture to worksheet from bytes with illegal cell coordinates. @@ -181,3 +190,15 @@ func TestDeletePicture(t *testing.T) { // 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. + _, _, _, _, err := f.drawingResize("SheetN", "A1", 1, 1, nil) + assert.EqualError(t, err, "sheet SheetN is not exist") + // Test calculate drawing resize with invalid coordinates. + _, _, _, _, err = f.drawingResize("Sheet1", "", 1, 1, nil) + assert.EqualError(t, err, `cannot convert cell "" to coordinates: invalid cell name ""`) + f.Sheet["xl/worksheets/sheet1.xml"].MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:A"}}} + assert.EqualError(t, f.AddPicture("Sheet1", "A1", filepath.Join("test", "images", "excel.jpg"), `{"autofit": true}`), `cannot convert cell "A" to coordinates: invalid cell name "A"`) +} diff --git a/rows.go b/rows.go index 6a676727ed..17216df24b 100644 --- a/rows.go +++ b/rows.go @@ -148,7 +148,8 @@ func (err ErrSheetNotExist) Error() string { return fmt.Sprintf("sheet %s is not exist", string(err.SheetName)) } -// Rows return a rows iterator. For example: +// 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 { diff --git a/xmlDrawing.go b/xmlDrawing.go index a5e43a1cbc..808bed5499 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -419,6 +419,7 @@ type formatPicture 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"` From 2efc7107ff30dc7f1e1a798082616ee488f99489 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 21 May 2020 22:57:58 +0800 Subject: [PATCH 239/957] - transform the range to the matrix on the first arg of the formula - typo fix - reset cell with and height when insert picture into merged cell with autofit --- calc.go | 22 ++++++++++------------ excelize.go | 9 +++++---- lib.go | 8 ++++---- picture.go | 1 + 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/calc.go b/calc.go index 61b6dac773..bff7866b6e 100644 --- a/calc.go +++ b/calc.go @@ -56,7 +56,7 @@ type cellRange struct { // formulaArg is the argument of a formula or function. type formulaArg struct { Value string - Matrix []string + Matrix [][]string } // formulaFuncs is the type of the formula functions. @@ -172,8 +172,8 @@ func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error) } for idx, val := range result { arg := formulaArg{Value: val} - if idx < len(matrix) { - arg.Matrix = matrix[idx] + if idx == 0 { + arg.Matrix = matrix } argsList.PushBack(arg) } @@ -1850,17 +1850,13 @@ func det(sqMtx [][]float64) float64 { // func (fn *formulaFuncs) MDETERM(argsList *list.List) (result string, err error) { var num float64 - var rows int var numMtx = [][]float64{} - var strMtx = [][]string{} - for arg := argsList.Front(); arg != nil; arg = arg.Next() { - if len(arg.Value.(formulaArg).Matrix) == 0 { - break - } - strMtx = append(strMtx, arg.Value.(formulaArg).Matrix) - rows++ + var strMtx = argsList.Front().Value.(formulaArg).Matrix + if argsList.Len() < 1 { + return } - for _, row := range strMtx { + var rows = len(strMtx) + for _, row := range argsList.Front().Value.(formulaArg).Matrix { if len(row) != rows { err = errors.New(formulaErrorVALUE) return @@ -2630,3 +2626,5 @@ func (fn *formulaFuncs) TRUNC(argsList *list.List) (result string, err error) { result = fmt.Sprintf("%g", float64(int(number*adjust))/adjust) return } + +// Statistical functions diff --git a/excelize.go b/excelize.go index 04e2e85a36..3fd25aa39f 100644 --- a/excelize.go +++ b/excelize.go @@ -28,7 +28,7 @@ import ( "golang.org/x/net/html/charset" ) -// File define a populated XLSX file struct. +// File define a populated spreadsheet file struct. type File struct { checked map[string]bool sheetMap map[string]string @@ -52,8 +52,8 @@ type File struct { type charsetTranscoderFn func(charset string, input io.Reader) (rdr io.Reader, err error) -// OpenFile take the name of an XLSX file and returns a populated XLSX file -// struct for it. +// OpenFile take the name of an spreadsheet file and returns a populated +// spreadsheet file struct for it. func OpenFile(filename string) (*File, error) { file, err := os.Open(filename) if err != nil { @@ -83,7 +83,8 @@ func newFile() *File { } } -// OpenReader take an io.Reader and return a populated XLSX file. +// OpenReader read data stream from io.Reader and return a populated +// spreadsheet file. func OpenReader(r io.Reader) (*File, error) { b, err := ioutil.ReadAll(r) if err != nil { diff --git a/lib.go b/lib.go index 79c7cd42cf..41b03c70e9 100644 --- a/lib.go +++ b/lib.go @@ -21,7 +21,7 @@ import ( "unsafe" ) -// ReadZipReader can be used to read an XLSX in memory without touching the +// ReadZipReader can be used to read the spreadsheet in memory without touching the // filesystem. func ReadZipReader(r *zip.Reader) (map[string][]byte, int, error) { fileList := make(map[string][]byte, len(r.File)) @@ -160,8 +160,8 @@ func ColumnNumberToName(num int) (string, error) { // // Example: // -// CellCoordinates("A1") // returns 1, 1, nil -// CellCoordinates("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) { const msg = "cannot convert cell %q to coordinates: %v" @@ -184,7 +184,7 @@ func CellNameToCoordinates(cell string) (int, int, error) { // // Example: // -// CoordinatesToCellName(1, 1) // returns "A1", nil +// excelize.CoordinatesToCellName(1, 1) // returns "A1", nil // func CoordinatesToCellName(col, row int) (string, error) { if col < 1 || row < 1 { diff --git a/picture.go b/picture.go index 71c3b8e9ec..cac1af2e66 100644 --- a/picture.go +++ b/picture.go @@ -607,6 +607,7 @@ func (f *File) drawingResize(sheet string, cell string, width, height float64, f } } if inMergeCell { + cellWidth, cellHeight = 0, 0 c, r = rng[0], rng[1] for col := rng[0] - 1; col < rng[2]; col++ { cellWidth += f.getColWidth(sheet, col) From 82bb1153d7b7ff1c50725bf34bd3cbc75b228137 Mon Sep 17 00:00:00 2001 From: sachin-puranik <41720019+sachin-puranik@users.noreply.github.com> Date: Sat, 23 May 2020 10:21:46 +0530 Subject: [PATCH 240/957] Improved error handling and stoped the crash due to fatel error (#593) close #624 --- lib.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lib.go b/lib.go index 41b03c70e9..5b7e6d0e7c 100644 --- a/lib.go +++ b/lib.go @@ -24,10 +24,13 @@ import ( // ReadZipReader can be used to read the spreadsheet in memory without touching the // filesystem. func ReadZipReader(r *zip.Reader) (map[string][]byte, int, error) { + var err error fileList := make(map[string][]byte, len(r.File)) worksheets := 0 for _, v := range r.File { - fileList[v.Name] = readFile(v) + if fileList[v.Name], err = readFile(v); err != nil { + return nil, 0, err + } if strings.HasPrefix(v.Name, "xl/worksheets/sheet") { worksheets++ } @@ -53,16 +56,17 @@ func (f *File) saveFileList(name string, content []byte) { } // Read file content as string in a archive file. -func readFile(file *zip.File) []byte { +func readFile(file *zip.File) ([]byte, error) { rc, err := file.Open() if err != nil { - log.Fatal(err) + log.Println(err) + return nil, err } dat := make([]byte, 0, file.FileInfo().Size()) buff := bytes.NewBuffer(dat) _, _ = io.Copy(buff, rc) rc.Close() - return buff.Bytes() + return buff.Bytes(), nil } // SplitCellName splits cell name to column name and row number. From 7f78464f7f6ecd87c5f5c53d7c00320fd53c4a03 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 23 May 2020 13:29:51 +0800 Subject: [PATCH 241/957] add test for ReadZipReader, close #642 --- excelize_test.go | 16 ++++++++++++++++ lib.go | 14 ++------------ lib_test.go | 6 ++++++ 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/excelize_test.go b/excelize_test.go index 8ee8051d1f..f839136de2 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -220,6 +220,22 @@ func TestOpenReader(t *testing.T) { _, err = OpenReader(r) assert.EqualError(t, err, "unexpected EOF") + + _, err = OpenReader(bytes.NewReader([]byte{ + 0x50, 0x4b, 0x03, 0x04, 0x0a, 0x00, 0x09, 0x00, 0x63, 0x00, 0x47, 0xa3, 0xb6, 0x50, 0x00, 0x00, + 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x0b, 0x00, 0x70, 0x61, + 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x01, 0x99, 0x07, 0x00, 0x02, 0x00, 0x41, 0x45, 0x03, 0x00, + 0x00, 0x21, 0x06, 0x59, 0xc0, 0x12, 0xf3, 0x19, 0xc7, 0x51, 0xd1, 0xc9, 0x31, 0xcb, 0xcc, 0x8a, + 0xe1, 0x44, 0xe1, 0x56, 0x20, 0x24, 0x1f, 0xba, 0x09, 0xda, 0x53, 0xd5, 0xef, 0x50, 0x4b, 0x07, + 0x08, 0x00, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x4b, 0x01, + 0x02, 0x1f, 0x00, 0x0a, 0x00, 0x09, 0x00, 0x63, 0x00, 0x47, 0xa3, 0xb6, 0x50, 0x00, 0x00, 0x00, + 0x00, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x61, 0x73, 0x73, 0x77, + 0x6f, 0x72, 0x64, 0x01, 0x99, 0x07, 0x00, 0x02, 0x00, 0x41, 0x45, 0x03, 0x00, 0x00, 0x50, 0x4b, + 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") } func TestBrokenFile(t *testing.T) { diff --git a/lib.go b/lib.go index 5b7e6d0e7c..91b3635ab8 100644 --- a/lib.go +++ b/lib.go @@ -15,7 +15,6 @@ import ( "container/list" "fmt" "io" - "log" "strconv" "strings" "unsafe" @@ -59,7 +58,6 @@ func (f *File) saveFileList(name string, content []byte) { func readFile(file *zip.File) ([]byte, error) { rc, err := file.Open() if err != nil { - log.Println(err) return nil, err } dat := make([]byte, 0, file.FileInfo().Size()) @@ -176,11 +174,7 @@ func CellNameToCoordinates(cell string) (int, int, error) { } col, err := ColumnNameToNumber(colname) - if err != nil { - return -1, -1, fmt.Errorf(msg, cell, err) - } - - return col, row, nil + return col, row, err } // CoordinatesToCellName converts [X, Y] coordinates to alpha-numeric cell @@ -195,11 +189,7 @@ func CoordinatesToCellName(col, row int) (string, error) { return "", fmt.Errorf("invalid cell coordinates [%d, %d]", col, row) } colname, err := ColumnNumberToName(col) - if err != nil { - // Error should never happens here. - return "", fmt.Errorf("invalid cell coordinates [%d, %d]: %v", col, row, err) - } - return fmt.Sprintf("%s%d", colname, row), nil + return fmt.Sprintf("%s%d", colname, row), err } // boolPtr returns a pointer to a bool with the given value. diff --git a/lib_test.go b/lib_test.go index 4605e707a8..0e717b25e0 100644 --- a/lib_test.go +++ b/lib_test.go @@ -208,3 +208,9 @@ func TestBytesReplace(t *testing.T) { s := []byte{0x01} assert.EqualValues(t, s, bytesReplace(s, []byte{}, []byte{}, 0)) } + +func TestStack(t *testing.T) { + s := NewStack() + assert.Equal(t, s.Peek(), nil) + assert.Equal(t, s.Pop(), nil) +} From a546427fd9324af8320e3f7062ddba8d2343a3c3 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 24 May 2020 20:20:22 +0800 Subject: [PATCH 242/957] Resolve #643, avoid creating duplicate style --- excelize_test.go | 24 ++---- styles.go | 220 ++++++++++++++++++++++++++++++++++++++++------- styles_test.go | 19 +++- table_test.go | 2 +- 4 files changed, 215 insertions(+), 50 deletions(-) diff --git a/excelize_test.go b/excelize_test.go index f839136de2..5b651091cb 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -768,16 +768,14 @@ func TestSetCellStyleCustomNumberFormat(t *testing.T) { 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;@"}`) - if err != nil { - t.Log(err) - } + 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;@"}`) - if err != nil { - t.Log(err) - } + style, err = f.NewStyle(`{"custom_number_format": "[$-380A]dddd\\,\\ dd\" de \"mmmm\" de \"yyyy;@","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;@"}`) + assert.NoError(t, err) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellStyleCustomNumberFormat.xlsx"))) } @@ -790,21 +788,15 @@ func TestSetCellStyleFill(t *testing.T) { var style int // Test set fill for cell with invalid parameter. style, err = f.NewStyle(`{"fill":{"type":"gradient","color":["#FFFFFF","#E0EBF5"],"shading":6}}`) - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, err) assert.NoError(t, f.SetCellStyle("Sheet1", "O23", "O23", style)) style, err = f.NewStyle(`{"fill":{"type":"gradient","color":["#FFFFFF"],"shading":1}}`) - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, err) assert.NoError(t, f.SetCellStyle("Sheet1", "O23", "O23", style)) style, err = f.NewStyle(`{"fill":{"type":"pattern","color":[],"pattern":1}}`) - if !assert.NoError(t, err) { - t.FailNow() - } + assert.NoError(t, err) assert.NoError(t, f.SetCellStyle("Sheet1", "O23", "O23", style)) style, err = f.NewStyle(`{"fill":{"type":"pattern","color":["#E0EBF5"],"pattern":19}}`) diff --git a/styles.go b/styles.go index 72b20718bf..2f9a41e947 100644 --- a/styles.go +++ b/styles.go @@ -18,6 +18,7 @@ import ( "io" "log" "math" + "reflect" "strconv" "strings" ) @@ -1920,30 +1921,110 @@ func (f *File) NewStyle(style interface{}) (int, error) { return cellXfsID, errors.New("invalid parameter type") } s := f.stylesReader() - numFmtID := setNumFmt(s, fs) + // check given style already exist. + if cellXfsID = f.getStyleID(s, fs); cellXfsID != -1 { + return cellXfsID, err + } + + numFmtID := newNumFmt(s, fs) if fs.Font != nil { - s.Fonts.Count++ - s.Fonts.Font = append(s.Fonts.Font, f.setFont(fs)) - fontID = s.Fonts.Count - 1 + fontID = f.getFontID(s, fs) + if fontID == -1 { + s.Fonts.Count++ + s.Fonts.Font = append(s.Fonts.Font, f.newFont(fs)) + fontID = s.Fonts.Count - 1 + } } - s.Borders.Count++ - s.Borders.Border = append(s.Borders.Border, setBorders(fs)) - borderID = s.Borders.Count - 1 + borderID = getBorderID(s, fs) + if borderID == -1 { + if len(fs.Border) == 0 { + borderID = 0 + } else { + s.Borders.Count++ + s.Borders.Border = append(s.Borders.Border, newBorders(fs)) + borderID = s.Borders.Count - 1 + } + } - if fill := setFills(fs, true); fill != nil { - s.Fills.Count++ - s.Fills.Fill = append(s.Fills.Fill, fill) - fillID = s.Fills.Count - 1 + if fillID = getFillID(s, fs); fillID == -1 { + if fill := newFills(fs, true); fill != nil { + s.Fills.Count++ + s.Fills.Fill = append(s.Fills.Fill, fill) + fillID = s.Fills.Count - 1 + } else { + fillID = 0 + } } - applyAlignment, alignment := fs.Alignment != nil, setAlignment(fs) - applyProtection, protection := fs.Protection != nil, setProtection(fs) + 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 } +var getXfIDFuncs = map[string]func(int, xlsxXf, *Style) bool{ + "numFmt": func(numFmtID int, xf xlsxXf, style *Style) bool { + return xf.NumFmtID != nil && *xf.NumFmtID == numFmtID + }, + "font": func(fontID int, xf xlsxXf, style *Style) bool { + if style.Font == nil { + return (xf.FontID == nil || *xf.FontID == 0) && (xf.ApplyFont == nil || *xf.ApplyFont == false) + } + return xf.FontID != nil && *xf.FontID == fontID && xf.ApplyFont != nil && *xf.ApplyFont == true + }, + "fill": func(fillID int, xf xlsxXf, style *Style) bool { + if style.Fill.Type == "" { + return (xf.FillID == nil || *xf.FillID == 0) && (xf.ApplyFill == nil || *xf.ApplyFill == false) + } + return xf.FillID != nil && *xf.FillID == fillID && xf.ApplyFill != nil && *xf.ApplyFill == true + }, + "border": func(borderID int, xf xlsxXf, style *Style) bool { + if len(style.Border) == 0 { + return (xf.BorderID == nil || *xf.BorderID == 0) && (xf.ApplyBorder == nil || *xf.ApplyBorder == false) + } + return xf.BorderID != nil && *xf.BorderID == borderID && xf.ApplyBorder != nil && *xf.ApplyBorder == true + }, + "alignment": func(ID int, xf xlsxXf, style *Style) bool { + if style.Alignment == nil { + return xf.ApplyAlignment == nil || *xf.ApplyAlignment == false + } + return reflect.DeepEqual(xf.Alignment, newAlignment(style)) && xf.ApplyBorder != nil && *xf.ApplyBorder == true + }, + "protection": func(ID int, xf xlsxXf, style *Style) bool { + if style.Protection == nil { + return xf.ApplyProtection == nil || *xf.ApplyProtection == false + } + return reflect.DeepEqual(xf.Protection, newProtection(style)) && xf.ApplyProtection != nil && *xf.ApplyProtection == true + }, +} + +// getStyleID provides a function to get styleID by given style. If given +// style is not exist, will return -1. +func (f *File) getStyleID(ss *xlsxStyleSheet, style *Style) (styleID int) { + styleID = -1 + if ss.CellXfs == nil { + return + } + numFmtID, borderID, fillID, fontID := style.NumFmt, getBorderID(ss, style), getFillID(ss, style), f.getFontID(ss, style) + if style.CustomNumFmt != nil { + numFmtID = getCustomNumFmtID(ss, style) + } + for xfID, xf := range ss.CellXfs.Xf { + if getXfIDFuncs["numFmt"](numFmtID, xf, style) && + getXfIDFuncs["font"](fontID, xf, style) && + getXfIDFuncs["fill"](fillID, xf, style) && + getXfIDFuncs["border"](borderID, xf, style) && + getXfIDFuncs["alignment"](0, xf, style) && + getXfIDFuncs["protection"](0, xf, style) { + styleID = xfID + return + } + } + return +} + // 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 @@ -1955,16 +2036,16 @@ func (f *File) NewConditionalStyle(style string) (int, error) { return 0, err } dxf := dxf{ - Fill: setFills(fs, false), + Fill: newFills(fs, false), } if fs.Alignment != nil { - dxf.Alignment = setAlignment(fs) + dxf.Alignment = newAlignment(fs) } if len(fs.Border) > 0 { - dxf.Border = setBorders(fs) + dxf.Border = newBorders(fs) } if fs.Font != nil { - dxf.Font = f.setFont(fs) + dxf.Font = f.newFont(fs) } dxfStr, _ := xml.Marshal(dxf) if s.Dxfs == nil { @@ -2000,9 +2081,25 @@ func (f *File) readDefaultFont() *xlsxFont { return s.Fonts.Font[0] } -// setFont provides a function to add font style by given cell format +// getFontID provides a function to get font ID. +// If given font is not exist, will return -1. +func (f *File) getFontID(styleSheet *xlsxStyleSheet, style *Style) (fontID int) { + fontID = -1 + if styleSheet.Fonts == nil || style.Font == nil { + return + } + for idx, fnt := range styleSheet.Fonts.Font { + if reflect.DeepEqual(*fnt, *f.newFont(style)) { + fontID = idx + return + } + } + return +} + +// newFont provides a function to add font style by given cell format // settings. -func (f *File) setFont(style *Style) *xlsxFont { +func (f *File) newFont(style *Style) *xlsxFont { fontUnderlineType := map[string]string{"single": "single", "double": "double"} if style.Font.Size < 1 { style.Font.Size = 11 @@ -2036,9 +2133,9 @@ func (f *File) setFont(style *Style) *xlsxFont { return &fnt } -// setNumFmt provides a function to check if number format code in the range +// newNumFmt provides a function to check if number format code in the range // of built-in values. -func setNumFmt(styleSheet *xlsxStyleSheet, style *Style) int { +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 { @@ -2048,6 +2145,9 @@ func setNumFmt(styleSheet *xlsxStyleSheet, style *Style) int { dp += "0" } if style.CustomNumFmt != nil { + if customNumFmtID := getCustomNumFmtID(styleSheet, style); customNumFmtID != -1 { + return customNumFmtID + } return setCustomNumFmt(styleSheet, style) } _, ok := builtInNumFmt[style.NumFmt] @@ -2102,6 +2202,22 @@ func setCustomNumFmt(styleSheet *xlsxStyleSheet, style *Style) int { return nf.NumFmtID } +// getCustomNumFmtID provides a function to get custom number format code ID. +// If given custom number format code is not exist, will return -1. +func getCustomNumFmtID(styleSheet *xlsxStyleSheet, style *Style) (customNumFmtID int) { + customNumFmtID = -1 + if styleSheet.NumFmts == nil { + return + } + for _, numFmt := range styleSheet.NumFmts.NumFmt { + if style.CustomNumFmt != nil && numFmt.FormatCode == *style.CustomNumFmt { + customNumFmtID = numFmt.NumFmtID + return + } + } + return +} + // setLangNumFmt provides a function to set number format code with language. func setLangNumFmt(styleSheet *xlsxStyleSheet, style *Style) int { numFmts, ok := langNumFmt[style.Lang] @@ -2129,9 +2245,29 @@ func setLangNumFmt(styleSheet *xlsxStyleSheet, style *Style) int { return nf.NumFmtID } -// setFills provides a function to add fill elements in the styles.xml by +// getFillID provides a function to get fill ID. If given fill is not +// exist, will return -1. +func getFillID(styleSheet *xlsxStyleSheet, style *Style) (fillID int) { + fillID = -1 + if styleSheet.Fills == nil || style.Fill.Type == "" { + return + } + fills := newFills(style, true) + if fills == nil { + return + } + for idx, fill := range styleSheet.Fills.Fill { + if reflect.DeepEqual(fill, fills) { + fillID = idx + return + } + } + return +} + +// newFills provides a function to add fill elements in the styles.xml by // given cell format settings. -func setFills(style *Style, fg bool) *xlsxFill { +func newFills(style *Style, fg bool) *xlsxFill { var patterns = []string{ "none", "solid", @@ -2212,11 +2348,11 @@ func setFills(style *Style, fg bool) *xlsxFill { return &fill } -// setAlignment provides a function to formatting information pertaining to +// newAlignment provides a function to formatting information pertaining to // text alignment in cells. There are a variety of choices for how text is // aligned both horizontally and vertically, as well as indentation settings, // and so on. -func setAlignment(style *Style) *xlsxAlignment { +func newAlignment(style *Style) *xlsxAlignment { var alignment xlsxAlignment if style.Alignment != nil { alignment.Horizontal = style.Alignment.Horizontal @@ -2232,9 +2368,9 @@ func setAlignment(style *Style) *xlsxAlignment { return &alignment } -// setProtection provides a function to set protection properties associated +// newProtection provides a function to set protection properties associated // with the cell. -func setProtection(style *Style) *xlsxProtection { +func newProtection(style *Style) *xlsxProtection { var protection xlsxProtection if style.Protection != nil { protection.Hidden = style.Protection.Hidden @@ -2243,9 +2379,25 @@ func setProtection(style *Style) *xlsxProtection { return &protection } -// setBorders provides a function to add border elements in the styles.xml by +// getBorderID provides a function to get border ID. If given border is not +// exist, will return -1. +func getBorderID(styleSheet *xlsxStyleSheet, style *Style) (borderID int) { + borderID = -1 + if styleSheet.Borders == nil || len(style.Border) == 0 { + return + } + for idx, border := range styleSheet.Borders.Border { + if reflect.DeepEqual(*border, *newBorders(style)) { + borderID = idx + return + } + } + return +} + +// newBorders provides a function to add border elements in the styles.xml by // given borders format settings. -func setBorders(style *Style) *xlsxBorder { +func newBorders(style *Style) *xlsxBorder { var styles = []string{ "none", "thin", @@ -2308,10 +2460,18 @@ func setCellXfs(style *xlsxStyleSheet, fontID, numFmtID, fillID, borderID int, a xf.ApplyNumberFormat = boolPtr(true) } xf.FillID = intPtr(fillID) + if fillID != 0 { + xf.ApplyFill = boolPtr(true) + } xf.BorderID = intPtr(borderID) + if borderID != 0 { + xf.ApplyBorder = boolPtr(true) + } style.CellXfs.Count++ xf.Alignment = alignment - xf.ApplyAlignment = boolPtr(applyAlignment) + if alignment != nil { + xf.ApplyAlignment = boolPtr(applyAlignment) + } if applyProtection { xf.ApplyProtection = boolPtr(applyProtection) xf.Protection = protection diff --git a/styles_test.go b/styles_test.go index 1ff0e4e39e..9b8ba39e19 100644 --- a/styles_test.go +++ b/styles_test.go @@ -26,9 +26,7 @@ func TestStyleFill(t *testing.T) { for _, testCase := range cases { xl := NewFile() styleID, err := xl.NewStyle(testCase.format) - if err != nil { - t.Fatal(err) - } + assert.NoError(t, err) styles := xl.stylesReader() style := styles.CellXfs.Xf[styleID] @@ -38,6 +36,13 @@ func TestStyleFill(t *testing.T) { assert.Equal(t, *style.FillID, 0, testCase.label) } } + f := NewFile() + styleID1, err := f.NewStyle(`{"fill":{"type":"pattern","pattern":1,"color":["#000000"]}}`) + assert.NoError(t, err) + styleID2, err := f.NewStyle(`{"fill":{"type":"pattern","pattern":1,"color":["#000000"]}}`) + assert.NoError(t, err) + assert.Equal(t, styleID1, styleID2) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestStyleFill.xlsx"))) } func TestSetConditionalFormat(t *testing.T) { @@ -232,3 +237,11 @@ func TestSetCellStyle(t *testing.T) { // Test set cell style on not exists worksheet. assert.EqualError(t, f.SetCellStyle("SheetN", "A1", "A2", 1), "sheet SheetN is not exist") } + +func TestGetStyleID(t *testing.T) { + assert.Equal(t, -1, NewFile().getStyleID(&xlsxStyleSheet{}, nil)) +} + +func TestGetFillID(t *testing.T) { + assert.Equal(t, -1, getFillID(NewFile().stylesReader(), &Style{Fill: Fill{Type: "unknown"}})) +} diff --git a/table_test.go b/table_test.go index 89c03e2446..127ee1bbf3 100644 --- a/table_test.go +++ b/table_test.go @@ -93,7 +93,7 @@ func TestAutoFilterError(t *testing.T) { } for i, format := range formats { t.Run(fmt.Sprintf("Expression%d", i+1), func(t *testing.T) { - err = f.AutoFilter("Sheet3", "D4", "B1", format) + err = f.AutoFilter("Sheet2", "D4", "B1", format) if assert.Error(t, err) { assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, i+1))) } From 1aeb8182357ae4f1415fccb586c85f26483f0d99 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 25 May 2020 00:22:58 +0800 Subject: [PATCH 243/957] avoid creating duplicate number format --- styles.go | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/styles.go b/styles.go index 2f9a41e947..a91aca675a 100644 --- a/styles.go +++ b/styles.go @@ -1036,9 +1036,7 @@ func (f *File) sharedStringsWriter() { // parseFormatStyleSet provides a function to parse the format settings of the // cells and conditional formats. func parseFormatStyleSet(style string) (*Style, error) { - format := Style{ - DecimalPlaces: 2, - } + format := Style{} err := json.Unmarshal([]byte(style), &format) return &format, err } @@ -1920,6 +1918,9 @@ func (f *File) NewStyle(style interface{}) (int, error) { default: return cellXfsID, errors.New("invalid parameter type") } + if fs.DecimalPlaces == 0 { + fs.DecimalPlaces = 2 + } s := f.stylesReader() // check given style already exist. if cellXfsID = f.getStyleID(s, fs); cellXfsID != -1 { @@ -1966,6 +1967,12 @@ func (f *File) NewStyle(style interface{}) (int, error) { var getXfIDFuncs = map[string]func(int, xlsxXf, *Style) bool{ "numFmt": func(numFmtID int, xf xlsxXf, style *Style) bool { + if style.NumFmt == 0 && style.CustomNumFmt == nil && numFmtID == -1 { + return xf.NumFmtID != nil || *xf.NumFmtID == 0 + } + if style.NegRed || style.Lang != "" || style.DecimalPlaces != 2 { + return false + } return xf.NumFmtID != nil && *xf.NumFmtID == numFmtID }, "font": func(fontID int, xf xlsxXf, style *Style) bool { @@ -2007,7 +2014,7 @@ func (f *File) getStyleID(ss *xlsxStyleSheet, style *Style) (styleID int) { if ss.CellXfs == nil { return } - numFmtID, borderID, fillID, fontID := style.NumFmt, getBorderID(ss, style), getFillID(ss, style), f.getFontID(ss, style) + 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) } @@ -2133,6 +2140,27 @@ func (f *File) newFont(style *Style) *xlsxFont { return &fnt } +// getNumFmtID provides a function to get number format code ID. +// If given number format code is not exist, will return -1. +func getNumFmtID(styleSheet *xlsxStyleSheet, style *Style) (numFmtID int) { + numFmtID = -1 + if styleSheet.NumFmts == nil { + return + } + if _, ok := builtInNumFmt[style.NumFmt]; ok { + return style.NumFmt + } + if fmtCode, ok := currencyNumFmt[style.NumFmt]; ok { + for _, numFmt := range styleSheet.NumFmts.NumFmt { + if numFmt.FormatCode == fmtCode { + numFmtID = numFmt.NumFmtID + return + } + } + } + return +} + // newNumFmt provides a function to check if number format code in the range // of built-in values. func newNumFmt(styleSheet *xlsxStyleSheet, style *Style) int { From 5c99300ee44de15e92bd8c5a92f2183c804d1379 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 26 May 2020 02:09:39 +0800 Subject: [PATCH 244/957] Fix #622, storage string to SST (shared string table) --- cell.go | 46 ++++++++++++++++++++++++++++++++++----------- rows.go | 10 ++++++++++ xmlSharedStrings.go | 2 +- 3 files changed, 46 insertions(+), 12 deletions(-) diff --git a/cell.go b/cell.go index 97feb0a6bb..e64ef2615e 100644 --- a/cell.go +++ b/cell.go @@ -274,10 +274,43 @@ func (f *File) SetCellStr(sheet, axis, value string) error { return err } cellData.S = f.prepareCellStyle(xlsx, col, cellData.S) - cellData.T, cellData.V, cellData.XMLSpace = setCellStr(value) + cellData.T, cellData.V, cellData.XMLSpace = f.setCellString(value) return err } +// setCellString provides a function to set string type to shared string +// table. +func (f *File) setCellString(value string) (t string, v string, ns xml.Attr) { + if len(value) > 32767 { + value = value[0:32767] + } + // Leading and ending space(s) character detection. + if len(value) > 0 && (value[0] == 32 || value[len(value)-1] == 32) { + ns = xml.Attr{ + Name: xml.Name{Space: NameSpaceXML, Local: "space"}, + Value: "preserve", + } + } + t = "s" + v = strconv.Itoa(f.setSharedString(value)) + return +} + +// setSharedString provides a function to add string to the share string table. +func (f *File) setSharedString(val string) int { + sst := f.sharedStringsReader() + for i, si := range sst.SI { + if si.T == val { + return i + } + } + sst.Count++ + sst.UniqueCount++ + sst.SI = append(sst.SI, xlsxSI{T: val}) + return sst.UniqueCount - 1 +} + +// setCellStr provides a function to set string type to cell. func setCellStr(value string) (t string, v string, ns xml.Attr) { if len(value) > 32767 { value = value[0:32767] @@ -590,7 +623,7 @@ func (f *File) SetCellRichText(sheet, cell string, runs []RichTextRun) error { for _, textRun := range runs { run := xlsxR{T: &xlsxT{Val: textRun.Text}} if strings.ContainsAny(textRun.Text, "\r\n ") { - run.T.Space = "preserve" + run.T.Space = xml.Attr{Name: xml.Name{Space: NameSpaceXML, Local: "space"}, Value: "preserve"} } fnt := textRun.Font if fnt != nil { @@ -625,15 +658,6 @@ func (f *File) SetCellRichText(sheet, cell string, runs []RichTextRun) error { sst.Count++ sst.UniqueCount++ cellData.T, cellData.V = "s", strconv.Itoa(len(sst.SI)-1) - f.addContentTypePart(0, "sharedStrings") - rels := f.relsReader("xl/_rels/workbook.xml.rels") - for _, rel := range rels.Relationships { - if rel.Target == "sharedStrings.xml" { - return err - } - } - // Update xl/_rels/workbook.xml.rels - f.addRels("xl/_rels/workbook.xml.rels", SourceRelationshipSharedStrings, "sharedStrings.xml", "") return err } diff --git a/rows.go b/rows.go index 17216df24b..5be3182b0e 100644 --- a/rows.go +++ b/rows.go @@ -285,12 +285,22 @@ func (f *File) sharedStringsReader() *xlsxSST { ss := f.readXML("xl/sharedStrings.xml") if len(ss) == 0 { ss = f.readXML("xl/SharedStrings.xml") + delete(f.XLSX, "xl/SharedStrings.xml") } if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(ss))). Decode(&sharedStrings); err != nil && err != io.EOF { log.Printf("xml decode error: %s", err) } f.SharedStrings = &sharedStrings + f.addContentTypePart(0, "sharedStrings") + rels := f.relsReader("xl/_rels/workbook.xml.rels") + for _, rel := range rels.Relationships { + if rel.Target == "sharedStrings.xml" { + return f.SharedStrings + } + } + // Update xl/_rels/workbook.xml.rels + f.addRels("xl/_rels/workbook.xml.rels", SourceRelationshipSharedStrings, "sharedStrings.xml", "") } return f.SharedStrings diff --git a/xmlSharedStrings.go b/xmlSharedStrings.go index a6525df177..6e34abc261 100644 --- a/xmlSharedStrings.go +++ b/xmlSharedStrings.go @@ -66,7 +66,7 @@ type xlsxR struct { // xlsxT directly maps the t element in the run properties. type xlsxT struct { XMLName xml.Name `xml:"t"` - Space string `xml:"xml:space,attr,omitempty"` + Space xml.Attr `xml:"space,attr,omitempty"` Val string `xml:",innerxml"` } From c168233e70db8f220bd07d9d6d277ae9e2a4a73f Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 27 May 2020 00:02:29 +0800 Subject: [PATCH 245/957] speedup get cell value from shared string table --- cell.go | 7 +++---- excelize.go | 2 ++ rows.go | 5 +++++ 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/cell.go b/cell.go index e64ef2615e..6981cce8f8 100644 --- a/cell.go +++ b/cell.go @@ -299,14 +299,13 @@ func (f *File) setCellString(value string) (t string, v string, ns xml.Attr) { // setSharedString provides a function to add string to the share string table. func (f *File) setSharedString(val string) int { sst := f.sharedStringsReader() - for i, si := range sst.SI { - if si.T == val { - return i - } + if i, ok := f.sharedStringsMap[val]; ok { + return i } sst.Count++ sst.UniqueCount++ sst.SI = append(sst.SI, xlsxSI{T: val}) + f.sharedStringsMap[val] = sst.UniqueCount - 1 return sst.UniqueCount - 1 } diff --git a/excelize.go b/excelize.go index 3fd25aa39f..3e0255aca9 100644 --- a/excelize.go +++ b/excelize.go @@ -38,6 +38,7 @@ type File struct { Drawings map[string]*xlsxWsDr Path string SharedStrings *xlsxSST + sharedStringsMap map[string]int Sheet map[string]*xlsxWorksheet SheetCount int Styles *xlsxStyleSheet @@ -75,6 +76,7 @@ func newFile() *File { sheetMap: make(map[string]string), Comments: make(map[string]*xlsxComments), Drawings: make(map[string]*xlsxWsDr), + sharedStringsMap: make(map[string]int), Sheet: make(map[string]*xlsxWorksheet), DecodeVMLDrawing: make(map[string]*decodeVmlDrawing), VMLDrawing: make(map[string]*vmlDrawing), diff --git a/rows.go b/rows.go index 5be3182b0e..352f1eb8df 100644 --- a/rows.go +++ b/rows.go @@ -292,6 +292,11 @@ func (f *File) sharedStringsReader() *xlsxSST { log.Printf("xml decode error: %s", err) } f.SharedStrings = &sharedStrings + for i := range sharedStrings.SI { + if sharedStrings.SI[i].T != "" { + f.sharedStringsMap[sharedStrings.SI[i].T] = i + } + } f.addContentTypePart(0, "sharedStrings") rels := f.relsReader("xl/_rels/workbook.xml.rels") for _, rel := range rels.Relationships { From 2ae631376b95ff0a59ea18c2c0befcd50135b020 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 29 May 2020 00:26:40 +0800 Subject: [PATCH 246/957] add limits for total columns, row and filename length --- cell.go | 10 +++++----- excelize_test.go | 1 + file.go | 4 ++++ lib.go | 7 ++++++- lib_test.go | 5 ++++- xmlDrawing.go | 9 +++++++++ 6 files changed, 29 insertions(+), 7 deletions(-) diff --git a/cell.go b/cell.go index 6981cce8f8..064c432e6f 100644 --- a/cell.go +++ b/cell.go @@ -281,8 +281,8 @@ func (f *File) SetCellStr(sheet, axis, value string) error { // setCellString provides a function to set string type to shared string // table. func (f *File) setCellString(value string) (t string, v string, ns xml.Attr) { - if len(value) > 32767 { - value = value[0:32767] + if len(value) > TotalCellChars { + value = value[0:TotalCellChars] } // Leading and ending space(s) character detection. if len(value) > 0 && (value[0] == 32 || value[len(value)-1] == 32) { @@ -311,8 +311,8 @@ func (f *File) setSharedString(val string) int { // setCellStr provides a function to set string type to cell. func setCellStr(value string) (t string, v string, ns xml.Attr) { - if len(value) > 32767 { - value = value[0:32767] + if len(value) > TotalCellChars { + value = value[0:TotalCellChars] } // Leading and ending space(s) character detection. if len(value) > 0 && (value[0] == 32 || value[len(value)-1] == 32) { @@ -476,7 +476,7 @@ func (f *File) SetCellHyperLink(sheet, axis, link, linkType string) error { xlsx.Hyperlinks = new(xlsxHyperlinks) } - if len(xlsx.Hyperlinks.Hyperlink) > 65529 { + if len(xlsx.Hyperlinks.Hyperlink) > TotalSheetHyperlinks { return errors.New("over maximum limit hyperlinks in a worksheet") } diff --git a/excelize_test.go b/excelize_test.go index 5b651091cb..b31e1c844e 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -166,6 +166,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")), "file name length exceeds maximum limit") } func TestSaveFile(t *testing.T) { diff --git a/file.go b/file.go index 8fe4115d9e..e8f29dae97 100644 --- a/file.go +++ b/file.go @@ -12,6 +12,7 @@ package excelize import ( "archive/zip" "bytes" + "errors" "fmt" "io" "os" @@ -62,6 +63,9 @@ func (f *File) Save() error { // SaveAs provides a function to create or update to an xlsx file at the // provided path. func (f *File) SaveAs(name string) error { + if len(name) > FileNameLength { + return errors.New("file name length exceeds maximum limit") + } file, err := os.OpenFile(name, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0666) if err != nil { return err diff --git a/lib.go b/lib.go index 91b3635ab8..d97bb20973 100644 --- a/lib.go +++ b/lib.go @@ -135,6 +135,9 @@ func ColumnNameToNumber(name string) (int, error) { } multi *= 26 } + if col > TotalColumns { + return -1, fmt.Errorf("column number exceeds maximum limit") + } return col, nil } @@ -172,7 +175,9 @@ func CellNameToCoordinates(cell string) (int, int, error) { if err != nil { return -1, -1, fmt.Errorf(msg, cell, err) } - + if row > TotalRows { + return -1, -1, fmt.Errorf("row number exceeds maximum limit") + } col, err := ColumnNameToNumber(colname) return col, row, err } diff --git a/lib_test.go b/lib_test.go index 0e717b25e0..229412ca14 100644 --- a/lib_test.go +++ b/lib_test.go @@ -23,7 +23,6 @@ var validColumns = []struct { {Name: "AZ", Num: 26 + 26}, {Name: "ZZ", Num: 26 + 26*26}, {Name: "AAA", Num: 26 + 26*26 + 1}, - {Name: "ZZZ", Num: 26 + 26*26 + 26*26*26}, } var invalidColumns = []struct { @@ -72,6 +71,8 @@ func TestColumnNameToNumber_Error(t *testing.T) { assert.Equalf(t, col.Num, out, msg, col.Name) } } + _, err := ColumnNameToNumber("XFE") + assert.EqualError(t, err, "column number exceeds maximum limit") } func TestColumnNumberToName_OK(t *testing.T) { @@ -172,6 +173,8 @@ func TestCellNameToCoordinates_Error(t *testing.T) { assert.Equalf(t, -1, r, msg, cell) } } + _, _, err := CellNameToCoordinates("A1048577") + assert.EqualError(t, err, "row number exceeds maximum limit") } func TestCoordinatesToCellName_OK(t *testing.T) { diff --git a/xmlDrawing.go b/xmlDrawing.go index 808bed5499..b2eeed67d1 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -80,6 +80,15 @@ const ( ExtURIMacExcelMX = "{64002731-A6B0-56B0-2670-7721B7C09600}" ) +// Excel specifications and limits +const ( + FileNameLength = 207 + TotalRows = 1048576 + TotalColumns = 16384 + TotalSheetHyperlinks = 65529 + TotalCellChars = 32767 +) + var supportImageTypes = map[string]string{".gif": ".gif", ".jpg": ".jpeg", ".jpeg": ".jpeg", ".png": ".png", ".tif": ".tiff", ".tiff": ".tiff"} // xlsxCNvPr directly maps the cNvPr (Non-Visual Drawing Properties). This From fa2571a17e869d5793d14dd67d8e2a6d15e80daf Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 30 May 2020 23:21:02 +0800 Subject: [PATCH 247/957] fn: SUMIF --- calc.go | 520 ++++++++++++++++++++++++++++++++++++--------------- calc_test.go | 33 +++- 2 files changed, 393 insertions(+), 160 deletions(-) diff --git a/calc.go b/calc.go index bff7866b6e..6bb15def1e 100644 --- a/calc.go +++ b/calc.go @@ -19,6 +19,7 @@ import ( "math" "math/rand" "reflect" + "regexp" "strconv" "strings" "time" @@ -53,10 +54,39 @@ type cellRange struct { To cellRef } +// formula criteria condition enumeration. +const ( + _ byte = iota + criteriaEq + criteriaLe + criteriaGe + criteriaL + criteriaG + criteriaBeg + criteriaEnd +) + +// formulaCriteria defined formula criteria parser result. +type formulaCriteria struct { + Type byte + Condition string +} + +// ArgType is the type if formula argument type. +type ArgType byte + +// Formula argument types enumeration. +const ( + ArgUnknown ArgType = iota + ArgString + ArgMatrix +) + // formulaArg is the argument of a formula or function. type formulaArg struct { - Value string - Matrix [][]string + String string + Matrix [][]formulaArg + Type ArgType } // formulaFuncs is the type of the formula functions. @@ -150,36 +180,30 @@ func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error) if token.TSubType == efp.TokenSubTypeRange { if !opftStack.Empty() { // parse reference: must reference at here - result, _, err := f.parseReference(sheet, token.TValue) + result, err := f.parseReference(sheet, token.TValue) if err != nil { return efp.Token{TValue: formulaErrorNAME}, err } - if len(result) != 1 { + if result.Type != ArgString { return efp.Token{}, errors.New(formulaErrorVALUE) } opfdStack.Push(efp.Token{ TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber, - TValue: result[0], + TValue: result.String, }) continue } if nextToken.TType == efp.TokenTypeArgument || nextToken.TType == efp.TokenTypeFunction { // parse reference: reference or range at here - result, matrix, err := f.parseReference(sheet, token.TValue) + result, err := f.parseReference(sheet, token.TValue) if err != nil { return efp.Token{TValue: formulaErrorNAME}, err } - for idx, val := range result { - arg := formulaArg{Value: val} - if idx == 0 { - arg.Matrix = matrix - } - argsList.PushBack(arg) - } - if len(result) == 0 { + if result.Type == ArgUnknown { return efp.Token{}, errors.New(formulaErrorVALUE) } + argsList.PushBack(result) continue } } @@ -201,7 +225,8 @@ func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error) } if !opfdStack.Empty() { argsList.PushBack(formulaArg{ - Value: opfdStack.Pop().(efp.Token).TValue, + String: opfdStack.Pop().(efp.Token).TValue, + Type: ArgString, }) } continue @@ -214,7 +239,8 @@ func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error) // current token is text if token.TType == efp.TokenTypeOperand && token.TSubType == efp.TokenSubTypeText { argsList.PushBack(formulaArg{ - Value: token.TValue, + String: token.TValue, + Type: ArgString, }) } @@ -232,7 +258,8 @@ func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error) // push opfd to args if opfdStack.Len() > 0 { argsList.PushBack(formulaArg{ - Value: opfdStack.Pop().(efp.Token).TValue, + String: opfdStack.Pop().(efp.Token).TValue, + Type: ArgString, }) } // call formula function to evaluate @@ -430,14 +457,14 @@ func isOperatorPrefixToken(token efp.Token) bool { func (f *File) parseToken(sheet string, token efp.Token, opdStack, optStack *Stack) error { // parse reference: must reference at here if token.TSubType == efp.TokenSubTypeRange { - result, _, err := f.parseReference(sheet, token.TValue) + result, err := f.parseReference(sheet, token.TValue) if err != nil { return errors.New(formulaErrorNAME) } - if len(result) != 1 { + if result.Type != ArgString { return errors.New(formulaErrorVALUE) } - token.TValue = result[0] + token.TValue = result.String token.TType = efp.TokenTypeOperand token.TSubType = efp.TokenSubTypeNumber } @@ -468,7 +495,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) (result []string, matrix [][]string, err error) { +func (f *File) parseReference(sheet, reference string) (arg formulaArg, err error) { reference = strings.Replace(reference, "$", "", -1) refs, cellRanges, cellRefs := list.New(), list.New(), list.New() for _, ref := range strings.Split(reference, ":") { @@ -507,39 +534,38 @@ func (f *File) parseReference(sheet, reference string) (result []string, matrix cellRefs.PushBack(e.Value.(cellRef)) refs.Remove(e) } - - result, matrix, err = f.rangeResolver(cellRefs, cellRanges) + arg, err = f.rangeResolver(cellRefs, cellRanges) return } // prepareValueRange prepare value range. func prepareValueRange(cr cellRange, valueRange []int) { - if cr.From.Row < valueRange[0] { + if cr.From.Row < valueRange[0] || valueRange[0] == 0 { valueRange[0] = cr.From.Row } - if cr.From.Col < valueRange[2] { + if cr.From.Col < valueRange[2] || valueRange[2] == 0 { valueRange[2] = cr.From.Col } - if cr.To.Row > valueRange[0] { + if cr.To.Row > valueRange[1] || valueRange[1] == 0 { valueRange[1] = cr.To.Row } - if cr.To.Col > valueRange[3] { + if cr.To.Col > valueRange[3] || valueRange[3] == 0 { valueRange[3] = cr.To.Col } } // prepareValueRef prepare value reference. func prepareValueRef(cr cellRef, valueRange []int) { - if cr.Row < valueRange[0] { + if cr.Row < valueRange[0] || valueRange[0] == 0 { valueRange[0] = cr.Row } - if cr.Col < valueRange[2] { + if cr.Col < valueRange[2] || valueRange[2] == 0 { valueRange[2] = cr.Col } - if cr.Row > valueRange[0] { + if cr.Row > valueRange[1] || valueRange[1] == 0 { valueRange[1] = cr.Row } - if cr.Col > valueRange[3] { + if cr.Col > valueRange[3] || valueRange[3] == 0 { valueRange[3] = cr.Col } } @@ -547,11 +573,10 @@ func prepareValueRef(cr cellRef, valueRange []int) { // 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) (result []string, matrix [][]string, err error) { +func (f *File) rangeResolver(cellRefs, cellRanges *list.List) (arg formulaArg, err error) { // value range order: from row, to row, from column, to column - valueRange := []int{1, 1, 1, 1} + valueRange := []int{0, 0, 0, 0} var sheet string - filter := map[string]string{} // prepare value range for temp := cellRanges.Front(); temp != nil; temp = temp.Next() { cr := temp.Value.(cellRange) @@ -560,6 +585,7 @@ func (f *File) rangeResolver(cellRefs, cellRanges *list.List) (result []string, } 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] prepareValueRange(cr, valueRange) if cr.From.Sheet != "" { sheet = cr.From.Sheet @@ -574,8 +600,9 @@ func (f *File) rangeResolver(cellRefs, cellRanges *list.List) (result []string, } // extract value from ranges if cellRanges.Len() > 0 { + arg.Type = ArgMatrix for row := valueRange[0]; row <= valueRange[1]; row++ { - var matrixRow = []string{} + var matrixRow = []formulaArg{} for col := valueRange[2]; col <= valueRange[3]; col++ { var cell, value string if cell, err = CoordinatesToCellName(col, row); err != nil { @@ -584,11 +611,12 @@ func (f *File) rangeResolver(cellRefs, cellRanges *list.List) (result []string, if value, err = f.GetCellValue(sheet, cell); err != nil { return } - filter[cell] = value - matrixRow = append(matrixRow, value) - result = append(result, value) + matrixRow = append(matrixRow, formulaArg{ + String: value, + Type: ArgString, + }) } - matrix = append(matrix, matrixRow) + arg.Matrix = append(arg.Matrix, matrixRow) } return } @@ -599,13 +627,10 @@ func (f *File) rangeResolver(cellRefs, cellRanges *list.List) (result []string, if cell, err = CoordinatesToCellName(cr.Col, cr.Row); err != nil { return } - if filter[cell], err = f.GetCellValue(cr.Sheet, cell); err != nil { + if arg.String, err = f.GetCellValue(cr.Sheet, cell); err != nil { return } - } - - for _, val := range filter { - result = append(result, val) + arg.Type = ArgString } return } @@ -630,6 +655,90 @@ func callFuncByName(receiver interface{}, name string, params []reflect.Value) ( return } +// formulaCriteriaParser parse formula criteria. +func formulaCriteriaParser(exp string) (fc *formulaCriteria) { + fc = &formulaCriteria{} + if exp == "" { + return + } + if match := regexp.MustCompile(`^([0-9]+)$`).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 = 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 + } + if strings.Contains(exp, "*") { + if strings.HasPrefix(exp, "*") { + fc.Type, fc.Condition = criteriaEnd, strings.TrimPrefix(exp, "*") + } + if strings.HasSuffix(exp, "*") { + fc.Type, fc.Condition = criteriaBeg, strings.TrimSuffix(exp, "*") + } + return + } + fc.Type, fc.Condition = criteriaEq, exp + return +} + +// formulaCriteriaEval evaluate formula criteria expression. +func formulaCriteriaEval(val string, criteria *formulaCriteria) (result bool, err error) { + var value, expected float64 + var prepareValue = func(val, cond string) (value float64, expected float64, err error) { + value, _ = strconv.ParseFloat(val, 64) + if expected, err = strconv.ParseFloat(criteria.Condition, 64); err != nil { + return + } + return + } + switch criteria.Type { + case criteriaEq: + return val == criteria.Condition, err + case criteriaLe: + if value, expected, err = prepareValue(val, criteria.Condition); err != nil { + return + } + return value <= expected, err + case criteriaGe: + if value, expected, err = prepareValue(val, criteria.Condition); err != nil { + return + } + return value >= expected, err + case criteriaL: + if value, expected, err = prepareValue(val, criteria.Condition); err != nil { + return + } + return value < expected, err + case criteriaG: + if value, expected, err = prepareValue(val, criteria.Condition); err != nil { + return + } + return value > expected, err + case criteriaBeg: + return strings.HasPrefix(val, criteria.Condition), err + case criteriaEnd: + return strings.HasSuffix(val, criteria.Condition), err + } + return +} + // Math and Trigonometric functions // ABS function returns the absolute value of any supplied number. The syntax @@ -643,7 +752,7 @@ func (fn *formulaFuncs) ABS(argsList *list.List) (result string, err error) { return } var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -663,7 +772,7 @@ func (fn *formulaFuncs) ACOS(argsList *list.List) (result string, err error) { return } var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -682,7 +791,7 @@ func (fn *formulaFuncs) ACOSH(argsList *list.List) (result string, err error) { return } var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -702,7 +811,7 @@ func (fn *formulaFuncs) ACOT(argsList *list.List) (result string, err error) { return } var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -721,7 +830,7 @@ func (fn *formulaFuncs) ACOTH(argsList *list.List) (result string, err error) { return } var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -741,7 +850,7 @@ func (fn *formulaFuncs) ARABIC(argsList *list.List) (result string, err error) { } charMap := map[rune]float64{'I': 1, 'V': 5, 'X': 10, 'L': 50, 'C': 100, 'D': 500, 'M': 1000} val, last, prefix := 0.0, 0.0, 1.0 - for _, char := range argsList.Front().Value.(formulaArg).Value { + for _, char := range argsList.Front().Value.(formulaArg).String { digit := 0.0 if char == '-' { prefix = -1 @@ -778,7 +887,7 @@ func (fn *formulaFuncs) ASIN(argsList *list.List) (result string, err error) { return } var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -797,7 +906,7 @@ func (fn *formulaFuncs) ASINH(argsList *list.List) (result string, err error) { return } var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -817,7 +926,7 @@ func (fn *formulaFuncs) ATAN(argsList *list.List) (result string, err error) { return } var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -836,7 +945,7 @@ func (fn *formulaFuncs) ATANH(argsList *list.List) (result string, err error) { return } var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -856,11 +965,11 @@ func (fn *formulaFuncs) ATAN2(argsList *list.List) (result string, err error) { return } var x, y float64 - if x, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + if x, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } - if y, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if y, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -884,11 +993,11 @@ func (fn *formulaFuncs) BASE(argsList *list.List) (result string, err error) { } var number float64 var radix, minLength int - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } - if radix, err = strconv.Atoi(argsList.Front().Next().Value.(formulaArg).Value); err != nil { + if radix, err = strconv.Atoi(argsList.Front().Next().Value.(formulaArg).String); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -897,7 +1006,7 @@ func (fn *formulaFuncs) BASE(argsList *list.List) (result string, err error) { return } if argsList.Len() > 2 { - if minLength, err = strconv.Atoi(argsList.Back().Value.(formulaArg).Value); err != nil { + if minLength, err = strconv.Atoi(argsList.Back().Value.(formulaArg).String); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -925,7 +1034,7 @@ func (fn *formulaFuncs) CEILING(argsList *list.List) (result string, err error) return } number, significance, res := 0.0, 1.0, 0.0 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -933,7 +1042,7 @@ func (fn *formulaFuncs) CEILING(argsList *list.List) (result string, err error) significance = -1 } if argsList.Len() > 1 { - if significance, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + if significance, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -969,7 +1078,7 @@ func (fn *formulaFuncs) CEILINGMATH(argsList *list.List) (result string, err err return } number, significance, mode := 0.0, 1.0, 1.0 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -977,7 +1086,7 @@ func (fn *formulaFuncs) CEILINGMATH(argsList *list.List) (result string, err err significance = -1 } if argsList.Len() > 1 { - if significance, err = strconv.ParseFloat(argsList.Front().Next().Value.(formulaArg).Value, 64); err != nil { + if significance, err = strconv.ParseFloat(argsList.Front().Next().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -987,7 +1096,7 @@ func (fn *formulaFuncs) CEILINGMATH(argsList *list.List) (result string, err err return } if argsList.Len() > 2 { - if mode, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + if mode, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -1020,7 +1129,7 @@ func (fn *formulaFuncs) CEILINGPRECISE(argsList *list.List) (result string, err return } number, significance := 0.0, 1.0 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -1032,7 +1141,7 @@ func (fn *formulaFuncs) CEILINGPRECISE(argsList *list.List) (result string, err return } if argsList.Len() > 1 { - if significance, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + if significance, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -1063,11 +1172,11 @@ func (fn *formulaFuncs) COMBIN(argsList *list.List) (result string, err error) { return } number, chosen, val := 0.0, 0.0, 1.0 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } - if chosen, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + if chosen, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -1098,11 +1207,11 @@ func (fn *formulaFuncs) COMBINA(argsList *list.List) (result string, err error) return } var number, chosen float64 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } - if chosen, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + if chosen, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -1117,10 +1226,12 @@ func (fn *formulaFuncs) COMBINA(argsList *list.List) (result string, err error) } args := list.New() args.PushBack(formulaArg{ - Value: fmt.Sprintf("%g", number+chosen-1), + String: fmt.Sprintf("%g", number+chosen-1), + Type: ArgString, }) args.PushBack(formulaArg{ - Value: fmt.Sprintf("%g", number-1), + String: fmt.Sprintf("%g", number-1), + Type: ArgString, }) return fn.COMBIN(args) } @@ -1136,7 +1247,7 @@ func (fn *formulaFuncs) COS(argsList *list.List) (result string, err error) { return } var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -1155,7 +1266,7 @@ func (fn *formulaFuncs) COSH(argsList *list.List) (result string, err error) { return } var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -1174,7 +1285,7 @@ func (fn *formulaFuncs) COT(argsList *list.List) (result string, err error) { return } var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -1197,7 +1308,7 @@ func (fn *formulaFuncs) COTH(argsList *list.List) (result string, err error) { return } var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -1220,7 +1331,7 @@ func (fn *formulaFuncs) CSC(argsList *list.List) (result string, err error) { return } var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -1243,7 +1354,7 @@ func (fn *formulaFuncs) CSCH(argsList *list.List) (result string, err error) { return } var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -1265,9 +1376,9 @@ func (fn *formulaFuncs) DECIMAL(argsList *list.List) (result string, err error) err = errors.New("DECIMAL requires 2 numeric arguments") return } - var text = argsList.Front().Value.(formulaArg).Value + var text = argsList.Front().Value.(formulaArg).String var radix int - if radix, err = strconv.Atoi(argsList.Back().Value.(formulaArg).Value); err != nil { + if radix, err = strconv.Atoi(argsList.Back().Value.(formulaArg).String); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -1294,7 +1405,7 @@ func (fn *formulaFuncs) DEGREES(argsList *list.List) (result string, err error) return } var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -1318,7 +1429,7 @@ func (fn *formulaFuncs) EVEN(argsList *list.List) (result string, err error) { return } var number float64 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -1347,7 +1458,7 @@ func (fn *formulaFuncs) EXP(argsList *list.List) (result string, err error) { return } var number float64 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -1375,7 +1486,7 @@ func (fn *formulaFuncs) FACT(argsList *list.List) (result string, err error) { return } var number float64 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -1397,7 +1508,7 @@ func (fn *formulaFuncs) FACTDOUBLE(argsList *list.List) (result string, err erro return } number, val := 0.0, 1.0 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -1423,11 +1534,11 @@ func (fn *formulaFuncs) FLOOR(argsList *list.List) (result string, err error) { return } var number, significance float64 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } - if significance, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + if significance, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -1461,7 +1572,7 @@ func (fn *formulaFuncs) FLOORMATH(argsList *list.List) (result string, err error return } number, significance, mode := 0.0, 1.0, 1.0 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -1469,7 +1580,7 @@ func (fn *formulaFuncs) FLOORMATH(argsList *list.List) (result string, err error significance = -1 } if argsList.Len() > 1 { - if significance, err = strconv.ParseFloat(argsList.Front().Next().Value.(formulaArg).Value, 64); err != nil { + if significance, err = strconv.ParseFloat(argsList.Front().Next().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -1479,7 +1590,7 @@ func (fn *formulaFuncs) FLOORMATH(argsList *list.List) (result string, err error return } if argsList.Len() > 2 { - if mode, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + if mode, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -1507,7 +1618,7 @@ func (fn *formulaFuncs) FLOORPRECISE(argsList *list.List) (result string, err er return } var number, significance float64 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -1519,7 +1630,7 @@ func (fn *formulaFuncs) FLOORPRECISE(argsList *list.List) (result string, err er return } if argsList.Len() > 1 { - if significance, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + if significance, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -1573,7 +1684,7 @@ func (fn *formulaFuncs) GCD(argsList *list.List) (result string, err error) { nums = []float64{} ) for arg := argsList.Front(); arg != nil; arg = arg.Next() { - token := arg.Value.(formulaArg).Value + token := arg.Value.(formulaArg).String if token == "" { continue } @@ -1614,7 +1725,7 @@ func (fn *formulaFuncs) INT(argsList *list.List) (result string, err error) { return } var number float64 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -1642,7 +1753,7 @@ func (fn *formulaFuncs) ISOCEILING(argsList *list.List) (result string, err erro return } var number, significance float64 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -1654,7 +1765,7 @@ func (fn *formulaFuncs) ISOCEILING(argsList *list.List) (result string, err erro return } if argsList.Len() > 1 { - if significance, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + if significance, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -1699,7 +1810,7 @@ func (fn *formulaFuncs) LCM(argsList *list.List) (result string, err error) { nums = []float64{} ) for arg := argsList.Front(); arg != nil; arg = arg.Next() { - token := arg.Value.(formulaArg).Value + token := arg.Value.(formulaArg).String if token == "" { continue } @@ -1740,7 +1851,7 @@ func (fn *formulaFuncs) LN(argsList *list.List) (result string, err error) { return } var number float64 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -1763,12 +1874,12 @@ func (fn *formulaFuncs) LOG(argsList *list.List) (result string, err error) { return } number, base := 0.0, 10.0 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } if argsList.Len() > 1 { - if base, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + if base, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -1800,7 +1911,7 @@ func (fn *formulaFuncs) LOG10(argsList *list.List) (result string, err error) { return } var number float64 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -1863,7 +1974,7 @@ func (fn *formulaFuncs) MDETERM(argsList *list.List) (result string, err error) } numRow := []float64{} for _, ele := range row { - if num, err = strconv.ParseFloat(ele, 64); err != nil { + if num, err = strconv.ParseFloat(ele.String, 64); err != nil { return } numRow = append(numRow, num) @@ -1885,11 +1996,11 @@ func (fn *formulaFuncs) MOD(argsList *list.List) (result string, err error) { return } var number, divisor float64 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } - if divisor, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + if divisor, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -1916,11 +2027,11 @@ func (fn *formulaFuncs) MROUND(argsList *list.List) (result string, err error) { return } var number, multiple float64 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } - if multiple, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + if multiple, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -1951,10 +2062,10 @@ func (fn *formulaFuncs) MULTINOMIAL(argsList *list.List) (result string, err err val, num, denom := 0.0, 0.0, 1.0 for arg := argsList.Front(); arg != nil; arg = arg.Next() { token := arg.Value.(formulaArg) - if token.Value == "" { + if token.String == "" { continue } - if val, err = strconv.ParseFloat(token.Value, 64); err != nil { + if val, err = strconv.ParseFloat(token.String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -1976,7 +2087,7 @@ func (fn *formulaFuncs) MUNIT(argsList *list.List) (result string, err error) { return } var dimension int - if dimension, err = strconv.Atoi(argsList.Front().Value.(formulaArg).Value); err != nil { + if dimension, err = strconv.Atoi(argsList.Front().Value.(formulaArg).String); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -2007,7 +2118,7 @@ func (fn *formulaFuncs) ODD(argsList *list.List) (result string, err error) { return } var number float64 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -2054,11 +2165,11 @@ func (fn *formulaFuncs) POWER(argsList *list.List) (result string, err error) { return } var x, y float64 - if x, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if x, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } - if y, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + if y, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -2083,14 +2194,32 @@ func (fn *formulaFuncs) PRODUCT(argsList *list.List) (result string, err error) val, product := 0.0, 1.0 for arg := argsList.Front(); arg != nil; arg = arg.Next() { token := arg.Value.(formulaArg) - if token.Value == "" { + switch token.Type { + case ArgUnknown: continue + case ArgString: + if token.String == "" { + continue + } + if val, err = strconv.ParseFloat(token.String, 64); err != nil { + err = errors.New(formulaErrorVALUE) + return + } + product = product * val + case ArgMatrix: + for _, row := range token.Matrix { + for _, value := range row { + if value.String == "" { + continue + } + if val, err = strconv.ParseFloat(value.String, 64); err != nil { + err = errors.New(formulaErrorVALUE) + return + } + product = product * val + } + } } - if val, err = strconv.ParseFloat(token.Value, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return - } - product = product * val } result = fmt.Sprintf("%g", product) return @@ -2107,11 +2236,11 @@ func (fn *formulaFuncs) QUOTIENT(argsList *list.List) (result string, err error) return } var x, y float64 - if x, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if x, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } - if y, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + if y, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -2133,7 +2262,7 @@ func (fn *formulaFuncs) RADIANS(argsList *list.List) (result string, err error) return } var angle float64 - if angle, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if angle, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -2166,11 +2295,11 @@ func (fn *formulaFuncs) RANDBETWEEN(argsList *list.List) (result string, err err return } var bottom, top int64 - if bottom, err = strconv.ParseInt(argsList.Front().Value.(formulaArg).Value, 10, 64); err != nil { + if bottom, err = strconv.ParseInt(argsList.Front().Value.(formulaArg).String, 10, 64); err != nil { err = errors.New(formulaErrorVALUE) return } - if top, err = strconv.ParseInt(argsList.Back().Value.(formulaArg).Value, 10, 64); err != nil { + if top, err = strconv.ParseInt(argsList.Back().Value.(formulaArg).String, 10, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -2213,12 +2342,12 @@ func (fn *formulaFuncs) ROMAN(argsList *list.List) (result string, err error) { } var number float64 var form int - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } if argsList.Len() > 1 { - if form, err = strconv.Atoi(argsList.Back().Value.(formulaArg).Value); err != nil { + if form, err = strconv.Atoi(argsList.Back().Value.(formulaArg).String); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -2298,11 +2427,11 @@ func (fn *formulaFuncs) ROUND(argsList *list.List) (result string, err error) { return } var number, digits float64 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } - if digits, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + if digits, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -2321,11 +2450,11 @@ func (fn *formulaFuncs) ROUNDDOWN(argsList *list.List) (result string, err error return } var number, digits float64 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } - if digits, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + if digits, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -2344,11 +2473,11 @@ func (fn *formulaFuncs) ROUNDUP(argsList *list.List) (result string, err error) return } var number, digits float64 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } - if digits, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + if digits, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -2367,7 +2496,7 @@ func (fn *formulaFuncs) SEC(argsList *list.List) (result string, err error) { return } var number float64 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -2386,7 +2515,7 @@ func (fn *formulaFuncs) SECH(argsList *list.List) (result string, err error) { return } var number float64 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -2407,7 +2536,7 @@ func (fn *formulaFuncs) SIGN(argsList *list.List) (result string, err error) { return } var val float64 - if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if val, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -2434,7 +2563,7 @@ func (fn *formulaFuncs) SIN(argsList *list.List) (result string, err error) { return } var number float64 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -2453,7 +2582,7 @@ func (fn *formulaFuncs) SINH(argsList *list.List) (result string, err error) { return } var number float64 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -2472,7 +2601,7 @@ func (fn *formulaFuncs) SQRT(argsList *list.List) (result string, err error) { return } var res float64 - var value = argsList.Front().Value.(formulaArg).Value + var value = argsList.Front().Value.(formulaArg).String if value == "" { result = "0" return @@ -2500,7 +2629,7 @@ func (fn *formulaFuncs) SQRTPI(argsList *list.List) (result string, err error) { return } var number float64 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -2517,14 +2646,79 @@ func (fn *formulaFuncs) SUM(argsList *list.List) (result string, err error) { var val, sum float64 for arg := argsList.Front(); arg != nil; arg = arg.Next() { token := arg.Value.(formulaArg) - if token.Value == "" { + switch token.Type { + case ArgUnknown: continue + case ArgString: + if token.String == "" { + continue + } + if val, err = strconv.ParseFloat(token.String, 64); err != nil { + err = errors.New(formulaErrorVALUE) + return + } + sum += val + case ArgMatrix: + for _, row := range token.Matrix { + for _, value := range row { + if value.String == "" { + continue + } + if val, err = strconv.ParseFloat(value.String, 64); err != nil { + err = errors.New(formulaErrorVALUE) + return + } + sum += val + } + } } - if val, err = strconv.ParseFloat(token.Value, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + } + result = fmt.Sprintf("%g", sum) + return +} + +// SUMIF function finds the values in a supplied array, that satisfy a given +// 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]) +// +func (fn *formulaFuncs) SUMIF(argsList *list.List) (result string, err error) { + if argsList.Len() < 2 { + err = errors.New("SUMIF requires at least 2 argument") + return + } + var criteria = formulaCriteriaParser(argsList.Front().Next().Value.(formulaArg).String) + var rangeMtx = argsList.Front().Value.(formulaArg).Matrix + var sumRange [][]formulaArg + if argsList.Len() == 3 { + sumRange = argsList.Back().Value.(formulaArg).Matrix + } + var sum, val float64 + for rowIdx, row := range rangeMtx { + for colIdx, col := range row { + var ok bool + fromVal := col.String + if col.String == "" { + continue + } + if ok, err = formulaCriteriaEval(fromVal, criteria); err != nil { + return + } + if ok { + if argsList.Len() == 3 { + if len(sumRange) <= rowIdx || len(sumRange[rowIdx]) <= colIdx { + continue + } + fromVal = sumRange[rowIdx][colIdx].String + } + if val, err = strconv.ParseFloat(fromVal, 64); err != nil { + err = errors.New(formulaErrorVALUE) + return + } + sum += val + } } - sum += val } result = fmt.Sprintf("%g", sum) return @@ -2539,14 +2733,30 @@ func (fn *formulaFuncs) SUMSQ(argsList *list.List) (result string, err error) { var val, sq float64 for arg := argsList.Front(); arg != nil; arg = arg.Next() { token := arg.Value.(formulaArg) - if token.Value == "" { - continue - } - if val, err = strconv.ParseFloat(token.Value, 64); err != nil { - err = errors.New(formulaErrorVALUE) - return + switch token.Type { + case ArgString: + if token.String == "" { + continue + } + if val, err = strconv.ParseFloat(token.String, 64); err != nil { + err = errors.New(formulaErrorVALUE) + return + } + sq += val * val + case ArgMatrix: + for _, row := range token.Matrix { + for _, value := range row { + if value.String == "" { + continue + } + if val, err = strconv.ParseFloat(value.String, 64); err != nil { + err = errors.New(formulaErrorVALUE) + return + } + sq += val * val + } + } } - sq += val * val } result = fmt.Sprintf("%g", sq) return @@ -2563,7 +2773,7 @@ func (fn *formulaFuncs) TAN(argsList *list.List) (result string, err error) { return } var number float64 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -2582,7 +2792,7 @@ func (fn *formulaFuncs) TANH(argsList *list.List) (result string, err error) { return } var number float64 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -2601,12 +2811,12 @@ func (fn *formulaFuncs) TRUNC(argsList *list.List) (result string, err error) { return } var number, digits, adjust, rtrim float64 - if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).Value, 64); err != nil { + if number, err = strconv.ParseFloat(argsList.Front().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } if argsList.Len() > 1 { - if digits, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).Value, 64); err != nil { + if digits, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil { err = errors.New(formulaErrorVALUE) return } @@ -2628,3 +2838,5 @@ func (fn *formulaFuncs) TRUNC(argsList *list.List) (result string, err error) { } // Statistical functions + +// Information functions diff --git a/calc_test.go b/calc_test.go index 7592078c74..3639af71a7 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1,20 +1,32 @@ package excelize import ( + "path/filepath" "testing" "github.com/stretchr/testify/assert" ) func TestCalcCellValue(t *testing.T) { + cellData := [][]interface{}{ + {1, 4, nil, "Month", "Team", "Sales"}, + {2, 5, nil, "Jan", "North 1", 36693}, + {3, nil, nil, "Jan", "North 2", 22100}, + {0, nil, nil, "Jan", "South 1", 53321}, + {nil, nil, nil, "Jan", "South 2", 34440}, + {nil, nil, nil, "Feb", "North 1", 29889}, + {nil, nil, nil, "Feb", "North 2", 50090}, + {nil, nil, nil, "Feb", "South 1", 32080}, + {nil, nil, nil, "Feb", "South 2", 45500}, + } prepareData := func() *File { f := NewFile() - f.SetCellValue("Sheet1", "A1", 1) - f.SetCellValue("Sheet1", "A2", 2) - f.SetCellValue("Sheet1", "A3", 3) - f.SetCellValue("Sheet1", "A4", 0) - f.SetCellValue("Sheet1", "B1", 4) - f.SetCellValue("Sheet1", "B2", 5) + for r, row := range cellData { + for c, value := range row { + cell, _ := CoordinatesToCellName(c+1, r+1) + assert.NoError(t, f.SetCellValue("Sheet1", cell, value)) + } + } return f } @@ -348,6 +360,12 @@ func TestCalcCellValue(t *testing.T) { "=((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", + // SUMIF + `=SUMIF(F1:F5, ">100")`: "146554", + `=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", // SUMSQ "=SUMSQ(A1:A4)": "14", "=SUMSQ(A1,B1,A2,B2,6)": "82", @@ -627,6 +645,8 @@ func TestCalcCellValue(t *testing.T) { "=SUM(1*)": "formula not valid", "=SUM(1/)": "formula not valid", `=SUM("X")`: "#VALUE!", + // SUMIF + "=SUMIF()": "SUMIF requires at least 2 argument", // SUMSQ `=SUMSQ("X")`: "#VALUE!", // TAN @@ -711,4 +731,5 @@ func TestCalcCellValue(t *testing.T) { assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "=UNSUPPORT(A1)")) _, err = f.CalcCellValue("Sheet1", "A1") assert.EqualError(t, err, "not support UNSUPPORT function") + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestCalcCellValue.xlsx"))) } From 22df34c4933bb28f6827b011cb6d9d3fd9f0e8d2 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 31 May 2020 15:13:52 +0800 Subject: [PATCH 248/957] fn: ISBLANK, ISERR, ISERROR, ISEVEN, ISNA, ISNONTEXT, ISODD, NA --- calc.go | 178 +++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 43 +++++++++++++ 2 files changed, 221 insertions(+) diff --git a/calc.go b/calc.go index 6bb15def1e..6f1f7f3f89 100644 --- a/calc.go +++ b/calc.go @@ -2840,3 +2840,181 @@ func (fn *formulaFuncs) TRUNC(argsList *list.List) (result string, err error) { // Statistical functions // Information functions + +// ISBLANK function tests if a specified cell is blank (empty) and if so, +// returns TRUE; Otherwise the function returns FALSE. The syntax of the +// function is: +// +// ISBLANK(value) +// +func (fn *formulaFuncs) ISBLANK(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("ISBLANK requires 1 argument") + return + } + token := argsList.Front().Value.(formulaArg) + result = "FALSE" + switch token.Type { + case ArgUnknown: + result = "TRUE" + case ArgString: + if token.String == "" { + result = "TRUE" + } + } + return +} + +// ISERR function tests if an initial supplied expression (or value) returns +// any Excel Error, except the #N/A error. If so, the function returns the +// 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) +// +func (fn *formulaFuncs) ISERR(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("ISERR requires 1 argument") + return + } + token := argsList.Front().Value.(formulaArg) + result = "FALSE" + if token.Type == ArgString { + for _, errType := range []string{formulaErrorDIV, formulaErrorNAME, formulaErrorNUM, formulaErrorVALUE, formulaErrorREF, formulaErrorNULL, formulaErrorSPILL, formulaErrorCALC, formulaErrorGETTINGDATA} { + if errType == token.String { + result = "TRUE" + } + } + } + return +} + +// ISERROR function tests if an initial supplied expression (or value) returns +// an Excel Error, and if so, returns the logical value TRUE; Otherwise the +// function returns FALSE. The syntax of the function is: +// +// ISERROR(value) +// +func (fn *formulaFuncs) ISERROR(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("ISERROR requires 1 argument") + return + } + token := argsList.Front().Value.(formulaArg) + result = "FALSE" + if token.Type == ArgString { + for _, errType := range []string{formulaErrorDIV, formulaErrorNAME, formulaErrorNA, formulaErrorNUM, formulaErrorVALUE, formulaErrorREF, formulaErrorNULL, formulaErrorSPILL, formulaErrorCALC, formulaErrorGETTINGDATA} { + if errType == token.String { + result = "TRUE" + } + } + } + return +} + +// ISEVEN function tests if a supplied number (or numeric expression) +// evaluates to an even number, and if so, returns TRUE; Otherwise, the +// function returns FALSE. The syntax of the function is: +// +// ISEVEN(value) +// +func (fn *formulaFuncs) ISEVEN(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("ISEVEN requires 1 argument") + return + } + token := argsList.Front().Value.(formulaArg) + result = "FALSE" + var numeric int + if token.Type == ArgString { + if numeric, err = strconv.Atoi(token.String); err != nil { + err = errors.New(formulaErrorVALUE) + return + } + if numeric == numeric/2*2 { + result = "TRUE" + return + } + } + return +} + +// ISNA function tests if an initial supplied expression (or value) returns +// the Excel #N/A Error, and if so, returns TRUE; Otherwise the function +// returns FALSE. The syntax of the function is: +// +// ISNA(value) +// +func (fn *formulaFuncs) ISNA(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("ISNA requires 1 argument") + return + } + token := argsList.Front().Value.(formulaArg) + result = "FALSE" + if token.Type == ArgString && token.String == formulaErrorNA { + result = "TRUE" + } + return +} + +// ISNONTEXT function 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: +// +// ISNONTEXT(value) +// +func (fn *formulaFuncs) ISNONTEXT(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("ISNONTEXT requires 1 argument") + return + } + token := argsList.Front().Value.(formulaArg) + result = "TRUE" + if token.Type == ArgString && token.String != "" { + result = "FALSE" + } + return +} + +// ISODD function tests if a supplied number (or numeric expression) evaluates +// to an odd number, and if so, returns TRUE; Otherwise, the function returns +// FALSE. The syntax of the function is: +// +// ISODD(value) +// +func (fn *formulaFuncs) ISODD(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("ISODD requires 1 argument") + return + } + token := argsList.Front().Value.(formulaArg) + result = "FALSE" + var numeric int + if token.Type == ArgString { + if numeric, err = strconv.Atoi(token.String); err != nil { + err = errors.New(formulaErrorVALUE) + return + } + if numeric != numeric/2*2 { + result = "TRUE" + return + } + } + return +} + +// NA function returns the Excel #N/A error. This error message has the +// 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() +// +func (fn *formulaFuncs) NA(argsList *list.List) (result string, err error) { + if argsList.Len() != 0 { + err = errors.New("NA accepts no arguments") + return + } + result = formulaErrorNA + return +} diff --git a/calc_test.go b/calc_test.go index 3639af71a7..4f2ca7bd59 100644 --- a/calc_test.go +++ b/calc_test.go @@ -384,6 +384,32 @@ func TestCalcCellValue(t *testing.T) { "=TRUNC(99.999,-1)": "90", "=TRUNC(-99.999,2)": "-99.99", "=TRUNC(-99.999,-1)": "-90", + // Information functions + // ISBLANK + "=ISBLANK(A1)": "FALSE", + "=ISBLANK(A5)": "TRUE", + // ISERR + "=ISERR(A1)": "FALSE", + "=ISERR(NA())": "FALSE", + // ISERROR + "=ISERROR(A1)": "FALSE", + "=ISERROR(NA())": "TRUE", + // ISEVEN + "=ISEVEN(A1)": "FALSE", + "=ISEVEN(A2)": "TRUE", + // ISNA + "=ISNA(A1)": "FALSE", + "=ISNA(NA())": "TRUE", + // ISNONTEXT + "=ISNONTEXT(A1)": "FALSE", + "=ISNONTEXT(A5)": "TRUE", + `=ISNONTEXT("Excelize")`: "FALSE", + "=ISNONTEXT(NA())": "FALSE", + // ISODD + "=ISODD(A1)": "TRUE", + "=ISODD(A2)": "FALSE", + // NA + "=NA()": "#N/A", } for formula, expected := range mathCalc { f := prepareData() @@ -659,6 +685,23 @@ func TestCalcCellValue(t *testing.T) { "=TRUNC()": "TRUNC requires at least 1 argument", `=TRUNC("X")`: "#VALUE!", `=TRUNC(1,"X")`: "#VALUE!", + // Information functions + // ISBLANK + "=ISBLANK(A1,A2)": "ISBLANK requires 1 argument", + // ISERR + "=ISERR()": "ISERR requires 1 argument", + // ISERROR + "=ISERROR()": "ISERROR requires 1 argument", + // ISEVEN + "=ISEVEN()": "ISEVEN requires 1 argument", + // ISNA + "=ISNA()": "ISNA requires 1 argument", + // ISNONTEXT + "=ISNONTEXT()": "ISNONTEXT requires 1 argument", + // ISODD + "=ISODD()": "ISODD requires 1 argument", + // NA + "=NA(1)": "NA accepts no arguments", } for formula, expected := range mathCalcError { f := prepareData() From b62950a39ef2063ec19c221a32d37bd01f197472 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 2 Jun 2020 23:39:41 +0800 Subject: [PATCH 249/957] fn: MEDIAN, ISNUMBER --- calc.go | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++++ calc_test.go | 12 +++++++++ 2 files changed, 81 insertions(+) diff --git a/calc.go b/calc.go index 6f1f7f3f89..13baeefcb2 100644 --- a/calc.go +++ b/calc.go @@ -20,6 +20,7 @@ import ( "math/rand" "reflect" "regexp" + "sort" "strconv" "strings" "time" @@ -2839,6 +2840,52 @@ func (fn *formulaFuncs) TRUNC(argsList *list.List) (result string, err error) { // Statistical functions +// MEDIAN function returns the statistical median (the middle value) of a list +// of supplied numbers. The syntax of the function is: +// +// MEDIAN(number1,[number2],...) +// +func (fn *formulaFuncs) MEDIAN(argsList *list.List) (result string, err error) { + if argsList.Len() == 0 { + err = errors.New("MEDIAN requires at least 1 argument") + return + } + values := []float64{} + var median, digits float64 + for token := argsList.Front(); token != nil; token = token.Next() { + arg := token.Value.(formulaArg) + switch arg.Type { + case ArgString: + if digits, err = strconv.ParseFloat(argsList.Back().Value.(formulaArg).String, 64); err != nil { + err = errors.New(formulaErrorVALUE) + return + } + values = append(values, digits) + 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 { + err = errors.New(formulaErrorVALUE) + return + } + values = append(values, digits) + } + } + } + } + sort.Float64s(values) + if len(values)%2 == 0 { + median = (values[len(values)/2-1] + values[len(values)/2]) / 2 + } else { + median = values[len(values)/2] + } + result = fmt.Sprintf("%g", median) + return +} + // Information functions // ISBLANK function tests if a specified cell is blank (empty) and if so, @@ -2977,6 +3024,28 @@ func (fn *formulaFuncs) ISNONTEXT(argsList *list.List) (result string, err error return } +// ISNUMBER function 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: +// +// ISNUMBER(value) +// +func (fn *formulaFuncs) ISNUMBER(argsList *list.List) (result string, err error) { + if argsList.Len() != 1 { + err = errors.New("ISNUMBER requires 1 argument") + return + } + token := argsList.Front().Value.(formulaArg) + result = "FALSE" + if token.Type == ArgString && token.String != "" { + if _, err = strconv.Atoi(token.String); err == nil { + result = "TRUE" + } + err = nil + } + return +} + // ISODD function tests if a supplied number (or numeric expression) evaluates // to an odd number, and if so, returns TRUE; Otherwise, the function returns // FALSE. The syntax of the function is: diff --git a/calc_test.go b/calc_test.go index 4f2ca7bd59..213f77a89f 100644 --- a/calc_test.go +++ b/calc_test.go @@ -384,6 +384,10 @@ func TestCalcCellValue(t *testing.T) { "=TRUNC(99.999,-1)": "90", "=TRUNC(-99.999,2)": "-99.99", "=TRUNC(-99.999,-1)": "-90", + // Statistical functions + // MEDIAN + "=MEDIAN(A1:A5,12)": "2", + "=MEDIAN(A1:A5)": "1.5", // Information functions // ISBLANK "=ISBLANK(A1)": "FALSE", @@ -405,6 +409,9 @@ func TestCalcCellValue(t *testing.T) { "=ISNONTEXT(A5)": "TRUE", `=ISNONTEXT("Excelize")`: "FALSE", "=ISNONTEXT(NA())": "FALSE", + // ISNUMBER + "=ISNUMBER(A1)": "TRUE", + "=ISNUMBER(D1)": "FALSE", // ISODD "=ISODD(A1)": "TRUE", "=ISODD(A2)": "FALSE", @@ -685,6 +692,9 @@ func TestCalcCellValue(t *testing.T) { "=TRUNC()": "TRUNC requires at least 1 argument", `=TRUNC("X")`: "#VALUE!", `=TRUNC(1,"X")`: "#VALUE!", + // Statistical functions + // MEDIAN + "=MEDIAN()": "MEDIAN requires at least 1 argument", // Information functions // ISBLANK "=ISBLANK(A1,A2)": "ISBLANK requires 1 argument", @@ -698,6 +708,8 @@ func TestCalcCellValue(t *testing.T) { "=ISNA()": "ISNA requires 1 argument", // ISNONTEXT "=ISNONTEXT()": "ISNONTEXT requires 1 argument", + // ISNUMBER + "=ISNUMBER()": "ISNUMBER requires 1 argument", // ISODD "=ISODD()": "ISODD requires 1 argument", // NA From b6dd7648a142901655cc4f76cb7d3a6e73338c8f Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 4 Jun 2020 00:35:54 +0800 Subject: [PATCH 250/957] fn: COUNTA --- calc.go | 34 +++++++++++++++++++++++++++++++--- calc_test.go | 3 +++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/calc.go b/calc.go index 13baeefcb2..86f7cffe6e 100644 --- a/calc.go +++ b/calc.go @@ -313,7 +313,7 @@ func calcAdd(opdStack *Stack) error { return nil } -// calcAdd evaluate subtraction arithmetic operations. +// calcSubtract evaluate subtraction arithmetic operations. func calcSubtract(opdStack *Stack) error { if opdStack.Len() < 2 { return errors.New("formula not valid") @@ -333,7 +333,7 @@ func calcSubtract(opdStack *Stack) error { return nil } -// calcAdd evaluate multiplication arithmetic operations. +// calcMultiply evaluate multiplication arithmetic operations. func calcMultiply(opdStack *Stack) error { if opdStack.Len() < 2 { return errors.New("formula not valid") @@ -353,7 +353,7 @@ func calcMultiply(opdStack *Stack) error { return nil } -// calcAdd evaluate division arithmetic operations. +// calcDivide evaluate division arithmetic operations. func calcDivide(opdStack *Stack) error { if opdStack.Len() < 2 { return errors.New("formula not valid") @@ -2840,6 +2840,34 @@ func (fn *formulaFuncs) TRUNC(argsList *list.List) (result string, err error) { // Statistical functions +// 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],...) +// +func (fn *formulaFuncs) COUNTA(argsList *list.List) (result string, err error) { + var count int + for token := argsList.Front(); token != nil; token = token.Next() { + arg := token.Value.(formulaArg) + switch arg.Type { + case ArgString: + if arg.String != "" { + count++ + } + case ArgMatrix: + for _, row := range arg.Matrix { + for _, value := range row { + if value.String != "" { + count++ + } + } + } + } + } + result = fmt.Sprintf("%d", count) + return +} + // MEDIAN function returns the statistical median (the middle value) of a list // of supplied numbers. The syntax of the function is: // diff --git a/calc_test.go b/calc_test.go index 213f77a89f..283b9c28ea 100644 --- a/calc_test.go +++ b/calc_test.go @@ -385,6 +385,9 @@ func TestCalcCellValue(t *testing.T) { "=TRUNC(-99.999,2)": "-99.99", "=TRUNC(-99.999,-1)": "-90", // Statistical functions + // COUNTA + `=COUNTA()`: "0", + `=COUNTA(A1:A5,B2:B5,"text",1,2)`: "8", // MEDIAN "=MEDIAN(A1:A5,12)": "2", "=MEDIAN(A1:A5)": "1.5", From eb150c0c22749a11618a3e77ffc9ad0d58f11056 Mon Sep 17 00:00:00 2001 From: heiy <287789299@qq.com> Date: Mon, 8 Jun 2020 18:23:38 +0800 Subject: [PATCH 251/957] escape html tag --- cell.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cell.go b/cell.go index 064c432e6f..0912dc4a79 100644 --- a/cell.go +++ b/cell.go @@ -13,6 +13,7 @@ import ( "encoding/xml" "errors" "fmt" + "html" "reflect" "strconv" "strings" @@ -620,7 +621,7 @@ func (f *File) SetCellRichText(sheet, cell string, runs []RichTextRun) error { sst := f.sharedStringsReader() textRuns := []xlsxR{} for _, textRun := range runs { - run := xlsxR{T: &xlsxT{Val: textRun.Text}} + run := xlsxR{T: &xlsxT{Val: html.EscapeString(textRun.Text)}} if strings.ContainsAny(textRun.Text, "\r\n ") { run.T.Space = xml.Attr{Name: xml.Name{Space: NameSpaceXML, Local: "space"}, Value: "preserve"} } From e9a4007c17f0db01b52bb40ab744c25e0f9e9673 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Pogeant?= Date: Tue, 16 Jun 2020 11:53:22 +0200 Subject: [PATCH 252/957] Implement columns iterator --- col.go | 167 ++++++++++++++++++++++++++++++++++++++++++++++++++++ col_test.go | 124 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 291 insertions(+) diff --git a/col.go b/col.go index 6f768003fe..561cec9b16 100644 --- a/col.go +++ b/col.go @@ -10,7 +10,10 @@ package excelize import ( + "bytes" + "encoding/xml" "errors" + "fmt" "math" "strings" @@ -24,6 +27,170 @@ const ( EMU int = 9525 ) +// Cols defines an iterator to a sheet +type Cols struct { + err error + curCol, totalCol, stashCol, totalRow int + sheet string + cols []xlsxCols + f *File + decoder *xml.Decoder +} + +// GetCols return all the columns in a sheet by given worksheet name (case sensitive). 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() +// } +// +func (f *File) GetCols(sheet string) ([][]string, error) { + cols, err := f.Cols(sheet) + if err != nil { + return nil, err + } + + results := make([][]string, 0, 64) + + for cols.Next() { + if cols.Error() != nil { + break + } + + col, err := cols.Rows() + if err != nil { + break + } + + results = append(results, col) + } + + return results, nil +} + +// Next will return true if the next col element is found. +func (cols *Cols) Next() bool { + cols.curCol++ + + return cols.curCol <= cols.totalCol +} + +// Error will return an error when the next col element is found. +func (cols *Cols) Error() error { + return cols.err +} + +// Rows return the current column's row values +func (cols *Cols) Rows() ([]string, error) { + var ( + err error + rows []string + ) + + if cols.stashCol >= cols.curCol { + return rows, err + } + + for i := 1; i <= cols.totalRow; i++ { + colName, _ := ColumnNumberToName(cols.curCol) + val, _ := cols.f.GetCellValue(cols.sheet, fmt.Sprintf("%s%d", colName, i)) + rows = append(rows, val) + } + + return rows, nil +} + +// 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() +// } +// +func (f *File) Cols(sheet string) (*Cols, error) { + name, ok := f.sheetMap[trimSheetName(sheet)] + if !ok { + return nil, ErrSheetNotExist{sheet} + } + + if f.Sheet[name] != nil { + output, _ := xml.Marshal(f.Sheet[name]) + f.saveFileList(name, replaceRelationshipsNameSpaceBytes(output)) + } + + var ( + inElement string + cols Cols + colsNum, rowsNum []int + ) + decoder := f.xmlNewDecoder(bytes.NewReader(f.readXML(name))) + + for { + token, _ := decoder.Token() + if token == nil { + break + } + + switch startElement := token.(type) { + case xml.StartElement: + inElement = startElement.Name.Local + if inElement == "dimension" { + colsNum = make([]int, 0) + rowsNum = make([]int, 0) + + for _, attr := range startElement.Attr { + if attr.Name.Local == "ref" { + sheetCoordinates := attr.Value + if i := strings.Index(sheetCoordinates, ":"); i <= -1 { + return &cols, errors.New("Sheet coordinates are wrong") + } + + coordinates := strings.Split(sheetCoordinates, ":") + for _, coordinate := range coordinates { + c, r, _ := SplitCellName(coordinate) + columnNum, _ := ColumnNameToNumber(c) + colsNum = append(colsNum, columnNum) + rowsNum = append(rowsNum, r) + } + } + } + + cols.totalCol = colsNum[1] - (colsNum[0] - 1) + cols.totalRow = rowsNum[1] - (rowsNum[0] - 1) + } + default: + } + } + + cols.f = f + cols.sheet = trimSheetName(sheet) + cols.decoder = f.xmlNewDecoder(bytes.NewReader(f.readXML(name))) + + return &cols, nil +} + // 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: diff --git a/col_test.go b/col_test.go index fcb1619ec9..a130f2bebd 100644 --- a/col_test.go +++ b/col_test.go @@ -1,12 +1,136 @@ package excelize import ( + "bytes" "path/filepath" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) +func TestCols(t *testing.T) { + const sheet2 = "Sheet2" + + f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + if !assert.NoError(t, err) { + t.FailNow() + } + + cols, err := f.Cols(sheet2) + if !assert.NoError(t, err) { + t.FailNow() + } + + var collectedRows [][]string + for cols.Next() { + rows, err := cols.Rows() + assert.NoError(t, err) + collectedRows = append(collectedRows, trimSliceSpace(rows)) + } + if !assert.NoError(t, cols.Error()) { + t.FailNow() + } + + returnedColumns, err := f.GetCols(sheet2) + assert.NoError(t, err) + for i := range returnedColumns { + returnedColumns[i] = trimSliceSpace(returnedColumns[i]) + } + if !assert.Equal(t, collectedRows, returnedColumns) { + t.FailNow() + } + + f = NewFile() + cells := []string{"C2", "C3", "C4"} + for _, cell := range cells { + assert.NoError(t, f.SetCellValue("Sheet1", cell, 1)) + } + _, err = f.Rows("Sheet1") + assert.NoError(t, err) + + f.Sheet["xl/worksheets/sheet1.xml"] = &xlsxWorksheet{ + Dimension: &xlsxDimension{ + Ref: "C2:C4", + }, + } + _, err = f.Rows("Sheet1") + assert.NoError(t, err) +} + +func TestColumnsIterator(t *testing.T) { + const ( + sheet2 = "Sheet2" + expectedNumCol = 4 + ) + + f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + require.NoError(t, err) + + cols, err := f.Cols(sheet2) + require.NoError(t, err) + + var colCount int + for cols.Next() { + colCount++ + require.True(t, colCount <= expectedNumCol, "colCount is greater than expected") + } + assert.Equal(t, expectedNumCol, colCount) + + f = NewFile() + cells := []string{"C2", "C3", "C4", "D2", "D3", "D4"} + for _, cell := range cells { + assert.NoError(t, f.SetCellValue("Sheet1", cell, 1)) + } + f.Sheet["xl/worksheets/sheet1.xml"] = &xlsxWorksheet{ + Dimension: &xlsxDimension{ + Ref: "C2:D4", + }, + } + cols, err = f.Cols("Sheet1") + require.NoError(t, err) + + colCount = 0 + for cols.Next() { + colCount++ + require.True(t, colCount <= 2, "colCount is greater than expected") + } + assert.Equal(t, 2, colCount) +} + +func TestColsError(t *testing.T) { + xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + if !assert.NoError(t, err) { + t.FailNow() + } + _, err = xlsx.Cols("SheetN") + assert.EqualError(t, err, "sheet SheetN is not exist") +} + +func TestColsRows(t *testing.T) { + f := NewFile() + f.NewSheet("Sheet1") + + cols, err := f.Cols("Sheet1") + assert.EqualError(t, err, `Sheet coordinates are wrong`) + + assert.NoError(t, f.SetCellValue("Sheet1", "A1", 1)) + f.Sheet["xl/worksheets/sheet1.xml"] = &xlsxWorksheet{ + Dimension: &xlsxDimension{ + Ref: "A1:A1", + }, + } + + cols.stashCol, cols.curCol = 0, 1 + cols, err = f.Cols("Sheet1") + assert.NoError(t, err) + + // Test if token is nil + cols.decoder = f.xmlNewDecoder(bytes.NewReader(nil)) + _, err = cols.Rows() + assert.NoError(t, err) +} + func TestColumnVisibility(t *testing.T) { t.Run("TestBook1", func(t *testing.T) { f, err := prepareTestBook1() From 5221729bc342c5b12883ebe03898a85f755233c9 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 22 Jun 2020 00:05:19 +0800 Subject: [PATCH 253/957] make columns iterator read cell streamingly and add max column limit on ColumnNumberToName --- cell.go | 3 -- col.go | 120 +++++++++++++++++++++++++++------------------------- col_test.go | 53 +++++++++++++++++------ lib.go | 3 ++ rows.go | 3 -- 5 files changed, 106 insertions(+), 76 deletions(-) diff --git a/cell.go b/cell.go index 0912dc4a79..fb0a83366f 100644 --- a/cell.go +++ b/cell.go @@ -41,9 +41,6 @@ var rwMutex sync.RWMutex func (f *File) GetCellValue(sheet, axis string) (string, error) { return f.getCellStringFunc(sheet, axis, func(x *xlsxWorksheet, c *xlsxC) (string, bool, error) { val, err := c.getValueFrom(f, f.sharedStringsReader()) - if err != nil { - return val, false, err - } return val, true, err }) } diff --git a/col.go b/col.go index 561cec9b16..6756f2fd87 100644 --- a/col.go +++ b/col.go @@ -13,8 +13,8 @@ import ( "bytes" "encoding/xml" "errors" - "fmt" "math" + "strconv" "strings" "github.com/mohae/deepcopy" @@ -34,10 +34,11 @@ type Cols struct { sheet string cols []xlsxCols f *File - decoder *xml.Decoder + 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.Cols("Sheet1") // if err != nil { @@ -60,29 +61,17 @@ func (f *File) GetCols(sheet string) ([][]string, error) { if err != nil { return nil, err } - results := make([][]string, 0, 64) - for cols.Next() { - if cols.Error() != nil { - break - } - - col, err := cols.Rows() - if err != nil { - break - } - + col, _ := cols.Rows() results = append(results, col) } - return results, nil } // Next will return true if the next col element is found. func (cols *Cols) Next() bool { cols.curCol++ - return cols.curCol <= cols.totalCol } @@ -91,27 +80,53 @@ func (cols *Cols) Error() error { return cols.err } -// Rows return the current column's row values +// Rows return the current column's row values. func (cols *Cols) Rows() ([]string, error) { var ( - err error - rows []string + err error + inElement string + cellCol, cellRow int + rows []string ) - if cols.stashCol >= cols.curCol { return rows, err } - - for i := 1; i <= cols.totalRow; i++ { - colName, _ := ColumnNumberToName(cols.curCol) - val, _ := cols.f.GetCellValue(cols.sheet, fmt.Sprintf("%s%d", colName, i)) - rows = append(rows, val) + d := cols.f.sharedStringsReader() + decoder := cols.f.xmlNewDecoder(bytes.NewReader(cols.sheetXML)) + for { + token, _ := decoder.Token() + if token == nil { + break + } + switch startElement := token.(type) { + case xml.StartElement: + inElement = startElement.Name.Local + if inElement == "c" { + for _, attr := range startElement.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, &startElement) + val, _ := colCell.getValueFrom(cols.f, d) + rows = append(rows, val) + } + } + } + } + } } - return rows, nil } -// Cols returns a columns iterator, used for streaming/reading data for a worksheet with a large data. For example: +// 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 { @@ -134,60 +149,51 @@ func (f *File) Cols(sheet string) (*Cols, error) { if !ok { return nil, ErrSheetNotExist{sheet} } - if f.Sheet[name] != nil { output, _ := xml.Marshal(f.Sheet[name]) f.saveFileList(name, replaceRelationshipsNameSpaceBytes(output)) } - var ( - inElement string - cols Cols - colsNum, rowsNum []int + inElement string + cols Cols + cellCol int + err error ) - decoder := f.xmlNewDecoder(bytes.NewReader(f.readXML(name))) - + cols.sheetXML = f.readXML(name) + decoder := f.xmlNewDecoder(bytes.NewReader(cols.sheetXML)) for { token, _ := decoder.Token() if token == nil { break } - switch startElement := token.(type) { case xml.StartElement: inElement = startElement.Name.Local - if inElement == "dimension" { - colsNum = make([]int, 0) - rowsNum = make([]int, 0) - + if inElement == "row" { for _, attr := range startElement.Attr { - if attr.Name.Local == "ref" { - sheetCoordinates := attr.Value - if i := strings.Index(sheetCoordinates, ":"); i <= -1 { - return &cols, errors.New("Sheet coordinates are wrong") + if attr.Name.Local == "r" { + if cols.totalRow, err = strconv.Atoi(attr.Value); err != nil { + return &cols, err } - - coordinates := strings.Split(sheetCoordinates, ":") - for _, coordinate := range coordinates { - c, r, _ := SplitCellName(coordinate) - columnNum, _ := ColumnNameToNumber(c) - colsNum = append(colsNum, columnNum) - rowsNum = append(rowsNum, r) + } + } + } + if inElement == "c" { + for _, attr := range startElement.Attr { + if attr.Name.Local == "r" { + if cellCol, _, err = CellNameToCoordinates(attr.Value); err != nil { + return &cols, err + } + if cellCol > cols.totalCol { + cols.totalCol = cellCol } } } - - cols.totalCol = colsNum[1] - (colsNum[0] - 1) - cols.totalRow = rowsNum[1] - (rowsNum[0] - 1) } - default: } } - cols.f = f cols.sheet = trimSheetName(sheet) - cols.decoder = f.xmlNewDecoder(bytes.NewReader(f.readXML(name))) - return &cols, nil } diff --git a/col_test.go b/col_test.go index a130f2bebd..fac78eb197 100644 --- a/col_test.go +++ b/col_test.go @@ -1,7 +1,6 @@ package excelize import ( - "bytes" "path/filepath" "testing" @@ -61,7 +60,7 @@ func TestCols(t *testing.T) { func TestColumnsIterator(t *testing.T) { const ( sheet2 = "Sheet2" - expectedNumCol = 4 + expectedNumCol = 9 ) f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) @@ -82,29 +81,57 @@ func TestColumnsIterator(t *testing.T) { for _, cell := range cells { assert.NoError(t, f.SetCellValue("Sheet1", cell, 1)) } - f.Sheet["xl/worksheets/sheet1.xml"] = &xlsxWorksheet{ - Dimension: &xlsxDimension{ - Ref: "C2:D4", - }, - } cols, err = f.Cols("Sheet1") require.NoError(t, err) colCount = 0 for cols.Next() { colCount++ - require.True(t, colCount <= 2, "colCount is greater than expected") + require.True(t, colCount <= 4, "colCount is greater than expected") } - assert.Equal(t, 2, colCount) + assert.Equal(t, 4, colCount) } func TestColsError(t *testing.T) { - xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + if !assert.NoError(t, err) { + t.FailNow() + } + _, err = f.Cols("SheetN") + assert.EqualError(t, err, "sheet SheetN is not exist") +} + +func TestGetColsError(t *testing.T) { + f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if !assert.NoError(t, err) { t.FailNow() } - _, err = xlsx.Cols("SheetN") + _, err = f.GetCols("SheetN") assert.EqualError(t, err, "sheet SheetN is not exist") + + f = NewFile() + delete(f.Sheet, "xl/worksheets/sheet1.xml") + f.XLSX["xl/worksheets/sheet1.xml"] = []byte(`B`) + f.checked = nil + _, err = f.GetCols("Sheet1") + assert.EqualError(t, err, `strconv.Atoi: parsing "A": invalid syntax`) + + f = NewFile() + delete(f.Sheet, "xl/worksheets/sheet1.xml") + f.XLSX["xl/worksheets/sheet1.xml"] = []byte(`B`) + f.checked = nil + _, err = f.GetCols("Sheet1") + assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`) + + f = NewFile() + cols, err := f.Cols("Sheet1") + assert.NoError(t, err) + cols.totalRow = 2 + cols.totalCol = 2 + cols.curCol = 1 + cols.decoder = []byte(`A`) + _, err = cols.Rows() + assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`) } func TestColsRows(t *testing.T) { @@ -112,7 +139,7 @@ func TestColsRows(t *testing.T) { f.NewSheet("Sheet1") cols, err := f.Cols("Sheet1") - assert.EqualError(t, err, `Sheet coordinates are wrong`) + assert.NoError(t, err) assert.NoError(t, f.SetCellValue("Sheet1", "A1", 1)) f.Sheet["xl/worksheets/sheet1.xml"] = &xlsxWorksheet{ @@ -126,7 +153,7 @@ func TestColsRows(t *testing.T) { assert.NoError(t, err) // Test if token is nil - cols.decoder = f.xmlNewDecoder(bytes.NewReader(nil)) + cols.decoder = nil _, err = cols.Rows() assert.NoError(t, err) } diff --git a/lib.go b/lib.go index d97bb20973..ae9287c075 100644 --- a/lib.go +++ b/lib.go @@ -152,6 +152,9 @@ func ColumnNumberToName(num int) (string, error) { if num < 1 { return "", fmt.Errorf("incorrect column number %d", num) } + if num > TotalColumns { + return "", fmt.Errorf("column number exceeds maximum limit") + } var col string for num > 0 { col = string((num-1)%26+65) + col diff --git a/rows.go b/rows.go index 352f1eb8df..f9770aab10 100644 --- a/rows.go +++ b/rows.go @@ -46,9 +46,6 @@ func (f *File) GetRows(sheet string) ([][]string, error) { } results := make([][]string, 0, 64) for rows.Next() { - if rows.Error() != nil { - break - } row, err := rows.Columns() if err != nil { break From 15fd56853fe1b63217fb963c951cf4fef7b56a08 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 22 Jun 2020 00:14:56 +0800 Subject: [PATCH 254/957] Update docs and typo fixed --- adjust.go | 8 +++++--- calcchain.go | 8 +++++--- cell.go | 8 +++++--- chart.go | 8 +++++--- col.go | 8 +++++--- col_test.go | 4 ++-- comment.go | 8 +++++--- datavalidation.go | 8 +++++--- date.go | 8 +++++--- docProps.go | 8 +++++--- drawing.go | 8 +++++--- errors.go | 8 +++++--- file.go | 8 +++++--- lib.go | 8 +++++--- merge.go | 8 +++++--- picture.go | 8 +++++--- pivotTable.go | 8 +++++--- rows.go | 8 +++++--- shape.go | 8 +++++--- sheet.go | 8 +++++--- sheetpr.go | 8 +++++--- sheetview.go | 8 +++++--- sparkline.go | 8 +++++--- stream.go | 8 +++++--- styles.go | 8 +++++--- table.go | 8 +++++--- templates.go | 8 +++++--- vmlDrawing.go | 8 +++++--- xmlApp.go | 8 +++++--- xmlCalcChain.go | 8 +++++--- xmlChart.go | 8 +++++--- xmlComments.go | 8 +++++--- xmlContentTypes.go | 8 +++++--- xmlCore.go | 8 +++++--- xmlDecodeDrawing.go | 8 +++++--- xmlDrawing.go | 8 +++++--- xmlPivotTable.go | 8 +++++--- xmlSharedStrings.go | 8 +++++--- xmlStyles.go | 8 +++++--- xmlTable.go | 8 +++++--- xmlTheme.go | 8 +++++--- xmlWorkbook.go | 8 +++++--- xmlWorksheet.go | 8 +++++--- 43 files changed, 212 insertions(+), 128 deletions(-) diff --git a/adjust.go b/adjust.go index 5056839da3..226ea9e6fc 100644 --- a/adjust.go +++ b/adjust.go @@ -3,9 +3,11 @@ // the LICENSE file. // // Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.10 or later. +// and read from XLSX / XLSM / XLTM files. Supports reading and writing +// spreadsheet documents generated by Microsoft Exce™ 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.10 or later. package excelize diff --git a/calcchain.go b/calcchain.go index f50fb1d575..03505079b0 100644 --- a/calcchain.go +++ b/calcchain.go @@ -3,9 +3,11 @@ // the LICENSE file. // // Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.10 or later. +// and read from XLSX / XLSM / XLTM files. Supports reading and writing +// spreadsheet documents generated by Microsoft Exce™ 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.10 or later. package excelize diff --git a/cell.go b/cell.go index fb0a83366f..fa46007cdb 100644 --- a/cell.go +++ b/cell.go @@ -3,9 +3,11 @@ // the LICENSE file. // // Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.10 or later. +// and read from XLSX / XLSM / XLTM files. Supports reading and writing +// spreadsheet documents generated by Microsoft Exce™ 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.10 or later. package excelize diff --git a/chart.go b/chart.go index 8fa0b5de61..b584c18253 100644 --- a/chart.go +++ b/chart.go @@ -3,9 +3,11 @@ // the LICENSE file. // // Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.10 or later. +// and read from XLSX / XLSM / XLTM files. Supports reading and writing +// spreadsheet documents generated by Microsoft Exce™ 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.10 or later. package excelize diff --git a/col.go b/col.go index 6756f2fd87..d6f069049a 100644 --- a/col.go +++ b/col.go @@ -3,9 +3,11 @@ // the LICENSE file. // // Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.10 or later. +// and read from XLSX / XLSM / XLTM files. Supports reading and writing +// spreadsheet documents generated by Microsoft Exce™ 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.10 or later. package excelize diff --git a/col_test.go b/col_test.go index fac78eb197..e6e7e29b8e 100644 --- a/col_test.go +++ b/col_test.go @@ -129,7 +129,7 @@ func TestGetColsError(t *testing.T) { cols.totalRow = 2 cols.totalCol = 2 cols.curCol = 1 - cols.decoder = []byte(`A`) + cols.sheetXML = []byte(`A`) _, err = cols.Rows() assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`) } @@ -153,7 +153,7 @@ func TestColsRows(t *testing.T) { assert.NoError(t, err) // Test if token is nil - cols.decoder = nil + cols.sheetXML = nil _, err = cols.Rows() assert.NoError(t, err) } diff --git a/comment.go b/comment.go index e224502389..28140bfe9b 100644 --- a/comment.go +++ b/comment.go @@ -3,9 +3,11 @@ // the LICENSE file. // // Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.10 or later. +// and read from XLSX / XLSM / XLTM files. Supports reading and writing +// spreadsheet documents generated by Microsoft Exce™ 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.10 or later. package excelize diff --git a/datavalidation.go b/datavalidation.go index 1aeb1dca3e..f76f9b3cc4 100644 --- a/datavalidation.go +++ b/datavalidation.go @@ -3,9 +3,11 @@ // the LICENSE file. // // Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.10 or later. +// and read from XLSX / XLSM / XLTM files. Supports reading and writing +// spreadsheet documents generated by Microsoft Exce™ 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.10 or later. package excelize diff --git a/date.go b/date.go index 172c32c58d..34c8989cb5 100644 --- a/date.go +++ b/date.go @@ -3,9 +3,11 @@ // the LICENSE file. // // Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.10 or later. +// and read from XLSX / XLSM / XLTM files. Supports reading and writing +// spreadsheet documents generated by Microsoft Exce™ 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.10 or later. package excelize diff --git a/docProps.go b/docProps.go index a61ee7170a..03604c9348 100644 --- a/docProps.go +++ b/docProps.go @@ -3,9 +3,11 @@ // the LICENSE file. // // Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.10 or later. +// and read from XLSX / XLSM / XLTM files. Supports reading and writing +// spreadsheet documents generated by Microsoft Exce™ 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.10 or later. package excelize diff --git a/drawing.go b/drawing.go index 7c09d4db1d..ea75e826c1 100644 --- a/drawing.go +++ b/drawing.go @@ -3,9 +3,11 @@ // the LICENSE file. // // Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.10 or later. +// and read from XLSX / XLSM / XLTM files. Supports reading and writing +// spreadsheet documents generated by Microsoft Exce™ 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.10 or later. package excelize diff --git a/errors.go b/errors.go index 5576ecdb68..a31c93ab43 100644 --- a/errors.go +++ b/errors.go @@ -3,9 +3,11 @@ // the LICENSE file. // // Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.10 or later. +// and read from XLSX / XLSM / XLTM files. Supports reading and writing +// spreadsheet documents generated by Microsoft Exce™ 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.10 or later. package excelize diff --git a/file.go b/file.go index e8f29dae97..34ec359114 100644 --- a/file.go +++ b/file.go @@ -3,9 +3,11 @@ // the LICENSE file. // // Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.10 or later. +// and read from XLSX / XLSM / XLTM files. Supports reading and writing +// spreadsheet documents generated by Microsoft Exce™ 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.10 or later. package excelize diff --git a/lib.go b/lib.go index ae9287c075..73edb66c8b 100644 --- a/lib.go +++ b/lib.go @@ -3,9 +3,11 @@ // the LICENSE file. // // Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.10 or later. +// and read from XLSX / XLSM / XLTM files. Supports reading and writing +// spreadsheet documents generated by Microsoft Exce™ 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.10 or later. package excelize diff --git a/merge.go b/merge.go index f29640ddbd..b233335090 100644 --- a/merge.go +++ b/merge.go @@ -3,9 +3,11 @@ // the LICENSE file. // // Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.10 or later. +// and read from XLSX / XLSM / XLTM files. Supports reading and writing +// spreadsheet documents generated by Microsoft Exce™ 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.10 or later. package excelize diff --git a/picture.go b/picture.go index cac1af2e66..79aa6ca54d 100644 --- a/picture.go +++ b/picture.go @@ -3,9 +3,11 @@ // the LICENSE file. // // Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.10 or later. +// and read from XLSX / XLSM / XLTM files. Supports reading and writing +// spreadsheet documents generated by Microsoft Exce™ 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.10 or later. package excelize diff --git a/pivotTable.go b/pivotTable.go index cf043817c7..f820d76026 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -3,9 +3,11 @@ // the LICENSE file. // // Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.10 or later. +// and read from XLSX / XLSM / XLTM files. Supports reading and writing +// spreadsheet documents generated by Microsoft Exce™ 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.10 or later. package excelize diff --git a/rows.go b/rows.go index f9770aab10..18d9957ef2 100644 --- a/rows.go +++ b/rows.go @@ -3,9 +3,11 @@ // the LICENSE file. // // Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.10 or later. +// and read from XLSX / XLSM / XLTM files. Supports reading and writing +// spreadsheet documents generated by Microsoft Exce™ 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.10 or later. package excelize diff --git a/shape.go b/shape.go index 0455b220d2..316061668e 100644 --- a/shape.go +++ b/shape.go @@ -3,9 +3,11 @@ // the LICENSE file. // // Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.10 or later. +// and read from XLSX / XLSM / XLTM files. Supports reading and writing +// spreadsheet documents generated by Microsoft Exce™ 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.10 or later. package excelize diff --git a/sheet.go b/sheet.go index 6a935b1fdc..56dac27de2 100644 --- a/sheet.go +++ b/sheet.go @@ -3,9 +3,11 @@ // the LICENSE file. // // Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.10 or later. +// and read from XLSX / XLSM / XLTM files. Supports reading and writing +// spreadsheet documents generated by Microsoft Exce™ 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.10 or later. package excelize diff --git a/sheetpr.go b/sheetpr.go index dbfb7341b3..ee3b23c504 100644 --- a/sheetpr.go +++ b/sheetpr.go @@ -3,9 +3,11 @@ // the LICENSE file. // // Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.10 or later. +// and read from XLSX / XLSM / XLTM files. Supports reading and writing +// spreadsheet documents generated by Microsoft Exce™ 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.10 or later. package excelize diff --git a/sheetview.go b/sheetview.go index fa3cfdfaaa..23a0377d9e 100644 --- a/sheetview.go +++ b/sheetview.go @@ -3,9 +3,11 @@ // the LICENSE file. // // Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.10 or later. +// and read from XLSX / XLSM / XLTM files. Supports reading and writing +// spreadsheet documents generated by Microsoft Exce™ 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.10 or later. package excelize diff --git a/sparkline.go b/sparkline.go index ce5be4c32c..9682db04e6 100644 --- a/sparkline.go +++ b/sparkline.go @@ -3,9 +3,11 @@ // the LICENSE file. // // Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.10 or later. +// and read from XLSX / XLSM / XLTM files. Supports reading and writing +// spreadsheet documents generated by Microsoft Exce™ 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.10 or later. package excelize diff --git a/stream.go b/stream.go index 838751d32c..81dea1ecb4 100644 --- a/stream.go +++ b/stream.go @@ -3,9 +3,11 @@ // the LICENSE file. // // Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.10 or later. +// and read from XLSX / XLSM / XLTM files. Supports reading and writing +// spreadsheet documents generated by Microsoft Exce™ 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.10 or later. package excelize diff --git a/styles.go b/styles.go index a91aca675a..2ae1cd8f3e 100644 --- a/styles.go +++ b/styles.go @@ -3,9 +3,11 @@ // the LICENSE file. // // Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.10 or later. +// and read from XLSX / XLSM / XLTM files. Supports reading and writing +// spreadsheet documents generated by Microsoft Exce™ 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.10 or later. package excelize diff --git a/table.go b/table.go index 5a0e46f88e..d59322ca6a 100644 --- a/table.go +++ b/table.go @@ -3,9 +3,11 @@ // the LICENSE file. // // Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.10 or later. +// and read from XLSX / XLSM / XLTM files. Supports reading and writing +// spreadsheet documents generated by Microsoft Exce™ 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.10 or later. package excelize diff --git a/templates.go b/templates.go index a7972e6169..5721150ce8 100644 --- a/templates.go +++ b/templates.go @@ -3,9 +3,11 @@ // the LICENSE file. // // Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.10 or later. +// and read from XLSX / XLSM / XLTM files. Supports reading and writing +// spreadsheet documents generated by Microsoft Exce™ 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.10 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 f2d55f17f1..185df28890 100644 --- a/vmlDrawing.go +++ b/vmlDrawing.go @@ -3,9 +3,11 @@ // the LICENSE file. // // Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.10 or later. +// and read from XLSX / XLSM / XLTM files. Supports reading and writing +// spreadsheet documents generated by Microsoft Exce™ 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.10 or later. package excelize diff --git a/xmlApp.go b/xmlApp.go index 5668cf641d..1d51095254 100644 --- a/xmlApp.go +++ b/xmlApp.go @@ -3,9 +3,11 @@ // the LICENSE file. // // Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.10 or later. +// and read from XLSX / XLSM / XLTM files. Supports reading and writing +// spreadsheet documents generated by Microsoft Exce™ 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.10 or later. package excelize diff --git a/xmlCalcChain.go b/xmlCalcChain.go index 69d5d8cad7..401bb5e307 100644 --- a/xmlCalcChain.go +++ b/xmlCalcChain.go @@ -3,9 +3,11 @@ // the LICENSE file. // // Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.10 or later. +// and read from XLSX / XLSM / XLTM files. Supports reading and writing +// spreadsheet documents generated by Microsoft Exce™ 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.10 or later. package excelize diff --git a/xmlChart.go b/xmlChart.go index 03b47a1f83..6f800d7220 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -3,9 +3,11 @@ // the LICENSE file. // // Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.10 or later. +// and read from XLSX / XLSM / XLTM files. Supports reading and writing +// spreadsheet documents generated by Microsoft Exce™ 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.10 or later. package excelize diff --git a/xmlComments.go b/xmlComments.go index 687c486d02..0f02b18813 100644 --- a/xmlComments.go +++ b/xmlComments.go @@ -3,9 +3,11 @@ // the LICENSE file. // // Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.10 or later. +// and read from XLSX / XLSM / XLTM files. Supports reading and writing +// spreadsheet documents generated by Microsoft Exce™ 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.10 or later. package excelize diff --git a/xmlContentTypes.go b/xmlContentTypes.go index 7acfe08760..458b117311 100644 --- a/xmlContentTypes.go +++ b/xmlContentTypes.go @@ -3,9 +3,11 @@ // the LICENSE file. // // Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.10 or later. +// and read from XLSX / XLSM / XLTM files. Supports reading and writing +// spreadsheet documents generated by Microsoft Exce™ 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.10 or later. package excelize diff --git a/xmlCore.go b/xmlCore.go index 6f71a3ef01..9d7fc4551d 100644 --- a/xmlCore.go +++ b/xmlCore.go @@ -3,9 +3,11 @@ // the LICENSE file. // // Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.10 or later. +// and read from XLSX / XLSM / XLTM files. Supports reading and writing +// spreadsheet documents generated by Microsoft Exce™ 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.10 or later. package excelize diff --git a/xmlDecodeDrawing.go b/xmlDecodeDrawing.go index 93e0e827f4..8aa22dbfa6 100644 --- a/xmlDecodeDrawing.go +++ b/xmlDecodeDrawing.go @@ -3,9 +3,11 @@ // the LICENSE file. // // Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.10 or later. +// and read from XLSX / XLSM / XLTM files. Supports reading and writing +// spreadsheet documents generated by Microsoft Exce™ 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.10 or later. package excelize diff --git a/xmlDrawing.go b/xmlDrawing.go index b2eeed67d1..9c7ef54cbd 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -3,9 +3,11 @@ // the LICENSE file. // // Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.10 or later. +// and read from XLSX / XLSM / XLTM files. Supports reading and writing +// spreadsheet documents generated by Microsoft Exce™ 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.10 or later. package excelize diff --git a/xmlPivotTable.go b/xmlPivotTable.go index 2eff026514..657a9e8c34 100644 --- a/xmlPivotTable.go +++ b/xmlPivotTable.go @@ -3,9 +3,11 @@ // the LICENSE file. // // Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.10 or later. +// and read from XLSX / XLSM / XLTM files. Supports reading and writing +// spreadsheet documents generated by Microsoft Exce™ 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.10 or later. package excelize diff --git a/xmlSharedStrings.go b/xmlSharedStrings.go index 6e34abc261..eac1672f7d 100644 --- a/xmlSharedStrings.go +++ b/xmlSharedStrings.go @@ -3,9 +3,11 @@ // the LICENSE file. // // Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.10 or later. +// and read from XLSX / XLSM / XLTM files. Supports reading and writing +// spreadsheet documents generated by Microsoft Exce™ 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.10 or later. package excelize diff --git a/xmlStyles.go b/xmlStyles.go index 42d535b5ac..07413dd996 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -3,9 +3,11 @@ // the LICENSE file. // // Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.10 or later. +// and read from XLSX / XLSM / XLTM files. Supports reading and writing +// spreadsheet documents generated by Microsoft Exce™ 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.10 or later. package excelize diff --git a/xmlTable.go b/xmlTable.go index 345337f24e..22d191e349 100644 --- a/xmlTable.go +++ b/xmlTable.go @@ -3,9 +3,11 @@ // the LICENSE file. // // Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.10 or later. +// and read from XLSX / XLSM / XLTM files. Supports reading and writing +// spreadsheet documents generated by Microsoft Exce™ 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.10 or later. package excelize diff --git a/xmlTheme.go b/xmlTheme.go index 76f13b4cbc..2f181e267c 100644 --- a/xmlTheme.go +++ b/xmlTheme.go @@ -3,9 +3,11 @@ // the LICENSE file. // // Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.10 or later. +// and read from XLSX / XLSM / XLTM files. Supports reading and writing +// spreadsheet documents generated by Microsoft Exce™ 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.10 or later. package excelize diff --git a/xmlWorkbook.go b/xmlWorkbook.go index bc59924911..733eb57c7b 100644 --- a/xmlWorkbook.go +++ b/xmlWorkbook.go @@ -3,9 +3,11 @@ // the LICENSE file. // // Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.10 or later. +// and read from XLSX / XLSM / XLTM files. Supports reading and writing +// spreadsheet documents generated by Microsoft Exce™ 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.10 or later. package excelize diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 316ffd783f..03b1a713c8 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -3,9 +3,11 @@ // the LICENSE file. // // Package excelize providing a set of functions that allow you to write to -// and read from XLSX files. Support reads and writes XLSX file generated by -// Microsoft Excel™ 2007 and later. Support save file without losing original -// charts of XLSX. This library needs Go version 1.10 or later. +// and read from XLSX / XLSM / XLTM files. Supports reading and writing +// spreadsheet documents generated by Microsoft Exce™ 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.10 or later. package excelize From 48f19f60aa3e162146a9dc4edf7b4c8cf687ec26 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 27 Jun 2020 00:02:47 +0800 Subject: [PATCH 255/957] support the row element without r attribute in the worksheet --- cell_test.go | 17 +++++++++++++++++ col.go | 18 +++++++----------- excelize.go | 21 +++++++++++++++------ rows.go | 40 ++++++++++++++++++++++------------------ 4 files changed, 61 insertions(+), 35 deletions(-) diff --git a/cell_test.go b/cell_test.go index 45e2f2488d..b2b1d54e7d 100644 --- a/cell_test.go +++ b/cell_test.go @@ -95,6 +95,23 @@ func TestSetCellBool(t *testing.T) { assert.EqualError(t, f.SetCellBool("Sheet1", "A", true), `cannot convert cell "A" to coordinates: invalid cell name "A"`) } +func TestGetCellValue(t *testing.T) { + // Test get cell value without r attribute of the row. + f := NewFile() + delete(f.Sheet, "xl/worksheets/sheet1.xml") + f.XLSX["xl/worksheets/sheet1.xml"] = []byte(`A3A4B4A7B7A8B8`) + f.checked = nil + cells := []string{"A3", "A4", "B4", "A7", "B7"} + rows, err := f.GetRows("Sheet1") + assert.Equal(t, [][]string{nil, nil, {"A3"}, {"A4", "B4"}, nil, nil, {"A7", "B7"}, {"A8", "B8"}}, rows) + assert.NoError(t, err) + for _, cell := range cells { + value, err := f.GetCellValue("Sheet1", cell) + assert.Equal(t, cell, value) + assert.NoError(t, err) + } +} + func TestGetCellFormula(t *testing.T) { // Test get cell formula on not exist worksheet. f := NewFile() diff --git a/col.go b/col.go index d6f069049a..0baa2e4dea 100644 --- a/col.go +++ b/col.go @@ -42,18 +42,14 @@ type Cols struct { // GetCols return all the columns in a sheet by given worksheet name (case // sensitive). For example: // -// cols, err := f.Cols("Sheet1") +// cols, err := f.GetCols("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") +// for _, col := range cols { +// for _, colCell := range col { +// fmt.Println(colCell, "\t") // } // fmt.Println() // } @@ -71,13 +67,13 @@ func (f *File) GetCols(sheet string) ([][]string, error) { return results, nil } -// Next will return true if the next col element is found. +// Next will return true if the next column is found. func (cols *Cols) Next() bool { cols.curCol++ return cols.curCol <= cols.totalCol } -// Error will return an error when the next col element is found. +// Error will return an error when the error occurs. func (cols *Cols) Error() error { return cols.err } @@ -127,7 +123,7 @@ func (cols *Cols) Rows() ([]string, error) { return rows, nil } -// Cols returns a columns iterator, used for streaming/reading data for a +// Cols returns a columns iterator, used for streaming reading data for a // worksheet with a large data. For example: // // cols, err := f.Cols("Sheet1") diff --git a/excelize.go b/excelize.go index 3e0255aca9..970759c136 100644 --- a/excelize.go +++ b/excelize.go @@ -191,16 +191,25 @@ func (f *File) workSheetReader(sheet string) (xlsx *xlsxWorksheet, err error) { // checkSheet provides a function to fill each row element and make that is // continuous in a worksheet of XML. func checkSheet(xlsx *xlsxWorksheet) { - row := len(xlsx.SheetData.Row) - if row >= 1 { - lastRow := xlsx.SheetData.Row[row-1].R - if lastRow >= row { - row = lastRow + var row int + for _, r := range xlsx.SheetData.Row { + if r.R != 0 && r.R > row { + row = r.R + continue } + row++ } sheetData := xlsxSheetData{Row: make([]xlsxRow, row)} + row = 0 for _, r := range xlsx.SheetData.Row { - sheetData.Row[r.R-1] = r + if r.R != 0 { + sheetData.Row[r.R-1] = r + row = r.R + continue + } + row++ + r.R = row + sheetData.Row[row-1] = r } for i := 1; i <= row; i++ { sheetData.Row[i-1].R = i diff --git a/rows.go b/rows.go index 18d9957ef2..38c1ecc8ff 100644 --- a/rows.go +++ b/rows.go @@ -25,18 +25,14 @@ import ( // GetRows return all the rows in a sheet by given worksheet name (case // sensitive). For example: // -// rows, err := f.Rows("Sheet1") +// rows, err := f.GetRows("Sheet1") // if err != nil { // fmt.Println(err) // return // } -// for rows.Next() { -// row, err := rows.Columns() -// if err != nil { -// fmt.Println(err) -// } +// for _, row := range rows { // for _, colCell := range row { -// fmt.Print(colCell, "\t") +// fmt.Println(colCell, "\t") // } // fmt.Println() // } @@ -57,7 +53,7 @@ func (f *File) GetRows(sheet string) ([][]string, error) { return results, nil } -// Rows defines an iterator to a sheet +// Rows defines an iterator to a sheet. type Rows struct { err error curRow, totalRow, stashRow int @@ -73,12 +69,12 @@ func (rows *Rows) Next() bool { return rows.curRow <= rows.totalRow } -// Error will return the error when the find next row element +// Error will return the error when the error occurs. func (rows *Rows) Error() error { return rows.err } -// Columns return the current row's column values +// Columns return the current row's column values. func (rows *Rows) Columns() ([]string, error) { var ( err error @@ -117,9 +113,13 @@ func (rows *Rows) Columns() ([]string, error) { if inElement == "c" { colCell := xlsxC{} _ = rows.decoder.DecodeElement(&colCell, &startElement) - cellCol, _, err = CellNameToCoordinates(colCell.R) - if err != nil { - return columns, err + if colCell.R != "" { + cellCol, _, err = CellNameToCoordinates(colCell.R) + if err != nil { + return columns, err + } + } else { + cellCol++ } blank := cellCol - len(columns) for i := 1; i < blank; i++ { @@ -177,10 +177,10 @@ func (f *File) Rows(sheet string) (*Rows, error) { f.saveFileList(name, replaceRelationshipsNameSpaceBytes(output)) } var ( - err error - inElement string - row int - rows Rows + err error + inElement string + row, curRow int + rows Rows ) decoder := f.xmlNewDecoder(bytes.NewReader(f.readXML(name))) for { @@ -194,12 +194,16 @@ func (f *File) Rows(sheet string) (*Rows, error) { if inElement == "row" { for _, attr := range startElement.Attr { if attr.Name.Local == "r" { - row, err = strconv.Atoi(attr.Value) + curRow, err = strconv.Atoi(attr.Value) if err != nil { return &rows, err } + row = curRow } } + if len(startElement.Attr) == 0 { + row++ + } rows.totalRow = row } default: From 1cbb05d4977fc1c03fa37d704118fd9c722e487d Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 28 Jun 2020 00:02:32 +0800 Subject: [PATCH 256/957] GetCols support the row element without r attribute in the worksheet --- cell_test.go | 3 +++ col.go | 55 +++++++++++++++++++++++++++++++++------------------- rows.go | 20 ++++++++----------- 3 files changed, 46 insertions(+), 32 deletions(-) diff --git a/cell_test.go b/cell_test.go index b2b1d54e7d..fb30596f18 100644 --- a/cell_test.go +++ b/cell_test.go @@ -110,6 +110,9 @@ func TestGetCellValue(t *testing.T) { assert.Equal(t, cell, value) assert.NoError(t, err) } + cols, err := f.GetCols("Sheet1") + assert.Equal(t, [][]string{{"", "", "A3", "A4", "", "", "A7", "A8"}, {"", "", "", "B4", "", "", "B7", "B8"}}, cols) + assert.NoError(t, err) } func TestGetCellFormula(t *testing.T) { diff --git a/col.go b/col.go index 0baa2e4dea..472106f3e4 100644 --- a/col.go +++ b/col.go @@ -48,8 +48,8 @@ type Cols struct { // return // } // for _, col := range cols { -// for _, colCell := range col { -// fmt.Println(colCell, "\t") +// for _, rowCell := range col { +// fmt.Print(rowCell, "\t") // } // fmt.Println() // } @@ -99,24 +99,34 @@ func (cols *Cols) Rows() ([]string, error) { switch startElement := token.(type) { case xml.StartElement: inElement = startElement.Name.Local + if inElement == "row" { + cellCol = 0 + cellRow++ + for _, attr := range startElement.Attr { + if attr.Name.Local == "r" { + cellRow, _ = strconv.Atoi(attr.Value) + } + } + } if inElement == "c" { + cellCol++ for _, attr := range startElement.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, &startElement) - val, _ := colCell.getValueFrom(cols.f, d) - rows = append(rows, val) - } } } + blank := cellRow - len(rows) + for i := 1; i < blank; i++ { + rows = append(rows, "") + } + if cellCol == cols.curCol { + colCell := xlsxC{} + _ = decoder.DecodeElement(&colCell, &startElement) + val, _ := colCell.getValueFrom(cols.f, d) + rows = append(rows, val) + } } } } @@ -152,10 +162,10 @@ func (f *File) Cols(sheet string) (*Cols, error) { f.saveFileList(name, replaceRelationshipsNameSpaceBytes(output)) } var ( - inElement string - cols Cols - cellCol int - err error + inElement string + cols Cols + cellCol, curRow, row int + err error ) cols.sheetXML = f.readXML(name) decoder := f.xmlNewDecoder(bytes.NewReader(cols.sheetXML)) @@ -168,25 +178,30 @@ func (f *File) Cols(sheet string) (*Cols, error) { case xml.StartElement: inElement = startElement.Name.Local if inElement == "row" { + row++ for _, attr := range startElement.Attr { if attr.Name.Local == "r" { - if cols.totalRow, err = strconv.Atoi(attr.Value); err != nil { + if curRow, err = strconv.Atoi(attr.Value); err != nil { return &cols, err } + row = curRow } } + cols.totalRow = row + cellCol = 0 } if inElement == "c" { + cellCol++ for _, attr := range startElement.Attr { if attr.Name.Local == "r" { if cellCol, _, err = CellNameToCoordinates(attr.Value); err != nil { return &cols, err } - if cellCol > cols.totalCol { - cols.totalCol = cellCol - } } } + if cellCol > cols.totalCol { + cols.totalCol = cellCol + } } } } diff --git a/rows.go b/rows.go index 38c1ecc8ff..b89d91671c 100644 --- a/rows.go +++ b/rows.go @@ -32,7 +32,7 @@ import ( // } // for _, row := range rows { // for _, colCell := range row { -// fmt.Println(colCell, "\t") +// fmt.Print(colCell, "\t") // } // fmt.Println() // } @@ -111,6 +111,7 @@ func (rows *Rows) Columns() ([]string, error) { } } if inElement == "c" { + cellCol++ colCell := xlsxC{} _ = rows.decoder.DecodeElement(&colCell, &startElement) if colCell.R != "" { @@ -118,8 +119,6 @@ func (rows *Rows) Columns() ([]string, error) { if err != nil { return columns, err } - } else { - cellCol++ } blank := cellCol - len(columns) for i := 1; i < blank; i++ { @@ -177,10 +176,10 @@ func (f *File) Rows(sheet string) (*Rows, error) { f.saveFileList(name, replaceRelationshipsNameSpaceBytes(output)) } var ( - err error - inElement string - row, curRow int - rows Rows + err error + inElement string + row int + rows Rows ) decoder := f.xmlNewDecoder(bytes.NewReader(f.readXML(name))) for { @@ -192,18 +191,15 @@ func (f *File) Rows(sheet string) (*Rows, error) { case xml.StartElement: inElement = startElement.Name.Local if inElement == "row" { + row++ for _, attr := range startElement.Attr { if attr.Name.Local == "r" { - curRow, err = strconv.Atoi(attr.Value) + row, err = strconv.Atoi(attr.Value) if err != nil { return &rows, err } - row = curRow } } - if len(startElement.Attr) == 0 { - row++ - } rows.totalRow = row } default: From f7bd0729c65fc82305328f7ac8fbaf329d1075c0 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 1 Jul 2020 22:41:29 +0800 Subject: [PATCH 257/957] Resolve #32, fix missing leading/leading spaces when working with SST --- cell.go | 25 +++++++++++++------------ rows.go | 4 ++-- xmlSharedStrings.go | 9 ++++++--- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/cell.go b/cell.go index fa46007cdb..0163c3bcb0 100644 --- a/cell.go +++ b/cell.go @@ -15,7 +15,6 @@ import ( "encoding/xml" "errors" "fmt" - "html" "reflect" "strconv" "strings" @@ -274,23 +273,16 @@ func (f *File) SetCellStr(sheet, axis, value string) error { return err } cellData.S = f.prepareCellStyle(xlsx, col, cellData.S) - cellData.T, cellData.V, cellData.XMLSpace = f.setCellString(value) + cellData.T, cellData.V = f.setCellString(value) return err } // setCellString provides a function to set string type to shared string // table. -func (f *File) setCellString(value string) (t string, v string, ns xml.Attr) { +func (f *File) setCellString(value string) (t string, v string) { if len(value) > TotalCellChars { value = value[0:TotalCellChars] } - // Leading and ending space(s) character detection. - if len(value) > 0 && (value[0] == 32 || value[len(value)-1] == 32) { - ns = xml.Attr{ - Name: xml.Name{Space: NameSpaceXML, Local: "space"}, - Value: "preserve", - } - } t = "s" v = strconv.Itoa(f.setSharedString(value)) return @@ -304,7 +296,16 @@ func (f *File) setSharedString(val string) int { } sst.Count++ sst.UniqueCount++ - sst.SI = append(sst.SI, xlsxSI{T: val}) + t := xlsxT{Val: val} + // Leading and ending space(s) character detection. + if len(val) > 0 && (val[0] == 32 || val[len(val)-1] == 32) { + ns := xml.Attr{ + Name: xml.Name{Space: NameSpaceXML, Local: "space"}, + Value: "preserve", + } + t.Space = ns + } + sst.SI = append(sst.SI, xlsxSI{T: &t}) f.sharedStringsMap[val] = sst.UniqueCount - 1 return sst.UniqueCount - 1 } @@ -620,7 +621,7 @@ func (f *File) SetCellRichText(sheet, cell string, runs []RichTextRun) error { sst := f.sharedStringsReader() textRuns := []xlsxR{} for _, textRun := range runs { - run := xlsxR{T: &xlsxT{Val: html.EscapeString(textRun.Text)}} + run := xlsxR{T: &xlsxT{Val: textRun.Text}} if strings.ContainsAny(textRun.Text, "\r\n ") { run.T.Space = xml.Attr{Name: xml.Name{Space: NameSpaceXML, Local: "space"}, Value: "preserve"} } diff --git a/rows.go b/rows.go index b89d91671c..392cc5d4d5 100644 --- a/rows.go +++ b/rows.go @@ -292,8 +292,8 @@ func (f *File) sharedStringsReader() *xlsxSST { } f.SharedStrings = &sharedStrings for i := range sharedStrings.SI { - if sharedStrings.SI[i].T != "" { - f.sharedStringsMap[sharedStrings.SI[i].T] = i + if sharedStrings.SI[i].T != nil { + f.sharedStringsMap[sharedStrings.SI[i].T.Val] = i } } f.addContentTypePart(0, "sharedStrings") diff --git a/xmlSharedStrings.go b/xmlSharedStrings.go index eac1672f7d..d5fe4a7237 100644 --- a/xmlSharedStrings.go +++ b/xmlSharedStrings.go @@ -38,7 +38,7 @@ type xlsxSST struct { // level - then the string item shall consist of multiple rich text runs which // collectively are used to express the string. type xlsxSI struct { - T string `xml:"t,omitempty"` + T *xlsxT `xml:"t,omitempty"` R []xlsxR `xml:"r"` } @@ -53,7 +53,10 @@ func (x xlsxSI) String() string { } return rows.String() } - return x.T + if x.T != nil { + return x.T.Val + } + return "" } // xlsxR represents a run of rich text. A rich text run is a region of text @@ -69,7 +72,7 @@ type xlsxR struct { type xlsxT struct { XMLName xml.Name `xml:"t"` Space xml.Attr `xml:"space,attr,omitempty"` - Val string `xml:",innerxml"` + Val string `xml:",chardata"` } // xlsxRPr (Run Properties) specifies a set of run properties which shall be From 49257c5918f3aa9f2730021a7e6a24b4835646fd Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 9 Jul 2020 01:24:11 +0800 Subject: [PATCH 258/957] support case-sensitive doc parts to improve compatibility --- README.md | 12 ++++++------ excelize.go | 11 ++++++++++- excelize_test.go | 12 ++++++------ lib.go | 10 +++++++++- picture.go | 4 ++-- rows.go | 4 ---- sheet.go | 33 +++++++++++++++++---------------- 7 files changed, 50 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index b3106df013..97db54a62e 100644 --- a/README.md +++ b/README.md @@ -29,9 +29,9 @@ go get github.com/360EntSecGroup-Skylar/excelize go get github.com/360EntSecGroup-Skylar/excelize/v2 ``` -### Create XLSX file +### Create spreadsheet -Here is a minimal example usage that will create XLSX file. +Here is a minimal example usage that will create spreadsheet file. ```go package main @@ -58,9 +58,9 @@ func main() { } ``` -### Reading XLSX file +### Reading spreadsheet -The following constitutes the bare to read a XLSX document. +The following constitutes the bare to read a spreadsheet document. ```go package main @@ -95,7 +95,7 @@ func main() { } ``` -### Add chart to XLSX file +### Add chart to spreadsheet file With Excelize chart generation and management is as easy as a few lines of code. You can build charts based off data in your worksheet or generate charts without any data in your worksheet at all. @@ -131,7 +131,7 @@ func main() { } ``` -### Add picture to XLSX file +### Add picture to spreadsheet file ```go package main diff --git a/excelize.go b/excelize.go index 970759c136..bac569afce 100644 --- a/excelize.go +++ b/excelize.go @@ -220,16 +220,25 @@ func checkSheet(xlsx *xlsxWorksheet) { // 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 { + var uniqPart = map[string]string{ + SourceRelationshipSharedStrings: "/xl/sharedStrings.xml", + } rels := f.relsReader(relPath) if rels == nil { rels = &xlsxRelationships{} } var rID int - for _, rel := range rels.Relationships { + for idx, rel := range rels.Relationships { ID, _ := strconv.Atoi(strings.TrimPrefix(rel.ID, "rId")) if ID > rID { rID = ID } + if relType == rel.Type { + if partName, ok := uniqPart[rel.Type]; ok { + rels.Relationships[idx].Target = partName + return rID + } + } } rID++ var ID bytes.Buffer diff --git a/excelize_test.go b/excelize_test.go index b31e1c844e..c2bd3ad828 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -22,7 +22,7 @@ import ( ) func TestOpenFile(t *testing.T) { - // Test update a XLSX file. + // Test update the spreadsheet file. f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) assert.NoError(t, err) @@ -154,11 +154,11 @@ func TestOpenFile(t *testing.T) { // Test read cell value with given axis large than exists row. _, err = f.GetCellValue("Sheet2", "E231") assert.NoError(t, err) - // Test get active worksheet of XLSX and get worksheet name of XLSX 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 XLSX by given worksheet name. + // Test get worksheet index of spreadsheet by given worksheet name. f.GetSheetIndex("Sheet1") - // Test get worksheet name of XLSX 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. f.GetSheetMap() @@ -261,7 +261,7 @@ func TestBrokenFile(t *testing.T) { }) t.Run("OpenNotExistsFile", func(t *testing.T) { - // Test open a XLSX 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") @@ -270,7 +270,7 @@ func TestBrokenFile(t *testing.T) { } func TestNewFile(t *testing.T) { - // Test create a XLSX file. + // Test create a spreadsheet file. f := NewFile() f.NewSheet("Sheet1") f.NewSheet("XLSXSheet2") diff --git a/lib.go b/lib.go index 73edb66c8b..5d18064c3e 100644 --- a/lib.go +++ b/lib.go @@ -26,10 +26,18 @@ import ( // filesystem. func ReadZipReader(r *zip.Reader) (map[string][]byte, int, error) { var err error + var docPart = map[string]string{ + "[content_types].xml": "[Content_Types].xml", + "xl/sharedstrings.xml": "xl/sharedStrings.xml", + } fileList := make(map[string][]byte, len(r.File)) worksheets := 0 for _, v := range r.File { - if fileList[v.Name], err = readFile(v); err != nil { + fileName := v.Name + if partName, ok := docPart[strings.ToLower(v.Name)]; ok { + fileName = partName + } + if fileList[fileName], err = readFile(v); err != nil { return nil, 0, err } if strings.HasPrefix(v.Name, "xl/worksheets/sheet") { diff --git a/picture.go b/picture.go index 79aa6ca54d..0e9e3bb6a7 100644 --- a/picture.go +++ b/picture.go @@ -474,7 +474,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 XLSX by given +// 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. func (f *File) DeletePicture(sheet, cell string) (err error) { @@ -496,7 +496,7 @@ func (f *File) DeletePicture(sheet, cell string) (err error) { } // getPicture provides a function to get picture base name and raw content -// embed in XLSX by given coordinates and drawing relationships. +// 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) { var ( wsDr *xlsxWsDr diff --git a/rows.go b/rows.go index 392cc5d4d5..87576c33bb 100644 --- a/rows.go +++ b/rows.go @@ -282,10 +282,6 @@ func (f *File) sharedStringsReader() *xlsxSST { if f.SharedStrings == nil { var sharedStrings xlsxSST ss := f.readXML("xl/sharedStrings.xml") - if len(ss) == 0 { - ss = f.readXML("xl/SharedStrings.xml") - delete(f.XLSX, "xl/SharedStrings.xml") - } if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(ss))). Decode(&sharedStrings); err != nil && err != io.EOF { log.Printf("xml decode error: %s", err) diff --git a/sheet.go b/sheet.go index 56dac27de2..bbe84c2df3 100644 --- a/sheet.go +++ b/sheet.go @@ -32,8 +32,9 @@ import ( ) // NewSheet provides function to create a new sheet by given worksheet name. -// When creating a new XLSX file, the default sheet will be created. Returns -// the number of sheets in the workbook (file) after appending the new sheet. +// When creating a new spreadsheet file, the default worksheet will be +// created. Returns the number of sheets in the workbook (file) after +// appending the new sheet. func (f *File) NewSheet(name string) int { // Check if the worksheet already exists if f.GetSheetIndex(name) != -1 { @@ -152,7 +153,7 @@ func trimCell(column []xlsxC) []xlsxC { } // setContentTypes provides a function to read and update property of contents -// type of XLSX. +// type of the spreadsheet. func (f *File) setContentTypes(partName, contentType string) { content := f.contentTypesReader() content.Overrides = append(content.Overrides, xlsxOverride{ @@ -174,8 +175,8 @@ func (f *File) setSheet(index int, name string) { f.Sheet[path] = &xlsx } -// setWorkbook update workbook property of XLSX. Maximum 31 characters are -// allowed in sheet title. +// 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{ @@ -204,9 +205,9 @@ func (f *File) setAppXML() { f.saveFileList("docProps/app.xml", []byte(templateDocpropsApp)) } -// replaceRelationshipsBytes; Some tools that read XLSX 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. +// 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. func replaceRelationshipsBytes(content []byte) []byte { oldXmlns := stringToBytes(`xmlns:relationships="http://schemas.openxmlformats.org/officeDocument/2006/relationships" relationships`) newXmlns := stringToBytes("r") @@ -263,7 +264,7 @@ func (f *File) SetActiveSheet(index int) { } // GetActiveSheetIndex provides a function to get active sheet index of the -// XLSX. If not found the active sheet will be return integer 0. +// spreadsheet. If not found the active sheet will be return integer 0. func (f *File) GetActiveSheetIndex() (index int) { var sheetID = f.getActiveSheetID() wb := f.workbookReader() @@ -278,7 +279,7 @@ func (f *File) GetActiveSheetIndex() (index int) { } // getActiveSheetID provides a function to get active sheet index of the -// XLSX. If not found the active sheet will be return integer 0. +// spreadsheet. If not found the active sheet will be return integer 0. func (f *File) getActiveSheetID() int { wb := f.workbookReader() if wb != nil { @@ -313,9 +314,9 @@ func (f *File) SetSheetName(oldName, newName string) { } } -// getSheetNameByID provides a function to get worksheet name of XLSX by given -// worksheet ID. If given sheet ID is invalid, will return an empty -// string. +// getSheetNameByID provides a function to get worksheet name of the +// spreadsheet by given worksheet ID. If given sheet ID is invalid, will +// return an empty string. func (f *File) getSheetNameByID(ID int) string { wb := f.workbookReader() if wb == nil || ID < 1 { @@ -341,9 +342,9 @@ func (f *File) GetSheetName(index int) (name string) { return } -// getSheetID provides a function to get worksheet ID of XLSX by given -// sheet name. If given worksheet name is invalid, will return an integer type -// value -1. +// 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 { var ID = -1 for sheetID, sheet := range f.GetSheetMap() { From 42b1c8148883844cf80b70a3096e6ee67be01f61 Mon Sep 17 00:00:00 2001 From: "Huy Bui (Kevin)" <2992996+huybuidev@users.noreply.github.com> Date: Sat, 11 Jul 2020 01:07:41 +0800 Subject: [PATCH 259/957] Resolve #661 Add Logarithmic scale option support on Y axis (#662) * Resolve #661 Add Logarithmic scale option support on Y axis Example usage: Add the following option into the format string when using AddChart: "y_axis":{"scaling":{"logbase":"10"}} * Change type of LogBase from attrValString to attrVarFloat * Add test case for testing Logarithmic Option in Y axis of charts * Move field `LogBase` in the format string up one level (remove `Scaling`) as suggested the owner Test cases are updated accordingly. --- chart_test.go | 113 ++++++++++++++++++++++++++++++++++++++++++++++++++ drawing.go | 7 ++++ xmlChart.go | 2 + 3 files changed, 122 insertions(+) diff --git a/chart_test.go b/chart_test.go index b35cb98cdd..6ee5f7c6b2 100644 --- a/chart_test.go +++ b/chart_test.go @@ -253,3 +253,116 @@ func TestDeleteChart(t *testing.T) { // Test delete chart on no chart worksheet. assert.NoError(t, NewFile().DeleteChart("Sheet1", "A1")) } + +func TestChartWithLogarithmicBase(t *testing.T) { + // Create test XLSX file with data + xlsx := NewFile() + sheet1 := xlsx.GetSheetName(0) + categories := map[string]float64{ + "A1": 1, + "A2": 2, + "A3": 3, + "A4": 4, + "A5": 5, + "A6": 6, + "A7": 7, + "A8": 8, + "A9": 9, + "A10": 10, + "B1": 0.1, + "B2": 1, + "B3": 2, + "B4": 3, + "B5": 20, + "B6": 30, + "B7": 100, + "B8": 500, + "B9": 700, + "B10": 5000, + } + for cell, v := range categories { + assert.NoError(t, xlsx.SetCellValue(sheet1, cell, v)) + } + + // Add two chart, one without and one with log scaling + assert.NoError(t, xlsx.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, xlsx.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, xlsx.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, xlsx.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, xlsx.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, xlsx.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"}}`)) + + // Export XLSX file for human confirmation + assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestChartWithLogarithmicBase10.xlsx"))) + + // Write the XLSX file to a buffer + var buffer bytes.Buffer + assert.NoError(t, xlsx.Write(&buffer)) + + // Read back the XLSX file from the buffer + newFile, err := OpenReader(&buffer) + assert.NoError(t, err) + + // Check the number of charts + expectedChartsCount := 6 + chartsNum := newFile.countCharts() + if !assert.Equal(t, expectedChartsCount, chartsNum, + "Expected %d charts, actual %d", expectedChartsCount, chartsNum) { + t.FailNow() + } + + chartSpaces := make([]xlsxChartSpace, expectedChartsCount) + type xmlChartContent []byte + xmlCharts := make([]xmlChartContent, expectedChartsCount) + expectedChartsLogBase := []float64{0, 10.5, 0, 2, 0, 1000} + var ok bool + + for i := 0; i < expectedChartsCount; i++ { + chartPath := fmt.Sprintf("xl/charts/chart%d.xml", i+1) + xmlCharts[i], ok = newFile.XLSX[chartPath] + assert.True(t, ok, "Can't open the %s", chartPath) + + err = xml.Unmarshal([]byte(xmlCharts[i]), &chartSpaces[i]) + if !assert.NoError(t, err) { + t.FailNow() + } + + chartLogBasePtr := chartSpaces[i].Chart.PlotArea.ValAx[0].Scaling.LogBase + if expectedChartsLogBase[i] == 0 { + if !assert.Nil(t, chartLogBasePtr, "LogBase is not nil") { + t.FailNow() + } + } else { + if !assert.NotNil(t, chartLogBasePtr, "LogBase is nil") { + t.FailNow() + } + if !assert.Equal(t, expectedChartsLogBase[i], *(chartLogBasePtr.Val), + "Expected log base to %f, actual %f", expectedChartsLogBase[i], *(chartLogBasePtr.Val)) { + t.FailNow() + } + } + } +} diff --git a/drawing.go b/drawing.go index ea75e826c1..3ce1282f3e 100644 --- a/drawing.go +++ b/drawing.go @@ -1000,10 +1000,17 @@ func (f *File) drawPlotAreaValAx(formatSet *formatChart) []*cAxs { if formatSet.YAxis.Maximum == 0 { max = nil } + var logBase *attrValFloat + // Follow OOXML requirements on + // [https://github.com/sc34wg4/OOXMLSchemas/blob/2b074ca2c5df38b18ac118646b329b508b5bdecc/Part1/OfficeOpenXML-XMLSchema-Strict/dml-chart.xsd#L1142-L1147] + if formatSet.YAxis.LogBase >= 2 && formatSet.YAxis.LogBase <= 1000 { + logBase = &attrValFloat{Val: float64Ptr(formatSet.YAxis.LogBase)} + } axs := []*cAxs{ { AxID: &attrValInt{Val: intPtr(753999904)}, Scaling: &cScaling{ + LogBase: logBase, Orientation: &attrValString{Val: stringPtr(orientation[formatSet.YAxis.ReverseOrder])}, Max: max, Min: min, diff --git a/xmlChart.go b/xmlChart.go index 6f800d7220..fae54263d9 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -380,6 +380,7 @@ type cChartLines struct { // cScaling directly maps the scaling element. This element contains // additional axis settings. type cScaling struct { + LogBase *attrValFloat `xml:"logBase"` Orientation *attrValString `xml:"orientation"` Max *attrValFloat `xml:"max"` Min *attrValFloat `xml:"min"` @@ -544,6 +545,7 @@ type formatChartAxis struct { Italic bool `json:"italic"` Underline bool `json:"underline"` } `json:"num_font"` + LogBase float64 `json:"logbase"` NameLayout formatLayout `json:"name_layout"` } From 0aa15106947965bdae9daae7571a4a3f569bf32d Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 11 Jul 2020 02:31:02 +0800 Subject: [PATCH 260/957] update docs and improve compatibility --- drawing.go | 2 -- lib_test.go | 3 +++ picture.go | 11 +++++---- picture_test.go | 5 ++++ rows_test.go | 2 ++ stream.go | 2 +- xmlStyles.go | 21 ++++++---------- xmlTheme.go | 6 ++--- xmlWorkbook.go | 8 +++--- xmlWorksheet.go | 66 +++++++++++++++++++++---------------------------- 10 files changed, 59 insertions(+), 67 deletions(-) diff --git a/drawing.go b/drawing.go index 3ce1282f3e..5e5bba9f8a 100644 --- a/drawing.go +++ b/drawing.go @@ -1001,8 +1001,6 @@ func (f *File) drawPlotAreaValAx(formatSet *formatChart) []*cAxs { max = nil } var logBase *attrValFloat - // Follow OOXML requirements on - // [https://github.com/sc34wg4/OOXMLSchemas/blob/2b074ca2c5df38b18ac118646b329b508b5bdecc/Part1/OfficeOpenXML-XMLSchema-Strict/dml-chart.xsd#L1142-L1147] if formatSet.YAxis.LogBase >= 2 && formatSet.YAxis.LogBase <= 1000 { logBase = &attrValFloat{Val: float64Ptr(formatSet.YAxis.LogBase)} } diff --git a/lib_test.go b/lib_test.go index 229412ca14..e4ccdcc4ee 100644 --- a/lib_test.go +++ b/lib_test.go @@ -95,6 +95,9 @@ func TestColumnNumberToName_Error(t *testing.T) { if assert.Error(t, err) { assert.Equal(t, "", out) } + + _, err = ColumnNumberToName(TotalColumns + 1) + assert.EqualError(t, err, "column number exceeds maximum limit") } func TestSplitCellName_OK(t *testing.T) { diff --git a/picture.go b/picture.go index 0e9e3bb6a7..c7f6e27845 100644 --- a/picture.go +++ b/picture.go @@ -549,11 +549,12 @@ func (f *File) getPictureFromWsDr(row, col int, drawingRelationships string, wsD for _, anchor = range wsDr.TwoCellAnchor { if anchor.From != nil && anchor.Pic != nil { if anchor.From.Col == col && anchor.From.Row == row { - drawRel = f.getDrawingRelationships(drawingRelationships, - anchor.Pic.BlipFill.Blip.Embed) - if _, ok = supportImageTypes[filepath.Ext(drawRel.Target)]; ok { - ret, buf = filepath.Base(drawRel.Target), f.XLSX[strings.Replace(drawRel.Target, "..", "xl", -1)] - return + if drawRel = f.getDrawingRelationships(drawingRelationships, + anchor.Pic.BlipFill.Blip.Embed); drawRel != nil { + if _, ok = supportImageTypes[filepath.Ext(drawRel.Target)]; ok { + ret, buf = filepath.Base(drawRel.Target), f.XLSX[strings.Replace(drawRel.Target, "..", "xl", -1)] + return + } } } } diff --git a/picture_test.go b/picture_test.go index 015d8540b7..f6f716efde 100644 --- a/picture_test.go +++ b/picture_test.go @@ -152,6 +152,11 @@ func TestGetPicture(t *testing.T) { assert.NoError(t, err) assert.Empty(t, file) assert.Empty(t, raw) + f, err = prepareTestBook1() + assert.NoError(t, err) + f.XLSX["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") } func TestAddDrawingPicture(t *testing.T) { diff --git a/rows_test.go b/rows_test.go index fd7196d05c..14537eb145 100644 --- a/rows_test.go +++ b/rows_test.go @@ -169,6 +169,8 @@ func TestSharedStringsReader(t *testing.T) { f := NewFile() f.XLSX["xl/sharedStrings.xml"] = MacintoshCyrillicCharset f.sharedStringsReader() + si := xlsxSI{} + assert.EqualValues(t, "", si.String()) } func TestRowVisibility(t *testing.T) { diff --git a/stream.go b/stream.go index 81dea1ecb4..bdc0d266c5 100644 --- a/stream.go +++ b/stream.go @@ -72,7 +72,7 @@ type StreamWriter struct { // func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) { sheetID := f.getSheetID(sheet) - if sheetID == 0 { + if sheetID == -1 { return nil, fmt.Errorf("sheet %s is not exist", sheet) } sw := &StreamWriter{ diff --git a/xmlStyles.go b/xmlStyles.go index 07413dd996..2884800097 100644 --- a/xmlStyles.go +++ b/xmlStyles.go @@ -13,9 +13,7 @@ package excelize import "encoding/xml" -// xlsxStyleSheet directly maps the stylesheet element in the namespace -// http://schemas.openxmlformats.org/spreadsheetml/2006/main - currently I have -// not checked it for completeness - it does as much as I need. +// xlsxStyleSheet is the root element of the Styles part. type xlsxStyleSheet struct { XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main styleSheet"` NumFmts *xlsxNumFmts `xml:"numFmts,omitempty"` @@ -55,9 +53,7 @@ type xlsxProtection struct { Locked bool `xml:"locked,attr"` } -// xlsxLine directly maps the line style element in the namespace -// http://schemas.openxmlformats.org/spreadsheetml/2006/main - currently I have -// not checked it for completeness - it does as much as I need. +// xlsxLine expresses a single set of cell border. type xlsxLine struct { Style string `xml:"style,attr,omitempty"` Color *xlsxColor `xml:"color,omitempty"` @@ -119,13 +115,10 @@ type xlsxFill struct { GradientFill *xlsxGradientFill `xml:"gradientFill,omitempty"` } -// xlsxPatternFill directly maps the patternFill element in the namespace -// http://schemas.openxmlformats.org/spreadsheetml/2006/main - currently I have -// not checked it for completeness - it does as much as I need. This element is -// used to specify cell fill information for pattern and solid color cell fills. -// For solid cell fills (no pattern), fgColor is used. For cell fills with -// patterns specified, then the cell fill color is specified by the bgColor -// element. +// xlsxPatternFill is used to specify cell fill information for pattern and +// solid color cell fills. For solid cell fills (no pattern), fgColor is used. +// For cell fills with patterns specified, then the cell fill color is +// specified by the bgColor element. type xlsxPatternFill struct { PatternType string `xml:"patternType,attr,omitempty"` FgColor xlsxColor `xml:"fgColor,omitempty"` @@ -303,7 +296,7 @@ type xlsxNumFmts struct { // format properties which indicate how to format and render the numeric value // of a cell. type xlsxNumFmt struct { - NumFmtID int `xml:"numFmtId,attr,omitempty"` + NumFmtID int `xml:"numFmtId,attr"` FormatCode string `xml:"formatCode,attr,omitempty"` } diff --git a/xmlTheme.go b/xmlTheme.go index 2f181e267c..e3588dc5b6 100644 --- a/xmlTheme.go +++ b/xmlTheme.go @@ -123,9 +123,9 @@ type xlsxBgFillStyleLst struct { BgFillStyleLst string `xml:",innerxml"` } -// xlsxClrScheme maps to children of the clrScheme element in the namespace -// http://schemas.openxmlformats.org/drawingml/2006/main - currently I have -// not checked it for completeness - it does as much as I need. +// 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"` diff --git a/xmlWorkbook.go b/xmlWorkbook.go index 733eb57c7b..89cacd9253 100644 --- a/xmlWorkbook.go +++ b/xmlWorkbook.go @@ -27,9 +27,9 @@ type xlsxRelationship struct { TargetMode string `xml:",attr,omitempty"` } -// xlsxWorkbook directly maps the workbook element from the namespace -// http://schemas.openxmlformats.org/spreadsheetml/2006/main - currently I have -// not checked it for completeness - it does as much as I need. +// xlsxWorkbook contains elements and attributes that encompass the data +// content of the workbook. The workbook's child elements each have their own +// subclause references. type xlsxWorkbook struct { XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main workbook"` FileVersion *xlsxFileVersion `xml:"fileVersion"` @@ -153,7 +153,7 @@ type xlsxSheets struct { type xlsxSheet struct { Name string `xml:"name,attr,omitempty"` SheetID int `xml:"sheetId,attr,omitempty"` - ID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr,omitempty"` + ID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr"` State string `xml:"state,attr,omitempty"` } diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 03b1a713c8..7cd73c4185 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -165,25 +165,20 @@ type xlsxSheetFormatPr struct { OutlineLevelCol uint8 `xml:"outlineLevelCol,attr,omitempty"` } -// xlsxSheetViews directly maps the sheetViews element in the namespace -// http://schemas.openxmlformats.org/spreadsheetml/2006/main - Worksheet views -// collection. +// xlsxSheetViews represents worksheet views collection. type xlsxSheetViews struct { XMLName xml.Name `xml:"sheetViews"` SheetView []xlsxSheetView `xml:"sheetView"` } -// xlsxSheetView directly maps the sheetView element in the namespace -// http://schemas.openxmlformats.org/spreadsheetml/2006/main - currently I have -// not checked it for completeness - it does as much as I need. A single sheet -// view definition. When more than one sheet view is defined in the file, it -// means that when opening the workbook, each sheet view corresponds to a -// separate window within the spreadsheet application, where each window is -// showing the particular sheet containing the same workbookViewId value, the -// last sheetView definition is loaded, and the others are discarded. When -// multiple windows are viewing the same sheet, multiple sheetView elements -// (with corresponding workbookView entries) are saved. -// See https://docs.microsoft.com/en-us/dotnet/api/documentformat.openxml.spreadsheet.sheetview +// xlsxSheetView represents a single sheet view definition. When more than one +// sheet view is defined in the file, it means that when opening the workbook, +// each sheet view corresponds to a separate window within the spreadsheet +// application, where each window is showing the particular sheet containing +// the same workbookViewId value, the last sheetView definition is loaded, and +// the others are discarded. When multiple windows are viewing the same sheet, +// multiple sheetView elements (with corresponding workbookView entries) are +// saved. type xlsxSheetView struct { WindowProtection bool `xml:"windowProtection,attr,omitempty"` ShowFormulas bool `xml:"showFormulas,attr,omitempty"` @@ -245,31 +240,27 @@ type xlsxSheetPr struct { PageSetUpPr *xlsxPageSetUpPr `xml:"pageSetUpPr,omitempty"` } -// xlsxOutlinePr maps to the outlinePr element -// SummaryBelow allows you to adjust the direction of grouper controls +// xlsxOutlinePr maps to the outlinePr element. SummaryBelow allows you to +// adjust the direction of grouper controls. type xlsxOutlinePr struct { SummaryBelow bool `xml:"summaryBelow,attr"` } -// xlsxPageSetUpPr directly maps the pageSetupPr element in the namespace -// http://schemas.openxmlformats.org/spreadsheetml/2006/main - Page setup -// properties of the worksheet. +// xlsxPageSetUpPr expresses page setup properties of the worksheet. type xlsxPageSetUpPr struct { AutoPageBreaks bool `xml:"autoPageBreaks,attr,omitempty"` - FitToPage bool `xml:"fitToPage,attr,omitempty"` // Flag indicating whether the Fit to Page print option is enabled. + FitToPage bool `xml:"fitToPage,attr,omitempty"` } -// xlsxTabColor directly maps the tabColor element in the namespace currently I -// have not checked it for completeness - it does as much as I need. +// xlsxTabColor represents background color of the sheet tab. type xlsxTabColor struct { RGB string `xml:"rgb,attr,omitempty"` Theme int `xml:"theme,attr,omitempty"` Tint float64 `xml:"tint,attr,omitempty"` } -// xlsxCols directly maps the cols element in the namespace -// http://schemas.openxmlformats.org/spreadsheetml/2006/main - currently I have -// not checked it for completeness - it does as much as I need. +// xlsxCols defines column width and column formatting for one or more columns +// of the worksheet. type xlsxCols struct { XMLName xml.Name `xml:"cols"` Col []xlsxCol `xml:"col"` @@ -293,18 +284,18 @@ type xlsxCol struct { // xlsxDimension directly maps the dimension element in the namespace // http://schemas.openxmlformats.org/spreadsheetml/2006/main - This element // specifies the used range of the worksheet. It specifies the row and column -// bounds of used cells in the worksheet. This is optional and is not required. -// Used cells include cells with formulas, text content, and cell formatting. -// When an entire column is formatted, only the first cell in that column is -// considered used. +// bounds of used cells in the worksheet. This is optional and is not +// required. Used cells include cells with formulas, text content, and cell +// formatting. When an entire column is formatted, only the first cell in that +// column is considered used. type xlsxDimension struct { XMLName xml.Name `xml:"dimension"` Ref string `xml:"ref,attr"` } -// xlsxSheetData directly maps the sheetData element in the namespace -// http://schemas.openxmlformats.org/spreadsheetml/2006/main - currently I have -// not checked it for completeness - it does as much as I need. +// xlsxSheetData collection represents the cell table itself. This collection +// expresses information about each cell, grouped together by rows in the +// worksheet. type xlsxSheetData struct { XMLName xml.Name `xml:"sheetData"` Row []xlsxRow `xml:"row"` @@ -440,9 +431,9 @@ type DataValidation struct { Formula2 string `xml:",innerxml"` } -// xlsxC directly maps the c element in the namespace -// http://schemas.openxmlformats.org/spreadsheetml/2006/main - currently I have -// not checked it for completeness - it does as much as I need. +// xlsxC collection represents a cell in the worksheet. Information about the +// cell's location (reference), value, data type, formatting, and formula is +// expressed here. // // This simple type is restricted to the values listed in the following table: // @@ -472,9 +463,8 @@ func (c *xlsxC) hasValue() bool { return c.S != 0 || c.V != "" || c.F != nil || c.T != "" } -// xlsxF directly maps the f element in the namespace -// http://schemas.openxmlformats.org/spreadsheetml/2006/main - currently I have -// not checked it for completeness - it does as much as I need. +// xlsxF represents a formula for the cell. The formula expression is +// contained in the character node of this element. type xlsxF struct { Content string `xml:",chardata"` T string `xml:"t,attr,omitempty"` // Formula type From 5993a07422b6ace5230f562551f014180a83515a Mon Sep 17 00:00:00 2001 From: jaby Date: Tue, 14 Jul 2020 17:05:43 +0200 Subject: [PATCH 261/957] Fix issue 665 (#666) --- calc.go | 17 +++++++++++++++++ calc_test.go | 26 ++++++++++++++++++++++++++ sheet.go | 2 +- 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/calc.go b/calc.go index 86f7cffe6e..f86b12c4d4 100644 --- a/calc.go +++ b/calc.go @@ -453,11 +453,28 @@ func isOperatorPrefixToken(token efp.Token) bool { return false } +func (f *File) getDefinedNameRefTo(definedNameName string, currentSheet string) (refTo string) { + for _, definedName := range f.GetDefinedName() { + if definedName.Name == definedNameName { + refTo = definedName.RefersTo + // worksheet scope takes precedence over scope workbook when both definedNames exist + if definedName.Scope == currentSheet { + break + } + } + } + return refTo +} + // 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 { // 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) if err != nil { return errors.New(formulaErrorNAME) diff --git a/calc_test.go b/calc_test.go index 283b9c28ea..4298aa7a39 100644 --- a/calc_test.go +++ b/calc_test.go @@ -790,4 +790,30 @@ func TestCalcCellValue(t *testing.T) { _, err = f.CalcCellValue("Sheet1", "A1") assert.EqualError(t, err, "not support UNSUPPORT function") assert.NoError(t, f.SaveAs(filepath.Join("test", "TestCalcCellValue.xlsx"))) + +} + +func TestCalcCellValueWithDefinedName(t *testing.T) { + cellData := [][]interface{}{ + {"A1 value", "B1 value", nil}, + } + prepareData := func() *File { + f := NewFile() + for r, row := range cellData { + for c, value := range row { + cell, _ := CoordinatesToCellName(c+1, r+1) + assert.NoError(t, f.SetCellValue("Sheet1", cell, value)) + } + } + assert.NoError(t, f.SetDefinedName(&DefinedName{Name: "defined_name1", RefersTo: "Sheet1!A1", Scope: "Workbook"})) + assert.NoError(t, f.SetDefinedName(&DefinedName{Name: "defined_name1", RefersTo: "Sheet1!B1", Scope: "Sheet1"})) + + return f + } + f := prepareData() + assert.NoError(t, f.SetCellFormula("Sheet1", "C1", "=defined_name1")) + result, err := f.CalcCellValue("Sheet1", "C1") + assert.NoError(t, err) + // DefinedName with scope WorkSheet takes precedence over DefinedName with scope Workbook, so we should get B1 value + assert.Equal(t, "B1 value", result, "=defined_name1") } diff --git a/sheet.go b/sheet.go index bbe84c2df3..20bf7c7b58 100644 --- a/sheet.go +++ b/sheet.go @@ -1421,7 +1421,7 @@ func (f *File) GetDefinedName() []DefinedName { RefersTo: dn.Data, Scope: "Workbook", } - if dn.LocalSheetID != nil { + if dn.LocalSheetID != nil && *dn.LocalSheetID >= 0 { definedName.Scope = f.getSheetNameByID(*dn.LocalSheetID + 1) } definedNames = append(definedNames, definedName) From 820a314cfbcaa4d32401b0b6c67bf65f064483ec Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 15 Jul 2020 23:32:00 +0800 Subject: [PATCH 262/957] Resolve #667, support shared string table without unique count --- rows.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rows.go b/rows.go index 87576c33bb..535b010593 100644 --- a/rows.go +++ b/rows.go @@ -286,6 +286,9 @@ func (f *File) sharedStringsReader() *xlsxSST { Decode(&sharedStrings); err != nil && err != io.EOF { log.Printf("xml decode error: %s", err) } + if sharedStrings.UniqueCount == 0 { + sharedStrings.UniqueCount = sharedStrings.Count + } f.SharedStrings = &sharedStrings for i := range sharedStrings.SI { if sharedStrings.SI[i].T != nil { From c922c32fb7571d3d40d3244e5635142bc390a3db Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 18 Jul 2020 15:15:16 +0800 Subject: [PATCH 263/957] support parse and generate XML element namespace dynamic, fix #651 --- cell.go | 1 + chart.go | 4 +- col.go | 2 +- comment.go | 1 + drawing.go | 7 ++-- excelize.go | 14 +++---- lib.go | 101 +++++++++++++++++++++++++++++++++++++++++++++++++- picture.go | 5 ++- rows.go | 15 +++++--- shape.go | 1 + sheet.go | 21 ++++++++--- sparkline.go | 2 +- stream.go | 2 +- styles.go | 4 +- table.go | 26 ++++++++----- xmlDrawing.go | 10 +++-- 16 files changed, 171 insertions(+), 45 deletions(-) diff --git a/cell.go b/cell.go index 0163c3bcb0..3293d19fe4 100644 --- a/cell.go +++ b/cell.go @@ -490,6 +490,7 @@ func (f *File) SetCellHyperLink(sheet, axis, link, linkType string) error { 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": linkData = xlsxHyperlink{ Ref: axis, diff --git a/chart.go b/chart.go index b584c18253..ae31f714ee 100644 --- a/chart.go +++ b/chart.go @@ -755,6 +755,7 @@ func (f *File) AddChart(sheet, cell, format string, combo ...string) error { f.addChart(formatSet, comboCharts) f.addContentTypePart(chartID, "chart") f.addContentTypePart(drawingID, "drawings") + f.addSheetNameSpace(sheet, SourceRelationship) return err } @@ -804,7 +805,8 @@ func (f *File) AddChartSheet(sheet, format string, combo ...string) error { // Update xl/workbook.xml f.setWorkbook(sheet, sheetID, rID) chartsheet, _ := xml.Marshal(cs) - f.saveFileList(path, replaceRelationshipsBytes(replaceRelationshipsNameSpaceBytes(chartsheet))) + f.addSheetNameSpace(sheet, NameSpaceSpreadSheet) + f.saveFileList(path, replaceRelationshipsBytes(f.replaceNameSpaceBytes(path, chartsheet))) return err } diff --git a/col.go b/col.go index 472106f3e4..5a8299ea9c 100644 --- a/col.go +++ b/col.go @@ -159,7 +159,7 @@ func (f *File) Cols(sheet string) (*Cols, error) { } if f.Sheet[name] != nil { output, _ := xml.Marshal(f.Sheet[name]) - f.saveFileList(name, replaceRelationshipsNameSpaceBytes(output)) + f.saveFileList(name, f.replaceNameSpaceBytes(name, output)) } var ( inElement string diff --git a/comment.go b/comment.go index 28140bfe9b..8414b401d2 100644 --- a/comment.go +++ b/comment.go @@ -109,6 +109,7 @@ func (f *File) AddComment(sheet, cell, format string) error { sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(f.sheetMap[trimSheetName(sheet)], "xl/worksheets/") + ".rels" rID := f.addRels(sheetRels, SourceRelationshipDrawingVML, sheetRelationshipsDrawingVML, "") f.addRels(sheetRels, SourceRelationshipComments, sheetRelationshipsComments, "") + f.addSheetNameSpace(sheet, SourceRelationship) f.addSheetLegacyDrawing(sheet, rID) } commentsXML := "xl/comments" + strconv.Itoa(commentID) + ".xml" diff --git a/drawing.go b/drawing.go index 5e5bba9f8a..ced747d708 100644 --- a/drawing.go +++ b/drawing.go @@ -47,6 +47,7 @@ func (f *File) prepareChartSheetDrawing(xlsx *xlsxChartsheet, drawingID int, she // Only allow one chart in a chartsheet. sheetRels := "xl/chartsheets/_rels/" + strings.TrimPrefix(f.sheetMap[trimSheetName(sheet)], "xl/chartsheets/") + ".rels" rID := f.addRels(sheetRels, SourceRelationshipDrawingML, sheetRelationshipsDrawingXML, "") + f.addSheetNameSpace(sheet, SourceRelationship) xlsx.Drawing = &xlsxDrawing{ RID: "rId" + strconv.Itoa(rID), } @@ -60,7 +61,7 @@ func (f *File) addChart(formatSet *formatChart, comboCharts []*formatChart) { xlsxChartSpace := xlsxChartSpace{ XMLNSc: NameSpaceDrawingMLChart, XMLNSa: NameSpaceDrawingML, - XMLNSr: SourceRelationship, + XMLNSr: SourceRelationship.Value, XMLNSc16r2: SourceRelationshipChart201506, Date1904: &attrValBool{Val: boolPtr(false)}, Lang: &attrValString{Val: stringPtr("en-US")}, @@ -1212,7 +1213,7 @@ func (f *File) addDrawingChart(sheet, drawingXML, cell string, width, height, rI URI: NameSpaceDrawingMLChart, Chart: &xlsxChart{ C: NameSpaceDrawingMLChart, - R: SourceRelationship, + R: SourceRelationship.Value, RID: "rId" + strconv.Itoa(rID), }, }, @@ -1252,7 +1253,7 @@ func (f *File) addSheetDrawingChart(drawingXML string, rID int, formatSet *forma URI: NameSpaceDrawingMLChart, Chart: &xlsxChart{ C: NameSpaceDrawingMLChart, - R: SourceRelationship, + R: SourceRelationship.Value, RID: "rId" + strconv.Itoa(rID), }, }, diff --git a/excelize.go b/excelize.go index bac569afce..5cc88e961f 100644 --- a/excelize.go +++ b/excelize.go @@ -30,6 +30,7 @@ import ( // File define a populated spreadsheet file struct. type File struct { + xmlAttr map[string][]xml.Attr checked map[string]bool sheetMap map[string]string CalcChain *xlsxCalcChain @@ -72,6 +73,7 @@ func OpenFile(filename string) (*File, error) { // newFile is object builder func newFile() *File { return &File{ + xmlAttr: make(map[string][]xml.Attr), checked: make(map[string]bool), sheetMap: make(map[string]string), Comments: make(map[string]*xlsxComments), @@ -166,6 +168,10 @@ func (f *File) workSheetReader(sheet string) (xlsx *xlsxWorksheet, err error) { return } xlsx = new(xlsxWorksheet) + if _, ok := f.xmlAttr[name]; !ok { + d := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(name)))) + f.xmlAttr[name] = append(f.xmlAttr[name], getRootElement(d)...) + } if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(name)))). Decode(xlsx); err != nil && err != io.EOF { err = fmt.Errorf("xml decode error: %s", err) @@ -254,14 +260,6 @@ func (f *File) addRels(relPath, relType, target, targetMode string) int { return rID } -// replaceRelationshipsNameSpaceBytes provides a function to replace -// XML tags to self-closing for compatible Microsoft Office Excel 2007. -func replaceRelationshipsNameSpaceBytes(contentMarshal []byte) []byte { - var oldXmlns = stringToBytes(` xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">`) - var newXmlns = []byte(templateNamespaceIDMap) - return bytesReplace(contentMarshal, oldXmlns, newXmlns, -1) -} - // 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 // cell have a linked value. Reference diff --git a/lib.go b/lib.go index 5d18064c3e..6dcd97ed61 100644 --- a/lib.go +++ b/lib.go @@ -15,6 +15,7 @@ import ( "archive/zip" "bytes" "container/list" + "encoding/xml" "fmt" "io" "strconv" @@ -243,11 +244,11 @@ func parseFormatSet(formatSet string) []byte { // Transitional namespaces. func namespaceStrictToTransitional(content []byte) []byte { var namespaceTranslationDic = map[string]string{ - StrictSourceRelationship: SourceRelationship, + StrictSourceRelationship: SourceRelationship.Value, StrictSourceRelationshipChart: SourceRelationshipChart, StrictSourceRelationshipComments: SourceRelationshipComments, StrictSourceRelationshipImage: SourceRelationshipImage, - StrictNameSpaceSpreadSheet: NameSpaceSpreadSheet, + StrictNameSpaceSpreadSheet: NameSpaceSpreadSheet.Value, } for s, n := range namespaceTranslationDic { content = bytesReplace(content, stringToBytes(s), stringToBytes(n), -1) @@ -319,6 +320,102 @@ func genSheetPasswd(plaintext string) string { return strings.ToUpper(strconv.FormatInt(password, 16)) } +// getRootElement extract root element attributes by given XML decoder. +func getRootElement(d *xml.Decoder) []xml.Attr { + tokenIdx := 0 + for { + token, _ := d.Token() + if token == nil { + break + } + switch startElement := token.(type) { + case xml.StartElement: + tokenIdx++ + if tokenIdx == 1 { + return startElement.Attr + } + } + } + return nil +} + +// genXMLNamespace generate serialized XML attributes with a multi namespace +// by given element attributes. +func genXMLNamespace(attr []xml.Attr) string { + var rootElement string + for _, v := range attr { + if lastSpace := getXMLNamespace(v.Name.Space, attr); lastSpace != "" { + rootElement += fmt.Sprintf("%s:%s=\"%s\" ", lastSpace, v.Name.Local, v.Value) + continue + } + rootElement += fmt.Sprintf("%s=\"%s\" ", v.Name.Local, v.Value) + } + return strings.TrimSpace(rootElement) + ">" +} + +// getXMLNamespace extract XML namespace from specified element name and attributes. +func getXMLNamespace(space string, attr []xml.Attr) string { + for _, attribute := range attr { + if attribute.Value == space { + return attribute.Name.Local + } + } + return space +} + +// 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 { + var oldXmlns = stringToBytes(`xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">`) + var newXmlns = []byte(templateNamespaceIDMap) + if attr, ok := f.xmlAttr[path]; ok { + newXmlns = []byte(genXMLNamespace(attr)) + } + return bytesReplace(contentMarshal, oldXmlns, newXmlns, -1) +} + +// addNameSpaces provides a function to add a XML attribute by the given +// component part path. +func (f *File) addNameSpaces(path string, ns xml.Attr) { + exist := false + mc := false + ignore := false + if attr, ok := f.xmlAttr[path]; ok { + for _, attribute := range attr { + if attribute.Name.Local == ns.Name.Local && attribute.Name.Space == ns.Name.Space { + exist = true + } + if attribute.Name.Local == "Ignorable" && attribute.Name.Space == "mc" { + ignore = true + } + if attribute.Name.Local == "mc" && attribute.Name.Space == "xmlns" { + mc = true + } + } + } + if !exist { + f.xmlAttr[path] = append(f.xmlAttr[path], ns) + if !mc { + f.xmlAttr[path] = append(f.xmlAttr[path], xml.Attr{ + Name: xml.Name{Local: "mc", Space: "xmlns"}, + Value: SourceRelationshipCompatibility, + }) + } + if !ignore { + f.xmlAttr[path] = append(f.xmlAttr[path], xml.Attr{ + Name: xml.Name{Local: "Ignorable", Space: "mc"}, + Value: ns.Name.Local, + }) + } + } +} + +// addSheetNameSpace add XML attribute for worksheet. +func (f *File) addSheetNameSpace(sheet string, ns xml.Attr) { + name, _ := f.sheetMap[trimSheetName(sheet)] + f.addNameSpaces(name, ns) +} + // Stack defined an abstract data type that serves as a collection of elements. type Stack struct { list *list.List diff --git a/picture.go b/picture.go index c7f6e27845..468cccdd81 100644 --- a/picture.go +++ b/picture.go @@ -168,6 +168,7 @@ func (f *File) AddPictureFromBytes(sheet, cell, format, name, extension string, return err } f.addContentTypePart(drawingID, "drawings") + f.addSheetNameSpace(sheet, SourceRelationship) return err } @@ -279,11 +280,11 @@ func (f *File) addDrawingPicture(sheet, drawingXML, cell, file string, width, he pic.NvPicPr.CNvPr.Name = "Picture " + strconv.Itoa(cNvPrID) if hyperlinkRID != 0 { pic.NvPicPr.CNvPr.HlinkClick = &xlsxHlinkClick{ - R: SourceRelationship, + R: SourceRelationship.Value, RID: "rId" + strconv.Itoa(hyperlinkRID), } } - pic.BlipFill.Blip.R = SourceRelationship + pic.BlipFill.Blip.R = SourceRelationship.Value pic.BlipFill.Blip.Embed = "rId" + strconv.Itoa(rID) pic.SpPr.PrstGeom.Prst = "rect" diff --git a/rows.go b/rows.go index 535b010593..320ba2fdf2 100644 --- a/rows.go +++ b/rows.go @@ -121,11 +121,8 @@ func (rows *Rows) Columns() ([]string, error) { } } blank := cellCol - len(columns) - for i := 1; i < blank; i++ { - columns = append(columns, "") - } val, _ := colCell.getValueFrom(rows.f, d) - columns = append(columns, val) + columns = append(appendSpace(blank, columns), val) } case xml.EndElement: inElement = startElement.Name.Local @@ -137,6 +134,14 @@ func (rows *Rows) Columns() ([]string, error) { return columns, err } +// 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++ { + s = append(s, "") + } + return s +} + // ErrSheetNotExist defines an error of sheet is not exist type ErrSheetNotExist struct { SheetName string @@ -173,7 +178,7 @@ func (f *File) Rows(sheet string) (*Rows, error) { if f.Sheet[name] != nil { // flush data output, _ := xml.Marshal(f.Sheet[name]) - f.saveFileList(name, replaceRelationshipsNameSpaceBytes(output)) + f.saveFileList(name, f.replaceNameSpaceBytes(name, output)) } var ( err error diff --git a/shape.go b/shape.go index 316061668e..0a5164b4a1 100644 --- a/shape.go +++ b/shape.go @@ -280,6 +280,7 @@ func (f *File) AddShape(sheet, cell, format string) error { sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(f.sheetMap[trimSheetName(sheet)], "xl/worksheets/") + ".rels" rID := f.addRels(sheetRels, SourceRelationshipDrawingML, sheetRelationshipsDrawingXML, "") f.addSheetDrawing(sheet, rID) + f.addSheetNameSpace(sheet, SourceRelationship) } err = f.addDrawingShape(sheet, drawingXML, cell, formatSet) if err != nil { diff --git a/sheet.go b/sheet.go index 20bf7c7b58..31a36eba32 100644 --- a/sheet.go +++ b/sheet.go @@ -92,15 +92,18 @@ func (f *File) contentTypesWriter() { // structure after deserialization. func (f *File) workbookReader() *xlsxWorkbook { var err error - if f.WorkBook == nil { f.WorkBook = new(xlsxWorkbook) + if _, ok := f.xmlAttr["xl/workbook.xml"]; !ok { + d := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML("xl/workbook.xml")))) + f.xmlAttr["xl/workbook.xml"] = append(f.xmlAttr["xl/workbook.xml"], getRootElement(d)...) + f.addNameSpaces("xl/workbook.xml", SourceRelationship) + } if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML("xl/workbook.xml")))). Decode(f.WorkBook); err != nil && err != io.EOF { log.Printf("xml decode error: %s", err) } } - return f.WorkBook } @@ -109,7 +112,7 @@ func (f *File) workbookReader() *xlsxWorkbook { func (f *File) workBookWriter() { if f.WorkBook != nil { output, _ := xml.Marshal(f.WorkBook) - f.saveFileList("xl/workbook.xml", replaceRelationshipsBytes(replaceRelationshipsNameSpaceBytes(output))) + f.saveFileList("xl/workbook.xml", replaceRelationshipsBytes(f.replaceNameSpaceBytes("xl/workbook.xml", output))) } } @@ -122,7 +125,7 @@ func (f *File) workSheetWriter() { f.Sheet[p].SheetData.Row[k].C = trimCell(v.C) } output, _ := xml.Marshal(sheet) - f.saveFileList(p, replaceRelationshipsBytes(replaceRelationshipsNameSpaceBytes(output))) + f.saveFileList(p, replaceRelationshipsBytes(f.replaceNameSpaceBytes(p, output))) ok := f.checked[p] if ok { delete(f.Sheet, p) @@ -173,6 +176,7 @@ func (f *File) setSheet(index int, name string) { path := "xl/worksheets/sheet" + strconv.Itoa(index) + ".xml" f.sheetMap[trimSheetName(name)] = path f.Sheet[path] = &xlsx + f.xmlAttr[path] = append(f.xmlAttr[path], NameSpaceSpreadSheet) } // setWorkbook update workbook property of the spreadsheet. Maximum 31 @@ -193,7 +197,7 @@ func (f *File) relsWriter() { if rel != nil { output, _ := xml.Marshal(rel) if strings.HasPrefix(path, "xl/worksheets/sheet/rels/sheet") { - output = replaceRelationshipsNameSpaceBytes(output) + output = f.replaceNameSpaceBytes(path, output) } f.saveFileList(path, replaceRelationshipsBytes(output)) } @@ -440,6 +444,7 @@ func (f *File) SetSheetBackground(sheet, picture string) error { sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(f.sheetMap[trimSheetName(sheet)], "xl/worksheets/") + ".rels" rID := f.addRels(sheetRels, SourceRelationshipImage, strings.Replace(name, "xl", "..", 1), "") f.addSheetPicture(sheet, rID) + f.addSheetNameSpace(sheet, SourceRelationship) f.setContentTypePartImageExtensions() return err } @@ -479,6 +484,7 @@ func (f *File) DeleteSheet(name string) { delete(f.XLSX, rels) delete(f.Relationships, rels) delete(f.Sheet, sheetXML) + delete(f.xmlAttr, sheetXML) f.SheetCount-- } } @@ -557,6 +563,9 @@ func (f *File) copySheet(from, to int) error { if ok { f.XLSX[toRels] = f.XLSX[fromRels] } + fromSheetXMLPath, _ := f.sheetMap[trimSheetName(fromSheet)] + fromSheetAttr, _ := f.xmlAttr[fromSheetXMLPath] + f.xmlAttr[path] = fromSheetAttr return err } @@ -779,7 +788,7 @@ func (f *File) SearchSheet(sheet, value string, reg ...bool) ([]string, error) { if f.Sheet[name] != nil { // flush data output, _ := xml.Marshal(f.Sheet[name]) - f.saveFileList(name, replaceRelationshipsNameSpaceBytes(output)) + f.saveFileList(name, f.replaceNameSpaceBytes(name, output)) } return f.searchSheet(name, value, regSearch) } diff --git a/sparkline.go b/sparkline.go index 9682db04e6..4004878e4e 100644 --- a/sparkline.go +++ b/sparkline.go @@ -455,7 +455,7 @@ func (f *File) AddSparkline(sheet string, opt *SparklineOption) (err error) { } ws.ExtLst.Ext = string(extBytes) } - + f.addSheetNameSpace(sheet, NameSpaceSpreadSheetX14) return } diff --git a/stream.go b/stream.go index bdc0d266c5..ec1e65b769 100644 --- a/stream.go +++ b/stream.go @@ -151,7 +151,7 @@ func (sw *StreamWriter) AddTable(hcell, vcell, format string) error { } table := xlsxTable{ - XMLNS: NameSpaceSpreadSheet, + XMLNS: NameSpaceSpreadSheet.Value, ID: tableID, Name: name, DisplayName: name, diff --git a/styles.go b/styles.go index 2ae1cd8f3e..d7b1460ec9 100644 --- a/styles.go +++ b/styles.go @@ -1022,7 +1022,7 @@ func (f *File) stylesReader() *xlsxStyleSheet { func (f *File) styleSheetWriter() { if f.Styles != nil { output, _ := xml.Marshal(f.Styles) - f.saveFileList("xl/styles.xml", replaceRelationshipsNameSpaceBytes(output)) + f.saveFileList("xl/styles.xml", f.replaceNameSpaceBytes("xl/styles.xml", output)) } } @@ -1031,7 +1031,7 @@ func (f *File) styleSheetWriter() { func (f *File) sharedStringsWriter() { if f.SharedStrings != nil { output, _ := xml.Marshal(f.SharedStrings) - f.saveFileList("xl/sharedStrings.xml", replaceRelationshipsNameSpaceBytes(output)) + f.saveFileList("xl/sharedStrings.xml", f.replaceNameSpaceBytes("xl/sharedStrings.xml", output)) } } diff --git a/table.go b/table.go index d59322ca6a..e26bbe2d5c 100644 --- a/table.go +++ b/table.go @@ -83,9 +83,11 @@ func (f *File) AddTable(sheet, hcell, vcell, format string) error { // 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, "") - f.addSheetTable(sheet, rID) - err = f.addTable(sheet, tableXML, hcol, hrow, vcol, vrow, tableID, formatSet) - if err != nil { + if err = f.addSheetTable(sheet, rID); err != nil { + return err + } + f.addSheetNameSpace(sheet, SourceRelationship) + if err = f.addTable(sheet, tableXML, hcol, hrow, vcol, vrow, tableID, formatSet); err != nil { return err } f.addContentTypePart(tableID, "table") @@ -106,16 +108,20 @@ func (f *File) countTables() int { // addSheetTable provides a function to add tablePart element to // xl/worksheets/sheet%d.xml by given worksheet name and relationship index. -func (f *File) addSheetTable(sheet string, rID int) { - xlsx, _ := f.workSheetReader(sheet) +func (f *File) addSheetTable(sheet string, rID int) error { + ws, err := f.workSheetReader(sheet) + if err != nil { + return err + } table := &xlsxTablePart{ RID: "rId" + strconv.Itoa(rID), } - if xlsx.TableParts == nil { - xlsx.TableParts = &xlsxTableParts{} + if ws.TableParts == nil { + ws.TableParts = &xlsxTableParts{} } - xlsx.TableParts.Count++ - xlsx.TableParts.TableParts = append(xlsx.TableParts.TableParts, table) + ws.TableParts.Count++ + ws.TableParts.TableParts = append(ws.TableParts.TableParts, table) + return err } // addTable provides a function to add table by given worksheet name, @@ -159,7 +165,7 @@ func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, formatSet name = "Table" + strconv.Itoa(i) } t := xlsxTable{ - XMLNS: NameSpaceSpreadSheet, + XMLNS: NameSpaceSpreadSheet.Value, ID: i, Name: name, DisplayName: name, diff --git a/xmlDrawing.go b/xmlDrawing.go index 9c7ef54cbd..24df0faf39 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -13,9 +13,15 @@ package excelize import "encoding/xml" +// Source relationship and namespace. +var ( + SourceRelationship = xml.Attr{Name: xml.Name{Local: "r", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/officeDocument/2006/relationships"} + 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"} +) + // Source relationship and namespace. const ( - SourceRelationship = "http://schemas.openxmlformats.org/officeDocument/2006/relationships" 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" @@ -37,8 +43,6 @@ const ( NameSpaceDrawingML = "http://schemas.openxmlformats.org/drawingml/2006/main" NameSpaceDrawingMLChart = "http://schemas.openxmlformats.org/drawingml/2006/chart" NameSpaceDrawingMLSpreadSheet = "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" - NameSpaceSpreadSheet = "http://schemas.openxmlformats.org/spreadsheetml/2006/main" - NameSpaceSpreadSheetX14 = "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main" NameSpaceSpreadSheetX15 = "http://schemas.microsoft.com/office/spreadsheetml/2010/11/main" NameSpaceSpreadSheetExcel2006Main = "http://schemas.microsoft.com/office/excel/2006/main" NameSpaceMacExcel2008Main = "http://schemas.microsoft.com/office/mac/excel/2008/main" From ca43c6511538f50581ec3bbe1e4ee275444f8049 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 19 Jul 2020 00:10:42 +0800 Subject: [PATCH 264/957] Update test for addTable --- lib.go | 5 +---- table.go | 10 ++-------- table_test.go | 6 ++++++ xmlDrawing.go | 8 ++++---- 4 files changed, 13 insertions(+), 16 deletions(-) diff --git a/lib.go b/lib.go index 6dcd97ed61..0efb074fa9 100644 --- a/lib.go +++ b/lib.go @@ -396,10 +396,7 @@ func (f *File) addNameSpaces(path string, ns xml.Attr) { if !exist { f.xmlAttr[path] = append(f.xmlAttr[path], ns) if !mc { - f.xmlAttr[path] = append(f.xmlAttr[path], xml.Attr{ - Name: xml.Name{Local: "mc", Space: "xmlns"}, - Value: SourceRelationshipCompatibility, - }) + f.xmlAttr[path] = append(f.xmlAttr[path], SourceRelationshipCompatibility) } if !ignore { f.xmlAttr[path] = append(f.xmlAttr[path], xml.Attr{ diff --git a/table.go b/table.go index e26bbe2d5c..64c87b16e6 100644 --- a/table.go +++ b/table.go @@ -287,14 +287,8 @@ func (f *File) AutoFilter(sheet, hcell, vcell, format string) error { } formatSet, _ := parseAutoFilterSet(format) - - var cellStart, cellEnd string - if cellStart, err = CoordinatesToCellName(hcol, hrow); err != nil { - return err - } - if cellEnd, err = CoordinatesToCellName(vcol, vrow); err != nil { - return err - } + cellStart, _ := CoordinatesToCellName(hcol, hrow) + cellEnd, _ := CoordinatesToCellName(vcol, vrow) ref, filterDB := cellStart+":"+cellEnd, "_xlnm._FilterDatabase" wb := f.workbookReader() sheetID := f.GetSheetIndex(sheet) diff --git a/table_test.go b/table_test.go index 127ee1bbf3..95738e1375 100644 --- a/table_test.go +++ b/table_test.go @@ -29,6 +29,8 @@ func TestAddTable(t *testing.T) { t.FailNow() } + // Test add table in not exist worksheet. + assert.EqualError(t, f.AddTable("SheetN", "B26", "A21", `{}`), "sheet SheetN is 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. @@ -100,6 +102,10 @@ func TestAutoFilterError(t *testing.T) { }) } + assert.EqualError(t, f.autoFilter("SheetN", "A1", 1, 1, &formatAutoFilter{ + Column: "A", + Expression: "", + }), "sheet SheetN is not exist") assert.EqualError(t, f.autoFilter("Sheet1", "A1", 1, 1, &formatAutoFilter{ Column: "-", Expression: "-", diff --git a/xmlDrawing.go b/xmlDrawing.go index 24df0faf39..64d2bc5fbe 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -15,9 +15,10 @@ import "encoding/xml" // Source relationship and namespace. var ( - SourceRelationship = xml.Attr{Name: xml.Name{Local: "r", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/officeDocument/2006/relationships"} - 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"} + 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"} + 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"} ) // Source relationship and namespace. @@ -39,7 +40,6 @@ const ( SourceRelationshipChart201506 = "http://schemas.microsoft.com/office/drawing/2015/06/chart" SourceRelationshipChart20070802 = "http://schemas.microsoft.com/office/drawing/2007/8/2/chart" SourceRelationshipChart2014 = "http://schemas.microsoft.com/office/drawing/2014/chart" - SourceRelationshipCompatibility = "http://schemas.openxmlformats.org/markup-compatibility/2006" NameSpaceDrawingML = "http://schemas.openxmlformats.org/drawingml/2006/main" NameSpaceDrawingMLChart = "http://schemas.openxmlformats.org/drawingml/2006/chart" NameSpaceDrawingMLSpreadSheet = "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" From 13e7bce6d22bd8989084f34280fffcefef7ad9b7 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 20 Jul 2020 00:05:37 +0800 Subject: [PATCH 265/957] improvement compatibility for the XML ignorable namespace --- lib.go | 21 ++++++++++++++++----- lib_test.go | 8 ++++++++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/lib.go b/lib.go index 0efb074fa9..2d2c1fa9a6 100644 --- a/lib.go +++ b/lib.go @@ -379,14 +379,14 @@ func (f *File) replaceNameSpaceBytes(path string, contentMarshal []byte) []byte func (f *File) addNameSpaces(path string, ns xml.Attr) { exist := false mc := false - ignore := false + ignore := -1 if attr, ok := f.xmlAttr[path]; ok { - for _, attribute := range attr { + for i, attribute := range attr { if attribute.Name.Local == ns.Name.Local && attribute.Name.Space == ns.Name.Space { exist = true } - if attribute.Name.Local == "Ignorable" && attribute.Name.Space == "mc" { - ignore = true + if attribute.Name.Local == "Ignorable" && getXMLNamespace(attribute.Name.Space, attr) == "mc" { + ignore = i } if attribute.Name.Local == "mc" && attribute.Name.Space == "xmlns" { mc = true @@ -398,12 +398,23 @@ func (f *File) addNameSpaces(path string, ns xml.Attr) { if !mc { f.xmlAttr[path] = append(f.xmlAttr[path], SourceRelationshipCompatibility) } - if !ignore { + if ignore == -1 { f.xmlAttr[path] = append(f.xmlAttr[path], xml.Attr{ Name: xml.Name{Local: "Ignorable", Space: "mc"}, Value: ns.Name.Local, }) + return } + f.setIgnorableNameSpace(path, ignore, ns) + } +} + +// setIgnorableNameSpace provides a function to set XML namespace as ignorable by the given +// attribute. +func (f *File) setIgnorableNameSpace(path string, index int, ns xml.Attr) { + ignorableNS := []string{"c14", "cdr14", "a14", "pic14", "x14", "xdr14", "x14ac", "dsp", "mso14", "dgm14", "x15", "x12ac", "x15ac", "xr", "xr2", "xr3", "xr4", "xr5", "xr6", "xr7", "xr8", "xr9", "xr10", "xr11", "xr12", "xr13", "xr14", "xr15", "x15", "x16", "x16r2", "mo", "mx", "mv", "o", "v"} + if inStrSlice(strings.Fields(f.xmlAttr[path][index].Value), ns.Name.Local) == -1 && inStrSlice(ignorableNS, ns.Name.Local) != -1 { + f.xmlAttr[path][index].Value = strings.TrimSpace(fmt.Sprintf("%s %s", f.xmlAttr[path][index].Value, ns.Name.Local)) } } diff --git a/lib_test.go b/lib_test.go index e4ccdcc4ee..f3e9b3e529 100644 --- a/lib_test.go +++ b/lib_test.go @@ -1,6 +1,7 @@ package excelize import ( + "encoding/xml" "fmt" "strconv" "strings" @@ -215,6 +216,13 @@ func TestBytesReplace(t *testing.T) { assert.EqualValues(t, s, bytesReplace(s, []byte{}, []byte{}, 0)) } +func TestSetIgnorableNameSpace(t *testing.T) { + f := NewFile() + f.xmlAttr["xml_path"] = []xml.Attr{{}} + f.setIgnorableNameSpace("xml_path", 0, xml.Attr{Name: xml.Name{Local: "c14"}}) + assert.EqualValues(t, "c14", f.xmlAttr["xml_path"][0].Value) +} + func TestStack(t *testing.T) { s := NewStack() assert.Equal(t, s.Peek(), nil) From ee35497935cac81b951d3a7b4f9dc3902c70e37c Mon Sep 17 00:00:00 2001 From: xuancanh Date: Wed, 22 Jul 2020 20:20:00 +0800 Subject: [PATCH 266/957] Fix #673 comment shape compatibility issue with the recent Excel versions * Fix comment shape compatibility issue * Using Go modules with Travis CI * Update .travis.yml Co-authored-by: Canh Nguyen Co-authored-by: xuri --- .travis.yml | 4 ++-- comment.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 92852cf487..84c797ea40 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,8 +19,8 @@ env: - GOARCH=386 script: - - go vet ./... - - go test ./... -v -coverprofile=coverage.txt -covermode=atomic + - env GO111MODULE=on go vet ./... + - env GO111MODULE=on go test ./... -v -coverprofile=coverage.txt -covermode=atomic after_success: - bash <(curl -s https://codecov.io/bash) diff --git a/comment.go b/comment.go index 8414b401d2..6010891318 100644 --- a/comment.go +++ b/comment.go @@ -164,7 +164,7 @@ func (f *File) addDrawingVML(commentID int, drawingVML, cell string, lineCount, }, VPath: &vPath{ Gradientshapeok: "t", - Connecttype: "miter", + Connecttype: "rect", }, }, } From 1c2e7c5c68f15739112e9c06d238b81318af0c97 Mon Sep 17 00:00:00 2001 From: WXDYGR <33148310+WXDYGR@users.noreply.github.com> Date: Wed, 5 Aug 2020 20:33:12 +0800 Subject: [PATCH 267/957] Update styles.go MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复获取百分比值时,GetCellValue返回值不准确的问题 --- styles.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/styles.go b/styles.go index d7b1460ec9..c3a2393160 100644 --- a/styles.go +++ b/styles.go @@ -909,7 +909,7 @@ func formatToC(i int, v string) string { return v } f = f * 100 - return fmt.Sprintf("%d%%", int(f)) + return fmt.Sprintf("%.f%%", f) } // formatToD provides a function to convert original string to special format From 843bd24e56450791ad122a2f3875956a0a70ec6e Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 6 Aug 2020 05:58:40 +0000 Subject: [PATCH 268/957] This closes #677 and closes #679, fix panic when enabling compiler inline flags --- lib.go | 11 ++--------- sheet.go | 4 ++-- stream.go | 2 +- 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/lib.go b/lib.go index 2d2c1fa9a6..acb4590142 100644 --- a/lib.go +++ b/lib.go @@ -20,7 +20,6 @@ import ( "io" "strconv" "strings" - "unsafe" ) // ReadZipReader can be used to read the spreadsheet in memory without touching the @@ -251,17 +250,11 @@ func namespaceStrictToTransitional(content []byte) []byte { StrictNameSpaceSpreadSheet: NameSpaceSpreadSheet.Value, } for s, n := range namespaceTranslationDic { - content = bytesReplace(content, stringToBytes(s), stringToBytes(n), -1) + content = bytesReplace(content, []byte(s), []byte(n), -1) } return content } -// stringToBytes cast a string to bytes pointer and assign the value of this -// pointer. -func stringToBytes(s string) []byte { - return *(*[]byte)(unsafe.Pointer(&s)) -} - // bytesReplace replace old bytes with given new. func bytesReplace(s, old, new []byte, n int) []byte { if n == 0 { @@ -366,7 +359,7 @@ 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 { - var oldXmlns = stringToBytes(`xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">`) + var oldXmlns = []byte(`xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">`) var newXmlns = []byte(templateNamespaceIDMap) if attr, ok := f.xmlAttr[path]; ok { newXmlns = []byte(genXMLNamespace(attr)) diff --git a/sheet.go b/sheet.go index 31a36eba32..a92221d4fb 100644 --- a/sheet.go +++ b/sheet.go @@ -213,8 +213,8 @@ func (f *File) setAppXML() { // 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 := stringToBytes(`xmlns:relationships="http://schemas.openxmlformats.org/officeDocument/2006/relationships" relationships`) - newXmlns := stringToBytes("r") + oldXmlns := []byte(`xmlns:relationships="http://schemas.openxmlformats.org/officeDocument/2006/relationships" relationships`) + newXmlns := []byte("r") return bytesReplace(content, oldXmlns, newXmlns, -1) } diff --git a/stream.go b/stream.go index ec1e65b769..19f5ca7625 100644 --- a/stream.go +++ b/stream.go @@ -367,7 +367,7 @@ func writeCell(buf *bufferedWriter, c xlsxC) { buf.WriteString(`>`) if c.V != "" { buf.WriteString(``) - xml.EscapeText(buf, stringToBytes(c.V)) + xml.EscapeText(buf, []byte(c.V)) buf.WriteString(``) } buf.WriteString(``) From c98fd7e5d05ee35d636304bb9864bb2943996c23 Mon Sep 17 00:00:00 2001 From: Wang Yaoshen Date: Fri, 14 Aug 2020 10:32:58 +0800 Subject: [PATCH 269/957] Fix issue #686 RemoveRow slice bounds out of range (#687) fix Hyperlinks update error --- adjust.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/adjust.go b/adjust.go index 226ea9e6fc..40898d9a69 100644 --- a/adjust.go +++ b/adjust.go @@ -109,14 +109,15 @@ func (f *File) adjustHyperlinks(xlsx *xlsxWorksheet, sheet string, dir adjustDir // order is important if offset < 0 { - for rowIdx, linkData := range xlsx.Hyperlinks.Hyperlink { + for i := len(xlsx.Hyperlinks.Hyperlink) - 1; i >= 0; i-- { + linkData := xlsx.Hyperlinks.Hyperlink[i] colNum, rowNum, _ := CellNameToCoordinates(linkData.Ref) if (dir == rows && num == rowNum) || (dir == columns && num == colNum) { f.deleteSheetRelationships(sheet, linkData.RID) if len(xlsx.Hyperlinks.Hyperlink) > 1 { - xlsx.Hyperlinks.Hyperlink = append(xlsx.Hyperlinks.Hyperlink[:rowIdx], - xlsx.Hyperlinks.Hyperlink[rowIdx+1:]...) + xlsx.Hyperlinks.Hyperlink = append(xlsx.Hyperlinks.Hyperlink[:i], + xlsx.Hyperlinks.Hyperlink[i+1:]...) } else { xlsx.Hyperlinks = nil } From 3cf3101fd99ad8901eac94ba4d15ad10cf3777a1 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 14 Aug 2020 10:19:27 +0000 Subject: [PATCH 270/957] test on Go 1.5 on Travis CI and update docs for the CalcCellValue --- .travis.yml | 1 + calc.go | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/.travis.yml b/.travis.yml index 84c797ea40..03825a8089 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,7 @@ go: - 1.12.x - 1.13.x - 1.14.x + - 1.15.x os: - linux diff --git a/calc.go b/calc.go index f86b12c4d4..54683a82c6 100644 --- a/calc.go +++ b/calc.go @@ -96,6 +96,19 @@ type formulaFuncs struct{} // 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. +// +// Supported formulas: +// +// ABS, ACOS, ACOSH, ACOT, ACOTH, ARABIC, ASIN, ASINH, ATAN2, ATANH, BASE, +// CEILING, CEILING.MATH, CEILING.PRECISE, COMBIN, COMBINA, COS, COSH, COT, +// COTH, COUNTA, CSC, CSCH, DECIMAL, DEGREES, EVEN, EXP, FACT, FACTDOUBLE, +// FLOOR, FLOOR.MATH, FLOOR.PRECISE, GCD, INT, ISBLANK, ISERR, ISERROR, +// ISEVEN, ISNA, ISNONTEXT, ISNUMBER, ISO.CEILING, ISODD, LCM, LN, LOG, +// LOG10, MDETERM, MEDIAN, MOD, MROUND, MULTINOMIAL, MUNIT, NA, ODD, PI, +// POWER, PRODUCT, QUOTIENT, RADIANS, RAND, RANDBETWEEN, ROUND, ROUNDDOWN, +// ROUNDUP, SEC, SECH, SIGN, SIN, SINH, SQRT, SQRTPI, SUM, SUMIF, SUMSQ, +// TAN, TANH, TRUNC +// func (f *File) CalcCellValue(sheet, cell string) (result string, err error) { var ( formula string From c3e92a51d744bc8420e0626b06ee3a0efd030341 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 14 Aug 2020 16:09:50 +0000 Subject: [PATCH 271/957] Compatible with Go 1.15, fix unit test failed on Windows and fixed #689 potential race condition --- .travis.yml | 2 +- cell.go | 20 +++++++------------- cell_test.go | 21 +++++++++++++++++++++ excelize.go | 6 +++++- lib.go | 2 +- rows.go | 4 ++++ stream_test.go | 1 + xmlWorksheet.go | 6 +++++- 8 files changed, 45 insertions(+), 17 deletions(-) diff --git a/.travis.yml b/.travis.yml index 03825a8089..a5c55f3ea5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,7 +21,7 @@ env: script: - env GO111MODULE=on go vet ./... - - env GO111MODULE=on go test ./... -v -coverprofile=coverage.txt -covermode=atomic + - env GO111MODULE=on go test -v -race ./... -coverprofile=coverage.txt -covermode=atomic after_success: - bash <(curl -s https://codecov.io/bash) diff --git a/cell.go b/cell.go index 3293d19fe4..383c02cf49 100644 --- a/cell.go +++ b/cell.go @@ -18,7 +18,6 @@ import ( "reflect" "strconv" "strings" - "sync" "time" ) @@ -33,8 +32,6 @@ const ( STCellFormulaTypeShared = "shared" ) -var rwMutex sync.RWMutex - // GetCellValue provides a function to get formatted value from cell by given // worksheet name and axis in XLSX 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, @@ -181,8 +178,6 @@ 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 { - rwMutex.Lock() - defer rwMutex.Unlock() xlsx, err := f.workSheetReader(sheet) if err != nil { return err @@ -204,8 +199,6 @@ 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 { - rwMutex.Lock() - defer rwMutex.Unlock() xlsx, err := f.workSheetReader(sheet) if err != nil { return err @@ -239,8 +232,6 @@ func setCellBool(value bool) (t string, v string) { // f.SetCellFloat("Sheet1", "A1", float64(x), 2, 32) // func (f *File) SetCellFloat(sheet, axis string, value float64, prec, bitSize int) error { - rwMutex.Lock() - defer rwMutex.Unlock() xlsx, err := f.workSheetReader(sheet) if err != nil { return err @@ -262,8 +253,6 @@ func setCellFloat(value float64, prec, 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 { - rwMutex.Lock() - defer rwMutex.Unlock() xlsx, err := f.workSheetReader(sheet) if err != nil { return err @@ -291,6 +280,8 @@ func (f *File) setCellString(value string) (t string, v string) { // setSharedString provides a function to add string to the share string table. func (f *File) setSharedString(val string) int { sst := f.sharedStringsReader() + f.Lock() + defer f.Unlock() if i, ok := f.sharedStringsMap[val]; ok { return i } @@ -371,8 +362,6 @@ type FormulaOpts struct { // SetCellFormula provides a function to set cell formula by given string and // worksheet name. func (f *File) SetCellFormula(sheet, axis, formula string, opts ...FormulaOpts) error { - rwMutex.Lock() - defer rwMutex.Unlock() xlsx, err := f.workSheetReader(sheet) if err != nil { return err @@ -697,6 +686,8 @@ func (f *File) SetSheetRow(sheet, axis string, slice interface{}) error { // getCellInfo does common preparation for all SetCell* methods. func (f *File) prepareCell(xlsx *xlsxWorksheet, sheet, cell string) (*xlsxC, int, int, error) { + xlsx.Lock() + defer xlsx.Unlock() var err error cell, err = f.mergeCellsParser(xlsx, cell) if err != nil { @@ -728,6 +719,9 @@ func (f *File) getCellStringFunc(sheet, axis string, fn func(x *xlsxWorksheet, c return "", err } + xlsx.Lock() + defer xlsx.Unlock() + lastRowNum := 0 if l := len(xlsx.SheetData.Row); l > 0 { lastRowNum = xlsx.SheetData.Row[l-1].R diff --git a/cell_test.go b/cell_test.go index fb30596f18..ba4cd83b09 100644 --- a/cell_test.go +++ b/cell_test.go @@ -4,12 +4,33 @@ import ( "fmt" "path/filepath" "strconv" + "sync" "testing" "time" "github.com/stretchr/testify/assert" ) +func TestConcurrency(t *testing.T) { + f := NewFile() + wg := new(sync.WaitGroup) + for i := 1; i <= 5; i++ { + wg.Add(1) + go func(val int) { + f.SetCellValue("Sheet1", fmt.Sprintf("A%d", val), val) + f.SetCellValue("Sheet1", fmt.Sprintf("B%d", val), strconv.Itoa(val)) + f.GetCellValue("Sheet1", fmt.Sprintf("A%d", val)) + wg.Done() + }(i) + } + wg.Wait() + val, err := f.GetCellValue("Sheet1", "A1") + if err != nil { + t.Error(err) + } + assert.Equal(t, "1", val) +} + func TestCheckCellInArea(t *testing.T) { f := NewFile() expectedTrueCellInAreaList := [][2]string{ diff --git a/excelize.go b/excelize.go index 5cc88e961f..bfb3abaad4 100644 --- a/excelize.go +++ b/excelize.go @@ -24,12 +24,14 @@ import ( "path" "strconv" "strings" + "sync" "golang.org/x/net/html/charset" ) // File define a populated spreadsheet file struct. type File struct { + sync.RWMutex xmlAttr map[string][]xml.Attr checked map[string]bool sheetMap map[string]string @@ -153,6 +155,8 @@ func (f *File) setDefaultTimeStyle(sheet, axis 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) (xlsx *xlsxWorksheet, err error) { + f.Lock() + defer f.Unlock() var ( name string ok bool @@ -323,7 +327,7 @@ func (f *File) AddVBAProject(bin string) error { var err error // Check vbaProject.bin exists first. if _, err = os.Stat(bin); os.IsNotExist(err) { - return err + return fmt.Errorf("stat %s: no such file or directory", bin) } if path.Ext(bin) != ".bin" { return errors.New("unsupported VBA project extension") diff --git a/lib.go b/lib.go index acb4590142..88aa3a117a 100644 --- a/lib.go +++ b/lib.go @@ -167,7 +167,7 @@ func ColumnNumberToName(num int) (string, error) { } var col string for num > 0 { - col = string((num-1)%26+65) + col + col = string(rune((num-1)%26+65)) + col num = (num - 1) / 26 } return col, nil diff --git a/rows.go b/rows.go index 320ba2fdf2..66dd16becf 100644 --- a/rows.go +++ b/rows.go @@ -284,6 +284,8 @@ func (f *File) GetRowHeight(sheet string, row int) (float64, error) { func (f *File) sharedStringsReader() *xlsxSST { var err error + f.Lock() + defer f.Unlock() if f.SharedStrings == nil { var sharedStrings xlsxSST ss := f.readXML("xl/sharedStrings.xml") @@ -318,6 +320,8 @@ func (f *File) sharedStringsReader() *xlsxSST { // inteded to be used with for range on rows an argument with the xlsx opened // file. func (xlsx *xlsxC) getValueFrom(f *File, d *xlsxSST) (string, error) { + f.Lock() + defer f.Unlock() switch xlsx.T { case "s": if xlsx.V != "" { diff --git a/stream_test.go b/stream_test.go index d89dad845a..d81b1d4b13 100644 --- a/stream_test.go +++ b/stream_test.go @@ -91,6 +91,7 @@ func TestStreamWriter(t *testing.T) { assert.NoError(t, err) _, err = streamWriter.rawData.Reader() assert.NoError(t, err) + assert.NoError(t, streamWriter.rawData.tmp.Close()) assert.NoError(t, os.Remove(streamWriter.rawData.tmp.Name())) // Test unsupport charset diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 7cd73c4185..2b39e64dfc 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -11,11 +11,15 @@ package excelize -import "encoding/xml" +import ( + "encoding/xml" + "sync" +) // xlsxWorksheet directly maps the worksheet element in the namespace // http://schemas.openxmlformats.org/spreadsheetml/2006/main. type xlsxWorksheet struct { + sync.RWMutex XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main worksheet"` SheetPr *xlsxSheetPr `xml:"sheetPr"` Dimension *xlsxDimension `xml:"dimension"` From bc704c854f270f5b53eaa6980c76950ad86410c7 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 15 Aug 2020 10:41:45 +0000 Subject: [PATCH 272/957] update stream writer fields offset --- stream.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/stream.go b/stream.go index 19f5ca7625..83b25f7d71 100644 --- a/stream.go +++ b/stream.go @@ -86,7 +86,7 @@ func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) { return nil, err } sw.rawData.WriteString(XMLHeader + ``) return sw, err } @@ -376,9 +376,9 @@ func writeCell(buf *bufferedWriter, c xlsxC) { // Flush ending the streaming writing process. func (sw *StreamWriter) Flush() error { sw.rawData.WriteString(``) - bulkAppendFields(&sw.rawData, sw.worksheet, 7, 37) + bulkAppendFields(&sw.rawData, sw.worksheet, 8, 38) sw.rawData.WriteString(sw.tableParts) - bulkAppendFields(&sw.rawData, sw.worksheet, 39, 39) + bulkAppendFields(&sw.rawData, sw.worksheet, 40, 40) sw.rawData.WriteString(``) if err := sw.rawData.Flush(); err != nil { return err From 4e4baac3bc1cd11026a35fb59b6a0d7903a44070 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 16 Aug 2020 03:48:11 +0000 Subject: [PATCH 273/957] using Mutex lock and update benchmark --- excelize.go | 2 +- file_test.go | 2 +- xmlWorksheet.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/excelize.go b/excelize.go index bfb3abaad4..75b0c135d7 100644 --- a/excelize.go +++ b/excelize.go @@ -31,7 +31,7 @@ import ( // File define a populated spreadsheet file struct. type File struct { - sync.RWMutex + sync.Mutex xmlAttr map[string][]xml.Attr checked map[string]bool sheetMap map[string]string diff --git a/file_test.go b/file_test.go index e27b754af1..9fc120c4c4 100644 --- a/file_test.go +++ b/file_test.go @@ -19,7 +19,7 @@ func BenchmarkWrite(b *testing.B) { if err != nil { b.Error(err) } - if err := f.SetCellDefault("Sheet1", val, s); err != nil { + if err := f.SetCellValue("Sheet1", val, s); err != nil { b.Error(err) } } diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 2b39e64dfc..bc81b0313f 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.RWMutex + sync.Mutex XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main worksheet"` SheetPr *xlsxSheetPr `xml:"sheetPr"` Dimension *xlsxDimension `xml:"dimension"` From 3c8c8c55c8128c5bb94fe28451f58fbc5fb4a118 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 18 Aug 2020 08:30:32 +0000 Subject: [PATCH 274/957] resolved #691, fix the scale for add picture not work --- picture.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/picture.go b/picture.go index 468cccdd81..9a646379c3 100644 --- a/picture.go +++ b/picture.go @@ -253,6 +253,9 @@ func (f *File) addDrawingPicture(sheet, drawingXML, cell, file string, width, he if err != nil { return err } + } else { + width = int(float64(width) * formatSet.XScale) + height = int(float64(height) * formatSet.YScale) } col-- row-- From 88de2f8d510b0959bbb672b80656d207bd0bc927 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 22 Aug 2020 18:58:43 +0800 Subject: [PATCH 275/957] Default row height compatibility with Apache OpenOffice and Kingsoft WPS, unit test update and typo fixed --- cell.go | 2 +- cell_test.go | 2 +- col.go | 1 + drawing.go | 18 +++++++++--------- merge.go | 2 ++ rows.go | 17 ++++++++++------- rows_test.go | 4 ++-- sheet.go | 8 +++++++- sparkline.go | 4 ++-- xmlDrawing.go | 29 +++++++++++++++-------------- 10 files changed, 50 insertions(+), 37 deletions(-) diff --git a/cell.go b/cell.go index 383c02cf49..5fe2157c43 100644 --- a/cell.go +++ b/cell.go @@ -517,7 +517,7 @@ func (f *File) SetCellHyperLink(sheet, axis, link, linkType string) error { // } // if err := f.SetCellRichText("Sheet1", "A1", []excelize.RichTextRun{ // { -// Text: "blod", +// Text: "bold", // Font: &excelize.Font{ // Bold: true, // Color: "2354e8", diff --git a/cell_test.go b/cell_test.go index ba4cd83b09..441a6946ed 100644 --- a/cell_test.go +++ b/cell_test.go @@ -190,7 +190,7 @@ func TestSetCellRichText(t *testing.T) { assert.NoError(t, f.SetColWidth("Sheet1", "A", "A", 44)) richTextRun := []RichTextRun{ { - Text: "blod", + Text: "bold", Font: &Font{ Bold: true, Color: "2354e8", diff --git a/col.go b/col.go index 5a8299ea9c..72db4be907 100644 --- a/col.go +++ b/col.go @@ -25,6 +25,7 @@ import ( // Define the default cell size and EMU unit of measurement. const ( defaultColWidthPixels float64 = 64 + defaultRowHeight float64 = 15 defaultRowHeightPixels float64 = 20 EMU int = 9525 ) diff --git a/drawing.go b/drawing.go index ced747d708..806c1b7fa3 100644 --- a/drawing.go +++ b/drawing.go @@ -59,10 +59,10 @@ func (f *File) prepareChartSheetDrawing(xlsx *xlsxChartsheet, drawingID int, she func (f *File) addChart(formatSet *formatChart, comboCharts []*formatChart) { count := f.countCharts() xlsxChartSpace := xlsxChartSpace{ - XMLNSc: NameSpaceDrawingMLChart, - XMLNSa: NameSpaceDrawingML, + XMLNSc: NameSpaceDrawingMLChart.Value, + XMLNSa: NameSpaceDrawingML.Value, XMLNSr: SourceRelationship.Value, - XMLNSc16r2: SourceRelationshipChart201506, + XMLNSc16r2: SourceRelationshipChart201506.Value, Date1904: &attrValBool{Val: boolPtr(false)}, Lang: &attrValString{Val: stringPtr("en-US")}, RoundedCorners: &attrValBool{Val: boolPtr(false)}, @@ -1143,8 +1143,8 @@ func (f *File) drawingParser(path string) (*xlsxWsDr, int) { if f.Drawings[path] == nil { content := xlsxWsDr{} - content.A = NameSpaceDrawingML - content.Xdr = NameSpaceDrawingMLSpreadSheet + content.A = NameSpaceDrawingML.Value + content.Xdr = NameSpaceDrawingMLSpreadSheet.Value if _, ok = f.XLSX[path]; ok { // Append Model decodeWsDr := decodeWsDr{} if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(path)))). @@ -1210,9 +1210,9 @@ func (f *File) addDrawingChart(sheet, drawingXML, cell string, width, height, rI }, Graphic: &xlsxGraphic{ GraphicData: &xlsxGraphicData{ - URI: NameSpaceDrawingMLChart, + URI: NameSpaceDrawingMLChart.Value, Chart: &xlsxChart{ - C: NameSpaceDrawingMLChart, + C: NameSpaceDrawingMLChart.Value, R: SourceRelationship.Value, RID: "rId" + strconv.Itoa(rID), }, @@ -1250,9 +1250,9 @@ func (f *File) addSheetDrawingChart(drawingXML string, rID int, formatSet *forma }, Graphic: &xlsxGraphic{ GraphicData: &xlsxGraphicData{ - URI: NameSpaceDrawingMLChart, + URI: NameSpaceDrawingMLChart.Value, Chart: &xlsxChart{ - C: NameSpaceDrawingMLChart, + C: NameSpaceDrawingMLChart.Value, R: SourceRelationship.Value, RID: "rId" + strconv.Itoa(rID), }, diff --git a/merge.go b/merge.go index b233335090..7bb6d4236d 100644 --- a/merge.go +++ b/merge.go @@ -97,6 +97,7 @@ func (f *File) MergeCell(sheet, hcell, vcell string) error { } else { xlsx.MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: ref}}} } + xlsx.MergeCells.Count = len(xlsx.MergeCells.Cells) return err } @@ -146,6 +147,7 @@ func (f *File) UnmergeCell(sheet string, hcell, vcell string) error { i++ } xlsx.MergeCells.Cells = xlsx.MergeCells.Cells[:i] + xlsx.MergeCells.Count = len(xlsx.MergeCells.Cells) return nil } diff --git a/rows.go b/rows.go index 66dd16becf..c6098e61b8 100644 --- a/rows.go +++ b/rows.go @@ -262,21 +262,24 @@ func (f *File) GetRowHeight(sheet string, row int) (float64, error) { if row < 1 { return defaultRowHeightPixels, newInvalidRowNumberError(row) } - - xlsx, err := f.workSheetReader(sheet) + var ht = defaultRowHeight + ws, err := f.workSheetReader(sheet) if err != nil { - return defaultRowHeightPixels, err + return ht, err } - if row > len(xlsx.SheetData.Row) { - return defaultRowHeightPixels, nil // it will be better to use 0, but we take care with BC + if ws.SheetFormatPr != nil { + ht = ws.SheetFormatPr.DefaultRowHeight + } + if row > len(ws.SheetData.Row) { + return ht, nil // it will be better to use 0, but we take care with BC } - for _, v := range xlsx.SheetData.Row { + for _, v := range ws.SheetData.Row { if v.R == row && v.Ht != 0 { return v.Ht, nil } } // Optimisation for when the row heights haven't changed. - return defaultRowHeightPixels, nil + return ht, nil } // sharedStringsReader provides a function to get the pointer to the structure diff --git a/rows_test.go b/rows_test.go index 14537eb145..e3ce9ee088 100644 --- a/rows_test.go +++ b/rows_test.go @@ -112,12 +112,12 @@ func TestRowHeight(t *testing.T) { // Test get row height that rows index over exists rows. height, err = xlsx.GetRowHeight(sheet1, 5) assert.NoError(t, err) - assert.Equal(t, defaultRowHeightPixels, height) + assert.Equal(t, defaultRowHeight, height) // Test get row height that rows heights haven't changed. height, err = xlsx.GetRowHeight(sheet1, 3) assert.NoError(t, err) - assert.Equal(t, defaultRowHeightPixels, height) + assert.Equal(t, defaultRowHeight, height) // Test set and get row height on not exists worksheet. assert.EqualError(t, xlsx.SetRowHeight("SheetN", 1, 111.0), "sheet SheetN is not exist") diff --git a/sheet.go b/sheet.go index a92221d4fb..dedd2d950e 100644 --- a/sheet.go +++ b/sheet.go @@ -1630,13 +1630,19 @@ func (f *File) relsReader(path string) *xlsxRelationships { func prepareSheetXML(xlsx *xlsxWorksheet, col int, row int) { rowCount := len(xlsx.SheetData.Row) sizeHint := 0 + var ht float64 + var customHeight bool + if xlsx.SheetFormatPr != nil { + ht = xlsx.SheetFormatPr.DefaultRowHeight + customHeight = true + } if rowCount > 0 { sizeHint = len(xlsx.SheetData.Row[rowCount-1].C) } if rowCount < row { // append missing rows for rowIdx := rowCount; rowIdx < row; rowIdx++ { - xlsx.SheetData.Row = append(xlsx.SheetData.Row, xlsxRow{R: rowIdx + 1, C: make([]xlsxC, 0, sizeHint)}) + xlsx.SheetData.Row = append(xlsx.SheetData.Row, xlsxRow{R: rowIdx + 1, CustomHeight: customHeight, Ht: ht, C: make([]xlsxC, 0, sizeHint)}) } } rowData := &xlsx.SheetData.Row[row-1] diff --git a/sparkline.go b/sparkline.go index 4004878e4e..b42207cbec 100644 --- a/sparkline.go +++ b/sparkline.go @@ -441,7 +441,7 @@ func (f *File) AddSparkline(sheet string, opt *SparklineOption) (err error) { } } else { groups = &xlsxX14SparklineGroups{ - XMLNSXM: NameSpaceSpreadSheetExcel2006Main, + XMLNSXM: NameSpaceSpreadSheetExcel2006Main.Value, SparklineGroups: []*xlsxX14SparklineGroup{group}, } if sparklineGroupsBytes, err = xml.Marshal(groups); err != nil { @@ -525,7 +525,7 @@ func (f *File) appendSparkline(ws *xlsxWorksheet, group *xlsxX14SparklineGroup, return } groups = &xlsxX14SparklineGroups{ - XMLNSXM: NameSpaceSpreadSheetExcel2006Main, + XMLNSXM: NameSpaceSpreadSheetExcel2006Main.Value, Content: decodeSparklineGroups.Content + string(sparklineGroupBytes), } if sparklineGroupsBytes, err = xml.Marshal(groups); err != nil { diff --git a/xmlDrawing.go b/xmlDrawing.go index 64d2bc5fbe..e3e496ae06 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -13,12 +13,22 @@ package excelize import "encoding/xml" -// Source relationship and namespace. +// 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"} - 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"} + 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"} + 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"} + 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"} ) // Source relationship and namespace. @@ -37,15 +47,6 @@ const ( 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" - SourceRelationshipChart201506 = "http://schemas.microsoft.com/office/drawing/2015/06/chart" - SourceRelationshipChart20070802 = "http://schemas.microsoft.com/office/drawing/2007/8/2/chart" - SourceRelationshipChart2014 = "http://schemas.microsoft.com/office/drawing/2014/chart" - NameSpaceDrawingML = "http://schemas.openxmlformats.org/drawingml/2006/main" - NameSpaceDrawingMLChart = "http://schemas.openxmlformats.org/drawingml/2006/chart" - NameSpaceDrawingMLSpreadSheet = "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" - NameSpaceSpreadSheetX15 = "http://schemas.microsoft.com/office/spreadsheetml/2010/11/main" - NameSpaceSpreadSheetExcel2006Main = "http://schemas.microsoft.com/office/excel/2006/main" - NameSpaceMacExcel2008Main = "http://schemas.microsoft.com/office/mac/excel/2008/main" NameSpaceXML = "http://www.w3.org/XML/1998/namespace" NameSpaceXMLSchemaInstance = "http://www.w3.org/2001/XMLSchema-instance" StrictSourceRelationship = "http://purl.oclc.org/ooxml/officeDocument/relationships" From 4177c1585e312bee00c1592af3df6423c366e806 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 1 Sep 2020 00:40:56 +0800 Subject: [PATCH 276/957] Resolve #199, init password protection spreadsheet support --- encrypt.go | 304 ++++++++++++++++++++++++++++++++++++++++++ excelize.go | 47 ++++--- excelize_test.go | 16 +-- go.mod | 19 ++- go.sum | 48 +++---- test/encryptSHA1.xlsx | Bin 0 -> 14336 bytes 6 files changed, 372 insertions(+), 62 deletions(-) create mode 100644 encrypt.go create mode 100644 test/encryptSHA1.xlsx diff --git a/encrypt.go b/encrypt.go new file mode 100644 index 0000000000..e5dc2af143 --- /dev/null +++ b/encrypt.go @@ -0,0 +1,304 @@ +// Copyright 2016 - 2020 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. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.10 or later. + +package excelize + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/md5" + "crypto/sha1" + "crypto/sha256" + "crypto/sha512" + "encoding/base64" + "encoding/binary" + "encoding/xml" + "hash" + "strings" + + "github.com/richardlehane/mscfb" + "golang.org/x/crypto/md4" + "golang.org/x/crypto/ripemd160" + "golang.org/x/text/encoding/unicode" +) + +var ( + blockKey = []byte{0x14, 0x6e, 0x0b, 0xe7, 0xab, 0xac, 0xd0, 0xd6} // Block keys used for encryption + packageOffset = 8 // First 8 bytes are the size of the stream + packageEncryptionChunkSize = 4096 + cryptoIdentifier = []byte{ // checking protect workbook by [MS-OFFCRYPTO] - v20181211 3.1 FeatureIdentifier + 0x3c, 0x00, 0x00, 0x00, 0x4d, 0x00, 0x69, 0x00, 0x63, 0x00, 0x72, 0x00, 0x6f, 0x00, 0x73, 0x00, + 0x6f, 0x00, 0x66, 0x00, 0x74, 0x00, 0x2e, 0x00, 0x43, 0x00, 0x6f, 0x00, 0x6e, 0x00, 0x74, 0x00, + 0x61, 0x00, 0x69, 0x00, 0x6e, 0x00, 0x65, 0x00, 0x72, 0x00, 0x2e, 0x00, 0x44, 0x00, 0x61, 0x00, + 0x74, 0x00, 0x61, 0x00, 0x53, 0x00, 0x70, 0x00, 0x61, 0x00, 0x63, 0x00, 0x65, 0x00, 0x73, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + } +) + +// Encryption specifies the encryption structure, streams, and storages are +// required when encrypting ECMA-376 documents. +type Encryption struct { + KeyData KeyData `xml:"keyData"` + DataIntegrity DataIntegrity `xml:"dataIntegrity"` + KeyEncryptors KeyEncryptors `xml:"keyEncryptors"` +} + +// KeyData specifies the cryptographic attributes used to encrypt the data. +type KeyData struct { + SaltSize int `xml:"saltSize,attr"` + BlockSize int `xml:"blockSize,attr"` + KeyBits int `xml:"keyBits,attr"` + HashSize int `xml:"hashSize,attr"` + CipherAlgorithm string `xml:"cipherAlgorithm,attr"` + CipherChaining string `xml:"cipherChaining,attr"` + HashAlgorithm string `xml:"hashAlgorithm,attr"` + SaltValue string `xml:"saltValue,attr"` +} + +// DataIntegrity specifies the encrypted copies of the salt and hash values +// used to help ensure that the integrity of the encrypted data has not been +// compromised. +type DataIntegrity struct { + EncryptedHmacKey string `xml:"encryptedHmacKey,attr"` + EncryptedHmacValue string `xml:"encryptedHmacValue,attr"` +} + +// KeyEncryptors specifies the key encryptors used to encrypt the data. +type KeyEncryptors struct { + KeyEncryptor []KeyEncryptor `xml:"keyEncryptor"` +} + +// KeyEncryptor specifies that the schema used by this encryptor is the schema +// specified for password-based encryptors. +type KeyEncryptor struct { + XMLName xml.Name `xml:"keyEncryptor"` + URI string `xml:"uri,attr"` + EncryptedKey EncryptedKey `xml:"encryptedKey"` +} + +// EncryptedKey used to generate the encrypting key. +type EncryptedKey struct { + XMLName xml.Name `xml:"http://schemas.microsoft.com/office/2006/keyEncryptor/password encryptedKey"` + SpinCount int `xml:"spinCount,attr"` + EncryptedVerifierHashInput string `xml:"encryptedVerifierHashInput,attr"` + EncryptedVerifierHashValue string `xml:"encryptedVerifierHashValue,attr"` + EncryptedKeyValue string `xml:"encryptedKeyValue,attr"` + KeyData +} + +// Decrypt API decrypt the CFB file format with Agile Encryption. Support +// cryptographic algorithm: MD4, MD5, RIPEMD-160, SHA1, SHA256, SHA384 and +// SHA512. +func Decrypt(raw []byte, opt *Options) (packageBuf []byte, err error) { + doc, err := mscfb.New(bytes.NewReader(raw)) + if err != nil { + return + } + encryptionInfoBuf, encryptedPackageBuf := extractPart(doc) + var encryptionInfo Encryption + if encryptionInfo, err = parseEncryptionInfo(encryptionInfoBuf[8:]); err != nil { + return + } + // Convert the password into an encryption key. + key, err := convertPasswdToKey(opt.Password, encryptionInfo) + if err != nil { + return + } + // Use the key to decrypt the package key. + encryptedKey := encryptionInfo.KeyEncryptors.KeyEncryptor[0].EncryptedKey + saltValue, err := base64.StdEncoding.DecodeString(encryptedKey.SaltValue) + if err != nil { + return + } + encryptedKeyValue, err := base64.StdEncoding.DecodeString(encryptedKey.EncryptedKeyValue) + if err != nil { + return + } + packageKey, err := crypt(false, encryptedKey.CipherAlgorithm, encryptedKey.CipherChaining, key, saltValue, encryptedKeyValue) + // Use the package key to decrypt the package. + return cryptPackage(false, packageKey, encryptedPackageBuf, encryptionInfo) +} + +// extractPart extract data from storage by specified part name. +func extractPart(doc *mscfb.Reader) (encryptionInfoBuf, encryptedPackageBuf []byte) { + for entry, err := doc.Next(); err == nil; entry, err = doc.Next() { + switch entry.Name { + case "EncryptionInfo": + buf := make([]byte, entry.Size) + i, _ := doc.Read(buf) + if i > 0 { + encryptionInfoBuf = buf + break + } + case "EncryptedPackage": + buf := make([]byte, entry.Size) + i, _ := doc.Read(buf) + if i > 0 { + encryptedPackageBuf = buf + break + } + } + } + return +} + +// convertPasswdToKey convert the password into an encryption key. +func convertPasswdToKey(passwd string, encryption Encryption) (key []byte, err error) { + var b bytes.Buffer + saltValue, err := base64.StdEncoding.DecodeString(encryption.KeyEncryptors.KeyEncryptor[0].EncryptedKey.SaltValue) + if err != nil { + return + } + b.Write(saltValue) + encoder := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder() + passwordBuffer, err := encoder.Bytes([]byte(passwd)) + if err != nil { + return + } + b.Write(passwordBuffer) + // Generate the initial hash. + key = hashing(encryption.KeyData.HashAlgorithm, b.Bytes()) + // Now regenerate until spin count. + for i := 0; i < encryption.KeyEncryptors.KeyEncryptor[0].EncryptedKey.SpinCount; i++ { + iterator := createUInt32LEBuffer(i) + key = hashing(encryption.KeyData.HashAlgorithm, iterator, key) + } + // Now generate the final hash. + key = hashing(encryption.KeyData.HashAlgorithm, key, blockKey) + // Truncate or pad as needed to get to length of keyBits. + keyBytes := encryption.KeyEncryptors.KeyEncryptor[0].EncryptedKey.KeyBits / 8 + if len(key) < keyBytes { + tmp := make([]byte, 0x36) + key = append(key, tmp...) + key = tmp + } else if len(key) > keyBytes { + key = key[:keyBytes] + } + return +} + +// hashing data by specified hash algorithm. +func hashing(hashAlgorithm string, buffer ...[]byte) (key []byte) { + var hashMap = map[string]hash.Hash{ + "md4": md4.New(), + "md5": md5.New(), + "ripemd-160": ripemd160.New(), + "sha1": sha1.New(), + "sha256": sha256.New(), + "sha384": sha512.New384(), + "sha512": sha512.New(), + } + handler, ok := hashMap[strings.ToLower(hashAlgorithm)] + if !ok { + return key + } + for _, buf := range buffer { + handler.Write(buf) + } + key = handler.Sum(nil) + return key +} + +// createUInt32LEBuffer create buffer with little endian 32-bit unsigned +// integer. +func createUInt32LEBuffer(value int) []byte { + buf := make([]byte, 4) + binary.LittleEndian.PutUint32(buf, uint32(value)) + return buf +} + +// parseEncryptionInfo parse the encryption info XML into an object. +func parseEncryptionInfo(encryptionInfo []byte) (encryption Encryption, err error) { + err = xml.Unmarshal(encryptionInfo, &encryption) + 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) { + block, err := aes.NewCipher(key) + if err != nil { + return input, err + } + stream := cipher.NewCBCDecrypter(block, iv) + stream.CryptBlocks(input, input) + return input, nil +} + +// cryptPackage encrypt / decrypt package by given packageKey and encryption +// info. +func cryptPackage(encrypt bool, packageKey, input []byte, encryption Encryption) (outputChunks []byte, err error) { + encryptedKey := encryption.KeyData + var offset = packageOffset + if encrypt { + offset = 0 + } + var i, start, end int + var iv, outputChunk []byte + for end < len(input) { + start = end + end = start + packageEncryptionChunkSize + + if end > len(input) { + end = len(input) + } + // Grab the next chunk + var inputChunk []byte + if (end + offset) < len(input) { + inputChunk = input[start+offset : end+offset] + } else { + inputChunk = input[start+offset : end] + } + + // Pad the chunk if it is not an integer multiple of the block size + remainder := len(inputChunk) % encryptedKey.BlockSize + if remainder != 0 { + inputChunk = append(inputChunk, make([]byte, encryptedKey.BlockSize-remainder)...) + } + // Create the initialization vector + iv, err = createIV(encrypt, i, 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) + if err != nil { + return + } + outputChunks = append(outputChunks, outputChunk...) + i++ + } + return +} + +// createIV create an initialization vector (IV). +func createIV(encrypt bool, blockKey int, encryption Encryption) ([]byte, error) { + encryptedKey := encryption.KeyData + // Create the block key from the current index + blockKeyBuf := createUInt32LEBuffer(blockKey) + var b bytes.Buffer + saltValue, err := base64.StdEncoding.DecodeString(encryptedKey.SaltValue) + if err != nil { + return nil, err + } + b.Write(saltValue) + b.Write(blockKeyBuf) + // Create the initialization vector by hashing the salt with the block key. + // Truncate or pad as needed to meet the block size. + iv := hashing(encryptedKey.HashAlgorithm, b.Bytes()) + if len(iv) < encryptedKey.BlockSize { + tmp := make([]byte, 0x36) + iv = append(iv, tmp...) + iv = tmp + } else if len(iv) > encryptedKey.BlockSize { + iv = iv[0:encryptedKey.BlockSize] + } + return iv, nil +} diff --git a/excelize.go b/excelize.go index 75b0c135d7..6e2b84993d 100644 --- a/excelize.go +++ b/excelize.go @@ -56,15 +56,30 @@ type File struct { type charsetTranscoderFn func(charset string, input io.Reader) (rdr io.Reader, err error) -// OpenFile take the name of an spreadsheet file and returns a populated -// spreadsheet file struct for it. -func OpenFile(filename string) (*File, error) { +// Options define the options for open spreadsheet. +type Options struct { + Password string +} + +// OpenFile take the name of an spreadsheet file and returns a populated 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 +// } +// +func OpenFile(filename string, opt ...Options) (*File, error) { file, err := os.Open(filename) if err != nil { return nil, err } defer file.Close() - f, err := OpenReader(file) + var option Options + for _, o := range opt { + option = o + } + f, err := OpenReader(file, option) if err != nil { return nil, err } @@ -91,25 +106,23 @@ func newFile() *File { // OpenReader read data stream from io.Reader and return a populated // spreadsheet file. -func OpenReader(r io.Reader) (*File, error) { +func OpenReader(r io.Reader, opt ...Options) (*File, error) { b, err := ioutil.ReadAll(r) if err != nil { return nil, err } - - zr, err := zip.NewReader(bytes.NewReader(b), int64(len(b))) - if err != nil { - identifier := []byte{ - // checking protect workbook by [MS-OFFCRYPTO] - v20181211 3.1 FeatureIdentifier - 0x3c, 0x00, 0x00, 0x00, 0x4d, 0x00, 0x69, 0x00, 0x63, 0x00, 0x72, 0x00, 0x6f, 0x00, 0x73, 0x00, - 0x6f, 0x00, 0x66, 0x00, 0x74, 0x00, 0x2e, 0x00, 0x43, 0x00, 0x6f, 0x00, 0x6e, 0x00, 0x74, 0x00, - 0x61, 0x00, 0x69, 0x00, 0x6e, 0x00, 0x65, 0x00, 0x72, 0x00, 0x2e, 0x00, 0x44, 0x00, 0x61, 0x00, - 0x74, 0x00, 0x61, 0x00, 0x53, 0x00, 0x70, 0x00, 0x61, 0x00, 0x63, 0x00, 0x65, 0x00, 0x73, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + if bytes.Contains(b, cryptoIdentifier) { + var option Options + for _, o := range opt { + option = o } - if bytes.Contains(b, identifier) { - return nil, errors.New("not support encrypted file currently") + b, err = Decrypt(b, &option) + if err != nil { + return nil, fmt.Errorf("decrypted file failed") } + } + zr, err := zip.NewReader(bytes.NewReader(b), int64(len(b))) + if err != nil { return nil, err } diff --git a/excelize_test.go b/excelize_test.go index c2bd3ad828..923e4c5b8a 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -201,14 +201,14 @@ func TestCharsetTranscoder(t *testing.T) { func TestOpenReader(t *testing.T) { _, err := OpenReader(strings.NewReader("")) assert.EqualError(t, err, "zip: not a valid zip file") - _, err = OpenReader(bytes.NewReader([]byte{ - 0x3c, 0x00, 0x00, 0x00, 0x4d, 0x00, 0x69, 0x00, 0x63, 0x00, 0x72, 0x00, 0x6f, 0x00, 0x73, 0x00, - 0x6f, 0x00, 0x66, 0x00, 0x74, 0x00, 0x2e, 0x00, 0x43, 0x00, 0x6f, 0x00, 0x6e, 0x00, 0x74, 0x00, - 0x61, 0x00, 0x69, 0x00, 0x6e, 0x00, 0x65, 0x00, 0x72, 0x00, 0x2e, 0x00, 0x44, 0x00, 0x61, 0x00, - 0x74, 0x00, 0x61, 0x00, 0x53, 0x00, 0x70, 0x00, 0x61, 0x00, 0x63, 0x00, 0x65, 0x00, 0x73, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - })) - assert.EqualError(t, err, "not support encrypted file currently") + _, err = OpenReader(bytes.NewReader(cryptoIdentifier)) + assert.EqualError(t, err, "decrypted file failed") + + f, err := OpenFile(filepath.Join("test", "encryptSHA1.xlsx"), Options{Password: "password"}) + assert.NoError(t, err) + val, err := f.GetCellValue("Sheet1", "A1") + assert.NoError(t, err) + assert.Equal(t, "SECRET", val) // Test unexpected EOF. var b bytes.Buffer diff --git a/go.mod b/go.mod index f94f33beb2..9b013566e4 100644 --- a/go.mod +++ b/go.mod @@ -1,17 +1,14 @@ module github.com/360EntSecGroup-Skylar/excelize/v2 -go 1.12 +go 1.15 require ( - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/kr/text v0.2.0 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 - github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect - github.com/stretchr/testify v1.5.1 - github.com/xuri/efp v0.0.0-20191019043341-b7dc4fe9aa91 - golang.org/x/image v0.0.0-20200430140353-33d19683fad8 - golang.org/x/net v0.0.0-20200506145744-7e3656a0809f - golang.org/x/text v0.3.2 // indirect - gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect - gopkg.in/yaml.v2 v2.2.8 // indirect + github.com/richardlehane/mscfb v1.0.3 + github.com/stretchr/testify v1.6.1 + github.com/xuri/efp v0.0.0-20200605144744-ba689101faaf + golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a + golang.org/x/image v0.0.0-20200801110659-972c09e46d76 + golang.org/x/net v0.0.0-20200822124328-c89045814202 + golang.org/x/text v0.3.3 ) diff --git a/go.sum b/go.sum index 7fa49fe501..bc606dfebf 100644 --- a/go.sum +++ b/go.sum @@ -1,39 +1,35 @@ -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +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/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 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/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 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.3 h1:rD8TBkYWkObWO0oLDFCbwMeZ4KoalxQy+QgniCj3nKI= +github.com/richardlehane/mscfb v1.0.3/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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/xuri/efp v0.0.0-20191019043341-b7dc4fe9aa91 h1:gp02YctZuIPTk0t7qI+wvg3VQwTPyNmSGG6ZqOsjSL8= -github.com/xuri/efp v0.0.0-20191019043341-b7dc4fe9aa91/go.mod h1:uBiSUepVYMhGTfDeBKKasV4GpgBlzJ46gXUBAqV8qLk= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/xuri/efp v0.0.0-20200605144744-ba689101faaf h1:spotWVWg9DP470pPFQ7LaYtUqDpWEOS/BUrSmwFZE4k= +github.com/xuri/efp v0.0.0-20200605144744-ba689101faaf/go.mod h1:uBiSUepVYMhGTfDeBKKasV4GpgBlzJ46gXUBAqV8qLk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/image v0.0.0-20200430140353-33d19683fad8 h1:6WW6V3x1P/jokJBpRQYUJnMHRP6isStQwCozxnU7XQw= -golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f h1:QBjCr1Fz5kw158VqdE9JfI9cJnl/ymnJWAdMuinqL7Y= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM= +golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/image v0.0.0-20200801110659-972c09e46d76 h1:U7GPaoQyQmX+CBRWXKrvRzWTbd+slqeSh8uARsIyhAw= +golang.org/x/image v0.0.0-20200801110659-972c09e46d76/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 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/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +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= diff --git a/test/encryptSHA1.xlsx b/test/encryptSHA1.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..e7bc852fb729473c7c9c033d4aede26e48e8dc21 GIT binary patch literal 14336 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 literal 0 HcmV?d00001 From 98f1a699033b76a1482edc03d533dd1f67bcd2d6 Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 2 Sep 2020 23:14:19 +0800 Subject: [PATCH 277/957] support ECMA-376 document standard encryption, ref #199 --- encrypt.go => crypt.go | 221 +++++++++++++++++++++++++++++++++++++---- drawing.go | 3 - excelize.go | 2 +- excelize_test.go | 10 +- test/encryptAES.xlsx | Bin 0 -> 8192 bytes xmlChart.go | 3 - 6 files changed, 209 insertions(+), 30 deletions(-) rename encrypt.go => crypt.go (62%) create mode 100644 test/encryptAES.xlsx diff --git a/encrypt.go b/crypt.go similarity index 62% rename from encrypt.go rename to crypt.go index e5dc2af143..f8dd597437 100644 --- a/encrypt.go +++ b/crypt.go @@ -20,6 +20,7 @@ import ( "encoding/base64" "encoding/binary" "encoding/xml" + "errors" "hash" "strings" @@ -33,6 +34,7 @@ var ( blockKey = []byte{0x14, 0x6e, 0x0b, 0xe7, 0xab, 0xac, 0xd0, 0xd6} // Block keys used for encryption packageOffset = 8 // First 8 bytes are the size of the stream packageEncryptionChunkSize = 4096 + iterCount = 50000 cryptoIdentifier = []byte{ // checking protect workbook by [MS-OFFCRYPTO] - v20181211 3.1 FeatureIdentifier 0x3c, 0x00, 0x00, 0x00, 0x4d, 0x00, 0x69, 0x00, 0x63, 0x00, 0x72, 0x00, 0x6f, 0x00, 0x73, 0x00, 0x6f, 0x00, 0x66, 0x00, 0x74, 0x00, 0x2e, 0x00, 0x43, 0x00, 0x6f, 0x00, 0x6e, 0x00, 0x74, 0x00, @@ -40,6 +42,9 @@ var ( 0x74, 0x00, 0x61, 0x00, 0x53, 0x00, 0x70, 0x00, 0x61, 0x00, 0x63, 0x00, 0x65, 0x00, 0x73, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, } + oleIdentifier = []byte{ + 0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1, + } ) // Encryption specifies the encryption structure, streams, and storages are @@ -93,37 +98,56 @@ type EncryptedKey struct { KeyData } -// Decrypt API decrypt the CFB file format with Agile Encryption. Support -// cryptographic algorithm: MD4, MD5, RIPEMD-160, SHA1, SHA256, SHA384 and -// SHA512. +// StandardEncryptionHeader structure is used by ECMA-376 document encryption +// [ECMA-376] and Office binary document RC4 CryptoAPI encryption, to specify +// encryption properties for an encrypted stream. +type StandardEncryptionHeader struct { + Flags uint32 + SizeExtra uint32 + AlgID uint32 + AlgIDHash uint32 + KeySize uint32 + ProviderType uint32 + Reserved1 uint32 + Reserved2 uint32 + CspName string +} + +// StandardEncryptionVerifier structure is used by Office Binary Document RC4 +// CryptoAPI Encryption and ECMA-376 Document Encryption. Every usage of this +// structure MUST specify the hashing algorithm and encryption algorithm used +// in the EncryptionVerifier structure. +type StandardEncryptionVerifier struct { + SaltSize uint32 + Salt []byte + EncryptedVerifier []byte + VerifierHashSize uint32 + EncryptedVerifierHash []byte +} + +// Decrypt API decrypt 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) { doc, err := mscfb.New(bytes.NewReader(raw)) if err != nil { return } encryptionInfoBuf, encryptedPackageBuf := extractPart(doc) - var encryptionInfo Encryption - if encryptionInfo, err = parseEncryptionInfo(encryptionInfoBuf[8:]); err != nil { + mechanism, err := encryptionMechanism(encryptionInfoBuf) + if err != nil || mechanism == "extensible" { return } - // Convert the password into an encryption key. - key, err := convertPasswdToKey(opt.Password, encryptionInfo) - if err != nil { - return + switch mechanism { + case "agile": + return agileDecrypt(encryptionInfoBuf, encryptedPackageBuf, opt) + case "standard": + return standardDecrypt(encryptionInfoBuf, encryptedPackageBuf, opt) + default: + err = errors.New("unsupport encryption mechanism") + break } - // Use the key to decrypt the package key. - encryptedKey := encryptionInfo.KeyEncryptors.KeyEncryptor[0].EncryptedKey - saltValue, err := base64.StdEncoding.DecodeString(encryptedKey.SaltValue) - if err != nil { - return - } - encryptedKeyValue, err := base64.StdEncoding.DecodeString(encryptedKey.EncryptedKeyValue) - if err != nil { - return - } - packageKey, err := crypt(false, encryptedKey.CipherAlgorithm, encryptedKey.CipherChaining, key, saltValue, encryptedKeyValue) - // Use the package key to decrypt the package. - return cryptPackage(false, packageKey, encryptedPackageBuf, encryptionInfo) + return } // extractPart extract data from storage by specified part name. @@ -149,6 +173,159 @@ func extractPart(doc *mscfb.Reader) (encryptionInfoBuf, encryptedPackageBuf []by return } +// encryptionMechanism parse password-protected documents created mechanism. +func encryptionMechanism(buffer []byte) (mechanism string, err error) { + if len(buffer) < 4 { + err = errors.New("unknown encryption mechanism") + return + } + versionMajor, versionMinor := binary.LittleEndian.Uint16(buffer[0:2]), binary.LittleEndian.Uint16(buffer[2:4]) + if versionMajor == 4 && versionMinor == 4 { + mechanism = "agile" + return + } else if (2 <= versionMajor && versionMajor <= 4) && versionMinor == 2 { + mechanism = "standard" + return + } else if (versionMajor == 3 || versionMajor == 4) && versionMinor == 3 { + mechanism = "extensible" + } + err = errors.New("unsupport encryption mechanism") + return +} + +// ECMA-376 Standard Encryption + +// standardDecrypt decrypt the CFB file format with ECMA-376 standard encryption. +func standardDecrypt(encryptionInfoBuf, encryptedPackageBuf []byte, opt *Options) ([]byte, error) { + encryptionHeaderSize := binary.LittleEndian.Uint32(encryptionInfoBuf[8:12]) + block := encryptionInfoBuf[12 : 12+encryptionHeaderSize] + header := StandardEncryptionHeader{ + Flags: binary.LittleEndian.Uint32(block[:4]), + SizeExtra: binary.LittleEndian.Uint32(block[4:8]), + AlgID: binary.LittleEndian.Uint32(block[8:12]), + AlgIDHash: binary.LittleEndian.Uint32(block[12:16]), + KeySize: binary.LittleEndian.Uint32(block[16:20]), + ProviderType: binary.LittleEndian.Uint32(block[20:24]), + Reserved1: binary.LittleEndian.Uint32(block[24:28]), + Reserved2: binary.LittleEndian.Uint32(block[28:32]), + CspName: string(block[32:]), + } + block = encryptionInfoBuf[12+encryptionHeaderSize:] + algIDMap := map[uint32]string{ + 0x0000660E: "AES-128", + 0x0000660F: "AES-192", + 0x00006610: "AES-256", + } + algorithm := "AES" + _, ok := algIDMap[header.AlgID] + if !ok { + algorithm = "RC4" + } + verifier := standardEncryptionVerifier(algorithm, block) + secretKey, err := standardConvertPasswdToKey(header, verifier, opt) + if err != nil { + return nil, err + } + // decrypted data + x := encryptedPackageBuf[8:] + blob, err := aes.NewCipher(secretKey) + if err != nil { + return nil, err + } + decrypted := make([]byte, len(x)) + size := 16 + for bs, be := 0, size; bs < len(x); bs, be = bs+size, be+size { + blob.Decrypt(decrypted[bs:be], x[bs:be]) + } + return decrypted, err +} + +// standardEncryptionVerifier extract ECMA-376 standard encryption verifier. +func standardEncryptionVerifier(algorithm string, blob []byte) StandardEncryptionVerifier { + verifier := StandardEncryptionVerifier{ + SaltSize: binary.LittleEndian.Uint32(blob[:4]), + Salt: blob[4:20], + EncryptedVerifier: blob[20:36], + VerifierHashSize: binary.LittleEndian.Uint32(blob[36:40]), + } + if algorithm == "RC4" { + verifier.EncryptedVerifierHash = blob[40:60] + } else if algorithm == "AES" { + verifier.EncryptedVerifierHash = blob[40:72] + } + return verifier +} + +// standardConvertPasswdToKey generate intermediate key from given password. +func standardConvertPasswdToKey(header StandardEncryptionHeader, verifier StandardEncryptionVerifier, opt *Options) ([]byte, error) { + encoder := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder() + passwordBuffer, err := encoder.Bytes([]byte(opt.Password)) + if err != nil { + return nil, err + } + key := hashing("sha1", verifier.Salt, passwordBuffer) + for i := 0; i < iterCount; i++ { + iterator := createUInt32LEBuffer(i) + key = hashing("sha1", iterator, key) + } + var block int + hfinal := hashing("sha1", key, createUInt32LEBuffer(block)) + cbRequiredKeyLength := int(header.KeySize) / 8 + cbHash := sha1.Size + buf1 := bytes.Repeat([]byte{0x36}, 64) + buf1 = append(standardXORBytes(hfinal, buf1[:cbHash]), buf1[cbHash:]...) + x1 := hashing("sha1", buf1) + buf2 := bytes.Repeat([]byte{0x5c}, 64) + buf2 = append(standardXORBytes(hfinal, buf2[:cbHash]), buf2[cbHash:]...) + x2 := hashing("sha1", buf2) + x3 := append(x1, x2...) + keyDerived := x3[:cbRequiredKeyLength] + return keyDerived, err +} + +// standardXORBytes perform XOR operations for two bytes slice. +func standardXORBytes(a, b []byte) []byte { + r := make([][2]byte, len(a), len(a)) + for i, e := range a { + r[i] = [2]byte{e, b[i]} + } + buf := make([]byte, len(a)) + for p, q := range r { + buf[p] = q[0] ^ q[1] + } + return buf +} + +// ECMA-376 Agile Encryption + +// 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) { + var encryptionInfo Encryption + if encryptionInfo, err = parseEncryptionInfo(encryptionInfoBuf[8:]); err != nil { + return + } + // Convert the password into an encryption key. + key, err := convertPasswdToKey(opt.Password, encryptionInfo) + if err != nil { + return + } + // Use the key to decrypt the package key. + encryptedKey := encryptionInfo.KeyEncryptors.KeyEncryptor[0].EncryptedKey + saltValue, err := base64.StdEncoding.DecodeString(encryptedKey.SaltValue) + if err != nil { + return + } + encryptedKeyValue, err := base64.StdEncoding.DecodeString(encryptedKey.EncryptedKeyValue) + if err != nil { + return + } + packageKey, err := crypt(false, encryptedKey.CipherAlgorithm, encryptedKey.CipherChaining, key, saltValue, encryptedKeyValue) + // Use the package key to decrypt the package. + return cryptPackage(false, packageKey, encryptedPackageBuf, encryptionInfo) +} + // convertPasswdToKey convert the password into an encryption key. func convertPasswdToKey(passwd string, encryption Encryption) (key []byte, err error) { var b bytes.Buffer diff --git a/drawing.go b/drawing.go index 806c1b7fa3..6c2f6357f5 100644 --- a/drawing.go +++ b/drawing.go @@ -59,10 +59,7 @@ func (f *File) prepareChartSheetDrawing(xlsx *xlsxChartsheet, drawingID int, she func (f *File) addChart(formatSet *formatChart, comboCharts []*formatChart) { count := f.countCharts() xlsxChartSpace := xlsxChartSpace{ - XMLNSc: NameSpaceDrawingMLChart.Value, XMLNSa: NameSpaceDrawingML.Value, - XMLNSr: SourceRelationship.Value, - XMLNSc16r2: SourceRelationshipChart201506.Value, Date1904: &attrValBool{Val: boolPtr(false)}, Lang: &attrValString{Val: stringPtr("en-US")}, RoundedCorners: &attrValBool{Val: boolPtr(false)}, diff --git a/excelize.go b/excelize.go index 6e2b84993d..2fc48e56a3 100644 --- a/excelize.go +++ b/excelize.go @@ -111,7 +111,7 @@ func OpenReader(r io.Reader, opt ...Options) (*File, error) { if err != nil { return nil, err } - if bytes.Contains(b, cryptoIdentifier) { + if bytes.Contains(b, oleIdentifier) { var option Options for _, o := range opt { option = o diff --git a/excelize_test.go b/excelize_test.go index 923e4c5b8a..f1cd65207e 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -201,15 +201,23 @@ func TestCharsetTranscoder(t *testing.T) { func TestOpenReader(t *testing.T) { _, err := OpenReader(strings.NewReader("")) assert.EqualError(t, err, "zip: not a valid zip file") - _, err = OpenReader(bytes.NewReader(cryptoIdentifier)) + _, err = OpenReader(bytes.NewReader(oleIdentifier)) assert.EqualError(t, err, "decrypted file failed") + // 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") assert.NoError(t, err) assert.Equal(t, "SECRET", val) + // 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") + assert.NoError(t, err) + assert.Equal(t, "SECRET", val) + // Test unexpected EOF. var b bytes.Buffer w := gzip.NewWriter(&b) diff --git a/test/encryptAES.xlsx b/test/encryptAES.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..27a595adbc1467b84658b81fecc897c4886806aa GIT binary patch literal 8192 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 F{{;@$HA4UZ literal 0 HcmV?d00001 diff --git a/xmlChart.go b/xmlChart.go index fae54263d9..c95393a334 100644 --- a/xmlChart.go +++ b/xmlChart.go @@ -18,10 +18,7 @@ import "encoding/xml" // charts, pie charts, scatter charts, or other types of charts. type xlsxChartSpace struct { XMLName xml.Name `xml:"http://schemas.openxmlformats.org/drawingml/2006/chart chartSpace"` - XMLNSc string `xml:"xmlns:c,attr"` XMLNSa string `xml:"xmlns:a,attr"` - XMLNSr string `xml:"xmlns:r,attr"` - XMLNSc16r2 string `xml:"xmlns:c16r2,attr"` Date1904 *attrValBool `xml:"date1904"` Lang *attrValString `xml:"lang"` RoundedCorners *attrValBool `xml:"roundedCorners"` From 1111de2fdb7da9aa9f039f5173a6dafa6d98029c Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 3 Sep 2020 23:44:43 +0800 Subject: [PATCH 278/957] improve compatibility for phonetic hint and sheet tab color --- xmlComments.go | 2 +- xmlSharedStrings.go | 6 ++++-- xmlWorksheet.go | 15 ++++++++++----- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/xmlComments.go b/xmlComments.go index 0f02b18813..f2b03a1e7a 100644 --- a/xmlComments.go +++ b/xmlComments.go @@ -69,7 +69,7 @@ type xlsxText struct { type xlsxPhoneticRun struct { Sb uint32 `xml:"sb,attr"` Eb uint32 `xml:"eb,attr"` - T string `xml:"t,attr"` + T string `xml:"t"` } // formatComment directly maps the format settings of the comment. diff --git a/xmlSharedStrings.go b/xmlSharedStrings.go index d5fe4a7237..f59119f5ee 100644 --- a/xmlSharedStrings.go +++ b/xmlSharedStrings.go @@ -38,8 +38,10 @@ type xlsxSST struct { // level - then the string item shall consist of multiple rich text runs which // collectively are used to express the string. type xlsxSI struct { - T *xlsxT `xml:"t,omitempty"` - R []xlsxR `xml:"r"` + T *xlsxT `xml:"t,omitempty"` + R []xlsxR `xml:"r"` + RPh []*xlsxPhoneticRun `xml:"rPh"` + PhoneticPr *xlsxPhoneticPr `xml:"phoneticPr"` } // String extracts characters from a string item. diff --git a/xmlWorksheet.go b/xmlWorksheet.go index bc81b0313f..0eaa8ee16b 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -234,11 +234,11 @@ type xlsxSheetPr struct { SyncVertical bool `xml:"syncVertical,attr,omitempty"` SyncRef string `xml:"syncRef,attr,omitempty"` TransitionEvaluation bool `xml:"transitionEvaluation,attr,omitempty"` + TransitionEntry bool `xml:"transitionEntry,attr,omitempty"` Published *bool `xml:"published,attr"` CodeName string `xml:"codeName,attr,omitempty"` FilterMode bool `xml:"filterMode,attr,omitempty"` EnableFormatConditionsCalculation *bool `xml:"enableFormatConditionsCalculation,attr"` - TransitionEntry bool `xml:"transitionEntry,attr,omitempty"` TabColor *xlsxTabColor `xml:"tabColor,omitempty"` OutlinePr *xlsxOutlinePr `xml:"outlinePr,omitempty"` PageSetUpPr *xlsxPageSetUpPr `xml:"pageSetUpPr,omitempty"` @@ -247,7 +247,10 @@ type xlsxSheetPr struct { // xlsxOutlinePr maps to the outlinePr element. SummaryBelow allows you to // adjust the direction of grouper controls. type xlsxOutlinePr struct { - SummaryBelow bool `xml:"summaryBelow,attr"` + ApplyStyles *bool `xml:"applyStyles,attr"` + SummaryBelow bool `xml:"summaryBelow,attr,omitempty"` + SummaryRight bool `xml:"summaryRight,attr,omitempty"` + ShowOutlineSymbols bool `xml:"showOutlineSymbols,attr,omitempty"` } // xlsxPageSetUpPr expresses page setup properties of the worksheet. @@ -258,9 +261,11 @@ type xlsxPageSetUpPr struct { // xlsxTabColor represents background color of the sheet tab. type xlsxTabColor struct { - RGB string `xml:"rgb,attr,omitempty"` - Theme int `xml:"theme,attr,omitempty"` - Tint float64 `xml:"tint,attr,omitempty"` + 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 From 01afc6e03f1d28f1806ecd1f3c6c043f6755bd01 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 6 Sep 2020 18:06:59 +0800 Subject: [PATCH 279/957] init ECMA-376 agile encryption support --- crypt.go | 181 +++++++++++++++++++++++++++++++++++++++++++++----- crypt_test.go | 23 +++++++ excelize.go | 14 ++-- file.go | 18 ++++- 4 files changed, 208 insertions(+), 28 deletions(-) create mode 100644 crypt_test.go diff --git a/crypt.go b/crypt.go index f8dd597437..8ae8332945 100644 --- a/crypt.go +++ b/crypt.go @@ -13,6 +13,7 @@ import ( "bytes" "crypto/aes" "crypto/cipher" + "crypto/hmac" "crypto/md5" "crypto/sha1" "crypto/sha256" @@ -22,6 +23,8 @@ import ( "encoding/xml" "errors" "hash" + "math/rand" + "reflect" "strings" "github.com/richardlehane/mscfb" @@ -32,7 +35,11 @@ import ( var ( blockKey = []byte{0x14, 0x6e, 0x0b, 0xe7, 0xab, 0xac, 0xd0, 0xd6} // Block keys used for encryption - packageOffset = 8 // First 8 bytes are the size of the stream + 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 iterCount = 50000 cryptoIdentifier = []byte{ // checking protect workbook by [MS-OFFCRYPTO] - v20181211 3.1 FeatureIdentifier @@ -50,6 +57,7 @@ var ( // Encryption specifies the encryption structure, streams, and storages are // required when encrypting ECMA-376 documents. type Encryption struct { + XMLName xml.Name `xml:"encryption"` KeyData KeyData `xml:"keyData"` DataIntegrity DataIntegrity `xml:"dataIntegrity"` KeyEncryptors KeyEncryptors `xml:"keyEncryptors"` @@ -150,6 +158,125 @@ func Decrypt(raw []byte, opt *Options) (packageBuf []byte, err error) { return } +// 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 + } + // Use the package key and the IV to encrypt the HMAC key. + encryptedHmacKey, err := 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, err := 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, err := 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) + 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 + } + // TODO: Create a new CFB. + _, _ = encryptedPackage, encryptionInfoBuffer + err = errors.New("not support encryption currently") + return +} + // extractPart extract data from storage by specified part name. func extractPart(doc *mscfb.Reader) (encryptionInfoBuf, encryptedPackageBuf []byte) { for entry, err := doc.Next(); err == nil; entry, err = doc.Next() { @@ -265,11 +392,11 @@ func standardConvertPasswdToKey(header StandardEncryptionHeader, verifier Standa } key := hashing("sha1", verifier.Salt, passwordBuffer) for i := 0; i < iterCount; i++ { - iterator := createUInt32LEBuffer(i) + iterator := createUInt32LEBuffer(i, 4) key = hashing("sha1", iterator, key) } var block int - hfinal := hashing("sha1", key, createUInt32LEBuffer(block)) + hfinal := hashing("sha1", key, createUInt32LEBuffer(block, 4)) cbRequiredKeyLength := int(header.KeySize) / 8 cbHash := sha1.Size buf1 := bytes.Repeat([]byte{0x36}, 64) @@ -299,15 +426,14 @@ func standardXORBytes(a, b []byte) []byte { // ECMA-376 Agile Encryption // agileDecrypt decrypt the CFB file format with ECMA-376 agile encryption. -// Support cryptographic algorithm: MD4, MD5, RIPEMD-160, SHA1, SHA256, SHA384 -// and SHA512. +// Support cryptographic algorithm: MD4, MD5, RIPEMD-160, SHA1, SHA256, SHA384 and SHA512. func agileDecrypt(encryptionInfoBuf, encryptedPackageBuf []byte, opt *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, encryptionInfo) + key, err := convertPasswdToKey(opt.Password, blockKey, encryptionInfo) if err != nil { return } @@ -327,7 +453,7 @@ func agileDecrypt(encryptionInfoBuf, encryptedPackageBuf []byte, opt *Options) ( } // convertPasswdToKey convert the password into an encryption key. -func convertPasswdToKey(passwd string, encryption Encryption) (key []byte, err error) { +func convertPasswdToKey(passwd string, blockKey []byte, encryption Encryption) (key []byte, err error) { var b bytes.Buffer saltValue, err := base64.StdEncoding.DecodeString(encryption.KeyEncryptors.KeyEncryptor[0].EncryptedKey.SaltValue) if err != nil { @@ -344,7 +470,7 @@ func convertPasswdToKey(passwd string, encryption Encryption) (key []byte, err e key = hashing(encryption.KeyData.HashAlgorithm, b.Bytes()) // Now regenerate until spin count. for i := 0; i < encryption.KeyEncryptors.KeyEncryptor[0].EncryptedKey.SpinCount; i++ { - iterator := createUInt32LEBuffer(i) + iterator := createUInt32LEBuffer(i, 4) key = hashing(encryption.KeyData.HashAlgorithm, iterator, key) } // Now generate the final hash. @@ -385,8 +511,8 @@ func hashing(hashAlgorithm string, buffer ...[]byte) (key []byte) { // createUInt32LEBuffer create buffer with little endian 32-bit unsigned // integer. -func createUInt32LEBuffer(value int) []byte { - buf := make([]byte, 4) +func createUInt32LEBuffer(value int, bufferSize int) []byte { + buf := make([]byte, bufferSize) binary.LittleEndian.PutUint32(buf, uint32(value)) return buf } @@ -404,7 +530,12 @@ func crypt(encrypt bool, cipherAlgorithm, cipherChaining string, key, iv, input if err != nil { return input, err } - stream := cipher.NewCBCDecrypter(block, iv) + var stream cipher.BlockMode + if encrypt { + stream = cipher.NewCBCEncrypter(block, iv) + } else { + stream = cipher.NewCBCDecrypter(block, iv) + } stream.CryptBlocks(input, input) return input, nil } @@ -440,7 +571,7 @@ func cryptPackage(encrypt bool, packageKey, input []byte, encryption Encryption) inputChunk = append(inputChunk, make([]byte, encryptedKey.BlockSize-remainder)...) } // Create the initialization vector - iv, err = createIV(encrypt, i, encryption) + iv, err = createIV(i, encryption) if err != nil { return } @@ -452,24 +583,29 @@ func cryptPackage(encrypt bool, packageKey, input []byte, encryption Encryption) outputChunks = append(outputChunks, outputChunk...) i++ } + if encrypt { + outputChunks = append(createUInt32LEBuffer(len(input), 8), outputChunks...) + } return } // createIV create an initialization vector (IV). -func createIV(encrypt bool, blockKey int, encryption Encryption) ([]byte, error) { +func createIV(blockKey interface{}, encryption Encryption) ([]byte, error) { encryptedKey := encryption.KeyData // Create the block key from the current index - blockKeyBuf := createUInt32LEBuffer(blockKey) - var b bytes.Buffer + var blockKeyBuf []byte + if reflect.TypeOf(blockKey).Kind() == reflect.Int { + blockKeyBuf = createUInt32LEBuffer(blockKey.(int), 4) + } else { + blockKeyBuf = blockKey.([]byte) + } saltValue, err := base64.StdEncoding.DecodeString(encryptedKey.SaltValue) if err != nil { return nil, err } - b.Write(saltValue) - b.Write(blockKeyBuf) // Create the initialization vector by hashing the salt with the block key. // Truncate or pad as needed to meet the block size. - iv := hashing(encryptedKey.HashAlgorithm, b.Bytes()) + iv := hashing(encryptedKey.HashAlgorithm, append(saltValue, blockKeyBuf...)) if len(iv) < encryptedKey.BlockSize { tmp := make([]byte, 0x36) iv = append(iv, tmp...) @@ -479,3 +615,12 @@ func createIV(encrypt bool, blockKey int, encryption Encryption) ([]byte, error) } return iv, nil } + +// randomBytes returns securely generated random bytes. It will return an error if the system's +// secure random number generator fails to function correctly, in which case the caller should not +// continue. +func randomBytes(n int) ([]byte, error) { + b := make([]byte, n) + _, err := rand.Read(b) + return b, err +} diff --git a/crypt_test.go b/crypt_test.go new file mode 100644 index 0000000000..c6acb38ba8 --- /dev/null +++ b/crypt_test.go @@ -0,0 +1,23 @@ +// Copyright 2016 - 2020 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. +// +// Package excelize providing a set of functions that allow you to write to +// and read from XLSX files. Support reads and writes XLSX file generated by +// Microsoft Excel™ 2007 and later. Support save file without losing original +// charts of XLSX. This library needs Go version 1.10 or later. + +package excelize + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestEncrypt(t *testing.T) { + f, err := OpenFile(filepath.Join("test", "encryptSHA1.xlsx"), Options{Password: "password"}) + assert.NoError(t, err) + assert.EqualError(t, f.SaveAs(filepath.Join("test", "TestEncrypt.xlsx"), Options{Password: "password"}), "not support encryption currently") +} diff --git a/excelize.go b/excelize.go index 2fc48e56a3..315f41bf5b 100644 --- a/excelize.go +++ b/excelize.go @@ -32,6 +32,7 @@ import ( // File define a populated spreadsheet file struct. type File struct { sync.Mutex + options *Options xmlAttr map[string][]xml.Attr checked map[string]bool sheetMap map[string]string @@ -75,11 +76,7 @@ func OpenFile(filename string, opt ...Options) (*File, error) { return nil, err } defer file.Close() - var option Options - for _, o := range opt { - option = o - } - f, err := OpenReader(file, option) + f, err := OpenReader(file, opt...) if err != nil { return nil, err } @@ -111,12 +108,12 @@ func OpenReader(r io.Reader, opt ...Options) (*File, error) { if err != nil { return nil, err } + f := newFile() if bytes.Contains(b, oleIdentifier) { - var option Options for _, o := range opt { - option = o + f.options = &o } - b, err = Decrypt(b, &option) + b, err = Decrypt(b, f.options) if err != nil { return nil, fmt.Errorf("decrypted file failed") } @@ -130,7 +127,6 @@ func OpenReader(r io.Reader, opt ...Options) (*File, error) { if err != nil { return nil, err } - f := newFile() f.SheetCount, f.XLSX = sheetCount, file f.CalcChain = f.calcChainReader() f.sheetMap = f.getSheetMap() diff --git a/file.go b/file.go index 34ec359114..bd05bc49e9 100644 --- a/file.go +++ b/file.go @@ -64,7 +64,7 @@ func (f *File) Save() error { // SaveAs provides a function to create or update to an xlsx file at the // provided path. -func (f *File) SaveAs(name string) error { +func (f *File) SaveAs(name string, opt ...Options) error { if len(name) > FileNameLength { return errors.New("file name length exceeds maximum limit") } @@ -73,6 +73,9 @@ func (f *File) SaveAs(name string) error { return err } defer file.Close() + for _, o := range opt { + f.options = &o + } return f.Write(file) } @@ -118,5 +121,18 @@ func (f *File) WriteToBuffer() (*bytes.Buffer, error) { return buf, err } } + + if f.options != nil { + if err := zw.Close(); err != nil { + return buf, err + } + b, err := Encrypt(buf.Bytes(), f.options) + if err != nil { + return buf, err + } + buf.Reset() + buf.Write(b) + return buf, nil + } return buf, zw.Close() } From 97bffe608dfefe69a64fb450ae654ca798c710db Mon Sep 17 00:00:00 2001 From: Eugene Androsov <53434131+EugeneAndrosovPaser@users.noreply.github.com> Date: Thu, 10 Sep 2020 19:45:52 +0300 Subject: [PATCH 280/957] Extend pivot table funtionality (#692) Add different pivot options Add header options to pivot table opts Add Style name options to pivot table opts --- pivotTable.go | 122 ++++++++++++++++++++++++++++++++++++----------- xmlPivotCache.go | 14 ++++++ xmlPivotTable.go | 18 +++---- 3 files changed, 117 insertions(+), 37 deletions(-) diff --git a/pivotTable.go b/pivotTable.go index f820d76026..3153f6eec0 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -21,12 +21,25 @@ import ( // PivotTableOption directly maps the format settings of the pivot table. type PivotTableOption struct { - DataRange string - PivotTableRange string - Rows []PivotTableField - Columns []PivotTableField - Data []PivotTableField - Filter []PivotTableField + 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 + ShowRowHeaders bool + ShowColHeaders bool + ShowRowStripes bool + ShowColStripes bool + ShowLastColumn bool + PivotTableStyleName string } // PivotTableField directly maps the field settings of the pivot table. @@ -49,9 +62,10 @@ 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 { - Data string - Name string - Subtotal string + Data string + Name string + Subtotal string + DefaultSubtotal bool } // AddPivotTable provides the method to add pivot table by given pivot table @@ -233,12 +247,25 @@ func (f *File) addPivotCache(pivotCacheID int, pivotCacheXML string, opt *PivotT }, CacheFields: &xlsxCacheFields{}, } + for _, name := range order { + defaultRowsSubtotal, rowOk := f.getPivotTableFieldNameDefaultSubtotal(name, opt.Rows) + defaultColumnsSubtotal, colOk := f.getPivotTableFieldNameDefaultSubtotal(name, opt.Columns) + sharedItems := xlsxSharedItems{ + Count: 0, + } + s := xlsxString{} + if (rowOk && !defaultRowsSubtotal) || (colOk && !defaultColumnsSubtotal) { + s = xlsxString{ + V: "", + } + sharedItems.Count++ + sharedItems.S = &s + } + pc.CacheFields.CacheField = append(pc.CacheFields.CacheField, &xlsxCacheField{ - Name: name, - SharedItems: &xlsxSharedItems{ - Count: 0, - }, + Name: name, + SharedItems: &sharedItems, }) } pc.CacheFields.Count = len(pc.CacheFields.CacheField) @@ -259,10 +286,24 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op hcell, _ := CoordinatesToCellName(coordinates[0], coordinates[1]) vcell, _ := CoordinatesToCellName(coordinates[2], coordinates[3]) + pivotTableStyle := func() string { + if opt.PivotTableStyleName == "" { + return "PivotStyleLight16" + } else { + return opt.PivotTableStyleName + } + } pt := xlsxPivotTableDefinition{ - Name: fmt.Sprintf("Pivot Table%d", pivotTableID), - CacheID: cacheID, - DataCaption: "Values", + Name: fmt.Sprintf("Pivot Table%d", pivotTableID), + CacheID: cacheID, + RowGrandTotals: &opt.RowGrandTotals, + ColGrandTotals: &opt.ColGrandTotals, + ShowDrill: &opt.ShowDrill, + UseAutoFormatting: &opt.UseAutoFormatting, + PageOverThenDown: &opt.PageOverThenDown, + MergeItem: &opt.MergeItem, + CompactData: &opt.CompactData, + DataCaption: "Values", Location: &xlsxLocation{ Ref: hcell + ":" + vcell, FirstDataCol: 1, @@ -283,10 +324,12 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op I: []*xlsxI{{}}, }, PivotTableStyleInfo: &xlsxPivotTableStyleInfo{ - Name: "PivotStyleLight16", - ShowRowHeaders: true, - ShowColHeaders: true, - ShowLastColumn: true, + Name: pivotTableStyle(), + ShowRowHeaders: opt.ShowRowHeaders, + ShowColHeaders: opt.ShowColHeaders, + ShowRowStripes: opt.ShowRowStripes, + ShowColStripes: opt.ShowColStripes, + ShowLastColumn: opt.ShowLastColumn, }, } @@ -440,17 +483,25 @@ func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opt *PivotTableOptio if err != nil { return err } + x := 0 for _, name := range order { if inPivotTableField(opt.Rows, name) != -1 { + defaultSubtotal, ok := f.getPivotTableFieldNameDefaultSubtotal(name, opt.Rows) + var items []*xlsxItem + if !ok || !defaultSubtotal { + items = append(items, &xlsxItem{X: &x}) + } else { + items = append(items, &xlsxItem{T: "default"}) + } + pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, &xlsxPivotField{ Axis: "axisRow", Name: f.getPivotTableFieldName(name, opt.Rows), Items: &xlsxItems{ - Count: 1, - Item: []*xlsxItem{ - {T: "default"}, - }, + Count: len(items), + Item: items, }, + DefaultSubtotal: &defaultSubtotal, }) continue } @@ -468,15 +519,21 @@ func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opt *PivotTableOptio continue } if inPivotTableField(opt.Columns, name) != -1 { + defaultSubtotal, ok := f.getPivotTableFieldNameDefaultSubtotal(name, opt.Columns) + var items []*xlsxItem + if !ok || !defaultSubtotal { + items = append(items, &xlsxItem{X: &x}) + } else { + items = append(items, &xlsxItem{T: "default"}) + } pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, &xlsxPivotField{ Axis: "axisCol", Name: f.getPivotTableFieldName(name, opt.Columns), Items: &xlsxItems{ - Count: 1, - Item: []*xlsxItem{ - {T: "default"}, - }, + Count: len(items), + Item: items, }, + DefaultSubtotal: &defaultSubtotal, }) continue } @@ -574,6 +631,15 @@ func (f *File) getPivotTableFieldName(name string, fields []PivotTableField) str return "" } +func (f *File) getPivotTableFieldNameDefaultSubtotal(name string, fields []PivotTableField) (bool, bool) { + for _, field := range fields { + if field.Data == name { + return field.DefaultSubtotal, true + } + } + return false, false +} + // addWorkbookPivotCache add the association ID of the pivot cache in xl/workbook.xml. func (f *File) addWorkbookPivotCache(RID int) int { wb := f.workbookReader() diff --git a/xmlPivotCache.go b/xmlPivotCache.go index feaec54f03..58d977af3a 100644 --- a/xmlPivotCache.go +++ b/xmlPivotCache.go @@ -182,6 +182,20 @@ type xlsxError struct { // xlsxString represents a character value in a PivotTable. type xlsxString struct { + V string `xml:"v,attr"` + U bool `xml:"u,attr,omitempty"` + F bool `xml:"f,attr,omitempty"` + C string `xml:"c,attr,omitempty"` + Cp int `xml:"cp,attr,omitempty"` + In int `xml:"in,attr,omitempty"` + Bc string `xml:"bc,attr,omitempty"` + Fc string `xml:"fc,attr,omitempty"` + I bool `xml:"i,attr,omitempty"` + Un bool `xml:"un,attr,omitempty"` + St bool `xml:"st,attr,omitempty"` + B bool `xml:"b,attr,omitempty"` + Tpls *xlsxTuples `xml:"tpls"` + X *attrValInt `xml:"x"` } // xlsxDateTime represents a date-time value in the PivotTable. diff --git a/xmlPivotTable.go b/xmlPivotTable.go index 657a9e8c34..dc8b76538c 100644 --- a/xmlPivotTable.go +++ b/xmlPivotTable.go @@ -48,7 +48,7 @@ type xlsxPivotTableDefinition struct { VisualTotals bool `xml:"visualTotals,attr,omitempty"` ShowMultipleLabel bool `xml:"showMultipleLabel,attr,omitempty"` ShowDataDropDown bool `xml:"showDataDropDown,attr,omitempty"` - ShowDrill bool `xml:"showDrill,attr,omitempty"` + ShowDrill *bool `xml:"showDrill,attr,omitempty"` PrintDrill bool `xml:"printDrill,attr,omitempty"` ShowMemberPropertyTips bool `xml:"showMemberPropertyTips,attr,omitempty"` ShowDataTips bool `xml:"showDataTips,attr,omitempty"` @@ -56,15 +56,15 @@ type xlsxPivotTableDefinition struct { EnableDrill bool `xml:"enableDrill,attr,omitempty"` EnableFieldProperties bool `xml:"enableFieldProperties,attr,omitempty"` PreserveFormatting bool `xml:"preserveFormatting,attr,omitempty"` - UseAutoFormatting bool `xml:"useAutoFormatting,attr,omitempty"` + UseAutoFormatting *bool `xml:"useAutoFormatting,attr,omitempty"` PageWrap int `xml:"pageWrap,attr,omitempty"` - PageOverThenDown bool `xml:"pageOverThenDown,attr,omitempty"` + PageOverThenDown *bool `xml:"pageOverThenDown,attr,omitempty"` SubtotalHiddenItems bool `xml:"subtotalHiddenItems,attr,omitempty"` - RowGrandTotals bool `xml:"rowGrandTotals,attr,omitempty"` - ColGrandTotals bool `xml:"colGrandTotals,attr,omitempty"` + RowGrandTotals *bool `xml:"rowGrandTotals,attr,omitempty"` + ColGrandTotals *bool `xml:"colGrandTotals,attr,omitempty"` FieldPrintTitles bool `xml:"fieldPrintTitles,attr,omitempty"` ItemPrintTitles bool `xml:"itemPrintTitles,attr,omitempty"` - MergeItem bool `xml:"mergeItem,attr,omitempty"` + MergeItem *bool `xml:"mergeItem,attr,omitempty"` ShowDropZones bool `xml:"showDropZones,attr,omitempty"` CreatedVersion int `xml:"createdVersion,attr,omitempty"` Indent int `xml:"indent,attr,omitempty"` @@ -74,7 +74,7 @@ type xlsxPivotTableDefinition struct { Compact bool `xml:"compact,attr"` Outline bool `xml:"outline,attr"` OutlineData bool `xml:"outlineData,attr,omitempty"` - CompactData bool `xml:"compactData,attr,omitempty"` + CompactData *bool `xml:"compactData,attr,omitempty"` Published bool `xml:"published,attr,omitempty"` GridDropZones bool `xml:"gridDropZones,attr,omitempty"` Immersive bool `xml:"immersive,attr,omitempty"` @@ -150,7 +150,7 @@ type xlsxPivotField struct { DataSourceSort bool `xml:"dataSourceSort,attr,omitempty"` NonAutoSortDefault bool `xml:"nonAutoSortDefault,attr,omitempty"` RankBy int `xml:"rankBy,attr,omitempty"` - DefaultSubtotal bool `xml:"defaultSubtotal,attr,omitempty"` + DefaultSubtotal *bool `xml:"defaultSubtotal,attr,omitempty"` SumSubtotal bool `xml:"sumSubtotal,attr,omitempty"` CountASubtotal bool `xml:"countASubtotal,attr,omitempty"` AvgSubtotal bool `xml:"avgSubtotal,attr,omitempty"` @@ -189,7 +189,7 @@ type xlsxItem struct { F bool `xml:"f,attr,omitempty"` M bool `xml:"m,attr,omitempty"` C bool `xml:"c,attr,omitempty"` - X int `xml:"x,attr,omitempty"` + X *int `xml:"x,attr,omitempty"` D bool `xml:"d,attr,omitempty"` E bool `xml:"e,attr,omitempty"` } From 96917e4617c9e7eb15c0ee1723a042f169321430 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 15 Sep 2020 23:31:24 +0800 Subject: [PATCH 281/957] Update docs and test case for the pivot table --- pivotTable.go | 13 +++-- pivotTable_test.go | 120 +++++++++++++++++++++++++++++++++------------ 2 files changed, 98 insertions(+), 35 deletions(-) diff --git a/pivotTable.go b/pivotTable.go index 3153f6eec0..0c1dd614ac 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -101,10 +101,16 @@ type PivotTableField struct { // if err := f.AddPivotTable(&excelize.PivotTableOption{ // DataRange: "Sheet1!$A$1:$E$31", // PivotTableRange: "Sheet1!$G$2:$M$34", -// Rows: []excelize.PivotTableField{{Data: "Month"}, {Data: "Year"}}, +// Rows: []excelize.PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, // Filter: []excelize.PivotTableField{{Data: "Region"}}, -// Columns: []excelize.PivotTableField{{Data: "Type"}}, +// 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) // } @@ -289,9 +295,8 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op pivotTableStyle := func() string { if opt.PivotTableStyleName == "" { return "PivotStyleLight16" - } else { - return opt.PivotTableStyleName } + return opt.PivotTableStyleName } pt := xlsxPivotTableDefinition{ Name: fmt.Sprintf("Pivot Table%d", pivotTableID), diff --git a/pivotTable_test.go b/pivotTable_test.go index cc80835b18..6e736409c2 100644 --- a/pivotTable_test.go +++ b/pivotTable_test.go @@ -28,54 +28,112 @@ func TestAddPivotTable(t *testing.T) { assert.NoError(t, f.AddPivotTable(&PivotTableOption{ DataRange: "Sheet1!$A$1:$E$31", PivotTableRange: "Sheet1!$G$2:$M$34", - Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}}, + Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, Filter: []PivotTableField{{Data: "Region"}}, - Columns: []PivotTableField{{Data: "Type"}}, + Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, Data: []PivotTableField{{Data: "Sales", Subtotal: "Sum", Name: "Summarize by Sum"}}, + RowGrandTotals: true, + ColGrandTotals: true, + ShowDrill: true, + ShowRowHeaders: true, + ShowColHeaders: true, + ShowLastColumn: true, })) // Use different order of coordinate tests assert.NoError(t, f.AddPivotTable(&PivotTableOption{ DataRange: "Sheet1!$A$1:$E$31", PivotTableRange: "Sheet1!$U$34:$O$2", - Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}}, - Columns: []PivotTableField{{Data: "Type"}}, + Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, + Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, Data: []PivotTableField{{Data: "Sales", Subtotal: "Average", Name: "Summarize by Average"}}, + RowGrandTotals: true, + ColGrandTotals: true, + ShowDrill: true, + ShowRowHeaders: true, + ShowColHeaders: true, + ShowLastColumn: true, })) assert.NoError(t, f.AddPivotTable(&PivotTableOption{ DataRange: "Sheet1!$A$1:$E$31", PivotTableRange: "Sheet1!$W$2:$AC$34", - Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}}, + Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, Columns: []PivotTableField{{Data: "Region"}}, Data: []PivotTableField{{Data: "Sales", Subtotal: "Count", Name: "Summarize by Count"}}, + RowGrandTotals: true, + ColGrandTotals: true, + ShowDrill: true, + ShowRowHeaders: true, + ShowColHeaders: true, + ShowLastColumn: true, })) assert.NoError(t, f.AddPivotTable(&PivotTableOption{ DataRange: "Sheet1!$A$1:$E$31", PivotTableRange: "Sheet1!$G$37:$W$50", Rows: []PivotTableField{{Data: "Month"}}, - Columns: []PivotTableField{{Data: "Region"}, {Data: "Year"}}, + Columns: []PivotTableField{{Data: "Region", DefaultSubtotal: true}, {Data: "Year"}}, Data: []PivotTableField{{Data: "Sales", Subtotal: "CountNums", Name: "Summarize by CountNums"}}, + RowGrandTotals: true, + ColGrandTotals: true, + ShowDrill: true, + ShowRowHeaders: true, + ShowColHeaders: true, + ShowLastColumn: true, })) assert.NoError(t, f.AddPivotTable(&PivotTableOption{ DataRange: "Sheet1!$A$1:$E$31", PivotTableRange: "Sheet1!$AE$2:$AG$33", - Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}}, + Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, Data: []PivotTableField{{Data: "Sales", Subtotal: "Max", Name: "Summarize by Max"}}, + RowGrandTotals: true, + ColGrandTotals: true, + ShowDrill: true, + ShowRowHeaders: true, + ShowColHeaders: true, + ShowLastColumn: true, + })) + // Create pivot table with empty subtotal field name and specified style + assert.NoError(t, f.AddPivotTable(&PivotTableOption{ + DataRange: "Sheet1!$A$1:$E$31", + PivotTableRange: "Sheet1!$AJ$2:$AP1$35", + Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, + Filter: []PivotTableField{{Data: "Region"}}, + Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, + Data: []PivotTableField{{Subtotal: "Sum", Name: "Summarize by Sum"}}, + RowGrandTotals: true, + ColGrandTotals: true, + ShowDrill: true, + ShowRowHeaders: true, + ShowColHeaders: true, + ShowLastColumn: true, + PivotTableStyleName: "PivotStyleLight19", })) f.NewSheet("Sheet2") assert.NoError(t, f.AddPivotTable(&PivotTableOption{ DataRange: "Sheet1!$A$1:$E$31", PivotTableRange: "Sheet2!$A$1:$AR$15", Rows: []PivotTableField{{Data: "Month"}}, - Columns: []PivotTableField{{Data: "Region"}, {Data: "Type"}, {Data: "Year"}}, + Columns: []PivotTableField{{Data: "Region", DefaultSubtotal: true}, {Data: "Type", DefaultSubtotal: true}, {Data: "Year"}}, Data: []PivotTableField{{Data: "Sales", Subtotal: "Min", Name: "Summarize by Min"}}, + RowGrandTotals: true, + ColGrandTotals: true, + ShowDrill: true, + ShowRowHeaders: true, + ShowColHeaders: true, + ShowLastColumn: true, })) assert.NoError(t, f.AddPivotTable(&PivotTableOption{ DataRange: "Sheet1!$A$1:$E$31", PivotTableRange: "Sheet2!$A$18:$AR$54", - Rows: []PivotTableField{{Data: "Month"}, {Data: "Type"}}, - Columns: []PivotTableField{{Data: "Region"}, {Data: "Year"}}, + Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Type"}}, + Columns: []PivotTableField{{Data: "Region", DefaultSubtotal: true}, {Data: "Year"}}, Data: []PivotTableField{{Data: "Sales", Subtotal: "Product", Name: "Summarize by Product"}}, + RowGrandTotals: true, + ColGrandTotals: true, + ShowDrill: true, + ShowRowHeaders: true, + ShowColHeaders: true, + ShowLastColumn: true, })) // Test empty pivot table options @@ -84,56 +142,56 @@ func TestAddPivotTable(t *testing.T) { assert.EqualError(t, f.AddPivotTable(&PivotTableOption{ DataRange: "Sheet1!$A$1:$A$1", PivotTableRange: "Sheet1!$U$34:$O$2", - Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}}, - Columns: []PivotTableField{{Data: "Type"}}, + Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, + Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, 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{ DataRange: "$A$1:$E$31", PivotTableRange: "Sheet1!$U$34:$O$2", - Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}}, - Columns: []PivotTableField{{Data: "Type"}}, + Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, + Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, 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{ DataRange: "SheetN!$A$1:$E$31", PivotTableRange: "Sheet1!$U$34:$O$2", - Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}}, - Columns: []PivotTableField{{Data: "Type"}}, + Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, + Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, Data: []PivotTableField{{Data: "Sales"}}, }), "sheet SheetN is 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", PivotTableRange: "$U$34:$O$2", - Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}}, - Columns: []PivotTableField{{Data: "Type"}}, + Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, + Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, 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{ DataRange: "Sheet1!$A$1:$E$31", PivotTableRange: "SheetN!$U$34:$O$2", - Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}}, - Columns: []PivotTableField{{Data: "Type"}}, + Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, + Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, Data: []PivotTableField{{Data: "Sales"}}, }), "sheet SheetN is not exist") // Test not exists worksheet in data range assert.EqualError(t, f.AddPivotTable(&PivotTableOption{ DataRange: "SheetN!$A$1:$E$31", PivotTableRange: "Sheet1!$U$34:$O$2", - Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}}, - Columns: []PivotTableField{{Data: "Type"}}, + Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, + Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, Data: []PivotTableField{{Data: "Sales"}}, }), "sheet SheetN is not exist") // Test invalid row number in data range assert.EqualError(t, f.AddPivotTable(&PivotTableOption{ DataRange: "Sheet1!$A$0:$E$31", PivotTableRange: "Sheet1!$U$34:$O$2", - Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}}, - Columns: []PivotTableField{{Data: "Type"}}, + Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, + Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, Data: []PivotTableField{{Data: "Sales"}}, }), `parameter 'DataRange' parsing error: cannot convert cell "A0" to coordinates: invalid cell name "A0"`) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddPivotTable1.xlsx"))) @@ -141,8 +199,8 @@ func TestAddPivotTable(t *testing.T) { assert.NoError(t, f.AddPivotTable(&PivotTableOption{ DataRange: "Sheet1!$A$1:$E$31", PivotTableRange: "Sheet1!$G$2:$M$34", - Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}}, - Columns: []PivotTableField{{Data: "Type"}}, + Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, + Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, Data: []PivotTableField{{Data: "Sales", Subtotal: "-", Name: strings.Repeat("s", 256)}}, })) @@ -158,8 +216,8 @@ func TestAddPivotTable(t *testing.T) { assert.EqualError(t, f.addPivotCache(0, "", &PivotTableOption{ DataRange: "$A$1:$E$31", PivotTableRange: "Sheet1!$U$34:$O$2", - Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}}, - Columns: []PivotTableField{{Data: "Type"}}, + 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") // Test add pivot table with empty options @@ -170,8 +228,8 @@ func TestAddPivotTable(t *testing.T) { assert.EqualError(t, f.addPivotFields(nil, &PivotTableOption{ DataRange: "$A$1:$E$31", PivotTableRange: "Sheet1!$U$34:$O$2", - Rows: []PivotTableField{{Data: "Month"}, {Data: "Year"}}, - Columns: []PivotTableField{{Data: "Type"}}, + Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, + Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, Data: []PivotTableField{{Data: "Sales"}}, }), `parameter 'DataRange' parsing error: parameter is invalid`) // Test get pivot fields index with empty data range @@ -208,7 +266,7 @@ func TestAddPivotColFields(t *testing.T) { // Test invalid data range assert.EqualError(t, f.addPivotColFields(&xlsxPivotTableDefinition{}, &PivotTableOption{ DataRange: "Sheet1!$A$1:$A$1", - Columns: []PivotTableField{{Data: "Type"}}, + Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, }), `parameter 'DataRange' parsing error: parameter is invalid`) } From 324f87bcaed9ec775c0b79627956a093ad481d36 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 18 Sep 2020 22:20:58 +0800 Subject: [PATCH 282/957] add checking and limits for the worksheet --- col.go | 3 +++ col_test.go | 2 ++ file.go | 2 +- rows.go | 4 +++- rows_test.go | 28 +++++++++++++--------------- styles.go | 37 +++++++++++++++++++++++-------------- styles_test.go | 5 +++++ xmlDrawing.go | 6 +++++- 8 files changed, 55 insertions(+), 32 deletions(-) diff --git a/col.go b/col.go index 72db4be907..19ce99b3d4 100644 --- a/col.go +++ b/col.go @@ -444,6 +444,9 @@ func (f *File) SetColWidth(sheet, startcol, endcol string, width float64) error if err != nil { return err } + if width > MaxColumnWidth { + return errors.New("the width of the column must be smaller than or equal to 255 characters") + } if min > max { min, max = max, min } diff --git a/col_test.go b/col_test.go index e6e7e29b8e..02c5ca2398 100644 --- a/col_test.go +++ b/col_test.go @@ -236,6 +236,8 @@ func TestOutlineLevel(t *testing.T) { assert.EqualError(t, err, "sheet Shee2 is not exist") assert.NoError(t, f.SetColWidth("Sheet2", "A", "D", 13)) + assert.EqualError(t, f.SetColWidth("Sheet2", "A", "D", MaxColumnWidth+1), "the width of the column must be smaller than or equal to 255 characters") + assert.NoError(t, f.SetColOutlineLevel("Sheet2", "B", 2)) assert.NoError(t, f.SetRowOutlineLevel("Sheet1", 2, 7)) assert.EqualError(t, f.SetColOutlineLevel("Sheet1", "D", 8), "invalid outline level") diff --git a/file.go b/file.go index bd05bc49e9..a35bc4d89e 100644 --- a/file.go +++ b/file.go @@ -65,7 +65,7 @@ func (f *File) Save() error { // SaveAs provides a function to create or update to an xlsx file at the // provided path. func (f *File) SaveAs(name string, opt ...Options) error { - if len(name) > FileNameLength { + if len(name) > MaxFileNameLength { return errors.New("file name length exceeds maximum limit") } file, err := os.OpenFile(name, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0666) diff --git a/rows.go b/rows.go index c6098e61b8..eb4b1dfe62 100644 --- a/rows.go +++ b/rows.go @@ -225,7 +225,9 @@ func (f *File) SetRowHeight(sheet string, row int, height float64) error { if row < 1 { return newInvalidRowNumberError(row) } - + if height > MaxRowHeight { + return errors.New("the height of the row must be smaller than or equal to 409 points") + } xlsx, err := f.workSheetReader(sheet) if err != nil { return err diff --git a/rows_test.go b/rows_test.go index e3ce9ee088..c469c01d47 100644 --- a/rows_test.go +++ b/rows_test.go @@ -91,40 +91,38 @@ func TestRowsError(t *testing.T) { } func TestRowHeight(t *testing.T) { - xlsx := NewFile() - sheet1 := xlsx.GetSheetName(0) + f := NewFile() + sheet1 := f.GetSheetName(0) - assert.EqualError(t, xlsx.SetRowHeight(sheet1, 0, defaultRowHeightPixels+1.0), "invalid row number 0") + assert.EqualError(t, f.SetRowHeight(sheet1, 0, defaultRowHeightPixels+1.0), "invalid row number 0") - _, err := xlsx.GetRowHeight("Sheet1", 0) + _, err := f.GetRowHeight("Sheet1", 0) assert.EqualError(t, err, "invalid row number 0") - assert.NoError(t, xlsx.SetRowHeight(sheet1, 1, 111.0)) - height, err := xlsx.GetRowHeight(sheet1, 1) + assert.NoError(t, f.SetRowHeight(sheet1, 1, 111.0)) + height, err := f.GetRowHeight(sheet1, 1) assert.NoError(t, err) assert.Equal(t, 111.0, height) - assert.NoError(t, xlsx.SetRowHeight(sheet1, 4, 444.0)) - height, err = xlsx.GetRowHeight(sheet1, 4) - assert.NoError(t, err) - assert.Equal(t, 444.0, height) + // Test set row height overflow max row height limit. + assert.EqualError(t, f.SetRowHeight(sheet1, 4, MaxRowHeight+1), "the height of the row must be smaller than or equal to 409 points") // Test get row height that rows index over exists rows. - height, err = xlsx.GetRowHeight(sheet1, 5) + 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. - height, err = xlsx.GetRowHeight(sheet1, 3) + 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. - assert.EqualError(t, xlsx.SetRowHeight("SheetN", 1, 111.0), "sheet SheetN is not exist") - _, err = xlsx.GetRowHeight("SheetN", 3) + assert.EqualError(t, f.SetRowHeight("SheetN", 1, 111.0), "sheet SheetN is not exist") + _, err = f.GetRowHeight("SheetN", 3) assert.EqualError(t, err, "sheet SheetN is not exist") - err = xlsx.SaveAs(filepath.Join("test", "TestRowHeight.xlsx")) + err = f.SaveAs(filepath.Join("test", "TestRowHeight.xlsx")) if !assert.NoError(t, err) { t.FailNow() } diff --git a/styles.go b/styles.go index c3a2393160..14bcecc488 100644 --- a/styles.go +++ b/styles.go @@ -1037,10 +1037,26 @@ func (f *File) sharedStringsWriter() { // parseFormatStyleSet provides a function to parse the format settings of the // cells and conditional formats. -func parseFormatStyleSet(style string) (*Style, error) { - format := Style{} - err := json.Unmarshal([]byte(style), &format) - return &format, err +func parseFormatStyleSet(style interface{}) (*Style, error) { + fs := Style{} + var err error + switch v := style.(type) { + case string: + err = json.Unmarshal([]byte(v), &fs) + case *Style: + fs = *v + default: + err = errors.New("invalid parameter type") + } + if fs.Font != nil { + if len(fs.Font.Family) > MaxFontFamilyLength { + return &fs, errors.New("the length of the font family name must be smaller than or equal to 31") + } + if fs.Font.Size > MaxFontSize { + return &fs, errors.New("font size must be between 1 and 409 points") + } + } + return &fs, err } // NewStyle provides a function to create the style for cells by given JSON or @@ -1909,16 +1925,9 @@ func (f *File) NewStyle(style interface{}) (int, error) { var fs *Style var err error var cellXfsID, fontID, borderID, fillID int - switch v := style.(type) { - case string: - fs, err = parseFormatStyleSet(v) - if err != nil { - return cellXfsID, err - } - case *Style: - fs = v - default: - return cellXfsID, errors.New("invalid parameter type") + fs, err = parseFormatStyleSet(style) + if err != nil { + return cellXfsID, err } if fs.DecimalPlaces == 0 { fs.DecimalPlaces = 2 diff --git a/styles_test.go b/styles_test.go index 9b8ba39e19..b68365bc06 100644 --- a/styles_test.go +++ b/styles_test.go @@ -3,6 +3,7 @@ package excelize import ( "fmt" "path/filepath" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -200,6 +201,10 @@ func TestNewStyle(t *testing.T) { assert.NoError(t, err) _, err = f.NewStyle(Style{}) assert.EqualError(t, err, "invalid parameter type") + _, err = f.NewStyle(&Style{Font: &Font{Family: strings.Repeat("s", MaxFontFamilyLength+1)}}) + assert.EqualError(t, err, "the length of the font family name must be smaller than or equal to 31") + _, err = f.NewStyle(&Style{Font: &Font{Size: MaxFontSize + 1}}) + assert.EqualError(t, err, "font size must be between 1 and 409 points") } func TestGetDefaultFont(t *testing.T) { diff --git a/xmlDrawing.go b/xmlDrawing.go index e3e496ae06..91b6b594ec 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -89,7 +89,11 @@ const ( // Excel specifications and limits const ( - FileNameLength = 207 + MaxFontFamilyLength = 31 + MaxFontSize = 409 + MaxFileNameLength = 207 + MaxColumnWidth = 255 + MaxRowHeight = 409 TotalRows = 1048576 TotalColumns = 16384 TotalSheetHyperlinks = 65529 From 89465f41b5a29ac2e1debe16b8cb6f59c344f917 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 22 Sep 2020 22:29:43 +0800 Subject: [PATCH 283/957] Update dependency package version and docs for the OpenFile --- excelize.go | 2 ++ file.go | 1 + go.mod | 4 ++-- go.sum | 8 ++++---- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/excelize.go b/excelize.go index 315f41bf5b..a90b765115 100644 --- a/excelize.go +++ b/excelize.go @@ -70,6 +70,8 @@ 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. func OpenFile(filename string, opt ...Options) (*File, error) { file, err := os.Open(filename) if err != nil { diff --git a/file.go b/file.go index a35bc4d89e..83ed2711ff 100644 --- a/file.go +++ b/file.go @@ -73,6 +73,7 @@ func (f *File) SaveAs(name string, opt ...Options) error { return err } defer file.Close() + f.options = nil for _, o := range opt { f.options = &o } diff --git a/go.mod b/go.mod index 9b013566e4..fb2c1e8b99 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/stretchr/testify v1.6.1 github.com/xuri/efp v0.0.0-20200605144744-ba689101faaf golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a - golang.org/x/image v0.0.0-20200801110659-972c09e46d76 - golang.org/x/net v0.0.0-20200822124328-c89045814202 + golang.org/x/image v0.0.0-20200922025426-e59bae62ef32 + golang.org/x/net v0.0.0-20200904194848-62affa334b73 golang.org/x/text v0.3.3 ) diff --git a/go.sum b/go.sum index bc606dfebf..e082e86359 100644 --- a/go.sum +++ b/go.sum @@ -17,11 +17,11 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/image v0.0.0-20200801110659-972c09e46d76 h1:U7GPaoQyQmX+CBRWXKrvRzWTbd+slqeSh8uARsIyhAw= -golang.org/x/image v0.0.0-20200801110659-972c09e46d76/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20200922025426-e59bae62ef32 h1:E+SEVulmY8U4+i6vSB88YSc2OKAFfvbHPU/uDTdQu7M= +golang.org/x/image v0.0.0-20200922025426-e59bae62ef32/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200904194848-62affa334b73 h1:MXfv8rhZWmFeqX3GNZRsd6vOLoaCHjYEX3qkRo3YBUA= +golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= From c49222023756d6740877f286cb7d3e7a5a0950eb Mon Sep 17 00:00:00 2001 From: jinhyuk-kim-ca <71794373+jinhyuk-kim-ca@users.noreply.github.com> Date: Sun, 27 Sep 2020 01:34:39 -0400 Subject: [PATCH 284/957] Pivot table generation fails when no Columns and multiple Data are provided. (#708) fix to create pivot table in case there is no input from Columns Co-authored-by: Jin Kim Co-authored-by: xuri --- pivotTable.go | 9 +++++++++ pivotTable_test.go | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/pivotTable.go b/pivotTable.go index 0c1dd614ac..a197c1fa29 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -460,6 +460,15 @@ func inPivotTableField(a []PivotTableField, x string) int { // definition and option. func (f *File) addPivotColFields(pt *xlsxPivotTableDefinition, opt *PivotTableOption) error { if len(opt.Columns) == 0 { + if len(opt.Data) <= 1 { + return nil + } + pt.ColFields = &xlsxColFields{} + // in order to create pivot table in case there is no input from Columns + pt.ColFields.Count = 1 + pt.ColFields.Field = append(pt.ColFields.Field, &xlsxField{ + X: -2, + }) return nil } diff --git a/pivotTable_test.go b/pivotTable_test.go index 6e736409c2..61bb07b15f 100644 --- a/pivotTable_test.go +++ b/pivotTable_test.go @@ -84,7 +84,7 @@ func TestAddPivotTable(t *testing.T) { DataRange: "Sheet1!$A$1:$E$31", PivotTableRange: "Sheet1!$AE$2:$AG$33", Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, - Data: []PivotTableField{{Data: "Sales", Subtotal: "Max", Name: "Summarize by Max"}}, + Data: []PivotTableField{{Data: "Sales", Subtotal: "Max", Name: "Summarize by Max"}, {Data: "Sales", Subtotal: "Average", Name: "Average of Sales"}}, RowGrandTotals: true, ColGrandTotals: true, ShowDrill: true, @@ -98,7 +98,7 @@ func TestAddPivotTable(t *testing.T) { PivotTableRange: "Sheet1!$AJ$2:$AP1$35", Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, Filter: []PivotTableField{{Data: "Region"}}, - Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, + Columns: []PivotTableField{}, Data: []PivotTableField{{Subtotal: "Sum", Name: "Summarize by Sum"}}, RowGrandTotals: true, ColGrandTotals: true, From 2bd359bd01312bedd1b5f76f68765d1d66fa9a95 Mon Sep 17 00:00:00 2001 From: Ludovic Braconnier Date: Wed, 30 Sep 2020 18:20:11 +0200 Subject: [PATCH 285/957] fix pivot fails in case of multi columns and multi data --- pivotTable.go | 7 +++++++ pivotTable_test.go | 14 ++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/pivotTable.go b/pivotTable.go index a197c1fa29..b7c80c2496 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -485,6 +485,13 @@ func (f *File) addPivotColFields(pt *xlsxPivotTableDefinition, opt *PivotTableOp }) } + //in order to create pivot in case there is many Columns and Many Datas + if len(opt.Data) > 1 { + pt.ColFields.Field = append(pt.ColFields.Field, &xlsxField{ + X: -2, + }) + } + // count col fields pt.ColFields.Count = len(pt.ColFields.Field) return err diff --git a/pivotTable_test.go b/pivotTable_test.go index 61bb07b15f..42103f3ed9 100644 --- a/pivotTable_test.go +++ b/pivotTable_test.go @@ -135,6 +135,20 @@ func TestAddPivotTable(t *testing.T) { ShowColHeaders: true, ShowLastColumn: true, })) + //Test Pivot table with many data, many rows, many cols + assert.NoError(t, f.AddPivotTable(&PivotTableOption{ + DataRange: "Sheet1!$A$1:$E$31", + PivotTableRange: "Sheet2!$A$56:$AG$90", + Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, + Columns: []PivotTableField{{Data: "Region", DefaultSubtotal: true}, {Data: "Type"}}, + Data: []PivotTableField{{Data: "Sales", Subtotal: "Sum", Name: "Sum of Sales"}, {Data: "Sales", Subtotal: "Average", Name: "Average of Sales"}}, + RowGrandTotals: true, + ColGrandTotals: true, + ShowDrill: true, + ShowRowHeaders: true, + ShowColHeaders: true, + ShowLastColumn: true, + })) // Test empty pivot table options assert.EqualError(t, f.AddPivotTable(nil), "parameter is required") From f2b8798a34aab4411a50861a4cdf47203edc3a19 Mon Sep 17 00:00:00 2001 From: Artem Kustikov Date: Sun, 4 Oct 2020 16:07:39 +0300 Subject: [PATCH 286/957] extend cell value load to support custom datetime format (#703) * extend cell value load to support custom datetime format * cleanup incorrect imports * fix numeric values conversion as done in legacy Excel * fix tests coverage * revert temporary package name fix * remove personal info from test XLSX files * remove unused dependencies * update format conversion in parseTime * new UT to increase code coverage * Resolve code review issue for PR #703 * Rename broken file name generated by unit test Co-authored-by: xuri --- .gitignore | 1 + cell.go | 18 +++- cell_test.go | 36 +++++++ crypt_test.go | 2 +- excelize.go | 2 +- excelize_test.go | 5 +- file.go | 2 +- rows.go | 19 ++++ rows_test.go | 37 ++++++- sheet_test.go | 86 ++++++++-------- sheetpr_test.go | 250 +++++++++++++++++++++++----------------------- sheetview_test.go | 92 +++++++++-------- styles.go | 104 ++++++++++++++----- styles_test.go | 46 +++++++++ 14 files changed, 452 insertions(+), 248 deletions(-) diff --git a/.gitignore b/.gitignore index a3fcff22aa..685d2bfdc5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ ~$*.xlsx test/Test*.xlsx +test/Test*.xlsm *.out *.test .idea diff --git a/cell.go b/cell.go index 5fe2157c43..11c6836ee6 100644 --- a/cell.go +++ b/cell.go @@ -762,9 +762,23 @@ func (f *File) formattedValue(s int, v string) string { return v } styleSheet := f.stylesReader() - ok := builtInNumFmtFunc[*styleSheet.CellXfs.Xf[s].NumFmtID] + if s >= len(styleSheet.CellXfs.Xf) { + return v + } + numFmtId := *styleSheet.CellXfs.Xf[s].NumFmtID + ok := builtInNumFmtFunc[numFmtId] if ok != nil { - return ok(*styleSheet.CellXfs.Xf[s].NumFmtID, v) + return ok(v, builtInNumFmt[numFmtId]) + } + for _, xlsxFmt := range styleSheet.NumFmts.NumFmt { + if xlsxFmt.NumFmtID == numFmtId { + format := strings.ToLower(xlsxFmt.FormatCode) + if strings.Contains(format, "y") || strings.Contains(format, "m") || strings.Contains(format, "d") || strings.Contains(format, "h") { + return parseTime(v, format) + } + + return v + } } return v } diff --git a/cell_test.go b/cell_test.go index 441a6946ed..a855344b76 100644 --- a/cell_test.go +++ b/cell_test.go @@ -111,6 +111,23 @@ func TestSetCellValue(t *testing.T) { assert.EqualError(t, f.SetCellValue("Sheet1", "A", time.Duration(1e13)), `cannot convert cell "A" to coordinates: invalid cell name "A"`) } +func TestSetCellValues(t *testing.T) { + f := NewFile() + err := f.SetCellValue("Sheet1", "A1", time.Date(2010, time.December, 31, 0, 0, 0, 0, time.UTC)) + assert.NoError(t, err) + + v, err := f.GetCellValue("Sheet1", "A1") + assert.NoError(t, err) + assert.Equal(t, v, "12/31/10 12:00") + + // 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) + + _, err = f.GetCellValue("Sheet1", "A1") + assert.EqualError(t, err, `strconv.ParseFloat: parsing "1600-12-31T00:00:00Z": invalid syntax`) +} + func TestSetCellBool(t *testing.T) { f := NewFile() assert.EqualError(t, f.SetCellBool("Sheet1", "A", true), `cannot convert cell "A" to coordinates: invalid cell name "A"`) @@ -264,3 +281,22 @@ func TestSetCellRichText(t *testing.T) { // Test set cell rich text with illegal cell coordinates assert.EqualError(t, f.SetCellRichText("Sheet1", "A", richTextRun), `cannot convert cell "A" to coordinates: invalid cell name "A"`) } + +func TestFormattedValue(t *testing.T) { + f := NewFile() + v := f.formattedValue(0, "43528") + assert.Equal(t, "43528", v) + + v = f.formattedValue(15, "43528") + assert.Equal(t, "43528", v) + + v = f.formattedValue(1, "43528") + assert.Equal(t, "43528", v) + customNumFmt := "[$-409]MM/DD/YYYY" + _, err := f.NewStyle(&Style{ + CustomNumFmt: &customNumFmt, + }) + assert.NoError(t, err) + v = f.formattedValue(1, "43528") + assert.Equal(t, "03/04/2019", v) +} diff --git a/crypt_test.go b/crypt_test.go index c6acb38ba8..f9a3fb7a6f 100644 --- a/crypt_test.go +++ b/crypt_test.go @@ -19,5 +19,5 @@ import ( func TestEncrypt(t *testing.T) { f, err := OpenFile(filepath.Join("test", "encryptSHA1.xlsx"), Options{Password: "password"}) assert.NoError(t, err) - assert.EqualError(t, f.SaveAs(filepath.Join("test", "TestEncrypt.xlsx"), Options{Password: "password"}), "not support encryption currently") + assert.EqualError(t, f.SaveAs(filepath.Join("test", "BadEncrypt.xlsx"), Options{Password: "password"}), "not support encryption currently") } diff --git a/excelize.go b/excelize.go index a90b765115..cca6616825 100644 --- a/excelize.go +++ b/excelize.go @@ -158,7 +158,7 @@ func (f *File) setDefaultTimeStyle(sheet, axis string, format int) error { } if s == 0 { style, _ := f.NewStyle(&Style{NumFmt: format}) - _ = f.SetCellStyle(sheet, axis, axis, style) + err = f.SetCellStyle(sheet, axis, axis, style) } return err } diff --git a/excelize_test.go b/excelize_test.go index f1cd65207e..890bcf61e6 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -257,7 +257,7 @@ func TestBrokenFile(t *testing.T) { t.Run("SaveAsEmptyStruct", func(t *testing.T) { // Test write file with broken file struct with given path. - assert.NoError(t, f.SaveAs(filepath.Join("test", "BrokenFile.SaveAsEmptyStruct.xlsx"))) + assert.NoError(t, f.SaveAs(filepath.Join("test", "BadWorkbook.SaveAsEmptyStruct.xlsx"))) }) t.Run("OpenBadWorkbook", func(t *testing.T) { @@ -1175,6 +1175,9 @@ 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") + + // Test set default time style on invalid cell + assert.EqualError(t, f.setDefaultTimeStyle("Sheet1", "", 42), "cannot convert cell \"\" to coordinates: invalid cell name \"\"") } func TestAddVBAProject(t *testing.T) { diff --git a/file.go b/file.go index 83ed2711ff..6a48c0c767 100644 --- a/file.go +++ b/file.go @@ -123,7 +123,7 @@ func (f *File) WriteToBuffer() (*bytes.Buffer, error) { } } - if f.options != nil { + if f.options != nil && f.options.Password != "" { if err := zw.Close(); err != nil { return buf, err } diff --git a/rows.go b/rows.go index eb4b1dfe62..50e7308570 100644 --- a/rows.go +++ b/rows.go @@ -345,6 +345,25 @@ func (xlsx *xlsxC) getValueFrom(f *File, d *xlsxSST) (string, error) { } return f.formattedValue(xlsx.S, xlsx.V), nil default: + // correct numeric values as legacy Excel app + // https://en.wikipedia.org/wiki/Numeric_precision_in_Microsoft_Excel + // In the top figure the fraction 1/9000 in Excel is displayed. + // Although this number has a decimal representation that is an infinite string of ones, + // Excel displays only the leading 15 figures. In the second line, the number one is added to the fraction, and again Excel displays only 15 figures. + const precision = 1000000000000000 + if len(xlsx.V) > 16 { + num, err := strconv.ParseFloat(xlsx.V, 64) + if err != nil { + return "", err + } + + num = math.Round(num*precision) / precision + val := fmt.Sprintf("%g", num) + if val != xlsx.V { + return f.formattedValue(xlsx.S, val), nil + } + } + return f.formattedValue(xlsx.S, xlsx.V), nil } } diff --git a/rows_test.go b/rows_test.go index c469c01d47..246233ffd7 100644 --- a/rows_test.go +++ b/rows_test.go @@ -817,7 +817,7 @@ func TestDuplicateMergeCells(t *testing.T) { assert.EqualError(t, f.duplicateMergeCells("SheetN", xlsx, 1, 2), "sheet SheetN is not exist") } -func TestGetValueFrom(t *testing.T) { +func TestGetValueFromInlineStr(t *testing.T) { c := &xlsxC{T: "inlineStr"} f := NewFile() d := &xlsxSST{} @@ -826,6 +826,20 @@ func TestGetValueFrom(t *testing.T) { assert.Equal(t, "", val) } +func TestGetValueFromNumber(t *testing.T) { + c := &xlsxC{T: "n", V: "2.2200000000000002"} + f := NewFile() + d := &xlsxSST{} + val, err := c.getValueFrom(f, d) + assert.NoError(t, err) + assert.Equal(t, "2.22", val) + + c = &xlsxC{T: "n", V: "2.220000ddsf0000000002-r"} + val, err = c.getValueFrom(f, d) + assert.NotNil(t, err) + assert.Equal(t, "strconv.ParseFloat: parsing \"2.220000ddsf0000000002-r\": invalid syntax", err.Error()) +} + func TestErrSheetNotExistError(t *testing.T) { err := ErrSheetNotExist{SheetName: "Sheet1"} assert.EqualValues(t, err.Error(), "sheet Sheet1 is not exist") @@ -842,6 +856,27 @@ func TestCheckRow(t *testing.T) { assert.EqualError(t, f.SetCellValue("Sheet1", "A1", false), `cannot convert cell "-" to coordinates: invalid cell name "-"`) } +func TestNumberFormats(t *testing.T) { + f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + if !assert.NoError(t, err) { + t.FailNow() + } + cells := make([][]string, 0) + cols, err := f.Cols("Sheet2") + if !assert.NoError(t, err) { + t.FailNow() + } + for cols.Next() { + col, err := cols.Rows() + assert.NoError(t, err) + if err != nil { + break + } + cells = append(cells, col) + } + assert.Equal(t, []string{"", "200", "450", "200", "510", "315", "127", "89", "348", "53", "37"}, cells[3]) +} + func BenchmarkRows(b *testing.B) { f, _ := OpenFile(filepath.Join("test", "Book1.xlsx")) for i := 0; i < b.N; i++ { diff --git a/sheet_test.go b/sheet_test.go index 0014220a26..890a4e5322 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -1,4 +1,4 @@ -package excelize_test +package excelize import ( "fmt" @@ -6,26 +6,24 @@ import ( "strings" "testing" - "github.com/360EntSecGroup-Skylar/excelize/v2" - "github.com/mohae/deepcopy" "github.com/stretchr/testify/assert" ) func ExampleFile_SetPageLayout() { - f := excelize.NewFile() + f := NewFile() if err := f.SetPageLayout( "Sheet1", - excelize.PageLayoutOrientation(excelize.OrientationLandscape), + PageLayoutOrientation(OrientationLandscape), ); err != nil { fmt.Println(err) } if err := f.SetPageLayout( "Sheet1", - excelize.PageLayoutPaperSize(10), - excelize.FitToHeight(2), - excelize.FitToWidth(2), + PageLayoutPaperSize(10), + FitToHeight(2), + FitToWidth(2), ); err != nil { fmt.Println(err) } @@ -33,12 +31,12 @@ func ExampleFile_SetPageLayout() { } func ExampleFile_GetPageLayout() { - f := excelize.NewFile() + f := NewFile() var ( - orientation excelize.PageLayoutOrientation - paperSize excelize.PageLayoutPaperSize - fitToHeight excelize.FitToHeight - fitToWidth excelize.FitToWidth + orientation PageLayoutOrientation + paperSize PageLayoutPaperSize + fitToHeight FitToHeight + fitToWidth FitToWidth ) if err := f.GetPageLayout("Sheet1", &orientation); err != nil { fmt.Println(err) @@ -67,7 +65,7 @@ func ExampleFile_GetPageLayout() { } func TestNewSheet(t *testing.T) { - f := excelize.NewFile() + f := NewFile() sheetID := f.NewSheet("Sheet2") f.SetActiveSheet(sheetID) // delete original sheet @@ -76,7 +74,7 @@ func TestNewSheet(t *testing.T) { } func TestSetPane(t *testing.T) { - f := excelize.NewFile() + 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"}]}`)) @@ -93,13 +91,13 @@ func TestPageLayoutOption(t *testing.T) { const sheet = "Sheet1" testData := []struct { - container excelize.PageLayoutOptionPtr - nonDefault excelize.PageLayoutOption + container PageLayoutOptionPtr + nonDefault PageLayoutOption }{ - {new(excelize.PageLayoutOrientation), excelize.PageLayoutOrientation(excelize.OrientationLandscape)}, - {new(excelize.PageLayoutPaperSize), excelize.PageLayoutPaperSize(10)}, - {new(excelize.FitToHeight), excelize.FitToHeight(2)}, - {new(excelize.FitToWidth), excelize.FitToWidth(2)}, + {new(PageLayoutOrientation), PageLayoutOrientation(OrientationLandscape)}, + {new(PageLayoutPaperSize), PageLayoutPaperSize(10)}, + {new(FitToHeight), FitToHeight(2)}, + {new(FitToWidth), FitToWidth(2)}, } for i, test := range testData { @@ -108,11 +106,11 @@ func TestPageLayoutOption(t *testing.T) { opt := test.nonDefault t.Logf("option %T", opt) - def := deepcopy.Copy(test.container).(excelize.PageLayoutOptionPtr) - val1 := deepcopy.Copy(def).(excelize.PageLayoutOptionPtr) - val2 := deepcopy.Copy(def).(excelize.PageLayoutOptionPtr) + def := deepcopy.Copy(test.container).(PageLayoutOptionPtr) + val1 := deepcopy.Copy(def).(PageLayoutOptionPtr) + val2 := deepcopy.Copy(def).(PageLayoutOptionPtr) - f := excelize.NewFile() + f := NewFile() // Get the default value assert.NoError(t, f.GetPageLayout(sheet, def), opt) // Get again and check @@ -150,7 +148,7 @@ func TestPageLayoutOption(t *testing.T) { } func TestSearchSheet(t *testing.T) { - f, err := excelize.OpenFile(filepath.Join("test", "SharedStrings.xlsx")) + f, err := OpenFile(filepath.Join("test", "SharedStrings.xlsx")) if !assert.NoError(t, err) { t.FailNow() } @@ -172,36 +170,36 @@ func TestSearchSheet(t *testing.T) { assert.EqualValues(t, expected, result) // Test search worksheet data after set cell value - f = excelize.NewFile() + f = NewFile() assert.NoError(t, f.SetCellValue("Sheet1", "A1", true)) _, err = f.SearchSheet("Sheet1", "") assert.NoError(t, err) } func TestSetPageLayout(t *testing.T) { - f := excelize.NewFile() + f := NewFile() // Test set page layout on not exists worksheet. assert.EqualError(t, f.SetPageLayout("SheetN"), "sheet SheetN is not exist") } func TestGetPageLayout(t *testing.T) { - f := excelize.NewFile() + f := NewFile() // Test get page layout on not exists worksheet. assert.EqualError(t, f.GetPageLayout("SheetN"), "sheet SheetN is not exist") } func TestSetHeaderFooter(t *testing.T) { - f := excelize.NewFile() + 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") // Test set header and footer with illegal setting. - assert.EqualError(t, f.SetHeaderFooter("Sheet1", &excelize.FormatHeaderFooter{ + assert.EqualError(t, f.SetHeaderFooter("Sheet1", &FormatHeaderFooter{ OddHeader: strings.Repeat("c", 256), }), "field OddHeader must be less than 255 characters") assert.NoError(t, f.SetHeaderFooter("Sheet1", nil)) - assert.NoError(t, f.SetHeaderFooter("Sheet1", &excelize.FormatHeaderFooter{ + assert.NoError(t, f.SetHeaderFooter("Sheet1", &FormatHeaderFooter{ DifferentFirst: true, DifferentOddEven: true, OddHeader: "&R&P", @@ -214,28 +212,28 @@ func TestSetHeaderFooter(t *testing.T) { } func TestDefinedName(t *testing.T) { - f := excelize.NewFile() - assert.NoError(t, f.SetDefinedName(&excelize.DefinedName{ + f := NewFile() + assert.NoError(t, f.SetDefinedName(&DefinedName{ Name: "Amount", RefersTo: "Sheet1!$A$2:$D$5", Comment: "defined name comment", Scope: "Sheet1", })) - assert.NoError(t, f.SetDefinedName(&excelize.DefinedName{ + assert.NoError(t, f.SetDefinedName(&DefinedName{ Name: "Amount", RefersTo: "Sheet1!$A$2:$D$5", Comment: "defined name comment", })) - assert.EqualError(t, f.SetDefinedName(&excelize.DefinedName{ + assert.EqualError(t, f.SetDefinedName(&DefinedName{ Name: "Amount", RefersTo: "Sheet1!$A$2:$D$5", Comment: "defined name comment", }), "the same name already exists on the scope") - assert.EqualError(t, f.DeleteDefinedName(&excelize.DefinedName{ + assert.EqualError(t, f.DeleteDefinedName(&DefinedName{ Name: "No Exist Defined Name", }), "no defined name on the scope") assert.Exactly(t, "Sheet1!$A$2:$D$5", f.GetDefinedName()[1].RefersTo) - assert.NoError(t, f.DeleteDefinedName(&excelize.DefinedName{ + assert.NoError(t, f.DeleteDefinedName(&DefinedName{ Name: "Amount", })) assert.Exactly(t, "Sheet1!$A$2:$D$5", f.GetDefinedName()[0].RefersTo) @@ -244,7 +242,7 @@ func TestDefinedName(t *testing.T) { } func TestGroupSheets(t *testing.T) { - f := excelize.NewFile() + f := NewFile() sheets := []string{"Sheet2", "Sheet3"} for _, sheet := range sheets { f.NewSheet(sheet) @@ -256,7 +254,7 @@ func TestGroupSheets(t *testing.T) { } func TestUngroupSheets(t *testing.T) { - f := excelize.NewFile() + f := NewFile() sheets := []string{"Sheet2", "Sheet3", "Sheet4", "Sheet5"} for _, sheet := range sheets { f.NewSheet(sheet) @@ -265,7 +263,7 @@ func TestUngroupSheets(t *testing.T) { } func TestInsertPageBreak(t *testing.T) { - f := excelize.NewFile() + f := NewFile() assert.NoError(t, f.InsertPageBreak("Sheet1", "A1")) assert.NoError(t, f.InsertPageBreak("Sheet1", "B2")) assert.NoError(t, f.InsertPageBreak("Sheet1", "C3")) @@ -276,7 +274,7 @@ func TestInsertPageBreak(t *testing.T) { } func TestRemovePageBreak(t *testing.T) { - f := excelize.NewFile() + f := NewFile() assert.NoError(t, f.RemovePageBreak("Sheet1", "A2")) assert.NoError(t, f.InsertPageBreak("Sheet1", "A2")) @@ -302,7 +300,7 @@ func TestRemovePageBreak(t *testing.T) { } func TestGetSheetName(t *testing.T) { - f, _ := excelize.OpenFile(filepath.Join("test", "Book1.xlsx")) + f, _ := OpenFile(filepath.Join("test", "Book1.xlsx")) assert.Equal(t, "Sheet1", f.GetSheetName(0)) assert.Equal(t, "Sheet2", f.GetSheetName(1)) assert.Equal(t, "", f.GetSheetName(-1)) @@ -314,7 +312,7 @@ func TestGetSheetMap(t *testing.T) { 1: "Sheet1", 2: "Sheet2", } - f, _ := excelize.OpenFile(filepath.Join("test", "Book1.xlsx")) + f, _ := OpenFile(filepath.Join("test", "Book1.xlsx")) sheetMap := f.GetSheetMap() for idx, name := range sheetMap { assert.Equal(t, expectedMap[idx], name) diff --git a/sheetpr_test.go b/sheetpr_test.go index 6e031518e1..29bd99efda 100644 --- a/sheetpr_test.go +++ b/sheetpr_test.go @@ -1,4 +1,4 @@ -package excelize_test +package excelize import ( "fmt" @@ -6,39 +6,37 @@ import ( "github.com/mohae/deepcopy" "github.com/stretchr/testify/assert" - - "github.com/360EntSecGroup-Skylar/excelize/v2" ) -var _ = []excelize.SheetPrOption{ - excelize.CodeName("hello"), - excelize.EnableFormatConditionsCalculation(false), - excelize.Published(false), - excelize.FitToPage(true), - excelize.AutoPageBreaks(true), - excelize.OutlineSummaryBelow(true), +var _ = []SheetPrOption{ + CodeName("hello"), + EnableFormatConditionsCalculation(false), + Published(false), + FitToPage(true), + AutoPageBreaks(true), + OutlineSummaryBelow(true), } -var _ = []excelize.SheetPrOptionPtr{ - (*excelize.CodeName)(nil), - (*excelize.EnableFormatConditionsCalculation)(nil), - (*excelize.Published)(nil), - (*excelize.FitToPage)(nil), - (*excelize.AutoPageBreaks)(nil), - (*excelize.OutlineSummaryBelow)(nil), +var _ = []SheetPrOptionPtr{ + (*CodeName)(nil), + (*EnableFormatConditionsCalculation)(nil), + (*Published)(nil), + (*FitToPage)(nil), + (*AutoPageBreaks)(nil), + (*OutlineSummaryBelow)(nil), } func ExampleFile_SetSheetPrOptions() { - f := excelize.NewFile() + f := NewFile() const sheet = "Sheet1" if err := f.SetSheetPrOptions(sheet, - excelize.CodeName("code"), - excelize.EnableFormatConditionsCalculation(false), - excelize.Published(false), - excelize.FitToPage(true), - excelize.AutoPageBreaks(true), - excelize.OutlineSummaryBelow(false), + CodeName("code"), + EnableFormatConditionsCalculation(false), + Published(false), + FitToPage(true), + AutoPageBreaks(true), + OutlineSummaryBelow(false), ); err != nil { fmt.Println(err) } @@ -46,16 +44,16 @@ func ExampleFile_SetSheetPrOptions() { } func ExampleFile_GetSheetPrOptions() { - f := excelize.NewFile() + f := NewFile() const sheet = "Sheet1" var ( - codeName excelize.CodeName - enableFormatConditionsCalculation excelize.EnableFormatConditionsCalculation - published excelize.Published - fitToPage excelize.FitToPage - autoPageBreaks excelize.AutoPageBreaks - outlineSummaryBelow excelize.OutlineSummaryBelow + codeName CodeName + enableFormatConditionsCalculation EnableFormatConditionsCalculation + published Published + fitToPage FitToPage + autoPageBreaks AutoPageBreaks + outlineSummaryBelow OutlineSummaryBelow ) if err := f.GetSheetPrOptions(sheet, @@ -89,15 +87,15 @@ func TestSheetPrOptions(t *testing.T) { const sheet = "Sheet1" testData := []struct { - container excelize.SheetPrOptionPtr - nonDefault excelize.SheetPrOption + container SheetPrOptionPtr + nonDefault SheetPrOption }{ - {new(excelize.CodeName), excelize.CodeName("xx")}, - {new(excelize.EnableFormatConditionsCalculation), excelize.EnableFormatConditionsCalculation(false)}, - {new(excelize.Published), excelize.Published(false)}, - {new(excelize.FitToPage), excelize.FitToPage(true)}, - {new(excelize.AutoPageBreaks), excelize.AutoPageBreaks(true)}, - {new(excelize.OutlineSummaryBelow), excelize.OutlineSummaryBelow(false)}, + {new(CodeName), CodeName("xx")}, + {new(EnableFormatConditionsCalculation), EnableFormatConditionsCalculation(false)}, + {new(Published), Published(false)}, + {new(FitToPage), FitToPage(true)}, + {new(AutoPageBreaks), AutoPageBreaks(true)}, + {new(OutlineSummaryBelow), OutlineSummaryBelow(false)}, } for i, test := range testData { @@ -106,11 +104,11 @@ func TestSheetPrOptions(t *testing.T) { opt := test.nonDefault t.Logf("option %T", opt) - def := deepcopy.Copy(test.container).(excelize.SheetPrOptionPtr) - val1 := deepcopy.Copy(def).(excelize.SheetPrOptionPtr) - val2 := deepcopy.Copy(def).(excelize.SheetPrOptionPtr) + def := deepcopy.Copy(test.container).(SheetPrOptionPtr) + val1 := deepcopy.Copy(def).(SheetPrOptionPtr) + val2 := deepcopy.Copy(def).(SheetPrOptionPtr) - f := excelize.NewFile() + f := NewFile() // Get the default value assert.NoError(t, f.GetSheetPrOptions(sheet, def), opt) // Get again and check @@ -148,46 +146,46 @@ func TestSheetPrOptions(t *testing.T) { } func TestSetSheetrOptions(t *testing.T) { - f := excelize.NewFile() + f := NewFile() // Test SetSheetrOptions on not exists worksheet. assert.EqualError(t, f.SetSheetPrOptions("SheetN"), "sheet SheetN is not exist") } func TestGetSheetPrOptions(t *testing.T) { - f := excelize.NewFile() + f := NewFile() // Test GetSheetPrOptions on not exists worksheet. assert.EqualError(t, f.GetSheetPrOptions("SheetN"), "sheet SheetN is not exist") } -var _ = []excelize.PageMarginsOptions{ - excelize.PageMarginBottom(1.0), - excelize.PageMarginFooter(1.0), - excelize.PageMarginHeader(1.0), - excelize.PageMarginLeft(1.0), - excelize.PageMarginRight(1.0), - excelize.PageMarginTop(1.0), +var _ = []PageMarginsOptions{ + PageMarginBottom(1.0), + PageMarginFooter(1.0), + PageMarginHeader(1.0), + PageMarginLeft(1.0), + PageMarginRight(1.0), + PageMarginTop(1.0), } -var _ = []excelize.PageMarginsOptionsPtr{ - (*excelize.PageMarginBottom)(nil), - (*excelize.PageMarginFooter)(nil), - (*excelize.PageMarginHeader)(nil), - (*excelize.PageMarginLeft)(nil), - (*excelize.PageMarginRight)(nil), - (*excelize.PageMarginTop)(nil), +var _ = []PageMarginsOptionsPtr{ + (*PageMarginBottom)(nil), + (*PageMarginFooter)(nil), + (*PageMarginHeader)(nil), + (*PageMarginLeft)(nil), + (*PageMarginRight)(nil), + (*PageMarginTop)(nil), } func ExampleFile_SetPageMargins() { - f := excelize.NewFile() + f := NewFile() const sheet = "Sheet1" if err := f.SetPageMargins(sheet, - excelize.PageMarginBottom(1.0), - excelize.PageMarginFooter(1.0), - excelize.PageMarginHeader(1.0), - excelize.PageMarginLeft(1.0), - excelize.PageMarginRight(1.0), - excelize.PageMarginTop(1.0), + PageMarginBottom(1.0), + PageMarginFooter(1.0), + PageMarginHeader(1.0), + PageMarginLeft(1.0), + PageMarginRight(1.0), + PageMarginTop(1.0), ); err != nil { fmt.Println(err) } @@ -195,16 +193,16 @@ func ExampleFile_SetPageMargins() { } func ExampleFile_GetPageMargins() { - f := excelize.NewFile() + f := NewFile() const sheet = "Sheet1" var ( - marginBottom excelize.PageMarginBottom - marginFooter excelize.PageMarginFooter - marginHeader excelize.PageMarginHeader - marginLeft excelize.PageMarginLeft - marginRight excelize.PageMarginRight - marginTop excelize.PageMarginTop + marginBottom PageMarginBottom + marginFooter PageMarginFooter + marginHeader PageMarginHeader + marginLeft PageMarginLeft + marginRight PageMarginRight + marginTop PageMarginTop ) if err := f.GetPageMargins(sheet, @@ -238,15 +236,15 @@ func TestPageMarginsOption(t *testing.T) { const sheet = "Sheet1" testData := []struct { - container excelize.PageMarginsOptionsPtr - nonDefault excelize.PageMarginsOptions + container PageMarginsOptionsPtr + nonDefault PageMarginsOptions }{ - {new(excelize.PageMarginTop), excelize.PageMarginTop(1.0)}, - {new(excelize.PageMarginBottom), excelize.PageMarginBottom(1.0)}, - {new(excelize.PageMarginLeft), excelize.PageMarginLeft(1.0)}, - {new(excelize.PageMarginRight), excelize.PageMarginRight(1.0)}, - {new(excelize.PageMarginHeader), excelize.PageMarginHeader(1.0)}, - {new(excelize.PageMarginFooter), excelize.PageMarginFooter(1.0)}, + {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 { @@ -255,11 +253,11 @@ func TestPageMarginsOption(t *testing.T) { opt := test.nonDefault t.Logf("option %T", opt) - def := deepcopy.Copy(test.container).(excelize.PageMarginsOptionsPtr) - val1 := deepcopy.Copy(def).(excelize.PageMarginsOptionsPtr) - val2 := deepcopy.Copy(def).(excelize.PageMarginsOptionsPtr) + def := deepcopy.Copy(test.container).(PageMarginsOptionsPtr) + val1 := deepcopy.Copy(def).(PageMarginsOptionsPtr) + val2 := deepcopy.Copy(def).(PageMarginsOptionsPtr) - f := excelize.NewFile() + f := NewFile() // Get the default value assert.NoError(t, f.GetPageMargins(sheet, def), opt) // Get again and check @@ -297,29 +295,29 @@ func TestPageMarginsOption(t *testing.T) { } func TestSetPageMargins(t *testing.T) { - f := excelize.NewFile() + f := NewFile() // Test set page margins on not exists worksheet. assert.EqualError(t, f.SetPageMargins("SheetN"), "sheet SheetN is not exist") } func TestGetPageMargins(t *testing.T) { - f := excelize.NewFile() + f := NewFile() // Test get page margins on not exists worksheet. assert.EqualError(t, f.GetPageMargins("SheetN"), "sheet SheetN is not exist") } func ExampleFile_SetSheetFormatPr() { - f := excelize.NewFile() + f := NewFile() const sheet = "Sheet1" if err := f.SetSheetFormatPr(sheet, - excelize.BaseColWidth(1.0), - excelize.DefaultColWidth(1.0), - excelize.DefaultRowHeight(1.0), - excelize.CustomHeight(true), - excelize.ZeroHeight(true), - excelize.ThickTop(true), - excelize.ThickBottom(true), + BaseColWidth(1.0), + DefaultColWidth(1.0), + DefaultRowHeight(1.0), + CustomHeight(true), + ZeroHeight(true), + ThickTop(true), + ThickBottom(true), ); err != nil { fmt.Println(err) } @@ -327,17 +325,17 @@ func ExampleFile_SetSheetFormatPr() { } func ExampleFile_GetSheetFormatPr() { - f := excelize.NewFile() + f := NewFile() const sheet = "Sheet1" var ( - baseColWidth excelize.BaseColWidth - defaultColWidth excelize.DefaultColWidth - defaultRowHeight excelize.DefaultRowHeight - customHeight excelize.CustomHeight - zeroHeight excelize.ZeroHeight - thickTop excelize.ThickTop - thickBottom excelize.ThickBottom + baseColWidth BaseColWidth + defaultColWidth DefaultColWidth + defaultRowHeight DefaultRowHeight + customHeight CustomHeight + zeroHeight ZeroHeight + thickTop ThickTop + thickBottom ThickBottom ) if err := f.GetSheetFormatPr(sheet, @@ -374,16 +372,16 @@ func TestSheetFormatPrOptions(t *testing.T) { const sheet = "Sheet1" testData := []struct { - container excelize.SheetFormatPrOptionsPtr - nonDefault excelize.SheetFormatPrOptions + container SheetFormatPrOptionsPtr + nonDefault SheetFormatPrOptions }{ - {new(excelize.BaseColWidth), excelize.BaseColWidth(1.0)}, - {new(excelize.DefaultColWidth), excelize.DefaultColWidth(1.0)}, - {new(excelize.DefaultRowHeight), excelize.DefaultRowHeight(1.0)}, - {new(excelize.CustomHeight), excelize.CustomHeight(true)}, - {new(excelize.ZeroHeight), excelize.ZeroHeight(true)}, - {new(excelize.ThickTop), excelize.ThickTop(true)}, - {new(excelize.ThickBottom), excelize.ThickBottom(true)}, + {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 { @@ -392,11 +390,11 @@ func TestSheetFormatPrOptions(t *testing.T) { opt := test.nonDefault t.Logf("option %T", opt) - def := deepcopy.Copy(test.container).(excelize.SheetFormatPrOptionsPtr) - val1 := deepcopy.Copy(def).(excelize.SheetFormatPrOptionsPtr) - val2 := deepcopy.Copy(def).(excelize.SheetFormatPrOptionsPtr) + def := deepcopy.Copy(test.container).(SheetFormatPrOptionsPtr) + val1 := deepcopy.Copy(def).(SheetFormatPrOptionsPtr) + val2 := deepcopy.Copy(def).(SheetFormatPrOptionsPtr) - f := excelize.NewFile() + f := NewFile() // Get the default value assert.NoError(t, f.GetSheetFormatPr(sheet, def), opt) // Get again and check @@ -434,26 +432,26 @@ func TestSheetFormatPrOptions(t *testing.T) { } func TestSetSheetFormatPr(t *testing.T) { - f := excelize.NewFile() + f := NewFile() assert.NoError(t, f.GetSheetFormatPr("Sheet1")) f.Sheet["xl/worksheets/sheet1.xml"].SheetFormatPr = nil - assert.NoError(t, f.SetSheetFormatPr("Sheet1", excelize.BaseColWidth(1.0))) + 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") } func TestGetSheetFormatPr(t *testing.T) { - f := excelize.NewFile() + f := NewFile() assert.NoError(t, f.GetSheetFormatPr("Sheet1")) f.Sheet["xl/worksheets/sheet1.xml"].SheetFormatPr = nil var ( - baseColWidth excelize.BaseColWidth - defaultColWidth excelize.DefaultColWidth - defaultRowHeight excelize.DefaultRowHeight - customHeight excelize.CustomHeight - zeroHeight excelize.ZeroHeight - thickTop excelize.ThickTop - thickBottom excelize.ThickBottom + baseColWidth BaseColWidth + defaultColWidth DefaultColWidth + defaultRowHeight DefaultRowHeight + customHeight CustomHeight + zeroHeight ZeroHeight + thickTop ThickTop + thickBottom ThickBottom ) assert.NoError(t, f.GetSheetFormatPr("Sheet1", &baseColWidth, diff --git a/sheetview_test.go b/sheetview_test.go index d999875030..e323e2380c 100644 --- a/sheetview_test.go +++ b/sheetview_test.go @@ -1,60 +1,58 @@ -package excelize_test +package excelize import ( "fmt" "testing" "github.com/stretchr/testify/assert" - - "github.com/360EntSecGroup-Skylar/excelize/v2" ) -var _ = []excelize.SheetViewOption{ - excelize.DefaultGridColor(true), - excelize.RightToLeft(false), - excelize.ShowFormulas(false), - excelize.ShowGridLines(true), - excelize.ShowRowColHeaders(true), - excelize.TopLeftCell("B2"), +var _ = []SheetViewOption{ + DefaultGridColor(true), + RightToLeft(false), + ShowFormulas(false), + ShowGridLines(true), + ShowRowColHeaders(true), + TopLeftCell("B2"), // SheetViewOptionPtr are also SheetViewOption - new(excelize.DefaultGridColor), - new(excelize.RightToLeft), - new(excelize.ShowFormulas), - new(excelize.ShowGridLines), - new(excelize.ShowRowColHeaders), - new(excelize.TopLeftCell), + new(DefaultGridColor), + new(RightToLeft), + new(ShowFormulas), + new(ShowGridLines), + new(ShowRowColHeaders), + new(TopLeftCell), } -var _ = []excelize.SheetViewOptionPtr{ - (*excelize.DefaultGridColor)(nil), - (*excelize.RightToLeft)(nil), - (*excelize.ShowFormulas)(nil), - (*excelize.ShowGridLines)(nil), - (*excelize.ShowRowColHeaders)(nil), - (*excelize.TopLeftCell)(nil), +var _ = []SheetViewOptionPtr{ + (*DefaultGridColor)(nil), + (*RightToLeft)(nil), + (*ShowFormulas)(nil), + (*ShowGridLines)(nil), + (*ShowRowColHeaders)(nil), + (*TopLeftCell)(nil), } func ExampleFile_SetSheetViewOptions() { - f := excelize.NewFile() + f := NewFile() const sheet = "Sheet1" if err := f.SetSheetViewOptions(sheet, 0, - excelize.DefaultGridColor(false), - excelize.RightToLeft(false), - excelize.ShowFormulas(true), - excelize.ShowGridLines(true), - excelize.ShowRowColHeaders(true), - excelize.ZoomScale(80), - excelize.TopLeftCell("C3"), + DefaultGridColor(false), + RightToLeft(false), + ShowFormulas(true), + ShowGridLines(true), + ShowRowColHeaders(true), + ZoomScale(80), + TopLeftCell("C3"), ); err != nil { fmt.Println(err) } - var zoomScale excelize.ZoomScale + var zoomScale ZoomScale fmt.Println("Default:") fmt.Println("- zoomScale: 80") - if err := f.SetSheetViewOptions(sheet, 0, excelize.ZoomScale(500)); err != nil { + if err := f.SetSheetViewOptions(sheet, 0, ZoomScale(500)); err != nil { fmt.Println(err) } @@ -65,7 +63,7 @@ func ExampleFile_SetSheetViewOptions() { fmt.Println("Used out of range value:") fmt.Println("- zoomScale:", zoomScale) - if err := f.SetSheetViewOptions(sheet, 0, excelize.ZoomScale(123)); err != nil { + if err := f.SetSheetViewOptions(sheet, 0, ZoomScale(123)); err != nil { fmt.Println(err) } @@ -87,18 +85,18 @@ func ExampleFile_SetSheetViewOptions() { } func ExampleFile_GetSheetViewOptions() { - f := excelize.NewFile() + f := NewFile() const sheet = "Sheet1" var ( - defaultGridColor excelize.DefaultGridColor - rightToLeft excelize.RightToLeft - showFormulas excelize.ShowFormulas - showGridLines excelize.ShowGridLines - showZeros excelize.ShowZeros - showRowColHeaders excelize.ShowRowColHeaders - zoomScale excelize.ZoomScale - topLeftCell excelize.TopLeftCell + defaultGridColor DefaultGridColor + rightToLeft RightToLeft + showFormulas ShowFormulas + showGridLines ShowGridLines + showZeros ShowZeros + showRowColHeaders ShowRowColHeaders + zoomScale ZoomScale + topLeftCell TopLeftCell ) if err := f.GetSheetViewOptions(sheet, 0, @@ -124,7 +122,7 @@ func ExampleFile_GetSheetViewOptions() { fmt.Println("- zoomScale:", zoomScale) fmt.Println("- topLeftCell:", `"`+topLeftCell+`"`) - if err := f.SetSheetViewOptions(sheet, 0, excelize.TopLeftCell("B2")); err != nil { + if err := f.SetSheetViewOptions(sheet, 0, TopLeftCell("B2")); err != nil { fmt.Println(err) } @@ -132,7 +130,7 @@ func ExampleFile_GetSheetViewOptions() { fmt.Println(err) } - if err := f.SetSheetViewOptions(sheet, 0, excelize.ShowGridLines(false)); err != nil { + if err := f.SetSheetViewOptions(sheet, 0, ShowGridLines(false)); err != nil { fmt.Println(err) } @@ -140,7 +138,7 @@ func ExampleFile_GetSheetViewOptions() { fmt.Println(err) } - if err := f.SetSheetViewOptions(sheet, 0, excelize.ShowZeros(false)); err != nil { + if err := f.SetSheetViewOptions(sheet, 0, ShowZeros(false)); err != nil { fmt.Println(err) } @@ -170,7 +168,7 @@ func ExampleFile_GetSheetViewOptions() { } func TestSheetViewOptionsErrors(t *testing.T) { - f := excelize.NewFile() + f := NewFile() const sheet = "Sheet1" assert.NoError(t, f.GetSheetViewOptions(sheet, 0)) diff --git a/styles.go b/styles.go index 14bcecc488..d4d0468904 100644 --- a/styles.go +++ b/styles.go @@ -21,6 +21,7 @@ import ( "log" "math" "reflect" + "regexp" "strconv" "strings" ) @@ -755,7 +756,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(i int, v string) string{ +var builtInNumFmtFunc = map[int]func(v string, format string) string{ 0: formatToString, 1: formatToInt, 2: formatToFloat, @@ -847,14 +848,14 @@ var criteriaType = map[string]string{ // formatToString provides a function to return original string by given // built-in number formats code and cell string. -func formatToString(i int, v string) string { +func formatToString(v string, format string) string { return v } // 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(i int, v string) string { +func formatToInt(v string, format string) string { f, err := strconv.ParseFloat(v, 64) if err != nil { return v @@ -865,7 +866,7 @@ func formatToInt(i int, v 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(i int, v string) string { +func formatToFloat(v string, format string) string { f, err := strconv.ParseFloat(v, 64) if err != nil { return v @@ -875,7 +876,7 @@ func formatToFloat(i int, v 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(i int, v string) string { +func formatToA(v string, format string) string { f, err := strconv.ParseFloat(v, 64) if err != nil { return v @@ -890,7 +891,7 @@ func formatToA(i int, v 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(i int, v string) string { +func formatToB(v string, format string) string { f, err := strconv.ParseFloat(v, 64) if err != nil { return v @@ -903,7 +904,7 @@ func formatToB(i int, v 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(i int, v string) string { +func formatToC(v string, format string) string { f, err := strconv.ParseFloat(v, 64) if err != nil { return v @@ -914,7 +915,7 @@ func formatToC(i int, v 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(i int, v string) string { +func formatToD(v string, format string) string { f, err := strconv.ParseFloat(v, 64) if err != nil { return v @@ -925,7 +926,7 @@ func formatToD(i int, v 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(i int, v string) string { +func formatToE(v string, format string) string { f, err := strconv.ParseFloat(v, 64) if err != nil { return v @@ -933,6 +934,8 @@ func formatToE(i int, v string) string { return fmt.Sprintf("%.e", f) } +var dateTimeFormatsCache = map[string]string{} + // parseTime provides a function to returns a string parsed using time.Time. // Replace Excel placeholders with Go time placeholders. For example, replace // yyyy with 2006. These are in a specific order, due to the fact that m is @@ -944,15 +947,46 @@ func formatToE(i int, v string) string { // arbitrary characters unused in Excel Date formats, and then at the end, // turn them to what they should actually be. Based off: // http://www.ozgrid.com/Excel/CustomFormats.htm -func parseTime(i int, v string) string { - f, err := strconv.ParseFloat(v, 64) +func parseTime(v string, format string) string { + var ( + f float64 + err error + goFmt string + ) + f, err = strconv.ParseFloat(v, 64) if err != nil { return v } val := timeFromExcelTime(f, false) - format := builtInNumFmt[i] + + if format == "" { + return v + } + + goFmt, found := dateTimeFormatsCache[format] + if found { + return val.Format(goFmt) + } + + goFmt = format + + if strings.Contains(goFmt, "[") { + var re = regexp.MustCompile(`\[.+\]`) + goFmt = re.ReplaceAllLiteralString(goFmt, "") + } + + // use only first variant + if strings.Contains(goFmt, ";") { + goFmt = goFmt[:strings.IndexByte(goFmt, ';')] + } replacements := []struct{ xltime, gotime string }{ + {"YYYY", "2006"}, + {"YY", "06"}, + {"MM", "01"}, + {"M", "1"}, + {"DD", "02"}, + {"D", "2"}, {"yyyy", "2006"}, {"yy", "06"}, {"mmmm", "%%%%"}, @@ -962,38 +996,59 @@ func parseTime(i int, v string) string { {"mmm", "Jan"}, {"mmss", "0405"}, {"ss", "05"}, + {"s", "5"}, {"mm:", "04:"}, {":mm", ":04"}, + {"m:", "4:"}, + {":m", ":4"}, {"mm", "01"}, {"am/pm", "pm"}, {"m/", "1/"}, {"%%%%", "January"}, {"&&&&", "Monday"}, } + + replacementsGlobal := []struct{ xltime, gotime string }{ + {"\\-", "-"}, + {"\\ ", " "}, + {"\\.", "."}, + {"\\", ""}, + } // It is the presence of the "am/pm" indicator that determines if this is // a 12 hour or 24 hours time format, not the number of 'h' characters. if is12HourTime(format) { - format = strings.Replace(format, "hh", "03", 1) - format = strings.Replace(format, "h", "3", 1) + goFmt = strings.Replace(goFmt, "hh", "3", 1) + goFmt = strings.Replace(goFmt, "h", "3", 1) + goFmt = strings.Replace(goFmt, "HH", "3", 1) + goFmt = strings.Replace(goFmt, "H", "3", 1) } else { - format = strings.Replace(format, "hh", "15", 1) - format = strings.Replace(format, "h", "15", 1) + goFmt = strings.Replace(goFmt, "hh", "15", 1) + goFmt = strings.Replace(goFmt, "h", "3", 1) + goFmt = strings.Replace(goFmt, "HH", "15", 1) + goFmt = strings.Replace(goFmt, "H", "3", 1) } + for _, repl := range replacements { - format = strings.Replace(format, repl.xltime, repl.gotime, 1) + goFmt = strings.Replace(goFmt, repl.xltime, repl.gotime, 1) + } + for _, repl := range replacementsGlobal { + goFmt = strings.Replace(goFmt, repl.xltime, repl.gotime, -1) } // If the hour is optional, strip it out, along with the possible dangling // colon that would remain. if val.Hour() < 1 { - format = strings.Replace(format, "]:", "]", 1) - format = strings.Replace(format, "[03]", "", 1) - format = strings.Replace(format, "[3]", "", 1) - format = strings.Replace(format, "[15]", "", 1) + goFmt = strings.Replace(goFmt, "]:", "]", 1) + goFmt = strings.Replace(goFmt, "[03]", "", 1) + goFmt = strings.Replace(goFmt, "[3]", "", 1) + goFmt = strings.Replace(goFmt, "[15]", "", 1) } else { - format = strings.Replace(format, "[3]", "3", 1) - format = strings.Replace(format, "[15]", "15", 1) + goFmt = strings.Replace(goFmt, "[3]", "3", 1) + goFmt = strings.Replace(goFmt, "[15]", "15", 1) } - return val.Format(format) + + dateTimeFormatsCache[format] = goFmt + + return val.Format(goFmt) } // is12HourTime checks whether an Excel time format string is a 12 hours form. @@ -2226,6 +2281,7 @@ func newNumFmt(styleSheet *xlsxStyleSheet, style *Style) int { // setCustomNumFmt provides a function to set custom number format code. func setCustomNumFmt(styleSheet *xlsxStyleSheet, style *Style) int { nf := xlsxNumFmt{FormatCode: *style.CustomNumFmt} + if styleSheet.NumFmts != nil { nf.NumFmtID = styleSheet.NumFmts.NumFmt[len(styleSheet.NumFmts.NumFmt)-1].NumFmtID + 1 styleSheet.NumFmts.NumFmt = append(styleSheet.NumFmts.NumFmt, &nf) diff --git a/styles_test.go b/styles_test.go index b68365bc06..8ce26a4e7b 100644 --- a/styles_test.go +++ b/styles_test.go @@ -201,10 +201,44 @@ func TestNewStyle(t *testing.T) { assert.NoError(t, err) _, err = f.NewStyle(Style{}) assert.EqualError(t, err, "invalid parameter type") + _, err = f.NewStyle(&Style{Font: &Font{Family: strings.Repeat("s", MaxFontFamilyLength+1)}}) assert.EqualError(t, err, "the length of the font family name must be smaller than or equal to 31") _, err = f.NewStyle(&Style{Font: &Font{Size: MaxFontSize + 1}}) assert.EqualError(t, err, "font size must be between 1 and 409 points") + + // new numeric custom style + fmt := "####;####" + f.Styles.NumFmts = nil + styleID, err = f.NewStyle(&Style{ + CustomNumFmt: &fmt, + }) + assert.NoError(t, err) + assert.Equal(t, 2, styleID) + + assert.NotNil(t, f.Styles) + assert.NotNil(t, f.Styles.CellXfs) + assert.NotNil(t, f.Styles.CellXfs.Xf) + + nf := f.Styles.CellXfs.Xf[styleID] + assert.Equal(t, 164, *nf.NumFmtID) + + // new 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) + + assert.NotNil(t, f.Styles) + assert.NotNil(t, f.Styles.CellXfs) + assert.NotNil(t, f.Styles.CellXfs.Xf) + + nf = f.Styles.CellXfs.Xf[styleID] + assert.Equal(t, 32, *nf.NumFmtID) } func TestGetDefaultFont(t *testing.T) { @@ -250,3 +284,15 @@ func TestGetStyleID(t *testing.T) { func TestGetFillID(t *testing.T) { assert.Equal(t, -1, getFillID(NewFile().stylesReader(), &Style{Fill: Fill{Type: "unknown"}})) } + +func TestParseTime(t *testing.T) { + assert.Equal(t, "2019", parseTime("43528", "YYYY")) + assert.Equal(t, "43528", parseTime("43528", "")) + + assert.Equal(t, "2019-03-04 05:05:42", parseTime("43528.2123", "YYYY-MM-DD hh:mm:ss")) + assert.Equal(t, "2019-03-04 05:05:42", parseTime("43528.2123", "YYYY-MM-DD hh:mm:ss;YYYY-MM-DD hh:mm:ss")) + assert.Equal(t, "3/4/2019 5:5:42", parseTime("43528.2123", "M/D/YYYY h:m:s")) + assert.Equal(t, "March", parseTime("43528", "mmmm")) + assert.Equal(t, "Monday", parseTime("43528", "dddd")) + +} From 93160287bb7fa6479c73ee031b5ed771972a17a8 Mon Sep 17 00:00:00 2001 From: Lijingfeng <170574@qq.com> Date: Mon, 5 Oct 2020 22:17:11 +0800 Subject: [PATCH 287/957] Optimize memory usage when stream flush and save (#659) * use io.Copy from stream temp file to zip Writer * fix nil * log * build * delete log * fix compatibility for office * Update go module Co-authored-by: lijingfeng Co-authored-by: xuri --- excelize.go | 1 + file.go | 20 ++++++++++++++++++++ go.mod | 2 +- stream.go | 44 ++++++++------------------------------------ 4 files changed, 30 insertions(+), 37 deletions(-) diff --git a/excelize.go b/excelize.go index cca6616825..0c0f74a81e 100644 --- a/excelize.go +++ b/excelize.go @@ -36,6 +36,7 @@ type File struct { xmlAttr map[string][]xml.Attr checked map[string]bool sheetMap map[string]string + streams map[string]*StreamWriter CalcChain *xlsxCalcChain Comments map[string]*xlsxComments ContentTypes *xlsxTypes diff --git a/file.go b/file.go index 6a48c0c767..4a2ab10658 100644 --- a/file.go +++ b/file.go @@ -110,6 +110,26 @@ func (f *File) WriteToBuffer() (*bytes.Buffer, error) { f.sharedStringsWriter() f.styleSheetWriter() + for path, stream := range f.streams { + fi, err := zw.Create(path) + if err != nil { + zw.Close() + return buf, err + } + var from io.Reader + from, err = stream.rawData.Reader() + if err != nil { + stream.rawData.Close() + return buf, err + } + _, err = io.Copy(fi, from) + if err != nil { + zw.Close() + return buf, err + } + stream.rawData.Close() + } + for path, content := range f.XLSX { fi, err := zw.Create(path) if err != nil { diff --git a/go.mod b/go.mod index fb2c1e8b99..4b6e77801f 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/360EntSecGroup-Skylar/excelize/v2 -go 1.15 +go 1.11 require ( github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 diff --git a/stream.go b/stream.go index 83b25f7d71..bbb1ec197b 100644 --- a/stream.go +++ b/stream.go @@ -85,6 +85,13 @@ func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) { if err != nil { return nil, err } + + sheetXML := fmt.Sprintf("xl/worksheets/sheet%d.xml", sw.SheetID) + if f.streams == nil { + f.streams = make(map[string]*StreamWriter) + } + f.streams[sheetXML] = sw + sw.rawData.WriteString(XMLHeader + ``) @@ -387,13 +394,8 @@ func (sw *StreamWriter) Flush() error { sheetXML := fmt.Sprintf("xl/worksheets/sheet%d.xml", sw.SheetID) delete(sw.File.Sheet, sheetXML) delete(sw.File.checked, sheetXML) + delete(sw.File.XLSX, sheetXML) - defer sw.rawData.Close() - b, err := sw.rawData.Bytes() - if err != nil { - return err - } - sw.File.XLSX[sheetXML] = b return nil } @@ -444,36 +446,6 @@ func (bw *bufferedWriter) Reader() (io.Reader, error) { return io.NewSectionReader(bw.tmp, 0, fi.Size()), nil } -// Bytes returns the entire content of the bufferedWriter. If a temp file is -// used, Bytes will efficiently allocate a buffer to prevent re-allocations. -func (bw *bufferedWriter) Bytes() ([]byte, error) { - if bw.tmp == nil { - return bw.buf.Bytes(), nil - } - - if err := bw.Flush(); err != nil { - return nil, err - } - - var buf bytes.Buffer - if fi, err := bw.tmp.Stat(); err == nil { - if size := fi.Size() + bytes.MinRead; size > bytes.MinRead { - if int64(int(size)) == size { - buf.Grow(int(size)) - } else { - return nil, bytes.ErrTooLarge - } - } - } - - if _, err := bw.tmp.Seek(0, 0); err != nil { - return nil, err - } - - _, err := buf.ReadFrom(bw.tmp) - return buf.Bytes(), err -} - // Sync will write the in-memory buffer to a temp file, if the in-memory // buffer has grown large enough. Any error will be returned. func (bw *bufferedWriter) Sync() (err error) { From d1926675f81fcf9afb658e1e51cd4e43d0d05bc9 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 11 Oct 2020 00:15:04 +0800 Subject: [PATCH 288/957] - Resolve #627, improve multi-series line chart compatibility with KingSoft WPS - Avoid to create duplicate style - Update unit test for the auto filter - Init code scanning alerts --- .github/workflows/codeql-analysis.yml | 71 +++++++++++++++++++++++++++ adjust_test.go | 8 +++ drawing.go | 8 ++- styles.go | 2 +- 4 files changed, 83 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/codeql-analysis.yml diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000000..9dddb5771b --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,71 @@ +# 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' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + 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' }} + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + 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 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/adjust_test.go b/adjust_test.go index 13e47ffeab..3997bd9242 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -49,6 +49,14 @@ func TestAdjustMergeCells(t *testing.T) { func TestAdjustAutoFilter(t *testing.T) { f := NewFile() + assert.NoError(t, f.adjustAutoFilter(&xlsxWorksheet{ + SheetData: xlsxSheetData{ + Row: []xlsxRow{{Hidden: true, R: 2}}, + }, + AutoFilter: &xlsxAutoFilter{ + Ref: "A1:A3", + }, + }, rows, 1, -1)) // testing adjustAutoFilter with illegal cell coordinates. assert.EqualError(t, f.adjustAutoFilter(&xlsxWorksheet{ AutoFilter: &xlsxAutoFilter{ diff --git a/drawing.go b/drawing.go index 6c2f6357f5..666b23d695 100644 --- a/drawing.go +++ b/drawing.go @@ -770,13 +770,11 @@ func (f *File) drawChartSeriesSpPr(i int, formatSet *formatChart) *cSpPr { Ln: &aLn{ 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)}, + }, }, } - if i+formatSet.order < 6 { - spPrLine.Ln.SolidFill = &aSolidFill{ - SchemeClr: &aSchemeClr{Val: "accent" + strconv.Itoa(i+formatSet.order+1)}, - } - } chartSeriesSpPr := map[string]*cSpPr{Line: spPrLine, Scatter: spPrScatter} return chartSeriesSpPr[formatSet.Type] } diff --git a/styles.go b/styles.go index d4d0468904..896eaa1bf6 100644 --- a/styles.go +++ b/styles.go @@ -2063,7 +2063,7 @@ var getXfIDFuncs = map[string]func(int, xlsxXf, *Style) bool{ if style.Alignment == nil { return xf.ApplyAlignment == nil || *xf.ApplyAlignment == false } - return reflect.DeepEqual(xf.Alignment, newAlignment(style)) && xf.ApplyBorder != nil && *xf.ApplyBorder == true + return reflect.DeepEqual(xf.Alignment, newAlignment(style)) }, "protection": func(ID int, xf xlsxXf, style *Style) bool { if style.Protection == nil { From ac3dce0beaef6cbf2b08db4a1e2b2a8d100c77ca Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 12 Oct 2020 00:05:27 +0800 Subject: [PATCH 289/957] - Resolve #711, update docs for the GetSheetIndex - Update unit test --- calc.go | 37 ++++++++++++++++--------------------- calc_test.go | 26 +++++++++++++++++++++++--- crypt_test.go | 12 ++++++++++++ rows_test.go | 2 +- sheet.go | 4 ++-- 5 files changed, 54 insertions(+), 27 deletions(-) diff --git a/calc.go b/calc.go index 54683a82c6..7b9785cc7f 100644 --- a/calc.go +++ b/calc.go @@ -700,19 +700,19 @@ func formulaCriteriaParser(exp string) (fc *formulaCriteria) { fc.Type, fc.Condition = criteriaEq, match[1] return } - if match := regexp.MustCompile(`^<(.*)$`).FindStringSubmatch(exp); len(match) > 1 { + 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 { + 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 { + 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 { + if match := regexp.MustCompile(`^>(.*)$`).FindStringSubmatch(exp); len(match) > 1 { fc.Type, fc.Condition = criteriaG, match[1] return } @@ -732,8 +732,11 @@ func formulaCriteriaParser(exp string) (fc *formulaCriteria) { // formulaCriteriaEval evaluate formula criteria expression. func formulaCriteriaEval(val string, criteria *formulaCriteria) (result bool, err error) { var value, expected float64 + var e error var prepareValue = func(val, cond string) (value float64, expected float64, err error) { - value, _ = strconv.ParseFloat(val, 64) + if value, err = strconv.ParseFloat(val, 64); err != nil { + return + } if expected, err = strconv.ParseFloat(criteria.Condition, 64); err != nil { return } @@ -743,25 +746,17 @@ func formulaCriteriaEval(val string, criteria *formulaCriteria) (result bool, er case criteriaEq: return val == criteria.Condition, err case criteriaLe: - if value, expected, err = prepareValue(val, criteria.Condition); err != nil { - return - } - return value <= expected, err + value, expected, e = prepareValue(val, criteria.Condition) + return value <= expected && e == nil, err case criteriaGe: - if value, expected, err = prepareValue(val, criteria.Condition); err != nil { - return - } - return value >= expected, err + value, expected, e = prepareValue(val, criteria.Condition) + return value >= expected && e == nil, err case criteriaL: - if value, expected, err = prepareValue(val, criteria.Condition); err != nil { - return - } - return value < expected, err + value, expected, e = prepareValue(val, criteria.Condition) + return value < expected && e == nil, err case criteriaG: - if value, expected, err = prepareValue(val, criteria.Condition); err != nil { - return - } - return value > expected, err + value, expected, e = prepareValue(val, criteria.Condition) + return value > expected && e == nil, err case criteriaBeg: return strings.HasPrefix(val, criteria.Condition), err case criteriaEnd: diff --git a/calc_test.go b/calc_test.go index 4298aa7a39..7d7b886e43 100644 --- a/calc_test.go +++ b/calc_test.go @@ -339,7 +339,8 @@ func TestCalcCellValue(t *testing.T) { "=SINH(0.5)": "0.5210953054937474", "=SINH(-2)": "-3.626860407847019", // SQRT - "=SQRT(4)": "2", + "=SQRT(4)": "2", + `=SQRT("")`: "0", // SQRTPI "=SQRTPI(5)": "3.963327297606011", "=SQRTPI(0.2)": "0.7926654595212022", @@ -361,7 +362,15 @@ func TestCalcCellValue(t *testing.T) { "=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", // 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", @@ -706,7 +715,8 @@ func TestCalcCellValue(t *testing.T) { // ISERROR "=ISERROR()": "ISERROR requires 1 argument", // ISEVEN - "=ISEVEN()": "ISEVEN requires 1 argument", + "=ISEVEN()": "ISEVEN requires 1 argument", + `=ISEVEN("text")`: "#VALUE!", // ISNA "=ISNA()": "ISNA requires 1 argument", // ISNONTEXT @@ -714,7 +724,8 @@ func TestCalcCellValue(t *testing.T) { // ISNUMBER "=ISNUMBER()": "ISNUMBER requires 1 argument", // ISODD - "=ISODD()": "ISODD requires 1 argument", + "=ISODD()": "ISODD requires 1 argument", + `=ISODD("text")`: "#VALUE!", // NA "=NA(1)": "NA accepts no arguments", } @@ -817,3 +828,12 @@ func TestCalcCellValueWithDefinedName(t *testing.T) { // DefinedName with scope WorkSheet takes precedence over DefinedName with scope Workbook, so we should get B1 value assert.Equal(t, "B1 value", result, "=defined_name1") } + +func TestDet(t *testing.T) { + assert.Equal(t, det([][]float64{ + {1, 2, 3, 4}, + {2, 3, 4, 5}, + {3, 4, 5, 6}, + {4, 5, 6, 7}, + }), float64(0)) +} diff --git a/crypt_test.go b/crypt_test.go index f9a3fb7a6f..6f712c1944 100644 --- a/crypt_test.go +++ b/crypt_test.go @@ -21,3 +21,15 @@ func TestEncrypt(t *testing.T) { assert.NoError(t, err) assert.EqualError(t, f.SaveAs(filepath.Join("test", "BadEncrypt.xlsx"), Options{Password: "password"}), "not support encryption currently") } + +func TestEncryptionMechanism(t *testing.T) { + mechanism, err := encryptionMechanism([]byte{3, 0, 3, 0}) + assert.Equal(t, mechanism, "extensible") + assert.EqualError(t, err, "unsupport encryption mechanism") + _, err = encryptionMechanism([]byte{}) + assert.EqualError(t, err, "unknown encryption mechanism") +} + +func TestHashing(t *testing.T) { + assert.Equal(t, hashing("unsupportHashAlgorithm", []byte{}), []uint8([]byte(nil))) +} diff --git a/rows_test.go b/rows_test.go index 246233ffd7..edbc4bd146 100644 --- a/rows_test.go +++ b/rows_test.go @@ -835,7 +835,7 @@ func TestGetValueFromNumber(t *testing.T) { assert.Equal(t, "2.22", val) c = &xlsxC{T: "n", V: "2.220000ddsf0000000002-r"} - val, err = c.getValueFrom(f, d) + _, err = c.getValueFrom(f, d) assert.NotNil(t, err) assert.Equal(t, "strconv.ParseFloat: parsing \"2.220000ddsf0000000002-r\": invalid syntax", err.Error()) } diff --git a/sheet.go b/sheet.go index dedd2d950e..a44e391007 100644 --- a/sheet.go +++ b/sheet.go @@ -360,8 +360,8 @@ func (f *File) getSheetID(name 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, it will return an -// integer type value 0. +// 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(name string) int { var idx = -1 for index, sheet := range f.GetSheetList() { From 02530e8c8ad94308458a33ac694e6ac9d3af4c87 Mon Sep 17 00:00:00 2001 From: xuri Date: Sat, 17 Oct 2020 00:57:12 +0800 Subject: [PATCH 290/957] Fix #701, init new formula function AND and OR, prevent formula lexer panic on retrieving the top token type --- calc.go | 296 ++++++++++++++++++++++++++++++++++++++++----------- calc_test.go | 95 +++++++++++++++++ go.mod | 8 +- go.sum | 27 ++--- 4 files changed, 337 insertions(+), 89 deletions(-) diff --git a/calc.go b/calc.go index 7b9785cc7f..111bc60c5f 100644 --- a/calc.go +++ b/calc.go @@ -93,21 +93,36 @@ type formulaArg struct { // formulaFuncs is the type of the formula functions. type formulaFuncs struct{} +// tokenPriority defined basic arithmetic operator priority. +var tokenPriority = map[string]int{ + "^": 5, + "*": 4, + "/": 4, + "+": 3, + "-": 3, + "=": 2, + "<": 2, + "<=": 2, + ">": 2, + ">=": 2, + "&": 1, +} + // 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. // // Supported formulas: // -// ABS, ACOS, ACOSH, ACOT, ACOTH, ARABIC, ASIN, ASINH, ATAN2, ATANH, BASE, -// CEILING, CEILING.MATH, CEILING.PRECISE, COMBIN, COMBINA, COS, COSH, COT, -// COTH, COUNTA, CSC, CSCH, DECIMAL, DEGREES, EVEN, EXP, FACT, FACTDOUBLE, -// FLOOR, FLOOR.MATH, FLOOR.PRECISE, GCD, INT, ISBLANK, ISERR, ISERROR, -// ISEVEN, ISNA, ISNONTEXT, ISNUMBER, ISO.CEILING, ISODD, LCM, LN, LOG, -// LOG10, MDETERM, MEDIAN, MOD, MROUND, MULTINOMIAL, MUNIT, NA, ODD, PI, -// POWER, PRODUCT, QUOTIENT, RADIANS, RAND, RANDBETWEEN, ROUND, ROUNDDOWN, -// ROUNDUP, SEC, SECH, SIGN, SIN, SINH, SQRT, SQRTPI, SUM, SUMIF, SUMSQ, -// TAN, TANH, TRUNC +// ABS, ACOS, ACOSH, ACOT, ACOTH, AND, ARABIC, ASIN, ASINH, ATAN2, ATANH, +// BASE, CEILING, CEILING.MATH, CEILING.PRECISE, COMBIN, COMBINA, COS, +// COSH, COT, COTH, COUNTA, CSC, CSCH, DECIMAL, DEGREES, EVEN, EXP, FACT, +// FACTDOUBLE, FLOOR, FLOOR.MATH, FLOOR.PRECISE, GCD, INT, ISBLANK, ISERR, +// ISERROR, ISEVEN, ISNA, ISNONTEXT, ISNUMBER, ISO.CEILING, ISODD, LCM, +// LN, LOG, LOG10, MDETERM, MEDIAN, MOD, MROUND, MULTINOMIAL, MUNIT, NA, +// ODD, OR, PI, POWER, PRODUCT, QUOTIENT, RADIANS, RAND, RANDBETWEEN, +// ROUND, ROUNDDOWN, ROUNDUP, SEC, SECH, SIGN, SIN, SINH, SQRT, SQRTPI, +// SUM, SUMIF, SUMSQ, TAN, TANH, TRUNC // func (f *File) CalcCellValue(sheet, cell string) (result string, err error) { var ( @@ -131,15 +146,9 @@ func (f *File) CalcCellValue(sheet, cell string) (result string, err error) { // getPriority calculate arithmetic operator priority. func getPriority(token efp.Token) (pri int) { - var priority = map[string]int{ - "*": 2, - "/": 2, - "+": 1, - "-": 1, - } - pri, _ = priority[token.TValue] + pri, _ = tokenPriority[token.TValue] if token.TValue == "-" && token.TType == efp.TokenTypeOperatorPrefix { - pri = 3 + pri = 6 } if token.TSubType == efp.TokenSubTypeStart && token.TType == efp.TokenTypeSubexpression { // ( pri = 0 @@ -306,18 +315,96 @@ func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error) return opdStack.Peek().(efp.Token), err } -// calcAdd evaluate addition arithmetic operations. -func calcAdd(opdStack *Stack) error { - if opdStack.Len() < 2 { - return errors.New("formula not valid") +// calcPow evaluate exponentiation arithmetic operations. +func calcPow(rOpd, lOpd string, opdStack *Stack) error { + lOpdVal, err := strconv.ParseFloat(lOpd, 64) + if err != nil { + return err + } + rOpdVal, err := strconv.ParseFloat(rOpd, 64) + if err != nil { + return err + } + result := math.Pow(lOpdVal, rOpdVal) + opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + return nil +} + +// calcEq evaluate equal arithmetic operations. +func calcEq(rOpd, lOpd string, opdStack *Stack) error { + opdStack.Push(efp.Token{TValue: strings.ToUpper(strconv.FormatBool(rOpd == lOpd)), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + return nil +} + +// calcL evaluate less than arithmetic operations. +func calcL(rOpd, lOpd string, opdStack *Stack) error { + lOpdVal, err := strconv.ParseFloat(lOpd, 64) + if err != nil { + return err + } + rOpdVal, err := strconv.ParseFloat(rOpd, 64) + if err != nil { + return err + } + opdStack.Push(efp.Token{TValue: strings.ToUpper(strconv.FormatBool(rOpdVal > lOpdVal)), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + return nil +} + +// calcLe evaluate less than or equal arithmetic operations. +func calcLe(rOpd, lOpd string, opdStack *Stack) error { + lOpdVal, err := strconv.ParseFloat(lOpd, 64) + if err != nil { + return err + } + rOpdVal, err := strconv.ParseFloat(rOpd, 64) + if err != nil { + return err } - rOpd := opdStack.Pop().(efp.Token) - lOpd := opdStack.Pop().(efp.Token) - lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64) + opdStack.Push(efp.Token{TValue: strings.ToUpper(strconv.FormatBool(rOpdVal >= lOpdVal)), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + return nil +} + +// calcG evaluate greater than or equal arithmetic operations. +func calcG(rOpd, lOpd string, opdStack *Stack) error { + lOpdVal, err := strconv.ParseFloat(lOpd, 64) + if err != nil { + return err + } + rOpdVal, err := strconv.ParseFloat(rOpd, 64) + if err != nil { + return err + } + opdStack.Push(efp.Token{TValue: strings.ToUpper(strconv.FormatBool(rOpdVal < lOpdVal)), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + return nil +} + +// calcGe evaluate greater than or equal arithmetic operations. +func calcGe(rOpd, lOpd string, opdStack *Stack) error { + lOpdVal, err := strconv.ParseFloat(lOpd, 64) if err != nil { return err } - rOpdVal, err := strconv.ParseFloat(rOpd.TValue, 64) + rOpdVal, err := strconv.ParseFloat(rOpd, 64) + if err != nil { + return err + } + opdStack.Push(efp.Token{TValue: strings.ToUpper(strconv.FormatBool(rOpdVal <= lOpdVal)), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + return nil +} + +// calcSplice evaluate splice '&' operations. +func calcSplice(rOpd, lOpd string, opdStack *Stack) error { + opdStack.Push(efp.Token{TValue: lOpd + rOpd, TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) + return nil +} + +// calcAdd evaluate addition arithmetic operations. +func calcAdd(rOpd, lOpd string, opdStack *Stack) error { + lOpdVal, err := strconv.ParseFloat(lOpd, 64) + if err != nil { + return err + } + rOpdVal, err := strconv.ParseFloat(rOpd, 64) if err != nil { return err } @@ -327,17 +414,12 @@ func calcAdd(opdStack *Stack) error { } // calcSubtract evaluate subtraction arithmetic operations. -func calcSubtract(opdStack *Stack) error { - if opdStack.Len() < 2 { - return errors.New("formula not valid") - } - rOpd := opdStack.Pop().(efp.Token) - lOpd := opdStack.Pop().(efp.Token) - lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64) +func calcSubtract(rOpd, lOpd string, opdStack *Stack) error { + lOpdVal, err := strconv.ParseFloat(lOpd, 64) if err != nil { return err } - rOpdVal, err := strconv.ParseFloat(rOpd.TValue, 64) + rOpdVal, err := strconv.ParseFloat(rOpd, 64) if err != nil { return err } @@ -347,17 +429,12 @@ func calcSubtract(opdStack *Stack) error { } // calcMultiply evaluate multiplication arithmetic operations. -func calcMultiply(opdStack *Stack) error { - if opdStack.Len() < 2 { - return errors.New("formula not valid") - } - rOpd := opdStack.Pop().(efp.Token) - lOpd := opdStack.Pop().(efp.Token) - lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64) +func calcMultiply(rOpd, lOpd string, opdStack *Stack) error { + lOpdVal, err := strconv.ParseFloat(lOpd, 64) if err != nil { return err } - rOpdVal, err := strconv.ParseFloat(rOpd.TValue, 64) + rOpdVal, err := strconv.ParseFloat(rOpd, 64) if err != nil { return err } @@ -366,18 +443,13 @@ func calcMultiply(opdStack *Stack) error { return nil } -// calcDivide evaluate division arithmetic operations. -func calcDivide(opdStack *Stack) error { - if opdStack.Len() < 2 { - return errors.New("formula not valid") - } - rOpd := opdStack.Pop().(efp.Token) - lOpd := opdStack.Pop().(efp.Token) - lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64) +// calcDiv evaluate division arithmetic operations. +func calcDiv(rOpd, lOpd string, opdStack *Stack) error { + lOpdVal, err := strconv.ParseFloat(lOpd, 64) if err != nil { return err } - rOpdVal, err := strconv.ParseFloat(rOpd.TValue, 64) + rOpdVal, err := strconv.ParseFloat(rOpd, 64) if err != nil { return err } @@ -403,24 +475,36 @@ func calculate(opdStack *Stack, opt efp.Token) error { result := 0 - opdVal opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) } - - if opt.TValue == "+" { - if err := calcAdd(opdStack); err != nil { - return err - } + tokenCalcFunc := map[string]func(rOpd, lOpd string, opdStack *Stack) error{ + "^": calcPow, + "*": calcMultiply, + "/": calcDiv, + "+": calcAdd, + "=": calcEq, + "<": calcL, + "<=": calcLe, + ">": calcG, + ">=": calcGe, + "&": calcSplice, } if opt.TValue == "-" && opt.TType == efp.TokenTypeOperatorInfix { - if err := calcSubtract(opdStack); err != nil { - return err + if opdStack.Len() < 2 { + return errors.New("formula not valid") } - } - if opt.TValue == "*" { - if err := calcMultiply(opdStack); err != nil { + rOpd := opdStack.Pop().(efp.Token) + lOpd := opdStack.Pop().(efp.Token) + if err := calcSubtract(rOpd.TValue, lOpd.TValue, opdStack); err != nil { return err } } - if opt.TValue == "/" { - if err := calcDivide(opdStack); err != nil { + fn, ok := tokenCalcFunc[opt.TValue] + if ok { + if opdStack.Len() < 2 { + return errors.New("formula not valid") + } + rOpd := opdStack.Pop().(efp.Token) + lOpd := opdStack.Pop().(efp.Token) + if err := fn(rOpd.TValue, lOpd.TValue, opdStack); err != nil { return err } } @@ -459,8 +543,8 @@ func (f *File) parseOperatorPrefixToken(optStack, opdStack *Stack, token efp.Tok // isOperatorPrefixToken determine if the token is parse operator prefix // token. func isOperatorPrefixToken(token efp.Token) bool { - if (token.TValue == "-" && token.TType == efp.TokenTypeOperatorPrefix) || - token.TValue == "+" || token.TValue == "-" || token.TValue == "*" || token.TValue == "/" { + _, ok := tokenPriority[token.TValue] + if (token.TValue == "-" && token.TType == efp.TokenTypeOperatorPrefix) || ok { return true } return false @@ -3140,3 +3224,87 @@ func (fn *formulaFuncs) NA(argsList *list.List) (result string, err error) { result = formulaErrorNA return } + +// Logical Functions + +// AND function tests a number of supplied conditions and returns TRUE or +// FALSE. +func (fn *formulaFuncs) AND(argsList *list.List) (result string, err error) { + if argsList.Len() == 0 { + err = errors.New("AND requires at least 1 argument") + return + } + if argsList.Len() > 30 { + err = errors.New("AND accepts at most 30 arguments") + return + } + var and = true + var val float64 + for arg := argsList.Front(); arg != nil; arg = arg.Next() { + token := arg.Value.(formulaArg) + switch token.Type { + case ArgUnknown: + continue + case ArgString: + if token.String == "TRUE" { + continue + } + if token.String == "FALSE" { + result = token.String + return + } + if val, err = strconv.ParseFloat(token.String, 64); err != nil { + err = errors.New(formulaErrorVALUE) + return + } + and = and && (val != 0) + case ArgMatrix: + // TODO + err = errors.New(formulaErrorVALUE) + return + } + } + result = strings.ToUpper(strconv.FormatBool(and)) + return +} + +// OR function tests a number of supplied conditions and returns either TRUE +// or FALSE. +func (fn *formulaFuncs) OR(argsList *list.List) (result string, err error) { + if argsList.Len() == 0 { + err = errors.New("OR requires at least 1 argument") + return + } + if argsList.Len() > 30 { + err = errors.New("OR accepts at most 30 arguments") + return + } + var or bool + var val float64 + for arg := argsList.Front(); arg != nil; arg = arg.Next() { + token := arg.Value.(formulaArg) + switch token.Type { + case ArgUnknown: + continue + case ArgString: + if token.String == "FALSE" { + continue + } + if token.String == "TRUE" { + or = true + continue + } + if val, err = strconv.ParseFloat(token.String, 64); err != nil { + err = errors.New(formulaErrorVALUE) + return + } + or = val != 0 + case ArgMatrix: + // TODO + err = errors.New(formulaErrorVALUE) + return + } + } + result = strings.ToUpper(strconv.FormatBool(or)) + return +} diff --git a/calc_test.go b/calc_test.go index 7d7b886e43..02b51610a1 100644 --- a/calc_test.go +++ b/calc_test.go @@ -1,7 +1,9 @@ package excelize import ( + "container/list" "path/filepath" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -31,6 +33,18 @@ func TestCalcCellValue(t *testing.T) { } mathCalc := map[string]string{ + "=2^3": "8", + "=1=1": "TRUE", + "=1=2": "FALSE", + "=1<2": "TRUE", + "=3<2": "FALSE", + "=2<=3": "TRUE", + "=2<=1": "FALSE", + "=2>1": "TRUE", + "=2>3": "FALSE", + "=2>=1": "TRUE", + "=2>=3": "FALSE", + "=1&2": "12", // ABS "=ABS(-1)": "1", "=ABS(-6.5)": "6.5", @@ -429,6 +443,20 @@ func TestCalcCellValue(t *testing.T) { "=ISODD(A2)": "FALSE", // NA "=NA()": "#N/A", + // 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", + // OR + "=OR(1)": "TRUE", + "=OR(0)": "FALSE", + "=OR(1=2,2=2)": "TRUE", + "=OR(1=2,2=3)": "FALSE", } for formula, expected := range mathCalc { f := prepareData() @@ -728,6 +756,16 @@ func TestCalcCellValue(t *testing.T) { `=ISODD("text")`: "#VALUE!", // NA "=NA(1)": "NA accepts no arguments", + // AND + `=AND("text")`: "#VALUE!", + `=AND(A1:B1)`: "#VALUE!", + "=AND()": "AND requires at least 1 argument", + "=AND(1" + strings.Repeat(",1", 30) + ")": "AND accepts at most 30 arguments", + // OR + `=OR("text")`: "#VALUE!", + `=OR(A1:B1)`: "#VALUE!", + "=OR()": "OR requires at least 1 argument", + "=OR(1" + strings.Repeat(",1", 30) + ")": "OR accepts at most 30 arguments", } for formula, expected := range mathCalcError { f := prepareData() @@ -829,6 +867,63 @@ func TestCalcCellValueWithDefinedName(t *testing.T) { assert.Equal(t, "B1 value", result, "=defined_name1") } +func TestCalcPow(t *testing.T) { + err := `strconv.ParseFloat: parsing "text": invalid syntax` + assert.EqualError(t, calcPow("1", "text", nil), err) + assert.EqualError(t, calcPow("text", "1", nil), err) + assert.EqualError(t, calcL("1", "text", nil), err) + assert.EqualError(t, calcL("text", "1", nil), err) + assert.EqualError(t, calcLe("1", "text", nil), err) + assert.EqualError(t, calcLe("text", "1", nil), err) + assert.EqualError(t, calcG("1", "text", nil), err) + assert.EqualError(t, calcG("text", "1", nil), err) + assert.EqualError(t, calcGe("1", "text", nil), err) + assert.EqualError(t, calcGe("text", "1", nil), err) + assert.EqualError(t, calcAdd("1", "text", nil), err) + assert.EqualError(t, calcAdd("text", "1", nil), err) + assert.EqualError(t, calcAdd("1", "text", nil), err) + assert.EqualError(t, calcAdd("text", "1", nil), err) + assert.EqualError(t, calcSubtract("1", "text", nil), err) + assert.EqualError(t, calcSubtract("text", "1", nil), err) + assert.EqualError(t, calcMultiply("1", "text", nil), err) + assert.EqualError(t, calcMultiply("text", "1", nil), err) + assert.EqualError(t, calcDiv("1", "text", nil), err) + assert.EqualError(t, calcDiv("text", "1", nil), err) +} + +func TestISBLANK(t *testing.T) { + argsList := list.New() + argsList.PushBack(formulaArg{ + Type: ArgUnknown, + }) + fn := formulaFuncs{} + result, err := fn.ISBLANK(argsList) + assert.Equal(t, result, "TRUE") + assert.NoError(t, err) +} + +func TestAND(t *testing.T) { + argsList := list.New() + argsList.PushBack(formulaArg{ + Type: ArgUnknown, + }) + fn := formulaFuncs{} + result, err := fn.AND(argsList) + assert.Equal(t, result, "TRUE") + assert.NoError(t, err) +} + +func TestOR(t *testing.T) { + argsList := list.New() + argsList.PushBack(formulaArg{ + Type: ArgUnknown, + }) + fn := formulaFuncs{} + result, err := fn.OR(argsList) + assert.Equal(t, result, "FALSE") + assert.NoError(t, err) +} + func TestDet(t *testing.T) { assert.Equal(t, det([][]float64{ {1, 2, 3, 4}, diff --git a/go.mod b/go.mod index 4b6e77801f..773f0a3914 100644 --- a/go.mod +++ b/go.mod @@ -5,10 +5,8 @@ go 1.11 require ( github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 github.com/richardlehane/mscfb v1.0.3 - github.com/stretchr/testify v1.6.1 - github.com/xuri/efp v0.0.0-20200605144744-ba689101faaf - golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a - golang.org/x/image v0.0.0-20200922025426-e59bae62ef32 - golang.org/x/net v0.0.0-20200904194848-62affa334b73 + github.com/xuri/efp v0.0.0-20201016154823-031c29024257 + golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee + golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0 golang.org/x/text v0.3.3 ) diff --git a/go.sum b/go.sum index e082e86359..17e16a5a58 100644 --- a/go.sum +++ b/go.sum @@ -1,35 +1,22 @@ -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/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.3 h1:rD8TBkYWkObWO0oLDFCbwMeZ4KoalxQy+QgniCj3nKI= github.com/richardlehane/mscfb v1.0.3/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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/xuri/efp v0.0.0-20200605144744-ba689101faaf h1:spotWVWg9DP470pPFQ7LaYtUqDpWEOS/BUrSmwFZE4k= -github.com/xuri/efp v0.0.0-20200605144744-ba689101faaf/go.mod h1:uBiSUepVYMhGTfDeBKKasV4GpgBlzJ46gXUBAqV8qLk= +github.com/xuri/efp v0.0.0-20201016154823-031c29024257 h1:6ldmGEJXtsRMwdR2KuS3esk9wjVJNvgk05/YY2XmOj0= +github.com/xuri/efp v0.0.0-20201016154823-031c29024257/go.mod h1:uBiSUepVYMhGTfDeBKKasV4GpgBlzJ46gXUBAqV8qLk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM= -golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/image v0.0.0-20200922025426-e59bae62ef32 h1:E+SEVulmY8U4+i6vSB88YSc2OKAFfvbHPU/uDTdQu7M= -golang.org/x/image v0.0.0-20200922025426-e59bae62ef32/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee h1:4yd7jl+vXjalO5ztz6Vc1VADv+S/80LGJmyl1ROJ2AI= +golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20200904194848-62affa334b73 h1:MXfv8rhZWmFeqX3GNZRsd6vOLoaCHjYEX3qkRo3YBUA= -golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0 h1:5kGOVHlq0euqwzgTC9Vu15p6fV1Wi0ArVi8da2urnVg= +golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 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= From 520aa679f34bafbc00626151075b0b123eceb516 Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 18 Oct 2020 00:01:33 +0800 Subject: [PATCH 291/957] Fix #706, #713 improve AddPicture performance, fix missing worksheet when rename with same names --- col.go | 23 ++--------------------- col_test.go | 4 ++++ drawing.go | 2 +- excelize_test.go | 11 ----------- picture.go | 2 +- shape.go | 2 +- sheet.go | 3 +++ sheet_test.go | 27 +++++++++++++++++++++++++-- 8 files changed, 37 insertions(+), 37 deletions(-) diff --git a/col.go b/col.go index 19ce99b3d4..f7a77da9a3 100644 --- a/col.go +++ b/col.go @@ -559,26 +559,7 @@ func flatCols(col xlsxCol, cols []xlsxCol, replacer func(fc, c xlsxCol) xlsxCol) // width # Width of object frame. // height # Height of object frame. // -// xAbs # Absolute distance to left side of object. -// yAbs # Absolute distance to top side of object. -// -func (f *File) positionObjectPixels(sheet string, col, row, x1, y1, width, height int) (int, int, int, int, int, int, int, int) { - xAbs := 0 - yAbs := 0 - - // Calculate the absolute x offset of the top-left vertex. - for colID := 1; colID <= col; colID++ { - xAbs += f.getColWidth(sheet, colID) - } - xAbs += x1 - - // Calculate the absolute y offset of the top-left vertex. - // Store the column change to allow optimisations. - for rowID := 1; rowID <= row; rowID++ { - yAbs += f.getRowHeight(sheet, rowID) - } - yAbs += y1 - +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) @@ -613,7 +594,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, xAbs, yAbs, colEnd, rowEnd, x2, y2 + return col, row, colEnd, rowEnd, x2, y2 } // getColWidth provides a function to get column width in pixels by given diff --git a/col_test.go b/col_test.go index 02c5ca2398..532f42855e 100644 --- a/col_test.go +++ b/col_test.go @@ -362,3 +362,7 @@ func TestRemoveCol(t *testing.T) { assert.NoError(t, f.SaveAs(filepath.Join("test", "TestRemoveCol.xlsx"))) } + +func TestConvertColWidthToPixels(t *testing.T) { + assert.Equal(t, -11.0, convertColWidthToPixels(-1)) +} diff --git a/drawing.go b/drawing.go index 666b23d695..96403b3f27 100644 --- a/drawing.go +++ b/drawing.go @@ -1178,7 +1178,7 @@ func (f *File) addDrawingChart(sheet, drawingXML, cell string, width, height, rI width = int(float64(width) * formatSet.XScale) height = int(float64(height) * formatSet.YScale) - colStart, rowStart, _, _, colEnd, rowEnd, x2, y2 := + colStart, rowStart, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, colIdx, rowIdx, formatSet.OffsetX, formatSet.OffsetY, width, height) content, cNvPrID := f.drawingParser(drawingXML) twoCellAnchor := xdrCellAnchor{} diff --git a/excelize_test.go b/excelize_test.go index 890bcf61e6..9c1b1e6299 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -954,17 +954,6 @@ func TestGetSheetComments(t *testing.T) { assert.Equal(t, "", f.getSheetComments("sheet0")) } -func TestSetActiveSheet(t *testing.T) { - f := NewFile() - f.WorkBook.BookViews = nil - f.SetActiveSheet(1) - f.WorkBook.BookViews = &xlsxBookViews{WorkBookView: []xlsxWorkBookView{}} - f.Sheet["xl/worksheets/sheet1.xml"].SheetViews = &xlsxSheetViews{SheetView: []xlsxSheetView{}} - f.SetActiveSheet(1) - f.Sheet["xl/worksheets/sheet1.xml"].SheetViews = nil - f.SetActiveSheet(1) -} - func TestSetSheetVisible(t *testing.T) { f := NewFile() f.WorkBook.Sheets.Sheet[0].Name = "SheetN" diff --git a/picture.go b/picture.go index 9a646379c3..2f685ffa6e 100644 --- a/picture.go +++ b/picture.go @@ -259,7 +259,7 @@ func (f *File) addDrawingPicture(sheet, drawingXML, cell, file string, width, he } col-- row-- - colStart, rowStart, _, _, colEnd, rowEnd, x2, y2 := + colStart, rowStart, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, col, row, formatSet.OffsetX, formatSet.OffsetY, width, height) content, cNvPrID := f.drawingParser(drawingXML) twoCellAnchor := xdrCellAnchor{} diff --git a/shape.go b/shape.go index 0a5164b4a1..2600e901f6 100644 --- a/shape.go +++ b/shape.go @@ -324,7 +324,7 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, formatSet *format width := int(float64(formatSet.Width) * formatSet.Format.XScale) height := int(float64(formatSet.Height) * formatSet.Format.YScale) - colStart, rowStart, _, _, colEnd, rowEnd, x2, y2 := + colStart, rowStart, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, colIdx, rowIdx, formatSet.Format.OffsetX, formatSet.Format.OffsetY, width, height) content, cNvPrID := f.drawingParser(drawingXML) diff --git a/sheet.go b/sheet.go index a44e391007..aaa72cc35b 100644 --- a/sheet.go +++ b/sheet.go @@ -308,6 +308,9 @@ func (f *File) getActiveSheetID() int { func (f *File) SetSheetName(oldName, newName string) { oldName = trimSheetName(oldName) newName = trimSheetName(newName) + if newName == oldName { + return + } content := f.workbookReader() for k, v := range content.Sheets.Sheet { if v.Name == oldName { diff --git a/sheet_test.go b/sheet_test.go index 890a4e5322..56f5f46854 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -300,7 +300,8 @@ func TestRemovePageBreak(t *testing.T) { } func TestGetSheetName(t *testing.T) { - f, _ := OpenFile(filepath.Join("test", "Book1.xlsx")) + f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + assert.NoError(t, err) assert.Equal(t, "Sheet1", f.GetSheetName(0)) assert.Equal(t, "Sheet2", f.GetSheetName(1)) assert.Equal(t, "", f.GetSheetName(-1)) @@ -312,10 +313,32 @@ func TestGetSheetMap(t *testing.T) { 1: "Sheet1", 2: "Sheet2", } - f, _ := OpenFile(filepath.Join("test", "Book1.xlsx")) + f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + assert.NoError(t, err) sheetMap := f.GetSheetMap() for idx, name := range sheetMap { assert.Equal(t, expectedMap[idx], name) } assert.Equal(t, len(sheetMap), 2) } + +func TestSetActiveSheet(t *testing.T) { + f := NewFile() + f.WorkBook.BookViews = nil + f.SetActiveSheet(1) + f.WorkBook.BookViews = &xlsxBookViews{WorkBookView: []xlsxWorkBookView{}} + f.Sheet["xl/worksheets/sheet1.xml"].SheetViews = &xlsxSheetViews{SheetView: []xlsxSheetView{}} + f.SetActiveSheet(1) + f.Sheet["xl/worksheets/sheet1.xml"].SheetViews = nil + f.SetActiveSheet(1) + f = NewFile() + f.SetActiveSheet(-1) + assert.Equal(t, f.GetActiveSheetIndex(), 0) +} + +func TestSetSheetName(t *testing.T) { + f := NewFile() + // Test set workksheet with the same name. + f.SetSheetName("Sheet1", "Sheet1") + assert.Equal(t, "Sheet1", f.GetSheetName(0)) +} From 4834a058aa3f953a7010704f2358633ae6e1d492 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 19 Oct 2020 23:55:54 +0800 Subject: [PATCH 292/957] This closes #714 and closes #715, fix wrong worksheet index returned by NewSheet in some case, fix panic on formatted value with no built-in number format ID --- cell.go | 3 +++ cell_test.go | 10 ++++++++++ sheet.go | 5 +++-- sheet_test.go | 2 ++ 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/cell.go b/cell.go index 11c6836ee6..bdda48c70e 100644 --- a/cell.go +++ b/cell.go @@ -770,6 +770,9 @@ func (f *File) formattedValue(s int, v string) string { if ok != nil { return ok(v, builtInNumFmt[numFmtId]) } + if styleSheet == nil || styleSheet.NumFmts == nil { + return v + } for _, xlsxFmt := range styleSheet.NumFmts.NumFmt { if xlsxFmt.NumFmtID == numFmtId { format := strings.ToLower(xlsxFmt.FormatCode) diff --git a/cell_test.go b/cell_test.go index a855344b76..f7072560a2 100644 --- a/cell_test.go +++ b/cell_test.go @@ -299,4 +299,14 @@ func TestFormattedValue(t *testing.T) { assert.NoError(t, err) v = f.formattedValue(1, "43528") assert.Equal(t, "03/04/2019", v) + + // formatted value with no built-in number format ID + assert.NoError(t, err) + f.Styles.NumFmts = nil + numFmtID := 5 + f.Styles.CellXfs.Xf = append(f.Styles.CellXfs.Xf, xlsxXf{ + NumFmtID: &numFmtID, + }) + v = f.formattedValue(1, "43528") + assert.Equal(t, "43528", v) } diff --git a/sheet.go b/sheet.go index aaa72cc35b..44067fee3f 100644 --- a/sheet.go +++ b/sheet.go @@ -37,8 +37,9 @@ import ( // appending the new sheet. func (f *File) NewSheet(name string) int { // Check if the worksheet already exists - if f.GetSheetIndex(name) != -1 { - return f.SheetCount + index := f.GetSheetIndex(name) + if index != -1 { + return index } f.DeleteSheet(name) f.SheetCount++ diff --git a/sheet_test.go b/sheet_test.go index 56f5f46854..1a59b65328 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -71,6 +71,8 @@ func TestNewSheet(t *testing.T) { // delete original sheet f.DeleteSheet(f.GetSheetName(f.GetSheetIndex("Sheet1"))) 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")) } func TestSetPane(t *testing.T) { From b812e9a1a8ebe9ab22c51553eb46b1f1cff1f7c1 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 22 Oct 2020 08:29:25 +0800 Subject: [PATCH 293/957] New formula function AND (#701) and update doc for the NewSheet (#714) --- calc.go | 52 +++++++++++++++++++++++++++++++++++++++++++++++----- calc_test.go | 20 ++++++++++++++++---- sheet.go | 8 ++++---- 3 files changed, 67 insertions(+), 13 deletions(-) diff --git a/calc.go b/calc.go index 111bc60c5f..bc3b6e973d 100644 --- a/calc.go +++ b/calc.go @@ -116,11 +116,11 @@ var tokenPriority = map[string]int{ // // ABS, ACOS, ACOSH, ACOT, ACOTH, AND, ARABIC, ASIN, ASINH, ATAN2, ATANH, // BASE, CEILING, CEILING.MATH, CEILING.PRECISE, COMBIN, COMBINA, COS, -// COSH, COT, COTH, COUNTA, CSC, CSCH, DECIMAL, DEGREES, EVEN, EXP, FACT, -// FACTDOUBLE, FLOOR, FLOOR.MATH, FLOOR.PRECISE, GCD, INT, ISBLANK, ISERR, -// ISERROR, ISEVEN, ISNA, ISNONTEXT, ISNUMBER, ISO.CEILING, ISODD, LCM, -// LN, LOG, LOG10, MDETERM, MEDIAN, MOD, MROUND, MULTINOMIAL, MUNIT, NA, -// ODD, OR, PI, POWER, PRODUCT, QUOTIENT, RADIANS, RAND, RANDBETWEEN, +// COSH, COT, COTH, COUNTA, CSC, CSCH, DATE, DECIMAL, DEGREES, EVEN, EXP, +// FACT, FACTDOUBLE, FLOOR, FLOOR.MATH, FLOOR.PRECISE, GCD, INT, ISBLANK, +// ISERR, ISERROR, ISEVEN, ISNA, ISNONTEXT, ISNUMBER, ISO.CEILING, ISODD, +// LCM, LN, LOG, LOG10, MDETERM, MEDIAN, MOD, MROUND, MULTINOMIAL, MUNIT, +// NA, ODD, OR, PI, POWER, PRODUCT, QUOTIENT, RADIANS, RAND, RANDBETWEEN, // ROUND, ROUNDDOWN, ROUNDUP, SEC, SECH, SIGN, SIN, SINH, SQRT, SQRTPI, // SUM, SUMIF, SUMSQ, TAN, TANH, TRUNC // @@ -3308,3 +3308,45 @@ func (fn *formulaFuncs) OR(argsList *list.List) (result string, err error) { result = strings.ToUpper(strconv.FormatBool(or)) return } + +// Date and Time Functions + +// DATE returns a date, from a user-supplied year, month and day. +func (fn *formulaFuncs) DATE(argsList *list.List) (result string, err error) { + if argsList.Len() != 3 { + err = errors.New("DATE requires 3 number arguments") + return + } + var year, month, day int + if year, err = strconv.Atoi(argsList.Front().Value.(formulaArg).String); err != nil { + err = errors.New("DATE requires 3 number arguments") + return + } + if month, err = strconv.Atoi(argsList.Front().Next().Value.(formulaArg).String); err != nil { + err = errors.New("DATE requires 3 number arguments") + return + } + if day, err = strconv.Atoi(argsList.Back().Value.(formulaArg).String); err != nil { + err = errors.New("DATE requires 3 number arguments") + return + } + d := makeDate(year, time.Month(month), day) + result = timeFromExcelTime(daysBetween(excelMinTime1900.Unix(), d)+1, false).String() + return +} + +// makeDate return date as a Unix time, the number of seconds elapsed since +// January 1, 1970 UTC. +func makeDate(y int, m time.Month, d int) int64 { + if y == 1900 && int(m) <= 2 { + d-- + } + date := time.Date(y, m, d, 0, 0, 0, 0, time.UTC) + return date.Unix() +} + +// daysBetween return time interval of the given start timestamp and end +// timestamp. +func daysBetween(startDate, endDate int64) float64 { + return float64(int(0.5 + float64((endDate-startDate)/86400))) +} diff --git a/calc_test.go b/calc_test.go index 02b51610a1..c6a7dbc700 100644 --- a/calc_test.go +++ b/calc_test.go @@ -407,14 +407,14 @@ func TestCalcCellValue(t *testing.T) { "=TRUNC(99.999,-1)": "90", "=TRUNC(-99.999,2)": "-99.99", "=TRUNC(-99.999,-1)": "-90", - // Statistical functions + // Statistical Functions // COUNTA `=COUNTA()`: "0", `=COUNTA(A1:A5,B2:B5,"text",1,2)`: "8", // MEDIAN "=MEDIAN(A1:A5,12)": "2", "=MEDIAN(A1:A5)": "1.5", - // Information functions + // Information Functions // ISBLANK "=ISBLANK(A1)": "FALSE", "=ISBLANK(A5)": "TRUE", @@ -443,6 +443,7 @@ func TestCalcCellValue(t *testing.T) { "=ISODD(A2)": "FALSE", // NA "=NA()": "#N/A", + // Logical Functions // AND "=AND(0)": "FALSE", "=AND(1)": "TRUE", @@ -457,6 +458,10 @@ func TestCalcCellValue(t *testing.T) { "=OR(0)": "FALSE", "=OR(1=2,2=2)": "TRUE", "=OR(1=2,2=3)": "FALSE", + // Date and Time Functions + // DATE + "=DATE(2020,10,21)": "2020-10-21 00:00:00 +0000 UTC", + "=DATE(1900,1,1)": "1899-12-31 00:00:00 +0000 UTC", } for formula, expected := range mathCalc { f := prepareData() @@ -732,10 +737,10 @@ func TestCalcCellValue(t *testing.T) { "=TRUNC()": "TRUNC requires at least 1 argument", `=TRUNC("X")`: "#VALUE!", `=TRUNC(1,"X")`: "#VALUE!", - // Statistical functions + // Statistical Functions // MEDIAN "=MEDIAN()": "MEDIAN requires at least 1 argument", - // Information functions + // Information Functions // ISBLANK "=ISBLANK(A1,A2)": "ISBLANK requires 1 argument", // ISERR @@ -756,6 +761,7 @@ func TestCalcCellValue(t *testing.T) { `=ISODD("text")`: "#VALUE!", // NA "=NA(1)": "NA accepts no arguments", + // Logical Functions // AND `=AND("text")`: "#VALUE!", `=AND(A1:B1)`: "#VALUE!", @@ -766,6 +772,12 @@ func TestCalcCellValue(t *testing.T) { `=OR(A1:B1)`: "#VALUE!", "=OR()": "OR requires at least 1 argument", "=OR(1" + strings.Repeat(",1", 30) + ")": "OR accepts at most 30 arguments", + // 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", } for formula, expected := range mathCalcError { f := prepareData() diff --git a/sheet.go b/sheet.go index 44067fee3f..8da2e89dae 100644 --- a/sheet.go +++ b/sheet.go @@ -31,10 +31,10 @@ import ( "github.com/mohae/deepcopy" ) -// NewSheet provides function to create a new sheet by given worksheet name. -// When creating a new spreadsheet file, the default worksheet will be -// created. Returns the number of sheets in the workbook (file) after -// appending the new sheet. +// 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 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 index := f.GetSheetIndex(name) From 9d470bb38f992d9f0da2168b7a576f9e212b7a88 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 23 Oct 2020 00:01:52 +0800 Subject: [PATCH 294/957] Update conversion between integer types and unit tests --- excelize_test.go | 41 ++++++++++++++++++++++++----------------- styles.go | 7 ++++--- styles_test.go | 13 +++++++++++++ 3 files changed, 41 insertions(+), 20 deletions(-) diff --git a/excelize_test.go b/excelize_test.go index 9c1b1e6299..b0483c7521 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -1110,26 +1110,33 @@ func TestSetSheetRow(t *testing.T) { assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetSheetRow.xlsx"))) } -func TestThemeColor(t *testing.T) { - t.Log(ThemeColor("000000", -0.1)) - t.Log(ThemeColor("000000", 0)) - t.Log(ThemeColor("000000", 1)) -} - func TestHSL(t *testing.T) { var hsl HSL - t.Log(hsl.RGBA()) - t.Log(hslModel(hsl)) - t.Log(hslModel(color.Gray16{Y: uint16(1)})) - t.Log(HSLToRGB(0, 1, 0.4)) - t.Log(HSLToRGB(0, 1, 0.6)) - t.Log(hueToRGB(0, 0, -1)) - t.Log(hueToRGB(0, 0, 2)) - t.Log(hueToRGB(0, 0, 1.0/7)) - t.Log(hueToRGB(0, 0, 0.4)) - t.Log(hueToRGB(0, 0, 2.0/4)) + r, g, b, a := hsl.RGBA() + assert.Equal(t, uint32(0), r) + assert.Equal(t, uint32(0), g) + assert.Equal(t, uint32(0), b) + assert.Equal(t, uint32(0xffff), a) + assert.Equal(t, HSL{0, 0, 0}, hslModel(hsl)) + assert.Equal(t, HSL{0, 0, 0}, hslModel(color.Gray16{Y: uint16(1)})) + R, G, B := HSLToRGB(0, 1, 0.4) + assert.Equal(t, uint8(204), R) + assert.Equal(t, uint8(0), G) + assert.Equal(t, uint8(0), B) + R, G, B = HSLToRGB(0, 1, 0.6) + assert.Equal(t, uint8(255), R) + assert.Equal(t, uint8(51), G) + assert.Equal(t, uint8(51), B) + assert.Equal(t, 0.0, hueToRGB(0, 0, -1)) + assert.Equal(t, 0.0, hueToRGB(0, 0, 2)) + 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)) - t.Log(RGBToHSL(0, 255, 255)) + 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) t.Log(RGBToHSL(250, 100, 50)) t.Log(RGBToHSL(50, 100, 250)) t.Log(RGBToHSL(250, 50, 100)) diff --git a/styles.go b/styles.go index 896eaa1bf6..decc89bd59 100644 --- a/styles.go +++ b/styles.go @@ -3110,12 +3110,10 @@ func (f *File) themeReader() *xlsxTheme { err error theme xlsxTheme ) - if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML("xl/theme/theme1.xml")))). Decode(&theme); err != nil && err != io.EOF { log.Printf("xml decoder error: %s", err) } - return &theme } @@ -3127,7 +3125,10 @@ func ThemeColor(baseColor string, tint float64) string { r, _ := strconv.ParseInt(baseColor[0:2], 16, 64) g, _ := strconv.ParseInt(baseColor[2:4], 16, 64) b, _ := strconv.ParseInt(baseColor[4:6], 16, 64) - h, s, l := RGBToHSL(uint8(r), uint8(g), uint8(b)) + var h, s, l float64 + if r >= 0 && r <= math.MaxUint8 && g >= 0 && g <= math.MaxUint8 && b >= 0 && b <= math.MaxUint8 { + h, s, l = RGBToHSL(uint8(r), uint8(g), uint8(b)) + } if tint < 0 { l *= (1 + tint) } else { diff --git a/styles_test.go b/styles_test.go index 8ce26a4e7b..e93aa708a1 100644 --- a/styles_test.go +++ b/styles_test.go @@ -2,6 +2,7 @@ package excelize import ( "fmt" + "math" "path/filepath" "strings" "testing" @@ -294,5 +295,17 @@ func TestParseTime(t *testing.T) { assert.Equal(t, "3/4/2019 5:5:42", parseTime("43528.2123", "M/D/YYYY h:m:s")) assert.Equal(t, "March", parseTime("43528", "mmmm")) assert.Equal(t, "Monday", parseTime("43528", "dddd")) +} +func TestThemeColor(t *testing.T) { + for _, clr := range [][]string{ + {"FF000000", ThemeColor("000000", -0.1)}, + {"FF000000", ThemeColor("000000", 0)}, + {"FF33FF33", ThemeColor("00FF00", 0.2)}, + {"FFFFFFFF", ThemeColor("000000", 1)}, + {"FFFFFFFF", ThemeColor(strings.Repeat(string(rune(math.MaxUint8+1)), 6), 1)}, + {"FFFFFFFF", ThemeColor(strings.Repeat(string(rune(-1)), 6), 1)}, + } { + assert.Equal(t, clr[0], clr[1]) + } } From fcca8a38389c7a7f99639dc142b9b10c827ac7ce Mon Sep 17 00:00:00 2001 From: Ted <37789839+Theodoree@users.noreply.github.com> Date: Tue, 3 Nov 2020 17:48:37 +0800 Subject: [PATCH 295/957] optimize memory allocation (#722) * optimize marshal * optimize mem alloc * add benchmark testing * add NewSheetWithRowNum testing * sync struct fields order * add BenchmarkNewSheetWithStreamWriter * delete NewSheetWithRowNum and benchmark test --- lib.go | 3 ++- sheet.go | 12 ++++++++++-- sheet_test.go | 34 ++++++++++++++++++++++++++++++++++ xmlWorksheet.go | 21 +++++++++++---------- 4 files changed, 57 insertions(+), 13 deletions(-) diff --git a/lib.go b/lib.go index 88aa3a117a..7dcc09ee02 100644 --- a/lib.go +++ b/lib.go @@ -206,8 +206,9 @@ func CoordinatesToCellName(col, row int) (string, error) { if col < 1 || row < 1 { return "", fmt.Errorf("invalid cell coordinates [%d, %d]", col, row) } + //Using itoa will save more memory colname, err := ColumnNumberToName(col) - return fmt.Sprintf("%s%d", colname, row), err + return colname + strconv.Itoa(row), err } // boolPtr returns a pointer to a bool with the given value. diff --git a/sheet.go b/sheet.go index 8da2e89dae..caf87d95f4 100644 --- a/sheet.go +++ b/sheet.go @@ -120,18 +120,26 @@ func (f *File) workBookWriter() { // workSheetWriter provides a function to save xl/worksheets/sheet%d.xml after // serialize structure. func (f *File) workSheetWriter() { + + // optimize memory alloc + var arr []byte + buffer := bytes.NewBuffer(arr) + encoder := xml.NewEncoder(buffer) + for p, sheet := range f.Sheet { if sheet != nil { for k, v := range sheet.SheetData.Row { f.Sheet[p].SheetData.Row[k].C = trimCell(v.C) } - output, _ := xml.Marshal(sheet) - f.saveFileList(p, replaceRelationshipsBytes(f.replaceNameSpaceBytes(p, output))) + // reusing buffer + encoder.Encode(sheet) + f.saveFileList(p, replaceRelationshipsBytes(f.replaceNameSpaceBytes(p, buffer.Bytes()))) ok := f.checked[p] if ok { delete(f.Sheet, p) f.checked[p] = false } + buffer.Reset() } } } diff --git a/sheet_test.go b/sheet_test.go index 1a59b65328..4626003d53 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -3,6 +3,7 @@ package excelize import ( "fmt" "path/filepath" + "strconv" "strings" "testing" @@ -344,3 +345,36 @@ func TestSetSheetName(t *testing.T) { f.SetSheetName("Sheet1", "Sheet1") assert.Equal(t, "Sheet1", f.GetSheetName(0)) } + +func BenchmarkNewSheet(b *testing.B) { + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + newSheetWithSet() + } + }) +} +func newSheetWithSet() { + file := NewFile() + file.NewSheet("sheet1") + for i := 0; i < 1000; i++ { + file.SetCellInt("sheet1", "A"+strconv.Itoa(i+1), i) + } + file = nil +} + +func BenchmarkFile_SaveAs(b *testing.B) { + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + newSheetWithSave() + } + + }) +} +func newSheetWithSave() { + file := NewFile() + file.NewSheet("sheet1") + for i := 0; i < 1000; i++ { + file.SetCellInt("sheet1", "A"+strconv.Itoa(i+1), i) + } + file.Save() +} diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 0eaa8ee16b..8880909cec 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -313,20 +313,20 @@ type xlsxSheetData struct { // xlsxRow directly maps the row element. The element expresses information // about an entire row of a worksheet, and contains all cell definitions for a // particular row in the worksheet. -type xlsxRow struct { - Collapsed bool `xml:"collapsed,attr,omitempty"` +type xlsxRow struct { // alignment word + 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"` - CustomHeight bool `xml:"customHeight,attr,omitempty"` - Hidden bool `xml:"hidden,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"` - Ph bool `xml:"ph,attr,omitempty"` - R int `xml:"r,attr,omitempty"` - S int `xml:"s,attr,omitempty"` - Spans string `xml:"spans,attr,omitempty"` - ThickBot bool `xml:"thickBot,attr,omitempty"` + Collapsed bool `xml:"collapsed,attr,omitempty"` ThickTop bool `xml:"thickTop,attr,omitempty"` - C []xlsxC `xml:"c"` + ThickBot bool `xml:"thickBot,attr,omitempty"` + Ph bool `xml:"ph,attr,omitempty"` } // xlsxSortState directly maps the sortState element. This collection @@ -456,6 +456,7 @@ type DataValidation struct { // s (Shared String) | Cell containing a shared string. // str (String) | Cell containing a formula string. // +// fixme: how to make this structure smaller; cur size is 152 bytes. it's be too bigger. type xlsxC struct { XMLName xml.Name `xml:"c"` XMLSpace xml.Attr `xml:"space,attr,omitempty"` From c82a185af83b8b3934efcb0b227e494a18f426ea Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 4 Nov 2020 00:28:20 +0800 Subject: [PATCH 296/957] Compatibility improvement: parse document core part (workbook) dynamically --- chart.go | 9 ++++--- excelize.go | 7 ++++-- excelize_test.go | 2 +- lib.go | 12 +++++----- pivotTable.go | 7 ++++-- rows.go | 13 ++++++---- sheet.go | 62 ++++++++++++++++++++++++++++++++---------------- xmlDrawing.go | 2 ++ xmlWorksheet.go | 3 +-- 9 files changed, 75 insertions(+), 42 deletions(-) diff --git a/chart.go b/chart.go index ae31f714ee..978a2f98d6 100644 --- a/chart.go +++ b/chart.go @@ -16,6 +16,7 @@ import ( "encoding/xml" "errors" "fmt" + "path/filepath" "strconv" "strings" ) @@ -800,9 +801,11 @@ func (f *File) AddChartSheet(sheet, format string, combo ...string) error { f.addContentTypePart(chartID, "chart") f.addContentTypePart(sheetID, "chartsheet") f.addContentTypePart(drawingID, "drawings") - // Update xl/_rels/workbook.xml.rels - rID := f.addRels("xl/_rels/workbook.xml.rels", SourceRelationshipChartsheet, fmt.Sprintf("chartsheets/sheet%d.xml", sheetID), "") - // Update xl/workbook.xml + // Update workbook.xml.rels + wbPath := f.getWorkbookPath() + wbRelsPath := strings.TrimPrefix(filepath.Join(filepath.Dir(wbPath), "_rels", filepath.Base(wbPath)+".rels"), string(filepath.Separator)) + rID := f.addRels(wbRelsPath, SourceRelationshipChartsheet, fmt.Sprintf("/xl/chartsheets/sheet%d.xml", sheetID), "") + // Update workbook.xml f.setWorkbook(sheet, sheetID, rID) chartsheet, _ := xml.Marshal(cs) f.addSheetNameSpace(sheet, NameSpaceSpreadSheet) diff --git a/excelize.go b/excelize.go index 0c0f74a81e..3a511d13a4 100644 --- a/excelize.go +++ b/excelize.go @@ -22,6 +22,7 @@ import ( "io/ioutil" "os" "path" + "path/filepath" "strconv" "strings" "sync" @@ -112,7 +113,7 @@ func OpenReader(r io.Reader, opt ...Options) (*File, error) { return nil, err } f := newFile() - if bytes.Contains(b, oleIdentifier) { + if bytes.Contains(b, oleIdentifier) && len(opt) > 0 { for _, o := range opt { f.options = &o } @@ -345,7 +346,9 @@ func (f *File) AddVBAProject(bin string) error { return errors.New("unsupported VBA project extension") } f.setContentTypePartVBAProjectExtensions() - wb := f.relsReader("xl/_rels/workbook.xml.rels") + wbPath := f.getWorkbookPath() + wbRelsPath := strings.TrimPrefix(filepath.Join(filepath.Dir(wbPath), "_rels", filepath.Base(wbPath)+".rels"), string(filepath.Separator)) + wb := f.relsReader(wbRelsPath) var rID int var ok bool for _, rel := range wb.Relationships { diff --git a/excelize_test.go b/excelize_test.go index b0483c7521..1b4887275b 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -201,7 +201,7 @@ func TestCharsetTranscoder(t *testing.T) { func TestOpenReader(t *testing.T) { _, err := OpenReader(strings.NewReader("")) assert.EqualError(t, err, "zip: not a valid zip file") - _, err = OpenReader(bytes.NewReader(oleIdentifier)) + _, err = OpenReader(bytes.NewReader(oleIdentifier), Options{Password: "password"}) assert.EqualError(t, err, "decrypted file failed") // Test open password protected spreadsheet created by Microsoft Office Excel 2010. diff --git a/lib.go b/lib.go index 7dcc09ee02..c89d69f0af 100644 --- a/lib.go +++ b/lib.go @@ -206,7 +206,6 @@ func CoordinatesToCellName(col, row int) (string, error) { if col < 1 || row < 1 { return "", fmt.Errorf("invalid cell coordinates [%d, %d]", col, row) } - //Using itoa will save more memory colname, err := ColumnNumberToName(col) return colname + strconv.Itoa(row), err } @@ -244,11 +243,12 @@ func parseFormatSet(formatSet string) []byte { // Transitional namespaces. func namespaceStrictToTransitional(content []byte) []byte { var namespaceTranslationDic = map[string]string{ - StrictSourceRelationship: SourceRelationship.Value, - StrictSourceRelationshipChart: SourceRelationshipChart, - StrictSourceRelationshipComments: SourceRelationshipComments, - StrictSourceRelationshipImage: SourceRelationshipImage, - StrictNameSpaceSpreadSheet: NameSpaceSpreadSheet.Value, + StrictSourceRelationship: SourceRelationship.Value, + StrictSourceRelationshipOfficeDocument: SourceRelationshipOfficeDocument, + StrictSourceRelationshipChart: SourceRelationshipChart, + StrictSourceRelationshipComments: SourceRelationshipComments, + StrictSourceRelationshipImage: SourceRelationshipImage, + StrictNameSpaceSpreadSheet: NameSpaceSpreadSheet.Value, } for s, n := range namespaceTranslationDic { content = bytesReplace(content, []byte(s), []byte(n), -1) diff --git a/pivotTable.go b/pivotTable.go index b7c80c2496..3a277c3e0b 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -15,6 +15,7 @@ import ( "encoding/xml" "errors" "fmt" + "path/filepath" "strconv" "strings" ) @@ -138,7 +139,9 @@ func (f *File) AddPivotTable(opt *PivotTableOption) error { } // workbook pivot cache - workBookPivotCacheRID := f.addRels("xl/_rels/workbook.xml.rels", SourceRelationshipPivotCache, fmt.Sprintf("pivotCache/pivotCacheDefinition%d.xml", pivotCacheID), "") + wbPath := f.getWorkbookPath() + wbRelsPath := strings.TrimPrefix(filepath.Join(filepath.Dir(wbPath), "_rels", filepath.Base(wbPath)+".rels"), string(filepath.Separator)) + workBookPivotCacheRID := f.addRels(wbRelsPath, SourceRelationshipPivotCache, fmt.Sprintf("/xl/pivotCache/pivotCacheDefinition%d.xml", pivotCacheID), "") cacheID := f.addWorkbookPivotCache(workBookPivotCacheRID) pivotCacheRels := "xl/pivotTables/_rels/pivotTable" + strconv.Itoa(pivotTableID) + ".xml.rels" @@ -661,7 +664,7 @@ func (f *File) getPivotTableFieldNameDefaultSubtotal(name string, fields []Pivot return false, false } -// addWorkbookPivotCache add the association ID of the pivot cache in xl/workbook.xml. +// addWorkbookPivotCache add the association ID of the pivot cache in workbook.xml. func (f *File) addWorkbookPivotCache(RID int) int { wb := f.workbookReader() if wb.PivotCaches == nil { diff --git a/rows.go b/rows.go index 50e7308570..7bbc43ddf3 100644 --- a/rows.go +++ b/rows.go @@ -19,7 +19,9 @@ import ( "io" "log" "math" + "path/filepath" "strconv" + "strings" ) // GetRows return all the rows in a sheet by given worksheet name (case @@ -288,7 +290,8 @@ func (f *File) GetRowHeight(sheet string, row int) (float64, error) { // after deserialization of xl/sharedStrings.xml. func (f *File) sharedStringsReader() *xlsxSST { var err error - + wbPath := f.getWorkbookPath() + relPath := strings.TrimPrefix(filepath.Join(filepath.Dir(wbPath), "_rels", filepath.Base(wbPath)+".rels"), string(filepath.Separator)) f.Lock() defer f.Unlock() if f.SharedStrings == nil { @@ -308,14 +311,14 @@ func (f *File) sharedStringsReader() *xlsxSST { } } f.addContentTypePart(0, "sharedStrings") - rels := f.relsReader("xl/_rels/workbook.xml.rels") + rels := f.relsReader(relPath) for _, rel := range rels.Relationships { - if rel.Target == "sharedStrings.xml" { + if rel.Target == "/xl/sharedStrings.xml" { return f.SharedStrings } } - // Update xl/_rels/workbook.xml.rels - f.addRels("xl/_rels/workbook.xml.rels", SourceRelationshipSharedStrings, "sharedStrings.xml", "") + // Update workbook.xml.rels + f.addRels(relPath, SourceRelationshipSharedStrings, "/xl/sharedStrings.xml", "") } return f.SharedStrings diff --git a/sheet.go b/sheet.go index caf87d95f4..9c9def78e3 100644 --- a/sheet.go +++ b/sheet.go @@ -22,6 +22,7 @@ import ( "log" "os" "path" + "path/filepath" "reflect" "regexp" "strconv" @@ -57,9 +58,11 @@ func (f *File) NewSheet(name string) int { f.setContentTypes("/xl/worksheets/sheet"+strconv.Itoa(sheetID)+".xml", ContentTypeSpreadSheetMLWorksheet) // Create new sheet /xl/worksheets/sheet%d.xml f.setSheet(sheetID, name) - // Update xl/_rels/workbook.xml.rels - rID := f.addRels("xl/_rels/workbook.xml.rels", SourceRelationshipWorkSheet, fmt.Sprintf("worksheets/sheet%d.xml", sheetID), "") - // Update xl/workbook.xml + // Update workbook.xml.rels + wbPath := f.getWorkbookPath() + wbRelsPath := strings.TrimPrefix(filepath.Join(filepath.Dir(wbPath), "_rels", filepath.Base(wbPath)+".rels"), string(filepath.Separator)) + rID := f.addRels(wbRelsPath, SourceRelationshipWorkSheet, fmt.Sprintf("/xl/worksheets/sheet%d.xml", sheetID), "") + // Update workbook.xml f.setWorkbook(name, sheetID, rID) return f.GetSheetIndex(name) } @@ -89,18 +92,33 @@ func (f *File) contentTypesWriter() { } } -// workbookReader provides a function to get the pointer to the xl/workbook.xml +// 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 { + for _, rel := range rels.Relationships { + if rel.Type == SourceRelationshipOfficeDocument { + path = strings.TrimPrefix(rel.Target, string(filepath.Separator)) + return + } + } + } + return +} + +// workbookReader provides a function to get the pointer to the workbook.xml // structure after deserialization. func (f *File) workbookReader() *xlsxWorkbook { var err error if f.WorkBook == nil { + wbPath := f.getWorkbookPath() f.WorkBook = new(xlsxWorkbook) - if _, ok := f.xmlAttr["xl/workbook.xml"]; !ok { - d := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML("xl/workbook.xml")))) - f.xmlAttr["xl/workbook.xml"] = append(f.xmlAttr["xl/workbook.xml"], getRootElement(d)...) - f.addNameSpaces("xl/workbook.xml", SourceRelationship) + if _, ok := f.xmlAttr[wbPath]; !ok { + d := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(wbPath)))) + f.xmlAttr[wbPath] = append(f.xmlAttr[wbPath], getRootElement(d)...) + f.addNameSpaces(wbPath, SourceRelationship) } - if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML("xl/workbook.xml")))). + 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) } @@ -108,31 +126,28 @@ func (f *File) workbookReader() *xlsxWorkbook { return f.WorkBook } -// workBookWriter provides a function to save xl/workbook.xml after serialize +// workBookWriter provides a function to save workbook.xml after serialize // structure. func (f *File) workBookWriter() { if f.WorkBook != nil { output, _ := xml.Marshal(f.WorkBook) - f.saveFileList("xl/workbook.xml", replaceRelationshipsBytes(f.replaceNameSpaceBytes("xl/workbook.xml", output))) + f.saveFileList(f.getWorkbookPath(), replaceRelationshipsBytes(f.replaceNameSpaceBytes(f.getWorkbookPath(), output))) } } // workSheetWriter provides a function to save xl/worksheets/sheet%d.xml after // serialize structure. func (f *File) workSheetWriter() { - - // optimize memory alloc var arr []byte buffer := bytes.NewBuffer(arr) encoder := xml.NewEncoder(buffer) - for p, sheet := range f.Sheet { if sheet != nil { for k, v := range sheet.SheetData.Row { f.Sheet[p].SheetData.Row[k].C = trimCell(v.C) } // reusing buffer - encoder.Encode(sheet) + _ = encoder.Encode(sheet) f.saveFileList(p, replaceRelationshipsBytes(f.replaceNameSpaceBytes(p, buffer.Bytes()))) ok := f.checked[p] if ok { @@ -419,10 +434,12 @@ func (f *File) GetSheetList() (list []string) { } // getSheetMap provides a function to get worksheet name and XML file path map -// of XLSX. +// of the spreadsheet. func (f *File) getSheetMap() map[string]string { content := f.workbookReader() - rels := f.relsReader("xl/_rels/workbook.xml.rels") + wbPath := f.getWorkbookPath() + wbRelsPath := strings.TrimPrefix(filepath.Join(filepath.Dir(wbPath), "_rels", filepath.Base(wbPath)+".rels"), string(filepath.Separator)) + rels := f.relsReader(wbRelsPath) maps := map[string]string{} for _, v := range content.Sheets.Sheet { for _, rel := range rels.Relationships { @@ -472,7 +489,9 @@ func (f *File) DeleteSheet(name string) { } sheetName := trimSheetName(name) wb := f.workbookReader() - wbRels := f.relsReader("xl/_rels/workbook.xml.rels") + wbPath := f.getWorkbookPath() + wbRelsPath := strings.TrimPrefix(filepath.Join(filepath.Dir(wbPath), "_rels", filepath.Base(wbPath)+".rels"), string(filepath.Separator)) + wbRels := f.relsReader(wbRelsPath) for idx, sheet := range wb.Sheets.Sheet { if sheet.Name == sheetName { wb.Sheets.Sheet = append(wb.Sheets.Sheet[:idx], wb.Sheets.Sheet[idx+1:]...) @@ -511,10 +530,11 @@ func (f *File) DeleteSheet(name string) { } // deleteSheetFromWorkbookRels provides a function to remove worksheet -// relationships by given relationships ID in the file -// xl/_rels/workbook.xml.rels. +// relationships by given relationships ID in the file workbook.xml.rels. func (f *File) deleteSheetFromWorkbookRels(rID string) string { - content := f.relsReader("xl/_rels/workbook.xml.rels") + wbPath := f.getWorkbookPath() + wbRelsPath := strings.TrimPrefix(filepath.Join(filepath.Dir(wbPath), "_rels", filepath.Base(wbPath)+".rels"), string(filepath.Separator)) + content := f.relsReader(wbRelsPath) for k, v := range content.Relationships { if v.ID == rID { content.Relationships = append(content.Relationships[:k], content.Relationships[k+1:]...) diff --git a/xmlDrawing.go b/xmlDrawing.go index 91b6b594ec..d2a59e189d 100644 --- a/xmlDrawing.go +++ b/xmlDrawing.go @@ -33,6 +33,7 @@ var ( // 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" @@ -50,6 +51,7 @@ const ( 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" diff --git a/xmlWorksheet.go b/xmlWorksheet.go index 8880909cec..e5ea60db4d 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -313,7 +313,7 @@ type xlsxSheetData struct { // xlsxRow directly maps the row element. The element expresses information // about an entire row of a worksheet, and contains all cell definitions for a // particular row in the worksheet. -type xlsxRow struct { // alignment word +type xlsxRow struct { // alignment word C []xlsxC `xml:"c"` R int `xml:"r,attr,omitempty"` Spans string `xml:"spans,attr,omitempty"` @@ -456,7 +456,6 @@ type DataValidation struct { // s (Shared String) | Cell containing a shared string. // str (String) | Cell containing a formula string. // -// fixme: how to make this structure smaller; cur size is 152 bytes. it's be too bigger. type xlsxC struct { XMLName xml.Name `xml:"c"` XMLSpace xml.Attr `xml:"space,attr,omitempty"` From cdc57db3b3758781bd053228bfb32879b3adf3de Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 4 Nov 2020 01:24:26 +0000 Subject: [PATCH 297/957] Fix race conditions --- rows.go | 4 ++-- xmlWorksheet.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/rows.go b/rows.go index 7bbc43ddf3..12d5ddf54d 100644 --- a/rows.go +++ b/rows.go @@ -290,10 +290,10 @@ func (f *File) GetRowHeight(sheet string, row int) (float64, error) { // after deserialization of xl/sharedStrings.xml. func (f *File) sharedStringsReader() *xlsxSST { var err error - wbPath := f.getWorkbookPath() - relPath := strings.TrimPrefix(filepath.Join(filepath.Dir(wbPath), "_rels", filepath.Base(wbPath)+".rels"), string(filepath.Separator)) f.Lock() defer f.Unlock() + wbPath := f.getWorkbookPath() + relPath := strings.TrimPrefix(filepath.Join(filepath.Dir(wbPath), "_rels", filepath.Base(wbPath)+".rels"), string(filepath.Separator)) if f.SharedStrings == nil { var sharedStrings xlsxSST ss := f.readXML("xl/sharedStrings.xml") diff --git a/xmlWorksheet.go b/xmlWorksheet.go index e5ea60db4d..26c8facedd 100644 --- a/xmlWorksheet.go +++ b/xmlWorksheet.go @@ -313,7 +313,7 @@ type xlsxSheetData struct { // xlsxRow directly maps the row element. The element expresses information // about an entire row of a worksheet, and contains all cell definitions for a // particular row in the worksheet. -type xlsxRow struct { // alignment word +type xlsxRow struct { C []xlsxC `xml:"c"` R int `xml:"r,attr,omitempty"` Spans string `xml:"spans,attr,omitempty"` From 5dd0b4aec2931079e064f1fb393b034ce4934540 Mon Sep 17 00:00:00 2001 From: xuri Date: Fri, 6 Nov 2020 20:03:13 +0800 Subject: [PATCH 298/957] using POSIX directory separator in zip path with Windows --- chart.go | 5 +---- excelize.go | 5 +---- excelize_test.go | 8 ++++++++ pivotTable.go | 5 +---- rows.go | 5 +---- sheet.go | 31 ++++++++++++++++++------------- sheet_test.go | 13 +++++++++++++ 7 files changed, 43 insertions(+), 29 deletions(-) diff --git a/chart.go b/chart.go index 978a2f98d6..c5b8fc8b9f 100644 --- a/chart.go +++ b/chart.go @@ -16,7 +16,6 @@ import ( "encoding/xml" "errors" "fmt" - "path/filepath" "strconv" "strings" ) @@ -802,9 +801,7 @@ func (f *File) AddChartSheet(sheet, format string, combo ...string) error { f.addContentTypePart(sheetID, "chartsheet") f.addContentTypePart(drawingID, "drawings") // Update workbook.xml.rels - wbPath := f.getWorkbookPath() - wbRelsPath := strings.TrimPrefix(filepath.Join(filepath.Dir(wbPath), "_rels", filepath.Base(wbPath)+".rels"), string(filepath.Separator)) - rID := f.addRels(wbRelsPath, SourceRelationshipChartsheet, fmt.Sprintf("/xl/chartsheets/sheet%d.xml", sheetID), "") + rID := f.addRels(f.getWorkbookRelsPath(), SourceRelationshipChartsheet, fmt.Sprintf("/xl/chartsheets/sheet%d.xml", sheetID), "") // Update workbook.xml f.setWorkbook(sheet, sheetID, rID) chartsheet, _ := xml.Marshal(cs) diff --git a/excelize.go b/excelize.go index 3a511d13a4..2cbf54d7d7 100644 --- a/excelize.go +++ b/excelize.go @@ -22,7 +22,6 @@ import ( "io/ioutil" "os" "path" - "path/filepath" "strconv" "strings" "sync" @@ -346,9 +345,7 @@ func (f *File) AddVBAProject(bin string) error { return errors.New("unsupported VBA project extension") } f.setContentTypePartVBAProjectExtensions() - wbPath := f.getWorkbookPath() - wbRelsPath := strings.TrimPrefix(filepath.Join(filepath.Dir(wbPath), "_rels", filepath.Base(wbPath)+".rels"), string(filepath.Separator)) - wb := f.relsReader(wbRelsPath) + wb := f.relsReader(f.getWorkbookRelsPath()) var rID int var ok bool for _, rel := range wb.Relationships { diff --git a/excelize_test.go b/excelize_test.go index 1b4887275b..d838a1fd59 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -21,6 +21,14 @@ import ( "github.com/stretchr/testify/assert" ) +func TestCurrency(t *testing.T) { + f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + assert.NoError(t, err) + // f.NewSheet("Sheet3") + go f.SetCellValue("Sheet1", "A1", "value") + go f.SetCellValue("Sheet2", "A1", "value") +} + func TestOpenFile(t *testing.T) { // Test update the spreadsheet file. f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) diff --git a/pivotTable.go b/pivotTable.go index 3a277c3e0b..96e362783e 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -15,7 +15,6 @@ import ( "encoding/xml" "errors" "fmt" - "path/filepath" "strconv" "strings" ) @@ -139,9 +138,7 @@ func (f *File) AddPivotTable(opt *PivotTableOption) error { } // workbook pivot cache - wbPath := f.getWorkbookPath() - wbRelsPath := strings.TrimPrefix(filepath.Join(filepath.Dir(wbPath), "_rels", filepath.Base(wbPath)+".rels"), string(filepath.Separator)) - workBookPivotCacheRID := f.addRels(wbRelsPath, SourceRelationshipPivotCache, fmt.Sprintf("/xl/pivotCache/pivotCacheDefinition%d.xml", pivotCacheID), "") + workBookPivotCacheRID := f.addRels(f.getWorkbookRelsPath(), SourceRelationshipPivotCache, fmt.Sprintf("/xl/pivotCache/pivotCacheDefinition%d.xml", pivotCacheID), "") cacheID := f.addWorkbookPivotCache(workBookPivotCacheRID) pivotCacheRels := "xl/pivotTables/_rels/pivotTable" + strconv.Itoa(pivotTableID) + ".xml.rels" diff --git a/rows.go b/rows.go index 12d5ddf54d..63b39472b7 100644 --- a/rows.go +++ b/rows.go @@ -19,9 +19,7 @@ import ( "io" "log" "math" - "path/filepath" "strconv" - "strings" ) // GetRows return all the rows in a sheet by given worksheet name (case @@ -292,8 +290,7 @@ func (f *File) sharedStringsReader() *xlsxSST { var err error f.Lock() defer f.Unlock() - wbPath := f.getWorkbookPath() - relPath := strings.TrimPrefix(filepath.Join(filepath.Dir(wbPath), "_rels", filepath.Base(wbPath)+".rels"), string(filepath.Separator)) + relPath := f.getWorkbookRelsPath() if f.SharedStrings == nil { var sharedStrings xlsxSST ss := f.readXML("xl/sharedStrings.xml") diff --git a/sheet.go b/sheet.go index 9c9def78e3..f493aaca99 100644 --- a/sheet.go +++ b/sheet.go @@ -59,9 +59,7 @@ func (f *File) NewSheet(name string) int { // Create new sheet /xl/worksheets/sheet%d.xml f.setSheet(sheetID, name) // Update workbook.xml.rels - wbPath := f.getWorkbookPath() - wbRelsPath := strings.TrimPrefix(filepath.Join(filepath.Dir(wbPath), "_rels", filepath.Base(wbPath)+".rels"), string(filepath.Separator)) - rID := f.addRels(wbRelsPath, SourceRelationshipWorkSheet, fmt.Sprintf("/xl/worksheets/sheet%d.xml", sheetID), "") + 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) @@ -98,7 +96,7 @@ func (f *File) getWorkbookPath() (path string) { if rels := f.relsReader("_rels/.rels"); rels != nil { for _, rel := range rels.Relationships { if rel.Type == SourceRelationshipOfficeDocument { - path = strings.TrimPrefix(rel.Target, string(filepath.Separator)) + path = strings.TrimPrefix(rel.Target, "/") return } } @@ -106,6 +104,19 @@ func (f *File) getWorkbookPath() (path string) { return } +// getWorkbookRelsPath provides a function to get the path of the workbook.xml.rels +// in the spreadsheet. +func (f *File) getWorkbookRelsPath() (path string) { + wbPath := f.getWorkbookPath() + wbDir := filepath.Dir(wbPath) + if wbDir == "." { + path = "_rels/" + filepath.Base(wbPath) + ".rels" + return + } + path = strings.TrimPrefix(filepath.Dir(wbPath)+"/_rels/"+filepath.Base(wbPath)+".rels", "/") + return +} + // workbookReader provides a function to get the pointer to the workbook.xml // structure after deserialization. func (f *File) workbookReader() *xlsxWorkbook { @@ -437,9 +448,7 @@ func (f *File) GetSheetList() (list []string) { // of the spreadsheet. func (f *File) getSheetMap() map[string]string { content := f.workbookReader() - wbPath := f.getWorkbookPath() - wbRelsPath := strings.TrimPrefix(filepath.Join(filepath.Dir(wbPath), "_rels", filepath.Base(wbPath)+".rels"), string(filepath.Separator)) - rels := f.relsReader(wbRelsPath) + rels := f.relsReader(f.getWorkbookRelsPath()) maps := map[string]string{} for _, v := range content.Sheets.Sheet { for _, rel := range rels.Relationships { @@ -489,9 +498,7 @@ func (f *File) DeleteSheet(name string) { } sheetName := trimSheetName(name) wb := f.workbookReader() - wbPath := f.getWorkbookPath() - wbRelsPath := strings.TrimPrefix(filepath.Join(filepath.Dir(wbPath), "_rels", filepath.Base(wbPath)+".rels"), string(filepath.Separator)) - wbRels := f.relsReader(wbRelsPath) + wbRels := f.relsReader(f.getWorkbookRelsPath()) for idx, sheet := range wb.Sheets.Sheet { if sheet.Name == sheetName { wb.Sheets.Sheet = append(wb.Sheets.Sheet[:idx], wb.Sheets.Sheet[idx+1:]...) @@ -532,9 +539,7 @@ func (f *File) DeleteSheet(name string) { // 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 { - wbPath := f.getWorkbookPath() - wbRelsPath := strings.TrimPrefix(filepath.Join(filepath.Dir(wbPath), "_rels", filepath.Base(wbPath)+".rels"), string(filepath.Separator)) - content := f.relsReader(wbRelsPath) + content := f.relsReader(f.getWorkbookRelsPath()) for k, v := range content.Relationships { if v.ID == rID { content.Relationships = append(content.Relationships[:k], content.Relationships[k+1:]...) diff --git a/sheet_test.go b/sheet_test.go index 4626003d53..bfe0ce3fcc 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -346,6 +346,19 @@ func TestSetSheetName(t *testing.T) { assert.Equal(t, "Sheet1", f.GetSheetName(0)) } +func TestGetWorkbookPath(t *testing.T) { + f := NewFile() + delete(f.XLSX, "_rels/.rels") + assert.Equal(t, "", f.getWorkbookPath()) +} + +func TestGetWorkbookRelsPath(t *testing.T) { + f := NewFile() + delete(f.XLSX, "xl/_rels/.rels") + f.XLSX["_rels/.rels"] = []byte(``) + assert.Equal(t, "_rels/workbook.xml.rels", f.getWorkbookRelsPath()) +} + func BenchmarkNewSheet(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { From 2514bb16c682679485dfb5298e1a5797b97bdcd7 Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 10 Nov 2020 23:48:09 +0800 Subject: [PATCH 299/957] Fix #724, standardize variable naming and update unit tests --- .travis.yml | 1 + README.md | 6 +- adjust.go | 95 ++++++++++++------------- calc_test.go | 13 ++++ cell.go | 112 ++++++++++++++--------------- chart.go | 8 +-- chart_test.go | 36 +++++----- col.go | 62 ++++++++--------- comment.go | 6 +- date_test.go | 2 +- drawing.go | 10 +-- excelize.go | 22 +++--- excelize_test.go | 8 --- file.go | 6 +- file_test.go | 2 +- merge.go | 38 +++++----- picture.go | 10 +-- rows.go | 123 ++++++++++++++++---------------- rows_test.go | 174 +++++++++++++++++++++++----------------------- shape.go | 6 +- sheet.go | 76 ++++++++++---------- sheetview.go | 12 ++-- sparkline_test.go | 2 +- stream_test.go | 2 +- styles.go | 22 +++--- styles_test.go | 10 +-- table.go | 12 ++-- 27 files changed, 439 insertions(+), 437 deletions(-) diff --git a/.travis.yml b/.travis.yml index a5c55f3ea5..cd22ebb80c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,6 +13,7 @@ go: os: - linux - osx + - windows env: jobs: diff --git a/README.md b/README.md index 97db54a62e..891641bc50 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ func main() { f.SetCellValue("Sheet1", "B2", 100) // Set active sheet of the workbook. f.SetActiveSheet(index) - // Save xlsx file by the given path. + // Save spreadsheet by the given path. if err := f.SaveAs("Book1.xlsx"); err != nil { fmt.Println(err) } @@ -124,7 +124,7 @@ func main() { fmt.Println(err) return } - // Save xlsx file by the given path. + // Save spreadsheet by the given path. if err := f.SaveAs("Book1.xlsx"); err != nil { fmt.Println(err) } @@ -163,7 +163,7 @@ func main() { 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 { fmt.Println(err) } - // Save the xlsx file with the origin path. + // Save the spreadsheet with the origin path. if err = f.Save(); err != nil { fmt.Println(err) } diff --git a/adjust.go b/adjust.go index 40898d9a69..f1ae5360f3 100644 --- a/adjust.go +++ b/adjust.go @@ -35,30 +35,30 @@ const ( // TODO: adjustPageBreaks, adjustComments, adjustDataValidations, adjustProtectedCells // func (f *File) adjustHelper(sheet string, dir adjustDirection, num, offset int) error { - xlsx, err := f.workSheetReader(sheet) + ws, err := f.workSheetReader(sheet) if err != nil { return err } if dir == rows { - f.adjustRowDimensions(xlsx, num, offset) + f.adjustRowDimensions(ws, num, offset) } else { - f.adjustColDimensions(xlsx, num, offset) + f.adjustColDimensions(ws, num, offset) } - f.adjustHyperlinks(xlsx, sheet, dir, num, offset) - if err = f.adjustMergeCells(xlsx, dir, num, offset); err != nil { + f.adjustHyperlinks(ws, sheet, dir, num, offset) + if err = f.adjustMergeCells(ws, dir, num, offset); err != nil { return err } - if err = f.adjustAutoFilter(xlsx, dir, num, offset); err != nil { + if err = f.adjustAutoFilter(ws, dir, num, offset); err != nil { return err } if err = f.adjustCalcChain(dir, num, offset); err != nil { return err } - checkSheet(xlsx) - _ = checkRow(xlsx) + checkSheet(ws) + _ = checkRow(ws) - if xlsx.MergeCells != nil && len(xlsx.MergeCells.Cells) == 0 { - xlsx.MergeCells = nil + if ws.MergeCells != nil && len(ws.MergeCells.Cells) == 0 { + ws.MergeCells = nil } return nil @@ -66,13 +66,13 @@ 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(xlsx *xlsxWorksheet, col, offset int) { - for rowIdx := range xlsx.SheetData.Row { - for colIdx, v := range xlsx.SheetData.Row[rowIdx].C { +func (f *File) adjustColDimensions(ws *xlsxWorksheet, col, offset int) { + 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 { - xlsx.SheetData.Row[rowIdx].C[colIdx].R, _ = CoordinatesToCellName(newCol, cellRow) + ws.SheetData.Row[rowIdx].C[colIdx].R, _ = CoordinatesToCellName(newCol, cellRow) } } } @@ -81,9 +81,9 @@ func (f *File) adjustColDimensions(xlsx *xlsxWorksheet, col, offset int) { // adjustRowDimensions provides a function to update row dimensions when // inserting or deleting rows or columns. -func (f *File) adjustRowDimensions(xlsx *xlsxWorksheet, row, offset int) { - for i := range xlsx.SheetData.Row { - r := &xlsx.SheetData.Row[i] +func (f *File) adjustRowDimensions(ws *xlsxWorksheet, row, offset int) { + for i := range ws.SheetData.Row { + r := &ws.SheetData.Row[i] if newRow := r.R + offset; r.R >= row && newRow > 0 { f.ajustSingleRowDimensions(r, newRow) } @@ -101,38 +101,35 @@ func (f *File) ajustSingleRowDimensions(r *xlsxRow, num int) { // adjustHyperlinks provides a function to update hyperlinks when inserting or // deleting rows or columns. -func (f *File) adjustHyperlinks(xlsx *xlsxWorksheet, sheet string, dir adjustDirection, num, offset int) { +func (f *File) adjustHyperlinks(ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset int) { // short path - if xlsx.Hyperlinks == nil || len(xlsx.Hyperlinks.Hyperlink) == 0 { + if ws.Hyperlinks == nil || len(ws.Hyperlinks.Hyperlink) == 0 { return } // order is important if offset < 0 { - for i := len(xlsx.Hyperlinks.Hyperlink) - 1; i >= 0; i-- { - linkData := xlsx.Hyperlinks.Hyperlink[i] + for i := len(ws.Hyperlinks.Hyperlink) - 1; i >= 0; i-- { + linkData := ws.Hyperlinks.Hyperlink[i] colNum, rowNum, _ := CellNameToCoordinates(linkData.Ref) if (dir == rows && num == rowNum) || (dir == columns && num == colNum) { f.deleteSheetRelationships(sheet, linkData.RID) - if len(xlsx.Hyperlinks.Hyperlink) > 1 { - xlsx.Hyperlinks.Hyperlink = append(xlsx.Hyperlinks.Hyperlink[:i], - xlsx.Hyperlinks.Hyperlink[i+1:]...) + if len(ws.Hyperlinks.Hyperlink) > 1 { + ws.Hyperlinks.Hyperlink = append(ws.Hyperlinks.Hyperlink[:i], + ws.Hyperlinks.Hyperlink[i+1:]...) } else { - xlsx.Hyperlinks = nil + ws.Hyperlinks = nil } } } } - - if xlsx.Hyperlinks == nil { + if ws.Hyperlinks == nil { return } - - for i := range xlsx.Hyperlinks.Hyperlink { - link := &xlsx.Hyperlinks.Hyperlink[i] // get reference + for i := range ws.Hyperlinks.Hyperlink { + link := &ws.Hyperlinks.Hyperlink[i] // get reference colNum, rowNum, _ := CellNameToCoordinates(link.Ref) - if dir == rows { if rowNum >= num { link.Ref, _ = CoordinatesToCellName(colNum, rowNum+offset) @@ -147,21 +144,21 @@ func (f *File) adjustHyperlinks(xlsx *xlsxWorksheet, sheet string, dir adjustDir // adjustAutoFilter provides a function to update the auto filter when // inserting or deleting rows or columns. -func (f *File) adjustAutoFilter(xlsx *xlsxWorksheet, dir adjustDirection, num, offset int) error { - if xlsx.AutoFilter == nil { +func (f *File) adjustAutoFilter(ws *xlsxWorksheet, dir adjustDirection, num, offset int) error { + if ws.AutoFilter == nil { return nil } - coordinates, err := f.areaRefToCoordinates(xlsx.AutoFilter.Ref) + coordinates, err := f.areaRefToCoordinates(ws.AutoFilter.Ref) if err != nil { return err } x1, y1, x2, y2 := coordinates[0], coordinates[1], coordinates[2], coordinates[3] if (dir == rows && y1 == num && offset < 0) || (dir == columns && x1 == num && x2 == num) { - xlsx.AutoFilter = nil - for rowIdx := range xlsx.SheetData.Row { - rowData := &xlsx.SheetData.Row[rowIdx] + ws.AutoFilter = nil + for rowIdx := range ws.SheetData.Row { + rowData := &ws.SheetData.Row[rowIdx] if rowData.R > y1 && rowData.R <= y2 { rowData.Hidden = false } @@ -172,7 +169,7 @@ func (f *File) adjustAutoFilter(xlsx *xlsxWorksheet, dir adjustDirection, num, o coordinates = f.adjustAutoFilterHelper(dir, coordinates, num, offset) x1, y1, x2, y2 = coordinates[0], coordinates[1], coordinates[2], coordinates[3] - if xlsx.AutoFilter.Ref, err = f.coordinatesToAreaRef([]int{x1, y1, x2, y2}); err != nil { + if ws.AutoFilter.Ref, err = f.coordinatesToAreaRef([]int{x1, y1, x2, y2}); err != nil { return err } return nil @@ -251,13 +248,13 @@ func (f *File) coordinatesToAreaRef(coordinates []int) (string, error) { // adjustMergeCells provides a function to update merged cells when inserting // or deleting rows or columns. -func (f *File) adjustMergeCells(xlsx *xlsxWorksheet, dir adjustDirection, num, offset int) error { - if xlsx.MergeCells == nil { +func (f *File) adjustMergeCells(ws *xlsxWorksheet, dir adjustDirection, num, offset int) error { + if ws.MergeCells == nil { return nil } - for i := 0; i < len(xlsx.MergeCells.Cells); i++ { - areaData := xlsx.MergeCells.Cells[i] + for i := 0; i < len(ws.MergeCells.Cells); i++ { + areaData := ws.MergeCells.Cells[i] coordinates, err := f.areaRefToCoordinates(areaData.Ref) if err != nil { return err @@ -265,21 +262,21 @@ func (f *File) adjustMergeCells(xlsx *xlsxWorksheet, dir adjustDirection, num, o x1, y1, x2, y2 := coordinates[0], coordinates[1], coordinates[2], coordinates[3] if dir == rows { if y1 == num && y2 == num && offset < 0 { - f.deleteMergeCell(xlsx, i) + f.deleteMergeCell(ws, i) i-- } y1 = f.adjustMergeCellsHelper(y1, num, offset) y2 = f.adjustMergeCellsHelper(y2, num, offset) } else { if x1 == num && x2 == num && offset < 0 { - f.deleteMergeCell(xlsx, i) + f.deleteMergeCell(ws, i) i-- } x1 = f.adjustMergeCellsHelper(x1, num, offset) x2 = f.adjustMergeCellsHelper(x2, num, offset) } if x1 == x2 && y1 == y2 { - f.deleteMergeCell(xlsx, i) + f.deleteMergeCell(ws, i) i-- } if areaData.Ref, err = f.coordinatesToAreaRef([]int{x1, y1, x2, y2}); err != nil { @@ -304,10 +301,10 @@ func (f *File) adjustMergeCellsHelper(pivot, num, offset int) int { } // deleteMergeCell provides a function to delete merged cell by given index. -func (f *File) deleteMergeCell(sheet *xlsxWorksheet, idx int) { - if len(sheet.MergeCells.Cells) > idx { - sheet.MergeCells.Cells = append(sheet.MergeCells.Cells[:idx], sheet.MergeCells.Cells[idx+1:]...) - sheet.MergeCells.Count = len(sheet.MergeCells.Cells) +func (f *File) deleteMergeCell(ws *xlsxWorksheet, idx int) { + if len(ws.MergeCells.Cells) > idx { + ws.MergeCells.Cells = append(ws.MergeCells.Cells[:idx], ws.MergeCells.Cells[idx+1:]...) + ws.MergeCells.Count = len(ws.MergeCells.Cells) } } diff --git a/calc_test.go b/calc_test.go index c6a7dbc700..9bf6e085ea 100644 --- a/calc_test.go +++ b/calc_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/xuri/efp" ) func TestCalcCellValue(t *testing.T) { @@ -854,6 +855,18 @@ func TestCalcCellValue(t *testing.T) { } +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 TestCalcCellValueWithDefinedName(t *testing.T) { cellData := [][]interface{}{ {"A1 value", "B1 value", nil}, diff --git a/cell.go b/cell.go index bdda48c70e..019f300dfb 100644 --- a/cell.go +++ b/cell.go @@ -131,15 +131,15 @@ func (f *File) setCellIntFunc(sheet, axis string, value interface{}) error { // setCellTimeFunc provides a method to process time type of value for // SetCellValue. func (f *File) setCellTimeFunc(sheet, axis string, value time.Time) error { - xlsx, err := f.workSheetReader(sheet) + ws, err := f.workSheetReader(sheet) if err != nil { return err } - cellData, col, _, err := f.prepareCell(xlsx, sheet, axis) + cellData, col, _, err := f.prepareCell(ws, sheet, axis) if err != nil { return err } - cellData.S = f.prepareCellStyle(xlsx, col, cellData.S) + cellData.S = f.prepareCellStyle(ws, col, cellData.S) var isNum bool cellData.T, cellData.V, isNum, err = setCellTime(value) @@ -178,15 +178,15 @@ 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 { - xlsx, err := f.workSheetReader(sheet) + ws, err := f.workSheetReader(sheet) if err != nil { return err } - cellData, col, _, err := f.prepareCell(xlsx, sheet, axis) + cellData, col, _, err := f.prepareCell(ws, sheet, axis) if err != nil { return err } - cellData.S = f.prepareCellStyle(xlsx, col, cellData.S) + cellData.S = f.prepareCellStyle(ws, col, cellData.S) cellData.T, cellData.V = setCellInt(value) return err } @@ -199,15 +199,15 @@ 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 { - xlsx, err := f.workSheetReader(sheet) + ws, err := f.workSheetReader(sheet) if err != nil { return err } - cellData, col, _, err := f.prepareCell(xlsx, sheet, axis) + cellData, col, _, err := f.prepareCell(ws, sheet, axis) if err != nil { return err } - cellData.S = f.prepareCellStyle(xlsx, col, cellData.S) + cellData.S = f.prepareCellStyle(ws, col, cellData.S) cellData.T, cellData.V = setCellBool(value) return err } @@ -232,15 +232,15 @@ func setCellBool(value bool) (t string, v string) { // f.SetCellFloat("Sheet1", "A1", float64(x), 2, 32) // func (f *File) SetCellFloat(sheet, axis string, value float64, prec, bitSize int) error { - xlsx, err := f.workSheetReader(sheet) + ws, err := f.workSheetReader(sheet) if err != nil { return err } - cellData, col, _, err := f.prepareCell(xlsx, sheet, axis) + cellData, col, _, err := f.prepareCell(ws, sheet, axis) if err != nil { return err } - cellData.S = f.prepareCellStyle(xlsx, col, cellData.S) + cellData.S = f.prepareCellStyle(ws, col, cellData.S) cellData.T, cellData.V = setCellFloat(value, prec, bitSize) return err } @@ -253,15 +253,15 @@ func setCellFloat(value float64, prec, 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 { - xlsx, err := f.workSheetReader(sheet) + ws, err := f.workSheetReader(sheet) if err != nil { return err } - cellData, col, _, err := f.prepareCell(xlsx, sheet, axis) + cellData, col, _, err := f.prepareCell(ws, sheet, axis) if err != nil { return err } - cellData.S = f.prepareCellStyle(xlsx, col, cellData.S) + cellData.S = f.prepareCellStyle(ws, col, cellData.S) cellData.T, cellData.V = f.setCellString(value) return err } @@ -321,15 +321,15 @@ 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 { - xlsx, err := f.workSheetReader(sheet) + ws, err := f.workSheetReader(sheet) if err != nil { return err } - cellData, col, _, err := f.prepareCell(xlsx, sheet, axis) + cellData, col, _, err := f.prepareCell(ws, sheet, axis) if err != nil { return err } - cellData.S = f.prepareCellStyle(xlsx, col, cellData.S) + cellData.S = f.prepareCellStyle(ws, col, cellData.S) cellData.T, cellData.V = setCellDefault(value) return err } @@ -362,11 +362,11 @@ type FormulaOpts struct { // SetCellFormula provides a function to set cell formula by given string and // worksheet name. func (f *File) SetCellFormula(sheet, axis, formula string, opts ...FormulaOpts) error { - xlsx, err := f.workSheetReader(sheet) + ws, err := f.workSheetReader(sheet) if err != nil { return err } - cellData, _, _, err := f.prepareCell(xlsx, sheet, axis) + cellData, _, _, err := f.prepareCell(ws, sheet, axis) if err != nil { return err } @@ -409,16 +409,16 @@ func (f *File) GetCellHyperLink(sheet, axis string) (bool, string, error) { return false, "", err } - xlsx, err := f.workSheetReader(sheet) + ws, err := f.workSheetReader(sheet) if err != nil { return false, "", err } - axis, err = f.mergeCellsParser(xlsx, axis) + axis, err = f.mergeCellsParser(ws, axis) if err != nil { return false, "", err } - if xlsx.Hyperlinks != nil { - for _, link := range xlsx.Hyperlinks.Hyperlink { + if ws.Hyperlinks != nil { + for _, link := range ws.Hyperlinks.Hyperlink { if link.Ref == axis { if link.RID != "" { return true, f.getSheetRelationshipsTargetByID(sheet, link.RID), err @@ -451,22 +451,22 @@ func (f *File) SetCellHyperLink(sheet, axis, link, linkType string) error { return err } - xlsx, err := f.workSheetReader(sheet) + ws, err := f.workSheetReader(sheet) if err != nil { return err } - axis, err = f.mergeCellsParser(xlsx, axis) + axis, err = f.mergeCellsParser(ws, axis) if err != nil { return err } var linkData xlsxHyperlink - if xlsx.Hyperlinks == nil { - xlsx.Hyperlinks = new(xlsxHyperlinks) + if ws.Hyperlinks == nil { + ws.Hyperlinks = new(xlsxHyperlinks) } - if len(xlsx.Hyperlinks.Hyperlink) > TotalSheetHyperlinks { + if len(ws.Hyperlinks.Hyperlink) > TotalSheetHyperlinks { return errors.New("over maximum limit hyperlinks in a worksheet") } @@ -489,7 +489,7 @@ func (f *File) SetCellHyperLink(sheet, axis, link, linkType string) error { return fmt.Errorf("invalid link type %q", linkType) } - xlsx.Hyperlinks.Hyperlink = append(xlsx.Hyperlinks.Hyperlink, linkData) + ws.Hyperlinks.Hyperlink = append(ws.Hyperlinks.Hyperlink, linkData) return nil } @@ -685,11 +685,11 @@ func (f *File) SetSheetRow(sheet, axis string, slice interface{}) error { } // getCellInfo does common preparation for all SetCell* methods. -func (f *File) prepareCell(xlsx *xlsxWorksheet, sheet, cell string) (*xlsxC, int, int, error) { - xlsx.Lock() - defer xlsx.Unlock() +func (f *File) prepareCell(ws *xlsxWorksheet, sheet, cell string) (*xlsxC, int, int, error) { + ws.Lock() + defer ws.Unlock() var err error - cell, err = f.mergeCellsParser(xlsx, cell) + cell, err = f.mergeCellsParser(ws, cell) if err != nil { return nil, 0, 0, err } @@ -698,19 +698,19 @@ func (f *File) prepareCell(xlsx *xlsxWorksheet, sheet, cell string) (*xlsxC, int return nil, 0, 0, err } - prepareSheetXML(xlsx, col, row) + prepareSheetXML(ws, col, row) - return &xlsx.SheetData.Row[row-1].C[col-1], col, row, err + 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. func (f *File) getCellStringFunc(sheet, axis string, fn func(x *xlsxWorksheet, c *xlsxC) (string, bool, error)) (string, error) { - xlsx, err := f.workSheetReader(sheet) + ws, err := f.workSheetReader(sheet) if err != nil { return "", err } - axis, err = f.mergeCellsParser(xlsx, axis) + axis, err = f.mergeCellsParser(ws, axis) if err != nil { return "", err } @@ -719,12 +719,12 @@ func (f *File) getCellStringFunc(sheet, axis string, fn func(x *xlsxWorksheet, c return "", err } - xlsx.Lock() - defer xlsx.Unlock() + ws.Lock() + defer ws.Unlock() lastRowNum := 0 - if l := len(xlsx.SheetData.Row); l > 0 { - lastRowNum = xlsx.SheetData.Row[l-1].R + if l := len(ws.SheetData.Row); l > 0 { + lastRowNum = ws.SheetData.Row[l-1].R } // keep in mind: row starts from 1 @@ -732,8 +732,8 @@ func (f *File) getCellStringFunc(sheet, axis string, fn func(x *xlsxWorksheet, c return "", nil } - for rowIdx := range xlsx.SheetData.Row { - rowData := &xlsx.SheetData.Row[rowIdx] + for rowIdx := range ws.SheetData.Row { + rowData := &ws.SheetData.Row[rowIdx] if rowData.R != row { continue } @@ -742,7 +742,7 @@ func (f *File) getCellStringFunc(sheet, axis string, fn func(x *xlsxWorksheet, c if axis != colData.R { continue } - val, ok, err := fn(xlsx, colData) + val, ok, err := fn(ws, colData) if err != nil { return "", err } @@ -776,7 +776,7 @@ func (f *File) formattedValue(s int, v string) string { for _, xlsxFmt := range styleSheet.NumFmts.NumFmt { if xlsxFmt.NumFmtID == numFmtId { format := strings.ToLower(xlsxFmt.FormatCode) - if strings.Contains(format, "y") || strings.Contains(format, "m") || strings.Contains(format, "d") || strings.Contains(format, "h") { + if strings.Contains(format, "y") || strings.Contains(format, "m") || strings.Contains(strings.Replace(format, "red", "", -1), "d") || strings.Contains(format, "h") { return parseTime(v, format) } @@ -788,9 +788,9 @@ func (f *File) formattedValue(s int, v string) string { // prepareCellStyle provides a function to prepare style index of cell in // worksheet by given column index and style index. -func (f *File) prepareCellStyle(xlsx *xlsxWorksheet, col, style int) int { - if xlsx.Cols != nil && style == 0 { - for _, c := range xlsx.Cols.Col { +func (f *File) prepareCellStyle(ws *xlsxWorksheet, col, style int) int { + if ws.Cols != nil && style == 0 { + for _, c := range ws.Cols.Col { if c.Min <= col && col <= c.Max { style = c.Style } @@ -801,16 +801,16 @@ func (f *File) prepareCellStyle(xlsx *xlsxWorksheet, col, style int) int { // mergeCellsParser provides a function to check merged cells in worksheet by // given axis. -func (f *File) mergeCellsParser(xlsx *xlsxWorksheet, axis string) (string, error) { +func (f *File) mergeCellsParser(ws *xlsxWorksheet, axis string) (string, error) { axis = strings.ToUpper(axis) - if xlsx.MergeCells != nil { - for i := 0; i < len(xlsx.MergeCells.Cells); i++ { - ok, err := f.checkCellInArea(axis, xlsx.MergeCells.Cells[i].Ref) + if ws.MergeCells != nil { + for i := 0; i < len(ws.MergeCells.Cells); i++ { + ok, err := f.checkCellInArea(axis, ws.MergeCells.Cells[i].Ref) if err != nil { return axis, err } if ok { - axis = strings.Split(xlsx.MergeCells.Cells[i].Ref, ":")[0] + axis = strings.Split(ws.MergeCells.Cells[i].Ref, ":")[0] } } } @@ -863,8 +863,8 @@ func isOverlap(rect1, rect2 []int) bool { // // Note that this function not validate ref tag to check the cell if or not in // allow area, and always return origin shared formula. -func getSharedForumula(xlsx *xlsxWorksheet, si string) string { - for _, r := range xlsx.SheetData.Row { +func getSharedForumula(ws *xlsxWorksheet, si 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 == si { return c.F.Content diff --git a/chart.go b/chart.go index c5b8fc8b9f..57f7838b2a 100644 --- a/chart.go +++ b/chart.go @@ -527,7 +527,7 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // fmt.Println(err) // return // } -// // Save xlsx file by the given path. +// // Save spreadsheet by the given path. // if err := f.SaveAs("Book1.xlsx"); err != nil { // fmt.Println(err) // } @@ -725,7 +725,7 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // fmt.Println(err) // return // } -// // Save xlsx file by the given path. +// // Save spreadsheet file by the given path. // if err := f.SaveAs("Book1.xlsx"); err != nil { // fmt.Println(err) // } @@ -733,7 +733,7 @@ func parseFormatChartSet(formatSet string) (*formatChart, error) { // func (f *File) AddChart(sheet, cell, format string, combo ...string) error { // Read sheet data. - xlsx, err := f.workSheetReader(sheet) + ws, err := f.workSheetReader(sheet) if err != nil { return err } @@ -745,7 +745,7 @@ func (f *File) AddChart(sheet, cell, format string, combo ...string) error { drawingID := f.countDrawings() + 1 chartID := f.countCharts() + 1 drawingXML := "xl/drawings/drawing" + strconv.Itoa(drawingID) + ".xml" - drawingID, drawingXML = f.prepareDrawing(xlsx, drawingID, sheet, drawingXML) + 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) diff --git a/chart_test.go b/chart_test.go index 6ee5f7c6b2..67d5683d1b 100644 --- a/chart_test.go +++ b/chart_test.go @@ -11,8 +11,8 @@ import ( ) func TestChartSize(t *testing.T) { - xlsx := NewFile() - sheet1 := xlsx.GetSheetName(0) + f := NewFile() + sheet1 := f.GetSheetName(0) categories := map[string]string{ "A2": "Small", @@ -23,7 +23,7 @@ func TestChartSize(t *testing.T) { "D1": "Pear", } for cell, v := range categories { - assert.NoError(t, xlsx.SetCellValue(sheet1, cell, v)) + assert.NoError(t, f.SetCellValue(sheet1, cell, v)) } values := map[string]int{ @@ -38,10 +38,10 @@ func TestChartSize(t *testing.T) { "D4": 8, } for cell, v := range values { - assert.NoError(t, xlsx.SetCellValue(sheet1, cell, v)) + assert.NoError(t, f.SetCellValue(sheet1, cell, v)) } - assert.NoError(t, xlsx.AddChart("Sheet1", "E4", `{"type":"col3DClustered","dimension":{"width":640, "height":480},`+ + 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"}],`+ @@ -49,8 +49,8 @@ func TestChartSize(t *testing.T) { var buffer bytes.Buffer - // Save xlsx file by the given path. - assert.NoError(t, xlsx.Write(&buffer)) + // Save spreadsheet by the given path. + assert.NoError(t, f.Write(&buffer)) newFile, err := OpenReader(&buffer) assert.NoError(t, err) @@ -256,8 +256,8 @@ func TestDeleteChart(t *testing.T) { func TestChartWithLogarithmicBase(t *testing.T) { // Create test XLSX file with data - xlsx := NewFile() - sheet1 := xlsx.GetSheetName(0) + f := NewFile() + sheet1 := f.GetSheetName(0) categories := map[string]float64{ "A1": 1, "A2": 2, @@ -281,46 +281,46 @@ func TestChartWithLogarithmicBase(t *testing.T) { "B10": 5000, } for cell, v := range categories { - assert.NoError(t, xlsx.SetCellValue(sheet1, cell, v)) + assert.NoError(t, f.SetCellValue(sheet1, cell, v)) } // Add two chart, one without and one with log scaling - assert.NoError(t, xlsx.AddChart(sheet1, "C1", + 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, xlsx.AddChart(sheet1, "M1", + 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, xlsx.AddChart(sheet1, "A25", + 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, xlsx.AddChart(sheet1, "F25", + 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, xlsx.AddChart(sheet1, "K25", + 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, xlsx.AddChart(sheet1, "P25", + 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"}}`)) // Export XLSX file for human confirmation - assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestChartWithLogarithmicBase10.xlsx"))) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestChartWithLogarithmicBase10.xlsx"))) // Write the XLSX file to a buffer var buffer bytes.Buffer - assert.NoError(t, xlsx.Write(&buffer)) + assert.NoError(t, f.Write(&buffer)) // Read back the XLSX file from the buffer newFile, err := OpenReader(&buffer) diff --git a/col.go b/col.go index f7a77da9a3..f3e502cf62 100644 --- a/col.go +++ b/col.go @@ -224,16 +224,16 @@ func (f *File) GetColVisible(sheet, col string) (bool, error) { return visible, err } - xlsx, err := f.workSheetReader(sheet) + ws, err := f.workSheetReader(sheet) if err != nil { return false, err } - if xlsx.Cols == nil { + if ws.Cols == nil { return visible, err } - for c := range xlsx.Cols.Col { - colData := &xlsx.Cols.Col[c] + for c := range ws.Cols.Col { + colData := &ws.Cols.Col[c] if colData.Min <= colNum && colNum <= colData.Max { visible = !colData.Hidden } @@ -271,7 +271,7 @@ func (f *File) SetColVisible(sheet, columns string, visible bool) error { if max < min { min, max = max, min } - xlsx, err := f.workSheetReader(sheet) + ws, err := f.workSheetReader(sheet) if err != nil { return err } @@ -282,13 +282,13 @@ func (f *File) SetColVisible(sheet, columns string, visible bool) error { Hidden: !visible, CustomWidth: true, } - if xlsx.Cols == nil { + if ws.Cols == nil { cols := xlsxCols{} cols.Col = append(cols.Col, colData) - xlsx.Cols = &cols + ws.Cols = &cols return nil } - xlsx.Cols.Col = flatCols(colData, xlsx.Cols.Col, func(fc, c xlsxCol) xlsxCol { + ws.Cols.Col = flatCols(colData, ws.Cols.Col, func(fc, c xlsxCol) xlsxCol { fc.BestFit = c.BestFit fc.Collapsed = c.Collapsed fc.CustomWidth = c.CustomWidth @@ -313,15 +313,15 @@ func (f *File) GetColOutlineLevel(sheet, col string) (uint8, error) { if err != nil { return level, err } - xlsx, err := f.workSheetReader(sheet) + ws, err := f.workSheetReader(sheet) if err != nil { return 0, err } - if xlsx.Cols == nil { + if ws.Cols == nil { return level, err } - for c := range xlsx.Cols.Col { - colData := &xlsx.Cols.Col[c] + for c := range ws.Cols.Col { + colData := &ws.Cols.Col[c] if colData.Min <= colNum && colNum <= colData.Max { level = colData.OutlineLevel } @@ -349,17 +349,17 @@ func (f *File) SetColOutlineLevel(sheet, col string, level uint8) error { OutlineLevel: level, CustomWidth: true, } - xlsx, err := f.workSheetReader(sheet) + ws, err := f.workSheetReader(sheet) if err != nil { return err } - if xlsx.Cols == nil { + if ws.Cols == nil { cols := xlsxCols{} cols.Col = append(cols.Col, colData) - xlsx.Cols = &cols + ws.Cols = &cols return err } - xlsx.Cols.Col = flatCols(colData, xlsx.Cols.Col, func(fc, c xlsxCol) xlsxCol { + ws.Cols.Col = flatCols(colData, ws.Cols.Col, func(fc, c xlsxCol) xlsxCol { fc.BestFit = c.BestFit fc.Collapsed = c.Collapsed fc.CustomWidth = c.CustomWidth @@ -384,7 +384,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 { - xlsx, err := f.workSheetReader(sheet) + ws, err := f.workSheetReader(sheet) if err != nil { return err } @@ -408,15 +408,15 @@ func (f *File) SetColStyle(sheet, columns string, styleID int) error { if max < min { min, max = max, min } - if xlsx.Cols == nil { - xlsx.Cols = &xlsxCols{} + if ws.Cols == nil { + ws.Cols = &xlsxCols{} } - xlsx.Cols.Col = flatCols(xlsxCol{ + ws.Cols.Col = flatCols(xlsxCol{ Min: min, Max: max, Width: 9, Style: styleID, - }, xlsx.Cols.Col, func(fc, c xlsxCol) xlsxCol { + }, ws.Cols.Col, func(fc, c xlsxCol) xlsxCol { fc.BestFit = c.BestFit fc.Collapsed = c.Collapsed fc.CustomWidth = c.CustomWidth @@ -451,7 +451,7 @@ func (f *File) SetColWidth(sheet, startcol, endcol string, width float64) error min, max = max, min } - xlsx, err := f.workSheetReader(sheet) + ws, err := f.workSheetReader(sheet) if err != nil { return err } @@ -461,13 +461,13 @@ func (f *File) SetColWidth(sheet, startcol, endcol string, width float64) error Width: width, CustomWidth: true, } - if xlsx.Cols == nil { + if ws.Cols == nil { cols := xlsxCols{} cols.Col = append(cols.Col, col) - xlsx.Cols = &cols + ws.Cols = &cols return err } - xlsx.Cols.Col = flatCols(col, xlsx.Cols.Col, func(fc, c xlsxCol) xlsxCol { + ws.Cols.Col = flatCols(col, ws.Cols.Col, func(fc, c xlsxCol) xlsxCol { fc.BestFit = c.BestFit fc.Collapsed = c.Collapsed fc.Hidden = c.Hidden @@ -623,13 +623,13 @@ func (f *File) GetColWidth(sheet, col string) (float64, error) { if err != nil { return defaultColWidthPixels, err } - xlsx, err := f.workSheetReader(sheet) + ws, err := f.workSheetReader(sheet) if err != nil { return defaultColWidthPixels, err } - if xlsx.Cols != nil { + if ws.Cols != nil { var width float64 - for _, v := range xlsx.Cols.Col { + for _, v := range ws.Cols.Col { if v.Min <= colNum && colNum <= v.Max { width = v.Width } @@ -670,12 +670,12 @@ func (f *File) RemoveCol(sheet, col string) error { return err } - xlsx, err := f.workSheetReader(sheet) + ws, err := f.workSheetReader(sheet) if err != nil { return err } - for rowIdx := range xlsx.SheetData.Row { - rowData := &xlsx.SheetData.Row[rowIdx] + for rowIdx := range ws.SheetData.Row { + rowData := &ws.SheetData.Row[rowIdx] for colIdx := range rowData.C { colName, _, _ := SplitCellName(rowData.C[colIdx].R) if colName == col { diff --git a/comment.go b/comment.go index 6010891318..1ef387790f 100644 --- a/comment.go +++ b/comment.go @@ -91,7 +91,7 @@ func (f *File) AddComment(sheet, cell, format string) error { return err } // Read sheet data. - xlsx, err := f.workSheetReader(sheet) + ws, err := f.workSheetReader(sheet) if err != nil { return err } @@ -99,9 +99,9 @@ func (f *File) AddComment(sheet, cell, format string) error { drawingVML := "xl/drawings/vmlDrawing" + strconv.Itoa(commentID) + ".vml" sheetRelationshipsComments := "../comments" + strconv.Itoa(commentID) + ".xml" sheetRelationshipsDrawingVML := "../drawings/vmlDrawing" + strconv.Itoa(commentID) + ".vml" - if xlsx.LegacyDrawing != nil { + if ws.LegacyDrawing != nil { // The worksheet already has a comments relationships, use the relationships drawing ../drawings/vmlDrawing%d.vml. - sheetRelationshipsDrawingVML = f.getSheetRelationshipsTargetByID(sheet, xlsx.LegacyDrawing.RID) + sheetRelationshipsDrawingVML = f.getSheetRelationshipsTargetByID(sheet, ws.LegacyDrawing.RID) commentID, _ = strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(sheetRelationshipsDrawingVML, "../drawings/vmlDrawing"), ".vml")) drawingVML = strings.Replace(sheetRelationshipsDrawingVML, "..", "xl", -1) } else { diff --git a/date_test.go b/date_test.go index ee01356efc..79462c5995 100644 --- a/date_test.go +++ b/date_test.go @@ -17,7 +17,7 @@ var trueExpectedDateList = []dateTest{ {0.0000000000000000, time.Date(1899, time.December, 30, 0, 0, 0, 0, time.UTC)}, {25569.000000000000, time.Unix(0, 0).UTC()}, - // Expected values extracted from real xlsx file + // Expected values extracted from real spreadsheet {1.0000000000000000, time.Date(1900, time.January, 1, 0, 0, 0, 0, time.UTC)}, {1.0000115740740740, time.Date(1900, time.January, 1, 0, 0, 1, 0, time.UTC)}, {1.0006944444444446, time.Date(1900, time.January, 1, 0, 1, 0, 0, time.UTC)}, diff --git a/drawing.go b/drawing.go index 96403b3f27..42eb420e95 100644 --- a/drawing.go +++ b/drawing.go @@ -24,11 +24,11 @@ import ( // prepareDrawing provides a function to prepare drawing ID and XML by given // drawingID, worksheet name and default drawingXML. -func (f *File) prepareDrawing(xlsx *xlsxWorksheet, drawingID int, sheet, drawingXML string) (int, string) { +func (f *File) prepareDrawing(ws *xlsxWorksheet, drawingID int, sheet, drawingXML string) (int, string) { sheetRelationshipsDrawingXML := "../drawings/drawing" + strconv.Itoa(drawingID) + ".xml" - if xlsx.Drawing != nil { + if ws.Drawing != nil { // The worksheet already has a picture or chart relationships, use the relationships drawing ../drawings/drawing%d.xml. - sheetRelationshipsDrawingXML = f.getSheetRelationshipsTargetByID(sheet, xlsx.Drawing.RID) + sheetRelationshipsDrawingXML = f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID) drawingID, _ = strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(sheetRelationshipsDrawingXML, "../drawings/drawing"), ".xml")) drawingXML = strings.Replace(sheetRelationshipsDrawingXML, "..", "xl", -1) } else { @@ -42,13 +42,13 @@ func (f *File) prepareDrawing(xlsx *xlsxWorksheet, drawingID int, sheet, drawing // prepareChartSheetDrawing provides a function to prepare drawing ID and XML // by given drawingID, worksheet name and default drawingXML. -func (f *File) prepareChartSheetDrawing(xlsx *xlsxChartsheet, drawingID int, sheet string) { +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" rID := f.addRels(sheetRels, SourceRelationshipDrawingML, sheetRelationshipsDrawingXML, "") f.addSheetNameSpace(sheet, SourceRelationship) - xlsx.Drawing = &xlsxDrawing{ + cs.Drawing = &xlsxDrawing{ RID: "rId" + strconv.Itoa(rID), } return diff --git a/excelize.go b/excelize.go index 2cbf54d7d7..5069756cf4 100644 --- a/excelize.go +++ b/excelize.go @@ -166,7 +166,7 @@ func (f *File) setDefaultTimeStyle(sheet, axis 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) (xlsx *xlsxWorksheet, err error) { +func (f *File) workSheetReader(sheet string) (ws *xlsxWorksheet, err error) { f.Lock() defer f.Unlock() var ( @@ -178,18 +178,18 @@ func (f *File) workSheetReader(sheet string) (xlsx *xlsxWorksheet, err error) { err = fmt.Errorf("sheet %s is not exist", sheet) return } - if xlsx = f.Sheet[name]; f.Sheet[name] == nil { + if ws = f.Sheet[name]; f.Sheet[name] == nil { if strings.HasPrefix(name, "xl/chartsheets") { err = fmt.Errorf("sheet %s is chart sheet", sheet) return } - xlsx = new(xlsxWorksheet) + ws = new(xlsxWorksheet) if _, ok := f.xmlAttr[name]; !ok { d := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(name)))) f.xmlAttr[name] = append(f.xmlAttr[name], getRootElement(d)...) } if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(name)))). - Decode(xlsx); err != nil && err != io.EOF { + Decode(ws); err != nil && err != io.EOF { err = fmt.Errorf("xml decode error: %s", err) return } @@ -198,13 +198,13 @@ func (f *File) workSheetReader(sheet string) (xlsx *xlsxWorksheet, err error) { f.checked = make(map[string]bool) } if ok = f.checked[name]; !ok { - checkSheet(xlsx) - if err = checkRow(xlsx); err != nil { + checkSheet(ws) + if err = checkRow(ws); err != nil { return } f.checked[name] = true } - f.Sheet[name] = xlsx + f.Sheet[name] = ws } return @@ -212,9 +212,9 @@ func (f *File) workSheetReader(sheet string) (xlsx *xlsxWorksheet, err error) { // checkSheet provides a function to fill each row element and make that is // continuous in a worksheet of XML. -func checkSheet(xlsx *xlsxWorksheet) { +func checkSheet(ws *xlsxWorksheet) { var row int - for _, r := range xlsx.SheetData.Row { + for _, r := range ws.SheetData.Row { if r.R != 0 && r.R > row { row = r.R continue @@ -223,7 +223,7 @@ func checkSheet(xlsx *xlsxWorksheet) { } sheetData := xlsxSheetData{Row: make([]xlsxRow, row)} row = 0 - for _, r := range xlsx.SheetData.Row { + for _, r := range ws.SheetData.Row { if r.R != 0 { sheetData.Row[r.R-1] = r row = r.R @@ -236,7 +236,7 @@ func checkSheet(xlsx *xlsxWorksheet) { for i := 1; i <= row; i++ { sheetData.Row[i-1].R = i } - xlsx.SheetData = sheetData + ws.SheetData = sheetData } // addRels provides a function to add relationships by given XML path, diff --git a/excelize_test.go b/excelize_test.go index d838a1fd59..1b4887275b 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -21,14 +21,6 @@ import ( "github.com/stretchr/testify/assert" ) -func TestCurrency(t *testing.T) { - f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) - assert.NoError(t, err) - // f.NewSheet("Sheet3") - go f.SetCellValue("Sheet1", "A1", "value") - go f.SetCellValue("Sheet2", "A1", "value") -} - func TestOpenFile(t *testing.T) { // Test update the spreadsheet file. f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) diff --git a/file.go b/file.go index 4a2ab10658..9adc8597da 100644 --- a/file.go +++ b/file.go @@ -23,7 +23,7 @@ import ( // NewFile provides a function to create new file by default template. For // example: // -// xlsx := NewFile() +// f := NewFile() // func NewFile() *File { file := make(map[string][]byte) @@ -54,7 +54,7 @@ func NewFile() *File { return f } -// Save provides a function to override the xlsx file with origin path. +// Save provides a function to override the spreadsheet with origin path. func (f *File) Save() error { if f.Path == "" { return fmt.Errorf("no path defined for file, consider File.WriteTo or File.Write") @@ -62,7 +62,7 @@ func (f *File) Save() error { return f.SaveAs(f.Path) } -// SaveAs provides a function to create or update to an xlsx file at the +// SaveAs provides a function to create or update to an spreadsheet at the // provided path. func (f *File) SaveAs(name string, opt ...Options) error { if len(name) > MaxFileNameLength { diff --git a/file_test.go b/file_test.go index 9fc120c4c4..0f979b8df9 100644 --- a/file_test.go +++ b/file_test.go @@ -24,7 +24,7 @@ func BenchmarkWrite(b *testing.B) { } } } - // Save xlsx file by the given path. + // Save spreadsheet by the given path. err := f.SaveAs("./test.xlsx") if err != nil { b.Error(err) diff --git a/merge.go b/merge.go index 7bb6d4236d..ec7815f31e 100644 --- a/merge.go +++ b/merge.go @@ -47,14 +47,14 @@ func (f *File) MergeCell(sheet, hcell, vcell string) error { hcell, _ = CoordinatesToCellName(rect1[0], rect1[1]) vcell, _ = CoordinatesToCellName(rect1[2], rect1[3]) - xlsx, err := f.workSheetReader(sheet) + ws, err := f.workSheetReader(sheet) if err != nil { return err } ref := hcell + ":" + vcell - if xlsx.MergeCells != nil { - for i := 0; i < len(xlsx.MergeCells.Cells); i++ { - cellData := xlsx.MergeCells.Cells[i] + if ws.MergeCells != nil { + for i := 0; i < len(ws.MergeCells.Cells); i++ { + cellData := ws.MergeCells.Cells[i] if cellData == nil { continue } @@ -70,7 +70,7 @@ func (f *File) MergeCell(sheet, hcell, vcell string) error { // Delete the merged cells of the overlapping area. if isOverlap(rect1, rect2) { - xlsx.MergeCells.Cells = append(xlsx.MergeCells.Cells[:i], xlsx.MergeCells.Cells[i+1:]...) + ws.MergeCells.Cells = append(ws.MergeCells.Cells[:i], ws.MergeCells.Cells[i+1:]...) i-- if rect1[0] > rect2[0] { @@ -93,11 +93,11 @@ func (f *File) MergeCell(sheet, hcell, vcell string) error { ref = hcell + ":" + vcell } } - xlsx.MergeCells.Cells = append(xlsx.MergeCells.Cells, &xlsxMergeCell{Ref: ref}) + ws.MergeCells.Cells = append(ws.MergeCells.Cells, &xlsxMergeCell{Ref: ref}) } else { - xlsx.MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: ref}}} + ws.MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: ref}}} } - xlsx.MergeCells.Count = len(xlsx.MergeCells.Cells) + ws.MergeCells.Count = len(ws.MergeCells.Cells) return err } @@ -108,7 +108,7 @@ func (f *File) MergeCell(sheet, hcell, vcell string) error { // // Attention: overlapped areas will also be unmerged. func (f *File) UnmergeCell(sheet string, hcell, vcell string) error { - xlsx, err := f.workSheetReader(sheet) + ws, err := f.workSheetReader(sheet) if err != nil { return err } @@ -121,12 +121,12 @@ func (f *File) UnmergeCell(sheet string, hcell, vcell string) error { _ = sortCoordinates(rect1) // return nil since no MergeCells in the sheet - if xlsx.MergeCells == nil { + if ws.MergeCells == nil { return nil } i := 0 - for _, cellData := range xlsx.MergeCells.Cells { + for _, cellData := range ws.MergeCells.Cells { if cellData == nil { continue } @@ -143,11 +143,11 @@ func (f *File) UnmergeCell(sheet string, hcell, vcell string) error { if isOverlap(rect1, rect2) { continue } - xlsx.MergeCells.Cells[i] = cellData + ws.MergeCells.Cells[i] = cellData i++ } - xlsx.MergeCells.Cells = xlsx.MergeCells.Cells[:i] - xlsx.MergeCells.Count = len(xlsx.MergeCells.Cells) + ws.MergeCells.Cells = ws.MergeCells.Cells[:i] + ws.MergeCells.Count = len(ws.MergeCells.Cells) return nil } @@ -155,15 +155,15 @@ func (f *File) UnmergeCell(sheet string, hcell, vcell string) error { // currently. func (f *File) GetMergeCells(sheet string) ([]MergeCell, error) { var mergeCells []MergeCell - xlsx, err := f.workSheetReader(sheet) + ws, err := f.workSheetReader(sheet) if err != nil { return mergeCells, err } - if xlsx.MergeCells != nil { - mergeCells = make([]MergeCell, 0, len(xlsx.MergeCells.Cells)) + if ws.MergeCells != nil { + mergeCells = make([]MergeCell, 0, len(ws.MergeCells.Cells)) - for i := range xlsx.MergeCells.Cells { - ref := xlsx.MergeCells.Cells[i].Ref + for i := range ws.MergeCells.Cells { + ref := ws.MergeCells.Cells[i].Ref axis := strings.Split(ref, ":")[0] val, _ := f.GetCellValue(sheet, axis) mergeCells = append(mergeCells, []string{ref, val}) diff --git a/picture.go b/picture.go index 2f685ffa6e..6adfa718af 100644 --- a/picture.go +++ b/picture.go @@ -145,14 +145,14 @@ func (f *File) AddPictureFromBytes(sheet, cell, format, name, extension string, return err } // Read sheet data. - xlsx, err := f.workSheetReader(sheet) + ws, err := f.workSheetReader(sheet) if err != nil { return err } // 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" - drawingID, drawingXML = f.prepareDrawing(xlsx, drawingID, sheet, drawingXML) + 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") drawingRID := f.addRels(drawingRels, SourceRelationshipImage, mediaStr, hyperlinkType) @@ -459,14 +459,14 @@ func (f *File) GetPicture(sheet, cell string) (string, []byte, error) { } col-- row-- - xlsx, err := f.workSheetReader(sheet) + ws, err := f.workSheetReader(sheet) if err != nil { return "", nil, err } - if xlsx.Drawing == nil { + if ws.Drawing == nil { return "", nil, err } - target := f.getSheetRelationshipsTargetByID(sheet, xlsx.Drawing.RID) + target := f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID) drawingXML := strings.Replace(target, "..", "xl", -1) _, ok := f.XLSX[drawingXML] if !ok { diff --git a/rows.go b/rows.go index 63b39472b7..3bbf4f21da 100644 --- a/rows.go +++ b/rows.go @@ -228,25 +228,25 @@ func (f *File) SetRowHeight(sheet string, row int, height float64) error { if height > MaxRowHeight { return errors.New("the height of the row must be smaller than or equal to 409 points") } - xlsx, err := f.workSheetReader(sheet) + ws, err := f.workSheetReader(sheet) if err != nil { return err } - prepareSheetXML(xlsx, 0, row) + prepareSheetXML(ws, 0, row) rowIdx := row - 1 - xlsx.SheetData.Row[rowIdx].Ht = height - xlsx.SheetData.Row[rowIdx].CustomHeight = true + ws.SheetData.Row[rowIdx].Ht = height + ws.SheetData.Row[rowIdx].CustomHeight = true return nil } // getRowHeight provides a function to get row height in pixels by given sheet // name and row index. func (f *File) getRowHeight(sheet string, row int) int { - xlsx, _ := f.workSheetReader(sheet) - for i := range xlsx.SheetData.Row { - v := &xlsx.SheetData.Row[i] + ws, _ := f.workSheetReader(sheet) + for i := range ws.SheetData.Row { + v := &ws.SheetData.Row[i] if v.R == row+1 && v.Ht != 0 { return int(convertRowHeightToPixels(v.Ht)) } @@ -322,28 +322,28 @@ func (f *File) sharedStringsReader() *xlsxSST { } // getValueFrom return a value from a column/row cell, this function is -// inteded to be used with for range on rows an argument with the xlsx opened -// file. -func (xlsx *xlsxC) getValueFrom(f *File, d *xlsxSST) (string, error) { +// inteded to be used with for range on rows an argument with the spreadsheet +// opened file. +func (c *xlsxC) getValueFrom(f *File, d *xlsxSST) (string, error) { f.Lock() defer f.Unlock() - switch xlsx.T { + switch c.T { case "s": - if xlsx.V != "" { + if c.V != "" { xlsxSI := 0 - xlsxSI, _ = strconv.Atoi(xlsx.V) + xlsxSI, _ = strconv.Atoi(c.V) if len(d.SI) > xlsxSI { - return f.formattedValue(xlsx.S, d.SI[xlsxSI].String()), nil + return f.formattedValue(c.S, d.SI[xlsxSI].String()), nil } } - return f.formattedValue(xlsx.S, xlsx.V), nil + return f.formattedValue(c.S, c.V), nil case "str": - return f.formattedValue(xlsx.S, xlsx.V), nil + return f.formattedValue(c.S, c.V), nil case "inlineStr": - if xlsx.IS != nil { - return f.formattedValue(xlsx.S, xlsx.IS.String()), nil + if c.IS != nil { + return f.formattedValue(c.S, c.IS.String()), nil } - return f.formattedValue(xlsx.S, xlsx.V), nil + return f.formattedValue(c.S, c.V), nil default: // correct numeric values as legacy Excel app // https://en.wikipedia.org/wiki/Numeric_precision_in_Microsoft_Excel @@ -351,20 +351,19 @@ func (xlsx *xlsxC) getValueFrom(f *File, d *xlsxSST) (string, error) { // Although this number has a decimal representation that is an infinite string of ones, // Excel displays only the leading 15 figures. In the second line, the number one is added to the fraction, and again Excel displays only 15 figures. const precision = 1000000000000000 - if len(xlsx.V) > 16 { - num, err := strconv.ParseFloat(xlsx.V, 64) + if len(c.V) > 16 { + num, err := strconv.ParseFloat(c.V, 64) if err != nil { return "", err } num = math.Round(num*precision) / precision val := fmt.Sprintf("%g", num) - if val != xlsx.V { - return f.formattedValue(xlsx.S, val), nil + if val != c.V { + return f.formattedValue(c.S, val), nil } } - - return f.formattedValue(xlsx.S, xlsx.V), nil + return f.formattedValue(c.S, c.V), nil } } @@ -378,12 +377,12 @@ func (f *File) SetRowVisible(sheet string, row int, visible bool) error { return newInvalidRowNumberError(row) } - xlsx, err := f.workSheetReader(sheet) + ws, err := f.workSheetReader(sheet) if err != nil { return err } - prepareSheetXML(xlsx, 0, row) - xlsx.SheetData.Row[row-1].Hidden = !visible + prepareSheetXML(ws, 0, row) + ws.SheetData.Row[row-1].Hidden = !visible return nil } @@ -398,14 +397,14 @@ func (f *File) GetRowVisible(sheet string, row int) (bool, error) { return false, newInvalidRowNumberError(row) } - xlsx, err := f.workSheetReader(sheet) + ws, err := f.workSheetReader(sheet) if err != nil { return false, err } - if row > len(xlsx.SheetData.Row) { + if row > len(ws.SheetData.Row) { return false, nil } - return !xlsx.SheetData.Row[row-1].Hidden, nil + return !ws.SheetData.Row[row-1].Hidden, nil } // SetRowOutlineLevel provides a function to set outline level number of a @@ -421,12 +420,12 @@ func (f *File) SetRowOutlineLevel(sheet string, row int, level uint8) error { if level > 7 || level < 1 { return errors.New("invalid outline level") } - xlsx, err := f.workSheetReader(sheet) + ws, err := f.workSheetReader(sheet) if err != nil { return err } - prepareSheetXML(xlsx, 0, row) - xlsx.SheetData.Row[row-1].OutlineLevel = level + prepareSheetXML(ws, 0, row) + ws.SheetData.Row[row-1].OutlineLevel = level return nil } @@ -440,14 +439,14 @@ func (f *File) GetRowOutlineLevel(sheet string, row int) (uint8, error) { if row < 1 { return 0, newInvalidRowNumberError(row) } - xlsx, err := f.workSheetReader(sheet) + ws, err := f.workSheetReader(sheet) if err != nil { return 0, err } - if row > len(xlsx.SheetData.Row) { + if row > len(ws.SheetData.Row) { return 0, nil } - return xlsx.SheetData.Row[row-1].OutlineLevel, nil + return ws.SheetData.Row[row-1].OutlineLevel, nil } // RemoveRow provides a function to remove single row by given worksheet name @@ -464,22 +463,22 @@ func (f *File) RemoveRow(sheet string, row int) error { return newInvalidRowNumberError(row) } - xlsx, err := f.workSheetReader(sheet) + ws, err := f.workSheetReader(sheet) if err != nil { return err } - if row > len(xlsx.SheetData.Row) { + if row > len(ws.SheetData.Row) { return f.adjustHelper(sheet, rows, row, -1) } keep := 0 - for rowIdx := 0; rowIdx < len(xlsx.SheetData.Row); rowIdx++ { - v := &xlsx.SheetData.Row[rowIdx] + for rowIdx := 0; rowIdx < len(ws.SheetData.Row); rowIdx++ { + v := &ws.SheetData.Row[rowIdx] if v.R != row { - xlsx.SheetData.Row[keep] = *v + ws.SheetData.Row[keep] = *v keep++ } } - xlsx.SheetData.Row = xlsx.SheetData.Row[:keep] + ws.SheetData.Row = ws.SheetData.Row[:keep] return f.adjustHelper(sheet, rows, row, -1) } @@ -526,20 +525,20 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) error { return newInvalidRowNumberError(row) } - xlsx, err := f.workSheetReader(sheet) + ws, err := f.workSheetReader(sheet) if err != nil { return err } - if row > len(xlsx.SheetData.Row) || row2 < 1 || row == row2 { + if row > len(ws.SheetData.Row) || row2 < 1 || row == row2 { return nil } var ok bool var rowCopy xlsxRow - for i, r := range xlsx.SheetData.Row { + for i, r := range ws.SheetData.Row { if r.R == row { - rowCopy = xlsx.SheetData.Row[i] + rowCopy = ws.SheetData.Row[i] ok = true break } @@ -553,13 +552,13 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) error { } idx2 := -1 - for i, r := range xlsx.SheetData.Row { + for i, r := range ws.SheetData.Row { if r.R == row2 { idx2 = i break } } - if idx2 == -1 && len(xlsx.SheetData.Row) >= row2 { + if idx2 == -1 && len(ws.SheetData.Row) >= row2 { return nil } @@ -567,23 +566,23 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) error { f.ajustSingleRowDimensions(&rowCopy, row2) if idx2 != -1 { - xlsx.SheetData.Row[idx2] = rowCopy + ws.SheetData.Row[idx2] = rowCopy } else { - xlsx.SheetData.Row = append(xlsx.SheetData.Row, rowCopy) + ws.SheetData.Row = append(ws.SheetData.Row, rowCopy) } - return f.duplicateMergeCells(sheet, xlsx, row, row2) + return f.duplicateMergeCells(sheet, ws, row, row2) } // duplicateMergeCells merge cells in the destination row if there are single // row merged cells in the copied row. -func (f *File) duplicateMergeCells(sheet string, xlsx *xlsxWorksheet, row, row2 int) error { - if xlsx.MergeCells == nil { +func (f *File) duplicateMergeCells(sheet string, ws *xlsxWorksheet, row, row2 int) error { + if ws.MergeCells == nil { return nil } if row > row2 { row++ } - for _, rng := range xlsx.MergeCells.Cells { + for _, rng := range ws.MergeCells.Cells { coordinates, err := f.areaRefToCoordinates(rng.Ref) if err != nil { return err @@ -592,8 +591,8 @@ func (f *File) duplicateMergeCells(sheet string, xlsx *xlsxWorksheet, row, row2 return nil } } - for i := 0; i < len(xlsx.MergeCells.Cells); i++ { - areaData := xlsx.MergeCells.Cells[i] + for i := 0; i < len(ws.MergeCells.Cells); i++ { + areaData := ws.MergeCells.Cells[i] coordinates, _ := f.areaRefToCoordinates(areaData.Ref) x1, y1, x2, y2 := coordinates[0], coordinates[1], coordinates[2], coordinates[3] if y1 == y2 && y1 == row { @@ -632,9 +631,9 @@ func (f *File) duplicateMergeCells(sheet string, xlsx *xlsxWorksheet, row, row2 // // Noteice: this method could be very slow for large spreadsheets (more than // 3000 rows one sheet). -func checkRow(xlsx *xlsxWorksheet) error { - for rowIdx := range xlsx.SheetData.Row { - rowData := &xlsx.SheetData.Row[rowIdx] +func checkRow(ws *xlsxWorksheet) error { + for rowIdx := range ws.SheetData.Row { + rowData := &ws.SheetData.Row[rowIdx] colCount := len(rowData.C) if colCount == 0 { @@ -665,7 +664,7 @@ func checkRow(xlsx *xlsxWorksheet) error { oldList := rowData.C newlist := make([]xlsxC, 0, lastCol) - rowData.C = xlsx.SheetData.Row[rowIdx].C[:0] + rowData.C = ws.SheetData.Row[rowIdx].C[:0] for colIdx := 0; colIdx < lastCol; colIdx++ { cellName, err := CoordinatesToCellName(colIdx+1, rowIdx+1) @@ -683,7 +682,7 @@ func checkRow(xlsx *xlsxWorksheet) error { if err != nil { return err } - xlsx.SheetData.Row[rowIdx].C[colNum-1] = *colData + ws.SheetData.Row[rowIdx].C[colNum-1] = *colData } } } diff --git a/rows_test.go b/rows_test.go index edbc4bd146..e49b28a36a 100644 --- a/rows_test.go +++ b/rows_test.go @@ -82,11 +82,11 @@ func TestRowsIterator(t *testing.T) { } func TestRowsError(t *testing.T) { - xlsx, err := OpenFile(filepath.Join("test", "Book1.xlsx")) + f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) if !assert.NoError(t, err) { t.FailNow() } - _, err = xlsx.Rows("SheetN") + _, err = f.Rows("SheetN") assert.EqualError(t, err, "sheet SheetN is not exist") } @@ -259,49 +259,49 @@ func TestRemoveRow(t *testing.T) { } func TestInsertRow(t *testing.T) { - xlsx := NewFile() - sheet1 := xlsx.GetSheetName(0) - r, err := xlsx.workSheetReader(sheet1) + f := NewFile() + sheet1 := f.GetSheetName(0) + r, err := f.workSheetReader(sheet1) assert.NoError(t, err) const ( colCount = 10 rowCount = 10 ) - fillCells(xlsx, sheet1, colCount, rowCount) + fillCells(f, sheet1, colCount, rowCount) - assert.NoError(t, xlsx.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External")) + assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/360EntSecGroup-Skylar/excelize", "External")) - assert.EqualError(t, xlsx.InsertRow(sheet1, -1), "invalid row number -1") + assert.EqualError(t, f.InsertRow(sheet1, -1), "invalid row number -1") - assert.EqualError(t, xlsx.InsertRow(sheet1, 0), "invalid row number 0") + assert.EqualError(t, f.InsertRow(sheet1, 0), "invalid row number 0") - assert.NoError(t, xlsx.InsertRow(sheet1, 1)) + assert.NoError(t, f.InsertRow(sheet1, 1)) if !assert.Len(t, r.SheetData.Row, rowCount+1) { t.FailNow() } - assert.NoError(t, xlsx.InsertRow(sheet1, 4)) + assert.NoError(t, f.InsertRow(sheet1, 4)) if !assert.Len(t, r.SheetData.Row, rowCount+2) { t.FailNow() } - assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestInsertRow.xlsx"))) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestInsertRow.xlsx"))) } // Testing internal sructure 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) { - xlsx := NewFile() - sheet1 := xlsx.GetSheetName(0) - r, err := xlsx.workSheetReader(sheet1) + f := NewFile() + sheet1 := f.GetSheetName(0) + r, err := f.workSheetReader(sheet1) assert.NoError(t, err) - assert.NoError(t, xlsx.InsertRow(sheet1, 1)) + assert.NoError(t, f.InsertRow(sheet1, 1)) assert.Len(t, r.SheetData.Row, 0) - assert.NoError(t, xlsx.InsertRow(sheet1, 2)) + assert.NoError(t, f.InsertRow(sheet1, 2)) assert.Len(t, r.SheetData.Row, 0) - assert.NoError(t, xlsx.InsertRow(sheet1, 99)) + assert.NoError(t, f.InsertRow(sheet1, 99)) assert.Len(t, r.SheetData.Row, 0) - assert.NoError(t, xlsx.SaveAs(filepath.Join("test", "TestInsertRowInEmptyFile.xlsx"))) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestInsertRowInEmptyFile.xlsx"))) } func TestDuplicateRowFromSingleRow(t *testing.T) { @@ -318,12 +318,12 @@ func TestDuplicateRowFromSingleRow(t *testing.T) { } t.Run("FromSingleRow", func(t *testing.T) { - xlsx := NewFile() - assert.NoError(t, xlsx.SetCellStr(sheet, "A1", cells["A1"])) - assert.NoError(t, xlsx.SetCellStr(sheet, "B1", cells["B1"])) + f := NewFile() + assert.NoError(t, f.SetCellStr(sheet, "A1", cells["A1"])) + assert.NoError(t, f.SetCellStr(sheet, "B1", cells["B1"])) - assert.NoError(t, xlsx.DuplicateRow(sheet, 1)) - if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.FromSingleRow_1"))) { + assert.NoError(t, f.DuplicateRow(sheet, 1)) + if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.FromSingleRow_1"))) { t.FailNow() } expect := map[string]string{ @@ -331,15 +331,15 @@ func TestDuplicateRowFromSingleRow(t *testing.T) { "A2": cells["A1"], "B2": cells["B1"], } for cell, val := range expect { - v, err := xlsx.GetCellValue(sheet, cell) + v, err := f.GetCellValue(sheet, cell) assert.NoError(t, err) if !assert.Equal(t, val, v, cell) { t.FailNow() } } - assert.NoError(t, xlsx.DuplicateRow(sheet, 2)) - if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.FromSingleRow_2"))) { + assert.NoError(t, f.DuplicateRow(sheet, 2)) + if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.FromSingleRow_2"))) { t.FailNow() } expect = map[string]string{ @@ -348,7 +348,7 @@ func TestDuplicateRowFromSingleRow(t *testing.T) { "A3": cells["A1"], "B3": cells["B1"], } for cell, val := range expect { - v, err := xlsx.GetCellValue(sheet, cell) + v, err := f.GetCellValue(sheet, cell) assert.NoError(t, err) if !assert.Equal(t, val, v, cell) { t.FailNow() @@ -371,16 +371,16 @@ func TestDuplicateRowUpdateDuplicatedRows(t *testing.T) { } t.Run("UpdateDuplicatedRows", func(t *testing.T) { - xlsx := NewFile() - assert.NoError(t, xlsx.SetCellStr(sheet, "A1", cells["A1"])) - assert.NoError(t, xlsx.SetCellStr(sheet, "B1", cells["B1"])) + f := NewFile() + assert.NoError(t, f.SetCellStr(sheet, "A1", cells["A1"])) + assert.NoError(t, f.SetCellStr(sheet, "B1", cells["B1"])) - assert.NoError(t, xlsx.DuplicateRow(sheet, 1)) + assert.NoError(t, f.DuplicateRow(sheet, 1)) - assert.NoError(t, xlsx.SetCellStr(sheet, "A2", cells["A2"])) - assert.NoError(t, xlsx.SetCellStr(sheet, "B2", cells["B2"])) + assert.NoError(t, f.SetCellStr(sheet, "A2", cells["A2"])) + assert.NoError(t, f.SetCellStr(sheet, "B2", cells["B2"])) - if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.UpdateDuplicatedRows"))) { + if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.UpdateDuplicatedRows"))) { t.FailNow() } expect := map[string]string{ @@ -388,7 +388,7 @@ func TestDuplicateRowUpdateDuplicatedRows(t *testing.T) { "A2": cells["A2"], "B2": cells["B2"], } for cell, val := range expect { - v, err := xlsx.GetCellValue(sheet, cell) + v, err := f.GetCellValue(sheet, cell) assert.NoError(t, err) if !assert.Equal(t, val, v, cell) { t.FailNow() @@ -419,11 +419,11 @@ func TestDuplicateRowFirstOfMultipleRows(t *testing.T) { } t.Run("FirstOfMultipleRows", func(t *testing.T) { - xlsx := newFileWithDefaults() + f := newFileWithDefaults() - assert.NoError(t, xlsx.DuplicateRow(sheet, 1)) + assert.NoError(t, f.DuplicateRow(sheet, 1)) - if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.FirstOfMultipleRows"))) { + if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.FirstOfMultipleRows"))) { t.FailNow() } expect := map[string]string{ @@ -433,7 +433,7 @@ func TestDuplicateRowFirstOfMultipleRows(t *testing.T) { "A4": cells["A3"], "B4": cells["B3"], } for cell, val := range expect { - v, err := xlsx.GetCellValue(sheet, cell) + v, err := f.GetCellValue(sheet, cell) assert.NoError(t, err) if !assert.Equal(t, val, v, cell) { t.FailNow() @@ -447,24 +447,24 @@ func TestDuplicateRowZeroWithNoRows(t *testing.T) { outFile := filepath.Join("test", "TestDuplicateRow.%s.xlsx") t.Run("ZeroWithNoRows", func(t *testing.T) { - xlsx := NewFile() + f := NewFile() - assert.EqualError(t, xlsx.DuplicateRow(sheet, 0), "invalid row number 0") + assert.EqualError(t, f.DuplicateRow(sheet, 0), "invalid row number 0") - if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.ZeroWithNoRows"))) { + if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.ZeroWithNoRows"))) { t.FailNow() } - val, err := xlsx.GetCellValue(sheet, "A1") + val, err := f.GetCellValue(sheet, "A1") assert.NoError(t, err) assert.Equal(t, "", val) - val, err = xlsx.GetCellValue(sheet, "B1") + val, err = f.GetCellValue(sheet, "B1") assert.NoError(t, err) assert.Equal(t, "", val) - val, err = xlsx.GetCellValue(sheet, "A2") + val, err = f.GetCellValue(sheet, "A2") assert.NoError(t, err) assert.Equal(t, "", val) - val, err = xlsx.GetCellValue(sheet, "B2") + val, err = f.GetCellValue(sheet, "B2") assert.NoError(t, err) assert.Equal(t, "", val) @@ -475,7 +475,7 @@ func TestDuplicateRowZeroWithNoRows(t *testing.T) { } for cell, val := range expect { - v, err := xlsx.GetCellValue(sheet, cell) + v, err := f.GetCellValue(sheet, cell) assert.NoError(t, err) if !assert.Equal(t, val, v, cell) { t.FailNow() @@ -489,11 +489,11 @@ func TestDuplicateRowMiddleRowOfEmptyFile(t *testing.T) { outFile := filepath.Join("test", "TestDuplicateRow.%s.xlsx") t.Run("MiddleRowOfEmptyFile", func(t *testing.T) { - xlsx := NewFile() + f := NewFile() - assert.NoError(t, xlsx.DuplicateRow(sheet, 99)) + assert.NoError(t, f.DuplicateRow(sheet, 99)) - if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.MiddleRowOfEmptyFile"))) { + if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.MiddleRowOfEmptyFile"))) { t.FailNow() } expect := map[string]string{ @@ -502,7 +502,7 @@ func TestDuplicateRowMiddleRowOfEmptyFile(t *testing.T) { "A100": "", } for cell, val := range expect { - v, err := xlsx.GetCellValue(sheet, cell) + v, err := f.GetCellValue(sheet, cell) assert.NoError(t, err) if !assert.Equal(t, val, v, cell) { t.FailNow() @@ -533,11 +533,11 @@ func TestDuplicateRowWithLargeOffsetToMiddleOfData(t *testing.T) { } t.Run("WithLargeOffsetToMiddleOfData", func(t *testing.T) { - xlsx := newFileWithDefaults() + f := newFileWithDefaults() - assert.NoError(t, xlsx.DuplicateRowTo(sheet, 1, 3)) + assert.NoError(t, f.DuplicateRowTo(sheet, 1, 3)) - if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.WithLargeOffsetToMiddleOfData"))) { + if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.WithLargeOffsetToMiddleOfData"))) { t.FailNow() } expect := map[string]string{ @@ -547,7 +547,7 @@ func TestDuplicateRowWithLargeOffsetToMiddleOfData(t *testing.T) { "A4": cells["A3"], "B4": cells["B3"], } for cell, val := range expect { - v, err := xlsx.GetCellValue(sheet, cell) + v, err := f.GetCellValue(sheet, cell) assert.NoError(t, err) if !assert.Equal(t, val, v, cell) { t.FailNow() @@ -578,11 +578,11 @@ func TestDuplicateRowWithLargeOffsetToEmptyRows(t *testing.T) { } t.Run("WithLargeOffsetToEmptyRows", func(t *testing.T) { - xlsx := newFileWithDefaults() + f := newFileWithDefaults() - assert.NoError(t, xlsx.DuplicateRowTo(sheet, 1, 7)) + assert.NoError(t, f.DuplicateRowTo(sheet, 1, 7)) - if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.WithLargeOffsetToEmptyRows"))) { + if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.WithLargeOffsetToEmptyRows"))) { t.FailNow() } expect := map[string]string{ @@ -592,7 +592,7 @@ func TestDuplicateRowWithLargeOffsetToEmptyRows(t *testing.T) { "A7": cells["A1"], "B7": cells["B1"], } for cell, val := range expect { - v, err := xlsx.GetCellValue(sheet, cell) + v, err := f.GetCellValue(sheet, cell) assert.NoError(t, err) if !assert.Equal(t, val, v, cell) { t.FailNow() @@ -623,11 +623,11 @@ func TestDuplicateRowInsertBefore(t *testing.T) { } t.Run("InsertBefore", func(t *testing.T) { - xlsx := newFileWithDefaults() + f := newFileWithDefaults() - assert.NoError(t, xlsx.DuplicateRowTo(sheet, 2, 1)) + assert.NoError(t, f.DuplicateRowTo(sheet, 2, 1)) - if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.InsertBefore"))) { + if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.InsertBefore"))) { t.FailNow() } @@ -638,7 +638,7 @@ func TestDuplicateRowInsertBefore(t *testing.T) { "A4": cells["A3"], "B4": cells["B3"], } for cell, val := range expect { - v, err := xlsx.GetCellValue(sheet, cell) + v, err := f.GetCellValue(sheet, cell) assert.NoError(t, err) if !assert.Equal(t, val, v, cell) { t.FailNow() @@ -669,11 +669,11 @@ func TestDuplicateRowInsertBeforeWithLargeOffset(t *testing.T) { } t.Run("InsertBeforeWithLargeOffset", func(t *testing.T) { - xlsx := newFileWithDefaults() + f := newFileWithDefaults() - assert.NoError(t, xlsx.DuplicateRowTo(sheet, 3, 1)) + assert.NoError(t, f.DuplicateRowTo(sheet, 3, 1)) - if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.InsertBeforeWithLargeOffset"))) { + if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.InsertBeforeWithLargeOffset"))) { t.FailNow() } @@ -684,7 +684,7 @@ func TestDuplicateRowInsertBeforeWithLargeOffset(t *testing.T) { "A4": cells["A3"], "B4": cells["B3"], } for cell, val := range expect { - v, err := xlsx.GetCellValue(sheet, cell) + v, err := f.GetCellValue(sheet, cell) assert.NoError(t, err) if !assert.Equal(t, val, v) { t.FailNow() @@ -717,12 +717,12 @@ func TestDuplicateRowInsertBeforeWithMergeCells(t *testing.T) { } t.Run("InsertBeforeWithLargeOffset", func(t *testing.T) { - xlsx := newFileWithDefaults() + f := newFileWithDefaults() - assert.NoError(t, xlsx.DuplicateRowTo(sheet, 2, 1)) - assert.NoError(t, xlsx.DuplicateRowTo(sheet, 1, 8)) + assert.NoError(t, f.DuplicateRowTo(sheet, 2, 1)) + assert.NoError(t, f.DuplicateRowTo(sheet, 1, 8)) - if !assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.InsertBeforeWithMergeCells"))) { + if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "TestDuplicateRow.InsertBeforeWithMergeCells"))) { t.FailNow() } @@ -732,7 +732,7 @@ func TestDuplicateRowInsertBeforeWithMergeCells(t *testing.T) { {"B1:C1", "B2 Value"}, } - mergeCells, err := xlsx.GetMergeCells(sheet) + mergeCells, err := f.GetMergeCells(sheet) assert.NoError(t, err) for idx, val := range expect { if !assert.Equal(t, val, mergeCells[idx]) { @@ -760,21 +760,21 @@ func TestDuplicateRowInvalidRownum(t *testing.T) { for _, row := range invalidIndexes { name := fmt.Sprintf("%d", row) t.Run(name, func(t *testing.T) { - xlsx := NewFile() + f := NewFile() for col, val := range cells { - assert.NoError(t, xlsx.SetCellStr(sheet, col, val)) + assert.NoError(t, f.SetCellStr(sheet, col, val)) } - assert.EqualError(t, xlsx.DuplicateRow(sheet, row), fmt.Sprintf("invalid row number %d", row)) + assert.EqualError(t, f.DuplicateRow(sheet, row), fmt.Sprintf("invalid row number %d", row)) for col, val := range cells { - v, err := xlsx.GetCellValue(sheet, col) + v, err := f.GetCellValue(sheet, col) assert.NoError(t, err) if !assert.Equal(t, val, v) { t.FailNow() } } - assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, name))) + assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, name))) }) } @@ -782,21 +782,21 @@ func TestDuplicateRowInvalidRownum(t *testing.T) { for _, row2 := range invalidIndexes { name := fmt.Sprintf("[%d,%d]", row1, row2) t.Run(name, func(t *testing.T) { - xlsx := NewFile() + f := NewFile() for col, val := range cells { - assert.NoError(t, xlsx.SetCellStr(sheet, col, val)) + assert.NoError(t, f.SetCellStr(sheet, col, val)) } - assert.EqualError(t, xlsx.DuplicateRowTo(sheet, row1, row2), fmt.Sprintf("invalid row number %d", row1)) + assert.EqualError(t, f.DuplicateRowTo(sheet, row1, row2), fmt.Sprintf("invalid row number %d", row1)) for col, val := range cells { - v, err := xlsx.GetCellValue(sheet, col) + v, err := f.GetCellValue(sheet, col) assert.NoError(t, err) if !assert.Equal(t, val, v) { t.FailNow() } } - assert.NoError(t, xlsx.SaveAs(fmt.Sprintf(outFile, name))) + assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, name))) }) } } @@ -809,12 +809,12 @@ func TestDuplicateRowTo(t *testing.T) { func TestDuplicateMergeCells(t *testing.T) { f := File{} - xlsx := &xlsxWorksheet{MergeCells: &xlsxMergeCells{ + ws := &xlsxWorksheet{MergeCells: &xlsxMergeCells{ Cells: []*xlsxMergeCell{{Ref: "A1:-"}}, }} - assert.EqualError(t, f.duplicateMergeCells("Sheet1", xlsx, 0, 0), `cannot convert cell "-" to coordinates: invalid cell name "-"`) - xlsx.MergeCells.Cells[0].Ref = "A1:B1" - assert.EqualError(t, f.duplicateMergeCells("SheetN", xlsx, 1, 2), "sheet SheetN is not exist") + 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") } func TestGetValueFromInlineStr(t *testing.T) { diff --git a/shape.go b/shape.go index 2600e901f6..2cc72abece 100644 --- a/shape.go +++ b/shape.go @@ -261,7 +261,7 @@ func (f *File) AddShape(sheet, cell, format string) error { return err } // Read sheet data. - xlsx, err := f.workSheetReader(sheet) + ws, err := f.workSheetReader(sheet) if err != nil { return err } @@ -270,9 +270,9 @@ func (f *File) AddShape(sheet, cell, format string) error { drawingXML := "xl/drawings/drawing" + strconv.Itoa(drawingID) + ".xml" sheetRelationshipsDrawingXML := "../drawings/drawing" + strconv.Itoa(drawingID) + ".xml" - if xlsx.Drawing != nil { + if ws.Drawing != nil { // The worksheet already has a shape or chart relationships, use the relationships drawing ../drawings/drawing%d.xml. - sheetRelationshipsDrawingXML = f.getSheetRelationshipsTargetByID(sheet, xlsx.Drawing.RID) + sheetRelationshipsDrawingXML = f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID) drawingID, _ = strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(sheetRelationshipsDrawingXML, "../drawings/drawing"), ".xml")) drawingXML = strings.Replace(sheetRelationshipsDrawingXML, "..", "xl", -1) } else { diff --git a/sheet.go b/sheet.go index f493aaca99..82eaae9591 100644 --- a/sheet.go +++ b/sheet.go @@ -202,7 +202,7 @@ func (f *File) setContentTypes(partName, contentType string) { // setSheet provides a function to update sheet property by given index. func (f *File) setSheet(index int, name string) { - xlsx := xlsxWorksheet{ + ws := xlsxWorksheet{ Dimension: &xlsxDimension{Ref: "A1"}, SheetViews: &xlsxSheetViews{ SheetView: []xlsxSheetView{{WorkbookViewID: 0}}, @@ -210,7 +210,7 @@ func (f *File) setSheet(index int, name string) { } path := "xl/worksheets/sheet" + strconv.Itoa(index) + ".xml" f.sheetMap[trimSheetName(name)] = path - f.Sheet[path] = &xlsx + f.Sheet[path] = &ws f.xmlAttr[path] = append(f.xmlAttr[path], NameSpaceSpreadSheet) } @@ -277,24 +277,24 @@ func (f *File) SetActiveSheet(index int) { } } for idx, name := range f.GetSheetList() { - xlsx, err := f.workSheetReader(name) + ws, err := f.workSheetReader(name) if err != nil { // Chartsheet or dialogsheet return } - if xlsx.SheetViews == nil { - xlsx.SheetViews = &xlsxSheetViews{ + if ws.SheetViews == nil { + ws.SheetViews = &xlsxSheetViews{ SheetView: []xlsxSheetView{{WorkbookViewID: 0}}, } } - if len(xlsx.SheetViews.SheetView) > 0 { - xlsx.SheetViews.SheetView[0].TabSelected = false + if len(ws.SheetViews.SheetView) > 0 { + ws.SheetViews.SheetView[0].TabSelected = false } if index == idx { - if len(xlsx.SheetViews.SheetView) > 0 { - xlsx.SheetViews.SheetView[0].TabSelected = true + if len(ws.SheetViews.SheetView) > 0 { + ws.SheetViews.SheetView[0].TabSelected = true } else { - xlsx.SheetViews.SheetView = append(xlsx.SheetViews.SheetView, xlsxSheetView{ + ws.SheetViews.SheetView = append(ws.SheetViews.SheetView, xlsxSheetView{ TabSelected: true, }) } @@ -746,7 +746,7 @@ func parseFormatPanesSet(formatSet string) (*formatPanes, error) { // func (f *File) SetPanes(sheet, panes string) error { fs, _ := parseFormatPanesSet(panes) - xlsx, err := f.workSheetReader(sheet) + ws, err := f.workSheetReader(sheet) if err != nil { return err } @@ -759,10 +759,10 @@ func (f *File) SetPanes(sheet, panes string) error { if fs.Freeze { p.State = "frozen" } - xlsx.SheetViews.SheetView[len(xlsx.SheetViews.SheetView)-1].Pane = p + ws.SheetViews.SheetView[len(ws.SheetViews.SheetView)-1].Pane = p if !(fs.Freeze) && !(fs.Split) { - if len(xlsx.SheetViews.SheetView) > 0 { - xlsx.SheetViews.SheetView[len(xlsx.SheetViews.SheetView)-1].Pane = nil + if len(ws.SheetViews.SheetView) > 0 { + ws.SheetViews.SheetView[len(ws.SheetViews.SheetView)-1].Pane = nil } } s := []*xlsxSelection{} @@ -773,7 +773,7 @@ func (f *File) SetPanes(sheet, panes string) error { SQRef: p.SQRef, }) } - xlsx.SheetViews.SheetView[len(xlsx.SheetViews.SheetView)-1].Selection = s + ws.SheetViews.SheetView[len(ws.SheetViews.SheetView)-1].Selection = s return err } @@ -1020,12 +1020,12 @@ func attrValToInt(name string, attrs []xml.Attr) (val int, err error) { // - No footer on the first page // func (f *File) SetHeaderFooter(sheet string, settings *FormatHeaderFooter) error { - xlsx, err := f.workSheetReader(sheet) + ws, err := f.workSheetReader(sheet) if err != nil { return err } if settings == nil { - xlsx.HeaderFooter = nil + ws.HeaderFooter = nil return err } @@ -1037,7 +1037,7 @@ func (f *File) SetHeaderFooter(sheet string, settings *FormatHeaderFooter) error return fmt.Errorf("field %s must be less than 255 characters", v.Type().Field(i).Name) } } - xlsx.HeaderFooter = &xlsxHeaderFooter{ + ws.HeaderFooter = &xlsxHeaderFooter{ AlignWithMargins: settings.AlignWithMargins, DifferentFirst: settings.DifferentFirst, DifferentOddEven: settings.DifferentOddEven, @@ -1062,7 +1062,7 @@ func (f *File) SetHeaderFooter(sheet string, settings *FormatHeaderFooter) error // }) // func (f *File) ProtectSheet(sheet string, settings *FormatSheetProtection) error { - xlsx, err := f.workSheetReader(sheet) + ws, err := f.workSheetReader(sheet) if err != nil { return err } @@ -1073,7 +1073,7 @@ func (f *File) ProtectSheet(sheet string, settings *FormatSheetProtection) error SelectLockedCells: true, } } - xlsx.SheetProtection = &xlsxSheetProtection{ + ws.SheetProtection = &xlsxSheetProtection{ AutoFilter: settings.AutoFilter, DeleteColumns: settings.DeleteColumns, DeleteRows: settings.DeleteRows, @@ -1092,18 +1092,18 @@ func (f *File) ProtectSheet(sheet string, settings *FormatSheetProtection) error Sort: settings.Sort, } if settings.Password != "" { - xlsx.SheetProtection.Password = genSheetPasswd(settings.Password) + ws.SheetProtection.Password = genSheetPasswd(settings.Password) } return err } // UnprotectSheet provides a function to unprotect an Excel worksheet. func (f *File) UnprotectSheet(sheet string) error { - xlsx, err := f.workSheetReader(sheet) + ws, err := f.workSheetReader(sheet) if err != nil { return err } - xlsx.SheetProtection = nil + ws.SheetProtection = nil return err } @@ -1494,19 +1494,19 @@ func (f *File) GroupSheets(sheets []string) error { return errors.New("group worksheet must contain an active worksheet") } // check worksheet exists - ws := []*xlsxWorksheet{} + wss := []*xlsxWorksheet{} for _, sheet := range sheets { - xlsx, err := f.workSheetReader(sheet) + worksheet, err := f.workSheetReader(sheet) if err != nil { return err } - ws = append(ws, xlsx) + wss = append(wss, worksheet) } - for _, s := range ws { - sheetViews := s.SheetViews.SheetView + for _, ws := range wss { + sheetViews := ws.SheetViews.SheetView if len(sheetViews) > 0 { for idx := range sheetViews { - s.SheetViews.SheetView[idx].TabSelected = true + ws.SheetViews.SheetView[idx].TabSelected = true } continue } @@ -1664,25 +1664,25 @@ func (f *File) relsReader(path string) *xlsxRelationships { // 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(xlsx *xlsxWorksheet, col int, row int) { - rowCount := len(xlsx.SheetData.Row) +func prepareSheetXML(ws *xlsxWorksheet, col int, row int) { + rowCount := len(ws.SheetData.Row) sizeHint := 0 var ht float64 var customHeight bool - if xlsx.SheetFormatPr != nil { - ht = xlsx.SheetFormatPr.DefaultRowHeight + if ws.SheetFormatPr != nil { + ht = ws.SheetFormatPr.DefaultRowHeight customHeight = true } if rowCount > 0 { - sizeHint = len(xlsx.SheetData.Row[rowCount-1].C) + sizeHint = len(ws.SheetData.Row[rowCount-1].C) } if rowCount < row { // append missing rows for rowIdx := rowCount; rowIdx < row; rowIdx++ { - xlsx.SheetData.Row = append(xlsx.SheetData.Row, xlsxRow{R: rowIdx + 1, CustomHeight: customHeight, Ht: ht, C: make([]xlsxC, 0, sizeHint)}) + ws.SheetData.Row = append(ws.SheetData.Row, xlsxRow{R: rowIdx + 1, CustomHeight: customHeight, Ht: ht, C: make([]xlsxC, 0, sizeHint)}) } } - rowData := &xlsx.SheetData.Row[row-1] + rowData := &ws.SheetData.Row[row-1] fillColumns(rowData, col, row) } @@ -1696,9 +1696,9 @@ func fillColumns(rowData *xlsxRow, col, row int) { } } -func makeContiguousColumns(xlsx *xlsxWorksheet, fromRow, toRow, colCount int) { +func makeContiguousColumns(ws *xlsxWorksheet, fromRow, toRow, colCount int) { for ; fromRow < toRow; fromRow++ { - rowData := &xlsx.SheetData.Row[fromRow-1] + rowData := &ws.SheetData.Row[fromRow-1] fillColumns(rowData, colCount, fromRow) } } diff --git a/sheetview.go b/sheetview.go index 23a0377d9e..a942fb42d7 100644 --- a/sheetview.go +++ b/sheetview.go @@ -140,21 +140,21 @@ func (o *ZoomScale) getSheetViewOption(view *xlsxSheetView) { } // getSheetView returns the SheetView object -func (f *File) getSheetView(sheetName string, viewIndex int) (*xlsxSheetView, error) { - xlsx, err := f.workSheetReader(sheetName) +func (f *File) getSheetView(sheet string, viewIndex int) (*xlsxSheetView, error) { + ws, err := f.workSheetReader(sheet) if err != nil { return nil, err } if viewIndex < 0 { - if viewIndex < -len(xlsx.SheetViews.SheetView) { + if viewIndex < -len(ws.SheetViews.SheetView) { return nil, fmt.Errorf("view index %d out of range", viewIndex) } - viewIndex = len(xlsx.SheetViews.SheetView) + viewIndex - } else if viewIndex >= len(xlsx.SheetViews.SheetView) { + 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 &(xlsx.SheetViews.SheetView[viewIndex]), err + return &(ws.SheetViews.SheetView[viewIndex]), err } // SetSheetViewOptions sets sheet view options. The viewIndex may be negative diff --git a/sparkline_test.go b/sparkline_test.go index 4b059ab982..45d3d727a2 100644 --- a/sparkline_test.go +++ b/sparkline_test.go @@ -211,7 +211,7 @@ func TestAddSparkline(t *testing.T) { Negative: true, })) - // Save xlsx file by the given path. + // Save spreadsheet by the given path. assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddSparkline.xlsx"))) // Test error exceptions diff --git a/stream_test.go b/stream_test.go index d81b1d4b13..4f1812efd6 100644 --- a/stream_test.go +++ b/stream_test.go @@ -69,7 +69,7 @@ func TestStreamWriter(t *testing.T) { } assert.NoError(t, streamWriter.Flush()) - // Save xlsx file by the given path. + // Save spreadsheet by the given path. assert.NoError(t, file.SaveAs(filepath.Join("test", "TestStreamWriter.xlsx"))) // Test close temporary file error. diff --git a/styles.go b/styles.go index decc89bd59..c2dc7fa989 100644 --- a/styles.go +++ b/styles.go @@ -27,8 +27,8 @@ import ( ) // Excel styles can reference number formats that are built-in, all of which -// have an id less than 164. This is a possibly incomplete list comprised of -// as many of them as I could find. +// 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", @@ -2580,15 +2580,15 @@ 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) { - xlsx, err := f.workSheetReader(sheet) + ws, err := f.workSheetReader(sheet) if err != nil { return 0, err } - cellData, col, _, err := f.prepareCell(xlsx, sheet, axis) + cellData, col, _, err := f.prepareCell(ws, sheet, axis) if err != nil { return 0, err } - return f.prepareCellStyle(xlsx, col, cellData.S), err + return f.prepareCellStyle(ws, col, cellData.S), err } // SetCellStyle provides a function to add style attribute for cells by given @@ -2682,16 +2682,16 @@ func (f *File) SetCellStyle(sheet, hcell, vcell string, styleID int) error { vcolIdx := vcol - 1 vrowIdx := vrow - 1 - xlsx, err := f.workSheetReader(sheet) + ws, err := f.workSheetReader(sheet) if err != nil { return err } - prepareSheetXML(xlsx, vcol, vrow) - makeContiguousColumns(xlsx, hrow, vrow, vcol) + prepareSheetXML(ws, vcol, vrow) + makeContiguousColumns(ws, hrow, vrow, vcol) for r := hrowIdx; r <= vrowIdx; r++ { for k := hcolIdx; k <= vcolIdx; k++ { - xlsx.SheetData.Row[r].C[k].S = styleID + ws.SheetData.Row[r].C[k].S = styleID } } return err @@ -2926,7 +2926,7 @@ func (f *File) SetConditionalFormat(sheet, area, formatSet string) error { "expression": drawConfFmtExp, } - xlsx, err := f.workSheetReader(sheet) + ws, err := f.workSheetReader(sheet) if err != nil { return err } @@ -2948,7 +2948,7 @@ func (f *File) SetConditionalFormat(sheet, area, formatSet string) error { } } - xlsx.ConditionalFormatting = append(xlsx.ConditionalFormatting, &xlsxConditionalFormatting{ + ws.ConditionalFormatting = append(ws.ConditionalFormatting, &xlsxConditionalFormatting{ SQRef: area, CfRule: cfRule, }) diff --git a/styles_test.go b/styles_test.go index e93aa708a1..02a48cc2b0 100644 --- a/styles_test.go +++ b/styles_test.go @@ -156,18 +156,18 @@ func TestSetConditionalFormat(t *testing.T) { }} for _, testCase := range cases { - xl := NewFile() + f := NewFile() const sheet = "Sheet1" const cellRange = "A1:A1" - err := xl.SetConditionalFormat(sheet, cellRange, testCase.format) + err := f.SetConditionalFormat(sheet, cellRange, testCase.format) if err != nil { t.Fatalf("%s", err) } - xlsx, err := xl.workSheetReader(sheet) + ws, err := f.workSheetReader(sheet) assert.NoError(t, err) - cf := xlsx.ConditionalFormatting + 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) @@ -185,7 +185,7 @@ func TestUnsetConditionalFormat(t *testing.T) { 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") - // Save xlsx file by the given path. + // Save spreadsheet by the given path. assert.NoError(t, f.SaveAs(filepath.Join("test", "TestUnsetConditionalFormat.xlsx"))) } diff --git a/table.go b/table.go index 64c87b16e6..59f1cfecdd 100644 --- a/table.go +++ b/table.go @@ -323,18 +323,18 @@ func (f *File) AutoFilter(sheet, hcell, vcell, format string) 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, refRange, col int, formatSet *formatAutoFilter) error { - xlsx, err := f.workSheetReader(sheet) + ws, err := f.workSheetReader(sheet) if err != nil { return err } - if xlsx.SheetPr != nil { - xlsx.SheetPr.FilterMode = true + if ws.SheetPr != nil { + ws.SheetPr.FilterMode = true } - xlsx.SheetPr = &xlsxSheetPr{FilterMode: true} + ws.SheetPr = &xlsxSheetPr{FilterMode: true} filter := &xlsxAutoFilter{ Ref: ref, } - xlsx.AutoFilter = filter + ws.AutoFilter = filter if formatSet.Column == "" || formatSet.Expression == "" { return nil } @@ -361,7 +361,7 @@ func (f *File) autoFilter(sheet, ref string, refRange, col int, formatSet *forma return err } f.writeAutoFilter(filter, expressions, tokens) - xlsx.AutoFilter = filter + ws.AutoFilter = filter return nil } From 2be4bfd410744201f96e79804ef644d26c47f49f Mon Sep 17 00:00:00 2001 From: Eugene Androsov <53434131+EugeneAndrosovPaser@users.noreply.github.com> Date: Sun, 15 Nov 2020 04:58:45 +0200 Subject: [PATCH 300/957] Fix row duplicate mechanism (#729) --- rows.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rows.go b/rows.go index 3bbf4f21da..04a06b946d 100644 --- a/rows.go +++ b/rows.go @@ -20,6 +20,8 @@ import ( "log" "math" "strconv" + + "github.com/mohae/deepcopy" ) // GetRows return all the rows in a sheet by given worksheet name (case @@ -538,7 +540,7 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) error { for i, r := range ws.SheetData.Row { if r.R == row { - rowCopy = ws.SheetData.Row[i] + rowCopy = deepcopy.Copy(ws.SheetData.Row[i]).(xlsxRow) ok = true break } From 92c8626f814c3bcb91e30f83de8e68c9b8bb9a5f Mon Sep 17 00:00:00 2001 From: xuri Date: Wed, 18 Nov 2020 22:08:40 +0800 Subject: [PATCH 301/957] Fixed #732, support single line with repeated row element in the sheet data --- cell_test.go | 21 ++++++++++++++++++++- col.go | 7 +++---- excelize.go | 8 +++++++- rows.go | 34 ++++++++++++++++------------------ 4 files changed, 46 insertions(+), 24 deletions(-) diff --git a/cell_test.go b/cell_test.go index f7072560a2..c934876edb 100644 --- a/cell_test.go +++ b/cell_test.go @@ -136,8 +136,9 @@ func TestSetCellBool(t *testing.T) { func TestGetCellValue(t *testing.T) { // Test get cell value without r attribute of the row. f := NewFile() + sheetData := `%s` delete(f.Sheet, "xl/worksheets/sheet1.xml") - f.XLSX["xl/worksheets/sheet1.xml"] = []byte(`A3A4B4A7B7A8B8`) + f.XLSX["xl/worksheets/sheet1.xml"] = []byte(fmt.Sprintf(sheetData, `A3A4B4A7B7A8B8`)) f.checked = nil cells := []string{"A3", "A4", "B4", "A7", "B7"} rows, err := f.GetRows("Sheet1") @@ -151,6 +152,24 @@ func TestGetCellValue(t *testing.T) { cols, err := f.GetCols("Sheet1") assert.Equal(t, [][]string{{"", "", "A3", "A4", "", "", "A7", "A8"}, {"", "", "", "B4", "", "", "B7", "B8"}}, cols) assert.NoError(t, err) + delete(f.Sheet, "xl/worksheets/sheet1.xml") + f.XLSX["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) + delete(f.Sheet, "xl/worksheets/sheet1.xml") + f.XLSX["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) + delete(f.Sheet, "xl/worksheets/sheet1.xml") + f.XLSX["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) } func TestGetCellFormula(t *testing.T) { diff --git a/col.go b/col.go index f3e502cf62..5d9122910e 100644 --- a/col.go +++ b/col.go @@ -103,10 +103,9 @@ func (cols *Cols) Rows() ([]string, error) { if inElement == "row" { cellCol = 0 cellRow++ - for _, attr := range startElement.Attr { - if attr.Name.Local == "r" { - cellRow, _ = strconv.Atoi(attr.Value) - } + attrR, _ := attrValToInt("r", startElement.Attr) + if attrR != 0 { + cellRow = attrR } } if inElement == "c" { diff --git a/excelize.go b/excelize.go index 5069756cf4..bcae48103b 100644 --- a/excelize.go +++ b/excelize.go @@ -219,11 +219,17 @@ func checkSheet(ws *xlsxWorksheet) { row = r.R continue } - row++ + if r.R != row { + row++ + } } sheetData := xlsxSheetData{Row: make([]xlsxRow, row)} row = 0 for _, r := range ws.SheetData.Row { + if r.R == row { + sheetData.Row[r.R-1].C = append(sheetData.Row[r.R-1].C, r.C...) + continue + } if r.R != 0 { sheetData.Row[r.R-1] = r row = r.R diff --git a/rows.go b/rows.go index 04a06b946d..4f93ed1162 100644 --- a/rows.go +++ b/rows.go @@ -79,10 +79,10 @@ func (rows *Rows) Error() error { // Columns return the current row's column values. func (rows *Rows) Columns() ([]string, error) { var ( - err error - inElement string - row, cellCol int - columns []string + err error + inElement string + attrR, cellCol, row int + columns []string ) if rows.stashRow >= rows.curRow { @@ -99,17 +99,13 @@ func (rows *Rows) Columns() ([]string, error) { case xml.StartElement: inElement = startElement.Name.Local if inElement == "row" { - for _, attr := range startElement.Attr { - if attr.Name.Local == "r" { - row, err = strconv.Atoi(attr.Value) - if err != nil { - return columns, err - } - if row > rows.curRow { - rows.stashRow = row - 1 - return columns, err - } - } + row++ + if attrR, err = attrValToInt("r", startElement.Attr); attrR != 0 { + row = attrR + } + if row > rows.curRow { + rows.stashRow = row - 1 + return columns, err } } if inElement == "c" { @@ -117,8 +113,7 @@ func (rows *Rows) Columns() ([]string, error) { colCell := xlsxC{} _ = rows.decoder.DecodeElement(&colCell, &startElement) if colCell.R != "" { - cellCol, _, err = CellNameToCoordinates(colCell.R) - if err != nil { + if cellCol, _, err = CellNameToCoordinates(colCell.R); err != nil { return columns, err } } @@ -128,7 +123,10 @@ func (rows *Rows) Columns() ([]string, error) { } case xml.EndElement: inElement = startElement.Name.Local - if inElement == "row" { + if row == 0 { + row = rows.curRow + } + if inElement == "row" && row+1 < rows.curRow { return columns, err } } From 599a8cb0bceb6cb14d3018360bb4c5140753c2b3 Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 19 Nov 2020 21:38:35 +0800 Subject: [PATCH 302/957] Fixed #727, rounding numeric with precision for formula calculation --- README.md | 2 +- README_zh.md | 2 +- calc.go | 15 ++++++++++ calc_test.go | 84 ++++++++++++++++++++++++++-------------------------- rows.go | 21 ++++++------- 5 files changed, 70 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index 891641bc50..6afcc7e729 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Go Report Card go.dev Licenses - Donate + Donate